@phnx-labs/agents-cli 1.20.74 → 1.20.76

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 (199) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
- * `agents feed` -- list open blocks (decisions agents are waiting on).
2
+ * `agents feed` -- operator inbox + agent status posts.
3
3
  *
4
+ * Default (`agents feed`): list open blocks (decisions agents are waiting on).
4
5
  * Aggregates block records from the local feed store and, with --host, from
5
6
  * reachable remote hosts via SSH passthrough. Each block carries enough
6
7
  * identity for `agents message` to route a reply back to the right agent.
@@ -8,6 +9,11 @@
8
9
  * Default view groups by **outcome** (ticket / PR / worktree / Unassigned) so
9
10
  * an operator sees dozens of deliverables, not ~1,100 agents. Pass `--flat`
10
11
  * for the legacy per-agent list.
12
+ *
13
+ * `agents feed post`: agent-callable free-text progress into the activity
14
+ * stream (milestone `status.posted`). Session/agent/host/runtime/pid identity
15
+ * is auto-stamped from env + the pid registry — no domain-specific flags.
16
+ * Humans watch via the feed activity lane / `agents activity`.
11
17
  */
12
18
  import type { Command } from 'commander';
13
19
  import { type OpenBlock } from '../lib/feed.js';
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
3
3
  import { ensureActivityLogHook, readRecentActivity, formatActivityLine } from '../lib/activity.js';
4
+ import { postFeedStatus } from '../lib/feed-post.js';
4
5
  import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
5
6
  import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
6
7
  import { machineId, normalizeHost } from '../lib/machine-id.js';
@@ -214,9 +215,9 @@ export function sessionHintsFromActive(sessions) {
214
215
  }));
215
216
  }
