@phnx-labs/agents-cli 1.20.10 → 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,11 @@
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
+
5
10
  **`agents inspect .` reads the project `.agents/`, and plugin drill-down shows bundled skills**
6
11
 
7
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.
@@ -56,4 +56,39 @@ export interface RepoTarget {
56
56
  export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
57
57
  /** List one resource kind from a single repo root — no layering, no overrides. */
58
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;
59
94
  export {};
@@ -229,6 +229,9 @@ export function collectRepoKind(repo, kind) {
229
229
  for (const entry of entries) {
230
230
  if (entry.name.startsWith('.'))
231
231
  continue;
232
+ // Build/tooling caches are never resources — they only inflate counts.
233
+ if (entry.name === '__pycache__' || entry.name === 'node_modules')
234
+ continue;
232
235
  const p = path.join(dir, entry.name);
233
236
  items.push({
234
237
  name: entry.name.replace(/\.(md|yaml|yml|toml|json)$/, ''),
@@ -240,14 +243,100 @@ export function collectRepoKind(repo, kind) {
240
243
  }
241
244
  return items.sort((a, b) => a.name.localeCompare(b.name));
242
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
+ }
243
327
  function renderRepoSummary(repo, options) {
244
328
  const git = repoGitInfo(repo.root);
245
329
  const manifests = REPO_MARKER_FILES.filter(m => fs.existsSync(path.join(repo.root, m)));
246
- const counts = {};
330
+ const manifest = repoManifestSummary(repo.root);
331
+ const kindData = {};
332
+ let totalBytes = 0, totalFiles = 0;
247
333
  if (!options.brief) {
248
334
  for (const kind of DRILLABLE_KINDS) {
249
335
  const items = collectRepoKind(repo, kind);
250
- 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;
251
340
  }
252
341
  }
253
342
  if (options.json) {
@@ -256,32 +345,65 @@ function renderRepoSummary(repo, options) {
256
345
  root: repo.root,
257
346
  git,
258
347
  manifests,
259
- 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
+ }])),
260
356
  }, null, 2));
261
357
  return;
262
358
  }
263
359
  console.log('\n' + chalk.bold(repo.label) + ' ' + chalk.gray('[dotagents repo]') + '\n');
264
- 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)}`);
265
363
  if (git) {
266
364
  const dirty = git.dirty > 0 ? ` ${chalk.gray('·')} ${chalk.yellow(`${git.dirty} dirty`)}` : '';
267
365
  const url = git.url ? ` ${chalk.gray('·')} ${chalk.gray(git.url)}` : '';
268
- 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
+ }
269
390
  }
270
- if (manifests.length > 0)
271
- rows.push(['manifests', manifests.join(', ')]);
272
- for (const [k, v] of rows)
273
- console.log(` ${k.padEnd(10)} ${v}`);
274
391
  if (!options.brief) {
392
+ console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} files`);
275
393
  console.log('\n' + chalk.bold('Resources'));
276
394
  for (const kind of DRILLABLE_KINDS) {
277
- 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());
278
400
  }
279
401
  }
280
402
  console.log('');
281
403
  console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
282
404
  console.log('');
283
405
  }
284
- function repoGitInfo(root) {
406
+ export function repoGitInfo(root) {
285
407
  const git = (args) => {
286
408
  try {
287
409
  return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
@@ -294,9 +416,33 @@ function repoGitInfo(root) {
294
416
  const branch = git('rev-parse --abbrev-ref HEAD');
295
417
  if (branch === null)
296
418
  return null;
297
- const status = git('status --porcelain');
298
- const dirty = status ? status.split('\n').filter(Boolean).length : 0;
299
- 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 };
300
446
  }
301
447
  // ─── Summary mode ────────────────────────────────────────────────────────────
302
448
  async function renderSummary(agent, version, versionHome, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.10",
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",