@phnx-labs/agents-cli 1.20.64 → 1.20.66
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 +51 -3
- package/README.md +156 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +159 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +66 -20
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +79 -4
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +205 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +45 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +36 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitors — durable event-triggered watchers.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `agents monitors` command tree: create, list, view, dry-run test,
|
|
5
|
+
* edit, logs, fire history, pause/resume, (re)pin the owner device, and remove.
|
|
6
|
+
* Mirrors `agents routines` (commands/routines.ts) and shares the same daemon +
|
|
7
|
+
* dispatch engine underneath — a monitor is a routine whose trigger is a watched
|
|
8
|
+
* source instead of a clock.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as yaml from 'yaml';
|
|
14
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, } from '../lib/daemon.js';
|
|
15
|
+
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, } from '../lib/monitors/config.js';
|
|
16
|
+
import { evaluateMonitorOnce } from '../lib/monitors/engine.js';
|
|
17
|
+
import { listFires, readState } from '../lib/monitors/state.js';
|
|
18
|
+
import { listRuns, getLatestRun, getRunDir } from '../lib/routines.js';
|
|
19
|
+
import { getMonitorsDir } from '../lib/state.js';
|
|
20
|
+
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
21
|
+
import { safeJoin } from '../lib/paths.js';
|
|
22
|
+
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
23
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
24
|
+
import { setHelpSections } from '../lib/help.js';
|
|
25
|
+
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
26
|
+
/** A one-line human label for what a monitor watches. */
|
|
27
|
+
function sourceLabel(source) {
|
|
28
|
+
switch (source.type) {
|
|
29
|
+
case 'command':
|
|
30
|
+
case 'poll':
|
|
31
|
+
return `${source.type}: ${source.command ?? ''}${source.interval ? ` @${source.interval}` : ''}`;
|
|
32
|
+
case 'poll-http':
|
|
33
|
+
return `poll-http: ${source.url ?? ''} @${source.interval ?? ''}`;
|
|
34
|
+
case 'ws':
|
|
35
|
+
return `ws: ${source.wsUrl ?? ''}`;
|
|
36
|
+
case 'file':
|
|
37
|
+
return `file: ${source.path ?? ''}`;
|
|
38
|
+
case 'device':
|
|
39
|
+
return `device: ${source.device ?? ''}`;
|
|
40
|
+
case 'webhook':
|
|
41
|
+
return `on ${source.webhook?.source}:${source.webhook?.event}`;
|
|
42
|
+
default:
|
|
43
|
+
return source.type;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** A one-line human label for a monitor's action. */
|
|
47
|
+
function actionLabel(action) {
|
|
48
|
+
switch (action.type) {
|
|
49
|
+
case 'run':
|
|
50
|
+
return `run ${action.agent ?? ''}`;
|
|
51
|
+
case 'routine':
|
|
52
|
+
return `routine ${action.routine ?? ''}`;
|
|
53
|
+
case 'notify':
|
|
54
|
+
return `notify ${action.notifyChannel ?? 'telegram'}`;
|
|
55
|
+
case 'webhook-out':
|
|
56
|
+
return `webhook-out ${action.url ?? ''}`;
|
|
57
|
+
default:
|
|
58
|
+
return action.type;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** A one-line human label for a monitor's owner/allowlist placement. */
|
|
62
|
+
function ownerLabel(monitor) {
|
|
63
|
+
if (monitor.device)
|
|
64
|
+
return monitor.device;
|
|
65
|
+
if (monitor.devices && monitor.devices.length > 0)
|
|
66
|
+
return monitor.devices.join(',');
|
|
67
|
+
return 'all';
|
|
68
|
+
}
|
|
69
|
+
/** Start or reload the background daemon so a newly-added monitor is watched. */
|
|
70
|
+
function ensureDaemonRunning() {
|
|
71
|
+
if (isDaemonRunning()) {
|
|
72
|
+
signalDaemonReload();
|
|
73
|
+
console.log(chalk.gray('Daemon reloaded'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const result = startDaemon();
|
|
77
|
+
if (result.pid) {
|
|
78
|
+
console.log(chalk.green(`Daemon started (PID: ${result.pid}). It will watch monitors in the background.`));
|
|
79
|
+
console.log(chalk.gray('Stop anytime with: agents routines stop'));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(chalk.yellow('Could not start the daemon. Start it manually with: agents routines start'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Validate a single device name against the registered fleet; exit on miss. */
|
|
86
|
+
async function validateDevice(name) {
|
|
87
|
+
const normalized = normalizeHost(name.trim());
|
|
88
|
+
if (!normalized) {
|
|
89
|
+
console.log(chalk.red('device name must be non-empty'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const registry = await loadDevices();
|
|
93
|
+
const registered = new Set(Object.keys(registry).map((k) => normalizeHost(k)));
|
|
94
|
+
if (!registered.has(normalized)) {
|
|
95
|
+
console.log(chalk.red(`Unknown device: ${normalized}`));
|
|
96
|
+
console.log(chalk.gray(`Registered: ${[...registered].sort().join(', ') || '(none)'}`));
|
|
97
|
+
console.log(chalk.gray('Enroll devices with: agents devices sync'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
/** Parse the source flags into a MonitorSource, exiting on missing/ambiguous input. */
|
|
103
|
+
function buildSource(options) {
|
|
104
|
+
const chosen = [];
|
|
105
|
+
if (options.watch)
|
|
106
|
+
chosen.push({ type: 'command', source: { type: 'command', command: options.watch } });
|
|
107
|
+
if (options.poll) {
|
|
108
|
+
chosen.push({ type: 'poll', source: { type: 'poll', command: options.poll[0], interval: options.poll[1] } });
|
|
109
|
+
}
|
|
110
|
+
if (options.pollHttp) {
|
|
111
|
+
chosen.push({
|
|
112
|
+
type: 'poll-http',
|
|
113
|
+
source: { type: 'poll-http', url: options.pollHttp[0], interval: options.pollHttp[1] },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (options.ws)
|
|
117
|
+
chosen.push({ type: 'ws', source: { type: 'ws', wsUrl: options.ws } });
|
|
118
|
+
if (options.watchFile)
|
|
119
|
+
chosen.push({ type: 'file', source: { type: 'file', path: options.watchFile } });
|
|
120
|
+
if (options.watchDevice) {
|
|
121
|
+
// Name is validated against the registry in the async add handler (fail fast,
|
|
122
|
+
// same gate as --device/--devices) — buildSource is sync so it can't await.
|
|
123
|
+
chosen.push({ type: 'device', source: { type: 'device', device: options.watchDevice } });
|
|
124
|
+
}
|
|
125
|
+
if (options.on) {
|
|
126
|
+
const raw = String(options.on);
|
|
127
|
+
const [src, event] = raw.includes(':') ? raw.split(':', 2) : ['github', raw];
|
|
128
|
+
if (src !== 'github' && src !== 'linear') {
|
|
129
|
+
console.log(chalk.red('--on source must be github or linear'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const webhook = { source: src, event };
|
|
133
|
+
if (options.repo)
|
|
134
|
+
webhook.repo = options.repo;
|
|
135
|
+
if (options.branch)
|
|
136
|
+
webhook.branch = options.branch;
|
|
137
|
+
if (options.action)
|
|
138
|
+
webhook.action = options.action;
|
|
139
|
+
if (options.teamKey)
|
|
140
|
+
webhook.teamKey = options.teamKey;
|
|
141
|
+
if (options.label)
|
|
142
|
+
webhook.label = options.label;
|
|
143
|
+
chosen.push({ type: 'webhook', source: { type: 'webhook', webhook } });
|
|
144
|
+
}
|
|
145
|
+
if (chosen.length === 0) {
|
|
146
|
+
console.log(chalk.red('A source is required: --watch, --poll, --poll-http, --ws, --watch-file, --watch-device, or --on'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (chosen.length > 1) {
|
|
150
|
+
console.log(chalk.red(`Exactly one source is allowed; got ${chosen.map((c) => c.type).join(', ')}`));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
return chosen[0].source;
|
|
154
|
+
}
|
|
155
|
+
/** Parse the condition flags into a MonitorCondition (default on-change). */
|
|
156
|
+
function buildCondition(options) {
|
|
157
|
+
const modes = [];
|
|
158
|
+
if (options.onChange)
|
|
159
|
+
modes.push('on-change');
|
|
160
|
+
if (options.match)
|
|
161
|
+
modes.push('match');
|
|
162
|
+
if (options.every)
|
|
163
|
+
modes.push('every');
|
|
164
|
+
if (modes.length > 1) {
|
|
165
|
+
console.log(chalk.red('--on-change, --match, and --every are mutually exclusive'));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const mode = modes[0] ?? (options.match ? 'match' : 'on-change');
|
|
169
|
+
const condition = { mode };
|
|
170
|
+
if (options.match)
|
|
171
|
+
condition.match = options.match;
|
|
172
|
+
if (options.dedupeKey)
|
|
173
|
+
condition.dedupeKey = options.dedupeKey;
|
|
174
|
+
return condition;
|
|
175
|
+
}
|
|
176
|
+
/** Parse the action flags into an ActionConfig, exiting on missing/ambiguous input. */
|
|
177
|
+
function buildAction(options) {
|
|
178
|
+
const chosen = [];
|
|
179
|
+
if (options.run) {
|
|
180
|
+
const action = { type: 'run', agent: options.run, prompt: options.prompt };
|
|
181
|
+
if (options.mode)
|
|
182
|
+
action.mode = options.mode;
|
|
183
|
+
if (options.effort)
|
|
184
|
+
action.effort = options.effort;
|
|
185
|
+
if (options.actionTimeout)
|
|
186
|
+
action.timeout = options.actionTimeout;
|
|
187
|
+
chosen.push(action);
|
|
188
|
+
}
|
|
189
|
+
if (options.routine)
|
|
190
|
+
chosen.push({ type: 'routine', routine: options.routine });
|
|
191
|
+
if (options.notify !== undefined) {
|
|
192
|
+
// --notify may be a bare flag (true) or carry a channel string.
|
|
193
|
+
const channel = typeof options.notify === 'string' ? options.notify : 'telegram';
|
|
194
|
+
chosen.push({ type: 'notify', notifyChannel: channel });
|
|
195
|
+
}
|
|
196
|
+
if (options.webhookOut)
|
|
197
|
+
chosen.push({ type: 'webhook-out', url: options.webhookOut });
|
|
198
|
+
if (chosen.length === 0) {
|
|
199
|
+
console.log(chalk.red('An action is required: --run <agent> --prompt, --routine, --notify, or --webhook-out'));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
if (chosen.length > 1) {
|
|
203
|
+
console.log(chalk.red(`Exactly one action is allowed; got ${chosen.map((c) => c.type).join(', ')}`));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
return chosen[0];
|
|
207
|
+
}
|
|
208
|
+
/** Interactive monitor picker. Returns the selected name or null on cancel/empty. */
|
|
209
|
+
async function pickMonitor(message, alternatives = []) {
|
|
210
|
+
const monitors = listMonitors();
|
|
211
|
+
if (monitors.length === 0) {
|
|
212
|
+
console.log(chalk.yellow('No monitors configured'));
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (!isInteractiveTerminal()) {
|
|
216
|
+
requireInteractiveSelection(message.replace(/:$/, ''), alternatives);
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const { select } = await import('@inquirer/prompts');
|
|
220
|
+
return await select({
|
|
221
|
+
message,
|
|
222
|
+
choices: monitors.map((m) => ({
|
|
223
|
+
value: m.name,
|
|
224
|
+
name: `${m.name} ${chalk.gray(`(${sourceLabel(m.source)} → ${actionLabel(m.action)})`)}`,
|
|
225
|
+
})),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (err instanceof Error && (err.name === 'ExitPromptError' || err.message.includes('User force closed'))) {
|
|
230
|
+
console.log(chalk.gray('Cancelled'));
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Register the `agents monitors` command tree. */
|
|
237
|
+
export function registerMonitorsCommands(program) {
|
|
238
|
+
const monitorsCmd = program
|
|
239
|
+
.command('monitors')
|
|
240
|
+
.description('Durable event-triggered watchers: watch a source, detect a change, fire an action. The daemon auto-starts on first add.');
|
|
241
|
+
setHelpSections(monitorsCmd, {
|
|
242
|
+
examples: `
|
|
243
|
+
# CI went red → triage it (poll a command, diff, fire an agent)
|
|
244
|
+
agents monitors add ci-red \\
|
|
245
|
+
--poll 'gh pr checks 1119 --json name,bucket' 30s --match 'fail' \\
|
|
246
|
+
--run claude --prompt 'CI failed on #1119: {event}. Diagnose and fix.' \\
|
|
247
|
+
--device yosemite-s0
|
|
248
|
+
|
|
249
|
+
# SSL cert issued → notify (poll an HTTPS endpoint every 8h)
|
|
250
|
+
agents monitors add cert-issued \\
|
|
251
|
+
--poll-http 'https://secure.ssl.com/team/.../co-ec1l5dgjofa' 8h \\
|
|
252
|
+
--match 'issued' --notify telegram --device zion
|
|
253
|
+
|
|
254
|
+
# Dry-run: evaluate the source once and show what it would emit (no action)
|
|
255
|
+
agents monitors test ci-red
|
|
256
|
+
|
|
257
|
+
# A fleet box going loaded → spin up an agent
|
|
258
|
+
agents monitors add box-loaded --watch-device yosemite-s0 --match loaded \\
|
|
259
|
+
--run claude --prompt 'yosemite-s0 is loaded: {event}. Investigate.'
|
|
260
|
+
`,
|
|
261
|
+
notes: `
|
|
262
|
+
A monitor is a routine whose trigger is a watched SOURCE instead of a clock.
|
|
263
|
+
It has three parts:
|
|
264
|
+
- SOURCE (--watch, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
265
|
+
- CONDITION (--on-change [default], --match <re>, --every; --dedupe-key)
|
|
266
|
+
- ACTION (--run <agent> --prompt, --routine, --notify, --webhook-out)
|
|
267
|
+
|
|
268
|
+
The fired event is injected into a run/routine prompt as {event}.
|
|
269
|
+
Pin the single OWNER device with --device (exactly-once). The daemon (shared
|
|
270
|
+
with routines) auto-starts on first add; manage it with 'agents routines start|stop'.
|
|
271
|
+
|
|
272
|
+
v1 evaluates poll sources (command, poll, poll-http, file, device). Push
|
|
273
|
+
sources (ws, webhook) are accepted but delivered through a receiver wired in
|
|
274
|
+
a follow-up.
|
|
275
|
+
`,
|
|
276
|
+
});
|
|
277
|
+
// ─── add ────────────────────────────────────────────────────────────────────
|
|
278
|
+
monitorsCmd
|
|
279
|
+
.command('add [nameOrPath]')
|
|
280
|
+
.description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon.')
|
|
281
|
+
// SOURCE
|
|
282
|
+
.option('--watch <cmd>', 'Run a shell command; its stdout is the observation')
|
|
283
|
+
.option('--poll <cmd...>', 'Re-run a command every interval: --poll "<cmd>" <interval> (e.g. 30s)')
|
|
284
|
+
.option('--poll-http <url...>', 'GET a URL every interval: --poll-http <url> <interval> (e.g. 15m)')
|
|
285
|
+
.option('--on <source:event>', 'Webhook trigger source: github:pull_request or linear:Issue')
|
|
286
|
+
.option('--ws <url>', 'WebSocket; each frame is an observation')
|
|
287
|
+
.option('--watch-file <path>', 'Watch a file or directory for changes')
|
|
288
|
+
.option('--watch-device <name>', 'A fleet device becomes the source (health/reachability)')
|
|
289
|
+
// webhook filters
|
|
290
|
+
.option('--repo <owner/name>', 'GitHub repo filter for --on github:<event>')
|
|
291
|
+
.option('--branch <name>', 'GitHub branch filter for --on github:<event>')
|
|
292
|
+
.option('--action <name>', 'Linear action filter for --on linear:<event>')
|
|
293
|
+
.option('--team-key <key>', 'Linear team key filter for --on linear:<event>')
|
|
294
|
+
.option('--label <name>', 'Linear issue label filter for --on linear:Issue')
|
|
295
|
+
// CONDITION
|
|
296
|
+
.option('--on-change', 'Fire when the observation differs from last-seen (the default)')
|
|
297
|
+
.option('--match <regex>', 'Fire when the observation matches this regex')
|
|
298
|
+
.option('--dedupe-key <expr>', 'Regex whose first match is the "same event" signature (default: full output)')
|
|
299
|
+
.option('--every', 'Fire on every observation (no dedupe) — rate-limit this')
|
|
300
|
+
// ACTION
|
|
301
|
+
.option('--run <agent>', 'Spawn an agent (claude, codex, ...) with the prompt on fire')
|
|
302
|
+
.option('--prompt <prompt>', 'Prompt for --run; {event} is replaced with the fired event')
|
|
303
|
+
.option('--mode <mode>', 'Execution mode for --run: plan, edit, auto, or skip')
|
|
304
|
+
.option('--effort <effort>', 'Reasoning effort for --run: low | medium | high | xhigh | max | auto')
|
|
305
|
+
.option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
|
|
306
|
+
.option('--routine <name>', 'Fire an existing routine on change')
|
|
307
|
+
.option('--notify [channel]', 'Send a notification (default channel: telegram)')
|
|
308
|
+
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
309
|
+
// PLACEMENT / hygiene
|
|
310
|
+
.option('--device <name>', 'OWNER device — the single machine that evaluates + fires (exactly-once)')
|
|
311
|
+
.option('--devices <list>', 'Allowlist (comma-separated): each device fires independently')
|
|
312
|
+
.option('--run-on <host>', 'Execute the ACTION on this machine over SSH (placement)')
|
|
313
|
+
.option('--rate-limit <spec>', 'Auto-pause if it fires more than N/<interval> (e.g. 5/1m)')
|
|
314
|
+
.option('--disabled', 'Create the monitor paused (enable later with resume)')
|
|
315
|
+
.action(async (nameOrPath, options) => {
|
|
316
|
+
// File mode: a single arg pointing at an existing .yml with no source flags.
|
|
317
|
+
const hasSourceFlag = Boolean(options.watch || options.poll || options.pollHttp || options.on || options.ws || options.watchFile || options.watchDevice);
|
|
318
|
+
if (!hasSourceFlag && nameOrPath && /\.ya?ml$/.test(nameOrPath) && fs.existsSync(path.resolve(nameOrPath))) {
|
|
319
|
+
const resolved = path.resolve(nameOrPath);
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = yaml.parse(fs.readFileSync(resolved, 'utf-8'));
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
console.log(chalk.red(`Invalid YAML: ${err.message}`));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
const name = parsed?.name || path.basename(resolved).replace(/\.ya?ml$/, '');
|
|
329
|
+
const config = { enabled: true, ...parsed, name };
|
|
330
|
+
const errors = validateMonitor(config);
|
|
331
|
+
if (errors.length > 0) {
|
|
332
|
+
console.log(chalk.red('Validation errors:'));
|
|
333
|
+
for (const err of errors)
|
|
334
|
+
console.log(chalk.red(` - ${err}`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
writeMonitor(config);
|
|
338
|
+
console.log(chalk.green(`Monitor '${name}' added`));
|
|
339
|
+
ensureDaemonRunning();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (!nameOrPath) {
|
|
343
|
+
console.log(chalk.red('Monitor name is required'));
|
|
344
|
+
console.log(chalk.gray('Usage: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const source = buildSource(options);
|
|
348
|
+
// A --watch-device source name must resolve to a registered fleet member —
|
|
349
|
+
// validate it (fail fast with the registered list) so a typo/removed device
|
|
350
|
+
// can't silently watch the local machine (same gate as --device/--devices).
|
|
351
|
+
if (source.type === 'device' && source.device) {
|
|
352
|
+
source.device = await validateDevice(source.device);
|
|
353
|
+
}
|
|
354
|
+
const condition = buildCondition(options);
|
|
355
|
+
const action = buildAction(options);
|
|
356
|
+
// Placement.
|
|
357
|
+
let device;
|
|
358
|
+
let devices;
|
|
359
|
+
if (options.device && options.devices) {
|
|
360
|
+
console.log(chalk.red('--device (single owner) and --devices (allowlist) are mutually exclusive'));
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
if (options.device)
|
|
364
|
+
device = await validateDevice(options.device);
|
|
365
|
+
if (options.devices) {
|
|
366
|
+
devices = [];
|
|
367
|
+
for (const d of String(options.devices).split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
368
|
+
devices.push(await validateDevice(d));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// --run-on with no owner pin would fire from every daemon → duplicate actions.
|
|
372
|
+
if (options.runOn && !device && !devices) {
|
|
373
|
+
device = machineId();
|
|
374
|
+
console.log(chalk.gray(`--run-on set with no --device/--devices: pinned owner to this machine (${device}).`));
|
|
375
|
+
}
|
|
376
|
+
let rateLimit;
|
|
377
|
+
if (options.rateLimit) {
|
|
378
|
+
const m = String(options.rateLimit).match(/^(\d+)\/(.+)$/);
|
|
379
|
+
if (!m) {
|
|
380
|
+
console.log(chalk.red('--rate-limit must be N/<interval>, e.g. 5/1m'));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
rateLimit = { max: parseInt(m[1], 10), per: m[2] };
|
|
384
|
+
}
|
|
385
|
+
const config = {
|
|
386
|
+
name: nameOrPath,
|
|
387
|
+
enabled: !options.disabled,
|
|
388
|
+
source,
|
|
389
|
+
condition,
|
|
390
|
+
action,
|
|
391
|
+
...(device ? { device } : {}),
|
|
392
|
+
...(devices ? { devices } : {}),
|
|
393
|
+
...(options.runOn ? { runOn: options.runOn } : {}),
|
|
394
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
395
|
+
};
|
|
396
|
+
const errors = validateMonitor(config);
|
|
397
|
+
if (errors.length > 0) {
|
|
398
|
+
console.log(chalk.red('Validation errors:'));
|
|
399
|
+
for (const err of errors)
|
|
400
|
+
console.log(chalk.red(` - ${err}`));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
// Coverage lint (Anthropic's "silence is not success"): warn when a --match
|
|
404
|
+
// names only a success-shaped token with no failure branch.
|
|
405
|
+
if (condition.mode === 'match' && condition.match && /^(issued|success|ok|pass(ed)?|done|ready)$/i.test(condition.match)) {
|
|
406
|
+
console.log(chalk.yellow(` Note: --match '${condition.match}' only fires on success — it stays silent if the source breaks or never matches.`));
|
|
407
|
+
}
|
|
408
|
+
writeMonitor(config);
|
|
409
|
+
console.log(chalk.green(`Monitor '${nameOrPath}' added`));
|
|
410
|
+
console.log(chalk.gray(` ${sourceLabel(source)} → [${condition.mode}] → ${actionLabel(action)} · owner: ${ownerLabel(config)}`));
|
|
411
|
+
ensureDaemonRunning();
|
|
412
|
+
});
|
|
413
|
+
// ─── list ────────────────────────────────────────────────────────────────────
|
|
414
|
+
monitorsCmd
|
|
415
|
+
.command('list')
|
|
416
|
+
.description('See all monitors: source, condition, action, owner, and last fire.')
|
|
417
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
418
|
+
.action((options) => {
|
|
419
|
+
const monitors = listMonitors();
|
|
420
|
+
if (options.json) {
|
|
421
|
+
const payload = monitors.map((m) => {
|
|
422
|
+
const state = readState(m.name);
|
|
423
|
+
return {
|
|
424
|
+
name: m.name,
|
|
425
|
+
enabled: m.enabled,
|
|
426
|
+
source: m.source,
|
|
427
|
+
condition: m.condition,
|
|
428
|
+
action: { type: m.action.type },
|
|
429
|
+
owner: ownerLabel(m),
|
|
430
|
+
runsHere: monitorRunsOnThisDevice(m),
|
|
431
|
+
lastSeenAt: state?.lastSeenAt ?? null,
|
|
432
|
+
lastFiredAt: state?.lastFiredAt ?? null,
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (monitors.length === 0) {
|
|
439
|
+
console.log(chalk.gray('No monitors configured'));
|
|
440
|
+
console.log(chalk.gray(' Add one: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log(chalk.bold('Monitors\n'));
|
|
444
|
+
for (const m of monitors) {
|
|
445
|
+
const state = readState(m.name);
|
|
446
|
+
const enabled = m.enabled ? chalk.green('on') : chalk.gray('off');
|
|
447
|
+
const here = monitorRunsOnThisDevice(m);
|
|
448
|
+
const owner = here ? ownerLabel(m) : chalk.gray(ownerLabel(m));
|
|
449
|
+
const lastFired = state?.lastFiredAt ? `fired ${state.lastFiredAt}` : chalk.gray('never fired');
|
|
450
|
+
console.log(` ${chalk.cyan(m.name.padEnd(22))} ${enabled.padEnd(3)} ${sourceLabel(m.source)}`);
|
|
451
|
+
console.log(` ${' '.repeat(22)} ${chalk.gray(`[${m.condition.mode}]`)} → ${actionLabel(m.action)} ${chalk.gray(`owner: ${owner}`)} ${chalk.gray(lastFired)}`);
|
|
452
|
+
}
|
|
453
|
+
console.log();
|
|
454
|
+
});
|
|
455
|
+
// ─── view ──────────────────────────────────────────────────────────────────
|
|
456
|
+
monitorsCmd
|
|
457
|
+
.command('view [name]')
|
|
458
|
+
.description('Show a monitor’s full YAML config plus its current watched-state and recent fires.')
|
|
459
|
+
.action(async (name) => {
|
|
460
|
+
if (!name) {
|
|
461
|
+
name = (await pickMonitor('Select monitor to view', ['agents monitors view <name>'])) ?? undefined;
|
|
462
|
+
if (!name)
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const monitor = readMonitor(name);
|
|
466
|
+
if (!monitor) {
|
|
467
|
+
console.log(chalk.red(`Monitor '${name}' not found`));
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
console.log(chalk.bold(`Monitor: ${name}\n`));
|
|
471
|
+
console.log(yaml.stringify(monitor));
|
|
472
|
+
const state = readState(name);
|
|
473
|
+
if (state) {
|
|
474
|
+
console.log(chalk.bold('Watched state'));
|
|
475
|
+
console.log(chalk.gray(` last seen: ${state.lastSeenAt}`));
|
|
476
|
+
if (state.lastFiredAt)
|
|
477
|
+
console.log(chalk.gray(` last fired: ${state.lastFiredAt}`));
|
|
478
|
+
console.log(chalk.gray(` last value: ${state.lastValue.replace(/\s+/g, ' ').slice(0, 120)}`));
|
|
479
|
+
}
|
|
480
|
+
const fires = listFires(name).slice(-5);
|
|
481
|
+
if (fires.length > 0) {
|
|
482
|
+
console.log(chalk.bold('\nRecent fires'));
|
|
483
|
+
for (const f of fires) {
|
|
484
|
+
console.log(` ${chalk.gray(f.firedAt)} ${f.action ?? '?'} ${f.ok === false ? chalk.red('failed') : chalk.green('ok')}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
// ─── test (DRY RUN) ───────────────────────────────────────────────────────────
|
|
489
|
+
monitorsCmd
|
|
490
|
+
.command('test [name]')
|
|
491
|
+
.description('DRY-RUN: evaluate the source once and print the emitted event + whether it would fire. No action is taken.')
|
|
492
|
+
.action(async (name) => {
|
|
493
|
+
if (!name) {
|
|
494
|
+
name = (await pickMonitor('Select monitor to test', ['agents monitors test <name>'])) ?? undefined;
|
|
495
|
+
if (!name)
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const monitor = readMonitor(name);
|
|
499
|
+
if (!monitor) {
|
|
500
|
+
console.log(chalk.red(`Monitor '${name}' not found`));
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
console.log(chalk.bold(`Dry-run: ${name}\n`));
|
|
504
|
+
console.log(chalk.gray(` ${sourceLabel(monitor.source)} · [${monitor.condition.mode}] · ${actionLabel(monitor.action)}\n`));
|
|
505
|
+
const { observation, decision } = await evaluateMonitorOnce(monitor);
|
|
506
|
+
if (!observation) {
|
|
507
|
+
console.log(chalk.yellow('No observation — this source is push-only (ws/webhook) or produced nothing this tick.'));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
console.log(chalk.bold('Observation'));
|
|
511
|
+
console.log(observation.raw.split('\n').slice(0, 20).map((l) => ` ${l}`).join('\n'));
|
|
512
|
+
if (observation.meta)
|
|
513
|
+
console.log(chalk.gray(` meta: ${JSON.stringify(observation.meta)}`));
|
|
514
|
+
const wouldFire = Boolean(decision?.fire);
|
|
515
|
+
console.log('');
|
|
516
|
+
console.log(`Would fire: ${wouldFire ? chalk.green('yes') : chalk.gray('no')}`);
|
|
517
|
+
if (decision?.event) {
|
|
518
|
+
console.log(chalk.bold('\nEmitted event'));
|
|
519
|
+
console.log(` summary: ${decision.event.summary}`);
|
|
520
|
+
console.log(chalk.gray(` → would ${actionLabel(monitor.action)}`));
|
|
521
|
+
}
|
|
522
|
+
else if (!wouldFire && monitor.condition.mode === 'on-change' && !readState(name)) {
|
|
523
|
+
console.log(chalk.gray(' (first observation establishes a baseline; a later change fires)'));
|
|
524
|
+
}
|
|
525
|
+
console.log(chalk.gray('\n(dry run — no action taken, no state written)'));
|
|
526
|
+
});
|
|
527
|
+
// ─── edit ─────────────────────────────────────────────────────────────────────
|
|
528
|
+
monitorsCmd
|
|
529
|
+
.command('edit [name]')
|
|
530
|
+
.description('Open a monitor’s YAML in $EDITOR.')
|
|
531
|
+
.action(async (name) => {
|
|
532
|
+
if (!name) {
|
|
533
|
+
name = (await pickMonitor('Select monitor to edit', ['agents monitors edit <name>'])) ?? undefined;
|
|
534
|
+
if (!name)
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
let monitorPath = getMonitorPath(name);
|
|
538
|
+
if (!monitorPath) {
|
|
539
|
+
const dir = getMonitorsDir();
|
|
540
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
541
|
+
monitorPath = safeJoin(dir, `${name}.yml`);
|
|
542
|
+
const template = yaml.stringify({
|
|
543
|
+
name,
|
|
544
|
+
source: { type: 'poll', command: 'echo hello', interval: '1m' },
|
|
545
|
+
condition: { mode: 'on-change' },
|
|
546
|
+
action: { type: 'notify', notifyChannel: 'telegram' },
|
|
547
|
+
});
|
|
548
|
+
fs.writeFileSync(monitorPath, template, 'utf-8');
|
|
549
|
+
console.log(chalk.gray(`Created new monitor file: ${monitorPath}`));
|
|
550
|
+
}
|
|
551
|
+
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WINDOWS ? 'notepad' : 'vi');
|
|
552
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
553
|
+
const { spawn } = await import('child_process');
|
|
554
|
+
const child = spawn(parts[0], [...parts.slice(1), monitorPath], { stdio: 'inherit' });
|
|
555
|
+
child.on('close', (code) => {
|
|
556
|
+
if (code !== 0)
|
|
557
|
+
return;
|
|
558
|
+
const monitor = readMonitor(name);
|
|
559
|
+
if (!monitor)
|
|
560
|
+
return;
|
|
561
|
+
const errors = validateMonitor(monitor);
|
|
562
|
+
if (errors.length > 0) {
|
|
563
|
+
console.log(chalk.yellow('\nWarning: monitor has validation errors:'));
|
|
564
|
+
for (const err of errors)
|
|
565
|
+
console.log(chalk.yellow(` - ${err}`));
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.log(chalk.green(`\nMonitor '${name}' saved`));
|
|
569
|
+
if (isDaemonRunning()) {
|
|
570
|
+
signalDaemonReload();
|
|
571
|
+
console.log(chalk.gray('Daemon reloaded'));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
// ─── logs (action run history) ─────────────────────────────────────────────────
|
|
577
|
+
monitorsCmd
|
|
578
|
+
.command('logs [name]')
|
|
579
|
+
.description('Show the latest action run’s status + report. --run for a specific run, --full for raw stdout.')
|
|
580
|
+
.option('-r, --run <runId>', 'Show a specific action run instead of the latest')
|
|
581
|
+
.option('-m, --full', 'Show the full raw stdout stream')
|
|
582
|
+
.action(async (name, options) => {
|
|
583
|
+
if (!name) {
|
|
584
|
+
name = (await pickMonitor('Select monitor to view logs', ['agents monitors logs <name>'])) ?? undefined;
|
|
585
|
+
if (!name)
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const run = options.run ? listRuns(name).find((r) => r.runId === options.run) : getLatestRun(name);
|
|
589
|
+
if (!run) {
|
|
590
|
+
console.log(chalk.yellow(`No action runs found for monitor '${name}'`));
|
|
591
|
+
console.log(chalk.gray(' (notify / webhook-out actions have no run log — see: agents monitors runs)'));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const logPath = path.join(getRunDir(name, run.runId), 'stdout.log');
|
|
595
|
+
if (options.full) {
|
|
596
|
+
if (!fs.existsSync(logPath)) {
|
|
597
|
+
console.log(chalk.yellow(`Log not found: ${logPath}`));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
console.log(chalk.gray(`Run: ${run.runId}\n`));
|
|
601
|
+
console.log(fs.readFileSync(logPath, 'utf-8'));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const statusColor = run.status === 'completed' ? chalk.green : run.status === 'running' ? chalk.yellow : chalk.red;
|
|
605
|
+
console.log(chalk.bold(name) + chalk.gray(` run ${run.runId}`));
|
|
606
|
+
console.log(statusColor(run.status) + chalk.gray(` ${run.startedAt}`));
|
|
607
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
608
|
+
const reportPath = path.join(getRunDir(name, run.runId), 'report.md');
|
|
609
|
+
if (fs.existsSync(reportPath)) {
|
|
610
|
+
console.log(fs.readFileSync(reportPath, 'utf-8').trimEnd());
|
|
611
|
+
}
|
|
612
|
+
else if (fs.existsSync(logPath)) {
|
|
613
|
+
console.log(fs.readFileSync(logPath, 'utf-8').split('\n').slice(-40).join('\n').trimEnd());
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
console.log(chalk.gray('(no output captured)'));
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
// ─── runs (fire history) ────────────────────────────────────────────────────────
|
|
620
|
+
monitorsCmd
|
|
621
|
+
.command('runs [name]')
|
|
622
|
+
.description('See a monitor’s fire history: when it fired, the action, and the outcome.')
|
|
623
|
+
.action(async (name) => {
|
|
624
|
+
if (!name) {
|
|
625
|
+
name = (await pickMonitor('Select monitor to view fires', ['agents monitors runs <name>'])) ?? undefined;
|
|
626
|
+
if (!name)
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const fires = listFires(name);
|
|
630
|
+
if (fires.length === 0) {
|
|
631
|
+
console.log(chalk.yellow(`No fires recorded for monitor '${name}'`));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
console.log(chalk.bold(`Fire history: ${name}\n`));
|
|
635
|
+
for (const f of fires.slice(-20)) {
|
|
636
|
+
const outcome = f.ok === false ? chalk.red('failed') : chalk.green('ok');
|
|
637
|
+
const runRef = f.runId ? chalk.gray(` run ${f.runId}`) : '';
|
|
638
|
+
console.log(` ${f.firedAt} ${(f.action ?? '?').padEnd(12)} ${outcome}${runRef}`);
|
|
639
|
+
console.log(chalk.gray(` ${f.summary.slice(0, 100)}`));
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
// ─── pause / resume ───────────────────────────────────────────────────────────
|
|
643
|
+
monitorsCmd
|
|
644
|
+
.command('pause [name]')
|
|
645
|
+
.description('Temporarily disable a monitor. Stops watching until resumed.')
|
|
646
|
+
.action(async (name) => {
|
|
647
|
+
if (!name) {
|
|
648
|
+
name = (await pickMonitor('Select monitor to pause', ['agents monitors pause <name>'])) ?? undefined;
|
|
649
|
+
if (!name)
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
setMonitorEnabled(name, false);
|
|
654
|
+
console.log(chalk.green(`Monitor '${name}' paused`));
|
|
655
|
+
if (isDaemonRunning())
|
|
656
|
+
signalDaemonReload();
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
console.log(chalk.red(err.message));
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
monitorsCmd
|
|
664
|
+
.command('resume [name]')
|
|
665
|
+
.description('Re-enable a paused monitor so the daemon watches it again.')
|
|
666
|
+
.action(async (name) => {
|
|
667
|
+
if (!name) {
|
|
668
|
+
name = (await pickMonitor('Select monitor to resume', ['agents monitors resume <name>'])) ?? undefined;
|
|
669
|
+
if (!name)
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
setMonitorEnabled(name, true);
|
|
674
|
+
console.log(chalk.green(`Monitor '${name}' resumed`));
|
|
675
|
+
if (isDaemonRunning())
|
|
676
|
+
signalDaemonReload();
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
console.log(chalk.red(err.message));
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
// ─── device (re-pin the owner) ──────────────────────────────────────────────────
|
|
684
|
+
monitorsCmd
|
|
685
|
+
.command('device [name]')
|
|
686
|
+
.description('View or (re)pin the OWNER device — the single machine that evaluates + fires (exactly-once).')
|
|
687
|
+
.option('--set <name>', 'Pin the owner to this device (strict fleet validation)')
|
|
688
|
+
.option('--clear', 'Remove the owner pin so the monitor runs on every device')
|
|
689
|
+
.action(async (name, options) => {
|
|
690
|
+
if (options.set !== undefined && options.clear) {
|
|
691
|
+
console.log(chalk.red('--set and --clear are mutually exclusive'));
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
if (!name) {
|
|
695
|
+
name = (await pickMonitor('Select monitor', ['agents monitors device <name> --set X'])) ?? undefined;
|
|
696
|
+
if (!name)
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const monitor = readMonitor(name);
|
|
700
|
+
if (!monitor) {
|
|
701
|
+
console.log(chalk.red(`Monitor '${name}' not found`));
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
if (options.clear) {
|
|
705
|
+
monitor.device = undefined;
|
|
706
|
+
monitor.devices = undefined;
|
|
707
|
+
writeMonitor(monitor);
|
|
708
|
+
console.log(chalk.green(`Owner cleared for '${name}' — evaluates on every device`));
|
|
709
|
+
if (isDaemonRunning())
|
|
710
|
+
signalDaemonReload();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (options.set !== undefined) {
|
|
714
|
+
const device = await validateDevice(options.set);
|
|
715
|
+
monitor.device = device;
|
|
716
|
+
monitor.devices = undefined;
|
|
717
|
+
writeMonitor(monitor);
|
|
718
|
+
console.log(chalk.green(`Owner for '${name}' set to: ${device}`));
|
|
719
|
+
if (isDaemonRunning())
|
|
720
|
+
signalDaemonReload();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
console.log(`Owner for '${name}': ${chalk.cyan(ownerLabel(monitor))}`);
|
|
724
|
+
console.log(chalk.gray(' Re-pin with: agents monitors device ' + name + ' --set <device>'));
|
|
725
|
+
});
|
|
726
|
+
// ─── remove ────────────────────────────────────────────────────────────────────
|
|
727
|
+
monitorsCmd
|
|
728
|
+
.command('remove [name]')
|
|
729
|
+
.description('Delete a monitor. Stops watching; past fire history remains on disk.')
|
|
730
|
+
.action(async (name) => {
|
|
731
|
+
if (!name) {
|
|
732
|
+
name = (await pickMonitor('Select monitor to remove', ['agents monitors remove <name>'])) ?? undefined;
|
|
733
|
+
if (!name)
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (deleteMonitor(name)) {
|
|
737
|
+
console.log(chalk.green(`Monitor '${name}' removed`));
|
|
738
|
+
if (isDaemonRunning()) {
|
|
739
|
+
signalDaemonReload();
|
|
740
|
+
console.log(chalk.gray('Daemon reloaded'));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
console.log(chalk.red(`Monitor '${name}' not found`));
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|