@phnx-labs/agents-cli 1.20.33 → 1.20.35

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -2
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +17 -12
  8. package/dist/commands/doctor.d.ts +11 -0
  9. package/dist/commands/doctor.js +108 -14
  10. package/dist/commands/events.d.ts +16 -0
  11. package/dist/commands/events.js +173 -0
  12. package/dist/commands/exec.js +6 -1
  13. package/dist/commands/hosts.js +28 -24
  14. package/dist/commands/import.js +1 -1
  15. package/dist/commands/inspect.d.ts +2 -0
  16. package/dist/commands/inspect.js +54 -7
  17. package/dist/commands/logs.d.ts +17 -0
  18. package/dist/commands/logs.js +139 -0
  19. package/dist/commands/models.d.ts +3 -0
  20. package/dist/commands/models.js +27 -8
  21. package/dist/commands/permissions.js +9 -2
  22. package/dist/commands/repo.d.ts +34 -0
  23. package/dist/commands/repo.js +243 -65
  24. package/dist/commands/resource-view.d.ts +20 -0
  25. package/dist/commands/resource-view.js +90 -28
  26. package/dist/commands/rules.js +1 -1
  27. package/dist/commands/secrets-migrate.js +23 -11
  28. package/dist/commands/secrets.d.ts +20 -0
  29. package/dist/commands/secrets.js +93 -17
  30. package/dist/commands/sessions-resume.d.ts +2 -0
  31. package/dist/commands/sessions-resume.js +209 -0
  32. package/dist/commands/sessions-tail.d.ts +10 -0
  33. package/dist/commands/sessions-tail.js +11 -0
  34. package/dist/commands/sessions.d.ts +73 -1
  35. package/dist/commands/sessions.js +273 -53
  36. package/dist/commands/status.d.ts +12 -0
  37. package/dist/commands/status.js +81 -0
  38. package/dist/commands/teams.js +70 -6
  39. package/dist/commands/versions.js +2 -1
  40. package/dist/commands/view.d.ts +42 -0
  41. package/dist/commands/view.js +213 -83
  42. package/dist/commands/wallet.d.ts +6 -0
  43. package/dist/commands/wallet.js +22 -5
  44. package/dist/index.js +61 -32
  45. package/dist/lib/acp/harnesses.d.ts +1 -1
  46. package/dist/lib/acp/harnesses.js +2 -2
  47. package/dist/lib/agents.d.ts +12 -0
  48. package/dist/lib/agents.js +145 -41
  49. package/dist/lib/browser/cdp.js +5 -1
  50. package/dist/lib/browser/chrome.js +20 -0
  51. package/dist/lib/browser/drivers/ssh.d.ts +27 -0
  52. package/dist/lib/browser/drivers/ssh.js +94 -19
  53. package/dist/lib/browser/ipc.d.ts +3 -0
  54. package/dist/lib/browser/ipc.js +13 -9
  55. package/dist/lib/browser/service.js +71 -15
  56. package/dist/lib/daemon.d.ts +35 -0
  57. package/dist/lib/daemon.js +33 -5
  58. package/dist/lib/devices/connect.d.ts +3 -2
  59. package/dist/lib/devices/connect.js +5 -3
  60. package/dist/lib/doctor-diff.js +29 -2
  61. package/dist/lib/drift-sync.d.ts +43 -0
  62. package/dist/lib/drift-sync.js +179 -0
  63. package/dist/lib/events.d.ts +9 -2
  64. package/dist/lib/events.js +70 -11
  65. package/dist/lib/exec.d.ts +15 -0
  66. package/dist/lib/exec.js +43 -11
  67. package/dist/lib/hooks.js +8 -2
  68. package/dist/lib/hosts/dispatch.js +6 -7
  69. package/dist/lib/hosts/logs.d.ts +16 -0
  70. package/dist/lib/hosts/logs.js +45 -0
  71. package/dist/lib/hosts/progress.d.ts +66 -0
  72. package/dist/lib/hosts/progress.js +125 -17
  73. package/dist/lib/hosts/ready.d.ts +23 -2
  74. package/dist/lib/hosts/ready.js +35 -13
  75. package/dist/lib/hosts/reconcile.d.ts +53 -0
  76. package/dist/lib/hosts/reconcile.js +81 -0
  77. package/dist/lib/hosts/tasks.d.ts +8 -0
  78. package/dist/lib/hosts/tasks.js +14 -0
  79. package/dist/lib/permissions.d.ts +4 -0
  80. package/dist/lib/permissions.js +35 -0
  81. package/dist/lib/picker.d.ts +24 -0
  82. package/dist/lib/picker.js +124 -0
  83. package/dist/lib/platform/winpath.d.ts +31 -2
  84. package/dist/lib/platform/winpath.js +133 -24
  85. package/dist/lib/plugin-marketplace.d.ts +30 -0
  86. package/dist/lib/plugin-marketplace.js +215 -2
  87. package/dist/lib/plugins.d.ts +5 -0
  88. package/dist/lib/plugins.js +45 -4
  89. package/dist/lib/pwsh.d.ts +11 -0
  90. package/dist/lib/pwsh.js +13 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/agent.d.ts +42 -1
  94. package/dist/lib/secrets/agent.js +89 -11
  95. package/dist/lib/secrets/bundles.js +40 -9
  96. package/dist/lib/secrets/filestore.js +31 -1
  97. package/dist/lib/secrets/index.d.ts +33 -1
  98. package/dist/lib/secrets/index.js +90 -9
  99. package/dist/lib/secrets/windows.d.ts +74 -0
  100. package/dist/lib/secrets/windows.js +440 -0
  101. package/dist/lib/session/active.d.ts +7 -0
  102. package/dist/lib/session/active.js +11 -3
  103. package/dist/lib/session/discover.js +94 -0
  104. package/dist/lib/session/parse.d.ts +6 -0
  105. package/dist/lib/session/parse.js +283 -2
  106. package/dist/lib/session/pid-registry.d.ts +21 -0
  107. package/dist/lib/session/pid-registry.js +80 -0
  108. package/dist/lib/session/remote-active.d.ts +26 -0
  109. package/dist/lib/session/remote-active.js +141 -0
  110. package/dist/lib/session/remote.js +2 -6
  111. package/dist/lib/session/render.js +1 -1
  112. package/dist/lib/session/sync/config.d.ts +8 -0
  113. package/dist/lib/session/sync/config.js +11 -2
  114. package/dist/lib/session/types.d.ts +1 -1
  115. package/dist/lib/session/types.js +1 -1
  116. package/dist/lib/shims.d.ts +30 -2
  117. package/dist/lib/shims.js +107 -50
  118. package/dist/lib/ssh-exec.d.ts +26 -3
  119. package/dist/lib/ssh-exec.js +45 -3
  120. package/dist/lib/ssh-tunnel.d.ts +24 -5
  121. package/dist/lib/ssh-tunnel.js +60 -62
  122. package/dist/lib/staleness/writers/hooks.js +1 -1
  123. package/dist/lib/startup/command-registry.d.ts +3 -0
  124. package/dist/lib/startup/command-registry.js +6 -0
  125. package/dist/lib/sync-status.d.ts +102 -0
  126. package/dist/lib/sync-status.js +135 -0
  127. package/dist/lib/teams/agents.d.ts +24 -0
  128. package/dist/lib/teams/agents.js +30 -1
  129. package/dist/lib/teams/registry.js +25 -9
  130. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  131. package/dist/lib/terminal/backends/ghostty.js +69 -0
  132. package/dist/lib/terminal/backends/index.d.ts +18 -0
  133. package/dist/lib/terminal/backends/index.js +29 -0
  134. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  135. package/dist/lib/terminal/backends/iterm.js +62 -0
  136. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  137. package/dist/lib/terminal/backends/tmux.js +23 -0
  138. package/dist/lib/terminal/engine.d.ts +39 -0
  139. package/dist/lib/terminal/engine.js +54 -0
  140. package/dist/lib/terminal/index.d.ts +14 -0
  141. package/dist/lib/terminal/index.js +5 -0
  142. package/dist/lib/terminal/policy.d.ts +11 -0
  143. package/dist/lib/terminal/policy.js +11 -0
  144. package/dist/lib/terminal/quote.d.ts +11 -0
  145. package/dist/lib/terminal/quote.js +13 -0
  146. package/dist/lib/terminal/shell.d.ts +6 -0
  147. package/dist/lib/terminal/shell.js +23 -0
  148. package/dist/lib/terminal/transport.d.ts +15 -0
  149. package/dist/lib/terminal/transport.js +40 -0
  150. package/dist/lib/terminal/types.d.ts +59 -0
  151. package/dist/lib/terminal/types.js +13 -0
  152. package/dist/lib/types.d.ts +27 -1
  153. package/dist/lib/usage.d.ts +30 -0
  154. package/dist/lib/usage.js +159 -2
  155. package/dist/lib/versions.d.ts +7 -3
  156. package/dist/lib/versions.js +10 -5
  157. package/dist/lib/whats-new.d.ts +9 -0
  158. package/dist/lib/whats-new.js +35 -0
  159. package/package.json +1 -1
