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.
@@ -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,CAmoB/C"}
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"}
@@ -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 { listJobs, getJobStats } = await import('../core/job-board.js');
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 status = url.searchParams.get('status');
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 jobs = listJobs(limit, status || undefined, agent ? { agent } : {});
1310
- const stats = getJobStats();
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
  /**