assistme 0.3.4 → 0.3.6
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/dist/{chunk-KX7ITO55.js → chunk-4YWS463E.js} +174 -22
- package/dist/{chunk-TTEGHE2E.js → chunk-JVA6DHXD.js} +6 -4
- package/dist/{config-PUIS2TQL.js → config-T4357GAE.js} +1 -1
- package/dist/index.js +513 -164
- package/dist/job-runner-JT3JWZBV.js +7 -0
- package/package.json +2 -1
- package/src/agent/event-hooks.ts +16 -8
- package/src/agent/job-runner.ts +52 -40
- package/src/agent/processor.ts +24 -27
- package/src/agent/scheduler.ts +22 -59
- package/src/agent/skill-evaluator.ts +45 -52
- package/src/agent/skills.ts +57 -36
- package/src/browser/controller.ts +434 -13
- package/src/mcp/browser-server.ts +1 -1
- package/src/tools/filesystem.ts +32 -35
- package/src/tools/shell.ts +18 -22
- package/src/utils/config.test.ts +1 -1
- package/src/utils/config.ts +15 -9
- package/src/utils/constants.ts +77 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/schemas.ts +115 -0
- package/dist/job-runner-P2L6MOOX.js +0 -7
package/dist/index.js
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AppError,
|
|
4
|
+
BrowseSkillRowSchema,
|
|
5
|
+
CDP_COMMAND_TIMEOUT_MS,
|
|
6
|
+
FRAME_CONTEXTS_MAX_SIZE,
|
|
3
7
|
JobRunner,
|
|
8
|
+
MAX_COMPLETE_TASK_RETRIES,
|
|
9
|
+
MAX_CONTENT_SEARCH_FILES,
|
|
10
|
+
MAX_CONTENT_SEARCH_RESULTS,
|
|
11
|
+
MAX_FILE_SEARCH_RESULTS,
|
|
12
|
+
MAX_HISTORY_ENTRIES,
|
|
13
|
+
MAX_HISTORY_RESPONSE_LENGTH,
|
|
14
|
+
MAX_RESPONSE_CONTENT_LENGTH,
|
|
15
|
+
MAX_SKILL_RECORD_RESULT_LENGTH,
|
|
16
|
+
MAX_TOOL_INPUT_LOG_LENGTH,
|
|
17
|
+
MAX_TOOL_RESULT_LENGTH,
|
|
18
|
+
SCHEDULER_INTERVAL_MS,
|
|
19
|
+
SHELL_MAX_OUTPUT,
|
|
20
|
+
SHELL_TIMEOUT_MS,
|
|
21
|
+
SKILL_DESCRIPTION_BUDGET_CHARS,
|
|
22
|
+
SkillCreateResultSchema,
|
|
23
|
+
SkillDecisionSchema,
|
|
24
|
+
SkillRowSchema,
|
|
25
|
+
WS_CONNECT_TIMEOUT_MS,
|
|
4
26
|
callMcpHandler,
|
|
27
|
+
errorMessage,
|
|
5
28
|
log,
|
|
6
29
|
newCorrelationId,
|
|
7
30
|
readAuthStore,
|
|
31
|
+
safeParse,
|
|
8
32
|
setCorrelationId,
|
|
9
33
|
setLogLevel,
|
|
10
34
|
writeAuthStore
|
|
11
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-4YWS463E.js";
|
|
12
36
|
import {
|
|
13
37
|
clearConfig,
|
|
14
38
|
getConfig,
|
|
15
39
|
getConfigPath,
|
|
16
40
|
setConfig
|
|
17
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-JVA6DHXD.js";
|
|
18
42
|
|
|
19
43
|
// src/index.ts
|
|
20
44
|
import { Command } from "commander";
|
|
@@ -54,7 +78,7 @@ async function logout() {
|
|
|
54
78
|
|
|
55
79
|
// src/db/session.ts
|
|
56
80
|
async function createSession(sessionName, workspacePath, version2) {
|
|
57
|
-
const { getConfig: getConfig2 } = await import("./config-
|
|
81
|
+
const { getConfig: getConfig2 } = await import("./config-T4357GAE.js");
|
|
58
82
|
const data = await callMcpHandler("session.create", {
|
|
59
83
|
session_name: sessionName,
|
|
60
84
|
workspace_path: workspacePath,
|
|
@@ -135,11 +159,11 @@ async function completeTask(messageId, resultSummary, tokenUsage) {
|
|
|
135
159
|
token_usage: tokenUsage || null
|
|
136
160
|
});
|
|
137
161
|
}
|
|
138
|
-
async function failTask(messageId,
|
|
162
|
+
async function failTask(messageId, errorMessage2) {
|
|
139
163
|
try {
|
|
140
164
|
await callMcpHandler("task.fail", {
|
|
141
165
|
message_id: messageId,
|
|
142
|
-
error:
|
|
166
|
+
error: errorMessage2
|
|
143
167
|
});
|
|
144
168
|
} catch (err) {
|
|
145
169
|
log.error(`Failed to update task status: ${err instanceof Error ? err.message : err}`);
|
|
@@ -360,6 +384,8 @@ var BrowserController = class {
|
|
|
360
384
|
connected = false;
|
|
361
385
|
currentTabId = null;
|
|
362
386
|
refCache = /* @__PURE__ */ new Map();
|
|
387
|
+
frameContexts = /* @__PURE__ */ new Map();
|
|
388
|
+
// refId → contextId
|
|
363
389
|
constructor(port = 9222) {
|
|
364
390
|
this.debugPort = port;
|
|
365
391
|
}
|
|
@@ -410,9 +436,9 @@ var BrowserController = class {
|
|
|
410
436
|
if (!settled) {
|
|
411
437
|
settled = true;
|
|
412
438
|
this.ws?.close();
|
|
413
|
-
reject(new Error(
|
|
439
|
+
reject(new Error(`Connection timeout (${WS_CONNECT_TIMEOUT_MS}ms)`));
|
|
414
440
|
}
|
|
415
|
-
},
|
|
441
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
416
442
|
this.ws.on("open", () => {
|
|
417
443
|
if (settled) return;
|
|
418
444
|
settled = true;
|
|
@@ -460,6 +486,8 @@ var BrowserController = class {
|
|
|
460
486
|
this.ws = null;
|
|
461
487
|
this.connected = false;
|
|
462
488
|
}
|
|
489
|
+
this.refCache.clear();
|
|
490
|
+
this.frameContexts.clear();
|
|
463
491
|
return "Disconnected from browser.";
|
|
464
492
|
}
|
|
465
493
|
// ── CDP Protocol ────────────────────────────────────────────────
|
|
@@ -479,7 +507,7 @@ var BrowserController = class {
|
|
|
479
507
|
const timeout = setTimeout(() => {
|
|
480
508
|
this.callbacks.delete(id);
|
|
481
509
|
reject(new Error(`CDP command timed out: ${method}`));
|
|
482
|
-
},
|
|
510
|
+
}, CDP_COMMAND_TIMEOUT_MS);
|
|
483
511
|
this.callbacks.set(id, (response) => {
|
|
484
512
|
clearTimeout(timeout);
|
|
485
513
|
if (response.error) {
|
|
@@ -653,13 +681,46 @@ URL: ${info.url}`;
|
|
|
653
681
|
const result = await this.send("Runtime.evaluate", {
|
|
654
682
|
expression: `
|
|
655
683
|
(function() {
|
|
656
|
-
|
|
684
|
+
var el = document.querySelector(${selectorJS});
|
|
685
|
+
|
|
686
|
+
// If not found in main document, search same-origin iframes
|
|
687
|
+
if (!el) {
|
|
688
|
+
var iframes = document.querySelectorAll('iframe');
|
|
689
|
+
for (var i = 0; i < iframes.length; i++) {
|
|
690
|
+
try {
|
|
691
|
+
var iframeDoc = iframes[i].contentDocument;
|
|
692
|
+
if (iframeDoc) {
|
|
693
|
+
el = iframeDoc.querySelector(${selectorJS});
|
|
694
|
+
if (el) break;
|
|
695
|
+
}
|
|
696
|
+
} catch(e) { /* cross-origin, skip */ }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
657
700
|
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
658
701
|
|
|
659
702
|
el.focus();
|
|
660
703
|
|
|
661
|
-
//
|
|
662
|
-
|
|
704
|
+
// Check if this is a contenteditable element (rich text editor)
|
|
705
|
+
var isContentEditable = el.isContentEditable ||
|
|
706
|
+
el.getAttribute('contenteditable') === 'true' ||
|
|
707
|
+
el.getAttribute('contenteditable') === '';
|
|
708
|
+
|
|
709
|
+
if (isContentEditable) {
|
|
710
|
+
// For contenteditable: select all content, then replace
|
|
711
|
+
var ownerDoc = el.ownerDocument;
|
|
712
|
+
var sel = ownerDoc.defaultView.getSelection();
|
|
713
|
+
var range = ownerDoc.createRange();
|
|
714
|
+
range.selectNodeContents(el);
|
|
715
|
+
sel.removeAllRanges();
|
|
716
|
+
sel.addRange(range);
|
|
717
|
+
// Use insertText command which respects undo stack and triggers input events
|
|
718
|
+
ownerDoc.execCommand('insertText', false, ${textJS});
|
|
719
|
+
return 'Typed into: ' + (el.tagName || '') + ' [contenteditable]';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// For input/textarea: clear and set value
|
|
723
|
+
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
663
724
|
window.HTMLInputElement.prototype, 'value'
|
|
664
725
|
)?.set || Object.getOwnPropertyDescriptor(
|
|
665
726
|
window.HTMLTextAreaElement.prototype, 'value'
|
|
@@ -679,7 +740,27 @@ URL: ${info.url}`;
|
|
|
679
740
|
`,
|
|
680
741
|
returnByValue: true
|
|
681
742
|
});
|
|
682
|
-
|
|
743
|
+
const textResult = result.result?.value || "";
|
|
744
|
+
if (textResult.startsWith("Element not found")) {
|
|
745
|
+
return this.typeAtFocus(text);
|
|
746
|
+
}
|
|
747
|
+
return textResult || "Text entered.";
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Type text into the currently focused element using CDP Input.insertText.
|
|
751
|
+
* This bypasses DOM queries entirely and works with any focused element,
|
|
752
|
+
* including those inside cross-origin iframes or shadow DOM.
|
|
753
|
+
*/
|
|
754
|
+
async typeAtFocus(text) {
|
|
755
|
+
this.ensureConnected();
|
|
756
|
+
const modKey = platform() === "darwin" ? "Meta" : "Control";
|
|
757
|
+
await this.pressKey(`${modKey}+a`);
|
|
758
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
759
|
+
await this.pressKey("Backspace");
|
|
760
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
761
|
+
await this.send("Input.insertText", { text });
|
|
762
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
763
|
+
return "Text entered (into focused element).";
|
|
683
764
|
}
|
|
684
765
|
async pressKey(key) {
|
|
685
766
|
this.ensureConnected();
|
|
@@ -929,6 +1010,7 @@ URL: ${info.url}`;
|
|
|
929
1010
|
inputType: r.type || "",
|
|
930
1011
|
box: r.box
|
|
931
1012
|
}));
|
|
1013
|
+
await this.discoverCrossOriginFrameRefs(refs);
|
|
932
1014
|
if (annotate && refs.length <= 40) {
|
|
933
1015
|
const refsJson = JSON.stringify(refs);
|
|
934
1016
|
await this.send("Runtime.evaluate", {
|
|
@@ -1003,6 +1085,199 @@ Refs:
|
|
|
1003
1085
|
}
|
|
1004
1086
|
return table;
|
|
1005
1087
|
}
|
|
1088
|
+
// ── Cross-Origin Iframe Discovery ────────────────────────────────
|
|
1089
|
+
/**
|
|
1090
|
+
* Use CDP's Page.getFrameTree + Runtime.evaluate with contextId to discover
|
|
1091
|
+
* interactive elements inside cross-origin iframes (e.g., ProtonMail editor,
|
|
1092
|
+
* Google Docs, embedded rich text editors).
|
|
1093
|
+
*
|
|
1094
|
+
* Same-origin iframes are already handled inline by the main snapshot JS.
|
|
1095
|
+
* This method handles the ones that threw cross-origin errors.
|
|
1096
|
+
*/
|
|
1097
|
+
async discoverCrossOriginFrameRefs(refs) {
|
|
1098
|
+
this.frameContexts.clear();
|
|
1099
|
+
try {
|
|
1100
|
+
const frameTree = await this.send("Page.getFrameTree");
|
|
1101
|
+
const mainFrameId = frameTree.frameTree?.frame?.id;
|
|
1102
|
+
const childFrames = frameTree.frameTree?.childFrames || [];
|
|
1103
|
+
if (childFrames.length === 0) return;
|
|
1104
|
+
const contexts = await this.getFrameContexts(mainFrameId || "");
|
|
1105
|
+
for (const child of childFrames) {
|
|
1106
|
+
const frameId = child.frame.id;
|
|
1107
|
+
const contextId = contexts.get(frameId);
|
|
1108
|
+
if (!contextId) continue;
|
|
1109
|
+
const iframeOffsetResult = await this.send("Runtime.evaluate", {
|
|
1110
|
+
expression: `
|
|
1111
|
+
(function() {
|
|
1112
|
+
var iframes = document.querySelectorAll('iframe');
|
|
1113
|
+
for (var i = 0; i < iframes.length; i++) {
|
|
1114
|
+
try {
|
|
1115
|
+
// Match by frame src or name
|
|
1116
|
+
var f = iframes[i];
|
|
1117
|
+
if (f.contentWindow) {
|
|
1118
|
+
var r = f.getBoundingClientRect();
|
|
1119
|
+
if (r.width > 10 && r.height > 10) {
|
|
1120
|
+
return JSON.stringify({ x: r.x, y: r.y, width: r.width, height: r.height, index: i });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} catch(e) {}
|
|
1124
|
+
}
|
|
1125
|
+
return 'null';
|
|
1126
|
+
})()
|
|
1127
|
+
`,
|
|
1128
|
+
returnByValue: true
|
|
1129
|
+
});
|
|
1130
|
+
let iframeOffset = { x: 0, y: 0 };
|
|
1131
|
+
try {
|
|
1132
|
+
const parsed = JSON.parse(
|
|
1133
|
+
iframeOffsetResult.result?.value || "null"
|
|
1134
|
+
);
|
|
1135
|
+
if (parsed) iframeOffset = { x: parsed.x, y: parsed.y };
|
|
1136
|
+
} catch {
|
|
1137
|
+
}
|
|
1138
|
+
const startRefId = refs.length + 1;
|
|
1139
|
+
try {
|
|
1140
|
+
const frameResult = await this.send("Runtime.evaluate", {
|
|
1141
|
+
expression: `
|
|
1142
|
+
(function() {
|
|
1143
|
+
var selectors = [
|
|
1144
|
+
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
1145
|
+
'[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
|
|
1146
|
+
'[role="combobox"]', '[role="listbox"]', '[role="menuitem"]', '[role="tab"]',
|
|
1147
|
+
'[role="switch"]', '[role="slider"]', '[role="option"]', '[role="searchbox"]',
|
|
1148
|
+
'[onclick]', '[tabindex]:not([tabindex="-1"])',
|
|
1149
|
+
'[contenteditable="true"]', '[contenteditable=""]'
|
|
1150
|
+
].join(', ');
|
|
1151
|
+
|
|
1152
|
+
var all = document.querySelectorAll(selectors);
|
|
1153
|
+
// Also check if the body itself is contenteditable
|
|
1154
|
+
if (document.body && (document.body.isContentEditable || document.body.getAttribute('contenteditable') === 'true')) {
|
|
1155
|
+
all = [document.body].concat(Array.from(all));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
var refs = [];
|
|
1159
|
+
var startId = ${startRefId};
|
|
1160
|
+
var vh = window.innerHeight;
|
|
1161
|
+
var vw = window.innerWidth;
|
|
1162
|
+
|
|
1163
|
+
for (var i = 0; i < all.length && refs.length < 20; i++) {
|
|
1164
|
+
var el = all[i];
|
|
1165
|
+
var rect = el.getBoundingClientRect();
|
|
1166
|
+
if (rect.width < 5 || rect.height < 5) continue;
|
|
1167
|
+
var style = window.getComputedStyle(el);
|
|
1168
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
1169
|
+
|
|
1170
|
+
var role = el.getAttribute('role') || '';
|
|
1171
|
+
if (!role) {
|
|
1172
|
+
var tag = el.tagName.toLowerCase();
|
|
1173
|
+
if (tag === 'a') role = 'link';
|
|
1174
|
+
else if (tag === 'button') role = 'button';
|
|
1175
|
+
else if (tag === 'input') {
|
|
1176
|
+
var t = (el.type || 'text').toLowerCase();
|
|
1177
|
+
if (t === 'checkbox') role = 'checkbox';
|
|
1178
|
+
else if (t === 'radio') role = 'radio';
|
|
1179
|
+
else if (t === 'submit' || t === 'button') role = 'button';
|
|
1180
|
+
else role = 'textbox';
|
|
1181
|
+
}
|
|
1182
|
+
else if (tag === 'select') role = 'combobox';
|
|
1183
|
+
else if (tag === 'textarea') role = 'textbox';
|
|
1184
|
+
else if (el.isContentEditable) role = 'textbox';
|
|
1185
|
+
else role = tag;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
var name = '';
|
|
1189
|
+
var ariaLabel = el.getAttribute('aria-label');
|
|
1190
|
+
if (ariaLabel) {
|
|
1191
|
+
name = ariaLabel;
|
|
1192
|
+
} else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
1193
|
+
name = el.getAttribute('placeholder') || el.getAttribute('name') || '';
|
|
1194
|
+
} else if (el.isContentEditable) {
|
|
1195
|
+
name = 'compose body';
|
|
1196
|
+
} else {
|
|
1197
|
+
name = (el.textContent || '').trim().slice(0, 60);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
var refId = startId + refs.length;
|
|
1201
|
+
el.setAttribute('data-assistme-ref', String(refId));
|
|
1202
|
+
|
|
1203
|
+
refs.push({
|
|
1204
|
+
id: refId,
|
|
1205
|
+
role: role,
|
|
1206
|
+
name: name,
|
|
1207
|
+
tag: el.tagName.toLowerCase(),
|
|
1208
|
+
type: el.getAttribute('type') || '',
|
|
1209
|
+
box: {
|
|
1210
|
+
x: Math.round(rect.x),
|
|
1211
|
+
y: Math.round(rect.y),
|
|
1212
|
+
width: Math.round(rect.width),
|
|
1213
|
+
height: Math.round(rect.height)
|
|
1214
|
+
},
|
|
1215
|
+
inFrame: true
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return JSON.stringify(refs);
|
|
1220
|
+
})()
|
|
1221
|
+
`,
|
|
1222
|
+
contextId,
|
|
1223
|
+
returnByValue: true
|
|
1224
|
+
});
|
|
1225
|
+
const frameRefs = JSON.parse(
|
|
1226
|
+
frameResult.result?.value || "[]"
|
|
1227
|
+
);
|
|
1228
|
+
for (const r of frameRefs) {
|
|
1229
|
+
refs.push({
|
|
1230
|
+
id: r.id,
|
|
1231
|
+
role: r.role,
|
|
1232
|
+
name: r.name,
|
|
1233
|
+
tag: r.tag,
|
|
1234
|
+
inputType: r.type || "",
|
|
1235
|
+
box: {
|
|
1236
|
+
x: Math.round(r.box.x + iframeOffset.x),
|
|
1237
|
+
y: Math.round(r.box.y + iframeOffset.y),
|
|
1238
|
+
width: r.box.width,
|
|
1239
|
+
height: r.box.height
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
if (this.frameContexts.size < FRAME_CONTEXTS_MAX_SIZE) {
|
|
1243
|
+
this.frameContexts.set(r.id, contextId);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get execution context IDs for each frame in the page.
|
|
1254
|
+
* Uses Runtime.executionContextCreated events collected during the session,
|
|
1255
|
+
* or falls back to evaluating in known frames.
|
|
1256
|
+
*/
|
|
1257
|
+
async getFrameContexts(_mainFrameId) {
|
|
1258
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
1259
|
+
try {
|
|
1260
|
+
await this.send("Runtime.enable").catch(() => {
|
|
1261
|
+
});
|
|
1262
|
+
const frameTree = await this.send("Page.getFrameTree");
|
|
1263
|
+
const childFrames = frameTree.frameTree?.childFrames || [];
|
|
1264
|
+
for (const child of childFrames) {
|
|
1265
|
+
try {
|
|
1266
|
+
const world = await this.send("Page.createIsolatedWorld", {
|
|
1267
|
+
frameId: child.frame.id,
|
|
1268
|
+
worldName: "assistme-snapshot",
|
|
1269
|
+
grantUniveralAccess: true
|
|
1270
|
+
});
|
|
1271
|
+
if (world.executionContextId) {
|
|
1272
|
+
contexts.set(child.frame.id, world.executionContextId);
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
return contexts;
|
|
1280
|
+
}
|
|
1006
1281
|
// ── Ref Resolution ────────────────────────────────────────────────
|
|
1007
1282
|
/**
|
|
1008
1283
|
* Resolve a ref ID to its current center coordinates in the viewport.
|
|
@@ -1115,9 +1390,85 @@ Refs:
|
|
|
1115
1390
|
returnByValue: true
|
|
1116
1391
|
});
|
|
1117
1392
|
const value = result.result?.value;
|
|
1118
|
-
if (
|
|
1393
|
+
if (value && value !== "null") {
|
|
1394
|
+
try {
|
|
1395
|
+
return JSON.parse(value);
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const frameContextId = this.frameContexts.get(refId);
|
|
1400
|
+
if (frameContextId) {
|
|
1401
|
+
return this.resolveRefInFrame(refId, frameContextId, role, name);
|
|
1402
|
+
}
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Resolve a ref inside a cross-origin iframe using its execution context.
|
|
1407
|
+
* Returns coordinates adjusted by the iframe's viewport offset.
|
|
1408
|
+
*/
|
|
1409
|
+
async resolveRefInFrame(refId, contextId, role, name) {
|
|
1410
|
+
const roleJS = JSON.stringify(role);
|
|
1411
|
+
const nameJS = JSON.stringify(name);
|
|
1119
1412
|
try {
|
|
1120
|
-
|
|
1413
|
+
const offsetResult = await this.send("Runtime.evaluate", {
|
|
1414
|
+
expression: `
|
|
1415
|
+
(function() {
|
|
1416
|
+
var iframes = document.querySelectorAll('iframe');
|
|
1417
|
+
for (var i = 0; i < iframes.length; i++) {
|
|
1418
|
+
var r = iframes[i].getBoundingClientRect();
|
|
1419
|
+
if (r.width > 10 && r.height > 10) {
|
|
1420
|
+
return JSON.stringify({ x: r.x, y: r.y });
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return JSON.stringify({ x: 0, y: 0 });
|
|
1424
|
+
})()
|
|
1425
|
+
`,
|
|
1426
|
+
returnByValue: true
|
|
1427
|
+
});
|
|
1428
|
+
const offset = JSON.parse(
|
|
1429
|
+
offsetResult.result?.value || '{"x":0,"y":0}'
|
|
1430
|
+
);
|
|
1431
|
+
const frameResult = await this.send("Runtime.evaluate", {
|
|
1432
|
+
expression: `
|
|
1433
|
+
(function() {
|
|
1434
|
+
var el = document.querySelector('[data-assistme-ref="${refId}"]');
|
|
1435
|
+
if (!el && ${roleJS} && ${nameJS}) {
|
|
1436
|
+
// Fallback: search by role
|
|
1437
|
+
var candidates = document.querySelectorAll('*');
|
|
1438
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
1439
|
+
var c = candidates[i];
|
|
1440
|
+
if (c.isContentEditable || c.getAttribute('contenteditable') === 'true') {
|
|
1441
|
+
el = c; break;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (!el) return 'null';
|
|
1446
|
+
|
|
1447
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1448
|
+
var r = el.getBoundingClientRect();
|
|
1449
|
+
if (r.width < 1 || r.height < 1) return JSON.stringify({ error: 'Zero size' });
|
|
1450
|
+
|
|
1451
|
+
return JSON.stringify({
|
|
1452
|
+
x: r.x + r.width / 2,
|
|
1453
|
+
y: r.y + r.height / 2,
|
|
1454
|
+
width: r.width,
|
|
1455
|
+
height: r.height
|
|
1456
|
+
});
|
|
1457
|
+
})()
|
|
1458
|
+
`,
|
|
1459
|
+
contextId,
|
|
1460
|
+
returnByValue: true
|
|
1461
|
+
});
|
|
1462
|
+
const value = frameResult.result?.value;
|
|
1463
|
+
if (!value || value === "null") return null;
|
|
1464
|
+
const parsed = JSON.parse(value);
|
|
1465
|
+
if (parsed.error) return parsed;
|
|
1466
|
+
return {
|
|
1467
|
+
x: parsed.x + offset.x,
|
|
1468
|
+
y: parsed.y + offset.y,
|
|
1469
|
+
width: parsed.width,
|
|
1470
|
+
height: parsed.height
|
|
1471
|
+
};
|
|
1121
1472
|
} catch {
|
|
1122
1473
|
return null;
|
|
1123
1474
|
}
|
|
@@ -1204,11 +1555,23 @@ Refs:
|
|
|
1204
1555
|
await new Promise((r) => setTimeout(r, 50));
|
|
1205
1556
|
await this.pressKey("Backspace");
|
|
1206
1557
|
await new Promise((r) => setTimeout(r, 50));
|
|
1207
|
-
const
|
|
1558
|
+
const frameContextId = this.frameContexts.get(refId);
|
|
1559
|
+
const clearEvalOpts = {
|
|
1208
1560
|
expression: `
|
|
1209
1561
|
(function() {
|
|
1210
1562
|
var el = document.querySelector('[data-assistme-ref="${refId}"]');
|
|
1211
1563
|
if (!el) return 'no_element';
|
|
1564
|
+
|
|
1565
|
+
// For contenteditable elements, check textContent instead of value
|
|
1566
|
+
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
|
1567
|
+
if (el.textContent && el.textContent.trim() !== '') {
|
|
1568
|
+
el.textContent = '';
|
|
1569
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1570
|
+
return 'js_cleared';
|
|
1571
|
+
}
|
|
1572
|
+
return 'ok';
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1212
1575
|
if (el.value !== undefined && el.value !== '') {
|
|
1213
1576
|
// Ctrl+A didn't work (some frameworks intercept it) \u2014 clear via JS
|
|
1214
1577
|
var setter = Object.getOwnPropertyDescriptor(
|
|
@@ -1226,9 +1589,13 @@ Refs:
|
|
|
1226
1589
|
})()
|
|
1227
1590
|
`,
|
|
1228
1591
|
returnByValue: true
|
|
1229
|
-
}
|
|
1592
|
+
};
|
|
1593
|
+
if (frameContextId) {
|
|
1594
|
+
clearEvalOpts.contextId = frameContextId;
|
|
1595
|
+
}
|
|
1596
|
+
const cleared = await this.send("Runtime.evaluate", clearEvalOpts);
|
|
1230
1597
|
const clearStatus = cleared.result?.value || "ok";
|
|
1231
|
-
if (clearStatus === "no_element") {
|
|
1598
|
+
if (clearStatus === "no_element" && !frameContextId) {
|
|
1232
1599
|
return {
|
|
1233
1600
|
success: false,
|
|
1234
1601
|
message: `Ref ${refLabel} not found after click. Take a new snapshot.`
|
|
@@ -1989,51 +2356,22 @@ import ora3 from "ora";
|
|
|
1989
2356
|
import { createInterface as createInterface2 } from "readline";
|
|
1990
2357
|
|
|
1991
2358
|
// src/agent/scheduler.ts
|
|
1992
|
-
|
|
2359
|
+
import { Cron } from "croner";
|
|
1993
2360
|
function getNextRunTime(cronExpr, timezone, fromDate) {
|
|
1994
2361
|
const now = fromDate || /* @__PURE__ */ new Date();
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
function parseField(expr, min, max) {
|
|
2001
|
-
const values = [];
|
|
2002
|
-
for (const part of expr.split(",")) {
|
|
2003
|
-
if (part === "*") {
|
|
2004
|
-
for (let i = min; i <= max; i++) values.push(i);
|
|
2005
|
-
} else if (part.startsWith("*/")) {
|
|
2006
|
-
const step = parseInt(part.slice(2));
|
|
2007
|
-
for (let i = min; i <= max; i += step) values.push(i);
|
|
2008
|
-
} else if (part.includes("-")) {
|
|
2009
|
-
const [start, end] = part.split("-").map(Number);
|
|
2010
|
-
for (let i = start; i <= end; i++) values.push(i);
|
|
2011
|
-
} else {
|
|
2012
|
-
values.push(parseInt(part));
|
|
2013
|
-
}
|
|
2362
|
+
try {
|
|
2363
|
+
const job = new Cron(cronExpr, { timezone: timezone || "UTC" });
|
|
2364
|
+
const next = job.nextRun(now);
|
|
2365
|
+
if (!next) {
|
|
2366
|
+
throw new Error(`No future run time found for cron expression: ${cronExpr}`);
|
|
2014
2367
|
}
|
|
2015
|
-
return
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
const useUTC = timezone === "UTC";
|
|
2023
|
-
const candidate = new Date(now.getTime() + 6e4);
|
|
2024
|
-
candidate.setSeconds(0, 0);
|
|
2025
|
-
for (let i = 0; i < 527040; i++) {
|
|
2026
|
-
const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
|
|
2027
|
-
const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
|
|
2028
|
-
const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
|
|
2029
|
-
const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
|
|
2030
|
-
const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
|
|
2031
|
-
if (minutes.includes(m) && hours.includes(h) && daysOfMonth.includes(dom) && months.includes(mon) && (dowExpr === "*" || daysOfWeek.includes(dow))) {
|
|
2032
|
-
return candidate;
|
|
2033
|
-
}
|
|
2034
|
-
candidate.setTime(candidate.getTime() + 6e4);
|
|
2035
|
-
}
|
|
2036
|
-
return new Date(now.getTime() + 864e5);
|
|
2368
|
+
return next;
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
if (err instanceof Error && err.message.includes("No future run time")) {
|
|
2371
|
+
throw err;
|
|
2372
|
+
}
|
|
2373
|
+
throw new Error(`Invalid cron expression "${cronExpr}": ${errorMessage(err)}`);
|
|
2374
|
+
}
|
|
2037
2375
|
}
|
|
2038
2376
|
var Scheduler = class {
|
|
2039
2377
|
timer = null;
|
|
@@ -2043,7 +2381,7 @@ var Scheduler = class {
|
|
|
2043
2381
|
this.onScheduledTask = onScheduledTask;
|
|
2044
2382
|
this.running = true;
|
|
2045
2383
|
await this.initializeNextRuns();
|
|
2046
|
-
this.timer = setInterval(() => this.checkDueTasks(),
|
|
2384
|
+
this.timer = setInterval(() => this.checkDueTasks(), SCHEDULER_INTERVAL_MS);
|
|
2047
2385
|
log.info("Scheduler started (checking every 30s)");
|
|
2048
2386
|
}
|
|
2049
2387
|
stop() {
|
|
@@ -2066,7 +2404,7 @@ var Scheduler = class {
|
|
|
2066
2404
|
}
|
|
2067
2405
|
}
|
|
2068
2406
|
} catch (err) {
|
|
2069
|
-
log.debug(`Scheduler init: ${err}`);
|
|
2407
|
+
log.debug(`Scheduler init: ${errorMessage(err)}`);
|
|
2070
2408
|
}
|
|
2071
2409
|
}
|
|
2072
2410
|
async checkDueTasks() {
|
|
@@ -2090,7 +2428,7 @@ var Scheduler = class {
|
|
|
2090
2428
|
last_error: null
|
|
2091
2429
|
});
|
|
2092
2430
|
} catch (err) {
|
|
2093
|
-
const errMsg =
|
|
2431
|
+
const errMsg = errorMessage(err);
|
|
2094
2432
|
await callMcpHandler("schedule.update", {
|
|
2095
2433
|
task_id: task.id,
|
|
2096
2434
|
last_error: errMsg
|
|
@@ -2098,7 +2436,7 @@ var Scheduler = class {
|
|
|
2098
2436
|
log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
|
|
2099
2437
|
}
|
|
2100
2438
|
} catch (err) {
|
|
2101
|
-
log.debug(`Scheduler check error: ${err}`);
|
|
2439
|
+
log.debug(`Scheduler check error: ${errorMessage(err)}`);
|
|
2102
2440
|
}
|
|
2103
2441
|
}
|
|
2104
2442
|
};
|
|
@@ -2618,7 +2956,7 @@ var SkillManager = class {
|
|
|
2618
2956
|
userId = null;
|
|
2619
2957
|
/** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
|
|
2620
2958
|
relevanceCache = /* @__PURE__ */ new Map();
|
|
2621
|
-
DESCRIPTION_BUDGET_CHARS =
|
|
2959
|
+
DESCRIPTION_BUDGET_CHARS = SKILL_DESCRIPTION_BUDGET_CHARS;
|
|
2622
2960
|
setUserId(userId) {
|
|
2623
2961
|
this.userId = userId;
|
|
2624
2962
|
}
|
|
@@ -2627,7 +2965,9 @@ var SkillManager = class {
|
|
|
2627
2965
|
try {
|
|
2628
2966
|
const data = await callMcpHandler("skill.load");
|
|
2629
2967
|
this.skills.clear();
|
|
2630
|
-
for (const
|
|
2968
|
+
for (const raw of data || []) {
|
|
2969
|
+
const row = safeParse(SkillRowSchema, raw);
|
|
2970
|
+
if (!row) continue;
|
|
2631
2971
|
const skill = this.rowToSkill(row);
|
|
2632
2972
|
this.skills.set(skill.name, skill);
|
|
2633
2973
|
}
|
|
@@ -2641,22 +2981,22 @@ var SkillManager = class {
|
|
|
2641
2981
|
}
|
|
2642
2982
|
rowToSkill(row) {
|
|
2643
2983
|
return {
|
|
2644
|
-
name: row.name,
|
|
2645
|
-
description: row.description
|
|
2646
|
-
version: row.version
|
|
2984
|
+
name: String(row.name),
|
|
2985
|
+
description: String(row.description ?? ""),
|
|
2986
|
+
version: String(row.version ?? "1.0.0"),
|
|
2647
2987
|
userInvocable: row.user_invocable !== false,
|
|
2648
2988
|
disableModelInvocation: row.disable_model_invocation === true,
|
|
2649
|
-
keywords: row.keywords
|
|
2650
|
-
allowedTools: row.allowed_tools
|
|
2651
|
-
argumentHint: row.argument_hint
|
|
2989
|
+
keywords: Array.isArray(row.keywords) ? row.keywords : [],
|
|
2990
|
+
allowedTools: Array.isArray(row.allowed_tools) ? row.allowed_tools : [],
|
|
2991
|
+
argumentHint: String(row.argument_hint ?? ""),
|
|
2652
2992
|
metadata: parseDbMetadata(row.metadata),
|
|
2653
|
-
homepage: row.homepage
|
|
2654
|
-
content: row.content,
|
|
2993
|
+
homepage: String(row.homepage ?? ""),
|
|
2994
|
+
content: String(row.content ?? ""),
|
|
2655
2995
|
filePath: "",
|
|
2656
2996
|
source: row.source || "manual",
|
|
2657
|
-
dbId: row.id,
|
|
2658
|
-
sourceSkillId: row.source_skill_id
|
|
2659
|
-
invocationCount: row.invocation_count
|
|
2997
|
+
dbId: row.id != null ? String(row.id) : void 0,
|
|
2998
|
+
sourceSkillId: row.source_skill_id != null ? String(row.source_skill_id) : void 0,
|
|
2999
|
+
invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
|
|
2660
3000
|
};
|
|
2661
3001
|
}
|
|
2662
3002
|
/** Invalidate caches when skills change (create, add, update, remove). */
|
|
@@ -2786,13 +3126,14 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2786
3126
|
metadata
|
|
2787
3127
|
}
|
|
2788
3128
|
);
|
|
2789
|
-
const
|
|
3129
|
+
const raw = Array.isArray(data) ? data[0] : data;
|
|
3130
|
+
const row = safeParse(SkillCreateResultSchema, raw);
|
|
2790
3131
|
if (!row) {
|
|
2791
|
-
log.debug(`Skill create returned
|
|
3132
|
+
log.debug(`Skill create returned invalid data for "${name}"`);
|
|
2792
3133
|
return null;
|
|
2793
3134
|
}
|
|
2794
3135
|
const id = row.out_id || row.id;
|
|
2795
|
-
const skillName = row.out_name || row.name;
|
|
3136
|
+
const skillName = row.out_name || row.name || name;
|
|
2796
3137
|
this.skills.set(skillName, {
|
|
2797
3138
|
name: skillName,
|
|
2798
3139
|
description,
|
|
@@ -2962,11 +3303,11 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2962
3303
|
});
|
|
2963
3304
|
if (data) {
|
|
2964
3305
|
return data.map((row) => ({
|
|
2965
|
-
name: row.name,
|
|
2966
|
-
description: row.description
|
|
2967
|
-
emoji: row.emoji
|
|
2968
|
-
source: row.source
|
|
2969
|
-
invocationCount: row.invocation_count
|
|
3306
|
+
name: String(row.name),
|
|
3307
|
+
description: String(row.description ?? ""),
|
|
3308
|
+
emoji: String(row.emoji ?? ""),
|
|
3309
|
+
source: String(row.source ?? "manual"),
|
|
3310
|
+
invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
|
|
2970
3311
|
}));
|
|
2971
3312
|
}
|
|
2972
3313
|
} catch {
|
|
@@ -3029,17 +3370,17 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
3029
3370
|
limit: options?.limit || 20,
|
|
3030
3371
|
offset: options?.offset || 0
|
|
3031
3372
|
});
|
|
3032
|
-
return (data || []).map((r) => ({
|
|
3373
|
+
return (data || []).map((r) => safeParse(BrowseSkillRowSchema, r)).filter(Boolean).map((r) => ({
|
|
3033
3374
|
id: r.id,
|
|
3034
3375
|
name: r.name,
|
|
3035
|
-
description: r.description
|
|
3036
|
-
emoji: r.emoji
|
|
3037
|
-
version: r.version
|
|
3038
|
-
authorName: r.author_name
|
|
3039
|
-
category: r.category
|
|
3040
|
-
installCount: r.install_count
|
|
3041
|
-
avgRating: r.avg_rating
|
|
3042
|
-
ratingCount: r.rating_count
|
|
3376
|
+
description: r.description,
|
|
3377
|
+
emoji: r.emoji,
|
|
3378
|
+
version: r.version,
|
|
3379
|
+
authorName: r.author_name,
|
|
3380
|
+
category: r.category,
|
|
3381
|
+
installCount: r.install_count,
|
|
3382
|
+
avgRating: r.avg_rating ?? null,
|
|
3383
|
+
ratingCount: r.rating_count
|
|
3043
3384
|
}));
|
|
3044
3385
|
} catch {
|
|
3045
3386
|
return [];
|
|
@@ -3064,8 +3405,12 @@ function substituteArguments(content, args) {
|
|
|
3064
3405
|
content = content.replace(/\$(\d+)(?!\w)/g, (_, i) => parts[parseInt(i)] || "");
|
|
3065
3406
|
return content;
|
|
3066
3407
|
}
|
|
3408
|
+
var SAFE_DYNAMIC_COMMANDS = /^(date|whoami|hostname|uname|pwd|echo|node\s+--version|npm\s+--version|git\s+(branch|rev-parse|log\s+--oneline)|cat\s+)/;
|
|
3067
3409
|
function preprocessDynamicContext(content, cwd) {
|
|
3068
3410
|
return content.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
3411
|
+
if (!SAFE_DYNAMIC_COMMANDS.test(cmd.trim())) {
|
|
3412
|
+
return `[command blocked: ${cmd}]`;
|
|
3413
|
+
}
|
|
3069
3414
|
try {
|
|
3070
3415
|
return execSync2(cmd, { timeout: 1e4, encoding: "utf-8", cwd }).trim();
|
|
3071
3416
|
} catch {
|
|
@@ -3140,7 +3485,9 @@ Respond with a JSON object now.`;
|
|
|
3140
3485
|
} else if (message.type === "result") {
|
|
3141
3486
|
const resultMsg = message;
|
|
3142
3487
|
if (resultMsg.subtype === "success" && "total_cost_usd" in resultMsg) {
|
|
3143
|
-
log.debug(
|
|
3488
|
+
log.debug(
|
|
3489
|
+
`Skill evaluation cost: $${resultMsg.total_cost_usd.toFixed(4)}`
|
|
3490
|
+
);
|
|
3144
3491
|
}
|
|
3145
3492
|
}
|
|
3146
3493
|
}
|
|
@@ -3149,13 +3496,9 @@ Respond with a JSON object now.`;
|
|
|
3149
3496
|
log.debug("Skill evaluation: no valid JSON in response");
|
|
3150
3497
|
return;
|
|
3151
3498
|
}
|
|
3152
|
-
if (!["create", "update", "skip"].includes(decision.action)) {
|
|
3153
|
-
log.debug("Skill evaluation: invalid action");
|
|
3154
|
-
return;
|
|
3155
|
-
}
|
|
3156
3499
|
await executeSkillDecision(decision, skillManager);
|
|
3157
3500
|
} catch (err) {
|
|
3158
|
-
log.debug(`Skill evaluation error: ${err}`);
|
|
3501
|
+
log.debug(`Skill evaluation error: ${errorMessage(err)}`);
|
|
3159
3502
|
}
|
|
3160
3503
|
}
|
|
3161
3504
|
async function executeSkillDecision(decision, skillManager) {
|
|
@@ -3165,13 +3508,17 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3165
3508
|
log.debug("Skill create skipped: missing name or instructions");
|
|
3166
3509
|
return;
|
|
3167
3510
|
}
|
|
3168
|
-
let skillName = decision.name;
|
|
3169
|
-
if (
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3511
|
+
let skillName = normalizeSkillName(decision.name);
|
|
3512
|
+
if (!skillName) {
|
|
3513
|
+
log.debug(`Skill create skipped: name "${decision.name}" cannot be normalized`);
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
const validationError = validateSkillName(skillName);
|
|
3517
|
+
if (validationError) {
|
|
3518
|
+
log.debug(`Skill create skipped: ${validationError}`);
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
if (skillName !== decision.name) {
|
|
3175
3522
|
log.debug(`Normalized skill name: "${decision.name}" \u2192 "${skillName}"`);
|
|
3176
3523
|
}
|
|
3177
3524
|
const existing = skillManager.findSimilar(skillName);
|
|
@@ -3230,25 +3577,28 @@ async function executeSkillDecision(decision, skillManager) {
|
|
|
3230
3577
|
}
|
|
3231
3578
|
function parseJsonResponse(text) {
|
|
3232
3579
|
const trimmed = text.trim();
|
|
3233
|
-
|
|
3234
|
-
const parsed = JSON.parse(trimmed);
|
|
3235
|
-
if (parsed.action) return parsed;
|
|
3236
|
-
} catch {
|
|
3237
|
-
}
|
|
3580
|
+
const candidates = [trimmed];
|
|
3238
3581
|
const start = trimmed.indexOf("{");
|
|
3239
|
-
if (start
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
} catch {
|
|
3248
|
-
return null;
|
|
3582
|
+
if (start !== -1) {
|
|
3583
|
+
let depth = 0;
|
|
3584
|
+
for (let i = start; i < trimmed.length; i++) {
|
|
3585
|
+
if (trimmed[i] === "{") depth++;
|
|
3586
|
+
else if (trimmed[i] === "}") depth--;
|
|
3587
|
+
if (depth === 0) {
|
|
3588
|
+
candidates.push(trimmed.slice(start, i + 1));
|
|
3589
|
+
break;
|
|
3249
3590
|
}
|
|
3250
3591
|
}
|
|
3251
3592
|
}
|
|
3593
|
+
for (const candidate of candidates) {
|
|
3594
|
+
try {
|
|
3595
|
+
const parsed = JSON.parse(candidate);
|
|
3596
|
+
const validated = safeParse(SkillDecisionSchema, parsed);
|
|
3597
|
+
if (validated) return validated;
|
|
3598
|
+
} catch {
|
|
3599
|
+
continue;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3252
3602
|
return null;
|
|
3253
3603
|
}
|
|
3254
3604
|
|
|
@@ -3295,14 +3645,16 @@ import { z } from "zod/v4";
|
|
|
3295
3645
|
|
|
3296
3646
|
// src/tools/filesystem.ts
|
|
3297
3647
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
3298
|
-
import { resolve, relative, join as join2 } from "path";
|
|
3648
|
+
import { resolve, relative, join as join2, sep } from "path";
|
|
3299
3649
|
import { glob } from "glob";
|
|
3300
3650
|
function assertWithinWorkspace(filePath) {
|
|
3301
3651
|
const config = getConfig();
|
|
3302
3652
|
const resolved = resolve(config.workspacePath, filePath);
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3653
|
+
const rel = relative(config.workspacePath, resolved);
|
|
3654
|
+
if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
|
|
3655
|
+
throw new AppError(
|
|
3656
|
+
`Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`,
|
|
3657
|
+
"PATH_TRAVERSAL"
|
|
3306
3658
|
);
|
|
3307
3659
|
}
|
|
3308
3660
|
return resolved;
|
|
@@ -3332,7 +3684,7 @@ async function searchFiles(pattern, directory) {
|
|
|
3332
3684
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
3333
3685
|
});
|
|
3334
3686
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
3335
|
-
return matches.slice(0,
|
|
3687
|
+
return matches.slice(0, MAX_FILE_SEARCH_RESULTS).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
|
|
3336
3688
|
}
|
|
3337
3689
|
async function listDirectory(path) {
|
|
3338
3690
|
const config = getConfig();
|
|
@@ -3342,9 +3694,7 @@ async function listDirectory(path) {
|
|
|
3342
3694
|
for (const entry of entries) {
|
|
3343
3695
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
3344
3696
|
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
3345
|
-
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
|
|
3346
|
-
(s) => ` (${formatSize(s.size)})`
|
|
3347
|
-
) : "";
|
|
3697
|
+
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then((s) => ` (${formatSize(s.size)})`) : "";
|
|
3348
3698
|
results.push(`${icon} ${entry.name}${info}`);
|
|
3349
3699
|
}
|
|
3350
3700
|
return results.join("\n") || "Empty directory.";
|
|
@@ -3362,9 +3712,14 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3362
3712
|
nodir: true,
|
|
3363
3713
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
3364
3714
|
});
|
|
3365
|
-
|
|
3715
|
+
let regex;
|
|
3716
|
+
try {
|
|
3717
|
+
regex = new RegExp(pattern, "gi");
|
|
3718
|
+
} catch {
|
|
3719
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
3720
|
+
}
|
|
3366
3721
|
const results = [];
|
|
3367
|
-
for (const file of files.slice(0,
|
|
3722
|
+
for (const file of files.slice(0, MAX_CONTENT_SEARCH_FILES)) {
|
|
3368
3723
|
try {
|
|
3369
3724
|
const content = await readFile(join2(cwd, file), "utf-8");
|
|
3370
3725
|
const lines = content.split("\n");
|
|
@@ -3373,10 +3728,10 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3373
3728
|
const relPath = relative(config.workspacePath, join2(cwd, file));
|
|
3374
3729
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
3375
3730
|
regex.lastIndex = 0;
|
|
3376
|
-
if (results.length >=
|
|
3731
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
3377
3732
|
}
|
|
3378
3733
|
}
|
|
3379
|
-
if (results.length >=
|
|
3734
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
3380
3735
|
} catch {
|
|
3381
3736
|
}
|
|
3382
3737
|
}
|
|
@@ -3385,8 +3740,6 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
3385
3740
|
|
|
3386
3741
|
// src/tools/shell.ts
|
|
3387
3742
|
import { exec } from "child_process";
|
|
3388
|
-
var TIMEOUT_MS = 3e4;
|
|
3389
|
-
var MAX_OUTPUT = 5e4;
|
|
3390
3743
|
var BLOCKED_PATTERNS = [
|
|
3391
3744
|
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
|
|
3392
3745
|
// rm -rf /, rm -fr /, etc.
|
|
@@ -3414,7 +3767,7 @@ function isBlocked(command) {
|
|
|
3414
3767
|
}
|
|
3415
3768
|
async function executeShell(command, cwd) {
|
|
3416
3769
|
if (isBlocked(command)) {
|
|
3417
|
-
throw new
|
|
3770
|
+
throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
|
|
3418
3771
|
}
|
|
3419
3772
|
const config = getConfig();
|
|
3420
3773
|
const workDir = cwd || config.workspacePath;
|
|
@@ -3423,7 +3776,7 @@ async function executeShell(command, cwd) {
|
|
|
3423
3776
|
command,
|
|
3424
3777
|
{
|
|
3425
3778
|
cwd: workDir,
|
|
3426
|
-
timeout:
|
|
3779
|
+
timeout: SHELL_TIMEOUT_MS,
|
|
3427
3780
|
maxBuffer: 1024 * 1024,
|
|
3428
3781
|
// 1MB buffer
|
|
3429
3782
|
env: { ...process.env, TERM: "dumb" }
|
|
@@ -3441,10 +3794,10 @@ ${stderr}` : "";
|
|
|
3441
3794
|
if (error && !stdout && !stderr) {
|
|
3442
3795
|
output = `Error: ${error.message}`;
|
|
3443
3796
|
}
|
|
3444
|
-
if (output.length >
|
|
3445
|
-
output = output.slice(0,
|
|
3797
|
+
if (output.length > SHELL_MAX_OUTPUT) {
|
|
3798
|
+
output = output.slice(0, SHELL_MAX_OUTPUT) + `
|
|
3446
3799
|
|
|
3447
|
-
[Output truncated at ${
|
|
3800
|
+
[Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
|
|
3448
3801
|
}
|
|
3449
3802
|
resolve2(output || "(no output)");
|
|
3450
3803
|
}
|
|
@@ -3811,7 +4164,7 @@ function createBrowserMcpServer() {
|
|
|
3811
4164
|
),
|
|
3812
4165
|
tool(
|
|
3813
4166
|
"browser_type",
|
|
3814
|
-
"Type text into an input field in the user's browser.",
|
|
4167
|
+
"Type text into an input field in the user's browser. If the CSS selector fails, automatically falls back to typing into the currently focused element. Works with contenteditable elements (rich text editors) and cross-origin iframes.",
|
|
3815
4168
|
{
|
|
3816
4169
|
selector: z.string().describe("CSS selector of the input element"),
|
|
3817
4170
|
text: z.string().describe("Text to type")
|
|
@@ -5104,7 +5457,7 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
5104
5457
|
const rawName = preInput.tool_name;
|
|
5105
5458
|
const displayName = stripMcpPrefix(rawName);
|
|
5106
5459
|
const toolInput = preInput.tool_input;
|
|
5107
|
-
log.tool(displayName, JSON.stringify(toolInput).slice(0,
|
|
5460
|
+
log.tool(displayName, JSON.stringify(toolInput).slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
5108
5461
|
await emitEvent(taskId, "tool_use_start", { name: displayName });
|
|
5109
5462
|
await emitEvent(taskId, "tool_use_input", { input: toolInput });
|
|
5110
5463
|
if (displayName === "browser_request_user_action") {
|
|
@@ -5123,15 +5476,15 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
5123
5476
|
const toolInput = postInput.tool_input;
|
|
5124
5477
|
const toolResponse = postInput.tool_response;
|
|
5125
5478
|
const resultStr = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
|
|
5126
|
-
log.result(resultStr.slice(0,
|
|
5479
|
+
log.result(resultStr.slice(0, MAX_TOOL_INPUT_LOG_LENGTH));
|
|
5127
5480
|
await emitEvent(taskId, "tool_result", {
|
|
5128
5481
|
name: displayName,
|
|
5129
|
-
result: resultStr.slice(0,
|
|
5482
|
+
result: resultStr.slice(0, MAX_TOOL_RESULT_LENGTH)
|
|
5130
5483
|
});
|
|
5131
5484
|
toolCallRecords.push({
|
|
5132
5485
|
name: displayName,
|
|
5133
5486
|
input: toolInput || {},
|
|
5134
|
-
result: resultStr.slice(0,
|
|
5487
|
+
result: resultStr.slice(0, MAX_SKILL_RECORD_RESULT_LENGTH)
|
|
5135
5488
|
});
|
|
5136
5489
|
return {};
|
|
5137
5490
|
};
|
|
@@ -5310,8 +5663,6 @@ var TaskTimeout = class {
|
|
|
5310
5663
|
}
|
|
5311
5664
|
}
|
|
5312
5665
|
};
|
|
5313
|
-
var MAX_HISTORY_ENTRIES = 10;
|
|
5314
|
-
var MAX_RESPONSE_LENGTH = 1500;
|
|
5315
5666
|
var TaskProcessor = class {
|
|
5316
5667
|
memoryManager = null;
|
|
5317
5668
|
skillManager;
|
|
@@ -5346,7 +5697,7 @@ var TaskProcessor = class {
|
|
|
5346
5697
|
async processTask(task) {
|
|
5347
5698
|
const config = getConfig();
|
|
5348
5699
|
resetEventSequence();
|
|
5349
|
-
const taskTimeoutMs =
|
|
5700
|
+
const taskTimeoutMs = config.taskTimeoutMinutes * 6e4;
|
|
5350
5701
|
newCorrelationId();
|
|
5351
5702
|
log.info(`Processing task ${task.id.slice(0, 8)}...`);
|
|
5352
5703
|
let finalResponse = "";
|
|
@@ -5385,7 +5736,7 @@ var TaskProcessor = class {
|
|
|
5385
5736
|
for (const entry of history) {
|
|
5386
5737
|
historyPrompt += `User: ${entry.prompt}
|
|
5387
5738
|
`;
|
|
5388
|
-
const truncated = entry.response.length >
|
|
5739
|
+
const truncated = entry.response.length > MAX_HISTORY_RESPONSE_LENGTH ? entry.response.slice(0, MAX_HISTORY_RESPONSE_LENGTH) + "\u2026" : entry.response;
|
|
5389
5740
|
historyPrompt += `Assistant: ${truncated}
|
|
5390
5741
|
|
|
5391
5742
|
`;
|
|
@@ -5464,16 +5815,11 @@ var TaskProcessor = class {
|
|
|
5464
5815
|
persistSession: true,
|
|
5465
5816
|
abortController
|
|
5466
5817
|
};
|
|
5467
|
-
const taskStartTime = Date.now();
|
|
5468
5818
|
try {
|
|
5469
5819
|
for await (const message of query2({
|
|
5470
5820
|
prompt: promptMessages(),
|
|
5471
5821
|
options
|
|
5472
5822
|
})) {
|
|
5473
|
-
if (Date.now() - taskStartTime > taskTimeoutMs) {
|
|
5474
|
-
finalResponse += "\n\n[Task timed out]";
|
|
5475
|
-
break;
|
|
5476
|
-
}
|
|
5477
5823
|
switch (message.type) {
|
|
5478
5824
|
case "assistant": {
|
|
5479
5825
|
const assistantMsg = message;
|
|
@@ -5485,7 +5831,8 @@ var TaskProcessor = class {
|
|
|
5485
5831
|
text: block.text
|
|
5486
5832
|
});
|
|
5487
5833
|
} else if (block.type === "thinking" && "thinking" in block) {
|
|
5488
|
-
const
|
|
5834
|
+
const thinkingBlock = block;
|
|
5835
|
+
const thinkingText = thinkingBlock.thinking;
|
|
5489
5836
|
log.debug(`Thinking: ${thinkingText.slice(0, 100)}...`);
|
|
5490
5837
|
await emitEvent(task.id, "thinking", {
|
|
5491
5838
|
text: thinkingText
|
|
@@ -5518,8 +5865,11 @@ var TaskProcessor = class {
|
|
|
5518
5865
|
break;
|
|
5519
5866
|
}
|
|
5520
5867
|
default:
|
|
5521
|
-
if (message.type === "system" && "subtype" in message
|
|
5522
|
-
|
|
5868
|
+
if (message.type === "system" && "subtype" in message) {
|
|
5869
|
+
const sysMsg = message;
|
|
5870
|
+
if (sysMsg.subtype === "init" && sysMsg.session_id) {
|
|
5871
|
+
agentSessionId = sysMsg.session_id;
|
|
5872
|
+
}
|
|
5523
5873
|
}
|
|
5524
5874
|
log.debug(`SDK message type: ${message.type}`);
|
|
5525
5875
|
break;
|
|
@@ -5528,10 +5878,9 @@ var TaskProcessor = class {
|
|
|
5528
5878
|
} finally {
|
|
5529
5879
|
taskTimeout.clear();
|
|
5530
5880
|
}
|
|
5531
|
-
const
|
|
5532
|
-
const truncatedResponse = finalResponse.length > MAX_CONTENT_LENGTH ? finalResponse.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
|
5881
|
+
const truncatedResponse = finalResponse.length > MAX_RESPONSE_CONTENT_LENGTH ? finalResponse.slice(0, MAX_RESPONSE_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
|
5533
5882
|
await withRetry(() => completeTask(task.id, truncatedResponse, tokenUsage), {
|
|
5534
|
-
maxRetries:
|
|
5883
|
+
maxRetries: MAX_COMPLETE_TASK_RETRIES,
|
|
5535
5884
|
baseDelayMs: 300,
|
|
5536
5885
|
label: "completeTask"
|
|
5537
5886
|
});
|
|
@@ -5549,7 +5898,7 @@ var TaskProcessor = class {
|
|
|
5549
5898
|
);
|
|
5550
5899
|
}
|
|
5551
5900
|
} catch (err) {
|
|
5552
|
-
const errorMsg =
|
|
5901
|
+
const errorMsg = errorMessage(err);
|
|
5553
5902
|
log.error(`Task failed: ${errorMsg}`);
|
|
5554
5903
|
await failTask(task.id, errorMsg);
|
|
5555
5904
|
await emitEvent(task.id, "error", { message: errorMsg });
|
|
@@ -6053,7 +6402,7 @@ function registerJobCommands(program2) {
|
|
|
6053
6402
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
6054
6403
|
try {
|
|
6055
6404
|
const userId = await getCurrentUserId();
|
|
6056
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6405
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-JT3JWZBV.js");
|
|
6057
6406
|
const runner = new JobRunner2();
|
|
6058
6407
|
const jobs = await runner.listJobs();
|
|
6059
6408
|
if (jobs.length === 0) {
|
|
@@ -6077,7 +6426,7 @@ function registerJobCommands(program2) {
|
|
|
6077
6426
|
jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
|
|
6078
6427
|
try {
|
|
6079
6428
|
const userId = await getCurrentUserId();
|
|
6080
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6429
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-JT3JWZBV.js");
|
|
6081
6430
|
const runner = new JobRunner2();
|
|
6082
6431
|
const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
|
|
6083
6432
|
if (runs.length === 0) {
|
|
@@ -6116,7 +6465,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
|
6116
6465
|
process.exit(1);
|
|
6117
6466
|
}
|
|
6118
6467
|
const userId = await getCurrentUserId();
|
|
6119
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
6468
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-JT3JWZBV.js");
|
|
6120
6469
|
const runner = new JobRunner2();
|
|
6121
6470
|
const job = await runner.loadJob(name);
|
|
6122
6471
|
if (!job) {
|