@phnx-labs/agents-cli 1.22.23 → 1.22.25
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 +359 -0
- package/README.md +19 -8
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +105 -9
- package/dist/commands/exec.js +99 -44
- package/dist/commands/feed.d.ts +23 -0
- package/dist/commands/feed.js +65 -2
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +301 -57
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +14 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +36 -1
- package/dist/commands/sessions-browser.js +165 -19
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +28 -12
- package/dist/commands/sessions.d.ts +23 -1
- package/dist/commands/sessions.js +58 -61
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/teams.js +3 -2
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +38 -2
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +15 -3
- package/dist/lib/exec.js +112 -19
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +27 -22
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +158 -85
- package/dist/lib/secrets/filestore.d.ts +7 -3
- package/dist/lib/secrets/filestore.js +25 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +45 -6
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +44 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
package/dist/lib/daemon.js
CHANGED
|
@@ -24,7 +24,9 @@ import { BrowserService } from './browser/service.js';
|
|
|
24
24
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
25
25
|
import { redactSecrets } from './redact.js';
|
|
26
26
|
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from './cli-entry.js';
|
|
27
|
-
import { isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
|
|
27
|
+
import { getConfigValue, isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
|
|
28
|
+
import { reapTerminalRoutineProcesses } from './routine-process-cleanup.js';
|
|
29
|
+
import { runWatchdogPass } from './watchdog/service.js';
|
|
28
30
|
const PID_FILE = 'daemon.pid';
|
|
29
31
|
const LOCK_FILE = 'daemon.lock';
|
|
30
32
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -34,6 +36,7 @@ const LOG_ROTATE_COUNT = 3;
|
|
|
34
36
|
const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
35
37
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
36
38
|
const MONITOR_TICK_MS = 60_000;
|
|
39
|
+
const WATCHDOG_TICK_MS = 3 * 60_000;
|
|
37
40
|
/**
|
|
38
41
|
* How often to re-scan for missed fires. Deliberately slower than the monitor
|
|
39
42
|
* tick: detection walks a week of cron occurrences per routine
|
|
@@ -572,6 +575,24 @@ export async function runDaemon() {
|
|
|
572
575
|
}
|
|
573
576
|
if (schedulerEnabledAtBoot)
|
|
574
577
|
bootScheduler();
|
|
578
|
+
let watchdogInFlight = false;
|
|
579
|
+
const runDaemonWatchdog = async () => {
|
|
580
|
+
if (watchdogInFlight || getConfigValue('watchdog.enabled').value !== true)
|
|
581
|
+
return;
|
|
582
|
+
watchdogInFlight = true;
|
|
583
|
+
try {
|
|
584
|
+
const result = await runWatchdogPass({ nudge: true });
|
|
585
|
+
log('INFO', `watchdog: ${result.counts.total} live, ${result.counts.stalled} stalled, ${result.counts.nudged} nudged`);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
log('ERROR', `watchdog tick failed: ${err.message}`);
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
watchdogInFlight = false;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const watchdogInterval = setInterval(() => { void runDaemonWatchdog(); }, WATCHDOG_TICK_MS);
|
|
595
|
+
const watchdogKickoff = setTimeout(() => { void runDaemonWatchdog(); }, 30_000);
|
|
575
596
|
// Monitor engine: event-triggered watchers, beside the cron scheduler. Same
|
|
576
597
|
// daemon, same dispatch seam — a monitor is a routine whose trigger is a
|
|
577
598
|
// watched source instead of a clock. Reloads on SIGHUP alongside the scheduler.
|
|
@@ -682,6 +703,9 @@ export async function runDaemon() {
|
|
|
682
703
|
const monitorInterval = setInterval(() => {
|
|
683
704
|
writeHeartbeat();
|
|
684
705
|
monitorRunningJobs();
|
|
706
|
+
const reaped = reapTerminalRoutineProcesses();
|
|
707
|
+
if (reaped.length > 0)
|
|
708
|
+
log('WARN', `Reaped ${reaped.length} terminal routine process group(s): ${reaped.join(', ')}`);
|
|
685
709
|
}, MONITOR_TICK_MS);
|
|
686
710
|
// Resource safety check: heal gaps between what DotAgents repos define and
|
|
687
711
|
// what's actually installed in each agent home — the slow rot that nothing
|
|
@@ -1066,6 +1090,8 @@ export async function runDaemon() {
|
|
|
1066
1090
|
monitorEngine.stop();
|
|
1067
1091
|
await browserIPC.stop();
|
|
1068
1092
|
clearInterval(monitorInterval);
|
|
1093
|
+
clearInterval(watchdogInterval);
|
|
1094
|
+
clearTimeout(watchdogKickoff);
|
|
1069
1095
|
clearInterval(healInterval);
|
|
1070
1096
|
clearTimeout(healKickoff);
|
|
1071
1097
|
clearInterval(autoDispatchInterval);
|
|
@@ -67,6 +67,13 @@ export const CONFIG_KEYS = [
|
|
|
67
67
|
type: 'bool',
|
|
68
68
|
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'watchdog.enabled',
|
|
72
|
+
yamlKey: 'watchdogEnabled',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'bool',
|
|
75
|
+
description: 'Whether the daemon runs the watchdog pass on this device.',
|
|
76
|
+
},
|
|
70
77
|
{
|
|
71
78
|
name: 'browser.remote-control',
|
|
72
79
|
yamlKey: 'browserRemoteControl',
|
|
@@ -10,7 +10,7 @@ export type FindingSeverity = 'critical' | 'warning';
|
|
|
10
10
|
* the JSON consumer group by kind. */
|
|
11
11
|
/** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
|
|
12
12
|
* below owns it, and a second copy in these comments is a fourth place to drift. */
|
|
13
|
-
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "exec-policy", "stale-cli"];
|
|
13
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "stale-cli"];
|
|
14
14
|
/**
|
|
15
15
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
16
16
|
* the builders below and asserted against both prose rubrics by
|
|
@@ -89,6 +89,12 @@ export interface LocalFindingInputs {
|
|
|
89
89
|
duplicateHooks?: DuplicateVersionHook[];
|
|
90
90
|
/** Credential-shaped exports found in the user's shell rc files (RUSH-1968). */
|
|
91
91
|
rcSecrets?: RcSecretFinding[];
|
|
92
|
+
/** True when the file-store master key is live in THIS process's environment.
|
|
93
|
+
* Distinct from `rcSecrets`, which scans FILES: a value inherited by a
|
|
94
|
+
* long-lived process survives deleting the rc line that set it, so the rc
|
|
95
|
+
* scan reports clean while the leak is still in flight (RUSH-1968). Never the
|
|
96
|
+
* value — only whether it is set. */
|
|
97
|
+
masterPassphraseInEnv?: boolean;
|
|
92
98
|
/** The effective PowerShell execution policy and the platform it was read on.
|
|
93
99
|
* Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
|
|
94
100
|
execPolicy?: {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* never-synced · stale · repo-behind · repo-drift · version-skew ·
|
|
24
24
|
* fleet-resource-gap · orphan · duplicate-hook ·
|
|
25
25
|
* duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
|
|
26
|
-
* rc-secret-export · exec-policy · stale-cli.
|
|
26
|
+
* rc-secret-export · env-secret-export · exec-policy · stale-cli.
|
|
27
27
|
* (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
|
|
28
28
|
* stale-sync states one `agents sync` resolves, not "needs you now".)
|
|
29
29
|
*
|
|
@@ -94,6 +94,7 @@ export const ALL_FINDING_KINDS = [
|
|
|
94
94
|
'duplicate-hook', // one hook materialized in several version homes, byte-identical
|
|
95
95
|
'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
|
|
96
96
|
'rc-secret-export', // credential-shaped export in a shell rc file
|
|
97
|
+
'env-secret-export', // the file-store master key live in THIS process's env
|
|
97
98
|
'exec-policy', // Windows execution policy blocks agents.ps1
|
|
98
99
|
'stale-cli',
|
|
99
100
|
];
|
|
@@ -133,6 +134,7 @@ export const FINDING_SEVERITY = {
|
|
|
133
134
|
'host-cli-missing': 'warning',
|
|
134
135
|
'host-cli-invalid': 'warning',
|
|
135
136
|
'rc-secret-export': 'warning',
|
|
137
|
+
'env-secret-export': 'warning',
|
|
136
138
|
'exec-policy': 'warning',
|
|
137
139
|
'stale-cli': 'warning',
|
|
138
140
|
};
|
|
@@ -240,6 +242,11 @@ export function remediationFor(finding) {
|
|
|
240
242
|
return 'fix the manifest';
|
|
241
243
|
case 'rc-secret-export':
|
|
242
244
|
return 'agents secrets add';
|
|
245
|
+
case 'env-secret-export':
|
|
246
|
+
// Not just "restart the shell": the value is in every long-lived parent
|
|
247
|
+
// that inherited it — an editor, a tmux server, the agents daemon — and
|
|
248
|
+
// each keeps handing it to new children until IT restarts.
|
|
249
|
+
return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
|
|
243
250
|
case 'exec-policy':
|
|
244
251
|
return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
|
|
245
252
|
case 'stale-cli':
|
|
@@ -450,6 +457,10 @@ export function buildLocalFindings(input) {
|
|
|
450
457
|
// into `agents secrets`.
|
|
451
458
|
for (const f of rcSecretFindings(device, input.rcSecrets ?? []))
|
|
452
459
|
out.push(f);
|
|
460
|
+
// The same key, live in this process's environment rather than in a file.
|
|
461
|
+
const envFinding = envSecretFinding(device, input.masterPassphraseInEnv ?? false);
|
|
462
|
+
if (envFinding)
|
|
463
|
+
out.push(envFinding);
|
|
453
464
|
// Windows execution policy blocking the generated agents.ps1 launcher.
|
|
454
465
|
const policyFinding = execPolicyFinding(device, input.execPolicy);
|
|
455
466
|
if (policyFinding)
|
|
@@ -572,6 +583,32 @@ function rcSecretFindings(device, rc) {
|
|
|
572
583
|
* policy itself. Returns null off Windows or on a permissive policy — pure, so
|
|
573
584
|
* both branches are testable without invoking PowerShell.
|
|
574
585
|
*/
|
|
586
|
+
/**
|
|
587
|
+
* The file-store master key sitting in THIS process's environment.
|
|
588
|
+
*
|
|
589
|
+
* `rc-hygiene.ts` scans FILES, which leaves a real hole: a value inherited by a
|
|
590
|
+
* long-lived process outlives the rc line that set it, so an operator who
|
|
591
|
+
* deletes `~/.zshenv:8` gets a clean `rc-secret-export` while every shell,
|
|
592
|
+
* editor and agent started before the edit still carries the key — and hands it
|
|
593
|
+
* to everything they spawn. Verified on a box with no rc export whose live
|
|
594
|
+
* environment still held the value, hashing identical to its key file.
|
|
595
|
+
*
|
|
596
|
+
* Warning, not critical: on the release home base the export is deliberate and
|
|
597
|
+
* scoped (`headless-sign-context.sh`), so the message names that exception
|
|
598
|
+
* rather than the check trying to detect it.
|
|
599
|
+
*/
|
|
600
|
+
function envSecretFinding(device, present) {
|
|
601
|
+
if (!present)
|
|
602
|
+
return null;
|
|
603
|
+
return finding({
|
|
604
|
+
severity: FINDING_SEVERITY['env-secret-export'], kind: 'env-secret-export', device,
|
|
605
|
+
// Never the value — only that it is set.
|
|
606
|
+
message: 'AGENTS_SECRETS_PASSPHRASE is set in this process environment — every '
|
|
607
|
+
+ 'child inherits it and any same-user process can read it from /proc/<pid>/environ. '
|
|
608
|
+
+ 'It outlives the shell rc line that set it, so deleting that line is not enough. '
|
|
609
|
+
+ '(Expected inside a release sign context, which sets it deliberately.)',
|
|
610
|
+
});
|
|
611
|
+
}
|
|
575
612
|
function execPolicyFinding(device, execPolicy) {
|
|
576
613
|
if (!execPolicy || execPolicy.platform !== 'win32')
|
|
577
614
|
return null;
|
|
@@ -881,6 +918,8 @@ function warningSubject(f) {
|
|
|
881
918
|
return 'orphans';
|
|
882
919
|
if (f.kind === 'rc-secret-export')
|
|
883
920
|
return 'shell rc';
|
|
921
|
+
if (f.kind === 'env-secret-export')
|
|
922
|
+
return 'environment';
|
|
884
923
|
if (f.kind === 'exec-policy')
|
|
885
924
|
return 'PowerShell';
|
|
886
925
|
if (f.kind === 'fleet-resource-gap')
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { AgentId } from './types.js';
|
|
23
23
|
import type { DiscoveredPlugin } from './types.js';
|
|
24
24
|
import { type HookWiringReport } from './hooks.js';
|
|
25
|
+
import { type ResourceInventory } from './resource-inventory.js';
|
|
25
26
|
export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
|
|
26
27
|
export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
|
|
27
28
|
export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
|
|
@@ -75,6 +76,8 @@ export interface VersionResourceReport {
|
|
|
75
76
|
/** Wiring inspection for the version's native settings.json (claude/droid).
|
|
76
77
|
* Populated only when the `hooks` kind is in scope. */
|
|
77
78
|
hookWiring?: HookWiringReport;
|
|
79
|
+
/** Harness-scoped hook state from the shared inventory engine. */
|
|
80
|
+
hookInventory?: ResourceInventory;
|
|
78
81
|
/** Source layers behind their upstream. Filled by the doctor command (a git
|
|
79
82
|
* probe), not the pure diff — undefined when uncomputed. */
|
|
80
83
|
sourceBehind?: SourceLayerBehind[];
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -33,7 +33,8 @@ import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } fr
|
|
|
33
33
|
import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
|
|
34
34
|
import { supports } from './capabilities.js';
|
|
35
35
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
|
|
36
|
-
import {
|
|
36
|
+
import { listHookEntriesFromDir } from './hooks.js';
|
|
37
|
+
import { getResourceInventory } from './resource-inventory.js';
|
|
37
38
|
const RULES_DOC_FILENAME = 'README.md';
|
|
38
39
|
const ALL_KINDS = [
|
|
39
40
|
'commands',
|
|
@@ -300,11 +301,10 @@ function diffSkills(agent, version, cwd, excludeProject = false) {
|
|
|
300
301
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
301
302
|
}
|
|
302
303
|
// ─── hooks ────────────────────────────────────────────────────────────────────
|
|
303
|
-
function diffHooks(agent, version, cwd) {
|
|
304
|
+
function diffHooks(agent, version, cwd, inventory) {
|
|
304
305
|
if (!AGENTS[agent].supportsHooks)
|
|
305
306
|
return [];
|
|
306
|
-
const
|
|
307
|
-
const installedByName = new Map(installedEntries.map((e) => [e.name, e]));
|
|
307
|
+
const installedByName = new Map(inventory.onDisk.map((e) => [e.name, e]));
|
|
308
308
|
// Sync intentionally excludes project/.agents/hooks/ — mirror that.
|
|
309
309
|
const layerBases = buildLayerBases(cwd, 'hooks', { excludeProject: true });
|
|
310
310
|
// Group source files the same way the hook installer does (basename across
|
|
@@ -329,14 +329,14 @@ function diffHooks(agent, version, cwd) {
|
|
|
329
329
|
continue;
|
|
330
330
|
}
|
|
331
331
|
const a = readSafe(src.entry.scriptPath);
|
|
332
|
-
const b = readSafe(installed.
|
|
332
|
+
const b = readSafe(installed.path);
|
|
333
333
|
let matches = a != null && b != null && normalize(a) === normalize(b);
|
|
334
|
-
if (matches && src.entry.dataFile && installed.
|
|
334
|
+
if (matches && src.entry.dataFile && installed.detail) {
|
|
335
335
|
const ad = readSafe(src.entry.dataFile);
|
|
336
|
-
const bd = readSafe(installed.
|
|
336
|
+
const bd = readSafe(installed.detail);
|
|
337
337
|
matches = ad != null && bd != null && normalize(ad) === normalize(bd);
|
|
338
338
|
}
|
|
339
|
-
else if (matches && (!!src.entry.dataFile !== !!installed.
|
|
339
|
+
else if (matches && (!!src.entry.dataFile !== !!installed.detail)) {
|
|
340
340
|
matches = false;
|
|
341
341
|
}
|
|
342
342
|
rows.push({
|
|
@@ -345,13 +345,13 @@ function diffHooks(agent, version, cwd) {
|
|
|
345
345
|
status: matches ? 'ok' : 'diff',
|
|
346
346
|
source: src.layer,
|
|
347
347
|
sourcePath: src.entry.scriptPath,
|
|
348
|
-
homePath: installed.
|
|
348
|
+
homePath: installed.path,
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
351
|
for (const [name, installed] of installedByName) {
|
|
352
352
|
if (seen.has(name))
|
|
353
353
|
continue;
|
|
354
|
-
rows.push({ kind: 'hooks', name, status: 'extra', homePath: installed.
|
|
354
|
+
rows.push({ kind: 'hooks', name, status: 'extra', homePath: installed.path });
|
|
355
355
|
}
|
|
356
356
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
357
357
|
}
|
|
@@ -609,12 +609,13 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
609
609
|
empty.commands = diffCommands(agent, version, cwd, excludeProject);
|
|
610
610
|
if (requested.has('skills'))
|
|
611
611
|
empty.skills = diffSkills(agent, version, cwd, excludeProject);
|
|
612
|
-
|
|
613
|
-
|
|
612
|
+
const hookInventory = requested.has('hooks') ? getResourceInventory(agent, version, 'hooks', { cwd }) : undefined;
|
|
613
|
+
if (hookInventory)
|
|
614
|
+
empty.hooks = diffHooks(agent, version, cwd, hookInventory);
|
|
614
615
|
// Wiring check: a hook FILE can be present and byte-identical to source (ok
|
|
615
616
|
// above) yet never referenced in settings.json, so it never fires. Only
|
|
616
617
|
// meaningful when hooks are in scope.
|
|
617
|
-
const hookWiring =
|
|
618
|
+
const hookWiring = hookInventory?.wiring;
|
|
618
619
|
if (requested.has('rules'))
|
|
619
620
|
empty.rules = diffRules(agent, version, cwd, excludeProject);
|
|
620
621
|
if (requested.has('mcp'))
|
|
@@ -654,6 +655,7 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
654
655
|
kinds: empty,
|
|
655
656
|
summary: { ok, diff, missing, extra },
|
|
656
657
|
...(hookWiring ? { hookWiring } : {}),
|
|
658
|
+
...(hookInventory ? { hookInventory } : {}),
|
|
657
659
|
};
|
|
658
660
|
}
|
|
659
661
|
export const DOCTOR_ALL_KINDS = ALL_KINDS;
|
|
@@ -36,6 +36,8 @@ export interface UnifiedQuery {
|
|
|
36
36
|
* Read a unified, newest-first event stream. Operational events come from
|
|
37
37
|
* events.ts `query()`; agent-semantic events from the activity logs, normalized
|
|
38
38
|
* to the same record shape and filtered identically. `limit` caps the merged
|
|
39
|
-
* result (each source is fetched up to `limit
|
|
39
|
+
* result (each source is fetched up to `limit` *after* its primary filters —
|
|
40
|
+
* eventTypes for activity, eventTypes/module/bundle for ops — so the top-N is
|
|
41
|
+
* exact for those filters).
|
|
40
42
|
*/
|
|
41
43
|
export declare function readUnifiedEvents(q?: UnifiedQuery): EventRecord[];
|
package/dist/lib/event-stream.js
CHANGED
|
@@ -46,7 +46,9 @@ function matches(r, q) {
|
|
|
46
46
|
* Read a unified, newest-first event stream. Operational events come from
|
|
47
47
|
* events.ts `query()`; agent-semantic events from the activity logs, normalized
|
|
48
48
|
* to the same record shape and filtered identically. `limit` caps the merged
|
|
49
|
-
* result (each source is fetched up to `limit
|
|
49
|
+
* result (each source is fetched up to `limit` *after* its primary filters —
|
|
50
|
+
* eventTypes for activity, eventTypes/module/bundle for ops — so the top-N is
|
|
51
|
+
* exact for those filters).
|
|
50
52
|
*/
|
|
51
53
|
export function readUnifiedEvents(q = {}) {
|
|
52
54
|
// `bundle` is filtered inside query()'s scan (before its limit cutoff) so a
|
|
@@ -66,10 +68,21 @@ export function readUnifiedEvents(q = {}) {
|
|
|
66
68
|
});
|
|
67
69
|
if (q.includeActivity === false)
|
|
68
70
|
return ops;
|
|
71
|
+
// Activity events always stamp module: 'activity'. A non-activity module
|
|
72
|
+
// filter can never match them — skip the activity scan entirely.
|
|
73
|
+
if (q.module != null && q.module !== 'activity')
|
|
74
|
+
return ops;
|
|
75
|
+
// Push eventTypes into the activity reader so `limit` is applied AFTER the
|
|
76
|
+
// event-type filter (readRecentActivity already does this for `events`).
|
|
77
|
+
// Without this, a rare match older than the newest-`limit` window of routine
|
|
78
|
+
// churn is silently dropped — the same class of bug as the ops-side bundle
|
|
79
|
+
// pre-filter above (RUSH-2093). Remaining filters (agent, sessionId, …) still
|
|
80
|
+
// run via matches() for fields activity.ts does not pre-filter.
|
|
69
81
|
const acts = readActivityAsEventRecords({
|
|
70
82
|
sinceMs: q.startDate?.getTime(),
|
|
71
83
|
limit: q.limit,
|
|
72
84
|
root: q.activityRoot,
|
|
85
|
+
events: q.eventTypes,
|
|
73
86
|
}).filter((r) => matches(r, q));
|
|
74
87
|
const merged = [...ops, ...acts].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
75
88
|
return typeof q.limit === 'number' ? merged.slice(0, q.limit) : merged;
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -226,6 +226,15 @@ export declare function query(options: {
|
|
|
226
226
|
bundle?: string;
|
|
227
227
|
limit?: number;
|
|
228
228
|
}): EventRecord[];
|
|
229
|
+
/**
|
|
230
|
+
* Scan event logs once for a set of session IDs and return browser/computer
|
|
231
|
+
* usage flags for each. O(files) instead of O(N × files) — safe to call
|
|
232
|
+
* outside a SQLite write transaction.
|
|
233
|
+
*/
|
|
234
|
+
export declare function queryToolUsageForSessions(sessionIds: ReadonlySet<string>): Map<string, {
|
|
235
|
+
usedBrowser: boolean;
|
|
236
|
+
usedComputer: boolean;
|
|
237
|
+
}>;
|
|
229
238
|
/**
|
|
230
239
|
* Get performance stats for a specific label.
|
|
231
240
|
*/
|
package/dist/lib/events.js
CHANGED
|
@@ -1052,6 +1052,64 @@ export function query(options) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
return results;
|
|
1054
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Scan event logs once for a set of session IDs and return browser/computer
|
|
1057
|
+
* usage flags for each. O(files) instead of O(N × files) — safe to call
|
|
1058
|
+
* outside a SQLite write transaction.
|
|
1059
|
+
*/
|
|
1060
|
+
export function queryToolUsageForSessions(sessionIds) {
|
|
1061
|
+
const result = new Map();
|
|
1062
|
+
if (sessionIds.size === 0)
|
|
1063
|
+
return result;
|
|
1064
|
+
for (const id of sessionIds) {
|
|
1065
|
+
result.set(id, { usedBrowser: false, usedComputer: false });
|
|
1066
|
+
}
|
|
1067
|
+
const BROWSER_EVENTS = new Set(['browser.navigate', 'browser.screenshot']);
|
|
1068
|
+
const COMPUTER_EVENTS = new Set(['computer.action']);
|
|
1069
|
+
eventsPath();
|
|
1070
|
+
migrateLegacyEventLogs();
|
|
1071
|
+
const files = listEventLogFiles().sort((a, b) => Number(b.currentActive) - Number(a.currentActive) || b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
|
|
1072
|
+
let remaining = sessionIds.size * 2; // each session needs up to 2 flags set
|
|
1073
|
+
for (const file of files) {
|
|
1074
|
+
if (remaining <= 0)
|
|
1075
|
+
break;
|
|
1076
|
+
let content;
|
|
1077
|
+
if (file.gzip) {
|
|
1078
|
+
try {
|
|
1079
|
+
content = gunzipSync(fs.readFileSync(file.path)).toString('utf-8');
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
content = fs.readFileSync(file.path, 'utf-8');
|
|
1087
|
+
}
|
|
1088
|
+
for (const line of content.trim().split('\n')) {
|
|
1089
|
+
if (!line)
|
|
1090
|
+
continue;
|
|
1091
|
+
try {
|
|
1092
|
+
const record = JSON.parse(line);
|
|
1093
|
+
const sid = record.sessionId;
|
|
1094
|
+
if (!sid || !result.has(sid))
|
|
1095
|
+
continue;
|
|
1096
|
+
const entry = result.get(sid);
|
|
1097
|
+
if (BROWSER_EVENTS.has(record.event) && !entry.usedBrowser) {
|
|
1098
|
+
entry.usedBrowser = true;
|
|
1099
|
+
remaining--;
|
|
1100
|
+
}
|
|
1101
|
+
else if (COMPUTER_EVENTS.has(record.event) && !entry.usedComputer) {
|
|
1102
|
+
entry.usedComputer = true;
|
|
1103
|
+
remaining--;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
// skip malformed lines
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1055
1113
|
// ─── Stats ────────────────────────────────────────────────────────────────────
|
|
1056
1114
|
/**
|
|
1057
1115
|
* Get performance stats for a specific label.
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function headlessPlanStallCommand(args: {
|
|
|
42
42
|
* (every agent supports edit-like behavior as its default).
|
|
43
43
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
44
44
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
45
|
-
* like antigravity/
|
|
45
|
+
* like antigravity/kiro have no plan flag; hard-failing made
|
|
46
46
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
47
47
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
48
48
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -61,8 +61,8 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
|
61
61
|
* `--prompt` + `--plan`, and grok's `--permission-mode plan` silently stalls at
|
|
62
62
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
63
63
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
64
|
-
* one-line stderr warning, mirroring the graceful plan→edit degrade
|
|
65
|
-
*
|
|
64
|
+
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
65
|
+
* and kiro get for having no plan flag at all. Interactive runs are never
|
|
66
66
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
67
67
|
* (agents run / teams) and the routine runner.
|
|
68
68
|
*/
|
|
@@ -389,7 +389,19 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
|
|
389
389
|
*/
|
|
390
390
|
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
|
|
391
391
|
redactEnvValues?: boolean;
|
|
392
|
+
envFile?: string;
|
|
392
393
|
}): string;
|
|
394
|
+
/**
|
|
395
|
+
* Serialize the pane env to a shell-sourceable file, created 0600 and exclusively
|
|
396
|
+
* (`wx`) so it can never adopt a pre-existing file's mode or content.
|
|
397
|
+
*
|
|
398
|
+
* EVERY key goes in the file, not a curated "secret-bearing" subset: a denylist
|
|
399
|
+
* has to be updated for each new credential and is wrong the moment someone
|
|
400
|
+
* forgets, whereas routing all of it through the file makes the guarantee hold by
|
|
401
|
+
* construction. Keys are filtered to valid shell identifiers for the same reason
|
|
402
|
+
* `env` needed it — an exported function (`BASH_FUNC_x%%`) is not assignable.
|
|
403
|
+
*/
|
|
404
|
+
export declare function writeTmuxEnvFile(env: NodeJS.ProcessEnv, filePath: string): void;
|
|
393
405
|
/**
|
|
394
406
|
* Trim a raw `tmux capture-pane` dump to its last `maxLines` non-empty lines
|
|
395
407
|
* (right-stripping each). Used by runInTmux to recap a fast-failed agent's
|