@phnx-labs/agents-cli 1.22.22 → 1.22.24

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +13 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/attach.d.ts +2 -0
  5. package/dist/commands/attach.js +32 -7
  6. package/dist/commands/defaults.js +2 -0
  7. package/dist/commands/doctor.js +20 -7
  8. package/dist/commands/exec.js +100 -34
  9. package/dist/commands/feed.d.ts +18 -0
  10. package/dist/commands/feed.js +44 -1
  11. package/dist/commands/focus.d.ts +108 -5
  12. package/dist/commands/focus.js +455 -32
  13. package/dist/commands/fork.d.ts +20 -2
  14. package/dist/commands/fork.js +91 -64
  15. package/dist/commands/go.d.ts +39 -1
  16. package/dist/commands/go.js +112 -7
  17. package/dist/commands/harness-wizard.d.ts +206 -0
  18. package/dist/commands/harness-wizard.js +403 -0
  19. package/dist/commands/harness.d.ts +12 -0
  20. package/dist/commands/harness.js +97 -101
  21. package/dist/commands/insights.js +6 -2
  22. package/dist/commands/inspect.js +39 -5
  23. package/dist/commands/menubar.js +6 -1
  24. package/dist/commands/models.js +1 -0
  25. package/dist/commands/modes.d.ts +12 -0
  26. package/dist/commands/modes.js +147 -0
  27. package/dist/commands/resume.js +10 -3
  28. package/dist/commands/secrets-sync.js +11 -13
  29. package/dist/commands/secrets.d.ts +2 -0
  30. package/dist/commands/secrets.js +79 -47
  31. package/dist/commands/sessions-browser.d.ts +35 -0
  32. package/dist/commands/sessions-browser.js +140 -14
  33. package/dist/commands/sessions-resume.d.ts +23 -3
  34. package/dist/commands/sessions-resume.js +73 -19
  35. package/dist/commands/sessions.d.ts +91 -5
  36. package/dist/commands/sessions.js +359 -125
  37. package/dist/commands/setup-secrets.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/view.js +2 -0
  40. package/dist/commands/watchdog.js +13 -2
  41. package/dist/index.js +2 -1
  42. package/dist/lib/agent-modes.d.ts +49 -0
  43. package/dist/lib/agent-modes.js +70 -0
  44. package/dist/lib/agents.d.ts +1 -1
  45. package/dist/lib/agents.js +132 -0
  46. package/dist/lib/codex-policy.d.ts +17 -0
  47. package/dist/lib/codex-policy.js +48 -0
  48. package/dist/lib/crabbox/lease.d.ts +25 -0
  49. package/dist/lib/crabbox/lease.js +62 -0
  50. package/dist/lib/daemon.js +70 -0
  51. package/dist/lib/doctor-diff.d.ts +3 -0
  52. package/dist/lib/doctor-diff.js +15 -13
  53. package/dist/lib/event-stream.d.ts +3 -1
  54. package/dist/lib/event-stream.js +14 -1
  55. package/dist/lib/exec.d.ts +16 -0
  56. package/dist/lib/exec.js +175 -62
  57. package/dist/lib/feed-broadcast.d.ts +1 -20
  58. package/dist/lib/feed-broadcast.js +31 -1
  59. package/dist/lib/hooks/cache.js +36 -3
  60. package/dist/lib/hooks.d.ts +19 -7
  61. package/dist/lib/hooks.js +100 -40
  62. package/dist/lib/hosts/session-index.d.ts +4 -0
  63. package/dist/lib/hosts/session-index.js +7 -0
  64. package/dist/lib/manifest.d.ts +12 -2
  65. package/dist/lib/manifest.js +60 -5
  66. package/dist/lib/mcp.js +44 -0
  67. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  68. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  69. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  70. package/dist/lib/menubar/install-menubar.js +128 -6
  71. package/dist/lib/models.d.ts +0 -5
  72. package/dist/lib/models.js +48 -0
  73. package/dist/lib/plugin-marketplace.js +9 -0
  74. package/dist/lib/pricing/prices.json +119 -92
  75. package/dist/lib/pricing/table.js +13 -0
  76. package/dist/lib/refresh.d.ts +5 -0
  77. package/dist/lib/refresh.js +37 -33
  78. package/dist/lib/remote-agents-json.d.ts +29 -1
  79. package/dist/lib/remote-agents-json.js +47 -10
  80. package/dist/lib/resource-inventory.d.ts +79 -0
  81. package/dist/lib/resource-inventory.js +122 -0
  82. package/dist/lib/resources/mcp.js +2 -0
  83. package/dist/lib/resources/permissions.js +3 -0
  84. package/dist/lib/resources/types.d.ts +2 -1
  85. package/dist/lib/resources.js +8 -5
  86. package/dist/lib/run-defaults.d.ts +2 -0
  87. package/dist/lib/run-defaults.js +23 -2
  88. package/dist/lib/runner.js +50 -36
  89. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  90. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  91. package/dist/lib/secrets/bundles.js +157 -65
  92. package/dist/lib/secrets/filestore.d.ts +5 -3
  93. package/dist/lib/secrets/filestore.js +12 -8
  94. package/dist/lib/secrets/index.d.ts +15 -1
  95. package/dist/lib/secrets/index.js +148 -53
  96. package/dist/lib/secrets/reaper.d.ts +97 -0
  97. package/dist/lib/secrets/reaper.js +219 -0
  98. package/dist/lib/secrets/remote.d.ts +29 -0
  99. package/dist/lib/secrets/remote.js +37 -1
  100. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  101. package/dist/lib/secrets/sync-passphrase.js +78 -0
  102. package/dist/lib/session/active.d.ts +36 -1
  103. package/dist/lib/session/active.js +60 -19
  104. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  105. package/dist/lib/session/actor-sidecar.js +67 -4
  106. package/dist/lib/session/db.d.ts +1 -1
  107. package/dist/lib/session/db.js +32 -1
  108. package/dist/lib/session/discover.js +168 -0
  109. package/dist/lib/session/parse.d.ts +10 -0
  110. package/dist/lib/session/parse.js +98 -0
  111. package/dist/lib/session/recovery.d.ts +37 -0
  112. package/dist/lib/session/recovery.js +95 -0
  113. package/dist/lib/session/remote-list.d.ts +10 -1
  114. package/dist/lib/session/remote-list.js +2 -8
  115. package/dist/lib/session/remote.d.ts +57 -3
  116. package/dist/lib/session/remote.js +90 -26
  117. package/dist/lib/session/resume-command.d.ts +6 -0
  118. package/dist/lib/session/resume-command.js +8 -0
  119. package/dist/lib/session/session-cache.d.ts +173 -0
  120. package/dist/lib/session/session-cache.js +399 -0
  121. package/dist/lib/session/types.d.ts +1 -1
  122. package/dist/lib/session/types.js +1 -1
  123. package/dist/lib/session/width.d.ts +1 -1
  124. package/dist/lib/session/width.js +12 -2
  125. package/dist/lib/shims.d.ts +2 -2
  126. package/dist/lib/shims.js +62 -8
  127. package/dist/lib/staleness/writers/hooks.js +1 -1
  128. package/dist/lib/startup/command-registry.d.ts +1 -0
  129. package/dist/lib/startup/command-registry.js +2 -0
  130. package/dist/lib/sync-umbrella.d.ts +5 -0
  131. package/dist/lib/sync-umbrella.js +5 -4
  132. package/dist/lib/tmux/session.d.ts +4 -2
  133. package/dist/lib/tmux/session.js +5 -5
  134. package/dist/lib/types.d.ts +3 -1
  135. package/dist/lib/usage.d.ts +13 -0
  136. package/dist/lib/usage.js +215 -0
  137. package/dist/lib/versions.js +13 -2
  138. package/package.json +1 -1
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Shared interactive step engine for `agents harness` create + edit.
3
+ *
4
+ * A single engine drives BOTH the create wizard (`agents harness add`/`fork`,
5
+ * previously `runHarnessWizard`) and the new edit wizard (`agents harness edit`,
6
+ * which was flag-only before). A "step" is a self-contained unit — decide whether
7
+ * it runs for the current draft, then prompt/validate/apply — so the two modes
8
+ * differ only in their step list, not in the runner.
9
+ *
10
+ * Design goals (RUSH-2219, parent RUSH-2218):
11
+ * - One runner, two modes. `create` builds a new harness from a source; `edit`
12
+ * loads an existing profile and re-asks each field pre-filled with its value.
13
+ * - Every step is skippable. A flag that already supplied the value pre-fills
14
+ * the draft and its step is not re-asked — so non-interactive scripting via
15
+ * flags is unchanged and the wizard only asks for what is missing.
16
+ * - Pure and injectable. The engine talks to the user through {@link WizardIO},
17
+ * an injected seam. Tests drive a scripted fake IO and assert which steps ran,
18
+ * with no TTY. {@link defaultWizardIO} is the production driver over
19
+ * `@inquirer/prompts`.
20
+ * - Typed extension points, not stubs. The three sibling subtasks plug in via
21
+ * {@link WizardHooks} without editing the engine: RUSH-2220 (model catalog +
22
+ * secrets surface) via `pickModel`, RUSH-2221 (connection test) via
23
+ * `connectionTest`, RUSH-2222 (edit matrix) via `editable`. Each hook is a
24
+ * real no-op extension point — absent, the scaffold falls back to today's
25
+ * behavior (free-text model, no test, resolver-sourced editability). None of
26
+ * them fakes a result. RUSH-2223 (cross-host portability, `fork --to-host`)
27
+ * extends the create source step; the seam is `HarnessDraft.toHost`.
28
+ */
29
+ import chalk from 'chalk';
30
+ import { baseUrlEnvKeyForHost, authEnvKeyForHost, modelEnvKeyForHost, validateProfileName, listProfiles, } from '../lib/profiles.js';
31
+ import { listPresets, getPreset } from '../lib/profiles-presets.js';
32
+ import { listBundles } from '../lib/secrets/bundles.js';
33
+ import { AGENTS, ALL_AGENT_IDS, isSelfUpdatingAgent, resolveAgentName } from '../lib/agents.js';
34
+ /**
35
+ * Resolver-sourced editability, the scaffold default behind the RUSH-2222 seam.
36
+ * Every field is read from the same maps the run-time resolver uses
37
+ * (`baseUrlEnvKeyForHost` / `authEnvKeyForHost` / `isSelfUpdatingAgent`), never a
38
+ * table hardcoded alongside them — so the wizard's enable/disable can never drift
39
+ * from what a run actually honors (repo rule: the capability table stays truthful,
40
+ * in lockstep with the code). RUSH-2222 may replace this via {@link WizardHooks.editable}
41
+ * to add per-param reasons; it must stay sourced from the resolver.
42
+ */
43
+ export function defaultEditable(host) {
44
+ return {
45
+ model: true,
46
+ baseUrl: baseUrlEnvKeyForHost(host) !== null,
47
+ auth: authEnvKeyForHost(host) !== null,
48
+ version: !isSelfUpdatingAgent(host),
49
+ fallback: true,
50
+ };
51
+ }
52
+ /** Resolve the host CLI a fork `source` runs under (the host `buildFork` will use). */
53
+ export function hostForSource(source) {
54
+ if (!source)
55
+ return undefined;
56
+ const native = resolveAgentName(source);
57
+ if (native)
58
+ return native;
59
+ // An existing custom harness: its own host.
60
+ const custom = listProfiles().find((p) => p.name === source);
61
+ return custom?.host.agent;
62
+ }
63
+ /**
64
+ * The engine. Walk the steps in order; for each, ask `decide` what to do, then
65
+ * prompt only when it says `'run'`. A `{ disabled }` decision surfaces the reason
66
+ * and moves on; `'skip'` is silent. Returns the finished draft.
67
+ */
68
+ export async function runWizardSteps(steps, draft, io, hooks = {}) {
69
+ for (const step of steps) {
70
+ const decision = step.decide(draft);
71
+ if (decision === 'run') {
72
+ await step.run(io, draft, hooks);
73
+ }
74
+ else if (decision !== 'skip') {
75
+ io.note(chalk.gray(`${step.id}: ${decision.disabled}`));
76
+ }
77
+ }
78
+ return draft;
79
+ }
80
+ // --- shared prompt fragments ------------------------------------------------
81
+ const NO_AUTH = '__none__';
82
+ const CUSTOM = '__custom__';
83
+ const TYPE_NOW = 'type';
84
+ const FROM_SECRETS = 'secrets';
85
+ const KEEP = '__keep__';
86
+ /** The first `_MODEL`-suffixed env var in a preset's static env block, if any. */
87
+ function presetModel(preset) {
88
+ return Object.entries(preset.env).find(([k]) => k.endsWith('_MODEL'))?.[1];
89
+ }
90
+ /** Providers offered in the custom-auth select, deduped from the preset catalog. */
91
+ function knownProviders() {
92
+ return [...new Set(listPresets().map((p) => p.provider))];
93
+ }
94
+ /**
95
+ * Prompt for a key source when a provider needs one: type it now, or copy it from
96
+ * an existing agents secrets bundle. Sets `draft.fromSecrets` for the bundle path;
97
+ * the type-now path is handled downstream (ensureProviderToken), unchanged. This
98
+ * is today's behavior, lifted verbatim; RUSH-2220 enriches the bundle browse.
99
+ */
100
+ async function askKeySource(io, draft, provider) {
101
+ const bundles = listBundles();
102
+ const source = bundles.length > 0
103
+ ? await io.select({
104
+ message: `How should '${provider}' get its key?`,
105
+ choices: [
106
+ { name: 'Type a key now', value: TYPE_NOW },
107
+ { name: 'Use an existing agents secrets bundle', value: FROM_SECRETS },
108
+ ],
109
+ })
110
+ : TYPE_NOW;
111
+ if (source !== FROM_SECRETS)
112
+ return;
113
+ const bundleName = await io.select({
114
+ message: 'Bundle',
115
+ choices: bundles.map((b) => ({
116
+ name: b.description ? `${b.name} ${chalk.gray(b.description)}` : b.name,
117
+ value: b.name,
118
+ })),
119
+ });
120
+ const bundle = bundles.find((b) => b.name === bundleName);
121
+ const keys = Object.keys(bundle.vars);
122
+ const key = keys.length === 1
123
+ ? keys[0]
124
+ : await io.select({ message: 'Key', choices: keys.map((k) => ({ name: k, value: k })) });
125
+ draft.fromSecrets = `${bundleName}:${key}`;
126
+ }
127
+ /** Free-text model prompt, unless RUSH-2220's catalog hook picks one first. */
128
+ async function askModel(io, draft, hooks, current) {
129
+ if (hooks.pickModel) {
130
+ const picked = await hooks.pickModel(io, draft.host, draft.version ?? draft.original?.host.version, current);
131
+ if (picked !== null)
132
+ return picked;
133
+ }
134
+ return io.input({ message: 'Model id', default: current });
135
+ }
136
+ // --- create steps -----------------------------------------------------------
137
+ /**
138
+ * The create step list — the shape of `agents harness add`/`fork`. Faithful to
139
+ * the previous `runHarnessWizard` sequence (source → preset|custom → model →
140
+ * provider → base URL → name → key source), re-expressed as engine steps so the
141
+ * sibling subtasks can gate/replace individual steps. Produces the same
142
+ * `{ source, name, opts }` draft the fork flow already persists.
143
+ */
144
+ export function createSteps() {
145
+ return [
146
+ {
147
+ id: 'source',
148
+ decide: (d) => (d.source ? 'skip' : 'run'),
149
+ async run(io, d) {
150
+ const customNames = listProfiles().map((p) => p.name);
151
+ d.source = await io.select({
152
+ message: 'Fork from',
153
+ choices: [
154
+ ...ALL_AGENT_IDS.map((id) => ({ name: `${AGENTS[id].name} ${chalk.gray('(native)')}`, value: id })),
155
+ ...customNames.map((n) => ({ name: `${n} ${chalk.gray('(custom harness)')}`, value: n })),
156
+ ],
157
+ });
158
+ d.host = hostForSource(d.source) ?? d.host;
159
+ d.defaultName = d.source;
160
+ },
161
+ },
162
+ {
163
+ id: 'preset',
164
+ decide: (d) => (d.custom || d.preset !== undefined || d.model !== undefined ? 'skip' : 'run'),
165
+ async run(io, d) {
166
+ const presets = listPresets();
167
+ const choice = await io.select({
168
+ message: 'Preset',
169
+ choices: [
170
+ ...presets.map((p) => ({ name: `${p.name} ${chalk.gray(p.description.slice(0, 60))}`, value: p.name })),
171
+ { name: 'Build custom (host + model + provider)', value: CUSTOM },
172
+ ],
173
+ });
174
+ if (choice === CUSTOM) {
175
+ d.custom = true;
176
+ return;
177
+ }
178
+ const preset = getPreset(choice);
179
+ d.preset = preset.name;
180
+ d.model = presetModel(preset);
181
+ d.baseUrl = preset.env.ANTHROPIC_BASE_URL || preset.env.OPENAI_BASE_URL || undefined;
182
+ d.authProvider = preset.authOptional ? undefined : preset.provider;
183
+ d.providerAsked = true;
184
+ // Pre-fill the name with the preset's own name (e.g. 'deepseek'), not a
185
+ // model detail, so users aren't nudged toward baking one into the identity.
186
+ d.defaultName = preset.name;
187
+ },
188
+ },
189
+ {
190
+ id: 'model',
191
+ decide: (d) => (d.custom && d.model === undefined ? 'run' : 'skip'),
192
+ async run(io, d, hooks) {
193
+ d.model = await askModel(io, d, hooks);
194
+ },
195
+ },
196
+ {
197
+ id: 'provider',
198
+ decide: (d) => (d.custom && !d.providerAsked ? 'run' : 'skip'),
199
+ async run(io, d) {
200
+ const choice = await io.select({
201
+ message: 'Provider',
202
+ choices: [
203
+ ...knownProviders().map((p) => ({ name: p, value: p })),
204
+ { name: 'no auth / host manages its own login', value: NO_AUTH },
205
+ ],
206
+ });
207
+ d.authProvider = choice === NO_AUTH ? undefined : choice;
208
+ d.providerAsked = true;
209
+ },
210
+ },
211
+ {
212
+ id: 'baseUrl',
213
+ decide: (d) => {
214
+ if (!d.custom || d.baseUrl !== undefined)
215
+ return 'skip';
216
+ // Endpoint slot is a function of the host's API format (§3.4): only the
217
+ // Anthropic/OpenAI-compatible hosts carry one. Skipping the prompt for the
218
+ // rest replaces the old silent-drop (`profileFromHostModel` discards a
219
+ // base URL the host can't honor) with an explicit reason.
220
+ const host = d.host ?? hostForSource(d.source);
221
+ if (host && baseUrlEnvKeyForHost(host) === null) {
222
+ return { disabled: `host '${host}' has no custom-endpoint slot — base URL not applicable` };
223
+ }
224
+ return 'run';
225
+ },
226
+ async run(io, d) {
227
+ const url = await io.input({ message: 'Base URL (optional)', default: '' });
228
+ d.baseUrl = url || undefined;
229
+ },
230
+ },
231
+ {
232
+ id: 'name',
233
+ decide: (d) => (d.name ? 'skip' : 'run'),
234
+ async run(io, d) {
235
+ d.name = await io.input({
236
+ message: 'Harness name',
237
+ default: d.defaultName,
238
+ validate: (v) => {
239
+ try {
240
+ validateProfileName(v);
241
+ return true;
242
+ }
243
+ catch (err) {
244
+ return err.message;
245
+ }
246
+ },
247
+ });
248
+ },
249
+ },
250
+ {
251
+ id: 'auth',
252
+ decide: (d) => (d.authProvider && d.fromSecrets === undefined ? 'run' : 'skip'),
253
+ async run(io, d) {
254
+ await askKeySource(io, d, d.authProvider);
255
+ },
256
+ },
257
+ connectionTestStep(),
258
+ ];
259
+ }
260
+ // --- edit steps -------------------------------------------------------------
261
+ /** The raw model id currently pinned on a profile (not the display label). */
262
+ function currentModel(p) {
263
+ return p.env[modelEnvKeyForHost(p.host.agent)];
264
+ }
265
+ /** The raw base URL currently pinned on a profile, if the host carries one. */
266
+ function currentBaseUrl(p) {
267
+ const key = baseUrlEnvKeyForHost(p.host.agent);
268
+ return key ? p.env[key] : undefined;
269
+ }
270
+ /**
271
+ * The edit step list — `agents harness edit <name>` with no flags on a TTY. Each
272
+ * step is pre-filled with the profile's current value and gated by the host's
273
+ * editability matrix (RUSH-2222 seam): an unsupported param reads as disabled with
274
+ * a reason instead of being silently accepted. Records into the same draft, which
275
+ * `runEditWizard` maps to the `EditOptions` shape the flag path already persists.
276
+ */
277
+ export function editSteps(original) {
278
+ const host = original.host.agent;
279
+ const editableFor = (hooks) => (hooks.editable ?? defaultEditable)(host);
280
+ // decide() has no access to hooks, so gate on the resolver default; a hook that
281
+ // narrows editability further is applied inside run(). The scaffold's default is
282
+ // the resolver truth, which is what the matrix subtask builds on.
283
+ const cap = defaultEditable(host);
284
+ return [
285
+ {
286
+ id: 'model',
287
+ decide: () => (cap.model ? 'run' : { disabled: `host '${host}' does not support a pinned model` }),
288
+ async run(io, d, hooks) {
289
+ if (!editableFor(hooks).model)
290
+ return;
291
+ d.model = await askModel(io, d, hooks, currentModel(original));
292
+ },
293
+ },
294
+ {
295
+ id: 'baseUrl',
296
+ decide: () => (cap.baseUrl ? 'run' : { disabled: `host '${host}' has no custom-endpoint slot` }),
297
+ async run(io, d, hooks) {
298
+ if (!editableFor(hooks).baseUrl)
299
+ return;
300
+ const url = await io.input({ message: 'Base URL', default: currentBaseUrl(original) ?? '' });
301
+ d.baseUrl = url || '';
302
+ },
303
+ },
304
+ {
305
+ id: 'auth',
306
+ decide: () => (cap.auth ? 'run' : { disabled: `host '${host}' manages its own login — no auth to edit` }),
307
+ async run(io, d, hooks) {
308
+ if (!editableFor(hooks).auth)
309
+ return;
310
+ const bundles = listBundles();
311
+ const choices = [
312
+ { name: 'Leave auth unchanged', value: KEEP },
313
+ { name: 'Repoint to a provider (enter a key)', value: TYPE_NOW },
314
+ ];
315
+ if (bundles.length > 0)
316
+ choices.push({ name: 'Copy a key from an agents secrets bundle', value: FROM_SECRETS });
317
+ const choice = await io.select({ message: 'Auth', choices });
318
+ if (choice === KEEP)
319
+ return;
320
+ const provider = await io.input({
321
+ message: 'Provider',
322
+ default: original.provider,
323
+ validate: (v) => (v.trim() ? true : 'Provider is required'),
324
+ });
325
+ d.authProvider = provider;
326
+ if (choice === FROM_SECRETS)
327
+ await askKeySource(io, d, provider);
328
+ },
329
+ },
330
+ {
331
+ id: 'version',
332
+ decide: () => cap.version ? 'run' : { disabled: `host '${host}' self-updates — its version can't be pinned` },
333
+ async run(io, d, hooks) {
334
+ if (!editableFor(hooks).version)
335
+ return;
336
+ d.version = await io.input({
337
+ message: 'Host CLI version (blank to unpin)',
338
+ default: original.host.version ?? '',
339
+ });
340
+ },
341
+ },
342
+ {
343
+ id: 'fallback',
344
+ decide: () => (cap.fallback ? 'run' : 'skip'),
345
+ async run(io, d) {
346
+ d.fallbackModel = await io.input({
347
+ message: 'Fallback model (same-host rate-limit retry; blank for none)',
348
+ default: original.fallback_model ?? '',
349
+ });
350
+ },
351
+ },
352
+ {
353
+ id: 'description',
354
+ decide: () => 'run',
355
+ async run(io, d) {
356
+ d.description = await io.input({ message: 'Description', default: original.description ?? '' });
357
+ },
358
+ },
359
+ connectionTestStep(),
360
+ ];
361
+ }
362
+ /**
363
+ * The connection-test step (RUSH-2221 seam). Kept in both step lists so the id is
364
+ * a stable member of the sequence, but the actual test needs the assembled profile
365
+ * (which the caller builds after the wizard), so the run is performed separately by
366
+ * {@link runConnectionTest}. The step itself decides `'skip'` — a real no-op
367
+ * extension point that fakes nothing.
368
+ */
369
+ function connectionTestStep() {
370
+ return {
371
+ id: 'connectionTest',
372
+ decide: () => 'skip',
373
+ async run() {
374
+ /* no-op: superseded by runConnectionTest, kept so the step id is stable */
375
+ },
376
+ };
377
+ }
378
+ /**
379
+ * Run the connection-test hook against a finished draft, if one is wired
380
+ * (RUSH-2221). Returns `null` when no hook is present (no test performed) so the
381
+ * caller can distinguish "not tested" from "tested and passed". Separated from the
382
+ * step list because the test needs the assembled profile, which the caller builds.
383
+ */
384
+ export async function runConnectionTest(draft, hooks) {
385
+ if (!hooks.connectionTest)
386
+ return null;
387
+ return hooks.connectionTest(draft);
388
+ }
389
+ /**
390
+ * Production {@link WizardIO} over `@inquirer/prompts`, lazy-imported so the
391
+ * dependency loads only when a wizard actually runs. `note` prints to stderr so it
392
+ * never contaminates a `--json`/piped stdout.
393
+ */
394
+ export async function defaultWizardIO() {
395
+ const { select, input, password, confirm } = await import('@inquirer/prompts');
396
+ return {
397
+ select: (opts) => select(opts),
398
+ input: (opts) => input(opts),
399
+ password: (opts) => password({ message: opts.message, mask: true }),
400
+ confirm: (opts) => confirm(opts),
401
+ note: (message) => console.error(message),
402
+ };
403
+ }
@@ -13,6 +13,7 @@
13
13
  import type { Command } from 'commander';
