@phnx-labs/agents-cli 1.20.63 → 1.20.65

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.
Files changed (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. package/package.json +2 -1
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Monitor (event-triggered watcher) configuration, validation, and CRUD.
3
+ *
4
+ * A monitor is a routine whose trigger is a *watched source* instead of a
5
+ * *clock*. It watches a SOURCE, detects a CONDITION change, and fires an ACTION —
6
+ * reusing the routines daemon, dispatch engine, device model, and notify path.
7
+ *
8
+ * Monitors are YAML files in ~/.agents/monitors/. This module owns the on-disk
9
+ * shape (mirroring lib/routines.ts read/write helpers), hand-rolled validation
10
+ * (mirroring validateJob — no zod), and the device-owner eligibility gate.
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as yaml from 'yaml';
15
+ import { getMonitorsDir, ensureAgentsDir } from '../state.js';
16
+ import { safeJoin, isSafeSegmentName } from '../paths.js';
17
+ import { atomicWriteFileSync } from '../fs-atomic.js';
18
+ import { machineId, normalizeHost } from '../machine-id.js';
19
+ import { ALL_AGENT_IDS } from '../agents.js';
20
+ /** Default values applied to every monitor config when fields are omitted. */
21
+ const MONITOR_DEFAULTS = {
22
+ enabled: true,
23
+ };
24
+ const SOURCE_TYPES = [
25
+ 'command',
26
+ 'poll',
27
+ 'poll-http',
28
+ 'webhook',
29
+ 'ws',
30
+ 'file',
31
+ 'device',
32
+ ];
33
+ const CONDITION_MODES = ['on-change', 'match', 'every'];
34
+ const ACTION_TYPES = ['run', 'routine', 'notify', 'webhook-out'];
35
+ /**
36
+ * Parse a human interval string (e.g. `30s`, `15m`, `8h`, `1d`, `1h30m`) into
37
+ * milliseconds. Unlike routines' parseTimeout, seconds are supported (polls tick
38
+ * in seconds). Returns null on empty/unparseable/zero input.
39
+ */
40
+ export function parseInterval(interval) {
41
+ const match = interval.trim().match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
42
+ if (!match)
43
+ return null;
44
+ const weeks = parseInt(match[1] || '0', 10);
45
+ const days = parseInt(match[2] || '0', 10);
46
+ const hours = parseInt(match[3] || '0', 10);
47
+ const minutes = parseInt(match[4] || '0', 10);
48
+ const seconds = parseInt(match[5] || '0', 10);
49
+ const ms = ((((weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000;
50
+ return ms > 0 ? ms : null;
51
+ }
52
+ /**
53
+ * True when the monitor may evaluate + fire on this machine. Owner semantics:
54
+ * `device` (single owner, exactly-once) → only that machine; else `devices`
55
+ * (allowlist) → any listed machine; else unrestricted. Both sides normalize so
56
+ * `Yosemite-S0` and `yosemite-s0.tailnet.ts.net` agree with `yosemite-s0`.
57
+ */
58
+ export function monitorRunsOnThisDevice(config) {
59
+ const self = machineId();
60
+ if (config.device)
61
+ return normalizeHost(config.device) === self;
62
+ if (config.devices && config.devices.length > 0) {
63
+ return config.devices.some((d) => normalizeHost(d) === self);
64
+ }
65
+ return true;
66
+ }
67
+ /** Count the populated source-payload fields to detect "two sources". */
68
+ function populatedSourceFields(source) {
69
+ const fields = [
70
+ ['command', source.command],
71
+ ['url', source.url],
72
+ ['wsUrl', source.wsUrl],
73
+ ['path', source.path],
74
+ ['device', source.device],
75
+ ['webhook', source.webhook],
76
+ ];
77
+ return fields.filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k]) => k);
78
+ }
79
+ /** Which source-payload field each source type requires. */
80
+ const SOURCE_TYPE_FIELD = {
81
+ command: 'command',
82
+ poll: 'command',
83
+ 'poll-http': 'url',
84
+ webhook: 'webhook',
85
+ ws: 'wsUrl',
86
+ file: 'path',
87
+ device: 'device',
88
+ };
89
+ /** Count the populated action-payload fields to detect "two actions". */
90
+ function populatedActionFields(action) {
91
+ const fields = [
92
+ ['agent', action.agent],
93
+ ['routine', action.routine],
94
+ ['url', action.url],
95
+ ];
96
+ return fields.filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k]) => k);
97
+ }
98
+ /**
99
+ * Validate a partial monitor config, returning a list of human-readable errors.
100
+ * Hand-rolled like validateJob (lib/routines.ts) — no zod. Rejects: no source,
101
+ * two sources, no action, two actions, match-mode without `match`, plus the
102
+ * per-type field/shape checks.
103
+ */
104
+ export function validateMonitor(config) {
105
+ const errors = [];
106
+ if (!config.name || typeof config.name !== 'string') {
107
+ errors.push('name is required');
108
+ }
109
+ else if (!isSafeSegmentName(config.name)) {
110
+ errors.push(`invalid name ${JSON.stringify(config.name)}: must be a single path segment ` +
111
+ `(no '/', '\\\\', or null bytes, and not '.' or '..')`);
112
+ }
113
+ // ─── SOURCE ───────────────────────────────────────────────────────────────
114
+ const source = config.source;
115
+ if (!source || typeof source !== 'object') {
116
+ errors.push('a source is required (source: { type, ... })');
117
+ }
118
+ else {
119
+ if (!source.type || !SOURCE_TYPES.includes(source.type)) {
120
+ errors.push(`source.type must be one of: ${SOURCE_TYPES.join(', ')}`);
121
+ }
122
+ const populated = populatedSourceFields(source);
123
+ if (source.type && SOURCE_TYPES.includes(source.type)) {
124
+ const required = SOURCE_TYPE_FIELD[source.type];
125
+ // Two sources: any populated field that doesn't belong to this type.
126
+ const stray = populated.filter((f) => f !== required);
127
+ if (stray.length > 0) {
128
+ errors.push(`source has conflicting fields (${[required, ...stray].join(', ')}); specify exactly one source`);
129
+ }
130
+ if (!populated.includes(required)) {
131
+ errors.push(`source.type '${source.type}' requires source.${required}`);
132
+ }
133
+ }
134
+ else if (populated.length > 1) {
135
+ errors.push(`source has conflicting fields (${populated.join(', ')}); specify exactly one source`);
136
+ }
137
+ // Interval-bearing sources must carry a parseable interval.
138
+ if ((source.type === 'poll' || source.type === 'poll-http') && source.interval === undefined) {
139
+ errors.push(`source.type '${source.type}' requires source.interval (e.g. 30s, 15m, 8h)`);
140
+ }
141
+ if (source.interval !== undefined && parseInterval(source.interval) === null) {
142
+ errors.push(`source.interval must be like 30s, 15m, 8h, 1d (got ${JSON.stringify(source.interval)})`);
143
+ }
144
+ if (source.webhook !== undefined) {
145
+ const w = source.webhook;
146
+ if (!w || typeof w !== 'object') {
147
+ errors.push('source.webhook must be an object');
148
+ }
149
+ else if (w.source !== 'github' && w.source !== 'linear') {
150
+ errors.push("source.webhook.source must be 'github' or 'linear'");
151
+ }
152
+ else if (!w.event || typeof w.event !== 'string') {
153
+ errors.push('source.webhook.event is required');
154
+ }
155
+ }
156
+ }
157
+ // ─── CONDITION ──────────────────────────────────────────────────────────────
158
+ const condition = config.condition;
159
+ if (!condition || typeof condition !== 'object') {
160
+ errors.push('a condition is required (condition: { mode, ... })');
161
+ }
162
+ else {
163
+ if (!condition.mode || !CONDITION_MODES.includes(condition.mode)) {
164
+ errors.push(`condition.mode must be one of: ${CONDITION_MODES.join(', ')}`);
165
+ }
166
+ if (condition.mode === 'match') {
167
+ if (!condition.match || typeof condition.match !== 'string') {
168
+ errors.push("condition.mode 'match' requires condition.match (a regex)");
169
+ }
170
+ }
171
+ for (const key of ['match', 'dedupeKey']) {
172
+ const val = condition[key];
173
+ if (val !== undefined) {
174
+ if (typeof val !== 'string') {
175
+ errors.push(`condition.${key} must be a regex string`);
176
+ }
177
+ else {
178
+ try {
179
+ new RegExp(val);
180
+ }
181
+ catch {
182
+ errors.push(`condition.${key} is not a valid regular expression: ${JSON.stringify(val)}`);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ // ─── ACTION ─────────────────────────────────────────────────────────────────
189
+ const action = config.action;
190
+ if (!action || typeof action !== 'object') {
191
+ errors.push('an action is required (action: { type, ... })');
192
+ }
193
+ else {
194
+ if (!action.type || !ACTION_TYPES.includes(action.type)) {
195
+ errors.push(`action.type must be one of: ${ACTION_TYPES.join(', ')}`);
196
+ }
197
+ const populatedAct = populatedActionFields(action);
198
+ const requiredByType = {
199
+ run: 'agent',
200
+ routine: 'routine',
201
+ 'webhook-out': 'url',
202
+ };
203
+ if (action.type && ACTION_TYPES.includes(action.type)) {
204
+ const required = requiredByType[action.type];
205
+ // Two actions: a populated field that belongs to a different action type.
206
+ const stray = populatedAct.filter((f) => f !== required);
207
+ if (stray.length > 0) {
208
+ errors.push(`action has conflicting fields (${[...(required ? [required] : []), ...stray].join(', ')}); specify exactly one action`);
209
+ }
210
+ if (required && !populatedAct.includes(required)) {
211
+ errors.push(`action.type '${action.type}' requires action.${required}`);
212
+ }
213
+ }
214
+ else if (populatedAct.length > 1) {
215
+ errors.push(`action has conflicting fields (${populatedAct.join(', ')}); specify exactly one action`);
216
+ }
217
+ if (action.type === 'run') {
218
+ if (action.agent && !ALL_AGENT_IDS.includes(action.agent)) {
219
+ errors.push(`action.agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
220
+ }
221
+ if (!action.prompt || typeof action.prompt !== 'string') {
222
+ errors.push("action.type 'run' requires action.prompt");
223
+ }
224
+ if (action.mode && !['plan', 'edit', 'auto', 'skip', 'full'].includes(action.mode)) {
225
+ errors.push("action.mode must be plan, edit, auto, or skip ('full' accepted as alias for skip)");
226
+ }
227
+ if (action.effort && !['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(action.effort)) {
228
+ errors.push('action.effort must be low, medium, high, xhigh, max, or auto');
229
+ }
230
+ }
231
+ if (action.type === 'webhook-out' && action.url) {
232
+ try {
233
+ // eslint-disable-next-line no-new
234
+ new URL(action.url);
235
+ }
236
+ catch {
237
+ errors.push(`action.url must be an absolute URL (got ${JSON.stringify(action.url)})`);
238
+ }
239
+ }
240
+ }
241
+ // ─── PLACEMENT ───────────────────────────────────────────────────────────────
242
+ if (config.device !== undefined && config.devices !== undefined) {
243
+ errors.push("device (single owner) and devices (allowlist) are mutually exclusive — pick one");
244
+ }
245
+ if (config.device !== undefined && (typeof config.device !== 'string' || config.device.trim() === '')) {
246
+ errors.push('device must be a non-empty device name');
247
+ }
248
+ if (config.devices !== undefined) {
249
+ if (!Array.isArray(config.devices)) {
250
+ errors.push('devices must be an array of device names');
251
+ }
252
+ else {
253
+ for (const d of config.devices) {
254
+ if (typeof d !== 'string' || d.trim() === '') {
255
+ errors.push('each entry in devices must be a non-empty device name');
256
+ break;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ if (config.runOn !== undefined && (typeof config.runOn !== 'string' || config.runOn.trim() === '')) {
262
+ errors.push('runOn must be a non-empty machine name (a registered host, device, capability tag, or user@host)');
263
+ }
264
+ // ─── HYGIENE ─────────────────────────────────────────────────────────────────
265
+ if (config.rateLimit !== undefined) {
266
+ const rl = config.rateLimit;
267
+ if (!rl || typeof rl !== 'object' || typeof rl.max !== 'number' || rl.max <= 0) {
268
+ errors.push('rateLimit.max must be a positive number');
269
+ }
270
+ if (!rl || typeof rl.per !== 'string' || parseInterval(rl.per) === null) {
271
+ errors.push('rateLimit.per must be an interval like 1m, 1h, 1d');
272
+ }
273
+ }
274
+ return errors;
275
+ }
276
+ function readMonitorFile(filePath) {
277
+ try {
278
+ const content = fs.readFileSync(filePath, 'utf-8');
279
+ const parsed = yaml.parse(content);
280
+ if (!parsed || typeof parsed !== 'object')
281
+ return null;
282
+ return {
283
+ ...MONITOR_DEFAULTS,
284
+ ...parsed,
285
+ name: parsed.name || path.basename(filePath).replace(/\.ya?ml$/, ''),
286
+ };
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
292
+ /** List all monitor configs in ~/.agents/monitors/. */
293
+ export function listMonitors() {
294
+ ensureAgentsDir();
295
+ const dir = getMonitorsDir();
296
+ if (!fs.existsSync(dir))
297
+ return [];
298
+ const monitors = [];
299
+ const seen = new Set();
300
+ for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))) {
301
+ const monitor = readMonitorFile(path.join(dir, file));
302
+ if (!monitor || seen.has(monitor.name))
303
+ continue;
304
+ seen.add(monitor.name);
305
+ monitors.push(monitor);
306
+ }
307
+ return monitors;
308
+ }
309
+ /** Read a single monitor config by name. Returns null if not found or corrupt. */
310
+ export function readMonitor(name) {
311
+ ensureAgentsDir();
312
+ const dir = getMonitorsDir();
313
+ for (const ext of ['.yml', '.yaml']) {
314
+ const filePath = safeJoin(dir, name + ext);
315
+ if (fs.existsSync(filePath))
316
+ return readMonitorFile(filePath);
317
+ }
318
+ return null;
319
+ }
320
+ /** Get the filesystem path of a monitor's YAML config, or null if not found. */
321
+ export function getMonitorPath(name) {
322
+ const dir = getMonitorsDir();
323
+ for (const ext of ['.yml', '.yaml']) {
324
+ const filePath = safeJoin(dir, name + ext);
325
+ if (fs.existsSync(filePath))
326
+ return filePath;
327
+ }
328
+ return null;
329
+ }
330
+ /** Write a monitor config to disk atomically, omitting fields that match defaults. */
331
+ export function writeMonitor(config) {
332
+ ensureAgentsDir();
333
+ const dir = getMonitorsDir();
334
+ fs.mkdirSync(dir, { recursive: true });
335
+ const ymlPath = safeJoin(dir, config.name + '.yml');
336
+ const yamlPath = safeJoin(dir, config.name + '.yaml');
337
+ if (fs.existsSync(ymlPath) && fs.existsSync(yamlPath)) {
338
+ throw new Error(`Monitor '${config.name}' has both .yml and .yaml files; resolve the ambiguity before editing.`);
339
+ }
340
+ const filePath = fs.existsSync(yamlPath) ? yamlPath : ymlPath;
341
+ const output = { ...config };
342
+ if (output.enabled === true)
343
+ delete output.enabled;
344
+ const devArr = output.devices;
345
+ if (!devArr || devArr.length === 0)
346
+ delete output.devices;
347
+ atomicWriteFileSync(filePath, yaml.stringify(output));
348
+ }
349
+ /** Delete a monitor config file by name. Returns true if the file existed. */
350
+ export function deleteMonitor(name) {
351
+ const dir = getMonitorsDir();
352
+ for (const ext of ['.yml', '.yaml']) {
353
+ const filePath = safeJoin(dir, name + ext);
354
+ if (fs.existsSync(filePath)) {
355
+ fs.unlinkSync(filePath);
356
+ return true;
357
+ }
358
+ }
359
+ return false;
360
+ }
361
+ /** Enable or disable a monitor by name. */
362
+ export function setMonitorEnabled(name, enabled) {
363
+ const monitor = readMonitor(name);
364
+ if (!monitor)
365
+ throw new Error(`Monitor '${name}' not found`);
366
+ monitor.enabled = enabled;
367
+ writeMonitor(monitor);
368
+ }
369
+ /** Check whether a monitor with the given name exists on disk. */
370
+ export function monitorExists(name) {
371
+ return readMonitor(name) !== null;
372
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Monitor action dispatch.
3
+ *
4
+ * On a fire, the monitor feeds the event to an action. Every `run`/`routine`
5
+ * action goes through the *same* detached spawn cron and webhook fires use
6
+ * (executeJobDetached, lib/runner.ts) — a monitor never duplicates spawn logic,
7
+ * it synthesizes a JobConfig and hands it to the one dispatch seam. `notify`
8
+ * reuses the openclaw Telegram path (lib/notify.ts); `webhook-out` POSTs the event.
9
+ */
10
+ import type { ActionConfig, MonitorConfig, MonitorEvent } from './config.js';
11
+ /** Outcome of a dispatched action. */
12
+ export interface DispatchResult {
13
+ kind: ActionConfig['type'];
14
+ ok: boolean;
15
+ /** Run id for `run`/`routine` actions dispatched through executeJobDetached. */
16
+ runId?: string;
17
+ error?: string;
18
+ }
19
+ /** Replace `{event}` in a prompt with the fired event summary. */
20
+ export declare function injectEvent(prompt: string, event: MonitorEvent): string;
21
+ /**
22
+ * Dispatch a monitor's action for a fired event. `run` synthesizes a JobConfig
23
+ * (event injected into the prompt, action fields mapped onto the routines shape,
24
+ * runOn → host placement) and calls executeJobDetached — the exact path routines
25
+ * use. `routine` fires an existing routine with the event injected. `notify` and
26
+ * `webhook-out` are terminal side-effects.
27
+ */
28
+ export declare function dispatchAction(monitor: MonitorConfig, event: MonitorEvent): Promise<DispatchResult>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Monitor action dispatch.
3
+ *
4
+ * On a fire, the monitor feeds the event to an action. Every `run`/`routine`
5
+ * action goes through the *same* detached spawn cron and webhook fires use
6
+ * (executeJobDetached, lib/runner.ts) — a monitor never duplicates spawn logic,
7
+ * it synthesizes a JobConfig and hands it to the one dispatch seam. `notify`
8
+ * reuses the openclaw Telegram path (lib/notify.ts); `webhook-out` POSTs the event.
9
+ */
10
+ import { execFile } from 'child_process';
11
+ import { promisify } from 'util';
12
+ import { executeJobDetached } from '../runner.js';
13
+ import { readJob } from '../routines.js';
14
+ import { buildOpenClawNotifyArgs } from '../notify.js';
15
+ const execFileAsync = promisify(execFile);
16
+ /** Replace `{event}` in a prompt with the fired event summary. */
17
+ export function injectEvent(prompt, event) {
18
+ return prompt.replace(/\{event\}/g, event.summary);
19
+ }
20
+ /**
21
+ * Dispatch a monitor's action for a fired event. `run` synthesizes a JobConfig
22
+ * (event injected into the prompt, action fields mapped onto the routines shape,
23
+ * runOn → host placement) and calls executeJobDetached — the exact path routines
24
+ * use. `routine` fires an existing routine with the event injected. `notify` and
25
+ * `webhook-out` are terminal side-effects.
26
+ */
27
+ export async function dispatchAction(monitor, event) {
28
+ const action = monitor.action;
29
+ if (action.type === 'run') {
30
+ const job = {
31
+ name: monitor.name,
32
+ agent: action.agent,
33
+ mode: action.mode ?? 'auto',
34
+ effort: action.effort ?? 'auto',
35
+ timeout: action.timeout ?? '10m',
36
+ enabled: true,
37
+ prompt: injectEvent(action.prompt ?? '', event),
38
+ ...(monitor.variables ? { variables: monitor.variables } : {}),
39
+ ...(monitor.version ? { version: monitor.version } : {}),
40
+ ...(monitor.runOn ? { host: monitor.runOn } : {}),
41
+ };
42
+ try {
43
+ const meta = await executeJobDetached(job);
44
+ return { kind: 'run', ok: true, runId: meta.runId };
45
+ }
46
+ catch (err) {
47
+ return { kind: 'run', ok: false, error: err.message };
48
+ }
49
+ }
50
+ if (action.type === 'routine') {
51
+ const routine = action.routine ? readJob(action.routine) : null;
52
+ if (!routine) {
53
+ return { kind: 'routine', ok: false, error: `routine '${action.routine}' not found` };
54
+ }
55
+ // Inject the event into the routine's prompt so the fired routine sees it.
56
+ const fired = { ...routine, prompt: injectEvent(routine.prompt ?? '', event) };
57
+ try {
58
+ const meta = await executeJobDetached(fired);
59
+ return { kind: 'routine', ok: true, runId: meta.runId };
60
+ }
61
+ catch (err) {
62
+ return { kind: 'routine', ok: false, error: err.message };
63
+ }
64
+ }
65
+ if (action.type === 'notify') {
66
+ const args = buildOpenClawNotifyArgs(event.summary, {
67
+ channel: action.notifyChannel ?? 'telegram',
68
+ });
69
+ try {
70
+ await execFileAsync('openclaw', args);
71
+ return { kind: 'notify', ok: true };
72
+ }
73
+ catch (err) {
74
+ return { kind: 'notify', ok: false, error: err.message };
75
+ }
76
+ }
77
+ // webhook-out
78
+ if (!action.url)
79
+ return { kind: 'webhook-out', ok: false, error: 'action.url is required' };
80
+ try {
81
+ const res = await fetch(action.url, {
82
+ method: 'POST',
83
+ headers: { 'content-type': 'application/json' },
84
+ body: JSON.stringify(event),
85
+ });
86
+ return { kind: 'webhook-out', ok: res.ok, ...(res.ok ? {} : { error: `HTTP ${res.status}` }) };
87
+ }
88
+ catch (err) {
89
+ return { kind: 'webhook-out', ok: false, error: err.message };
90
+ }
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Monitor evaluate/fire loop.
3
+ *
4
+ * Modeled on the routines daemon: a single MonitorEngine lives inside runDaemon()
5
+ * beside the cron JobScheduler. On each tick it evaluates every enabled monitor
6
+ * that is DUE and owned by this device, applies the condition through the native
7
+ * state-diff store, and on a fire dispatches the action, writes a fire record, and
8
+ * updates state. A per-monitor rate limit auto-pauses a firehose.
9
+ *
10
+ * v1 covers the poll model (command, poll, poll-http, file, device). Push sources
11
+ * (ws, webhook) return null from `evaluate` — they deliver through `subscribe` /
12
+ * the webhook receiver, wired in a follow-up; the engine treats them as inert.
13
+ */
14
+ import { type MonitorConfig, type MonitorEvent } from './config.js';
15
+ import { type Observation } from './sources/index.js';
16
+ /** The fire/no-fire decision for one observation, plus what to persist. */
17
+ export interface FireDecision {
18
+ fire: boolean;
19
+ /** The value whose de-dupe signature is stored on persist. */
20
+ value: string;
21
+ /** The de-dupe key (regex) applied to the value, if any. */
22
+ dedupeKey?: string;
23
+ /** Persist `value` as the new baseline even when not firing (on-change baseline). */
24
+ persist: boolean;
25
+ /** The event to dispatch, present iff `fire`. */
26
+ event: MonitorEvent | null;
27
+ }
28
+ /**
29
+ * Apply a monitor's condition to an observation. Pure (reads state, never
30
+ * writes), so both the tick loop and the `test` dry-run share it.
31
+ */
32
+ export declare function decideFire(monitor: MonitorConfig, observation: Observation): FireDecision;
33
+ /** One evaluation of a monitor's source + condition, without side effects. Used by `test`. */
34
+ export declare function evaluateMonitorOnce(monitor: MonitorConfig): Promise<{
35
+ observation: Observation | null;
36
+ decision: FireDecision | null;
37
+ }>;
38
+ type LogFn = (level: string, message: string) => void;
39
+ /** The durable monitor engine. One instance per daemon. */
40
+ export declare class MonitorEngine {
41
+ private logFn;
42
+ private timer;
43
+ private monitors;
44
+ private lastEval;
45
+ private ticking;
46
+ constructor(logFn?: LogFn);
47
+ /** Load owned+enabled monitors and start the tick loop. */
48
+ start(): void;
49
+ /** Reload monitor configs (SIGHUP). */
50
+ reload(): void;
51
+ /** Stop the tick loop. */
52
+ stop(): void;
53
+ private loadAll;
54
+ private intervalMs;
55
+ private isDue;
56
+ /** Evaluate every due monitor once. Overlap-guarded so a slow cycle never stacks. */
57
+ tick(): Promise<void>;
58
+ private runMonitor;
59
+ private fire;
60
+ }
61
+ export {};