neuron-inspector 0.8.1 → 1.1.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.
Files changed (69) hide show
  1. package/README.md +345 -42
  2. package/dist/automations/executor.d.ts +21 -0
  3. package/dist/automations/executor.js +835 -0
  4. package/dist/automations/executor.js.map +1 -0
  5. package/dist/automations/index.d.ts +7 -0
  6. package/dist/automations/index.js +21 -0
  7. package/dist/automations/index.js.map +1 -0
  8. package/dist/automations/store.d.ts +15 -0
  9. package/dist/automations/store.js +193 -0
  10. package/dist/automations/store.js.map +1 -0
  11. package/dist/automations/tools.d.ts +14 -0
  12. package/dist/automations/tools.js +735 -0
  13. package/dist/automations/tools.js.map +1 -0
  14. package/dist/automations/types.d.ts +211 -0
  15. package/dist/automations/types.js +4 -0
  16. package/dist/automations/types.js.map +1 -0
  17. package/dist/business-tools.d.ts +17 -0
  18. package/dist/business-tools.js +3065 -0
  19. package/dist/business-tools.js.map +1 -0
  20. package/dist/collections/resolver.js +2 -2
  21. package/dist/collections/resolver.js.map +1 -1
  22. package/dist/collections/store.js +2 -2
  23. package/dist/collections/store.js.map +1 -1
  24. package/dist/killswitch.js +2 -2
  25. package/dist/killswitch.js.map +1 -1
  26. package/dist/monitor.d.ts +77 -2
  27. package/dist/monitor.js +331 -53
  28. package/dist/monitor.js.map +1 -1
  29. package/dist/paths.d.ts +16 -0
  30. package/dist/paths.js +39 -0
  31. package/dist/paths.js.map +1 -0
  32. package/dist/recipe-tools.js +28 -3
  33. package/dist/recipe-tools.js.map +1 -1
  34. package/dist/recipes.d.ts +2 -0
  35. package/dist/recipes.js +34 -5
  36. package/dist/recipes.js.map +1 -1
  37. package/dist/scheduler.js +4 -8
  38. package/dist/scheduler.js.map +1 -1
  39. package/dist/server.js +191 -19
  40. package/dist/server.js.map +1 -1
  41. package/dist/session-state.js +2 -2
  42. package/dist/session-state.js.map +1 -1
  43. package/dist/tools.js +28 -0
  44. package/dist/tools.js.map +1 -1
  45. package/package.json +2 -2
  46. package/recipes/linkedin-job-scraper/agent.md +144 -0
  47. package/recipes/linkedin-job-scraper/learnings.md +3 -0
  48. package/recipes/linkedin-job-scraper/recipe.yaml +71 -0
  49. package/recipes/sheets-data-entry/agent.md +166 -0
  50. package/recipes/sheets-data-entry/learnings.md +3 -0
  51. package/recipes/sheets-data-entry/recipe.yaml +78 -0
  52. package/recipes/x-job-scraper/agent.md +156 -0
  53. package/recipes/x-job-scraper/learnings.md +3 -0
  54. package/recipes/x-job-scraper/recipe.yaml +72 -0
  55. package/dist/__tests__/correlation.test.d.ts +0 -1
  56. package/dist/__tests__/correlation.test.js +0 -53
  57. package/dist/__tests__/correlation.test.js.map +0 -1
  58. package/dist/__tests__/monitor.test.d.ts +0 -1
  59. package/dist/__tests__/monitor.test.js +0 -286
  60. package/dist/__tests__/monitor.test.js.map +0 -1
  61. package/dist/__tests__/recipes.test.d.ts +0 -1
  62. package/dist/__tests__/recipes.test.js +0 -415
  63. package/dist/__tests__/recipes.test.js.map +0 -1
  64. package/dist/__tests__/scheduler.test.d.ts +0 -1
  65. package/dist/__tests__/scheduler.test.js +0 -316
  66. package/dist/__tests__/scheduler.test.js.map +0 -1
  67. package/dist/__tests__/session-state.test.d.ts +0 -1
  68. package/dist/__tests__/session-state.test.js +0 -335
  69. package/dist/__tests__/session-state.test.js.map +0 -1
package/dist/monitor.js CHANGED
@@ -1,53 +1,46 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import * as os from "node:os";
4
3
  import YAML from "yaml";
