@phnx-labs/agents-cli 1.22.23 → 1.22.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/README.md +5 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.js +80 -28
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +300 -56
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +26 -11
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.js +34 -59
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +12 -0
- package/dist/lib/exec.js +88 -9
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +22 -17
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +24 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/commands/go.js
CHANGED
|
@@ -28,8 +28,9 @@ import { isPromptCancelled } from './utils.js';
|
|
|
28
28
|
import { focusAction } from './focus.js';
|
|
29
29
|
import { machineId } from '../lib/session/sync/config.js';
|
|
30
30
|
import { attachTmux, runTmux } from '../lib/tmux/binary.js';
|
|
31
|
+
import { paneExitStatus } from '../lib/tmux/session.js';
|
|
31
32
|
import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
32
|
-
import { sshStream, assertValidSshTarget, shellQuote } from '../lib/ssh-exec.js';
|
|
33
|
+
import { sshExec, sshStream, assertValidSshTarget, shellQuote } from '../lib/ssh-exec.js';
|
|
33
34
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
34
35
|
const execFileAsync = promisify(execFile);
|
|
35
36
|
export function registerGoCommand(program) {
|
|
@@ -208,15 +209,48 @@ export function describeWhere(s, self) {
|
|
|
208
209
|
return { label: `${s.host ?? 'shell'} on ${remote}`, action: `open a shell on ${remote}` };
|
|
209
210
|
return { label: s.host ?? 'unknown terminal', action: 'resume it (no live attach rail)' };
|
|
210
211
|
}
|
|
211
|
-
/**
|
|
212
|
+
/** Probe the tmux process, not just retained provenance. This is deliberately
|
|
213
|
+
* called immediately before an attach so remain-on-exit panes cannot masquerade
|
|
214
|
+
* as living agent sessions. */
|
|
215
|
+
export async function probeAttachRail(s, self) {
|
|
216
|
+
const mux = s.provenance?.mux;
|
|
217
|
+
if (mux?.kind !== 'tmux' || !mux.pane)
|
|
218
|
+
return { state: 'missing' };
|
|
219
|
+
const remote = s.machine && s.machine !== self ? s.machine : undefined;
|
|
220
|
+
if (!remote) {
|
|
221
|
+
const pane = await paneExitStatus(mux.pane, mux.socket ?? getDefaultSocketPath());
|
|
222
|
+
if (!pane.found)
|
|
223
|
+
return { state: 'missing' };
|
|
224
|
+
return pane.dead ? { state: 'dead', exitStatus: pane.status } : { state: 'alive' };
|
|
225
|
+
}
|
|
226
|
+
assertValidSshTarget(remote);
|
|
227
|
+
const sock = mux.socket ? `-S ${shellQuote(mux.socket)} ` : '';
|
|
228
|
+
const pane = shellQuote(mux.pane);
|
|
229
|
+
const command = `v=$(tmux ${sock}display-message -pt ${pane} -p '#{pane_dead} #{pane_dead_status}' 2>/dev/null) || { echo missing; exit 0; }; ` +
|
|
230
|
+
`printf '%s\\n' "$v"`;
|
|
231
|
+
const result = sshExec(remote, command, { timeoutMs: 15_000, multiplex: true });
|
|
232
|
+
if (result.code !== 0)
|
|
233
|
+
return { state: 'missing' };
|
|
234
|
+
const value = result.stdout.trim();
|
|
235
|
+
if (value === 'missing' || !value)
|
|
236
|
+
return { state: 'missing' };
|
|
237
|
+
const [dead, rawStatus] = value.split(/\s+/);
|
|
238
|
+
const exitStatus = Number.parseInt(rawStatus ?? '', 10);
|
|
239
|
+
return dead === '1'
|
|
240
|
+
? { state: 'dead', exitStatus: Number.isFinite(exitStatus) ? exitStatus : undefined }
|
|
241
|
+
: { state: 'alive' };
|
|
242
|
+
}
|
|
243
|
+
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
244
|
+
* start recovery, because both would violate the caller's no-fork intent. */
|
|
212
245
|
export async function refuseFallback(s, remote) {
|
|
213
246
|
if (remote) {
|
|
214
|
-
console.log(chalk.yellow(
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
console.log(chalk.yellow(`Can't attach ${shortId(s)} on ${remote} — it has no living tmux pane.`));
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
217
250
|
}
|
|
218
251
|
console.log(chalk.yellow(`Can't jump to ${shortId(s)} — it's in ${s.host ?? 'an unknown terminal'} with no attach rail (not tmux/Ghostty).`) +
|
|
219
252
|
chalk.gray(`\nTry: agents sessions resume ${shortId(s)}`));
|
|
253
|
+
process.exitCode = 1;
|
|
220
254
|
}
|
|
221
255
|
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
222
256
|
const remote = s.machine && s.machine !== self ? s.machine : undefined;
|
|
@@ -224,6 +258,11 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
224
258
|
// Path C: remote tmux — ssh in and attach, resolving the pane's session on the remote.
|
|
225
259
|
if (remote) {
|
|
226
260
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
261
|
+
const liveness = await probeAttachRail(s, self);
|
|
262
|
+
if (liveness.state !== 'alive') {
|
|
263
|
+
await fallback(s, remote);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
227
266
|
assertValidSshTarget(remote);
|
|
228
267
|
const sock = mux.socket ? `-S ${shellQuote(mux.socket)} ` : '';
|
|
229
268
|
const p = shellQuote(mux.pane);
|
|
@@ -240,6 +279,11 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
240
279
|
}
|
|
241
280
|
// Path B: local tmux — attach (or switch-client if we're already inside tmux).
|
|
242
281
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
282
|
+
const liveness = await probeAttachRail(s, self);
|
|
283
|
+
if (liveness.state !== 'alive') {
|
|
284
|
+
await fallback(s, undefined);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
243
287
|
const socket = mux.socket ?? getDefaultSocketPath();
|
|
244
288
|
const { session, window } = await resolveLocalPane(socket, mux.pane);
|
|
245
289
|
if (session && window != null) {
|
|
@@ -340,7 +340,10 @@ async function insightsAction(options) {
|
|
|
340
340
|
console.error(chalk.red('error: --min-messages must be a non-negative integer'));
|
|
341
341
|
process.exit(1);
|
|
342
342
|
}
|
|
343
|
-
|
|
343
|
+
// `--all` is the spelling people reach for; `--since all` is what the window parser
|
|
344
|
+
// speaks. Accept both rather than failing on an unknown option, and let an explicit
|
|
345
|
+
// --since win so `--all --since 7d` is not silently contradictory.
|
|
346
|
+
const since = options.since ?? (options.all ? 'all' : '30d');
|
|
344
347
|
const sinceMs = since === 'all' ? undefined : parseTimeFilter(since);
|
|
345
348
|
// Refresh the index first, exactly as `agents cost` does, so a report never silently
|
|
346
349
|
// describes a stale picture of disk.
|
|
@@ -436,6 +439,7 @@ export function registerInsightsCommand(program) {
|
|
|
436
439
|
.description('How you work — tools, friction, and rhythm, split by the account that did the work')
|
|
437
440
|
.option('--json', 'Output the full report as JSON')
|
|
438
441
|
.option('--since <time>', 'Window: 7d, 4w, 3mo, an ISO date, or "all" (default 30d)')
|
|
442
|
+
.option('--all', 'Every session ever indexed. Alias for --since all')
|
|
439
443
|
.option('--by <dimension>', 'Group by: account (default), agent, project, or day')
|
|
440
444
|
.option('--account <match>', 'Only sessions whose account key, email, or org contains this')
|
|
441
445
|
.option('--agent <id>', 'Only one harness (claude, codex, droid, …)')
|
|
@@ -454,7 +458,7 @@ export function registerInsightsCommand(program) {
|
|
|
454
458
|
agents insights --by project --since 90d
|
|
455
459
|
|
|
456
460
|
# One account only, all of its history
|
|
457
|
-
agents insights --account "Turing Labs" --
|
|
461
|
+
agents insights --account "Turing Labs" --all
|
|
458
462
|
|
|
459
463
|
# Machine-readable, for a dashboard or a slash command
|
|
460
464
|
agents insights --json
|
package/dist/commands/inspect.js
CHANGED
|
@@ -22,12 +22,14 @@ import * as yaml from 'yaml';
|
|
|
22
22
|
import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
|
|
23
23
|
import { supports } from '../lib/capabilities.js';
|
|
24
24
|
import { resolveConfiguredModel } from '../lib/models.js';
|
|
25
|
+
import { getAgentModesCatalog } from '../lib/agent-modes.js';
|
|
25
26
|
import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
26
27
|
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
27
28
|
import { getVersionHomePath, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
|
|
28
29
|
import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
|
|
29
30
|
import { getAgentResources, listResources, } from '../lib/resources.js';
|
|
30
31
|
import { listHookEntriesFromDir } from '../lib/hooks.js';
|
|
32
|
+
import { getResourceInventory } from '../lib/resource-inventory.js';
|
|
31
33
|
import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js';
|
|
32
34
|
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
|
|
33
35
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
@@ -113,7 +115,7 @@ function truncateValueForPrefix(prefix, value) {
|
|
|
113
115
|
// ─── Command registration ────────────────────────────────────────────────────
|
|
114
116
|
export function registerInspectCommand(program) {
|
|
115
117
|
const cmd = addHostOption(program.command('inspect <target>'))
|
|
116
|
-
.description('Inspect one installed agent at one version, or a DotAgents repo
|
|
118
|
+
.description('Inspect one installed agent harness at one version (not a model), or a DotAgents repo — paths, capabilities, resources, and hook capable/on-disk/wired state.')
|
|
117
119
|
.option('--brief', 'header + capabilities only; skip resources/sessions')
|
|
118
120
|
.option('--json', 'machine-readable JSON output');
|
|
119
121
|
for (const kind of DRILLABLE_KINDS) {
|
|
@@ -575,12 +577,14 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
575
577
|
const isIsolatedDefault = isolated && getIsolatedDefault(agent) === version;
|
|
576
578
|
const capabilities = collectCapabilities(agent, version);
|
|
577
579
|
const itemsByKind = options.brief ? null : collectItemsByKind(agent, versionHome);
|
|
580
|
+
const hookInventory = options.brief ? null : getResourceInventory(agent, version, 'hooks');
|
|
578
581
|
const hookByScript = options.brief ? null : hookManifestByScript(loadCentralHookManifest());
|
|
579
582
|
const mcpConfigs = options.brief ? null : new Map(listMcpServerConfigs().map(s => [s.name, s.config]));
|
|
580
583
|
const sessions = options.brief ? null : {
|
|
581
584
|
total: safeCountSessions(agent),
|
|
582
585
|
};
|
|
583
586
|
if (options.json) {
|
|
587
|
+
const modeCat = getAgentModesCatalog(agent, version);
|
|
584
588
|
const json = {
|
|
585
589
|
agent,
|
|
586
590
|
version,
|
|
@@ -594,7 +598,14 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
594
598
|
strategy,
|
|
595
599
|
installedShim: cliState?.installed === true ? cliState.path : null,
|
|
596
600
|
capabilities,
|
|
597
|
-
|
|
601
|
+
modes: {
|
|
602
|
+
supported: modeCat.modes.map((m) => m.mode),
|
|
603
|
+
defaultMode: modeCat.defaultMode,
|
|
604
|
+
configuredMode: modeCat.configuredMode,
|
|
605
|
+
headlessPlan: modeCat.headlessPlan,
|
|
606
|
+
unsupported: modeCat.unsupported,
|
|
607
|
+
},
|
|
608
|
+
resources: itemsByKind ? summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs, hookInventory) : null,
|
|
598
609
|
sessions,
|
|
599
610
|
};
|
|
600
611
|
console.log(JSON.stringify(json, null, 2));
|
|
@@ -623,12 +634,23 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
623
634
|
const reason = res.ok ? '' : chalk.gray(`(${res.reason}${res.need ? ' ' + res.need : ''})`);
|
|
624
635
|
console.log(` ${cap.padEnd(10)} ${mark} ${reason}`);
|
|
625
636
|
}
|
|
637
|
+
// Permission modes are a separate axis from the capability booleans above —
|
|
638
|
+
// same data as `agents modes <agent>`, kept next to the rest of the inspect
|
|
639
|
+
// surface so a human/agent does not have to leave this view.
|
|
640
|
+
{
|
|
641
|
+
const modeCat = getAgentModesCatalog(agent, version);
|
|
642
|
+
const modeList = modeCat.modes.map((m) => (m.isDefault ? `${m.mode}*` : m.mode)).join('/');
|
|
643
|
+
const cfg = modeCat.configuredMode ? chalk.gray(` run default: ${modeCat.configuredMode}`) : '';
|
|
644
|
+
console.log(` ${'modes'.padEnd(10)} ${chalk.cyan(modeList)}${cfg}`);
|
|
645
|
+
console.log(chalk.gray(` agents modes ${agent} · agents models ${agent}@${version}`));
|
|
646
|
+
}
|
|
626
647
|
if (itemsByKind) {
|
|
627
648
|
console.log('\n' + chalk.bold('Resources'));
|
|
628
649
|
for (const kind of SIMPLE_KINDS) {
|
|
629
650
|
printSimpleResourceRow(kind, itemsByKind[kind]);
|
|
630
651
|
}
|
|
631
|
-
|
|
652
|
+
console.log(` ${chalk.bold('Hooks')} ${chalk.gray(formatHookInventoryCounts(hookInventory))}`);
|
|
653
|
+
printExpandedSection('Hook files', hookRows(itemsByKind.hooks, hookByScript));
|
|
632
654
|
printExpandedSection('Plugins', pluginRows(itemsByKind.plugins));
|
|
633
655
|
printExpandedSection('MCP', mcpRows(itemsByKind.mcp, mcpConfigs));
|
|
634
656
|
}
|
|
@@ -769,13 +791,20 @@ function printSimpleResourceRow(kind, items) {
|
|
|
769
791
|
* simple kinds add `names`, and the rich kinds add structured `items` (hook
|
|
770
792
|
* events/predicates, mcp transport/url/command, plugin version + group counts).
|
|
771
793
|
*/
|
|
772
|
-
function summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs) {
|
|
794
|
+
function summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs, hookInventory) {
|
|
773
795
|
const out = {};
|
|
774
796
|
for (const kind of DRILLABLE_KINDS) {
|
|
775
797
|
const items = itemsByKind[kind];
|
|
776
798
|
const base = { total: items.length, bySource: countBySource(items.map(i => i.source)) };
|
|
777
799
|
if (kind === 'hooks') {
|
|
778
|
-
out[kind] = { ...base,
|
|
800
|
+
out[kind] = { ...base,
|
|
801
|
+
capable: hookInventory.capable,
|
|
802
|
+
declared: hookInventory.declared,
|
|
803
|
+
onDisk: hookInventory.onDisk,
|
|
804
|
+
wired: hookInventory.wired,
|
|
805
|
+
unmanaged: hookInventory.unmanaged,
|
|
806
|
+
wiringSupported: hookInventory.wiringSupported,
|
|
807
|
+
items: items.map(i => {
|
|
779
808
|
const h = hookByScript.get(i.name);
|
|
780
809
|
return { name: i.name, source: i.source, events: h?.events ?? [], matcher: h?.matcher, matches: h?.matches, cache: h?.cache };
|
|
781
810
|
}) };
|
|
@@ -800,6 +829,11 @@ function summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs) {
|
|
|
800
829
|
}
|
|
801
830
|
return out;
|
|
802
831
|
}
|
|
832
|
+
function formatHookInventoryCounts(inventory) {
|
|
833
|
+
const wired = inventory.wiringSupported ? String(inventory.wired.length) : 'unknown';
|
|
834
|
+
const unmanaged = inventory.unmanaged.length > 0 ? ` · unmanaged ${inventory.unmanaged.length}` : '';
|
|
835
|
+
return `capable ${inventory.capable ? 'yes' : 'no'} · on-disk ${inventory.onDisk.length} · wired ${wired}${unmanaged}`;
|
|
836
|
+
}
|
|
803
837
|
function collectKind(agent, versionHome, kind) {
|
|
804
838
|
switch (kind) {
|
|
805
839
|
case 'commands':
|
package/dist/commands/menubar.js
CHANGED
|
@@ -51,7 +51,12 @@ function printStatus(s, opts = {}) {
|
|
|
51
51
|
console.log(chalk.gray(' End them with `agents menubar setup`.'));
|
|
52
52
|
}
|
|
53
53
|
if (s.stale) {
|
|
54
|
-
|
|
54
|
+
// Not "runs on next startup": the self-heal only reinstalls from the install
|
|
55
|
+
// that owns the helper, or from another one once the takeover cooldown has
|
|
56
|
+
// passed (mayInstallMenubarHelper) — so on a box with several agents-cli
|
|
57
|
+
// copies this can persist for a while. `setup` bypasses the gate and is the
|
|
58
|
+
// immediate fix.
|
|
59
|
+
console.log(chalk.yellow('\n Installed AGI Menu is stale — `agents menubar setup` updates it now.'));
|
|
55
60
|
}
|
|
56
61
|
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
57
62
|
console.log(chalk.gray('\n Set it up with `agents menubar setup`.'));
|
package/dist/commands/models.js
CHANGED
|
@@ -72,6 +72,7 @@ export function registerModelsCommand(program) {
|
|
|
72
72
|
if (!agentSpec && !all) {
|
|
73
73
|
console.log(chalk.gray('\n `agents models <agent>` for one harness · `--all` for the full model list'));
|
|
74
74
|
console.log(chalk.gray(' `agents models tier set <agent> <tier> <model>` to override a tier'));
|
|
75
|
+
console.log(chalk.gray(' Permission modes (--mode plan|edit|auto|skip): `agents modes <agent>`'));
|
|
75
76
|
}
|
|
76
77
|
});
|
|
77
78
|
// Override subcommands. These WRITE agents.yaml so the user never hand-edits it;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents modes` — list the permission modes a harness accepts for
|
|
3
|
+
* `agents run` / `agents teams add`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of `agents models`: humans and orchestrating agents read this before
|
|
6
|
+
* picking `--mode plan|edit|auto|skip`. Modes are per-agent today (not version-
|
|
7
|
+
* gated); `agent@version` is accepted so the configured run.defaults mode for
|
|
8
|
+
* that version can be shown beside the catalog.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/** Register `agents modes [agent[@version]]`. */
|
|
12
|
+
export declare function registerModesCommand(program: Command): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents modes` — list the permission modes a harness accepts for
|
|
3
|
+
* `agents run` / `agents teams add`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of `agents models`: humans and orchestrating agents read this before
|
|
6
|
+
* picking `--mode plan|edit|auto|skip`. Modes are per-agent today (not version-
|
|
7
|
+
* gated); `agent@version` is accepted so the configured run.defaults mode for
|
|
8
|
+
* that version can be shown beside the catalog.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { AGENTS, ALL_AGENT_IDS, agentLabel, formatAgentError, resolveAgentName, } from '../lib/agents.js';
|
|
12
|
+
import { formatModeFlags, getAgentModesCatalog, } from '../lib/agent-modes.js';
|
|
13
|
+
import { setHelpSections } from '../lib/help.js';
|
|
14
|
+
import { getGlobalDefault, listInstalledVersions, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
|
|
15
|
+
/** Agents that show in the no-arg overview (skip hard-deprecated). */
|
|
16
|
+
const MODE_AGENTS = ALL_AGENT_IDS.filter((id) => !AGENTS[id].deprecated?.hard);
|
|
17
|
+
/** Register `agents modes [agent[@version]]`. */
|
|
18
|
+
export function registerModesCommand(program) {
|
|
19
|
+
const modes = program
|
|
20
|
+
.command('modes [agentSpec]')
|
|
21
|
+
.description('Show which permission modes (--mode plan|edit|auto|skip) a harness supports for agents run / teams add')
|
|
22
|
+
.option('--json', 'Output machine-readable JSON')
|
|
23
|
+
.action((agentSpec, options) => {
|
|
24
|
+
const targets = resolveTargets(agentSpec);
|
|
25
|
+
if (targets.length === 0)
|
|
26
|
+
process.exit(1);
|
|
27
|
+
if (options.json) {
|
|
28
|
+
const out = targets.map(({ agent, version }) => {
|
|
29
|
+
const catalog = getAgentModesCatalog(agent, version);
|
|
30
|
+
return {
|
|
31
|
+
agent,
|
|
32
|
+
version,
|
|
33
|
+
defaultMode: catalog.defaultMode,
|
|
34
|
+
configuredMode: catalog.configuredMode,
|
|
35
|
+
configuredModeSource: catalog.configuredModeSource,
|
|
36
|
+
headlessPlan: catalog.headlessPlan,
|
|
37
|
+
modes: catalog.modes.map((m) => ({
|
|
38
|
+
mode: m.mode,
|
|
39
|
+
flags: m.flags,
|
|
40
|
+
description: m.description,
|
|
41
|
+
isDefault: m.isDefault,
|
|
42
|
+
})),
|
|
43
|
+
unsupported: catalog.unsupported,
|
|
44
|
+
notes: catalog.notes,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
console.log(JSON.stringify(out, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let printed = 0;
|
|
51
|
+
for (const { agent, version, isDefault } of targets) {
|
|
52
|
+
if (printed > 0)
|
|
53
|
+
console.log();
|
|
54
|
+
printCatalog(getAgentModesCatalog(agent, version), version, isDefault);
|
|
55
|
+
printed++;
|
|
56
|
+
}
|
|
57
|
+
if (!agentSpec) {
|
|
58
|
+
console.log(chalk.gray('\n `agents modes <agent>` for one harness · `agents modes claude@2.1.219` for a version\'s run default'));
|
|
59
|
+
console.log(chalk.gray(' Pass --mode on `agents run` / `agents teams add`. Models: `agents models <agent>`.'));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
setHelpSections(modes, {
|
|
63
|
+
examples: `
|
|
64
|
+
agents modes
|
|
65
|
+
agents modes claude
|
|
66
|
+
agents modes claude@2.1.219
|
|
67
|
+
agents modes cursor --json
|
|
68
|
+
`,
|
|
69
|
+
notes: `
|
|
70
|
+
Permission modes control how much the agent can do (not which LLM model
|
|
71
|
+
runs — that is --model / agents models). Supported modes come from each
|
|
72
|
+
harness's capabilities table; unsupported requests degrade (auto→edit,
|
|
73
|
+
plan→safest native) or error (skip when missing).
|
|
74
|
+
'full' is a silent alias for skip.
|
|
75
|
+
agent@version is accepted so any configured run.defaults mode for that
|
|
76
|
+
version is shown; the mode list itself is per-agent today.
|
|
77
|
+
`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function resolveTargets(agentSpec) {
|
|
81
|
+
if (!agentSpec) {
|
|
82
|
+
const targets = [];
|
|
83
|
+
for (const agent of MODE_AGENTS) {
|
|
84
|
+
const version = getGlobalDefault(agent) || listInstalledVersions(agent)[0] || null;
|
|
85
|
+
targets.push({ agent, version, isDefault: true });
|
|
86
|
+
}
|
|
87
|
+
return targets;
|
|
88
|
+
}
|
|
89
|
+
const [agentName, versionSpec] = agentSpec.split('@');
|
|
90
|
+
const agent = resolveAgentName(agentName);
|
|
91
|
+
if (!agent) {
|
|
92
|
+
console.error(chalk.red(formatAgentError(agentName, MODE_AGENTS)));
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (AGENTS[agent].deprecated?.hard) {
|
|
96
|
+
console.error(chalk.yellow(`${agent} is deprecated and no longer supports run modes.`));
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
if (versionSpec === 'all') {
|
|
100
|
+
const installed = listInstalledVersions(agent);
|
|
101
|
+
if (installed.length === 0) {
|
|
102
|
+
return [{ agent, version: null, isDefault: true }];
|
|
103
|
+
}
|
|
104
|
+
return installed.map((v) => ({
|
|
105
|
+
agent,
|
|
106
|
+
version: v,
|
|
107
|
+
isDefault: v === getGlobalDefault(agent),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
if (versionSpec) {
|
|
111
|
+
// resolveVersionAlias exits process if the concrete version is not installed.
|
|
112
|
+
const version = resolveVersionAlias(agent, versionSpec) ?? null;
|
|
113
|
+
return [{ agent, version, isDefault: version === getGlobalDefault(agent) }];
|
|
114
|
+
}
|
|
115
|
+
const version = resolveVersion(agent, process.cwd()) || getGlobalDefault(agent) || null;
|
|
116
|
+
return [{ agent, version, isDefault: true }];
|
|
117
|
+
}
|
|
118
|
+
function printCatalog(catalog, version, isDefault) {
|
|
119
|
+
const tag = version
|
|
120
|
+
? ` ${chalk.bold(version)}${isDefault ? chalk.gray(' (default)') : ''}`
|
|
121
|
+
: chalk.gray(' (not installed)');
|
|
122
|
+
console.log(`${agentLabel(catalog.agent)}${tag}`);
|
|
123
|
+
console.log(chalk.gray(' modes:'));
|
|
124
|
+
for (const entry of catalog.modes) {
|
|
125
|
+
const star = entry.isDefault ? chalk.cyan('*') : ' ';
|
|
126
|
+
const flags = chalk.gray(formatModeFlags(entry.flags));
|
|
127
|
+
console.log(` ${star} ${chalk.cyan(entry.mode.padEnd(6))} ${chalk.bold(entry.description)} ${flags}`);
|
|
128
|
+
}
|
|
129
|
+
if (catalog.unsupported.length > 0) {
|
|
130
|
+
console.log(chalk.gray(` unsupported: ${catalog.unsupported.join(', ')}`));
|
|
131
|
+
}
|
|
132
|
+
const defaultBits = [`native default ${chalk.white(catalog.defaultMode)}`];
|
|
133
|
+
if (catalog.configuredMode) {
|
|
134
|
+
const src = catalog.configuredModeSource ? chalk.gray(` (${catalog.configuredModeSource})`) : '';
|
|
135
|
+
defaultBits.push(`run default ${chalk.white(catalog.configuredMode)}${src}`);
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.gray(` ${defaultBits.join(' · ')}`));
|
|
138
|
+
if (!catalog.headlessPlan && catalog.modes.some((m) => m.mode === 'plan')) {
|
|
139
|
+
console.log(chalk.yellow(' headless plan: not supported (interactive plan still works)'));
|
|
140
|
+
}
|
|
141
|
+
for (const note of catalog.notes) {
|
|
142
|
+
// Keep the full-alias note quiet; surface degrades more visibly.
|
|
143
|
+
if (note.startsWith("'full'"))
|
|
144
|
+
continue;
|
|
145
|
+
console.log(chalk.gray(` note: ${note}`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -10,9 +10,10 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { listRemoteBundles, MIN_PASSPHRASE_LEN, pullBundle, pushBundle, } from '../lib/secrets/sync.js';
|
|
11
11
|
import { bundleExists, listBundles } from '../lib/secrets/bundles.js';
|
|
12
12
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
13
|
+
import { missingSyncPassphraseMessage, resolveSyncPassphraseFromEnv, warnEnvPassphraseReadableOnce, } from '../lib/secrets/sync-passphrase.js';
|
|
13
14
|
async function promptPassphrase(message, confirm = false) {
|
|
14
15
|
if (!isInteractiveTerminal()) {
|
|
15
|
-
throw new Error(
|
|
16
|
+
throw new Error(missingSyncPassphraseMessage());
|
|
16
17
|
}
|
|
17
18
|
const { password } = await import('@inquirer/prompts');
|
|
18
19
|
const first = await password({ message, mask: true });
|
|
@@ -25,21 +26,18 @@ async function promptPassphrase(message, confirm = false) {
|
|
|
25
26
|
throw new Error('Passphrases do not match.');
|
|
26
27
|
return first;
|
|
27
28
|
}
|
|
28
|
-
// Print the env-var-source warning at most once per process so a `--all` push
|
|
29
|
-
// over many bundles doesn't flood stderr with the same notice.
|
|
30
|
-
let envPassphraseWarned = false;
|
|
31
29
|
function passphraseFromEnvOrPrompt(confirm) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Env resolution (current name, then the deprecated master-key name) lives in
|
|
31
|
+
// lib/secrets/sync-passphrase.ts so this command and `agents sync` share one
|
|
32
|
+
// chokepoint — otherwise each call site warns separately and the "once per
|
|
33
|
+
// process" promise is a lie.
|
|
34
|
+
const { value } = resolveSyncPassphraseFromEnv();
|
|
35
|
+
if (value) {
|
|
36
|
+
if (value.length < MIN_PASSPHRASE_LEN) {
|
|
35
37
|
return Promise.reject(new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`));
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process.stderr.write(chalk.yellow('warn: using AGENTS_SECRETS_PASSPHRASE. Env vars are readable by other same-user processes ' +
|
|
40
|
-
'(/proc, ps, crash dumps, CI logs) — rotate the passphrase after CI use.\n'));
|
|
41
|
-
}
|
|
42
|
-
return Promise.resolve(fromEnv);
|
|
39
|
+
warnEnvPassphraseReadableOnce();
|
|
40
|
+
return Promise.resolve(value);
|
|
43
41
|
}
|
|
44
42
|
return promptPassphrase('Sync passphrase', confirm);
|
|
45
43
|
}
|
|
@@ -48,9 +48,11 @@ export declare function parseImportSource(opts: {
|
|
|
48
48
|
* defaults). Shared with the command action so the wiring is unit-testable
|
|
49
49
|
* without a live SSH session.
|
|
50
50
|
*/
|
|
51
|
+
export declare function resolveUnlockTtlMs(ttl: string | undefined, until: string | undefined, now?: number): number;
|
|
51
52
|
export declare function buildRemoteUnlockArgs(names: string[], opts: {
|
|
52
53
|
all?: boolean;
|
|
53
54
|
ttl?: string;
|
|
55
|
+
until?: string;
|
|
54
56
|
durable?: boolean;
|
|
55
57
|
}): string[];
|
|
56
58
|
/**
|
package/dist/commands/secrets.js
CHANGED
|
@@ -31,6 +31,7 @@ import { readMeta } from '../lib/state.js';
|
|
|
31
31
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
32
32
|
import { emit, query } from '../lib/events.js';
|
|
33
33
|
import { emitSecretAudit } from '../lib/secrets/audit.js';
|
|
34
|
+
import { SYNC_PASSPHRASE_ENV, resolveSyncPassphraseFromEnv } from '../lib/secrets/sync-passphrase.js';
|
|
34
35
|
import { getBundleUsage, getAllBundleUsage, getUsageHistory, SECRET_USAGE_EVENTS, } from '../lib/secrets/usage-db.js';
|
|
35
36
|
import { frequentlyPromptedBundles } from '../lib/secrets/unlock-hints.js';
|
|
36
37
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
@@ -266,11 +267,31 @@ function maybePrintSyncedHint(name, stillPresent) {
|
|
|
266
267
|
* defaults). Shared with the command action so the wiring is unit-testable
|
|
267
268
|
* without a live SSH session.
|
|
268
269
|
*/
|
|
270
|
+
export function resolveUnlockTtlMs(ttl, until, now = Date.now()) {
|
|
271
|
+
if (ttl && until)
|
|
272
|
+
throw new Error('--ttl and --until are mutually exclusive.');
|
|
273
|
+
if (until) {
|
|
274
|
+
const expiresAt = Date.parse(until);
|
|
275
|
+
if (!Number.isFinite(expiresAt))
|
|
276
|
+
throw new Error("Invalid --until '" + until + "'. Use an ISO date or timestamp.");
|
|
277
|
+
if (expiresAt <= now)
|
|
278
|
+
throw new Error("Invalid --until '" + until + "': date must be in the future.");
|
|
279
|
+
return expiresAt - now;
|
|
280
|
+
}
|
|
281
|
+
if (ttl) {
|
|
282
|
+
const secs = parseDuration(ttl);
|
|
283
|
+
if (!secs)
|
|
284
|
+
throw new Error("Invalid --ttl '" + ttl + "'. Use e.g. 30m, 2h, 8h, 3d.");
|
|
285
|
+
return secs * 1000;
|
|
286
|
+
}
|
|
287
|
+
return secretsHoldMs();
|
|
288
|
+
}
|
|
269
289
|
export function buildRemoteUnlockArgs(names, opts) {
|
|
270
290
|
return [
|
|
271
291
|
'unlock',
|
|
272
292
|
...(opts.all ? ['--all'] : names),
|
|
273
293
|
...(opts.ttl ? ['--ttl', opts.ttl] : []),
|
|
294
|
+
...(opts.until ? ['--until', opts.until] : []),
|
|
274
295
|
// Forward --durable so a remote unlock honors it too; without this the remote
|
|
275
296
|
// silently falls back to its own secrets.agent.durable default (off).
|
|
276
297
|
...(opts.durable ? ['--durable'] : []),
|
|
@@ -1957,7 +1978,7 @@ Examples:
|
|
|
1957
1978
|
.option('--synced', 'When creating the bundle, store it in the age-encrypted synced secrets file')
|
|
1958
1979
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
1959
1980
|
.option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
|
|
1960
|
-
.option('--from-file <path>',
|
|
1981
|
+
.option('--from-file <path>', `Import from an AES-256-GCM encrypted offline bundle file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of export --to-file)`)
|
|
1961
1982
|
.option('--from-ssh', 'Pull the bundle from a fleet peer over SSH and import it locally (requires --host)')
|
|
1962
1983
|
.option('--host <peer>', 'SSH peer to pull from when using --from-ssh (host alias or user@host)')
|
|
1963
1984
|
.action(async (bundleName, opts) => {
|
|
@@ -1973,9 +1994,10 @@ Examples:
|
|
|
1973
1994
|
throw new Error('--from-file, --from-ssh, and --from/--from-1password are mutually exclusive; pick one import source.');
|
|
1974
1995
|
}
|
|
1975
1996
|
if (opts.fromFile) {
|
|
1976
|
-
|
|
1997
|
+
// Transport, not the local store's master key — see lib/secrets/sync-passphrase.ts.
|
|
1998
|
+
const passphrase = resolveSyncPassphraseFromEnv().value ?? '';
|
|
1977
1999
|
if (!passphrase) {
|
|
1978
|
-
throw new Error(
|
|
2000
|
+
throw new Error(`--from-file needs ${SYNC_PASSPHRASE_ENV} set to decrypt the bundle file.`);
|
|
1979
2001
|
}
|
|
1980
2002
|
const env = importBundleFromFile(opts.fromFile, passphrase);
|
|
1981
2003
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
@@ -2063,7 +2085,7 @@ Examples:
|
|
|
2063
2085
|
.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')
|
|
2064
2086
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
2065
2087
|
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
2066
|
-
.option('--to-file <path>',
|
|
2088
|
+
.option('--to-file <path>', `Write the bundle as an AES-256-GCM encrypted offline file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of import --from-file)`)
|
|
2067
2089
|
.action(async (bundleName, opts) => {
|
|
2068
2090
|
try {
|
|
2069
2091
|
// `--device` is an alias for `--host` (fleet vocabulary parity with
|
|
@@ -2074,9 +2096,10 @@ Examples:
|
|
|
2074
2096
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
2075
2097
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
2076
2098
|
if (opts.toFile) {
|
|
2077
|
-
|
|
2099
|
+
// Transport, not the local store's master key — see lib/secrets/sync-passphrase.ts.
|
|
2100
|
+
const passphrase = resolveSyncPassphraseFromEnv().value ?? '';
|
|
2078
2101
|
if (!passphrase) {
|
|
2079
|
-
throw new Error(
|
|
2102
|
+
throw new Error(`--to-file needs ${SYNC_PASSPHRASE_ENV} set to encrypt the bundle. ` +
|
|
2080
2103
|
'Set it for this command, then supply the same value when importing.');
|
|
2081
2104
|
}
|
|
2082
2105
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', keyMode: 'storage', agentOnly: true });
|
|
@@ -2483,11 +2506,16 @@ Examples:
|
|
|
2483
2506
|
.command('unlock [names...]')
|
|
2484
2507
|
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
2485
2508
|
.option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
|
|
2509
|
+
.option('--until <date>', 'Hold until this absolute date or timestamp (for example 2026-08-06T12:00:00Z). Mutually exclusive with --ttl.')
|
|
2486
2510
|
.option('--durable', 'Keep the unlock across sleep + reboot too (default: survives upgrade/restart but re-locks on sleep). Set secrets.agent.durable in agents.yaml to make this the default.')
|
|
2487
2511
|
.option('--for <agent>', 'Narrow the unlock to ONE harness type (for example claude, codex, or kimi). Default: the grant is global — every harness and a plain shell can read it, so one Touch ID covers them all.')
|
|
2488
2512
|
.option('--all', 'Unlock every configured bundle')
|
|
2489
2513
|
.option('--host <target>', 'Unlock the bundle(s) on this remote machine over SSH instead of locally (file-backed bundles only — the remote\'s passphrase prompt surfaces on your terminal over a -tt session). Single-valued (NOT variadic) so it never swallows the bundle name: `unlock <name> --host <machine>`.')
|
|
2490
2514
|
.action(async (names, opts) => {
|
|
2515
|
+
if (opts.ttl && opts.until) {
|
|
2516
|
+
console.error(chalk.red('--ttl and --until are mutually exclusive.'));
|
|
2517
|
+
process.exit(1);
|
|
2518
|
+
}
|
|
2491
2519
|
// Single-valued (not variadic): a variadic --host greedily consumes the
|
|
2492
2520
|
// positional bundle name (`unlock --host mac wztest` -> host=[mac,wztest],
|
|
2493
2521
|
// names=[]). Unlock targets one remote at a time anyway.
|
|
@@ -2524,6 +2552,15 @@ Examples:
|
|
|
2524
2552
|
process.exit(1);
|
|
2525
2553
|
return;
|
|
2526
2554
|
}
|
|
2555
|
+
if (opts.until) {
|
|
2556
|
+
try {
|
|
2557
|
+
resolveUnlockTtlMs(undefined, opts.until);
|
|
2558
|
+
}
|
|
2559
|
+
catch (err) {
|
|
2560
|
+
console.error(chalk.red(err.message));
|
|
2561
|
+
process.exit(1);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2527
2564
|
if (process.platform !== 'darwin') {
|
|
2528
2565
|
// No broker + no biometry prompt off darwin: secrets already resolve
|
|
2529
2566
|
// durably from the OS store (libsecret / Credential Manager) on every
|
|
@@ -2537,15 +2574,15 @@ Examples:
|
|
|
2537
2574
|
console.error(chalk.red('Specify one or more bundle names, or --all.'));
|
|
2538
2575
|
process.exit(1);
|
|
2539
2576
|
}
|
|
2540
|
-
let ttlMs
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
ttlMs = secs * 1000;
|
|
2577
|
+
let ttlMs;
|
|
2578
|
+
try {
|
|
2579
|
+
ttlMs = resolveUnlockTtlMs(opts.ttl, opts.until);
|
|
2580
|
+
}
|
|
2581
|
+
catch (err) {
|
|
2582
|
+
console.error(chalk.red(err.message));
|
|
2583
|
+
process.exit(1);
|
|
2548
2584
|
}
|
|
2585
|
+
const expiresAt = Date.now() + ttlMs;
|
|
2549
2586
|
if (!(await ensureAgentRunning())) {
|
|
2550
2587
|
console.error(chalk.red('Could not start the secrets broker.'));
|
|
2551
2588
|
process.exit(1);
|
|
@@ -2571,7 +2608,7 @@ Examples:
|
|
|
2571
2608
|
noAgent: true,
|
|
2572
2609
|
caller: 'unlock secrets',
|
|
2573
2610
|
agent: harness,
|
|
2574
|
-
duration: humanRemaining(
|
|
2611
|
+
duration: humanRemaining(expiresAt),
|
|
2575
2612
|
keyMode: 'storage',
|
|
2576
2613
|
});
|
|
2577
2614
|
// Migrations are authorized only by this explicit unlock. The bundle
|
|
@@ -2587,7 +2624,7 @@ Examples:
|
|
|
2587
2624
|
saveSession(name, {
|
|
2588
2625
|
bundle,
|
|
2589
2626
|
env,
|
|
2590
|
-
expiresAt
|
|
2627
|
+
expiresAt,
|
|
2591
2628
|
sleepPersist: durable,
|
|
2592
2629
|
harness,
|
|
2593
2630
|
});
|
|
@@ -2606,7 +2643,7 @@ Examples:
|
|
|
2606
2643
|
agent: harness,
|
|
2607
2644
|
ttlMs,
|
|
2608
2645
|
});
|
|
2609
|
-
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(
|
|
2646
|
+
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(expiresAt)})`)}`);
|
|
2610
2647
|
}
|
|
2611
2648
|
else {
|
|
2612
2649
|
console.error(chalk.red(`Failed to load '${name}' into the agent.`));
|