@phnx-labs/agents-cli 1.20.31 → 1.20.33

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 (74) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/commands/commands.js +3 -3
  3. package/dist/commands/computer-actions.js +1 -0
  4. package/dist/commands/cost.js +2 -2
  5. package/dist/commands/doctor.js +2 -2
  6. package/dist/commands/exec.js +56 -1
  7. package/dist/commands/hooks.js +3 -3
  8. package/dist/commands/inspect.js +13 -17
  9. package/dist/commands/mcp.js +3 -3
  10. package/dist/commands/permissions.js +3 -3
  11. package/dist/commands/rules.js +2 -2
  12. package/dist/commands/sessions.js +18 -1
  13. package/dist/commands/skills.js +3 -3
  14. package/dist/commands/ssh.js +23 -0
  15. package/dist/commands/sync.js +2 -2
  16. package/dist/commands/teams.js +7 -12
  17. package/dist/commands/usage.js +2 -2
  18. package/dist/commands/utils.d.ts +8 -0
  19. package/dist/commands/utils.js +20 -0
  20. package/dist/commands/versions.js +2 -2
  21. package/dist/commands/view.js +33 -9
  22. package/dist/commands/workflows.js +3 -3
  23. package/dist/index.js +12 -0
  24. package/dist/lib/agent-spec/index.d.ts +18 -0
  25. package/dist/lib/agent-spec/index.js +35 -0
  26. package/dist/lib/agent-spec/primitives.d.ts +28 -0
  27. package/dist/lib/agent-spec/primitives.js +57 -0
  28. package/dist/lib/agent-spec/provider.d.ts +2 -0
  29. package/dist/lib/agent-spec/provider.js +9 -0
  30. package/dist/lib/agent-spec/resolve.d.ts +33 -0
  31. package/dist/lib/agent-spec/resolve.js +174 -0
  32. package/dist/lib/agent-spec/types.d.ts +57 -0
  33. package/dist/lib/agent-spec/types.js +18 -0
  34. package/dist/lib/crabbox/cli.d.ts +98 -0
  35. package/dist/lib/crabbox/cli.js +218 -0
  36. package/dist/lib/crabbox/lease.d.ts +41 -0
  37. package/dist/lib/crabbox/lease.js +73 -0
  38. package/dist/lib/crabbox/runtimes.d.ts +57 -0
  39. package/dist/lib/crabbox/runtimes.js +109 -0
  40. package/dist/lib/daemon.js +32 -0
  41. package/dist/lib/devices/pending.d.ts +18 -0
  42. package/dist/lib/devices/pending.js +103 -0
  43. package/dist/lib/devices/sync.d.ts +21 -2
  44. package/dist/lib/devices/sync.js +26 -10
  45. package/dist/lib/hosts/dispatch.d.ts +27 -10
  46. package/dist/lib/hosts/dispatch.js +55 -19
  47. package/dist/lib/hosts/option.d.ts +14 -0
  48. package/dist/lib/hosts/option.js +19 -0
  49. package/dist/lib/hosts/passthrough.d.ts +30 -0
  50. package/dist/lib/hosts/passthrough.js +141 -0
  51. package/dist/lib/hosts/remote-cmd.d.ts +36 -0
  52. package/dist/lib/hosts/remote-cmd.js +56 -0
  53. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  54. package/dist/lib/secrets/bundles.js +29 -20
  55. package/dist/lib/secrets/index.d.ts +11 -0
  56. package/dist/lib/secrets/index.js +18 -1
  57. package/dist/lib/secrets/linux.d.ts +14 -0
  58. package/dist/lib/secrets/linux.js +21 -0
  59. package/dist/lib/session/active.d.ts +8 -0
  60. package/dist/lib/session/active.js +18 -1
  61. package/dist/lib/session/provenance.d.ts +56 -0
  62. package/dist/lib/session/provenance.js +157 -0
  63. package/dist/lib/ssh-exec.d.ts +22 -0
  64. package/dist/lib/ssh-exec.js +59 -2
  65. package/dist/lib/ssh-tunnel.d.ts +0 -5
  66. package/dist/lib/ssh-tunnel.js +65 -8
  67. package/dist/lib/state.d.ts +2 -0
  68. package/dist/lib/state.js +2 -0
  69. package/dist/lib/sync-umbrella.js +10 -6
  70. package/dist/lib/versions.d.ts +13 -4
  71. package/dist/lib/versions.js +27 -20
  72. package/package.json +2 -1
  73. package/dist/lib/agent-spec.d.ts +0 -36
  74. package/dist/lib/agent-spec.js +0 -157
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.31
6
+
7
+ **`agents sessions <id>`: a catch-up digest for switching between many agents (#502)**
8
+
9
+ - Opening a single session now leads with its auto-inferred title (user `/rename` > Claude `ai-title` > first-prompt topic) and PR / worktree / ticket badges, then a **Changes** section that groups touched files by directory and tags each as created / modified / deleted (with a `+N ~N -N` summary) instead of the old flat "Modified" list, a **Tools** histogram (per-tool call counts), and a **Tests** verdict parsed from the last `vitest` / `jest` / `pytest` / `go test` / `cargo test` / `tsc` run. The same signals are folded into the interactive picker preview.
10
+ - `agents sessions --active` now collapses the many subagent/fork PIDs of one session into a single row with a `×N` count instead of printing dozens of identical lines. Source: `src/lib/session/digest.ts`, `src/lib/session/render.ts`, `src/lib/session/active.ts`, `src/commands/sessions.ts`, `src/commands/sessions-picker.ts`.
11
+
12
+ ## 1.20.30
13
+
14
+ **`agents sessions` live state engine: waiting / PR / worktree / ticket detection + reliable preview (#494)**
15
+
16
+ - `agents sessions --active` infers real activity from each transcript's tail — **working** / **waiting** / **idle** — rather than the old mtime-only running/idle guess, using structural signals (Claude `ExitPlanMode` / `AskUserQuestion`) plus a question + mtime heuristic for Codex. It detects and badges a PR opened during the session (`gh pr create` + the resulting pull URL), a git worktree (`.agents/worktrees/<slug>/`), and a Linear/Jira ticket (from the prompt or branch), and shows the latest turn as the preview instead of the first prompt.
17
+ - `--waiting` filters `--active` to only sessions blocked on your input and exits non-zero (a scriptable gate); `--tree` groups the listing by directory, dropping the id/version columns while keeping the short-id handle.
18
+ - The preview line is now width-correct: measurement is ANSI- and wide-char-aware and reads `$COLUMNS` first, so it no longer wraps or drifts under tmux or over `--host` SSH (the remote is handed the caller's width). Session index schema v7 persists the PR / worktree / ticket signals so historical listings carry them too. Source: `src/lib/session/state.ts`, `src/lib/session/tail.ts`, `src/lib/session/width.ts`, `src/lib/session/{discover,db,active}.ts`, `src/commands/sessions.ts`.
19
+
5
20
  **`agents sessions --host <machine>`: query a remote machine's sessions live over SSH**
6
21
 
7
22
  - `agents sessions "<query>" --host <alias|user@host>` runs the same session query on a remote machine's own index over SSH and streams the result back — repeat `--host` (or pass several) to fan out across machines. SSH access is the only auth; there's no daemon or shared store. Targets are validated against a strict allowlist (`SSH_TARGET_RE`) to block flag-smuggling, and the forwarded invocation is double-quoted (`shellQuote`) so a query like `$(whoami)` survives as a literal string on both shell layers. Source: `src/lib/session/remote.ts`, `src/commands/sessions.ts`, `docs/05-sessions.md`.
@@ -9,9 +9,9 @@ import { cloneRepo } from '../lib/git.js';
9
9
  import { discoverCommands, resolveCommandSource, installCommandCentrally, listCentralCommands, listInstalledCommandsWithScope, getCommandInfo, diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
10
10
  import { getCommandsDir } from '../lib/state.js';
11
11
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
12
- import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveInstalledAgentTargets, } from '../lib/versions.js';
12
+ import { listInstalledVersions, getGlobalDefault, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveInstalledAgentTargets, } from '../lib/versions.js';
13
13
  import { recordVersionResources } from '../lib/state.js';
14
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, resolveAgentTargetsAutoInstalling, } from './utils.js';
14
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, resolveAgentTargetsAutoInstalling, resolveListFilterOrExit, } from './utils.js';
15
15
  /** Register the `agents commands` command tree (list, add, remove, sync, prune, view). */
16
16
  export function registerCommandsCommands(program) {
17
17
  const commandsCmd = program
@@ -59,7 +59,7 @@ When to use:
59
59
  process.exit(1);
60
60
  }
61
61
  filterAgent = resolved;
62
- filterVersion = resolveVersionAlias(resolved, parts[1]);
62
+ filterVersion = resolveListFilterOrExit(resolved, parts[1]);
63
63
  }
