tide-commander 0.63.4 → 0.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -19,11 +19,11 @@
19
19
  <meta name="apple-mobile-web-app-title" content="Tide CMD" />
20
20
  <link rel="apple-touch-icon" href="/assets/icons/icon-192.png" />
21
21
  <title>Tide Commander</title>
22
- <script type="module" crossorigin src="/assets/main-BjXJNCLa.js"></script>
22
+ <script type="module" crossorigin src="/assets/main-m3KGINE8.js"></script>
23
23
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-4iQNXcoo.js">
26
- <link rel="stylesheet" crossorigin href="/assets/main-CSdXdPFi.css">
26
+ <link rel="stylesheet" crossorigin href="/assets/main-BVPM78wy.css">
27
27
  </head>
28
28
  <body>
29
29
  <div id="app"></div>
@@ -9,6 +9,7 @@
9
9
  <meta name="author" content="Tide Commander" />
10
10
  <meta name="theme-color" content="#0a0a0f" />
11
11
  <meta name="robots" content="index, follow" />
12
+ <meta name="google-site-verification" content="wZH-YHZmhsyL_aq0C_UgYdD6uBC5h40XJ3fI2TW_ESw" />
12
13
  <link rel="canonical" href="https://tidecommander.com/" />
13
14
 
14
15
  <!-- Open Graph -->
@@ -66,9 +67,9 @@
66
67
  "runtimePlatform": "Node.js"
67
68
  }
68
69
  </script>
69
- <script type="module" crossorigin src="/assets/landing-Drz7Kr9V.js"></script>
70
+ <script type="module" crossorigin src="/assets/landing-DdrW9aMA.js"></script>
70
71
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
71
- <link rel="stylesheet" crossorigin href="/assets/landing-TJg05mGt.css">
72
+ <link rel="stylesheet" crossorigin href="/assets/landing-Dtk9M_cY.css">
72
73
  </head>
73
74
  <body>
74
75
  <!-- Navigation -->
@@ -5,10 +5,12 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { StringDecoder } from 'node:string_decoder';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { checkNpmVersion } from '../shared/version.js';
8
9
  const PID_DIR = path.join(os.homedir(), '.local', 'share', 'tide-commander');
9
10
  const PID_FILE = path.join(PID_DIR, 'server.pid');
10
11
  const META_FILE = path.join(PID_DIR, 'server-meta.json');
11
12
  const LOG_FILE = path.join(process.cwd(), 'logs', 'server.log');
