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.
- package/README.md +345 -42
- package/dist/automations/executor.d.ts +21 -0
- package/dist/automations/executor.js +835 -0
- package/dist/automations/executor.js.map +1 -0
- package/dist/automations/index.d.ts +7 -0
- package/dist/automations/index.js +21 -0
- package/dist/automations/index.js.map +1 -0
- package/dist/automations/store.d.ts +15 -0
- package/dist/automations/store.js +193 -0
- package/dist/automations/store.js.map +1 -0
- package/dist/automations/tools.d.ts +14 -0
- package/dist/automations/tools.js +735 -0
- package/dist/automations/tools.js.map +1 -0
- package/dist/automations/types.d.ts +211 -0
- package/dist/automations/types.js +4 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/business-tools.d.ts +17 -0
- package/dist/business-tools.js +3065 -0
- package/dist/business-tools.js.map +1 -0
- package/dist/collections/resolver.js +2 -2
- package/dist/collections/resolver.js.map +1 -1
- package/dist/collections/store.js +2 -2
- package/dist/collections/store.js.map +1 -1
- package/dist/killswitch.js +2 -2
- package/dist/killswitch.js.map +1 -1
- package/dist/monitor.d.ts +77 -2
- package/dist/monitor.js +331 -53
- package/dist/monitor.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +39 -0
- package/dist/paths.js.map +1 -0
- package/dist/recipe-tools.js +28 -3
- package/dist/recipe-tools.js.map +1 -1
- package/dist/recipes.d.ts +2 -0
- package/dist/recipes.js +34 -5
- package/dist/recipes.js.map +1 -1
- package/dist/scheduler.js +4 -8
- package/dist/scheduler.js.map +1 -1
- package/dist/server.js +191 -19
- package/dist/server.js.map +1 -1
- package/dist/session-state.js +2 -2
- package/dist/session-state.js.map +1 -1
- package/dist/tools.js +28 -0
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
- package/recipes/linkedin-job-scraper/agent.md +144 -0
- package/recipes/linkedin-job-scraper/learnings.md +3 -0
- package/recipes/linkedin-job-scraper/recipe.yaml +71 -0
- package/recipes/sheets-data-entry/agent.md +166 -0
- package/recipes/sheets-data-entry/learnings.md +3 -0
- package/recipes/sheets-data-entry/recipe.yaml +78 -0
- package/recipes/x-job-scraper/agent.md +156 -0
- package/recipes/x-job-scraper/learnings.md +3 -0
- package/recipes/x-job-scraper/recipe.yaml +72 -0
- package/dist/__tests__/correlation.test.d.ts +0 -1
- package/dist/__tests__/correlation.test.js +0 -53
- package/dist/__tests__/correlation.test.js.map +0 -1
- package/dist/__tests__/monitor.test.d.ts +0 -1
- package/dist/__tests__/monitor.test.js +0 -286
- package/dist/__tests__/monitor.test.js.map +0 -1
- package/dist/__tests__/recipes.test.d.ts +0 -1
- package/dist/__tests__/recipes.test.js +0 -415
- package/dist/__tests__/recipes.test.js.map +0 -1
- package/dist/__tests__/scheduler.test.d.ts +0 -1
- package/dist/__tests__/scheduler.test.js +0 -316
- package/dist/__tests__/scheduler.test.js.map +0 -1
- package/dist/__tests__/session-state.test.d.ts +0 -1
- package/dist/__tests__/session-state.test.js +0 -335
- 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
|
-
|
|
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
|
|
8
|
+
return path.join(NEURON_DIR, "monitors.yaml");
|
|
17
9
|
}
|
|
18
10
|
function alertsPath() {
|
|
19
|
-
return
|
|
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(
|
|
15
|
+
if (!fs.existsSync(monitorsPath())) {
|
|
24
16
|
return [];
|
|
25
17
|
}
|
|
26
|
-
const content = fs.readFileSync(
|
|
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(
|
|
25
|
+
fs.writeFileSync(monitorsPath(), YAML.stringify(store), "utf-8");
|
|
34
26
|
}
|
|
35
27
|
function loadAlerts() {
|
|
36
28
|
ensureNeuronDir();
|
|
37
|
-
if (!fs.existsSync(
|
|
29
|
+
if (!fs.existsSync(alertsPath())) {
|
|
38
30
|
return [];
|
|
39
31
|
}
|
|
40
|
-
const content = fs.readFileSync(
|
|
32
|
+
const content = fs.readFileSync(alertsPath(), "utf-8");
|
|
41
33
|
const store = YAML.parse(content);
|
|
42
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
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: ["
|
|
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
|
},
|