@@ -23,9 +23,26 @@ import { listProfiles, profileSummary } from '../lib/profiles.js';
23
23
  import { loadManifest, isStale } from '../lib/staleness/index.js';
24
24
  import { confirm } from '@inquirer/prompts';
25
25
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
26
+ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
26
27
  // Shown in the email column for agents that are signed in but expose no email
27
- // address locally (Antigravity, Kimi store an opaque OAuth/JWT credential).
28
+ // address locally (Antigravity stores an opaque OAuth grant with no identity).
28
29
  const SIGNED_IN_LABEL = 'signed in';
30
+ /**
31
+ * Text for the account (email) column. Prefers the real email; otherwise, for a
32
+ * signed-in agent whose credential carries no email but does carry an opaque
33
+ * account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
34
+ * distinctly instead of a generic "signed in". Falls back to "signed in" when we
35
+ * have neither (Antigravity), and empty when signed out.
36
+ */
37
+ function accountColumnLabel(info) {
38
+ if (!info)
39
+ return '';
40
+ if (info.email)
41
+ return info.email;
42
+ if (info.signedIn)
43
+ return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
44
+ return '';
45
+ }
29
46
  /**
30
47
  * Group profile summaries by their host harness, optionally filtered to a
31
48
  * single agent. Profile YAMLs that fail validation are silently skipped by
@@ -51,6 +68,8 @@ function profileKindAndModel(model, planWidth) {
51
68
  return `${kind} ${model}`;
52
69
  }
53
70
  function termLink(text, filePath) {
71
+ if (!process.stdout.isTTY)
72
+ return text;
54
73
  const url = `file://${filePath}`;
55
74
  return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
56
75
  }
@@ -135,13 +154,18 @@ function shouldRenderSection(key, filter) {
135
154
  return filter[key] === true;
136
155
  }
137
156
  /** Trim a description to a column-friendly snippet. Strips newlines, collapses whitespace. */