14
14
  import { type AddProfileOptions } from './profiles.js';
15
15
  import { type Profile, type ForkProfileOptions } from '../lib/profiles.js';
16
+ import { type HarnessDraft } from './harness-wizard.js';
16
17
  /**
17
18
  * Print one custom harness. Shared by `agents harness view <name>` and by
18
19
  * `agents view <name>` — a custom harness resolves as an agent type there, so
@@ -76,4 +77,15 @@ export declare function forkNeedsWizard(source: string | undefined, name: string
76
77
  * `agents harness add <preset-name>` (no flags) still resolves via the preset
77
78
  * fallback instead of being routed into the wizard. */
78
79
  export declare function addNeedsWizard(name: string | undefined, opts: AddProfileOptions): boolean;
80
+ /**
81
+ * Map a finished edit-wizard draft onto {@link EditOptions}, keeping only the
82
+ * fields the user actually changed from the profile's current values. Unchanged
83
+ * accepts (the wizard pre-fills each prompt with the current value) drop out, so
84
+ * the resulting {@link buildEdit} touches nothing the user left alone — and the
85
+ * "no changes" case is detectable via {@link hasEditFlags}. Base-URL clearing is
86
+ * intentionally not expressed here: the flag path can't clear it either (an empty
87
+ * `--base-url` is a no-op in `forkProfile`), so the wizard matches that until a
88
+ * later subtask adds explicit clearing.
89
+ */
90
+ export declare function draftToEditOptions(draft: HarnessDraft, original: Profile): EditOptions;
79
91
  export declare function registerHarnessCommands(program: Command): void;