@phnx-labs/agents-cli 1.22.29 → 1.22.30
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 +82 -0
- package/README.md +39 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -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 +1 -1
- package/dist/lib/migrate.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- 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 +19 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
package/dist/commands/exec.js
CHANGED
|
@@ -445,17 +445,18 @@ async function handleTerminalHandoff(agentSpec, options, prompt) {
|
|
|
445
445
|
// the whole Kimi/DeepSeek/Qwen/GLM path — for `--terminal` runs only.
|
|
446
446
|
const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('@')[0];
|
|
447
447
|
const knownAgent = resolveAgentName(rawTarget);
|
|
448
|
-
|
|
448
|
+
const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
|
|
449
|
+
import('../lib/profiles.js'),
|
|
450
|
+
import('../lib/workflows.js'),
|
|
451
|
+
]);
|
|
452
|
+
const hasProfile = profileExists(rawTarget);
|
|
453
|
+
if (knownAgent && !hasProfile && isAgentHardDeprecated(knownAgent)) {
|
|
449
454
|
console.error(chalk.red(hardDeprecationError(knownAgent)));
|
|
450
455
|
process.exit(1);
|
|
451
456
|
}
|
|
452
457
|
if (!knownAgent) {
|
|
453
|
-
const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
|
|
454
|
-
import('../lib/profiles.js'),
|
|
455
|
-
import('../lib/workflows.js'),
|
|
456
|
-
]);
|
|
457
458
|
const probeCwd = options.cwd ?? process.cwd();
|
|
458
|
-
if (!
|
|
459
|
+
if (!hasProfile && !resolveWorkflowRef(rawTarget, probeCwd)) {
|
|
459
460
|
console.error(chalk.red(`Unknown agent, profile, or workflow: ${rawTarget}. See \`agents list\` for the installed harnesses.`));
|
|
460
461
|
process.exit(1);
|
|
461
462
|
}
|
|
@@ -545,6 +546,7 @@ export function registerRunCommand(program) {
|
|
|
545
546
|
.option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,antigravity). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
|
|
546
547
|
.option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
|
|
547
548
|
.option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then balanced (spreads load across healthy accounts and skips any that are rate-limited). (Legacy `rotate` accepted as alias for `balanced`.)')
|
|
549
|
+
.option('--account <label>', 'Run a healthy installed version currently signed into this named account (never falls back to another identity)')
|
|
548
550
|
.option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for claude via @zed-industries/claude-code-acp adapter. Unified event stream; emits ndjson when --json.')
|
|
549
551
|
.option('-y, --yes', 'Skip the interactive budget-confirm prompt (require_confirm_over). Never skips a hard budget block.', false)
|
|
550
552
|
.option('--loop', 'Re-inject the prompt/entrypoint each iteration until a stop condition (issue #332). Guards (--max-iterations, --budget, --until) are enforced outside the agent. Writes a checkpoint after every iteration for --resume-checkpoint.')
|
|
@@ -812,8 +814,10 @@ export function registerRunCommand(program) {
|
|
|
812
814
|
process.exit(1);
|
|
813
815
|
}
|
|
814
816
|
// Hard-deprecated harnesses cannot be run — point the user at the successor.
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
+
const runBaseAgentName = normalizedAgentSpec.split('@')[0];
|
|
818
|
+
const runBaseAgentId = resolveAgentName(runBaseAgentName);
|
|
819
|
+
const { profileExists: runProfileExists } = await import('../lib/profiles.js');
|
|
820
|
+
if (runBaseAgentId && !runProfileExists(runBaseAgentName) && isAgentHardDeprecated(runBaseAgentId)) {
|
|
817
821
|
console.error(chalk.red(hardDeprecationError(runBaseAgentId)));
|
|
818
822
|
process.exit(1);
|
|
819
823
|
}
|
|
@@ -1567,6 +1571,7 @@ export function registerRunCommand(program) {
|
|
|
1567
1571
|
agent: runAgent,
|
|
1568
1572
|
version: resumeId ? undefined : runVersion,
|
|
1569
1573
|
strategy: resumeId ? undefined : runStrategy,
|
|
1574
|
+
account: resumeId ? undefined : options.account,
|
|
1570
1575
|
fallback: options.fallback,
|
|
1571
1576
|
prompt,
|
|
1572
1577
|
mode: forwardedMode,
|
|
@@ -1654,6 +1659,7 @@ export function registerRunCommand(program) {
|
|
|
1654
1659
|
agent: runAgent,
|
|
1655
1660
|
version: resumeId ? undefined : runVersion,
|
|
1656
1661
|
strategy: resumeId ? undefined : runStrategy,
|
|
1662
|
+
account: resumeId ? undefined : options.account,
|
|
1657
1663
|
fallback: options.fallback,
|
|
1658
1664
|
prompt,
|
|
1659
1665
|
mode: forwardedMode,
|
|
@@ -1840,11 +1846,11 @@ export function registerRunCommand(program) {
|
|
|
1840
1846
|
// degenerates to a no-op ("I'll wait for the completion notification").
|
|
1841
1847
|
let workflowHasSubagents = false;
|
|
1842
1848
|
const cwd = options.cwd ?? process.cwd();
|
|
1849
|
+
if (accountPickerRequested && profileExists(rawAgent)) {
|
|
1850
|
+
console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with @ instead.`));
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1843
1853
|
if (accountPickerRequested && !isValidAgent(rawAgent)) {
|
|
1844
|
-
if (profileExists(rawAgent)) {
|
|
1845
|
-
console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with @ instead.`));
|
|
1846
|
-
process.exit(1);
|
|
1847
|
-
}
|
|
1848
1854
|
if (resolveWorkflowRef(rawAgent, cwd)) {
|
|
1849
1855
|
console.error(chalk.red(`Account selection is not available for workflow '${rawAgent}'. Run a concrete agent with @ instead.`));
|
|
1850
1856
|
process.exit(1);
|
|
@@ -1886,14 +1892,11 @@ export function registerRunCommand(program) {
|
|
|
1886
1892
|
process.stderr.write(chalk.yellow(`[agents] --session-id ignored: auto picked ${agent} (only claude accepts a forced session id)\n`));
|
|
1887
1893
|
}
|
|
1888
1894
|
}
|
|
1889
|
-
else if (isValidAgent(rawAgent)) {
|
|
1890
|
-
agent = rawAgent;
|
|
1891
|
-
}
|
|
1892
1895
|
else if (profileExists(rawAgent)) {
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1896
|
+
// A profile by this exact name exists. Profiles bind (host agent,
|
|
1897
|
+
// version, env overrides, keychain-backed auth) so Chinese models
|
|
1898
|
+
// (Kimi, DeepSeek, Qwen, GLM) can run inside Claude Code without a
|
|
1899
|
+
// local proxy, including when the profile name matches a native id.
|
|
1897
1900
|
try {
|
|
1898
1901
|
const resolved = resolveProfileForRun(rawAgent, options.model);
|
|
1899
1902
|
agent = resolved.agent;
|
|
@@ -1925,6 +1928,9 @@ export function registerRunCommand(program) {
|
|
|
1925
1928
|
process.exit(1);
|
|
1926
1929
|
}
|
|
1927
1930
|
}
|
|
1931
|
+
else if (isValidAgent(rawAgent)) {
|
|
1932
|
+
agent = rawAgent;
|
|
1933
|
+
}
|
|
1928
1934
|
else if (resolveWorkflowRef(rawAgent, cwd)) {
|
|
1929
1935
|
// Workflow: explicit directory, project .agents/workflows/<name>, user, system, or extra repo.
|
|
1930
1936
|
// Resolution follows resource precedence: direct path, then project > user > system > extras.
|
|
@@ -2148,6 +2154,23 @@ export function registerRunCommand(program) {
|
|
|
2148
2154
|
}
|
|
2149
2155
|
}
|
|
2150
2156
|
version = resolveVersionAlias(agent, version);
|
|
2157
|
+
if (options.account) {
|
|
2158
|
+
if (options.cloud || options.provider || options.lease) {
|
|
2159
|
+
console.error(chalk.red('--account selects a local installed identity and cannot be combined with cloud or lease placement.'));
|
|
2160
|
+
process.exit(1);
|
|
2161
|
+
}
|
|
2162
|
+
const { resolveAccountLabel } = await import('../lib/account-labels.js');
|
|
2163
|
+
try {
|
|
2164
|
+
const selected = await resolveAccountLabel(agent, options.account);
|
|
2165
|
+
if (version && version !== selected)
|
|
2166
|
+
throw new Error(`${agent}@${version} is not a healthy match for account '${options.account}'.`);
|
|
2167
|
+
version ??= selected;
|
|
2168
|
+
}
|
|
2169
|
+
catch (err) {
|
|
2170
|
+
console.error(chalk.red(err.message));
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2151
2174
|
// --resume: resolve a prior conversation and rewrite the run target to
|
|
2152
2175
|
// continue it. `version` here is already the alias-resolved candidate-version
|
|
2153
2176
|
// FILTER (undefined for default/any, concrete for @latest/@oldest/@x.y.z);
|
|
@@ -2320,7 +2343,7 @@ export function registerRunCommand(program) {
|
|
|
2320
2343
|
// the bare primary still resolves through the strategy — otherwise every
|
|
2321
2344
|
// `agents run claude --fallback codex` run lands on the pinned default
|
|
2322
2345
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
2323
|
-
if (!accountPickerRequested && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
2346
|
+
if (!accountPickerRequested && !options.account && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
2324
2347
|
if (version) {
|
|
2325
2348
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
2326
2349
|
}
|
|
@@ -2434,6 +2457,29 @@ export function registerRunCommand(program) {
|
|
|
2434
2457
|
version = healed;
|
|
2435
2458
|
}
|
|
2436
2459
|
}
|
|
2460
|
+
// The harness may simply not be on this machine. The self-heal above only
|
|
2461
|
+
// runs when a managed version resolved, so with nothing installed we used
|
|
2462
|
+
// to fall through and spawn the bare `cliCommand`, which dies as
|
|
2463
|
+
// `exec: cursor-agent: not found` (exit 127) after a misleading
|
|
2464
|
+
// "looks logged out" banner (RUSH-2339). Probe the executable
|
|
2465
|
+
// buildExecCommand will actually spawn and fail loud instead.
|
|
2466
|
+
//
|
|
2467
|
+
// This is an EXISTENCE probe, not "does agents-cli manage a version". A
|
|
2468
|
+
// harness the user installed themselves (Homebrew, a vendor `curl | sh`, a
|
|
2469
|
+
// distro package) has no version home and MUST still launch — the PATH
|
|
2470
|
+
// branch of resolveLaunchBinary is what keeps that working.
|
|
2471
|
+
{
|
|
2472
|
+
const { resolveLaunchBinary } = await import('../lib/exec.js');
|
|
2473
|
+
// `version` already carries the self-heal's resolution above (it assigns
|
|
2474
|
+
// `version = healed` whenever a version resolved at all), so re-deriving
|
|
2475
|
+
// it with resolveVersion here would be dead.
|
|
2476
|
+
if (!resolveLaunchBinary(agent, version)) {
|
|
2477
|
+
const target = version ? `${agent}@${version}` : agent;
|
|
2478
|
+
console.error(chalk.red(`agents: ${target} is not installed on this machine.`));
|
|
2479
|
+
console.error(chalk.yellow(`Install it with: agents add ${target}`));
|
|
2480
|
+
process.exit(1);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2437
2483
|
const defaultVersion = version ?? resolveVersion(agent, cwd);
|
|
2438
2484
|
// Re-apply the active rules preset before every launch (issue: preset
|
|
2439
2485
|
// changes via `setActiveRulesPreset` only took effect after an explicit
|
|
@@ -11,7 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
|
-
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
14
|
+
import { assertSchedulerEnabled, assertDaemonEnabled, isDaemonEnabled } from '../lib/device-config.js';
|
|
15
15
|
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
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';
|
|
@@ -344,6 +344,7 @@ function parseRoutineTrigger(options) {
|
|
|
344
344
|
function ensureSchedulerRunning(opts = {}) {
|
|
345
345
|
const log = opts.stderr ? console.error : console.log;
|
|
346
346
|
try {
|
|
347
|
+
assertDaemonEnabled();
|
|
347
348
|
assertSchedulerEnabled();
|
|
348
349
|
}
|
|
349
350
|
catch (err) {
|
|
@@ -1282,11 +1283,18 @@ export function registerRoutinesCommands(program) {
|
|
|
1282
1283
|
console.log(` ${chalk.cyan(job.name)} — missed ${chalk.gray(job.expectedAt.toLocaleString())}, last ran ${chalk.gray(last)}`);
|
|
1283
1284
|
}
|
|
1284
1285
|
// Need the daemon alive so spawned jobs are monitored and meta.json is
|
|
1285
|
-
// finalized. Start it if it isn't already running.
|
|
1286
|
+
// finalized. Start it if it isn't already running — unless daemon.enabled
|
|
1287
|
+
// is off, in which case this auto-start is skipped with a stated reason.
|
|
1286
1288
|
if (!options.dryRun && !isDaemonRunning()) {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1289
|
+
try {
|
|
1290
|
+
assertDaemonEnabled();
|
|
1291
|
+
const started = startDaemon();
|
|
1292
|
+
if (started.pid) {
|
|
1293
|
+
console.log(chalk.gray(`\nStarted scheduler (PID: ${started.pid}) so catchup runs are monitored.`));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
console.log(chalk.yellow(`\n${err.message}`));
|
|
1290
1298
|
}
|
|
1291
1299
|
}
|
|
1292
1300
|
console.log(chalk.bold(options.dryRun ? '\nRecording misses...' : '\nTriggering catchup runs...'));
|
|
@@ -1372,11 +1380,18 @@ export function registerRoutinesCommands(program) {
|
|
|
1372
1380
|
return;
|
|
1373
1381
|
}
|
|
1374
1382
|
// Fired jobs run detached via executeJobDetached (the same path cron
|
|
1375
|
-
// uses). Keep the daemon alive so each run's meta.json is finalized
|
|
1383
|
+
// uses). Keep the daemon alive so each run's meta.json is finalized —
|
|
1384
|
+
// unless daemon.enabled is off, in which case this auto-start is
|
|
1385
|
+
// skipped with a stated reason.
|
|
1376
1386
|
if (!isDaemonRunning()) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1387
|
+
if (!isDaemonEnabled()) {
|
|
1388
|
+
console.log(chalk.yellow(`Daemon is disabled (daemon.enabled=false) — run(s) below fire but are not monitored. Re-enable with: agents daemon enable`));
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
const started = startDaemon();
|
|
1392
|
+
if (started.pid) {
|
|
1393
|
+
console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
|
|
1394
|
+
}
|
|
1380
1395
|
}
|
|
1381
1396
|
}
|
|
1382
1397
|
const fired = await fireWebhookJobs(webhook);
|
|
@@ -1649,8 +1664,11 @@ export function registerRoutinesCommands(program) {
|
|
|
1649
1664
|
.description('Start the background scheduler. Usually unnecessary — it auto-starts when you add your first routine.')
|
|
1650
1665
|
.action(() => {
|
|
1651
1666
|
try {
|
|
1652
|
-
// A manual start on a
|
|
1653
|
-
//
|
|
1667
|
+
// A manual start on a disabled device refuses with the same message the
|
|
1668
|
+
// auto-start surfaces give — `agents routines start` is a convenience
|
|
1669
|
+
// wrapper around the daemon, not the deliberate override. Use
|
|
1670
|
+
// `agents daemon start` to bypass daemon.enabled explicitly.
|
|
1671
|
+
assertDaemonEnabled();
|
|
1654
1672
|
assertSchedulerEnabled();
|
|
1655
1673
|
}
|
|
1656
1674
|
catch (err) {
|
|
@@ -149,6 +149,23 @@ export declare function renderViewStatusLine(b: SecretsBundle, heldExpiresAt: nu
|
|
|
149
149
|
* and a hard abort at inject time, i.e. after it had already broken something.
|
|
150
150
|
*/
|
|
151
151
|
export declare function renderExpiringCol(b: SecretsBundle, now?: number): string;
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an existing import target bundle (inheriting its backend) or create a
|
|
154
|
+
* new one with the requested backend. Refuses to silently downgrade a
|
|
155
|
+
* keychain-backed bundle to `file` — shared by every `import` source so the
|
|
156
|
+
* guard can't drift between them.
|
|
157
|
+
*
|
|
158
|
+
* `force` additionally recreates a bundle whose METADATA RECORD is present but
|
|
159
|
+
* undecryptable — a file store whose key was lost or rotated out from under it.
|
|
160
|
+
* That is precisely the state provisioning exists to repair (import is how a box
|
|
161
|
+
* gets its bundles back), and without this the import dies on `readBundle` and
|
|
162
|
+
* the only route left is deleting the record by hand on an already-degraded
|
|
163
|
+
* store. It is gated on `--force` on purpose: recreating unconditionally would
|
|
164
|
+
* destroy a perfectly healthy bundle for someone who merely forgot to set
|
|
165
|
+
* `AGENTS_SECRETS_PASSPHRASE`, which is the hazard `readBundleIfDecryptable`
|
|
166
|
+
* exists to describe. `--force` already means "overwrite what is there".
|
|
167
|
+
*/
|
|
168
|
+
export declare function resolveImportBundle(name: string, backendOpt: string | undefined, synced?: boolean, force?: boolean): SecretsBundle;
|
|
152
169
|
/** Register the `agents secrets` command tree. */
|
|
153
170
|
export declare function registerSecretsCommands(program: Command): void;
|
|
154
171
|
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
package/dist/commands/secrets.js
CHANGED
|
@@ -804,16 +804,31 @@ export function renderExpiringCol(b, now = Date.now()) {
|
|
|
804
804
|
* new one with the requested backend. Refuses to silently downgrade a
|
|
805
805
|
* keychain-backed bundle to `file` — shared by every `import` source so the
|
|
806
806
|
* guard can't drift between them.
|
|
807
|
+
*
|
|
808
|
+
* `force` additionally recreates a bundle whose METADATA RECORD is present but
|
|
809
|
+
* undecryptable — a file store whose key was lost or rotated out from under it.
|
|
810
|
+
* That is precisely the state provisioning exists to repair (import is how a box
|
|
811
|
+
* gets its bundles back), and without this the import dies on `readBundle` and
|
|
812
|
+
* the only route left is deleting the record by hand on an already-degraded
|
|
813
|
+
* store. It is gated on `--force` on purpose: recreating unconditionally would
|
|
814
|
+
* destroy a perfectly healthy bundle for someone who merely forgot to set
|
|
815
|
+
* `AGENTS_SECRETS_PASSPHRASE`, which is the hazard `readBundleIfDecryptable`
|
|
816
|
+
* exists to describe. `--force` already means "overwrite what is there".
|
|
807
817
|
*/
|
|
808
|
-
function resolveImportBundle(name, backendOpt, synced = false) {
|
|
818
|
+
export function resolveImportBundle(name, backendOpt, synced = false, force = false) {
|
|
809
819
|
const requestedBackend = synced ? 'vault' : resolveBackendOpt(backendOpt);
|
|
810
820
|
if (bundleExists(name)) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
821
|
+
// readBundleIfDecryptable nulls ONLY on BundleUndecryptableError; a locked
|
|
822
|
+
// keychain or logged-out vault still throws, so a recoverable state can
|
|
823
|
+
// never be mistaken for a lost key and silently overwritten.
|
|
824
|
+
const bundle = force ? readBundleIfDecryptable(name) : readBundle(name);
|
|
825
|
+
if (bundle) {
|
|
826
|
+
if (requestedBackend !== 'keychain' && bundle.backend !== requestedBackend) {
|
|
827
|
+
throw new Error(`Bundle '${name}' already exists with a different backend; ` +
|
|
828
|
+
`delete it first to recreate it as ${requestedBackend === 'vault' ? 'synced' : `${requestedBackend}-backed`}.`);
|
|
829
|
+
}
|
|
830
|
+
return bundle;
|
|
815
831
|
}
|
|
816
|
-
return bundle;
|
|
817
832
|
}
|
|
818
833
|
return { name, backend: requestedBackend === 'keychain' ? undefined : requestedBackend, vars: {} };
|
|
819
834
|
}
|
|
@@ -1984,7 +1999,7 @@ Examples:
|
|
|
1984
1999
|
}
|
|
1985
2000
|
const env = importBundleFromFile(opts.fromFile, passphrase);
|
|
1986
2001
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
1987
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
2002
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced, opts.force);
|
|
1988
2003
|
const { added, skipped } = applyEnvToBundle(bundle, env, opts);
|
|
1989
2004
|
emitSecretAudit({ event: 'secrets.import', bundle: bundle.name, operation: 'import --from-file', source: 'file', status: 'success', keyCount: added });
|
|
1990
2005
|
console.log(chalk.green(`Imported ${added} key(s) from file${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
@@ -1998,7 +2013,7 @@ Examples:
|
|
|
1998
2013
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
1999
2014
|
const target = await resolveHostSshTarget(opts.host);
|
|
2000
2015
|
const env = await remoteResolveEnv(target, resolvedBundleName, { osLookupName: opts.host });
|
|
2001
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
2016
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced, opts.force);
|
|
2002
2017
|
const { added, skipped } = applyEnvToBundle(bundle, env, opts);
|
|
2003
2018
|
emitSecretAudit({ event: 'secrets.import', bundle: bundle.name, operation: 'import --from-ssh', source: 'ssh', host: opts.host, status: 'success', keyCount: added });
|
|
2004
2019
|
console.log(chalk.green(`Imported ${added} key(s) from ${opts.host}${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
@@ -2026,7 +2041,7 @@ Examples:
|
|
|
2026
2041
|
// to downgrade keychain -> file) or creates it with the requested backend
|
|
2027
2042
|
// so a single `import --backend file` works (what `export --host ...
|
|
2028
2043
|
// --remote-backend file` drives on the remote).
|
|
2029
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
2044
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced, opts.force);
|
|
2030
2045
|
if (source.kind === '1password') {
|
|
2031
2046
|
assertOpAvailable();
|
|
2032
2047
|
const vault = await resolveVault(source.vault);
|
|
@@ -2431,16 +2446,16 @@ Examples:
|
|
|
2431
2446
|
.command('lease <bundle>')
|
|
2432
2447
|
.description('Hold only an explicit subset of a bundle until an independent expiry.')
|
|
2433
2448
|
.requiredOption('--keys <keys>', 'Comma-separated key subset')
|
|
2434
|
-
.requiredOption('--
|
|
2449
|
+
.requiredOption('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d)')
|
|
2435
2450
|
.option('--agent <agent>', 'Narrow the lease to one harness; default is global')
|
|
2436
2451
|
.option('--durable', 'Keep the lease across sleep as well as broker restart')
|
|
2437
2452
|
.action(async (name, opts) => {
|
|
2438
2453
|
if (process.platform !== 'darwin') {
|
|
2439
2454
|
throw new Error('Scoped lease brokering is not available on this platform yet.');
|
|
2440
2455
|
}
|
|
2441
|
-
const seconds = parseDuration(opts.
|
|
2456
|
+
const seconds = parseDuration(opts.ttl);
|
|
2442
2457
|
if (seconds === null)
|
|
2443
|
-
throw new Error(`Invalid lease duration '${opts.
|
|
2458
|
+
throw new Error(`Invalid lease duration '${opts.ttl}'.`);
|
|
2444
2459
|
const ttlMs = seconds * 1000;
|
|
2445
2460
|
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2446
2461
|
const { bundle, env } = readAndResolveBundleEnv(name, {
|
|
@@ -2516,7 +2531,7 @@ Examples:
|
|
|
2516
2531
|
.option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
|
|
2517
2532
|
.option('--until <date>', 'Hold until this absolute date or timestamp (for example 2026-08-06T12:00:00Z). Mutually exclusive with --ttl.')
|
|
2518
2533
|
.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.')
|
|
2519
|
-
.option('--
|
|
2534
|
+
.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.')
|
|
2520
2535
|
.option('--all', 'Unlock every configured bundle')
|
|
2521
2536
|
.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>`.')
|
|
2522
2537
|
.action(async (names, opts) => {
|
|
@@ -2602,12 +2617,12 @@ Examples:
|
|
|
2602
2617
|
// (single-instance start lock, #414) and best-effort — never blocks unlock.
|
|
2603
2618
|
ensureDaemonStarted();
|
|
2604
2619
|
let loaded = 0;
|
|
2605
|
-
// An unlock is a deliberate act, so it grants GLOBALLY unless `--
|
|
2620
|
+
// An unlock is a deliberate act, so it grants GLOBALLY unless `--agent`
|
|
2606
2621
|
// narrows it to one harness. It must NOT inherit the ambient
|
|
2607
2622
|
// AGENTS_AGENT_NAME: that silently scoped a terminal unlock to whichever
|
|
2608
2623
|
// agent happened to launch the shell, leaving the grant unreadable to
|
|
2609
2624
|
// every other reader for its whole TTL.
|
|
2610
|
-
const harness = opts.
|
|
2625
|
+
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2611
2626
|
for (const name of targets) {
|
|
2612
2627
|
try {
|
|
2613
2628
|
// noAgent: read the real keychain (one Touch ID) rather than the
|
|
@@ -560,7 +560,7 @@ function helpFor(_f, mode) {
|
|
|
560
560
|
if (mode === 'search') {
|
|
561
561
|
return 'type to filter · ↑↓ navigate · esc exit search · ⏎ resume';
|
|
562
562
|
}
|
|
563
|
-
return 's search · r running · f favorites · *
|
|
563
|
+
return 's search · r running · f favorites · * favorite · c teams · t team · a agent · d device · p project · w window · tab preview · y copy-cmd · ⏎ resume · esc quit';
|
|
564
564
|
}
|
|
565
565
|
/**
|
|
566
566
|
* Launch the interactive session browser. `initial` seeds the filter (e.g.
|
|
@@ -589,8 +589,8 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
|
|
|
589
589
|
// needs it — fetch it once, lazily, the first time running is toggled on.
|
|
590
590
|
let liveCache = null;
|
|
591
591
|
// Re-read every load (it's an mtime-memoized parse of one small file), so the
|
|
592
|
-
// `*` key's reload picks up the
|
|
593
|
-
//
|
|
592
|
+
// `*` key's reload picks up the favorite it just wrote — and so does one
|
|
593
|
+
// favorited by another session on this machine.
|
|
594
594
|
let favorites = new Set();
|
|
595
595
|
// Generation guard: two quick keypresses can start overlapping loads whose
|
|
596
596
|
// SSH fan-outs settle out of order. dynamicPicker's own gen ref guards which
|
|
@@ -700,12 +700,12 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
|
|
|
700
700
|
},
|
|
701
701
|
onKey: (name, f, active, query) => {
|
|
702
702
|
if (name === '*') {
|
|
703
|
-
// Only a row with a real session id can be
|
|
703
|
+
// Only a row with a real session id can be favorited: a projected live row
|
|
704
704
|
// with no id is keyed by pid, which is gone the moment the process is.
|
|
705
705
|
if (!active || active.id.startsWith(LIVE_ROW_PREFIX))
|
|
706
|
-
return 'nothing to
|
|
706
|
+
return 'nothing to favorite on this row';
|
|
707
707
|
const on = toggleFavorite(active.id);
|
|
708
|
-
// reload so the row's
|
|
708
|
+
// reload so the row's favorite glyph is repainted — labels are memoized per row.
|
|
709
709
|
return { flash: on ? `★ favorited ${active.shortId}` : `☆ unfavorited ${active.shortId}`, reload: true };
|
|
710
710
|
}
|
|
711
711
|
// Both cases: `hotkeyToken` hands `onKey` the literal character, and this
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents sessions favorite` — the non-TTY half of the
|
|
2
|
+
* `agents sessions favorite` — the non-TTY half of the favorite hotkey.
|
|
3
3
|
*
|
|
4
|
-
* The `*` hotkey in the interactive browser is how a human
|
|
5
|
-
* is how a script, an agent, or a machine without a TTY does the same thing,
|
|
6
|
-
* it is what makes the feature testable end to end without driving a terminal
|
|
7
|
-
* Both write the one store in `lib/session/favorites.ts`.
|
|
4
|
+
* The `*` hotkey in the interactive browser is how a human favorites a session;
|
|
5
|
+
* this is how a script, an agent, or a machine without a TTY does the same thing,
|
|
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/favorites.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: favoriting
|
|
13
|
+
* the wrong session is invisible until the user wonders where their favorite went.
|
|
14
14
|
*/
|
|
15
15
|
export declare function resolveFavoriteTarget(idQuery: string): {
|
|
16
16
|
id: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents sessions favorite` — the non-TTY half of the
|
|
2
|
+
* `agents sessions favorite` — the non-TTY half of the favorite hotkey.
|
|
3
3
|
*
|
|
4
|
-
* The `*` hotkey in the interactive browser is how a human
|
|
5
|
-
* is how a script, an agent, or a machine without a TTY does the same thing,
|
|
6
|
-
* it is what makes the feature testable end to end without driving a terminal
|
|
7
|
-
* Both write the one store in `lib/session/favorites.ts`.
|
|
4
|
+
* The `*` hotkey in the interactive browser is how a human favorites a session;
|
|
5
|
+
* this is how a script, an agent, or a machine without a TTY does the same thing,
|
|
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/favorites.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -13,15 +13,15 @@ import { isCompleteSessionId } from '../lib/session/discover.js';
|
|
|
13
13
|
import { isFavorite, listFavorites, setFavorite } from '../lib/session/favorites.js';
|
|
14
14
|
/**
|
|
15
15
|
* Resolve one user-typed id (usually the 8-char short id the listing prints) to
|
|
16
|
-
* a full session id. Ambiguity is an ERROR, not a silent first-match:
|
|
17
|
-
* the wrong session is invisible until the user wonders where their
|
|
16
|
+
* a full session id. Ambiguity is an ERROR, not a silent first-match: favoriting
|
|
17
|
+
* the wrong session is invisible until the user wonders where their favorite went.
|
|
18
18
|
*/
|
|
19
19
|
export function resolveFavoriteTarget(idQuery) {
|
|
20
20
|
const matches = findSessionsById(idQuery);
|
|
21
21
|
// A COMPLETE id needs no index entry: the id is the key the store is built on,
|
|
22
22
|
// and requiring a transcript row would refuse exactly the newest sessions — a
|
|
23
|
-
// live one that has not been indexed yet. The browser's `*`
|
|
24
|
-
// the live row, so demanding a DB hit here would make the two disagree.
|
|
23
|
+
// live one that has not been indexed yet. The browser's `*` favorites those
|
|
24
|
+
// from the live row, so demanding a DB hit here would make the two disagree.
|
|
25
25
|
if (matches.length === 0) {
|
|
26
26
|
return isCompleteSessionId(idQuery.trim())
|
|
27
27
|
? { id: idQuery.trim() }
|
|
@@ -36,30 +36,30 @@ export function resolveFavoriteTarget(idQuery) {
|
|
|
36
36
|
export function registerSessionsFavoriteCommand(sessionsCmd) {
|
|
37
37
|
const cmd = sessionsCmd
|
|
38
38
|
.command('favorite')
|
|
39
|
-
.argument('[ids...]', 'Session ids to
|
|
40
|
-
.description('
|
|
41
|
-
.option('--remove', '
|
|
42
|
-
.option('--list', 'List the
|
|
39
|
+
.argument('[ids...]', 'Session ids to favorite (full or short id prefix)')
|
|
40
|
+
.description('Favorite sessions so they are easy to find again — list them with --favorites, or `f` in the browser.')
|
|
41
|
+
.option('--remove', 'Remove the given sessions from favorites instead of adding them')
|
|
42
|
+
.option('--list', 'List the favorited sessions (the default when no ids are given)')
|
|
43
43
|
.option('--json', 'Output JSON');
|
|
44
44
|
setHelpSections(cmd, {
|
|
45
45
|
examples: `
|
|
46
|
-
#
|
|
46
|
+
# Favorite a session by its short id (the 8 chars the listing prints)
|
|
47
47
|
agents sessions favorite 26c27162
|
|
48
48
|
|
|
49
|
-
# See what is
|
|
49
|
+
# See what is favorited
|
|
50
50
|
agents sessions favorite --list
|
|
51
51
|
|
|
52
|
-
# Browse only the
|
|
52
|
+
# Browse only the favorited ones
|
|
53
53
|
agents sessions --favorites
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Remove it from favorites again
|
|
56
56
|
agents sessions favorite 26c27162 --remove
|
|
57
57
|
`,
|
|
58
58
|
notes: `
|
|
59
|
-
In the interactive browser (\`agents sessions\`), \`*\`
|
|
60
|
-
session and \`f\` filters the list down to the
|
|
59
|
+
In the interactive browser (\`agents sessions\`), \`*\` favorites the highlighted
|
|
60
|
+
session and \`f\` filters the list down to the favorited ones.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Favorites live in ~/.agents/.history/favorites.json, keyed by session id, so
|
|
63
63
|
they survive a reindex of the session cache. They are per-machine: session
|
|
64
64
|
sync carries transcripts, not this file.
|
|
65
65
|
`,
|
|
@@ -74,18 +74,18 @@ export function registerSessionsFavoriteCommand(sessionsCmd) {
|
|
|
74
74
|
// owns; it is still declared on this command so `--help` documents it.
|
|
75
75
|
const json = self.optsWithGlobals().json === true;
|
|
76
76
|
if (options.list || ids.length === 0) {
|
|
77
|
-
const
|
|
77
|
+
const favorited = [...listFavorites()].sort();
|
|
78
78
|
if (json) {
|
|
79
|
-
process.stdout.write(JSON.stringify({ favorites:
|
|
79
|
+
process.stdout.write(JSON.stringify({ favorites: favorited }, null, 2) + '\n');
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
if (
|
|
83
|
-
console.log(chalk.gray('No favorited sessions.
|
|
82
|
+
if (favorited.length === 0) {
|
|
83
|
+
console.log(chalk.gray('No favorited sessions. Favorite one with `agents sessions favorite <id>`.'));
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
-
for (const id of
|
|
86
|
+
for (const id of favorited)
|
|
87
87
|
console.log(`${chalk.yellow('★')} ${id}`);
|
|
88
|
-
console.log(chalk.gray(`\n${
|
|
88
|
+
console.log(chalk.gray(`\n${favorited.length} favorite${favorited.length === 1 ? '' : 's'}.`));
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
const on = !options.remove;
|
|
@@ -96,8 +96,8 @@ export function registerSessionsFavoriteCommand(sessionsCmd) {
|
|
|
96
96
|
results.push({ query: idQuery, error: resolved.error });
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
//
|
|
100
|
-
// no-op the store already short-circuits — report the resulting state.
|
|
99
|
+
// Unfavoriting something that was never favorited, or favoriting it twice,
|
|
100
|
+
// is a no-op the store already short-circuits — report the resulting state.
|
|
101
101
|
setFavorite(resolved.id, on);
|
|
102
102
|
results.push({ query: idQuery, id: resolved.id, favorite: isFavorite(resolved.id) });
|
|
103
103
|
}
|
|
@@ -112,8 +112,8 @@ export function registerSessionsFavoriteCommand(sessionsCmd) {
|
|
|
112
112
|
console.log(`${r.favorite ? chalk.yellow('★ favorited') : chalk.gray('☆ unfavorited')} ${r.id}`);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
// A failed lookup is a failed command — a script must not read "
|
|
116
|
-
// a zero exit when nothing was
|
|
115
|
+
// A failed lookup is a failed command — a script must not read "favorited"
|
|
116
|
+
// from a zero exit when nothing was favorited.
|
|
117
117
|
if (results.some((r) => r.error))
|
|
118
118
|
process.exitCode = 1;
|
|
119
119
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionEvent, SessionMeta, TodoProgress } from '../lib/session/types.js';
|
|
2
|
-
import { classifyFileChanges } from '../lib/session/digest.js';
|
|
2
|
+
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
3
|
+
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from '../lib/session/highlights.js';
|
|
3
4
|
/**
|
|
4
5
|
* Compact checklist tally for list rows and previews (RUSH-2045).
|
|
5
6
|
* Example: `✓6/8 · A5 wiring runner`. Empty string when there is no list.
|
|
@@ -31,6 +32,12 @@ export interface SessionPickerConfig {
|
|
|
31
32
|
/** Lines the caller printed above the prompt (hidden-session footer). */
|
|
32
33
|
linesAbovePrompt?: number;
|
|
33
34
|
}
|
|
35
|
+
export declare function clearPreviewMemoryCacheForTest(): void;
|
|
36
|
+
export declare function loadSessionPreviewDigest(session: SessionMeta): {
|
|
37
|
+
digest?: SessionPreviewDigest;
|
|
38
|
+
events: SessionEvent[];
|
|
39
|
+
error?: string;
|
|
40
|
+
};
|
|
34
41
|
/** Build a cached multi-line preview string for display in the session picker. */
|
|
35
42
|
export declare function buildPreview(session: SessionMeta): string;
|
|
36
43
|
/**
|
|
@@ -65,6 +72,31 @@ export declare function extractTiming(session: Pick<SessionMeta, 'timestamp' | '
|
|
|
65
72
|
lastActiveAgo?: string;
|
|
66
73
|
duration?: string;
|
|
67
74
|
};
|
|
75
|
+
export interface SessionPreviewDigest {
|
|
76
|
+
schemaVersion: 1;
|
|
77
|
+
firstUser: string;
|
|
78
|
+
lastAssistant: string;
|
|
79
|
+
filesRead: number;
|
|
80
|
+
toolCalls: number;
|
|
81
|
+
planFile: string;
|
|
82
|
+
todos?: TodoProgress;
|
|
83
|
+
subAgentCount: number;
|
|
84
|
+
toolTags: string[];
|
|
85
|
+
changes: ReturnType<typeof changeCounts>;
|
|
86
|
+
dirs: string[];
|
|
87
|
+
repos: string[];
|
|
88
|
+
artifacts: ReturnType<typeof extractArtifacts>;
|
|
89
|
+
skills: ReturnType<typeof extractSkills>;
|
|
90
|
+
plugins: string[];
|
|
91
|
+
hooks: ReturnType<typeof extractHooks>;
|
|
92
|
+
links: ReturnType<typeof extractLinks>;
|
|
93
|
+
errorCount: number;
|
|
94
|
+
firstError?: string;
|
|
95
|
+
toolHistogram: ReturnType<typeof toolHistogram>;
|
|
96
|
+
test: ReturnType<typeof detectTestResult>;
|
|
97
|
+
}
|
|
98
|
+
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
99
|
+
export declare function buildSessionPreviewDigest(events: SessionEvent[], session: SessionMeta): SessionPreviewDigest;
|
|
68
100
|
/**
|
|
69
101
|
* Unique directories the session touched, compact and human-readable.
|
|
70
102
|
* Prefer `session.recentDirectoriesTouched` — the scan records it on the row, so
|