138
- function summarizeDescription(desc, maxLen = 80) {
157
+ export function summarizeDescription(desc, maxLen = 80) {
139
158
  if (!desc)
140
159
  return '';
141
160
  const cleaned = desc.replace(/\s+/g, ' ').trim();
142
- if (cleaned.length <= maxLen)
143
- return cleaned;
144
- return cleaned.slice(0, maxLen - 1).trimEnd() + '…';
161
+ return truncateToWidth(cleaned, maxLen);
162
+ }
163
+ export function descriptionForPrefix(desc, prefix) {
164
+ if (!desc)
165
+ return '';
166
+ const visiblePrefix = prefix.replace(/\x1b]8;;[^\x1b]*(?:\x1b\\|\x07)/g, '');
167
+ const budget = Math.max(1, terminalWidth() - stringWidth(visiblePrefix));
168
+ return summarizeDescription(desc, budget);
145
169
  }
146
170
  function getProfileSummaries(filterAgentId) {
147
171
  return listProfiles()
@@ -207,8 +231,10 @@ function renderHostClisSection(cwd) {
207
231
  const status = installed ? chalk.green('installed') : chalk.red('missing ');
208
232
  const linkedName = termLink(manifest.name.padEnd(nameWidth), linkTarget(manifest.path));
209
233
  const tag = hostCliSourceTag(manifest.source);
210
- const desc = manifest.description ? chalk.gray(` ${summarizeDescription(manifest.description, 60)}`) : '';
211
- console.log(` ${status} ${chalk.cyan(linkedName)} ${tag}${desc}`);
234
+ const prefix = ` ${status} ${chalk.cyan(linkedName)} ${tag}`;
235
+ const descSnippet = descriptionForPrefix(manifest.description, `${prefix} `);
236
+ const desc = descSnippet ? chalk.gray(` ${descSnippet}`) : '';
237
+ console.log(prefix + desc);
212
238
  }
213
239
  if (anyMissing) {
214
240
  console.log(chalk.gray(' Install missing with `agents cli install`'));
@@ -310,7 +336,10 @@ async function showInstalledVersions(filterAgentId) {
310
336
  return info;
311
337
  return {
312
338
  ...info,
313
- plan: canon.plan,
339
+ // Prefer a plan the live usage fetch surfaced (Kimi reports membership
340
+ // tier in /usages; its local auth file has none) and fall back to the
341
+ // account-derived plan (Claude's billingType).
342
+ plan: usageByKey.get(key)?.snapshot?.plan ?? canon.plan,
314
343
  // Throttle state comes from the live usage windows, not the pay-as-you-go
315
344
  // overage flag that AccountInfo.usageStatus used to carry. A maxed window
316
345
  // means rate-limited; no snapshot means no badge. See
@@ -353,10 +382,9 @@ async function showInstalledVersions(filterAgentId) {
353
382
  maxVerLabel = Math.max(maxVerLabel, label.length);
354
383
  const rawInfo = infoMap.get(`${agentId}:${v}`);
355
384
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
356
- if (info?.email)
357
- maxEmail = Math.max(maxEmail, info.email.length);
358
- else if (info?.signedIn)
359
- maxEmail = Math.max(maxEmail, SIGNED_IN_LABEL.length);
385
+ const accountLabel = accountColumnLabel(info);
386
+ if (accountLabel)
387
+ maxEmail = Math.max(maxEmail, accountLabel.length);
360
388
  if (info?.plan)
361
389
  maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
362
390
  }
@@ -432,9 +460,10 @@ async function showInstalledVersions(filterAgentId) {
432
460
  }
433
461
  else {
434
462
  if (hasEmail || hasUsage || hasActive || signedIn) {
435
- // Signed-in agents without a local email (Antigravity, Kimi) show a
436
- // "signed in" placeholder so they read as logged in, not blank.
437
- const display = vInfo?.email || (signedIn ? SIGNED_IN_LABEL : '');
463
+ // Signed-in agents without a local email show their account id
464
+ // (Kimi's user_id) or a "signed in" placeholder (Antigravity) so they
465
+ // read as logged in, not blank.
466
+ const display = accountColumnLabel(vInfo);
438
467
  const emailCol = display.padEnd(maxEmail);
439
468
  parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
440
469
  }
@@ -515,7 +544,7 @@ async function showInstalledVersions(filterAgentId) {
515
544
  const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null);
516
545
  const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
517
546
  if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
518
- const gDisplay = gInfo?.email || (gInfo?.signedIn ? SIGNED_IN_LABEL : '');
547
+ const gDisplay = accountColumnLabel(gInfo);
519
548
  parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
520
549
  }
521
550
  if (gUsageStr || gActiveStr)
@@ -686,53 +715,9 @@ async function showAgentResources(agentId, requestedVersion, filter) {
686
715
  }
687
716
  }
688
717
  const home = getVersionHomePath(agentId, version);
689
- // Get git sync status if ~/.agents/ is a git repo
690
- const userAgentsDir = getUserAgentsDir();
691
- const hasGitRepo = isGitRepo(userAgentsDir);
692
- const commandsSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'commands') : null;
693
- const skillsSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'skills') : null;
694
- const hooksSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'hooks') : null;
695
- const memorySync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'rules') : null;
696
- // Helper to determine sync state for a resource
697
- const getSyncState = (resourceName, resourceType, syncStatus) => {
698
- if (!syncStatus)
699
- return undefined;
700
- let relativePath;
701
- if (resourceType === 'commands') {
702
- relativePath = `commands/${resourceName}.md`;
703
- }
704
- else if (resourceType === 'skills') {
705
- relativePath = `skills/${resourceName}`;
706
- }
707
- else if (resourceType === 'hooks') {
708
- relativePath = `hooks/${resourceName}`;
709
- }
710
- else {
711
- // Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
712
- const centralName = getCentralRulesFileName(agentId);
713
- relativePath = `rules/${centralName}`;
714
- }
715
- const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
716
- const isNew = syncStatus.new.some(matchesPath);
717
- const isStaged = syncStatus.staged.some(matchesPath);
718
- const isModified = syncStatus.modified.some(matchesPath);
719
- const isDeleted = syncStatus.deleted.some(matchesPath);
720
- const isSynced = syncStatus.synced.some(matchesPath);
721
- if (isNew || isStaged) {
722
- return 'new';
723
- }
724
- if (isModified) {
725
- return 'modified';
726
- }
727
- if (isDeleted) {
728
- return 'deleted';
729
- }
730
- if (isSynced) {
731
- return 'synced';
732
- }
733
- // Not in any array = local-only (untracked with no files)
734
- return 'new';
735
- };
718
+ // Git sync status if ~/.agents/ is a git repo (shared loader — see
719
+ // loadResourceSyncData / resolveResourceSyncState, reused by --json --resources).
720
+ const { hasGitRepo, commands: commandsSync, skills: skillsSync, hooks: hooksSync, memory: memorySync } = await loadResourceSyncData();
736
721
  const resources = getAgentResources(agentId, {
737
722
  cwd,
738
723
  scope: 'all',
@@ -745,23 +730,23 @@ async function showAgentResources(agentId, requestedVersion, filter) {
745
730
  version,
746
731
  commands: resources.commands.map(r => ({
747
732
  ...r,
748
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'commands', commandsSync),
733
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'commands', commandsSync),
749
734
  })),
750
735
  skills: resources.skills.map(r => ({
751
736
  ...r,
752
737
  // ruleCount of 0 is noise — every skill has 0 unless it ships subrules, which is rare.
753
738
  ruleCount: r.ruleCount && r.ruleCount > 0 ? r.ruleCount : undefined,
754
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'skills', skillsSync),
739
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'skills', skillsSync),
755
740
  })),