5
- const NEURON_DIR = path.join(os.homedir(), ".neuron");
6
- const MONITORS_FILE = path.join(NEURON_DIR, "monitors.yaml");
7
- const ALERTS_FILE = path.join(NEURON_DIR, "monitor-alerts.yaml");
4
+ import { NEURON_DIR, ensureNeuronDir } from "./paths.js";
8
5
  const MAX_ALERTS = 100;
9
6
  const activeIntervals = new Map();
10
- function ensureNeuronDir() {
11
- if (!fs.existsSync(NEURON_DIR)) {
12
- fs.mkdirSync(NEURON_DIR, { recursive: true });
13
- }
14
- }
15
7
  function monitorsPath() {
16
- return MONITORS_FILE;
8
+ return path.join(NEURON_DIR, "monitors.yaml");
17
9
  }
18
10
  function alertsPath() {
19
- return ALERTS_FILE;
11
+ return path.join(NEURON_DIR, "monitor-alerts.yaml");
20
12
  }
21
- function loadMonitors() {
13
+ export function loadMonitors() {
22
14
  ensureNeuronDir();
23
- if (!fs.existsSync(MONITORS_FILE)) {
15
+ if (!fs.existsSync(monitorsPath())) {
24
16
  return [];
25
17
  }
26
- const content = fs.readFileSync(MONITORS_FILE, "utf-8");
18
+ const content = fs.readFileSync(monitorsPath(), "utf-8");
27
19
  const store = YAML.parse(content);
28
20
  return store?.monitors || [];
29
21
  }
30
- function saveMonitors(monitors) {
22
+ export function saveMonitors(monitors) {
31
23
  ensureNeuronDir();
32
24
  const store = { monitors };
33
- fs.writeFileSync(MONITORS_FILE, YAML.stringify(store), "utf-8");
25
+ fs.writeFileSync(monitorsPath(), YAML.stringify(store), "utf-8");
34
26
  }
35
27
  function loadAlerts() {
36
28
  ensureNeuronDir();
37
- if (!fs.existsSync(ALERTS_FILE)) {
29
+ if (!fs.existsSync(alertsPath())) {
38
30
  return [];
39
31
  }
40
- const content = fs.readFileSync(ALERTS_FILE, "utf-8");
32
+ const content = fs.readFileSync(alertsPath(), "utf-8");
41
33
  const store = YAML.parse(content);
42
- return store?.alerts || [];
34
+ const alerts = store?.alerts || [];
35
+ return alerts.length > MAX_ALERTS ? alerts.slice(-MAX_ALERTS) : alerts;
43
36
  }
44
37
  function saveAlerts(alerts) {
45
38
  ensureNeuronDir();
46
39
  const trimmed = alerts.slice(-MAX_ALERTS);
47
40
  const store = { alerts: trimmed };
48
- fs.writeFileSync(ALERTS_FILE, YAML.stringify(store), "utf-8");
41
+ fs.writeFileSync(alertsPath(), YAML.stringify(store), "utf-8");
49
42
  }
50
- function addAlert(alert) {
43
+ export function addAlert(alert) {
51
44
  const alerts = loadAlerts();
52
45
  alerts.push(alert);
53
46
  saveAlerts(alerts);
@@ -55,23 +48,187 @@ function addAlert(alert) {
55
48
  function generateId() {
56
49
  return `mon_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
57
50
  }
58
- async function checkMonitor(monitor, checkFn) {
51
+ export async function executeMonitorActions(actions, ctx, tabId, triggerData, monitorUrl) {
52
+ const results = [];
53
+ // Browser actions need a tab — open one if not provided
54
+ const browserActions = new Set(["click", "type", "navigate", "scroll", "screenshot", "extract", "evaluate"]);
55
+ let resolvedTabId = tabId;
56
+ for (const action of actions) {
57
+ const start = Date.now();
58
+ try {
59
+ // Lazy-open a tab for the first browser action that needs one
60
+ if (browserActions.has(action.type) && resolvedTabId === undefined) {
61
+ const url = monitorUrl || triggerData.url;
62
+ if (!url)
63
+ throw new Error("No URL available to open a tab for browser action");
64
+ const tab = (await ctx.pending.call(ctx.ws, "openTab", { url }, 30_000));
65
+ resolvedTabId = tab?.tabId;
66
+ if (!resolvedTabId)
67
+ throw new Error("Failed to open tab for monitor action");
68
+ await new Promise((r) => setTimeout(r, 3000));
69
+ }
70
+ let result;
71
+ switch (action.type) {
72
+ case "click": {
73
+ const args = { tabId: resolvedTabId };
74
+ if (action.config.selector)
75
+ args.selectors = [action.config.selector];
76
+ if (action.config.text)
77
+ args.texts = [action.config.text];
78
+ result = await ctx.pending.call(ctx.ws, "clickElement", args, 15_000);
79
+ break;
80
+ }
81
+ case "type": {
82
+ const args = {
83
+ tabId: resolvedTabId,
84
+ selectors: [action.config.selector],
85
+ value: action.config.value,
86
+ };
87
+ result = await ctx.pending.call(ctx.ws, "typeText", args, 15_000);
88
+ break;
89
+ }
90
+ case "navigate": {
91
+ result = await ctx.pending.call(ctx.ws, "navigateTo", { tabId: resolvedTabId, url: action.config.url }, 30_000);
92
+ // Wait for load
93
+ await new Promise((r) => setTimeout(r, 2000));
94
+ break;
95
+ }
96
+ case "scroll": {
97
+ const args = {
98
+ tabId: resolvedTabId,
99
+ deltaY: action.config.pixels || 600,
100
+ smooth: true,
101
+ };
102
+ result = await ctx.pending.call(ctx.ws, "scrollPage", args, 10_000);
103
+ break;
104
+ }
105
+ case "evaluate": {
106
+ result = await ctx.pending.call(ctx.ws, "evaluateJS", {
107
+ tabId: resolvedTabId,
108
+ expression: action.config.expression,
109
+ }, 15_000);
110
+ break;
111
+ }
112
+ case "screenshot": {
113
+ result = await ctx.pending.call(ctx.ws, "takeScreenshot", { tabId: resolvedTabId }, 15_000);
114
+ break;
115
+ }
116
+ case "extract": {
117
+ const args = { tabId: resolvedTabId };
118
+ if (action.config.selector)
119
+ args.selector = action.config.selector;
120
+ result = await ctx.pending.call(ctx.ws, "extractData", args, 15_000);
121
+ break;
122
+ }
123
+ case "webhook": {
124
+ const url = action.config.url;
125
+ const method = action.config.method || "POST";
126
+ const customBody = action.config.body;
127
+ const customHeaders = action.config.headers;
128
+ const headers = {
129
+ "Content-Type": "application/json",
130
+ ...customHeaders,
131
+ };
132
+ const body = customBody || JSON.stringify(triggerData);
133
+ const res = await fetch(url, { method, headers, body });
134
+ result = { status: res.status, statusText: res.statusText };
135
+ break;
136
+ }
137
+ default:
138
+ throw new Error(`Unknown action type: ${action.type}`);
139
+ }
140
+ results.push({
141
+ action_id: action.id,
142
+ type: action.type,
143
+ status: "success",
144
+ duration_ms: Date.now() - start,
145
+ result,
146
+ });
147
+ }
148
+ catch (err) {
149
+ results.push({
150
+ action_id: action.id,
151
+ type: action.type,
152
+ status: "failed",
153
+ duration_ms: Date.now() - start,
154
+ error: err.message,
155
+ });
156
+ if (action.on_error !== "continue") {
157
+ break; // stop chain
158
+ }
159
+ }
160
+ }
161
+ return results;
162
+ }
163
+ async function checkMonitor(monitor, checkFn, ctx) {
59
164
  try {
60
- const result = await checkFn(monitor.url, monitor.condition.selector);
61
165
  const { condition } = monitor;
166
+ // ── Buffer-based conditions (no tab needed) ──────────────
167
+ if (condition.type === "request_match") {
168
+ if (!ctx)
169
+ throw new Error("request_match requires bridge context");
170
+ const filters = {
171
+ sinceMs: monitor.last_check_at ? new Date(monitor.last_check_at).getTime() : Date.now() - monitor.check_interval_minutes * 60_000,
172
+ limit: 200,
173
+ };
174
+ if (condition.urlPattern)
175
+ filters.urlPattern = condition.urlPattern;
176
+ if (condition.method)
177
+ filters.method = condition.method;
178
+ if (condition.statusMin != null)
179
+ filters.statusMin = condition.statusMin;
180
+ if (condition.statusMax != null)
181
+ filters.statusMax = condition.statusMax;
182
+ const result = await ctx.pending.call(ctx.ws, "getRecentRequests", filters, 15_000);
183
+ const requests = Array.isArray(result) ? result : result.requests || [];
184
+ if (requests.length > 0) {
185
+ return {
186
+ triggered: true,
187
+ details: `${requests.length} request(s) matched (${condition.urlPattern || "all"}${condition.statusMin ? ` status>=${condition.statusMin}` : ""}${condition.statusMax ? ` status<=${condition.statusMax}` : ""})`,
188
+ currentValue: JSON.stringify(requests.slice(0, 5)),
189
+ };
190
+ }
191
+ return { triggered: false };
192
+ }
193
+ if (condition.type === "console_match") {
194
+ if (!ctx)
195
+ throw new Error("console_match requires bridge context");
196
+ const filters = {
197
+ sinceMs: monitor.last_check_at ? new Date(monitor.last_check_at).getTime() : Date.now() - monitor.check_interval_minutes * 60_000,
198
+ limit: 200,
199
+ };
200
+ if (condition.search)
201
+ filters.search = condition.search;
202
+ if (condition.levels?.length)
203
+ filters.levels = condition.levels;
204
+ const result = await ctx.pending.call(ctx.ws, "getRecentLogs", filters, 15_000);
205
+ const logs = Array.isArray(result) ? result : result.logs || [];
206
+ if (logs.length > 0) {
207
+ return {
208
+ triggered: true,
209
+ details: `${logs.length} console message(s) matched (${condition.search || "all"}${condition.levels?.length ? ` levels=${condition.levels.join(",")}` : ""})`,
210
+ currentValue: JSON.stringify(logs.slice(0, 5)),
211
+ };
212
+ }
213
+ return { triggered: false };
214
+ }
215
+ // ── Page-based conditions (open tab, extract) ────────────
216
+ const result = await checkFn(monitor.url, monitor.condition.selector);
217
+ const tabId = result.tabId;
62
218
  switch (condition.type) {
63
219
  case "content_change": {
64
220
  if (!condition.baseline) {
65
- return { triggered: false, currentValue: result.text };
221
+ return { triggered: false, currentValue: result.text, tabId };
66
222
  }
67
223
  if (result.text !== condition.baseline) {
68
224
  return {
69
225
  triggered: true,
70
226
  details: "Content changed",
71
227
  currentValue: result.text,
228
+ tabId,
72
229
  };
73
230
  }
74
- return { triggered: false };
231
+ return { triggered: false, tabId };
75
232
  }
76
233
  case "text_appears": {
77
234
  if (!condition.text) {
@@ -82,25 +239,27 @@ async function checkMonitor(monitor, checkFn) {
82
239
  triggered: true,
83
240
  details: `Text "${condition.text}" appeared on page`,
84
241
  currentValue: result.text,
242
+ tabId,
85
243
  };
86
244
  }
87
- return { triggered: false };
245
+ return { triggered: false, tabId };
88
246
  }
89
247
  case "text_disappears": {
90
248
  if (!condition.text) {
91
249
  throw new Error("text_disappears condition requires 'text' parameter");
92
250
  }
93
251
  if (!condition.baseline) {
94
- return { triggered: false, currentValue: result.text };
252
+ return { triggered: false, currentValue: result.text, tabId };
95
253
  }
96
254
  if (condition.baseline.includes(condition.text) && !result.text.includes(condition.text)) {
97
255
  return {
98
256
  triggered: true,
99
257
  details: `Text "${condition.text}" disappeared from page`,
100
258
  currentValue: result.text,
259
+ tabId,
101
260
  };
102
261
  }
103
- return { triggered: false };
262
+ return { triggered: false, tabId };
104
263
  }
105
264
  case "price_below": {
106
265
  if (condition.threshold === undefined) {
@@ -114,14 +273,15 @@ async function checkMonitor(monitor, checkFn) {
114
273
  triggered: true,
115
274
  details: `Price ${price} is below threshold ${condition.threshold}`,
116
275
  currentValue: price.toString(),
276
+ tabId,
117
277
  };
118
278
  }
119
279
  }
120
- return { triggered: false };
280
+ return { triggered: false, tabId };
121
281
  }
122
282
  case "new_items": {
123
283
  if (!condition.baseline) {
124
- return { triggered: false, currentValue: JSON.stringify(result.items || []) };
284
+ return { triggered: false, currentValue: JSON.stringify(result.items || []), tabId };
125
285
  }
126
286
  const baselineItems = JSON.parse(condition.baseline);
127
287
  const currentItems = result.items || [];
@@ -130,25 +290,27 @@ async function checkMonitor(monitor, checkFn) {
130
290
  triggered: true,
131
291
  details: `New items detected: ${currentItems.length - baselineItems.length} added`,
132
292
  currentValue: JSON.stringify(currentItems),
293
+ tabId,
133
294
  };
134
295
  }
135
- return { triggered: false };
296
+ return { triggered: false, tabId };
136
297
  }
137
298
  case "selector_change": {
138
299
  if (!condition.selector) {
139
300
  throw new Error("selector_change condition requires 'selector' parameter");
140
301
  }
141
302
  if (!condition.baseline) {
142
- return { triggered: false, currentValue: result.text };
303
+ return { triggered: false, currentValue: result.text, tabId };
143
304
  }
144
305
  if (result.text !== condition.baseline) {
145
306
  return {
146
307
  triggered: true,
147
308
  details: "Selector content changed",
148
309
  currentValue: result.text,
310
+ tabId,
149
311
  };
150
312
  }
151
- return { triggered: false };
313
+ return { triggered: false, tabId };
152
314
  }
153
315
  default:
154
316
  throw new Error(`Unknown condition type: ${condition.type}`);
@@ -159,8 +321,8 @@ async function checkMonitor(monitor, checkFn) {
159
321
  return { triggered: false };
160
322
  }
161
323
  }
162
- async function runMonitorCheck(monitor, checkFn) {
163
- const checkResult = await checkMonitor(monitor, checkFn);
324
+ async function runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, ctx) {
325
+ const checkResult = await checkMonitor(monitor, checkFn, ctx);
164
326
  const monitors = loadMonitors();
165
327
  const monitorIndex = monitors.findIndex((m) => m.id === monitor.id);
166
328
  if (monitorIndex === -1)
@@ -182,6 +344,29 @@ async function runMonitorCheck(monitor, checkFn) {
182
344
  previous_value: monitor.condition.baseline,
183
345
  current_value: checkResult.currentValue,
184
346
  };
347
+ // Execute actions if defined — tabId may be undefined for buffer-based conditions
348
+ if (monitor.actions?.length && actionFn) {
349
+ try {
350
+ const actionResults = await actionFn(monitor.actions, checkResult.tabId, {
351
+ monitor_name: monitor.name,
352
+ monitor_id: monitor.id,
353
+ url: monitor.url,
354
+ condition_type: monitor.condition.type,
355
+ triggered_at: alert.triggered_at,
356
+ details: alert.details,
357
+ previous_value: alert.previous_value,
358
+ current_value: alert.current_value,
359
+ });
360
+ alert.action_results = actionResults;
361
+ const failed = actionResults.filter((r) => r.status === "failed");
362
+ if (failed.length > 0) {
363
+ console.error(`[MONITOR ACTIONS] ${monitor.name}: ${failed.length}/${actionResults.length} action(s) failed`);
364
+ }
365
+ }
366
+ catch (err) {
367
+ console.error(`[MONITOR ACTIONS] ${monitor.name}: action execution error:`, err);
368
+ }
369
+ }
185
370
  addAlert(alert);
186
371
  console.error(`[MONITOR ALERT] ${monitor.name}: ${alert.details}`);
187
372
  if (checkResult.currentValue) {
@@ -192,8 +377,15 @@ async function runMonitorCheck(monitor, checkFn) {
192
377
  monitors[monitorIndex].last_result = "no_change";
193
378
  }
194
379
  saveMonitors(monitors);
380
+ // Close the tab after actions are done (if the check opened one)
381
+ if (checkResult.tabId && closeTabFn) {
382
+ try {
383
+ await closeTabFn(checkResult.tabId);
384
+ }
385
+ catch { /* ok */ }
386
+ }
195
387
  }
196
- export function startMonitor(checkFn) {
388
+ export function startMonitor(checkFn, actionFn, closeTabFn, getCtx) {
197
389
  for (const interval of activeIntervals.values()) {
198
390
  clearInterval(interval);
199
391
  }
@@ -210,12 +402,12 @@ export function startMonitor(checkFn) {
210
402
  const delay = (i + 1) * STAGGER_MS;
211
403
  const timeout = setTimeout(() => {
212
404
  // First check after the staggered delay
213
- runMonitorCheck(monitor, checkFn).catch((err) => {
405
+ runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, getCtx?.()).catch((err) => {
214
406
  console.error(`Error running monitor ${monitor.id}:`, err);
215
407
  });
216
408
  // Then schedule the repeating interval
217
409
  const interval = setInterval(() => {
218
- runMonitorCheck(monitor, checkFn).catch((err) => {
410
+ runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, getCtx?.()).catch((err) => {
219
411
  console.error(`Error running monitor ${monitor.id}:`, err);
220
412
  });
221
413
  }, intervalMs);
@@ -229,7 +421,7 @@ export function startMonitor(checkFn) {
229
421
  console.error(`[monitor] Scheduled ${enabled.length} monitor(s), staggered over ${(enabled.length * STAGGER_MS / 1000)}s`);
230
422
  }
231
423
  }
232
- export async function handleMonitorTool(name, args, checkFn) {
424
+ export async function handleMonitorTool(name, args, checkFn, actionFn, closeTabFn, getCtx) {
233
425
  switch (name) {
234
426
  case "neuron_monitor_create": {
235
427
  const url = args.url;
@@ -237,15 +429,21 @@ export async function handleMonitorTool(name, args, checkFn) {
237
429
  const checkIntervalMinutes = args.check_interval_minutes || 60;
238
430
  const condition = args.condition;
239
431
  const enabled = args.enabled !== undefined ? args.enabled : true;
240
- if (!url || !monitorName || !condition) {
241
- throw new Error("Missing required parameters: url, name, condition");
432
+ const actions = args.actions;
433
+ if (!monitorName || !condition) {
434
+ throw new Error("Missing required parameters: name, condition");
435
+ }
436
+ const isBufferBased = condition.type === "request_match" || condition.type === "console_match";
437
+ if (!isBufferBased && !url) {
438
+ throw new Error("URL is required for page-based conditions");
242
439
  }
243
440
  const monitor = {
244
441
  id: generateId(),
245
- url,
442
+ url: url || "",
246
443
  name: monitorName,
247
444
  check_interval_minutes: checkIntervalMinutes,
248
445
  condition,
446
+ actions,
249
447
  enabled,
250
448
  created_at: new Date().toISOString(),
251
449
  alert_count: 0,
@@ -256,15 +454,17 @@ export async function handleMonitorTool(name, args, checkFn) {
256
454
  if (enabled && checkFn) {
257
455
  const intervalMs = checkIntervalMinutes * 60 * 1000;
258
456
  const interval = setInterval(() => {
259
- runMonitorCheck(monitor, checkFn).catch((err) => {
457
+ runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, getCtx?.()).catch((err) => {
260
458
  console.error(`Error running monitor ${monitor.id}:`, err);
261
459
  });
262
460
  }, intervalMs);
263
461
  activeIntervals.set(monitor.id, interval);
264
462
  }
463
+ const bridgePort = parseInt(process.env.NEURON_BRIDGE_PORT ?? "7377", 10);
265
464
  return {
266
465
  success: true,
267
466
  monitor_id: monitor.id,
467
+ webhook_url: `http://127.0.0.1:${bridgePort}/hooks/${monitor.id}`,
268
468
  message: `Monitor "${monitorName}" created successfully`,
269
469
  };
270
470
  }
@@ -329,7 +529,7 @@ export async function handleMonitorTool(name, args, checkFn) {
329
529
  if (!monitor) {
330
530
  throw new Error(`Monitor ${monitorId} not found`);
331
531
  }
332
- await runMonitorCheck(monitor, checkFn);
532
+ await runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, getCtx?.());
333
533
  const updated = loadMonitors().find((m) => m.id === monitorId);
334
534
  return {
335
535
  monitor_id: monitorId,
@@ -342,7 +542,7 @@ export async function handleMonitorTool(name, args, checkFn) {
342
542
  const results = [];
343
543
  for (const monitor of monitors) {
344
544
  if (monitor.enabled) {
345
- await runMonitorCheck(monitor, checkFn);
545
+ await runMonitorCheck(monitor, checkFn, actionFn, closeTabFn, getCtx?.());
346
546
  const updated = loadMonitors().find((m) => m.id === monitor.id);
347
547
  results.push({
348
548
  monitor_id: monitor.id,
@@ -390,6 +590,9 @@ export async function handleMonitorTool(name, args, checkFn) {
390
590
  const newCond = args.condition;
391
591
  monitor.condition = { ...monitor.condition, ...newCond };
392
592
  }
593
+ if (args.actions !== undefined) {
594
+ monitor.actions = args.actions;
595
+ }
393
596
  saveMonitors(monitors);
394
597
  // Restart interval if interval or enabled changed
395
598
  const existingInterval = activeIntervals.get(monitorId);
@@ -401,7 +604,7 @@ export async function handleMonitorTool(name, args, checkFn) {
401
604
  const intervalMs = monitor.check_interval_minutes * 60 * 1000;
402
605
  const m = monitor;
403
606
  const interval = setInterval(() => {
404
- runMonitorCheck(m, checkFn).catch((err) => {
607
+ runMonitorCheck(m, checkFn, actionFn, closeTabFn, getCtx?.()).catch((err) => {
405
608
  console.error(`Error running monitor ${m.id}:`, err);
406
609
  });
407
610
  }, intervalMs);
@@ -417,6 +620,7 @@ export async function handleMonitorTool(name, args, checkFn) {
417
620
  enabled: monitor.enabled,
418
621
  check_interval_minutes: monitor.check_interval_minutes,
419
622
  condition: monitor.condition,
623
+ actions: monitor.actions,
420
624
  },
421
625
  };
422
626
  }
@@ -443,7 +647,7 @@ export async function handleMonitorTool(name, args, checkFn) {
443
647
  const intervalMs = monitors[idx].check_interval_minutes * 60 * 1000;
444
648
  const m = monitors[idx];
445
649
  const interval = setInterval(() => {
446
- runMonitorCheck(m, checkFn).catch((err) => {
650
+ runMonitorCheck(m, checkFn, actionFn, closeTabFn, getCtx?.()).catch((err) => {
447
651
  console.error(`Error running monitor ${m.id}:`, err);
448
652
  });
449
653
  }, intervalMs);
@@ -461,13 +665,13 @@ export async function handleMonitorTool(name, args, checkFn) {
461
665
  export const MONITOR_TOOLS = [
462
666
  {
463
667
  name: "neuron_monitor_create",
464
- description: "Create a persistent page monitor that watches a URL for changes and alerts when conditions are met (price drops, new job postings, content changes). Monitors run in the background at specified intervals.",
668
+ description: "Create a persistent monitor that watches a URL for changes, matches HTTP traffic patterns, or watches console messages. Monitors run in the background at specified intervals. Page-based conditions (content_change, text_appears, etc.) require a URL. Buffer-based conditions (request_match, console_match) query the extension's ring buffers directly without opening a tab.",
465
669
  inputSchema: {
466
670
  type: "object",
467
671
  properties: {
468
672
  url: {
469
673
  type: "string",
470
- description: "The URL to monitor",
674
+ description: "The URL to monitor. Required for page-based conditions, optional for request_match/console_match",
471
675
  },
472
676
  name: {
473
677
  type: "string",
@@ -475,7 +679,7 @@ export const MONITOR_TOOLS = [
475
679
  },
476
680
  check_interval_minutes: {
477
681
  type: "number",
478
- description: "How often to check the page (in minutes). Default: 60",
682
+ description: "How often to check (in minutes). Default: 60",
479
683
  default: 60,
480
684
  },
481
685
  condition: {
@@ -491,8 +695,10 @@ export const MONITOR_TOOLS = [
491
695
  "price_below",
492
696
  "new_items",
493
697
  "selector_change",
698
+ "request_match",
699
+ "console_match",
494
700
  ],
495
- description: "Type of condition: content_change (any change), text_appears, text_disappears, price_below (numeric comparison), new_items (list growth), selector_change (specific element change)",
701
+ description: "Type of condition: content_change (any change), text_appears, text_disappears, price_below (numeric comparison), new_items (list growth), selector_change (specific element change), request_match (HTTP requests in ring buffers), console_match (console messages in ring buffers)",
496
702
  },
497
703
  selector: {
498
704
  type: "string",
@@ -506,6 +712,31 @@ export const MONITOR_TOOLS = [
506
712
  type: "number",
507
713
  description: "Numeric threshold for price_below condition",
508
714
  },
715
+ urlPattern: {
716
+ type: "string",
717
+ description: "URL substring to match for request_match condition",
718
+ },
719
+ method: {
720
+ type: "string",
721
+ description: "HTTP method filter for request_match (e.g. GET, POST)",
722
+ },
723
+ statusMin: {
724
+ type: "number",
725
+ description: "Minimum HTTP status code for request_match (e.g. 400)",
726
+ },
727
+ statusMax: {
728
+ type: "number",
729
+ description: "Maximum HTTP status code for request_match (e.g. 599)",
730
+ },
731
+ search: {
732
+ type: "string",
733
+ description: "Substring to search in console messages for console_match",
734
+ },
735
+ levels: {
736
+ type: "array",
737
+ items: { type: "string" },
738
+ description: "Console log levels to match for console_match (e.g. [\"error\", \"warn\"])",
739
+ },
509
740
  },
510
741
  required: ["type"],
511
742
  },
@@ -514,8 +745,34 @@ export const MONITOR_TOOLS = [
514
745
  description: "Whether the monitor is active. Default: true",
515
746
  default: true,
516
747
  },
748
+ actions: {
749
+ type: "array",
750
+ description: "Actions to execute when the condition triggers. Each action runs in sequence; chain stops on first failure unless on_error is 'continue'.",
751
+ items: {
752
+ type: "object",
753
+ properties: {
754
+ id: { type: "string", description: "Unique action ID" },
755
+ type: {
756
+ type: "string",
757
+ enum: ["click", "type", "navigate", "scroll", "evaluate", "screenshot", "extract", "webhook"],
758
+ description: "Action type to execute",
759
+ },
760
+ config: {
761
+ type: "object",
762
+ description: "Action-specific configuration. click: {selector, text}. type: {selector, value}. navigate: {url}. scroll: {pixels}. evaluate: {expression}. screenshot: {}. extract: {selector}. webhook: {url, method}.",
763
+ },
764
+ on_error: {
765
+ type: "string",
766
+ enum: ["stop", "continue"],
767
+ description: "What to do if this action fails. Default: stop",
768
+ },
769
+ label: { type: "string", description: "Human-readable label for this action" },
770
+ },
771
+ required: ["id", "type", "config"],
772
+ },
773
+ },
517
774
  },
518
- required: ["url", "name", "condition"],
775
+ required: ["name", "condition"],
519
776
  },
520
777
  },
521
778
  {
@@ -613,17 +870,38 @@ export const MONITOR_TOOLS = [
613
870
  properties: {
614
871
  type: {
615
872
  type: "string",
616
- enum: ["content_change", "text_appears", "text_disappears", "price_below", "new_items", "selector_change"],
873
+ enum: ["content_change", "text_appears", "text_disappears", "price_below", "new_items", "selector_change", "request_match", "console_match"],
617
874
  },
618
875
  selector: { type: "string" },
619
876
  text: { type: "string" },
620
877
  threshold: { type: "number" },
878
+ urlPattern: { type: "string" },
879
+ method: { type: "string" },
880
+ statusMin: { type: "number" },
881
+ statusMax: { type: "number" },
882
+ search: { type: "string" },
883
+ levels: { type: "array", items: { type: "string" } },
621
884
  },
622
885
  },
623
886
  enabled: {
624
887
  type: "boolean",
625
888
  description: "Whether the monitor is active",
626
889
  },
890
+ actions: {
891
+ type: "array",
892
+ description: "Replace the monitor's action chain. Same schema as neuron_monitor_create.",
893
+ items: {
894
+ type: "object",
895
+ properties: {
896
+ id: { type: "string" },
897
+ type: { type: "string", enum: ["click", "type", "navigate", "scroll", "evaluate", "screenshot", "extract", "webhook"] },
898
+ config: { type: "object" },
899
+ on_error: { type: "string", enum: ["stop", "continue"] },
900
+ label: { type: "string" },
901
+ },
902
+ required: ["id", "type", "config"],
903
+ },
904
+ },
627
905
  },
628
906
  required: ["monitor_id"],
629
907
  },