@phnx-labs/agents-cli 1.20.93 → 1.21.0
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 +264 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +7 -0
- package/dist/lib/activity.js +19 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -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/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +17 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preferences onboarding — the guided path over the same device-config keys the
|
|
3
|
+
* `agents devices …` commands write (lib/device-config.ts). Two questions, each
|
|
4
|
+
* TTY-only, skippable, and absent non-interactively:
|
|
5
|
+
*
|
|
6
|
+
* 1. "Which machine do you sit at?" → `interactive.host` (central config:)
|
|
7
|
+
* 2. "Which browser should agents drive on THIS machine?"
|
|
8
|
+
* → `browser.profile` (device-local default)
|
|
9
|
+
*
|
|
10
|
+
* Shared by the bare `agents setup` flow (a short step after the capability
|
|
11
|
+
* hub) and by `agents setup fleet` (interactive host after a successful sync).
|
|
12
|
+
* Unset always keeps today's behavior — skipping is a real choice, not a
|
|
13
|
+
* partial state.
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
17
|
+
import { DEFAULT_BROWSER_PROFILE_NAME, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, getProfile, } from '../lib/browser/profiles.js';
|
|
18
|
+
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
19
|
+
import { getConfigValue, setConfigValue } from '../lib/device-config.js';
|
|
20
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
21
|
+
import { machineId } from '../lib/machine-id.js';
|
|
22
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
23
|
+
const SKIP = '__skip__';
|
|
24
|
+
/** Registered macOS device names, sorted — the interactive-host candidates. */
|
|
25
|
+
export function macDeviceNames(reg) {
|
|
26
|
+
return Object.values(reg)
|
|
27
|
+
.filter((d) => d.platform === 'macos')
|
|
28
|
+
.map((d) => d.name)
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The interactive-host picker's highlighted default: this machine when it is a
|
|
33
|
+
* candidate (the common case — you run setup on the box you sit at), else the
|
|
34
|
+
* first candidate. null when there are no candidates.
|
|
35
|
+
*/
|
|
36
|
+
export function defaultInteractiveHostChoice(candidates, self = machineId()) {
|
|
37
|
+
if (candidates.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
return candidates.includes(self) ? self : candidates[0];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The browser picker's highlighted default — the same browser auto-detect
|
|
43
|
+
* would win, since `listInstalledBrowsers` returns platform priority order
|
|
44
|
+
* (macOS: chrome first). null when nothing is installed.
|
|
45
|
+
*/
|
|
46
|
+
export function defaultBrowserChoice(installed) {
|
|
47
|
+
return installed.length > 0 ? installed[0].browserType : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Offer to set the interactive host when none is configured and the registry
|
|
51
|
+
* has more than one macOS device. Returns true when a host was set. Silent
|
|
52
|
+
* no-op non-TTY, when already set, or with fewer than two candidates (the
|
|
53
|
+
* answer is obvious or absent).
|
|
54
|
+
*/
|
|
55
|
+
export async function maybePickInteractiveHost() {
|
|
56
|
+
if (!isInteractiveTerminal())
|
|
57
|
+
return false;
|
|
58
|
+
if (getConfigValue('interactive.host').value !== undefined)
|
|
59
|
+
return false;
|
|
60
|
+
const macs = macDeviceNames(await loadDevices());
|
|
61
|
+
if (macs.length < 2)
|
|
62
|
+
return false;
|
|
63
|
+
const { select } = await import('@inquirer/prompts');
|
|
64
|
+
const self = machineId();
|
|
65
|
+
const picked = await select({
|
|
66
|
+
message: 'Which machine do you sit at? (agents open browser windows and artifacts there)',
|
|
67
|
+
default: defaultInteractiveHostChoice(macs, self) ?? SKIP,
|
|
68
|
+
choices: [
|
|
69
|
+
...macs.map((n) => ({ name: n === self ? `${n} ${chalk.dim('(this machine)')}` : n, value: n })),
|
|
70
|
+
{ name: `Skip ${chalk.dim('— decide later: agents devices set-interactive <name>')}`, value: SKIP },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
if (picked === SKIP)
|
|
74
|
+
return false;
|
|
75
|
+
setConfigValue('interactive.host', picked);
|
|
76
|
+
console.log(chalk.green(`Interactive host: '${picked}'`) + chalk.dim(' — marked ★ interactive in `agents devices list`.'));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
81
|
+
* Only asked when there is nothing to preserve: no configured device default
|
|
82
|
+
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
83
|
+
* choice — never re-pinned behind their back). Returns true when a profile was
|
|
84
|
+
* created and set as the device default. Silent no-op non-TTY.
|
|
85
|
+
*/
|
|
86
|
+
export async function maybePickBrowserProfile() {
|
|
87
|
+
if (!isInteractiveTerminal())
|
|
88
|
+
return false;
|
|
89
|
+
if (getConfiguredDefaultProfileName())
|
|
90
|
+
return false;
|
|
91
|
+
if (await getProfile(DEFAULT_BROWSER_PROFILE_NAME))
|
|
92
|
+
return false;
|
|
93
|
+
const installed = listInstalledBrowsers();
|
|
94
|
+
if (installed.length === 0)
|
|
95
|
+
return false;
|
|
96
|
+
const { select } = await import('@inquirer/prompts');
|
|
97
|
+
const picked = await select({
|
|
98
|
+
message: 'Which browser should agents drive on THIS machine?',
|
|
99
|
+
default: defaultBrowserChoice(installed) ?? SKIP,
|
|
100
|
+
choices: [
|
|
101
|
+
...installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
102
|
+
{ name: `Skip ${chalk.dim('— auto-detect on first use (priority order)')}`, value: SKIP },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
if (picked === SKIP)
|
|
106
|
+
return false;
|
|
107
|
+
const chosen = installed.find((b) => b.browserType === picked) ?? installed[0];
|
|
108
|
+
const freePort = await findFreeProfilePort();
|
|
109
|
+
const profile = {
|
|
110
|
+
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
111
|
+
description: `${chosen.browserType} profile (chosen during setup)`,
|
|
112
|
+
browser: chosen.browserType,
|
|
113
|
+
binary: chosen.binary,
|
|
114
|
+
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
115
|
+
viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
|
|
116
|
+
};
|
|
117
|
+
await createProfile(profile);
|
|
118
|
+
// The same key `agents browser profiles set-default` writes (device-local).
|
|
119
|
+
setConfigValue('browser.profile', profile.name);
|
|
120
|
+
console.log(chalk.green(`Browser: '${chosen.browserType}'`) +
|
|
121
|
+
chalk.dim(` — profile "${profile.name}" is this machine's default (agents browser profiles set-default to change).`));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The bare-`agents setup` preferences step: interactive host, then browser.
|
|
126
|
+
* Runs AFTER the capability hub so it never delays the bootstrap. Never
|
|
127
|
+
* throws — a prompt cancel or a failed pick ends the step quietly and lets
|
|
128
|
+
* setup complete (the same semantics as the capability hub).
|
|
129
|
+
*/
|
|
130
|
+
export async function runPreferencesStep() {
|
|
131
|
+
if (!isInteractiveTerminal())
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const pickedHost = await maybePickInteractiveHost();
|
|
135
|
+
const pickedBrowser = await maybePickBrowserProfile();
|
|
136
|
+
if (pickedHost || pickedBrowser)
|
|
137
|
+
console.log();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Cancel (ctrl-c) or a picker failure — the step is optional; end it.
|
|
141
|
+
}
|
|
142
|
+
}
|
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 { runPreferencesStep } from './setup-preferences.js';
|
|
27
28
|
const HOME = os.homedir();
|
|
28
29
|
/**
|
|
29
30
|
* Import an existing unmanaged agent installation into agents-cli.
|
|
@@ -130,15 +131,6 @@ export async function runSetup(program, options = {}) {
|
|
|
130
131
|
if (dev.ok && dev.synced > 0) {
|
|
131
132
|
console.log(chalk.gray(`Discovered ${dev.synced} device${dev.synced === 1 ? '' : 's'} on your tailnet (agents devices list).`));
|
|
132
133
|
}
|
|
133
|
-
// Offer guided cross-machine session-sync provisioning (interactive, opt-in,
|
|
134
|
-
// and never blocking — any failure/decline falls through to the rest of setup).
|
|
135
|
-
try {
|
|
136
|
-
const { promptAndProvisionSessionSync } = await import('./sync-provision.js');
|
|
137
|
-
await promptAndProvisionSessionSync({ explicit: false });
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
console.log(chalk.yellow(`Session-sync setup skipped: ${err.message}`));
|
|
141
|
-
}
|
|
142
134
|
// Offer to import existing unmanaged installations
|
|
143
135
|
if (unmanaged.length > 0 && isInteractiveTerminal()) {
|
|
144
136
|
console.log(chalk.bold('\nFound existing installations:\n'));
|
|
@@ -201,6 +193,10 @@ export async function runSetup(program, options = {}) {
|
|
|
201
193
|
// own guided flow. TTY-only and fully opt-in — a non-interactive `agents setup`
|
|
202
194
|
// stops at the system-repo bootstrap above, unchanged.
|
|
203
195
|
await runSetupHub();
|
|
196
|
+
// Preferences step: the two questions that keep agents off the wrong machine —
|
|
197
|
+
// which box you sit at (interactive host) and which browser agents drive here.
|
|
198
|
+
// TTY-only, skippable, and writes the same keys as `agents devices …`.
|
|
199
|
+
await runPreferencesStep();
|
|
204
200
|
console.log(chalk.bold('\nSetup complete. Try:'));
|
|
205
201
|
console.log(chalk.cyan(' agents view ') + chalk.gray(' # see what\'s installed'));
|
|
206
202
|
console.log(chalk.cyan(' agents run <agent> "hello" ') + chalk.gray(' # run an agent'));
|
|
@@ -307,6 +303,9 @@ export function registerSetupCommand(program) {
|
|
|
307
303
|
1. Clones the system repo into ~/.agents/.system/
|
|
308
304
|
2. Imports any unmanaged agent installations it finds
|
|
309
305
|
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet)
|
|
306
|
+
4. On a TTY, asks preferences: which machine you sit at (interactive host)
|
|
307
|
+
and which browser agents drive here — both skippable, both the same
|
|
308
|
+
keys 'agents devices set-interactive' / 'browser profiles set-default' write
|
|
310
309
|
|
|
311
310
|
Capability setup can also be run any time on its own:
|
|
312
311
|
agents setup browser # detect a browser + create the default profile
|
package/dist/commands/ssh.js
CHANGED
|
@@ -47,9 +47,12 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
|
|
|
47
47
|
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
|
|
48
48
|
import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
49
49
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
50
|
+
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from '../lib/device-config.js';
|
|
51
|
+
import { setHelpSections } from '../lib/help.js';
|
|
50
52
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
51
|
-
* command is running on so it stands out from the rest of the tailnet.
|
|
52
|
-
|
|
53
|
+
* command is running on so it stands out from the rest of the tailnet.
|
|
54
|
+
* `isInteractive` marks the configured interactive host (`devices set-interactive`). */
|
|
55
|
+
function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
|
|
53
56
|
const addr = hostNameFor(d) ?? chalk.gray('no address');
|
|
54
57
|
// Prefer a fresh live verdict (this run's probe, else the written-back
|
|
55
58
|
// reachability) over the stale tailscale.online snapshot (RUSH-1965).
|
|
@@ -63,7 +66,8 @@ function deviceSummary(d, isSelf = false, stats) {
|
|
|
63
66
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
64
67
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
65
68
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
66
|
-
|
|
69
|
+
const interactive = isInteractive ? chalk.yellow(' ★ interactive') : '';
|
|
70
|
+
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}${interactive}`;
|
|
67
71
|
}
|
|
68
72
|
const HEADROOM_BADGE = {
|
|
69
73
|
idle: chalk.green('○ idle'),
|
|
@@ -90,9 +94,9 @@ function pctCell(v, width) {
|
|
|
90
94
|
* Without it (probe skipped) the classic reachability line is used. A fleet
|
|
91
95
|
* capacity summary is appended whenever stats were gathered.
|
|
92
96
|
*/
|
|
93
|
-
function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
97
|
+
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
94
98
|
if (!statsMap)
|
|
95
|
-
return names.map((n) => deviceSummary(reg[n], n === self));
|
|
99
|
+
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost));
|
|
96
100
|
const lines = [];
|
|
97
101
|
const head = ' ' +
|
|
98
102
|
chalk.gray('device'.padEnd(16)) +
|
|
@@ -132,7 +136,8 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
|
132
136
|
: '';
|
|
133
137
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
134
138
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
135
|
-
|
|
139
|
+
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
140
|
+
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}`);
|
|
136
141
|
}
|
|
137
142
|
// Fleet capacity summary — total cores + how much RAM is free right now.
|
|
138
143
|
const cap = fleetCapacity(statsMap.values());
|
|
@@ -685,10 +690,13 @@ function registerDevicesCommands(program) {
|
|
|
685
690
|
Typical workflow:
|
|
686
691
|
agents devices sync # curate: pick which tailscale nodes to keep (TTY)
|
|
687
692
|
agents devices sync --yes # non-interactive: register all non-ignored nodes
|
|
688
|
-
agents devices list # see what's registered
|
|
693
|
+
agents devices list # see what's registered (★ = interactive host)
|
|
689
694
|
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
690
695
|
agents devices disable zion # exclude a device from Factory auto-launch
|
|
691
696
|
agents devices prefer mac-mini # boost a device in Factory auto-launch ranking
|
|
697
|
+
agents devices set-interactive zion # where agents show YOU artifacts
|
|
698
|
+
agents devices configure mac-mini --max-agents 4 --scheduler off
|
|
699
|
+
agents devices note mac-mini "runs the releases — don't reboot"
|
|
692
700
|
agents devices set win-mini --auth password --bundle muqsit
|
|
693
701
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
694
702
|
agents fleet update # roll out latest agents-cli to every online device
|
|
@@ -824,12 +832,214 @@ Typical workflow:
|
|
|
824
832
|
process.exit(1);
|
|
825
833
|
}
|
|
826
834
|
});
|
|
835
|
+
const setInteractiveCmd = devicesCmd
|
|
836
|
+
.command('set-interactive [name]')
|
|
837
|
+
.description('Get or set the interactive host — the one device that shows YOU artifacts (browser opens, dashboards, rendered plans). Stored fleet-wide as config.interactiveHost in central agents.yaml.')
|
|
838
|
+
.option('--unset', 'clear the interactive host')
|
|
839
|
+
.option('--json', 'output machine-readable JSON')
|
|
840
|
+
.action(async (name, opts) => {
|
|
841
|
+
try {
|
|
842
|
+
if (opts.unset) {
|
|
843
|
+
unsetConfigValue('interactive.host');
|
|
844
|
+
if (opts.json)
|
|
845
|
+
process.stdout.write(JSON.stringify({ interactiveHost: null }, null, 2) + '\n');
|
|
846
|
+
else
|
|
847
|
+
console.log(chalk.green('Cleared the interactive host.'));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (name) {
|
|
851
|
+
await mustGetDevice(name);
|
|
852
|
+
setConfigValue('interactive.host', name);
|
|
853
|
+
if (opts.json)
|
|
854
|
+
process.stdout.write(JSON.stringify({ interactiveHost: name }, null, 2) + '\n');
|
|
855
|
+
else
|
|
856
|
+
console.log(chalk.green(`Interactive host: '${name}'`) + chalk.gray(' — agents show you artifacts there. Clear with --unset.'));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const current = getConfigValue('interactive.host').value;
|
|
860
|
+
if (opts.json) {
|
|
861
|
+
process.stdout.write(JSON.stringify({ interactiveHost: current ?? null }, null, 2) + '\n');
|
|
862
|
+
}
|
|
863
|
+
else if (current) {
|
|
864
|
+
console.log(`${chalk.bold('Interactive host:')} ${chalk.cyan(current)}`);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
console.log(chalk.gray("No interactive host set. Set one with 'agents devices set-interactive <name>'."));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
console.error(chalk.red(err.message));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
setHelpSections(setInteractiveCmd, {
|
|
876
|
+
examples: `
|
|
877
|
+
agents devices set-interactive zion # zion is where artifacts open for you
|
|
878
|
+
agents devices set-interactive # print the current interactive host
|
|
879
|
+
agents devices set-interactive --unset # back to no interactive host
|
|
880
|
+
agents devices set-interactive --json # machine-readable (for skills)
|
|
881
|
+
`,
|
|
882
|
+
notes: `
|
|
883
|
+
The interactive host answers "which online macOS device do I show this on?"
|
|
884
|
+
so skills stop guessing. It is marked ★ interactive in 'agents devices list'.
|
|
885
|
+
The value syncs fleet-wide (central agents.yaml, config.interactiveHost);
|
|
886
|
+
per-device settings live under 'agents devices configure' instead.
|
|
887
|
+
`,
|
|
888
|
+
});
|
|
889
|
+
const configureCmd = devicesCmd
|
|
890
|
+
.command('configure <name>')
|
|
891
|
+
.description('Get or set per-device config: --max-agents, --scheduler. Written to ~/.agents/devices/<name>/agents.yaml (works for any device — the devices/ tree syncs). Unset = default behavior.')
|
|
892
|
+
.option('--max-agents <n>', 'cap concurrent agents (Factory auto-launch counts device-wide; teams placement counts the team’s roster on the device)')
|
|
893
|
+
.option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device (takes effect on daemon reload/restart)')
|
|
894
|
+
.option('--json', 'output machine-readable JSON')
|
|
895
|
+
.action(async (name, opts) => {
|
|
896
|
+
try {
|
|
897
|
+
await mustGetDevice(name);
|
|
898
|
+
const parseOnOff = (flag, raw) => {
|
|
899
|
+
if (raw === 'on')
|
|
900
|
+
return true;
|
|
901
|
+
if (raw === 'off')
|
|
902
|
+
return false;
|
|
903
|
+
throw new Error(`--${flag} expects 'on' or 'off', got '${raw}'.`);
|
|
904
|
+
};
|
|
905
|
+
const writes = [];
|
|
906
|
+
if (opts.maxAgents !== undefined) {
|
|
907
|
+
const n = Number(opts.maxAgents);
|
|
908
|
+
if (!Number.isInteger(n))
|
|
909
|
+
throw new Error(`--max-agents expects an integer, got '${opts.maxAgents}'.`);
|
|
910
|
+
writes.push(['agents.max-concurrent', n]);
|
|
911
|
+
}
|
|
912
|
+
if (opts.scheduler !== undefined)
|
|
913
|
+
writes.push(['scheduler.enabled', parseOnOff('scheduler', opts.scheduler)]);
|
|
914
|
+
if (writes.length > 0) {
|
|
915
|
+
for (const [key, value] of writes)
|
|
916
|
+
setConfigValue(key, value, { device: name });
|
|
917
|
+
if (!opts.json) {
|
|
918
|
+
for (const [key, value] of writes) {
|
|
919
|
+
console.log(chalk.green(`Set ${key} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (opts.json || writes.length === 0) {
|
|
924
|
+
const entries = listConfig({ device: name }).filter((e) => e.spec.scope === 'device');
|
|
925
|
+
if (opts.json) {
|
|
926
|
+
const config = {};
|
|
927
|
+
for (const e of entries)
|
|
928
|
+
if (e.value !== undefined)
|
|
929
|
+
config[e.spec.name] = e.value;
|
|
930
|
+
process.stdout.write(JSON.stringify({ device: name, config }, null, 2) + '\n');
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
console.log(chalk.bold(`Config for '${name}'`));
|
|
934
|
+
for (const e of entries) {
|
|
935
|
+
const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
|
|
936
|
+
console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
console.error(chalk.red(err.message));
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
setHelpSections(configureCmd, {
|
|
947
|
+
examples: `
|
|
948
|
+
agents devices configure mac-mini --max-agents 4 # cap concurrent agents
|
|
949
|
+
agents devices configure mac-mini --scheduler off # no routines firing there
|
|
950
|
+
agents devices configure mac-mini # print its current config
|
|
951
|
+
agents devices configure mac-mini --json # machine-readable
|
|
952
|
+
`,
|
|
953
|
+
notes: `
|
|
954
|
+
Run it on any machine for any device: the value lands in
|
|
955
|
+
~/.agents/devices/<name>/agents.yaml locally and reaches the device on the
|
|
956
|
+
next 'agents repo push/pull'. Unset keys keep today's behavior.
|
|
957
|
+
--scheduler takes effect when the daemon reloads or restarts on that
|
|
958
|
+
device ('agents routines start' / the reload a 'routines add' sends).
|
|
959
|
+
For the default browser profile use 'agents browser profiles set-default
|
|
960
|
+
<name>'; for free-form text use 'agents devices note'.
|
|
961
|
+
`,
|
|
962
|
+
});
|
|
963
|
+
const noteCmd = devicesCmd
|
|
964
|
+
.command('note <name> [text...]')
|
|
965
|
+
.description('Append a free-form note to a device (repeat to append more). No text prints the notes; --clear empties them.')
|
|
966
|
+
.option('--clear', 'remove all notes from the device')
|
|
967
|
+
.option('--json', 'output machine-readable JSON')
|
|
968
|
+
.action(async (name, text, opts) => {
|
|
969
|
+
try {
|
|
970
|
+
await mustGetDevice(name);
|
|
971
|
+
if (opts.clear) {
|
|
972
|
+
unsetConfigValue('notes', { device: name });
|
|
973
|
+
if (opts.json)
|
|
974
|
+
process.stdout.write(JSON.stringify({ device: name, notes: [] }, null, 2) + '\n');
|
|
975
|
+
else
|
|
976
|
+
console.log(chalk.green(`Cleared notes on '${name}'.`));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (text.length > 0) {
|
|
980
|
+
const existing = getConfigValue('notes', { device: name }).value ?? [];
|
|
981
|
+
const notes = [...existing, text.join(' ')];
|
|
982
|
+
setConfigValue('notes', notes, { device: name });
|
|
983
|
+
if (opts.json)
|
|
984
|
+
process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
|
|
985
|
+
else
|
|
986
|
+
console.log(chalk.green(`Noted on '${name}':`) + ` ${text.join(' ')}`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const notes = getConfigValue('notes', { device: name }).value ?? [];
|
|
990
|
+
if (opts.json) {
|
|
991
|
+
process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
|
|
992
|
+
}
|
|
993
|
+
else if (notes.length > 0) {
|
|
994
|
+
console.log(chalk.bold(`Notes for '${name}'`));
|
|
995
|
+
for (const n of notes)
|
|
996
|
+
console.log(` ${chalk.gray('•')} ${n}`);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices note ${name} "..."'.`));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
console.error(chalk.red(err.message));
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
setHelpSections(noteCmd, {
|
|
1008
|
+
examples: `
|
|
1009
|
+
agents devices note mac-mini "runs the releases — don't reboot"
|
|
1010
|
+
agents devices note mac-mini "4 displays attached" # appends a second note
|
|
1011
|
+
agents devices note mac-mini # print its notes
|
|
1012
|
+
agents devices note mac-mini --clear # drop them all
|
|
1013
|
+
`,
|
|
1014
|
+
notes: `
|
|
1015
|
+
Notes are operator memory for a box (why it exists, what to never do to
|
|
1016
|
+
it). They sync like every other device doc; 'agents devices list --json'
|
|
1017
|
+
carries them under config.notes.
|
|
1018
|
+
`,
|
|
1019
|
+
});
|
|
1020
|
+
/** Device-scope config block for `list --json`, keyed by yamlKey (set keys only). */
|
|
1021
|
+
const deviceConfigJson = (name) => {
|
|
1022
|
+
const config = {};
|
|
1023
|
+
for (const entry of listConfig({ device: name })) {
|
|
1024
|
+
if (entry.spec.scope !== 'device' || entry.value === undefined)
|
|
1025
|
+
continue;
|
|
1026
|
+
config[entry.spec.yamlKey] = entry.value;
|
|
1027
|
+
}
|
|
1028
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
1029
|
+
};
|
|
827
1030
|
const runList = async (opts = {}) => {
|
|
828
1031
|
const reg = await loadDevices();
|
|
829
1032
|
const names = Object.keys(reg).sort();
|
|
1033
|
+
const interactiveHost = getConfigValue('interactive.host').value;
|
|
830
1034
|
if (opts.json) {
|
|
831
|
-
// Registry
|
|
832
|
-
|
|
1035
|
+
// Registry + local device docs, always fast — the Factory extension polls
|
|
1036
|
+
// this path. Each row carries its device-scope `config` (maxAgents,
|
|
1037
|
+
// schedulerEnabled, notes, defaultBrowserProfile — set keys
|
|
1038
|
+
// only) and an `interactive` flag for the configured interactive host.
|
|
1039
|
+
process.stdout.write(JSON.stringify(names.map((n) => {
|
|
1040
|
+
const config = deviceConfigJson(n);
|
|
1041
|
+
return { ...reg[n], interactive: n === interactiveHost, ...(config ? { config } : {}) };
|
|
1042
|
+
}), null, 2) + '\n');
|
|
833
1043
|
return;
|
|
834
1044
|
}
|
|
835
1045
|
if (names.length === 0) {
|
|
@@ -867,7 +1077,7 @@ Typical workflow:
|
|
|
867
1077
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
868
1078
|
}
|
|
869
1079
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
870
|
-
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
1080
|
+
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost))
|
|
871
1081
|
console.log(line);
|
|
872
1082
|
if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
|
|
873
1083
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `agents sync` — synchronize central resources into an installed agent version.
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
|
-
* agents sync # umbrella: fetch config repos -> reconcile all (secrets
|
|
6
|
-
* agents sync --repos|--secrets
|
|
5
|
+
* agents sync # umbrella: fetch config repos -> reconcile all (secrets opt-in)
|
|
6
|
+
* agents sync --repos|--secrets # umbrella: fetch only those, then reconcile
|
|
7
7
|
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
8
|
* agents sync --local # umbrella: reconcile all, no fetch
|
|
9
9
|
* agents sync system # one repo: git pull --rebase (pull-only mirror)
|
package/dist/commands/sync.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `agents sync` — synchronize central resources into an installed agent version.
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
|
-
* agents sync # umbrella: fetch config repos -> reconcile all (secrets
|
|
6
|
-
* agents sync --repos|--secrets
|
|
5
|
+
* agents sync # umbrella: fetch config repos -> reconcile all (secrets opt-in)
|
|
6
|
+
* agents sync --repos|--secrets # umbrella: fetch only those, then reconcile
|
|
7
7
|
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
8
|
* agents sync --local # umbrella: reconcile all, no fetch
|
|
9
9
|
* agents sync system # one repo: git pull --rebase (pull-only mirror)
|
|
@@ -47,7 +47,7 @@ import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../l
|
|
|
47
47
|
export function registerSyncCommand(program) {
|
|
48
48
|
addHostOption(program.command('sync [agentSpec] [repo]'))
|
|
49
49
|
.summary('Make this machine current, or sync resources into one agent')
|
|
50
|
-
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nGive a DotAgent repo name ALONE — "agents sync system" / "agents sync user" / "agents sync <alias>" — to git-sync that one repo: refuse if the tree is dirty, else git pull --rebase against origin. The user repo and extra aliases also push local commits up; the system repo is a pull-only mirror.\n\nWith NO agent, runs the umbrella verb: fetch the config repos then reconcile them into every installed agent. Secrets
|
|
50
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nGive a DotAgent repo name ALONE — "agents sync system" / "agents sync user" / "agents sync <alias>" — to git-sync that one repo: refuse if the tree is dirty, else git pull --rebase against origin. The user repo and extra aliases also push local commits up; the system repo is a pull-only mirror.\n\nWith NO agent, runs the umbrella verb: fetch the config repos then reconcile them into every installed agent. Secrets are opt-in — add --secrets to pull secret bundles. Session transcripts are queryable live via "agents sessions --host <machine>", or moved with "agents sessions export/import". Also: --cloud (fetch only), --local (reconcile only).')
|
|
51
51
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
52
52
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
|
|
53
53
|
.option('--repo <name>', 'Scope the sync to a single DotAgent repo: system / user / project / <alias> (also accepted as a positional)')
|
|
@@ -60,7 +60,6 @@ export function registerSyncCommand(program) {
|
|
|
60
60
|
// Umbrella verb (no agent given): make this machine current.
|
|
61
61
|
.option('--repos', 'Umbrella: git-pull ~/.agents + enabled ~/.agents-* extras', false)
|
|
62
62
|
.option('--secrets', 'Umbrella: pull encrypted secret bundles from the remote', false)
|
|
63
|
-
.option('--sessions', 'Umbrella: sync session transcripts across machines', false)
|
|
64
63
|
.option('--cloud', 'Umbrella: fetch all remote state but skip the local reconcile', false)
|
|
65
64
|
.option('--local', "Umbrella: reconcile resources into installed agents only (no fetch)", false)
|
|
66
65
|
.action(async (agentSpec, repo, opts) => {
|
|
@@ -206,7 +205,7 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
|
|
|
206
205
|
// Interactive bare `agents sync` (a TTY, no --yes, no scope flag) drops into
|
|
207
206
|
// the two-checklist picker: which repos to sync from, which agents to sync
|
|
208
207
|
// into. Any explicit flag or --yes keeps the non-interactive umbrella below.
|
|
209
|
-
const anyExplicitFlag = !!(opts.repos || opts.secrets || opts.
|
|
208
|
+
const anyExplicitFlag = !!(opts.repos || opts.secrets || opts.cloud || opts.local);
|
|
210
209
|
if (!quiet && !opts.yes && !anyExplicitFlag && isInteractiveTerminal()) {
|
|
211
210
|
await runInteractiveReconcile(opts, outLog, errLog);
|
|
212
211
|
return;
|
|
@@ -214,7 +213,6 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
|
|
|
214
213
|
const flags = {
|
|
215
214
|
repos: opts.repos,
|
|
216
215
|
secrets: opts.secrets,
|
|
217
|
-
sessions: opts.sessions,
|
|
218
216
|
cloud: opts.cloud,
|
|
219
217
|
local: opts.local,
|
|
220
218
|
};
|
|
@@ -238,9 +236,6 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
|
|
|
238
236
|
if (result.secrets) {
|
|
239
237
|
parts.push(result.secrets.skipped ? 'secrets skipped' : `secrets ${result.secrets.pulled} pulled`);
|
|
240
238
|
}
|
|
241
|
-
if (result.sessions) {
|
|
242
|
-
parts.push(result.sessions.ran ? `sessions ${result.sessions.merged} merged` : 'sessions off');
|
|
243
|
-
}
|
|
244
239
|
if (result.reconciled)
|
|
245
240
|
parts.push('reconciled');
|
|
246
241
|
outLog(chalk.green(`✓ sync: ${parts.join(' · ') || 'nothing to do'}`));
|
|
@@ -24,6 +24,9 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
24
24
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
25
25
|
import { getRuntimeStateDir } from '../lib/state.js';
|
|
26
26
|
import { setJobEnabled } from '../lib/routines.js';
|
|
27
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
28
|
+
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
29
|
+
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
27
30
|
import { ensureWatchdogRoutine, isWatchdogRoutineEnabled, watchdogRoutineExists, WATCHDOG_ROUTINE_NAME, WATCHDOG_ROUTINE_SCHEDULE, } from '../lib/watchdog/routine.js';
|
|
28
31
|
import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
29
32
|
/** Default state dir the runner and these subcommands share. */
|
|
@@ -58,6 +61,21 @@ function humanMs(ms) {
|
|
|
58
61
|
return `${Math.round(ms / 60_000)}m`;
|
|
59
62
|
return `${Math.round(ms / 1000)}s`;
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the mailbox liveness sweep against the same session set the tick just used.
|
|
66
|
+
* Idempotent and cheap: archiving a message twice is a no-op, and GC only scans
|
|
67
|
+
* directories. Failures are swallowed so a mailbox-root problem cannot break the
|
|
68
|
+
* watchdog tick.
|
|
69
|
+
*/
|
|
70
|
+
async function runMailboxGc(sessions) {
|
|
71
|
+
const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
|
|
72
|
+
try {
|
|
73
|
+
gcMailbox(activeBoxIds);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// GC is best-effort housekeeping; the next tick will retry.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
function colorForOutcome(o) {
|
|
62
80
|
if (o.injected)
|
|
63
81
|
return chalk.green;
|
|
@@ -118,7 +136,7 @@ export function registerWatchdogCommand(program) {
|
|
|
118
136
|
// path is the daemon routine, which runs `agents watchdog --nudge` — so the
|
|
119
137
|
// routine's enabled state IS the on/off switch, not a flag read here.
|
|
120
138
|
const computeWillInject = () => opts.nudge === true;
|
|
121
|
-
const tickOnce = async (willInject) => runWatchdogTick({
|
|
139
|
+
const tickOnce = async (willInject, sessions) => runWatchdogTick({
|
|
122
140
|
nudge: willInject,
|
|
123
141
|
nudgeText: opts.text,
|
|
124
142
|
smart: opts.smart === true,
|
|
@@ -126,10 +144,13 @@ export function registerWatchdogCommand(program) {
|
|
|
126
144
|
thresholds,
|
|
127
145
|
allowGhosttyFocus: opts.allowGhosttyFocus === true,
|
|
128
146
|
stateDir: stateDir(),
|
|
147
|
+
sessions,
|
|
129
148
|
});
|
|
130
149
|
if (!opts.watch) {
|
|
131
150
|
const willInject = computeWillInject();
|
|
132
|
-
const
|
|
151
|
+
const sessions = await getActiveSessions();
|
|
152
|
+
const result = await tickOnce(willInject, sessions);
|
|
153
|
+
await runMailboxGc(sessions);
|
|
133
154
|
if (opts.json)
|
|
134
155
|
console.log(JSON.stringify(result, null, 2));
|
|
135
156
|
else
|
|
@@ -146,7 +167,9 @@ export function registerWatchdogCommand(program) {
|
|
|
146
167
|
while (true) {
|
|
147
168
|
// Re-evaluated each tick: picks up enable/disable flips mid-run.
|
|
148
169
|
const willInject = computeWillInject();
|
|
149
|
-
const
|
|
170
|
+
const sessions = await getActiveSessions();
|
|
171
|
+
const result = await tickOnce(willInject, sessions);
|
|
172
|
+
await runMailboxGc(sessions);
|
|
150
173
|
if (opts.json)
|
|
151
174
|
console.log(JSON.stringify(result));
|
|
152
175
|
else
|