756
741
  skillErrors: resources.skillErrors,
757
742
  mcp: resources.mcp.map(r => ({ name: r.name, scope: r.scope, syncState: r.scope === 'project' ? undefined : 'synced' })),
758
743
  memory: resources.memory.map(r => ({
759
744
  ...r,
760
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'memory', memorySync),
745
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'memory', memorySync),
761
746
  })),
762
747
  hooks: resources.hooks.map(r => ({
763
748
  ...r,
764
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'hooks', hooksSync),
749
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'hooks', hooksSync),
765
750
  })),
766
751
  workflows: resources.workflows.map(r => ({ name: r.name, path: r.path, scope: r.scope })),
767
752
  };
@@ -796,9 +781,10 @@ async function showAgentResources(agentId, requestedVersion, filter) {
796
781
  : chalk.gray('[system]');
797
782
  display += ` ${sourceTag}`;
798
783
  const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
799
- const descSnippet = summarizeDescription(r.description);
784
+ const prefix = ` ${display}${syncStr}`;
785
+ const descSnippet = descriptionForPrefix(r.description, `${prefix} `);
800
786
  const descStr = descSnippet ? chalk.gray(` ${descSnippet}`) : '';
801
- console.log(` ${display}${syncStr}${descStr}`);
787
+ console.log(prefix + descStr);
802
788
  }
