@phnx-labs/agents-cli 1.22.7 → 1.22.9
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 +97 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +89 -21
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -2
- package/dist/commands/harness.js +316 -20
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- 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/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +14 -2
- package/dist/commands/view.d.ts +7 -0
- package/dist/commands/view.js +1 -1
- package/dist/index.js +11 -1
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -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/device-config.js +8 -0
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +41 -3
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +2 -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/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +31 -9
- package/dist/lib/profiles.js +83 -13
- package/dist/lib/redact.js +4 -0
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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/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/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +17 -1
- 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/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) {
|
|
@@ -58,7 +60,6 @@ export function buildFork(source, name, opts) {
|
|
|
58
60
|
baseUrl: opts.baseUrl,
|
|
59
61
|
provider: opts.authProvider,
|
|
60
62
|
version: opts.version,
|
|
61
|
-
label: opts.label,
|
|
62
63
|
description: opts.description,
|
|
63
64
|
});
|
|
64
65
|
}
|
|
@@ -75,7 +76,6 @@ export function buildFork(source, name, opts) {
|
|
|
75
76
|
baseUrl: opts.baseUrl,
|
|
76
77
|
provider: opts.authProvider,
|
|
77
78
|
authEnvVar: opts.authProvider ? authEnvKeyForHostOrThrow(host) : undefined,
|
|
78
|
-
label: opts.label,
|
|
79
79
|
description: opts.description ?? `Forked from ${host}: ${opts.model}`,
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -87,6 +87,215 @@ function authEnvKeyForHostOrThrow(host) {
|
|
|
87
87
|
}
|
|
88
88
|
return key;
|
|
89
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
|
+
}
|
|
90
299
|
export function registerHarnessCommands(program) {
|
|
91
300
|
const cmd = program
|
|
92
301
|
.command('harness')
|
|
@@ -112,6 +321,13 @@ Examples:
|
|
|
112
321
|
# Per-run model override still wins
|
|
113
322
|
agents run spark --model opencode/big-pickle "quick pass"
|
|
114
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
|
+
|
|
115
331
|
# See custom harnesses, addable presets, and native harnesses
|
|
116
332
|
agents harness list
|
|
117
333
|
|
|
@@ -121,18 +337,27 @@ Examples:
|
|
|
121
337
|
# Custom harnesses are just profiles — this also works via 'agents profiles'
|
|
122
338
|
`);
|
|
123
339
|
cmd
|
|
124
|
-
.command('add
|
|
125
|
-
.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.')
|
|
126
342
|
.option('--host <agent>', 'Host CLI to run under (opencode, claude, codex, grok, antigravity, ...) — pair with --model')
|
|
127
343
|
.option('--model <id>', 'Model id to pin on the host (e.g., meta/muse-spark-1.1) — pair with --host')
|
|
128
344
|
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
129
345
|
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider (for private endpoints)')
|
|
130
346
|
.option('--preset <preset>', 'Apply a built-in preset instead of --host/--model')
|
|
131
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)")
|
|
132
349
|
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
133
350
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
134
351
|
.action(async (name, opts) => {
|
|
135
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
|
+
}
|
|
136
361
|
await addProfile(name, opts, 'Harness');
|
|
137
362
|
}
|
|
138
363
|
catch (err) {
|
|
@@ -141,14 +366,14 @@ Examples:
|
|
|
141
366
|
}
|
|
142
367
|
});
|
|
143
368
|
cmd
|
|
144
|
-
.command('fork
|
|
145
|
-
.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.')
|
|
146
371
|
.option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
|
|
147
372
|
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
148
373
|
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
|
|
149
374
|
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
150
|
-
.option('--label <text>', 'Human-facing name shown by `agents view` (defaults to <name>)')
|
|
151
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)")
|
|
152
377
|
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
153
378
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
154
379
|
.addHelpText('after', `
|
|
@@ -161,21 +386,92 @@ Examples:
|
|
|
161
386
|
|
|
162
387
|
# Copy an existing harness and swap only the model
|
|
163
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
|
|
164
395
|
`)
|
|
165
396
|
.action(async (source, name, opts) => {
|
|
166
397
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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;
|
|
170
405
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
446
|
+
}
|
|
447
|
+
else if (opts.authProvider) {
|
|
175
448
|
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
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"`));
|
|
179
475
|
}
|
|
180
476
|
catch (err) {
|
|
181
477
|
console.error(chalk.red(err.message));
|
|
@@ -315,9 +315,9 @@ export function registerMonitorsCommands(program) {
|
|
|
315
315
|
.option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
|
|
316
316
|
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
317
317
|
// PLACEMENT / hygiene
|
|
318
|
-
.option('--device <name>', 'OWNER
|
|
318
|
+
.option('--device <name>', 'OWNER (not body placement) — the single machine that evaluates + fires (exactly-once). See docs/00-concepts.md#placement.')
|
|
319
319
|
.option('--devices <list>', 'Allowlist (comma-separated): each device fires independently')
|
|
320
|
-
.option('--run-on <host>', '
|
|
320
|
+
.option('--run-on <host>', 'BODY placement — execute the ACTION on this machine over SSH (same idea as run --where device:<host>)')
|
|
321
321
|
.option('--rate-limit <spec>', 'Auto-pause if it fires more than N/<interval> (e.g. 5/1m)')
|
|
322
322
|
.option('--disabled', 'Create the monitor paused (enable later with resume)')
|
|
323
323
|
.action(async (nameOrPath, options) => {
|
|
@@ -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"`));
|
|
@@ -628,8 +628,8 @@ export function registerRoutinesCommands(program) {
|
|
|
628
628
|
.option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
|
|
629
629
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
630
630
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
631
|
-
.option('--run-on <name>', '
|
|
632
|
-
.option('--placement <strategy>', `
|
|
631
|
+
.option('--run-on <name>', 'BODY placement: execute the job body on this machine over SSH (registered host, device, capability tag, or user@host). Sets hostStrategy=host. Not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given. Same model as agents run --where device:<name> (docs/00-concepts.md#placement).')
|
|
632
|
+
.option('--placement <strategy>', `BODY placement strategy: ${HOST_STRATEGIES.join('|')} (default: local, or host when --run-on is set). Maps to the shared Placement model (local|device|fleet|cloud). Not the same as --host (which manages routines on a remote machine).`)
|
|
633
633
|
.option('--run-cwd <dir>', 'Working directory on the --run-on host (--remote-cwd is taken by the remote-management passthrough)')
|
|
634
634
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
635
635
|
.option('--on <source:event>', 'Webhook trigger instead of/in addition to a schedule: github:pull_request or linear:Issue')
|
|
@@ -36,6 +36,8 @@ function disabledReason(candidate) {
|
|
|
36
36
|
return undefined;
|
|
37
37
|
if (readiness.reason === 'signed_out')
|
|
38
38
|
return 'logged out';
|
|
39
|
+
if (readiness.reason === 'revoked')
|
|
40
|
+
return 'needs re-login';
|
|
39
41
|
if (readiness.reason === 'out_of_credits')
|
|
40
42
|
return 'out of credits';
|
|
41
43
|
const windows = candidate.usageSnapshot?.windows ?? [];
|
|
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { truncate, humanDuration } from '../lib/format.js';
|
|
12
12
|
import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
|
|
13
13
|
import { safeTeamText } from '../lib/session/team-filter.js';
|
|
14
|
-
import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
|
|
14
|
+
import { cleanSessionPrompt, extractSessionTopic, isSyntheticUserMessage } from '../lib/session/prompt.js';
|
|
15
15
|
import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
|
|
16
16
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
17
17
|
import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
|
|
@@ -383,7 +383,7 @@ function formatCompactPreview(events, session) {
|
|
|
383
383
|
const knownToolUsage = session.usedBrowser !== undefined;
|
|
384
384
|
for (const event of events) {
|
|
385
385
|
if (event.type === 'message') {
|
|
386
|
-
if (event.role === 'user' && !firstUser && event.content) {
|
|
386
|
+
if (event.role === 'user' && !event._synthetic && !firstUser && event.content) {
|
|
387
387
|
if (!SYSTEM_MESSAGE_PATTERNS.some(p => p.test(event.content))) {
|
|
388
388
|
firstUser = event.content;
|
|
389
389
|
}
|
|
@@ -443,7 +443,7 @@ function formatCompactPreview(events, session) {
|
|
|
443
443
|
lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(first.trim(), termWidth - 12)));
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
-
else if (session.topic) {
|
|
446
|
+
else if (session.topic && !isSyntheticUserMessage(session.topic)) {
|
|
447
447
|
lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(session.topic.trim(), termWidth - 12)));
|
|
448
448
|
}
|
|
449
449
|
// Compact checklist: ✓done/total · current step (RUSH-2045).
|