@phnx-labs/agents-cli 1.20.35 → 1.20.37
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- 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.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- 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 +156 -75
- 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.js +20 -8
- package/dist/commands/sessions-sync.d.ts +3 -0
- package/dist/commands/sessions-sync.js +44 -4
- package/dist/commands/sessions.d.ts +78 -1
- package/dist/commands/sessions.js +506 -66
- 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/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- 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/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- 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 +112 -8
- 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 +57 -0
- package/dist/lib/daemon.js +196 -18
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/resolve-target.d.ts +24 -0
- package/dist/lib/devices/resolve-target.js +80 -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 +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- 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 +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- 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 +14 -0
- package/dist/lib/hosts/tasks.js +15 -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/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- 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/plugins.js +16 -6
- 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 +73 -0
- package/dist/lib/session/active.js +199 -22
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +41 -5
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +127 -29
- package/dist/lib/session/ghostty-tabs.d.ts +33 -0
- package/dist/lib/session/ghostty-tabs.js +126 -0
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/relative-time.js +6 -2
- package/dist/lib/session/remote-active.js +19 -17
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +205 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +20 -4
- package/dist/lib/session/sync/config.d.ts +14 -15
- package/dist/lib/session/sync/config.js +60 -20
- package/dist/lib/session/types.d.ts +23 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -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 +21 -13
- 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/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/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- 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/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- 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/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -23,6 +23,8 @@ import { extractSessionTopic } from './prompt.js';
|
|
|
23
23
|
import { parseAntigravity } from './parse.js';
|
|
24
24
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand } from './state.js';
|
|
25
25
|
import { costOfUsage } from '../pricing/index.js';
|
|
26
|
+
import { machineId } from './sync/config.js';
|
|
27
|
+
import { mapBounded } from '../concurrency.js';
|
|
26
28
|
import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
|
|
27
29
|
const HOME = os.homedir();
|
|
28
30
|
// Versions can live under either repo: the user repo (current canonical
|
|
@@ -56,28 +58,68 @@ export async function discoverSessions(options) {
|
|
|
56
58
|
const onProgress = options?.onProgress;
|
|
57
59
|
if (tryClaimScan(process.pid)) {
|
|
58
60
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
case 'antigravity': return scanAntigravityIncremental(onProgress);
|
|
65
|
-
case 'opencode': return scanOpenCodeIncremental();
|
|
66
|
-
case 'openclaw': return scanOpenClawIncremental();
|
|
67
|
-
case 'rush': return scanRushIncremental(onProgress);
|
|
68
|
-
case 'hermes': return scanHermesIncremental(onProgress);
|
|
69
|
-
case 'kimi': return scanKimiIncremental(onProgress);
|
|
70
|
-
case 'droid': return scanDroidIncremental(onProgress);
|
|
71
|
-
}
|
|
72
|
-
}));
|
|
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));
|
|
73
66
|
}
|
|
74
67
|
finally {
|
|
75
68
|
releaseScan(process.pid);
|
|
76
69
|
}
|
|
77
70
|
}
|
|
78
71
|
const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
|
|
72
|
+
for (const s of sessions)
|
|
73
|
+
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
79
74
|
return sessions;
|
|
80
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
|
+
}
|
|
81
123
|
/**
|
|
82
124
|
* Count sessions in scope without running an incremental scan. Assumes the DB
|
|
83
125
|
* is already fresh (typically true because `discoverSessions` ran first this
|
|
@@ -417,6 +459,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
417
459
|
shortId: sessionId.slice(0, 8),
|
|
418
460
|
agent: 'claude',
|
|
419
461
|
timestamp: scan.timestamp,
|
|
462
|
+
lastActivity: scan.lastActivity,
|
|
420
463
|
project: cwd ? path.basename(cwd) : undefined,
|
|
421
464
|
cwd,
|
|
422
465
|
filePath,
|
|
@@ -443,6 +486,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
443
486
|
shortId: sessionId.slice(0, 8),
|
|
444
487
|
agent: 'claude',
|
|
445
488
|
timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
489
|
+
lastActivity: scan.lastActivity,
|
|
446
490
|
filePath,
|
|
447
491
|
account,
|
|
448
492
|
label,
|
|
@@ -464,10 +508,37 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
464
508
|
// Codex account info
|
|
465
509
|
// ---------------------------------------------------------------------------
|
|
466
510
|
let cachedCodexAccount;
|
|
467
|
-
/**
|
|
511
|
+
/** Number of times the auth.json JWT was actually base64-decoded. Test seam for the lazy-decode contract. */
|
|
512
|
+
let codexAccountResolveCount = 0;
|
|
513
|
+
/**
|
|
514
|
+
* Base64url-decode a JWT and return its `email` claim, if present. Split out so
|
|
515
|
+
* the decode is a single, testable step — and so it only runs when someone
|
|
516
|
+
* actually reads the Codex account (see the lazy resolution below).
|
|
517
|
+
*/
|
|
518
|
+
export function decodeJwtEmail(idToken) {
|
|
519
|
+
const parts = idToken.split('.');
|
|
520
|
+
if (parts.length < 2)
|
|
521
|
+
return undefined;
|
|
522
|
+
try {
|
|
523
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
524
|
+
return typeof payload.email === 'string' ? payload.email : undefined;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return undefined; // malformed JWT
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Extract the Codex account email from the JWT id_token in auth.json.
|
|
532
|
+
*
|
|
533
|
+
* Memoized and resolved LAZILY: the credential-harvesting-shaped JWT decode
|
|
534
|
+
* (base64-decoding ~/.codex/auth.json) only runs when the account is actually
|
|
535
|
+
* needed to build a session's metadata — never eagerly during the bulk scan.
|
|
536
|
+
* A scan with no changed Codex files never touches the auth file.
|
|
537
|
+
*/
|
|
468
538
|
function getCodexAccount() {
|
|
469
539
|
if (cachedCodexAccount !== undefined)
|
|
470
540
|
return cachedCodexAccount || undefined;
|
|
541
|
+
codexAccountResolveCount++;
|
|
471
542
|
const candidates = [path.join(HOME, '.codex', 'auth.json')];
|
|
472
543
|
for (const root of VERSIONS_ROOTS) {
|
|
473
544
|
const versionsBase = path.join(root, 'versions', 'codex');
|
|
@@ -487,27 +558,35 @@ function getCodexAccount() {
|
|
|
487
558
|
const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
488
559
|
const idToken = data.tokens?.id_token;
|
|
489
560
|
if (idToken) {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
cachedCodexAccount = payload.email;
|
|
495
|
-
return payload.email;
|
|
496
|
-
}
|
|
561
|
+
const email = decodeJwtEmail(idToken);
|
|
562
|
+
if (email) {
|
|
563
|
+
cachedCodexAccount = email;
|
|
564
|
+
return email;
|
|
497
565
|
}
|
|
498
566
|
}
|
|
499
567
|
}
|
|
500
|
-
catch { /* auth file
|
|
568
|
+
catch { /* auth file malformed */ }
|
|
501
569
|
}
|
|
502
570
|
cachedCodexAccount = '';
|
|
503
571
|
return undefined;
|
|
504
572
|
}
|
|
573
|
+
/** Test seam: how many times getCodexAccount has actually resolved (decoded) since the last reset. */
|
|
574
|
+
export function __codexAccountResolveCountForTest() {
|
|
575
|
+
return codexAccountResolveCount;
|
|
576
|
+
}
|
|
577
|
+
/** Test seam: clear the memoized account + resolve counter so laziness can be observed from a clean slate. */
|
|
578
|
+
export function __resetCodexAccountCacheForTest() {
|
|
579
|
+
cachedCodexAccount = undefined;
|
|
580
|
+
codexAccountResolveCount = 0;
|
|
581
|
+
}
|
|
505
582
|
// ---------------------------------------------------------------------------
|
|
506
583
|
// Codex
|
|
507
584
|
// ---------------------------------------------------------------------------
|
|
508
585
|
/** Incrementally re-scan changed Codex session files and upsert into the DB. */
|
|
509
586
|
async function scanCodexIncremental(onProgress) {
|
|
510
|
-
|
|
587
|
+
// Lazy: getCodexAccount (the auth.json JWT decode) is only resolved by
|
|
588
|
+
// readCodexMeta when a changed session actually needs it — never eagerly here,
|
|
589
|
+
// so a no-op scan (changed.length === 0) never touches the credential file.
|
|
511
590
|
const currentVersion = await getCurrentAgentVersion('codex');
|
|
512
591
|
const filePaths = [];
|
|
513
592
|
for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
|
|
@@ -532,7 +611,7 @@ async function scanCodexIncremental(onProgress) {
|
|
|
532
611
|
let parsed = 0;
|
|
533
612
|
for (const { filePath, scan } of changed) {
|
|
534
613
|
try {
|
|
535
|
-
const result = await readCodexMeta(filePath,
|
|
614
|
+
const result = await readCodexMeta(filePath, getCodexAccount, currentVersion);
|
|
536
615
|
if (result && !seen.has(result.meta.id)) {
|
|
537
616
|
seen.add(result.meta.id);
|
|
538
617
|
// Prefer the Codex-generated title over the first-prompt fallback.
|
|
@@ -596,8 +675,14 @@ function readCodexThreadNames() {
|
|
|
596
675
|
}
|
|
597
676
|
return titles;
|
|
598
677
|
}
|
|
599
|
-
/**
|
|
600
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Stream-parse a single Codex JSONL file to extract session metadata.
|
|
680
|
+
*
|
|
681
|
+
* `resolveAccount` is a lazy thunk (not a resolved string): the JWT decode it
|
|
682
|
+
* performs is deferred until we know this file is a real session worth building
|
|
683
|
+
* metadata for, and only then — never during the file walk / stat phase.
|
|
684
|
+
*/
|
|
685
|
+
export async function readCodexMeta(filePath, resolveAccount, currentVersion) {
|
|
601
686
|
const scan = await scanCodexSession(filePath);
|
|
602
687
|
const sessionId = scan.sessionId || '';
|
|
603
688
|
if (!sessionId)
|
|
@@ -610,6 +695,7 @@ async function readCodexMeta(filePath, account, currentVersion) {
|
|
|
610
695
|
// Codex `session_meta` only carries the start time; use file mtime when
|
|
611
696
|
// it's newer so long-running sessions register as recently active.
|
|
612
697
|
timestamp: pickLatestCodexTimestamp(scan.timestamp, filePath),
|
|
698
|
+
lastActivity: scan.lastActivity,
|
|
613
699
|
project: cwd ? path.basename(cwd) : undefined,
|
|
614
700
|
cwd,
|
|
615
701
|
filePath,
|
|
@@ -620,7 +706,7 @@ async function readCodexMeta(filePath, account, currentVersion) {
|
|
|
620
706
|
tokenCount: scan.tokenCount,
|
|
621
707
|
costUsd: scan.costUsd,
|
|
622
708
|
durationMs: scan.durationMs,
|
|
623
|
-
account,
|
|
709
|
+
account: resolveAccount?.(),
|
|
624
710
|
prUrl: scan.prUrl,
|
|
625
711
|
prNumber: scan.prNumber,
|
|
626
712
|
worktreeSlug: scan.worktreeSlug,
|
|
@@ -804,6 +890,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
804
890
|
shortId: sessionId.slice(0, 8),
|
|
805
891
|
agent: 'gemini',
|
|
806
892
|
timestamp: startTime || (stat ? stat.mtime.toISOString() : new Date().toISOString()),
|
|
893
|
+
lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
|
|
807
894
|
project,
|
|
808
895
|
cwd,
|
|
809
896
|
filePath,
|
|
@@ -1010,6 +1097,7 @@ async function scanOpenCodeIncremental() {
|
|
|
1010
1097
|
s.directory,
|
|
1011
1098
|
s.version,
|
|
1012
1099
|
s.time_created,
|
|
1100
|
+
s.time_updated,
|
|
1013
1101
|
COALESCE(stats.message_count, 0),
|
|
1014
1102
|
stats.token_count,
|
|
1015
1103
|
COALESCE(stats.has_token_data, 0)
|
|
@@ -1042,20 +1130,26 @@ async function scanOpenCodeIncremental() {
|
|
|
1042
1130
|
for (const line of out.split('\n')) {
|
|
1043
1131
|
if (!line.trim())
|
|
1044
1132
|
continue;
|
|
1045
|
-
const [id, title, directory, version, timeCreatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
|
|
1133
|
+
const [id, title, directory, version, timeCreatedStr, timeUpdatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
|
|
1046
1134
|
if (!id)
|
|
1047
1135
|
continue;
|
|
1048
1136
|
const timeCreated = parseInt(timeCreatedStr, 10);
|
|
1137
|
+
const timeUpdated = parseInt(timeUpdatedStr, 10);
|
|
1049
1138
|
const messageCount = parseInt(messageCountStr, 10);
|
|
1050
1139
|
const tokenCount = parseInt(tokenCountStr, 10);
|
|
1051
1140
|
const hasTokenData = hasTokenDataStr === '1';
|
|
1052
1141
|
const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
|
|
1142
|
+
// OpenCode is one shared DB, not one file per session — its row carries a
|
|
1143
|
+
// per-session updated time. Set lastActivity explicitly (falling back to
|
|
1144
|
+
// creation, never the whole-DB mtime the ScanStamp would otherwise supply).
|
|
1145
|
+
const lastActivity = Number.isNaN(timeUpdated) ? timestamp : new Date(timeUpdated).toISOString();
|
|
1053
1146
|
const topic = title || undefined;
|
|
1054
1147
|
const meta = {
|
|
1055
1148
|
id,
|
|
1056
1149
|
shortId: id.replace(/^ses_/, '').slice(0, 8),
|
|
1057
1150
|
agent: 'opencode',
|
|
1058
1151
|
timestamp,
|
|
1152
|
+
lastActivity,
|
|
1059
1153
|
project: directory ? path.basename(directory) : undefined,
|
|
1060
1154
|
cwd: directory ? normalizeCwd(directory) : undefined,
|
|
1061
1155
|
filePath: `${OPENCODE_DB}#${id}`,
|
|
@@ -1488,6 +1582,7 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
1488
1582
|
shortId: sessionId.slice(0, 8),
|
|
1489
1583
|
agent: 'droid',
|
|
1490
1584
|
timestamp: scan.timestamp || (stat ? stat.mtime.toISOString() : new Date().toISOString()),
|
|
1585
|
+
lastActivity: scan.lastActivity,
|
|
1491
1586
|
project: cwd ? path.basename(cwd) : undefined,
|
|
1492
1587
|
cwd,
|
|
1493
1588
|
filePath,
|
|
@@ -1609,6 +1704,7 @@ async function scanDroidSession(filePath) {
|
|
|
1609
1704
|
model,
|
|
1610
1705
|
messageCount,
|
|
1611
1706
|
durationMs,
|
|
1707
|
+
lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
|
|
1612
1708
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
1613
1709
|
};
|
|
1614
1710
|
}
|
|
@@ -1794,6 +1890,7 @@ export async function scanClaudeSession(filePath) {
|
|
|
1794
1890
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
1795
1891
|
costUsd: sawCost ? costUsd : undefined,
|
|
1796
1892
|
durationMs,
|
|
1893
|
+
lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
|
|
1797
1894
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
1798
1895
|
prUrl,
|
|
1799
1896
|
prNumber,
|
|
@@ -1937,6 +2034,7 @@ async function scanCodexSession(filePath) {
|
|
|
1937
2034
|
tokenCount,
|
|
1938
2035
|
costUsd,
|
|
1939
2036
|
durationMs,
|
|
2037
|
+
lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
|
|
1940
2038
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
1941
2039
|
prUrl,
|
|
1942
2040
|
prNumber,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort Ghostty tab-number detection for `agents sessions --active`.
|
|
3
|
+
*
|
|
4
|
+
* Ghostty (macOS) exposes read-only AppleScript: every tab has an `index` and a
|
|
5
|
+
* `name` (title); every surface a `working directory`. It exposes NO per-tab env
|
|
6
|
+
* var and NO tty/pid on a surface — so a session is matched to its tab by
|
|
7
|
+
* `working directory` (with title as a tiebreak). This is display sugar only:
|
|
8
|
+
* one bounded, non-fatal osascript call, run by the renderer, never on the
|
|
9
|
+
* discovery / --json / --waiting path. Any failure (Ghostty not running,
|
|
10
|
+
* Automation permission denied, timeout) degrades silently to no tab number.
|
|
11
|
+
*/
|
|
12
|
+
import type { ActiveSession } from './active.js';
|
|
13
|
+
/** One Ghostty surface (terminal), flattened from window -> tab -> surface. */
|
|
14
|
+
export interface GhosttySurface {
|
|
15
|
+
windowIndex: number;
|
|
16
|
+
tabIndex: number;
|
|
17
|
+
cwd: string;
|
|
18
|
+
title: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Enumerate every Ghostty surface (window/tab/cwd/title) via one read-only
|
|
22
|
+
* osascript call. Returns [] on ANY failure — Ghostty not running, Automation
|
|
23
|
+
* permission not granted, timeout, or a parse miss. Never throws, never prompts.
|
|
24
|
+
*/
|
|
25
|
+
export declare function enumerateGhosttyTabs(timeoutMs?: number): Promise<GhosttySurface[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Assign a Ghostty tab number to each `host === 'ghostty'` session by matching
|
|
28
|
+
* its cwd to a surface's working directory; ties (same cwd) are broken by title
|
|
29
|
+
* containment against the session's label/topic/kind. Deliberately conservative:
|
|
30
|
+
* a session that can't be uniquely resolved gets NO number — a wrong jump target
|
|
31
|
+
* is worse than none. Pure and unit-tested.
|
|
32
|
+
*/
|
|
33
|
+
export declare function assignGhosttyTabs(sessions: ActiveSession[], surfaces: GhosttySurface[]): Map<ActiveSession, number>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort Ghostty tab-number detection for `agents sessions --active`.
|
|
3
|
+
*
|
|
4
|
+
* Ghostty (macOS) exposes read-only AppleScript: every tab has an `index` and a
|
|
5
|
+
* `name` (title); every surface a `working directory`. It exposes NO per-tab env
|
|
6
|
+
* var and NO tty/pid on a surface — so a session is matched to its tab by
|
|
7
|
+
* `working directory` (with title as a tiebreak). This is display sugar only:
|
|
8
|
+
* one bounded, non-fatal osascript call, run by the renderer, never on the
|
|
9
|
+
* discovery / --json / --waiting path. Any failure (Ghostty not running,
|
|
10
|
+
* Automation permission denied, timeout) degrades silently to no tab number.
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
// Field delimiter: ASCII Unit Separator (0x1F). AppleScript's `tab` keyword does
|
|
16
|
+
// not resolve to a tab byte via `osascript -e`, but `character id 31` does — and
|
|
17
|
+
// 0x1F never appears in a cwd or a tab title, so parsing is unambiguous.
|
|
18
|
+
const ENUM_SCRIPT = `tell application "Ghostty"
|
|
19
|
+
set fd to (character id 31)
|
|
20
|
+
set out to ""
|
|
21
|
+
set wi to 0
|
|
22
|
+
repeat with w in windows
|
|
23
|
+
set wi to wi + 1
|
|
24
|
+
repeat with t in tabs of w
|
|
25
|
+
set ti to index of t
|
|
26
|
+
repeat with s in terminals of t
|
|
27
|
+
set out to out & wi & fd & ti & fd & (working directory of s) & fd & (name of s) & linefeed
|
|
28
|
+
end repeat
|
|
29
|
+
end repeat
|
|
30
|
+
end repeat
|
|
31
|
+
return out
|
|
32
|
+
end tell`;
|
|
33
|
+
/**
|
|
34
|
+
* Enumerate every Ghostty surface (window/tab/cwd/title) via one read-only
|
|
35
|
+
* osascript call. Returns [] on ANY failure — Ghostty not running, Automation
|
|
36
|
+
* permission not granted, timeout, or a parse miss. Never throws, never prompts.
|
|
37
|
+
*/
|
|
38
|
+
export async function enumerateGhosttyTabs(timeoutMs = 1500) {
|
|
39
|
+
if (process.platform !== 'darwin')
|
|
40
|
+
return [];
|
|
41
|
+
let stdout;
|
|
42
|
+
try {
|
|
43
|
+
({ stdout } = await execFileAsync('osascript', ['-e', ENUM_SCRIPT], {
|
|
44
|
+
timeout: timeoutMs,
|
|
45
|
+
killSignal: 'SIGKILL',
|
|
46
|
+
maxBuffer: 1024 * 1024,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const line of stdout.split('\n')) {
|
|
55
|
+
if (!line)
|
|
56
|
+
continue;
|
|
57
|
+
const f = line.split('\u001f');
|
|
58
|
+
if (f.length < 4)
|
|
59
|
+
continue;
|
|
60
|
+
const windowIndex = parseInt(f[0], 10);
|
|
61
|
+
const tabIndex = parseInt(f[1], 10);
|
|
62
|
+
if (!Number.isFinite(windowIndex) || !Number.isFinite(tabIndex))
|
|
63
|
+
continue;
|
|
64
|
+
out.push({ windowIndex, tabIndex, cwd: f[2], title: f[3] });
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** Trailing-slash-insensitive cwd key. */
|
|
69
|
+
function cwdKey(p) {
|
|
70
|
+
return (p ?? '').replace(/\/+$/, '');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Normalize a tab title / hint for containment matching: drop a leading run of
|
|
74
|
+
* non-alphanumerics (Ghostty prefixes the title with a spinner glyph like `⠐ `
|
|
75
|
+
* or `✳ ` while the agent runs) and lowercase. Without this, a title that starts
|
|
76
|
+
* with the session's exact topic still fails a substring test.
|
|
77
|
+
*/
|
|
78
|
+
function normText(s) {
|
|
79
|
+
return s.replace(/^[^\p{L}\p{N}]+/u, '').toLowerCase().trim();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Assign a Ghostty tab number to each `host === 'ghostty'` session by matching
|
|
83
|
+
* its cwd to a surface's working directory; ties (same cwd) are broken by title
|
|
84
|
+
* containment against the session's label/topic/kind. Deliberately conservative:
|
|
85
|
+
* a session that can't be uniquely resolved gets NO number — a wrong jump target
|
|
86
|
+
* is worse than none. Pure and unit-tested.
|
|
87
|
+
*/
|
|
88
|
+
export function assignGhosttyTabs(sessions, surfaces) {
|
|
89
|
+
const result = new Map();
|
|
90
|
+
if (surfaces.length === 0)
|
|
91
|
+
return result;
|
|
92
|
+
const byCwd = new Map();
|
|
93
|
+
for (const s of surfaces) {
|
|
94
|
+
const k = cwdKey(s.cwd);
|
|
95
|
+
const bucket = byCwd.get(k);
|
|
96
|
+
if (bucket)
|
|
97
|
+
bucket.push(s);
|
|
98
|
+
else
|
|
99
|
+
byCwd.set(k, [s]);
|
|
100
|
+
}
|
|
101
|
+
for (const sess of sessions) {
|
|
102
|
+
if (sess.host !== 'ghostty')
|
|
103
|
+
continue;
|
|
104
|
+
const candidates = byCwd.get(cwdKey(sess.cwd));
|
|
105
|
+
if (!candidates || candidates.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
if (candidates.length === 1) {
|
|
108
|
+
result.set(sess, candidates[0].tabIndex);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Tie: break by title containment (glyph-stripped, lowercased). Hints are
|
|
112
|
+
// what the session knows about itself; a good hint is >=8 chars so short
|
|
113
|
+
// fragments don't cause spurious matches.
|
|
114
|
+
const hints = [sess.label, sess.topic, sess.preview]
|
|
115
|
+
.filter((h) => !!h && normText(h).length >= 8)
|
|
116
|
+
.map(normText);
|
|
117
|
+
const matches = candidates.filter(c => {
|
|
118
|
+
const title = normText(c.title);
|
|
119
|
+
return title.length >= 8 && hints.some(h => title.includes(h) || h.includes(title));
|
|
120
|
+
});
|
|
121
|
+
// Only assign when the tiebreak is unambiguous (exactly one title match).
|
|
122
|
+
if (matches.length === 1)
|
|
123
|
+
result.set(sess, matches[0].tabIndex);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -875,19 +876,28 @@ const ANTIGRAVITY_TOOL_MAP = {
|
|
|
875
876
|
* request step and a completion step that share the id).
|
|
876
877
|
*/
|
|
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.
|
|
878
881
|
let rows;
|
|
882
|
+
let db;
|
|
879
883
|
try {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
885
|
-
});
|
|
884
|
+
db = new Database(dbPath);
|
|
885
|
+
rows = db
|
|
886
|
+
.prepare('SELECT idx, step_type, step_payload FROM steps ORDER BY idx;')
|
|
887
|
+
.all();
|
|
886
888
|
}
|
|
887
889
|
catch {
|
|
888
|
-
/* DB not accessible,
|
|
890
|
+
/* DB not accessible, sqlite module unavailable, or query failed */
|
|
889
891
|
return [];
|
|
890
892
|
}
|
|
893
|
+
finally {
|
|
894
|
+
try {
|
|
895
|
+
db?.close();
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
/* best-effort close */
|
|
899
|
+
}
|
|
900
|
+
}
|
|
891
901
|
// Single timestamp for the whole session: the steps table carries no per-step
|
|
892
902
|
// time column, so fall back to the DB file's mtime for a stable, sortable value.
|
|
893
903
|
let timestamp = new Date().toISOString();
|
|
@@ -899,20 +909,13 @@ export function parseAntigravity(dbPath) {
|
|
|
899
909
|
}
|
|
900
910
|
const events = [];
|
|
901
911
|
const seenCallIds = new Set();
|
|
902
|
-
for (const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
continue;
|
|
908
|
-
const tab2 = line.indexOf('\t', tab1 + 1);
|
|
909
|
-
if (tab2 === -1)
|
|
910
|
-
continue;
|
|
911
|
-
const hex = line.slice(tab2 + 1).trim();
|
|
912
|
-
// sqlite3 quote() renders a BLOB as X'...'; anything else (NULL) is skipped.
|
|
913
|
-
if (!hex.startsWith("X'") || !hex.endsWith("'"))
|
|
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))
|
|
914
917
|
continue;
|
|
915
|
-
const bytes =
|
|
918
|
+
const bytes = payload;
|
|
916
919
|
let fields;
|
|
917
920
|
try {
|
|
918
921
|
fields = decodeProtoMessage(bytes);
|
|
@@ -13,6 +13,7 @@ export interface PidSessionEntry {
|
|
|
13
13
|
tmuxPane?: string;
|
|
14
14
|
startedAtMs: number;
|
|
15
15
|
}
|
|
16
|
+
export declare function extractSessionIdArg(args: string[]): string | undefined;
|
|
16
17
|
/** Record a launched agent process. Never throws — the registry is an optimization. */
|
|
17
18
|
export declare function writePidSessionEntry(entry: PidSessionEntry): void;
|
|
18
19
|
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
@@ -19,6 +19,30 @@
|
|
|
19
19
|
import fs from 'fs';
|
|
20
20
|
import path from 'path';
|
|
21
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
|
+
}
|
|
22
46
|
function pidRegistryDir() {
|
|
23
47
|
return path.join(getTerminalsDir(), 'by-pid');
|
|
24
48
|
}
|
|
@@ -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
|
/**
|