@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
package/dist/lib/self-update.js
CHANGED
|
@@ -327,9 +327,89 @@ function packageRootForEntry(real) {
|
|
|
327
327
|
}
|
|
328
328
|
return null;
|
|
329
329
|
}
|
|
330
|
+
export function buildMultiInstallInventory(runningRoot, runningVersion, installs) {
|
|
331
|
+
const byRoot = new Map();
|
|
332
|
+
byRoot.set(runningRoot, { packageRoot: runningRoot, version: runningVersion, note: 'running' });
|
|
333
|
+
for (const install of installs) {
|
|
334
|
+
const notes = [install.packageRoot === runningRoot
|
|
335
|
+
? 'running'
|
|
336
|
+
: install.binPath
|
|
337
|
+
? `agents on PATH: ${install.binPath}`
|
|
338
|
+
: 'discovered install'];
|
|
339
|
+
if (!install.atomicHelperInstall)
|
|
340
|
+
notes.push('unsafe legacy helper installer — remove this copy');
|
|
341
|
+
byRoot.set(install.packageRoot, {
|
|
342
|
+
packageRoot: install.packageRoot,
|
|
343
|
+
version: install.version,
|
|
344
|
+
note: notes.join('; '),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return [...byRoot.values()];
|
|
348
|
+
}
|
|
349
|
+
function childDirectories(parent) {
|
|
350
|
+
try {
|
|
351
|
+
return fs.readdirSync(parent, { withFileTypes: true })
|
|
352
|
+
.filter((entry) => entry.isDirectory())
|
|
353
|
+
.map((entry) => path.join(parent, entry.name));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function knownPackageRoots(opts) {
|
|
360
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
361
|
+
const fnmRoots = opts.fnmDir !== undefined
|
|
362
|
+
? [opts.fnmDir]
|
|
363
|
+
: [process.env.FNM_DIR, path.join(homeDir, '.local', 'share', 'fnm')]
|
|
364
|
+
.filter((value) => Boolean(value));
|
|
365
|
+
const roots = [];
|
|
366
|
+
const packageTail = path.join('lib', 'node_modules', ...NPM_PACKAGE_NAME.split('/'));
|
|
367
|
+
for (const nodeDir of childDirectories(path.join(homeDir, '.nvm', 'versions', 'node'))) {
|
|
368
|
+
roots.push(path.join(nodeDir, packageTail));
|
|
369
|
+
}
|
|
370
|
+
for (const fnmRoot of fnmRoots) {
|
|
371
|
+
for (const versionDir of childDirectories(path.join(fnmRoot, 'node-versions'))) {
|
|
372
|
+
roots.push(path.join(versionDir, 'installation', packageTail));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
roots.push(path.join(homeDir, '.volta', 'tools', 'image', 'packages', ...NPM_PACKAGE_NAME.split('/'), packageTail), path.join(homeDir, '.local', packageTail), path.join(homeDir, '.bun', 'install', 'global', 'node_modules', ...NPM_PACKAGE_NAME.split('/')));
|
|
376
|
+
const npmCacheDir = opts.npmCacheDir ?? process.env.npm_config_cache ?? path.join(homeDir, '.npm');
|
|
377
|
+
for (const npxRunDir of childDirectories(path.join(npmCacheDir, '_npx'))) {
|
|
378
|
+
roots.push(path.join(npxRunDir, 'node_modules', ...NPM_PACKAGE_NAME.split('/')));
|
|
379
|
+
}
|
|
380
|
+
const globalNodeModulesDirs = opts.globalNodeModulesDirs ?? [
|
|
381
|
+
'/opt/homebrew/lib/node_modules',
|
|
382
|
+
'/usr/local/lib/node_modules',
|
|
383
|
+
'/usr/lib/node_modules',
|
|
384
|
+
];
|
|
385
|
+
for (const nodeModulesDir of globalNodeModulesDirs) {
|
|
386
|
+
roots.push(path.join(nodeModulesDir, ...NPM_PACKAGE_NAME.split('/')));
|
|
387
|
+
}
|
|
388
|
+
return roots;
|
|
389
|
+
}
|
|
390
|
+
function readAgentsCliInstall(packageRoot, binPath) {
|
|
391
|
+
let canonicalRoot;
|
|
392
|
+
let pkg;
|
|
393
|
+
try {
|
|
394
|
+
canonicalRoot = fs.realpathSync(packageRoot);
|
|
395
|
+
pkg = JSON.parse(fs.readFileSync(path.join(canonicalRoot, 'package.json'), 'utf-8'));
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
|
|
401
|
+
return null;
|
|
402
|
+
return {
|
|
403
|
+
...(binPath ? { binPath } : {}),
|
|
404
|
+
packageRoot: canonicalRoot,
|
|
405
|
+
version: pkg.version,
|
|
406
|
+
atomicHelperInstall: fs.existsSync(path.join(canonicalRoot, 'dist', 'lib', 'app-bundle-install.js')),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
330
409
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
410
|
+
* Resolve every `agents` entrypoint on PATH, then inspect the bounded global
|
|
411
|
+
* install layouts used by NVM, fnm, Volta, Bun, npm, and npx. More than one
|
|
412
|
+
* distinct package root means upgrades,
|
|
333
413
|
* shims, and the command the user types can act on different copies — the
|
|
334
414
|
* divergence behind silently-failing self-updates.
|
|
335
415
|
*
|
|
@@ -343,11 +423,17 @@ function packageRootForEntry(real) {
|
|
|
343
423
|
* @phnx-labs/agents-cli is some other tool and is skipped.
|
|
344
424
|
* POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
|
|
345
425
|
*/
|
|
346
|
-
export function findAgentsCliInstalls(pathEnv) {
|
|
426
|
+
export function findAgentsCliInstalls(pathEnv, opts = {}) {
|
|
347
427
|
if (process.platform === 'win32')
|
|
348
428
|
return [];
|
|
349
429
|
const installs = [];
|
|
350
430
|
const seenRoots = new Set();
|
|
431
|
+
const addInstall = (install) => {
|
|
432
|
+
if (!install || seenRoots.has(install.packageRoot))
|
|
433
|
+
return;
|
|
434
|
+
seenRoots.add(install.packageRoot);
|
|
435
|
+
installs.push(install);
|
|
436
|
+
};
|
|
351
437
|
for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) {
|
|
352
438
|
const candidate = path.join(dir, 'agents');
|
|
353
439
|
let real;
|
|
@@ -360,19 +446,10 @@ export function findAgentsCliInstalls(pathEnv) {
|
|
|
360
446
|
const packageRoot = packageRootForEntry(real);
|
|
361
447
|
if (!packageRoot)
|
|
362
448
|
continue;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
|
|
373
|
-
continue;
|
|
374
|
-
seenRoots.add(packageRoot);
|
|
375
|
-
installs.push({ binPath: candidate, packageRoot, version: pkg.version });
|
|
449
|
+
addInstall(readAgentsCliInstall(packageRoot, candidate));
|
|
450
|
+
}
|
|
451
|
+
for (const packageRoot of knownPackageRoots(opts)) {
|
|
452
|
+
addInstall(readAgentsCliInstall(packageRoot));
|
|
376
453
|
}
|
|
377
454
|
return installs;
|
|
378
455
|
}
|
|
@@ -23,6 +23,42 @@ export declare function resolveOwner(pidActor: string | null | undefined, sessio
|
|
|
23
23
|
* cwds are just gathered at a bounded spawn rate instead of a single burst.
|
|
24
24
|
*/
|
|
25
25
|
export declare const LSOF_CONCURRENCY = 4;
|
|
26
|
+
/**
|
|
27
|
+
* Process-local process-table memo (#2047). One `getActiveSessions` scan can
|
|
28
|
+
* call `ps -A` from the terminal path, the headless path, and host ancestry —
|
|
29
|
+
* without this, each is a full process-table snapshot. 5s is well under the
|
|
30
|
+
* ~10–30s menu-bar / `--active` poll so a quiet re-poll usually reuses the
|
|
31
|
+
* snapshot; a brand-new agent pid still appears on the next full refresh.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PROCESS_TABLE_FRESH_MS = 5000;
|
|
34
|
+
/**
|
|
35
|
+
* How long {@link listUnattributedActive} may reuse its last full `ps`+`lsof`
|
|
36
|
+
* result (#2047). Between full rescans the previous rows are re-emitted after
|
|
37
|
+
* dropping dead PIDs and PIDs that are now attributed. A *shrinking* attributed
|
|
38
|
+
* set forces a rescan so a process that just left teams/terminals can reappear
|
|
39
|
+
* as headless instead of vanishing until the next cold poll.
|
|
40
|
+
*/
|
|
41
|
+
export declare const UNATTRIBUTED_RESCAN_MS = 15000;
|
|
42
|
+
/** Test seam: override the clock used by process-table / unattributed memos. */
|
|
43
|
+
export declare function setActiveScanClockForTest(fn?: () => number): void;
|
|
44
|
+
/** Test seam: drop the process-table + unattributed memos and their counters. */
|
|
45
|
+
export declare function clearActiveScanCachesForTest(): void;
|
|
46
|
+
/** Test seam: how many times the live `ps` / CIM path actually ran. */
|
|
47
|
+
export declare function processTableLiveReadCountForTest(): number;
|
|
48
|
+
/** Test seam: how many full unattributed (ps + lsof) rescans ran. */
|
|
49
|
+
export declare function unattributedFullRescanCountForTest(): number;
|
|
50
|
+
/**
|
|
51
|
+
* True when any pid in `prev` is absent from `next` — the attributed set
|
|
52
|
+
* shrank, so a process may need to reappear as unattributed. Pure for tests.
|
|
53
|
+
*/
|
|
54
|
+
export declare function attributedSetLostPids(prev: Set<number>, next: Set<number>): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Drop rows whose pid is now attributed or no longer alive. Pure over the
|
|
57
|
+
* inputs so the unattributed TTL reuse path is unit-testable without a live
|
|
58
|
+
* process table. `alive` receives the row's `startedAtMs` so callers can apply
|
|
59
|
+
* the same pid-reuse guard {@link isPidAlive} uses elsewhere.
|
|
60
|
+
*/
|
|
61
|
+
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
26
62
|
export type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
27
63
|
/**
|
|
28
64
|
* Every status here is COMPUTED by the framework from observable signals — PID
|
|
@@ -348,6 +384,8 @@ export declare function resolveNamesToSessionIds(sessionNames: string[], deps: {
|
|
|
348
384
|
* the start time can't be read, we keep the existence answer.
|
|
349
385
|
*/
|
|
350
386
|
export declare function isPidAlive(pid: number, startedAtMs?: number): boolean;
|
|
387
|
+
/** Test seam: drop the Claude session-file path memo. */
|
|
388
|
+
export declare function clearClaudeSessionFileCacheForTest(): void;
|
|
351
389
|
/**
|
|
352
390
|
* Resolve a Claude transcript for `cwd` across the given project roots, newest
|
|
353
391
|
* mtime winning when the same id/cwd resolves in more than one version home
|
|
@@ -431,6 +469,8 @@ interface LiveSignals {
|
|
|
431
469
|
/** Rolling output-token throughput (tokens/sec). Undefined when no usage in the tail. */
|
|
432
470
|
tokPerSec?: number;
|
|
433
471
|
}
|
|
472
|
+
/** Test seam: drop the in-process live-signals memo. */
|
|
473
|
+
export declare function clearLiveSignalsCacheForTest(): void;
|
|
434
474
|
/**
|
|
435
475
|
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
436
476
|
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
@@ -444,6 +484,9 @@ interface LiveSignals {
|
|
|
444
484
|
* `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
445
485
|
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
446
486
|
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
487
|
+
*
|
|
488
|
+
* Unchanged-mtime hits reuse the previous result (see {@link liveSignalsCache})
|
|
489
|
+
* so a 30s active-session poll does not re-tail every quiet transcript.
|
|
447
490
|
*/
|
|
448
491
|
export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean): LiveSignals;
|
|
449
492
|
/**
|
|
@@ -523,6 +566,12 @@ export declare function foldSubordinateAgents(candidates: AgentCandidate[], ppid
|
|
|
523
566
|
* Classified by walking the ppid chain: any recognised UI ancestor (IDE
|
|
524
567
|
* helper, terminal-app, or multiplexer) means `terminal`; nothing of the
|
|
525
568
|
* sort means `headless` (daemon, launchd-spawned, orphan).
|
|
569
|
+
*
|
|
570
|
+
* Full `ps`+`lsof` rescans are throttled to {@link UNATTRIBUTED_RESCAN_MS}
|
|
571
|
+
* (#2047). Between rescans the previous rows are re-emitted after dropping
|
|
572
|
+
* dead PIDs and PIDs that are now attributed. A shrinking attributed set
|
|
573
|
+
* forces a rescan so a process that just left teams/terminals can reappear
|
|
574
|
+
* as headless.
|
|
526
575
|
*/
|
|
527
576
|
export declare function listUnattributedActive(attributed: Set<number>): Promise<ActiveSession[]>;
|
|
528
577
|
/** One tmux pane's resolved agent identity for the authoritative source. */
|
|
@@ -71,6 +71,74 @@ const LSOF_STAGGER_MS = 10;
|
|
|
71
71
|
*/
|
|
72
72
|
const LSOF_TIMEOUT_MS = 5_000;
|
|
73
73
|
const PS_SNAPSHOT_TIMEOUT_MS = 10_000;
|
|
74
|
+
/**
|
|
75
|
+
* Process-local process-table memo (#2047). One `getActiveSessions` scan can
|
|
76
|
+
* call `ps -A` from the terminal path, the headless path, and host ancestry —
|
|
77
|
+
* without this, each is a full process-table snapshot. 5s is well under the
|
|
78
|
+
* ~10–30s menu-bar / `--active` poll so a quiet re-poll usually reuses the
|
|
79
|
+
* snapshot; a brand-new agent pid still appears on the next full refresh.
|
|
80
|
+
*/
|
|
81
|
+
export const PROCESS_TABLE_FRESH_MS = 5_000;
|
|
82
|
+
/**
|
|
83
|
+
* How long {@link listUnattributedActive} may reuse its last full `ps`+`lsof`
|
|
84
|
+
* result (#2047). Between full rescans the previous rows are re-emitted after
|
|
85
|
+
* dropping dead PIDs and PIDs that are now attributed. A *shrinking* attributed
|
|
86
|
+
* set forces a rescan so a process that just left teams/terminals can reappear
|
|
87
|
+
* as headless instead of vanishing until the next cold poll.
|
|
88
|
+
*/
|
|
89
|
+
export const UNATTRIBUTED_RESCAN_MS = 15_000;
|
|
90
|
+
/** Injectable clock for the active-scan memos (tests only). */
|
|
91
|
+
let activeScanNow = () => Date.now();
|
|
92
|
+
/** Test seam: override the clock used by process-table / unattributed memos. */
|
|
93
|
+
export function setActiveScanClockForTest(fn) {
|
|
94
|
+
activeScanNow = fn ?? (() => Date.now());
|
|
95
|
+
}
|
|
96
|
+
let processTableCache;
|
|
97
|
+
let processTableLiveReads = 0;
|
|
98
|
+
let unattributedCache;
|
|
99
|
+
let unattributedFullRescans = 0;
|
|
100
|
+
/** Test seam: drop the process-table + unattributed memos and their counters. */
|
|
101
|
+
export function clearActiveScanCachesForTest() {
|
|
102
|
+
processTableCache = undefined;
|
|
103
|
+
processTableLiveReads = 0;
|
|
104
|
+
unattributedCache = undefined;
|
|
105
|
+
unattributedFullRescans = 0;
|
|
106
|
+
activeScanNow = () => Date.now();
|
|
107
|
+
}
|
|
108
|
+
/** Test seam: how many times the live `ps` / CIM path actually ran. */
|
|
109
|
+
export function processTableLiveReadCountForTest() {
|
|
110
|
+
return processTableLiveReads;
|
|
111
|
+
}
|
|
112
|
+
/** Test seam: how many full unattributed (ps + lsof) rescans ran. */
|
|
113
|
+
export function unattributedFullRescanCountForTest() {
|
|
114
|
+
return unattributedFullRescans;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True when any pid in `prev` is absent from `next` — the attributed set
|
|
118
|
+
* shrank, so a process may need to reappear as unattributed. Pure for tests.
|
|
119
|
+
*/
|
|
120
|
+
export function attributedSetLostPids(prev, next) {
|
|
121
|
+
for (const p of prev) {
|
|
122
|
+
if (!next.has(p))
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Drop rows whose pid is now attributed or no longer alive. Pure over the
|
|
129
|
+
* inputs so the unattributed TTL reuse path is unit-testable without a live
|
|
130
|
+
* process table. `alive` receives the row's `startedAtMs` so callers can apply
|
|
131
|
+
* the same pid-reuse guard {@link isPidAlive} uses elsewhere.
|
|
132
|
+
*/
|
|
133
|
+
export function filterCachedUnattributed(sessions, attributed, alive) {
|
|
134
|
+
return sessions.filter((s) => {
|
|
135
|
+
if (s.pid == null)
|
|
136
|
+
return false;
|
|
137
|
+
if (attributed.has(s.pid))
|
|
138
|
+
return false;
|
|
139
|
+
return alive(s.pid, s.startedAtMs);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
74
142
|
export function activeStatusFromCloudStatus(status) {
|
|
75
143
|
switch (status) {
|
|
76
144
|
case 'running':
|
|
@@ -373,6 +441,19 @@ function readLiveTerminals() {
|
|
|
373
441
|
function claudeProjectDirName(cwd) {
|
|
374
442
|
return cwd.replace(/[/.]/g, '-');
|
|
375
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Process-local memo for Claude transcript path resolution. Each active-session
|
|
446
|
+
* poll re-walks every Claude version-home `projects/` tree for every live pid
|
|
447
|
+
* (#2047). A resolved path is sticky while the file still exists; a miss or a
|
|
448
|
+
* vanished file re-walks. Cap size so a long-lived process cannot retain every
|
|
449
|
+
* cwd it has ever seen.
|
|
450
|
+
*/
|
|
451
|
+
const CLAUDE_SESSION_FILE_CACHE_MAX = 256;
|
|
452
|
+
const claudeSessionFileCache = new Map();
|
|
453
|
+
/** Test seam: drop the Claude session-file path memo. */
|
|
454
|
+
export function clearClaudeSessionFileCacheForTest() {
|
|
455
|
+
claudeSessionFileCache.clear();
|
|
456
|
+
}
|
|
376
457
|
/**
|
|
377
458
|
* Locate the active Claude session file for a process. If we know the session
|
|
378
459
|
* UUID (from terminal env or team parent), prefer the exact match. Otherwise
|
|
@@ -390,6 +471,28 @@ function claudeProjectDirName(cwd) {
|
|
|
390
471
|
* the rest of the CLI uses, so this stays in lockstep with discovery.
|
|
391
472
|
*/
|
|
392
473
|
function findClaudeSessionFile(cwd, sessionId) {
|
|
474
|
+
// Only memoize when the exact session UUID is known. Without an id the
|
|
475
|
+
// resolver picks newest-by-mtime under the project dir; caching that path
|
|
476
|
+
// would stick to an old transcript after a new session starts in the same cwd.
|
|
477
|
+
if (sessionId) {
|
|
478
|
+
const cacheKey = `${cwd}\0${sessionId}`;
|
|
479
|
+
const hit = claudeSessionFileCache.get(cacheKey);
|
|
480
|
+
if (hit !== undefined) {
|
|
481
|
+
try {
|
|
482
|
+
if (fs.existsSync(hit))
|
|
483
|
+
return hit;
|
|
484
|
+
}
|
|
485
|
+
catch { /* re-resolve */ }
|
|
486
|
+
claudeSessionFileCache.delete(cacheKey);
|
|
487
|
+
}
|
|
488
|
+
const resolved = pickClaudeSessionFileAcrossRoots(getAgentSessionDirs('claude', 'projects'), cwd, sessionId);
|
|
489
|
+
if (resolved) {
|
|
490
|
+
if (claudeSessionFileCache.size >= CLAUDE_SESSION_FILE_CACHE_MAX)
|
|
491
|
+
claudeSessionFileCache.clear();
|
|
492
|
+
claudeSessionFileCache.set(cacheKey, resolved);
|
|
493
|
+
}
|
|
494
|
+
return resolved;
|
|
495
|
+
}
|
|
393
496
|
return pickClaudeSessionFileAcrossRoots(getAgentSessionDirs('claude', 'projects'), cwd, sessionId);
|
|
394
497
|
}
|
|
395
498
|
/**
|
|
@@ -562,6 +665,19 @@ function parseTailEventsForKind(agent, sessionFile) {
|
|
|
562
665
|
}
|
|
563
666
|
return events.length > LIVE_STATE_MAX_EVENTS ? events.slice(-LIVE_STATE_MAX_EVENTS) : events;
|
|
564
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Process-local memo for {@link computeLiveSignals}. The menu-bar / `--active`
|
|
670
|
+
* poll re-reads every live session on a ~30s tick (#2047). When a transcript's
|
|
671
|
+
* mtime (and pidAlive / kind / cwd) are unchanged, re-parsing the tail yields
|
|
672
|
+
* the same signals — skip the parse. Bound the map so a long-lived daemon that
|
|
673
|
+
* sees thousands of sessions over its life cannot retain them forever.
|
|
674
|
+
*/
|
|
675
|
+
const LIVE_SIGNALS_CACHE_MAX = 512;
|
|
676
|
+
const liveSignalsCache = new Map();
|
|
677
|
+
/** Test seam: drop the in-process live-signals memo. */
|
|
678
|
+
export function clearLiveSignalsCacheForTest() {
|
|
679
|
+
liveSignalsCache.clear();
|
|
680
|
+
}
|
|
565
681
|
/**
|
|
566
682
|
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
567
683
|
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
@@ -575,6 +691,9 @@ function parseTailEventsForKind(agent, sessionFile) {
|
|
|
575
691
|
* `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
576
692
|
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
577
693
|
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
694
|
+
*
|
|
695
|
+
* Unchanged-mtime hits reuse the previous result (see {@link liveSignalsCache})
|
|
696
|
+
* so a 30s active-session poll does not re-tail every quiet transcript.
|
|
578
697
|
*/
|
|
579
698
|
export function computeLiveSignals(kind, sessionFile, cwd, pidAlive) {
|
|
580
699
|
if (!sessionFile)
|
|
@@ -584,21 +703,31 @@ export function computeLiveSignals(kind, sessionFile, cwd, pidAlive) {
|
|
|
584
703
|
mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
585
704
|
}
|
|
586
705
|
catch { /* vanished between calls */ }
|
|
706
|
+
const cacheKey = `${kind}\0${sessionFile}\0${cwd ?? ''}`;
|
|
707
|
+
const cached = liveSignalsCache.get(cacheKey);
|
|
708
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.pidAlive === pidAlive) {
|
|
709
|
+
return cached.signals;
|
|
710
|
+
}
|
|
587
711
|
const ctx = { cwd, pidAlive, mtimeMs, activeWindowMs: ACTIVE_MTIME_WINDOW_MS };
|
|
712
|
+
let signals = {};
|
|
588
713
|
if (kind === 'claude' || kind === 'codex') {
|
|
589
714
|
const { events, content } = readSessionTailWithRaw(sessionFile, kind);
|
|
590
|
-
if (events.length
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
715
|
+
if (events.length > 0) {
|
|
716
|
+
const state = inferSessionState(events, ctx);
|
|
717
|
+
const tokPerSec = computeTokPerSec(content, kind);
|
|
718
|
+
signals = { state, tokPerSec: tokPerSec > 0 ? tokPerSec : undefined };
|
|
719
|
+
}
|
|
595
720
|
}
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
721
|
+
else if (isSessionTrackedAgent(kind)) {
|
|
722
|
+
const events = parseTailEventsForKind(kind, sessionFile);
|
|
723
|
+
if (events.length > 0) {
|
|
724
|
+
signals = { state: inferSessionState(events, ctx) };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (liveSignalsCache.size >= LIVE_SIGNALS_CACHE_MAX)
|
|
728
|
+
liveSignalsCache.clear();
|
|
729
|
+
liveSignalsCache.set(cacheKey, { mtimeMs, pidAlive, signals });
|
|
730
|
+
return signals;
|
|
602
731
|
}
|
|
603
732
|
/** Map inferred activity onto the coarse ActiveStatus used by the renderer and counts. */
|
|
604
733
|
function statusFromActivity(activity) {
|
|
@@ -880,8 +1009,24 @@ const HOST_MATCHERS = [
|
|
|
880
1009
|
* walk ancestry chains to attribute child processes to their terminal hosts.
|
|
881
1010
|
* `comm` may be an absolute path for shim-launched agents, so basename before
|
|
882
1011
|
* matching against AGENT_CLI_NAMES.
|
|
1012
|
+
*
|
|
1013
|
+
* Memoized for {@link PROCESS_TABLE_FRESH_MS} so one active-session scan (and a
|
|
1014
|
+
* quiet re-poll within the window) does not re-shell `ps -A` / CIM for every
|
|
1015
|
+
* caller — terminal host detection, headless attribution, and `hostFromPid`
|
|
1016
|
+
* all share the same snapshot (#2047).
|
|
883
1017
|
*/
|
|
884
1018
|
async function readProcessTable() {
|
|
1019
|
+
const now = activeScanNow();
|
|
1020
|
+
if (processTableCache && now - processTableCache.at < PROCESS_TABLE_FRESH_MS) {
|
|
1021
|
+
return processTableCache.rows;
|
|
1022
|
+
}
|
|
1023
|
+
const rows = await readProcessTableLive();
|
|
1024
|
+
processTableCache = { at: now, rows };
|
|
1025
|
+
return rows;
|
|
1026
|
+
}
|
|
1027
|
+
/** Uncached process-table snapshot (the live `ps` / CIM path). */
|
|
1028
|
+
async function readProcessTableLive() {
|
|
1029
|
+
processTableLiveReads += 1;
|
|
885
1030
|
if (process.platform === 'win32')
|
|
886
1031
|
return readProcessTableWin32();
|
|
887
1032
|
let out;
|
|
@@ -1138,8 +1283,27 @@ export function foldSubordinateAgents(candidates, ppidMap, readEntry) {
|
|
|
1138
1283
|
* Classified by walking the ppid chain: any recognised UI ancestor (IDE
|
|
1139
1284
|
* helper, terminal-app, or multiplexer) means `terminal`; nothing of the
|
|
1140
1285
|
* sort means `headless` (daemon, launchd-spawned, orphan).
|
|
1286
|
+
*
|
|
1287
|
+
* Full `ps`+`lsof` rescans are throttled to {@link UNATTRIBUTED_RESCAN_MS}
|
|
1288
|
+
* (#2047). Between rescans the previous rows are re-emitted after dropping
|
|
1289
|
+
* dead PIDs and PIDs that are now attributed. A shrinking attributed set
|
|
1290
|
+
* forces a rescan so a process that just left teams/terminals can reappear
|
|
1291
|
+
* as headless.
|
|
1141
1292
|
*/
|
|
1142
1293
|
export async function listUnattributedActive(attributed) {
|
|
1294
|
+
const now = activeScanNow();
|
|
1295
|
+
if (unattributedCache &&
|
|
1296
|
+
now - unattributedCache.at < UNATTRIBUTED_RESCAN_MS &&
|
|
1297
|
+
!attributedSetLostPids(unattributedCache.attributed, attributed)) {
|
|
1298
|
+
return filterCachedUnattributed(unattributedCache.sessions, attributed, isPidAlive);
|
|
1299
|
+
}
|
|
1300
|
+
const out = await listUnattributedActiveLive(attributed);
|
|
1301
|
+
unattributedCache = { at: now, attributed: new Set(attributed), sessions: out };
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
/** Unthrottled headless scan (live process table + cwd probes). */
|
|
1305
|
+
async function listUnattributedActiveLive(attributed) {
|
|
1306
|
+
unattributedFullRescans += 1;
|
|
1143
1307
|
const table = await readProcessTable();
|
|
1144
1308
|
const procByPid = new Map();
|
|
1145
1309
|
const ppidMap = new Map();
|
|
@@ -19,6 +19,22 @@ export interface BashCommandInfo {
|
|
|
19
19
|
summary: string;
|
|
20
20
|
signal: 'high' | 'mid' | 'low';
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* TOOL_REGISTRY (category + action, aliases flattened to their own keys)
|
|
24
|
+
* serialized as the body of a Python dict literal. The embedded activity-log
|
|
25
|
+
* hook script (`ACTIVITY_LOG_HOOK_SCRIPT` in `activity.ts`) runs standalone
|
|
26
|
+
* under `python3` and can't import this module, so its taxonomy is generated
|
|
27
|
+
* from this single source instead of hand-duplicated (#1889 — the two had
|
|
28
|
+
* already drifted: the hand-authored copy was missing `rmdir`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function pythonToolRegistryLiteral(indent?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* VALUE_FLAGS serialized as the body of a Python dict of sets. Same reason as
|
|
33
|
+
* {@link pythonToolRegistryLiteral}: the activity-log hook script embeds this
|
|
34
|
+
* table and must not hand-drift from the TypeScript source (#1889 — the Python
|
|
35
|
+
* copy was missing `agents`/`linear` two-level tools).
|
|
36
|
+
*/
|
|
37
|
+
export declare function pythonValueFlagsLiteral(indent?: string): string;
|
|
22
38
|
export declare function unwrapCommand(cmd: string): string;
|
|
23
39
|
/**
|
|
24
40
|
* Split a possibly-compound Bash command into simple commands, then tokenize each
|
|
@@ -95,6 +95,25 @@ const TOOL_REGISTRY = {
|
|
|
95
95
|
sleep: { category: 'wait', signal: 'low', action: 'sleeping' },
|
|
96
96
|
wait: { category: 'wait', signal: 'low', action: 'waiting' },
|
|
97
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* TOOL_REGISTRY (category + action, aliases flattened to their own keys)
|
|
100
|
+
* serialized as the body of a Python dict literal. The embedded activity-log
|
|
101
|
+
* hook script (`ACTIVITY_LOG_HOOK_SCRIPT` in `activity.ts`) runs standalone
|
|
102
|
+
* under `python3` and can't import this module, so its taxonomy is generated
|
|
103
|
+
* from this single source instead of hand-duplicated (#1889 — the two had
|
|
104
|
+
* already drifted: the hand-authored copy was missing `rmdir`).
|
|
105
|
+
*/
|
|
106
|
+
export function pythonToolRegistryLiteral(indent = ' ') {
|
|
107
|
+
const lines = [];
|
|
108
|
+
for (const [name, info] of Object.entries(TOOL_REGISTRY)) {
|
|
109
|
+
const entry = `{"category": ${JSON.stringify(info.category)}, "action": ${JSON.stringify(info.action)}}`;
|
|
110
|
+
lines.push(`${indent}${JSON.stringify(name)}: ${entry},`);
|
|
111
|
+
for (const alias of info.aliases ?? []) {
|
|
112
|
+
lines.push(`${indent}${JSON.stringify(alias)}: ${entry},`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
98
117
|
/**
|
|
99
118
|
* Two-level tools (bucket key includes the subcommand) mapped to the flags that
|
|
100
119
|
* consume the *following* token as their value, per tool. The subcommand scan
|
|
@@ -124,6 +143,25 @@ const VALUE_FLAGS = {
|
|
|
124
143
|
agents: new Set(['-H', '--host', '--device']),
|
|
125
144
|
linear: new Set(),
|
|
126
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* VALUE_FLAGS serialized as the body of a Python dict of sets. Same reason as
|
|
148
|
+
* {@link pythonToolRegistryLiteral}: the activity-log hook script embeds this
|
|
149
|
+
* table and must not hand-drift from the TypeScript source (#1889 — the Python
|
|
150
|
+
* copy was missing `agents`/`linear` two-level tools).
|
|
151
|
+
*/
|
|
152
|
+
export function pythonValueFlagsLiteral(indent = ' ') {
|
|
153
|
+
const lines = [];
|
|
154
|
+
for (const [name, flags] of Object.entries(VALUE_FLAGS)) {
|
|
155
|
+
if (flags.size === 0) {
|
|
156
|
+
lines.push(`${indent}${JSON.stringify(name)}: set(),`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Stable order for golden-string equality (install/write checks).
|
|
160
|
+
const items = [...flags].sort().map((f) => JSON.stringify(f)).join(', ');
|
|
161
|
+
lines.push(`${indent}${JSON.stringify(name)}: {${items}},`);
|
|
162
|
+
}
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
127
165
|
/** Tools whose bucket key includes the second token (subcommand). */
|
|
128
166
|
const TWO_LEVEL_TOOLS = new Set(Object.keys(VALUE_FLAGS));
|
|
129
167
|
/** Remote wrappers whose bucket key carries an `ssh→` prefix on the inner command. */
|
|
@@ -145,10 +183,20 @@ export function unwrapCommand(cmd) {
|
|
|
145
183
|
const env = s.match(/^([A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)+(.+)$/);
|
|
146
184
|
if (env)
|
|
147
185
|
return unwrapCommand(env[2]);
|
|
186
|
+
// export VAR=value prefix, same shape as the bare env-var prefix above (#1889)
|
|
187
|
+
const exportPrefix = s.match(/^export\s+(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s*(?:&&|;|\n)\s*([\s\S]+)$/);
|
|
188
|
+
if (exportPrefix)
|
|
189
|
+
return unwrapCommand(exportPrefix[1]);
|
|
148
190
|
// sudo / time prefix (value-taking flags like `-u user` consume their argument)
|
|
149
191
|
const prefix = s.match(/^(?:sudo|time)(?:\s+(?:-[uUgGhpCrtDR]\s+\S+|-\S+))*\s+(.+)$/);
|
|
150
192
|
if (prefix)
|
|
151
193
|
return unwrapCommand(prefix[1]);
|
|
194
|
+
// set -euo pipefail && command / set -x; command (shell-option prefix; `-o`
|
|
195
|
+
// mode names like `pipefail` trail a flag without their own `-`, so consume
|
|
196
|
+
// everything up to the first operator rather than itemizing flag shapes)
|
|
197
|
+
const setPrefix = s.match(/^set\s+[^&;\n]+?(?:&&|;|\n)\s*([\s\S]+)$/);
|
|
198
|
+
if (setPrefix)
|
|
199
|
+
return unwrapCommand(setPrefix[1]);
|
|
152
200
|
// cd foo && command (also `cd foo; command` and newline-separated `cd foo\ncommand`)
|
|
153
201
|
const cd = s.match(/^cd\s+\S+\s*(?:&&|;|\n)\s*([\s\S]+)$/);
|
|
154
202
|
if (cd)
|
|
@@ -157,6 +205,23 @@ export function unwrapCommand(cmd) {
|
|
|
157
205
|
const npx = s.match(/^(?:npx|bunx)\s+(?:-\S+\s+)*(.+)$/);
|
|
158
206
|
if (npx)
|
|
159
207
|
return unwrapCommand(npx[1]);
|
|
208
|
+
// for X in ...; do command; done — unwrap to the loop body, not the `for` keyword
|
|
209
|
+
const forLoop = s.match(/^for\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$/);
|
|
210
|
+
if (forLoop)
|
|
211
|
+
return unwrapCommand(forLoop[1]);
|
|
212
|
+
// until condition; do command; done
|
|
213
|
+
const untilLoop = s.match(/^until\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$/);
|
|
214
|
+
if (untilLoop)
|
|
215
|
+
return unwrapCommand(untilLoop[1]);
|
|
216
|
+
// if condition; then command; fi — unwrap to the `then` branch, the real work;
|
|
217
|
+
// the condition itself is usually a test/comparison, not the command of interest
|
|
218
|
+
const ifCond = s.match(/^if\s+[\s\S]+?\bthen\b\s*([\s\S]+?)\s*;?\s*(?:elif\b|else\b|fi\b)[\s\S]*$/);
|
|
219
|
+
if (ifCond)
|
|
220
|
+
return unwrapCommand(ifCond[1]);
|
|
221
|
+
// (command) subshell, e.g. `(gh pr create --title x)` or `(cd /repo && npm i)`
|
|
222
|
+
const subshell = s.match(/^\(\s*([\s\S]+?)\s*\)\s*(?:&&|;|\|\|)[\s\S]*$/) ?? s.match(/^\(\s*([\s\S]+?)\s*\)\s*$/);
|
|
223
|
+
if (subshell)
|
|
224
|
+
return unwrapCommand(subshell[1]);
|
|
160
225
|
return s;
|
|
161
226
|
}
|
|
162
227
|
function splitOnOperators(cmd) {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type IndexedToolCall } from './tool-calls.js';
|
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 37;
|
|
16
16
|
/**
|
|
17
17
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
18
18
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -26,7 +26,8 @@ export declare const RESOURCE_INDEX_VERSION = 1;
|
|
|
26
26
|
* re-derives on the next `agents insights` instead of silently reporting stale
|
|
27
27
|
* numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
30
|
+
export declare const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
30
31
|
/** Raw row shape returned from the sessions table. */
|
|
31
32
|
export interface SessionRow {
|
|
32
33
|
id: string;
|
|
@@ -50,7 +51,11 @@ export interface SessionRow {
|
|
|
50
51
|
message_count: number | null;
|
|
51
52
|
token_count: number | null;
|
|
52
53
|
output_tokens: number | null;
|
|
54
|
+
input_tokens: number | null;
|
|
55
|
+
cache_read_tokens: number | null;
|
|
56
|
+
cache_write_tokens: number | null;
|
|
53
57
|
cost_usd: number | null;
|
|
58
|
+
cost_usd_nocache: number | null;
|
|
54
59
|
duration_ms: number | null;
|
|
55
60
|
model: string | null;
|
|
56
61
|
tool_call_count: number | null;
|
|
@@ -318,6 +323,10 @@ export declare function latestSessionFileForCwd(agent: SessionAgentId, cwd: stri
|
|
|
318
323
|
maxAgeMs?: number;
|
|
319
324
|
nowMs?: number;
|
|
320
325
|
}): string | undefined;
|
|
326
|
+
/** Process-local diagnostics for the real-filesystem existence-cache tests. */
|
|
327
|
+
export declare function getSessionExistenceCacheStats(): {
|
|
328
|
+
sweeps: number;
|
|
329
|
+
};
|
|
321
330
|
/** Query sessions from the database, applying filters and ordering by last-activity descending (default). */
|
|
322
331
|
export declare function querySessions(options?: QueryOptions): SessionMeta[];
|
|
323
332
|
/** Count sessions matching the given filter options. */
|
|
@@ -335,11 +344,23 @@ export interface UsageRollupRow {
|
|
|
335
344
|
*/
|
|
336
345
|
label?: string;
|
|
337
346
|
costUsd: number;
|
|
347
|
+
/**
|
|
348
|
+
* USD cost priced as if caching were off (cache read/write at the input rate),
|
|
349
|
+
* summed from `cost_usd_nocache`. Backs `agents output --pricing no-cache`.
|
|
350
|
+
* Equals `costUsd` for rows whose sessions record no cache split (RUSH-2287).
|
|
351
|
+
*/
|
|
352
|
+
costUsdNoCache: number;
|
|
338
353
|
durationMs: number;
|
|
339
354
|
sessionCount: number;
|
|
340
355
|
tokenCount: number;
|
|
341
356
|
/** Real generated (output) tokens — excludes cache-read/-write context. */
|
|
342
357
|
outputTokens: number;
|
|
358
|
+
/** Uncached input tokens summed across the group (0 where no harness recorded a split). */
|
|
359
|
+
inputTokens: number;
|
|
360
|
+
/** Cache-read tokens summed across the group. */
|
|
361
|
+
cacheReadTokens: number;
|
|
362
|
+
/** Cache-write (cache-creation) tokens summed across the group. */
|
|
363
|
+
cacheWriteTokens: number;
|
|
343
364
|
}
|
|
344
365
|
/** What to group a usage rollup by. */
|
|
345
366
|
/**
|