64
64
  const rows = buildCommandRows({ filterAgent, filterVersion });
65
65
  spinner.stop();
@@ -485,6 +485,7 @@ export function registerActionCommands(program) {
485
485
  .option('--bundle <id>', 'Bundle id (e.g. com.apple.TextEdit)')
486
486
  .option('--path <p>', 'Path to the .app bundle')
487
487
  .option('--name <s>', 'App name (resolved via /Applications and LaunchServices)')
488
+ .option('--host <device>', 'Drive a remote Windows device (requires `agents computer start --host <device>` first)')
488
489
  .option('--json', 'Emit JSON')
489
490
  .action(async (opts) => {
490
491
  if (!opts.bundle && !opts.path && !opts.name) {
@@ -1,11 +1,11 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
3
4
  import { queryUsageRollup, topSessionsByCost, } from '../lib/session/db.js';
4
5
  import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
5
6
  import { formatDuration } from '../lib/session/render.js';
6
7
  export function registerCostCommand(program) {
7
- program
8
- .command('cost')
8
+ addHostOption(program.command('cost'))
9
9
  .description('Roll up $ cost and duration across local agent sessions')
10
10
  .option('--json', 'Output the rollup as JSON')
11
11
  .option('--since <time>', 'Only sessions newer than this (e.g., 7d, 4w, or ISO date)')
@@ -1,3 +1,4 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import { checkAllClis } from '../lib/teams/agents.js';
3
4
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
@@ -475,8 +476,7 @@ async function runFix(parsed, opts) {
475
476
  }
476
477
  // ─── command registration ────────────────────────────────────────────────────
477
478
  export function registerDoctorCommand(program) {
478
- const doctorCmd = program
479
- .command('doctor [target]')
479
+ const doctorCmd = addHostOption(program.command('doctor [target]'))
480
480
  .description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
481
481
  .option('--json', 'Output machine-readable JSON')
482
482
  .option('--diff', 'In target mode, include unified diffs for divergent files')
@@ -136,7 +136,9 @@ export function registerRunCommand(program) {
136
136
  .option('--host <name>', 'Offload this run onto a registered agent host over SSH instead of running locally. See `agents hosts`.')
137
137
  .option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
138
138
  .option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
139
- .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
139
+ .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
140
+ .option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
141
+ .option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.');
140
142
  // `--on` and `--computer` are hidden aliases of `--host` — same behavior.
141
143
  runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
142
144
  runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
@@ -191,6 +193,59 @@ export function registerRunCommand(program) {
191
193
  // Use command.args (all positional strings) and strip the declared positional args from the front.
192
194
  const declaredArgCount = prompt !== undefined ? 2 : 1;
193
195
  const passthroughArgs = command.args.slice(declaredArgCount);
196
+ // --lease: invent a disposable cloud box for this run (via crabbox), run
197
+ // the agent there, then tear it down. Unlike --host, nothing is registered.
198
+ if (options.lease) {
199
+ if (prompt === undefined) {
200
+ console.error(chalk.red('A prompt is required for leased runs: agents run <agent> "<task>" --lease'));
201
+ process.exit(1);
202
+ }
203
+ const backend = typeof options.lease === 'string' ? options.lease : undefined;
204
+ const { detectSignedInRuntimes, pickRuntimes } = await import('../lib/crabbox/runtimes.js');
205
+ const { leaseAndRun } = await import('../lib/crabbox/lease.js');
206
+ const { confirm } = await import('@inquirer/prompts');
207
+ const detected = await detectSignedInRuntimes();
208
+ const runtimes = await pickRuntimes(detected);
209
+ if (runtimes.length === 0) {
210
+ console.error(chalk.yellow('No runtimes selected. Sign into one locally (e.g. run `claude` once) then retry.'));
211
+ process.exit(1);
212
+ }
213
+ // Security gate: copying auth tokens to an ephemeral cloud box is opt-in
214
+ // per run. Name the runtimes + accounts so the user sees what ships.
215
+ const names = runtimes
216
+ .map((id) => {
217
+ const d = detected.find((x) => x.id === id);
218
+ return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
219
+ })
220
+ .join(', ');
221
+ const ok = await confirm({
222
+ message: `Copy credentials for ${names} to a disposable cloud box, run there, then destroy it?`,
223
+ default: false,
224
+ });
225
+ if (!ok) {
226
+ console.error(chalk.yellow('Aborted — no credentials pushed, no box leased.'));
227
+ process.exit(1);
228
+ }
229
+ try {
230
+ const { exitCode, box, toreDown } = await leaseAndRun({
231
+ agent: agentSpec.split('@')[0],
232
+ prompt,
233
+ mode: options.mode,
234
+ model: options.model,
235
+ backend,
236
+ runtimes,
237
+ detected,
238
+ secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
239
+ keep: options.keepBox,
240
+ });
241
+ console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop --id ${box.slug}`));
242
+ process.exit(exitCode === null ? 1 : exitCode);
243
+ }
244
+ catch (err) {
245
+ console.error(chalk.red(err.message));
246
+ process.exit(1);
247
+ }
248
+ }
194
249
  // --host/--on/--computer: offload this run onto a registered agent host
195
250
  // over SSH instead of running locally. The three flags are aliases.
196
251
  const hostGiven = [options.host, options.on, options.computer].filter((v) => !!v);
@@ -8,9 +8,9 @@ import { AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/
8
8
  import { supports, capableAgents } from '../lib/capabilities.js';
9
9
  import { cloneRepo } from '../lib/git.js';
10
10
  import { discoverHooksFromRepo, installHooksCentrally, listCentralHooks, listInstalledHooksWithScope, getHookInfo, parseHookManifest, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
11
- import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveInstalledAgentTargets, } from '../lib/versions.js';
11
+ import { listInstalledVersions, getGlobalDefault, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveInstalledAgentTargets, } from '../lib/versions.js';
12
12
  import { recordVersionResources } from '../lib/state.js';
13
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, resolveAgentTargetsAutoInstalling, } from './utils.js';
13
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, resolveAgentTargetsAutoInstalling, resolveListFilterOrExit, } from './utils.js';
14
14
  /** Register the `agents hooks` command tree (list, add, remove, sync, prune, view). */
15
15
  export function registerHooksCommands(program) {
16
16
  const hooksCmd = program.command('hooks')
@@ -61,7 +61,7 @@ When to use:
61
61
  console.log(chalk.red(formatAgentError(agentName, [...capableAgents('hooks')])));
62
62
  process.exit(1);
63
63
  }
64
- requestedVersion = resolveVersionAlias(agentId, parts[1]) ?? null;
64
+ requestedVersion = resolveListFilterOrExit(agentId, parts[1]) ?? null;
65
65
  }
66
66
  // Load hook manifest for event display
67
67
  const hookManifest = parseHookManifest();
@@ -12,6 +12,7 @@
12
12
  * AGENT.md / the file itself) so users can click straight to the source.
13
13
  */
14
14
  import { execSync } from 'child_process';
15
+ import { addHostOption } from '../lib/hosts/option.js';
15
16
  import * as fs from 'fs';
16
17
  import * as os from 'os';
17
18
  import * as path from 'path';
@@ -19,6 +20,7 @@ import chalk from 'chalk';
19
20
  import * as yaml from 'yaml';
20
21
  import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
21
22
  import { supports } from '../lib/capabilities.js';
23
+ import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
22
24
  import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
23
25
  import { getVersionHomePath } from '../lib/versions.js';
24
26
  import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
@@ -68,8 +70,7 @@ const CAPABILITY_NAMES = [
68
70
  ];
69
71
  // ─── Command registration ────────────────────────────────────────────────────
70
72
  export function registerInspectCommand(program) {
71
- const cmd = program
72
- .command('inspect <target>')
73
+ const cmd = addHostOption(program.command('inspect <target>'))
73
74
  .description('Inspect one installed agent at one version, or a DotAgents repo (user|system|project|alias|path) — paths, capabilities, resources, drill into any kind.')
74
75
  .option('--brief', 'header + capabilities only; skip resources/sessions')
75
76
  .option('--json', 'machine-readable JSON output');
@@ -125,25 +126,20 @@ export async function inspectAction(target, options) {
125
126
  await renderSummary(agent, version, versionHome, options);
126
127
  }
127
128
  function parseTarget(target) {
128
- const [rawAgent, rawVersion] = target.split('@');
129
- const agent = resolveAgentName(rawAgent || '');
130
- if (!agent) {
131
- console.error(chalk.red(`Unknown agent: ${rawAgent}`));
132
- console.error(chalk.gray(`Known agents: ${Object.keys(AGENTS).join(', ')}`));
133
- process.exit(1);
129
+ // Route through the agent-spec engine: bare resolves project pin → global
130
+ // default sole installed (the meta-only lookup here previously ignored
131
+ // project pins); @default/@latest/@oldest/@x.y.z all handled uniformly.
132
+ try {
133
+ const { agent, version } = resolveSingleAgentTarget(target);
134
+ return { agent, version };
134
135
  }
135
- let version = rawVersion;
136
- if (!version || version === 'default') {
137
- const meta = readMeta();
138
- const def = meta.agents?.[agent];
139
- if (!def) {
140
- console.error(chalk.red(`No default version set for ${agent}.`));
141
- console.error(chalk.gray(`Pass a version: agents inspect ${agent}@<version>`));
136
+ catch (e) {
137
+ if (e instanceof AgentSpecError) {
138
+ console.error(chalk.red(e.message));
142
139
  process.exit(1);
143
140
  }
144
- version = def;
141
+ throw e;
145
142
  }
146
- return { agent, version };
147
143
  }
148
144
  function pickDrillKind(options) {
149
145
  const active = [];
@@ -7,9 +7,9 @@ import { readManifest, writeManifest, createDefaultManifest } from '../lib/manif
7
7
  import { listMcpServerConfigs, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { getMcpDir } from '../lib/state.js';
10
- import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, resolveVersionAlias, syncResourcesToVersion, } from '../lib/versions.js';
10
+ import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
11
11
  import { getUserAgentsDir } from '../lib/state.js';
12
- import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, promptRemovalTargets, parseCommaSeparatedList, ensureAgentVersionsInstalled, resolveAgentTargetsAutoInstalling, resolveInstalledAgentTargetsAutoInstalling, VersionNotInstalledError, } from './utils.js';
12
+ import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, promptRemovalTargets, parseCommaSeparatedList, ensureAgentVersionsInstalled, resolveAgentTargetsAutoInstalling, resolveInstalledAgentTargetsAutoInstalling, VersionNotInstalledError, resolveListFilterOrExit, } from './utils.js';
13
13
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
14
14
  /**
15
15
  * Parse a comma-separated --agents string into validated agent IDs and
@@ -159,7 +159,7 @@ When to use:
159
159
  process.exit(1);
160
160
  }
161
161
  filterAgent = resolved;
162
- filterVersion = resolveVersionAlias(resolved, parts[1]);
162
+ filterVersion = resolveListFilterOrExit(resolved, parts[1]);
163
163
  }
164
164
  const rows = buildMcpRows({ filterAgent, filterVersion });
165
165
  spinner.stop();
@@ -8,9 +8,9 @@ import { AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { capableAgents, isCapable } from '../lib/capabilities.js';
10
10
  import { listInstalledPermissions, discoverPermissionsFromRepo, installPermissionSet, removePermissionSet, applyPermissionsToVersion, readAgentPermissions, exportPermissionsFromPath, getDefaultPermissionSet, computePermissionsDiff, mergePermissionSets, saveDefaultPermissionSet, containsBroadGrants, } from '../lib/permissions.js';
11
- import { listInstalledVersions, getGlobalDefault, getVersionHomePath, promptAgentVersionSelection, resolveVersionAlias, } from '../lib/versions.js';
11
+ import { listInstalledVersions, getGlobalDefault, getVersionHomePath, promptAgentVersionSelection, } from '../lib/versions.js';
12
12
  import { recordVersionResources } from '../lib/state.js';
13
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, } from './utils.js';
13
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, resolveListFilterOrExit, } from './utils.js';
14
14
  export function shouldRefuseBroadPermissions(permissions, allowBroadPermissions) {
15
15
  return !allowBroadPermissions && permissions.some((perm) => containsBroadGrants(perm.set) !== null);
16
16
  }
@@ -116,7 +116,7 @@ When to use:
116
116
  console.log(chalk.red(formatAgentError(agentName, capableAgents('allowlist'))));
117
117
  process.exit(1);
118
118
  }
119
- const requestedVersion = resolveVersionAlias(agentId, parts[1]) ?? null;
119
+ const requestedVersion = resolveListFilterOrExit(agentId, parts[1]) ?? null;
120
120
  if (!isCapable(agentId, 'allowlist')) {
121
121
  console.log(chalk.yellow(`${AGENTS[agentId].name} does not support fine-grained permissions`));
122
122
  return;
@@ -11,7 +11,7 @@ import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResou
11
11
  import { recordVersionResources, getActiveRulesPreset, setActiveRulesPreset } from '../lib/state.js';
12
12
  import { discoverRulesLayers } from '../lib/rules/compose.js';
13
13
  import * as yaml from 'yaml';
14
- import { isPromptCancelled, formatPath, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, requireDestructiveArg, resolveAgentTargetsAutoInstalling, } from './utils.js';
14
+ import { isPromptCancelled, formatPath, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, requireDestructiveArg, resolveAgentTargetsAutoInstalling, resolveListFilterOrExit, } from './utils.js';
15
15
  /** Register the `agents rules` command tree (list, add, view, remove). */
16
16
  export function registerRulesCommands(program) {
17
17
  const rulesCmd = program
@@ -73,7 +73,7 @@ Project rules & @-imports:
73
73
  console.log(chalk.red(formatAgentError(agentName)));
74
74
  process.exit(1);
75
75
  }
76
- requestedVersion = resolveVersionAlias(agentId, parts[1]) ?? null;
76
+ requestedVersion = resolveListFilterOrExit(agentId, parts[1]) ?? null;
77
77
  }
78
78
  const renderVersionRules = (agentId, version, isDefault, home) => {
79
79
  const agent = AGENTS[agentId];
@@ -215,6 +215,23 @@ function signalBadges(s) {
215
215
  parts.push(chalk.magenta(`wt:${s.worktree.slug}`));
216
216
  return parts.join(' ');
217
217
  }
218
+ /**
219
+ * Compact provenance badge: how to reach the session, not what it's doing.
220
+ * `ssh` flags a remote host; the tmux pane id is the send-keys target the feed
221
+ * would type back into. Local, non-tmux sessions add nothing (the common case).
222
+ */
223
+ function provenanceBadge(p) {
224
+ if (!p)
225
+ return '';
226
+ const parts = [];
227
+ if (p.transport === 'ssh')
228
+ parts.push(chalk.red('ssh'));
229
+ if (p.mux?.kind === 'tmux' && p.mux.pane)
230
+ parts.push(chalk.green(`tmux ${p.mux.pane}`));
231
+ else if (p.mux?.kind === 'screen')
232
+ parts.push(chalk.green('screen'));
233
+ return parts.join(' ');
234
+ }
218
235
  /**
219
236
  * Render a single agent-session row inside an already-printed group header.
220
237
  * Indent is the leading whitespace (2 spaces for flat groups, 4 inside a
@@ -228,7 +245,7 @@ function printActiveRow(s, indent) {
228
245
  const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
229
246
  const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
230
247
  const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
231
- const badges = (fork ? fork : '') + signalBadges(s);
248
+ const badges = (fork ? fork : '') + [signalBadges(s), provenanceBadge(s.provenance)].filter(Boolean).join(' ');
232
249
  const desc = buildSessionDescription(s) || '-';
233
250
  // Fill the remaining width with the preview so nothing wraps under tmux/SSH.
234
251
  const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
@@ -8,9 +8,9 @@ import { AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/
8
8
  import { capableAgents } from '../lib/capabilities.js';
9
9
  import { cloneRepo } from '../lib/git.js';
10
10
  import { discoverSkillsFromRepo, installSkillCentrally, listInstalledSkills, listInstalledSkillsWithScope, getSkillInfo, getSkillRules, getSkillsDir, countSkillFiles, tryParseSkillMetadata, diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
11
- import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
11
+ import { getGlobalDefault, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
12
12
  import { recordVersionResources } from '../lib/state.js';
13
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, promptRemovalTargets, } from './utils.js';
13
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, promptRemovalTargets, resolveListFilterOrExit, } from './utils.js';
14
14
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
15
15
  /** Register the `agents skills` command tree (list, add, remove, sync, prune, view). */
16
16
  export function registerSkillsCommands(program) {
@@ -59,7 +59,7 @@ When to use:
59
59
  process.exit(1);
60
60
  }
61
61
  filterAgent = resolved;
62
- filterVersion = resolveVersionAlias(resolved, parts[1]);
62
+ filterVersion = resolveListFilterOrExit(resolved, parts[1]);
63
63
  }
64
64
  const rows = await buildSkillRows({ filterAgent, filterVersion });
65
65
  spinner.stop();
@@ -20,6 +20,7 @@ import { machineId } from '../lib/session/sync/config.js';
20
20
  import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
21
21
  import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
22
22
  import { planDeviceReconciliation, runDeviceSync } from '../lib/devices/sync.js';
23
+ import { clearPendingSentinel } from '../lib/devices/pending.js';
23
24
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
24
25
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
25
26
  import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
@@ -154,6 +155,27 @@ Typical workflow:
154
155
  process.exit(1);
155
156
  }
156
157
  });
158
+ devicesCmd
159
+ .command('register <name>')
160
+ .description('Register a discovered (pending) node by name — used by the menu-bar "NEW DEVICES → Register" action.')
161
+ .action(async (name) => {
162
+ try {
163
+ const nodes = parseTailscaleStatus(tailscaleStatusJson());
164
+ const node = nodes.find((n) => n.name === name);
165
+ if (!node) {
166
+ console.error(chalk.red(`'${name}' is not a current tailscale node. See 'agents devices sync'.`));
167
+ process.exit(1);
168
+ }
169
+ await removeIgnored(name); // a re-registered node is no longer dismissed
170
+ const d = await upsertDevice(name, nodeToDeviceInput(node));
171
+ clearPendingSentinel(name); // drop the notification immediately
172
+ console.log(chalk.green(`Registered '${name}'`) + chalk.gray(` (${d.platform})`));
173
+ }
174
+ catch (err) {
175
+ console.error(chalk.red(err.message));
176
+ process.exit(1);
177
+ }
178
+ });
157
179
  devicesCmd
158
180
  .command('ignore <name>')
159
181
  .description('Dismiss a node from auto-discovery so it is never re-suggested (and remove it from the registry if present).')
@@ -161,6 +183,7 @@ Typical workflow:
161
183
  try {
162
184
  await removeDevice(name);
163
185
  await addIgnored(name);
186
+ clearPendingSentinel(name); // drop the notification immediately
164
187
  console.log(chalk.green(`Ignored '${name}'`) + chalk.gray(" — it won't be suggested again. Undo with `agents devices unignore`."));
165
188
  }
166
189
  catch (err) {
@@ -35,10 +35,10 @@ import { compileRulesForProject } from '../lib/rules/compile.js';
35
35
  import { runLaunchSync } from '../lib/project-launch.js';
36
36
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
37
37
  import { runUmbrellaSync } from '../lib/sync-umbrella.js';
38
+ import { addHostOption } from '../lib/hosts/option.js';
38
39
  /** Register the `agents sync` command. */
39
40
  export function registerSyncCommand(program) {
40
- program
41
- .command('sync [agentSpec] [repo]')
41
+ addHostOption(program.command('sync [agentSpec] [repo]'))
42
42
  .summary('Make this machine current, or sync resources into one agent')
43
43
  .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
44
44
  .option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import * as fs from 'fs/promises';
3
+ import { addHostOption } from '../lib/hosts/option.js';
3
4
  import * as path from 'path';
4
5
  import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
6
  import { resolveProvider } from '../lib/cloud/registry.js';
@@ -673,8 +674,7 @@ export function registerTeamsCommands(program) {
673
674
  `,
674
675
  });
675
676
  // list
676
- teams
677
- .command('list [query]')
677
+ addHostOption(teams.command('list [query]'))
678
678
  .alias('ls')
679
679
  .description('List your teams, most recent activity first')
680
680
  .option('-a, --agent <agent>', 'Filter: only teams with this agent (e.g. claude or claude@2.1.112)')
@@ -785,8 +785,7 @@ export function registerTeamsCommands(program) {
785
785
  printTeamTable(rows);
786
786
  });
787
787
  // create
788
- teams
789
- .command('create <team>')
788
+ addHostOption(teams.command('create <team>'))
790
789
  .aliases(['c', 'new'])
791
790
  .description('Start a new team. No teammates yet; add them with `teams add`.')
792
791
  .option('-d, --description <text>', 'One-line summary of what this team is working on')
@@ -825,8 +824,7 @@ export function registerTeamsCommands(program) {
825
824
  }
826
825
  });
827
826
  // add
828
- teams
829
- .command('add <team> <teammate> <task>')
827
+ addHostOption(teams.command('add <team> <teammate> <task>'))
830
828
  .alias('a')
831
829
  .description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
832
830
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
@@ -1041,8 +1039,7 @@ export function registerTeamsCommands(program) {
1041
1039
  }
1042
1040
  });
1043
1041
  // status
1044
- teams
1045
- .command('status [team]')
1042
+ addHostOption(teams.command('status [team]'))
1046
1043
  .aliases(['s', 'st', 'check'])
1047
1044
  .description("Check in on a team: status, files touched, recent commands, last messages. Pass --verbose for the full per-teammate dump; --since for delta polling.")
1048
1045
  .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
@@ -1136,8 +1133,7 @@ export function registerTeamsCommands(program) {
1136
1133
  console.log(chalk.gray(`${running.length} teammate${running.length === 1 ? '' : 's'} running. See 'agents sessions --active' for the full cross-context view.`));
1137
1134
  });
1138
1135
  // start — fire any staged teammates whose --after deps have all completed
1139
- teams
1140
- .command('start [team]')
1136
+ addHostOption(teams.command('start [team]'))
1141
1137
  .description('Launch any pending teammates whose --after dependencies are satisfied. Use --watch to keep draining the DAG as teammates finish and as new tasks are added mid-flight.')
1142
1138
  .option('--json', 'Output machine-readable JSON')
1143
1139
  .option('--watch', 'Keep running: poll every --interval seconds, fire new waves, exit when the DAG drains.')
@@ -1190,8 +1186,7 @@ export function registerTeamsCommands(program) {
1190
1186
  }
1191
1187
  });
