@phnx-labs/agents-cli 1.20.9 → 1.20.11

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **`agents inspect <repo>` summary now shows what's actually inside, not just counts**
6
+
7
+ - The bare repo summary gained four enrichments so it reads as an inventory instead of a tally: (1) **resource name previews** — each kind lists its first few names with a `…(+N)` tail; (2) **manifest summary** — `agents.yaml` is parsed for its `run.<agent>.strategy` and any `agents.<agent>` version pins, shown under `manifests` instead of just the filename; (3) **git detail** — last commit (sha, subject, relative time), ahead/behind upstream when non-zero, and the names of dirty files; (4) **size + file counts** — total repo size and a per-kind byte size. `--json` carries all of it (`git.lastCommit`, `git.ahead/behind`, `manifest`, `size`, and per-kind `{count, bytes, files, names}`); `--brief` still skips resources and size.
8
+ - Fixed a path-parse bug surfaced by the dirty-files list: the shared git helper trimmed leading whitespace, which clipped the first character off the first `git status --porcelain` path; status is now read untrimmed.
9
+
10
+ **`agents inspect .` reads the project `.agents/`, and plugin drill-down shows bundled skills**
11
+
12
+ - `agents inspect .` (and any path to a repo root) now resolves to the project's nested `.agents/` tree when that tree is a populated DotAgents root, instead of the project root itself. Previously a top-level `agents.yaml` version-pin or an unrelated source `skills/` dir at the repo root was mistaken for a DotAgents root, so `inspect .` reported the wrong directory's resources (e.g. `plugins 0` while the real `.agents/plugins/` held a plugin). A bare `.agents`-named dir still resolves to itself, and standalone clones / extra repos that keep resources at the top level (using `.agents/` only for worktrees) are unaffected — their nested `.agents/` is not a DotAgents root, so the top level still wins.
13
+ - `agents inspect <repo> --plugins` now reads plugin bundles through the plugin discoverer: the list shows each plugin's manifest description, and drilling into one (`--plugins <name>`) reports its bundled skills, commands, subagents, hooks, MCP servers, and version. Previously plugins were treated as opaque directories with no description and no view into what they ship.
14
+
5
15
  **Single-typo agent names auto-correct everywhere, not just `agents run`**
6
16
 
7
17
  - `agents view cladue` used to print `Unknown agent 'cladue'` even though `agents run cladue` auto-corrected. `resolveAgentName` — the canonical resolver behind `view`, `usage`, `inspect`, `doctor`, `sync`, `models`, `skills`, `hooks`, `import`, `sessions --agent`, and every `agent@version` spec (`agents add claud@latest`, `agents use codx@2.1.170`) — now falls back to Damerau-Levenshtein distance-1 matching against canonical ids and multi-letter aliases: `cladue` -> `claude` (transposition), `kim` -> `kimi`, `codx` -> `codex`, `gemni` -> `gemini`.
@@ -24,6 +24,8 @@ interface ResourceItem {
24
24
  linkTarget: string;
25
25
  /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
26
  description: string;
27
+ /** Extra detail rows surfaced in detail mode (e.g. a plugin's bundled skills/commands). */
28
+ extra?: Array<[string, string]>;
27
29
  }
