@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/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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
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
|
|
@@ -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[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { isAlive, killTree } from './platform/index.js';
|
|
5
|
+
import { getRunsDir } from './state.js';
|
|
6
|
+
function processMatchesRun(meta) {
|
|
7
|
+
if (!meta.pid || !meta.spawnedAt)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
const startedAt = execFileSync('powershell.exe', [
|
|
12
|
+
'-NoProfile',
|
|
13
|
+
'-NonInteractive',
|
|
14
|
+
'-Command',
|
|
15
|
+
`(Get-Process -Id ${meta.pid} -ErrorAction Stop).StartTime.ToUniversalTime().ToString("o")`,
|
|
16
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
17
|
+
const processStart = Date.parse(startedAt);
|
|
18
|
+
return Number.isFinite(processStart) && Math.abs(processStart - meta.spawnedAt) < 30_000;
|
|
19
|
+
}
|
|
20
|
+
const elapsed = execFileSync('ps', ['-p', String(meta.pid), '-o', 'etime='], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
}).trim();
|
|
24
|
+
if (!elapsed)
|
|
25
|
+
return false;
|
|
26
|
+
const fields = elapsed.replace(/-/g, ':').split(':').reverse();
|
|
27
|
+
const seconds = Number(fields[0] ?? 0)
|
|
28
|
+
+ Number(fields[1] ?? 0) * 60
|
|
29
|
+
+ Number(fields[2] ?? 0) * 3600
|
|
30
|
+
+ Number(fields[3] ?? 0) * 86400;
|
|
31
|
+
return Math.abs((Date.now() - seconds * 1000) - meta.spawnedAt) < 30_000;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Reap process groups whose durable run record is already terminal. */
|
|
38
|
+
export function reapTerminalRoutineProcesses(opts = {}) {
|
|
39
|
+
const runsDir = opts.runsDir ?? getRunsDir();
|
|
40
|
+
const alive = opts.alive ?? isAlive;
|
|
41
|
+
const owns = opts.owns ?? processMatchesRun;
|
|
42
|
+
const terminate = opts.terminate ?? ((pid) => killTree(process.platform === 'win32' ? pid : -pid));
|
|
43
|
+
if (!fs.existsSync(runsDir))
|
|
44
|
+
return [];
|
|
45
|
+
const reaped = [];
|
|
46
|
+
for (const job of fs.readdirSync(runsDir, { withFileTypes: true })) {
|
|
47
|
+
if (!job.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const jobDir = path.join(runsDir, job.name);
|
|
50
|
+
for (const run of fs.readdirSync(jobDir, { withFileTypes: true })) {
|
|
51
|
+
if (!run.isDirectory())
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const meta = JSON.parse(fs.readFileSync(path.join(jobDir, run.name, 'meta.json'), 'utf-8'));
|
|
55
|
+
if (!['failed', 'timeout'].includes(meta.status) || !meta.pid || meta.hostTaskId || meta.cloudTaskId)
|
|
56
|
+
continue;
|
|
57
|
+
const completedAt = Date.parse(meta.completedAt ?? '');
|
|
58
|
+
if (!Number.isFinite(completedAt) || Date.now() - completedAt < 5_000)
|
|
59
|
+
continue;
|
|
60
|
+
if (!alive(meta.pid))
|
|
61
|
+
continue;
|
|
62
|
+
if (!owns(meta))
|
|
63
|
+
continue;
|
|
64
|
+
terminate(meta.pid);
|
|
65
|
+
reaped.push(meta.pid);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Corrupt or concurrently-replaced records are left untouched.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return reaped;
|
|
73
|
+
}
|
|
@@ -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;
|
package/dist/lib/run-defaults.js
CHANGED
|
@@ -17,6 +17,7 @@ import { readMeta, updateMeta } from './state.js';
|
|
|
17
17
|
import { getProjectRunConfigs } from './run-config.js';
|
|
18
18
|
import chalk from 'chalk';
|
|
19
19
|
const VERSION_RE = /^(?:\*|latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
|
|
20
|
+
const RUN_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
20
21
|
function isAgentId(value) {
|
|
21
22
|
return value in AGENTS;
|
|
22
23
|
}
|
|
@@ -42,6 +43,12 @@ function normalizeRunDefaults(defaults, selector) {
|
|
|
42
43
|
}
|
|
43
44
|
out.model = defaults.model.trim();
|
|
44
45
|
}
|
|
46
|
+
if (defaults.effort !== undefined) {
|
|
47
|
+
if (typeof defaults.effort !== 'string' || !RUN_EFFORTS.includes(defaults.effort)) {
|
|
48
|
+
throw new Error('Invalid effort in run.defaults.' + selector + ': use one of: ' + RUN_EFFORTS.join(', ') + '.');
|
|
49
|
+
}
|
|
50
|
+
out.effort = defaults.effort;
|
|
51
|
+
}
|
|
45
52
|
return out;
|
|
46
53
|
}
|
|
47
54
|
export function parseRunDefaultSelector(input) {
|
|
@@ -97,6 +104,10 @@ export function resolveRunDefaultsFromConfig(runConfig, agent, version) {
|
|
|
97
104
|
resolved.model = wildcard.model;
|
|
98
105
|
resolved.sources.model = wildcardSelector;
|
|
99
106
|
}
|
|
107
|
+
if (wildcard?.effort) {
|
|
108
|
+
resolved.effort = wildcard.effort;
|
|
109
|
+
resolved.sources.effort = wildcardSelector;
|
|
110
|
+
}
|
|
100
111
|
if (exactSelector && defaults[exactSelector]) {
|
|
101
112
|
const exact = normalizeRunDefaults(defaults[exactSelector], exactSelector);
|
|
102
113
|
if (exact.mode) {
|
|
@@ -107,6 +118,10 @@ export function resolveRunDefaultsFromConfig(runConfig, agent, version) {
|
|
|
107
118
|
resolved.model = exact.model;
|
|
108
119
|
resolved.sources.model = exactSelector;
|
|
109
120
|
}
|
|
121
|
+
if (exact.effort) {
|
|
122
|
+
resolved.effort = exact.effort;
|
|
123
|
+
resolved.sources.effort = exactSelector;
|
|
124
|
+
}
|
|
110
125
|
}
|
|
111
126
|
return resolved;
|
|
112
127
|
}
|
|
@@ -122,6 +137,10 @@ export function resolveRunDefaultsFromConfigs(runConfigs, agent, version) {
|
|
|
122
137
|
resolved.model = next.model;
|
|
123
138
|
resolved.sources.model = next.sources.model;
|
|
124
139
|
}
|
|
140
|
+
if (next.effort) {
|
|
141
|
+
resolved.effort = next.effort;
|
|
142
|
+
resolved.sources.effort = next.sources.effort;
|
|
143
|
+
}
|
|
125
144
|
}
|
|
126
145
|
return resolved;
|
|
127
146
|
}
|
|
@@ -135,6 +154,8 @@ export function formatRunDefaultEntry(entry) {
|
|
|
135
154
|
parts.push(`mode ${chalk.white(entry.defaults.mode)}`);
|
|
136
155
|
if (entry.defaults.model)
|
|
137
156
|
parts.push(`model ${chalk.white(entry.defaults.model)}`);
|
|
157
|
+
if (entry.defaults.effort)
|
|
158
|
+
parts.push('effort ' + chalk.white(entry.defaults.effort));
|
|
138
159
|
return `${chalk.cyan(entry.selector.padEnd(22))} ${parts.join(' ')}`;
|
|
139
160
|
}
|
|
140
161
|
export function listRunDefaults() {
|
|
@@ -149,8 +170,8 @@ export function listRunDefaults() {
|
|
|
149
170
|
export function setRunDefault(selectorInput, defaultsInput) {
|
|
150
171
|
const parsed = parseRunDefaultSelector(selectorInput);
|
|
151
172
|
const defaults = normalizeRunDefaults(defaultsInput, parsed.selector);
|
|
152
|
-
if (!defaults.mode && !defaults.model) {
|
|
153
|
-
throw new Error('Set at least one default: --mode <mode
|
|
173
|
+
if (!defaults.mode && !defaults.model && !defaults.effort) {
|
|
174
|
+
throw new Error('Set at least one default: --mode <mode>, --model <model>, or --effort <effort>.');
|
|
154
175
|
}
|
|
155
176
|
updateMeta((meta) => {
|
|
156
177
|
const run = { ...(meta.run ?? {}) };
|