@phnx-labs/agents-cli 1.20.34 → 1.20.35

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -1
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/cost.js +17 -12
  6. package/dist/commands/doctor.d.ts +1 -0
  7. package/dist/commands/doctor.js +59 -14
  8. package/dist/commands/events.d.ts +16 -0
  9. package/dist/commands/events.js +173 -0
  10. package/dist/commands/exec.js +6 -1
  11. package/dist/commands/hosts.js +28 -24
  12. package/dist/commands/inspect.d.ts +2 -0
  13. package/dist/commands/inspect.js +54 -7
  14. package/dist/commands/logs.d.ts +17 -0
  15. package/dist/commands/logs.js +139 -0
  16. package/dist/commands/models.d.ts +3 -0
  17. package/dist/commands/models.js +27 -8
  18. package/dist/commands/permissions.js +9 -2
  19. package/dist/commands/repo.d.ts +34 -0
  20. package/dist/commands/repo.js +243 -65
  21. package/dist/commands/resource-view.d.ts +20 -0
  22. package/dist/commands/resource-view.js +90 -28
  23. package/dist/commands/secrets.js +40 -16
  24. package/dist/commands/sessions-resume.d.ts +2 -0
  25. package/dist/commands/sessions-resume.js +209 -0
  26. package/dist/commands/sessions-tail.d.ts +10 -0
  27. package/dist/commands/sessions-tail.js +11 -0
  28. package/dist/commands/sessions.d.ts +73 -1
  29. package/dist/commands/sessions.js +273 -53
  30. package/dist/commands/view.d.ts +3 -0
  31. package/dist/commands/view.js +19 -8
  32. package/dist/commands/wallet.d.ts +6 -0
  33. package/dist/commands/wallet.js +22 -5
  34. package/dist/index.js +58 -31
  35. package/dist/lib/agents.js +30 -9
  36. package/dist/lib/browser/cdp.js +5 -1
  37. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  38. package/dist/lib/browser/drivers/ssh.js +76 -16
  39. package/dist/lib/browser/ipc.d.ts +3 -0
  40. package/dist/lib/browser/ipc.js +13 -9
  41. package/dist/lib/browser/service.js +71 -15
  42. package/dist/lib/daemon.d.ts +35 -0
  43. package/dist/lib/daemon.js +33 -5
  44. package/dist/lib/devices/connect.d.ts +3 -2
  45. package/dist/lib/devices/connect.js +5 -3
  46. package/dist/lib/events.d.ts +9 -2
  47. package/dist/lib/events.js +70 -11
  48. package/dist/lib/exec.js +22 -0
  49. package/dist/lib/hooks.js +8 -2
  50. package/dist/lib/hosts/dispatch.js +6 -7
  51. package/dist/lib/hosts/logs.d.ts +16 -0
  52. package/dist/lib/hosts/logs.js +45 -0
  53. package/dist/lib/hosts/progress.d.ts +66 -0
  54. package/dist/lib/hosts/progress.js +125 -17
  55. package/dist/lib/hosts/ready.d.ts +23 -2
  56. package/dist/lib/hosts/ready.js +35 -13
  57. package/dist/lib/hosts/reconcile.d.ts +53 -0
  58. package/dist/lib/hosts/reconcile.js +81 -0
  59. package/dist/lib/hosts/tasks.d.ts +8 -0
  60. package/dist/lib/hosts/tasks.js +14 -0
  61. package/dist/lib/permissions.d.ts +4 -0
  62. package/dist/lib/permissions.js +35 -0
  63. package/dist/lib/picker.d.ts +24 -0
  64. package/dist/lib/picker.js +124 -0
  65. package/dist/lib/plugin-marketplace.d.ts +30 -0
  66. package/dist/lib/plugin-marketplace.js +215 -2
  67. package/dist/lib/plugins.d.ts +5 -0
  68. package/dist/lib/plugins.js +45 -4
  69. package/dist/lib/session/active.d.ts +7 -0
  70. package/dist/lib/session/active.js +11 -3
  71. package/dist/lib/session/discover.js +94 -0
  72. package/dist/lib/session/parse.d.ts +6 -0
  73. package/dist/lib/session/parse.js +283 -2
  74. package/dist/lib/session/pid-registry.d.ts +21 -0
  75. package/dist/lib/session/pid-registry.js +80 -0
  76. package/dist/lib/session/remote-active.d.ts +26 -0
  77. package/dist/lib/session/remote-active.js +141 -0
  78. package/dist/lib/session/remote.js +2 -6
  79. package/dist/lib/session/render.js +1 -1
  80. package/dist/lib/session/sync/config.d.ts +8 -0
  81. package/dist/lib/session/sync/config.js +11 -2
  82. package/dist/lib/session/types.d.ts +1 -1
  83. package/dist/lib/session/types.js +1 -1
  84. package/dist/lib/shims.d.ts +11 -3
  85. package/dist/lib/shims.js +54 -30
  86. package/dist/lib/ssh-exec.d.ts +26 -3
  87. package/dist/lib/ssh-exec.js +45 -3
  88. package/dist/lib/ssh-tunnel.d.ts +24 -5
  89. package/dist/lib/ssh-tunnel.js +60 -62
  90. package/dist/lib/staleness/writers/hooks.js +1 -1
  91. package/dist/lib/startup/command-registry.d.ts +2 -0
  92. package/dist/lib/startup/command-registry.js +4 -0
  93. package/dist/lib/teams/registry.js +25 -9
  94. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  95. package/dist/lib/terminal/backends/ghostty.js +69 -0
  96. package/dist/lib/terminal/backends/index.d.ts +18 -0
  97. package/dist/lib/terminal/backends/index.js +29 -0
  98. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  99. package/dist/lib/terminal/backends/iterm.js +62 -0
  100. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  101. package/dist/lib/terminal/backends/tmux.js +23 -0
  102. package/dist/lib/terminal/engine.d.ts +39 -0
  103. package/dist/lib/terminal/engine.js +54 -0
  104. package/dist/lib/terminal/index.d.ts +14 -0
  105. package/dist/lib/terminal/index.js +5 -0
  106. package/dist/lib/terminal/policy.d.ts +11 -0
  107. package/dist/lib/terminal/policy.js +11 -0
  108. package/dist/lib/terminal/quote.d.ts +11 -0
  109. package/dist/lib/terminal/quote.js +13 -0
  110. package/dist/lib/terminal/shell.d.ts +6 -0
  111. package/dist/lib/terminal/shell.js +23 -0
  112. package/dist/lib/terminal/transport.d.ts +15 -0
  113. package/dist/lib/terminal/transport.js +40 -0
  114. package/dist/lib/terminal/types.d.ts +59 -0
  115. package/dist/lib/terminal/types.js +13 -0
  116. package/dist/lib/types.d.ts +7 -0
  117. package/dist/lib/versions.d.ts +7 -3
  118. package/dist/lib/versions.js +10 -5
  119. package/dist/lib/whats-new.d.ts +9 -0
  120. package/dist/lib/whats-new.js +35 -0
  121. package/package.json +1 -1
