@phnx-labs/agents-cli 1.20.0 → 1.20.3

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 (105) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +4 -4
  3. package/dist/commands/cli.js +3 -3
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +24 -7
  6. package/dist/commands/exec.js +36 -16
  7. package/dist/commands/feedback.d.ts +7 -0
  8. package/dist/commands/feedback.js +89 -0
  9. package/dist/commands/helper.d.ts +12 -0
  10. package/dist/commands/helper.js +87 -0
  11. package/dist/commands/hooks.js +86 -7
  12. package/dist/commands/mcp.js +166 -10
  13. package/dist/commands/packages.js +196 -27
  14. package/dist/commands/permissions.js +21 -6
  15. package/dist/commands/profiles.d.ts +8 -0
  16. package/dist/commands/profiles.js +117 -4
  17. package/dist/commands/pull.js +4 -4
  18. package/dist/commands/routines.js +6 -6
  19. package/dist/commands/rules.js +8 -4
  20. package/dist/commands/secrets-migrate.d.ts +24 -0
  21. package/dist/commands/secrets-migrate.js +198 -0
  22. package/dist/commands/secrets-sync.d.ts +11 -0
  23. package/dist/commands/secrets-sync.js +155 -0
  24. package/dist/commands/secrets.js +74 -39
  25. package/dist/commands/skills.js +22 -5
  26. package/dist/commands/subagents.js +69 -49
  27. package/dist/commands/teams.js +48 -10
  28. package/dist/commands/utils.d.ts +33 -0
  29. package/dist/commands/utils.js +139 -0
  30. package/dist/commands/versions.js +4 -4
  31. package/dist/commands/view.d.ts +6 -0
  32. package/dist/commands/view.js +164 -8
  33. package/dist/commands/workflows.js +29 -6
  34. package/dist/index.js +4 -0
  35. package/dist/lib/acp/client.js +6 -1
  36. package/dist/lib/agents.d.ts +4 -0
  37. package/dist/lib/agents.js +18 -14
  38. package/dist/lib/auto-pull-worker.js +18 -1
  39. package/dist/lib/browser/chrome.js +4 -0
  40. package/dist/lib/browser/drivers/ssh.js +1 -1
  41. package/dist/lib/browser/profiles.d.ts +3 -3
  42. package/dist/lib/browser/profiles.js +3 -3
  43. package/dist/lib/browser/service.js +19 -0
  44. package/dist/lib/browser/types.d.ts +4 -4
  45. package/dist/lib/cli-resources.d.ts +36 -8
  46. package/dist/lib/cli-resources.js +268 -46
  47. package/dist/lib/cloud/factory.d.ts +1 -1
  48. package/dist/lib/cloud/factory.js +1 -1
  49. package/dist/lib/events.d.ts +16 -2
  50. package/dist/lib/events.js +33 -2
  51. package/dist/lib/exec.d.ts +39 -11
  52. package/dist/lib/exec.js +90 -31
  53. package/dist/lib/help.js +11 -5
  54. package/dist/lib/hooks/cache.d.ts +38 -0
  55. package/dist/lib/hooks/cache.js +242 -0
  56. package/dist/lib/hooks/profile.d.ts +33 -0
  57. package/dist/lib/hooks/profile.js +129 -0
  58. package/dist/lib/hooks.d.ts +0 -10
  59. package/dist/lib/hooks.js +68 -15
  60. package/dist/lib/mcp.d.ts +15 -0
  61. package/dist/lib/mcp.js +40 -0
  62. package/dist/lib/permissions.d.ts +13 -0
  63. package/dist/lib/permissions.js +51 -1
  64. package/dist/lib/plugins.js +15 -1
  65. package/dist/lib/profiles-presets.d.ts +26 -0
  66. package/dist/lib/profiles-presets.js +187 -8
  67. package/dist/lib/profiles.d.ts +34 -0
  68. package/dist/lib/profiles.js +112 -1
  69. package/dist/lib/routines-format.d.ts +17 -5
  70. package/dist/lib/routines-format.js +37 -16
  71. package/dist/lib/routines.d.ts +1 -1
  72. package/dist/lib/routines.js +2 -2
  73. package/dist/lib/runner.js +64 -10
  74. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  77. package/dist/lib/secrets/bundles.d.ts +18 -22
  78. package/dist/lib/secrets/bundles.js +75 -99
  79. package/dist/lib/secrets/index.d.ts +51 -27
  80. package/dist/lib/secrets/index.js +147 -156
  81. package/dist/lib/secrets/install-helper.d.ts +45 -0
  82. package/dist/lib/secrets/install-helper.js +165 -0
  83. package/dist/lib/secrets/linux.js +4 -4
  84. package/dist/lib/secrets/sync.d.ts +56 -0
  85. package/dist/lib/secrets/sync.js +180 -0
  86. package/dist/lib/session/render.js +4 -4
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/shims.d.ts +4 -1
  89. package/dist/lib/shims.js +5 -35
  90. package/dist/lib/state.d.ts +14 -1
  91. package/dist/lib/state.js +49 -5
  92. package/dist/lib/teams/agents.d.ts +5 -4
  93. package/dist/lib/teams/agents.js +47 -21
  94. package/dist/lib/teams/api.d.ts +2 -1
  95. package/dist/lib/teams/api.js +4 -3
  96. package/dist/lib/types.d.ts +57 -1
  97. package/dist/lib/types.js +2 -0
  98. package/dist/lib/usage.d.ts +27 -2
  99. package/dist/lib/usage.js +100 -17
  100. package/dist/lib/versions.d.ts +35 -1
  101. package/dist/lib/versions.js +267 -64
  102. package/package.json +9 -8
  103. package/scripts/install-helper.js +97 -0
  104. package/scripts/postinstall.js +16 -0
  105. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
