@phnx-labs/agents-cli 1.22.22 → 1.22.23
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 +209 -0
- package/README.md +8 -2
- package/dist/commands/doctor.js +15 -7
- package/dist/commands/exec.js +20 -6
- package/dist/commands/focus.d.ts +76 -4
- package/dist/commands/focus.js +219 -40
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +25 -0
- package/dist/commands/go.js +63 -2
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets.js +25 -30
- package/dist/commands/sessions-resume.d.ts +21 -3
- package/dist/commands/sessions-resume.js +50 -11
- package/dist/commands/sessions.d.ts +81 -5
- package/dist/commands/sessions.js +325 -66
- package/dist/commands/watchdog.js +13 -2
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.js +88 -54
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks.js +12 -2
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/runner.js +28 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +118 -49
- package/dist/lib/secrets/reaper.d.ts +87 -0
- package/dist/lib/secrets/reaper.js +184 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +40 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
- package/dist/bin/agents +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
package/dist/commands/harness.js
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import { addProfile, ensureProviderToken, applyFromSecrets } from './profiles.js';
|
|
15
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';
|
|
16
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, editProfile, renameProfile, profileFromHostModel, authEnvKeyForHost, modelEnvKeyForHost, baseUrlEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
|
|
17
17
|
import { listPresets, getPreset } from '../lib/profiles-presets.js';
|
|
18
|
-
import { listBundles } from '../lib/secrets/bundles.js';
|
|
19
18
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
19
|
+
import { runWizardSteps, createSteps, editSteps, defaultWizardIO, } from './harness-wizard.js';
|
|
20
20
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
21
21
|
function nativeModes(id) {
|
|
22
22
|
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
@@ -193,108 +193,93 @@ async function runForkFlow(source, name, opts) {
|
|
|
193
193
|
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
194
194
|
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
195
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
196
|
/**
|
|
201
197
|
* Interactive `agents harness add`/`fork` wizard — runs when required info is
|
|
202
|
-
* missing and stdout
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* build an
|
|
198
|
+
* missing and stdin+stdout are a TTY (see {@link forkNeedsWizard}, {@link addNeedsWizard}).
|
|
199
|
+
* Drives the shared step engine ({@link createSteps}) and maps its finished draft
|
|
200
|
+
* back to the same `(source, name, opts)` shape {@link buildFork} accepts via
|
|
201
|
+
* {@link runForkFlow}, so a wizard run and a hand-written fork call build an
|
|
202
|
+
* identical profile.
|
|
206
203
|
*/
|
|
207
|
-
async function
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
baseUrl =
|
|
204
|
+
async function runCreateWizard() {
|
|
205
|
+
const io = await defaultWizardIO();
|
|
206
|
+
const draft = await runWizardSteps(createSteps(), { mode: 'create' }, io);
|
|
207
|
+
return {
|
|
208
|
+
source: draft.source,
|
|
209
|
+
name: draft.name,
|
|
210
|
+
opts: {
|
|
211
|
+
model: draft.model,
|
|
212
|
+
baseUrl: draft.baseUrl,
|
|
213
|
+
authProvider: draft.authProvider,
|
|
214
|
+
fromSecrets: draft.fromSecrets,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Map a finished edit-wizard draft onto {@link EditOptions}, keeping only the
|
|
220
|
+
* fields the user actually changed from the profile's current values. Unchanged
|
|
221
|
+
* accepts (the wizard pre-fills each prompt with the current value) drop out, so
|
|
222
|
+
* the resulting {@link buildEdit} touches nothing the user left alone — and the
|
|
223
|
+
* "no changes" case is detectable via {@link hasEditFlags}. Base-URL clearing is
|
|
224
|
+
* intentionally not expressed here: the flag path can't clear it either (an empty
|
|
225
|
+
* `--base-url` is a no-op in `forkProfile`), so the wizard matches that until a
|
|
226
|
+
* later subtask adds explicit clearing.
|
|
227
|
+
*/
|
|
228
|
+
export function draftToEditOptions(draft, original) {
|
|
229
|
+
const host = original.host.agent;
|
|
230
|
+
const curModel = original.env[modelEnvKeyForHost(host)];
|
|
231
|
+
const baseKey = baseUrlEnvKeyForHost(host);
|
|
232
|
+
const curBaseUrl = baseKey ? original.env[baseKey] : undefined;
|
|
233
|
+
const curVersion = original.host.version ?? '';
|
|
234
|
+
const curFallback = original.fallback_model ?? '';
|
|
235
|
+
const curDescription = original.description ?? '';
|
|
236
|
+
const opts = {};
|
|
237
|
+
if (draft.model !== undefined && draft.model !== curModel)
|
|
238
|
+
opts.model = draft.model;
|
|
239
|
+
if (draft.baseUrl && draft.baseUrl !== curBaseUrl)
|
|
240
|
+
opts.baseUrl = draft.baseUrl;
|
|
241
|
+
if (draft.authProvider !== undefined)
|
|
242
|
+
opts.authProvider = draft.authProvider;
|
|
243
|
+
if (draft.fromSecrets !== undefined)
|
|
244
|
+
opts.fromSecrets = draft.fromSecrets;
|
|
245
|
+
if (draft.version !== undefined && draft.version !== curVersion)
|
|
246
|
+
opts.version = draft.version;
|
|
247
|
+
if (draft.fallbackModel !== undefined && draft.fallbackModel !== curFallback)
|
|
248
|
+
opts.fallbackModel = draft.fallbackModel;
|
|
249
|
+
if (draft.description !== undefined && draft.description !== curDescription)
|
|
250
|
+
opts.description = draft.description;
|
|
251
|
+
return opts;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Interactive `agents harness edit <name>` wizard — runs when no edit flags were
|
|
255
|
+
* given and stdin+stdout are a TTY. Loads the profile, drives the shared step
|
|
256
|
+
* engine ({@link editSteps}) pre-filled with current values, then persists via the
|
|
257
|
+
* same build+write path as the flag-driven edit. `--key-stdin` is honored for the
|
|
258
|
+
* auth step's key entry. When the user changes nothing, it says so and writes
|
|
259
|
+
* nothing.
|
|
260
|
+
*/
|
|
261
|
+
async function runEditWizard(name, cliOpts) {
|
|
262
|
+
if (!profileExists(name)) {
|
|
263
|
+
throw new Error(`Harness '${name}' not found. Create it first: agents harness add ${name} ...`);
|
|
244
264
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
defaultName = preset.name;
|
|
265
|
+
const original = readProfile(name);
|
|
266
|
+
const io = await defaultWizardIO();
|
|
267
|
+
const draft = await runWizardSteps(editSteps(original), { mode: 'edit', original, host: original.host.agent, name }, io);
|
|
268
|
+
const opts = { ...draftToEditOptions(draft, original), keyStdin: cliOpts.keyStdin };
|
|
269
|
+
if (!hasEditFlags(opts)) {
|
|
270
|
+
console.log(chalk.gray(`No changes made to '${name}'.`));
|
|
271
|
+
return;
|
|
253
272
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
}
|
|
273
|
+
const edited = buildEdit(name, opts);
|
|
274
|
+
if (opts.fromSecrets) {
|
|
275
|
+
await applyFromSecrets(edited, opts.fromSecrets, opts.authProvider);
|
|
276
|
+
}
|
|
277
|
+
else if (opts.authProvider) {
|
|
278
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
296
279
|
}
|
|
297
|
-
|
|
280
|
+
writeProfile(edited);
|
|
281
|
+
console.log(chalk.green(`Harness '${name}' updated.`));
|
|
282
|
+
console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
|
|
298
283
|
}
|
|
299
284
|
export function registerHarnessCommands(program) {
|
|
300
285
|
const cmd = program
|
|
@@ -354,7 +339,7 @@ Examples:
|
|
|
354
339
|
if (!isInteractiveTerminal()) {
|
|
355
340
|
throw new Error("'agents harness add' needs --preset or --host + --model (or a name and an interactive terminal for the wizard).");
|
|
356
341
|
}
|
|
357
|
-
const wiz = await
|
|
342
|
+
const wiz = await runCreateWizard();
|
|
358
343
|
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
359
344
|
return;
|
|
360
345
|
}
|
|
@@ -399,7 +384,7 @@ Examples:
|
|
|
399
384
|
if (!isInteractiveTerminal()) {
|
|
400
385
|
throw new Error("'agents harness fork' needs <source> and <name> (or an interactive terminal for the wizard).");
|
|
401
386
|
}
|
|
402
|
-
const wiz = await
|
|
387
|
+
const wiz = await runCreateWizard();
|
|
403
388
|
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
404
389
|
return;
|
|
405
390
|
}
|
|
@@ -412,7 +397,7 @@ Examples:
|
|
|
412
397
|
});
|
|
413
398
|
cmd
|
|
414
399
|
.command('edit <name>')
|
|
415
|
-
.description('Edit an existing custom harness in place — model, endpoint, auth, version, description, fallback.')
|
|
400
|
+
.description('Edit an existing custom harness in place — model, endpoint, auth, version, description, fallback. Omit flags in a terminal for the interactive wizard.')
|
|
416
401
|
.option('--model <id>', 'Swap the pinned model')
|
|
417
402
|
.option('--base-url <url>', 'Swap the custom endpoint base URL')
|
|
418
403
|
.option('--auth-provider <provider>', 'Repoint auth at a different provider (keychain-backed)')
|
|
@@ -437,9 +422,20 @@ Examples:
|
|
|
437
422
|
|
|
438
423
|
# Copy a key out of an existing secrets bundle instead of typing it
|
|
439
424
|
agents harness edit corp --from-secrets prod:OPENROUTER_KEY
|
|
425
|
+
|
|
426
|
+
# No flags, in an interactive terminal: a wizard walks each field pre-filled
|
|
427
|
+
agents harness edit deepseek
|
|
440
428
|
`)
|
|
441
429
|
.action(async (name, opts) => {
|
|
442
430
|
try {
|
|
431
|
+
// No edit flags + a real terminal → the interactive wizard, pre-filled
|
|
432
|
+
// with current values. Any flag (or a non-interactive caller) takes the
|
|
433
|
+
// flag path unchanged; a flagless non-interactive call still errors via
|
|
434
|
+
// buildEdit's EDIT_FLAGS_HELP.
|
|
435
|
+
if (!hasEditFlags(opts) && isInteractiveTerminal()) {
|
|
436
|
+
await runEditWizard(name, opts);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
443
439
|
const edited = buildEdit(name, opts);
|
|
444
440
|
if (opts.fromSecrets) {
|
|
445
441
|
await applyFromSecrets(edited, opts.fromSecrets, opts.authProvider);
|
package/dist/commands/resume.js
CHANGED
|
@@ -22,8 +22,8 @@ export function buildResumeRunArgs(session, prompt, options) {
|
|
|
22
22
|
}
|
|
23
23
|
export function registerResumeCommand(program) {
|
|
24
24
|
const cmd = program
|
|
25
|
-
.command('resume <session
|
|
26
|
-
.description('Resume a session with its original harness, version, device, account, cwd, and mode')
|
|
25
|
+
.command('resume <session> [prompt]')
|
|
26
|
+
.description('Resume a session by id, tmux alias, or exact label with its original harness, version, device, account, cwd, and mode. Searches the fleet automatically; a local full-id hit resumes with zero SSH.')
|
|
27
27
|
.option('-m, --mode <mode>', 'Override the recorded launch mode')
|
|
28
28
|
.option('-i, --interactive', 'Resume interactively even when a prompt is provided')
|
|
29
29
|
.option('--headless', 'Resume headlessly (a prompt is required)')
|
|
@@ -71,10 +71,17 @@ export function registerResumeCommand(program) {
|
|
|
71
71
|
# Resume and continue headlessly
|
|
72
72
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 "finish the tests"
|
|
73
73
|
|
|
74
|
+
# Resume by exact label (auto-resumes the one match)
|
|
75
|
+
agents resume "fix the flaky ssh test"
|
|
76
|
+
|
|
77
|
+
# Resume by durable tmux alias or its unique suffix
|
|
78
|
+
agents resume ag-codex-c1f3d813
|
|
79
|
+
agents resume c1f3d813
|
|
80
|
+
|
|
74
81
|
# Deliberately change permissions
|
|
75
82
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 --mode edit`,
|
|
76
83
|
notes: `
|
|
77
|
-
|
|
84
|
+
A full ID resolves from the local session database first (zero SSH) and, on a local miss, fans out with the first peer holding it cancelling the rest. An exact label always consults the fleet (labels are not globally unique) and auto-resumes the one match; a cross-machine label collision surfaces as an ambiguity.
|
|
78
85
|
Use agents run auto --resume <id> when the original account is unavailable and another harness may continue.`,
|
|
79
86
|
});
|
|
80
87
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, buildRemoteFileImportCommand, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
@@ -333,10 +333,6 @@ function getCliVersion() {
|
|
|
333
333
|
return '0.0.0';
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
|
-
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
337
|
-
function shellQuote(s) {
|
|
338
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
339
|
-
}
|
|
340
336
|
/**
|
|
341
337
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
342
338
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
@@ -1332,7 +1328,7 @@ export function registerSecretsCommands(program) {
|
|
|
1332
1328
|
if (bundle.allow_exec)
|
|
1333
1329
|
console.log(chalk.yellow('allow_exec: true'));
|
|
1334
1330
|
if (bundle.backend === 'file')
|
|
1335
|
-
console.log(chalk.gray('backend: file (
|
|
1331
|
+
console.log(chalk.gray('backend: file (encrypted at rest; headless reads via a machine-local key, or AGENTS_SECRETS_PASSPHRASE if set — no Touch ID)'));
|
|
1336
1332
|
if (bundle.backend === 'vault')
|
|
1337
1333
|
console.log(chalk.gray('storage: synced (age-encrypted ~/.agents/vault.age; needs agents login)'));
|
|
1338
1334
|
if (bundlePolicy(bundle) === 'never') {
|
|
@@ -1588,7 +1584,7 @@ export function registerSecretsCommands(program) {
|
|
|
1588
1584
|
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
1589
1585
|
}
|
|
1590
1586
|
if (backend === 'file') {
|
|
1591
|
-
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE
|
|
1587
|
+
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted at rest under a machine-local key (or AGENTS_SECRETS_PASSPHRASE if set); headless reads, no Touch ID.'));
|
|
1592
1588
|
}
|
|
1593
1589
|
if (backend === 'vault') {
|
|
1594
1590
|
console.log(chalk.gray('Synced: items are encrypted in ~/.agents/vault.age. Copy that file with your sync tool of choice.'));
|
|
@@ -2064,7 +2060,7 @@ Examples:
|
|
|
2064
2060
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
2065
2061
|
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
2066
2062
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
2067
|
-
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file
|
|
2063
|
+
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
|
|
2068
2064
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
2069
2065
|
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
2070
2066
|
.option('--to-file <path>', 'Write the bundle as an AES-256-GCM encrypted offline file (needs AGENTS_SECRETS_PASSPHRASE; symmetric counterpart of import --from-file)')
|
|
@@ -2097,20 +2093,16 @@ Examples:
|
|
|
2097
2093
|
for (const h of hosts)
|
|
2098
2094
|
assertValidSshTarget(h);
|
|
2099
2095
|
const remoteBackend = parseBackendOpt(opts.remoteBackend);
|
|
2100
|
-
// For a file-backed remote bundle
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
//
|
|
2104
|
-
//
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
throw new Error('--remote-backend file needs AGENTS_SECRETS_PASSPHRASE set locally to encrypt the ' +
|
|
2111
|
-
'bundle at rest on the remote. Set it for this command, then unlock it the same way per run.');
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2096
|
+
// For a file-backed remote bundle a passphrase is OPTIONAL. The file
|
|
2097
|
+
// store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
|
|
2098
|
+
// unset the remote `import --backend file` auto-provisions the remote's
|
|
2099
|
+
// own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
|
|
2100
|
+
// are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
|
|
2101
|
+
// the operator opts in by setting it (e.g. to key the bundle off-disk
|
|
2102
|
+
// under a shared secret) — shipped as the FIRST stdin line so it never
|
|
2103
|
+
// lands in argv / `ps` / the remote shell history. Forcing a shared
|
|
2104
|
+
// passphrase would defeat headless reads, so we no longer require one.
|
|
2105
|
+
const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
|
|
2114
2106
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, keyMode: 'storage', agentOnly: true });
|
|
2115
2107
|
const dotenv = bundleEnvToDotenv(env);
|
|
2116
2108
|
const keyCount = Object.keys(env).length;
|
|
@@ -2126,19 +2118,22 @@ Examples:
|
|
|
2126
2118
|
for (const host of hosts) {
|
|
2127
2119
|
let res;
|
|
2128
2120
|
if (remoteBackend === 'file') {
|
|
2129
|
-
// File backend
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
2121
|
+
// File backend: headless-readable via the remote's machine-local key
|
|
2122
|
+
// when no passphrase is set; otherwise forwards AGENTS_SECRETS_PASSPHRASE
|
|
2123
|
+
// as the FIRST stdin line (consumed by `read`, so it never lands in
|
|
2124
|
+
// argv / `ps` / remote history), then the .env. Both build a POSIX
|
|
2125
|
+
// `bash -lc` command — refuse a Windows target cleanly rather than
|
|
2126
|
+
// emit broken PowerShell.
|
|
2134
2127
|
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2135
2128
|
failures++;
|
|
2136
2129
|
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
2137
2130
|
continue;
|
|
2138
2131
|
}
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2132
|
+
const { remoteCmd, input } = buildRemoteFileImportCommand(resolvedBundleName, dotenv, {
|
|
2133
|
+
passphrase: remotePassphrase,
|
|
2134
|
+
force: opts.force,
|
|
2135
|
+
});
|
|
2136
|
+
res = sshExec(host, remoteCmd, { input });
|
|
2142
2137
|
}
|
|
2143
2138
|
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2144
2139
|
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { type Packing } from '../lib/terminal/index.js';
|
|
3
|
-
|
|
2
|
+
import { type Backend, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
3
|
+
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
4
|
+
export declare const CONFIRM_THRESHOLD = 5;
|
|
5
|
+
export interface ResumeOptions {
|
|
4
6
|
agent?: string;
|
|
5
7
|
all?: boolean;
|
|
6
8
|
teams?: boolean;
|
|
@@ -16,5 +18,21 @@ interface ResumeOptions {
|
|
|
16
18
|
splits?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|
|
21
|
+
/** IDs and tmux aliases are actions, not picker search text. Human phrases keep
|
|
22
|
+
* the existing pre-filtered picker, while an explicit identity resumes directly. */
|
|
23
|
+
export declare function isDirectResumeSelector(query: string): boolean;
|
|
24
|
+
/** Re-enter through the top-level command so fleet routing and harness policy
|
|
25
|
+
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
26
|
+
export declare function resumeSelectorInPlace(selector: string): Promise<void>;
|
|
27
|
+
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
28
|
+
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
29
|
+
* only when the process is no longer attachable. */
|
|
30
|
+
export declare function dispatchSessionLifecycleInPlace(selector: string, hosts?: string[]): Promise<void>;
|
|
31
|
+
export declare function buildSessionLifecycleArgs(selector: string, hosts?: string[]): string[];
|
|
19
32
|
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
20
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
35
|
+
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
36
|
+
* user dismissed the chooser).
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveBackend(options: ResumeOptions, ctx: EngineContext, count: number): Promise<Backend | 'inplace' | 'cancel'>;
|
|
@@ -14,18 +14,21 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
14
14
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
15
15
|
import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
16
16
|
import { buildPreview } from './sessions-picker.js';
|
|
17
|
-
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor,
|
|
17
|
+
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
18
18
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
19
19
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
20
20
|
import { setHelpSections } from '../lib/help.js';
|
|
21
21
|
import { confirm } from '@inquirer/prompts';
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { buildCanonicalResumeCommand } from '../lib/session/resume-command.js';
|
|
24
|
+
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
22
25
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
23
|
-
const CONFIRM_THRESHOLD = 5;
|
|
26
|
+
export const CONFIRM_THRESHOLD = 5;
|
|
24
27
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
25
28
|
const cmd = sessionsCmd
|
|
26
29
|
.command('resume')
|
|
27
|
-
.argument('[query]', '
|
|
28
|
-
.description('
|
|
30
|
+
.argument('[query]', 'Session id/tmux alias to reopen directly, or text that filters the picker')
|
|
31
|
+
.description('Reopen one session by canonical identity, or multi-select history into terminal tabs/splits.')
|
|
29
32
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
30
33
|
.option('--all', 'Include sessions from every directory (not just current project)')
|
|
31
34
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
@@ -46,6 +49,10 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
46
49
|
# Pre-filter the pool before selecting (space in the filter → use [query])
|
|
47
50
|
agents sessions resume "auth middleware"
|
|
48
51
|
|
|
52
|
+
# Reopen one session from any device by UUID prefix or tmux alias
|
|
53
|
+
agents sessions resume 019fd114
|
|
54
|
+
agents sessions resume ag-codex-c1f3d813
|
|
55
|
+
|
|
49
56
|
# Force a backend / side-by-side splits / a remote host
|
|
50
57
|
agents sessions resume --ghostty
|
|
51
58
|
agents sessions resume --vscodium
|
|
@@ -53,7 +60,8 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
53
60
|
agents sessions resume --host zion --tmux
|
|
54
61
|
`,
|
|
55
62
|
notes: `
|
|
56
|
-
-
|
|
63
|
+
- A UUID/prefix or ag-<agent>-<suffix> alias bypasses the picker: a live pane is attached; an inactive session resumes on its owning device.
|
|
64
|
+
- With no identity selector, space toggles a session, enter confirms, and tab toggles the preview pane.
|
|
57
65
|
- Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
|
|
58
66
|
- Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
|
|
59
67
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
|
|
@@ -71,6 +79,10 @@ async function sessionsResumeAction(query, options) {
|
|
|
71
79
|
process.exitCode = 1;
|
|
72
80
|
return;
|
|
73
81
|
}
|
|
82
|
+
if (query && isDirectResumeSelector(query)) {
|
|
83
|
+
await dispatchSessionLifecycleInPlace(query.trim(), options.host ? [options.host] : []);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
74
86
|
const { agent, version } = parseAgentFilter(options.agent);
|
|
75
87
|
const limit = parseInt(options.limit || '200', 10);
|
|
76
88
|
const since = options.since ?? (options.all ? undefined : '30d');
|
|
@@ -118,11 +130,7 @@ async function sessionsResumeAction(query, options) {
|
|
|
118
130
|
// 2. Split the selection into resumable surfaces and skipped agents (no silent drop).
|
|
119
131
|
const items = [];
|
|
120
132
|
for (const s of chosen) {
|
|
121
|
-
const command =
|
|
122
|
-
if (!command) {
|
|
123
|
-
console.log(chalk.yellow(` skip ${s.shortId} — resume is not supported for ${s.agent} sessions yet`));
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
133
|
+
const command = buildCanonicalResumeCommand(s.id);
|
|
126
134
|
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
127
135
|
items.push({ session: s, cwd, command });
|
|
128
136
|
}
|
|
@@ -179,6 +187,37 @@ async function sessionsResumeAction(query, options) {
|
|
|
179
187
|
});
|
|
180
188
|
console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
|
|
181
189
|
}
|
|
190
|
+
/** IDs and tmux aliases are actions, not picker search text. Human phrases keep
|
|
191
|
+
* the existing pre-filtered picker, while an explicit identity resumes directly. */
|
|
192
|
+
export function isDirectResumeSelector(query) {
|
|
193
|
+
const selector = query.trim();
|
|
194
|
+
return looksLikeSessionId(selector) || /^ag-[a-z][a-z0-9-]*-[0-9a-f]{8}$/i.test(selector);
|
|
195
|
+
}
|
|
196
|
+
/** Re-enter through the top-level command so fleet routing and harness policy
|
|
197
|
+
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
198
|
+
export async function resumeSelectorInPlace(selector) {
|
|
199
|
+
await spawnCliInPlace(['resume', selector]);
|
|
200
|
+
}
|
|
201
|
+
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
202
|
+
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
203
|
+
* only when the process is no longer attachable. */
|
|
204
|
+
export async function dispatchSessionLifecycleInPlace(selector, hosts = []) {
|
|
205
|
+
await spawnCliInPlace(buildSessionLifecycleArgs(selector, hosts));
|
|
206
|
+
}
|
|
207
|
+
export function buildSessionLifecycleArgs(selector, hosts = []) {
|
|
208
|
+
return ['sessions', 'focus', selector, ...hosts.flatMap(host => ['--host', host])];
|
|
209
|
+
}
|
|
210
|
+
function asyncExitCode(child) {
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
child.once('error', () => resolve(127));
|
|
213
|
+
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function spawnCliInPlace(args) {
|
|
217
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: 'inherit' });
|
|
218
|
+
const exitCode = await asyncExitCode(child);
|
|
219
|
+
process.exitCode = exitCode;
|
|
220
|
+
}
|
|
182
221
|
export function resolveResumePacking(options) {
|
|
183
222
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
184
223
|
}
|
|
@@ -187,7 +226,7 @@ export function resolveResumePacking(options) {
|
|
|
187
226
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
188
227
|
* user dismissed the chooser).
|
|
189
228
|
*/
|
|
190
|
-
async function resolveBackend(options, ctx, count) {
|
|
229
|
+
export async function resolveBackend(options, ctx, count) {
|
|
191
230
|
const forced = options.iterm ? 'iterm'
|
|
192
231
|
: options.ghostty ? 'ghostty'
|
|
193
232
|
: options.tmux ? 'tmux'
|