@phnx-labs/agents-cli 1.20.73 → 1.20.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* In all forms, the agent's config dir (e.g. ~/.openclaw) is also moved under
|
|
21
21
|
* management — same behavior as the first-run `agents setup` import flow.
|
|
22
22
|
*/
|
|
23
|
+
import { withIsolationBoundary } from '../lib/isolation-boundary-report.js';
|
|
24
|
+
import { assertIsolationBoundary, createVersionedAlias } from '../lib/shims.js';
|
|
25
|
+
import { markVersionIsolated } from '../lib/versions.js';
|
|
23
26
|
import chalk from 'chalk';
|
|
24
27
|
import ora from 'ora';
|
|
25
28
|
import * as fs from 'fs';
|
|
@@ -29,7 +32,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
29
32
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
30
33
|
import { AGENTS, getCliPath, getCliVersion, agentLabel, resolveAgentName } from '../lib/agents.js';
|
|
31
34
|
import { getVersionDir } from '../lib/versions.js';
|
|
32
|
-
import { finalizeImport, importAgentBinary, importAgentConfig, importInstallScriptBinary, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
|
|
35
|
+
import { finalizeImport, importAgentBinary, importAgentConfig, importInstallScriptBinary, seedIsolatedConfigFromLocal, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
|
|
33
36
|
import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
|
|
34
37
|
async function runImport(agentArg, opts) {
|
|
35
38
|
const agentId = resolveAgentName(agentArg);
|
|
@@ -39,6 +42,16 @@ async function runImport(agentArg, opts) {
|
|
|
39
42
|
process.exit(1);
|
|
40
43
|
}
|
|
41
44
|
const agent = AGENTS[agentId];
|
|
45
|
+
// Import registers the adopted install as a NORMAL version and only then sets the
|
|
46
|
+
// default / creates the shim / repoints the config. By that point the agent has a
|
|
47
|
+
// non-isolated version, so it is no longer protected and the primitive gates —
|
|
48
|
+
// which read the state as it is at call time — would let the adoption through.
|
|
49
|
+
// The boundary has to be checked against the state BEFORE the import mutates it.
|
|
50
|
+
// --isolated adopts nothing, so the boundary does not apply to it — it is in fact
|
|
51
|
+
// the supported way to bring a local install in while protection is on.
|
|
52
|
+
if (!opts.isolated) {
|
|
53
|
+
assertIsolationBoundary(agentId, 'adopt your existing install');
|
|
54
|
+
}
|
|
42
55
|
// installScript-based agents (Grok, Antigravity, Cursor, Kiro, Goose, Roo)
|
|
43
56
|
// don't have an npm package; their binary lives wherever the curl/brew
|
|
44
57
|
// installer dropped it. We adopt by symlinking that PATH binary directly
|
|
@@ -121,7 +134,7 @@ async function runImport(agentArg, opts) {
|
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
|
-
let version = opts.
|
|
137
|
+
let version = opts.as;
|
|
125
138
|
if (!version) {
|
|
126
139
|
if (!useDirectBinaryImport && globalPath) {
|
|
127
140
|
try {
|
|
@@ -144,7 +157,7 @@ async function runImport(agentArg, opts) {
|
|
|
144
157
|
}
|
|
145
158
|
if (!version) {
|
|
146
159
|
console.error(chalk.red(`Could not determine version for ${agentLabel(agentId)}.`));
|
|
147
|
-
console.error(chalk.gray('Pass --
|
|
160
|
+
console.error(chalk.gray('Pass --as <version> explicitly.'));
|
|
148
161
|
process.exit(1);
|
|
149
162
|
}
|
|
150
163
|
if (!isValidImportVersion(version)) {
|
|
@@ -154,7 +167,7 @@ async function runImport(agentArg, opts) {
|
|
|
154
167
|
}
|
|
155
168
|
const versionDir = getVersionDir(agentId, version);
|
|
156
169
|
const fromLabel = useDirectBinaryImport ? installScriptBinary : globalPath;
|
|
157
|
-
console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
|
|
170
|
+
console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}${opts.isolated ? ' (isolated copy)' : ''}`));
|
|
158
171
|
console.log(` from: ${chalk.gray(fromLabel)}`);
|
|
159
172
|
console.log(` into: ${chalk.gray(versionDir)}`);
|
|
160
173
|
const configDirExists = fs.existsSync(agent.configDir);
|
|
@@ -165,6 +178,16 @@ async function runImport(agentArg, opts) {
|
|
|
165
178
|
configAlreadyManaged = true;
|
|
166
179
|
console.log(` config: ${chalk.gray(`${agent.configDir} (already managed — will skip)`)}`);
|
|
167
180
|
}
|
|
181
|
+
else if (opts.isolated) {
|
|
182
|
+
// This summary is printed BEFORE the branch that does the work, so it has to
|
|
183
|
+
// describe the mode it is actually about to run. Saying "will be moved" under
|
|
184
|
+
// --isolated announced the exact adoption the flag exists to prevent — a
|
|
185
|
+
// confirmation prompt that misdescribes the operation is worse than none.
|
|
186
|
+
console.log(` config: ${chalk.gray(`${agent.configDir} (will be COPIED — your original stays put)`)}`);
|
|
187
|
+
if (!opts.withAuth) {
|
|
188
|
+
console.log(` ${chalk.gray('credentials are skipped; pass --with-auth to include them')}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
168
191
|
else {
|
|
169
192
|
console.log(` config: ${chalk.gray(`${agent.configDir} (will be moved into version home)`)}`);
|
|
170
193
|
}
|
|
@@ -175,7 +198,9 @@ async function runImport(agentArg, opts) {
|
|
|
175
198
|
if (!opts.yes && isInteractiveTerminal()) {
|
|
176
199
|
console.log();
|
|
177
200
|
const proceed = await confirm({
|
|
178
|
-
message:
|
|
201
|
+
message: opts.isolated
|
|
202
|
+
? `Import ${agentLabel(agentId)} v${version} as an isolated copy?`
|
|
203
|
+
: `Import ${agentLabel(agentId)} v${version} into agents-cli?`,
|
|
179
204
|
default: true,
|
|
180
205
|
}).catch((err) => {
|
|
181
206
|
if (isPromptCancelled(err))
|
|
@@ -191,7 +216,33 @@ async function runImport(agentArg, opts) {
|
|
|
191
216
|
// user-visible side effect (renaming ~/.<agent>/), so if it fails we don't
|
|
192
217
|
// want a stranded symlink farm. Binary registration is cheap and reversible
|
|
193
218
|
// — if it fails after config, the next `agents import` call retries cleanly.
|
|
194
|
-
const willImportConfig = configDirExists && !configAlreadyManaged;
|
|
219
|
+
const willImportConfig = configDirExists && !configAlreadyManaged && !opts.isolated;
|
|
220
|
+
if (opts.isolated && configDirExists && configAlreadyManaged) {
|
|
221
|
+
console.log(chalk.gray(` Skipping config copy: ${agent.configDir} is a managed symlink, not your real settings.`));
|
|
222
|
+
}
|
|
223
|
+
else if (opts.isolated && configDirExists) {
|
|
224
|
+
// COPY the user's settings in; never move, never symlink. The original stays
|
|
225
|
+
// exactly where it is — that is what separates this from adoption.
|
|
226
|
+
const seedSpinner = ora(`Copying ${agent.configDir} into the isolated copy...`).start();
|
|
227
|
+
const seed = seedIsolatedConfigFromLocal(agentId, version, { withAuth: opts.withAuth, all: opts.all });
|
|
228
|
+
if (seed.error) {
|
|
229
|
+
seedSpinner.fail(`Config: ${seed.error}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
else if (seed.seeded) {
|
|
233
|
+
seedSpinner.succeed(`Settings copied (${seed.from} -> ${seed.to}); your original is untouched`);
|
|
234
|
+
if (seed.skippedRuntime.length > 0) {
|
|
235
|
+
console.log(chalk.gray(` Runtime state NOT copied: ${seed.skippedRuntime.join(', ')} (regenerated as you use it; --all to include)`));
|
|
236
|
+
}
|
|
237
|
+
if (seed.skippedAuth.length > 0) {
|
|
238
|
+
console.log(chalk.gray(` Credentials NOT copied: ${seed.skippedAuth.join(', ')}`));
|
|
239
|
+
console.log(chalk.gray(' The copy signs in separately. Use --with-auth to copy them too.'));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
seedSpinner.info('No existing config to copy.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
195
246
|
if (willImportConfig) {
|
|
196
247
|
const cfgSpinner = ora(`Importing config dir for ${agentLabel(agentId)} v${version}...`).start();
|
|
197
248
|
const cfgResult = await importAgentConfig(agentId, version);
|
|
@@ -224,6 +275,19 @@ async function runImport(agentArg, opts) {
|
|
|
224
275
|
// Wire the imported version into the resolver: global default, main shim,
|
|
225
276
|
// versioned alias, home-file symlinks. Idempotent — safe to call even if
|
|
226
277
|
// importAgentConfig already set the global default.
|
|
278
|
+
if (opts.isolated) {
|
|
279
|
+
// The isolated finalizer: launchable alias + marker, and nothing else. No global
|
|
280
|
+
// default, no bare shim, no config symlink — the same shape as
|
|
281
|
+
// `agents add --isolated`.
|
|
282
|
+
createVersionedAlias(agentId, version);
|
|
283
|
+
markVersionIsolated(agentId, version);
|
|
284
|
+
console.log();
|
|
285
|
+
console.log(chalk.green(`${agentLabel(agentId)} v${version} imported as an isolated copy.`));
|
|
286
|
+
console.log(chalk.gray(` Your ${agent.configDir} and ${agent.cliCommand} launcher are untouched.`));
|
|
287
|
+
console.log(chalk.gray(` Run it: agents run ${agentId}@${version}`));
|
|
288
|
+
console.log(chalk.gray(` Or make it the default isolated copy: agents use ${agentId}@${version}`));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
227
291
|
const finalizeSpinner = ora(`Wiring ${agentLabel(agentId)} v${version} as the active version...`).start();
|
|
228
292
|
try {
|
|
229
293
|
finalizeImport(agentId, version);
|
|
@@ -242,13 +306,22 @@ export function registerImportCommand(program) {
|
|
|
242
306
|
.command('import')
|
|
243
307
|
.argument('<agent>', 'Agent id (e.g. openclaw, claude, codex)')
|
|
244
308
|
.description('Import an existing unmanaged agent install into agents-cli')
|
|
245
|
-
|
|
309
|
+
// NOT `--version`. The program declares `.version(VERSION)` (src/index.ts), which
|
|
310
|
+
// claims `-V, --version` globally and wins over a subcommand option of the same
|
|
311
|
+
// name — so `agents import codex --version 1.2.3` printed the CLI's own version
|
|
312
|
+
// and exited without importing anything. The flag had been unreachable since it
|
|
313
|
+
// was introduced, and the "could not determine version" error even advised using
|
|
314
|
+
// it. Renamed so it actually reaches the command.
|
|
315
|
+
.option('--as <version>', 'Version label to import as (otherwise read from package.json)')
|
|
246
316
|
.option('--from-path <path>', 'Path to the npm package dir (otherwise auto-detected from PATH)')
|
|
317
|
+
.option('--isolated', 'Copy the install into a self-contained isolated version instead of adopting it')
|
|
318
|
+
.option('--with-auth', 'With --isolated, also copy credentials into the sandbox (skipped by default)')
|
|
319
|
+
.option('--all', 'With --isolated, also copy session history, logs and caches (skipped by default)')
|
|
247
320
|
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
248
321
|
.addHelpText('after', `
|
|
249
322
|
Examples:
|
|
250
323
|
$ agents import openclaw Auto-detect via PATH
|
|
251
|
-
$ agents import openclaw --
|
|
324
|
+
$ agents import openclaw --as 2026.3.8 Pin a version label
|
|
252
325
|
$ agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
|
|
253
326
|
|
|
254
327
|
# installScript-based agents (curl/brew installers, no npm package):
|
|
@@ -257,6 +330,11 @@ Examples:
|
|
|
257
330
|
$ agents import cursor Adopt ~/.local/bin/cursor-agent
|
|
258
331
|
$ agents import antigravity --from-path ~/.local/bin/agy
|
|
259
332
|
|
|
333
|
+
# Copy your setup into a sandbox instead of adopting it:
|
|
334
|
+
$ agents import codex --isolated New isolated copy at the local version
|
|
335
|
+
$ agents import codex --isolated --as 0.146.0 Re-seed an EXISTING isolated copy
|
|
336
|
+
$ agents import codex --isolated --with-auth ...and share credentials with it
|
|
337
|
+
|
|
260
338
|
When to use:
|
|
261
339
|
When an agent CLI is already installed globally and you want to bring it
|
|
262
340
|
under agents-cli management without reinstalling. Creates a symlink farm
|
|
@@ -265,5 +343,5 @@ When to use:
|
|
|
265
343
|
npm-style packages (claude, codex, gemini, opencode, openclaw) and
|
|
266
344
|
installScript-based agents (grok, antigravity, cursor, kiro, goose).
|
|
267
345
|
`)
|
|
268
|
-
.action(runImport);
|
|
346
|
+
.action((...args) => withIsolationBoundary(() => runImport(...args)));
|
|
269
347
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -21,6 +21,7 @@ import { truncate, termLink } from '../lib/format.js';
|
|
|
21
21
|
import * as yaml from 'yaml';
|
|
22
22
|
import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
|
|
23
23
|
import { supports } from '../lib/capabilities.js';
|
|
24
|
+
import { resolveConfiguredModel } from '../lib/models.js';
|
|
24
25
|
import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
25
26
|
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
26
27
|
import { getVersionHomePath } from '../lib/versions.js';
|
|
@@ -585,8 +586,10 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
585
586
|
console.log(JSON.stringify(json, null, 2));
|
|
586
587
|
return;
|
|
587
588
|
}
|
|
588
|
-
// Plain text
|
|
589
|
-
const
|
|
589
|
+
// Plain text — model sits right beside the version, same priority (no label).
|
|
590
|
+
const configuredModel = resolveConfiguredModel(agent, version)?.model;
|
|
591
|
+
const modelPart = configuredModel ? ' ' + chalk.yellow(configuredModel) : '';
|
|
592
|
+
const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${modelPart}${isDefault ? ' ' + chalk.green('[default]') : ''}`;
|
|
590
593
|
console.log('\n' + head + '\n');
|
|
591
594
|
const rows = [
|
|
592
595
|
['install', versionHome],
|
|
@@ -15,5 +15,25 @@ import { type Preset } from '../lib/profiles-presets.js';
|
|
|
15
15
|
* without mocking @inquirer/prompts.
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildProfileFromCollection(name: string, preset: Preset, collected: Record<string, string>, version?: string): Profile;
|
|
18
|
+
/** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
|
|
19
|
+
export interface AddProfileOptions {
|
|
20
|
+
preset?: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
authProvider?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
keyStdin?: boolean;
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a profile ("custom harness"). Two paths:
|
|
31
|
+
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
32
|
+
* (no preset needed). This is what makes a model like Muse Spark a named,
|
|
33
|
+
* runnable harness.
|
|
34
|
+
* - otherwise: apply a built-in preset (existing behavior).
|
|
35
|
+
* `label` only tunes the success wording (Profile vs Harness). Throws on error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function addProfile(name: string, opts: AddProfileOptions, label?: 'Profile' | 'Harness'): Promise<void>;
|
|
18
38
|
/** Register the `agents profiles` command tree. */
|
|
19
39
|
export declare function registerProfilesCommands(program: Command): void;
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { readStdinSync } from '../lib/format.js';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
|
-
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
|
|
12
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, profileFromHostModel, baseUrlEnvKeyForHost, authEnvKeyForHost, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
|
|
13
13
|
import { getPreset, listPresets, expandPreset } from '../lib/profiles-presets.js';
|
|
14
14
|
import { hasKeychainToken, keychainItemName, setKeychainToken, deleteKeychainToken, } from '../lib/secrets/profiles.js';
|
|
15
15
|
import { isInteractiveTerminal } from './utils.js';
|
|
16
16
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
17
17
|
import { getActiveResourceProfileName, getResourceProfilePreset, listResourceProfileNames, setActiveResourceProfile, upsertResourceProfilePreset, validateResourceProfileName, } from '../lib/resource-profiles.js';
|
|
18
|
-
import { AGENTS } from '../lib/agents.js';
|
|
18
|
+
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
19
19
|
import { listInstalledVersions, syncResourcesToVersion } from '../lib/versions.js';
|
|
20
20
|
/**
|
|
21
21
|
* Pure helper: builds a Profile from collected wizard inputs. Extracted so the
|
|
@@ -31,6 +31,7 @@ export function buildProfileFromCollection(name, preset, collected, version) {
|
|
|
31
31
|
envVar: preset.authEnvVar,
|
|
32
32
|
keychainItem: keychainItemName(preset.provider),
|
|
33
33
|
},
|
|
34
|
+
authOptional: preset.authOptional,
|
|
34
35
|
description: preset.description,
|
|
35
36
|
preset: preset.name,
|
|
36
37
|
provider: preset.provider,
|
|
@@ -65,6 +66,66 @@ async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
|
65
66
|
setKeychainToken(item, token);
|
|
66
67
|
console.log(chalk.green(`Stored in keychain: ${item}`));
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a profile ("custom harness"). Two paths:
|
|
71
|
+
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
72
|
+
* (no preset needed). This is what makes a model like Muse Spark a named,
|
|
73
|
+
* runnable harness.
|
|
74
|
+
* - otherwise: apply a built-in preset (existing behavior).
|
|
75
|
+
* `label` only tunes the success wording (Profile vs Harness). Throws on error.
|
|
76
|
+
*/
|
|
77
|
+
export async function addProfile(name, opts, label = 'Profile') {
|
|
78
|
+
validateProfileName(name);
|
|
79
|
+
if (profileExists(name) && !opts.force) {
|
|
80
|
+
throw new Error(`${label} '${name}' already exists. Use --force to overwrite.`);
|
|
81
|
+
}
|
|
82
|
+
// One-shot host + model → custom harness, no preset required.
|
|
83
|
+
if (opts.host || opts.model) {
|
|
84
|
+
if (!opts.host || !opts.model) {
|
|
85
|
+
throw new Error('Both --host <agent> and --model <id> are required to build a harness from a host + model.');
|
|
86
|
+
}
|
|
87
|
+
if (!ALL_AGENT_IDS.includes(opts.host)) {
|
|
88
|
+
throw new Error(`Unknown host '${opts.host}'. Valid hosts: ${ALL_AGENT_IDS.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
const host = opts.host;
|
|
91
|
+
if (opts.baseUrl && !baseUrlEnvKeyForHost(host)) {
|
|
92
|
+
console.error(chalk.yellow(`Note: --base-url has no known env var for host '${host}'; ignoring it.`));
|
|
93
|
+
}
|
|
94
|
+
let authEnvVar;
|
|
95
|
+
if (opts.authProvider) {
|
|
96
|
+
const key = authEnvKeyForHost(host);
|
|
97
|
+
if (!key) {
|
|
98
|
+
throw new Error(`--auth-provider is set but host '${host}' has no known auth env var. Use a preset or a hand-written profile YAML.`);
|
|
99
|
+
}
|
|
100
|
+
authEnvVar = key;
|
|
101
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
102
|
+
}
|
|
103
|
+
const profile = profileFromHostModel(name, host, opts.model, {
|
|
104
|
+
version: opts.version,
|
|
105
|
+
baseUrl: opts.baseUrl,
|
|
106
|
+
provider: opts.authProvider,
|
|
107
|
+
authEnvVar,
|
|
108
|
+
});
|
|
109
|
+
writeProfile(profile);
|
|
110
|
+
console.log(chalk.green(`${label} '${name}' added — ${host} + ${opts.model}.`));
|
|
111
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Preset path.
|
|
115
|
+
const presetName = opts.preset || name;
|
|
116
|
+
const preset = getPreset(presetName);
|
|
117
|
+
if (!preset) {
|
|
118
|
+
throw new Error(`No preset '${presetName}'.\nAvailable presets: ${listPresets().map((p) => p.name).join(', ')}\n` +
|
|
119
|
+
'Or build a custom harness: --host <agent> --model <id>.');
|
|
120
|
+
}
|
|
121
|
+
if (!preset.authOptional) {
|
|
122
|
+
await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
|
|
123
|
+
}
|
|
124
|
+
const profile = profileFromPreset(name, preset, opts.version);
|
|
125
|
+
writeProfile(profile);
|
|
126
|
+
console.log(chalk.green(`${label} '${name}' added.`));
|
|
127
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
128
|
+
}
|
|
68
129
|
/** Format a single profile as a table row for the `profiles list` output. */
|
|
69
130
|
function renderProfileRow(p) {
|
|
70
131
|
const host = p.host.version ? `${p.host.agent}@${p.host.version}` : p.host.agent;
|
|
@@ -462,27 +523,13 @@ Examples:
|
|
|
462
523
|
.option('--version <version>', 'Pin the host CLI version (e.g., 2.1.113)')
|
|
463
524
|
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
464
525
|
.option('--force', 'Overwrite an existing profile with the same name')
|
|
526
|
+
.addHelpText('after', '\nTo build a custom harness from a host CLI + model in one shot, use `agents harness add`.\n')
|
|
465
527
|
.action(async (name, opts) => {
|
|
528
|
+
// Preset-only surface here — `--host` on `profiles` is reserved for remote
|
|
529
|
+
// device routing (see lib/hosts/passthrough.ts). The host+model one-shot
|
|
530
|
+
// lives on `agents harness add`, which owns its own `--host`.
|
|
466
531
|
try {
|
|
467
|
-
|
|
468
|
-
console.error(chalk.red(`Profile '${name}' already exists. Use --force to overwrite.`));
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
const presetName = opts.preset || name;
|
|
472
|
-
const preset = getPreset(presetName);
|
|
473
|
-
if (!preset) {
|
|
474
|
-
console.error(chalk.red(`No preset '${presetName}'.`));
|
|
475
|
-
console.error(chalk.gray('Available presets: ' + listPresets().map((p) => p.name).join(', ')));
|
|
476
|
-
console.error(chalk.gray('Or pass --preset <name> to pick explicitly.'));
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|
|
479
|
-
if (!preset.authOptional) {
|
|
480
|
-
await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
|
|
481
|
-
}
|
|
482
|
-
const profile = profileFromPreset(name, preset, opts.version);
|
|
483
|
-
writeProfile(profile);
|
|
484
|
-
console.log(chalk.green(`Profile '${name}' added.`));
|
|
485
|
-
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
532
|
+
await addProfile(name, { preset: opts.preset, version: opts.version, keyStdin: opts.keyStdin, force: opts.force }, 'Profile');
|
|
486
533
|
}
|
|
487
534
|
catch (err) {
|
|
488
535
|
console.error(chalk.red(err.message));
|
package/dist/commands/repo.js
CHANGED
|
@@ -812,6 +812,7 @@ export function registerRepoCommands(program) {
|
|
|
812
812
|
console.log(chalk.gray('No repos to pull.'));
|
|
813
813
|
return;
|
|
814
814
|
}
|
|
815
|
+
let anyPulled = false;
|
|
815
816
|
for (const t of targets) {
|
|
816
817
|
if (!fs.existsSync(t.dir) || !isGitRepo(t.dir)) {
|
|
817
818
|
// A plain (never-cloned) user repo — setup makes ~/.agents a bare dir and
|
|
@@ -851,11 +852,24 @@ export function registerRepoCommands(program) {
|
|
|
851
852
|
const result = await pullRepo(t.dir);
|
|
852
853
|
if (result.success) {
|
|
853
854
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
|
|
855
|
+
anyPulled = true;
|
|
854
856
|
}
|
|
855
857
|
else {
|
|
856
858
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
857
859
|
}
|
|
858
860
|
}
|
|
861
|
+
// RUSH-1980: a pull rewrites the routine YAML on disk, but the daemon's
|
|
862
|
+
// scheduler froze its JobConfigs (device pins included) at load. Without a
|
|
863
|
+
// reload it keeps firing the pre-pull pins — a routine re-pinned to another
|
|
864
|
+
// host still fires here, double-firing across the fleet. SIGHUP the daemon
|
|
865
|
+
// so scheduler.reloadAll() re-reads the synced YAML and device pins refresh.
|
|
866
|
+
// No-op when the daemon isn't running (or on Windows, which has no SIGHUP).
|
|
867
|
+
if (anyPulled) {
|
|
868
|
+
const { isDaemonRunning, signalDaemonReload } = await import('../lib/daemon.js');
|
|
869
|
+
if (isDaemonRunning() && signalDaemonReload()) {
|
|
870
|
+
console.log(chalk.gray('Reloaded the routines daemon (device pins refreshed).'));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
859
873
|
});
|
|
860
874
|
repoCmd
|
|
861
875
|
.command('push [alias]')
|
|
@@ -783,18 +783,26 @@ export function registerRoutinesCommands(program) {
|
|
|
783
783
|
try {
|
|
784
784
|
const result = await executeJob(job);
|
|
785
785
|
const logPath = `${getRunDir(name, result.meta.runId)}/stdout.log`;
|
|
786
|
+
const succeeded = result.meta.status === 'completed';
|
|
786
787
|
if (options.json) {
|
|
787
788
|
writeJson({
|
|
788
|
-
ok:
|
|
789
|
+
ok: succeeded,
|
|
789
790
|
job: name,
|
|
790
791
|
logDir: getRunDir(name, result.meta.runId),
|
|
791
792
|
...runMetaJson(result.meta),
|
|
792
793
|
logPath,
|
|
793
794
|
reportPath: result.reportPath ?? null,
|
|
794
795
|
});
|
|
796
|
+
// A failed run must exit non-zero so cron wrappers, `&&` chains, and
|
|
797
|
+
// `--json` consumers actually see the failure (a logged-out agent used
|
|
798
|
+
// to exit 0 with ok:true, hiding the whole auth-failure epidemic). Set
|
|
799
|
+
// exitCode rather than process.exit() so the JSON payload is fully
|
|
800
|
+
// flushed to a pipe before the process ends.
|
|
801
|
+
if (!succeeded)
|
|
802
|
+
process.exitCode = 1;
|
|
795
803
|
return;
|
|
796
804
|
}
|
|
797
|
-
if (
|
|
805
|
+
if (succeeded) {
|
|
798
806
|
spinner.succeed(`Job completed (exit code: ${result.meta.exitCode})`);
|
|
799
807
|
}
|
|
800
808
|
else if (result.meta.status === 'timeout') {
|
|
@@ -805,10 +813,15 @@ export function registerRoutinesCommands(program) {
|
|
|
805
813
|
}
|
|
806
814
|
console.log(chalk.gray(` Run: ${result.meta.runId}`));
|
|
807
815
|
console.log(chalk.gray(` Log: ${logPath}`));
|
|
816
|
+
if (result.meta.errorMessage) {
|
|
817
|
+
console.log(chalk.gray(` Reason: ${result.meta.errorMessage}`));
|
|
818
|
+
}
|
|
808
819
|
if (result.reportPath) {
|
|
809
820
|
console.log(chalk.bold('\nReport:\n'));
|
|
810
821
|
console.log(fs.readFileSync(result.reportPath, 'utf-8'));
|
|
811
822
|
}
|
|
823
|
+
if (!succeeded)
|
|
824
|
+
process.exitCode = 1;
|
|
812
825
|
}
|
|
813
826
|
catch (err) {
|
|
814
827
|
if (options.json) {
|
package/dist/commands/secrets.js
CHANGED
|
@@ -15,10 +15,10 @@ 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,
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } 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
|
-
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
23
23
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
24
24
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -225,17 +225,29 @@ async function importFromICloud(bundleName, opts) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
|
-
* Printed under a
|
|
229
|
-
*
|
|
230
|
-
*
|
|
228
|
+
* Printed under a read failure: if the name matches a bundle stranded in the
|
|
229
|
+
* iCloud Keychain (pre-device-local-cutover era), point at the recovery command
|
|
230
|
+
* instead of leaving a dead end.
|
|
231
|
+
*
|
|
232
|
+
* `stillPresent` distinguishes the two failures, because they need different
|
|
233
|
+
* commands. A MISSING bundle imports straight from iCloud. A bundle that is on
|
|
234
|
+
* disk but undecryptable does not: `import` reads the existing bundle before
|
|
235
|
+
* writing into it, so it fails the same way the read just did. That one has to
|
|
236
|
+
* be deleted first — which is why naming the wrong command here bricked the
|
|
237
|
+
* name entirely.
|
|
231
238
|
*/
|
|
232
|
-
function maybePrintSyncedHint(name) {
|
|
239
|
+
function maybePrintSyncedHint(name, stillPresent) {
|
|
233
240
|
if (process.platform !== 'darwin')
|
|
234
241
|
return;
|
|
235
242
|
try {
|
|
236
|
-
if (discoverSyncedBundles().some((c) => c.name === name))
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
if (!discoverSyncedBundles().some((c) => c.name === name))
|
|
244
|
+
return;
|
|
245
|
+
console.error(chalk.yellow(stillPresent
|
|
246
|
+
? `A legacy iCloud Keychain copy of '${name}' exists, but the local copy is unreadable and ` +
|
|
247
|
+
`import writes into it. Delete the local copy first, then recover:\n` +
|
|
248
|
+
` agents secrets delete ${name}\n` +
|
|
249
|
+
` agents secrets import ${name} --from icloud`
|
|
250
|
+
: `A legacy iCloud Keychain copy of '${name}' exists. Recover it with: agents secrets import ${name} --from icloud`));
|
|
239
251
|
}
|
|
240
252
|
catch {
|
|
241
253
|
// Hint only — never mask the original error.
|
|
@@ -383,12 +395,12 @@ async function browseRemote(targets, args, tty) {
|
|
|
383
395
|
};
|
|
384
396
|
if (tty) {
|
|
385
397
|
for (const t of targets) {
|
|
386
|
-
const target = await
|
|
398
|
+
const target = await resolveHostSshTarget(t);
|
|
387
399
|
render(t, remoteSecretsRaw(target, args, { tty: true, osLookupName: t }));
|
|
388
400
|
}
|
|
389
401
|
}
|
|
390
402
|
else {
|
|
391
|
-
const resolved = await Promise.all(targets.map(async (t) => ({ name: t, target: await
|
|
403
|
+
const resolved = await Promise.all(targets.map(async (t) => ({ name: t, target: await resolveHostSshTarget(t) })));
|
|
392
404
|
const results = resolved.map(({ name, target }) => remoteSecretsRaw(target, args, { osLookupName: name }));
|
|
393
405
|
targets.forEach((t, i) => render(t, results[i]));
|
|
394
406
|
}
|
|
@@ -871,7 +883,7 @@ export function registerSecretsCommands(program) {
|
|
|
871
883
|
}
|
|
872
884
|
catch (err) {
|
|
873
885
|
console.error(chalk.red(err.message));
|
|
874
|
-
maybePrintSyncedHint(resolvedName);
|
|
886
|
+
maybePrintSyncedHint(resolvedName, bundleExists(resolvedName));
|
|
875
887
|
process.exit(1);
|
|
876
888
|
}
|
|
877
889
|
const entries = describeBundle(bundle);
|
|
@@ -1465,16 +1477,27 @@ Examples:
|
|
|
1465
1477
|
.action(async (name, opts) => {
|
|
1466
1478
|
try {
|
|
1467
1479
|
const resolvedName = name ?? (await pickBundleName('delete'));
|
|
1468
|
-
|
|
1480
|
+
// An undecryptable bundle (lost/rotated passphrase) must still be
|
|
1481
|
+
// deletable: deletion is the ONLY way out of that state, and every other
|
|
1482
|
+
// verb — view, add, import — refuses to touch the name until it's gone.
|
|
1483
|
+
// Its key refs are unreadable, so its keychain items cannot be
|
|
1484
|
+
// enumerated for purging; say so rather than claiming a clean purge.
|
|
1485
|
+
const bundle = readBundleIfDecryptable(resolvedName);
|
|
1469
1486
|
if (!opts.yes) {
|
|
1470
1487
|
if (!isInteractiveTerminal()) {
|
|
1471
1488
|
console.error(chalk.red(`Refusing to delete '${resolvedName}' without --yes in a non-interactive shell.`));
|
|
1472
1489
|
process.exit(1);
|
|
1473
1490
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1491
|
+
let suffix;
|
|
1492
|
+
if (!bundle) {
|
|
1493
|
+
suffix = ' (unreadable — its keychain items cannot be enumerated and will be left in place)';
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
const keychainCount = describeBundle(bundle).filter((e) => e.kind === 'keychain').length;
|
|
1497
|
+
suffix = keychainCount && !opts.keepSecrets
|
|
1498
|
+
? ` and purge ${keychainCount} keychain item${keychainCount === 1 ? '' : 's'}`
|
|
1499
|
+
: '';
|
|
1500
|
+
}
|
|
1478
1501
|
const { confirm } = await import('@inquirer/prompts');
|
|
1479
1502
|
const proceed = await confirm({
|
|
1480
1503
|
message: `Delete bundle '${resolvedName}'${suffix}?`,
|
|
@@ -1485,7 +1508,7 @@ Examples:
|
|
|
1485
1508
|
return;
|
|
1486
1509
|
}
|
|
1487
1510
|
}
|
|
1488
|
-
if (!opts.keepSecrets) {
|
|
1511
|
+
if (bundle && !opts.keepSecrets) {
|
|
1489
1512
|
const store = bundleItemStore(bundle.backend);
|
|
1490
1513
|
for (const { item } of keychainItemsForBundle(bundle)) {
|
|
1491
1514
|
store.delete(item);
|
|
@@ -1583,7 +1606,7 @@ Examples:
|
|
|
1583
1606
|
}
|
|
1584
1607
|
assertValidSshTarget(opts.host);
|
|
1585
1608
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
1586
|
-
const target = await
|
|
1609
|
+
const target = await resolveHostSshTarget(opts.host);
|
|
1587
1610
|
const env = await remoteResolveEnv(target, resolvedBundleName, { osLookupName: opts.host });
|
|
1588
1611
|
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
1589
1612
|
const { added, skipped } = applyEnvToBundle(bundle, env, opts);
|
|
@@ -1848,7 +1871,7 @@ Examples:
|
|
|
1848
1871
|
// locally instead.
|
|
1849
1872
|
const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
|
|
1850
1873
|
assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
|
|
1851
|
-
secretEnv = await remoteResolveEnv(await
|
|
1874
|
+
secretEnv = await remoteResolveEnv(await resolveHostSshTarget(execOpts.host), bundleName, { osLookupName: execOpts.host });
|
|
1852
1875
|
}
|
|
1853
1876
|
else {
|
|
1854
1877
|
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
@@ -2052,7 +2075,7 @@ Examples:
|
|
|
2052
2075
|
const unlockArgs = buildRemoteUnlockArgs(names, opts);
|
|
2053
2076
|
let failures = 0;
|
|
2054
2077
|
for (const h of hosts) {
|
|
2055
|
-
const target = await
|
|
2078
|
+
const target = await resolveHostSshTarget(h);
|
|
2056
2079
|
// FOREGROUND stream (stdio inherited), NOT the piped remoteSecretsRaw:
|
|
2057
2080
|
// the remote's passphrase prompt only surfaces if the remote process
|
|
2058
2081
|
// sees a real TTY, which requires our local terminal to pass straight
|
|
@@ -214,7 +214,7 @@ function helpFor(_f, mode) {
|
|
|
214
214
|
if (mode === 'search') {
|
|
215
215
|
return 'type to filter · ↑↓ navigate · esc exit search · ⏎ resume';
|
|
216
216
|
}
|
|
217
|
-
return 's search · r running · c teams · a agent · d device · p project · w window · y copy-cmd · ⏎ resume · esc quit';
|
|
217
|
+
return 's search · r running · c teams · a agent · d device · p project · w window · tab preview · y copy-cmd · ⏎ resume · esc quit';
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
220
220
|
* Launch the interactive session browser. `initial` seeds the filter (e.g.
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
import * as fs from 'fs';
|
|
21
21
|
import * as path from 'path';
|
|
22
22
|
import chalk from 'chalk';
|
|
23
|
-
import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
|
|
23
|
+
import { discoverSessions, resolveSessionById, isCompleteSessionId } from '../lib/session/discover.js';
|
|
24
|
+
import { findSessionsById } from '../lib/session/db.js';
|
|
24
25
|
import { filterSessionsByQuery, parseAgentFilter } from './sessions.js';
|
|
25
26
|
import { listLocalTranscripts, SYNC_AGENTS } from '../lib/session/sync/agents.js';
|
|
26
27
|
import { machineId } from '../lib/machine-id.js';
|
|
@@ -228,16 +229,29 @@ function selectSessions(metas, selectors) {
|
|
|
228
229
|
const byId = [];
|
|
229
230
|
const unmatched = [];
|
|
230
231
|
for (const sel of selectors) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
const trimmed = sel.trim();
|
|
233
|
+
// Same reason as resolveSessionQuery: the discovered pool is a minority of
|
|
234
|
+
// the index, so a complete id absent from it may still be indexed here.
|
|
235
|
+
const hits = resolveSessionById(metas, trimmed);
|
|
236
|
+
const resolved = hits.length > 0 || !isCompleteSessionId(trimmed) ? hits : findSessionsById(trimmed);
|
|
237
|
+
if (resolved.length > 0)
|
|
238
|
+
byId.push(...resolved);
|
|
234
239
|
else
|
|
235
|
-
unmatched.push(
|
|
240
|
+
unmatched.push(trimmed);
|
|
236
241
|
}
|
|
237
242
|
if (byId.length > 0 && unmatched.length === 0) {
|
|
238
243
|
const seen = new Set();
|
|
239
244
|
return byId.filter(s => (seen.has(s.id) ? false : (seen.add(s.id), true)));
|
|
240
245
|
}
|
|
246
|
+
// A selector that is a COMPLETE session id and still missed cannot be widened:
|
|
247
|
+
// the id is unique, so the text query below could only bundle sessions that
|
|
248
|
+
// merely mention it. Bundling those would ship unrelated transcripts to
|
|
249
|
+
// whoever receives the export, so select nothing and let the caller report it.
|
|
250
|
+
const missingIds = unmatched.filter(isCompleteSessionId);
|
|
251
|
+
if (missingIds.length > 0) {
|
|
252
|
+
process.stderr.write(chalk.red(`No session with id ${missingIds.join(', ')} on this machine.\n`));
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
241
255
|
// Any selector that isn't an id → treat the whole thing as a text query.
|
|
242
256
|
return filterSessionsByQuery(metas, selectors.join(' '));
|
|
243
257
|
}
|