1192
1188
  // stop
1193
- teams
1194
- .command('stop [team] [teammate]')
1189
+ addHostOption(teams.command('stop [team] [teammate]'))
1195
1190
  .description('Stop a running teammate. Can be restarted later. Cleans up worktree if no uncommitted changes.')
1196
1191
  .option('--json', 'Output machine-readable JSON')
1197
1192
  .action(async (team, ref, opts) => {
@@ -1,3 +1,4 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
3
4
  import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
@@ -5,8 +6,7 @@ import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
5
6
  /** Agents whose CLI surfaces usage data we can read today. */
6
7
  const USAGE_SUPPORTED = new Set(['claude', 'codex']);
7
8
  export function registerUsageCommand(program) {
8
- program
9
- .command('usage [agent]')
9
+ addHostOption(program.command('usage [agent]'))
10
10
  .description('Show rate-limit / quota usage per agent')
11
11
  .addHelpText('after', `
12
12
  Examples:
@@ -6,6 +6,14 @@
6
6
  */
7
7
  import type { AgentId } from '../lib/types.js';
8
8
  import { type InstalledAgentTargetResult, type VersionSelectionResult } from '../lib/versions.js';
9
+ /**
10
+ * Resolve a read/list command's `@version` filter through the agent-spec engine,
11
+ * exiting cleanly on a bad spec. Drop-in for the old
12
+ * `resolveVersionAlias(agent, parts[1])`:
13
+ * undefined → show all · @default/@pinned → the default version ·
14
+ * @latest/@oldest/@x.y.z → concrete.
15
+ */
16
+ export declare function resolveListFilterOrExit(agent: AgentId, qualifier: string | undefined | null): string | undefined;
9
17
  /**
10
18
  * Check if an error is from user cancelling a prompt (Ctrl+C)
11
19
  */
@@ -11,6 +11,26 @@ import ora from 'ora';
11
11
  import { confirm } from '@inquirer/prompts';
12
12
  import { agentLabel, resolveAgentName } from '../lib/agents.js';
13
13
  import { installVersion, listInstalledVersions, resolveAgentVersionTargets, resolveInstalledAgentTargets, } from '../lib/versions.js';
14
+ import { resolveListFilter, AgentSpecError } from '../lib/agent-spec/index.js';
15
+ /**
16
+ * Resolve a read/list command's `@version` filter through the agent-spec engine,
17
+ * exiting cleanly on a bad spec. Drop-in for the old
18
+ * `resolveVersionAlias(agent, parts[1])`:
19
+ * undefined → show all · @default/@pinned → the default version ·
20
+ * @latest/@oldest/@x.y.z → concrete.
21
+ */
22
+ export function resolveListFilterOrExit(agent, qualifier) {
23
+ try {
24
+ return resolveListFilter(agent, qualifier);
25
+ }
26
+ catch (e) {
27
+ if (e instanceof AgentSpecError) {
28
+ console.error(chalk.red(e.message));
29
+ process.exit(1);
30
+ }
31
+ throw e;
32
+ }
33
+ }
14
34
  /**
15
35
  * Check if an error is from user cancelling a prompt (Ctrl+C)
16
36
  */
@@ -1,3 +1,4 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
@@ -762,8 +763,7 @@ export function registerVersionsCommands(program) {
762
763
  }
763
764
  });
764
765
  // Deprecated: use `agents view` instead
765
- program
766
- .command('list [agent]')
766
+ addHostOption(program.command('list [agent]'))
767
767
  .description('List installed agent CLI versions')
768
768
  .action(async (agentArg) => {
769
769
  console.log(chalk.red('Deprecated: "agents list" is now "agents view"\n'));