@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -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/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- 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/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- 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 +44 -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 +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- 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 +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- 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 +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- 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 +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- 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/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
package/dist/lib/refresh.js
CHANGED
|
@@ -29,7 +29,7 @@ import { isPromptCancelled } from '../commands/utils.js';
|
|
|
29
29
|
* hook reads from a fixed path so it survives version upgrades. If the root
|
|
30
30
|
* file doesn't exist yet but an agent-scoped one does, hoist the first one found.
|
|
31
31
|
*/
|
|
32
|
-
function migratePromptcutsToRoot(agentsDir) {
|
|
32
|
+
function migratePromptcutsToRoot(agentsDir, quiet = false) {
|
|
33
33
|
const rootPath = path.join(agentsDir, 'promptcuts.yaml');
|
|
34
34
|
if (fs.existsSync(rootPath))
|
|
35
35
|
return;
|
|
@@ -39,7 +39,8 @@ function migratePromptcutsToRoot(agentsDir) {
|
|
|
39
39
|
if (fs.existsSync(legacyPath)) {
|
|
40
40
|
try {
|
|
41
41
|
fs.renameSync(legacyPath, rootPath);
|
|
42
|
-
|
|
42
|
+
if (!quiet)
|
|
43
|
+
console.log(chalk.gray(`Moved ${dir}/promptcuts.yaml → promptcuts.yaml (repo root)`));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
@@ -56,16 +57,19 @@ function migratePromptcutsToRoot(agentsDir) {
|
|
|
56
57
|
* Idempotent — safe to run repeatedly. No network operations.
|
|
57
58
|
*/
|
|
58
59
|
export async function refresh(options = {}) {
|
|
59
|
-
const { agentFilter, skipPrompts = false, skipClis = false } = options;
|
|
60
|
+
const { agentFilter, skipPrompts = false, skipClis = false, quiet = false } = options;
|
|
60
61
|
const agentsDir = getUserAgentsDir();
|
|
61
|
-
|
|
62
|
+
// Gate every human progress line so --json / fleet fan-out can parse stdout.
|
|
63
|
+
const log = (...args) => { if (!quiet)
|
|
64
|
+
console.log(...args); };
|
|
65
|
+
migratePromptcutsToRoot(agentsDir, quiet);
|
|
62
66
|
const manifest = readManifest(agentsDir);
|
|
63
67
|
if (!manifest) {
|
|
64
|
-
|
|
68
|
+
log(chalk.gray(`No ${MANIFEST_FILENAME} found`));
|
|
65
69
|
}
|
|
66
70
|
// 1. Install/upgrade CLI versions from agents.yaml
|
|
67
71
|
if (!skipClis && manifest?.agents) {
|
|
68
|
-
|
|
72
|
+
log(chalk.bold('\nCLI Versions:\n'));
|
|
69
73
|
const cliAgents = Object.keys(manifest.agents);
|
|
70
74
|
for (const agentId of cliAgents) {
|
|
71
75
|
if (agentFilter && agentId !== agentFilter)
|
|
@@ -94,7 +98,7 @@ export async function refresh(options = {}) {
|
|
|
94
98
|
}
|
|
95
99
|
// 2. Register MCP servers
|
|
96
100
|
if (manifest?.mcp && Object.keys(manifest.mcp).length > 0) {
|
|
97
|
-
|
|
101
|
+
log(chalk.bold('\nMCP Servers:\n'));
|
|
98
102
|
for (const [name, config] of Object.entries(manifest.mcp)) {
|
|
99
103
|
if (!config.command || config.transport === 'http')
|
|
100
104
|
continue;
|
|
@@ -109,7 +113,7 @@ export async function refresh(options = {}) {
|
|
|
109
113
|
const label = result.version
|
|
110
114
|
? `${agentLabel(result.agentId)}@${result.version}`
|
|
111
115
|
: agentLabel(result.agentId);
|
|
112
|
-
|
|
116
|
+
log(` ${chalk.green('+')} ${name} -> ${label}`);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -147,13 +151,13 @@ export async function refresh(options = {}) {
|
|
|
147
151
|
forceFullSync = true;
|
|
148
152
|
}
|
|
149
153
|
else if (!hasAnySynced) {
|
|
150
|
-
|
|
154
|
+
log(chalk.yellow(`\n${agentLabel(agentId)}@${defaultVer} has no synced resources.`));
|
|
151
155
|
const userSelection = await promptResourceSelection(agentId);
|
|
152
156
|
if (userSelection)
|
|
153
157
|
selection = userSelection;
|
|
154
158
|
}
|
|
155
159
|
else if (hasNewResources(newResources, agentId, defaultVer)) {
|
|
156
|
-
|
|
160
|
+
log(chalk.cyan(`\n${agentLabel(agentId)}@${defaultVer}:`));
|
|
157
161
|
const userSelection = await promptNewResourceSelection(agentId, newResources, defaultVer);
|
|
158
162
|
if (userSelection)
|
|
159
163
|
selection = userSelection;
|
|
@@ -184,13 +188,13 @@ export async function refresh(options = {}) {
|
|
|
184
188
|
const verNote = versionsToSync.length > 1
|
|
185
189
|
? chalk.gray(` (${versionsToSync.length} versions)`)
|
|
186
190
|
: '';
|
|
187
|
-
|
|
191
|
+
log(chalk.green(` Synced: ${[...kinds].join(', ')}`) + verNote);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
catch (err) {
|
|
192
196
|
if (isPromptCancelled(err)) {
|
|
193
|
-
|
|
197
|
+
log(chalk.gray('Skipped resource selection'));
|
|
194
198
|
}
|
|
195
199
|
else {
|
|
196
200
|
throw err;
|
|
@@ -215,24 +219,24 @@ export async function refresh(options = {}) {
|
|
|
215
219
|
const result = registerHooksToSettings(agentId, home, hookManifest);
|
|
216
220
|
hookRegistered += result.registered.length;
|
|
217
221
|
for (const error of result.errors) {
|
|
218
|
-
|
|
222
|
+
log(chalk.yellow(` Hook warning: ${error}`));
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
if (hookRegistered > 0) {
|
|
223
|
-
|
|
227
|
+
log(chalk.green(`\nRegistered ${hookRegistered} hook lifecycle event(s)`));
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
// 5. Auto-add shims to PATH
|
|
227
231
|
if (!isShimsInPath()) {
|
|
228
232
|
const pathResult = addShimsToPath();
|
|
229
233
|
if (pathResult.success && !pathResult.alreadyPresent) {
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
log(chalk.green(`\nAdded shims to ${pathResult.location}`));
|
|
235
|
+
log(chalk.gray(pathResult.reloadHint));
|
|
232
236
|
}
|
|
233
237
|
else if (!pathResult.success) {
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
log(chalk.yellow('\nCould not auto-add shims to PATH:'));
|
|
239
|
+
log(chalk.gray(getPathSetupInstructions()));
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
// 6. Prompt for missing default versions
|
|
@@ -273,63 +277,63 @@ export async function refresh(options = {}) {
|
|
|
273
277
|
setGlobalDefault(agentId, version);
|
|
274
278
|
const symlinkResult = await switchConfigSymlink(agentId, version);
|
|
275
279
|
if (!symlinkResult.success) {
|
|
276
|
-
|
|
280
|
+
log(chalk.yellow(`Warning: ${symlinkResult.error}`));
|
|
277
281
|
}
|
|
278
282
|
else if (symlinkResult.backupPath) {
|
|
279
|
-
|
|
283
|
+
log(chalk.gray(`Backed up existing config to: ${symlinkResult.backupPath}`));
|
|
280
284
|
}
|
|
281
285
|
switchHomeFileSymlinks(agentId, version);
|
|
282
|
-
|
|
286
|
+
log(chalk.green(`Set ${agentLabel(agent.id)}@${version} as default`));
|
|
283
287
|
}
|
|
284
288
|
}
|
|
285
289
|
// 7. Install declared host-CLIs
|
|
286
290
|
try {
|
|
287
291
|
const { statuses, errors } = listCliStatus(process.cwd());
|
|
288
292
|
for (const err of errors) {
|
|
289
|
-
|
|
293
|
+
log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
|
|
290
294
|
}
|
|
291
295
|
const missing = statuses.filter((s) => !s.installed);
|
|
292
296
|
if (missing.length > 0) {
|
|
293
|
-
|
|
297
|
+
log(chalk.bold('\nDeclared CLIs missing from this host:'));
|
|
294
298
|
for (const s of missing) {
|
|
295
299
|
const method = selectInstallMethod(s.manifest);
|
|
296
300
|
const action = method ? describeMethod(method) : chalk.red('no compatible install method');
|
|
297
|
-
|
|
301
|
+
log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
|
|
298
302
|
}
|
|
299
|
-
|
|
303
|
+
log('');
|
|
300
304
|
if (!skipPrompts) {
|
|
301
305
|
const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
|
|
302
306
|
if (proceed) {
|
|
303
307
|
for (const s of missing) {
|
|
304
|
-
|
|
308
|
+
log(chalk.bold(`\n→ ${s.manifest.name}`));
|
|
305
309
|
const result = installCli(s.manifest);
|
|
306
310
|
if (result.error) {
|
|
307
|
-
|
|
311
|
+
log(chalk.red(` ${result.error}`));
|
|
308
312
|
continue;
|
|
309
313
|
}
|
|
310
314
|
if (result.installed) {
|
|
311
|
-
|
|
315
|
+
log(chalk.green(` installed`));
|
|
312
316
|
if (s.manifest.postInstall) {
|
|
313
|
-
|
|
317
|
+
log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
|
|
314
318
|
}
|
|
315
319
|
}
|
|
316
320
|
else {
|
|
317
|
-
|
|
321
|
+
log(chalk.yellow(` install ran but \`${describeCheck(s.manifest.check)}\` still fails`));
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
}
|
|
321
325
|
else {
|
|
322
|
-
|
|
326
|
+
log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
else {
|
|
326
|
-
|
|
330
|
+
log(chalk.gray(`Run 'agents cli install' to install them.`));
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
334
|
catch (err) {
|
|
331
335
|
if (!isPromptCancelled(err)) {
|
|
332
|
-
|
|
336
|
+
log(chalk.yellow(`CLI install skipped: ${err.message}`));
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
339
|
}
|
|
@@ -12,6 +12,34 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
12
12
|
quiet?: boolean;
|
|
13
13
|
/** Per-peer deadline. Long-running maintenance commands override 12 seconds. */
|
|
14
14
|
timeoutMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Opt-in early-exit for a globally-unique lookup (a full session UUID). When a
|
|
17
|
+
* peer returns an item that satisfies {@link isDefinitive}, the fan-out
|
|
18
|
+
* resolves immediately and SIGTERMs every still-outstanding peer instead of
|
|
19
|
+
* waiting for the slowest one to hit {@link REMOTE_TIMEOUT_MS}.
|
|
20
|
+
*
|
|
21
|
+
* OMITTED BY DEFAULT — tool-search, program-count, and the default session
|
|
22
|
+
* listing keep the all-settle behavior (ambiguity, or a label/prefix conflict,
|
|
23
|
+
* is only known once every peer has answered). Only the full-UUID resolve path
|
|
24
|
+
* opts in, since only a UUID guarantees the first hit is the only hit.
|
|
25
|
+
*/
|
|
26
|
+
earlyExit?: {
|
|
27
|
+
isDefinitive: (item: T, machine: string) => boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The SSH boundary as a swappable dependency so the fan-out logic is testable
|
|
32
|
+
* without a live tailnet. Production wires this to the real {@link sshCapture}.
|
|
33
|
+
*/
|
|
34
|
+
export type SshCaptureFn = (target: string, remoteCmd: string, opts: {
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
code: number | null;
|
|
39
|
+
stdout: string;
|
|
40
|
+
}>;
|
|
41
|
+
export interface GatherRemoteAgentsJsonDeps {
|
|
42
|
+
capture?: SshCaptureFn;
|
|
15
43
|
}
|
|
16
44
|
export interface RemoteAgentsJsonParseResult<T> {
|
|
17
45
|
items: T[];
|
|
@@ -35,4 +63,4 @@ export declare function parseRemoteAgentsJsonPayload<T>(stdout: string, machine:
|
|
|
35
63
|
/** Build the command one peer runs, with a guard that prevents recursive fan-out. */
|
|
36
64
|
export declare function remoteAgentsJsonCommand(args: string[], noFanoutEnv: string, os?: string): string;
|
|
37
65
|
/** Query explicit hosts, or every registered online peer when hosts is omitted. */
|
|
38
|
-
export declare function gatherRemoteAgentsJson<T>(options: RemoteAgentsJsonOptions<T
|
|
66
|
+
export declare function gatherRemoteAgentsJson<T>(options: RemoteAgentsJsonOptions<T>, deps?: GatherRemoteAgentsJsonDeps): Promise<RemoteAgentsJsonResult<T>>;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* healthy results from the rest of the fleet.
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
+
import { setMaxListeners } from 'node:events';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from './ssh-exec.js';
|
|
12
13
|
import { sshTargetFor } from './devices/connect.js';
|
|
@@ -32,9 +33,13 @@ export function remoteAgentsJsonCommand(args, noFanoutEnv, os) {
|
|
|
32
33
|
const inner = `${noFanoutEnv}=1 agents ${args.map(shellQuote).join(' ')}`;
|
|
33
34
|
return `bash -lc ${shellQuote(inner)}`;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
const sshCapture = (target, remoteCmd, { timeoutMs, signal }) => {
|
|
36
37
|
assertValidSshTarget(target);
|
|
37
38
|
return new Promise((resolve) => {
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
resolve({ code: null, stdout: '' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
38
43
|
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
39
44
|
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
40
45
|
let stdout = '';
|
|
@@ -44,19 +49,25 @@ function sshCapture(target, remoteCmd, timeoutMs) {
|
|
|
44
49
|
return;
|
|
45
50
|
settled = true;
|
|
46
51
|
clearTimeout(timer);
|
|
52
|
+
signal?.removeEventListener('abort', onAbort);
|
|
47
53
|
resolve({ code, stdout });
|
|
48
54
|
};
|
|
49
55
|
const timer = setTimeout(() => {
|
|
50
56
|
child.kill('SIGKILL');
|
|
51
57
|
done(null);
|
|
52
58
|
}, timeoutMs);
|
|
59
|
+
// A definitive hit on a fast peer cancels the rest: SIGTERM the child so an
|
|
60
|
+
// unreachable peer is not left running to its full timeout.
|
|
61
|
+
const onAbort = () => { child.kill('SIGTERM'); done(null); };
|
|
62
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
53
63
|
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
54
64
|
child.on('error', () => done(null));
|
|
55
65
|
child.on('close', (code) => done(code));
|
|
56
66
|
});
|
|
57
|
-
}
|
|
67
|
+
};
|
|
58
68
|
/** Query explicit hosts, or every registered online peer when hosts is omitted. */
|
|
59
|
-
export async function gatherRemoteAgentsJson(options) {
|
|
69
|
+
export async function gatherRemoteAgentsJson(options, deps = {}) {
|
|
70
|
+
const capture = deps.capture ?? sshCapture;
|
|
60
71
|
const self = machineId();
|
|
61
72
|
const targets = [];
|
|
62
73
|
if (options.hosts && options.hosts.length > 0) {
|
|
@@ -100,24 +111,50 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
100
111
|
}
|
|
101
112
|
const skipped = [];
|
|
102
113
|
const parseFailed = [];
|
|
114
|
+
// The controller exists ONLY for early-exit: the first definitive hit aborts
|
|
115
|
+
// every still-outstanding peer. When earlyExit is not requested there is no
|
|
116
|
+
// controller and no per-peer abort listener, so the default path is a plain
|
|
117
|
+
// all-settle Promise.all, byte-identical to before (tool-search, program-count).
|
|
118
|
+
const controller = options.earlyExit ? new AbortController() : undefined;
|
|
119
|
+
// A fleet can hold more peers than Node's default 10-listener cap; each peer's
|
|
120
|
+
// capture adds one abort listener, so lift the cap to avoid a spurious warning.
|
|
121
|
+
if (controller)
|
|
122
|
+
setMaxListeners(targets.length + 1, controller.signal);
|
|
123
|
+
let earlyResolved = false;
|
|
103
124
|
const results = await Promise.all(targets.map(async (target) => {
|
|
104
125
|
const command = remoteAgentsJsonCommand(options.args, options.noFanoutEnv, target.os);
|
|
105
|
-
const result = await
|
|
126
|
+
const result = await capture(target.target, command, {
|
|
127
|
+
timeoutMs: options.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
128
|
+
signal: controller?.signal,
|
|
129
|
+
});
|
|
130
|
+
// A peer we deliberately cancelled is neither a hit nor a failure — it never
|
|
131
|
+
// got to answer, so it must not pollute skipped/parseFailed (which drive the
|
|
132
|
+
// "unreachable" listing and the fail-closed partial-resolution gate).
|
|
133
|
+
const cancelled = controller?.signal.aborted ?? false;
|
|
106
134
|
if (result.code !== 0) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
if (!cancelled) {
|
|
136
|
+
skipped.push(target.name);
|
|
137
|
+
if (!options.quiet) {
|
|
138
|
+
process.stderr.write(chalk.gray(` ${target.name}: unreachable or no agents CLI — skipped\n`));
|
|
139
|
+
}
|
|
110
140
|
}
|
|
111
141
|
return [];
|
|
112
142
|
}
|
|
113
143
|
const parsed = parseRemoteAgentsJsonPayload(result.stdout, target.machine, options.parse);
|
|
114
144
|
if (parsed.parseFailed) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
if (!cancelled) {
|
|
146
|
+
parseFailed.push(target.name);
|
|
147
|
+
if (!options.quiet) {
|
|
148
|
+
process.stderr.write(chalk.gray(` ${target.name}: unreachable or no agents CLI — skipped\n`));
|
|
149
|
+
}
|
|
118
150
|
}
|
|
119
151
|
return [];
|
|
120
152
|
}
|
|
153
|
+
if (controller && options.earlyExit && !earlyResolved
|
|
154
|
+
&& parsed.items.some((item) => options.earlyExit.isDefinitive(item, target.machine))) {
|
|
155
|
+
earlyResolved = true;
|
|
156
|
+
controller.abort(); // SIGTERM the remaining peers; they settle fast below.
|
|
157
|
+
}
|
|
121
158
|
return parsed.items;
|
|
122
159
|
}));
|
|
123
160
|
return { items: results.flat(), deviceCount: targets.length, skipped, parseFailed, discoveryFailed: false };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource inventory — the single chokepoint for "what does this agent@version
|
|
3
|
+
* have" (RUSH-2238, parent RUSH-2236; spec: openspec/specs/resource-inventory).
|
|
4
|
+
*
|
|
5
|
+
* One API answers four orthogonal questions per (agent, version, kind):
|
|
6
|
+
*
|
|
7
|
+
* capable — the agent+version feature flags allow the kind at all
|
|
8
|
+
* declared — resources the DotAgents layers (project/user/system/extra)
|
|
9
|
+
* declare for sync
|
|
10
|
+
* onDisk — resources actually installed in the version home filesystem
|
|
11
|
+
* (the installed-resource truth — NOT agents.yaml tracking data)
|
|
12
|
+
* wired — resources the harness native config references so they fire
|
|
13
|
+
* unmanaged — onDisk − declared (orphans no layer declares anymore)
|
|
14
|
+
*
|
|
15
|
+
* Callers (inspect, doctor, view, sync) MUST come here rather than
|
|
16
|
+
* re-implementing path joins or config parsing per command. Wiring parsing
|
|
17
|
+
* currently covers the settings.json family (claude, droid,
|
|
18
|
+
* muse), Grok hooks.json, and Kimi config.toml via checkVersionHookWiring.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentId } from './types.js';
|
|
21
|
+
import { type HookWiringReport } from './hooks.js';
|
|
22
|
+
/** Resource kinds the inventory can report. Only 'hooks' is implemented so far
|
|
23
|
+
* (RUSH-2238); the remaining kinds land with their follow-up tickets. */
|
|
24
|
+
export type InventoryKind = 'hooks' | 'skills' | 'commands' | 'plugins' | 'mcp';
|
|
25
|
+
/** A reference to one resource in one state (declared / onDisk / wired). */
|
|
26
|
+
export interface ResourceRef {
|
|
27
|
+
/** Stable install name (script basename without extension for hooks). */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Declared/onDisk: absolute file path. Wired: the command string the native
|
|
30
|
+
* config references. */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Origin: a layer name for declared ('project' | 'user' | 'system' | extra
|
|
33
|
+
* repo alias), 'user'/'project' scope or 'version-home' for onDisk,
|
|
34
|
+
* 'native-config' for wired. */
|
|
35
|
+
source: string;
|
|
36
|
+
/** Optional context — e.g. the lifecycle events a wired hook fires on. */
|
|
37
|
+
detail?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ResourceInventory {
|
|
40
|
+
agent: AgentId;
|
|
41
|
+
version: string;
|
|
42
|
+
kind: InventoryKind;
|
|
43
|
+
/** Agent+version feature flags allow this kind (version-aware gate). */
|
|
44
|
+
capable: boolean;
|
|
45
|
+
/** Declared in the DotAgents layers. */
|
|
46
|
+
declared: ResourceRef[];
|
|
47
|
+
/** Installed on disk in the version home. */
|
|
48
|
+
onDisk: ResourceRef[];
|
|
49
|
+
/** Referenced by the harness native config (empty when wiringSupported is
|
|
50
|
+
* false — the format parser for that harness is not implemented yet). */
|
|
51
|
+
wired: ResourceRef[];
|
|
52
|
+
/** onDisk − declared: installed orphans no layer declares. */
|
|
53
|
+
unmanaged: ResourceRef[];
|
|
54
|
+
/** Whether this harness's native wiring was read successfully. When false,
|
|
55
|
+
* `wired: []` means "unknown" (unsupported family OR unparseable config),
|
|
56
|
+
* NOT "nothing wired". Format-supported but corrupt configs set this false. */
|
|
57
|
+
wiringSupported: boolean;
|
|
58
|
+
/** Native-format diagnostic retained for doctor; derived in this same pass. */
|
|
59
|
+
wiring?: HookWiringReport;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The single inventory API. Harness-scoped (claude/codex/grok/kimi/droid) —
|
|
63
|
+
* never a model or profile. Throws for kinds without an implementation rather
|
|
64
|
+
* than returning a half-empty report that looks like truth.
|
|
65
|
+
*/
|
|
66
|
+
export declare function getResourceInventory(agent: AgentId, version: string, kind: InventoryKind, opts?: {
|
|
67
|
+
cwd?: string;
|
|
68
|
+
}): ResourceInventory;
|
|
69
|
+
/**
|
|
70
|
+
* On-disk hook listing for an explicit home (version home or effective home)
|
|
71
|
+
* plus the project overlay — the one listing `getAgentResources` routes
|
|
72
|
+
* through, so inspect/doctor/view share a single hooks source. Paths resolve
|
|
73
|
+
* through the absolute-hooksDir-safe join (RUSH-2237), so grok/kimi version
|
|
74
|
+
* homes list their real hooks.
|
|
75
|
+
*/
|
|
76
|
+
export declare function listOnDiskHooks(agent: AgentId, opts?: {
|
|
77
|
+
home?: string;
|
|
78
|
+
cwd?: string;
|
|
79
|
+
}): ResourceRef[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource inventory — the single chokepoint for "what does this agent@version
|
|
3
|
+
* have" (RUSH-2238, parent RUSH-2236; spec: openspec/specs/resource-inventory).
|
|
4
|
+
*
|
|
5
|
+
* One API answers four orthogonal questions per (agent, version, kind):
|
|
6
|
+
*
|
|
7
|
+
* capable — the agent+version feature flags allow the kind at all
|
|
8
|
+
* declared — resources the DotAgents layers (project/user/system/extra)
|
|
9
|
+
* declare for sync
|
|
10
|
+
* onDisk — resources actually installed in the version home filesystem
|
|
11
|
+
* (the installed-resource truth — NOT agents.yaml tracking data)
|
|
12
|
+
* wired — resources the harness native config references so they fire
|
|
13
|
+
* unmanaged — onDisk − declared (orphans no layer declares anymore)
|
|
14
|
+
*
|
|
15
|
+
* Callers (inspect, doctor, view, sync) MUST come here rather than
|
|
16
|
+
* re-implementing path joins or config parsing per command. Wiring parsing
|
|
17
|
+
* currently covers the settings.json family (claude, droid,
|
|
18
|
+
* muse), Grok hooks.json, and Kimi config.toml via checkVersionHookWiring.
|
|
19
|
+
*/
|
|
20
|
+
import { AGENTS } from './agents.js';
|
|
21
|
+
import { supports } from './capabilities.js';
|
|
22
|
+
import { listResources } from './resources.js';
|
|
23
|
+
import { checkVersionHookWiring, listHooksInVersionHome, listInstalledHooksWithScope, } from './hooks.js';
|
|
24
|
+
const IMPLEMENTED_KINDS = ['hooks'];
|
|
25
|
+
/**
|
|
26
|
+
* The single inventory API. Harness-scoped (claude/codex/grok/kimi/droid) —
|
|
27
|
+
* never a model or profile. Throws for kinds without an implementation rather
|
|
28
|
+
* than returning a half-empty report that looks like truth.
|
|
29
|
+
*/
|
|
30
|
+
export function getResourceInventory(agent, version, kind, opts = {}) {
|
|
31
|
+
if (!IMPLEMENTED_KINDS.includes(kind)) {
|
|
32
|
+
// Fail loud at the boundary: the kind is part of the API shape but its
|
|
33
|
+
// inventory lands with a RUSH-2236 follow-up — a silent empty report would
|
|
34
|
+
// read as "nothing installed".
|
|
35
|
+
throw new Error(`getResourceInventory: kind '${kind}' is not implemented yet (implemented: ${IMPLEMENTED_KINDS.join(', ')}; tracked under RUSH-2236)`);
|
|
36
|
+
}
|
|
37
|
+
return hooksInventory(agent, version, opts.cwd);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* On-disk hook listing for an explicit home (version home or effective home)
|
|
41
|
+
* plus the project overlay — the one listing `getAgentResources` routes
|
|
42
|
+
* through, so inspect/doctor/view share a single hooks source. Paths resolve
|
|
43
|
+
* through the absolute-hooksDir-safe join (RUSH-2237), so grok/kimi version
|
|
44
|
+
* homes list their real hooks.
|
|
45
|
+
*/
|
|
46
|
+
export function listOnDiskHooks(agent, opts = {}) {
|
|
47
|
+
return listInstalledHooksWithScope(agent, opts.cwd ?? process.cwd(), { home: opts.home }).map((h) => ({
|
|
48
|
+
name: h.name,
|
|
49
|
+
path: h.path,
|
|
50
|
+
source: h.scope,
|
|
51
|
+
detail: h.dataFile,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function hooksInventory(agent, version, cwd) {
|
|
55
|
+
const capable = supports(agent, 'hooks', version).ok;
|
|
56
|
+
// Declared: the layered DotAgents repos are the declaration surface. Hook
|
|
57
|
+
// script names carry their extension here ('git-guard.sh'); normalize to the
|
|
58
|
+
// extension-less install name so declared/onDisk sets compare.
|
|
59
|
+
const declared = listResources('hooks', cwd).map((r) => ({
|
|
60
|
+
name: r.name.replace(/\.[^./]+$/, ''),
|
|
61
|
+
path: r.path,
|
|
62
|
+
source: r.source,
|
|
63
|
+
}));
|
|
64
|
+
// onDisk / wired need the version home; both are empty for an agent whose
|
|
65
|
+
// hooks capability is gated off (unknown agents included — AGENTS lookup
|
|
66
|
+
// would otherwise throw).
|
|
67
|
+
let onDisk = [];
|
|
68
|
+
let wired = [];
|
|
69
|
+
let wiringSupported = false;
|
|
70
|
+
let wiring;
|
|
71
|
+
if (capable && AGENTS[agent]?.supportsHooks) {
|
|
72
|
+
onDisk = listHooksInVersionHome(agent, version).map((e) => ({
|
|
73
|
+
name: e.name,
|
|
74
|
+
path: e.scriptPath,
|
|
75
|
+
source: 'version-home',
|
|
76
|
+
detail: e.dataFile,
|
|
77
|
+
}));
|
|
78
|
+
const report = checkVersionHookWiring(agent, version);
|
|
79
|
+
wiring = report;
|
|
80
|
+
// supported=true means the harness format is known; settingsUnparseable means
|
|
81
|
+
// we could not trust the file. Only report wired counts when the parse succeeded —
|
|
82
|
+
// otherwise inspect/doctor would render "wired 0" for corrupt configs (PR #2140 review).
|
|
83
|
+
wiringSupported = report.supported && !report.settingsUnparseable;
|
|
84
|
+
if (wiringSupported) {
|
|
85
|
+
// One ref per hook, events folded into detail — a hook wired under
|
|
86
|
+
// PreToolUse AND Stop is one inventory entry, not two.
|
|
87
|
+
const eventsByName = new Map();
|
|
88
|
+
const commandByName = new Map();
|
|
89
|
+
for (const issue of report.wired) {
|
|
90
|
+
let events = eventsByName.get(issue.name);
|
|
91
|
+
if (!events) {
|
|
92
|
+
events = new Set();
|
|
93
|
+
eventsByName.set(issue.name, events);
|
|
94
|
+
commandByName.set(issue.name, issue.command);
|
|
95
|
+
}
|
|
96
|
+
events.add(issue.event);
|
|
97
|
+
}
|
|
98
|
+
wired = [...eventsByName.entries()]
|
|
99
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
100
|
+
.map(([name, events]) => ({
|
|
101
|
+
name,
|
|
102
|
+
path: commandByName.get(name),
|
|
103
|
+
source: 'native-config',
|
|
104
|
+
detail: [...events].sort().join('/'),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const declaredNames = new Set(declared.map((r) => r.name));
|
|
109
|
+
const unmanaged = onDisk.filter((r) => !declaredNames.has(r.name));
|
|
110
|
+
return {
|
|
111
|
+
agent,
|
|
112
|
+
version,
|
|
113
|
+
kind: 'hooks',
|
|
114
|
+
capable,
|
|
115
|
+
declared,
|
|
116
|
+
onDisk,
|
|
117
|
+
wired,
|
|
118
|
+
unmanaged,
|
|
119
|
+
wiringSupported,
|
|
120
|
+
wiring,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -127,6 +127,8 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
127
127
|
case 'pi':
|
|
128
128
|
// omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
|
|
129
129
|
return path.join(versionHome, '.omp', 'agent', '.mcp.json');
|
|
130
|
+
case 'muse':
|
|
131
|
+
return path.join(versionHome, '.config', 'muse', 'settings.json');
|
|
130
132
|
default:
|
|
131
133
|
return null;
|
|
132
134
|
}
|
|
@@ -81,6 +81,9 @@ function getAgentConfigPath(agent, versionHome) {
|
|
|
81
81
|
return path.join(versionHome, '.hermes', 'config.yaml');
|
|
82
82
|
case 'goose':
|
|
83
83
|
return path.join(versionHome, '.config', 'goose', 'permission.yaml');
|
|
84
|
+
case 'muse':
|
|
85
|
+
// Muse approval defaults live in settings.json; no separate permissions file.
|
|
86
|
+
return path.join(versionHome, '.config', 'muse', 'settings.json');
|
|
84
87
|
default:
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Union: All resources from all layers are combined
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
import type { AgentId } from '../types.js';
|
|
9
|
+
export type { AgentId };
|
|
9
10
|
/** Resource origin. Precedence (highest first): project > user > plugin > system. */
|
|
10
11
|
export type Layer = 'system' | 'user' | 'project' | 'plugin';
|
|
11
12
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
package/dist/lib/resources.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as path from 'path';
|
|
|
7
7
|
import { AGENTS, listInstalledMcpsWithScope } from './agents.js';
|
|
8
8
|
import { listInstalledCommandsWithScope } from './commands.js';
|
|
9
9
|
import { listInstalledSkillsWithScope } from './skills.js';
|
|
10
|
-
import {
|
|
10
|
+
import { listOnDiskHooks } from './resource-inventory.js';
|
|
11
11
|
import { listInstalledInstructionsWithScope } from './rules/rules.js';
|
|
12
12
|
import { getEffectiveHome } from './versions.js';
|
|
13
13
|
import { listMcpServerConfigs } from './mcp.js';
|
|
@@ -352,11 +352,14 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
// Hooks
|
|
355
|
+
// Hooks — routed through the resource-inventory chokepoint (RUSH-2238) so
|
|
356
|
+
// inspect/doctor/view share one listing and one (absolute-hooksDir-safe)
|
|
357
|
+
// path resolution.
|
|
356
358
|
const hooks = [];
|
|
357
|
-
for (const
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
for (const ref of listOnDiskHooks(agentId, { cwd, home })) {
|
|
360
|
+
const hookScope = ref.source;
|
|
361
|
+
if (shouldInclude(hookScope)) {
|
|
362
|
+
hooks.push({ name: ref.name, path: ref.path, scope: hookScope });
|
|
360
363
|
}
|
|
361
364
|
}
|
|
362
365
|
// Workflows
|
|
@@ -21,6 +21,7 @@ export interface ResolvedRunDefaults extends RunDefaults {
|
|
|
21
21
|
sources: {
|
|
22
22
|
mode?: string;
|
|
23
23
|
model?: string;
|
|
24
|
+
effort?: string;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
export interface RunDefaultEntry {
|
|
@@ -30,6 +31,7 @@ export interface RunDefaultEntry {
|
|
|
30
31
|
type RunDefaultsInput = {
|
|
31
32
|
mode?: unknown;
|
|
32
33
|
model?: unknown;
|
|
34
|
+
effort?: unknown;
|
|
33
35
|
};
|
|
34
36
|
export declare function normalizeRunDefaultMode(input: string): Mode;
|
|
35
37
|
export declare function parseRunDefaultSelector(input: string): ParsedRunDefaultSelector;
|