@@ -20,6 +20,7 @@ import { walkForFiles } from '../fs-walk.js';
20
20
  import { getConfigSymlinkVersion } from '../shims.js';
21
21
  import { SESSION_AGENTS } from './types.js';
22
22
  import { extractSessionTopic } from './prompt.js';
23
+ import { parseAntigravity } from './parse.js';
23
24
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand } from './state.js';
24
25
  import { costOfUsage } from '../pricing/index.js';
25
26
  import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
@@ -60,6 +61,7 @@ export async function discoverSessions(options) {
60
61
  case 'claude': return scanClaudeIncremental(onProgress);
61
62
  case 'codex': return scanCodexIncremental(onProgress);
62
63
  case 'gemini': return scanGeminiIncremental(onProgress);
64
+ case 'antigravity': return scanAntigravityIncremental(onProgress);
63
65
  case 'opencode': return scanOpenCodeIncremental();
64
66
  case 'openclaw': return scanOpenClawIncremental();
65
67
  case 'rush': return scanRushIncremental(onProgress);
@@ -864,6 +866,98 @@ function buildGeminiProjectMap() {
864
866
  return map;
865
867
  }
866
868
  // ---------------------------------------------------------------------------
869
+ // Antigravity
870
+ //
871
+ // Antigravity stores one SQLite DB per conversation at
872
+ // ~/.gemini/antigravity-cli/conversations/<trajectory-uuid>.db. The filename
873
+ // (minus .db) is the canonical session id. Each DB is stat'd against the ledger;
874
+ // only changed DBs are re-parsed (via parseAntigravity, which shells out to
875
+ // sqlite3). Tool count doubles as the message count; the toolSummary of the
876
+ // first tool call becomes the topic, and any run_command's Cwd fills in cwd.
877
+ // ---------------------------------------------------------------------------
878
+ /** Incrementally re-scan changed Antigravity conversation DBs and upsert into the DB. */
879
+ async function scanAntigravityIncremental(onProgress) {
880
+ const currentVersion = await getCurrentAgentVersion('antigravity');
881
+ const filePaths = [];
882
+ const seenPaths = new Set();
883
+ for (const conversationsDir of getAgentSessionDirs('antigravity', 'conversations')) {
884
+ let files;
885
+ try {
886
+ files = fs.readdirSync(conversationsDir).filter(f => f.endsWith('.db'));
887
+ }
888
+ catch {
889
+ continue;
890
+ }
891
+ for (const file of files) {
892
+ const fp = path.join(conversationsDir, file);
893
+ if (seenPaths.has(fp))
894
+ continue;
895
+ seenPaths.add(fp);
896
+ filePaths.push(fp);
897
+ }
898
+ }
899
+ const changed = filterChangedFiles(filePaths);
900
+ if (changed.length === 0)
901
+ return;
902
+ onProgress?.({ agent: 'antigravity', parsed: 0, total: changed.length });
903
+ const entries = [];
904
+ const touched = [];
905
+ const seen = new Set();
906
+ let parsed = 0;
907
+ for (const { filePath, scan } of changed) {
908
+ try {
909
+ const result = readAntigravityMeta(filePath, currentVersion);
910
+ if (result && !seen.has(result.meta.id)) {
911
+ seen.add(result.meta.id);
912
+ entries.push({ meta: result.meta, content: result.content, scan });
913
+ }
914
+ else {
915
+ touched.push({ filePath, scan });
916
+ }
917
+ }
918
+ catch {
919
+ touched.push({ filePath, scan });
920
+ }
921
+ parsed++;
922
+ onProgress?.({ agent: 'antigravity', parsed, total: changed.length });
923
+ }
924
+ upsertSessionsBatch(entries);
925
+ recordScans(touched);
926
+ }
927
+ /** Parse a single Antigravity conversation DB to extract session metadata. */
928
+ function readAntigravityMeta(filePath, currentVersion) {
929
+ const sessionId = path.basename(filePath).replace(/\.db$/, '');
930
+ if (!sessionId)
931
+ return null;
932
+ const events = parseAntigravity(filePath);
933
+ // cwd: first run_command carries the working directory in its Cwd arg.
934
+ let cwd;
935
+ const contentParts = [];
936
+ for (const e of events) {
937
+ if (!cwd && typeof e.args?.Cwd === 'string' && e.args.Cwd)
938
+ cwd = e.args.Cwd;
939
+ if (e.content)
940
+ contentParts.push(e.content);
941
+ }
942
+ const normalizedCwd = cwd ? normalizeCwd(cwd) : undefined;
943
+ // Topic: the first tool's human summary is a decent one-line label.
944
+ const topic = events.find(e => e.content)?.content;
945
+ const stat = safeStatSync(filePath);
946
+ const meta = {
947
+ id: sessionId,
948
+ shortId: sessionId.slice(0, 8),
949
+ agent: 'antigravity',
950
+ timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
951
+ project: normalizedCwd ? path.basename(normalizedCwd) : undefined,
952
+ cwd: normalizedCwd,
953
+ filePath,
954
+ version: resolveSessionVersion('antigravity', filePath, undefined, currentVersion),
955
+ topic: topic ? topic.slice(0, 120) : undefined,
956
+ messageCount: events.length,
957
+ };
958
+ return { meta, content: contentParts.join('\n') };
959
+ }
960
+ // ---------------------------------------------------------------------------
867
961
  // OpenCode
868
962
  // ---------------------------------------------------------------------------
869
963
  const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
@@ -50,6 +50,12 @@ export declare function parseCodex(filePath: string): SessionEvent[];
50
50
  export declare function parseCodexContent(content: string): SessionEvent[];
51
51
  /** Parse a Gemini JSON session file into normalized events. */
52
52
  export declare function parseGemini(filePath: string): SessionEvent[];
53
+ /**
54
+ * Parse an Antigravity conversation SQLite DB into normalized tool_use events.
55
+ * Deduped by the tool-call id so each tool appears once (Antigravity writes a
56
+ * request step and a completion step that share the id).
57
+ */
58
+ export declare function parseAntigravity(dbPath: string): SessionEvent[];
53
59
  /**
54
60
  * Parse an OpenCode session from its SQLite database.
55
61
  * filePath format: "/path/to/opencode.db#session_id"
@@ -96,6 +96,9 @@ export function parseSession(filePath, agent) {
96
96
  case 'gemini':
97
97
  events = parseGemini(filePath);
98
98
  break;
99
+ case 'antigravity':
100
+ events = parseAntigravity(filePath);
101
+ break;
99
102
  case 'opencode':
100
103
  events = parseOpenCode(filePath);
101
104
  break;
@@ -131,6 +134,12 @@ export function detectAgent(filePath) {
131
134
  return 'claude';
132
135
  if (filePath.includes('/.codex/') || filePath.includes('\\.codex\\'))
133
136
  return 'codex';
137
+ // Antigravity lives under ~/.gemini/antigravity-cli/conversations/<uuid>.db, so
138
+ // it must be matched BEFORE the generic /.gemini/ check below or it would be
139
+ // misdetected as Gemini.
140
+ if ((filePath.includes('/antigravity-cli/conversations/') || filePath.includes('\\antigravity-cli\\conversations\\'))
141
+ && filePath.endsWith('.db'))
142
+ return 'antigravity';
134
143
  if (filePath.includes('/.gemini/') || filePath.includes('\\.gemini\\'))
135
144
  return 'gemini';
136
145
  if (filePath.includes('/.grok/') || filePath.includes('\\.grok\\'))
@@ -178,6 +187,11 @@ export function summarizeToolUse(tool, args) {
178
187
  case 'WebSearch':
179
188
  case 'WebFetch':
180
189
  return `${tool}: ${truncate(args.query || args.url || '', 80)}`;
190
+ // Codex plan tool: arrives as a function_call with {plan:[{step,status}]}.
191
+ case 'update_plan': {
192
+ const steps = Array.isArray(args.plan) ? args.plan.length : 0;
193
+ return `Plan: ${steps} step${steps === 1 ? '' : 's'}`;
194
+ }
181
195
  // Codex tools
182
196
  case 'exec_command':
183
197
  return `Bash: ${truncate(String(args.command || args.cmd || '').replace(/\n/g, ' ').trim(), 120)}`;
@@ -418,6 +432,16 @@ export function parseClaudeContent(content) {
418
432
  export function parseCodex(filePath) {
419
433
  return parseCodexContent(safeReadSessionFile(filePath));
420
434
  }
435
+ /**
436
+ * Extract the target file path from a Codex apply_patch envelope. The patch body
437
+ * opens with `*** Begin Patch` and carries one or more file ops of the form
438
+ * `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>`.
439
+ * Returns the first file path found, or undefined for an unparseable body.
440
+ */
441
+ function applyPatchTargetPath(input) {
442
+ const m = input.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/m);
443
+ return m ? m[1].trim() : undefined;
444
+ }
421
445
  /**
422
446
  * Parse Codex JSONL *content* (already read into a string) into normalized
423
447
  * events. Split from `parseCodex` so the tail reader can parse just the last
@@ -449,6 +473,23 @@ export function parseCodexContent(content) {
449
473
  });
450
474
  continue;
451
475
  }
476
+ if (lineType === 'event_msg') {
477
+ // Web search is reported out-of-band as event_msg (not response_item).
478
+ // The paired begin (`web_search_call`) has no query; only `web_search_end`
479
+ // carries the resolved query string, so we emit exactly one tool_use per
480
+ // search off the end event and ignore the begin to avoid duplicates.
481
+ if (payload.type === 'web_search_end') {
482
+ const query = typeof payload.query === 'string' ? payload.query : '';
483
+ events.push({
484
+ type: 'tool_use',
485
+ agent: 'codex',
486
+ timestamp,
487
+ tool: 'WebSearch',
488
+ args: { query },
489
+ });
490
+ }
491
+ continue;
492
+ }
452
493
  if (lineType === 'response_item') {
453
494
  const ptype = payload.type;
454
495
  if (ptype === 'message') {
@@ -523,6 +564,45 @@ export function parseCodexContent(content) {
523
564
  if (callId)
524
565
  callMap.delete(callId);
525
566
  }
567
+ else if (ptype === 'custom_tool_call') {
568
+ // Codex edits arrive as custom_tool_call (apply_patch), NOT function_call.
569
+ const rawName = payload.name || 'unknown';
570
+ const input = typeof payload.input === 'string' ? payload.input : '';
571
+ const isApplyPatch = rawName === 'apply_patch';
572
+ const patchPath = isApplyPatch ? applyPatchTargetPath(input) : undefined;
573
+ // Normalize apply_patch to the shared Edit tool so it flows through
574
+ // artifact discovery (WRITE_TOOLS) and the Edit summarizer.
575
+ const tool = isApplyPatch ? 'Edit' : rawName;
576
+ const args = { input: input.length > 500 ? input.slice(0, 497) + '...' : input };
577
+ if (patchPath)
578
+ args.file_path = patchPath;
579
+ const callId = payload.call_id || payload.id;
580
+ if (callId)
581
+ callMap.set(callId, { name: tool, args });
582
+ events.push({
583
+ type: 'tool_use',
584
+ agent: 'codex',
585
+ timestamp,
586
+ tool,
587
+ args,
588
+ path: patchPath,
589
+ });
590
+ }
591
+ else if (ptype === 'custom_tool_call_output') {
592
+ const callId = payload.call_id || payload.id;
593
+ const callInfo = callId ? callMap.get(callId) : undefined;
594
+ const output = String(payload.output || '');
595
+ events.push({
596
+ type: 'tool_result',
597
+ agent: 'codex',
598
+ timestamp,
599
+ tool: callInfo?.name,
600
+ success: true,
601
+ output: output.length > 500 ? output.slice(0, 497) + '...' : output,
602
+ });
603
+ if (callId)
604
+ callMap.delete(callId);
605
+ }
526
606
  else if (ptype === 'reasoning') {
527
607
  // Codex reasoning -- try to get the readable summary
528
608
  const summaries = payload.summary || [];
@@ -670,6 +750,204 @@ function extractGeminiContent(content) {
670
750
  }
671
751
  return '';
672
752
  }
753
+ /** Read a base-128 varint from `b` at offset `i`; returns [value, nextOffset]. */
754
+ function readVarint(b, i) {
755
+ let shift = 0;
756
+ let val = 0;
757
+ for (;;) {
758
+ const byte = b[i++];
759
+ val += (byte & 0x7f) * 2 ** shift;
760
+ if ((byte & 0x80) === 0)
761
+ break;
762
+ shift += 7;
763
+ }
764
+ return [val, i];
765
+ }
766
+ /** Decode a protobuf message into a flat list of fields (one nesting level). */
767
+ function decodeProtoMessage(b) {
768
+ const out = [];
769
+ let i = 0;
770
+ while (i < b.length) {
771
+ let tag;
772
+ [tag, i] = readVarint(b, i);
773
+ const field = tag >>> 3;
774
+ const wire = tag & 7;
775
+ if (wire === 0) {
776
+ let v;
777
+ [v, i] = readVarint(b, i);
778
+ out.push({ field, wire, value: v });
779
+ }
780
+ else if (wire === 2) {
781
+ let len;
782
+ [len, i] = readVarint(b, i);
783
+ out.push({ field, wire, value: b.subarray(i, i + len) });
784
+ i += len;
785
+ }
786
+ else if (wire === 5) {
787
+ i += 4; // fixed32 — skipped
788
+ out.push({ field, wire, value: 0 });
789
+ }
790
+ else if (wire === 1) {
791
+ i += 8; // fixed64 — skipped
792
+ out.push({ field, wire, value: 0 });
793
+ }
794
+ else {
795
+ break; // unknown wire type — stop to avoid runaway reads
796
+ }
797
+ }
798
+ return out;
799
+ }
800
+ const ANTIGRAVITY_TEXT_DECODER = new TextDecoder('utf-8', { fatal: false });
801
+ /**
802
+ * Recursively locate the tool-call node: the first sub-message that carries an
803
+ * f2 string tool-name AND an f3 JSON-args string. Also captures the f1 call id
804
+ * (used to dedupe the request + completion steps) and the f30/f31 human labels.
805
+ */
806
+ function findAntigravityToolCall(fields) {
807
+ let id;
808
+ let name;
809
+ let argsJson;
810
+ let summary;
811
+ let action;
812
+ const subs = [];
813
+ for (const f of fields) {
814
+ if (f.wire !== 2)
815
+ continue;
816
+ const bytes = f.value;
817
+ const s = ANTIGRAVITY_TEXT_DECODER.decode(bytes);
818
+ if (f.field === 1 && !id && /^[a-z0-9]{4,16}$/.test(s))
819
+ id = s;
820
+ else if (f.field === 2 && !name && /^[a-z][a-z_]{2,30}$/.test(s))
821
+ name = s;
822
+ else if (f.field === 3 && !argsJson && s.startsWith('{') && s.includes('"toolAction"'))
823
+ argsJson = s;
824
+ else if (f.field === 30 && !summary)
825
+ summary = s;
826
+ else if (f.field === 31 && !action)
827
+ action = s;
828
+ else {
829
+ try {
830
+ subs.push(decodeProtoMessage(bytes));
831
+ }
832
+ catch {
833
+ /* not a nested message */
834
+ }
835
+ }
836
+ }
837
+ if (name && argsJson) {
838
+ let args = {};
839
+ try {
840
+ args = JSON.parse(argsJson);
841
+ }
842
+ catch {
843
+ args = { _raw: argsJson };
844
+ }
845
+ return { id, name, args, summary, action };
846
+ }
847
+ for (const sub of subs) {
848
+ const hit = findAntigravityToolCall(sub);
849
+ if (hit)
850
+ return hit;
851
+ }
852
+ return null;
853
+ }
854
+ /**
855
+ * Map an Antigravity tool name onto the shared normalized vocabulary that the
856
+ * renderer already handles (so no render.ts changes are needed). Unknown tools
857
+ * pass through untouched (with their JSON args) so future tools are captured.
858
+ */
859
+ const ANTIGRAVITY_TOOL_MAP = {
860
+ run_command: 'Bash',
861
+ view_file: 'Read',
862
+ read_file: 'Read',
863
+ list_dir: 'LS',
864
+ grep_search: 'Grep',
865
+ replace_file_content: 'Edit',
866
+ write_to_file: 'Write',
867
+ // Web tools surface identically once observed:
868
+ search_web: 'WebSearch',
869
+ read_url: 'WebFetch',
870
+ execute_url: 'WebFetch',
871
+ };
872
+ /**
873
+ * Parse an Antigravity conversation SQLite DB into normalized tool_use events.
874
+ * Deduped by the tool-call id so each tool appears once (Antigravity writes a
875
+ * request step and a completion step that share the id).
876
+ */
877
+ export function parseAntigravity(dbPath) {
878
+ let rows;
879
+ try {
880
+ rows = execFileSync('sqlite3', ['-separator', '\t', dbPath, 'SELECT idx, step_type, quote(step_payload) FROM steps ORDER BY idx;'], {
881
+ encoding: 'utf-8',
882
+ timeout: 10000,
883
+ maxBuffer: 64 * 1024 * 1024,
884
+ stdio: ['ignore', 'pipe', 'ignore'],
885
+ });
886
+ }
887
+ catch {
888
+ /* DB not accessible, sqlite3 missing, or query failed */
889
+ return [];
890
+ }
891
+ // Single timestamp for the whole session: the steps table carries no per-step
892
+ // time column, so fall back to the DB file's mtime for a stable, sortable value.
893
+ let timestamp = new Date().toISOString();
894
+ try {
895
+ timestamp = fs.statSync(dbPath).mtime.toISOString();
896
+ }
897
+ catch {
898
+ /* file vanished between query and stat — keep now() */
899
+ }
900
+ const events = [];
901
+ const seenCallIds = new Set();
902
+ for (const line of rows.split('\n')) {
903
+ if (!line.trim())
904
+ continue;
905
+ const tab1 = line.indexOf('\t');
906
+ if (tab1 === -1)
907
+ continue;
908
+ const tab2 = line.indexOf('\t', tab1 + 1);
909
+ if (tab2 === -1)
910
+ continue;
911
+ const hex = line.slice(tab2 + 1).trim();
912
+ // sqlite3 quote() renders a BLOB as X'...'; anything else (NULL) is skipped.
913
+ if (!hex.startsWith("X'") || !hex.endsWith("'"))
914
+ continue;
915
+ const bytes = Uint8Array.from(Buffer.from(hex.slice(2, -1), 'hex'));
916
+ let fields;
917
+ try {
918
+ fields = decodeProtoMessage(bytes);
919
+ }
920
+ catch {
921
+ continue;
922
+ }
923
+ const call = findAntigravityToolCall(fields);
924
+ if (!call)
925
+ continue;
926
+ // Dedupe: the request + completion steps of one tool share the f1 call id.
927
+ if (call.id) {
928
+ if (seenCallIds.has(call.id))
929
+ continue;
930
+ seenCallIds.add(call.id);
931
+ }
932
+ const norm = ANTIGRAVITY_TOOL_MAP[call.name] || call.name;
933
+ const a = call.args || {};
934
+ events.push({
935
+ type: 'tool_use',
936
+ agent: 'antigravity',
937
+ timestamp,
938
+ tool: norm,
939
+ args: a,
940
+ command: norm === 'Bash' ? a.CommandLine : undefined,
941
+ // Antigravity uses PascalCase arg keys; probe the known path-bearing ones.
942
+ path: a.AbsolutePath || a.TargetFile || a.DirectoryPath || a.SearchPath || undefined,
943
+ // Antigravity's own short human label — free, high quality. It surfaces
944
+ // either as the f30 string or (more reliably) as `toolSummary` inside the
945
+ // f3 JSON args.
946
+ content: call.summary || (typeof a.toolSummary === 'string' ? a.toolSummary : undefined) || undefined,
947
+ });
948
+ }
949
+ return events;
950
+ }
673
951
  // ---------------------------------------------------------------------------
