@phnx-labs/agents-cli 1.20.32 → 1.20.34

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 (110) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/commands.js +3 -3
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +2 -2
  8. package/dist/commands/doctor.d.ts +10 -0
  9. package/dist/commands/doctor.js +51 -2
  10. package/dist/commands/exec.js +56 -1
  11. package/dist/commands/hooks.js +3 -3
  12. package/dist/commands/import.js +1 -1
  13. package/dist/commands/inspect.js +13 -17
  14. package/dist/commands/mcp.js +3 -3
  15. package/dist/commands/permissions.js +3 -3
  16. package/dist/commands/rules.js +3 -3
  17. package/dist/commands/secrets-migrate.js +23 -11
  18. package/dist/commands/secrets.d.ts +20 -0
  19. package/dist/commands/secrets.js +53 -1
  20. package/dist/commands/sessions.js +18 -1
  21. package/dist/commands/skills.js +3 -3
  22. package/dist/commands/status.d.ts +12 -0
  23. package/dist/commands/status.js +81 -0
  24. package/dist/commands/sync.js +2 -2
  25. package/dist/commands/teams.js +77 -18
  26. package/dist/commands/usage.js +2 -2
  27. package/dist/commands/utils.d.ts +8 -0
  28. package/dist/commands/utils.js +20 -0
  29. package/dist/commands/versions.js +4 -3
  30. package/dist/commands/view.d.ts +39 -0
  31. package/dist/commands/view.js +217 -84
  32. package/dist/commands/workflows.js +3 -3
  33. package/dist/index.js +16 -2
  34. package/dist/lib/acp/harnesses.d.ts +1 -1
  35. package/dist/lib/acp/harnesses.js +2 -2
  36. package/dist/lib/agent-spec/index.d.ts +18 -0
  37. package/dist/lib/agent-spec/index.js +35 -0
  38. package/dist/lib/agent-spec/primitives.d.ts +28 -0
  39. package/dist/lib/agent-spec/primitives.js +57 -0
  40. package/dist/lib/agent-spec/provider.d.ts +2 -0
  41. package/dist/lib/agent-spec/provider.js +9 -0
  42. package/dist/lib/agent-spec/resolve.d.ts +33 -0
  43. package/dist/lib/agent-spec/resolve.js +174 -0
  44. package/dist/lib/agent-spec/types.d.ts +57 -0
  45. package/dist/lib/agent-spec/types.js +18 -0
  46. package/dist/lib/agents.d.ts +12 -0
  47. package/dist/lib/agents.js +115 -32
  48. package/dist/lib/browser/chrome.js +20 -0
  49. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  50. package/dist/lib/browser/drivers/ssh.js +18 -3
  51. package/dist/lib/crabbox/cli.d.ts +98 -0
  52. package/dist/lib/crabbox/cli.js +218 -0
  53. package/dist/lib/crabbox/lease.d.ts +41 -0
  54. package/dist/lib/crabbox/lease.js +73 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +57 -0
  56. package/dist/lib/crabbox/runtimes.js +109 -0
  57. package/dist/lib/doctor-diff.js +29 -2
  58. package/dist/lib/drift-sync.d.ts +43 -0
  59. package/dist/lib/drift-sync.js +179 -0
  60. package/dist/lib/exec.d.ts +15 -0
  61. package/dist/lib/exec.js +21 -11
  62. package/dist/lib/hosts/dispatch.d.ts +27 -10
  63. package/dist/lib/hosts/dispatch.js +55 -19
  64. package/dist/lib/hosts/option.d.ts +14 -0
  65. package/dist/lib/hosts/option.js +19 -0
  66. package/dist/lib/hosts/passthrough.d.ts +30 -0
  67. package/dist/lib/hosts/passthrough.js +141 -0
  68. package/dist/lib/hosts/remote-cmd.d.ts +36 -0
  69. package/dist/lib/hosts/remote-cmd.js +56 -0
  70. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  71. package/dist/lib/platform/winpath.d.ts +31 -2
  72. package/dist/lib/platform/winpath.js +133 -24
  73. package/dist/lib/pwsh.d.ts +11 -0
  74. package/dist/lib/pwsh.js +13 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/agent.d.ts +42 -1
  78. package/dist/lib/secrets/agent.js +89 -11
  79. package/dist/lib/secrets/bundles.js +61 -21
  80. package/dist/lib/secrets/filestore.js +31 -1
  81. package/dist/lib/secrets/index.d.ts +44 -1
  82. package/dist/lib/secrets/index.js +108 -10
  83. package/dist/lib/secrets/linux.d.ts +14 -0
  84. package/dist/lib/secrets/linux.js +21 -0
  85. package/dist/lib/secrets/windows.d.ts +74 -0
  86. package/dist/lib/secrets/windows.js +440 -0
  87. package/dist/lib/session/active.d.ts +8 -0
  88. package/dist/lib/session/active.js +18 -1
  89. package/dist/lib/session/provenance.d.ts +56 -0
  90. package/dist/lib/session/provenance.js +157 -0
  91. package/dist/lib/shims.d.ts +20 -0
  92. package/dist/lib/shims.js +53 -20
  93. package/dist/lib/ssh-exec.d.ts +22 -0
  94. package/dist/lib/ssh-exec.js +59 -2
  95. package/dist/lib/ssh-tunnel.d.ts +0 -5
  96. package/dist/lib/ssh-tunnel.js +65 -8
  97. package/dist/lib/startup/command-registry.d.ts +1 -0
  98. package/dist/lib/startup/command-registry.js +2 -0
  99. package/dist/lib/sync-status.d.ts +102 -0
  100. package/dist/lib/sync-status.js +135 -0
  101. package/dist/lib/teams/agents.d.ts +24 -0
  102. package/dist/lib/teams/agents.js +30 -1
  103. package/dist/lib/types.d.ts +20 -1
  104. package/dist/lib/usage.d.ts +30 -0
  105. package/dist/lib/usage.js +159 -2
  106. package/dist/lib/versions.d.ts +2 -4
  107. package/dist/lib/versions.js +7 -20
  108. package/package.json +2 -1
  109. package/dist/lib/agent-spec.d.ts +0 -36
  110. package/dist/lib/agent-spec.js +0 -157
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.34
6
+
7
+ **Test suite runs remotely on a crabbox VM (#525, #540)**
8
+
9
+ - `scripts/release.sh`'s test gate now runs `bun install && bun run build && bun run test` on a leased crabbox VM via `scripts/sandbox.sh` instead of freezing the local machine, matching CI's Build→Test order (crabbox's sync honors `.gitignore`, so the gitignored `dist/` is built on the box). A new `bun run test:remote` offloads the suite the same way for local dev. Publishing still happens locally — only the signed macOS keychain helper can be produced and notarized here, and crabbox boxes are Linux. Source: `scripts/sandbox.sh`, `scripts/release.sh`, `package.json`.
10
+ - `scripts/sandbox.sh` box acquisition is now robust: secrets load via `agents secrets export --plaintext` (the bare form now hard-errors), a missing `.crabbox.yaml` no longer aborts the script under `set -e`, and the agents-cli/claude install is gated to PR mode so test-mode runs match GitHub CI. Box selection gates on `crabbox status … ready=true` — skipping failed-bootstrap duds (which still report `status=running`) and warming a fresh box if none are ready — keyed on the stable `profile` label rather than an ephemeral slug. A dedicated `agents-cli` crabbox profile (`.crabbox.yaml`) isolates this repo's warm pool. Source: `scripts/sandbox.sh`, `.crabbox.yaml`.
11
+
5
12
  ## 1.20.31
6
13
 
7
14
  **`agents sessions <id>`: a catch-up digest for switching between many agents (#502)**
package/README.md CHANGED
@@ -230,6 +230,30 @@ Profile YAML has no secrets -- safe to `agents repo push` to a shared repo. `age
230
230
 
231
231
  ---
232
232
 
233
+ ## Run on your own machines
234
+
235
+ Dispatch any read-only or config command -- and `agents run` itself -- to another machine over SSH. No daemon.
236
+
237
+ ```bash
238
+ # Enroll a machine (from ~/.ssh/config, or inline with user@address)
239
+ agents hosts add gpu-box
240
+ agents hosts check gpu-box # reachable? which agents-cli version?
241
+
242
+ # Run there instead of locally
243
+ agents run claude --host gpu-box "profile this build"
244
+ agents view claude --host gpu-box # inspect the remote install
245
+ agents sync --host gpu-box # make the remote machine current
246
+
247
+ # Your Tailscale fleet, auto-discovered
248
+ agents devices sync # ingest `tailscale status`
249
+ agents ssh mac-mini # hardened SSH: fails fast if offline,
250
+ # PowerShell on Windows, password-from-Keychain
251
+ ```
252
+
253
+ **Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](docs/00-concepts.md#devices--hosts).
254
+
255
+ ---
256
+
233
257
  ## Teams
234
258
 
235
259
  ```bash
@@ -646,11 +670,13 @@ By default, secrets sync via iCloud Keychain to your other Macs. With `--no-iclo
646
670
 
647
671
  Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/agents.ts](src/lib/agents.ts) (`capabilities`); gates use `supports(agent, cap, version)` from [src/lib/capabilities.ts](src/lib/capabilities.ts). Full matrix also in [docs/00-concepts.md](docs/00-concepts.md).
648
672
 
673
+ > **† Gemini CLI is deprecated.** Google retired it for free, Pro, and Ultra tiers on **June 18, 2026** (announced at Google I/O 2026); the `gemini` command no longer serves requests on those tiers. agents-cli still manages existing installs, but warns on `agents add gemini` and `agents teams add … gemini`. New setups should use **Antigravity CLI** (`antigravity`), Google's official successor — see [the transition notice](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/).
674
+
649
675
  | Agent | Versions | Hooks | MCP | Permissions | Skills | Commands | Plugins | Subagents | Rules | Workflows |
650
676
  |-------|----------|-------|-----|-------------|--------|----------|---------|-----------|-------|-----------|
651
677
  | Claude Code | yes | yes | yes | yes | yes | yes | yes | yes | `CLAUDE.md` | yes |
652
678
  | Codex CLI | yes | >= 0.116.0 | yes | no | yes | < 0.117.0 · skills ($name, >= 0.117) | >= 0.128.0 | no | `AGENTS.md` | no |
653
- | Gemini CLI | yes | >= 0.26.0 | yes | no | yes | yes (.toml) | no | no | `GEMINI.md` | no |
679
+ | Gemini CLI | yes | >= 0.26.0 | yes | no | yes | yes (.toml) | no | no | `GEMINI.md` | no |
654
680
  | Antigravity | yes | yes | yes | yes | yes | yes | yes | no | `AGENTS.md` | no |
655
681
  | Grok Build | yes | yes | yes | yes | yes | skills ($name) | yes | no | `AGENTS.md` | no |
656
682
  | OpenClaw | yes | yes | yes | no | yes | gateway | yes | yes | `workspace/AGENTS.md` | no |
@@ -672,7 +698,7 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
672
698
  |-------|----------|-------|---------------|
673
699
  | Claude Code | yes | yes | yes |
674
700
  | Codex CLI | yes | yes | yes |
675
- | Gemini CLI | yes | yes | yes |
701
+ | Gemini CLI | yes | yes | yes |
676
702
  | Cursor | -- | yes | -- |
677
703
  | OpenCode | -- | yes | -- |
678
704
  | Grok Build | -- | yes | yes |
@@ -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) {
@@ -12,6 +12,29 @@ export declare function shouldBlockOffPlatform(opts: {
12
12
  tcpConfigured: boolean;
13
13
  host?: string;
14
14
  }): boolean;
15
+ /**
16
+ * Sniff the image format from the leading magic bytes: PNG starts with the
17
+ * 8-byte signature `89 50 4E 47` ("\x89PNG"), JPEG with `FF D8 FF`. Returns the
18
+ * canonical file extension, or null for anything else.
19
+ */
20
+ export declare function detectImageFormat(buf: Buffer): '.png' | '.jpg' | null;
21
+ /**
22
+ * Make the screenshot filename honest about its bytes. The two helper backends
23
+ * encode DIFFERENT formats and neither re-encodes to match the requested name:
24
+ * the macOS helper (ScreenCaptureKit) returns JPEG
25
+ * (packages/computer-helper/Sources/ComputerHelper/Screenshot.swift:207,212),
26
+ * the Windows helper returns PNG
27
+ * (packages/computer-helper-win/Screenshot.cs:33). So a fixed default extension
28
+ * cannot be correct for both — the only honest path is to sniff the real format
29
+ * and swap the extension to match. Pure so it's unit-testable.
30
+ *
31
+ * Returns the path to write to (caller's path with its extension corrected) and
32
+ * whether a correction was made. Unknown formats pass through unchanged.
33
+ */
34
+ export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
35
+ path: string;
36
+ corrected: boolean;
37
+ };
15
38
  export declare function registerComputerCommand(program: Command): void;
16
39
  export declare function registerComputerSubcommands(program: Command): void;
17
40
  export { resolveHelperExec as resolveHelperPath };
@@ -33,6 +33,42 @@ export function shouldBlockOffPlatform(opts) {
33
33
  return false; // remote path resolves its own endpoint
34
34
  return true;
35
35
  }
36
+ /**
37
+ * Sniff the image format from the leading magic bytes: PNG starts with the
38
+ * 8-byte signature `89 50 4E 47` ("\x89PNG"), JPEG with `FF D8 FF`. Returns the
39
+ * canonical file extension, or null for anything else.
40
+ */
41
+ export function detectImageFormat(buf) {
42
+ if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
43
+ return '.png';
44
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
45
+ return '.jpg';
46
+ return null;
47
+ }
48
+ /**
49
+ * Make the screenshot filename honest about its bytes. The two helper backends
50
+ * encode DIFFERENT formats and neither re-encodes to match the requested name:
51
+ * the macOS helper (ScreenCaptureKit) returns JPEG
52
+ * (packages/computer-helper/Sources/ComputerHelper/Screenshot.swift:207,212),
53
+ * the Windows helper returns PNG
54
+ * (packages/computer-helper-win/Screenshot.cs:33). So a fixed default extension
55
+ * cannot be correct for both — the only honest path is to sniff the real format
56
+ * and swap the extension to match. Pure so it's unit-testable.
57
+ *
58
+ * Returns the path to write to (caller's path with its extension corrected) and
59
+ * whether a correction was made. Unknown formats pass through unchanged.
60
+ */
61
+ export function reconcileScreenshotExt(outPath, buf) {
62
+ const actual = detectImageFormat(buf);
63
+ if (!actual)
64
+ return { path: outPath, corrected: false };
65
+ const cur = path.extname(outPath).toLowerCase();
66
+ const alreadyMatches = cur === actual || (actual === '.jpg' && (cur === '.jpg' || cur === '.jpeg'));
67
+ if (alreadyMatches)
68
+ return { path: outPath, corrected: false };
69
+ const base = outPath.slice(0, outPath.length - path.extname(outPath).length);
70
+ return { path: base + actual, corrected: true };
71
+ }
36
72
  export function registerComputerCommand(program) {
37
73
  const computer = program
38
74
  .command('computer')
@@ -128,8 +164,8 @@ function registerScreenshotCommand(program) {
128
164
  .option('--list', 'List the app\'s windows (id/title/layer/bounds) instead of capturing — reveals modals/popups')
129
165
  .option('--window-id <n>', 'Capture a specific window by id (from --list)', (v) => parseInt(v, 10))
130
166
  .option('--display', 'Capture the whole display the app is on (composites stacked modals)')
131
- .option('--out <path>', 'Output JPEG path', './computer-screenshot.jpg')
132
- .option('--quality <n>', 'JPEG quality 1-100', (v) => parseInt(v, 10), 85)
167
+ .option('--out <path>', 'Output image path — extension auto-corrected to the encoded format (JPEG on macOS, PNG on a Windows --host)', './computer-screenshot.jpg')
168
+ .option('--quality <n>', 'JPEG quality 1-100 (macOS capture only; the Windows helper encodes lossless PNG and ignores this)', (v) => parseInt(v, 10), 85)
133
169
  .option('--json', 'Emit JSON (metadata for captures; window list for --list)')
134
170
  .action(async (opts) => {
135
171
  const quality = Math.max(1, Math.min(100, opts.quality || 85));
@@ -176,7 +212,10 @@ function registerScreenshotCommand(program) {
176
212
  process.exit(1);
177
213
  }
178
214
  const buf = Buffer.from(b64, 'base64');
179
- const outPath = path.resolve(opts.out);
215
+ // Sniff the real format and correct the extension so the filename never
216
+ // lies about its bytes (macOS -> JPEG, Windows helper -> PNG).
217
+ const requested = path.resolve(opts.out);
218
+ const { path: outPath, corrected } = reconcileScreenshotExt(requested, buf);
180
219
  fs.writeFileSync(outPath, buf);
181
220
  if (opts.json) {
182
221
  // Drop the heavy base64 from the metadata echo; report where it went.
@@ -184,6 +223,9 @@ function registerScreenshotCommand(program) {
184
223
  console.log(JSON.stringify(meta, null, 2));
185
224
  }
186
225
  else {
226
+ if (corrected) {
227
+ console.log(`note: bytes are ${path.extname(outPath).slice(1).toUpperCase()}; corrected extension from ${path.basename(requested)}`);
228
+ }
187
229
  const origin = res.origin || [];
188
230
  const originStr = origin.length === 2 ? `, origin [${origin.join(',')}], scale ${res.scale ?? '?'}` : '';
189
231
  console.log(`saved: ${outPath} (${res.width ?? '?'}x${res.height ?? '?'}, ${buf.byteLength} bytes${originStr})`);
@@ -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)')
@@ -22,4 +22,14 @@
22
22
  * apply pending sync.
23
23
  */
24
24
  import type { Command } from 'commander';
25
+ /**
26
+ * Windows-only advisory lines. When the effective PowerShell execution policy
27
+ * blocks unsigned local `.ps1` scripts (`Restricted`/`AllSigned`), the generated
28
+ * `agents.ps1` launcher fails in PowerShell even when it is on PATH. Surface the
29
+ * remediation; the `.cmd` companion still works, so this is a warning, not an
30
+ * error, and doctor never auto-changes the policy. Pure — returns `[]` on
31
+ * non-Windows or a permissive policy, so it is testable without invoking
32
+ * PowerShell.
33
+ */
34
+ export declare function execPolicyWarningLines(platform: NodeJS.Platform, policy: string | null): string[];
25
35
  export declare function registerDoctorCommand(program: Command): void;
@@ -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';
@@ -11,6 +12,7 @@ import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
11
12
  import { listCliStatus } from '../lib/cli-resources.js';
12
13
  import { setHelpSections } from '../lib/help.js';
13
14
  import { heal, healChangedAnything } from '../lib/heal.js';
15
+ import { blocksLocalScripts, getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
14
16
  import * as fs from 'fs';
15
17
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
16
18
  // ─── overview mode (no target) ────────────────────────────────────────────────
@@ -207,6 +209,48 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
207
209
  for (const err of hostClis.errors) {
208
210
  console.log(` ${chalk.red('err ')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
209
211
  }
212
+ // On Windows a Restricted/AllSigned execution policy silently breaks the
213
+ // generated `agents.ps1` launcher — postinstall diagnoses it interactively,
214
+ // but a non-interactive install never sees that. Surface it here too.
215
+ renderExecPolicyAdvisory();
216
+ }
217
+ // ─── windows execution-policy advisory ─────────────────────────────────────────
218
+ /**
219
+ * Windows-only advisory lines. When the effective PowerShell execution policy
220
+ * blocks unsigned local `.ps1` scripts (`Restricted`/`AllSigned`), the generated
221
+ * `agents.ps1` launcher fails in PowerShell even when it is on PATH. Surface the
222
+ * remediation; the `.cmd` companion still works, so this is a warning, not an
223
+ * error, and doctor never auto-changes the policy. Pure — returns `[]` on
224
+ * non-Windows or a permissive policy, so it is testable without invoking
225
+ * PowerShell.
226
+ */
227
+ export function execPolicyWarningLines(platform, policy) {
228
+ if (platform !== 'win32')
229
+ return [];
230
+ if (!blocksLocalScripts(policy))
231
+ return [];
232
+ return [
233
+ `PowerShell execution policy is ${policy} — it blocks the generated agents.ps1 launcher.`,
234
+ 'Fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned',
235
+ 'The agents.cmd shim still works regardless of the policy.',
236
+ ];
237
+ }
238
+ function renderExecPolicyAdvisory() {
239
+ // Only probe the policy on Windows — getEffectiveExecutionPolicy() spawns
240
+ // powershell, which is a wasted (doomed) process on POSIX where the advisory
241
+ // never applies.
242
+ if (process.platform !== 'win32')
243
+ return;
244
+ const lines = execPolicyWarningLines(process.platform, getEffectiveExecutionPolicy());
245
+ if (lines.length === 0)
246
+ return;
247
+ console.log();
248
+ console.log(chalk.bold('Execution policy (Windows)'));
249
+ const [headline, ...rest] = lines;
250
+ console.log(` ${chalk.yellow('warn ')} ${headline}`);
251
+ for (const line of rest) {
252
+ console.log(chalk.gray(` ${line}`));
253
+ }
210
254
  }
211
255
  function parseTargetArg(arg) {
212
256
  const at = arg.indexOf('@');
@@ -475,8 +519,7 @@ async function runFix(parsed, opts) {
475
519
  }
476
520
  // ─── command registration ────────────────────────────────────────────────────
477
521
  export function registerDoctorCommand(program) {
478
- const doctorCmd = program
479
- .command('doctor [target]')
522
+ const doctorCmd = addHostOption(program.command('doctor [target]'))
480
523
  .description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
481
524
  .option('--json', 'Output machine-readable JSON')
482
525
  .option('--diff', 'In target mode, include unified diffs for divergent files')
@@ -547,6 +590,12 @@ export function registerDoctorCommand(program) {
547
590
  return;
548
591
  }
549
592
  renderOverviewText(clis, syncRows, orphanRows, hostClis);
593
+ // Point at the interactive reconcile when anything is out of sync — the
594
+ // report shouldn't be a dead end. `agents status` runs the unified
595
+ // home-reading engine and offers to sync (opt-in, never auto-fires here).
596
+ if (syncRows.some((r) => r.status !== 'fresh')) {
597
+ console.log(chalk.gray('\nRun `agents status` to review and sync what has drifted.'));
598
+ }
550
599
  return;
551
600
  }
552
601
  const parsed = parseTargetArg(target);
@@ -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();
@@ -263,7 +263,7 @@ When to use:
263
263
  pointing at the existing install — nothing is copied or moved (except the
264
264
  agent's config dir, which is moved into the version's home). Works for both
265
265
  npm-style packages (claude, codex, gemini, opencode, openclaw) and
266
- installScript-based agents (grok, antigravity, cursor, kiro, goose, roo).
266
+ installScript-based agents (grok, antigravity, cursor, kiro, goose).
267
267
  `)
268
268
  .action(runImport);
269
269
  }
@@ -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
@@ -51,7 +51,7 @@ Project rules & @-imports:
51
51
  @path imports inside AGENTS.md/CLAUDE.md are resolved at session start by the agent
52
52
  itself, not by agents-cli. Support is per-agent:
53
53
  Inlined natively: claude, gemini
54
- Literal text: codex, cursor, opencode, copilot, amp, kiro, goose, roo
54
+ Literal text: codex, cursor, opencode, copilot, amp, kiro, goose
55
55
 
56
56
  For rules that need to work across all agents, inline the content rather than using
57
57
  @-imports — the second group will load '@path/to/file.md' as a literal string.
@@ -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];