803
789
  }
804
790
  // Render promptcuts (cross-agent, not per-version). Shortcuts are layered
@@ -827,13 +813,10 @@ async function showAgentResources(agentId, requestedVersion, filter) {
827
813
  cliVersion: version,
828
814
  info: accountInfo,
829
815
  });
830
- const emailStr = accountInfo.email
831
- ? chalk.cyan(` ${accountInfo.email}`)
832
- : accountInfo.signedIn
833
- ? chalk.cyan(` ${SIGNED_IN_LABEL}`)
834
- : '';
816
+ const accountLabel = accountColumnLabel(accountInfo);
817
+ const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
835
818
  const status = chalk.green(version);
836
- const usageStr = formatUsageSummary(accountInfo.plan, null);
819
+ const usageStr = formatUsageSummary(usageInfo.snapshot?.plan ?? accountInfo.plan, null);
837
820
  const usagePart = usageStr ? ` ${usageStr}` : '';
838
821
  console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
839
822
  const usageLines = formatUsageSection(usageInfo);
@@ -973,14 +956,144 @@ async function showAgentResources(agentId, requestedVersion, filter) {
973
956
  console.log(chalk.gray('Legend:'), chalk.green('Tracked'), chalk.blue('Local-only'), chalk.yellow('Modified'), chalk.red('Deleted'));
974
957
  }
975
958
  }
