@phnx-labs/agents-cli 1.22.57 → 1.22.58
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 +56 -0
- package/dist/bootstrap.js +8 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/db.d.ts +20 -1
- package/dist/lib/session/db.js +139 -9
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -29,9 +29,11 @@ import { discoverPlugins, marketplaceSpecForName } from './plugins/plugins.js';
|
|
|
29
29
|
import { pluginInstallDir, repairableManifestFields } from './plugins/plugin-marketplace.js';
|
|
30
30
|
import { markdownToToml } from './convert.js';
|
|
31
31
|
import { listCommandsInVersionHome, getVersionCommandsDir, listPluginCommandNames } from './commands.js';
|
|
32
|
-
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } from './command-skills.js';
|
|
32
|
+
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName, skillSourceExists, readSkillSourceCommandMarker } from './command-skills.js';
|
|
33
|
+
import { trustedSkillRoots } from './staleness/writers/sources.js';
|
|
33
34
|
import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
|
|
34
35
|
import { supports } from './capabilities.js';
|
|
36
|
+
import { isDirectoryDoc } from './resources.js';
|
|
35
37
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './plugins/skills.js';
|
|
36
38
|
import { listHookEntriesFromDir } from './hooks/install.js';
|
|
37
39
|
import { getResourceInventory } from './resource-inventory.js';
|
|
@@ -115,16 +117,46 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
115
117
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
116
118
|
continue;
|
|
117
119
|
const name = entry.name.replace(/\.md$/, '');
|
|
120
|
+
// Directory docs (README/AGENTS/CLAUDE/GEMINI) live in commands/ but are
|
|
121
|
+
// documentation, not commands. `discoverCommands` and `resolveResource`
|
|
122
|
+
// both refuse them, so the sync writer never installs them — mirror that
|
|
123
|
+
// here or every one false-reports as a missing command no sync can clear.
|
|
124
|
+
if (isDirectoryDoc('commands', name))
|
|
125
|
+
continue;
|
|
118
126
|
if (sourceByName.has(name))
|
|
119
127
|
continue;
|
|
120
128
|
sourceByName.set(name, { layer: base.layer, path: path.join(base.path, entry.name), alias: base.alias });
|
|
121
129
|
}
|
|
122
130
|
}
|
|
131
|
+
// The trusted skill roots the commands-as-skills writer consults to decide
|
|
132
|
+
// whether a real skill of the same name already owns a command's target slot.
|
|
133
|
+
const skillRoots = asSkill ? trustedSkillRoots() : [];
|
|
123
134
|
const rows = [];
|
|
124
135
|
const seen = new Set();
|
|
125
136
|
for (const [name, src] of sourceByName) {
|
|
126
137
|
seen.add(name);
|
|
127
138
|
if (!installed.has(name)) {
|
|
139
|
+
// Command-as-skill agents (codex >= 0.117, kimi): when a real skill of the
|
|
140
|
+
// same name exists in the sources, the skill wins the shared
|
|
141
|
+
// `skills/<name>/` slot and `installCommandSkillToVersion` deliberately
|
|
142
|
+
// writes no command wrapper (versions.ts keeps the real skill in
|
|
143
|
+
// skillsToSync and overwrites any wrapper). The command's behavior is
|
|
144
|
+
// provided by that same-named skill, so it is NOT missing drift — reporting
|
|
145
|
+
// it so drove an unclearable "N missing" loop (PHNX-3186). Mirror the
|
|
146
|
+
// writer's skip predicate exactly: a skill source of this name whose
|
|
147
|
+
// `agents_command` marker is not this command (a real skill, or a different
|
|
148
|
+
// command).
|
|
149
|
+
if (asSkill && skillSourceExists(name, skillRoots) && readSkillSourceCommandMarker(name, skillRoots) !== name) {
|
|
150
|
+
rows.push({
|
|
151
|
+
kind: 'commands',
|
|
152
|
+
name,
|
|
153
|
+
status: 'ok',
|
|
154
|
+
source: src.layer,
|
|
155
|
+
sourcePath: src.path,
|
|
156
|
+
detail: 'provided by same-named skill',
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
128
160
|
rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
|
|
129
161
|
continue;
|
|
130
162
|
}
|
|
@@ -180,6 +212,11 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
180
212
|
for (const name of installed) {
|
|
181
213
|
if (seen.has(name))
|
|
182
214
|
continue;
|
|
215
|
+
// Directory docs are excluded from the source scan above; a leftover copy in
|
|
216
|
+
// the home is not a command orphan — leave it out of the command diff
|
|
217
|
+
// entirely rather than flip it to a spurious `extra` row.
|
|
218
|
+
if (isDirectoryDoc('commands', name))
|
|
219
|
+
continue;
|
|
183
220
|
if (pluginCommands.has(name))
|
|
184
221
|
continue;
|
|
185
222
|
const extraHome = asSkill
|
|
@@ -421,11 +458,15 @@ function diffRules(agent, version, cwd, excludeProject = false) {
|
|
|
421
458
|
const versionHome = getVersionHomePath(agent, version);
|
|
422
459
|
const configDir = path.join(versionHome, agentConfigDirName(agent));
|
|
423
460
|
const sourcesByName = listRulesNames(cwd, excludeProject);
|
|
424
|
-
// Files actually present in the version home.
|
|
461
|
+
// Files actually present in the version home. Include *.md siblings AND the
|
|
462
|
+
// agent's own instructions filename even when it is not *.md — cursor's rules
|
|
463
|
+
// file is `.cursorrules`, so an `.md`-only scan never saw it and reported the
|
|
464
|
+
// AGENTS rule `missing` on every sync, a phantom no reconcile could clear
|
|
465
|
+
// (PHNX-3186).
|
|
425
466
|
const homeFiles = new Set();
|
|
426
467
|
if (fs.existsSync(configDir)) {
|
|
427
468
|
for (const f of fs.readdirSync(configDir)) {
|
|
428
|
-
if (!f.endsWith('.md'))
|
|
469
|
+
if (!f.endsWith('.md') && f !== agentConfig.instructionsFile)
|
|
429
470
|
continue;
|
|
430
471
|
homeFiles.add(f);
|
|
431
472
|
}
|
|
@@ -537,14 +578,38 @@ export function describePluginDrift(central, mirrorDir) {
|
|
|
537
578
|
parts.push(`${mVer}→${cVer}`);
|
|
538
579
|
if (mManifest && repairableManifestFields(mManifest).length > 0)
|
|
539
580
|
parts.push('invalid manifest');
|
|
581
|
+
const centralSkills = listPluginSkillDirs(central.root);
|
|
540
582
|
const mirrorSkills = new Set(listPluginSkillDirs(mirrorDir));
|
|
541
|
-
const missSkills =
|
|
583
|
+
const missSkills = centralSkills.filter((s) => !mirrorSkills.has(s)).sort();
|
|
584
|
+
const centralCmds = listPluginCommandFiles(central.root);
|
|
542
585
|
const mirrorCmds = new Set(listPluginCommandFiles(mirrorDir));
|
|
543
|
-
const missCmds =
|
|
586
|
+
const missCmds = centralCmds.filter((c) => !mirrorCmds.has(c)).sort();
|
|
544
587
|
if (missSkills.length)
|
|
545
588
|
parts.push(`missing skill${missSkills.length > 1 ? 's' : ''}: ${missSkills.join(', ')}`);
|
|
546
589
|
if (missCmds.length)
|
|
547
590
|
parts.push(`missing command${missCmds.length > 1 ? 's' : ''}: ${missCmds.join(', ')}`);
|
|
591
|
+
// Content drift of a skill/command that exists in BOTH — the mirror kept the
|
|
592
|
+
// dir/file but its bytes went stale (PHNX-2955: a skill edit landed on origin
|
|
593
|
+
// and was pulled into central, but the per-version marketplace copy was never
|
|
594
|
+
// refreshed, so agents kept executing the OLD skill text while `plugins list`
|
|
595
|
+
// and `doctor` reported it `everywhere`/`ok`). Presence alone missed this; a
|
|
596
|
+
// content compare is what turns a stale mirror into a reportable `diff`.
|
|
597
|
+
const staleSkills = centralSkills
|
|
598
|
+
.filter((s) => mirrorSkills.has(s))
|
|
599
|
+
.filter((s) => !dirsContentMatch(path.join(central.root, 'skills', s), path.join(mirrorDir, 'skills', s)))
|
|
600
|
+
.sort();
|
|
601
|
+
const staleCmds = centralCmds
|
|
602
|
+
.filter((c) => mirrorCmds.has(c))
|
|
603
|
+
.filter((c) => {
|
|
604
|
+
const a = readSafe(path.join(central.root, 'commands', `${c}.md`));
|
|
605
|
+
const b = readSafe(path.join(mirrorDir, 'commands', `${c}.md`));
|
|
606
|
+
return a == null || b == null || normalize(a) !== normalize(b);
|
|
607
|
+
})
|
|
608
|
+
.sort();
|
|
609
|
+
if (staleSkills.length)
|
|
610
|
+
parts.push(`stale skill${staleSkills.length > 1 ? 's' : ''}: ${staleSkills.join(', ')}`);
|
|
611
|
+
if (staleCmds.length)
|
|
612
|
+
parts.push(`stale command${staleCmds.length > 1 ? 's' : ''}: ${staleCmds.join(', ')}`);
|
|
548
613
|
return parts.length ? parts.join(', ') : null;
|
|
549
614
|
}
|
|
550
615
|
function diffPlugins(agent, version, cwd) {
|
|
@@ -622,8 +687,13 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
622
687
|
empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
|
|
623
688
|
if (requested.has('permissions'))
|
|
624
689
|
empty.permissions = diffPresenceOnly('permissions', available.permissions, synced.permissions);
|
|
625
|
-
|
|
626
|
-
|
|
690
|
+
// Subagents are version-gated (e.g. kimi >= 0.29.0). A version below the floor
|
|
691
|
+
// is never written any subagent by the sync writer, so counting the source
|
|
692
|
+
// ones as "missing" is phantom drift no sync can clear (PHNX-3186). Zero the
|
|
693
|
+
// available set when unsupported; a stale installed copy still surfaces `extra`.
|
|
694
|
+
if (requested.has('subagents')) {
|
|
695
|
+
empty.subagents = diffPresenceOnly('subagents', supports(agent, 'subagents', version).ok ? available.subagents : [], synced.subagents);
|
|
696
|
+
}
|
|
627
697
|
if (requested.has('plugins'))
|
|
628
698
|
empty.plugins = diffPlugins(agent, version, cwd);
|
|
629
699
|
if (requested.has('promptcuts'))
|
|
@@ -33,3 +33,20 @@ export interface ResolveContext {
|
|
|
33
33
|
* sync scopes, and `login: skip` are still returned (probe/report only).
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveDesired(manifest: FleetManifest, ctx: ResolveContext): DeviceDesired[];
|
|
36
|
+
/**
|
|
37
|
+
* The message to print when a reconcile resolved zero target devices.
|
|
38
|
+
*
|
|
39
|
+
* An explicitly empty roster (`fleet.devices: {}` — the default on a fresh box)
|
|
40
|
+
* is the ACTIONABLE case: the engine ran fine, there is simply nothing declared
|
|
41
|
+
* to converge, so name the empty key and how to fill it. A gray "nothing to
|
|
42
|
+
* apply" there reads like the command is dead — the exact confusion that made
|
|
43
|
+
* `apply` look like a broken command during the RUSH-2981 surface review
|
|
44
|
+
* (PHNX-3422). Any OTHER zero-target case (a `devices: all` fleet with no online
|
|
45
|
+
* peers, or an explicit roster whose every name was unresolved — off-tailnet,
|
|
46
|
+
* ignored, or a typo, the only names `resolveDesired` drops) already had its
|
|
47
|
+
* reason surfaced above, so it stays a plain note.
|
|
48
|
+
*/
|
|
49
|
+
export declare function emptyTargetsMessage(manifest: FleetManifest): {
|
|
50
|
+
style: 'hint' | 'plain';
|
|
51
|
+
lines: string[];
|
|
52
|
+
};
|
|
@@ -169,3 +169,29 @@ export function resolveDesired(manifest, ctx) {
|
|
|
169
169
|
}
|
|
170
170
|
return out;
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The message to print when a reconcile resolved zero target devices.
|
|
174
|
+
*
|
|
175
|
+
* An explicitly empty roster (`fleet.devices: {}` — the default on a fresh box)
|
|
176
|
+
* is the ACTIONABLE case: the engine ran fine, there is simply nothing declared
|
|
177
|
+
* to converge, so name the empty key and how to fill it. A gray "nothing to
|
|
178
|
+
* apply" there reads like the command is dead — the exact confusion that made
|
|
179
|
+
* `apply` look like a broken command during the RUSH-2981 surface review
|
|
180
|
+
* (PHNX-3422). Any OTHER zero-target case (a `devices: all` fleet with no online
|
|
181
|
+
* peers, or an explicit roster whose every name was unresolved — off-tailnet,
|
|
182
|
+
* ignored, or a typo, the only names `resolveDesired` drops) already had its
|
|
183
|
+
* reason surfaced above, so it stays a plain note.
|
|
184
|
+
*/
|
|
185
|
+
export function emptyTargetsMessage(manifest) {
|
|
186
|
+
const rosterEmpty = manifest.devices !== 'all' && Object.keys(manifest.devices).length === 0;
|
|
187
|
+
if (rosterEmpty) {
|
|
188
|
+
return {
|
|
189
|
+
style: 'hint',
|
|
190
|
+
lines: [
|
|
191
|
+
'fleet.devices is empty — nothing to converge.',
|
|
192
|
+
'Declare a roster in agents.yaml: `fleet: { devices: all }` for every online box, or name them (`fleet: { devices: { <name>: {} } }`), then re-run.',
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { style: 'plain', lines: ['No target devices — nothing to apply.'] };
|
|
197
|
+
}
|
|
@@ -334,21 +334,37 @@ export declare function parseHookManifest(opts?: {
|
|
|
334
334
|
}): Record<string, ManifestHook>;
|
|
335
335
|
export declare function selectHookManifest(manifest: Record<string, ManifestHook>, selected: string[]): Record<string, ManifestHook>;
|
|
336
336
|
/**
|
|
337
|
-
* Hook
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
337
|
+
* Hook files present in a version home but absent from every configured
|
|
338
|
+
* SOURCE — genuine orphans left behind by a removed/renamed source hook.
|
|
339
|
+
*
|
|
340
|
+
* The definition is deliberately source-based, not manifest-based (PHNX-2693).
|
|
341
|
+
* Sync copies EVERY source hook file into a version home — registered hooks AND
|
|
342
|
+
* the helper / test / benchmark scripts that sit alongside them — but only the
|
|
343
|
+
* registered ones appear in `parseHookManifest`. Diffing installed names against
|
|
344
|
+
* the manifest therefore flagged every source-present-but-unregistered file
|
|
345
|
+
* (e.g. `permission-handler`, `verify-work-state`, `*_test`, `benchmark_*`) as
|
|
346
|
+
* an orphan, so `agents prune cleanup` offered to trash ~1000 in-use files. An
|
|
347
|
+
* installed hook is only truly orphaned when NO configured source still carries
|
|
348
|
+
* a file of that name — which is exactly what the `prune`/`doctor` help already
|
|
349
|
+
* promised ("present in a version home but missing from every configured
|
|
350
|
+
* source").
|
|
343
351
|
*
|
|
344
352
|
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
345
|
-
* installed hook names and the
|
|
353
|
+
* installed hook names and the source hook script paths.
|
|
354
|
+
*/
|
|
355
|
+
export declare function unmanagedHookNames(installedHookNames: string[], sourceHookScripts: string[]): string[];
|
|
356
|
+
/**
|
|
357
|
+
* Every hook script path across the resolved SOURCE roots — user
|
|
358
|
+
* (`~/.agents/hooks`), system (`~/.agents/.system/hooks`), and each enabled
|
|
359
|
+
* extra repo's `hooks/` — grouped the same way sync materializes them
|
|
360
|
+
* ({@link listHookEntriesFromDir}, which also descends one-level event-group
|
|
361
|
+
* dirs). This is the set an installed hook must be absent from to count as an
|
|
362
|
+
* orphan.
|
|
346
363
|
*/
|
|
347
|
-
export declare function
|
|
364
|
+
export declare function listResolvedSourceHookScripts(): string[];
|
|
348
365
|
/**
|
|
349
|
-
* The
|
|
350
|
-
*
|
|
351
|
-
* shadow/override warnings the registrar path prints.
|
|
366
|
+
* The orphan hooks (see {@link unmanagedHookNames}) sitting in one version home:
|
|
367
|
+
* installed hook files whose name matches no file in any configured source root.
|
|
352
368
|
*/
|
|
353
369
|
export declare function listUnmanagedHooksInVersionHome(agent: AgentId, version: string): string[];
|
|
354
370
|
/**
|
|
@@ -1594,33 +1594,58 @@ export function selectHookManifest(manifest, selected) {
|
|
|
1594
1594
|
return Object.fromEntries(Object.entries(manifest).filter(([name, hook]) => selectedHooks.has(name) || selectedHooks.has(path.basename(hook.script))));
|
|
1595
1595
|
}
|
|
1596
1596
|
/**
|
|
1597
|
-
* Hook
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1597
|
+
* Hook files present in a version home but absent from every configured
|
|
1598
|
+
* SOURCE — genuine orphans left behind by a removed/renamed source hook.
|
|
1599
|
+
*
|
|
1600
|
+
* The definition is deliberately source-based, not manifest-based (PHNX-2693).
|
|
1601
|
+
* Sync copies EVERY source hook file into a version home — registered hooks AND
|
|
1602
|
+
* the helper / test / benchmark scripts that sit alongside them — but only the
|
|
1603
|
+
* registered ones appear in `parseHookManifest`. Diffing installed names against
|
|
1604
|
+
* the manifest therefore flagged every source-present-but-unregistered file
|
|
1605
|
+
* (e.g. `permission-handler`, `verify-work-state`, `*_test`, `benchmark_*`) as
|
|
1606
|
+
* an orphan, so `agents prune cleanup` offered to trash ~1000 in-use files. An
|
|
1607
|
+
* installed hook is only truly orphaned when NO configured source still carries
|
|
1608
|
+
* a file of that name — which is exactly what the `prune`/`doctor` help already
|
|
1609
|
+
* promised ("present in a version home but missing from every configured
|
|
1610
|
+
* source").
|
|
1603
1611
|
*
|
|
1604
1612
|
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
1605
|
-
* installed hook names and the
|
|
1613
|
+
* installed hook names and the source hook script paths.
|
|
1614
|
+
*/
|
|
1615
|
+
export function unmanagedHookNames(installedHookNames, sourceHookScripts) {
|
|
1616
|
+
const inSource = new Set(sourceHookScripts.map((s) => path.basename(s).replace(/\.[^.]+$/, '')));
|
|
1617
|
+
return installedHookNames.filter((name) => !inSource.has(name)).sort();
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Every hook script path across the resolved SOURCE roots — user
|
|
1621
|
+
* (`~/.agents/hooks`), system (`~/.agents/.system/hooks`), and each enabled
|
|
1622
|
+
* extra repo's `hooks/` — grouped the same way sync materializes them
|
|
1623
|
+
* ({@link listHookEntriesFromDir}, which also descends one-level event-group
|
|
1624
|
+
* dirs). This is the set an installed hook must be absent from to count as an
|
|
1625
|
+
* orphan.
|
|
1606
1626
|
*/
|
|
1607
|
-
export function
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1627
|
+
export function listResolvedSourceHookScripts() {
|
|
1628
|
+
const roots = [
|
|
1629
|
+
getUserHooksDir(),
|
|
1630
|
+
getSystemHooksDir(),
|
|
1631
|
+
...getEnabledExtraRepos().map((e) => path.join(e.dir, 'hooks')),
|
|
1632
|
+
];
|
|
1633
|
+
const scripts = [];
|
|
1634
|
+
for (const root of roots) {
|
|
1635
|
+
for (const entry of listHookEntriesFromDir(root))
|
|
1636
|
+
scripts.push(entry.scriptPath);
|
|
1637
|
+
}
|
|
1638
|
+
return scripts;
|
|
1610
1639
|
}
|
|
1611
1640
|
/**
|
|
1612
|
-
* The
|
|
1613
|
-
*
|
|
1614
|
-
* shadow/override warnings the registrar path prints.
|
|
1641
|
+
* The orphan hooks (see {@link unmanagedHookNames}) sitting in one version home:
|
|
1642
|
+
* installed hook files whose name matches no file in any configured source root.
|
|
1615
1643
|
*/
|
|
1616
1644
|
export function listUnmanagedHooksInVersionHome(agent, version) {
|
|
1617
1645
|
if (!AGENTS[agent].supportsHooks)
|
|
1618
1646
|
return [];
|
|
1619
|
-
const scripts = Object.values(parseHookManifest({ warn: false }))
|
|
1620
|
-
.map((h) => h.script)
|
|
1621
|
-
.filter((s) => typeof s === 'string');
|
|
1622
1647
|
const installed = listHooksInVersionHome(agent, version).map((e) => e.name);
|
|
1623
|
-
return unmanagedHookNames(installed,
|
|
1648
|
+
return unmanagedHookNames(installed, listResolvedSourceHookScripts());
|
|
1624
1649
|
}
|
|
1625
1650
|
// Codex events that support a matcher field (matches tool name or session type).
|
|
1626
1651
|
// UserPromptSubmit and Stop never include a matcher.
|