674
952
  // OpenCode parser
675
953
  // ---------------------------------------------------------------------------
@@ -1086,7 +1364,10 @@ export function parseKimi(filePath) {
1086
1364
  const fn = event.function || {};
1087
1365
  const toolName = typeof event.name === 'string' ? event.name : (fn.name || 'unknown');
1088
1366
  let args = {};
1089
- if (typeof fn.arguments === 'string') {
1367
+ if (event.args && typeof event.args === 'object') {
1368
+ args = event.args;
1369
+ }
1370
+ else if (typeof fn.arguments === 'string') {
1090
1371
  try {
1091
1372
  args = JSON.parse(fn.arguments);
1092
1373
  }
@@ -1212,7 +1493,7 @@ export function parseDroid(filePath) {
1212
1493
  tool: toolName,
1213
1494
  args: toolInput,
1214
1495
  path: toolInput.file_path || toolInput.path || undefined,
1215
- command: toolName === 'Bash' ? toolInput.command : undefined,
1496
+ command: (toolName === 'Bash' || toolName === 'Execute') ? toolInput.command : undefined,
1216
1497
  });
1217
1498
  }
1218
1499
  else if (block.type === 'tool_result') {
@@ -0,0 +1,21 @@
1
+ export interface PidSessionEntry {
2
+ pid: number;
3
+ agent: string;
4
+ /** The launch session id. Present for agents launched with a known id (Claude). */
5
+ sessionId?: string;
6
+ cwd?: string;
7
+ /**
8
+ * `$TMUX_PANE` at launch — stored for diagnostics and possible future
9
+ * disambiguation. NOT currently consulted on read: the listing path keys
10
+ * purely on pid (stale entries are pruned when the pid dies), so this is
11
+ * metadata, not an anti-collision key.
12
+ */
13
+ tmuxPane?: string;
14
+ startedAtMs: number;
15
+ }
16
+ /** Record a launched agent process. Never throws — the registry is an optimization. */
17
+ export declare function writePidSessionEntry(entry: PidSessionEntry): void;
18
+ /** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
19
+ export declare function readPidSessionEntry(pid: number): PidSessionEntry | undefined;
20
+ /** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
21
+ export declare function prunePidSessionRegistry(isAlive: (pid: number) => boolean): void;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Per-PID session registry — the headless equivalent of the swarmify VS Code
3
+ * extension's `live-terminals.json`.
4
+ *
5
+ * On a machine with no terminal extension (a bare SSH/tmux host), the only way
6
+ * `ag sessions --active` can attribute a `ps`-discovered agent process to a
7
+ * session is to guess "newest .jsonl in the cwd" — which collapses N agents
8
+ * sharing one repo onto a single session (all rows show the same topic/id).
9
+ *
10
+ * `ag run` closes that gap by recording, at spawn time, one file per launched
11
+ * agent process keyed by its OS pid: the exact session id it was launched with
12
+ * (Claude is started with `--session-id <uuid>`, so the launcher knows it),
13
+ * plus agent/cwd/tmux pane. The active-sessions headless path reads it back for
14
+ * an exact pid -> session match instead of a heuristic.
15
+ *
16
+ * Best-effort throughout: a failed write or a corrupt file degrades to the old
17
+ * heuristic, never throws into the launch or the listing path.
18
+ */
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import { getTerminalsDir } from '../state.js';
22
+ function pidRegistryDir() {
23
+ return path.join(getTerminalsDir(), 'by-pid');
24
+ }
25
+ function entryPath(pid) {
26
+ return path.join(pidRegistryDir(), `${pid}.json`);
27
+ }
28
+ /** Record a launched agent process. Never throws — the registry is an optimization. */
29
+ export function writePidSessionEntry(entry) {
30
+ if (!entry.pid || entry.pid < 1)
31
+ return;
32
+ try {
33
+ fs.mkdirSync(pidRegistryDir(), { recursive: true });
34
+ fs.writeFileSync(entryPath(entry.pid), JSON.stringify(entry), 'utf8');
35
+ }
36
+ catch {
37
+ /* degrade to the newest-jsonl heuristic */
38
+ }
39
+ }
40
+ /** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
41
+ export function readPidSessionEntry(pid) {
42
+ let raw;
43
+ try {
44
+ raw = fs.readFileSync(entryPath(pid), 'utf8');
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
52
+ return parsed;
53
+ }
54
+ }
55
+ catch {
56
+ /* unparseable */
57
+ }
58
+ return undefined;
59
+ }
60
+ /** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
61
+ export function prunePidSessionRegistry(isAlive) {
62
+ let files;
63
+ try {
64
+ files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ for (const f of files) {
70
+ const pid = Number(f.slice(0, -'.json'.length));
71
+ if (!Number.isInteger(pid) || isAlive(pid))
72
+ continue;
73
+ try {
74
+ fs.unlinkSync(path.join(pidRegistryDir(), f));
75
+ }
76
+ catch {
77
+ /* raced with another writer/pruner */
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,26 @@
1
+ import type { ActiveSession } from './active.js';
2
+ /**
3
+ * Recursion guard, passed as an env var (not a CLI flag) so an OLDER remote
4
+ * `agents` that predates this feature ignores it harmlessly instead of erroring
5
+ * on an unknown option. A remote new enough to fan out reads it and stays local.
6
+ */
7
+ export declare const NO_FANOUT_ENV = "AGENTS_SESSIONS_LOCAL";
8
+ /**
9
+ * Parse a peer's `--active --json` stdout into active sessions, tagging each
10
+ * with `machine`. Defensive against version skew / partial output: non-JSON or
11
+ * a non-array yields `[]`, and non-object entries are dropped rather than
12
+ * throwing. Exported for unit testing without a live tailnet.
13
+ */
14
+ export declare function parseRemoteActive(stdout: string, machine: string): ActiveSession[];
15
+ export interface RemoteActiveResult {
16
+ sessions: ActiveSession[];
17
+ /** How many peer machines we attempted to reach (drives the empty-fleet tip). */
18
+ deviceCount: number;
19
+ }
20
+ /**
21
+ * Gather active sessions from other machines. With an explicit `hosts` list
22
+ * (from `--host`), fan out to exactly those. Otherwise sweep the registered,
23
+ * online devices from `ag devices`, excluding this machine and any without an
24
+ * address. Results from all peers run in parallel and are flattened.
25
+ */
26
+ export declare function gatherRemoteActive(hosts?: string[]): Promise<RemoteActiveResult>;