959
+ const ALL_RESOURCE_SECTIONS = ['commands', 'skills', 'mcp', 'memory', 'hooks', 'workflows', 'plugins'];
960
+ /** Load git sync-state for the tracked resource kinds. Shared by the human
961
+ * detail view (showAgentResources) and the `--json --resources` path. */
962
+ async function loadResourceSyncData() {
963
+ const userAgentsDir = getUserAgentsDir();
964
+ const hasGitRepo = isGitRepo(userAgentsDir);
965
+ return {
966
+ hasGitRepo,
967
+ commands: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'commands') : null,
968
+ skills: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'skills') : null,
969
+ hooks: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'hooks') : null,
970
+ memory: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'rules') : null,
971
+ };
972
+ }
973
+ /** Resolve one resource's git sync-state. Extracted from showAgentResources so
974
+ * the human view and the JSON path derive drift identically. */
975
+ function resolveResourceSyncState(agentId, resourceName, resourceType, syncStatus) {
976
+ if (!syncStatus)
977
+ return undefined;
978
+ let relativePath;
979
+ if (resourceType === 'commands') {
980
+ relativePath = `commands/${resourceName}.md`;
981
+ }
982
+ else if (resourceType === 'skills') {
983
+ relativePath = `skills/${resourceName}`;
984
+ }
985
+ else if (resourceType === 'hooks') {
986
+ relativePath = `hooks/${resourceName}`;
987
+ }
988
+ else {
989
+ // Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
990
+ const centralName = getCentralRulesFileName(agentId);
991
+ relativePath = `rules/${centralName}`;
992
+ }
993
+ const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
994
+ if (syncStatus.new.some(matchesPath) || syncStatus.staged.some(matchesPath))
995
+ return 'new';
996
+ if (syncStatus.modified.some(matchesPath))
997
+ return 'modified';
998
+ if (syncStatus.deleted.some(matchesPath))
999
+ return 'deleted';
1000
+ if (syncStatus.synced.some(matchesPath))
1001
+ return 'synced';
1002
+ // Not in any array = local-only (untracked with no files)
1003
+ return 'new';
1004
+ }
1005
+ /** Collect one version's resources for `--json`, limited to `sections`. Scans
1006
+ * the version's `home`, so per-version differences are reported accurately. */
1007
+ function collectVersionResources(agentId, home, cwd, cliInstalled, sections, sync) {
1008
+ const res = getAgentResources(agentId, { cwd, scope: 'all', cliInstalled, home });
1009
+ const out = {};
1010
+ const withSync = (r, type, syncStatus) => {
1011
+ const item = { name: r.name, scope: r.scope };
1012
+ if (r.scope !== 'project') {
1013
+ const s = resolveResourceSyncState(agentId, r.name, type, syncStatus);
1014
+ if (s)
1015
+ item.syncState = s;
1016
+ }
1017
+ if (r.description)
1018
+ item.description = r.description;
1019
+ return item;
1020
+ };
1021
+ if (sections.has('commands'))
1022
+ out.commands = res.commands.map((r) => withSync(r, 'commands', sync.commands));
1023
+ if (sections.has('skills')) {
1024
+ out.skills = res.skills.map((r) => {
1025
+ const item = withSync(r, 'skills', sync.skills);
1026
+ // ruleCount of 0 is noise — every skill has 0 unless it ships subrules.
1027
+ if (r.ruleCount && r.ruleCount > 0)
1028
+ item.ruleCount = r.ruleCount;
1029
+ return item;
1030
+ });
1031
+ }
1032
+ if (sections.has('mcp')) {
1033
+ out.mcp = res.mcp.map((r) => {
1034
+ const item = { name: r.name, scope: r.scope };
1035
+ if (r.scope !== 'project')
1036
+ item.syncState = 'synced';
1037
+ return item;
1038
+ });
1039
+ }
1040
+ if (sections.has('memory'))
1041
+ out.memory = res.memory.map((r) => withSync(r, 'memory', sync.memory));
1042
+ if (sections.has('hooks'))
1043
+ out.hooks = res.hooks.map((r) => withSync(r, 'hooks', sync.hooks));
1044
+ if (sections.has('workflows'))
1045
+ out.workflows = res.workflows.map((r) => ({ name: r.name, scope: r.scope }));
1046
+ if (sections.has('plugins')) {
1047
+ out.plugins = discoverPlugins()
1048
+ .filter((p) => pluginSupportsAgent(p, agentId))
1049
+ .map((p) => ({ name: p.name }));
1050
+ }
1051
+ return out;
1052
+ }
1053
+ /** Build the set of resource sections to include in `--json` from the
1054
+ * `--resources` / `--detailed` flags, plus (in --json mode) the per-section
1055
+ * boolean filters (`--skills` etc.) that plain `--json` historically ignored. */
1056
+ export function parseResourceSections(options, jsonMode) {
1057
+ const set = new Set();
1058
+ const addAll = () => ALL_RESOURCE_SECTIONS.forEach((s) => set.add(s));
1059
+ if (options.detailed)
1060
+ addAll();
1061
+ const rv = options.resources;
1062
+ if (rv !== undefined) {
1063
+ if (rv === true || String(rv).trim() === '' || String(rv).toLowerCase() === 'all') {
1064
+ addAll();
1065
+ }
1066
+ else {
1067
+ for (const raw of String(rv).split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)) {
1068
+ const section = raw === 'rules' ? 'memory' : raw; // --rules is the memory file
1069
+ if (ALL_RESOURCE_SECTIONS.includes(section))
1070
+ set.add(section);
1071
+ }
1072
+ }
1073
+ }
1074
+ if (jsonMode) {
1075
+ const flagToSection = [
1076
+ ['commands', 'commands'], ['skills', 'skills'], ['mcp', 'mcp'],
1077
+ ['workflows', 'workflows'], ['plugins', 'plugins'], ['hooks', 'hooks'], ['rules', 'memory'],
1078
+ ];
1079
+ for (const [flag, section] of flagToSection)
1080
+ if (options[flag])
1081
+ set.add(section);
1082
+ }
1083
+ return set;
1084
+ }
976
1085
  /**
977
1086
  * Collect structured info for one or more agents without rendering to the
978
1087
  * terminal. Used by `--json` output and any programmatic consumer (e.g. the
979
1088
  * agents-cli extension's "resume current session in best available version"
980
1089
  * command).
981
1090
  */