@@ -4,6 +4,8 @@
4
4
  * Small helpers used across multiple commands: prompt cancellation detection,
5
5
  * table formatting, spinner management, and platform-specific workarounds.
6
6
  */
7
+ import type { AgentId } from '../lib/types.js';
8
+ import { type InstalledAgentTargetResult, type VersionSelectionResult } from '../lib/versions.js';
7
9
  /**
8
10
  * Check if an error is from user cancelling a prompt (Ctrl+C)
9
11
  */
@@ -58,3 +60,34 @@ export declare function promptRemovalTargets(resourceName: string, targets: Remo
58
60
  * Format a path for display, using ~ for home directory
59
61
  */
60
62
  export declare function formatPath(fullPath: string, cwd?: string): string;
63
+ /**
64
+ * Make sure every specific `agent@x.y.z` the user typed is installed before
65
+ * the caller resolves targets. Returns true if the caller should continue,
66
+ * false if the user declined the prompt. Exported so non-standard caller
67
+ * shapes (e.g. mcp.ts's manifest-shaped parser) can run the pre-flight
68
+ * without going through resolveAgentVersionTargets first.
69
+ */
70
+ export declare function ensureAgentVersionsInstalled(value: string, availableAgents: readonly AgentId[], options?: {
71
+ yes?: boolean;
72
+ }): Promise<boolean>;
73
+ /**
74
+ * Resolve a `--agents` selector and, if any requested `agent@version` isn't
75
+ * installed yet, prompt to install it (or auto-install with --yes) before
76
+ * delegating to resolveAgentVersionTargets. Returns null when the user
77
+ * declines the install prompt — callers should treat that as a clean cancel.
78
+ */
79
+ export declare function resolveAgentTargetsAutoInstalling(value: string, availableAgents: readonly AgentId[], options?: {
80
+ yes?: boolean;
81
+ allVersions?: boolean;
82
+ }): Promise<VersionSelectionResult | null>;
83
+ /**
84
+ * Same as resolveAgentTargetsAutoInstalling but returns the broader
85
+ * InstalledAgentTargetResult that includes `directAgents` (for paths like
86
+ * `agents install` and `mcp register` that fall through to unmanaged homes
87
+ * when no managed version is installed).
88
+ */
89
+ export declare function resolveInstalledAgentTargetsAutoInstalling(value: string, availableAgents: readonly AgentId[], options?: {
90
+ yes?: boolean;
91
+ allVersions?: boolean;
92
+ }): Promise<InstalledAgentTargetResult | null>;
93
+ export { VersionNotInstalledError } from '../lib/versions.js';
@@ -7,6 +7,10 @@
7
7
  import * as os from 'os';
8
8
  import { spawnSync } from 'child_process';
9
9
  import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import { confirm } from '@inquirer/prompts';
12
+ import { agentLabel, resolveAgentName } from '../lib/agents.js';
13
+ import { installVersion, listInstalledVersions, resolveAgentVersionTargets, resolveInstalledAgentTargets, } from '../lib/versions.js';
10
14
  /**
11
15
  * Check if an error is from user cancelling a prompt (Ctrl+C)
12
16
  */
@@ -135,3 +139,138 @@ export function formatPath(fullPath, cwd) {
135
139
  }
136
140
  return fullPath;
137
141
  }
