@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session → Terminal-Engine injection adapter.
|
|
3
|
+
*
|
|
4
|
+
* The Terminal Engine owns the injection primitive (`injectIntoTerminal`,
|
|
5
|
+
* src/lib/terminal/inject.ts). This thin adapter maps a session's provenance
|
|
6
|
+
* `ReplyRail` (the addressable terminal the feed already derives — provenance.ts:47)
|
|
7
|
+
* to the engine's `InjectTarget`, so a caller can go session → keystrokes in one
|
|
8
|
+
* hop. Kept on the session side because `ReplyRail` is a session concept; the
|
|
9
|
+
* engine stays agnostic of how a target was discovered.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReplyRail } from './provenance.js';
|
|
12
|
+
import type { InjectTarget } from '../terminal/inject.js';
|
|
13
|
+
/**
|
|
14
|
+
* Map a session's `ReplyRail` to an engine `InjectTarget`. Today only tmux rails
|
|
15
|
+
* are externally addressable (provenance.ts:143-149); a null rail yields null and
|
|
16
|
+
* the caller must supply a target another way (a pty id, a macOS window).
|
|
17
|
+
*/
|
|
18
|
+
export declare function injectTargetFromReplyRail(rail: ReplyRail): InjectTarget | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session → Terminal-Engine injection adapter.
|
|
3
|
+
*
|
|
4
|
+
* The Terminal Engine owns the injection primitive (`injectIntoTerminal`,
|
|
5
|
+
* src/lib/terminal/inject.ts). This thin adapter maps a session's provenance
|
|
6
|
+
* `ReplyRail` (the addressable terminal the feed already derives — provenance.ts:47)
|
|
7
|
+
* to the engine's `InjectTarget`, so a caller can go session → keystrokes in one
|
|
8
|
+
* hop. Kept on the session side because `ReplyRail` is a session concept; the
|
|
9
|
+
* engine stays agnostic of how a target was discovered.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Map a session's `ReplyRail` to an engine `InjectTarget`. Today only tmux rails
|
|
13
|
+
* are externally addressable (provenance.ts:143-149); a null rail yields null and
|
|
14
|
+
* the caller must supply a target another way (a pty id, a macOS window).
|
|
15
|
+
*/
|
|
16
|
+
export function injectTargetFromReplyRail(rail) {
|
|
17
|
+
if (rail && rail.rail === 'tmux') {
|
|
18
|
+
return { backend: 'tmux', pane: rail.target, socket: rail.socket };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -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"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { execFileSync } from 'child_process';
|
|
11
|
+
import Database from '../sqlite.js';
|
|
11
12
|
/**
|
|
12
13
|
* Largest session file we will load into memory. Above this we throw a clean
|
|
13
14
|
* error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
|
|
@@ -96,6 +97,9 @@ export function parseSession(filePath, agent) {
|
|
|
96
97
|
case 'gemini':
|
|
97
98
|
events = parseGemini(filePath);
|
|
98
99
|
break;
|
|
100
|
+
case 'antigravity':
|
|
101
|
+
events = parseAntigravity(filePath);
|
|
102
|
+
break;
|
|
99
103
|
case 'opencode':
|
|
100
104
|
events = parseOpenCode(filePath);
|
|
101
105
|
break;
|
|
@@ -131,6 +135,12 @@ export function detectAgent(filePath) {
|
|
|
131
135
|
return 'claude';
|
|
132
136
|
if (filePath.includes('/.codex/') || filePath.includes('\\.codex\\'))
|
|
133
137
|
return 'codex';
|
|
138
|
+
// Antigravity lives under ~/.gemini/antigravity-cli/conversations/<uuid>.db, so
|
|
139
|
+
// it must be matched BEFORE the generic /.gemini/ check below or it would be
|
|
140
|
+
// misdetected as Gemini.
|
|
141
|
+
if ((filePath.includes('/antigravity-cli/conversations/') || filePath.includes('\\antigravity-cli\\conversations\\'))
|
|
142
|
+
&& filePath.endsWith('.db'))
|
|
143
|
+
return 'antigravity';
|
|
134
144
|
if (filePath.includes('/.gemini/') || filePath.includes('\\.gemini\\'))
|
|
135
145
|
return 'gemini';
|
|
136
146
|
if (filePath.includes('/.grok/') || filePath.includes('\\.grok\\'))
|
|
@@ -178,6 +188,11 @@ export function summarizeToolUse(tool, args) {
|
|
|
178
188
|
case 'WebSearch':
|
|
179
189
|
case 'WebFetch':
|
|
180
190
|
return `${tool}: ${truncate(args.query || args.url || '', 80)}`;
|
|
191
|
+
// Codex plan tool: arrives as a function_call with {plan:[{step,status}]}.
|
|
192
|
+
case 'update_plan': {
|
|
193
|
+
const steps = Array.isArray(args.plan) ? args.plan.length : 0;
|
|
194
|
+
return `Plan: ${steps} step${steps === 1 ? '' : 's'}`;
|
|
195
|
+
}
|
|
181
196
|
// Codex tools
|
|
182
197
|
case 'exec_command':
|
|
183
198
|
return `Bash: ${truncate(String(args.command || args.cmd || '').replace(/\n/g, ' ').trim(), 120)}`;
|
|
@@ -418,6 +433,16 @@ export function parseClaudeContent(content) {
|
|
|
418
433
|
export function parseCodex(filePath) {
|
|
419
434
|
return parseCodexContent(safeReadSessionFile(filePath));
|
|
420
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Extract the target file path from a Codex apply_patch envelope. The patch body
|
|
438
|
+
* opens with `*** Begin Patch` and carries one or more file ops of the form
|
|
439
|
+
* `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>`.
|
|
440
|
+
* Returns the first file path found, or undefined for an unparseable body.
|
|
441
|
+
*/
|
|
442
|
+
function applyPatchTargetPath(input) {
|
|
443
|
+
const m = input.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/m);
|
|
444
|
+
return m ? m[1].trim() : undefined;
|
|
445
|
+
}
|
|
421
446
|
/**
|
|
422
447
|
* Parse Codex JSONL *content* (already read into a string) into normalized
|
|
423
448
|
* events. Split from `parseCodex` so the tail reader can parse just the last
|
|
@@ -449,6 +474,23 @@ export function parseCodexContent(content) {
|
|
|
449
474
|
});
|
|
450
475
|
continue;
|
|
451
476
|
}
|
|
477
|
+
if (lineType === 'event_msg') {
|
|
478
|
+
// Web search is reported out-of-band as event_msg (not response_item).
|
|
479
|
+
// The paired begin (`web_search_call`) has no query; only `web_search_end`
|
|
480
|
+
// carries the resolved query string, so we emit exactly one tool_use per
|
|
481
|
+
// search off the end event and ignore the begin to avoid duplicates.
|
|
482
|
+
if (payload.type === 'web_search_end') {
|
|
483
|
+
const query = typeof payload.query === 'string' ? payload.query : '';
|
|
484
|
+
events.push({
|
|
485
|
+
type: 'tool_use',
|
|
486
|
+
agent: 'codex',
|
|
487
|
+
timestamp,
|
|
488
|
+
tool: 'WebSearch',
|
|
489
|
+
args: { query },
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
452
494
|
if (lineType === 'response_item') {
|
|
453
495
|
const ptype = payload.type;
|
|
454
496
|
if (ptype === 'message') {
|
|
@@ -523,6 +565,45 @@ export function parseCodexContent(content) {
|
|
|
523
565
|
if (callId)
|
|
524
566
|
callMap.delete(callId);
|
|
525
567
|
}
|
|
568
|
+
else if (ptype === 'custom_tool_call') {
|
|
569
|
+
// Codex edits arrive as custom_tool_call (apply_patch), NOT function_call.
|
|
570
|
+
const rawName = payload.name || 'unknown';
|
|
571
|
+
const input = typeof payload.input === 'string' ? payload.input : '';
|
|
572
|
+
const isApplyPatch = rawName === 'apply_patch';
|
|
573
|
+
const patchPath = isApplyPatch ? applyPatchTargetPath(input) : undefined;
|
|
574
|
+
// Normalize apply_patch to the shared Edit tool so it flows through
|
|
575
|
+
// artifact discovery (WRITE_TOOLS) and the Edit summarizer.
|
|
576
|
+
const tool = isApplyPatch ? 'Edit' : rawName;
|
|
577
|
+
const args = { input: input.length > 500 ? input.slice(0, 497) + '...' : input };
|
|
578
|
+
if (patchPath)
|
|
579
|
+
args.file_path = patchPath;
|
|
580
|
+
const callId = payload.call_id || payload.id;
|
|
581
|
+
if (callId)
|
|
582
|
+
callMap.set(callId, { name: tool, args });
|
|
583
|
+
events.push({
|
|
584
|
+
type: 'tool_use',
|
|
585
|
+
agent: 'codex',
|
|
586
|
+
timestamp,
|
|
587
|
+
tool,
|
|
588
|
+
args,
|
|
589
|
+
path: patchPath,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else if (ptype === 'custom_tool_call_output') {
|
|
593
|
+
const callId = payload.call_id || payload.id;
|
|
594
|
+
const callInfo = callId ? callMap.get(callId) : undefined;
|
|
595
|
+
const output = String(payload.output || '');
|
|
596
|
+
events.push({
|
|
597
|
+
type: 'tool_result',
|
|
598
|
+
agent: 'codex',
|
|
599
|
+
timestamp,
|
|
600
|
+
tool: callInfo?.name,
|
|
601
|
+
success: true,
|
|
602
|
+
output: output.length > 500 ? output.slice(0, 497) + '...' : output,
|
|
603
|
+
});
|
|
604
|
+
if (callId)
|
|
605
|
+
callMap.delete(callId);
|
|
606
|
+
}
|
|
526
607
|
else if (ptype === 'reasoning') {
|
|
527
608
|
// Codex reasoning -- try to get the readable summary
|
|
528
609
|
const summaries = payload.summary || [];
|
|
@@ -670,6 +751,206 @@ function extractGeminiContent(content) {
|
|
|
670
751
|
}
|
|
671
752
|
return '';
|
|
672
753
|
}
|
|
754
|
+
/** Read a base-128 varint from `b` at offset `i`; returns [value, nextOffset]. */
|
|
755
|
+
function readVarint(b, i) {
|
|
756
|
+
let shift = 0;
|
|
757
|
+
let val = 0;
|
|
758
|
+
for (;;) {
|
|
759
|
+
const byte = b[i++];
|
|
760
|
+
val += (byte & 0x7f) * 2 ** shift;
|
|
761
|
+
if ((byte & 0x80) === 0)
|
|
762
|
+
break;
|
|
763
|
+
shift += 7;
|
|
764
|
+
}
|
|
765
|
+
return [val, i];
|
|
766
|
+
}
|
|
767
|
+
/** Decode a protobuf message into a flat list of fields (one nesting level). */
|
|
768
|
+
function decodeProtoMessage(b) {
|
|
769
|
+
const out = [];
|
|
770
|
+
let i = 0;
|
|
771
|
+
while (i < b.length) {
|
|
772
|
+
let tag;
|
|
773
|
+
[tag, i] = readVarint(b, i);
|
|
774
|
+
const field = tag >>> 3;
|
|
775
|
+
const wire = tag & 7;
|
|
776
|
+
if (wire === 0) {
|
|
777
|
+
let v;
|
|
778
|
+
[v, i] = readVarint(b, i);
|
|
779
|
+
out.push({ field, wire, value: v });
|
|
780
|
+
}
|
|
781
|
+
else if (wire === 2) {
|
|
782
|
+
let len;
|
|
783
|
+
[len, i] = readVarint(b, i);
|
|
784
|
+
out.push({ field, wire, value: b.subarray(i, i + len) });
|
|
785
|
+
i += len;
|
|
786
|
+
}
|
|
787
|
+
else if (wire === 5) {
|
|
788
|
+
i += 4; // fixed32 — skipped
|
|
789
|
+
out.push({ field, wire, value: 0 });
|
|
790
|
+
}
|
|
791
|
+
else if (wire === 1) {
|
|
792
|
+
i += 8; // fixed64 — skipped
|
|
793
|
+
out.push({ field, wire, value: 0 });
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
break; // unknown wire type — stop to avoid runaway reads
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
const ANTIGRAVITY_TEXT_DECODER = new TextDecoder('utf-8', { fatal: false });
|
|
802
|
+
/**
|
|
803
|
+
* Recursively locate the tool-call node: the first sub-message that carries an
|
|
804
|
+
* f2 string tool-name AND an f3 JSON-args string. Also captures the f1 call id
|
|
805
|
+
* (used to dedupe the request + completion steps) and the f30/f31 human labels.
|
|
806
|
+
*/
|
|
807
|
+
function findAntigravityToolCall(fields) {
|
|
808
|
+
let id;
|
|
809
|
+
let name;
|
|
810
|
+
let argsJson;
|
|
811
|
+
let summary;
|
|
812
|
+
let action;
|
|
813
|
+
const subs = [];
|
|
814
|
+
for (const f of fields) {
|
|
815
|
+
if (f.wire !== 2)
|
|
816
|
+
continue;
|
|
817
|
+
const bytes = f.value;
|
|
818
|
+
const s = ANTIGRAVITY_TEXT_DECODER.decode(bytes);
|
|
819
|
+
if (f.field === 1 && !id && /^[a-z0-9]{4,16}$/.test(s))
|
|
820
|
+
id = s;
|
|
821
|
+
else if (f.field === 2 && !name && /^[a-z][a-z_]{2,30}$/.test(s))
|
|
822
|
+
name = s;
|
|
823
|
+
else if (f.field === 3 && !argsJson && s.startsWith('{') && s.includes('"toolAction"'))
|
|
824
|
+
argsJson = s;
|
|
825
|
+
else if (f.field === 30 && !summary)
|
|
826
|
+
summary = s;
|
|
827
|
+
else if (f.field === 31 && !action)
|
|
828
|
+
action = s;
|
|
829
|
+
else {
|
|
830
|
+
try {
|
|
831
|
+
subs.push(decodeProtoMessage(bytes));
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
/* not a nested message */
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (name && argsJson) {
|
|
839
|
+
let args = {};
|
|
840
|
+
try {
|
|
841
|
+
args = JSON.parse(argsJson);
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
args = { _raw: argsJson };
|
|
845
|
+
}
|
|
846
|
+
return { id, name, args, summary, action };
|
|
847
|
+
}
|
|
848
|
+
for (const sub of subs) {
|
|
849
|
+
const hit = findAntigravityToolCall(sub);
|
|
850
|
+
if (hit)
|
|
851
|
+
return hit;
|
|
852
|
+
}
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Map an Antigravity tool name onto the shared normalized vocabulary that the
|
|
857
|
+
* renderer already handles (so no render.ts changes are needed). Unknown tools
|
|
858
|
+
* pass through untouched (with their JSON args) so future tools are captured.
|
|
859
|
+
*/
|
|
860
|
+
const ANTIGRAVITY_TOOL_MAP = {
|
|
861
|
+
run_command: 'Bash',
|
|
862
|
+
view_file: 'Read',
|
|
863
|
+
read_file: 'Read',
|
|
864
|
+
list_dir: 'LS',
|
|
865
|
+
grep_search: 'Grep',
|
|
866
|
+
replace_file_content: 'Edit',
|
|
867
|
+
write_to_file: 'Write',
|
|
868
|
+
// Web tools surface identically once observed:
|
|
869
|
+
search_web: 'WebSearch',
|
|
870
|
+
read_url: 'WebFetch',
|
|
871
|
+
execute_url: 'WebFetch',
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Parse an Antigravity conversation SQLite DB into normalized tool_use events.
|
|
875
|
+
* Deduped by the tool-call id so each tool appears once (Antigravity writes a
|
|
876
|
+
* request step and a completion step that share the id).
|
|
877
|
+
*/
|
|
878
|
+
export function parseAntigravity(dbPath) {
|
|
879
|
+
// Read the raw BLOB payloads through the node/bun SQLite wrapper (not the
|
|
880
|
+
// `sqlite3` CLI) so this works on every OS — the CLI is absent on Windows.
|
|
881
|
+
let rows;
|
|
882
|
+
let db;
|
|
883
|
+
try {
|
|
884
|
+
db = new Database(dbPath);
|
|
885
|
+
rows = db
|
|
886
|
+
.prepare('SELECT idx, step_type, step_payload FROM steps ORDER BY idx;')
|
|
887
|
+
.all();
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
/* DB not accessible, sqlite module unavailable, or query failed */
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
try {
|
|
895
|
+
db?.close();
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
/* best-effort close */
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// Single timestamp for the whole session: the steps table carries no per-step
|
|
902
|
+
// time column, so fall back to the DB file's mtime for a stable, sortable value.
|
|
903
|
+
let timestamp = new Date().toISOString();
|
|
904
|
+
try {
|
|
905
|
+
timestamp = fs.statSync(dbPath).mtime.toISOString();
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
/* file vanished between query and stat — keep now() */
|
|
909
|
+
}
|
|
910
|
+
const events = [];
|
|
911
|
+
const seenCallIds = new Set();
|
|
912
|
+
for (const row of rows) {
|
|
913
|
+
const payload = row.step_payload;
|
|
914
|
+
// Both node:sqlite and bun:sqlite return a BLOB as a Uint8Array (Buffer is
|
|
915
|
+
// a subclass). NULL / non-blob payloads are skipped.
|
|
916
|
+
if (!(payload instanceof Uint8Array))
|
|
917
|
+
continue;
|
|
918
|
+
const bytes = payload;
|
|
919
|
+
let fields;
|
|
920
|
+
try {
|
|
921
|
+
fields = decodeProtoMessage(bytes);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const call = findAntigravityToolCall(fields);
|
|
927
|
+
if (!call)
|
|
928
|
+
continue;
|
|
929
|
+
// Dedupe: the request + completion steps of one tool share the f1 call id.
|
|
930
|
+
if (call.id) {
|
|
931
|
+
if (seenCallIds.has(call.id))
|
|
932
|
+
continue;
|
|
933
|
+
seenCallIds.add(call.id);
|
|
934
|
+
}
|
|
935
|
+
const norm = ANTIGRAVITY_TOOL_MAP[call.name] || call.name;
|
|
936
|
+
const a = call.args || {};
|
|
937
|
+
events.push({
|
|
938
|
+
type: 'tool_use',
|
|
939
|
+
agent: 'antigravity',
|
|
940
|
+
timestamp,
|
|
941
|
+
tool: norm,
|
|
942
|
+
args: a,
|
|
943
|
+
command: norm === 'Bash' ? a.CommandLine : undefined,
|
|
944
|
+
// Antigravity uses PascalCase arg keys; probe the known path-bearing ones.
|
|
945
|
+
path: a.AbsolutePath || a.TargetFile || a.DirectoryPath || a.SearchPath || undefined,
|
|
946
|
+
// Antigravity's own short human label — free, high quality. It surfaces
|
|
947
|
+
// either as the f30 string or (more reliably) as `toolSummary` inside the
|
|
948
|
+
// f3 JSON args.
|
|
949
|
+
content: call.summary || (typeof a.toolSummary === 'string' ? a.toolSummary : undefined) || undefined,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return events;
|
|
953
|
+
}
|
|
673
954
|
// ---------------------------------------------------------------------------
|
|
674
955
|
// OpenCode parser
|
|
675
956
|
// ---------------------------------------------------------------------------
|
|
@@ -1086,7 +1367,10 @@ export function parseKimi(filePath) {
|
|
|
1086
1367
|
const fn = event.function || {};
|
|
1087
1368
|
const toolName = typeof event.name === 'string' ? event.name : (fn.name || 'unknown');
|
|
1088
1369
|
let args = {};
|
|
1089
|
-
if (typeof
|
|
1370
|
+
if (event.args && typeof event.args === 'object') {
|
|
1371
|
+
args = event.args;
|
|
1372
|
+
}
|
|
1373
|
+
else if (typeof fn.arguments === 'string') {
|
|
1090
1374
|
try {
|
|
1091
1375
|
args = JSON.parse(fn.arguments);
|
|
1092
1376
|
}
|
|
@@ -1212,7 +1496,7 @@ export function parseDroid(filePath) {
|
|
|
1212
1496
|
tool: toolName,
|
|
1213
1497
|
args: toolInput,
|
|
1214
1498
|
path: toolInput.file_path || toolInput.path || undefined,
|
|
1215
|
-
command: toolName === 'Bash' ? toolInput.command : undefined,
|
|
1499
|
+
command: (toolName === 'Bash' || toolName === 'Execute') ? toolInput.command : undefined,
|
|
1216
1500
|
});
|
|
1217
1501
|
}
|
|
1218
1502
|
else if (block.type === 'tool_result') {
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export declare function extractSessionIdArg(args: string[]): string | undefined;
|
|
17
|
+
/** Record a launched agent process. Never throws — the registry is an optimization. */
|
|
18
|
+
export declare function writePidSessionEntry(entry: PidSessionEntry): void;
|
|
19
|
+
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
20
|
+
export declare function readPidSessionEntry(pid: number): PidSessionEntry | undefined;
|
|
21
|
+
/** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
|
|
22
|
+
export declare function prunePidSessionRegistry(isAlive: (pid: number) => boolean): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
/**
|
|
23
|
+
* Pull an explicit `--session-id <uuid>` (or `--session-id=<uuid>`) out of a
|
|
24
|
+
* raw agent arg vector. The transparent shim forwards args untouched, but when
|
|
25
|
+
* a launcher (Claude Code background jobs, IDE harnesses) already names the
|
|
26
|
+
* session, recording it gives the same exact pid -> session mapping `ag run`
|
|
27
|
+
* gets from generating the id itself.
|
|
28
|
+
*/
|
|
29
|
+
const SESSION_ID_VALUE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
+
export function extractSessionIdArg(args) {
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const a = args[i];
|
|
33
|
+
if (a === '--session-id') {
|
|
34
|
+
const v = args[i + 1];
|
|
35
|
+
if (v && SESSION_ID_VALUE_RE.test(v))
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
else if (a.startsWith('--session-id=')) {
|
|
39
|
+
const v = a.slice('--session-id='.length);
|
|
40
|
+
if (SESSION_ID_VALUE_RE.test(v))
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function pidRegistryDir() {
|
|
47
|
+
return path.join(getTerminalsDir(), 'by-pid');
|
|
48
|
+
}
|
|
49
|
+
function entryPath(pid) {
|
|
50
|
+
return path.join(pidRegistryDir(), `${pid}.json`);
|
|
51
|
+
}
|
|
52
|
+
/** Record a launched agent process. Never throws — the registry is an optimization. */
|
|
53
|
+
export function writePidSessionEntry(entry) {
|
|
54
|
+
if (!entry.pid || entry.pid < 1)
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(pidRegistryDir(), { recursive: true });
|
|
58
|
+
fs.writeFileSync(entryPath(entry.pid), JSON.stringify(entry), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* degrade to the newest-jsonl heuristic */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
65
|
+
export function readPidSessionEntry(pid) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(entryPath(pid), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* unparseable */
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
|
|
85
|
+
export function prunePidSessionRegistry(isAlive) {
|
|
86
|
+
let files;
|
|
87
|
+
try {
|
|
88
|
+
files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const f of files) {
|
|
94
|
+
const pid = Number(f.slice(0, -'.json'.length));
|
|
95
|
+
if (!Number.isInteger(pid) || isAlive(pid))
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(path.join(pidRegistryDir(), f));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* raced with another writer/pruner */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -13,11 +13,16 @@ export interface MuxLocation {
|
|
|
13
13
|
/** screen session name from $STY, e.g. '12345.pts-0.host'. */
|
|
14
14
|
session?: string;
|
|
15
15
|
}
|
|
16
|
-
/** How the feed can type back into a session, derived from rails that exist today. */
|
|
16
|
+
/** How the feed can type back into a session, derived from env rails that exist today. */
|
|
17
17
|
export type ReplyRail = {
|
|
18
18
|
rail: 'tmux';
|
|
19
19
|
target: string;
|
|
20
20
|
socket?: string;
|
|
21
|
+
}
|
|
22
|
+
/** iTerm2 split addressed by its session UUID (the part of $ITERM_SESSION_ID after ':'). */
|
|
23
|
+
| {
|
|
24
|
+
rail: 'iterm';
|
|
25
|
+
session: string;
|
|
21
26
|
} | null;
|
|
22
27
|
export interface SessionProvenance {
|
|
23
28
|
/** Machine the process runs on — os.hostname(). Drives HOSTS grouping. */
|
|
@@ -34,7 +39,14 @@ export interface SessionProvenance {
|
|
|
34
39
|
reply: ReplyRail;
|
|
35
40
|
}
|
|
36
41
|
/** Env vars that carry provenance. Kept small so the macOS `ps` scan stays cheap. */
|
|
37
|
-
export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY"];
|
|
42
|
+
export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY", "ITERM_SESSION_ID"];
|
|
43
|
+
/**
|
|
44
|
+
* Extract the iTerm2 session UUID from `$ITERM_SESSION_ID`, whose value is
|
|
45
|
+
* `w<window>t<tab>p<pane>:<UUID>` — the `:`-suffix is exactly what iTerm2's
|
|
46
|
+
* `id of session` returns, so it's the address for `tell session id …`. Returns
|
|
47
|
+
* undefined for an empty/absent value; tolerates a bare value with no `:`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseItermSession(value?: string): string | undefined;
|
|
38
50
|
/** Parse the NUL-separated body of /proc/<pid>/environ into a plain object. */
|
|
39
51
|
export declare function parseProcEnviron(buf: string): Record<string, string>;
|
|
40
52
|
/**
|
|
@@ -15,9 +15,22 @@
|
|
|
15
15
|
* (gone, or owned by another uid) yields `undefined`, never a guess.
|
|
16
16
|
*
|
|
17
17
|
* `reply` is a read-only hint, not a send channel: it reports whether a rail
|
|
18
|
-
* that can type back into this session exists today
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* that can type back into this session exists today and, if so, the exact
|
|
19
|
+
* address to type into. Two env-derived rails, in precedence order:
|
|
20
|
+
*
|
|
21
|
+
* - tmux — `$TMUX_PANE` (+ `$TMUX` socket): `tmux send-keys -t <pane>`. Works
|
|
22
|
+
* inside ANY host app (a tmux pane can live under iTerm/Ghostty/VS
|
|
23
|
+
* Code), so it wins whenever present.
|
|
24
|
+
* - iterm — `$ITERM_SESSION_ID` (`w<n>t<n>p<n>:<UUID>`): the exact iTerm2
|
|
25
|
+
* split, addressable by its session UUID via `tell session id …`
|
|
26
|
+
* (focus-safe, no `activate`). Used when there's no tmux above it.
|
|
27
|
+
*
|
|
28
|
+
* Both are inherited env vars, so the split the agent lives in is recovered with
|
|
29
|
+
* zero cooperation from the agent. Inherited/ignored stdin with neither rail =>
|
|
30
|
+
* null (not env-addressable — resolution may still find an IDE rail off disk,
|
|
31
|
+
* see src/lib/terminal/resolve.ts). The feed uses `reply` to decide whether to
|
|
32
|
+
* show a Send box; the resolver turns it into a concrete inject target (Gap 2,
|
|
33
|
+
* src/lib/terminal/inject.ts).
|
|
21
34
|
*/
|
|
22
35
|
import * as os from 'os';
|
|
23
36
|
import { execFile } from 'child_process';
|
|
@@ -32,7 +45,20 @@ export const PROVENANCE_ENV_KEYS = [
|
|
|
32
45
|
'TMUX_PANE',
|
|
33
46
|
'TERM_PROGRAM',
|
|
34
47
|
'STY',
|
|
48
|
+
'ITERM_SESSION_ID',
|
|
35
49
|
];
|
|
50
|
+
/**
|
|
51
|
+
* Extract the iTerm2 session UUID from `$ITERM_SESSION_ID`, whose value is
|
|
52
|
+
* `w<window>t<tab>p<pane>:<UUID>` — the `:`-suffix is exactly what iTerm2's
|
|
53
|
+
* `id of session` returns, so it's the address for `tell session id …`. Returns
|
|
54
|
+
* undefined for an empty/absent value; tolerates a bare value with no `:`.
|
|
55
|
+
*/
|
|
56
|
+
export function parseItermSession(value) {
|
|
57
|
+
if (!value)
|
|
58
|
+
return undefined;
|
|
59
|
+
const uuid = value.includes(':') ? value.slice(value.lastIndexOf(':') + 1) : value;
|
|
60
|
+
return uuid.trim() || undefined;
|
|
61
|
+
}
|
|
36
62
|
/** Parse the NUL-separated body of /proc/<pid>/environ into a plain object. */
|
|
37
63
|
export function parseProcEnviron(buf) {
|
|
38
64
|
const env = {};
|
|
@@ -99,13 +125,18 @@ export function deriveProvenance(env, hostname) {
|
|
|
99
125
|
else if (env.STY) {
|
|
100
126
|
mux = { kind: 'screen', session: env.STY };
|
|
101
127
|
}
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
128
|
+
// Env-addressable rails, in precedence order. A tmux pane wins because it can
|
|
129
|
+
// live inside any host app (`tmux send-keys -t <pane>` reaches it regardless of
|
|
130
|
+
// whether iTerm/Ghostty/VS Code is above it). Absent tmux, an iTerm2 split is
|
|
131
|
+
// addressable by its session UUID. Everything else (inherited stdin from
|
|
132
|
+
// `agents run`, ignored stdin from teams, a plain VS Code integrated terminal)
|
|
133
|
+
// is not env-addressable — the resolver may still find an IDE rail off disk.
|
|
134
|
+
const itermSession = parseItermSession(env.ITERM_SESSION_ID);
|
|
106
135
|
const reply = mux?.kind === 'tmux' && mux.pane
|
|
107
136
|
? { rail: 'tmux', target: mux.pane, socket: mux.socket }
|
|
108
|
-
:
|
|
137
|
+
: itermSession
|
|
138
|
+
? { rail: 'iterm', session: itermSession }
|
|
139
|
+
: null;
|
|
109
140
|
return {
|
|
110
141
|
host: hostname,
|
|
111
142
|
transport: ssh ? 'ssh' : 'local',
|
|
@@ -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>;
|