@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
|
@@ -24,13 +24,23 @@ import { promisify } from 'util';
|
|
|
24
24
|
import { listActiveTasks } from '../cloud/store.js';
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
26
|
import { getTerminalsDir } from '../state.js';
|
|
27
|
+
import { readPidSessionEntry, prunePidSessionRegistry } from './pid-registry.js';
|
|
27
28
|
import { buildClaudeLabelMap } from './discover.js';
|
|
28
29
|
import { latestSessionFileForCwd } from './db.js';
|
|
29
30
|
import { extractSessionTopic } from './prompt.js';
|
|
30
31
|
import { readSessionTail } from './tail.js';
|
|
31
32
|
import { inferSessionState } from './state.js';
|
|
32
33
|
import { detectProvenance } from './provenance.js';
|
|
34
|
+
import { mapBounded } from '../concurrency.js';
|
|
33
35
|
const execFileAsync = promisify(execFile);
|
|
36
|
+
/**
|
|
37
|
+
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
38
|
+
* fan-out: a simultaneous system-wide `lsof` burst reads to behavioral EDR
|
|
39
|
+
* (CrowdStrike Falcon) as lateral-movement recon. Results are identical — the
|
|
40
|
+
* cwds are just gathered at a bounded spawn rate instead of a single burst.
|
|
41
|
+
*/
|
|
42
|
+
export const LSOF_CONCURRENCY = 4;
|
|
43
|
+
const LSOF_STAGGER_MS = 10;
|
|
34
44
|
const HOME = os.homedir();
|
|
35
45
|
const LIVE_TERMINALS_FILE = path.join(getTerminalsDir(), 'live-terminals.json');
|
|
36
46
|
/**
|
|
@@ -48,6 +58,19 @@ const AGENT_CLI_NAMES = {
|
|
|
48
58
|
opencode: 'opencode',
|
|
49
59
|
droid: 'droid',
|
|
50
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an agent kind from a process's reported executable. `comm` may be an
|
|
63
|
+
* absolute path (shim-launched agents), and Windows image names carry an
|
|
64
|
+
* `.exe` suffix (`claude.exe`), so basename + suffix-strip before the lookup.
|
|
65
|
+
*/
|
|
66
|
+
function agentKindFromComm(commRaw) {
|
|
67
|
+
const base = path.basename(commRaw);
|
|
68
|
+
const stripped = base.replace(/\.exe$/i, '');
|
|
69
|
+
// Windows image names compare case-insensitively; POSIX comms stay exact —
|
|
70
|
+
// macOS's Claude desktop app process is named 'Claude' and must NOT match.
|
|
71
|
+
const key = stripped === base ? base : stripped.toLowerCase();
|
|
72
|
+
return AGENT_CLI_NAMES[key];
|
|
73
|
+
}
|
|
51
74
|
function isPidAlive(pid) {
|
|
52
75
|
if (!pid || pid < 1)
|
|
53
76
|
return false;
|
|
@@ -354,14 +377,14 @@ export function listCloudActive() {
|
|
|
354
377
|
* and a terminal-app is preferred over the multiplexer inside it.
|
|
355
378
|
*/
|
|
356
379
|
const HOST_MATCHERS = [
|
|
357
|
-
// IDE renderers (Electron helper processes)
|
|
358
|
-
{ host: 'code', tokens: ['Code Helper', 'Code - Insiders Helper'] },
|
|
359
|
-
{ host: 'cursor', tokens: ['Cursor Helper'] },
|
|
360
|
-
{ host: 'codium', tokens: ['VSCodium Helper'] },
|
|
361
|
-
{ host: 'windsurf', tokens: ['Windsurf Helper'] },
|
|
380
|
+
// IDE renderers (Electron helper processes on macOS, image names on Windows)
|
|
381
|
+
{ host: 'code', tokens: ['Code Helper', 'Code - Insiders Helper', 'Code.exe'] },
|
|
382
|
+
{ host: 'cursor', tokens: ['Cursor Helper', 'Cursor.exe'] },
|
|
383
|
+
{ host: 'codium', tokens: ['VSCodium Helper', 'VSCodium.exe'] },
|
|
384
|
+
{ host: 'windsurf', tokens: ['Windsurf Helper', 'Windsurf.exe'] },
|
|
362
385
|
// Native terminal apps
|
|
363
386
|
{ host: 'iterm', tokens: ['iTerm2', 'iTermServer', 'iTerm'] },
|
|
364
|
-
{ host: 'terminal', tokens: ['Terminal.app', '/Applications/Utilities/Terminal.app'] },
|
|
387
|
+
{ host: 'terminal', tokens: ['Terminal.app', '/Applications/Utilities/Terminal.app', 'WindowsTerminal.exe'] },
|
|
365
388
|
{ host: 'warp', tokens: ['Warp.app', 'stable_'] },
|
|
366
389
|
{ host: 'alacritty', tokens: ['alacritty', 'Alacritty'] },
|
|
367
390
|
{ host: 'kitty', tokens: ['kitty'] },
|
|
@@ -379,6 +402,8 @@ const HOST_MATCHERS = [
|
|
|
379
402
|
* matching against AGENT_CLI_NAMES.
|
|
380
403
|
*/
|
|
381
404
|
async function readProcessTable() {
|
|
405
|
+
if (process.platform === 'win32')
|
|
406
|
+
return readProcessTableWin32();
|
|
382
407
|
let out;
|
|
383
408
|
try {
|
|
384
409
|
({ stdout: out } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,comm='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
|
|
@@ -396,8 +421,41 @@ async function readProcessTable() {
|
|
|
396
421
|
if (!Number.isFinite(pid) || !Number.isFinite(ppid))
|
|
397
422
|
continue;
|
|
398
423
|
const commRaw = m[3].trim();
|
|
399
|
-
|
|
400
|
-
|
|
424
|
+
rows.push({ pid, ppid, comm: commRaw, kind: agentKindFromComm(commRaw) });
|
|
425
|
+
}
|
|
426
|
+
return rows;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Windows process table in one CIM query (`wmic` is removed on current
|
|
430
|
+
* Windows 11, so PowerShell is the stable interface). Same pid/ppid/comm
|
|
431
|
+
* shape as the POSIX `ps` snapshot; `Name` is the image name (`claude.exe`).
|
|
432
|
+
*/
|
|
433
|
+
async function readProcessTableWin32() {
|
|
434
|
+
let out;
|
|
435
|
+
try {
|
|
436
|
+
({ stdout: out } = await execFileAsync('powershell.exe', [
|
|
437
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
438
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name | ConvertTo-Csv -NoTypeInformation',
|
|
439
|
+
], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true }));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
return parseWin32ProcessCsv(out);
|
|
445
|
+
}
|
|
446
|
+
/** Parse `ConvertTo-Csv` output of Win32_Process rows. Exported for tests. */
|
|
447
|
+
export function parseWin32ProcessCsv(out) {
|
|
448
|
+
const rows = [];
|
|
449
|
+
for (const line of out.split(/\r?\n/)) {
|
|
450
|
+
const m = line.trim().match(/^"(\d+)","(\d+)","(.*)"$/);
|
|
451
|
+
if (!m)
|
|
452
|
+
continue;
|
|
453
|
+
const pid = parseInt(m[1], 10);
|
|
454
|
+
const ppid = parseInt(m[2], 10);
|
|
455
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid))
|
|
456
|
+
continue;
|
|
457
|
+
const comm = m[3].replace(/""/g, '"');
|
|
458
|
+
rows.push({ pid, ppid, comm, kind: agentKindFromComm(comm) });
|
|
401
459
|
}
|
|
402
460
|
return rows;
|
|
403
461
|
}
|
|
@@ -417,12 +475,25 @@ function hasAttributedAncestor(pid, ppidMap, attributed) {
|
|
|
417
475
|
}
|
|
418
476
|
return false;
|
|
419
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Resolve every candidate PID's cwd, bounded and staggered so the probes no
|
|
480
|
+
* longer fan out as one simultaneous system-wide `lsof` burst (a behavioral-EDR
|
|
481
|
+
* recon trigger). Order matches the input `pids`. The `probe` seam is injectable
|
|
482
|
+
* for testing the bound; production always uses the real `lsof`-backed probe.
|
|
483
|
+
*/
|
|
484
|
+
export function resolveCwds(pids, probe = getCwdForPid) {
|
|
485
|
+
return mapBounded(pids, probe, { concurrency: LSOF_CONCURRENCY, staggerMs: LSOF_STAGGER_MS });
|
|
486
|
+
}
|
|
420
487
|
/**
|
|
421
488
|
* Resolve a process's current working directory via `lsof`. The `-a` flag
|
|
422
489
|
* ANDs the filters; without it macOS treats `-p` and `-d` as a union and
|
|
423
490
|
* returns the cwd of every process on the system.
|
|
424
491
|
*/
|
|
425
492
|
async function getCwdForPid(pid) {
|
|
493
|
+
// No lsof on Windows and no cheap foreign-process cwd API; the pid registry
|
|
494
|
+
// written by `ag run` supplies the cwd for registry-launched agents instead.
|
|
495
|
+
if (process.platform === 'win32')
|
|
496
|
+
return undefined;
|
|
426
497
|
let out;
|
|
427
498
|
try {
|
|
428
499
|
const res = await execFileAsync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
|
|
@@ -469,6 +540,99 @@ const UI_HOSTS = new Set([
|
|
|
469
540
|
'iterm', 'terminal', 'warp', 'alacritty', 'kitty', 'hyper', 'wezterm', 'ghostty',
|
|
470
541
|
'tmux', 'screen',
|
|
471
542
|
]);
|
|
543
|
+
/**
|
|
544
|
+
* Find the launch registry entry recorded by a WRAPPER of this process. The
|
|
545
|
+
* shim delegate records the pid it spawned, but on Windows the `.cmd` shell
|
|
546
|
+
* path makes that a cmd.exe intermediary whose child is the real agent binary
|
|
547
|
+
* — so the agent pid itself has no entry and the wrapper one ancestor up does.
|
|
548
|
+
* The nearest entry wins, and only if its agent matches the candidate's kind:
|
|
549
|
+
* a claude session shelling out to codex must not hand codex its identity.
|
|
550
|
+
*/
|
|
551
|
+
export function readAncestorSessionEntry(pid, ppidMap, kind, readEntry = readPidSessionEntry) {
|
|
552
|
+
let cur = ppidMap.get(pid);
|
|
553
|
+
const seen = new Set();
|
|
554
|
+
while (cur && cur > 1 && !seen.has(cur)) {
|
|
555
|
+
const entry = readEntry(cur);
|
|
556
|
+
if (entry)
|
|
557
|
+
return entry.agent === kind ? entry : undefined;
|
|
558
|
+
seen.add(cur);
|
|
559
|
+
cur = ppidMap.get(cur);
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Collapse agent processes spawned by another live agent process of the same
|
|
565
|
+
* kind onto their nearest kept ancestor. Claude runs subagents, forks, and
|
|
566
|
+
* even its bundled ripgrep as child `claude` processes — on POSIX those
|
|
567
|
+
* children resolve to the parent's cwd and collapse in dedupeBySession, but
|
|
568
|
+
* where no cwd can be recovered (Windows has no lsof) every fork would print
|
|
569
|
+
* as its own headless row. Two exceptions keep their own row: a candidate with
|
|
570
|
+
* its own registry entry — on its pid OR on a wrapper ancestor strictly below
|
|
571
|
+
* the pid it would fold into (the shim's entry lands on the cmd.exe
|
|
572
|
+
* intermediary on Windows) — and a child of a *different* agent kind (claude
|
|
573
|
+
* shelling out to codex is a real second session, not a fork).
|
|
574
|
+
* Returns the kept roots plus, per root pid, how many descendants folded in.
|
|
575
|
+
*/
|
|
576
|
+
export function foldSubordinateAgents(candidates, ppidMap, readEntry) {
|
|
577
|
+
const kindByPid = new Map(candidates.map(c => [c.pid, c.kind]));
|
|
578
|
+
const nearestSameKindAncestor = (pid, kind) => {
|
|
579
|
+
let cur = ppidMap.get(pid);
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
while (cur && cur > 1 && !seen.has(cur)) {
|
|
582
|
+
if (kindByPid.get(cur) === kind)
|
|
583
|
+
return cur;
|
|
584
|
+
seen.add(cur);
|
|
585
|
+
cur = ppidMap.get(cur);
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
588
|
+
};
|
|
589
|
+
// Own launch identity: a matching-kind registry entry on the candidate or on
|
|
590
|
+
// any wrapper between it and the pid it would fold into (exclusive). Entries
|
|
591
|
+
// above the fold target belong to that ancestor's session, not this one.
|
|
592
|
+
const hasOwnSession = (c, stopPid) => {
|
|
593
|
+
if (readEntry(c.pid)?.agent === c.kind)
|
|
594
|
+
return true;
|
|
595
|
+
let cur = ppidMap.get(c.pid);
|
|
596
|
+
const seen = new Set();
|
|
597
|
+
while (cur && cur > 1 && cur !== stopPid && !seen.has(cur)) {
|
|
598
|
+
if (readEntry(cur)?.agent === c.kind)
|
|
599
|
+
return true;
|
|
600
|
+
seen.add(cur);
|
|
601
|
+
cur = ppidMap.get(cur);
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
};
|
|
605
|
+
const keptPids = new Set();
|
|
606
|
+
for (const c of candidates) {
|
|
607
|
+
const foldTarget = nearestSameKindAncestor(c.pid, c.kind);
|
|
608
|
+
if (foldTarget === undefined || hasOwnSession(c, foldTarget)) {
|
|
609
|
+
keptPids.add(c.pid);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const kept = [];
|
|
613
|
+
const foldedByRoot = new Map();
|
|
614
|
+
for (const c of candidates) {
|
|
615
|
+
if (keptPids.has(c.pid)) {
|
|
616
|
+
kept.push(c);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
// Walk up through folded intermediates to the nearest kept same-kind pid.
|
|
620
|
+
let cur = nearestSameKindAncestor(c.pid, c.kind);
|
|
621
|
+
const seen = new Set();
|
|
622
|
+
while (cur !== undefined && !keptPids.has(cur) && !seen.has(cur)) {
|
|
623
|
+
seen.add(cur);
|
|
624
|
+
cur = nearestSameKindAncestor(cur, c.kind);
|
|
625
|
+
}
|
|
626
|
+
// A fold target must itself be a kept row (a ppid cycle from pid reuse
|
|
627
|
+
// can orphan the whole chain) — otherwise keep the row rather than drop it.
|
|
628
|
+
if (cur === undefined || !keptPids.has(cur)) {
|
|
629
|
+
kept.push(c);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
foldedByRoot.set(cur, (foldedByRoot.get(cur) ?? 0) + 1);
|
|
633
|
+
}
|
|
634
|
+
return { kept, foldedByRoot };
|
|
635
|
+
}
|
|
472
636
|
/**
|
|
473
637
|
* Agent processes not attributed to a team or the runtime registry.
|
|
474
638
|
* Classified by walking the ppid chain: any recognised UI ancestor (IDE
|
|
@@ -494,13 +658,24 @@ export async function listUnattributedActive(attributed) {
|
|
|
494
658
|
continue;
|
|
495
659
|
candidates.push({ pid, kind });
|
|
496
660
|
}
|
|
497
|
-
//
|
|
498
|
-
|
|
661
|
+
// Forks/subagents of a live agent process collapse onto their root before
|
|
662
|
+
// the cwd probes — fewer spawns, and one session stays one row even when
|
|
663
|
+
// cwd-based dedupe is unavailable (Windows).
|
|
664
|
+
const { kept, foldedByRoot } = foldSubordinateAgents(candidates, ppidMap, readPidSessionEntry);
|
|
665
|
+
// Bounded + staggered lsof probes: same cwds, but a trickle of spawns instead
|
|
666
|
+
// of one simultaneous system-wide burst that behavioral EDR flags as recon.
|
|
667
|
+
const cwds = await resolveCwds(kept.map(c => c.pid));
|
|
499
668
|
const out = [];
|
|
500
|
-
for (let i = 0; i <
|
|
501
|
-
const { pid, kind } =
|
|
502
|
-
|
|
503
|
-
|
|
669
|
+
for (let i = 0; i < kept.length; i++) {
|
|
670
|
+
const { pid, kind } = kept[i];
|
|
671
|
+
// The per-pid registry (written by `ag run` and the shim delegate) gives
|
|
672
|
+
// the EXACT session id this pid was launched with — so N agents in one cwd
|
|
673
|
+
// resolve to N distinct sessions instead of all collapsing onto the newest
|
|
674
|
+
// .jsonl. The shim's entry may sit on a wrapper ancestor (Windows .cmd
|
|
675
|
+
// path). Absent entirely (direct launch outside agents-cli) → heuristic.
|
|
676
|
+
const entry = readPidSessionEntry(pid) ?? readAncestorSessionEntry(pid, ppidMap, kind);
|
|
677
|
+
const cwd = cwds[i] ?? entry?.cwd ?? undefined;
|
|
678
|
+
const sessionFile = findSessionFileForKind(kind, cwd, entry?.sessionId);
|
|
504
679
|
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
505
680
|
const host = detectHost(pid, procByPid);
|
|
506
681
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
@@ -511,11 +686,14 @@ export async function listUnattributedActive(attributed) {
|
|
|
511
686
|
host,
|
|
512
687
|
pid,
|
|
513
688
|
cwd,
|
|
514
|
-
sessionId: sessionIdFromFile(sessionFile),
|
|
689
|
+
sessionId: entry?.sessionId ?? sessionIdFromFile(sessionFile),
|
|
515
690
|
topic,
|
|
516
691
|
sessionFile,
|
|
692
|
+
pidCount: 1 + (foldedByRoot.get(pid) ?? 0),
|
|
517
693
|
}, state, sessionFile));
|
|
518
694
|
}
|
|
695
|
+
// Housekeeping: drop registry files for pids that have since died.
|
|
696
|
+
prunePidSessionRegistry(isPidAlive);
|
|
519
697
|
return out;
|
|
520
698
|
}
|
|
521
699
|
/**
|
|
@@ -573,10 +751,11 @@ function dedupeBySession(sessions) {
|
|
|
573
751
|
}
|
|
574
752
|
const existing = byKey.get(key);
|
|
575
753
|
if (existing) {
|
|
576
|
-
|
|
754
|
+
// Carry pre-folded fork counts (headless rows arrive with pidCount set).
|
|
755
|
+
existing.pidCount = (existing.pidCount ?? 1) + (s.pidCount ?? 1);
|
|
577
756
|
}
|
|
578
757
|
else {
|
|
579
|
-
s.pidCount = 1;
|
|
758
|
+
s.pidCount = s.pidCount ?? 1;
|
|
580
759
|
byKey.set(key, s);
|
|
581
760
|
out.push(s);
|
|
582
761
|
}
|
|
@@ -74,6 +74,22 @@ interface ClaudeSessionScan {
|
|
|
74
74
|
* TTL, no external lock files needed.
|
|
75
75
|
*/
|
|
76
76
|
export declare function discoverSessions(options?: DiscoverOptions): Promise<SessionMeta[]>;
|
|
77
|
+
/**
|
|
78
|
+
* How many agents' dotfile dirs we scan at once, and the minimum spacing between
|
|
79
|
+
* successive scan starts. A small bound + stagger turns a simultaneous bulk
|
|
80
|
+
* multi-dotfile sweep (a behavioral-EDR file-enumeration trigger) into a trickle.
|
|
81
|
+
*/
|
|
82
|
+
export declare const DOTFILE_SCAN_CONCURRENCY = 2;
|
|
83
|
+
/** Run each agent's incremental scan, bounded + staggered. Order is irrelevant (each scan writes its own rows). */
|
|
84
|
+
export declare function scanAgentsBounded<T>(items: readonly T[], run: (item: T) => Promise<void>): Promise<void[]>;
|
|
85
|
+
/**
|
|
86
|
+
* The machine a discovered session originated on. Cross-machine sync mirrors a
|
|
87
|
+
* remote transcript to backups/<agent>/<machine>/<subdir>/… (see mirrorPath in
|
|
88
|
+
* sync/agents.ts); every other transcript is a live-home file on this box. So:
|
|
89
|
+
* when the path sits under the agent's backups root, the first segment below it
|
|
90
|
+
* is the origin machine id; otherwise it's the local machine.
|
|
91
|
+
*/
|
|
92
|
+
export declare function machineForSessionFile(filePath: string, agent: string): string;
|
|
77
93
|
/**
|
|
78
94
|
* Count sessions in scope without running an incremental scan. Assumes the DB
|
|
79
95
|
* is already fresh (typically true because `discoverSessions` ran first this
|
|
@@ -105,8 +121,29 @@ export declare function getAgentSessionDirs(agent: string, subdir: string): stri
|
|
|
105
121
|
* recent startedAt.
|
|
106
122
|
*/
|
|
107
123
|
export declare function buildClaudeLabelMap(): Map<string, string | null>;
|
|
124
|
+
/**
|
|
125
|
+
* Base64url-decode a JWT and return its `email` claim, if present. Split out so
|
|
126
|
+
* the decode is a single, testable step — and so it only runs when someone
|
|
127
|
+
* actually reads the Codex account (see the lazy resolution below).
|
|
128
|
+
*/
|
|
129
|
+
export declare function decodeJwtEmail(idToken: string): string | undefined;
|
|
130
|
+
/** Test seam: how many times getCodexAccount has actually resolved (decoded) since the last reset. */
|
|
131
|
+
export declare function __codexAccountResolveCountForTest(): number;
|
|
132
|
+
/** Test seam: clear the memoized account + resolve counter so laziness can be observed from a clean slate. */
|
|
133
|
+
export declare function __resetCodexAccountCacheForTest(): void;
|
|
108
134
|
/** Parse the lines of a Codex `session_index.jsonl` into a session id -> title map. */
|
|
109
135
|
export declare function parseCodexThreadNameIndex(raw: string): Map<string, string>;
|
|
136
|
+
/**
|
|
137
|
+
* Stream-parse a single Codex JSONL file to extract session metadata.
|
|
138
|
+
*
|
|
139
|
+
* `resolveAccount` is a lazy thunk (not a resolved string): the JWT decode it
|
|
140
|
+
* performs is deferred until we know this file is a real session worth building
|
|
141
|
+
* metadata for, and only then — never during the file walk / stat phase.
|
|
142
|
+
*/
|
|
143
|
+
export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string): Promise<{
|
|
144
|
+
meta: SessionMeta;
|
|
145
|
+
content: string;
|
|
146
|
+
} | null>;
|
|
110
147
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
111
148
|
export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
|
|
112
149
|
/** Read up to maxLines non-empty lines from the beginning of a file. */
|
|
@@ -20,8 +20,11 @@ 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';
|
|
26
|
+
import { machineId } from './sync/config.js';
|
|
27
|
+
import { mapBounded } from '../concurrency.js';
|
|
25
28
|
import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
|
|
26
29
|
const HOME = os.homedir();
|
|
27
30
|
// Versions can live under either repo: the user repo (current canonical
|
|
@@ -55,27 +58,68 @@ export async function discoverSessions(options) {
|
|
|
55
58
|
const onProgress = options?.onProgress;
|
|
56
59
|
if (tryClaimScan(process.pid)) {
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case 'opencode': return scanOpenCodeIncremental();
|
|
64
|
-
case 'openclaw': return scanOpenClawIncremental();
|
|
65
|
-
case 'rush': return scanRushIncremental(onProgress);
|
|
66
|
-
case 'hermes': return scanHermesIncremental(onProgress);
|
|
67
|
-
case 'kimi': return scanKimiIncremental(onProgress);
|
|
68
|
-
case 'droid': return scanDroidIncremental(onProgress);
|
|
69
|
-
}
|
|
70
|
-
}));
|
|
61
|
+
// Bounded + staggered instead of a single Promise.all: scanning every
|
|
62
|
+
// agent's dotfile dir (~/.claude, ~/.codex, ~/.gemini, …) simultaneously
|
|
63
|
+
// reads to behavioral EDR (CrowdStrike Falcon) as a ransomware-style bulk
|
|
64
|
+
// file-enumeration sweep. Same dirs, same results — just not all at once.
|
|
65
|
+
await scanAgentsBounded(agents, agent => dispatchAgentScan(agent, onProgress));
|
|
71
66
|
}
|
|
72
67
|
finally {
|
|
73
68
|
releaseScan(process.pid);
|
|
74
69
|
}
|
|
75
70
|
}
|
|
76
71
|
const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
|
|
72
|
+
for (const s of sessions)
|
|
73
|
+
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
77
74
|
return sessions;
|
|
78
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* How many agents' dotfile dirs we scan at once, and the minimum spacing between
|
|
78
|
+
* successive scan starts. A small bound + stagger turns a simultaneous bulk
|
|
79
|
+
* multi-dotfile sweep (a behavioral-EDR file-enumeration trigger) into a trickle.
|
|
80
|
+
*/
|
|
81
|
+
export const DOTFILE_SCAN_CONCURRENCY = 2;
|
|
82
|
+
const DOTFILE_SCAN_STAGGER_MS = 15;
|
|
83
|
+
/** Run each agent's incremental scan, bounded + staggered. Order is irrelevant (each scan writes its own rows). */
|
|
84
|
+
export function scanAgentsBounded(items, run) {
|
|
85
|
+
return mapBounded(items, run, {
|
|
86
|
+
concurrency: DOTFILE_SCAN_CONCURRENCY,
|
|
87
|
+
staggerMs: DOTFILE_SCAN_STAGGER_MS,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Dispatch a single agent's incremental dotfile scan. */
|
|
91
|
+
function dispatchAgentScan(agent, onProgress) {
|
|
92
|
+
switch (agent) {
|
|
93
|
+
case 'claude': return scanClaudeIncremental(onProgress);
|
|
94
|
+
case 'codex': return scanCodexIncremental(onProgress);
|
|
95
|
+
case 'gemini': return scanGeminiIncremental(onProgress);
|
|
96
|
+
case 'antigravity': return scanAntigravityIncremental(onProgress);
|
|
97
|
+
case 'opencode': return scanOpenCodeIncremental();
|
|
98
|
+
case 'openclaw': return scanOpenClawIncremental();
|
|
99
|
+
case 'rush': return scanRushIncremental(onProgress);
|
|
100
|
+
case 'hermes': return scanHermesIncremental(onProgress);
|
|
101
|
+
case 'kimi': return scanKimiIncremental(onProgress);
|
|
102
|
+
case 'droid': return scanDroidIncremental(onProgress);
|
|
103
|
+
default: return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let _localMachineId;
|
|
107
|
+
/**
|
|
108
|
+
* The machine a discovered session originated on. Cross-machine sync mirrors a
|
|
109
|
+
* remote transcript to backups/<agent>/<machine>/<subdir>/… (see mirrorPath in
|
|
110
|
+
* sync/agents.ts); every other transcript is a live-home file on this box. So:
|
|
111
|
+
* when the path sits under the agent's backups root, the first segment below it
|
|
112
|
+
* is the origin machine id; otherwise it's the local machine.
|
|
113
|
+
*/
|
|
114
|
+
export function machineForSessionFile(filePath, agent) {
|
|
115
|
+
const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
|
|
116
|
+
if (filePath.startsWith(base)) {
|
|
117
|
+
const seg = filePath.slice(base.length).split(path.sep)[0];
|
|
118
|
+
if (seg)
|
|
119
|
+
return seg;
|
|
120
|
+
}
|
|
121
|
+
return (_localMachineId ??= machineId());
|
|
122
|
+
}
|
|
79
123
|
/**
|
|
80
124
|
* Count sessions in scope without running an incremental scan. Assumes the DB
|
|
81
125
|
* is already fresh (typically true because `discoverSessions` ran first this
|
|
@@ -462,10 +506,37 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
462
506
|
// Codex account info
|
|
463
507
|
// ---------------------------------------------------------------------------
|
|
464
508
|
let cachedCodexAccount;
|
|
465
|
-
/**
|
|
509
|
+
/** Number of times the auth.json JWT was actually base64-decoded. Test seam for the lazy-decode contract. */
|
|
510
|
+
let codexAccountResolveCount = 0;
|
|
511
|
+
/**
|
|
512
|
+
* Base64url-decode a JWT and return its `email` claim, if present. Split out so
|
|
513
|
+
* the decode is a single, testable step — and so it only runs when someone
|
|
514
|
+
* actually reads the Codex account (see the lazy resolution below).
|
|
515
|
+
*/
|
|
516
|
+
export function decodeJwtEmail(idToken) {
|
|
517
|
+
const parts = idToken.split('.');
|
|
518
|
+
if (parts.length < 2)
|
|
519
|
+
return undefined;
|
|
520
|
+
try {
|
|
521
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
522
|
+
return typeof payload.email === 'string' ? payload.email : undefined;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return undefined; // malformed JWT
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Extract the Codex account email from the JWT id_token in auth.json.
|
|
530
|
+
*
|
|
531
|
+
* Memoized and resolved LAZILY: the credential-harvesting-shaped JWT decode
|
|
532
|
+
* (base64-decoding ~/.codex/auth.json) only runs when the account is actually
|
|
533
|
+
* needed to build a session's metadata — never eagerly during the bulk scan.
|
|
534
|
+
* A scan with no changed Codex files never touches the auth file.
|
|
535
|
+
*/
|
|
466
536
|
function getCodexAccount() {
|
|
467
537
|
if (cachedCodexAccount !== undefined)
|
|
468
538
|
return cachedCodexAccount || undefined;
|
|
539
|
+
codexAccountResolveCount++;
|
|
469
540
|
const candidates = [path.join(HOME, '.codex', 'auth.json')];
|
|
470
541
|
for (const root of VERSIONS_ROOTS) {
|
|
471
542
|
const versionsBase = path.join(root, 'versions', 'codex');
|
|
@@ -485,27 +556,35 @@ function getCodexAccount() {
|
|
|
485
556
|
const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
486
557
|
const idToken = data.tokens?.id_token;
|
|
487
558
|
if (idToken) {
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
cachedCodexAccount = payload.email;
|
|
493
|
-
return payload.email;
|
|
494
|
-
}
|
|
559
|
+
const email = decodeJwtEmail(idToken);
|
|
560
|
+
if (email) {
|
|
561
|
+
cachedCodexAccount = email;
|
|
562
|
+
return email;
|
|
495
563
|
}
|
|
496
564
|
}
|
|
497
565
|
}
|
|
498
|
-
catch { /* auth file
|
|
566
|
+
catch { /* auth file malformed */ }
|
|
499
567
|
}
|
|
500
568
|
cachedCodexAccount = '';
|
|
501
569
|
return undefined;
|
|
502
570
|
}
|
|
571
|
+
/** Test seam: how many times getCodexAccount has actually resolved (decoded) since the last reset. */
|
|
572
|
+
export function __codexAccountResolveCountForTest() {
|
|
573
|
+
return codexAccountResolveCount;
|
|
574
|
+
}
|
|
575
|
+
/** Test seam: clear the memoized account + resolve counter so laziness can be observed from a clean slate. */
|
|
576
|
+
export function __resetCodexAccountCacheForTest() {
|
|
577
|
+
cachedCodexAccount = undefined;
|
|
578
|
+
codexAccountResolveCount = 0;
|
|
579
|
+
}
|
|
503
580
|
// ---------------------------------------------------------------------------
|
|
504
581
|
// Codex
|
|
505
582
|
// ---------------------------------------------------------------------------
|
|
506
583
|
/** Incrementally re-scan changed Codex session files and upsert into the DB. */
|
|
507
584
|
async function scanCodexIncremental(onProgress) {
|
|
508
|
-
|
|
585
|
+
// Lazy: getCodexAccount (the auth.json JWT decode) is only resolved by
|
|
586
|
+
// readCodexMeta when a changed session actually needs it — never eagerly here,
|
|
587
|
+
// so a no-op scan (changed.length === 0) never touches the credential file.
|
|
509
588
|
const currentVersion = await getCurrentAgentVersion('codex');
|
|
510
589
|
const filePaths = [];
|
|
511
590
|
for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
|
|
@@ -530,7 +609,7 @@ async function scanCodexIncremental(onProgress) {
|
|
|
530
609
|
let parsed = 0;
|
|
531
610
|
for (const { filePath, scan } of changed) {
|
|
532
611
|
try {
|
|
533
|
-
const result = await readCodexMeta(filePath,
|
|
612
|
+
const result = await readCodexMeta(filePath, getCodexAccount, currentVersion);
|
|
534
613
|
if (result && !seen.has(result.meta.id)) {
|
|
535
614
|
seen.add(result.meta.id);
|
|
536
615
|
// Prefer the Codex-generated title over the first-prompt fallback.
|
|
@@ -594,8 +673,14 @@ function readCodexThreadNames() {
|
|
|
594
673
|
}
|
|
595
674
|
return titles;
|
|
596
675
|
}
|
|
597
|
-
/**
|
|
598
|
-
|
|
676
|
+
/**
|
|
677
|
+
* Stream-parse a single Codex JSONL file to extract session metadata.
|
|
678
|
+
*
|
|
679
|
+
* `resolveAccount` is a lazy thunk (not a resolved string): the JWT decode it
|
|
680
|
+
* performs is deferred until we know this file is a real session worth building
|
|
681
|
+
* metadata for, and only then — never during the file walk / stat phase.
|
|
682
|
+
*/
|
|
683
|
+
export async function readCodexMeta(filePath, resolveAccount, currentVersion) {
|
|
599
684
|
const scan = await scanCodexSession(filePath);
|
|
600
685
|
const sessionId = scan.sessionId || '';
|
|
601
686
|
if (!sessionId)
|
|
@@ -618,7 +703,7 @@ async function readCodexMeta(filePath, account, currentVersion) {
|
|
|
618
703
|
tokenCount: scan.tokenCount,
|
|
619
704
|
costUsd: scan.costUsd,
|
|
620
705
|
durationMs: scan.durationMs,
|
|
621
|
-
account,
|
|
706
|
+
account: resolveAccount?.(),
|
|
622
707
|
prUrl: scan.prUrl,
|
|
623
708
|
prNumber: scan.prNumber,
|
|
624
709
|
worktreeSlug: scan.worktreeSlug,
|
|
@@ -864,6 +949,98 @@ function buildGeminiProjectMap() {
|
|
|
864
949
|
return map;
|
|
865
950
|
}
|
|
866
951
|
// ---------------------------------------------------------------------------
|
|
952
|
+
// Antigravity
|
|
953
|
+
//
|
|
954
|
+
// Antigravity stores one SQLite DB per conversation at
|
|
955
|
+
// ~/.gemini/antigravity-cli/conversations/<trajectory-uuid>.db. The filename
|
|
956
|
+
// (minus .db) is the canonical session id. Each DB is stat'd against the ledger;
|
|
957
|
+
// only changed DBs are re-parsed (via parseAntigravity, which shells out to
|
|
958
|
+
// sqlite3). Tool count doubles as the message count; the toolSummary of the
|
|
959
|
+
// first tool call becomes the topic, and any run_command's Cwd fills in cwd.
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
/** Incrementally re-scan changed Antigravity conversation DBs and upsert into the DB. */
|
|
962
|
+
async function scanAntigravityIncremental(onProgress) {
|
|
963
|
+
const currentVersion = await getCurrentAgentVersion('antigravity');
|
|
964
|
+
const filePaths = [];
|
|
965
|
+
const seenPaths = new Set();
|
|
966
|
+
for (const conversationsDir of getAgentSessionDirs('antigravity', 'conversations')) {
|
|
967
|
+
let files;
|
|
968
|
+
try {
|
|
969
|
+
files = fs.readdirSync(conversationsDir).filter(f => f.endsWith('.db'));
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
for (const file of files) {
|
|
975
|
+
const fp = path.join(conversationsDir, file);
|
|
976
|
+
if (seenPaths.has(fp))
|
|
977
|
+
continue;
|
|
978
|
+
seenPaths.add(fp);
|
|
979
|
+
filePaths.push(fp);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const changed = filterChangedFiles(filePaths);
|
|
983
|
+
if (changed.length === 0)
|
|
984
|
+
return;
|
|
985
|
+
onProgress?.({ agent: 'antigravity', parsed: 0, total: changed.length });
|
|
986
|
+
const entries = [];
|
|
987
|
+
const touched = [];
|
|
988
|
+
const seen = new Set();
|
|
989
|
+
let parsed = 0;
|
|
990
|
+
for (const { filePath, scan } of changed) {
|
|
991
|
+
try {
|
|
992
|
+
const result = readAntigravityMeta(filePath, currentVersion);
|
|
993
|
+
if (result && !seen.has(result.meta.id)) {
|
|
994
|
+
seen.add(result.meta.id);
|
|
995
|
+
entries.push({ meta: result.meta, content: result.content, scan });
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
touched.push({ filePath, scan });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
touched.push({ filePath, scan });
|
|
1003
|
+
}
|
|
1004
|
+
parsed++;
|
|
1005
|
+
onProgress?.({ agent: 'antigravity', parsed, total: changed.length });
|
|
1006
|
+
}
|
|
1007
|
+
upsertSessionsBatch(entries);
|
|
1008
|
+
recordScans(touched);
|
|
1009
|
+
}
|
|
1010
|
+
/** Parse a single Antigravity conversation DB to extract session metadata. */
|
|
1011
|
+
function readAntigravityMeta(filePath, currentVersion) {
|
|
1012
|
+
const sessionId = path.basename(filePath).replace(/\.db$/, '');
|
|
1013
|
+
if (!sessionId)
|
|
1014
|
+
return null;
|
|
1015
|
+
const events = parseAntigravity(filePath);
|
|
1016
|
+
// cwd: first run_command carries the working directory in its Cwd arg.
|
|
1017
|
+
let cwd;
|
|
1018
|
+
const contentParts = [];
|
|
1019
|
+
for (const e of events) {
|
|
1020
|
+
if (!cwd && typeof e.args?.Cwd === 'string' && e.args.Cwd)
|
|
1021
|
+
cwd = e.args.Cwd;
|
|
1022
|
+
if (e.content)
|
|
1023
|
+
contentParts.push(e.content);
|
|
1024
|
+
}
|
|
1025
|
+
const normalizedCwd = cwd ? normalizeCwd(cwd) : undefined;
|
|
1026
|
+
// Topic: the first tool's human summary is a decent one-line label.
|
|
1027
|
+
const topic = events.find(e => e.content)?.content;
|
|
1028
|
+
const stat = safeStatSync(filePath);
|
|
1029
|
+
const meta = {
|
|
1030
|
+
id: sessionId,
|
|
1031
|
+
shortId: sessionId.slice(0, 8),
|
|
1032
|
+
agent: 'antigravity',
|
|
1033
|
+
timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
1034
|
+
project: normalizedCwd ? path.basename(normalizedCwd) : undefined,
|
|
1035
|
+
cwd: normalizedCwd,
|
|
1036
|
+
filePath,
|
|
1037
|
+
version: resolveSessionVersion('antigravity', filePath, undefined, currentVersion),
|
|
1038
|
+
topic: topic ? topic.slice(0, 120) : undefined,
|
|
1039
|
+
messageCount: events.length,
|
|
1040
|
+
};
|
|
1041
|
+
return { meta, content: contentParts.join('\n') };
|
|
1042
|
+
}
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
867
1044
|
// OpenCode
|
|
868
1045
|
// ---------------------------------------------------------------------------
|
|
869
1046
|
const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
|