142
+ /**
143
+ * Parse a --agents selector and collect every (agentId, specificVersion) pair
144
+ * the user requested where the version is a concrete x.y.z (not `default`,
145
+ * not `all`, not `latest`) and is NOT currently installed.
146
+ *
147
+ * This is the lookahead the auto-install wrappers use to decide whether to
148
+ * prompt + install before delegating to resolveAgentVersionTargets.
149
+ */
150
+ function collectMissingVersions(value, availableAgents) {
151
+ const missing = [];
152
+ const seen = new Set();
153
+ for (const raw of value.split(',').map((s) => s.trim()).filter(Boolean)) {
154
+ // Literal `all` / `all@all` expand to per-agent — never missing.
155
+ if (raw === 'all' || raw === 'all@all')
156
+ continue;
157
+ const atIndex = raw.indexOf('@');
158
+ if (atIndex === -1)
159
+ continue; // bare agent → resolves to default; never missing in this sense
160
+ const agentToken = raw.slice(0, atIndex).trim();
161
+ const versionToken = raw.slice(atIndex + 1).trim();
162
+ // Non-specific selectors handled by the underlying resolver.
163
+ if (!versionToken || versionToken === 'default' || versionToken === 'all')
164
+ continue;
165
+ const agentId = resolveAgentName(agentToken);
166
+ if (!agentId || !availableAgents.includes(agentId))
167
+ continue;
168
+ const installed = listInstalledVersions(agentId);
169
+ if (installed.includes(versionToken))
170
+ continue;
171
+ const key = `${agentId}@${versionToken}`;
172
+ if (seen.has(key))
173
+ continue;
174
+ seen.add(key);
175
+ missing.push({ agentId, version: versionToken });
176
+ }
177
+ return missing;
178
+ }
179
+ /**
180
+ * Sequentially install every requested missing version with a per-version
181
+ * spinner. Aborts via process.exit(1) on the first failure — the user
182
+ * already approved the install so a partial-install outcome is worse than
183
+ * a hard stop.
184
+ */
185
+ async function installMissingVersions(missing) {
186
+ for (const { agentId, version } of missing) {
187
+ const label = `${agentLabel(agentId)}@${version}`;
188
+ const spinner = ora(`Installing ${label}...`).start();
189
+ try {
190
+ const result = await installVersion(agentId, version, (msg) => {
191
+ spinner.text = msg;
192
+ });
193
+ if (!result.success) {
194
+ spinner.fail(`Failed to install ${label}: ${result.error ?? 'unknown error'}`);
195
+ process.exit(1);
196
+ }
197
+ spinner.succeed(`Installed ${label}`);
198
+ }
199
+ catch (err) {
200
+ spinner.fail(`Failed to install ${label}: ${err.message}`);
201
+ process.exit(1);
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Make sure every specific `agent@x.y.z` the user typed is installed before
207
+ * the caller resolves targets. Returns true if the caller should continue,
208
+ * false if the user declined the prompt. Exported so non-standard caller
209
+ * shapes (e.g. mcp.ts's manifest-shaped parser) can run the pre-flight
210
+ * without going through resolveAgentVersionTargets first.
211
+ */
212
+ export async function ensureAgentVersionsInstalled(value, availableAgents, options = {}) {
213
+ const missing = collectMissingVersions(value, availableAgents);
214
+ if (missing.length === 0)
215
+ return true;
216
+ const summary = missing.map((m) => `${agentLabel(m.agentId)}@${m.version}`).join(', ');
217
+ if (!options.yes) {
218
+ if (!isInteractiveTerminal()) {
219
+ console.error(chalk.red(`Missing agent version(s): ${summary}`));
220
+ console.error(chalk.gray('In a scripted shell, opt in to auto-install:'));
221
+ console.error(chalk.cyan(` rerun with --yes`));
222
+ console.error(chalk.gray('Or pre-install:'));
223
+ for (const m of missing) {
224
+ console.error(chalk.cyan(` agents add ${m.agentId}@${m.version}`));
225
+ }
226
+ process.exit(1);
227
+ }
228
+ console.log(chalk.yellow(`\nThe following agent version(s) are not installed:`));
229
+ for (const m of missing) {
230
+ console.log(` ${chalk.cyan(`${agentLabel(m.agentId)}@${m.version}`)}`);
231
+ }
232
+ let proceed;
233
+ try {
234
+ proceed = await confirm({
235
+ message: `Install ${missing.length} missing version${missing.length === 1 ? '' : 's'}?`,
236
+ default: true,
237
+ });
238
+ }
239
+ catch (err) {
240
+ if (isPromptCancelled(err))
241
+ return false;
242
+ throw err;
243
+ }
244
+ if (!proceed)
245
+ return false;
246
+ }
247
+ await installMissingVersions(missing);
248
+ return true;
249
+ }
250
+ /**
251
+ * Resolve a `--agents` selector and, if any requested `agent@version` isn't
252
+ * installed yet, prompt to install it (or auto-install with --yes) before
253
+ * delegating to resolveAgentVersionTargets. Returns null when the user
254
+ * declines the install prompt — callers should treat that as a clean cancel.
255
+ */
256
+ export async function resolveAgentTargetsAutoInstalling(value, availableAgents, options = {}) {
257
+ const ok = await ensureAgentVersionsInstalled(value, availableAgents, options);
258
+ if (!ok)
259
+ return null;
260
+ return resolveAgentVersionTargets(value, availableAgents, { allVersions: options.allVersions });
261
+ }
262
+ /**
263
+ * Same as resolveAgentTargetsAutoInstalling but returns the broader
264
+ * InstalledAgentTargetResult that includes `directAgents` (for paths like
265
+ * `agents install` and `mcp register` that fall through to unmanaged homes
266
+ * when no managed version is installed).
267
+ */
268
+ export async function resolveInstalledAgentTargetsAutoInstalling(value, availableAgents, options = {}) {
269
+ const ok = await ensureAgentVersionsInstalled(value, availableAgents, options);
270
+ if (!ok)
271
+ return null;
272
+ return resolveInstalledAgentTargets(value, availableAgents, { allVersions: options.allVersions });
273
+ }
274
+ // Re-export so callers can `catch (err) { if (err instanceof VersionNotInstalledError) … }`
275
+ // without reaching into ../lib/versions directly.
276
+ export { VersionNotInstalledError } from '../lib/versions.js';
@@ -7,7 +7,7 @@ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } f
7
7
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
8
8
  import { viewAction } from './view.js';
9
9
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
10
- import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
10
+ import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
11
11
  import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
12
12
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
13
13
  import { tryAutoPull } from '../lib/git.js';
@@ -255,7 +255,7 @@ export function registerVersionsCommands(program) {
255
255
  }
256
256
  const { agent, version } = parsed;
257
257
  const agentConfig = AGENTS[agent];
258
- if (!agentConfig.npmPackage) {
258
+ if (!agentConfig.npmPackage && !agentConfig.installScript) {
259
259
  console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
260
260
  continue;
261
261
  }
@@ -293,7 +293,7 @@ export function registerVersionsCommands(program) {
293
293
  // Smart resource detection: compare available vs ACTUALLY synced (source of truth: files)
294
294
  const available = getAvailableResources();
295
295
  const actuallySynced = getActuallySyncedResources(agent, installedVersion);
296
- const newResources = getNewResources(available, actuallySynced);
296
+ const newResources = getNewResources(available, actuallySynced, getProjectOnlyResources());
297
297
  const hasAnySynced = actuallySynced.commands.length > 0 ||
298
298
  actuallySynced.skills.length > 0 ||
299
299
  actuallySynced.hooks.length > 0 ||
@@ -577,7 +577,7 @@ export function registerVersionsCommands(program) {
577
577
  // Smart resource detection: compare available vs ACTUALLY synced (source of truth: files, not tracking)
578
578
  const available = getAvailableResources();
579
579
  const actuallySynced = getActuallySyncedResources(agentId, finalVersion);
580
- const newResources = getNewResources(available, actuallySynced);
580
+ const newResources = getNewResources(available, actuallySynced, getProjectOnlyResources());
581
581
  // Check if anything is actually synced (source of truth: actual files)
582
582
  const hasAnySynced = actuallySynced.commands.length > 0 ||
583
583
  actuallySynced.skills.length > 0 ||
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { Command } from 'commander';
10
10
  import type { AgentId } from '../lib/types.js';
11
+ import { type ProfileSummary } from '../lib/profiles.js';
11
12
  /** Machine-readable entry for a single installed version. */
12
13
  export interface ViewJsonVersion {
13
14
  version: string;
@@ -16,6 +17,10 @@ export interface ViewJsonVersion {
16
17
  email: string | null;
17
18
  plan: string | null;
18
19
  usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null;
20
+ overageCredits?: {
21
+ amount: number;
22
+ currency: string;
23
+ } | null;
19
24
  windows: Array<{
20
25
  key: 'session' | 'week' | 'sonnet_week';
21
26
  usedPercent: number;
@@ -28,6 +33,7 @@ export interface ViewJsonVersion {
28
33
  export interface ViewJsonAgent {
29
34
  agent: AgentId;
30
35
  versions: ViewJsonVersion[];
36
+ profiles: ProfileSummary[];
31
37
  }
32
38
  /**
33
39
  * Prune older installed versions that share an email with a newer installed
@@ -3,9 +3,9 @@ import ora from 'ora';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
6
- import { formatUsageSection, formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
6
+ import { formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
7
7
  import { readManifest } from '../lib/manifest.js';
8
- import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
8
+ import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
9
9
  import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
10
10
  import { getAgentResources } from '../lib/resources.js';
11
11
  import { WORKFLOW_CAPABLE_AGENTS } from '../lib/workflows.js';
@@ -16,8 +16,33 @@ import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
16
16
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
17
17
  import { composeRulesFromState } from '../lib/rules/compose.js';
18
18
  import { getConfiguredRunStrategy } from '../lib/rotate.js';
19
+ import { listProfiles, profileSummary } from '../lib/profiles.js';
19
20
  import { confirm } from '@inquirer/prompts';
20
21
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
22
+ /**
23
+ * Group profile summaries by their host harness, optionally filtered to a
24
+ * single agent. Profile YAMLs that fail validation are silently skipped by
25
+ * `listProfiles` so this never throws on a malformed file.
26
+ */
27
+ function getProfilesByAgent(filterAgentId) {
28
+ const byAgent = new Map();
29
+ for (const profile of listProfiles()) {
30
+ if (filterAgentId && profile.host.agent !== filterAgentId)
31
+ continue;
32
+ const summary = profileSummary(profile);
33
+ const existing = byAgent.get(profile.host.agent);
34
+ if (existing)
35
+ existing.push(summary);
36
+ else
37
+ byAgent.set(profile.host.agent, [summary]);
38
+ }
39
+ return byAgent;
40
+ }
41
+ /** Build the usage-column equivalent for a profile row: "profile <model>". */
42
+ function profileKindAndModel(model, planWidth) {
43
+ const kind = 'profile'.padEnd(Math.max(planWidth, 'profile'.length));
44
+ return `${kind} ${model}`;
45
+ }
21
46
  function termLink(text, filePath) {
22
47
  const url = `file://${filePath}`;
23
48
  return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
@@ -88,6 +113,32 @@ function getProjectVersionFromCwd(agent) {
88
113
  return null;
89
114
  }
90
115
  }
116
+ function getProfileSummaries(filterAgentId) {
117
+ return listProfiles()
118
+ .filter((profile) => !filterAgentId || profile.host.agent === filterAgentId)
119
+ .map(profileSummary);
120
+ }
121
+ function renderProfilesSection(profiles) {
122
+ if (profiles.length === 0)
123
+ return;
124
+ const nameWidth = Math.max(4, ...profiles.map((p) => p.name.length));
125
+ const hostWidth = Math.max(4, ...profiles.map((p) => p.host.length));
126
+ const providerWidth = Math.max(8, ...profiles.map((p) => p.provider.length));
127
+ console.log(chalk.bold('Profiles\n'));
128
+ console.log(` ${chalk.gray('NAME'.padEnd(nameWidth))} ` +
129
+ `${chalk.gray('HOST'.padEnd(hostWidth))} ` +
130
+ `${chalk.gray('PROVIDER'.padEnd(providerWidth))} ` +
131
+ chalk.gray('MODEL'));
132
+ for (const profile of profiles) {
133
+ console.log(` ${chalk.cyan(profile.name.padEnd(nameWidth))} ` +
134
+ `${profile.host.padEnd(hostWidth)} ` +
135
+ `${profile.provider.padEnd(providerWidth)} ` +
136
+ chalk.gray(profile.model));
137
+ }
138
+ console.log(chalk.gray('\n Run: agents run <profile> [prompt]'));
139
+ console.log(chalk.gray(' agents profiles view <profile>'));
140
+ console.log();
141
+ }
91
142
  /**
92
143
  * Show installed versions for one or all agents.
93
144
  * Called when: `agents view` or `agents view claude`
@@ -101,6 +152,8 @@ async function showInstalledVersions(filterAgentId) {
101
152
  spinner.stop();
102
153
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
103
154
  const showPaths = !!filterAgentId;
155
+ const profilesByAgent = getProfilesByAgent(filterAgentId);
156
+ const profileSummaries = [...profilesByAgent.values()].flat();
104
157
  // Auto-heal stale versioned aliases. Pre-v2 aliases (e.g. pre-CLAUDE_CONFIG_DIR
105
158
  // claude shims) silently route login through the default version's symlinked
106
159
  // home, so `agents view` would never reflect the right account. Regenerate on
@@ -190,15 +243,20 @@ async function showInstalledVersions(filterAgentId) {
190
243
  // Separate version-managed from globally-installed agents
191
244
  const versionManaged = [];
192
245
  const globallyInstalled = [];
246
+ const profileOnly = [];
193
247
  for (const agentId of agentsToShow) {
194
248
  const versions = listInstalledVersions(agentId);
195
249
  const cliState = cliStates[agentId];
250
+ const hasProfiles = (profilesByAgent.get(agentId)?.length ?? 0) > 0;
196
251
  if (versions.length > 0) {
197
252
  versionManaged.push(agentId);
198
253
  }
199
254
  else if (cliState?.installed) {
200
255
  globallyInstalled.push(agentId);
201
256
  }
257
+ else if (hasProfiles) {
258
+ profileOnly.push(agentId);
259
+ }
202
260
  }
203
261
  // Show version-managed agents
204
262
  if (versionManaged.length > 0) {
@@ -207,6 +265,7 @@ async function showInstalledVersions(filterAgentId) {
207
265
  let maxEmail = 0;
208
266
  let maxPlanWidth = 3;
209
267
  let maxUsageWidth = 0;
268
+ let maxStatusWidth = 0;
210
269
  for (const agentId of versionManaged) {
211
270
  const versions = listInstalledVersions(agentId);
212
271
  const globalDefault = getGlobalDefault(agentId);
@@ -220,8 +279,14 @@ async function showInstalledVersions(filterAgentId) {
220
279
  if (info?.plan)
221
280
  maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
222
281
  }
282
+ // Profile rows share these columns with version rows so they line up.
283
+ for (const profile of profilesByAgent.get(agentId) ?? []) {
284
+ maxVerLabel = Math.max(maxVerLabel, profile.name.length);
285
+ maxEmail = Math.max(maxEmail, profile.auth.length);
286
+ maxPlanWidth = Math.max(maxPlanWidth, 'profile'.length);
287
+ }
223
288
  }
224
- // Second pass: compute max visible usage width (now that maxPlanWidth is settled)
289
+ // Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled)
225
290
  for (const agentId of versionManaged) {
226
291
  const versions = listInstalledVersions(agentId);
227
292
  for (const v of versions) {
@@ -231,6 +296,12 @@ async function showInstalledVersions(filterAgentId) {
231
296
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
232
297
  const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth);
233
298
  maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
299
+ const statusStr = formatUsageStatusBadge(info?.usageStatus);
300
+ maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
301
+ }
302
+ for (const profile of profilesByAgent.get(agentId) ?? []) {
303
+ const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
304
+ maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageEquivalent));
234
305
  }
235
306
  }
236
307
  for (const agentId of versionManaged) {
@@ -280,6 +351,11 @@ async function showInstalledVersions(filterAgentId) {
280
351
  const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
281
352
  parts.push(usageStr + usagePad);
282
353
  }
354
+ const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
355
+ if (maxStatusWidth > 0) {
356
+ const statusPad = ' '.repeat(Math.max(0, maxStatusWidth - visibleWidth(statusStr)));
357
+ parts.push(statusStr + statusPad);
358
+ }
283
359
  if (hasActive)
284
360
  parts.push(activeStr);
285
361
  }
@@ -289,6 +365,18 @@ async function showInstalledVersions(filterAgentId) {
289
365
  console.log(chalk.gray(` ${versionDir}`));
290
366
  }
291
367
  }
368
+ // Profile rows share the same columns as versions: name | auth | "profile"+model.
369
+ // No status badge, no last-active — profiles don't accumulate usage state.
370
+ for (const profile of profilesByAgent.get(agentId) ?? []) {
371
+ const nameCol = chalk.cyan(profile.name.padEnd(maxVerLabel));
372
+ const authCol = chalk.gray(profile.auth.padEnd(maxEmail));
373
+ const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
374
+ const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageEquivalent)));
375
+ console.log(` ${nameCol} ${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
376
+ if (showPaths) {
377
+ console.log(chalk.gray(` ${profile.path}`));
378
+ }
379
+ }
292
380
  // Check for project override
293
381
  const projectVersion = getProjectVersionFromCwd(agentId);
294
382
  if (projectVersion && projectVersion !== globalDefault) {
@@ -305,6 +393,19 @@ async function showInstalledVersions(filterAgentId) {
305
393
  const cliState = cliStates[agentId];
306
394
  return `${cliState?.version || 'installed'} (global)`.length;
307
395
  }));
396
+ // Pre-pass: max badge width so rows with `lastActive` line up whether or
397
+ // not THIS row carries a throttle badge. Without this, the row that DOES
398
+ // have "out of credits" shifts every other row's `lastActive` left by
399
+ // ~16 chars, exactly what the version-managed block at maxStatusWidth
400
+ // already solves above.
401
+ let gMaxStatusWidth = 0;
402
+ for (const agentId of globallyInstalled) {
403
+ const gInfoRaw = globalInfoMap.get(agentId);
404
+ const gInfo = gInfoRaw ? mergeCanonical(gInfoRaw) : undefined;
405
+ const w = visibleWidth(formatUsageStatusBadge(gInfo?.usageStatus));
406
+ if (w > gMaxStatusWidth)
407
+ gMaxStatusWidth = w;
408
+ }
308
409
  for (const agentId of globallyInstalled) {
309
410
  const agent = AGENTS[agentId];
310
411
  const cliState = cliStates[agentId];
@@ -323,25 +424,77 @@ async function showInstalledVersions(filterAgentId) {
323
424
  parts.push(gInfo?.email ? chalk.cyan(gInfo.email) : '');
324
425
  if (gUsageStr || gActiveStr)
325
426
  parts.push(gUsageStr);
427
+ const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
428
+ if (gMaxStatusWidth > 0) {
429
+ const statusPad = ' '.repeat(Math.max(0, gMaxStatusWidth - visibleWidth(gStatusStr)));
430
+ parts.push(gStatusStr + statusPad);
431
+ }
326
432
  if (gActiveStr)
327
433
  parts.push(gActiveStr);
328
434
  console.log(parts.join(' '));
329
435
  if (showPaths && cliState?.path) {
330
436
  console.log(chalk.gray(` ${cliState.path}`));
331
437
  }
438
+ // Profile rows under a globally-installed harness. Use a simpler
439
+ // alignment here since this section doesn't share column state with
440
+ // the version-managed block.
441
+ const profilesHere = profilesByAgent.get(agentId) ?? [];
442
+ if (profilesHere.length > 0) {
443
+ const nameWidth = Math.max(globalMaxVerLabel, ...profilesHere.map((p) => p.name.length));
444
+ const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));
445
+ for (const profile of profilesHere) {
446
+ console.log(` ${chalk.cyan(profile.name.padEnd(nameWidth))} ` +
447
+ `${chalk.gray(profile.auth.padEnd(authWidth))} ` +
448
+ `${chalk.gray('profile')} ` +
449
+ chalk.gray(profile.model));
450
+ if (showPaths) {
451
+ console.log(chalk.gray(` ${profile.path}`));
452
+ }
453
+ }
454
+ }
332
455
  if (agent.npmPackage && cliState?.version) {
333
456
  console.log(chalk.gray(` Manage: agents add ${agentId}@${cliState.version} -y`));
334
457
  }
335
458
  console.log();
336
459
  }
337
460
  }
461
+ // Agents with no install but with profiles defined — render under the same
462
+ // harness header so users find them where they look.
463
+ if (profileOnly.length > 0) {
464
+ if (versionManaged.length === 0 && globallyInstalled.length === 0) {
465
+ console.log(chalk.bold('Profile-only Agents\n'));
466
+ }
467
+ for (const agentId of profileOnly) {
468
+ const profilesHere = profilesByAgent.get(agentId) ?? [];
469
+ console.log(` ${chalk.bold(agentLabel(agentId))}${chalk.yellow(' (profile only)')}`);
470
+ const nameWidth = Math.max(...profilesHere.map((p) => p.name.length));
471
+ const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));
472
+ for (const profile of profilesHere) {
473
+ console.log(` ${chalk.cyan(profile.name.padEnd(nameWidth))} ` +
474
+ `${chalk.gray(profile.auth.padEnd(authWidth))} ` +
475
+ `${chalk.gray('profile')} ` +
476
+ chalk.gray(profile.model));
477
+ if (showPaths) {
478
+ console.log(chalk.gray(` ${profile.path}`));
479
+ }
480
+ }
481
+ console.log();
482
+ }
483
+ }
338
484
  // If filtering to a specific agent and not found
339
- if (filterAgentId && versionManaged.length === 0 && globallyInstalled.length === 0) {
485
+ if (filterAgentId &&
486
+ versionManaged.length === 0 &&
487
+ globallyInstalled.length === 0 &&
488
+ profileOnly.length === 0) {
340
489
  console.log(` ${chalk.bold(agentLabel(filterAgentId))}: ${chalk.gray('not installed')}`);
341
490
  console.log();
342
491
  }
343
492
  // No agents installed at all
344
- if (versionManaged.length === 0 && globallyInstalled.length === 0 && !filterAgentId) {
493
+ if (versionManaged.length === 0 &&
494
+ globallyInstalled.length === 0 &&
495
+ profileOnly.length === 0 &&
496
+ profileSummaries.length === 0 &&
497
+ !filterAgentId) {
345
498
  console.log(chalk.gray(' No agent CLIs installed.'));
346
499
  console.log(chalk.gray(' Run: agents add claude@latest'));
347
500
  console.log();
@@ -363,7 +516,8 @@ async function showInstalledVersions(filterAgentId) {
363
516
  if (defaultVersion) {
364
517
  const available = getAvailableResources();
365
518
  const synced = getActuallySyncedResources(filterAgentId, defaultVersion);
366
- const newResources = getNewResources(available, synced);
519
+ const projectOnly = getProjectOnlyResources();
520
+ const newResources = getNewResources(available, synced, projectOnly);
367
521
  if (hasNewResources(newResources, filterAgentId, defaultVersion)) {
368
522
  try {
369
523
  const selection = await promptNewResourceSelection(filterAgentId, newResources, defaultVersion);
@@ -742,6 +896,7 @@ async function collectAgentsJson(filterAgentId) {
742
896
  email: info.email,
743
897
  plan: info.plan,
744
898
  usageStatus: info.usageStatus,
899
+ overageCredits: info.overageCredits,
745
900
  windows: snapshot
746
901
  ? snapshot.windows.map((w) => ({
747
902
  key: w.key,
@@ -758,6 +913,7 @@ async function collectAgentsJson(filterAgentId) {
758
913
  else
759
914
  byAgent.set(agentId, [entry]);
760
915
  }
916
+ const profilesByAgent = getProfilesByAgent(filterAgentId);
761
917
  const out = [];
762
918
  for (const agentId of agentsToShow) {
763
919
  const versions = byAgent.get(agentId) ?? [];
@@ -766,7 +922,7 @@ async function collectAgentsJson(filterAgentId) {
766
922
  return a.isDefault ? -1 : 1;
767
923
  return compareVersions(b.version, a.version);
768
924
  });
769
- out.push({ agent: agentId, versions });
925
+ out.push({ agent: agentId, versions, profiles: profilesByAgent.get(agentId) ?? [] });
770
926
  }
771
927
  return out;
772
928
  }
@@ -1009,7 +1165,7 @@ export async function viewAction(agentArg, options) {
1009
1165
  // --json ignores the @version suffix (detailed resource view is not yet
1010
1166
  // exposed as structured data). Emit the version list for the agent.
1011
1167
  const data = await collectAgentsJson(agentId);
1012
- console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [] }, null, 2));
1168
+ console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], profiles: [] }, null, 2));
1013
1169
  return;
1014
1170
  }
1015
1171
  if (requestedVersion) {
@@ -7,9 +7,9 @@ import { select, checkbox } from '@inquirer/prompts';
7
7
  import { resolveAgentName, agentLabel } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { WORKFLOW_CAPABLE_AGENTS, discoverWorkflowsFromRepo, installWorkflowCentrally, removeWorkflow, listInstalledWorkflows, listWorkflowsForAgent, removeWorkflowFromVersion, iterWorkflowsCapableVersions, } from '../lib/workflows.js';
10
- import { getVersionHomePath, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, resolveAgentVersionTargets, } from '../lib/versions.js';
10
+ import { getVersionHomePath, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, } from '../lib/versions.js';
11
11
  import { recordVersionResources, getUserWorkflowsDir } from '../lib/state.js';
12
- import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, printWithPager, promptRemovalTargets, } from './utils.js';
12
+ import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, printWithPager, promptRemovalTargets, parseCommaSeparatedList, resolveAgentTargetsAutoInstalling, } from './utils.js';
13
13
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
14
14
  /** Register the `agents workflows` command tree (list, view, add, remove). */
15
15
  export function registerWorkflowsCommands(program) {
@@ -92,18 +92,25 @@ Examples:
92
92
  workflowsCmd
93
93
  .command('add [source]')
94
94
  .description('Install workflows from a source (GitHub, local) or pick from central storage')
95
- .option('-a, --agents <list>', 'Targets: claude, claude@2.1.138')
95
+ .option('-a, --agents <list>', 'Targets: claude, claude@2.1.138, claude@all, all')
96
+ .option('--names <list>', 'Workflow names from the source (comma-separated)')
96
97
  .option('-y, --yes', 'Skip confirmation prompts')
97
98
  .addHelpText('after', `
98
99
  Examples:
99
100
  # Install from GitHub
100
101
  agents workflows add gh:user/workflows
101
102
 
103
+ # Pluck specific workflows from a multi-workflow repo
104
+ agents workflows add gh:user/workflows --names rdev,plan
105
+
102
106
  # Install a local workflow directory (must contain WORKFLOW.md)
103
107
  agents workflows add ./rdev
104
108
 
105
109
  # Install and sync to a specific version
106
110
  agents workflows add gh:user/workflows --agents claude@2.1.138
111
+
112
+ # Install across every installed Claude version
113
+ agents workflows add gh:user/workflows --agents claude@all
107
114
  `)
108
115
  .action(async (source, options) => {
109
116
  try {
@@ -156,11 +163,23 @@ Examples:
156
163
  }
157
164
  spinner.succeed('Using local path');
158
165
  }
159
- const discovered = discoverWorkflowsFromRepo(localPath);
166
+ let discovered = discoverWorkflowsFromRepo(localPath);
160
167
  if (discovered.length === 0) {
161
168
  console.log(chalk.yellow('No workflows found (looking for WORKFLOW.md files)'));
162
169
  return;
163
170
  }
171
+ // --names filter: pluck specific workflows from a multi-workflow source.
172
+ const requestedNames = parseCommaSeparatedList(options.names);
173
+ if (requestedNames.length > 0) {
174
+ const discoveredNames = new Set(discovered.map((w) => w.name));
175
+ const missing = requestedNames.filter((n) => !discoveredNames.has(n));
176
+ if (missing.length > 0) {
177
+ console.log(chalk.red(`\nWorkflow(s) not found in source: ${missing.join(', ')}`));
178
+ console.log(chalk.gray(`Available: ${[...discoveredNames].join(', ')}`));
179
+ process.exit(1);
180
+ }
181
+ discovered = discovered.filter((w) => requestedNames.includes(w.name));
182
+ }
164
183
  console.log(chalk.bold(`\nFound ${discovered.length} workflow(s):`));
165
184
  for (const w of discovered) {
166
185
  console.log(`\n ${chalk.cyan(w.name)}: ${w.frontmatter.description || 'no description'}`);
@@ -191,13 +210,17 @@ Examples:
191
210
  let selectedAgents;
192
211
  let versionSelections;
193
212
  if (options.agents) {
194
- const result = resolveAgentVersionTargets(options.agents, WORKFLOW_CAPABLE_AGENTS);
213
+ const result = await resolveAgentTargetsAutoInstalling(options.agents, WORKFLOW_CAPABLE_AGENTS, { yes: options.yes });
214
+ if (!result) {
215
+ console.log(chalk.gray('Cancelled.'));
216
+ return;
217
+ }
195
218
  selectedAgents = result.selectedAgents;
196
219
  versionSelections = result.versionSelections;
197
220
  }
198
221
  else {
199
222
  const result = await promptAgentVersionSelection(WORKFLOW_CAPABLE_AGENTS, {
200
- skipPrompts: options.yes || !isInteractiveTerminal(),
223
+ skipPrompts: options.yes,
201
224
  });
202
225
  selectedAgents = result.selectedAgents;
203
226
  versionSelections = result.versionSelections;