216
217
  export function registerFeedCommand(program) {
217
- program
218
+ const feed = program
218
219
  .command('feed')
219
- .description('List open blocks -- decisions agents are waiting on (grouped by outcome)')
220
+ .description('Open blocks (needs you) + agent status posts (feed post)')
220
221
  .option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
221
222
  .option('--flat', 'List one block per agent instead of grouping by outcome')
222
223
  .option('--all', 'Include stalls/FYIs that policy would suppress (default: hide them)')
@@ -225,8 +226,50 @@ export function registerFeedCommand(program) {
225
226
  .option('--device <target...>', 'Alias for --host; repeatable')
226
227
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
227
228
  .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
228
- .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task')
229
- .action(async (opts) => {
229
+ .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
230
+ feed
231
+ .command('post')
232
+ .description('Post a status update to the fleet activity stream (for agents)')
233
+ .argument('<text...>', 'What just happened — one short human line')
234
+ .option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
235
+ .option('--json', 'Emit the written event as JSON')
236
+ .addHelpText('after', `
237
+ Examples:
238
+ # Inside an agents-cli run (session identity is already in the env):
239
+ agents feed post "CHANGELOG pushed; watching CI and mac-mini E2E"
240
+ agents feed post "ready for review" --json
241
+
242
+ # Outside a run, pass the session explicitly:
243
+ agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
244
+
245
+ Identity (session, agent, host, runtime, pid, launchId) is stamped automatically.
246
+ Domain facts (tickets, PRs) are not CLI flags — join them on the session at read time.
247
+ `)
248
+ .action((textParts, opts, cmd) => {
249
+ // Parent `feed` also declares `--json` (for the list view). Commander
250
+ // binds the flag on the parent, so a `feed post … --json` lands on
251
+ // parent.opts().json — not the child. Read both.
252
+ const flags = {
253
+ session: opts?.session ?? cmd?.opts?.()?.session,
254
+ json: Boolean(opts?.json ?? cmd?.opts?.()?.json ?? cmd?.parent?.opts?.()?.json),
255
+ };
256
+ try {
257
+ const { event } = postFeedStatus({
258
+ text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
259
+ sessionId: flags.session,
260
+ });
261
+ if (flags.json) {
262
+ console.log(JSON.stringify(event, null, 2));
263
+ return;
264
+ }
265
+ console.log(formatActivityLine(event, { showHost: true }).trimStart());
266
+ }
267
+ catch (err) {
268
+ console.error(chalk.red(err.message));
269
+ process.exitCode = 1;
270
+ }
271
+ });
272
+ feed.action(async (opts) => {
230
273
  if (opts.device?.length)
231
274
  opts.host = [...(opts.host ?? []), ...opts.device];
232
275
  const self = machineId();
@@ -17,8 +17,15 @@ import type { Command } from 'commander';
17
17
  import { type ActiveSession } from '../lib/session/active.js';
18
18
  import type { SessionMeta } from '../lib/session/types.js';
19
19
  export declare function registerGoCommand(program: Command): void;
20
- /** Live jump targets (local + remote), keyed by session id. Cloud excluded (no pid). */
21
- export declare function gatherLiveTargets(local: boolean): Promise<{
20
+ /**
21
+ * Live jump targets (local + remote), keyed by session id. Cloud is excluded by
22
+ * default (it has no local pid to attach), but `detach` opts in with
23
+ * `includeCloud` so it can resolve a cloud id and refuse it with a clear message
24
+ * instead of a bare "no live session".
25
+ */
26
+ export declare function gatherLiveTargets(local: boolean, opts?: {
27
+ includeCloud?: boolean;
28
+ }): Promise<{
22
29
  self: string;
23
30
  activeById: Map<string, ActiveSession>;
24
31
  }>;
@@ -20,6 +20,7 @@ import { promisify } from 'util';
20
20
  import { getActiveSessions, findSessionFileForKind } from '../lib/session/active.js';
21
21
  import { gatherRemoteActive } from '../lib/session/remote-active.js';
22
22
  import { discoverSessions } from '../lib/session/discover.js';
23
+ import { deriveShortId } from '../lib/session/short-id.js';
23
24
  import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive } from './sessions.js';
24
25
  import { focusAction } from './focus.js';
25
26
  import { machineId } from '../lib/session/sync/config.js';
@@ -39,8 +40,13 @@ export function registerGoCommand(program) {
39
40
  await focusAction(id, { local: opts.local, attachOnly: true });
40
41
  });
41
42
  }
42
- /** Live jump targets (local + remote), keyed by session id. Cloud excluded (no pid). */
43
- export async function gatherLiveTargets(local) {
43
+ /**
44
+ * Live jump targets (local + remote), keyed by session id. Cloud is excluded by
45
+ * default (it has no local pid to attach), but `detach` opts in with
46
+ * `includeCloud` so it can resolve a cloud id and refuse it with a clear message
47
+ * instead of a bare "no live session".
48
+ */
49
+ export async function gatherLiveTargets(local, opts = {}) {
44
50
  const self = machineId();
45
51
  const localActive = await getActiveSessions();
46
52
  for (const s of localActive)
@@ -55,9 +61,13 @@ export async function gatherLiveTargets(local) {
55
61
  catch { /* remote sweep is best-effort */ }
56
62
  }
57
63
  const activeById = new Map();
58
- for (const s of active)
59
- if (s.context !== 'cloud' && s.sessionId)
60
- activeById.set(s.sessionId, s);
64
+ for (const s of active) {
65
+ if (!s.sessionId)
66
+ continue;
67
+ if (s.context === 'cloud' && !opts.includeCloud)
68
+ continue;
69
+ activeById.set(s.sessionId, s);
70
+ }
61
71
  return { self, activeById };
62
72
  }
63
73
  /** Interactive pick over the live sessions' rich SessionMeta; returns the chosen live session. */
@@ -99,7 +109,7 @@ function synthMeta(s, self) {
99
109
  const filePath = remote ? '' : (findSessionFileForKind(s.kind, s.cwd, s.sessionId) ?? '');
100
110
  return {
101
111
  id: s.sessionId,
102
- shortId: s.sessionId.slice(0, 8),
112
+ shortId: deriveShortId(s.sessionId),
103
113
  agent: s.kind,
104
114
  timestamp: new Date(s.startedAtMs ?? Date.now()).toISOString(),
105
115
  filePath,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Custom harness commands (`agents harness`).
3
+ *
4
+ * A "custom harness" is a named (host CLI + model) combo — e.g. OpenCode pinned
5
+ * to meta/muse-spark-1.1, called `spark`. It runs like a native agent type
6
+ * (`agents run spark`) and syncs across devices via `agents repo push user`.
7
+ *
8
+ * Mechanism: harnesses ARE profiles (same ~/.agents/profiles/*.yml, same run
9
+ * resolution). This command group is the harness-flavored surface over that
10
+ * layer + a discovery `list` spanning custom harnesses, addable presets, and the
11
+ * native harness registry. The `agents profiles` tree stays unchanged.
12
+ */
13
+ import type { Command } from 'commander';
14
+ export declare function registerHarnessCommands(program: Command): void;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Custom harness commands (`agents harness`).
3
+ *
4
+ * A "custom harness" is a named (host CLI + model) combo — e.g. OpenCode pinned
5
+ * to meta/muse-spark-1.1, called `spark`. It runs like a native agent type
6
+ * (`agents run spark`) and syncs across devices via `agents repo push user`.
7
+ *
8
+ * Mechanism: harnesses ARE profiles (same ~/.agents/profiles/*.yml, same run
9
+ * resolution). This command group is the harness-flavored surface over that
10
+ * layer + a discovery `list` spanning custom harnesses, addable presets, and the
11
+ * native harness registry. The `agents profiles` tree stays unchanged.
12
+ */
13
+ import chalk from 'chalk';
14
+ import { addProfile } from './profiles.js';
15
+ import { listProfiles, readProfile, deleteProfile, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, getProfilePath, } from '../lib/profiles.js';
16
+ import { listPresets } from '../lib/profiles-presets.js';
17
+ import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
18
+ /** Short capability summary for a native harness — its supported run modes. */
19
+ function nativeModes(id) {
20
+ const modes = AGENTS[id]?.capabilities?.modes ?? [];
21
+ return modes.length ? modes.join('/') : '-';
22
+ }
23
+ export function registerHarnessCommands(program) {
24
+ const cmd = program
25
+ .command('harness')
26
+ .alias('harnesses')
27
+ .description('Custom harnesses — name a (host CLI + model) combo and run it like a native agent type.')
28
+ .addHelpText('after', `
29
+ A custom harness pins a host CLI (opencode, claude, codex, grok, gemini, ...) to a
30
+ model and gives it a name. 'agents run <name>' then behaves like a native agent
31
+ type, and 'agents repo push user' syncs it to every device.
32
+
33
+ Examples:
34
+ # Meta Muse Spark 1.1 through OpenCode, called 'spark'
35
+ agents harness add spark --host opencode --model meta/muse-spark-1.1
36
+ agents run spark "refactor api/handlers/checkout.py"
37
+
38
+ # Per-run model override still wins
39
+ agents run spark --model opencode/big-pickle "quick pass"
40
+
41
+ # See custom harnesses, addable presets, and native harnesses
42
+ agents harness list
43
+
44
+ # Private OpenAI/Anthropic-compatible endpoint (stores a keychain key)
45
+ agents harness add corp --host claude --model gpt-x --base-url https://gw.corp/v1 --auth-provider corp
46
+
47
+ # Custom harnesses are just profiles — this also works via 'agents profiles'
48
+ `);
49
+ cmd
50
+ .command('add <name>')
51
+ .description('Create a custom harness from a host + model (or apply a built-in preset).')
52
+ .option('--host <agent>', 'Host CLI to run under (opencode, claude, codex, grok, gemini, ...) — pair with --model')
53
+ .option('--model <id>', 'Model id to pin on the host (e.g., meta/muse-spark-1.1) — pair with --host')
54
+ .option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
55
+ .option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider (for private endpoints)')
56
+ .option('--preset <preset>', 'Apply a built-in preset instead of --host/--model')
57
+ .option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
58
+ .option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
59
+ .option('--force', 'Overwrite an existing harness with the same name')
60
+ .action(async (name, opts) => {
61
+ try {
62
+ await addProfile(name, opts, 'Harness');
63
+ }
64
+ catch (err) {
65
+ console.error(chalk.red(err.message));
66
+ process.exit(1);
67
+ }
68
+ });
69
+ cmd
70
+ .command('list')
71
+ .alias('ls')
72
+ .description('List custom harnesses, addable presets, and native harnesses.')
73
+ .option('--json', 'Emit JSON instead of a table')
74
+ .action((opts) => {
75
+ const custom = listProfiles();
76
+ const presets = listPresets();
77
+ const native = ALL_AGENT_IDS.map((id) => ({ id, name: AGENTS[id].name, modes: nativeModes(id) }));
78
+ if (opts.json) {
79
+ console.log(JSON.stringify({
80
+ custom: custom.map((p) => ({
81
+ name: p.name,
82
+ host: p.host.agent,
83
+ model: profileModelLabel(p),
84
+ provider: profileProviderLabel(p),
85
+ })),
86
+ presets: presets.map((p) => ({ name: p.name, provider: p.provider, description: p.description })),
87
+ native,
88
+ }, null, 2));
89
+ return;
90
+ }
91
+ console.log(chalk.bold('Custom harnesses') + chalk.gray(' (yours — agents run <name>)'));
92
+ if (custom.length === 0) {
93
+ console.log(chalk.gray(' none yet — try: agents harness add spark --host opencode --model meta/muse-spark-1.1'));
94
+ }
95
+ else {
96
+ for (const p of custom) {
97
+ console.log(` ${chalk.cyan(p.name.padEnd(16))} ${profileHostLabel(p).padEnd(14)} ${chalk.gray(profileModelLabel(p))}`);
98
+ }
99
+ }
100
+ console.log('');
101
+ console.log(chalk.bold('Presets') + chalk.gray(' (addable — agents harness add <name>)'));
102
+ for (const p of presets) {
103
+ console.log(` ${chalk.cyan(p.name.padEnd(16))} ${p.provider.padEnd(12)} ${chalk.gray(p.description.slice(0, 70))}`);
104
+ }
105
+ console.log('');
106
+ console.log(chalk.bold('Native harnesses') + chalk.gray(' (built-in — agents run <id>)'));
107
+ for (const n of native) {
108
+ console.log(` ${chalk.cyan(n.id.padEnd(16))} ${n.name.padEnd(14)} ${chalk.gray('modes: ' + n.modes)}`);
109
+ }
110
+ });
111
+ cmd
112
+ .command('view <name>')
113
+ .alias('show')
114
+ .description('Show one custom harness (host, model, provider, auth, path).')
115
+ .action((name) => {
116
+ try {
117
+ const p = readProfile(name);
118
+ console.log(chalk.bold(p.name));
119
+ if (p.description)
120
+ console.log(chalk.gray(p.description));
121
+ console.log('');
122
+ console.log(`Host: ${profileHostLabel(p)}`);
123
+ console.log(`Model: ${profileModelLabel(p)}`);
124
+ console.log(`Provider: ${profileProviderLabel(p)}`);
125
+ console.log(`Auth: ${profileAuthLabel(p)}`);
126
+ console.log(chalk.gray(getProfilePath(p.name)));
127
+ }
128
+ catch (err) {
129
+ console.error(chalk.red(err.message));
130
+ process.exit(1);
131
+ }
132
+ });
133
+ cmd
134
+ .command('remove <name>')
135
+ .alias('rm')
136
+ .description('Delete a custom harness (keychain token is kept).')
137
+ .action((name) => {
138
+ const existed = deleteProfile(name);
139
+ if (!existed) {
140
+ console.error(chalk.red(`Harness '${name}' not found.`));
141
+ process.exit(1);
142
+ }
143
+ console.log(chalk.green(`Harness '${name}' removed.`));
144
+ });
145
+ }
@@ -134,7 +134,7 @@ async function runImport(agentArg, opts) {
134
134
  }
135
135
  }
136
136
  }
137
- let version = opts.version;
137
+ let version = opts.as;
138
138
  if (!version) {
139
139
  if (!useDirectBinaryImport && globalPath) {
140
140
  try {
@@ -157,7 +157,7 @@ async function runImport(agentArg, opts) {
157
157
  }
158
158
  if (!version) {
159
159
  console.error(chalk.red(`Could not determine version for ${agentLabel(agentId)}.`));
160
- console.error(chalk.gray('Pass --version <version> explicitly.'));
160
+ console.error(chalk.gray('Pass --as <version> explicitly.'));
161
161
  process.exit(1);
162
162
  }
163
163
  if (!isValidImportVersion(version)) {
@@ -167,7 +167,7 @@ async function runImport(agentArg, opts) {
167
167
  }
168
168
  const versionDir = getVersionDir(agentId, version);
169
169
  const fromLabel = useDirectBinaryImport ? installScriptBinary : globalPath;
170
- console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
170
+ console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}${opts.isolated ? ' (isolated copy)' : ''}`));
171
171
  console.log(` from: ${chalk.gray(fromLabel)}`);
172
172
  console.log(` into: ${chalk.gray(versionDir)}`);
173
173
  const configDirExists = fs.existsSync(agent.configDir);
@@ -178,6 +178,16 @@ async function runImport(agentArg, opts) {
178
178
  configAlreadyManaged = true;
179
179
  console.log(` config: ${chalk.gray(`${agent.configDir} (already managed — will skip)`)}`);
180
180
  }
181
+ else if (opts.isolated) {
182
+ // This summary is printed BEFORE the branch that does the work, so it has to
183
+ // describe the mode it is actually about to run. Saying "will be moved" under
184
+ // --isolated announced the exact adoption the flag exists to prevent — a
185
+ // confirmation prompt that misdescribes the operation is worse than none.
186
+ console.log(` config: ${chalk.gray(`${agent.configDir} (will be COPIED — your original stays put)`)}`);
187
+ if (!opts.withAuth) {
188
+ console.log(` ${chalk.gray('credentials are skipped; pass --with-auth to include them')}`);
189
+ }
190
+ }
181
191
  else {
182
192
  console.log(` config: ${chalk.gray(`${agent.configDir} (will be moved into version home)`)}`);
183
193
  }
@@ -188,7 +198,9 @@ async function runImport(agentArg, opts) {
188
198
  if (!opts.yes && isInteractiveTerminal()) {
189
199
  console.log();
190
200
  const proceed = await confirm({
191
- message: `Import ${agentLabel(agentId)} v${version} into agents-cli?`,
201
+ message: opts.isolated
202
+ ? `Import ${agentLabel(agentId)} v${version} as an isolated copy?`
203
+ : `Import ${agentLabel(agentId)} v${version} into agents-cli?`,
192
204
  default: true,
193
205
  }).catch((err) => {
194
206
  if (isPromptCancelled(err))
@@ -205,17 +217,23 @@ async function runImport(agentArg, opts) {
205
217
  // want a stranded symlink farm. Binary registration is cheap and reversible
206
218
  // — if it fails after config, the next `agents import` call retries cleanly.
207
219
  const willImportConfig = configDirExists && !configAlreadyManaged && !opts.isolated;
208
- if (opts.isolated && configDirExists) {
220
+ if (opts.isolated && configDirExists && configAlreadyManaged) {
221
+ console.log(chalk.gray(` Skipping config copy: ${agent.configDir} is a managed symlink, not your real settings.`));
222
+ }
223
+ else if (opts.isolated && configDirExists) {
209
224
  // COPY the user's settings in; never move, never symlink. The original stays
210
225
  // exactly where it is — that is what separates this from adoption.
211
226
  const seedSpinner = ora(`Copying ${agent.configDir} into the isolated copy...`).start();
212
- const seed = seedIsolatedConfigFromLocal(agentId, version, { withAuth: opts.withAuth });
227
+ const seed = seedIsolatedConfigFromLocal(agentId, version, { withAuth: opts.withAuth, all: opts.all });
213
228
  if (seed.error) {
214
229
  seedSpinner.fail(`Config: ${seed.error}`);
215
230
  process.exit(1);
216
231
  }
217
232
  else if (seed.seeded) {
218
233
  seedSpinner.succeed(`Settings copied (${seed.from} -> ${seed.to}); your original is untouched`);
234
+ if (seed.skippedRuntime.length > 0) {
235
+ console.log(chalk.gray(` Runtime state NOT copied: ${seed.skippedRuntime.join(', ')} (regenerated as you use it; --all to include)`));
236
+ }
219
237
  if (seed.skippedAuth.length > 0) {
220
238
  console.log(chalk.gray(` Credentials NOT copied: ${seed.skippedAuth.join(', ')}`));
221
239
  console.log(chalk.gray(' The copy signs in separately. Use --with-auth to copy them too.'));
@@ -288,15 +306,22 @@ export function registerImportCommand(program) {
288
306
  .command('import')
289
307
  .argument('<agent>', 'Agent id (e.g. openclaw, claude, codex)')
290
308
  .description('Import an existing unmanaged agent install into agents-cli')
291
- .option('--version <version>', 'Pin a version label (otherwise read from package.json)')
309
+ // NOT `--version`. The program declares `.version(VERSION)` (src/index.ts), which
310
+ // claims `-V, --version` globally and wins over a subcommand option of the same
311
+ // name — so `agents import codex --version 1.2.3` printed the CLI's own version
312
+ // and exited without importing anything. The flag had been unreachable since it
313
+ // was introduced, and the "could not determine version" error even advised using
314
+ // it. Renamed so it actually reaches the command.
315
+ .option('--as <version>', 'Version label to import as (otherwise read from package.json)')
292
316
  .option('--from-path <path>', 'Path to the npm package dir (otherwise auto-detected from PATH)')
293
317
  .option('--isolated', 'Copy the install into a self-contained isolated version instead of adopting it')
294
318
  .option('--with-auth', 'With --isolated, also copy credentials into the sandbox (skipped by default)')
319
+ .option('--all', 'With --isolated, also copy session history, logs and caches (skipped by default)')
295
320
  .option('-y, --yes', 'Skip the confirmation prompt')
296
321
  .addHelpText('after', `
297
322
  Examples:
298
323
  $ agents import openclaw Auto-detect via PATH
299
- $ agents import openclaw --version 2026.3.8 Pin a version label
324
+ $ agents import openclaw --as 2026.3.8 Pin a version label
300
325
  $ agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
301
326
 
302
327
  # installScript-based agents (curl/brew installers, no npm package):
@@ -305,6 +330,11 @@ Examples:
305
330
  $ agents import cursor Adopt ~/.local/bin/cursor-agent
306
331
  $ agents import antigravity --from-path ~/.local/bin/agy
307
332
 
333
+ # Copy your setup into a sandbox instead of adopting it:
334
+ $ agents import codex --isolated New isolated copy at the local version
335
+ $ agents import codex --isolated --as 0.146.0 Re-seed an EXISTING isolated copy
336
+ $ agents import codex --isolated --with-auth ...and share credentials with it
337
+
308
338
  When to use:
309
339
  When an agent CLI is already installed globally and you want to bring it
310
340
  under agents-cli management without reinstalling. Creates a symlink farm
@@ -21,6 +21,7 @@ import { truncate, termLink } from '../lib/format.js';
21
21
  import * as yaml from 'yaml';
22
22
  import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
23
23
  import { supports } from '../lib/capabilities.js';
24
+ import { resolveConfiguredModel } from '../lib/models.js';
24
25
  import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
25
26
  import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
26
27
  import { getVersionHomePath } from '../lib/versions.js';
@@ -585,8 +586,10 @@ async function renderSummary(agent, version, versionHome, options) {
585
586
  console.log(JSON.stringify(json, null, 2));
586
587
  return;
587
588
  }
588
- // Plain text
589
- const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${isDefault ? ' ' + chalk.green('[default]') : ''}`;
589
+ // Plain text — model sits right beside the version, same priority (no label).
590
+ const configuredModel = resolveConfiguredModel(agent, version)?.model;
591
+ const modelPart = configuredModel ? ' ' + chalk.yellow(configuredModel) : '';
592
+ const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${modelPart}${isDefault ? ' ' + chalk.green('[default]') : ''}`;
590
593
  console.log('\n' + head + '\n');
591
594
  const rows = [
592
595
  ['install', versionHome],
@@ -15,5 +15,25 @@ import { type Preset } from '../lib/profiles-presets.js';
15
15
  * without mocking @inquirer/prompts.
16
16
  */
17
17
  export declare function buildProfileFromCollection(name: string, preset: Preset, collected: Record<string, string>, version?: string): Profile;
18
+ /** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
19
+ export interface AddProfileOptions {
20
+ preset?: string;
21
+ host?: string;
22
+ model?: string;
23
+ baseUrl?: string;
24
+ authProvider?: string;
25
+ version?: string;
26
+ keyStdin?: boolean;
27
+ force?: boolean;
28
+ }
29
+ /**
30
+ * Create a profile ("custom harness"). Two paths:
31
+ * - `--host <agent> --model <id>`: one-shot custom harness from a host + model
32
+ * (no preset needed). This is what makes a model like Muse Spark a named,
33
+ * runnable harness.
34
+ * - otherwise: apply a built-in preset (existing behavior).
35
+ * `label` only tunes the success wording (Profile vs Harness). Throws on error.
36
+ */
37
+ export declare function addProfile(name: string, opts: AddProfileOptions, label?: 'Profile' | 'Harness'): Promise<void>;
18
38
  /** Register the `agents profiles` command tree. */
19
39
  export declare function registerProfilesCommands(program: Command): void;
@@ -9,13 +9,13 @@
9
9
  import chalk from 'chalk';
10
10
  import { readStdinSync } from '../lib/format.js';
11
11
  import { spawn } from 'child_process';
12
- import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
12
+ import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, profileFromHostModel, baseUrlEnvKeyForHost, authEnvKeyForHost, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
13
13
  import { getPreset, listPresets, expandPreset } from '../lib/profiles-presets.js';
14
14
  import { hasKeychainToken, keychainItemName, setKeychainToken, deleteKeychainToken, } from '../lib/secrets/profiles.js';
15
15
  import { isInteractiveTerminal } from './utils.js';
16
16
  import { getAgentsInvocation } from '../lib/daemon.js';
17
17
  import { getActiveResourceProfileName, getResourceProfilePreset, listResourceProfileNames, setActiveResourceProfile, upsertResourceProfilePreset, validateResourceProfileName, } from '../lib/resource-profiles.js';
18
- import { AGENTS } from '../lib/agents.js';
18
+ import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
19
19
  import { listInstalledVersions, syncResourcesToVersion } from '../lib/versions.js';
20
20
  /**
21
21
  * Pure helper: builds a Profile from collected wizard inputs. Extracted so the
@@ -31,6 +31,7 @@ export function buildProfileFromCollection(name, preset, collected, version) {
31
31
  envVar: preset.authEnvVar,
32
32
  keychainItem: keychainItemName(preset.provider),
33
33
  },
34
+ authOptional: preset.authOptional,
34
35
  description: preset.description,
35
36
  preset: preset.name,
36
37
  provider: preset.provider,
@@ -65,6 +66,66 @@ async function ensureProviderToken(provider, signupUrl, fromStdin) {
65
66
  setKeychainToken(item, token);
66
67
  console.log(chalk.green(`Stored in keychain: ${item}`));
67
68
  }
69
+ /**
70
+ * Create a profile ("custom harness"). Two paths:
71
+ * - `--host <agent> --model <id>`: one-shot custom harness from a host + model
72
+ * (no preset needed). This is what makes a model like Muse Spark a named,
73
+ * runnable harness.
74
+ * - otherwise: apply a built-in preset (existing behavior).
75
+ * `label` only tunes the success wording (Profile vs Harness). Throws on error.
76
+ */
77
+ export async function addProfile(name, opts, label = 'Profile') {
78
+ validateProfileName(name);
79
+ if (profileExists(name) && !opts.force) {
80
+ throw new Error(`${label} '${name}' already exists. Use --force to overwrite.`);
81
+ }
82
+ // One-shot host + model → custom harness, no preset required.
83
+ if (opts.host || opts.model) {
84
+ if (!opts.host || !opts.model) {
85
+ throw new Error('Both --host <agent> and --model <id> are required to build a harness from a host + model.');
86
+ }
87
+ if (!ALL_AGENT_IDS.includes(opts.host)) {
88
+ throw new Error(`Unknown host '${opts.host}'. Valid hosts: ${ALL_AGENT_IDS.join(', ')}`);
89
+ }
90
+ const host = opts.host;
91
+ if (opts.baseUrl && !baseUrlEnvKeyForHost(host)) {
92
+ console.error(chalk.yellow(`Note: --base-url has no known env var for host '${host}'; ignoring it.`));
93
+ }
94
+ let authEnvVar;
95
+ if (opts.authProvider) {
96
+ const key = authEnvKeyForHost(host);
97
+ if (!key) {
98
+ throw new Error(`--auth-provider is set but host '${host}' has no known auth env var. Use a preset or a hand-written profile YAML.`);
99
+ }
100
+ authEnvVar = key;
101
+ await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
102
+ }
103
+ const profile = profileFromHostModel(name, host, opts.model, {
104
+ version: opts.version,
105
+ baseUrl: opts.baseUrl,
106
+ provider: opts.authProvider,
107
+ authEnvVar,
108
+ });
109
+ writeProfile(profile);
110
+ console.log(chalk.green(`${label} '${name}' added — ${host} + ${opts.model}.`));
111
+ console.log(chalk.gray(`Try: agents run ${name} "hello"`));
112
+ return;
113
+ }
114
+ // Preset path.
115
+ const presetName = opts.preset || name;
116
+ const preset = getPreset(presetName);
117
+ if (!preset) {
118
+ throw new Error(`No preset '${presetName}'.\nAvailable presets: ${listPresets().map((p) => p.name).join(', ')}\n` +
119
+ 'Or build a custom harness: --host <agent> --model <id>.');
120
+ }
121
+ if (!preset.authOptional) {
122
+ await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
123
+ }
124
+ const profile = profileFromPreset(name, preset, opts.version);
125
+ writeProfile(profile);
126
+ console.log(chalk.green(`${label} '${name}' added.`));
127
+ console.log(chalk.gray(`Try: agents run ${name} "hello"`));
128
+ }
68
129
  /** Format a single profile as a table row for the `profiles list` output. */
69
130
  function renderProfileRow(p) {
70
131
  const host = p.host.version ? `${p.host.agent}@${p.host.version}` : p.host.agent;
@@ -462,27 +523,13 @@ Examples:
462
523
  .option('--version <version>', 'Pin the host CLI version (e.g., 2.1.113)')
463
524
  .option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
464
525
  .option('--force', 'Overwrite an existing profile with the same name')
526
+ .addHelpText('after', '\nTo build a custom harness from a host CLI + model in one shot, use `agents harness add`.\n')
465
527
  .action(async (name, opts) => {
528
+ // Preset-only surface here — `--host` on `profiles` is reserved for remote
529
+ // device routing (see lib/hosts/passthrough.ts). The host+model one-shot
530
+ // lives on `agents harness add`, which owns its own `--host`.
466
531
  try {
467
- if (profileExists(name) && !opts.force) {
468
- console.error(chalk.red(`Profile '${name}' already exists. Use --force to overwrite.`));
469
- process.exit(1);
470
- }
471
- const presetName = opts.preset || name;
472
- const preset = getPreset(presetName);
473
- if (!preset) {
474
- console.error(chalk.red(`No preset '${presetName}'.`));
475
- console.error(chalk.gray('Available presets: ' + listPresets().map((p) => p.name).join(', ')));
476
- console.error(chalk.gray('Or pass --preset <name> to pick explicitly.'));
477
- process.exit(1);
478
- }
479
- if (!preset.authOptional) {
480
- await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
481
- }
482
- const profile = profileFromPreset(name, preset, opts.version);
483
- writeProfile(profile);
484
- console.log(chalk.green(`Profile '${name}' added.`));
485
- console.log(chalk.gray(`Try: agents run ${name} "hello"`));
532
+ await addProfile(name, { preset: opts.preset, version: opts.version, keyStdin: opts.keyStdin, force: opts.force }, 'Profile');
486
533
  }
487
534
  catch (err) {
488
535
  console.error(chalk.red(err.message));
@@ -812,6 +812,7 @@ export function registerRepoCommands(program) {
812
812
  console.log(chalk.gray('No repos to pull.'));
813
813
  return;
814
814
  }
815
+ let anyPulled = false;
815
816
  for (const t of targets) {
816
817
  if (!fs.existsSync(t.dir) || !isGitRepo(t.dir)) {
817
818
  // A plain (never-cloned) user repo — setup makes ~/.agents a bare dir and
@@ -851,11 +852,24 @@ export function registerRepoCommands(program) {
851
852
  const result = await pullRepo(t.dir);
852
853
  if (result.success) {
853
854
  spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
855
+ anyPulled = true;
854
856
  }
855
857
  else {
856
858
  spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
857
859
  }
858
860
  }
861
+ // RUSH-1980: a pull rewrites the routine YAML on disk, but the daemon's
862
+ // scheduler froze its JobConfigs (device pins included) at load. Without a
863
+ // reload it keeps firing the pre-pull pins — a routine re-pinned to another
864
+ // host still fires here, double-firing across the fleet. SIGHUP the daemon
865
+ // so scheduler.reloadAll() re-reads the synced YAML and device pins refresh.
866
+ // No-op when the daemon isn't running (or on Windows, which has no SIGHUP).
867
+ if (anyPulled) {
868
+ const { isDaemonRunning, signalDaemonReload } = await import('../lib/daemon.js');
869
+ if (isDaemonRunning() && signalDaemonReload()) {
870
+ console.log(chalk.gray('Reloaded the routines daemon (device pins refreshed).'));
871
+ }
872
+ }
859
873
  });
860
874
  repoCmd
861
875
  .command('push [alias]')