28
30
  interface InspectOptions {
29
31
  brief?: boolean;
@@ -54,4 +56,39 @@ export interface RepoTarget {
54
56
  export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
55
57
  /** List one resource kind from a single repo root — no layering, no overrides. */
56
58
  export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
59
+ /** Recursive size + file count of a path; symlinks are not followed. */
60
+ export declare function pathSize(p: string): {
61
+ bytes: number;
62
+ files: number;
63
+ };
64
+ /** Human byte size: "84 KB", "3.1 MB". */
65
+ export declare function formatBytes(n: number): string;
66
+ export interface ManifestSummary {
67
+ /** `run.<agent>.strategy` pairs from agents.yaml. */
68
+ strategies: Array<{
69
+ agent: string;
70
+ strategy: string;
71
+ }>;
72
+ /** `agents.<agent>` version pins from agents.yaml, when present. */
73
+ versions: Array<{
74
+ agent: string;
75
+ version: string;
76
+ }>;
77
+ }
78
+ /** Parse the repo's own agents.yaml into the version pins + run strategies it declares. */
79
+ export declare function repoManifestSummary(root: string): ManifestSummary | null;
80
+ export interface RepoGitInfo {
81
+ branch: string;
82
+ dirty: number;
83
+ dirtyFiles: string[];
84
+ url: string | null;
85
+ lastCommit: {
86
+ sha: string;
87
+ subject: string;
88
+ relative: string;
89
+ } | null;
90
+ ahead: number | null;
91
+ behind: number | null;
92
+ }
93
+ export declare function repoGitInfo(root: string): RepoGitInfo | null;
57
94
  export {};
@@ -23,7 +23,7 @@ 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 { discoverPlugins } from '../lib/plugins.js';
26
+ import { discoverPlugins, discoverPluginsInDir } from '../lib/plugins.js';
27
27
  import { countSessionsInScope } from '../lib/session/discover.js';
28
28
  import { damerauLevenshtein } from '../lib/fuzzy.js';
29
29
  /** Resource kinds the inspect command can drill into. */
@@ -161,18 +161,23 @@ export function resolveRepoTarget(target, cwd) {
161
161
  const stat = safeStat(abs);
162
162
  if (!stat || !stat.isDirectory())
163
163
  return null;
164
- // A dir that is itself a DotAgents root wins over its nested .agents/ —
165
- // extra repos like ~/.agents-extras keep resources at the top level and use
166
- // .agents/ only for worktrees.
164
+ // A dir literally named `.agents` is the root itself.
165
+ if (path.basename(abs) === '.agents') {
166
+ return { label: path.basename(path.dirname(abs)), root: abs };
167
+ }
168
+ // A nested `.agents/` that is a populated DotAgents root wins over `abs` — the
169
+ // project case (`agents inspect .` from a repo root whose resources live under
170
+ // `.agents/`, while the repo's own top-level `skills/`, `agents.yaml` pin, etc.
171
+ // are unrelated source, not a DotAgents tree).
172
+ const nested = path.join(abs, '.agents');
173
+ if (isDotAgentsRoot(nested)) {
174
+ return { label: path.basename(abs), root: nested };
175
+ }
176
+ // Otherwise treat `abs` itself as the root: standalone clones and extra repos
177
+ // like ~/.agents-extras keep resources at the top level and use `.agents/`
178
+ // only for worktrees (so their nested `.agents/` is not a DotAgents root).
167
179
  if (isDotAgentsRoot(abs)) {
168
- const label = path.basename(abs) === '.agents' ? path.basename(path.dirname(abs)) : path.basename(abs);
169
- return { label, root: abs };
170
- }
171
- if (path.basename(abs) !== '.agents') {
172
- const nested = path.join(abs, '.agents');
173
- if (safeStat(nested)?.isDirectory()) {
174
- return { label: path.basename(abs), root: nested };
175
- }
180
+ return { label: path.basename(abs), root: abs };
176
181
  }
177
182
  return null;
178
183
  }
@@ -204,6 +209,14 @@ async function inspectRepo(repo, options) {
204
209
  }
205
210
  /** List one resource kind from a single repo root — no layering, no overrides. */
206
211
  export function collectRepoKind(repo, kind) {
212
+ // Plugins are bundles with a manifest + nested skills/commands/hooks — read
213
+ // them through the plugin discoverer so the manifest description and bundled
214
+ // resources surface, rather than treating each as an opaque directory.
215
+ if (kind === 'plugins') {
216
+ return discoverPluginsInDir(path.join(repo.root, 'plugins'))
217
+ .map(p => pluginToItem(p, repo.label))
218
+ .sort((a, b) => a.name.localeCompare(b.name));
219
+ }
207
220
  const dir = path.join(repo.root, kind);
208
221
  let entries;
209
222
  try {
@@ -216,6 +229,9 @@ export function collectRepoKind(repo, kind) {
216
229
  for (const entry of entries) {
217
230
  if (entry.name.startsWith('.'))
218
231
  continue;
232
+ // Build/tooling caches are never resources — they only inflate counts.
233
+ if (entry.name === '__pycache__' || entry.name === 'node_modules')
234
+ continue;
219
235
  const p = path.join(dir, entry.name);
220
236
  items.push({
221
237
  name: entry.name.replace(/\.(md|yaml|yml|toml|json)$/, ''),
@@ -227,14 +243,100 @@ export function collectRepoKind(repo, kind) {
227
243
  }
228
244
  return items.sort((a, b) => a.name.localeCompare(b.name));
229
245
  }
246
+ /** A few resource names for the at-a-glance preview, with a `…(+N)` tail. */
247
+ function previewNames(items, n) {
248
+ if (items.length === 0)
249
+ return '';
250
+ const shown = items.slice(0, n).map(i => i.name);
251
+ const extra = items.length - shown.length;
252
+ return shown.join(', ') + (extra > 0 ? ` …(+${extra})` : '');
253
+ }
254
+ /** Recursive size + file count of a path; symlinks are not followed. */
255
+ export function pathSize(p) {
256
+ let stat;
257
+ try {
258
+ stat = fs.lstatSync(p);
259
+ }
260
+ catch {
261
+ return { bytes: 0, files: 0 };
262
+ }
263
+ if (stat.isSymbolicLink())
264
+ return { bytes: 0, files: 0 };
265
+ if (stat.isFile())
266
+ return { bytes: stat.size, files: 1 };
267
+ if (!stat.isDirectory())
268
+ return { bytes: 0, files: 0 };
269
+ let entries;
270
+ try {
271
+ entries = fs.readdirSync(p, { withFileTypes: true });
272
+ }
273
+ catch {
274
+ return { bytes: 0, files: 0 };
275
+ }
276
+ let bytes = 0, files = 0;
277
+ for (const e of entries) {
278
+ const sub = pathSize(path.join(p, e.name));
279
+ bytes += sub.bytes;
280
+ files += sub.files;
281
+ }
282
+ return { bytes, files };
283
+ }
284
+ /** Human byte size: "84 KB", "3.1 MB". */
285
+ export function formatBytes(n) {
286
+ if (n < 1024)
287
+ return `${n} B`;
288
+ const units = ['KB', 'MB', 'GB', 'TB'];
289
+ let v = n / 1024, i = 0;
290
+ while (v >= 1024 && i < units.length - 1) {
291
+ v /= 1024;
292
+ i++;
293
+ }
294
+ return `${v >= 10 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
295
+ }
296
+ /** Parse the repo's own agents.yaml into the version pins + run strategies it declares. */
297
+ export function repoManifestSummary(root) {
298
+ let parsed;
299
+ try {
300
+ parsed = yaml.parse(fs.readFileSync(path.join(root, 'agents.yaml'), 'utf-8'));
301
+ }
302
+ catch {
303
+ return null;
304
+ }
305
+ if (!parsed || typeof parsed !== 'object')
306
+ return null;
307
+ const obj = parsed;
308
+ const strategies = [];
309
+ if (obj.run && typeof obj.run === 'object') {
310
+ for (const [agent, cfg] of Object.entries(obj.run)) {
311
+ const strategy = cfg && typeof cfg === 'object' ? cfg.strategy : undefined;
312
+ if (typeof strategy === 'string')
313
+ strategies.push({ agent, strategy });
314
+ }
315
+ }
316
+ const versions = [];
317
+ if (obj.agents && typeof obj.agents === 'object') {
318
+ for (const [agent, ver] of Object.entries(obj.agents)) {
319
+ if (typeof ver === 'string')
320
+ versions.push({ agent, version: ver });
321
+ }
322
+ }
323
+ if (strategies.length === 0 && versions.length === 0)
324
+ return null;
325
+ return { strategies, versions };
326
+ }
230
327
  function renderRepoSummary(repo, options) {
231
328
  const git = repoGitInfo(repo.root);
232
329
  const manifests = REPO_MARKER_FILES.filter(m => fs.existsSync(path.join(repo.root, m)));
233
- const counts = {};
330
+ const manifest = repoManifestSummary(repo.root);
331
+ const kindData = {};
332
+ let totalBytes = 0, totalFiles = 0;
234
333
  if (!options.brief) {
235
334
  for (const kind of DRILLABLE_KINDS) {
236
335
  const items = collectRepoKind(repo, kind);
237
- counts[kind] = { total: items.length, bySource: { [repo.label]: items.length } };
336
+ const size = pathSize(path.join(repo.root, kind));
337
+ kindData[kind] = { items, size };
338
+ totalBytes += size.bytes;
339
+ totalFiles += size.files;
238
340
  }
239
341
  }
240
342
  if (options.json) {
@@ -243,32 +345,65 @@ function renderRepoSummary(repo, options) {
243
345
  root: repo.root,
244
346
  git,
245
347
  manifests,
246
- resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => [kind, counts[kind].total])),
348
+ manifest,
349
+ size: options.brief ? null : { bytes: totalBytes, files: totalFiles },
350
+ resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => [kind, {
351
+ count: kindData[kind].items.length,
352
+ bytes: kindData[kind].size.bytes,
353
+ files: kindData[kind].size.files,
354
+ names: kindData[kind].items.map(i => i.name),
355
+ }])),
247
356
  }, null, 2));
248
357
  return;
249
358
  }
250
359
  console.log('\n' + chalk.bold(repo.label) + ' ' + chalk.gray('[dotagents repo]') + '\n');
251
- const rows = [['root', termLink(repo.root, repo.root)]];
360
+ // Indent for continuation sub-rows: 2 leading + 10 key column + 1 space.
361
+ const sub = (label, value) => console.log(` ${''.padEnd(10)} ${chalk.gray(label.padEnd(8))} ${value}`);
362
+ console.log(` ${'root'.padEnd(10)} ${termLink(repo.root, repo.root)}`);
252
363
  if (git) {
253
364
  const dirty = git.dirty > 0 ? ` ${chalk.gray('·')} ${chalk.yellow(`${git.dirty} dirty`)}` : '';
254
365
  const url = git.url ? ` ${chalk.gray('·')} ${chalk.gray(git.url)}` : '';
255
- rows.push(['git', `${git.branch}${dirty}${url}`]);
366
+ console.log(` ${'git'.padEnd(10)} ${git.branch}${dirty}${url}`);
367
+ if (git.lastCommit) {
368
+ const rel = git.lastCommit.relative ? ` ${chalk.gray(`(${git.lastCommit.relative})`)}` : '';
369
+ sub('last', `${chalk.cyan(git.lastCommit.sha)} ${truncate(git.lastCommit.subject, 60)}${rel}`);
370
+ }
371
+ if (git.ahead !== null && git.behind !== null && (git.ahead > 0 || git.behind > 0)) {
372
+ sub('sync', `ahead ${git.ahead} ${chalk.gray('·')} behind ${git.behind}`);
373
+ }
374
+ if (git.dirtyFiles.length > 0) {
375
+ const shown = git.dirtyFiles.slice(0, 4).join(', ');
376
+ const extra = git.dirtyFiles.length - Math.min(4, git.dirtyFiles.length);
377
+ sub('dirty', chalk.yellow(shown + (extra > 0 ? ` …(+${extra})` : '')));
378
+ }
379
+ }
380
+ if (manifests.length > 0) {
381
+ console.log(` ${'manifests'.padEnd(10)} ${manifests.join(', ')}`);
382
+ if (manifest) {
383
+ if (manifest.versions.length > 0) {
384
+ sub('versions', manifest.versions.map(v => `${v.agent} ${chalk.cyan(v.version)}`).join(chalk.gray(' · ')));
385
+ }
386
+ if (manifest.strategies.length > 0) {
387
+ sub('run', manifest.strategies.map(s => `${s.agent}:${s.strategy}`).join(chalk.gray(' · ')));
388
+ }
389
+ }
256
390
  }
257
- if (manifests.length > 0)
258
- rows.push(['manifests', manifests.join(', ')]);
259
- for (const [k, v] of rows)
260
- console.log(` ${k.padEnd(10)} ${v}`);
261
391
  if (!options.brief) {
392
+ console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} files`);
262
393
  console.log('\n' + chalk.bold('Resources'));
263
394
  for (const kind of DRILLABLE_KINDS) {
264
- console.log(` ${kind.padEnd(10)} ${String(counts[kind].total).padStart(4)}`);
395
+ const { items, size } = kindData[kind];
396
+ const count = String(items.length).padStart(4);
397
+ const sz = items.length > 0 ? formatBytes(size.bytes).padStart(8) : ''.padEnd(8);
398
+ const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 4), 60)) : '';
399
+ console.log(` ${kind.padEnd(10)} ${count} ${sz} ${preview}`.trimEnd());
265
400
  }
266
401
  }
267
402
  console.log('');
268
403
  console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
269
404
  console.log('');
270
405
  }
271
- function repoGitInfo(root) {
406
+ export function repoGitInfo(root) {
272
407
  const git = (args) => {
273
408
  try {
274
409
  return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
@@ -281,9 +416,33 @@ function repoGitInfo(root) {
281
416
  const branch = git('rev-parse --abbrev-ref HEAD');
282
417
  if (branch === null)
283
418
  return null;
284
- const status = git('status --porcelain');
285
- const dirty = status ? status.split('\n').filter(Boolean).length : 0;
286
- return { branch, dirty, url: git('remote get-url origin') };
419
+ // Read status WITHOUT trimming — git()'s .trim() would strip the leading
420
+ // space of the first porcelain line (`XY path`), corrupting the path slice.
421
+ let statusRaw;
422
+ try {
423
+ statusRaw = execSync(`git -C ${JSON.stringify(root)} status --porcelain`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
424
+ }
425
+ catch {
426
+ statusRaw = null;
427
+ }
428
+ const dirtyFiles = statusRaw ? statusRaw.split('\n').filter(Boolean).map(l => l.slice(3)) : [];
429
+ let lastCommit = null;
430
+ const log = git('log -1 --format=%h%x1f%s%x1f%cr');
431
+ if (log) {
432
+ const [sha, subject, relative] = log.split('\x1f');
433
+ if (sha)
434
+ lastCommit = { sha, subject: subject ?? '', relative: relative ?? '' };
435
+ }
436
+ let ahead = null, behind = null;
437
+ const counts = git("rev-list --left-right --count '@{upstream}...HEAD'");
438
+ if (counts) {
439
+ const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
440
+ if (Number.isFinite(b) && Number.isFinite(a)) {
441
+ behind = b;
442
+ ahead = a;
443
+ }
444
+ }
445
+ return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git('remote get-url origin'), lastCommit, ahead, behind };
287
446
  }
288
447
  // ─── Summary mode ────────────────────────────────────────────────────────────
289
448
  async function renderSummary(agent, version, versionHome, options) {
@@ -486,14 +645,35 @@ function collectKind(agent, versionHome, kind) {
486
645
  }
487
646
  }
488
647
  function pluginItems() {
489
- const plugins = discoverPlugins();
490
- return plugins.map(p => ({
491
- name: p.name,
492
- source: 'user',
493
- path: p.root,
494
- linkTarget: linkTarget(p.root),
495
- description: p.manifest.description ?? '',
496
- }));
648
+ return discoverPlugins().map(p => pluginToItem(p, 'user'));
649
+ }
650
+ /**
651
+ * Map a discovered plugin to a resource item, surfacing the manifest description
652
+ * and the bundle's nested resources (skills, commands, hooks, ...) as detail rows.
653
+ */
654
+ function pluginToItem(plugin, source) {
655
+ const extra = [];
656
+ const list = (names) => names.length <= 8 ? names.join(', ') : `${names.slice(0, 8).join(', ')}, +${names.length - 8} more`;
657
+ if (plugin.skills.length)
658
+ extra.push(['skills', `${plugin.skills.length} (${list(plugin.skills)})`]);
659
+ if (plugin.commands.length)
660
+ extra.push(['commands', `${plugin.commands.length} (${list(plugin.commands)})`]);
661
+ if (plugin.agentDefs.length)
662
+ extra.push(['subagents', `${plugin.agentDefs.length} (${list(plugin.agentDefs)})`]);
663
+ if (plugin.hooks.length)
664
+ extra.push(['hooks', String(plugin.hooks.length)]);
665
+ if (plugin.mcpServers.length)
666
+ extra.push(['mcp', list(plugin.mcpServers)]);
667
+ if (plugin.manifest.version)
668
+ extra.push(['version', plugin.manifest.version]);
669
+ return {
670
+ name: plugin.name,
671
+ source,
672
+ path: plugin.root,
673
+ linkTarget: linkTarget(plugin.root),
674
+ description: plugin.manifest.description ?? '',
675
+ extra,
676
+ };
497
677
  }
498
678
  function entriesFromAgentResources(agent, versionHome, kind) {
499
679
  const res = getAgentResources(agent, { home: versionHome });
@@ -562,6 +742,10 @@ function buildDetailRows(item, kind) {
562
742
  rows.push(['tools', fm.tools.join(', ')]);
563
743
  }
564
744
  }
745
+ // Plugin bundles carry their nested resources as pre-built rows.
746
+ if (kind === 'plugins' && item.extra) {
747
+ rows.push(...item.extra);
748
+ }
565
749
  return rows;
566
750
  }
567
751
  function findMatches(items, query) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.9",
3
+ "version": "1.20.11",
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",