982
- async function collectAgentsJson(filterAgentId) {
1091
+ async function collectAgentsJson(filterAgentId, resourceSections) {
983
1092
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
1093
+ const wantResources = !!resourceSections && resourceSections.size > 0;
1094
+ // Only pay for the git-status + resource scans when resources were requested.
1095
+ const resourceSync = wantResources ? await loadResourceSyncData() : null;
1096
+ const cliStates = wantResources ? await getAllCliStates() : null;
984
1097
  const infoFetches = [];
985
1098
  for (const agentId of agentsToShow) {
986
1099
  for (const ver of listInstalledVersions(agentId)) {
@@ -1004,7 +1117,10 @@ async function collectAgentsJson(filterAgentId) {
1004
1117
  return info;
1005
1118
  return {
1006
1119
  ...info,
1007
- plan: canon.plan,
1120
+ // Prefer a plan the live usage fetch surfaced (Kimi reports membership
1121
+ // tier in /usages; its local auth file has none) and fall back to the
1122
+ // account-derived plan (Claude's billingType).
1123
+ plan: usageByKey.get(key)?.snapshot?.plan ?? canon.plan,
1008
1124
  // Throttle state comes from the live usage windows, not the pay-as-you-go
1009
1125
  // overage flag that AccountInfo.usageStatus used to carry. A maxed window
1010
1126
  // means rate-limited; no snapshot means no badge. See
@@ -1014,7 +1130,7 @@ async function collectAgentsJson(filterAgentId) {
1014
1130
  };
1015
1131
  };
1016
1132
  const byAgent = new Map();
1017
- for (const { agentId, version, info: rawInfo } of infoResults) {
1133
+ for (const { agentId, version, home, info: rawInfo } of infoResults) {
1018
1134
  const info = mergeCanonical(rawInfo);
1019
1135
  const globalDefault = getGlobalDefault(agentId);
1020
1136
  const usageKey = getUsageLookupKey(info);
@@ -1025,6 +1141,7 @@ async function collectAgentsJson(filterAgentId) {
1025
1141
  isDefault: version === globalDefault,
1026
1142
  signedIn: info.signedIn,
1027
1143
  email: info.email,
1144
+ accountId: info.accountId,
1028
1145
  plan: info.plan,
1029
1146
  usageStatus: info.usageStatus,
1030
1147
  overageCredits: info.overageCredits,
@@ -1038,6 +1155,9 @@ async function collectAgentsJson(filterAgentId) {
1038
1155
  lastActive: info.lastActive ? info.lastActive.toISOString() : null,
1039
1156
  path: getVersionDir(agentId, version),
1040
1157
  };
1158
+ if (wantResources && resourceSync) {
1159
+ entry.resources = collectVersionResources(agentId, home, process.cwd(), cliStates?.[agentId]?.installed ?? false, resourceSections, resourceSync);
1160
+ }
1041
1161
  const existing = byAgent.get(agentId);
1042
1162
  if (existing)
1043
1163
  existing.push(entry);
@@ -1248,7 +1368,10 @@ export async function pruneDuplicates(filterAgentId, yes, dryRun) {
1248
1368
  * Exported for use by deprecated aliases.
1249
1369
  */
1250
1370
  export async function viewAction(agentArg, options) {
1251
- const json = options?.json === true;
1371
+ // --resources / --detailed imply --json (they only shape structured output).
1372
+ const explicitResources = options?.detailed === true || options?.resources !== undefined;
1373
+ const json = options?.json === true || explicitResources;
1374
+ const resourceSections = parseResourceSections(options ?? {}, json);
1252
1375
  const prune = options?.prune === true;
1253
1376
  const yes = options?.yes === true;
1254
1377
  const dryRun = options?.dryRun === true;
@@ -1280,7 +1403,7 @@ export async function viewAction(agentArg, options) {
1280
1403
  return;
1281
1404
  }
1282
1405
  if (json) {
1283
- const data = await collectAgentsJson();
1406
+ const data = await collectAgentsJson(undefined, resourceSections);
1284
1407
  console.log(JSON.stringify(data, null, 2));
1285
1408
  return;
1286
1409
  }
@@ -1329,9 +1452,9 @@ export async function viewAction(agentArg, options) {
1329
1452
  return;
1330
1453
  }
1331
1454
  if (json) {
1332
- // --json ignores the @version suffix (detailed resource view is not yet
1333
- // exposed as structured data). Emit the version list for the agent.
1334
- const data = await collectAgentsJson(agentId);
1455
+ // --json ignores the @version suffix, but --resources/--detailed (or a
1456
+ // section flag) now attach each version's resource inventory + sync-state.
1457
+ const data = await collectAgentsJson(agentId, resourceSections);
1335
1458
  console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], profiles: [] }, null, 2));
1336
1459
  return;
1337
1460
  }
@@ -1354,6 +1477,8 @@ export function registerViewCommand(program) {
1354
1477
  addHostOption(program.command('view [agent]'))
1355
1478
  .description('Show what agent CLIs are installed and which versions you have. Inspect resources when you pass agent@version.')
1356
1479
  .option('--json', 'Emit machine-readable JSON (version list, usage, signed-in status).')
1480
+ .option('--resources [sections]', 'In --json mode, include each version\'s resources: "all" (default) or a comma list (skills,plugins,mcp,commands,workflows,memory,hooks). Implies --json.')
1481
+ .option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
1357
1482
  .option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
1358
1483
  .option('--dry-run', 'With --prune, show duplicate versions without deleting')
1359
1484
  .option('-y, --yes', 'Skip the prune confirmation prompt.')
@@ -1381,6 +1506,11 @@ Examples:
1381
1506
  # Machine-readable output (used by tools that pick a version programmatically)
1382
1507
  agents view claude --json
1383
1508
 
1509
+ # One call: full inventory + what's synced on a host (installed agents,
1510
+ # versions, accounts, usage, and per-version resource sync-state)
1511
+ agents view claude --host yosemite-s0 --json --resources all
1512
+ agents view claude --json --resources skills,plugins # just those sections
1513
+
1384
1514
  # Prune older versions that duplicate an account already used by a newer version
1385
1515
  agents view --prune --dry-run
1386
1516
  agents view claude --prune
@@ -11,4 +11,10 @@
11
11
  * agents-cli.secrets.wallet.<id> JSON {pan, cvc, cardholder}
12
12
  */
13
13
  import type { Command } from 'commander';
14
+ import { type CardMetadata } from '../lib/wallet/index.js';
15
+ /** Width of the id column (incl. its 2-space gap): full UUID >=104, short id >=80, none below. */
16
+ export declare function walletIdWidth(cols: number): number;
17
+ /** Nickname column width. Shrinks so the whole row fits even at the 60-col floor. */
18
+ export declare function walletNickWidth(cols: number): number;
19
+ export declare function renderRow(c: CardMetadata, cols?: number): string;
14
20
  export declare function registerWalletCommands(program: Command): void;
@@ -11,6 +11,7 @@
11
11
  * agents-cli.secrets.wallet.<id> JSON {pan, cvc, cardholder}
12
12
  */
13
13
  import chalk from 'chalk';
14
+ import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
14
15
  import { addCard, detectBrand, isValidLuhn, listCards, removeCard, renameCard, showCard, } from '../lib/wallet/index.js';
15
16
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
16
17
  import { setHelpSections } from '../lib/help.js';
@@ -29,12 +30,25 @@ function brandLabel(b) {
29
30
  function formatExp(month, year) {
30
31
  return `${month}/${year.slice(-2)}`;
31
32
  }
32
- function renderRow(c) {
33
- const nick = c.nickname.padEnd(24);
33
+ /** Width of the id column (incl. its 2-space gap): full UUID >=104, short id >=80, none below. */
34
+ export function walletIdWidth(cols) {
35
+ return cols >= 104 ? 2 + 36 : cols >= 80 ? 2 + 8 : 0;
36
+ }
37
+ /** Nickname column width. Shrinks so the whole row fits even at the 60-col floor. */
38
+ export function walletNickWidth(cols) {
39
+ const rest = 2 + 1 + 18 + 1 + 10 + 1 + 9 + walletIdWidth(cols); // everything but the nickname
40
+ return Math.max(8, Math.min(24, cols - rest));
41
+ }
42
+ export function renderRow(c, cols = terminalWidth()) {
43
+ const nickW = walletNickWidth(cols);
44
+ const nick = truncateToWidth(c.nickname, nickW).padEnd(nickW);
34
45
  const brand = brandLabel(c.brand).padEnd(18);
35
46
  const last4 = `•••• ${c.last4}`.padEnd(10);
36
47
  const exp = formatExp(c.exp_month, c.exp_year);
37
- return ` ${chalk.cyan(nick)} ${brand} ${last4} ${chalk.gray('exp ' + exp)} ${chalk.gray(c.id)}`;
48
+ // Full 36-char UUID pushed rows to ~106 chars; size the id to the terminal.
49
+ const id = cols >= 104 ? c.id : cols >= 80 ? c.id.slice(0, 8) : '';
50
+ const idCol = id ? ` ${chalk.gray(id)}` : '';
51
+ return ` ${chalk.cyan(nick)} ${brand} ${last4} ${chalk.gray('exp ' + exp)}${idCol}`;
38
52
  }
39
53
  async function promptString(message, validate) {
40
54
  const { input } = await import('@inquirer/prompts');
@@ -131,9 +145,12 @@ export function registerWalletCommands(program) {
131
145
  console.log(chalk.gray('No cards in wallet. Try: agents wallet add'));
132
146
  return;
133
147
  }
134
- console.log(chalk.bold(` ${'NICKNAME'.padEnd(24)} ${'BRAND'.padEnd(18)} ${'LAST4'.padEnd(10)} EXP ID`));
148
+ const walletCols = terminalWidth();
149
+ const nickW = walletNickWidth(walletCols);
150
+ const idHeader = walletIdWidth(walletCols) ? ' ID' : '';
151
+ console.log(chalk.bold(` ${'NICKNAME'.padEnd(nickW)} ${'BRAND'.padEnd(18)} ${'LAST4'.padEnd(10)} EXP${idHeader}`));
135
152
  for (const c of cards)
136
- console.log(renderRow(c));
153
+ console.log(renderRow(c, walletCols));
137
154
  }
138
155
  catch (err) {
139
156
  console.error(chalk.red(err.message));