@phnx-labs/agents-cli 1.22.8 → 1.22.10
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.
- package/CHANGELOG.md +64 -4
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/exec.js +41 -9
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -1
- package/dist/commands/harness.js +316 -76
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/secrets.js +38 -39
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/ssh.js +30 -8
- package/dist/commands/teams.js +12 -1
- package/dist/commands/workflows.js +1 -1
- package/dist/index.js +9 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.js +18 -1
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/plugins.d.ts +14 -5
- package/dist/lib/plugins.js +29 -5
- package/dist/lib/redact.js +4 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources/workflows.d.ts +1 -1
- package/dist/lib/resources/workflows.js +22 -20
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +9 -0
- package/dist/lib/secrets/bundles.js +9 -23
- package/dist/lib/secrets/headless.d.ts +3 -15
- package/dist/lib/secrets/headless.js +5 -33
- package/dist/lib/secrets/index.d.ts +8 -0
- package/dist/lib/secrets/index.js +16 -36
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/share/config.js +5 -7
- package/dist/lib/startup/command-registry.js +15 -1
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/dist/lib/workflows.d.ts +19 -4
- package/dist/lib/workflows.js +81 -7
- package/package.json +1 -1
package/dist/commands/harness.js
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { addProfile, ensureProviderToken } from './profiles.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { addProfile, ensureProviderToken, applyFromSecrets } from './profiles.js';
|
|
15
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
16
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, editProfile, renameProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
|
|
17
|
+
import { listPresets, getPreset } from '../lib/profiles-presets.js';
|
|
18
|
+
import { listBundles } from '../lib/secrets/bundles.js';
|
|
17
19
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
18
20
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
19
21
|
function nativeModes(id) {
|
|
@@ -85,6 +87,215 @@ function authEnvKeyForHostOrThrow(host) {
|
|
|
85
87
|
}
|
|
86
88
|
return key;
|
|
87
89
|
}
|
|
90
|
+
/** Map `agents harness edit` flags onto {@link ForkProfileOptions} — the same
|
|
91
|
+
* override shape `editProfile`/`forkProfile` apply. `--version ''` (unpin) is
|
|
92
|
+
* handled by the caller ({@link buildEdit}), not here: `forkProfile`'s own
|
|
93
|
+
* ternary treats an empty string as "no override" and would otherwise inherit
|
|
94
|
+
* the source's version instead of clearing it. */
|
|
95
|
+
export function buildEditOverrides(opts) {
|
|
96
|
+
const overrides = {};
|
|
97
|
+
if (opts.model !== undefined)
|
|
98
|
+
overrides.model = opts.model;
|
|
99
|
+
if (opts.baseUrl !== undefined)
|
|
100
|
+
overrides.baseUrl = opts.baseUrl;
|
|
101
|
+
if (opts.authProvider !== undefined)
|
|
102
|
+
overrides.provider = opts.authProvider;
|
|
103
|
+
if (opts.version)
|
|
104
|
+
overrides.version = opts.version;
|
|
105
|
+
if (opts.description !== undefined)
|
|
106
|
+
overrides.description = opts.description;
|
|
107
|
+
return overrides;
|
|
108
|
+
}
|
|
109
|
+
/** True when at least one recognized edit flag was given. */
|
|
110
|
+
export function hasEditFlags(opts) {
|
|
111
|
+
return (opts.model !== undefined ||
|
|
112
|
+
opts.baseUrl !== undefined ||
|
|
113
|
+
opts.authProvider !== undefined ||
|
|
114
|
+
opts.version !== undefined ||
|
|
115
|
+
opts.description !== undefined ||
|
|
116
|
+
opts.fallbackModel !== undefined ||
|
|
117
|
+
opts.fromSecrets !== undefined);
|
|
118
|
+
}
|
|
119
|
+
const EDIT_FLAGS_HELP = 'No changes given. Available flags: --model, --base-url, --auth-provider, --version, --description, --fallback-model, --from-secrets.';
|
|
120
|
+
/**
|
|
121
|
+
* Build the edited harness for `agents harness edit <name>` — the profile-shape
|
|
122
|
+
* transform only; the caller applies `--from-secrets`/`--auth-provider` (async
|
|
123
|
+
* keychain side effects) and persists with `writeProfile`. Mirrors
|
|
124
|
+
* {@link buildFork}'s split between a pure builder and the action's IO.
|
|
125
|
+
*/
|
|
126
|
+
export function buildEdit(name, opts) {
|
|
127
|
+
if (!profileExists(name)) {
|
|
128
|
+
throw new Error(`Harness '${name}' not found. Create it first: agents harness add ${name} ...`);
|
|
129
|
+
}
|
|
130
|
+
if (!hasEditFlags(opts)) {
|
|
131
|
+
throw new Error(EDIT_FLAGS_HELP);
|
|
132
|
+
}
|
|
133
|
+
const source = readProfile(name);
|
|
134
|
+
const edited = editProfile(source, buildEditOverrides(opts));
|
|
135
|
+
if (opts.version === '')
|
|
136
|
+
delete edited.host.version;
|
|
137
|
+
if (opts.fallbackModel !== undefined) {
|
|
138
|
+
if (opts.fallbackModel === '')
|
|
139
|
+
delete edited.fallback_model;
|
|
140
|
+
else
|
|
141
|
+
edited.fallback_model = opts.fallbackModel;
|
|
142
|
+
}
|
|
143
|
+
return edited;
|
|
144
|
+
}
|
|
145
|
+
/** True when `agents harness fork` was given too little to proceed without the wizard. */
|
|
146
|
+
export function forkNeedsWizard(source, name, opts) {
|
|
147
|
+
if (!source || !name)
|
|
148
|
+
return true;
|
|
149
|
+
if (!profileExists(source) && resolveAgentName(source) && !opts.model)
|
|
150
|
+
return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
/** True when `agents harness add` was given too little to proceed without the wizard.
|
|
154
|
+
* Mirrors {@link addProfile}'s own error condition in ./profiles.js so a bare
|
|
155
|
+
* `agents harness add <preset-name>` (no flags) still resolves via the preset
|
|
156
|
+
* fallback instead of being routed into the wizard. */
|
|
157
|
+
export function addNeedsWizard(name, opts) {
|
|
158
|
+
if (!name)
|
|
159
|
+
return true;
|
|
160
|
+
if (opts.preset)
|
|
161
|
+
return false;
|
|
162
|
+
if (opts.host && opts.model)
|
|
163
|
+
return false;
|
|
164
|
+
if (opts.host || opts.model)
|
|
165
|
+
return true;
|
|
166
|
+
return !getPreset(name);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Shared build+persist flow for a fork — used by `agents harness fork`'s
|
|
170
|
+
* flag-driven path AND by the wizard (both for `fork` and, when it falls back
|
|
171
|
+
* to the wizard, `add`), so a wizard run and a hand-written `fork` call build an
|
|
172
|
+
* identical profile.
|
|
173
|
+
*/
|
|
174
|
+
async function runForkFlow(source, name, opts) {
|
|
175
|
+
validateProfileName(name);
|
|
176
|
+
if (profileExists(name) && !opts.force) {
|
|
177
|
+
throw new Error(`Harness '${name}' already exists. Use --force to overwrite.`);
|
|
178
|
+
}
|
|
179
|
+
// Build first so a bad source/flag combination fails before prompting for a
|
|
180
|
+
// key (or copying one from a bundle) the user would then have stored for
|
|
181
|
+
// nothing.
|
|
182
|
+
const forked = buildFork(source, name, opts);
|
|
183
|
+
if (opts.fromSecrets) {
|
|
184
|
+
// A fork's `auth` (when present) may just be inherited by reference from
|
|
185
|
+
// its source, not established for this harness itself -- never trust it
|
|
186
|
+
// as "safe to rotate" without an explicit --auth-provider.
|
|
187
|
+
await applyFromSecrets(forked, opts.fromSecrets, opts.authProvider, { allowInheritedAuth: false });
|
|
188
|
+
}
|
|
189
|
+
else if (opts.authProvider) {
|
|
190
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
191
|
+
}
|
|
192
|
+
writeProfile(forked);
|
|
193
|
+
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
194
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
195
|
+
}
|
|
196
|
+
/** The first `_MODEL`-suffixed env var in a preset's static env block, if any. */
|
|
197
|
+
function presetModel(preset) {
|
|
198
|
+
return Object.entries(preset.env).find(([k]) => k.endsWith('_MODEL'))?.[1];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Interactive `agents harness add`/`fork` wizard — runs when required info is
|
|
202
|
+
* missing and stdout is a TTY (see {@link forkNeedsWizard}, {@link addNeedsWizard}).
|
|
203
|
+
* Always resolves to the same `(source, name, opts)` shape {@link buildFork}
|
|
204
|
+
* accepts via {@link runForkFlow}, so a wizard run and a hand-written fork call
|
|
205
|
+
* build an identical profile.
|
|
206
|
+
*/
|
|
207
|
+
async function runHarnessWizard() {
|
|
208
|
+
const { select, input } = await import('@inquirer/prompts');
|
|
209
|
+
const customNames = listProfiles().map((p) => p.name);
|
|
210
|
+
const source = await select({
|
|
211
|
+
message: 'Fork from',
|
|
212
|
+
choices: [
|
|
213
|
+
...ALL_AGENT_IDS.map((id) => ({ name: `${AGENTS[id].name} ${chalk.gray('(native)')}`, value: id })),
|
|
214
|
+
...customNames.map((n) => ({ name: `${n} ${chalk.gray('(custom harness)')}`, value: n })),
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
const presets = listPresets();
|
|
218
|
+
const CUSTOM = '__custom__';
|
|
219
|
+
const presetChoice = await select({
|
|
220
|
+
message: 'Preset',
|
|
221
|
+
choices: [
|
|
222
|
+
...presets.map((p) => ({ name: `${p.name} ${chalk.gray(p.description.slice(0, 60))}`, value: p.name })),
|
|
223
|
+
{ name: 'Build custom (host + model + provider)', value: CUSTOM },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
let model;
|
|
227
|
+
let baseUrl;
|
|
228
|
+
let authProvider;
|
|
229
|
+
let defaultName = source;
|
|
230
|
+
if (presetChoice === CUSTOM) {
|
|
231
|
+
model = await input({ message: 'Model id' });
|
|
232
|
+
const NO_AUTH = '__none__';
|
|
233
|
+
const providers = [...new Set(presets.map((p) => p.provider))];
|
|
234
|
+
const providerChoice = await select({
|
|
235
|
+
message: 'Provider',
|
|
236
|
+
choices: [
|
|
237
|
+
...providers.map((p) => ({ name: p, value: p })),
|
|
238
|
+
{ name: 'no auth / host manages its own login', value: NO_AUTH },
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
authProvider = providerChoice === NO_AUTH ? undefined : providerChoice;
|
|
242
|
+
const baseUrlInput = await input({ message: 'Base URL (optional)', default: '' });
|
|
243
|
+
baseUrl = baseUrlInput || undefined;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const preset = getPreset(presetChoice);
|
|
247
|
+
model = presetModel(preset);
|
|
248
|
+
baseUrl = preset.env.ANTHROPIC_BASE_URL || preset.env.OPENAI_BASE_URL;
|
|
249
|
+
authProvider = preset.authOptional ? undefined : preset.provider;
|
|
250
|
+
// Pre-fill with the preset's own name (e.g. 'deepseek'), not a model
|
|
251
|
+
// detail, so users aren't nudged toward baking one into the identity name.
|
|
252
|
+
defaultName = preset.name;
|
|
253
|
+
}
|
|
254
|
+
const name = await input({
|
|
255
|
+
message: 'Harness name',
|
|
256
|
+
default: defaultName,
|
|
257
|
+
validate: (v) => {
|
|
258
|
+
try {
|
|
259
|
+
validateProfileName(v);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return err.message;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
const opts = { model, baseUrl, authProvider };
|
|
268
|
+
if (authProvider) {
|
|
269
|
+
const bundles = listBundles();
|
|
270
|
+
const TYPE_NOW = 'type';
|
|
271
|
+
const FROM_SECRETS = 'secrets';
|
|
272
|
+
const keySource = bundles.length > 0
|
|
273
|
+
? await select({
|
|
274
|
+
message: `How should '${authProvider}' get its key?`,
|
|
275
|
+
choices: [
|
|
276
|
+
{ name: 'Type a key now', value: TYPE_NOW },
|
|
277
|
+
{ name: 'Use an existing agents secrets bundle', value: FROM_SECRETS },
|
|
278
|
+
],
|
|
279
|
+
})
|
|
280
|
+
: TYPE_NOW;
|
|
281
|
+
if (keySource === FROM_SECRETS) {
|
|
282
|
+
const bundleName = await select({
|
|
283
|
+
message: 'Bundle',
|
|
284
|
+
choices: bundles.map((b) => ({
|
|
285
|
+
name: b.description ? `${b.name} ${chalk.gray(b.description)}` : b.name,
|
|
286
|
+
value: b.name,
|
|
287
|
+
})),
|
|
288
|
+
});
|
|
289
|
+
const bundle = bundles.find((b) => b.name === bundleName);
|
|
290
|
+
const keys = Object.keys(bundle.vars);
|
|
291
|
+
const key = keys.length === 1
|
|
292
|
+
? keys[0]
|
|
293
|
+
: await select({ message: 'Key', choices: keys.map((k) => ({ name: k, value: k })) });
|
|
294
|
+
opts.fromSecrets = `${bundleName}:${key}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { source, name, opts };
|
|
298
|
+
}
|
|
88
299
|
export function registerHarnessCommands(program) {
|
|
89
300
|
const cmd = program
|
|
90
301
|
.command('harness')
|
|
@@ -110,6 +321,13 @@ Examples:
|
|
|
110
321
|
# Per-run model override still wins
|
|
111
322
|
agents run spark --model opencode/big-pickle "quick pass"
|
|
112
323
|
|
|
324
|
+
# Edit a harness in place, or give it a new name
|
|
325
|
+
agents harness edit deepseek --fallback-model deepseek/deepseek-chat-v3
|
|
326
|
+
agents harness rename deepseek deepseek-classic
|
|
327
|
+
|
|
328
|
+
# No args, in an interactive terminal: a wizard walks you through host, model, provider, key
|
|
329
|
+
agents harness add
|
|
330
|
+
|
|
113
331
|
# See custom harnesses, addable presets, and native harnesses
|
|
114
332
|
agents harness list
|
|
115
333
|
|
|
@@ -119,18 +337,27 @@ Examples:
|
|
|
119
337
|
# Custom harnesses are just profiles — this also works via 'agents profiles'
|
|
120
338
|
`);
|
|
121
339
|
cmd
|
|
122
|
-
.command('add
|
|
123
|
-
.description('Create a custom harness from a host + model (or apply a built-in preset).')
|
|
340
|
+
.command('add [name]')
|
|
341
|
+
.description('Create a custom harness from a host + model (or apply a built-in preset). Omit flags in a terminal for the interactive wizard.')
|
|
124
342
|
.option('--host <agent>', 'Host CLI to run under (opencode, claude, codex, grok, antigravity, ...) — pair with --model')
|
|
125
343
|
.option('--model <id>', 'Model id to pin on the host (e.g., meta/muse-spark-1.1) — pair with --host')
|
|
126
344
|
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
127
345
|
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider (for private endpoints)')
|
|
128
346
|
.option('--preset <preset>', 'Apply a built-in preset instead of --host/--model')
|
|
129
347
|
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
348
|
+
.option('--from-secrets <bundle>[:<key>]', "Copy a value from an agents secrets bundle into this harness's own keychain item, once (not a live link)")
|
|
130
349
|
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
131
350
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
132
351
|
.action(async (name, opts) => {
|
|
133
352
|
try {
|
|
353
|
+
if (addNeedsWizard(name, opts)) {
|
|
354
|
+
if (!isInteractiveTerminal()) {
|
|
355
|
+
throw new Error("'agents harness add' needs --preset or --host + --model (or a name and an interactive terminal for the wizard).");
|
|
356
|
+
}
|
|
357
|
+
const wiz = await runHarnessWizard();
|
|
358
|
+
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
134
361
|
await addProfile(name, opts, 'Harness');
|
|
135
362
|
}
|
|
136
363
|
catch (err) {
|
|
@@ -139,13 +366,14 @@ Examples:
|
|
|
139
366
|
}
|
|
140
367
|
});
|
|
141
368
|
cmd
|
|
142
|
-
.command('fork
|
|
143
|
-
.description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness.')
|
|
369
|
+
.command('fork [source] [name]')
|
|
370
|
+
.description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness. Omit args in a terminal for the interactive wizard.')
|
|
144
371
|
.option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
|
|
145
372
|
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
146
373
|
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
|
|
147
374
|
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
148
375
|
.option('--description <text>', 'One-line description')
|
|
376
|
+
.option('--from-secrets <bundle>[:<key>]', "Copy a value from an agents secrets bundle into this harness's own keychain item, once (not a live link)")
|
|
149
377
|
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
150
378
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
151
379
|
.addHelpText('after', `
|
|
@@ -158,21 +386,92 @@ Examples:
|
|
|
158
386
|
|
|
159
387
|
# Copy an existing harness and swap only the model
|
|
160
388
|
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
389
|
+
|
|
390
|
+
# Copy a key out of an existing secrets bundle instead of typing it
|
|
391
|
+
agents harness fork claude corp --model gpt-x --auth-provider corp --from-secrets prod:OPENROUTER_KEY
|
|
392
|
+
|
|
393
|
+
# No args, in an interactive terminal: walks through source, preset/model, name, key
|
|
394
|
+
agents harness fork
|
|
161
395
|
`)
|
|
162
396
|
.action(async (source, name, opts) => {
|
|
163
397
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
398
|
+
if (forkNeedsWizard(source, name, opts)) {
|
|
399
|
+
if (!isInteractiveTerminal()) {
|
|
400
|
+
throw new Error("'agents harness fork' needs <source> and <name> (or an interactive terminal for the wizard).");
|
|
401
|
+
}
|
|
402
|
+
const wiz = await runHarnessWizard();
|
|
403
|
+
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
await runForkFlow(source, name, opts);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error(chalk.red(err.message));
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
cmd
|
|
414
|
+
.command('edit <name>')
|
|
415
|
+
.description('Edit an existing custom harness in place — model, endpoint, auth, version, description, fallback.')
|
|
416
|
+
.option('--model <id>', 'Swap the pinned model')
|
|
417
|
+
.option('--base-url <url>', 'Swap the custom endpoint base URL')
|
|
418
|
+
.option('--auth-provider <provider>', 'Repoint auth at a different provider (keychain-backed)')
|
|
419
|
+
.option('--version <version>', 'Re-pin the host CLI version (pass an empty string to unpin)')
|
|
420
|
+
.option('--description <text>', 'Update the one-line description')
|
|
421
|
+
.option('--fallback-model <id>', 'Secondary model retried on the same host on a rate limit (pass an empty string to clear it)')
|
|
422
|
+
.option('--from-secrets <bundle>[:<key>]', "Copy a value from an agents secrets bundle into this harness's own keychain item, once (not a live link)")
|
|
423
|
+
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
424
|
+
.addHelpText('after', `
|
|
425
|
+
Examples:
|
|
426
|
+
# Swap the pinned model
|
|
427
|
+
agents harness edit deepseek --model deepseek/deepseek-v3.2
|
|
428
|
+
|
|
429
|
+
# Repoint auth at a different provider and re-enter the key
|
|
430
|
+
agents harness edit corp --auth-provider corp2
|
|
431
|
+
|
|
432
|
+
# Unpin the host CLI version
|
|
433
|
+
agents harness edit spark --version ""
|
|
434
|
+
|
|
435
|
+
# Add a same-host fallback model for rate-limit retries
|
|
436
|
+
agents harness edit deepseek --fallback-model deepseek/deepseek-chat-v3
|
|
437
|
+
|
|
438
|
+
# Copy a key out of an existing secrets bundle instead of typing it
|
|
439
|
+
agents harness edit corp --from-secrets prod:OPENROUTER_KEY
|
|
440
|
+
`)
|
|
441
|
+
.action(async (name, opts) => {
|
|
442
|
+
try {
|
|
443
|
+
const edited = buildEdit(name, opts);
|
|
444
|
+
if (opts.fromSecrets) {
|
|
445
|
+
await applyFromSecrets(edited, opts.fromSecrets, opts.authProvider);
|
|
167
446
|
}
|
|
168
|
-
|
|
169
|
-
// for a key the user would then have stored for nothing.
|
|
170
|
-
const forked = buildFork(source, name, opts);
|
|
171
|
-
if (opts.authProvider)
|
|
447
|
+
else if (opts.authProvider) {
|
|
172
448
|
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
console.log(chalk.
|
|
449
|
+
}
|
|
450
|
+
writeProfile(edited);
|
|
451
|
+
console.log(chalk.green(`Harness '${name}' updated.`));
|
|
452
|
+
console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
console.error(chalk.red(err.message));
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
cmd
|
|
460
|
+
.command('rename <old-name> <new-name>')
|
|
461
|
+
.description('Rename a custom harness (updates forkedFrom lineage on any harness forked from it). Errors on a name collision.')
|
|
462
|
+
.addHelpText('after', `
|
|
463
|
+
Examples:
|
|
464
|
+
# Rename 'spark' to 'muse'
|
|
465
|
+
agents harness rename spark muse
|
|
466
|
+
|
|
467
|
+
# Rename then run under the new name
|
|
468
|
+
agents harness rename deepseek ds && agents run ds "hello"
|
|
469
|
+
`)
|
|
470
|
+
.action((oldName, newName) => {
|
|
471
|
+
try {
|
|
472
|
+
renameProfile(oldName, newName);
|
|
473
|
+
console.log(chalk.green(`Harness '${oldName}' renamed to '${newName}'.`));
|
|
474
|
+
console.log(chalk.gray(`Try: agents run ${newName} "hello"`));
|
|
176
475
|
}
|
|
177
476
|
catch (err) {
|
|
178
477
|
console.error(chalk.red(err.message));
|
|
@@ -246,63 +545,4 @@ Examples:
|
|
|
246
545
|
}
|
|
247
546
|
console.log(chalk.green(`Harness '${name}' removed.`));
|
|
248
547
|
});
|
|
249
|
-
cmd
|
|
250
|
-
.command('edit <name>')
|
|
251
|
-
.description('Edit a custom harness in place (model, endpoint, host version, description).')
|
|
252
|
-
// Flags mirror ForkProfileOptions exactly — `editProfile` reuses
|
|
253
|
-
// `forkProfile`'s override logic, so anything it does not accept would be a
|
|
254
|
-
// flag that parses and does nothing. There is deliberately no --label: the
|
|
255
|
-
// header `agents view` prints is derived from the harness name.
|
|
256
|
-
.option('--model <id>', 'Swap the pinned model (e.g., meta/muse-spark-2.0)')
|
|
257
|
-
.option('--base-url <url>', 'Set or replace the endpoint base URL (claude/codex hosts)')
|
|
258
|
-
.option('--version <ver>', 'Re-pin the host CLI version. Empty string unpins (tracks latest).')
|
|
259
|
-
.option('--description <text>', 'Set the one-line description')
|
|
260
|
-
.addHelpText('after', `
|
|
261
|
-
Examples:
|
|
262
|
-
# Upgrade the pinned model
|
|
263
|
-
agents harness edit spark --model meta/muse-spark-2.0
|
|
264
|
-
|
|
265
|
-
# Point to a new private endpoint
|
|
266
|
-
agents harness edit corp --base-url https://gw.corp/v2
|
|
267
|
-
|
|
268
|
-
# Track the host's latest instead of a pinned version
|
|
269
|
-
agents harness edit spark --version ""
|
|
270
|
-
|
|
271
|
-
# The display name is derived from the harness name, so to change it, rename:
|
|
272
|
-
agents harness rename spark muse-spark
|
|
273
|
-
`)
|
|
274
|
-
.action((name, opts) => {
|
|
275
|
-
try {
|
|
276
|
-
const edited = editProfile(readProfile(name), opts);
|
|
277
|
-
writeProfile(edited);
|
|
278
|
-
console.log(chalk.green(`Harness '${name}' updated.`));
|
|
279
|
-
console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
console.error(chalk.red(err.message));
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
cmd
|
|
287
|
-
.command('rename <name> <new-name>')
|
|
288
|
-
.description('Rename a custom harness — updates the file name and its internal name field.')
|
|
289
|
-
.addHelpText('after', `
|
|
290
|
-
Examples:
|
|
291
|
-
# Rename 'spark' to 'muse'
|
|
292
|
-
agents harness rename spark muse
|
|
293
|
-
|
|
294
|
-
# Rename then run under the new name
|
|
295
|
-
agents harness rename deepseek ds && agents run ds "hello"
|
|
296
|
-
`)
|
|
297
|
-
.action((name, newName) => {
|
|
298
|
-
try {
|
|
299
|
-
renameProfile(name, newName);
|
|
300
|
-
console.log(chalk.green(`Harness '${name}' renamed to '${newName}'.`));
|
|
301
|
-
console.log(chalk.gray(`Run: agents run ${newName} "hello"`));
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
console.error(chalk.red(err.message));
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
548
|
}
|
|
@@ -28,7 +28,49 @@ export interface AddProfileOptions {
|
|
|
28
28
|
version?: string;
|
|
29
29
|
keyStdin?: boolean;
|
|
30
30
|
force?: boolean;
|
|
31
|
+
/** `<bundle>` or `<bundle>:<key>` — see {@link applyFromSecrets}. */
|
|
32
|
+
fromSecrets?: string;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Copy a value from an `agents secrets` bundle into a profile's own keychain
|
|
36
|
+
* item — a one-time copy, not a live link. `spec` is `<bundle>` or
|
|
37
|
+
* `<bundle>:<key>`; the key is required only when the bundle has more than one.
|
|
38
|
+
*
|
|
39
|
+
* Reads the bundle value via {@link getKeychainToken} — this pops Touch ID on
|
|
40
|
+
* macOS once, for that bundle-namespaced item (`agents-cli.secrets.*`, gated by
|
|
41
|
+
* `keychainItemRequiresUserPresence` in `../lib/secrets/index.ts`) — then writes
|
|
42
|
+
* it to `keychainItemName(provider)`, a plain `agents-cli.<provider>.token`
|
|
43
|
+
* item. That item matches neither the `agents-cli.secrets.` nor
|
|
44
|
+
* `agents-cli.bundles.` prefix, so every later read of the harness's own key is
|
|
45
|
+
* silent — no repeat Touch ID prompt.
|
|
46
|
+
*
|
|
47
|
+
* Provider precedence: an explicit `--auth-provider` on the same call always
|
|
48
|
+
* wins (freshest intent); otherwise, only when the profile already has a real
|
|
49
|
+
* auth binding (`profile.auth` is set), its own `provider` (so editing an
|
|
50
|
+
* already-provisioned harness rotates its existing key without repeating
|
|
51
|
+
* `--auth-provider`); otherwise the bundle's own name. The `profile.auth`
|
|
52
|
+
* gate matters because `profileFromHostModel`/`forkProfile` default
|
|
53
|
+
* `profile.provider` to the *host* id (e.g. `claude`) even when no auth is
|
|
54
|
+
* attached yet — trusting that default here would silently overwrite the
|
|
55
|
+
* host's own keychain item (`agents-cli.claude.token`) on a bare `--host
|
|
56
|
+
* --model --from-secrets` add with no `--auth-provider`.
|
|
57
|
+
*
|
|
58
|
+
* Attaches `profile.auth` when the profile has none yet (a bare `--host
|
|
59
|
+
* --model` harness, or a native-host fork with no prior auth binding).
|
|
60
|
+
*
|
|
61
|
+
* `allowInheritedAuth` (default `true`) gates the "reuse `profile.auth`'s own
|
|
62
|
+
* provider" branch above. It must be `false` when `profile` came from
|
|
63
|
+
* **forking** an already-provisioned harness: `forkProfile` copies `auth` by
|
|
64
|
+
* reference from the source when no `--auth-provider` override is given, so
|
|
65
|
+
* `profile.auth` being set there means "the SOURCE harness's binding", not
|
|
66
|
+
* "this harness's own" — trusting it would silently overwrite the source's
|
|
67
|
+
* shared keychain item. Editing a harness's own already-established auth is
|
|
68
|
+
* the one case where reuse is genuinely correct, so callers on that path keep
|
|
69
|
+
* the default.
|
|
70
|
+
*/
|
|
71
|
+
export declare function applyFromSecrets(profile: Profile, spec: string, explicitAuthProvider?: string, opts?: {
|
|
72
|
+
allowInheritedAuth?: boolean;
|
|
73
|
+
}): Promise<void>;
|
|
32
74
|
/**
|
|
33
75
|
* Create a profile ("custom harness"). Two paths:
|
|
34
76
|
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
@@ -11,7 +11,9 @@ import { readStdinSync } from '../lib/format.js';
|
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
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
|
-
import { hasKeychainToken, keychainItemName, setKeychainToken, deleteKeychainToken, } from '../lib/secrets/profiles.js';
|
|
14
|
+
import { hasKeychainToken, keychainItemName, setKeychainToken, deleteKeychainToken, getKeychainToken, } from '../lib/secrets/profiles.js';
|
|
15
|
+
import { parseBundleValue, secretsKeychainItem } from '../lib/secrets/index.js';
|
|
16
|
+
import { readBundle } from '../lib/secrets/bundles.js';
|
|
15
17
|
import { isInteractiveTerminal } from './utils.js';
|
|
16
18
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
17
19
|
import { getActiveResourceProfileName, getResourceProfilePreset, listResourceProfileNames, setActiveResourceProfile, upsertResourceProfilePreset, validateResourceProfileName, } from '../lib/resource-profiles.js';
|
|
@@ -66,6 +68,87 @@ export async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
|
66
68
|
setKeychainToken(item, token);
|
|
67
69
|
console.log(chalk.green(`Stored in keychain: ${item}`));
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Copy a value from an `agents secrets` bundle into a profile's own keychain
|
|
73
|
+
* item — a one-time copy, not a live link. `spec` is `<bundle>` or
|
|
74
|
+
* `<bundle>:<key>`; the key is required only when the bundle has more than one.
|
|
75
|
+
*
|
|
76
|
+
* Reads the bundle value via {@link getKeychainToken} — this pops Touch ID on
|
|
77
|
+
* macOS once, for that bundle-namespaced item (`agents-cli.secrets.*`, gated by
|
|
78
|
+
* `keychainItemRequiresUserPresence` in `../lib/secrets/index.ts`) — then writes
|
|
79
|
+
* it to `keychainItemName(provider)`, a plain `agents-cli.<provider>.token`
|
|
80
|
+
* item. That item matches neither the `agents-cli.secrets.` nor
|
|
81
|
+
* `agents-cli.bundles.` prefix, so every later read of the harness's own key is
|
|
82
|
+
* silent — no repeat Touch ID prompt.
|
|
83
|
+
*
|
|
84
|
+
* Provider precedence: an explicit `--auth-provider` on the same call always
|
|
85
|
+
* wins (freshest intent); otherwise, only when the profile already has a real
|
|
86
|
+
* auth binding (`profile.auth` is set), its own `provider` (so editing an
|
|
87
|
+
* already-provisioned harness rotates its existing key without repeating
|
|
88
|
+
* `--auth-provider`); otherwise the bundle's own name. The `profile.auth`
|
|
89
|
+
* gate matters because `profileFromHostModel`/`forkProfile` default
|
|
90
|
+
* `profile.provider` to the *host* id (e.g. `claude`) even when no auth is
|
|
91
|
+
* attached yet — trusting that default here would silently overwrite the
|
|
92
|
+
* host's own keychain item (`agents-cli.claude.token`) on a bare `--host
|
|
93
|
+
* --model --from-secrets` add with no `--auth-provider`.
|
|
94
|
+
*
|
|
95
|
+
* Attaches `profile.auth` when the profile has none yet (a bare `--host
|
|
96
|
+
* --model` harness, or a native-host fork with no prior auth binding).
|
|
97
|
+
*
|
|
98
|
+
* `allowInheritedAuth` (default `true`) gates the "reuse `profile.auth`'s own
|
|
99
|
+
* provider" branch above. It must be `false` when `profile` came from
|
|
100
|
+
* **forking** an already-provisioned harness: `forkProfile` copies `auth` by
|
|
101
|
+
* reference from the source when no `--auth-provider` override is given, so
|
|
102
|
+
* `profile.auth` being set there means "the SOURCE harness's binding", not
|
|
103
|
+
* "this harness's own" — trusting it would silently overwrite the source's
|
|
104
|
+
* shared keychain item. Editing a harness's own already-established auth is
|
|
105
|
+
* the one case where reuse is genuinely correct, so callers on that path keep
|
|
106
|
+
* the default.
|
|
107
|
+
*/
|
|
108
|
+
export async function applyFromSecrets(profile, spec, explicitAuthProvider, opts) {
|
|
109
|
+
const sep = spec.indexOf(':');
|
|
110
|
+
const bundleName = sep === -1 ? spec : spec.slice(0, sep);
|
|
111
|
+
const requestedKey = sep === -1 ? undefined : spec.slice(sep + 1);
|
|
112
|
+
const bundle = readBundle(bundleName);
|
|
113
|
+
const keys = Object.keys(bundle.vars);
|
|
114
|
+
const key = requestedKey ?? (keys.length === 1 ? keys[0] : undefined);
|
|
115
|
+
if (!key) {
|
|
116
|
+
throw new Error(keys.length === 0
|
|
117
|
+
? `Bundle '${bundleName}' has no keys.`
|
|
118
|
+
: `Bundle '${bundleName}' has ${keys.length} keys (${keys.join(', ')}); pick one with --from-secrets ${bundleName}:<key>.`);
|
|
119
|
+
}
|
|
120
|
+
if (!(key in bundle.vars)) {
|
|
121
|
+
throw new Error(`Bundle '${bundleName}' has no key '${key}'. Available: ${keys.join(', ') || '(none)'}.`);
|
|
122
|
+
}
|
|
123
|
+
const parsed = parseBundleValue(bundle.vars[key]);
|
|
124
|
+
let value;
|
|
125
|
+
if ('literal' in parsed) {
|
|
126
|
+
value = parsed.literal;
|
|
127
|
+
}
|
|
128
|
+
else if (parsed.ref.provider === 'keychain') {
|
|
129
|
+
value = getKeychainToken(secretsKeychainItem(bundle.name, parsed.ref.value));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw new Error(`Bundle '${bundleName}' key '${key}' is a '${parsed.ref.provider}:' reference, not a keychain-backed secret — --from-secrets only copies keychain-backed values.`);
|
|
133
|
+
}
|
|
134
|
+
if (profile.auth && !explicitAuthProvider && opts?.allowInheritedAuth === false) {
|
|
135
|
+
throw new Error(`Harness '${profile.name}' inherited its auth binding from the harness it was forked from ` +
|
|
136
|
+
`(provider '${profile.provider}') — pass --auth-provider <name> explicitly with --from-secrets ` +
|
|
137
|
+
`on a fork, so it never silently overwrites the source harness's shared keychain item.`);
|
|
138
|
+
}
|
|
139
|
+
const provider = explicitAuthProvider || (profile.auth ? profile.provider : undefined) || bundleName;
|
|
140
|
+
const item = keychainItemName(provider);
|
|
141
|
+
setKeychainToken(item, value);
|
|
142
|
+
if (!profile.auth) {
|
|
143
|
+
const envVar = authEnvKeyForHost(profile.host.agent);
|
|
144
|
+
if (!envVar) {
|
|
145
|
+
throw new Error(`Host '${profile.host.agent}' has no known auth env var; --from-secrets cannot attach auth to this harness.`);
|
|
146
|
+
}
|
|
147
|
+
profile.provider = provider;
|
|
148
|
+
profile.auth = { envVar, keychainItem: item };
|
|
149
|
+
profile.authOptional = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
69
152
|
/**
|
|
70
153
|
* Create a profile ("custom harness"). Two paths:
|
|
71
154
|
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
@@ -98,7 +181,8 @@ export async function addProfile(name, opts, label = 'Profile') {
|
|
|
98
181
|
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
182
|
}
|
|
100
183
|
authEnvVar = key;
|
|
101
|
-
|
|
184
|
+
if (!opts.fromSecrets)
|
|
185
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
102
186
|
}
|
|
103
187
|
const profile = profileFromHostModel(name, host, opts.model, {
|
|
104
188
|
version: opts.version,
|
|
@@ -106,6 +190,8 @@ export async function addProfile(name, opts, label = 'Profile') {
|
|
|
106
190
|
provider: opts.authProvider,
|
|
107
191
|
authEnvVar,
|
|
108
192
|
});
|
|
193
|
+
if (opts.fromSecrets)
|
|
194
|
+
await applyFromSecrets(profile, opts.fromSecrets, opts.authProvider);
|
|
109
195
|
writeProfile(profile);
|
|
110
196
|
console.log(chalk.green(`${label} '${name}' added — ${host} + ${opts.model}.`));
|
|
111
197
|
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
@@ -118,10 +204,12 @@ export async function addProfile(name, opts, label = 'Profile') {
|
|
|
118
204
|
throw new Error(`No preset '${presetName}'.\nAvailable presets: ${listPresets().map((p) => p.name).join(', ')}\n` +
|
|
119
205
|
'Or build a custom harness: --host <agent> --model <id>.');
|
|
120
206
|
}
|
|
121
|
-
if (!preset.authOptional) {
|
|
207
|
+
if (!opts.fromSecrets && !preset.authOptional) {
|
|
122
208
|
await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
|
|
123
209
|
}
|
|
124
210
|
const profile = profileFromPreset(name, preset, opts.version);
|
|
211
|
+
if (opts.fromSecrets)
|
|
212
|
+
await applyFromSecrets(profile, opts.fromSecrets, opts.authProvider);
|
|
125
213
|
writeProfile(profile);
|
|
126
214
|
console.log(chalk.green(`${label} '${name}' added.`));
|
|
127
215
|
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|