@phnx-labs/agents-cli 1.22.24 → 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 +167 -1
- package/README.md +14 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +100 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/menubar.js +8 -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/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +24 -2
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/teams.js +3 -2
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +36 -1
- 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/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- 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/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/mcp.js +5 -1
- 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/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/runner.js +5 -5
- 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/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- 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.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +15 -0
- package/dist/lib/startup/command-registry.js +42 -0
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -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
|
@@ -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')
|
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
|
*/
|
package/dist/lib/exec.js
CHANGED
|
@@ -86,7 +86,7 @@ export function headlessPlanStallCommand(args) {
|
|
|
86
86
|
* (every agent supports edit-like behavior as its default).
|
|
87
87
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
88
88
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
89
|
-
* like antigravity/
|
|
89
|
+
* like antigravity/kiro have no plan flag; hard-failing made
|
|
90
90
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
91
91
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
92
92
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -106,7 +106,7 @@ export function resolveMode(agent, requested) {
|
|
|
106
106
|
}
|
|
107
107
|
if (requested === 'plan') {
|
|
108
108
|
// No read-only mode on this agent. modes[0] is the declared safest mode
|
|
109
|
-
// (edit for antigravity/
|
|
109
|
+
// (edit for antigravity/kiro/…). Prefer that over hard-fail so
|
|
110
110
|
// uniform multi-agent `--mode plan` dispatches still run.
|
|
111
111
|
return supported[0];
|
|
112
112
|
}
|
|
@@ -121,8 +121,8 @@ export function resolveMode(agent, requested) {
|
|
|
121
121
|
* `--prompt` + `--plan`, and grok's `--permission-mode plan` silently stalls at
|
|
122
122
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
123
123
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
124
|
-
* one-line stderr warning, mirroring the graceful plan→edit degrade
|
|
125
|
-
*
|
|
124
|
+
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
125
|
+
* and kiro get for having no plan flag at all. Interactive runs are never
|
|
126
126
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
127
127
|
* (agents run / teams) and the routine runner.
|
|
128
128
|
*/
|
|
@@ -142,12 +142,8 @@ export function resolveHeadlessMode(agent, requested, interactive, warningContex
|
|
|
142
142
|
if (mode !== requested) {
|
|
143
143
|
const subject = warningContext ? `${warningContext}: ` : '';
|
|
144
144
|
if (requested === 'plan') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: `${agent} has no read-only 'plan' mode`;
|
|
148
|
-
warn(`[agents] ${subject}${limitation}; ` +
|
|
149
|
-
`running '${mode}' (writable) instead${agent === 'cursor' ? ' (RUSH-2101)' : ''}. ` +
|
|
150
|
-
`Pass --mode ${mode} to silence this.\n`);
|
|
145
|
+
warn(`[agents] ${subject}${agent} has no read-only 'plan' mode; ` +
|
|
146
|
+
`running '${mode}' (writable) instead. Pass --mode ${mode} to silence this.\n`);
|
|
151
147
|
}
|
|
152
148
|
else {
|
|
153
149
|
warn(`[agents] ${subject}${agent} has no '${requested}' mode; using '${mode}'.\n`);
|
|
@@ -494,6 +490,7 @@ export const AGENT_COMMANDS = {
|
|
|
494
490
|
base: ['cursor-agent'],
|
|
495
491
|
promptFlag: '-p',
|
|
496
492
|
modeFlags: {
|
|
493
|
+
plan: ['--plan'],
|
|
497
494
|
edit: [],
|
|
498
495
|
skip: ['-f'],
|
|
499
496
|
},
|
|
@@ -687,6 +684,23 @@ export const AGENT_COMMANDS = {
|
|
|
687
684
|
// `resume` subcommand — special-cased in buildExecCommand.
|
|
688
685
|
resume: { flag: '--session-id' },
|
|
689
686
|
},
|
|
687
|
+
// Warp Agent CLI (`oz agent run`). Headless is `-p/--prompt "<task>"`; JSON is
|
|
688
|
+
// the global `--output-format json`. Autonomy comes from the selected agent
|
|
689
|
+
// profile (`--profile`), not a per-run permission flag, so the single `edit`
|
|
690
|
+
// mode maps to no flags (mirrors hermes). `--model` overrides the base model
|
|
691
|
+
// (`oz model list`). No `resume`: Oz's `oz agent run --conversation <id>`
|
|
692
|
+
// continues a SERVER-SIDE conversation by id, and warp is not session-tracked
|
|
693
|
+
// (absent from SESSION_AGENTS), so agents-cli has no local id to resume from —
|
|
694
|
+
// declaring it would make nativeResume(warp) true against an unreachable path.
|
|
695
|
+
warp: {
|
|
696
|
+
base: ['oz', 'agent', 'run'],
|
|
697
|
+
promptFlag: '-p',
|
|
698
|
+
modeFlags: {
|
|
699
|
+
edit: [],
|
|
700
|
+
},
|
|
701
|
+
jsonFlags: ['--output-format', 'json'],
|
|
702
|
+
modelFlag: '--model',
|
|
703
|
+
},
|
|
690
704
|
};
|
|
691
705
|
/**
|
|
692
706
|
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
@@ -89,6 +89,9 @@ export interface SessionOutcomeHint {
|
|
|
89
89
|
worktreeSlug?: string | null;
|
|
90
90
|
branch?: string | null;
|
|
91
91
|
project?: string | null;
|
|
92
|
+
host?: string | null;
|
|
93
|
+
origin?: 'cli' | 'routine' | null;
|
|
94
|
+
routineName?: string | null;
|
|
92
95
|
}
|
|
93
96
|
/**
|
|
94
97
|
* Overlay session meta onto a block when the block itself is missing ticket/PR/
|
package/dist/lib/feed-outcome.js
CHANGED
|
@@ -152,21 +152,22 @@ export function groupBlocksByOutcome(blocks) {
|
|
|
152
152
|
const groups = [];
|
|
153
153
|
for (const { outcome, blocks: members } of byKey.values()) {
|
|
154
154
|
const mailboxes = new Set(members.map((b) => b.mailboxId));
|
|
155
|
-
|
|
156
|
-
let answered = 0;
|
|
157
|
-
let parked = 0;
|
|
155
|
+
const states = new Map();
|
|
158
156
|
for (const b of members) {
|
|
159
|
-
|
|
160
|
-
parked
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
const next = b.parkedAt
|
|
158
|
+
? 'parked'
|
|
159
|
+
: b.answer || b.defaultedAt || b.continuedAt ? 'answered' : 'open';
|
|
160
|
+
const current = states.get(b.mailboxId);
|
|
161
|
+
// One agent occupies one state. Its most actionable block wins.
|
|
162
|
+
if (!current || next === 'open' || (next === 'parked' && current === 'answered')) {
|
|
163
|
+
states.set(b.mailboxId, next);
|
|
164
|
+
}
|
|
165
165
|
}
|
|
166
|
+
const count = (state) => [...states.values()].filter((value) => value === state).length;
|
|
166
167
|
groups.push({
|
|
167
168
|
outcome,
|
|
168
169
|
blocks: members,
|
|
169
|
-
counts: { agents: mailboxes.size, open, answered, parked },
|
|
170
|
+
counts: { agents: mailboxes.size, open: count('open'), answered: count('answered'), parked: count('parked') },
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
173
|
groups.sort((a, b) => {
|
|
@@ -222,6 +223,13 @@ export function enrichBlockFromSession(block, hint) {
|
|
|
222
223
|
next.worktreeSlug = hint.worktreeSlug;
|
|
223
224
|
if (!next.project && hint.project)
|
|
224
225
|
next.project = hint.project;
|
|
226
|
+
if (hint.host && hint.host !== 'terminal' && next.runtime === 'terminal')
|
|
227
|
+
next.runtime = hint.host;
|
|
228
|
+
if (hint.origin === 'routine') {
|
|
229
|
+
next.origin = 'routine';
|
|
230
|
+
if (hint.routineName)
|
|
231
|
+
next.routineName = hint.routineName;
|
|
232
|
+
}
|
|
225
233
|
return next;
|
|
226
234
|
}
|
|
227
235
|
/**
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface OpenBlock {
|
|
|
36
36
|
mailboxId: string;
|
|
37
37
|
host: string;
|
|
38
38
|
runtime: string;
|
|
39
|
+
/** Indexed launch origin, added at read time when the live session is known. */
|
|
40
|
+
origin?: 'cli' | 'routine';
|
|
41
|
+
/** Routine definition name when origin is `routine`. */
|
|
42
|
+
routineName?: string;
|
|
39
43
|
/** Project/repo name this block belongs to (derived from cwd, worktree-aware). */
|
|
40
44
|
project?: string;
|
|
41
45
|
ts: string;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* table or, when `--host`/`--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
|
+
import { type Host } from './types.js';
|
|
19
20
|
import { type DeviceProfile, type DeviceRegistry } from '../devices/registry.js';
|
|
20
21
|
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
21
22
|
/** Per-command remote behaviour. Absence from this map = not host-routable here. */
|
|
@@ -60,4 +61,24 @@ export declare function runFleetPassthrough(command: string, allArgs: string[],
|
|
|
60
61
|
* @param allArgs `process.argv.slice(2)` — the command name followed by its args.
|
|
61
62
|
*/
|
|
62
63
|
export declare function maybeRunOnHost(command: string, allArgs: string[], opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Run `agents <forwardedArgs>` on `host` over SSH, streaming its output, and
|
|
66
|
+
* return the exit code. The single place the SSH hop is built, so every remote
|
|
67
|
+
* `agents` invocation carries identical env semantics.
|
|
68
|
+
*
|
|
69
|
+
* Forwards actor provenance (`AGENTS_ACTOR`/`GIT_` vars) across the hop, merged UNDER
|
|
70
|
+
* `extraEnv` so a caller-supplied PATH still wins — without this the remote
|
|
71
|
+
* re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
72
|
+
* (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
73
|
+
* AGENTS_FLEET_REMOTE marks this as a fleet-dispatched run so the far side can
|
|
74
|
+
* gate consent-sensitive actions — the browser consent gate
|
|
75
|
+
* (lib/browser/remote-control.ts) reads it to allow/deny a cross-machine drive.
|
|
76
|
+
*/
|
|
77
|
+
export declare function streamAgentsOnHost(host: Host, forwardedArgs: string[], opts?: {
|
|
78
|
+
remoteCwd?: string;
|
|
79
|
+
interactive?: boolean;
|
|
80
|
+
extraEnv?: Record<string, string>;
|
|
81
|
+
remoteOs?: string;
|
|
82
|
+
target?: string;
|
|
83
|
+
}): number;
|
|
63
84
|
export {};
|
|
@@ -29,6 +29,7 @@ import { loadDevices } from '../devices/registry.js';
|
|
|
29
29
|
import { isSelfHost } from '../devices/self-host.js';
|
|
30
30
|
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
31
31
|
import { platformGroupLabel } from '../devices/health-report.js';
|
|
32
|
+
import { isKnownTopLevelCommand } from '../startup/command-registry.js';
|
|
32
33
|
/**
|
|
33
34
|
* First-class groups that run transparently on a remote via SSH when
|
|
34
35
|
* `--host`/`--device` is present. Keep both canonical names and aliases
|
|
@@ -436,6 +437,15 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
436
437
|
if (!isAll && command !== 'routines')
|
|
437
438
|
return false;
|
|
438
439
|
}
|
|
440
|
+
// A command that does not exist is an unknown-command error, not a routing
|
|
441
|
+
// error. The router runs BEFORE commander parses, so without this gate a typo
|
|
442
|
+
// (`agents session resume --host box`) was answered with "does not support
|
|
443
|
+
// --host/--device" — a true statement about a command the user never typed,
|
|
444
|
+
// and the exact opposite of the truth for the `sessions` they meant, which
|
|
445
|
+
// does support it. Fall through so commander reports `unknown command` (and
|
|
446
|
+
// its did-you-mean). RUSH-2022.
|
|
447
|
+
if (!isKnownTopLevelCommand(command))
|
|
448
|
+
return false;
|
|
439
449
|
const spec = REMOTE_PASSTHROUGH[command];
|
|
440
450
|
if (!spec) {
|
|
441
451
|
// Flag was accepted (no raw commander "unknown option") but this group has
|
|
@@ -532,20 +542,37 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
532
542
|
const doctorPath = isDoctorCommand && !/^win/i.test((remoteOs ?? '').trim())
|
|
533
543
|
? { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' }
|
|
534
544
|
: undefined;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
+
process.exitCode = streamAgentsOnHost(host, forwarded, {
|
|
546
|
+
remoteCwd,
|
|
547
|
+
interactive,
|
|
548
|
+
extraEnv: doctorPath,
|
|
549
|
+
remoteOs,
|
|
550
|
+
target,
|
|
551
|
+
});
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Run `agents <forwardedArgs>` on `host` over SSH, streaming its output, and
|
|
556
|
+
* return the exit code. The single place the SSH hop is built, so every remote
|
|
557
|
+
* `agents` invocation carries identical env semantics.
|
|
558
|
+
*
|
|
559
|
+
* Forwards actor provenance (`AGENTS_ACTOR`/`GIT_` vars) across the hop, merged UNDER
|
|
560
|
+
* `extraEnv` so a caller-supplied PATH still wins — without this the remote
|
|
561
|
+
* re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
562
|
+
* (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
563
|
+
* AGENTS_FLEET_REMOTE marks this as a fleet-dispatched run so the far side can
|
|
564
|
+
* gate consent-sensitive actions — the browser consent gate
|
|
565
|
+
* (lib/browser/remote-control.ts) reads it to allow/deny a cross-machine drive.
|
|
566
|
+
*/
|
|
567
|
+
export function streamAgentsOnHost(host, forwardedArgs, opts = {}) {
|
|
568
|
+
const target = opts.target ?? sshTargetFor(host);
|
|
569
|
+
const remoteOs = opts.remoteOs ?? resolveRemoteOsSync(host.name);
|
|
570
|
+
const env = withActorEnv({ ...opts.extraEnv, AGENTS_FLEET_REMOTE: '1' });
|
|
571
|
+
const remoteCmd = buildRemoteAgentsInvocation(forwardedArgs, opts.remoteCwd, remoteOs, env);
|
|
572
|
+
const code = sshStream(target, remoteCmd, { tty: !!opts.interactive, multiplex: true });
|
|
545
573
|
if (code === 255) {
|
|
546
574
|
console.error(chalk.red(`${host.name}: unreachable over SSH (asleep, offline, or host key changed?).`) +
|
|
547
575
|
chalk.gray(' Check: agents hosts check ' + host.name));
|
|
548
576
|
}
|
|
549
|
-
|
|
550
|
-
return true;
|
|
577
|
+
return code;
|
|
551
578
|
}
|
package/dist/lib/mcp.js
CHANGED
|
@@ -596,6 +596,7 @@ function writeMcpConfigSupportsAgent(agentId) {
|
|
|
596
596
|
case 'hermes':
|
|
597
597
|
case 'pi':
|
|
598
598
|
case 'muse':
|
|
599
|
+
case 'warp':
|
|
599
600
|
return true;
|
|
600
601
|
default:
|
|
601
602
|
return false;
|
|
@@ -621,7 +622,10 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
|
|
|
621
622
|
// omp reads the same Claude `{ "mcpServers": {...} }` schema from .mcp.json
|
|
622
623
|
// (stdio: command/args/env; http/sse: url/headers — transport inferred from
|
|
623
624
|
// command/url presence).
|
|
624
|
-
case 'pi':
|
|
625
|
+
case 'pi':
|
|
626
|
+
// Oz reads the same Claude `{ "mcpServers": {...} }` schema from
|
|
627
|
+
// `.warp/.mcp.json` (stdio + http with headers).
|
|
628
|
+
case 'warp': {
|
|
625
629
|
let config = {};
|
|
626
630
|
if (fs.existsSync(configPath)) {
|
|
627
631
|
try {
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WatchdogTickResult } from '../watchdog/runner.js';
|
|
2
|
+
export interface MenubarSnapshot {
|
|
3
|
+
version: 1;
|
|
4
|
+
capturedAt: string;
|
|
5
|
+
routines: Record<string, unknown>[];
|
|
6
|
+
recentSessions: Record<string, unknown>[];
|
|
7
|
+
activeSessions: Record<string, unknown>[];
|
|
8
|
+
watchdog: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
lastTick: Pick<WatchdogTickResult, 'didNudge' | 'counts'> | null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function readLastWatchdogTick(stateDir?: string): WatchdogTickResult | null;
|
|
14
|
+
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
15
|
+
export declare function computeMenubarSnapshot(): Promise<MenubarSnapshot>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { buildRoutineListJson } from '../../commands/routines.js';
|
|
4
|
+
import { backfillActiveRowsFromIndex, serializeActiveSessionsForJson, serializeSessionsJson } from '../../commands/sessions.js';
|
|
5
|
+
import { getConfigValue } from '../device-config.js';
|
|
6
|
+
import { querySessions } from '../session/db.js';
|
|
7
|
+
import { readActiveSessionsCache } from '../session/session-cache.js';
|
|
8
|
+
import { getRuntimeStateDir } from '../state.js';
|
|
9
|
+
export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
18
|
+
export async function computeMenubarSnapshot() {
|
|
19
|
+
const [routines, recent] = await Promise.all([
|
|
20
|
+
Promise.resolve(buildRoutineListJson()),
|
|
21
|
+
Promise.resolve(querySessions({ limit: 40, skipExistenceCheck: true })),
|
|
22
|
+
]);
|
|
23
|
+
const active = readActiveSessionsCache('local');
|
|
24
|
+
const activeSessions = active?.sessions ?? [];
|
|
25
|
+
backfillActiveRowsFromIndex(activeSessions);
|
|
26
|
+
return {
|
|
27
|
+
version: 1,
|
|
28
|
+
capturedAt: new Date().toISOString(),
|
|
29
|
+
routines,
|
|
30
|
+
recentSessions: JSON.parse(serializeSessionsJson(recent)),
|
|
31
|
+
activeSessions: serializeActiveSessionsForJson(activeSessions),
|
|
32
|
+
watchdog: {
|
|
33
|
+
enabled: getConfigValue('watchdog.enabled').value === true,
|
|
34
|
+
lastTick: (() => {
|
|
35
|
+
const tick = readLastWatchdogTick();
|
|
36
|
+
return tick ? { didNudge: tick.didNudge, counts: tick.counts } : null;
|
|
37
|
+
})(),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/lib/plugins.js
CHANGED
|
@@ -68,8 +68,20 @@ export function discoverPluginsInDir(pluginsDir, spec = { kind: 'user' }) {
|
|
|
68
68
|
continue;
|
|
69
69
|
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
70
70
|
const manifest = loadPluginManifest(pluginRoot);
|
|
71
|
-
if (!manifest)
|
|
71
|
+
if (!manifest) {
|
|
72
|
+
// A directory that looks like a plugin root but has no valid manifest is
|
|
73
|
+
// silently invisible to every downstream command (list/info/sync, and the
|
|
74
|
+
// materialize-into-version-homes copy) with no other diagnostic anywhere
|
|
75
|
+
// in the chain — it can sit here indefinitely, on the correct git commit,
|
|
76
|
+
// and never surface as broken (RUSH-2270: the `work` plugin shipped
|
|
77
|
+
// without .claude-plugin/plugin.json and nothing noticed for a full merge
|
|
78
|
+
// cycle). Warn once here, at the one place that decides discoverability,
|
|
79
|
+
// so the gap is visible the moment `agents sync` runs.
|
|
80
|
+
const manifestPath = path.join(entry.name, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
81
|
+
process.stderr.write(`agents-cli: '${entry.name}' in ${pluginsDir} has no valid ${manifestPath} ` +
|
|
82
|
+
`(missing, malformed JSON, or missing name/version) — skipped, not discovered as a plugin.\n`);
|
|
72
83
|
continue;
|
|
84
|
+
}
|
|
73
85
|
plugins.push(buildDiscoveredPlugin(pluginRoot, manifest, spec));
|
|
74
86
|
}
|
|
75
87
|
return plugins;
|
|
@@ -129,6 +129,9 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
129
129
|
return path.join(versionHome, '.omp', 'agent', '.mcp.json');
|
|
130
130
|
case 'muse':
|
|
131
131
|
return path.join(versionHome, '.config', 'muse', 'settings.json');
|
|
132
|
+
case 'warp':
|
|
133
|
+
// Oz reads user-scope MCP from ~/.warp/.mcp.json (Claude schema).
|
|
134
|
+
return path.join(versionHome, '.warp', '.mcp.json');
|
|
132
135
|
default:
|
|
133
136
|
return null;
|
|
134
137
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunMeta } from './routines.js';
|
|
2
|
+
export interface RoutineProcessCleanupOptions {
|
|
3
|
+
runsDir?: string;
|
|
4
|
+
alive?: (pid: number) => boolean;
|
|
5
|
+
owns?: (meta: RunMeta) => boolean;
|
|
6
|
+
terminate?: (pid: number) => void;
|
|
7
|
+
}
|
|
8
|
+
/** Reap process groups whose durable run record is already terminal. */
|
|
9
|
+
export declare function reapTerminalRoutineProcesses(opts?: RoutineProcessCleanupOptions): number[];
|