@phnx-labs/agents-cli 1.20.13 → 1.20.15
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 +17 -0
- package/dist/commands/doctor.js +51 -7
- package/dist/commands/inspect.d.ts +17 -0
- package/dist/commands/inspect.js +277 -28
- package/dist/commands/prune.js +8 -5
- package/dist/commands/repo.js +22 -1
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +56 -1
- package/dist/lib/browser/service.js +28 -18
- package/dist/lib/hooks.d.ts +21 -1
- package/dist/lib/hooks.js +32 -2
- package/dist/lib/plugins.d.ts +10 -0
- package/dist/lib/plugins.js +14 -2
- package/dist/lib/rotate.d.ts +7 -0
- package/dist/lib/rotate.js +17 -7
- package/dist/lib/shims.js +12 -2
- package/dist/lib/state.d.ts +18 -0
- package/dist/lib/state.js +73 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
**`agents inspect` summary: expanded detail for hooks, plugins, and MCP**
|
|
6
|
+
|
|
7
|
+
- The bare `agents inspect <agent>` / `agents inspect <repo>` summary no longer collapses everything to a count table. Simple kinds (commands, skills, rules, subagents, workflows) keep a count line but now preview a few names; the rich kinds get their own expanded sections: **hooks** show their events + `matches:` predicates + cache (`PreToolUse(Bash) · git_dirty · prompt~"deploy" (5m cache)`), **plugins** show version + bundle contents (`v2.1.0 skills:6 commands:5 hooks:2 mcp:1`), and **MCP** show transport + url/command. Drill-down flags (`--hooks`, `--plugins`, `--mcp`) and `--brief` are unchanged; `--json` gains the structured detail additively (existing keys retained).
|
|
8
|
+
- Hook detail joins installed hooks to the manifest by **script basename** (installed hooks are named after their script file while the manifest keys on the logical name), and the repo Hooks section uses the grouped hook reader so a script + its data file collapse to one clean entry.
|
|
9
|
+
|
|
10
|
+
**Plugin hooks were misreported — fixed**
|
|
11
|
+
|
|
12
|
+
- `discoverPluginHooks` read the **top-level** keys of a plugin's `hooks/hooks.json`, so the official `{ description, hooks: { SessionStart: [...] } }` format surfaced as `description, hooks` instead of the real events. It now reads the `hooks` wrapper when present (falling back to top-level keys for the flat format), so `agents inspect --plugin <name>` and the plugin row show the actual lifecycle events (e.g. `SessionStart, PreToolUse, …`).
|
|
13
|
+
|
|
14
|
+
**`agents doctor` / `agents prune`: precise orphan-hooks detection**
|
|
15
|
+
|
|
16
|
+
- Orphan-hook detection now flags hook scripts present in a version home that **no `agents.yaml`/`hooks.yaml` entry registers** — i.e. scripts that sync to disk but are never wired to a lifecycle event, so they never fire. This replaces the source-diff heuristic, which compared only against the user hooks dir and so **false-flagged valid system-sourced, registered hooks** (e.g. `03-linear-inject`, `04-capture`) as orphans — meaning `agents prune cleanup` could have deleted live hooks. Doctor's Orphans section and `prune cleanup hooks` now share this single manifest-based definition. `parseHookManifest` gained a silent (`{ warn: false }`) option so the diagnostic doesn't emit shadow/override warnings.
|
|
17
|
+
|
|
18
|
+
**Regression coverage: resource sync from extras repos**
|
|
19
|
+
|
|
20
|
+
- Added end-to-end regression tests (`src/lib/__tests__/extras-sync.test.ts`) locking in two behaviors for repos registered via `agents repo add` (`~/.agents-<alias>/`): a top-level `commands/<name>.md` is written into the agent's version home on `agents sync`, and plugins under `plugins/<name>/` are synthesized into a registered `agents-<alias>` marketplace on launch. Both already work in `main`; the tests exercise the real sync path (no mocking, isolated `$HOME`) so the extras-repo behavior can't silently regress (#313, #314).
|
|
21
|
+
|
|
5
22
|
**Windows: `agents` is discoverable right after `npm i -g`**
|
|
6
23
|
|
|
7
24
|
- On a global Windows install, postinstall now prepends npm's global-bin dir (where `agents.cmd`/`agents.ps1` live) to the **User PATH** via the .NET environment API. Node's installer normally adds it, but winget / portable / nvm-windows setups often don't — and then `npm i -g @phnx-labs/agents-cli` succeeds yet `agents` is "not recognized". The shims dir (claude/codex/…) is still left to `agents setup`, which the user can now run because `agents` resolves.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,9 +5,10 @@ import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalled
|
|
|
5
5
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
6
6
|
import { diffVersionCommands, iterCommandsCapableVersions } from '../lib/commands.js';
|
|
7
7
|
import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
|
|
8
|
-
import {
|
|
8
|
+
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from '../lib/hooks.js';
|
|
9
9
|
import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
10
10
|
import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
|
|
11
|
+
import { listCliStatus } from '../lib/cli-resources.js';
|
|
11
12
|
import { setHelpSections } from '../lib/help.js';
|
|
12
13
|
import * as fs from 'fs';
|
|
13
14
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
@@ -53,16 +54,20 @@ function countOrphans() {
|
|
|
53
54
|
if (diff.orphans.length > 0)
|
|
54
55
|
ensure(agent, version).skills = diff.orphans.length;
|
|
55
56
|
}
|
|
57
|
+
// Orphan hooks are scripts in the version home that no agents.yaml/hooks.yaml
|
|
58
|
+
// entry registers — so the registrar never wires them to an event and they
|
|
59
|
+
// never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
|
|
60
|
+
// which false-flags valid system-sourced registered hooks.)
|
|
56
61
|
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
57
62
|
if (version !== getGlobalDefault(agent))
|
|
58
63
|
continue;
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
ensure(agent, version).hooks =
|
|
64
|
+
const dead = listUnmanagedHooksInVersionHome(agent, version);
|
|
65
|
+
if (dead.length > 0)
|
|
66
|
+
ensure(agent, version).hooks = dead.length;
|
|
62
67
|
}
|
|
63
68
|
return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
|
|
64
69
|
}
|
|
65
|
-
function renderOverviewText(clis, syncRows, orphanRows) {
|
|
70
|
+
function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
66
71
|
console.log(chalk.bold('Agent CLIs'));
|
|
67
72
|
if (Object.keys(clis).length === 0) {
|
|
68
73
|
console.log(chalk.gray(' (no agents reported)'));
|
|
@@ -116,6 +121,31 @@ function renderOverviewText(clis, syncRows, orphanRows) {
|
|
|
116
121
|
}
|
|
117
122
|
console.log(chalk.gray(' Run `agents prune cleanup` to remove.'));
|
|
118
123
|
}
|
|
124
|
+
console.log();
|
|
125
|
+
// Host CLIs are host-global (declared in any DotAgents repo's cli/, installed
|
|
126
|
+
// to PATH — not synced into version homes), so they live in the overview, not
|
|
127
|
+
// the per-version resource diff. Source tag shows which repo layer declared
|
|
128
|
+
// each, including user-level and extra repos.
|
|
129
|
+
console.log(chalk.bold('Host CLIs'));
|
|
130
|
+
if (hostClis.statuses.length === 0) {
|
|
131
|
+
console.log(chalk.gray(' (none declared — add one with `agents cli add <name>`)'));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const nameWidth = Math.max(...hostClis.statuses.map((s) => s.manifest.name.length));
|
|
135
|
+
for (const { manifest, installed } of hostClis.statuses) {
|
|
136
|
+
const label = manifest.name.padEnd(nameWidth);
|
|
137
|
+
const src = chalk.gray(`[${manifest.source}]`);
|
|
138
|
+
if (installed) {
|
|
139
|
+
console.log(` ${chalk.green('ready')} ${label} ${src} ${chalk.gray(manifest.description || '')}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(` ${chalk.red('miss ')} ${label} ${src} ${chalk.gray(`not installed — run \`agents cli install ${manifest.name}\``)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const err of hostClis.errors) {
|
|
147
|
+
console.log(` ${chalk.red('err ')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
148
|
+
}
|
|
119
149
|
}
|
|
120
150
|
function parseTargetArg(arg) {
|
|
121
151
|
const at = arg.indexOf('@');
|
|
@@ -346,11 +376,25 @@ export function registerDoctorCommand(program) {
|
|
|
346
376
|
const clis = checkAllClis();
|
|
347
377
|
const syncRows = checkSyncStatus(cwd);
|
|
348
378
|
const orphanRows = countOrphans();
|
|
379
|
+
const hostClis = listCliStatus(cwd);
|
|
349
380
|
if (opts.json) {
|
|
350
|
-
console.log(JSON.stringify({
|
|
381
|
+
console.log(JSON.stringify({
|
|
382
|
+
clis,
|
|
383
|
+
sync: syncRows,
|
|
384
|
+
orphans: orphanRows,
|
|
385
|
+
hostClis: {
|
|
386
|
+
statuses: hostClis.statuses.map((s) => ({
|
|
387
|
+
name: s.manifest.name,
|
|
388
|
+
source: s.manifest.source,
|
|
389
|
+
description: s.manifest.description ?? null,
|
|
390
|
+
installed: s.installed,
|
|
391
|
+
})),
|
|
392
|
+
errors: hostClis.errors,
|
|
393
|
+
},
|
|
394
|
+
}, null, 2));
|
|
351
395
|
return;
|
|
352
396
|
}
|
|
353
|
-
renderOverviewText(clis, syncRows, orphanRows);
|
|
397
|
+
renderOverviewText(clis, syncRows, orphanRows, hostClis);
|
|
354
398
|
return;
|
|
355
399
|
}
|
|
356
400
|
const parsed = parseTargetArg(target);
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* AGENT.md / the file itself) so users can click straight to the source.
|
|
13
13
|
*/
|
|
14
14
|
import { Command } from 'commander';
|
|
15
|
+
import type { ManifestHook } from '../lib/types.js';
|
|
16
|
+
import { type McpYamlConfig } from '../lib/mcp.js';
|
|
15
17
|
import { type PluginResourceGroup } from '../lib/plugins.js';
|
|
16
18
|
/** Resource kinds the inspect command can drill into. */
|
|
17
19
|
declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
|
|
@@ -101,4 +103,19 @@ export interface RepoGitInfo {
|
|
|
101
103
|
behind: number | null;
|
|
102
104
|
}
|
|
103
105
|
export declare function repoGitInfo(root: string): RepoGitInfo | null;
|
|
106
|
+
/**
|
|
107
|
+
* Compact one-liner for a hook from its manifest entry: the firing events (with
|
|
108
|
+
* the matcher/tool-name in parens), then a `·`-separated predicate summary, then
|
|
109
|
+
* an optional cache tail. Plain text — the caller applies color.
|
|
110
|
+
*/
|
|
111
|
+
export declare function summarizeHook(hook: ManifestHook): string;
|
|
112
|
+
/** Compact one-liner for an MCP server: padded transport + the url (http) or command line (stdio). */
|
|
113
|
+
export declare function summarizeMcp(cfg: McpYamlConfig): string;
|
|
114
|
+
/**
|
|
115
|
+
* Index a hook manifest by script basename (no extension). Installed hooks are
|
|
116
|
+
* named after their script file (`04-capture-…`), while the manifest is keyed by
|
|
117
|
+
* logical name (`capture-…`) with the filename in `script:` — so we join on the
|
|
118
|
+
* script basename, not the manifest key.
|
|
119
|
+
*/
|
|
120
|
+
export declare function hookManifestByScript(manifest: Record<string, ManifestHook>): Map<string, ManifestHook>;
|
|
104
121
|
export {};
|
package/dist/commands/inspect.js
CHANGED
|
@@ -23,6 +23,8 @@ import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ge
|
|
|
23
23
|
import { getVersionHomePath } from '../lib/versions.js';
|
|
24
24
|
import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
|
|
25
25
|
import { getAgentResources, listResources, } from '../lib/resources.js';
|
|
26
|
+
import { listHookEntriesFromDir } from '../lib/hooks.js';
|
|
27
|
+
import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js';
|
|
26
28
|
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
|
|
27
29
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
28
30
|
import { countSessionsInScope } from '../lib/session/discover.js';
|
|
@@ -38,6 +40,14 @@ const DRILLABLE_KINDS = [
|
|
|
38
40
|
'workflows',
|
|
39
41
|
'subagents',
|
|
40
42
|
];
|
|
43
|
+
/**
|
|
44
|
+
* Summary-view partition. SIMPLE kinds render as a one-line count + name preview;
|
|
45
|
+
* RICH kinds (hooks/plugins/mcp) get their own expanded section showing each
|
|
46
|
+
* item's key detail (events/predicates, bundle contents, transport/url). Together
|
|
47
|
+
* they cover every DrillableKind.
|
|
48
|
+
*/
|
|
49
|
+
const SIMPLE_KINDS = ['commands', 'skills', 'rules', 'subagents', 'workflows'];
|
|
50
|
+
const RICH_KINDS = ['hooks', 'plugins', 'mcp'];
|
|
41
51
|
/**
|
|
42
52
|
* Singular aliases for the plural drill-down flags. `--plugin code` reads as
|
|
43
53
|
* "show the one plugin named code" — a required-value flag that always lands in
|
|
@@ -355,6 +365,9 @@ function renderRepoSummary(repo, options) {
|
|
|
355
365
|
const manifest = repoManifestSummary(repo.root);
|
|
356
366
|
const kindData = {};
|
|
357
367
|
let totalBytes = 0, totalFiles = 0;
|
|
368
|
+
let repoHookByScript = new Map();
|
|
369
|
+
let repoHookItemList = [];
|
|
370
|
+
let repoMcpConfigs = new Map();
|
|
358
371
|
if (!options.brief) {
|
|
359
372
|
for (const kind of DRILLABLE_KINDS) {
|
|
360
373
|
const items = collectRepoKind(repo, kind);
|
|
@@ -363,6 +376,9 @@ function renderRepoSummary(repo, options) {
|
|
|
363
376
|
totalBytes += size.bytes;
|
|
364
377
|
totalFiles += size.files;
|
|
365
378
|
}
|
|
379
|
+
repoHookByScript = hookManifestByScript(hookManifestFromFile(path.join(repo.root, 'agents.yaml')));
|
|
380
|
+
repoHookItemList = repoHookItems(repo);
|
|
381
|
+
repoMcpConfigs = new Map(discoverMcpConfigsFromRepo(repo.root).map(s => [s.name, s.config]));
|
|
366
382
|
}
|
|
367
383
|
if (options.json) {
|
|
368
384
|
console.log(JSON.stringify({
|
|
@@ -372,12 +388,34 @@ function renderRepoSummary(repo, options) {
|
|
|
372
388
|
manifests,
|
|
373
389
|
manifest,
|
|
374
390
|
size: options.brief ? null : { bytes: totalBytes, files: totalFiles },
|
|
375
|
-
resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
391
|
+
resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => {
|
|
392
|
+
const size = kindData[kind].size;
|
|
393
|
+
// Hooks use the grouped reader (clean names) instead of the raw readdir.
|
|
394
|
+
const items = kind === 'hooks' ? repoHookItemList : kindData[kind].items;
|
|
395
|
+
const base = {
|
|
396
|
+
count: items.length,
|
|
397
|
+
bytes: size.bytes,
|
|
398
|
+
files: size.files,
|
|
399
|
+
names: items.map(i => i.name),
|
|
400
|
+
};
|
|
401
|
+
if (kind === 'hooks')
|
|
402
|
+
return [kind, { ...base, items: items.map(i => {
|
|
403
|
+
const h = repoHookByScript.get(i.name);
|
|
404
|
+
return { name: i.name, events: h?.events ?? [], matcher: h?.matcher, matches: h?.matches, cache: h?.cache };
|
|
405
|
+
}) }];
|
|
406
|
+
if (kind === 'mcp')
|
|
407
|
+
return [kind, { ...base, items: items.map(i => {
|
|
408
|
+
const c = repoMcpConfigs.get(i.name);
|
|
409
|
+
return { name: i.name, transport: c?.transport, url: c?.url, command: c?.command, args: c?.args };
|
|
410
|
+
}) }];
|
|
411
|
+
if (kind === 'plugins')
|
|
412
|
+
return [kind, { ...base, items: items.map(i => ({
|
|
413
|
+
name: i.name,
|
|
414
|
+
version: i.extra?.find(([k]) => k === 'version')?.[1],
|
|
415
|
+
groups: Object.fromEntries((i.groups ?? []).map(g => [g.label, g.items.length])),
|
|
416
|
+
})) }];
|
|
417
|
+
return [kind, base];
|
|
418
|
+
})),
|
|
381
419
|
}, null, 2));
|
|
382
420
|
return;
|
|
383
421
|
}
|
|
@@ -416,13 +454,16 @@ function renderRepoSummary(repo, options) {
|
|
|
416
454
|
if (!options.brief) {
|
|
417
455
|
console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} files`);
|
|
418
456
|
console.log('\n' + chalk.bold('Resources'));
|
|
419
|
-
for (const kind of
|
|
457
|
+
for (const kind of SIMPLE_KINDS) {
|
|
420
458
|
const { items, size } = kindData[kind];
|
|
421
459
|
const count = String(items.length).padStart(4);
|
|
422
460
|
const sz = items.length > 0 ? formatBytes(size.bytes).padStart(8) : ''.padEnd(8);
|
|
423
461
|
const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 4), 60)) : '';
|
|
424
462
|
console.log(` ${kind.padEnd(10)} ${count} ${sz} ${preview}`.trimEnd());
|
|
425
463
|
}
|
|
464
|
+
printExpandedSection('Hooks', hookRows(repoHookItemList, repoHookByScript));
|
|
465
|
+
printExpandedSection('Plugins', pluginRows(kindData.plugins.items));
|
|
466
|
+
printExpandedSection('MCP', mcpRows(kindData.mcp.items, repoMcpConfigs));
|
|
426
467
|
}
|
|
427
468
|
console.log('');
|
|
428
469
|
console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
|
|
@@ -480,7 +521,9 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
480
521
|
const shimPath = path.join(getShimsDir(), AGENTS[agent].cliCommand);
|
|
481
522
|
const aliasPath = getVersionedAliasPath(agent, version);
|
|
482
523
|
const capabilities = collectCapabilities(agent, version);
|
|
483
|
-
const
|
|
524
|
+
const itemsByKind = options.brief ? null : collectItemsByKind(agent, versionHome);
|
|
525
|
+
const hookByScript = options.brief ? null : hookManifestByScript(loadCentralHookManifest());
|
|
526
|
+
const mcpConfigs = options.brief ? null : new Map(listMcpServerConfigs().map(s => [s.name, s.config]));
|
|
484
527
|
const sessions = options.brief ? null : {
|
|
485
528
|
total: safeCountSessions(agent),
|
|
486
529
|
};
|
|
@@ -496,7 +539,7 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
496
539
|
strategy,
|
|
497
540
|
installedShim: cliState?.installed === true ? cliState.path : null,
|
|
498
541
|
capabilities,
|
|
499
|
-
resources:
|
|
542
|
+
resources: itemsByKind ? summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs) : null,
|
|
500
543
|
sessions,
|
|
501
544
|
};
|
|
502
545
|
console.log(JSON.stringify(json, null, 2));
|
|
@@ -521,15 +564,14 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
521
564
|
const reason = res.ok ? '' : chalk.gray(`(${res.reason}${res.need ? ' ' + res.need : ''})`);
|
|
522
565
|
console.log(` ${cap.padEnd(10)} ${mark} ${reason}`);
|
|
523
566
|
}
|
|
524
|
-
if (
|
|
567
|
+
if (itemsByKind) {
|
|
525
568
|
console.log('\n' + chalk.bold('Resources'));
|
|
526
|
-
for (const kind of
|
|
527
|
-
|
|
528
|
-
if (!c)
|
|
529
|
-
continue;
|
|
530
|
-
const breakdown = formatScopeBreakdown(c.bySource);
|
|
531
|
-
console.log(` ${kind.padEnd(10)} ${String(c.total).padStart(4)} ${chalk.gray(breakdown)}`);
|
|
569
|
+
for (const kind of SIMPLE_KINDS) {
|
|
570
|
+
printSimpleResourceRow(kind, itemsByKind[kind]);
|
|
532
571
|
}
|
|
572
|
+
printExpandedSection('Hooks', hookRows(itemsByKind.hooks, hookByScript));
|
|
573
|
+
printExpandedSection('Plugins', pluginRows(itemsByKind.plugins));
|
|
574
|
+
printExpandedSection('MCP', mcpRows(itemsByKind.mcp, mcpConfigs));
|
|
533
575
|
}
|
|
534
576
|
if (sessions) {
|
|
535
577
|
console.log('\n' + chalk.bold('Sessions'));
|
|
@@ -649,14 +691,52 @@ function collectCapabilities(agent, version) {
|
|
|
649
691
|
}
|
|
650
692
|
return out;
|
|
651
693
|
}
|
|
652
|
-
function
|
|
694
|
+
function collectItemsByKind(agent, versionHome) {
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const kind of DRILLABLE_KINDS)
|
|
697
|
+
out[kind] = collectKind(agent, versionHome, kind);
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
/** A simple-kind count row: `kind N user:30 system:12 name, name …(+K)`. */
|
|
701
|
+
function printSimpleResourceRow(kind, items) {
|
|
702
|
+
const count = String(items.length).padStart(4);
|
|
703
|
+
const breakdown = chalk.gray(scopeBreakdownPlain(countBySource(items.map(i => i.source))).padEnd(18));
|
|
704
|
+
const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 3), 48)) : '';
|
|
705
|
+
console.log(` ${kind.padEnd(10)} ${count} ${breakdown} ${preview}`.trimEnd());
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Build the `resources` JSON: every kind keeps `total` + `bySource` (back-compat),
|
|
709
|
+
* simple kinds add `names`, and the rich kinds add structured `items` (hook
|
|
710
|
+
* events/predicates, mcp transport/url/command, plugin version + group counts).
|
|
711
|
+
*/
|
|
712
|
+
function summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs) {
|
|
653
713
|
const out = {};
|
|
654
714
|
for (const kind of DRILLABLE_KINDS) {
|
|
655
|
-
const items =
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
715
|
+
const items = itemsByKind[kind];
|
|
716
|
+
const base = { total: items.length, bySource: countBySource(items.map(i => i.source)) };
|
|
717
|
+
if (kind === 'hooks') {
|
|
718
|
+
out[kind] = { ...base, items: items.map(i => {
|
|
719
|
+
const h = hookByScript.get(i.name);
|
|
720
|
+
return { name: i.name, source: i.source, events: h?.events ?? [], matcher: h?.matcher, matches: h?.matches, cache: h?.cache };
|
|
721
|
+
}) };
|
|
722
|
+
}
|
|
723
|
+
else if (kind === 'mcp') {
|
|
724
|
+
out[kind] = { ...base, items: items.map(i => {
|
|
725
|
+
const c = mcpConfigs.get(i.name);
|
|
726
|
+
return { name: i.name, source: i.source, transport: c?.transport, url: c?.url, command: c?.command, args: c?.args };
|
|
727
|
+
}) };
|
|
728
|
+
}
|
|
729
|
+
else if (kind === 'plugins') {
|
|
730
|
+
out[kind] = { ...base, items: items.map(i => ({
|
|
731
|
+
name: i.name,
|
|
732
|
+
source: i.source,
|
|
733
|
+
version: i.extra?.find(([k]) => k === 'version')?.[1],
|
|
734
|
+
groups: Object.fromEntries((i.groups ?? []).map(g => [g.label, g.items.length])),
|
|
735
|
+
})) };
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
out[kind] = { ...base, names: items.map(i => i.name) };
|
|
739
|
+
}
|
|
660
740
|
}
|
|
661
741
|
return out;
|
|
662
742
|
}
|
|
@@ -782,6 +862,181 @@ function buildDetailRows(item, kind) {
|
|
|
782
862
|
}
|
|
783
863
|
return rows;
|
|
784
864
|
}
|
|
865
|
+
/** `system` → `sys`; everything else unchanged. Keeps the tag column narrow. */
|
|
866
|
+
function abbrevSource(s) {
|
|
867
|
+
return s === 'system' ? 'sys' : s;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Compact one-liner for a hook from its manifest entry: the firing events (with
|
|
871
|
+
* the matcher/tool-name in parens), then a `·`-separated predicate summary, then
|
|
872
|
+
* an optional cache tail. Plain text — the caller applies color.
|
|
873
|
+
*/
|
|
874
|
+
export function summarizeHook(hook) {
|
|
875
|
+
const events = (hook.events ?? []).join('/') || '(no event)';
|
|
876
|
+
let matcher = hook.matcher;
|
|
877
|
+
if (!matcher && hook.matches?.tool_name) {
|
|
878
|
+
const tn = hook.matches.tool_name;
|
|
879
|
+
matcher = Array.isArray(tn) ? tn.join('|') : tn;
|
|
880
|
+
}
|
|
881
|
+
const head = matcher ? `${events}(${matcher})` : events;
|
|
882
|
+
const parts = [head];
|
|
883
|
+
const preds = summarizeMatches(hook.matches);
|
|
884
|
+
if (preds)
|
|
885
|
+
parts.push(preds);
|
|
886
|
+
let line = parts.join(' · ');
|
|
887
|
+
const ttl = hookCacheTtl(hook.cache);
|
|
888
|
+
if (ttl)
|
|
889
|
+
line += ` (${ttl} cache)`;
|
|
890
|
+
return line;
|
|
891
|
+
}
|
|
892
|
+
/** `·`-separated predicate summary from a hook's `matches:` block (tool_name omitted — shown in the matcher parens). */
|
|
893
|
+
function summarizeMatches(m) {
|
|
894
|
+
if (!m)
|
|
895
|
+
return '';
|
|
896
|
+
const bits = [];
|
|
897
|
+
if (m.git_dirty)
|
|
898
|
+
bits.push('git_dirty');
|
|
899
|
+
if (m.prompt_contains)
|
|
900
|
+
bits.push(`prompt~"${truncate(m.prompt_contains, 24)}"`);
|
|
901
|
+
if (m.prompt_matches)
|
|
902
|
+
bits.push(`prompt=/${truncate(m.prompt_matches, 24)}/`);
|
|
903
|
+
if (m.tool_args_match)
|
|
904
|
+
bits.push(`args=/${truncate(m.tool_args_match, 20)}/`);
|
|
905
|
+
if (m.cwd_includes) {
|
|
906
|
+
const c = Array.isArray(m.cwd_includes) ? m.cwd_includes.join('|') : m.cwd_includes;
|
|
907
|
+
bits.push(`cwd~${truncate(c, 24)}`);
|
|
908
|
+
}
|
|
909
|
+
if (m.project_has)
|
|
910
|
+
bits.push(`has ${m.project_has}`);
|
|
911
|
+
return bits.join(' · ');
|
|
912
|
+
}
|
|
913
|
+
/** Normalize a hook cache shorthand/object to a display ttl ("5m", "1h"); null when uncached. */
|
|
914
|
+
function hookCacheTtl(cache) {
|
|
915
|
+
if (cache === undefined || cache === null)
|
|
916
|
+
return null;
|
|
917
|
+
if (typeof cache === 'string')
|
|
918
|
+
return cache.replace(/-bg$/, '');
|
|
919
|
+
return String(cache.ttl);
|
|
920
|
+
}
|
|
921
|
+
/** Compact one-liner for an MCP server: padded transport + the url (http) or command line (stdio). */
|
|
922
|
+
export function summarizeMcp(cfg) {
|
|
923
|
+
const target = cfg.transport === 'http'
|
|
924
|
+
? (cfg.url ?? '')
|
|
925
|
+
: [cfg.command, ...(cfg.args ?? [])].filter(Boolean).join(' ');
|
|
926
|
+
return `${cfg.transport.padEnd(5)} ${truncate(target, 60)}`.trimEnd();
|
|
927
|
+
}
|
|
928
|
+
/** Print `Title (N)` then up to `max` aligned `[source] name detail` rows with a `…(+K)` tail. */
|
|
929
|
+
function printExpandedSection(title, rows, max = 6) {
|
|
930
|
+
console.log('\n' + chalk.bold(title) + chalk.gray(` (${rows.length})`));
|
|
931
|
+
if (rows.length === 0) {
|
|
932
|
+
console.log(chalk.gray(' (none)'));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const shown = rows.slice(0, max);
|
|
936
|
+
const nameW = Math.max(...shown.map(r => r.name.length));
|
|
937
|
+
for (const r of shown) {
|
|
938
|
+
const tag = chalk.gray(`[${abbrevSource(r.source)}]`.padEnd(8));
|
|
939
|
+
const padded = r.name.padEnd(nameW);
|
|
940
|
+
const name = r.linkTarget ? termLink(chalk.cyan(padded), r.linkTarget) : chalk.cyan(padded);
|
|
941
|
+
const detail = r.detail ? ' ' + chalk.gray(r.detail) : '';
|
|
942
|
+
console.log(` ${tag} ${name}${detail}`);
|
|
943
|
+
}
|
|
944
|
+
if (rows.length > max)
|
|
945
|
+
console.log(chalk.gray(` …(+${rows.length - max})`));
|
|
946
|
+
}
|
|
947
|
+
/** Tally a source list into `{user: n, system: m}`. */
|
|
948
|
+
function countBySource(sources) {
|
|
949
|
+
const out = {};
|
|
950
|
+
for (const s of sources)
|
|
951
|
+
out[s] = (out[s] || 0) + 1;
|
|
952
|
+
return out;
|
|
953
|
+
}
|
|
954
|
+
/** Unbracketed scope breakdown for the simple count rows: `user:30 system:12`. */
|
|
955
|
+
function scopeBreakdownPlain(bySource) {
|
|
956
|
+
return Object.entries(bySource).map(([k, v]) => `${k}:${v}`).join(' ');
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Index a hook manifest by script basename (no extension). Installed hooks are
|
|
960
|
+
* named after their script file (`04-capture-…`), while the manifest is keyed by
|
|
961
|
+
* logical name (`capture-…`) with the filename in `script:` — so we join on the
|
|
962
|
+
* script basename, not the manifest key.
|
|
963
|
+
*/
|
|
964
|
+
export function hookManifestByScript(manifest) {
|
|
965
|
+
const out = new Map();
|
|
966
|
+
for (const hook of Object.values(manifest)) {
|
|
967
|
+
if (hook && typeof hook.script === 'string') {
|
|
968
|
+
out.set(path.basename(hook.script).replace(/\.[^.]+$/, ''), hook);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
973
|
+
/** Build hook rows by enriching the installed hook items with manifest events/predicates. */
|
|
974
|
+
function hookRows(items, byScript) {
|
|
975
|
+
return items.map(item => {
|
|
976
|
+
const hook = byScript.get(item.name);
|
|
977
|
+
return {
|
|
978
|
+
source: item.source,
|
|
979
|
+
name: item.name,
|
|
980
|
+
linkTarget: item.linkTarget,
|
|
981
|
+
// Hooks are shell scripts with no human description — show events/predicates
|
|
982
|
+
// from the manifest, or nothing rather than a meaningless shebang line.
|
|
983
|
+
detail: hook ? summarizeHook(hook) : '',
|
|
984
|
+
};
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/** Build plugin rows: `vVERSION skills:6 commands:5 …` from the bundle's groups. */
|
|
988
|
+
function pluginRows(items) {
|
|
989
|
+
return items.map(item => {
|
|
990
|
+
const version = item.extra?.find(([k]) => k === 'version')?.[1];
|
|
991
|
+
const counts = (item.groups ?? []).map(g => `${g.label}:${g.items.length}`).join(' ');
|
|
992
|
+
const detail = [version ? `v${version}` : '', counts].filter(Boolean).join(' ');
|
|
993
|
+
return { source: item.source, name: item.name, detail, linkTarget: item.linkTarget };
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/** Build MCP rows by joining the installed mcp items with their full configs (transport/url/command). */
|
|
997
|
+
function mcpRows(items, configs) {
|
|
998
|
+
return items.map(item => {
|
|
999
|
+
const cfg = configs.get(item.name);
|
|
1000
|
+
return { source: item.source, name: item.name, detail: cfg ? summarizeMcp(cfg) : item.description };
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/** Read a repo/agents.yaml `hooks:` section into a name→ManifestHook map (best-effort). */
|
|
1004
|
+
function hookManifestFromFile(agentsYamlPath) {
|
|
1005
|
+
try {
|
|
1006
|
+
const meta = yaml.parse(fs.readFileSync(agentsYamlPath, 'utf-8'));
|
|
1007
|
+
return meta?.hooks ?? {};
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return {};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Merge the system + user `agents.yaml` hook manifests (user wins on key
|
|
1015
|
+
* collision). Built directly from the two layer files rather than via
|
|
1016
|
+
* `parseHookManifest()` so inspecting never emits the shadow/override warnings
|
|
1017
|
+
* that the registrar path prints.
|
|
1018
|
+
*/
|
|
1019
|
+
function loadCentralHookManifest() {
|
|
1020
|
+
return {
|
|
1021
|
+
...hookManifestFromFile(path.join(getSystemAgentsDir(), 'agents.yaml')),
|
|
1022
|
+
...hookManifestFromFile(path.join(getUserAgentsDir(), 'agents.yaml')),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Hook items for a repo's Hooks section. Uses the grouped hook reader (script +
|
|
1027
|
+
* data file collapsed into one entry, non-hook files like promptcuts.yaml or
|
|
1028
|
+
* README.md filtered out) rather than a naive readdir, so names are clean and
|
|
1029
|
+
* join cleanly against the manifest by script basename.
|
|
1030
|
+
*/
|
|
1031
|
+
function repoHookItems(repo) {
|
|
1032
|
+
return listHookEntriesFromDir(path.join(repo.root, 'hooks')).map(h => ({
|
|
1033
|
+
name: h.name,
|
|
1034
|
+
source: repo.label,
|
|
1035
|
+
path: h.scriptPath,
|
|
1036
|
+
linkTarget: h.scriptPath,
|
|
1037
|
+
description: '',
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
785
1040
|
function findMatches(items, query) {
|
|
786
1041
|
const q = query.toLowerCase();
|
|
787
1042
|
const out = [];
|
|
@@ -979,9 +1234,3 @@ function truncate(s, n) {
|
|
|
979
1234
|
return s;
|
|
980
1235
|
return s.slice(0, n - 1) + '…';
|
|
981
1236
|
}
|
|
982
|
-
function formatScopeBreakdown(bySource) {
|
|
983
|
-
const entries = Object.entries(bySource);
|
|
984
|
-
if (entries.length === 0)
|
|
985
|
-
return '';
|
|
986
|
-
return '[' + entries.map(([k, v]) => `${k}:${v}`).join(' ') + ']';
|
|
987
|
-
}
|
package/dist/commands/prune.js
CHANGED
|
@@ -26,7 +26,7 @@ import chalk from 'chalk';
|
|
|
26
26
|
import { confirm } from '@inquirer/prompts';
|
|
27
27
|
import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
|
|
28
28
|
import { diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
|
|
29
|
-
import {
|
|
29
|
+
import { listUnmanagedHooksInVersionHome, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
|
|
30
30
|
import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins.js';
|
|
31
31
|
import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
|
|
32
32
|
import { getGlobalDefault } from '../lib/versions.js';
|
|
@@ -64,9 +64,12 @@ function collectOrphans(types, all) {
|
|
|
64
64
|
}
|
|
65
65
|
if (types.includes('hooks')) {
|
|
66
66
|
for (const { agent, version } of scopePairs(iterHooksCapableVersions(), all)) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// Orphan hooks = scripts present in the version home that no
|
|
68
|
+
// agents.yaml/hooks.yaml entry registers, so they never fire. Same
|
|
69
|
+
// definition the doctor overview reports.
|
|
70
|
+
const orphans = listUnmanagedHooksInVersionHome(agent, version);
|
|
71
|
+
if (orphans.length > 0) {
|
|
72
|
+
groups.push({ type: 'hooks', agent, version, orphans });
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -210,7 +213,7 @@ async function runOrphanPrune(resourceTypes, options) {
|
|
|
210
213
|
return;
|
|
211
214
|
}
|
|
212
215
|
const total = groups.reduce((n, g) => n + g.orphans.length, 0);
|
|
213
|
-
console.log(chalk.bold('Orphans (in version home,
|
|
216
|
+
console.log(chalk.bold('Orphans (in version home, unmanaged by any source)\n'));
|
|
214
217
|
for (const g of groups) {
|
|
215
218
|
const label = `${g.type} · ${g.agent}@${g.version}`;
|
|
216
219
|
console.log(` ${chalk.cyan(label)} ${g.orphans.join(', ')}`);
|
package/dist/commands/repo.js
CHANGED
|
@@ -23,7 +23,7 @@ function resolveRepoPath(target) {
|
|
|
23
23
|
}
|
|
24
24
|
return path.join(HOME, `.agents-${trimmed}`);
|
|
25
25
|
}
|
|
26
|
-
import { ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
|
|
26
|
+
import { applyExtraAliasToVersions, ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
|
|
27
27
|
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin } from '../lib/git.js';
|
|
28
28
|
import { DEFAULT_SYSTEM_REPO } from '../lib/types.js';
|
|
29
29
|
import { ALL_AGENT_IDS, isAgentName, resolveAgentName } from '../lib/agents.js';
|
|
@@ -259,6 +259,7 @@ export function registerRepoCommands(program) {
|
|
|
259
259
|
const commit = log.latest?.hash.slice(0, 8) || 'unknown';
|
|
260
260
|
extras[alias] = { url: targetDir, path: targetDir, enabled: true };
|
|
261
261
|
updateMeta({ extraRepos: extras });
|
|
262
|
+
syncExtraAliasAcrossVersions(alias, true);
|
|
262
263
|
spinner.succeed(`Created ${targetDir} (${commit})`);
|
|
263
264
|
console.log(chalk.gray(`\nRegistered as "${alias}". Edit files there, then add your own git remote when ready.`));
|
|
264
265
|
}
|
|
@@ -306,6 +307,7 @@ export function registerRepoCommands(program) {
|
|
|
306
307
|
if (parsed.type === 'local') {
|
|
307
308
|
extras[alias] = { url: parsed.url, path: parsed.url, enabled: true };
|
|
308
309
|
updateMeta({ extraRepos: extras });
|
|
310
|
+
syncExtraAliasAcrossVersions(alias, true);
|
|
309
311
|
syncMarketplacesForDefaults();
|
|
310
312
|
console.log(chalk.green(`Registered local repo "${alias}" -> ${parsed.url}`));
|
|
311
313
|
return;
|
|
@@ -342,6 +344,7 @@ export function registerRepoCommands(program) {
|
|
|
342
344
|
}
|
|
343
345
|
extras[alias] = { url: parsed.url, path: targetDir, enabled: true };
|
|
344
346
|
updateMeta({ extraRepos: extras });
|
|
347
|
+
syncExtraAliasAcrossVersions(alias, true);
|
|
345
348
|
syncMarketplacesForDefaults();
|
|
346
349
|
console.log(chalk.gray(`\nRegistered as "${alias}". Skills and commands from this repo will be`));
|
|
347
350
|
console.log(chalk.gray(`picked up automatically the next time you launch any agent.`));
|
|
@@ -379,6 +382,7 @@ export function registerRepoCommands(program) {
|
|
|
379
382
|
}
|
|
380
383
|
delete extras[alias];
|
|
381
384
|
updateMeta({ extraRepos: extras });
|
|
385
|
+
syncExtraAliasAcrossVersions(alias, false);
|
|
382
386
|
syncMarketplacesForDefaults();
|
|
383
387
|
console.log(chalk.green(`Removed "${alias}"`));
|
|
384
388
|
});
|
|
@@ -592,6 +596,19 @@ function collectRepoTargets(alias) {
|
|
|
592
596
|
}
|
|
593
597
|
return [found];
|
|
594
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Keep already-installed versions' selectors in sync with an extra-repo change:
|
|
601
|
+
* add `<alias>:*` when the repo is registered/enabled, strip it when removed.
|
|
602
|
+
* Newly-installed versions inherit it from `defaultPatterns()` at scaffold time,
|
|
603
|
+
* so without this a repo added after install is invisible to existing versions.
|
|
604
|
+
*/
|
|
605
|
+
function syncExtraAliasAcrossVersions(alias, add) {
|
|
606
|
+
const n = applyExtraAliasToVersions(alias, add);
|
|
607
|
+
if (n > 0) {
|
|
608
|
+
const verb = add ? 'Added to' : 'Removed from';
|
|
609
|
+
console.log(chalk.gray(`${verb} ${n} existing version selector${n === 1 ? '' : 's'}.`));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
595
612
|
async function toggle(alias, enabled) {
|
|
596
613
|
const meta = readMeta();
|
|
597
614
|
const extras = { ...(meta.extraRepos || {}) };
|
|
@@ -606,6 +623,10 @@ async function toggle(alias, enabled) {
|
|
|
606
623
|
}
|
|
607
624
|
extras[alias] = { ...extras[alias], enabled };
|
|
608
625
|
updateMeta({ extraRepos: extras });
|
|
626
|
+
// Re-enabling backfills the alias into existing versions; disabling leaves the
|
|
627
|
+
// selectors (resolution skips disabled extras) so a later enable is a no-op.
|
|
628
|
+
if (enabled)
|
|
629
|
+
syncExtraAliasAcrossVersions(alias, true);
|
|
609
630
|
syncMarketplacesForDefaults();
|
|
610
631
|
console.log(chalk.green(`${enabled ? 'Enabled' : 'Disabled'} "${alias}"`));
|
|
611
632
|
}
|
package/dist/commands/view.d.ts
CHANGED
package/dist/commands/view.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readManifest } from '../lib/manifest.js';
|
|
|
8
8
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
|
|
9
9
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
10
10
|
import { getAgentResources } from '../lib/resources.js';
|
|
11
|
+
import { listCliStatus } from '../lib/cli-resources.js';
|
|
11
12
|
import { isCapable } from '../lib/capabilities.js';
|
|
12
13
|
import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins.js';
|
|
13
14
|
import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
|
|
@@ -114,7 +115,7 @@ function getProjectVersionFromCwd(agent) {
|
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
|
-
const SECTION_KEYS = ['commands', 'skills', 'mcp', 'workflows', 'plugins', 'rules', 'hooks', 'promptcuts'];
|
|
118
|
+
const SECTION_KEYS = ['commands', 'skills', 'mcp', 'workflows', 'plugins', 'rules', 'hooks', 'promptcuts', 'cli'];
|
|
118
119
|
/**
|
|
119
120
|
* Decide whether a section should render given the filter. If no flags are set,
|
|
120
121
|
* everything renders (current behavior). If any flag is set, only those sections
|
|
@@ -167,6 +168,51 @@ function renderProfilesSection(profiles) {
|
|
|
167
168
|
* Show installed versions for one or all agents.
|
|
168
169
|
* Called when: `agents view` or `agents view claude`
|
|
169
170
|
*/
|
|
171
|
+
/** Color the source-layer tag for a host CLI, matching the rules-section convention. */
|
|
172
|
+
function hostCliSourceTag(source) {
|
|
173
|
+
if (source === 'project')
|
|
174
|
+
return chalk.blue('[project]');
|
|
175
|
+
if (source === 'user')
|
|
176
|
+
return chalk.cyan('[user]');
|
|
177
|
+
if (source === 'system')
|
|
178
|
+
return chalk.gray('[system]');
|
|
179
|
+
// Anything else is an extra repo, tagged by its alias.
|
|
180
|
+
return chalk.magenta(`[${source}]`);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render the host-CLI section. Host CLIs are host-global: declared in any
|
|
184
|
+
* DotAgents repo's `cli/` (project > user > system > extras), installed to PATH
|
|
185
|
+
* rather than copied into a version home. They render identically in the overview
|
|
186
|
+
* and in a per-agent detail view because every agent on the host shares them.
|
|
187
|
+
* The source tag shows which repo layer declared each — so user-level and
|
|
188
|
+
* extra-repo manifests are visibly supported.
|
|
189
|
+
*/
|
|
190
|
+
function renderHostClisSection(cwd) {
|
|
191
|
+
const { statuses, errors } = listCliStatus(cwd);
|
|
192
|
+
console.log(chalk.bold('\nHost CLIs\n'));
|
|
193
|
+
if (statuses.length === 0) {
|
|
194
|
+
console.log(` ${chalk.gray('none declared')} ${chalk.gray('— add one with `agents cli add <name>`')}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const nameWidth = Math.max(...statuses.map((s) => s.manifest.name.length));
|
|
198
|
+
let anyMissing = false;
|
|
199
|
+
for (const { manifest, installed } of statuses) {
|
|
200
|
+
if (!installed)
|
|
201
|
+
anyMissing = true;
|
|
202
|
+
const status = installed ? chalk.green('installed') : chalk.red('missing ');
|
|
203
|
+
const linkedName = termLink(manifest.name.padEnd(nameWidth), linkTarget(manifest.path));
|
|
204
|
+
const tag = hostCliSourceTag(manifest.source);
|
|
205
|
+
const desc = manifest.description ? chalk.gray(` ${summarizeDescription(manifest.description, 60)}`) : '';
|
|
206
|
+
console.log(` ${status} ${chalk.cyan(linkedName)} ${tag}${desc}`);
|
|
207
|
+
}
|
|
208
|
+
if (anyMissing) {
|
|
209
|
+
console.log(chalk.gray(' Install missing with `agents cli install`'));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const err of errors) {
|
|
213
|
+
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
170
216
|
async function showInstalledVersions(filterAgentId) {
|
|
171
217
|
const spinnerText = filterAgentId
|
|
172
218
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
@@ -537,6 +583,10 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
537
583
|
console.log(chalk.gray(' Run: agents add claude@latest'));
|
|
538
584
|
console.log();
|
|
539
585
|
}
|
|
586
|
+
// Host CLIs are host-global, not per-agent — show them once in the overview.
|
|
587
|
+
if (!filterAgentId) {
|
|
588
|
+
renderHostClisSection(process.cwd());
|
|
589
|
+
}
|
|
540
590
|
// Check for new resources when viewing a specific agent
|
|
541
591
|
if (filterAgentId && versionManaged.length > 0) {
|
|
542
592
|
const defaultVersion = getGlobalDefault(filterAgentId);
|
|
@@ -892,6 +942,9 @@ async function showAgentResources(agentId, requestedVersion, filter) {
|
|
|
892
942
|
if (shouldRenderSection('promptcuts', filter)) {
|
|
893
943
|
renderPromptcuts();
|
|
894
944
|
}
|
|
945
|
+
if (shouldRenderSection('cli', filter)) {
|
|
946
|
+
renderHostClisSection(cwd);
|
|
947
|
+
}
|
|
895
948
|
// Show legend at the end if git repo exists and we showed all sections.
|
|
896
949
|
// Filtered single-section views skip it — noise for promptcuts or plugins.
|
|
897
950
|
if (hasGitRepo && !anyFilterSet) {
|
|
@@ -1183,6 +1236,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1183
1236
|
rules: options?.rules,
|
|
1184
1237
|
hooks: options?.hooks,
|
|
1185
1238
|
promptcuts: options?.promptcuts,
|
|
1239
|
+
cli: options?.cli,
|
|
1186
1240
|
};
|
|
1187
1241
|
const filterIsSet = SECTION_KEYS.some((k) => filter[k]);
|
|
1188
1242
|
if (!agentArg) {
|
|
@@ -1263,6 +1317,7 @@ export function registerViewCommand(program) {
|
|
|
1263
1317
|
.option('--rules', 'Show only rules in the detail view.')
|
|
1264
1318
|
.option('--hooks', 'Show only hooks in the detail view.')
|
|
1265
1319
|
.option('--promptcuts', 'Show only promptcuts in the detail view.')
|
|
1320
|
+
.option('--cli', 'Show only host CLIs (declared in cli/, installed to PATH).')
|
|
1266
1321
|
.addHelpText('after', `
|
|
1267
1322
|
Examples:
|
|
1268
1323
|
# Show all installed agents with versions, accounts, and usage
|
|
@@ -1701,24 +1701,34 @@ export class BrowserService {
|
|
|
1701
1701
|
targetId: tabId,
|
|
1702
1702
|
flatten: true,
|
|
1703
1703
|
}));
|
|
1704
|
-
// Inject a
|
|
1705
|
-
//
|
|
1706
|
-
//
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1709
|
-
//
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1713
|
-
//
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1704
|
+
// Inject a stealth shim before any page script runs. Chromium exposes
|
|
1705
|
+
// navigator.webdriver = true whenever a remote-debug transport is attached;
|
|
1706
|
+
// Cloudflare Turnstile, hCaptcha, and similar bot checks read it first.
|
|
1707
|
+
//
|
|
1708
|
+
// Only attach-to-running profiles (conn.pid === 0 — Comet / Arc / Brave the
|
|
1709
|
+
// user launched themselves) need this. Browsers agents-cli spawns already
|
|
1710
|
+
// carry the --disable-blink-features=AutomationControlled launch flag, which
|
|
1711
|
+
// makes navigator.webdriver a native Navigator.prototype getter returning
|
|
1712
|
+
// false — indistinguishable from an untouched browser. Injecting on top of
|
|
1713
|
+
// that is actively harmful: it defines an OWN getter on the instance, and an
|
|
1714
|
+
// own `webdriver` descriptor (native lives on the prototype) returning
|
|
1715
|
+
// `undefined` (native returns `false`) is itself a tampering signal that
|
|
1716
|
+
// bot.sannysoft.com and similar tests flag as "WebDriver present".
|
|
1717
|
+
//
|
|
1718
|
+
// When we do inject (attach mode), mirror native semantics exactly: define
|
|
1719
|
+
// on Navigator.prototype and return false, so no own descriptor leaks and
|
|
1720
|
+
// the value matches a real browser. Non-page targets (workers, service
|
|
1721
|
+
// workers) reject these calls; swallow the error and keep going.
|
|
1722
|
+
if (conn.pid === 0) {
|
|
1723
|
+
try {
|
|
1724
|
+
await conn.cdp.send('Page.enable', {}, sessionId);
|
|
1725
|
+
await conn.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
1726
|
+
source: "Object.defineProperty(Navigator.prototype,'webdriver',{get:()=>false,configurable:true});",
|
|
1727
|
+
}, sessionId);
|
|
1728
|
+
}
|
|
1729
|
+
catch {
|
|
1730
|
+
// Target doesn't support Page domain — nothing to inject.
|
|
1731
|
+
}
|
|
1722
1732
|
}
|
|
1723
1733
|
conn.sessionCache.set(tabId, sessionId);
|
|
1724
1734
|
return sessionId;
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -123,7 +123,27 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
123
123
|
*
|
|
124
124
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
125
125
|
*/
|
|
126
|
-
export declare function parseHookManifest(
|
|
126
|
+
export declare function parseHookManifest(opts?: {
|
|
127
|
+
warn?: boolean;
|
|
128
|
+
}): Record<string, ManifestHook>;
|
|
129
|
+
/**
|
|
130
|
+
* Hook script files present on disk that no manifest entry declares — "dead"
|
|
131
|
+
* hooks. The registrar only wires manifest-declared hooks into an agent's
|
|
132
|
+
* native config (settings.json / config.toml), matching the installed file to a
|
|
133
|
+
* manifest entry by script basename. So a file whose basename matches no
|
|
134
|
+
* manifest `script:` is never registered: it occupies the hooks dir and shows
|
|
135
|
+
* up in listings, but no lifecycle event ever fires it.
|
|
136
|
+
*
|
|
137
|
+
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
138
|
+
* installed hook names and the manifest's script paths.
|
|
139
|
+
*/
|
|
140
|
+
export declare function unmanagedHookNames(installedHookNames: string[], manifestScripts: string[]): string[];
|
|
141
|
+
/**
|
|
142
|
+
* The dead hooks (see {@link unmanagedHookNames}) sitting in one version home.
|
|
143
|
+
* Reads the merged hook manifest silently — a diagnostic must not emit the
|
|
144
|
+
* shadow/override warnings the registrar path prints.
|
|
145
|
+
*/
|
|
146
|
+
export declare function listUnmanagedHooksInVersionHome(agent: AgentId, version: string): string[];
|
|
127
147
|
export declare function registerHooksToSettings(agentId: AgentId, versionHome: string, hookManifest?: Record<string, ManifestHook>, agentsDirOverride?: string): {
|
|
128
148
|
registered: string[];
|
|
129
149
|
errors: string[];
|
package/dist/lib/hooks.js
CHANGED
|
@@ -650,7 +650,8 @@ export function listCentralHooks() {
|
|
|
650
650
|
*
|
|
651
651
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
652
652
|
*/
|
|
653
|
-
export function parseHookManifest() {
|
|
653
|
+
export function parseHookManifest(opts = {}) {
|
|
654
|
+
const warn = opts.warn !== false;
|
|
654
655
|
const merged = {};
|
|
655
656
|
const systemHooks = {};
|
|
656
657
|
// System layer: hooks: section of agents.yaml (npm-shipped, separate repo).
|
|
@@ -673,7 +674,7 @@ export function parseHookManifest() {
|
|
|
673
674
|
const meta = yaml.parse(fs.readFileSync(userMetaPath, 'utf-8'));
|
|
674
675
|
if (meta?.hooks)
|
|
675
676
|
for (const [name, def] of Object.entries(meta.hooks)) {
|
|
676
|
-
if (systemHooks[name] && def.override !== true) {
|
|
677
|
+
if (warn && systemHooks[name] && def.override !== true) {
|
|
677
678
|
const action = def.enabled === false ? 'disables' : 'shadows';
|
|
678
679
|
console.warn(`[agents hooks] User-layer hook '${name}' ${action} system-shipped hook. Set 'override: true' to silence this warning.`);
|
|
679
680
|
}
|
|
@@ -689,6 +690,35 @@ export function parseHookManifest() {
|
|
|
689
690
|
}
|
|
690
691
|
return merged;
|
|
691
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Hook script files present on disk that no manifest entry declares — "dead"
|
|
695
|
+
* hooks. The registrar only wires manifest-declared hooks into an agent's
|
|
696
|
+
* native config (settings.json / config.toml), matching the installed file to a
|
|
697
|
+
* manifest entry by script basename. So a file whose basename matches no
|
|
698
|
+
* manifest `script:` is never registered: it occupies the hooks dir and shows
|
|
699
|
+
* up in listings, but no lifecycle event ever fires it.
|
|
700
|
+
*
|
|
701
|
+
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
702
|
+
* installed hook names and the manifest's script paths.
|
|
703
|
+
*/
|
|
704
|
+
export function unmanagedHookNames(installedHookNames, manifestScripts) {
|
|
705
|
+
const managed = new Set(manifestScripts.map((s) => path.basename(s).replace(/\.[^.]+$/, '')));
|
|
706
|
+
return installedHookNames.filter((name) => !managed.has(name)).sort();
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* The dead hooks (see {@link unmanagedHookNames}) sitting in one version home.
|
|
710
|
+
* Reads the merged hook manifest silently — a diagnostic must not emit the
|
|
711
|
+
* shadow/override warnings the registrar path prints.
|
|
712
|
+
*/
|
|
713
|
+
export function listUnmanagedHooksInVersionHome(agent, version) {
|
|
714
|
+
if (!AGENTS[agent].supportsHooks)
|
|
715
|
+
return [];
|
|
716
|
+
const scripts = Object.values(parseHookManifest({ warn: false }))
|
|
717
|
+
.map((h) => h.script)
|
|
718
|
+
.filter((s) => typeof s === 'string');
|
|
719
|
+
const installed = listHooksInVersionHome(agent, version).map((e) => e.name);
|
|
720
|
+
return unmanagedHookNames(installed, scripts);
|
|
721
|
+
}
|
|
692
722
|
// Codex events that support a matcher field (matches tool name or session type).
|
|
693
723
|
// UserPromptSubmit and Stop never include a matcher.
|
|
694
724
|
const CODEX_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'SessionStart']);
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -74,6 +74,16 @@ export declare function getPlugin(name: string): DiscoveredPlugin | null;
|
|
|
74
74
|
* Otherwise defaults to all plugin-capable agents.
|
|
75
75
|
*/
|
|
76
76
|
export declare function pluginSupportsAgent(plugin: DiscoveredPlugin, agent: AgentId): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The lifecycle events a plugin hooks into, read from hooks/hooks.json.
|
|
79
|
+
*
|
|
80
|
+
* The official plugin format wraps the event map under a `hooks` key
|
|
81
|
+
* (`{ description, hooks: { SessionStart: [...], PreToolUse: [...] } }`), so the
|
|
82
|
+
* meaningful keys are the events — NOT the top-level keys (`description`,
|
|
83
|
+
* `hooks`). Older/flat files put the event names at the top level directly; we
|
|
84
|
+
* read whichever object actually holds the event map.
|
|
85
|
+
*/
|
|
86
|
+
export declare function discoverPluginHooks(pluginRoot: string): string[];
|
|
77
87
|
/** Discover command .md files inside a plugin's commands/ directory. */
|
|
78
88
|
export declare function discoverPluginCommands(pluginRoot: string): string[];
|
|
79
89
|
/** Discover agent definition .md files inside a plugin's agents/ directory. */
|
package/dist/lib/plugins.js
CHANGED
|
@@ -213,13 +213,25 @@ function discoverPluginSkills(pluginRoot) {
|
|
|
213
213
|
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
214
214
|
.map(d => d.name);
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
/**
|
|
217
|
+
* The lifecycle events a plugin hooks into, read from hooks/hooks.json.
|
|
218
|
+
*
|
|
219
|
+
* The official plugin format wraps the event map under a `hooks` key
|
|
220
|
+
* (`{ description, hooks: { SessionStart: [...], PreToolUse: [...] } }`), so the
|
|
221
|
+
* meaningful keys are the events — NOT the top-level keys (`description`,
|
|
222
|
+
* `hooks`). Older/flat files put the event names at the top level directly; we
|
|
223
|
+
* read whichever object actually holds the event map.
|
|
224
|
+
*/
|
|
225
|
+
export function discoverPluginHooks(pluginRoot) {
|
|
217
226
|
const hooksFile = path.join(pluginRoot, 'hooks', 'hooks.json');
|
|
218
227
|
if (!fs.existsSync(hooksFile))
|
|
219
228
|
return [];
|
|
220
229
|
try {
|
|
221
230
|
const content = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
222
|
-
|
|
231
|
+
const eventMap = content.hooks && typeof content.hooks === 'object' && !Array.isArray(content.hooks)
|
|
232
|
+
? content.hooks
|
|
233
|
+
: content;
|
|
234
|
+
return Object.keys(eventMap);
|
|
223
235
|
}
|
|
224
236
|
catch {
|
|
225
237
|
return [];
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface RotateCandidate {
|
|
|
11
11
|
agent: AgentId;
|
|
12
12
|
version: string;
|
|
13
13
|
email: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Per-org usage/quota key (e.g. `claude:org=<orgUuid>`) — the unit rate
|
|
16
|
+
* limits are actually measured in. Distinct orgs signed in under the same
|
|
17
|
+
* email have distinct keys, so this is the correct dedup boundary; null when
|
|
18
|
+
* no usage identity is available (then we fall back to email).
|
|
19
|
+
*/
|
|
20
|
+
usageKey: string | null;
|
|
14
21
|
usageStatus: AccountInfo['usageStatus'];
|
|
15
22
|
usageSnapshot: UsageSnapshot | null;
|
|
16
23
|
authValid: boolean;
|
package/dist/lib/rotate.js
CHANGED
|
@@ -105,19 +105,29 @@ function compareCandidates(a, b) {
|
|
|
105
105
|
return ta - tb;
|
|
106
106
|
return Math.random() - 0.5;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Identity a candidate dedups on. Quota is tracked per-org, so two versions
|
|
110
|
+
* that share an org are the same rate-limit bucket and must collapse — but two
|
|
111
|
+
* orgs under the same email (e.g. Enterprise + Personal on one Google identity)
|
|
112
|
+
* are genuinely separate buckets and must stay distinct. Prefer the org usage
|
|
113
|
+
* key; fall back to email only when no usage identity is available.
|
|
114
|
+
*/
|
|
115
|
+
function candidateIdentity(c) {
|
|
116
|
+
return c.usageKey ?? c.email;
|
|
117
|
+
}
|
|
108
118
|
function dedupeAndSortCandidates(candidates) {
|
|
109
|
-
const
|
|
119
|
+
const byIdentity = new Map();
|
|
110
120
|
for (const c of candidates) {
|
|
111
|
-
const
|
|
112
|
-
const existing =
|
|
121
|
+
const id = candidateIdentity(c);
|
|
122
|
+
const existing = byIdentity.get(id);
|
|
113
123
|
if (!existing) {
|
|
114
|
-
|
|
124
|
+
byIdentity.set(id, c);
|
|
115
125
|
continue;
|
|
116
126
|
}
|
|
117
127
|
if (compareCandidates(c, existing) < 0)
|
|
118
|
-
|
|
128
|
+
byIdentity.set(id, c);
|
|
119
129
|
}
|
|
120
|
-
return [...
|
|
130
|
+
return [...byIdentity.values()].sort(compareCandidates);
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
123
133
|
* Pick a healthy candidate using weighted random by remaining capacity.
|
|
@@ -252,7 +262,7 @@ async function collectRunCandidates(agent) {
|
|
|
252
262
|
const usageSnapshot = usageKey
|
|
253
263
|
? usageByKey.get(usageKey)?.snapshot ?? null
|
|
254
264
|
: null;
|
|
255
|
-
return { ...candidate, usageSnapshot };
|
|
265
|
+
return { ...candidate, usageKey, usageSnapshot };
|
|
256
266
|
});
|
|
257
267
|
}
|
|
258
268
|
/**
|
package/dist/lib/shims.js
CHANGED
|
@@ -685,7 +685,10 @@ export function ensureVersionedAliasCurrent(agent, version) {
|
|
|
685
685
|
createVersionedAlias(agent, version);
|
|
686
686
|
return 'created';
|
|
687
687
|
}
|
|
688
|
-
|
|
688
|
+
// Upgrade-only (newest-wins), same rationale as ensureShimCurrent: never
|
|
689
|
+
// downgrade an alias stamped by a newer install sharing the shims dir.
|
|
690
|
+
const onDisk = readVersionedAliasSchemaVersion(agent, version);
|
|
691
|
+
if (onDisk === null || onDisk < VERSIONED_ALIAS_SCHEMA_VERSION) {
|
|
689
692
|
createVersionedAlias(agent, version);
|
|
690
693
|
return 'updated';
|
|
691
694
|
}
|
|
@@ -1278,7 +1281,14 @@ export function ensureShimCurrent(agent) {
|
|
|
1278
1281
|
createShim(agent);
|
|
1279
1282
|
return 'created';
|
|
1280
1283
|
}
|
|
1281
|
-
|
|
1284
|
+
// Upgrade-only (newest-wins): regenerate only when the on-disk shim is
|
|
1285
|
+
// unversioned/unreadable (null) or OLDER than this binary. Never downgrade a
|
|
1286
|
+
// shim stamped by a NEWER agents-cli install. Two installs at different
|
|
1287
|
+
// SHIM_SCHEMA_VERSION sharing ~/.agents/.cache/shims/ (e.g. a dev build on
|
|
1288
|
+
// PATH alongside a Hermes-bundled published copy) otherwise ping-pong —
|
|
1289
|
+
// rewriting every shim on each alternating launch and adding boot latency.
|
|
1290
|
+
const onDisk = readShimSchemaVersion(agent);
|
|
1291
|
+
if (onDisk === null || onDisk < SHIM_SCHEMA_VERSION) {
|
|
1282
1292
|
createShim(agent);
|
|
1283
1293
|
return 'updated';
|
|
1284
1294
|
}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -247,6 +247,24 @@ export declare function recordVersionResources(_agent: AgentId, _version: string
|
|
|
247
247
|
* Pass all resource types you want to initialize in one call to batch the write.
|
|
248
248
|
*/
|
|
249
249
|
export declare function ensureVersionResourcePatterns(agent: AgentId, version: string, updates: Partial<Record<Exclude<keyof VersionResources, 'rulesPreset'>, ResourcePattern[]>>): void;
|
|
250
|
+
/**
|
|
251
|
+
* Insert `<alias>:*` at the canonical position (after the system/user/other-extra
|
|
252
|
+
* includes, before `project:*`), unless the alias is already referenced — as an
|
|
253
|
+
* include (`alias:...`) or an exclude (`!alias:...`). Returns a new array when it
|
|
254
|
+
* changes, otherwise the same reference (so callers can detect no-ops cheaply).
|
|
255
|
+
*/
|
|
256
|
+
export declare function withAlias(list: ResourcePattern[], alias: string): ResourcePattern[];
|
|
257
|
+
/** Strip every reference to `<alias>:...` / `!<alias>:...` from a selector list. */
|
|
258
|
+
export declare function withoutAlias(list: ResourcePattern[], alias: string): ResourcePattern[];
|
|
259
|
+
/**
|
|
260
|
+
* Backfill (add=true) or strip (add=false) an extra-repo alias across every
|
|
261
|
+
* already-installed version's selectors. New versions get the alias via
|
|
262
|
+
* `defaultPatterns()` at scaffold time; this keeps existing versions in sync
|
|
263
|
+
* when an extra repo is registered/enabled or removed. Only touches selector
|
|
264
|
+
* lists that are already set — an unset list is left for `defaultPatterns()`.
|
|
265
|
+
* Returns the number of (agent, version) pairs changed.
|
|
266
|
+
*/
|
|
267
|
+
export declare function applyExtraAliasToVersions(alias: string, add: boolean): number;
|
|
250
268
|
export declare function getVersionResources(agent: AgentId, version: string): VersionResources | null;
|
|
251
269
|
/** Active rules preset for an agent@version. Defaults to "default" when unset. */
|
|
252
270
|
export declare function getActiveRulesPreset(agent: AgentId, version: string): string;
|
package/dist/lib/state.js
CHANGED
|
@@ -711,6 +711,79 @@ export function ensureVersionResourcePatterns(agent, version, updates) {
|
|
|
711
711
|
if (changed)
|
|
712
712
|
writeMeta(meta);
|
|
713
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Resource types that resolve across the extra-repo layer. Mirrors
|
|
716
|
+
* `defaultPatterns()`: extras feed commands/skills/hooks/subagents/plugins/
|
|
717
|
+
* workflows, but never permissions (`system:*`) or mcp (`user:*`).
|
|
718
|
+
*/
|
|
719
|
+
const EXTRA_ELIGIBLE_TYPES = [
|
|
720
|
+
'commands', 'skills', 'hooks', 'subagents', 'plugins', 'workflows',
|
|
721
|
+
];
|
|
722
|
+
/**
|
|
723
|
+
* Insert `<alias>:*` at the canonical position (after the system/user/other-extra
|
|
724
|
+
* includes, before `project:*`), unless the alias is already referenced — as an
|
|
725
|
+
* include (`alias:...`) or an exclude (`!alias:...`). Returns a new array when it
|
|
726
|
+
* changes, otherwise the same reference (so callers can detect no-ops cheaply).
|
|
727
|
+
*/
|
|
728
|
+
export function withAlias(list, alias) {
|
|
729
|
+
const prefix = `${alias}:`;
|
|
730
|
+
if (list.some(p => p === `${alias}:*` || p.startsWith(prefix) || p.startsWith(`!${prefix}`))) {
|
|
731
|
+
return list;
|
|
732
|
+
}
|
|
733
|
+
const next = [...list];
|
|
734
|
+
const projIdx = next.findIndex(p => p === 'project:*' || p.startsWith('project:'));
|
|
735
|
+
if (projIdx >= 0)
|
|
736
|
+
next.splice(projIdx, 0, `${alias}:*`);
|
|
737
|
+
else
|
|
738
|
+
next.push(`${alias}:*`);
|
|
739
|
+
return next;
|
|
740
|
+
}
|
|
741
|
+
/** Strip every reference to `<alias>:...` / `!<alias>:...` from a selector list. */
|
|
742
|
+
export function withoutAlias(list, alias) {
|
|
743
|
+
const prefix = `${alias}:`;
|
|
744
|
+
const next = list.filter(p => !(p.startsWith(prefix) || p.startsWith(`!${prefix}`)));
|
|
745
|
+
return next.length === list.length ? list : next;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Backfill (add=true) or strip (add=false) an extra-repo alias across every
|
|
749
|
+
* already-installed version's selectors. New versions get the alias via
|
|
750
|
+
* `defaultPatterns()` at scaffold time; this keeps existing versions in sync
|
|
751
|
+
* when an extra repo is registered/enabled or removed. Only touches selector
|
|
752
|
+
* lists that are already set — an unset list is left for `defaultPatterns()`.
|
|
753
|
+
* Returns the number of (agent, version) pairs changed.
|
|
754
|
+
*/
|
|
755
|
+
export function applyExtraAliasToVersions(alias, add) {
|
|
756
|
+
const meta = readMeta();
|
|
757
|
+
if (!meta.versions)
|
|
758
|
+
return 0;
|
|
759
|
+
let changed = false;
|
|
760
|
+
let count = 0;
|
|
761
|
+
for (const versions of Object.values(meta.versions)) {
|
|
762
|
+
if (!versions)
|
|
763
|
+
continue;
|
|
764
|
+
for (const vr of Object.values(versions)) {
|
|
765
|
+
if (!vr)
|
|
766
|
+
continue;
|
|
767
|
+
let touched = false;
|
|
768
|
+
for (const type of EXTRA_ELIGIBLE_TYPES) {
|
|
769
|
+
const cur = vr[type];
|
|
770
|
+
if (!Array.isArray(cur) || cur.length === 0)
|
|
771
|
+
continue;
|
|
772
|
+
const next = add ? withAlias(cur, alias) : withoutAlias(cur, alias);
|
|
773
|
+
if (next !== cur) {
|
|
774
|
+
vr[type] = next;
|
|
775
|
+
touched = true;
|
|
776
|
+
changed = true;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (touched)
|
|
780
|
+
count++;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (changed)
|
|
784
|
+
writeMeta(meta);
|
|
785
|
+
return count;
|
|
786
|
+
}
|
|
714
787
|
export function getVersionResources(agent, version) {
|
|
715
788
|
const meta = readMeta();
|
|
716
789
|
return meta.versions?.[agent]?.[version] || null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.15",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|