agim-cli 1.1.2 → 1.1.4
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/CHANGELOG.md +104 -0
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/core/a2a.d.ts +19 -0
- package/dist/core/a2a.d.ts.map +1 -1
- package/dist/core/a2a.js +55 -4
- package/dist/core/a2a.js.map +1 -1
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +19 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/artifacts.d.ts +86 -0
- package/dist/core/artifacts.d.ts.map +1 -0
- package/dist/core/artifacts.js +306 -0
- package/dist/core/artifacts.js.map +1 -0
- package/dist/core/commands/a2a.d.ts.map +1 -1
- package/dist/core/commands/a2a.js +19 -5
- package/dist/core/commands/a2a.js.map +1 -1
- package/dist/core/job-board.d.ts +5 -0
- package/dist/core/job-board.d.ts.map +1 -1
- package/dist/core/job-board.js +32 -0
- package/dist/core/job-board.js.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +27 -2
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +58 -6
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
- package/dist/web/public/memos.html +71 -15
- package/dist/web/public/reminders.html +89 -15
- package/dist/web/public/tasks.html +555 -8
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +275 -4
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/web/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAsDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAsDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAsqB/C"}
|
package/dist/web/server.js
CHANGED
|
@@ -361,6 +361,41 @@ export async function startWebServer(options) {
|
|
|
361
361
|
if (url.pathname === '/api/audit' && req.method === 'GET') {
|
|
362
362
|
return handleAudit(req, res, url);
|
|
363
363
|
}
|
|
364
|
+
// v1.1.2 — Outbox tab. List rows by status, plus aggregate stats and
|
|
365
|
+
// a retry endpoint for the giving_up row state.
|
|
366
|
+
if (url.pathname === '/api/outbox' && req.method === 'GET') {
|
|
367
|
+
return handleListOutbox(req, res, url);
|
|
368
|
+
}
|
|
369
|
+
if (url.pathname === '/api/outbox/stats' && req.method === 'GET') {
|
|
370
|
+
return handleOutboxStats(req, res);
|
|
371
|
+
}
|
|
372
|
+
const outboxRetryMatch = url.pathname.match(/^\/api\/outbox\/(\d+)\/retry$/);
|
|
373
|
+
if (outboxRetryMatch && req.method === 'POST') {
|
|
374
|
+
return handleOutboxRetry(req, res, parseInt(outboxRetryMatch[1], 10));
|
|
375
|
+
}
|
|
376
|
+
// v1.1.3 — A2A tab. Stats over inline rows with parent_id; recent
|
|
377
|
+
// calls; and a tree walk rooted at a given jobId.
|
|
378
|
+
if (url.pathname === '/api/a2a/stats' && req.method === 'GET') {
|
|
379
|
+
return handleA2AStats(req, res);
|
|
380
|
+
}
|
|
381
|
+
if (url.pathname === '/api/a2a/recent' && req.method === 'GET') {
|
|
382
|
+
return handleA2ARecent(req, res, url);
|
|
383
|
+
}
|
|
384
|
+
const a2aTreeMatch = url.pathname.match(/^\/api\/a2a\/tree\/(\d+)$/);
|
|
385
|
+
if (a2aTreeMatch && req.method === 'GET') {
|
|
386
|
+
return handleA2ATree(req, res, parseInt(a2aTreeMatch[1], 10));
|
|
387
|
+
}
|
|
388
|
+
// v1.1.3 — Artifacts. List per-job + download single file. The
|
|
389
|
+
// download endpoint also doubles as the binary view for the A2A
|
|
390
|
+
// tab's "📎 commits.txt" link.
|
|
391
|
+
const artifactsListMatch = url.pathname.match(/^\/api\/artifacts\/(\d+)$/);
|
|
392
|
+
if (artifactsListMatch && req.method === 'GET') {
|
|
393
|
+
return handleArtifactsList(req, res, parseInt(artifactsListMatch[1], 10));
|
|
394
|
+
}
|
|
395
|
+
const artifactsFileMatch = url.pathname.match(/^\/api\/artifacts\/(\d+)\/file\/([\w.+\-]+)$/);
|
|
396
|
+
if (artifactsFileMatch && req.method === 'GET') {
|
|
397
|
+
return handleArtifactsFile(req, res, parseInt(artifactsFileMatch[1], 10), artifactsFileMatch[2]);
|
|
398
|
+
}
|
|
364
399
|
// PR-B: agent health snapshot (circuit breaker + rate-limiter remaining
|
|
365
400
|
// + latency p50/95/99) consumed by the Health tab in /tasks.
|
|
366
401
|
if (url.pathname === '/api/agent-health' && req.method === 'GET') {
|
|
@@ -1302,12 +1337,24 @@ async function handleHealth(_req, res) {
|
|
|
1302
1337
|
}
|
|
1303
1338
|
async function handleListJobs(_req, res, url) {
|
|
1304
1339
|
try {
|
|
1305
|
-
const
|
|
1340
|
+
const jb = await import('../core/job-board.js');
|
|
1306
1341
|
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '50', 10) || 50, 1), 500);
|
|
1307
|
-
const
|
|
1342
|
+
const validStatuses = new Set([
|
|
1343
|
+
'pending', 'running', 'completed', 'delivered', 'failed',
|
|
1344
|
+
'cancelled', 'interrupted', 'replaced', 'abandoned',
|
|
1345
|
+
]);
|
|
1346
|
+
const statusRaw = url.searchParams.get('status') || '';
|
|
1347
|
+
const status = validStatuses.has(statusRaw) ? statusRaw : undefined;
|
|
1308
1348
|
const agent = url.searchParams.get('agent') || undefined;
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1349
|
+
const kindRaw = url.searchParams.get('kind') || '';
|
|
1350
|
+
const kind = (kindRaw === 'inline' || kindRaw === 'job') ? kindRaw : undefined;
|
|
1351
|
+
const opts = {};
|
|
1352
|
+
if (agent)
|
|
1353
|
+
opts.agent = agent;
|
|
1354
|
+
if (kind)
|
|
1355
|
+
opts.kind = kind;
|
|
1356
|
+
const jobs = jb.listJobs(limit, status, opts);
|
|
1357
|
+
const stats = jb.getJobStats();
|
|
1311
1358
|
sendJson(res, 200, { jobs, stats });
|
|
1312
1359
|
}
|
|
1313
1360
|
catch (err) {
|
|
@@ -2424,6 +2471,230 @@ function sendJson(res, status, data) {
|
|
|
2424
2471
|
res.end(JSON.stringify(data));
|
|
2425
2472
|
}
|
|
2426
2473
|
// ============================================
|
|
2474
|
+
// Outbox (v1.1.2)
|
|
2475
|
+
// ============================================
|
|
2476
|
+
async function handleListOutbox(_req, res, url) {
|
|
2477
|
+
try {
|
|
2478
|
+
const { listOutbox } = await import('../core/outbox.js');
|
|
2479
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '50', 10) || 50, 1), 500);
|
|
2480
|
+
const statusRaw = url.searchParams.get('status') || '';
|
|
2481
|
+
const status = ['pending', 'delivered', 'giving_up'].includes(statusRaw)
|
|
2482
|
+
? statusRaw
|
|
2483
|
+
: undefined;
|
|
2484
|
+
const threadKey = url.searchParams.get('thread') || undefined;
|
|
2485
|
+
const rows = listOutbox({ limit, status, threadKey });
|
|
2486
|
+
sendJson(res, 200, { rows });
|
|
2487
|
+
}
|
|
2488
|
+
catch (err) {
|
|
2489
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
async function handleOutboxStats(_req, res) {
|
|
2493
|
+
try {
|
|
2494
|
+
const { getOutboxStats } = await import('../core/outbox.js');
|
|
2495
|
+
sendJson(res, 200, getOutboxStats());
|
|
2496
|
+
}
|
|
2497
|
+
catch (err) {
|
|
2498
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
async function handleOutboxRetry(_req, res, id) {
|
|
2502
|
+
try {
|
|
2503
|
+
const { retryGivingUp } = await import('../core/outbox.js');
|
|
2504
|
+
const ok = retryGivingUp(id);
|
|
2505
|
+
if (!ok) {
|
|
2506
|
+
sendJson(res, 404, { ok: false, error: `outbox #${id} not found or not in giving_up state` });
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
sendJson(res, 200, { ok: true, id });
|
|
2510
|
+
}
|
|
2511
|
+
catch (err) {
|
|
2512
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
// ============================================
|
|
2516
|
+
// A2A (v1.1.3)
|
|
2517
|
+
// ============================================
|
|
2518
|
+
// Open a read-only handle to jobs.db. We do NOT reuse job-board's
|
|
2519
|
+
// writer handle: web reads are read-only, and a separate connection
|
|
2520
|
+
// keeps the writer free of long SELECTs (jobs.db is in WAL mode, so
|
|
2521
|
+
// concurrent reads alongside the writer are fine).
|
|
2522
|
+
//
|
|
2523
|
+
// Async because we're under ESM — `require()` isn't available at
|
|
2524
|
+
// runtime even though the type imports above keep `import('better-sqlite3').Database`
|
|
2525
|
+
// usable at compile time.
|
|
2526
|
+
async function openJobsDbReadOnly() {
|
|
2527
|
+
try {
|
|
2528
|
+
const path = await import('node:path');
|
|
2529
|
+
const { AGIM_HOME } = await import('../core/agim-paths.js');
|
|
2530
|
+
const Database = (await import('better-sqlite3')).default;
|
|
2531
|
+
const d = new Database(path.join(AGIM_HOME, 'jobs.db'), { readonly: true, fileMustExist: true });
|
|
2532
|
+
d.pragma('journal_mode = WAL');
|
|
2533
|
+
return d;
|
|
2534
|
+
}
|
|
2535
|
+
catch (err) {
|
|
2536
|
+
rootLogger.warn({ component: 'web', event: 'web.jobs_db_open_failed', err: err instanceof Error ? err.message : String(err) }, 'failed to open jobs.db for A2A read');
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
async function handleA2AStats(_req, res) {
|
|
2541
|
+
const d = await openJobsDbReadOnly();
|
|
2542
|
+
if (!d) {
|
|
2543
|
+
sendJson(res, 503, { error: 'jobs.db not available' });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
try {
|
|
2547
|
+
const total = d.prepare("SELECT COUNT(*) AS n FROM jobs WHERE kind='inline' AND parent_id IS NOT NULL").get().n;
|
|
2548
|
+
const recent24h = d.prepare("SELECT COUNT(*) AS n FROM jobs WHERE kind='inline' AND parent_id IS NOT NULL AND created_at >= datetime('now','-1 day')").get().n;
|
|
2549
|
+
const maxDepth = d.prepare("SELECT MAX(call_depth) AS m FROM jobs WHERE kind='inline' AND parent_id IS NOT NULL").get().m ?? 0;
|
|
2550
|
+
const byStatus = d.prepare("SELECT status, COUNT(*) AS n FROM jobs WHERE kind='inline' AND parent_id IS NOT NULL GROUP BY status").all();
|
|
2551
|
+
const byAgent = d.prepare("SELECT agent, COUNT(*) AS n FROM jobs WHERE kind='inline' AND parent_id IS NOT NULL GROUP BY agent ORDER BY n DESC LIMIT 10").all();
|
|
2552
|
+
sendJson(res, 200, { total, recent24h, maxDepth, byStatus, byAgent });
|
|
2553
|
+
}
|
|
2554
|
+
catch (err) {
|
|
2555
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2556
|
+
}
|
|
2557
|
+
finally {
|
|
2558
|
+
try {
|
|
2559
|
+
d.close();
|
|
2560
|
+
}
|
|
2561
|
+
catch { /* ignore */ }
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
async function handleA2ARecent(_req, res, url) {
|
|
2565
|
+
const d = await openJobsDbReadOnly();
|
|
2566
|
+
if (!d) {
|
|
2567
|
+
sendJson(res, 503, { error: 'jobs.db not available' });
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
try {
|
|
2571
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '20', 10) || 20, 1), 200);
|
|
2572
|
+
const rows = d.prepare(`SELECT id, agent, parent_id, call_depth, status,
|
|
2573
|
+
substr(prompt, 1, 200) AS prompt,
|
|
2574
|
+
substr(result, 1, 500) AS result,
|
|
2575
|
+
error,
|
|
2576
|
+
created_at, started_at, completed_at, delivered_at
|
|
2577
|
+
FROM jobs
|
|
2578
|
+
WHERE kind='inline' AND parent_id IS NOT NULL
|
|
2579
|
+
ORDER BY id DESC
|
|
2580
|
+
LIMIT ?`).all(limit);
|
|
2581
|
+
sendJson(res, 200, { rows });
|
|
2582
|
+
}
|
|
2583
|
+
catch (err) {
|
|
2584
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2585
|
+
}
|
|
2586
|
+
finally {
|
|
2587
|
+
try {
|
|
2588
|
+
d.close();
|
|
2589
|
+
}
|
|
2590
|
+
catch { /* ignore */ }
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
async function handleA2ATree(_req, res, rootId) {
|
|
2594
|
+
const dMaybe = await openJobsDbReadOnly();
|
|
2595
|
+
if (!dMaybe) {
|
|
2596
|
+
sendJson(res, 503, { error: 'jobs.db not available' });
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const d = dMaybe; // captured non-null binding for nested async closures
|
|
2600
|
+
try {
|
|
2601
|
+
const { listOutputs } = await import('../core/artifacts.js');
|
|
2602
|
+
const rootRow = d.prepare(`SELECT id, agent, status, parent_id, call_depth,
|
|
2603
|
+
substr(prompt, 1, 400) AS prompt,
|
|
2604
|
+
substr(result, 1, 1000) AS result, error,
|
|
2605
|
+
created_at, started_at, completed_at, delivered_at
|
|
2606
|
+
FROM jobs WHERE id = ?`).get(rootId);
|
|
2607
|
+
if (!rootRow) {
|
|
2608
|
+
sendJson(res, 404, { error: `job #${rootId} not found` });
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
const visited = new Set();
|
|
2612
|
+
async function build(row, depth) {
|
|
2613
|
+
visited.add(row.id);
|
|
2614
|
+
const node = { ...row, outputs: await listOutputs(row.id), children: [] };
|
|
2615
|
+
if (depth >= 10)
|
|
2616
|
+
return node;
|
|
2617
|
+
const kids = d.prepare(`SELECT id, agent, status, parent_id, call_depth,
|
|
2618
|
+
substr(prompt, 1, 400) AS prompt,
|
|
2619
|
+
substr(result, 1, 1000) AS result, error,
|
|
2620
|
+
created_at, started_at, completed_at, delivered_at
|
|
2621
|
+
FROM jobs WHERE parent_id = ? ORDER BY id`).all(row.id);
|
|
2622
|
+
for (const k of kids) {
|
|
2623
|
+
if (!visited.has(k.id))
|
|
2624
|
+
node.children.push(await build(k, depth + 1));
|
|
2625
|
+
}
|
|
2626
|
+
return node;
|
|
2627
|
+
}
|
|
2628
|
+
const tree = await build(rootRow, 0);
|
|
2629
|
+
sendJson(res, 200, { tree });
|
|
2630
|
+
}
|
|
2631
|
+
catch (err) {
|
|
2632
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2633
|
+
}
|
|
2634
|
+
finally {
|
|
2635
|
+
try {
|
|
2636
|
+
d.close();
|
|
2637
|
+
}
|
|
2638
|
+
catch { /* ignore */ }
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
// ============================================
|
|
2642
|
+
// Artifacts (v1.1.3)
|
|
2643
|
+
// ============================================
|
|
2644
|
+
async function handleArtifactsList(_req, res, jobId) {
|
|
2645
|
+
try {
|
|
2646
|
+
const { statJob } = await import('../core/artifacts.js');
|
|
2647
|
+
const s = await statJob(jobId);
|
|
2648
|
+
sendJson(res, 200, { jobId, inputs: s.inputs, outputs: s.outputs, totalBytes: s.bytes });
|
|
2649
|
+
}
|
|
2650
|
+
catch (err) {
|
|
2651
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
async function handleArtifactsFile(_req, res, jobId, name) {
|
|
2655
|
+
try {
|
|
2656
|
+
// Inputs are read-only-from-caller (just copies). Outputs are what
|
|
2657
|
+
// the callee produced. The web caller picks by name; we look in
|
|
2658
|
+
// outputs first (the common case for downloads) and fall back to
|
|
2659
|
+
// inputs if not found.
|
|
2660
|
+
const { readOutput, getArtifactsDir } = await import('../core/artifacts.js');
|
|
2661
|
+
let buf = await readOutput(jobId, name);
|
|
2662
|
+
if (!buf) {
|
|
2663
|
+
// Try inputs dir directly (no helper, reuse fs).
|
|
2664
|
+
const { existsSync, promises: fs } = await import('node:fs');
|
|
2665
|
+
const path = await import('node:path');
|
|
2666
|
+
const { inputDir } = getArtifactsDir(jobId);
|
|
2667
|
+
const p = path.join(inputDir, name);
|
|
2668
|
+
if (existsSync(p) && !name.includes('/') && name !== '..') {
|
|
2669
|
+
buf = await fs.readFile(p);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
if (!buf) {
|
|
2673
|
+
sendJson(res, 404, { error: `artifact ${name} not found for job ${jobId}` });
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
// Best-effort MIME — most agim artifacts are text/JSON/code so we
|
|
2677
|
+
// default to text/plain and let .json / .md / .csv keep their type.
|
|
2678
|
+
const lower = name.toLowerCase();
|
|
2679
|
+
const contentType = lower.endsWith('.json') ? 'application/json' :
|
|
2680
|
+
lower.endsWith('.md') ? 'text/markdown' :
|
|
2681
|
+
lower.endsWith('.csv') ? 'text/csv' :
|
|
2682
|
+
lower.endsWith('.html') ? 'text/html' :
|
|
2683
|
+
lower.endsWith('.png') ? 'image/png' :
|
|
2684
|
+
lower.endsWith('.jpg') || lower.endsWith('.jpeg') ? 'image/jpeg' :
|
|
2685
|
+
'text/plain; charset=utf-8';
|
|
2686
|
+
res.writeHead(200, {
|
|
2687
|
+
'Content-Type': contentType,
|
|
2688
|
+
'Content-Length': buf.length,
|
|
2689
|
+
'Content-Disposition': `inline; filename="${name.replace(/"/g, '_')}"`,
|
|
2690
|
+
});
|
|
2691
|
+
res.end(buf);
|
|
2692
|
+
}
|
|
2693
|
+
catch (err) {
|
|
2694
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
// ============================================
|
|
2427
2698
|
// WebSocket chat handlers
|
|
2428
2699
|
// ============================================
|
|
2429
2700
|
/**
|