@phnx-labs/agents-cli 1.22.30 → 1.22.32
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 +72 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +164 -36
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +59 -93
- package/dist/commands/{sessions-favorite.d.ts → sessions-bookmark.d.ts} +7 -7
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +38 -38
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +11 -9
- package/dist/commands/sessions.js +114 -65
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +4 -2
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +118 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +0 -1
- package/dist/lib/secrets/agent.js +0 -4
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
- package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
package/dist/commands/focus.js
CHANGED
|
@@ -32,7 +32,7 @@ const INHERITED_FOCUS_OPTIONS = [
|
|
|
32
32
|
'local', 'host', 'device', 'active', 'agent',
|
|
33
33
|
'claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode',
|
|
34
34
|
'all', 'teams', 'inTeam', 'routine', 'project', 'skill', 'plugin',
|
|
35
|
-
'since', 'until', 'limit', '
|
|
35
|
+
'since', 'until', 'limit', 'bookmarks', 'unmanaged', 'sort',
|
|
36
36
|
'working', 'idle', 'waiting', 'orphan', 'orphaned', 'crashed',
|
|
37
37
|
'closed', 'abandoned', 'queued', 'unknown',
|
|
38
38
|
];
|
|
@@ -93,7 +93,7 @@ export function registerFocusCommand(program) {
|
|
|
93
93
|
.option('--since <time>', 'Only sessions newer than this (for example 7d)')
|
|
94
94
|
.option('--until <time>', 'Only sessions older than this timestamp')
|
|
95
95
|
.option('-n, --limit <n>', 'Maximum candidates to load', '500')
|
|
96
|
-
.option('--
|
|
96
|
+
.option('--bookmarks', 'Only bookmarked sessions')
|
|
97
97
|
.option('--unmanaged', 'Also include native-home sessions outside managed versions')
|
|
98
98
|
.option('--sort <field>', 'Order candidates by recent, cost, or duration', 'recent')
|
|
99
99
|
.option('--working', 'Only live sessions currently doing work')
|
|
@@ -184,7 +184,7 @@ export async function focusAction(id, opts) {
|
|
|
184
184
|
agent: agentSelector ?? focusOptionAgent(opts),
|
|
185
185
|
teams: opts.teams === true,
|
|
186
186
|
team: opts.inTeam,
|
|
187
|
-
|
|
187
|
+
bookmarks: opts.bookmarks === true,
|
|
188
188
|
projectScope: opts.all || hosts.length > 0 || !!opts.project || idLookup ? 'all' : 'repo',
|
|
189
189
|
project: opts.project,
|
|
190
190
|
window: opts.since ?? (opts.all || idLookup ? undefined : '30d'),
|
|
@@ -304,7 +304,7 @@ function looksLikeIdentitySelector(selector) {
|
|
|
304
304
|
function hasFocusFilters(opts, statuses) {
|
|
305
305
|
return statuses.length > 0 || !!(opts.active || opts.local || opts.host?.length || opts.device?.length || focusOptionAgent(opts) ||
|
|
306
306
|
opts.all || opts.teams || opts.inTeam || opts.routine || opts.project || opts.skill || opts.plugin ||
|
|
307
|
-
opts.since || opts.until || opts.
|
|
307
|
+
opts.since || opts.until || opts.bookmarks || opts.unmanaged || (opts.sort && opts.sort !== 'recent'));
|
|
308
308
|
}
|
|
309
309
|
function focusSort(value) {
|
|
310
310
|
if (!value || value === 'recent')
|
|
@@ -370,6 +370,21 @@ async function focusResolvedSession(meta, liveById, self, fallback, attachOnly)
|
|
|
370
370
|
}
|
|
371
371
|
await resumeSessionInPlace(meta);
|
|
372
372
|
}
|
|
373
|
+
/** Focus one row selected by the shared session browser through the same
|
|
374
|
+
* attach/recover decision as `agents sessions focus <id>`. */
|
|
375
|
+
export async function focusSelectedSession(meta, active, self) {
|
|
376
|
+
if (active && !active.sessionId) {
|
|
377
|
+
if (isAttachableLiveSession(active)) {
|
|
378
|
+
await jumpTo(active, self, resumeInNewTab);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
console.log(chalk.yellow('This live session has no session id or living attach rail to focus.'));
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const liveById = active ? new Map([[meta.id, active]]) : new Map();
|
|
386
|
+
await focusResolvedSession(meta, liveById, self, resumeInNewTab, false);
|
|
387
|
+
}
|
|
373
388
|
function activeFromMeta(meta) {
|
|
374
389
|
return {
|
|
375
390
|
context: 'headless',
|
|
@@ -16,6 +16,7 @@ import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_
|
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
17
17
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroup, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, } from '../lib/routines.js';
|
|
18
18
|
import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
|
|
19
|
+
import { evaluateActivationReadinessLive } from '../lib/routine-readiness.js';
|
|
19
20
|
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
20
21
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
21
22
|
import { getRoutinesDir } from '../lib/state.js';
|
|
@@ -724,6 +725,8 @@ export function registerRoutinesCommands(program) {
|
|
|
724
725
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
725
726
|
.option('--project <name>', 'Associate with a named project (repeatable; use --all-projects for all)', collectProject, [])
|
|
726
727
|
.option('--all-projects', 'Associate this routine with all defined projects (sets projects: ["*"])')
|
|
728
|
+
.option('--project-anchor <name>', 'Singular EXECUTION anchor: the named project whose base directory the run lands in (distinct from --project, which is grouping metadata). Rootless (Linear-imported) projects anchor a relative --cwd at the target home.')
|
|
729
|
+
.option('--cwd <path>', 'Portable execution directory. Relative values resolve under --project-anchor when usable, otherwise under the execution target $HOME. Supersedes --run-cwd/remoteCwd.')
|
|
727
730
|
.option('--json', 'Emit machine-readable JSON with the created routine id and status')
|
|
728
731
|
.action(async (nameOrPath, options) => {
|
|
729
732
|
// Check if inline mode (has flags) or file mode
|
|
@@ -852,6 +855,8 @@ export function registerRoutinesCommands(program) {
|
|
|
852
855
|
...(options.runOn ? { host: options.runOn } : {}),
|
|
853
856
|
...(hostStrategy ? { hostStrategy } : {}),
|
|
854
857
|
...(options.runCwd ? { remoteCwd: options.runCwd } : {}),
|
|
858
|
+
...(options.projectAnchor ? { project: options.projectAnchor } : {}),
|
|
859
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
855
860
|
...(runOnce ? { runOnce: true } : {}),
|
|
856
861
|
...(options.catchup === false ? { catchup: false } : {}),
|
|
857
862
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
@@ -867,7 +872,23 @@ export function registerRoutinesCommands(program) {
|
|
|
867
872
|
process.exit(1);
|
|
868
873
|
}
|
|
869
874
|
writeJob(config);
|
|
870
|
-
|
|
875
|
+
// Readiness gate: a routine only activates when its execution context
|
|
876
|
+
// resolves and the harness is available. A proven blocker saves the
|
|
877
|
+
// definition PAUSED with a stable code + repair, so a broken routine can
|
|
878
|
+
// never fire (and storm) — the plan's save-paused contract.
|
|
879
|
+
const deviceMatch = !devices || devices.map(normalizeHost).includes(normalizeHost(machineId()));
|
|
880
|
+
const readiness = await evaluateActivationReadinessLive(config);
|
|
881
|
+
const activate = config.enabled && deviceMatch && readiness.ready;
|
|
882
|
+
setJobEnabled(config.name, activate);
|
|
883
|
+
if (config.enabled && deviceMatch && !readiness.ready) {
|
|
884
|
+
const r = readiness.readiness;
|
|
885
|
+
if (!options.json) {
|
|
886
|
+
console.log(chalk.yellow(`Saved paused — not ready to activate: ${r.code}`));
|
|
887
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
888
|
+
if (r.repair)
|
|
889
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
871
892
|
if (options.json) {
|
|
872
893
|
writeJson({
|
|
873
894
|
ok: true,
|
|
@@ -875,8 +896,11 @@ export function registerRoutinesCommands(program) {
|
|
|
875
896
|
job: config,
|
|
876
897
|
jobId: config.name,
|
|
877
898
|
name: config.name,
|
|
878
|
-
status: 'added',
|
|
899
|
+
status: activate ? 'added' : 'added_paused',
|
|
879
900
|
enabled: config.enabled,
|
|
901
|
+
activated: activate,
|
|
902
|
+
ready: readiness.ready,
|
|
903
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
880
904
|
schedule: config.schedule ?? null,
|
|
881
905
|
trigger: config.trigger ?? null,
|
|
882
906
|
});
|
|
@@ -933,7 +957,17 @@ export function registerRoutinesCommands(program) {
|
|
|
933
957
|
console.error(chalk.yellow(`Schedule "${config.schedule}" pins minute, hour, day, and month; treating it as one-shot. Prefer --at for one-time routines.`));
|
|
934
958
|
}
|
|
935
959
|
writeJob(config);
|
|
936
|
-
|
|
960
|
+
const deviceMatch = !config.devices || config.devices.map(normalizeHost).includes(normalizeHost(machineId()));
|
|
961
|
+
const readiness = await evaluateActivationReadinessLive(config);
|
|
962
|
+
const activate = config.enabled && deviceMatch && readiness.ready;
|
|
963
|
+
setJobEnabled(config.name, activate);
|
|
964
|
+
if (config.enabled && deviceMatch && !readiness.ready && !options.json) {
|
|
965
|
+
const r = readiness.readiness;
|
|
966
|
+
console.log(chalk.yellow(`Saved paused — not ready to activate: ${r.code}`));
|
|
967
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
968
|
+
if (r.repair)
|
|
969
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
970
|
+
}
|
|
937
971
|
if (options.json) {
|
|
938
972
|
writeJson({
|
|
939
973
|
ok: true,
|
|
@@ -941,8 +975,11 @@ export function registerRoutinesCommands(program) {
|
|
|
941
975
|
job: config,
|
|
942
976
|
jobId: config.name,
|
|
943
977
|
name: config.name,
|
|
944
|
-
status: 'added',
|
|
978
|
+
status: activate ? 'added' : 'added_paused',
|
|
945
979
|
enabled: config.enabled,
|
|
980
|
+
activated: activate,
|
|
981
|
+
ready: readiness.ready,
|
|
982
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
946
983
|
schedule: config.schedule ?? null,
|
|
947
984
|
trigger: config.trigger ?? null,
|
|
948
985
|
});
|
|
@@ -1029,7 +1066,8 @@ export function registerRoutinesCommands(program) {
|
|
|
1029
1066
|
});
|
|
1030
1067
|
routinesCmd
|
|
1031
1068
|
.command('edit [name]')
|
|
1032
|
-
.description('
|
|
1069
|
+
.description('Edit a prefilled routine transactionally; invalid YAML never replaces the live definition.')
|
|
1070
|
+
.option('--yaml', 'Open the raw YAML in $EDITOR (the current edit surface)')
|
|
1033
1071
|
.option('--state-to <name>', 'Update the Linear current-state filter before opening the editor')
|
|
1034
1072
|
.option('--state-from <name>', 'Update the Linear previous-state filter before opening the editor')
|
|
1035
1073
|
.action(async (name, options) => {
|
|
@@ -1048,53 +1086,55 @@ export function registerRoutinesCommands(program) {
|
|
|
1048
1086
|
existing.trigger.stateTo = options.stateTo || undefined;
|
|
1049
1087
|
if (options.stateFrom !== undefined)
|
|
1050
1088
|
existing.trigger.stateFrom = options.stateFrom || undefined;
|
|
1051
|
-
writeJob(existing);
|
|
1052
1089
|
}
|
|
1053
1090
|
const jobPath = getJobPath(name);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1091
|
+
const cronDir = getRoutinesDir();
|
|
1092
|
+
fs.mkdirSync(cronDir, { recursive: true });
|
|
1093
|
+
const targetPath = jobPath || safeJoin(cronDir, `${name}.yml`);
|
|
1094
|
+
const editPath = safeJoin(cronDir, `.${name}.edit-${process.pid}.yml`);
|
|
1095
|
+
const initial = existing
|
|
1096
|
+
? yaml.stringify(existing)
|
|
1097
|
+
: yaml.stringify({
|
|
1060
1098
|
name,
|
|
1061
1099
|
schedule: '0 9 * * *',
|
|
1062
1100
|
agent: 'claude',
|
|
1063
1101
|
prompt: 'Your prompt here',
|
|
1064
1102
|
});
|
|
1065
|
-
|
|
1066
|
-
console.log(chalk.gray(`Created new job file: ${newPath}`));
|
|
1067
|
-
}
|
|
1068
|
-
const targetPath = jobPath || path.join(getRoutinesDir(), `${name}.yml`);
|
|
1103
|
+
fs.writeFileSync(editPath, initial, { encoding: 'utf-8', mode: 0o600 });
|
|
1069
1104
|
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WINDOWS ? 'notepad' : 'vi');
|
|
1070
1105
|
const editorParts = editor.split(/\s+/).filter(Boolean);
|
|
1071
1106
|
const editorBin = editorParts[0];
|
|
1072
|
-
const editorArgs = [...editorParts.slice(1),
|
|
1107
|
+
const editorArgs = [...editorParts.slice(1), editPath];
|
|
1073
1108
|
const { spawn: spawnSync } = await import('child_process');
|
|
1074
1109
|
const child = spawnSync(editorBin, editorArgs, {
|
|
1075
1110
|
stdio: 'inherit',
|
|
1076
1111
|
});
|
|
1077
|
-
child.on('close', (code) => {
|
|
1078
|
-
if (code
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1112
|
+
child.on('close', async (code) => {
|
|
1113
|
+
if (code !== 0) {
|
|
1114
|
+
fs.rmSync(editPath, { force: true });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const raw = fs.readFileSync(editPath, 'utf-8');
|
|
1119
|
+
const job = yaml.parse(raw);
|
|
1120
|
+
const errors = validateJob(job);
|
|
1121
|
+
if (errors.length > 0)
|
|
1122
|
+
throw new Error(errors.join('\n'));
|
|
1123
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1124
|
+
fs.renameSync(editPath, targetPath);
|
|
1125
|
+
if (!readiness.ready)
|
|
1126
|
+
setJobEnabled(job.name, false);
|
|
1127
|
+
console.log(chalk.green(`\nJob '${name}' saved${readiness.ready ? '' : ' paused (not ready)'}`));
|
|
1128
|
+
if (isDaemonRunning()) {
|
|
1129
|
+
signalDaemonReload();
|
|
1130
|
+
console.log(chalk.gray('Daemon reloaded'));
|
|
1096
1131
|
}
|
|
1097
1132
|
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
fs.rmSync(editPath, { force: true });
|
|
1135
|
+
console.error(chalk.red(`\nRoutine not saved: ${err.message}`));
|
|
1136
|
+
process.exitCode = 1;
|
|
1137
|
+
}
|
|
1098
1138
|
});
|
|
1099
1139
|
});
|
|
1100
1140
|
routinesCmd
|
|
@@ -1498,6 +1538,79 @@ export function registerRoutinesCommands(program) {
|
|
|
1498
1538
|
console.log(chalk.gray(`Run: ${runId}\n`));
|
|
1499
1539
|
console.log(fs.readFileSync(reportPath, 'utf-8'));
|
|
1500
1540
|
});
|
|
1541
|
+
routinesCmd
|
|
1542
|
+
.command('doctor [name]')
|
|
1543
|
+
.description('Check a routine\'s execution-context and harness readiness. Bare or --all checks every routine; --fix applies safe activation repairs (activate a now-ready paused routine; pause a broken active one).')
|
|
1544
|
+
.option('--all', 'Check every routine (the default when no name is given)')
|
|
1545
|
+
.option('--fix', 'Apply safe, deterministic activation repairs')
|
|
1546
|
+
.option('--json', 'Machine-readable output')
|
|
1547
|
+
.action(async (name, options) => {
|
|
1548
|
+
const all = options.all || !name;
|
|
1549
|
+
let targets;
|
|
1550
|
+
if (all) {
|
|
1551
|
+
targets = listAllJobs();
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
const job = readJob(name);
|
|
1555
|
+
if (!job) {
|
|
1556
|
+
if (options.json) {
|
|
1557
|
+
writeJson({ ok: false, error: `routine '${name}' not found` });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
console.error(chalk.red(`Routine '${name}' not found`));
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
targets = [job];
|
|
1564
|
+
}
|
|
1565
|
+
const thisDevice = normalizeHost(machineId());
|
|
1566
|
+
const results = await Promise.all(targets.map(async (job) => {
|
|
1567
|
+
const deviceMatch = !job.devices || job.devices.map(normalizeHost).includes(thisDevice);
|
|
1568
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1569
|
+
// `job.enabled` reflects THIS device's activation state (applyDeviceActivation).
|
|
1570
|
+
let action;
|
|
1571
|
+
if (options.fix && deviceMatch) {
|
|
1572
|
+
if (readiness.ready && !job.enabled) {
|
|
1573
|
+
setJobEnabled(job.name, true);
|
|
1574
|
+
action = 'activated';
|
|
1575
|
+
}
|
|
1576
|
+
else if (!readiness.ready && job.enabled) {
|
|
1577
|
+
setJobEnabled(job.name, false);
|
|
1578
|
+
action = 'paused';
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
name: job.name,
|
|
1583
|
+
ready: readiness.ready,
|
|
1584
|
+
active: job.enabled,
|
|
1585
|
+
deviceScoped: deviceMatch,
|
|
1586
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
1587
|
+
...(action ? { action } : {}),
|
|
1588
|
+
};
|
|
1589
|
+
}));
|
|
1590
|
+
if (options.fix && results.some((r) => r.action) && isDaemonRunning()) {
|
|
1591
|
+
signalDaemonReload();
|
|
1592
|
+
}
|
|
1593
|
+
if (options.json) {
|
|
1594
|
+
writeJson({ ok: true, results });
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const blocked = results.filter((r) => !r.ready);
|
|
1598
|
+
for (const r of results) {
|
|
1599
|
+
if (r.ready) {
|
|
1600
|
+
console.log(`${chalk.green('✓')} ${r.name}${r.action === 'activated' ? chalk.gray(' (activated)') : ''}`);
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
const rd = r.readiness;
|
|
1604
|
+
console.log(`${chalk.red('✗')} ${r.name} — ${chalk.yellow(rd.code)}${r.action === 'paused' ? chalk.gray(' (paused)') : ''}`);
|
|
1605
|
+
console.log(chalk.gray(` ${rd.message}`));
|
|
1606
|
+
if (rd.repair)
|
|
1607
|
+
console.log(chalk.gray(` repair: ${rd.repair}`));
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (blocked.length > 0 && !options.fix) {
|
|
1611
|
+
console.log(chalk.gray(`\n${blocked.length} routine${blocked.length === 1 ? '' : 's'} blocked — re-run with --fix to pause them, or apply each repair above.`));
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1501
1614
|
routinesCmd
|
|
1502
1615
|
.command('resume [name]')
|
|
1503
1616
|
.description('Re-enable a paused routine so the daemon schedules it again')
|
|
@@ -1509,6 +1622,21 @@ export function registerRoutinesCommands(program) {
|
|
|
1509
1622
|
return;
|
|
1510
1623
|
}
|
|
1511
1624
|
try {
|
|
1625
|
+
// Resume re-runs readiness — it can never bypass a proven blocker (the
|
|
1626
|
+
// plan: "resume cannot bypass readiness"). A blocked routine stays paused.
|
|
1627
|
+
const job = readJob(name);
|
|
1628
|
+
if (job) {
|
|
1629
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1630
|
+
if (!readiness.ready) {
|
|
1631
|
+
const r = readiness.readiness;
|
|
1632
|
+
console.log(chalk.red(`Cannot resume '${name}' — not ready: ${r.code}`));
|
|
1633
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
1634
|
+
if (r.repair)
|
|
1635
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
1636
|
+
console.log(chalk.gray(` fix it, then: agents routines doctor ${name} --fix`));
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1512
1640
|
setJobEnabled(name, true);
|
|
1513
1641
|
console.log(chalk.green(`Job '${name}' resumed`));
|
|
1514
1642
|
if (isDaemonRunning()) {
|
|
@@ -12,6 +12,7 @@ import { bundleEnvToDotenv } from '../lib/secrets/push.js';
|
|
|
12
12
|
export { bundleEnvToDotenv };
|
|
13
13
|
import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
14
14
|
import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
|
|
15
|
+
import { type SecretLease } from '../lib/secrets/lease.js';
|
|
15
16
|
import { type BundleUsageSummary } from '../lib/secrets/usage-db.js';
|
|
16
17
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
17
18
|
/**
|
|
@@ -51,6 +52,25 @@ export declare function parseImportSource(opts: {
|
|
|
51
52
|
* without a live SSH session.
|
|
52
53
|
*/
|
|
53
54
|
export declare function resolveUnlockTtlMs(ttl: string | undefined, until: string | undefined, now?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Decide what an `unlock` holds: the whole bundle env, or — with --keys — only
|
|
57
|
+
* the resolved subset behind a lease that scopes the broker entry (agent.ts
|
|
58
|
+
* re-selects on load) and its own expiry. Fails closed: createSecretLease throws
|
|
59
|
+
* on an unknown or empty key subset. The unlock action's single scoping seam,
|
|
60
|
+
* exported so the whole-bundle vs scoped-subset decision is unit-testable without
|
|
61
|
+
* a live broker.
|
|
62
|
+
*/
|
|
63
|
+
export declare function scopeHeldEnv(input: {
|
|
64
|
+
bundle: string;
|
|
65
|
+
env: Record<string, string>;
|
|
66
|
+
keys: string | null;
|
|
67
|
+
ttlMs: number;
|
|
68
|
+
harness: string;
|
|
69
|
+
sleepPersist: boolean;
|
|
70
|
+
}): {
|
|
71
|
+
heldEnv: Record<string, string>;
|
|
72
|
+
lease?: SecretLease;
|
|
73
|
+
};
|
|
54
74
|
export declare function buildRemoteUnlockArgs(names: string[], opts: {
|
|
55
75
|
all?: boolean;
|
|
56
76
|
ttl?: string;
|
package/dist/commands/secrets.js
CHANGED
|
@@ -25,8 +25,8 @@ import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainTok
|
|
|
25
25
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
26
26
|
import { GLOBAL_HARNESS } from '../lib/secrets/scope.js';
|
|
27
27
|
import { createSecretLease, selectLeasedEnv } from '../lib/secrets/lease.js';
|
|
28
|
-
import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing,
|
|
29
|
-
import {
|
|
28
|
+
import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
29
|
+
import { saveSession, deleteBundleSessions, deleteAllSessions } from '../lib/secrets/session-store.js';
|
|
30
30
|
import { getCliVersionFresh } from '../lib/version.js';
|
|
31
31
|
import { readMeta } from '../lib/state.js';
|
|
32
32
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
@@ -287,6 +287,30 @@ export function resolveUnlockTtlMs(ttl, until, now = Date.now()) {
|
|
|
287
287
|
}
|
|
288
288
|
return secretsHoldMs();
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Decide what an `unlock` holds: the whole bundle env, or — with --keys — only
|
|
292
|
+
* the resolved subset behind a lease that scopes the broker entry (agent.ts
|
|
293
|
+
* re-selects on load) and its own expiry. Fails closed: createSecretLease throws
|
|
294
|
+
* on an unknown or empty key subset. The unlock action's single scoping seam,
|
|
295
|
+
* exported so the whole-bundle vs scoped-subset decision is unit-testable without
|
|
296
|
+
* a live broker.
|
|
297
|
+
*/
|
|
298
|
+
export function scopeHeldEnv(input) {
|
|
299
|
+
// null = no --keys → hold the whole bundle. An empty-string --keys is a
|
|
300
|
+
// deliberate (if empty) scope request and falls through to createSecretLease,
|
|
301
|
+
// which fails closed ('requires at least one key') rather than holding it all.
|
|
302
|
+
if (input.keys === null)
|
|
303
|
+
return { heldEnv: input.env };
|
|
304
|
+
const lease = createSecretLease({
|
|
305
|
+
bundle: input.bundle,
|
|
306
|
+
keys: input.keys.split(','),
|
|
307
|
+
availableKeys: Object.keys(input.env),
|
|
308
|
+
ttlMs: input.ttlMs,
|
|
309
|
+
harness: input.harness,
|
|
310
|
+
sleepPersist: input.sleepPersist,
|
|
311
|
+
});
|
|
312
|
+
return { heldEnv: selectLeasedEnv(lease, input.env), lease };
|
|
313
|
+
}
|
|
290
314
|
export function buildRemoteUnlockArgs(names, opts) {
|
|
291
315
|
return [
|
|
292
316
|
'unlock',
|
|
@@ -2442,89 +2466,6 @@ Examples:
|
|
|
2442
2466
|
console.log(password);
|
|
2443
2467
|
}
|
|
2444
2468
|
});
|
|
2445
|
-
cmd
|
|
2446
|
-
.command('lease <bundle>')
|
|
2447
|
-
.description('Hold only an explicit subset of a bundle until an independent expiry.')
|
|
2448
|
-
.requiredOption('--keys <keys>', 'Comma-separated key subset')
|
|
2449
|
-
.requiredOption('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d)')
|
|
2450
|
-
.option('--agent <agent>', 'Narrow the lease to one harness; default is global')
|
|
2451
|
-
.option('--durable', 'Keep the lease across sleep as well as broker restart')
|
|
2452
|
-
.action(async (name, opts) => {
|
|
2453
|
-
if (process.platform !== 'darwin') {
|
|
2454
|
-
throw new Error('Scoped lease brokering is not available on this platform yet.');
|
|
2455
|
-
}
|
|
2456
|
-
const seconds = parseDuration(opts.ttl);
|
|
2457
|
-
if (seconds === null)
|
|
2458
|
-
throw new Error(`Invalid lease duration '${opts.ttl}'.`);
|
|
2459
|
-
const ttlMs = seconds * 1000;
|
|
2460
|
-
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2461
|
-
const { bundle, env } = readAndResolveBundleEnv(name, {
|
|
2462
|
-
noAgent: true,
|
|
2463
|
-
interactiveUnlock: true,
|
|
2464
|
-
caller: 'lease secrets',
|
|
2465
|
-
agent: harness,
|
|
2466
|
-
keyMode: 'storage',
|
|
2467
|
-
});
|
|
2468
|
-
const lease = createSecretLease({
|
|
2469
|
-
bundle: name,
|
|
2470
|
-
keys: opts.keys.split(','),
|
|
2471
|
-
availableKeys: Object.keys(env),
|
|
2472
|
-
ttlMs,
|
|
2473
|
-
harness,
|
|
2474
|
-
sleepPersist: opts.durable,
|
|
2475
|
-
});
|
|
2476
|
-
const leasedEnv = selectLeasedEnv(lease, env);
|
|
2477
|
-
if (!(await ensureAgentRunning()) || !(await agentLoad(name, bundle, leasedEnv, ttlMs, harness, lease))) {
|
|
2478
|
-
throw new Error('Could not load the scoped lease into the secrets broker.');
|
|
2479
|
-
}
|
|
2480
|
-
ensureDaemonStarted();
|
|
2481
|
-
saveSession(name, {
|
|
2482
|
-
bundle,
|
|
2483
|
-
env: leasedEnv,
|
|
2484
|
-
expiresAt: lease.expiresAt,
|
|
2485
|
-
sleepPersist: lease.sleepPersist,
|
|
2486
|
-
harness,
|
|
2487
|
-
lease,
|
|
2488
|
-
});
|
|
2489
|
-
emitSecretAudit({
|
|
2490
|
-
event: 'secrets.unlocked', bundle: name, operation: 'lease', source: lease.sleepPersist ? 'broker+durable' : 'broker',
|
|
2491
|
-
status: 'success', keys: lease.keys, keyCount: lease.keys.length, agent: harness, ttlMs: lease.expiresAt - lease.createdAt,
|
|
2492
|
-
});
|
|
2493
|
-
console.log(`${chalk.green('leased')} ${chalk.cyan(lease.id)} ${chalk.gray(`(${name}: ${lease.keys.join(', ')}, ${humanRemaining(lease.expiresAt)})`)}`);
|
|
2494
|
-
});
|
|
2495
|
-
cmd
|
|
2496
|
-
.command('leases')
|
|
2497
|
-
.description('List active scoped secret leases.')
|
|
2498
|
-
.action(async () => {
|
|
2499
|
-
const brokerLeases = (await agentStatus()).filter((entry) => entry.leaseId);
|
|
2500
|
-
const byId = new Map(brokerLeases.map((entry) => [entry.leaseId, entry]));
|
|
2501
|
-
for (const { name, lease } of activeLeaseSessions()) {
|
|
2502
|
-
if (!byId.has(lease.id))
|
|
2503
|
-
byId.set(lease.id, { name, expiresAt: lease.expiresAt, keyCount: lease.keys.length, harness: lease.harness, leaseId: lease.id, keys: lease.keys });
|
|
2504
|
-
}
|
|
2505
|
-
const leases = [...byId.values()];
|
|
2506
|
-
if (leases.length === 0) {
|
|
2507
|
-
console.log(chalk.gray('No active secret leases.'));
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
console.log(`${'ID'.padEnd(24)} ${'BUNDLE'.padEnd(24)} ${'KEYS'.padEnd(24)} EXPIRES IN`);
|
|
2511
|
-
for (const lease of leases) {
|
|
2512
|
-
console.log(`${lease.leaseId.padEnd(24)} ${lease.name.padEnd(24)} ${(lease.keys ?? []).join(',').padEnd(24)} ${humanRemaining(lease.expiresAt)}`);
|
|
2513
|
-
}
|
|
2514
|
-
});
|
|
2515
|
-
cmd
|
|
2516
|
-
.command('revoke <lease-id>')
|
|
2517
|
-
.description('Revoke one scoped secret lease immediately.')
|
|
2518
|
-
.action(async (leaseId) => {
|
|
2519
|
-
if (agentSocketExists() && !(await agentPing()).reachable) {
|
|
2520
|
-
throw new Error('Cannot revoke while the secrets broker is using an incompatible protocol. Restart the broker and retry.');
|
|
2521
|
-
}
|
|
2522
|
-
const persisted = deleteLeaseSession(leaseId);
|
|
2523
|
-
const wiped = await agentRevoke(leaseId);
|
|
2524
|
-
if (wiped + persisted === 0)
|
|
2525
|
-
throw new Error(`Secret lease '${leaseId}' is not active.`);
|
|
2526
|
-
console.log(chalk.green(`Revoked secret lease ${leaseId}.`));
|
|
2527
|
-
});
|
|
2528
2469
|
cmd
|
|
2529
2470
|
.command('unlock [names...]')
|
|
2530
2471
|
.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.')
|
|
@@ -2532,6 +2473,7 @@ Examples:
|
|
|
2532
2473
|
.option('--until <date>', 'Hold until this absolute date or timestamp (for example 2026-08-06T12:00:00Z). Mutually exclusive with --ttl.')
|
|
2533
2474
|
.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.')
|
|
2534
2475
|
.option('--agent <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.')
|
|
2476
|
+
.option('--keys <keys>', 'Hold ONLY this comma-separated subset of the bundle\'s keys instead of the whole bundle. Scopes exactly one bundle; fails closed on an unknown key. `secrets status` shows which keys a scoped hold covers.')
|
|
2535
2477
|
.option('--all', 'Unlock every configured bundle')
|
|
2536
2478
|
.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>`.')
|
|
2537
2479
|
.action(async (names, opts) => {
|
|
@@ -2539,6 +2481,19 @@ Examples:
|
|
|
2539
2481
|
console.error(chalk.red('--ttl and --until are mutually exclusive.'));
|
|
2540
2482
|
process.exit(1);
|
|
2541
2483
|
}
|
|
2484
|
+
if (opts.keys !== undefined) {
|
|
2485
|
+
// --keys scopes ONE bundle to an explicit subset. It is local-only (a
|
|
2486
|
+
// remote --host unlock holds the whole file-backed bundle) and cannot
|
|
2487
|
+
// combine with --all, which would hold every bundle whole.
|
|
2488
|
+
if (opts.host) {
|
|
2489
|
+
console.error(chalk.red('--keys is local-only; a remote (--host) unlock holds the whole file-backed bundle.'));
|
|
2490
|
+
process.exit(1);
|
|
2491
|
+
}
|
|
2492
|
+
if (opts.all || !names || names.length !== 1) {
|
|
2493
|
+
console.error(chalk.red('--keys scopes ONE bundle to a subset of its keys: name exactly one bundle (not --all).'));
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2542
2497
|
// Single-valued (not variadic): a variadic --host greedily consumes the
|
|
2543
2498
|
// positional bundle name (`unlock --host mac wztest` -> host=[mac,wztest],
|
|
2544
2499
|
// names=[]). Unlock targets one remote at a time anyway.
|
|
@@ -2623,6 +2578,7 @@ Examples:
|
|
|
2623
2578
|
// agent happened to launch the shell, leaving the grant unreadable to
|
|
2624
2579
|
// every other reader for its whole TTL.
|
|
2625
2580
|
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2581
|
+
const durable = opts.durable ?? secretsAgentDurable();
|
|
2626
2582
|
for (const name of targets) {
|
|
2627
2583
|
try {
|
|
2628
2584
|
// noAgent: read the real keychain (one Touch ID) rather than the
|
|
@@ -2639,17 +2595,23 @@ Examples:
|
|
|
2639
2595
|
// ACL heal can rewrite that already-read value without another read.
|
|
2640
2596
|
maybeAutoRekey();
|
|
2641
2597
|
healKeychainBundleMetadataAclOnce(new Map([[bundle.name, JSON.stringify(bundle)]]));
|
|
2642
|
-
|
|
2598
|
+
// With --keys, hold ONLY the resolved subset behind a lease that scopes
|
|
2599
|
+
// the broker entry and its own expiry; without it, the whole bundle is
|
|
2600
|
+
// held exactly as before. scopeHeldEnv fails closed on an unknown key.
|
|
2601
|
+
const { heldEnv, lease } = scopeHeldEnv({ bundle: name, env, keys: opts.keys ?? null, ttlMs, harness, sleepPersist: durable });
|
|
2602
|
+
const heldExpiresAt = lease ? lease.expiresAt : expiresAt;
|
|
2603
|
+
if (await agentLoad(name, bundle, heldEnv, ttlMs, harness, lease)) {
|
|
2643
2604
|
loaded++;
|
|
2644
2605
|
// Persist a durable session snapshot so the unlock survives a daemon
|
|
2645
2606
|
// restart / upgrade (and sleep too, with --durable). session-store.ts.
|
|
2646
|
-
|
|
2607
|
+
// A scoped hold persists its lease so it rehydrates scoped.
|
|
2647
2608
|
saveSession(name, {
|
|
2648
2609
|
bundle,
|
|
2649
|
-
env,
|
|
2650
|
-
expiresAt,
|
|
2610
|
+
env: heldEnv,
|
|
2611
|
+
expiresAt: heldExpiresAt,
|
|
2651
2612
|
sleepPersist: durable,
|
|
2652
2613
|
harness,
|
|
2614
|
+
lease,
|
|
2653
2615
|
});
|
|
2654
2616
|
// Audit the GRANT itself — the broker + durable session now serve this
|
|
2655
2617
|
// bundle prompt-free for the whole TTL, to every reader in `harness`
|
|
@@ -2661,12 +2623,13 @@ Examples:
|
|
|
2661
2623
|
operation: 'unlock',
|
|
2662
2624
|
source: durable ? 'broker+durable' : 'broker',
|
|
2663
2625
|
status: 'success',
|
|
2664
|
-
keyCount: Object.keys(
|
|
2665
|
-
keys: Object.keys(
|
|
2626
|
+
keyCount: Object.keys(heldEnv).length,
|
|
2627
|
+
keys: Object.keys(heldEnv).sort(),
|
|
2666
2628
|
agent: harness,
|
|
2667
2629
|
ttlMs,
|
|
2668
2630
|
});
|
|
2669
|
-
|
|
2631
|
+
const scoped = lease ? chalk.gray(` scoped to ${lease.keys.join(', ')}`) : '';
|
|
2632
|
+
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(heldEnv).length} keys, ${humanRemaining(heldExpiresAt)})`)}${scoped}`);
|
|
2670
2633
|
}
|
|
2671
2634
|
else {
|
|
2672
2635
|
console.error(chalk.red(`Failed to load '${name}' into the agent.`));
|
|
@@ -2748,7 +2711,10 @@ Examples:
|
|
|
2748
2711
|
else {
|
|
2749
2712
|
console.log(chalk.bold(`${'BUNDLE'.padEnd(24)} ${'KEYS'.padEnd(5)} LOCKS IN`));
|
|
2750
2713
|
for (const e of entries) {
|
|
2751
|
-
|
|
2714
|
+
// A scoped hold (unlock --keys) carries the held key names, so name them
|
|
2715
|
+
// rather than leave the bundle looking whole.
|
|
2716
|
+
const scoped = e.keys?.length ? chalk.gray(` scoped: ${e.keys.join(', ')}`) : '';
|
|
2717
|
+
console.log(`${chalk.cyan(e.name.padEnd(24))} ${String(e.keyCount).padEnd(5)} ${humanRemaining(e.expiresAt)}${scoped}`);
|
|
2752
2718
|
}
|
|
2753
2719
|
console.log(chalk.gray('Reads of held bundles are silent; any bundle not listed prompts once on its next read.'));
|
|
2754
2720
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents sessions
|
|
2
|
+
* `agents sessions bookmark` — the non-TTY half of the bookmark hotkey.
|
|
3
3
|
*
|
|
4
|
-
* The `*` hotkey in the interactive browser is how a human
|
|
4
|
+
* The `*` hotkey in the interactive browser is how a human bookmarks a session;
|
|
5
5
|
* this is how a script, an agent, or a machine without a TTY does the same thing,
|
|
6
6
|
* and it is what makes the feature testable end to end without driving a terminal
|
|
7
|
-
* UI. Both write the one store in `lib/session/
|
|
7
|
+
* UI. Both write the one store in `lib/session/bookmarks.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
10
|
/**
|
|
11
11
|
* Resolve one user-typed id (usually the 8-char short id the listing prints) to
|
|
12
|
-
* a full session id. Ambiguity is an ERROR, not a silent first-match:
|
|
13
|
-
* the wrong session is invisible until the user wonders where their
|
|
12
|
+
* a full session id. Ambiguity is an ERROR, not a silent first-match: bookmarking
|
|
13
|
+
* the wrong session is invisible until the user wonders where their bookmark went.
|
|
14
14
|
*/
|
|
15
|
-
export declare function
|
|
15
|
+
export declare function resolveBookmarkTarget(idQuery: string): {
|
|
16
16
|
id: string;
|
|
17
17
|
} | {
|
|
18
18
|
error: string;
|
|
19
19
|
};
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function registerSessionsBookmarkCommand(sessionsCmd: Command): void;
|