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/assets/landing-Dtk9M_cY.css +1 -0
- package/dist/assets/main-BVPM78wy.css +1 -0
- package/dist/assets/main-m3KGINE8.js +300 -0
- package/dist/index.html +2 -2
- package/dist/src/packages/landing/index.html +3 -2
- package/dist/src/packages/server/cli.js +17 -34
- package/dist/src/packages/server/codex/json-event-parser.js +18 -9
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +5 -0
- package/dist/src/packages/server/data/index.js +17 -3
- package/dist/src/packages/server/routes/files.js +127 -0
- package/dist/src/packages/server/services/boss-message-service.js +62 -4
- package/dist/src/packages/server/websocket/handlers/database-handler.js +66 -22
- package/dist/src/packages/shared/version.js +107 -0
- package/package.json +2 -1
- package/dist/assets/landing-TJg05mGt.css +0 -1
- package/dist/assets/main-BjXJNCLa.js +0 -296
- package/dist/assets/main-CSdXdPFi.css +0 -1
- /package/dist/assets/{landing-Drz7Kr9V.js → landing-DdrW9aMA.js} +0 -0
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
287
|
-
if (
|
|
288
|
-
printUpdateNotice(
|
|
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
|
|
521
|
-
if (
|
|
522
|
-
printUpdateNotice(
|
|
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}
|
|
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
|
|
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}
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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.
|
|
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
|
-
###
|
|
231
|
+
### 4. WORK PLANNING
|
|
174
232
|
For complex multi-part tasks → create a **work-plan** with parallel/sequential phases.
|
|
175
233
|
|
|
176
|
-
###
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|