@phnx-labs/agents-cli 1.22.13 → 1.22.15
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 +30 -0
- package/README.md +4 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/routines.js +53 -71
- package/dist/commands/secrets.js +10 -6
- package/dist/commands/sessions-optimize.d.ts +14 -0
- package/dist/commands/sessions-optimize.js +41 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- 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 +4 -3
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +17 -0
- package/dist/lib/projects.js +17 -1
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/session/db.d.ts +19 -0
- package/dist/lib/session/db.js +24 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
|
@@ -14,7 +14,7 @@ import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemo
|
|
|
14
14
|
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
15
15
|
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
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,
|
|
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, } from '../lib/routines.js';
|
|
18
18
|
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
19
19
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
20
20
|
import { getRoutinesDir } from '../lib/state.js';
|
|
@@ -29,6 +29,9 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
29
29
|
import { loadDevices, loadDevicesSync } from '../lib/devices/registry.js';
|
|
30
30
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
31
31
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
32
|
+
import { devicesWithRoutineEnabled } from '../lib/routine-activation.js';
|
|
33
|
+
import { spawnSync } from 'node:child_process';
|
|
34
|
+
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
32
35
|
/**
|
|
33
36
|
* Human-friendly wall-clock a run took (e.g. " · 3 min", " · 45 sec"), or ""
|
|
34
37
|
* when it hasn't completed or timestamps are unparseable. Leading separator lets
|
|
@@ -131,7 +134,7 @@ function placementTag(job) {
|
|
|
131
134
|
return '->cloud';
|
|
132
135
|
}
|
|
133
136
|
function deviceLabel(job, width) {
|
|
134
|
-
const full = [job.
|
|
137
|
+
const full = [devicesWithRoutineEnabled(job.name).join(','), placementTag(job)]
|
|
135
138
|
.filter(Boolean)
|
|
136
139
|
.join(' ');
|
|
137
140
|
const raw = full.length === 0 ? 'all' : full;
|
|
@@ -182,9 +185,9 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
182
185
|
add(`host:${normalizeHost(host)}`, titleWithDeviceState(`Host: ${host}`, host, registry), job);
|
|
183
186
|
continue;
|
|
184
187
|
}
|
|
185
|
-
const devices = job.
|
|
188
|
+
const devices = devicesWithRoutineEnabled(job.name);
|
|
186
189
|
if (devices.length === 0) {
|
|
187
|
-
add('
|
|
190
|
+
add('disabled', 'Disabled', job);
|
|
188
191
|
continue;
|
|
189
192
|
}
|
|
190
193
|
for (const device of devices) {
|
|
@@ -202,12 +205,14 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
202
205
|
return 0;
|
|
203
206
|
if (group.key === 'fleet')
|
|
204
207
|
return 1;
|
|
205
|
-
if (group.key === '
|
|
208
|
+
if (group.key === 'disabled')
|
|
206
209
|
return 2;
|
|
207
|
-
if (group.key
|
|
210
|
+
if (group.key === 'cloud')
|
|
208
211
|
return 3;
|
|
209
|
-
if (group.key.startsWith('
|
|
212
|
+
if (group.key.startsWith('device:'))
|
|
210
213
|
return 4;
|
|
214
|
+
if (group.key.startsWith('host:'))
|
|
215
|
+
return 5;
|
|
211
216
|
return 5;
|
|
212
217
|
};
|
|
213
218
|
return [...groups.values()].sort((a, b) => order(a) - order(b) || a.title.localeCompare(b.title));
|
|
@@ -545,6 +550,7 @@ export function registerRoutinesCommands(program) {
|
|
|
545
550
|
const nowJson = new Date();
|
|
546
551
|
const payload = jobs.map((job) => {
|
|
547
552
|
const latestRun = localLatestRun(job);
|
|
553
|
+
const enabledDevices = devicesWithRoutineEnabled(job.name);
|
|
548
554
|
return {
|
|
549
555
|
name: job.name,
|
|
550
556
|
agent: job.agent ?? null,
|
|
@@ -555,7 +561,8 @@ export function registerRoutinesCommands(program) {
|
|
|
555
561
|
scheduleHuman: fireConditionLabel(job),
|
|
556
562
|
trigger: job.trigger ?? null,
|
|
557
563
|
timezone: job.timezone ?? null,
|
|
558
|
-
devices:
|
|
564
|
+
devices: enabledDevices,
|
|
565
|
+
enabledDevices,
|
|
559
566
|
host: job.host ?? null,
|
|
560
567
|
hostStrategy: resolveHostStrategy(job),
|
|
561
568
|
source: job.source ?? null,
|
|
@@ -628,7 +635,7 @@ export function registerRoutinesCommands(program) {
|
|
|
628
635
|
.option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
|
|
629
636
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
630
637
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
631
|
-
.option('--run-on <name>', 'BODY placement: execute the job body on this machine over SSH (registered host, device, capability tag, or user@host). Sets hostStrategy=host.
|
|
638
|
+
.option('--run-on <name>', 'BODY placement: execute the job body on this machine over SSH (registered host, device, capability tag, or user@host). Sets hostStrategy=host. Same model as agents run --where device:<name> (docs/00-concepts.md#placement).')
|
|
632
639
|
.option('--placement <strategy>', `BODY placement strategy: ${HOST_STRATEGIES.join('|')} (default: local, or host when --run-on is set). Maps to the shared Placement model (local|device|fleet|cloud). Not the same as --host (which manages routines on a remote machine).`)
|
|
633
640
|
.option('--run-cwd <dir>', 'Working directory on the --run-on host (--remote-cwd is taken by the remote-management passthrough)')
|
|
634
641
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
@@ -727,15 +734,6 @@ export function registerRoutinesCommands(program) {
|
|
|
727
734
|
console.error(chalk.red('--placement host requires --run-on <name>'));
|
|
728
735
|
process.exit(1);
|
|
729
736
|
}
|
|
730
|
-
// Off-box placement without a --devices pin would fire on EVERY daemon
|
|
731
|
-
// in the fleet, each dispatching once — duplicate runs (RUSH-1980).
|
|
732
|
-
// Pin to this machine unless the user chose an explicit eligibility set.
|
|
733
|
-
const strategyForPin = hostStrategy
|
|
734
|
-
?? (options.runOn ? 'host' : 'local');
|
|
735
|
-
if (placementRequiresFiringPin(strategyForPin) && !devices) {
|
|
736
|
-
devices = [machineId()];
|
|
737
|
-
console.error(chalk.gray(`--placement ${strategyForPin} with no --devices: pinned firing to this machine (${devices[0]}).`));
|
|
738
|
-
}
|
|
739
737
|
const config = {
|
|
740
738
|
name: nameOrPath,
|
|
741
739
|
...(schedule ? { schedule } : {}),
|
|
@@ -767,6 +765,7 @@ export function registerRoutinesCommands(program) {
|
|
|
767
765
|
process.exit(1);
|
|
768
766
|
}
|
|
769
767
|
writeJob(config);
|
|
768
|
+
setJobEnabled(config.name, config.enabled && (!devices || devices.map(normalizeHost).includes(normalizeHost(machineId()))));
|
|
770
769
|
if (options.json) {
|
|
771
770
|
writeJson({
|
|
772
771
|
ok: true,
|
|
@@ -831,14 +830,8 @@ export function registerRoutinesCommands(program) {
|
|
|
831
830
|
config.runOnce = true;
|
|
832
831
|
console.error(chalk.yellow(`Schedule "${config.schedule}" pins minute, hour, day, and month; treating it as one-shot. Prefer --at for one-time routines.`));
|
|
833
832
|
}
|
|
834
|
-
// Same duplicate-fire guard as --placement/--run-on: off-box placement
|
|
835
|
-
// with no eligibility pin would fire from every daemon in the fleet.
|
|
836
|
-
const fileStrategy = resolveHostStrategy(config);
|
|
837
|
-
if (placementRequiresFiringPin(fileStrategy) && (!config.devices || config.devices.length === 0)) {
|
|
838
|
-
config.devices = [machineId()];
|
|
839
|
-
console.error(chalk.gray(`${fileStrategy} placement with no devices pin: pinned firing to this machine (${config.devices[0]}).`));
|
|
840
|
-
}
|
|
841
833
|
writeJob(config);
|
|
834
|
+
setJobEnabled(config.name, config.enabled && (!config.devices || config.devices.map(normalizeHost).includes(normalizeHost(machineId()))));
|
|
842
835
|
if (options.json) {
|
|
843
836
|
writeJson({
|
|
844
837
|
ok: true,
|
|
@@ -1435,12 +1428,13 @@ export function registerRoutinesCommands(program) {
|
|
|
1435
1428
|
process.exit(1);
|
|
1436
1429
|
}
|
|
1437
1430
|
});
|
|
1438
|
-
//
|
|
1431
|
+
// Device activation management for a single routine. Each mutation executes
|
|
1432
|
+
// on the target device so only that host writes devices/<hostname>/agents.yaml.
|
|
1439
1433
|
routinesCmd
|
|
1440
1434
|
.command('devices [name]')
|
|
1441
|
-
.description('View or change
|
|
1442
|
-
.option('--set <devices>', 'Replace the
|
|
1443
|
-
.option('--clear', '
|
|
1435
|
+
.description('View or change the devices where a routine is enabled. Without flags, opens an interactive picker (requires a TTY).')
|
|
1436
|
+
.option('--set <devices>', 'Replace the enabled-device set with this comma-separated list')
|
|
1437
|
+
.option('--clear', 'Disable the routine on every registered device')
|
|
1444
1438
|
.action(async (name, options) => {
|
|
1445
1439
|
const hasSet = options.set !== undefined;
|
|
1446
1440
|
if (hasSet && options.clear) {
|
|
@@ -1457,33 +1451,37 @@ export function registerRoutinesCommands(program) {
|
|
|
1457
1451
|
console.log(chalk.red(`Job '${name}' not found`));
|
|
1458
1452
|
process.exit(1);
|
|
1459
1453
|
}
|
|
1454
|
+
const applyDevices = async (selected) => {
|
|
1455
|
+
const registry = await loadDevices();
|
|
1456
|
+
const registered = Object.keys(registry).map(normalizeHost);
|
|
1457
|
+
const all = [...new Set([...registered, machineId()].map(normalizeHost))].sort();
|
|
1458
|
+
const selectedSet = new Set(selected.map(normalizeHost));
|
|
1459
|
+
const unknown = [...selectedSet].filter((device) => !all.includes(device));
|
|
1460
|
+
if (unknown.length > 0)
|
|
1461
|
+
throw new Error(`Unknown device${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
1462
|
+
for (const device of all) {
|
|
1463
|
+
const action = selectedSet.has(device) ? 'resume' : 'pause';
|
|
1464
|
+
if (device === normalizeHost(machineId())) {
|
|
1465
|
+
setJobEnabled(name, action === 'resume');
|
|
1466
|
+
if (isDaemonRunning())
|
|
1467
|
+
signalDaemonReload();
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const launch = getCliLaunch(['routines', action, name, '--host', device]);
|
|
1471
|
+
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
1472
|
+
if ((result.status ?? 1) !== 0)
|
|
1473
|
+
throw new Error(`Could not ${action} '${name}' on ${device}`);
|
|
1474
|
+
}
|
|
1475
|
+
console.log(chalk.green(selected.length === 0
|
|
1476
|
+
? `Routine '${name}' disabled on every registered device`
|
|
1477
|
+
: `Routine '${name}' enabled on: ${selected.join(', ')}`));
|
|
1478
|
+
};
|
|
1460
1479
|
if (options.clear) {
|
|
1461
|
-
|
|
1462
|
-
if (placementRequiresFiringPin(strategy)) {
|
|
1463
|
-
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1464
|
-
console.error(chalk.gray(`Keep a single-machine pin (e.g. --set ${machineId()}) or switch to --placement local.`));
|
|
1465
|
-
process.exit(1);
|
|
1466
|
-
}
|
|
1467
|
-
job.devices = undefined;
|
|
1468
|
-
writeJob(job);
|
|
1469
|
-
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
1470
|
-
if (isDaemonRunning())
|
|
1471
|
-
signalDaemonReload();
|
|
1480
|
+
await applyDevices([]);
|
|
1472
1481
|
return;
|
|
1473
1482
|
}
|
|
1474
1483
|
if (hasSet) {
|
|
1475
|
-
|
|
1476
|
-
job.devices = devices;
|
|
1477
|
-
// Re-validate so host/fleet/cloud can't be left with an empty set via --set ""
|
|
1478
|
-
const errs = validateJob(job);
|
|
1479
|
-
if (errs.length > 0) {
|
|
1480
|
-
console.error(chalk.red(errs.join('\n')));
|
|
1481
|
-
process.exit(1);
|
|
1482
|
-
}
|
|
1483
|
-
writeJob(job);
|
|
1484
|
-
console.log(chalk.green(`Devices for '${name}' set to: ${devices.join(', ')}`));
|
|
1485
|
-
if (isDaemonRunning())
|
|
1486
|
-
signalDaemonReload();
|
|
1484
|
+
await applyDevices(await parseAndValidateDevices(options.set));
|
|
1487
1485
|
return;
|
|
1488
1486
|
}
|
|
1489
1487
|
// Interactive picker
|
|
@@ -1496,34 +1494,18 @@ export function registerRoutinesCommands(program) {
|
|
|
1496
1494
|
console.log(chalk.yellow('No devices registered. Enroll with: agents devices sync'));
|
|
1497
1495
|
return;
|
|
1498
1496
|
}
|
|
1499
|
-
const currentSet = new Set((
|
|
1497
|
+
const currentSet = new Set(devicesWithRoutineEnabled(name).map(normalizeHost));
|
|
1500
1498
|
try {
|
|
1501
1499
|
const { checkbox } = await import('@inquirer/prompts');
|
|
1502
1500
|
const selected = await checkbox({
|
|
1503
|
-
message: `Devices
|
|
1501
|
+
message: `Devices where '${name}' is enabled (space to toggle, enter to confirm):`,
|
|
1504
1502
|
choices: registeredNames.map((d) => ({
|
|
1505
1503
|
value: d,
|
|
1506
1504
|
name: d,
|
|
1507
1505
|
checked: currentSet.has(d),
|
|
1508
1506
|
})),
|
|
1509
1507
|
});
|
|
1510
|
-
|
|
1511
|
-
const strategy = resolveHostStrategy(job);
|
|
1512
|
-
if (placementRequiresFiringPin(strategy)) {
|
|
1513
|
-
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
job.devices = undefined;
|
|
1517
|
-
writeJob(job);
|
|
1518
|
-
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
1519
|
-
}
|
|
1520
|
-
else {
|
|
1521
|
-
job.devices = selected;
|
|
1522
|
-
writeJob(job);
|
|
1523
|
-
console.log(chalk.green(`Devices for '${name}' set to: ${selected.join(', ')}`));
|
|
1524
|
-
}
|
|
1525
|
-
if (isDaemonRunning())
|
|
1526
|
-
signalDaemonReload();
|
|
1508
|
+
await applyDevices(selected);
|
|
1527
1509
|
}
|
|
1528
1510
|
catch (err) {
|
|
1529
1511
|
if (isPromptCancelled(err)) {
|
package/dist/commands/secrets.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
|
18
18
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
|
-
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
23
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
24
24
|
import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
|
|
@@ -1254,7 +1254,10 @@ export function registerSecretsCommands(program) {
|
|
|
1254
1254
|
const { env } = readAndResolveBundleEnv(bundle.name, {
|
|
1255
1255
|
caller: 'view --reveal --json',
|
|
1256
1256
|
keyMode: 'storage',
|
|
1257
|
-
|
|
1257
|
+
// An explicit human `--reveal` at a terminal is a deliberate value
|
|
1258
|
+
// access → resolve interactively (one Touch ID). Under an agent
|
|
1259
|
+
// (AGENTS_RUNTIME) or headless (no TTY) it stays broker-only.
|
|
1260
|
+
agentOnly: isHeadlessSecretsContext() || !isInteractiveTerminal(),
|
|
1258
1261
|
});
|
|
1259
1262
|
for (const entry of entries) {
|
|
1260
1263
|
if (entry.kind === 'keychain' && env[entry.key] !== undefined) {
|
|
@@ -1369,15 +1372,16 @@ export function registerSecretsCommands(program) {
|
|
|
1369
1372
|
console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
|
|
1370
1373
|
process.exit(1);
|
|
1371
1374
|
}
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
+
// An explicit human `view --reveal` at a terminal is a deliberate value
|
|
1376
|
+
// access: resolve interactively (one Touch ID) so a locked bundle shows
|
|
1377
|
+
// its values. Under an agent (AGENTS_RUNTIME) or headless (no TTY) it
|
|
1378
|
+
// stays broker-only and errors with the unlock hint — never a sheet.
|
|
1375
1379
|
const revealedValues = new Map();
|
|
1376
1380
|
if (reveal) {
|
|
1377
1381
|
const { env } = readAndResolveBundleEnv(bundle.name, {
|
|
1378
1382
|
caller: 'view --reveal',
|
|
1379
1383
|
keyMode: 'storage',
|
|
1380
|
-
agentOnly:
|
|
1384
|
+
agentOnly: isHeadlessSecretsContext() || !isInteractiveTerminal(),
|
|
1381
1385
|
});
|
|
1382
1386
|
for (const entry of entries) {
|
|
1383
1387
|
if (entry.kind === 'keychain' && env[entry.key] !== undefined) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions optimize` — compact the FTS5 session/tool-call search index.
|
|
3
|
+
*
|
|
4
|
+
* The scanner delete+inserts a session's docs into the `tool_call_text` /
|
|
5
|
+
* `session_text` FTS5 indexes on every rescan (and on every extractor-version
|
|
6
|
+
* bump), and FTS5 never merges the resulting segments on its own. Over thousands
|
|
7
|
+
* of sessions and re-index passes the `%_data` shadow tables bloat with hundreds
|
|
8
|
+
* of thousands of unmerged segments — gigabytes of index for tens of MB of
|
|
9
|
+
* actual content — and `agents sessions` queries slow to a crawl. This runs the
|
|
10
|
+
* FTS5 `'optimize'` command to merge every segment into one and purge tombstones.
|
|
11
|
+
* Non-destructive: no searchable content is lost.
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
export declare function registerSessionsOptimizeCommand(sessionsCmd: Command): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { optimizeSessionSearchIndex } from '../lib/session/db.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
export function registerSessionsOptimizeCommand(sessionsCmd) {
|
|
5
|
+
const cmd = sessionsCmd
|
|
6
|
+
.command('optimize')
|
|
7
|
+
.description('Compact the session search index (FTS5), reclaiming bloat from repeated re-indexing')
|
|
8
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
9
|
+
.action((_opts, c) => {
|
|
10
|
+
const opts = c.optsWithGlobals();
|
|
11
|
+
const results = optimizeSessionSearchIndex();
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
console.log(JSON.stringify(results, null, 2));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const r of results) {
|
|
17
|
+
const merged = Math.max(0, r.segmentsBefore - r.segmentsAfter);
|
|
18
|
+
console.log(` ${chalk.cyan(r.table)}: ${r.segmentsBefore} -> ${r.segmentsAfter} segments ` +
|
|
19
|
+
`(${chalk.green(merged)} merged)`);
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk.gray(' Reclaimed space stays as reusable pages inside the file; VACUUM (daemon stopped) returns it to disk.'));
|
|
22
|
+
});
|
|
23
|
+
setHelpSections(cmd, {
|
|
24
|
+
examples: `
|
|
25
|
+
# Compact the session/tool search index once it has grown fragmented
|
|
26
|
+
agents sessions optimize
|
|
27
|
+
|
|
28
|
+
# Machine-readable segment counts
|
|
29
|
+
agents sessions optimize --json
|
|
30
|
+
|
|
31
|
+
# Wire it to a weekly routine so the index never re-bloats
|
|
32
|
+
agents routines add sessions-optimize --schedule "0 4 * * 0" --agent claude \\
|
|
33
|
+
--prompt "Run: agents sessions optimize"
|
|
34
|
+
`,
|
|
35
|
+
notes: `
|
|
36
|
+
- FTS5 appends a segment on every insert and tombstones every delete; the scanner delete+inserts a session's docs on each rescan and never self-merges, so \`tool_call_text_data\` / \`session_text_data\` bloat with unmerged segments — GBs of index for tens of MB of content, and queries slow down.
|
|
37
|
+
- This runs FTS5 \`'optimize'\`: it merges every segment into one and purges tombstones. Non-destructive — no searchable content is lost.
|
|
38
|
+
- Reclaimed space becomes reusable free pages inside the DB file. To return it to the OS, stop the daemon (\`agents routines stop\`) and run \`VACUUM\` against \`~/.agents/.history/sessions/sessions.db\`.
|
|
39
|
+
`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -61,6 +61,7 @@ import { registerSessionsImportCommand } from './sessions-import.js';
|
|
|
61
61
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
62
62
|
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
63
63
|
import { registerSessionsStatsCommand } from './sessions-stats.js';
|
|
64
|
+
import { registerSessionsOptimizeCommand } from './sessions-optimize.js';
|
|
64
65
|
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
65
66
|
import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
|
|
66
67
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
@@ -3811,6 +3812,7 @@ export function registerSessionsCommands(program) {
|
|
|
3811
3812
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
3812
3813
|
registerSessionsBackfillCommand(sessionsCmd);
|
|
3813
3814
|
registerSessionsStatsCommand(sessionsCmd);
|
|
3815
|
+
registerSessionsOptimizeCommand(sessionsCmd);
|
|
3814
3816
|
// Observe-umbrella alias (Phase 3): roster → sessions --active.
|
|
3815
3817
|
registerSessionsObserveAliases(program);
|
|
3816
3818
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
4
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
5
|
+
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
6
|
+
import { devicesWithRoutineEnabled } from '../lib/routine-activation.js';
|
|
7
|
+
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
8
|
+
import { isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
9
|
+
const ROUTINE = 'watchdog';
|
|
10
|
+
export async function runWatchdogSetupWizard() {
|
|
11
|
+
if (!readJob(ROUTINE)) {
|
|
12
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
13
|
+
}
|
|
14
|
+
const registry = await loadDevices();
|
|
15
|
+
const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
|
|
16
|
+
const enabled = new Set(devicesWithRoutineEnabled(ROUTINE).map(normalizeHost));
|
|
17
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
18
|
+
requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --host <device>']);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
22
|
+
const selected = await checkbox({
|
|
23
|
+
message: 'Devices where the watchdog should run:',
|
|
24
|
+
choices: devices.map((device) => ({ name: device, value: device, checked: enabled.has(device) })),
|
|
25
|
+
});
|
|
26
|
+
const selectedSet = new Set(selected);
|
|
27
|
+
for (const device of devices) {
|
|
28
|
+
const on = selectedSet.has(device);
|
|
29
|
+
if (device === normalizeHost(machineId())) {
|
|
30
|
+
setJobEnabled(ROUTINE, on);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const launch = getCliLaunch(['routines', on ? 'resume' : 'pause', ROUTINE, '--host', device]);
|
|
34
|
+
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
35
|
+
if ((result.status ?? 1) !== 0)
|
|
36
|
+
throw new Error(`Could not configure watchdog on ${device}`);
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.green(selected.length === 0
|
|
39
|
+
? 'Watchdog disabled on every registered device'
|
|
40
|
+
: `Watchdog enabled on: ${selected.join(', ')}`));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isPromptCancelled(error)) {
|
|
44
|
+
console.log(chalk.gray('Cancelled'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function registerSetupWatchdogCommand(setup) {
|
|
51
|
+
setup.command('watchdog')
|
|
52
|
+
.description('Choose the devices where the built-in watchdog routine runs.')
|
|
53
|
+
.action(runWatchdogSetupWizard);
|
|
54
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerSetupShareCommand, runShareWizard } from './setup-share.js';
|
|
|
24
24
|
import { registerSetupMineCommand } from './setup-mine.js';
|
|
25
25
|
import { registerSetupSecretsCommand } from './setup-secrets.js';
|
|
26
26
|
import { registerSetupFleetCommand } from './setup-fleet.js';
|
|
27
|
+
import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
|
|
27
28
|
import { runPreferencesStep } from './setup-preferences.js';
|
|
28
29
|
const HOME = os.homedir();
|
|
29
30
|
/**
|
|
@@ -247,6 +248,7 @@ async function runSetupHub() {
|
|
|
247
248
|
{ name: 'share — publish shareable links (Cloudflare R2 + Worker)', value: 'share' },
|
|
248
249
|
{ name: 'secrets — choose storage defaults and import existing credentials', value: 'secrets' },
|
|
249
250
|
{ name: 'fleet — discover Tailscale devices and configure SSH access', value: 'fleet' },
|
|
251
|
+
{ name: 'watchdog — resume stalled agent sessions on selected devices', value: 'watchdog' },
|
|
250
252
|
],
|
|
251
253
|
});
|
|
252
254
|
for (const pick of picks) {
|
|
@@ -261,6 +263,8 @@ async function runSetupHub() {
|
|
|
261
263
|
await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
|
|
262
264
|
else if (pick === 'fleet')
|
|
263
265
|
await import('./setup-fleet.js').then((m) => m.runFleetSetupWizard());
|
|
266
|
+
else if (pick === 'watchdog')
|
|
267
|
+
await runWatchdogSetupWizard();
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
catch (err) {
|
|
@@ -283,6 +287,7 @@ export function registerSetupCommand(program) {
|
|
|
283
287
|
registerSetupMineCommand(setupCmd);
|
|
284
288
|
registerSetupSecretsCommand(setupCmd);
|
|
285
289
|
registerSetupFleetCommand(setupCmd);
|
|
290
|
+
registerSetupWatchdogCommand(setupCmd);
|
|
286
291
|
setHelpSections(setupCmd, {
|
|
287
292
|
examples: `
|
|
288
293
|
# First-time setup (clones the system repo into ~/.agents/.system/)
|
|
@@ -297,12 +302,13 @@ export function registerSetupCommand(program) {
|
|
|
297
302
|
agents setup share
|
|
298
303
|
agents setup secrets
|
|
299
304
|
agents setup fleet
|
|
305
|
+
agents setup watchdog
|
|
300
306
|
`,
|
|
301
307
|
notes: `
|
|
302
308
|
What it does:
|
|
303
309
|
1. Clones the system repo into ~/.agents/.system/
|
|
304
310
|
2. Imports any unmanaged agent installations it finds
|
|
305
|
-
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet)
|
|
311
|
+
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet/watchdog)
|
|
306
312
|
4. On a TTY, asks preferences: which machine you sit at (interactive host)
|
|
307
313
|
and which browser agents drive here — both skippable, both the same
|
|
308
314
|
keys 'agents devices set-interactive' / 'browser profiles set-default' write
|
|
@@ -313,6 +319,7 @@ export function registerSetupCommand(program) {
|
|
|
313
319
|
agents setup share # provision or join a Cloudflare share endpoint
|
|
314
320
|
agents setup secrets # choose secrets backend/policy defaults + import
|
|
315
321
|
agents setup fleet # discover Tailscale devices + configure SSH access
|
|
322
|
+
agents setup watchdog # choose which devices run the watchdog routine
|
|
316
323
|
|
|
317
324
|
To install CLIs from agents.yaml and sync resources into version homes:
|
|
318
325
|
agents sync --local -y
|
package/dist/commands/ssh.d.ts
CHANGED
|
@@ -20,6 +20,17 @@ import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
|
20
20
|
* the row formatting is deterministic in tests.
|
|
21
21
|
*/
|
|
22
22
|
export declare function renderLeasedBoxesSection(boxes: CrabboxBox[], nowSecs: number): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Whether `agents devices list` shows the "Leased boxes" section (RUSH-2190).
|
|
25
|
+
* Opt-in via --all only: loading it scans the keychain for bundle credentials
|
|
26
|
+
* and can raise a Touch ID sheet after the table has printed, so the default
|
|
27
|
+
* list must never reach for it. --no-stats stays the hard "instant, no provider
|
|
28
|
+
* calls" opt-out even when --all is passed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function showLeasedBoxesSection(opts: {
|
|
31
|
+
all?: boolean;
|
|
32
|
+
stats?: boolean;
|
|
33
|
+
}): boolean;
|
|
23
34
|
/**
|
|
24
35
|
* Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
|
|
25
36
|
*
|
package/dist/commands/ssh.js
CHANGED
|
@@ -191,6 +191,16 @@ function loadLeasedBoxesSection() {
|
|
|
191
191
|
return []; // crabbox not installed / no provider creds — omit the section
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether `agents devices list` shows the "Leased boxes" section (RUSH-2190).
|
|
196
|
+
* Opt-in via --all only: loading it scans the keychain for bundle credentials
|
|
197
|
+
* and can raise a Touch ID sheet after the table has printed, so the default
|
|
198
|
+
* list must never reach for it. --no-stats stays the hard "instant, no provider
|
|
199
|
+
* calls" opt-out even when --all is passed.
|
|
200
|
+
*/
|
|
201
|
+
export function showLeasedBoxesSection(opts) {
|
|
202
|
+
return opts.all === true && opts.stats !== false;
|
|
203
|
+
}
|
|
194
204
|
/**
|
|
195
205
|
* `agents ssh <slug>` targeting a leased crabbox box. crabbox provisions a
|
|
196
206
|
* per-lease identity key, so we ssh via crabbox's OWN emitted invocation
|
|
@@ -1152,10 +1162,13 @@ Typical workflow:
|
|
|
1152
1162
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
|
1153
1163
|
}
|
|
1154
1164
|
// Ephemeral crabbox leases live alongside the registered fleet but are never
|
|
1155
|
-
// written into the registry — surface them as their own live section
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1165
|
+
// written into the registry — surface them as their own live section, but only
|
|
1166
|
+
// behind --all (RUSH-2190). Reading them routes through crabboxEnv, whose
|
|
1167
|
+
// bundle auto-detect scans the keychain and can raise a Touch ID sheet AFTER
|
|
1168
|
+
// the table has printed — unacceptable for the default list, which hooks and
|
|
1169
|
+
// other non-interactive callers rely on. The predicate is exported so the
|
|
1170
|
+
// gate itself is unit-tested; keep it the ONLY condition guarding this call.
|
|
1171
|
+
if (showLeasedBoxesSection(opts)) {
|
|
1159
1172
|
for (const line of loadLeasedBoxesSection())
|
|
1160
1173
|
console.log(line);
|
|
1161
1174
|
}
|
|
@@ -1170,6 +1183,7 @@ Typical workflow:
|
|
|
1170
1183
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
1171
1184
|
.option('--live', 'alias of --refresh (shorter to type)')
|
|
1172
1185
|
.option('-f, --full', 'full mode: add per-device core count and free/total memory')
|
|
1186
|
+
.option('--all', 'also show ephemeral leased boxes (live crabbox call; may need bundle secrets)')
|
|
1173
1187
|
.action(runList);
|
|
1174
1188
|
devicesCmd
|
|
1175
1189
|
.command('status')
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
11
|
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog
|
|
13
|
+
* agents watchdog on|off turn the always-on watchdog routine on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
15
|
*
|
|
16
16
|
* The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
|
|
17
|
-
* hand-rolled loop
|
|
18
|
-
*
|
|
19
|
-
* `disable` pauses it. See ../lib/watchdog/routine.ts.
|
|
17
|
+
* hand-rolled loop. Its immutable definition comes from the system DotAgents
|
|
18
|
+
* repo; on/off only changes this device's routine membership.
|
|
20
19
|
*/
|
|
21
20
|
import type { Command } from 'commander';
|
|
22
21
|
/** Register the `agents watchdog` command tree. */
|