13
+ const PACKAGE_NAME = 'tide-commander';
12
14
  function findProjectRoot(startDir) {
13
15
  let current = startDir;
14
16
  while (current !== path.dirname(current)) {
@@ -283,9 +285,9 @@ async function statusCommand() {
283
285
  console.log(`${green}✓ Running${reset} (PID: ${pid})`);
284
286
  console.log(`${blue}${bright}🚀 Access: ${url}${reset}`);
285
287
  console.log(` Version: ${version}`);
286
- const latestVer = await checkForUpdates(version);
287
- if (latestVer) {
288
- printUpdateNotice(latestVer);
288
+ const npmVersion = await checkNpmVersion(PACKAGE_NAME, version);
289
+ if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
290
+ printUpdateNotice(npmVersion.latestVersion);
289
291
  }
290
292
  if (uptime) {
291
293
  console.log(` Uptime: ${uptime}`);
@@ -406,29 +408,6 @@ function getProcessUptime(pid) {
406
408
  }
407
409
  return null;
408
410
  }
409
- async function checkForUpdates(currentVersion) {
410
- if (currentVersion === 'unknown')
411
- return null;
412
- try {
413
- const controller = new AbortController();
414
- const timeout = setTimeout(() => controller.abort(), 3000);
415
- const res = await fetch('https://registry.npmjs.org/tide-commander/latest', {
416
- signal: controller.signal,
417
- headers: { 'Accept': 'application/json' },
418
- });
419
- clearTimeout(timeout);
420
- if (!res.ok)
421
- return null;
422
- const data = await res.json();
423
- if (data.version && data.version !== currentVersion) {
424
- return data.version;
425
- }
426
- return null;
427
- }
428
- catch {
429
- return null;
430
- }
431
- }
432
411
  function printUpdateNotice(latestVersion) {
433
412
  const yellow = '\x1b[33m';
434
413
  const bright = '\x1b[1m';
@@ -517,9 +496,9 @@ async function main() {
517
496
  console.log(`${cyan}${'═'.repeat(60)}${reset}`);
518
497
  console.log(`${blue}${bright}🚀 Open: ${url}${reset}`);
519
498
  console.log(` Version: ${currentVer}`);
520
- const latest = await checkForUpdates(currentVer);
521
- if (latest) {
522
- printUpdateNotice(latest);
499
+ const npmVersion = await checkNpmVersion(PACKAGE_NAME, currentVer);
500
+ if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
501
+ printUpdateNotice(npmVersion.latestVersion);
523
502
  }
524
503
  console.log(`${cyan}${'─'.repeat(60)}${reset}`);
525
504
  console.log(`${dim}Commands:${reset}`);
@@ -527,7 +506,9 @@ async function main() {
527
506
  console.log(` ${yellow}tide-commander stop${reset} ${dim}Stop the server${reset}`);
528
507
  console.log(` ${yellow}tide-commander logs -f${reset} ${dim}Follow live server logs${reset}`);
529
508
  console.log(` ${yellow}tide-commander --help${reset} ${dim}Show all options${reset}`);
530
- console.log(`${cyan}${'═'.repeat(60)}${reset}\n`);
509
+ console.log(`${cyan}${'═'.repeat(60)}${reset}`);
510
+ console.log(`${dim}⭐ If you find Tide Commander useful, please give it a star:${reset}`);
511
+ console.log(`${yellow}https://github.com/deivid11/tide-commander${reset}\n`);
531
512
  return;
532
513
  }
533
514
  }
@@ -576,9 +557,9 @@ async function main() {
576
557
  console.log(`${green}✓${reset} Started in background (PID: ${child.pid ?? 'unknown'})`);
577
558
  console.log(`${blue}${bright}🚀 Open: ${url}${reset}`);
578
559
  console.log(` Version: ${currentVersion}`);
579
- const latestVersion = await checkForUpdates(currentVersion);
580
- if (latestVersion) {
581
- printUpdateNotice(latestVersion);
560
+ const npmVersion = await checkNpmVersion(PACKAGE_NAME, currentVersion);
561
+ if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
562
+ printUpdateNotice(npmVersion.latestVersion);
582
563
  }
583
564
  console.log(`${cyan}${'─'.repeat(60)}${reset}`);
584
565
  console.log(`${dim}Commands:${reset}`);
@@ -586,7 +567,9 @@ async function main() {
586
567
  console.log(` ${yellow}tide-commander stop${reset} ${dim}Stop the server${reset}`);
587
568
  console.log(` ${yellow}tide-commander logs -f${reset} ${dim}Follow live server logs${reset}`);
588
569
  console.log(` ${yellow}tide-commander --help${reset} ${dim}Show all options${reset}`);
589
- console.log(`${cyan}${'═'.repeat(60)}${reset}\n`);
570
+ console.log(`${cyan}${'═'.repeat(60)}${reset}`);
571
+ console.log(`${dim}⭐ If you find Tide Commander useful, please give it a star:${reset}`);
572
+ console.log(`${yellow}https://github.com/deivid11/tide-commander${reset}\n`);
590
573
  return;
591
574
  }
592
575
  if (child.pid) {
@@ -75,6 +75,7 @@ function parseEnvelope(value) {
75
75
  */
76
76
  export class CodexJsonEventParser {
77
77
  activeToolByItemId = new Map();
78
+ lastAgentMessageText;
78
79
  parseLine(line) {
79
80
  const trimmed = line.trim();
80
81
  if (!trimmed)
@@ -95,6 +96,10 @@ export class CodexJsonEventParser {
95
96
  return this.parseItemStarted(event.item);
96
97
  }
97
98
  if (event.type === 'item.completed') {
99
+ // Capture agent message text for later use in step_complete
100
+ if (event.item?.type === 'agent_message' && event.item?.text) {
101
+ this.lastAgentMessageText = event.item.text;
102
+ }
98
103
  return this.parseItemCompleted(event.item);
99
104
  }
100
105
  if (event.type === 'turn.completed') {
@@ -187,16 +192,20 @@ export class CodexJsonEventParser {
187
192
  parseTurnCompleted(usage) {
188
193
  if (!usage)
189
194
  return [];
190
- return [
191
- {
192
- type: 'step_complete',
193
- tokens: {
194
- input: usage.input_tokens ?? 0,
195
- output: usage.output_tokens ?? 0,
196
- cacheRead: usage.cached_input_tokens,
197
- },
195
+ const event = {
196
+ type: 'step_complete',
197
+ tokens: {
198
+ input: usage.input_tokens ?? 0,
199
+ output: usage.output_tokens ?? 0,
200
+ cacheRead: usage.cached_input_tokens,
198
201
  },
199
- ];
202
+ };
203
+ // Include the last agent message as resultText for boss delegation processing
204
+ if (this.lastAgentMessageText) {
205
+ event.resultText = this.lastAgentMessageText;
206
+ this.lastAgentMessageText = undefined; // Reset for next turn
207
+ }
208
+ return [event];
200
209
  }
201
210
  buildWebSearchToolInput(item) {
202
211
  return {
@@ -43,6 +43,11 @@ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/j
43
43
  - Replace \`YOUR_AGENT_ID\` with your actual agent ID from the system prompt
44
44
  - Keep messages under 50 characters
45
45
  - **IMPORTANT: Do NOT use exclamation marks (!) in messages** - they cause bash history expansion errors
46
+ - **CRITICAL: Send notification ONLY when YOUR task is 100% done**
47
+ - If you delegated work to another agent, wait for their response/completion BEFORE notifying
48
+ - If you used a tool or spawned a subagent, verify output before notifying
49
+ - If task involves waiting for other agents to finish, do NOT notify until they confirm completion
50
+ - Only notify when YOU have nothing more to do on this task
46
51
  - Send notification as your FINAL action after completing work
47
52
  - Do NOT skip this step - the user relies on notifications
48
53
  - The \`&\` runs both commands in parallel (curl for mobile/browser, gdbus for Linux desktop)`,
@@ -237,16 +237,27 @@ export function deleteSupervisorHistory(histories, agentId) {
237
237
  // ============================================================================
238
238
  // Building Persistence
239
239
  // ============================================================================
240
+ // Buildings cache to avoid reading from disk on every call
241
+ let buildingsCache = null;
242
+ let buildingsCacheMtime = 0;
240
243
  /**
241
- * Load buildings from disk
244
+ * Load buildings from disk (cached, invalidated by file mtime)
242
245
  */
243
246
  export function loadBuildings() {
244
247
  ensureDataDir();
245
248
  try {
246
249
  if (fs.existsSync(BUILDINGS_FILE)) {
250
+ const stat = fs.statSync(BUILDINGS_FILE);
251
+ const mtime = stat.mtimeMs;
252
+ // Return cached data if file hasn't changed
253
+ if (buildingsCache !== null && mtime === buildingsCacheMtime) {
254
+ return buildingsCache;
255
+ }
247
256
  const data = JSON.parse(fs.readFileSync(BUILDINGS_FILE, 'utf-8'));
248
- log.log(` Loaded ${data.buildings?.length || 0} buildings from ${BUILDINGS_FILE}`);
249
- return data.buildings || [];
257
+ buildingsCache = data.buildings || [];
258
+ buildingsCacheMtime = mtime;
259
+ log.log(` Loaded ${buildingsCache.length} buildings from ${BUILDINGS_FILE}`);
260
+ return buildingsCache;
250
261
  }
251
262
  }
252
263
  catch (err) {
@@ -266,6 +277,9 @@ export function saveBuildings(buildings) {
266
277
  version: '1.0.0',
267
278
  };
268
279
  fs.writeFileSync(BUILDINGS_FILE, JSON.stringify(data, null, 2));
280
+ // Invalidate cache so next load picks up the new data
281
+ buildingsCache = buildings;
282
+ buildingsCacheMtime = fs.statSync(BUILDINGS_FILE).mtimeMs;
269
283
  }
270
284
  catch (err) {
271
285
  log.error(' Failed to save buildings:', err);
@@ -349,6 +349,44 @@ function buildTree(dirPath, depth, maxDepth) {
349
349
  }
350
350
  return nodes;
351
351
  }
352
+ function ensureAbsoluteExistingPath(targetPath) {
353
+ if (!targetPath || !path.isAbsolute(targetPath))
354
+ return null;
355
+ return fs.existsSync(targetPath) ? targetPath : null;
356
+ }
357
+ function resolveUniqueCopyPath(targetDir, sourceName) {
358
+ const extension = path.extname(sourceName);
359
+ const baseName = extension ? sourceName.slice(0, -extension.length) : sourceName;
360
+ let attempt = 0;
361
+ while (true) {
362
+ const suffix = attempt === 0 ? ' copy' : ` copy ${attempt + 1}`;
363
+ const candidateName = `${baseName}${suffix}${extension}`;
364
+ const candidatePath = path.join(targetDir, candidateName);
365
+ if (!fs.existsSync(candidatePath))
366
+ return candidatePath;
367
+ attempt++;
368
+ }
369
+ }
370
+ function copyPathToDirectory(sourcePath, targetDir) {
371
+ const sourceName = path.basename(sourcePath);
372
+ const directTarget = path.join(targetDir, sourceName);
373
+ const destinationPath = fs.existsSync(directTarget)
374
+ ? resolveUniqueCopyPath(targetDir, sourceName)
375
+ : directTarget;
376
+ const sourceStats = fs.statSync(sourcePath);
377
+ if (sourceStats.isDirectory()) {
378
+ const rel = path.relative(sourcePath, targetDir);
379
+ const targetInsideSource = rel && !rel.startsWith('..') && !path.isAbsolute(rel);
380
+ if (targetInsideSource) {
381
+ throw new Error('Cannot copy a folder into itself');
382
+ }
383
+ fs.cpSync(sourcePath, destinationPath, { recursive: true, force: false, errorOnExist: true });
384
+ }
385
+ else {
386
+ fs.copyFileSync(sourcePath, destinationPath);
387
+ }
388
+ return destinationPath;
389
+ }
352
390
  // GET /api/files/tree - Get recursive directory tree
353
391
  router.get('/tree', async (req, res) => {
354
392
  try {
@@ -383,6 +421,95 @@ router.get('/tree', async (req, res) => {
383
421
  res.status(500).json({ error: err.message });
384
422
  }
385
423
  });
424
+ // POST /api/files/rename - Rename file or folder in place
425
+ router.post('/rename', (req, res) => {
426
+ try {
427
+ const { path: sourcePath, newName } = req.body;
428
+ const validatedSource = sourcePath && ensureAbsoluteExistingPath(sourcePath);
429
+ if (!validatedSource) {
430
+ res.status(400).json({ error: 'Invalid or missing source path' });
431
+ return;
432
+ }
433
+ const nextName = (newName || '').trim();
434
+ if (!nextName || nextName === '.' || nextName === '..') {
435
+ res.status(400).json({ error: 'Invalid new name' });
436
+ return;
437
+ }
438
+ if (nextName.includes('/') || nextName.includes('\\')) {
439
+ res.status(400).json({ error: 'Name must not contain path separators' });
440
+ return;
441
+ }
442
+ const destinationPath = path.join(path.dirname(validatedSource), nextName);
443
+ if (validatedSource === destinationPath) {
444
+ res.json({ success: true, newPath: destinationPath });
445
+ return;
446
+ }
447
+ if (fs.existsSync(destinationPath)) {
448
+ res.status(409).json({ error: 'Target already exists' });
449
+ return;
450
+ }
451
+ fs.renameSync(validatedSource, destinationPath);
452
+ res.json({ success: true, oldPath: validatedSource, newPath: destinationPath });
453
+ }
454
+ catch (err) {
455
+ log.error(' Failed to rename path:', err);
456
+ res.status(500).json({ error: err.message });
457
+ }
458
+ });
459
+ // POST /api/files/copy - Copy file/folder into target directory
460
+ router.post('/copy', (req, res) => {
461
+ try {
462
+ const { sourcePath, targetDir } = req.body;
463
+ const validatedSource = sourcePath && ensureAbsoluteExistingPath(sourcePath);
464
+ if (!validatedSource) {
465
+ res.status(400).json({ error: 'Invalid or missing source path' });
466
+ return;
467
+ }
468
+ const validatedTargetDir = targetDir && ensureAbsoluteExistingPath(targetDir);
469
+ if (!validatedTargetDir) {
470
+ res.status(400).json({ error: 'Invalid or missing target directory' });
471
+ return;
472
+ }
473
+ const targetStats = fs.statSync(validatedTargetDir);
474
+ if (!targetStats.isDirectory()) {
475
+ res.status(400).json({ error: 'Target must be a directory' });
476
+ return;
477
+ }
478
+ const destinationPath = copyPathToDirectory(validatedSource, validatedTargetDir);
479
+ res.json({ success: true, sourcePath: validatedSource, destinationPath });
480
+ }
481
+ catch (err) {
482
+ log.error(' Failed to copy path:', err);
483
+ res.status(500).json({ error: err.message });
484
+ }
485
+ });
486
+ // POST /api/files/paste - Alias for copy operation from clipboard source
487
+ router.post('/paste', (req, res) => {
488
+ try {
489
+ const { sourcePath, targetDir } = req.body;
490
+ const validatedSource = sourcePath && ensureAbsoluteExistingPath(sourcePath);
491
+ if (!validatedSource) {
492
+ res.status(400).json({ error: 'Invalid or missing source path' });
493
+ return;
494
+ }
495
+ const validatedTargetDir = targetDir && ensureAbsoluteExistingPath(targetDir);
496
+ if (!validatedTargetDir) {
497
+ res.status(400).json({ error: 'Invalid or missing target directory' });
498
+ return;
499
+ }
500
+ const targetStats = fs.statSync(validatedTargetDir);
501
+ if (!targetStats.isDirectory()) {
502
+ res.status(400).json({ error: 'Target must be a directory' });
503
+ return;
504
+ }
505
+ const destinationPath = copyPathToDirectory(validatedSource, validatedTargetDir);
506
+ res.json({ success: true, sourcePath: validatedSource, destinationPath });
507
+ }
508
+ catch (err) {
509
+ log.error(' Failed to paste path:', err);
510
+ res.status(500).json({ error: err.message });
511
+ }
512
+ });
386
513
  // Helper function to search files recursively
387
514
  function searchFiles(dirPath, query, results, maxResults, depth = 0) {
388
515
  if (results.length >= maxResults || depth > 10)
@@ -167,14 +167,72 @@ For any task → **delegate immediately**. This includes:
167
167
 
168
168
  No lengthy analysis needed - just delegate.
169
169
 
170
- ### 2. CODEBASE ANALYSIS
170
+ ### 2. GET DETAILED AGENT INFORMATION
171
+
172
+ When you need more detail about an agent beyond what's in your team context, use these API endpoints:
173
+
174
+ #### Get Agent Details:
175
+ \`GET /api/agents/<agent-id>\`
176
+ - Returns full agent object with all properties
177
+ - Use when you need complete agent information (cwd, sessionId, capabilities, status, etc.)
178
+ - Shows agent's current configuration and metadata
179
+
180
+ #### Get Agent Conversation History:
181
+ \`GET /api/agents/<agent-id>/history?limit=50&offset=0\`
182
+ - Returns recent conversation messages with pagination
183
+ - Shows what the agent has been working on and discussing
184
+ - Useful to understand agent's recent context and decisions
185
+ - Parameters: limit (default 50), offset (default 0)
186
+ - Shows both user queries and agent responses
187
+
188
+ #### Search Agent History:
189
+ \`GET /api/agents/<agent-id>/search?q=<search-term>&limit=50\`
190
+ - Search agent's conversation history for specific keywords
191
+ - Example: \`/api/agents/abc123/search?q=database\` finds "database" mentions
192
+ - Returns matching messages from agent's conversations
193
+ - Great for quickly locating relevant work and decisions
194
+
195
+ #### Get Agent Sessions:
196
+ \`GET /api/agents/<agent-id>/sessions\`
197
+ - Lists all Claude Code sessions for the agent
198
+ - Shows session metadata: message counts, timestamps, first message preview
199
+ - Useful for understanding what projects agent has worked on
200
+
201
+ #### Get All Agent Tool History:
202
+ \`GET /api/agents/tool-history?limit=100\`
203
+ - Get recent tool usage across all agents (or specific agent)
204
+ - Shows which tools agents have been using and timestamps
205
+ - Helpful for understanding team activity patterns and tool usage
206
+
207
+ #### Get Agent Status (Quick Polling):
208
+ \`GET /api/agents/status\`
209
+ - Lightweight endpoint for quick agent status checks
210
+ - Returns: id, status, currentTask, currentTool, isProcessRunning
211
+ - Use when you need fast status without full agent details
212
+
213
+ **When to use these endpoints:**
214
+ - **User asks "what has Agent X been working on?"** → Use \`/history\` to see recent conversations
215
+ - **User asks "what did Agent X say about Y?"** → Use \`/search?q=Y\` to find mentions
216
+ - **User wants full agent details** → Use \`/agents/<id>\` for complete metadata
217
+ - **You need to verify agent's recent work before delegating** → Use \`/history\` or \`/search\`
218
+ - **User asks "is Agent X busy?"** → Use \`/agents/status\` for quick check
219
+ - **You want to understand project history** → Use \`/sessions\` to list all sessions
220
+
221
+ **Example workflow:**
222
+ 1. User: "Check on Scout Alpha's progress on the auth module"
223
+ 2. Boss: Fetch \`/api/agents/scout-alpha-id/search?q=auth\` to find auth-related conversations
224
+ 3. Boss: Provides summary to user: "Scout Alpha has been working on JWT implementation..."
225
+ 4. User: "Have them continue with refresh token logic"
226
+ 5. Boss: Delegates to Scout with context from search results
227
+
228
+ ### 3. CODEBASE ANALYSIS
171
229
  When asked to "analyze" → delegate to **scouts** first via analysis-request block.
172
230
 
173
- ### 3. WORK PLANNING
231
+ ### 4. WORK PLANNING
174
232
  For complex multi-part tasks → create a **work-plan** with parallel/sequential phases.
175
233
 
176
- ### 4. TEAM STATUS
177
- Answer questions about your team using the context provided.
234
+ ### 5. TEAM STATUS
235
+ Answer questions about your team using the context provided. For deep dives into specific agents, use the API endpoints above.
178
236
 
179
237
  ---
180
238
 
@@ -133,33 +133,77 @@ export async function handleGetTableSchema(ctx, payload) {
133
133
  * Handle execute_query message
134
134
  */
135
135
  export async function handleExecuteQuery(ctx, payload) {
136
- const { buildingId, connectionId, database, query, limit = 1000 } = payload;
136
+ const { buildingId, connectionId, database, query, limit = 1000, silent = false, requestId } = payload;
137
137
  const connection = getConnection(buildingId, connectionId);
138
138
  if (!connection) {
139
139
  ctx.sendError('Connection not found');
140
140
  return;
141
141
  }
142
- log.log(`Executing query on ${connection.name}/${database}: ${query.substring(0, 100)}...`);
143
- const result = await databaseService.executeQuery(connection, database, query, limit);
144
- // Add to history
145
- databaseService.addToHistory(buildingId, result);
146
- // Send result
147
- ctx.sendToClient({
148
- type: 'query_result',
149
- payload: {
150
- buildingId,
151
- result,
152
- },
153
- });
154
- // Send updated history
155
- const history = databaseService.getHistory(buildingId);
156
- ctx.sendToClient({
157
- type: 'query_history_update',
158
- payload: {
159
- buildingId,
160
- history,
161
- },
162
- });
142
+ const isSilent = silent === true;
143
+ log.log(`Executing query on ${connection.name}/${database}: ${query.substring(0, 100)}...${isSilent ? ' (SILENT MODE)' : ''}`);
144
+ try {
145
+ const result = await databaseService.executeQuery(connection, database, query, limit);
146
+ const metric = result.affectedRows ?? result.rowCount ?? 0;
147
+ const metricLabel = result.affectedRows !== undefined ? 'affectedRows' : 'rowCount';
148
+ const isSuccess = result.status === 'success';
149
+ log.log(`Query execution complete status=${result.status} duration=${result.duration}ms ${metricLabel}=${metric}${isSilent ? ' (result not sent to UI)' : ''}`);
150
+ // Always add to history (even for silent queries)
151
+ databaseService.addToHistory(buildingId, result);
152
+ // If silent mode, don't send result back to UI
153
+ if (isSilent) {
154
+ if (!isSuccess) {
155
+ log.warn(`Silent execution failed: ${result.error ?? 'Unknown error'}`);
156
+ }
157
+ ctx.sendToClient({
158
+ type: 'silent_query_result',
159
+ payload: {
160
+ buildingId,
161
+ query,
162
+ requestId,
163
+ success: isSuccess,
164
+ affectedRows: isSuccess ? result.affectedRows : undefined,
165
+ error: !isSuccess ? (result.error ?? 'Unknown error') : undefined,
166
+ },
167
+ });
168
+ log.log(`Silent execution completed - sent silent_query_result status=${isSuccess ? 'success' : 'error'}`);
169
+ return;
170
+ }
171
+ // Send result
172
+ ctx.sendToClient({
173
+ type: 'query_result',
174
+ payload: {
175
+ buildingId,
176
+ result,
177
+ },
178
+ });
179
+ // Send updated history
180
+ const history = databaseService.getHistory(buildingId);
181
+ ctx.sendToClient({
182
+ type: 'query_history_update',
183
+ payload: {
184
+ buildingId,
185
+ history,
186
+ },
187
+ });
188
+ }
189
+ catch (error) {
190
+ log.error(`Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
191
+ if (isSilent) {
192
+ ctx.sendToClient({
193
+ type: 'silent_query_result',
194
+ payload: {
195
+ buildingId,
196
+ query,
197
+ requestId,
198
+ success: false,
199
+ error: error instanceof Error ? error.message : 'Unknown error',
200
+ },
201
+ });
202
+ }
203
+ else {
204
+ ctx.sendError(`Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
205
+ }
206
+ }
163
207
  }
164
208
  /**
165
209
  * Handle request_query_history message
@@ -0,0 +1,107 @@
1
+ function normalizeVersion(version) {
2
+ return version.trim().replace(/^v/i, '');
3
+ }
4
+ function parseSemver(version) {
5
+ const normalized = normalizeVersion(version);
6
+ const match = normalized.match(/^(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?/);
7
+ if (!match)
8
+ return null;
9
+ const core = match[1].split('.').map(part => Number.parseInt(part, 10));
10
+ if (core.some(Number.isNaN))
11
+ return null;
12
+ const prerelease = match[2] ? match[2].split('.') : [];
13
+ return { core, prerelease };
14
+ }
15
+ function comparePrerelease(a, b) {
16
+ if (a.length === 0 && b.length === 0)
17
+ return 0;
18
+ if (a.length === 0)
19
+ return 1;
20
+ if (b.length === 0)
21
+ return -1;
22
+ const maxLen = Math.max(a.length, b.length);
23
+ for (let i = 0; i < maxLen; i += 1) {
24
+ const left = a[i];
25
+ const right = b[i];
26
+ if (left === undefined)
27
+ return -1;
28
+ if (right === undefined)
29
+ return 1;
30
+ const leftNum = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null;
31
+ const rightNum = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null;
32
+ if (leftNum !== null && rightNum !== null) {
33
+ if (leftNum > rightNum)
34
+ return 1;
35
+ if (leftNum < rightNum)
36
+ return -1;
37
+ continue;
38
+ }
39
+ if (leftNum !== null)
40
+ return -1;
41
+ if (rightNum !== null)
42
+ return 1;
43
+ const cmp = left.localeCompare(right);
44
+ if (cmp !== 0)
45
+ return cmp > 0 ? 1 : -1;
46
+ }
47
+ return 0;
48
+ }
49
+ export function compareVersions(a, b) {
50
+ const left = parseSemver(a);
51
+ const right = parseSemver(b);
52
+ if (!left || !right)
53
+ return null;
54
+ const maxLen = Math.max(left.core.length, right.core.length);
55
+ for (let i = 0; i < maxLen; i += 1) {
56
+ const leftPart = left.core[i] ?? 0;
57
+ const rightPart = right.core[i] ?? 0;
58
+ if (leftPart > rightPart)
59
+ return 1;
60
+ if (leftPart < rightPart)
61
+ return -1;
62
+ }
63
+ return comparePrerelease(left.prerelease, right.prerelease);
64
+ }
65
+ export function getVersionRelation(currentVersion, latestVersion) {
66
+ const compared = compareVersions(currentVersion, latestVersion);
67
+ if (compared === null)
68
+ return 'unknown';
69
+ if (compared < 0)
70
+ return 'behind';
71
+ if (compared > 0)
72
+ return 'ahead';
73
+ return 'equal';
74
+ }
75
+ export async function fetchLatestNpmVersion(packageName, options = {}) {
76
+ const { timeoutMs = 3000, fetchImpl = fetch } = options;
77
+ try {
78
+ const controller = new AbortController();
79
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
80
+ const response = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
81
+ signal: controller.signal,
82
+ headers: { Accept: 'application/json' },
83
+ });
84
+ clearTimeout(timeout);
85
+ if (!response.ok)
86
+ return null;
87
+ const data = await response.json();
88
+ return typeof data.version === 'string' ? data.version : null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ export async function checkNpmVersion(packageName, currentVersion, options = {}) {
95
+ if (!currentVersion || currentVersion === 'unknown') {
96
+ return { currentVersion, latestVersion: null, relation: 'unknown' };
97
+ }
98
+ const latestVersion = await fetchLatestNpmVersion(packageName, options);
99
+ if (!latestVersion) {
100
+ return { currentVersion, latestVersion: null, relation: 'unknown' };
101
+ }
102
+ return {
103
+ currentVersion,
104
+ latestVersion,
105
+ relation: getVersionRelation(currentVersion, latestVersion),
106
+ };
107
+ }