scalix 1.0.0 → 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/.env.example +29 -0
- package/README.md +1 -1
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +352 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +160 -35
- package/dist/constants.js +40 -6
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +109 -20
- package/dist/hooks.js +7 -1
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/project-index.js +2 -0
- package/dist/sandbox.js +135 -11
- package/dist/sessions.js +37 -2
- package/dist/settings.js +111 -6
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +7 -0
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +6 -2
- package/app.py +0 -55
package/dist/tools-browser.js
CHANGED
|
@@ -12,7 +12,6 @@ async function getPlaywright() {
|
|
|
12
12
|
if (playwrightAvailable === false)
|
|
13
13
|
return null;
|
|
14
14
|
try {
|
|
15
|
-
// @ts-expect-error playwright is an optional peer dependency
|
|
16
15
|
const pw = await import('playwright');
|
|
17
16
|
playwrightAvailable = true;
|
|
18
17
|
return pw;
|
|
@@ -95,20 +94,62 @@ export const browserNavigateTool = tool({
|
|
|
95
94
|
}
|
|
96
95
|
},
|
|
97
96
|
});
|
|
97
|
+
const ANNOTATION_SCRIPT = `(() => {
|
|
98
|
+
const selectors = 'a, button, input, select, textarea, [role="button"], [onclick], [tabindex]:not([tabindex="-1"])';
|
|
99
|
+
const elements = [...document.querySelectorAll(selectors)].filter(el => {
|
|
100
|
+
const rect = el.getBoundingClientRect();
|
|
101
|
+
const style = window.getComputedStyle(el);
|
|
102
|
+
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
|
103
|
+
});
|
|
104
|
+
const overlay = document.createElement('div');
|
|
105
|
+
overlay.id = '__scalix_annotations__';
|
|
106
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647';
|
|
107
|
+
const annotations = [];
|
|
108
|
+
elements.forEach((el, i) => {
|
|
109
|
+
const rect = el.getBoundingClientRect();
|
|
110
|
+
const idx = i + 1;
|
|
111
|
+
const box = document.createElement('div');
|
|
112
|
+
box.style.cssText = 'position:fixed;border:2px solid red;pointer-events:none;box-sizing:border-box;'
|
|
113
|
+
+ 'left:' + rect.left + 'px;top:' + rect.top + 'px;width:' + rect.width + 'px;height:' + rect.height + 'px';
|
|
114
|
+
const label = document.createElement('span');
|
|
115
|
+
label.textContent = String(idx);
|
|
116
|
+
label.style.cssText = 'position:absolute;top:-14px;left:-2px;background:red;color:white;font:bold 10px sans-serif;padding:1px 3px;border-radius:2px;line-height:12px';
|
|
117
|
+
box.appendChild(label);
|
|
118
|
+
overlay.appendChild(box);
|
|
119
|
+
annotations.push({
|
|
120
|
+
index: idx,
|
|
121
|
+
tag: el.tagName.toLowerCase(),
|
|
122
|
+
text: (el.textContent || el.getAttribute('placeholder') || el.getAttribute('aria-label') || '').trim().slice(0, 60),
|
|
123
|
+
x: Math.round(rect.left),
|
|
124
|
+
y: Math.round(rect.top),
|
|
125
|
+
width: Math.round(rect.width),
|
|
126
|
+
height: Math.round(rect.height),
|
|
127
|
+
centerX: Math.round(rect.left + rect.width / 2),
|
|
128
|
+
centerY: Math.round(rect.top + rect.height / 2),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
document.body.appendChild(overlay);
|
|
132
|
+
return annotations;
|
|
133
|
+
})()`;
|
|
98
134
|
export const browserScreenshotTool = tool({
|
|
99
135
|
name: 'browser_screenshot',
|
|
100
|
-
description: 'Take a screenshot of the current browser page. Returns the image as a base64 data URL for vision analysis.',
|
|
136
|
+
description: 'Take a screenshot of the current browser page. Returns the image as a base64 data URL for vision analysis. Use annotate=true to overlay numbered markers on all interactive elements with their coordinates.',
|
|
101
137
|
inputSchema: z.object({
|
|
102
138
|
fullPage: z.boolean().optional().describe('Capture full scrollable page (default: false, viewport only)'),
|
|
103
139
|
selector: z.string().max(500).optional().describe('CSS selector to screenshot a specific element'),
|
|
104
140
|
outputPath: z.string().max(500).optional().describe('Save screenshot to file (default: temporary)'),
|
|
141
|
+
annotate: z.boolean().optional().describe('Overlay numbered markers on interactive elements with coordinates (default: false)'),
|
|
105
142
|
}),
|
|
106
|
-
execute: async ({ fullPage, selector, outputPath }) => {
|
|
143
|
+
execute: async ({ fullPage, selector, outputPath, annotate }) => {
|
|
107
144
|
const session = await ensureBrowser();
|
|
108
145
|
if (!session) {
|
|
109
146
|
return { error: 'Browser not running. Use browser_navigate first.' };
|
|
110
147
|
}
|
|
111
148
|
try {
|
|
149
|
+
let annotations;
|
|
150
|
+
if (annotate) {
|
|
151
|
+
annotations = await session.page.evaluate(ANNOTATION_SCRIPT);
|
|
152
|
+
}
|
|
112
153
|
const outPath = outputPath
|
|
113
154
|
? sandboxPath(outputPath)
|
|
114
155
|
: path.join(process.cwd(), '.scalix', `browser-${Date.now()}.png`);
|
|
@@ -122,17 +163,31 @@ export const browserScreenshotTool = tool({
|
|
|
122
163
|
else {
|
|
123
164
|
await session.page.screenshot({ path: outPath, fullPage: fullPage ?? false });
|
|
124
165
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await
|
|
166
|
+
let dataUrl;
|
|
167
|
+
let size;
|
|
168
|
+
try {
|
|
169
|
+
dataUrl = await encodeImageToDataUrl(outPath);
|
|
170
|
+
size = (await fs.stat(outPath)).size;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (!outputPath) {
|
|
174
|
+
await fs.unlink(outPath).catch(() => { });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (annotate) {
|
|
178
|
+
await session.page.evaluate(() => {
|
|
179
|
+
const overlay = document.getElementById('__scalix_annotations__');
|
|
180
|
+
if (overlay)
|
|
181
|
+
overlay.remove();
|
|
182
|
+
});
|
|
129
183
|
}
|
|
130
184
|
return {
|
|
131
185
|
url: session.page.url(),
|
|
132
186
|
title: await session.page.title(),
|
|
133
187
|
path: outputPath || undefined,
|
|
134
|
-
size
|
|
188
|
+
size,
|
|
135
189
|
dataUrl,
|
|
190
|
+
annotations,
|
|
136
191
|
};
|
|
137
192
|
}
|
|
138
193
|
catch (error) {
|
|
@@ -142,35 +197,92 @@ export const browserScreenshotTool = tool({
|
|
|
142
197
|
});
|
|
143
198
|
export const browserClickTool = tool({
|
|
144
199
|
name: 'browser_click',
|
|
145
|
-
description: 'Click an element on the current browser page by CSS selector or
|
|
200
|
+
description: 'Click an element on the current browser page by CSS selector, text content, or pixel coordinates. Use coordinates after taking an annotated screenshot to click at precise positions.',
|
|
146
201
|
inputSchema: z.object({
|
|
147
202
|
selector: z.string().max(500).optional().describe('CSS selector to click'),
|
|
148
203
|
text: z.string().max(200).optional().describe('Click element containing this text'),
|
|
204
|
+
x: z.number().optional().describe('X pixel coordinate to click at'),
|
|
205
|
+
y: z.number().optional().describe('Y pixel coordinate to click at'),
|
|
149
206
|
button: z.enum(['left', 'right', 'middle']).optional().describe('Mouse button (default: left)'),
|
|
150
207
|
}),
|
|
151
|
-
execute: async ({ selector, text, button }) => {
|
|
208
|
+
execute: async ({ selector, text, x, y, button }) => {
|
|
152
209
|
const session = await ensureBrowser();
|
|
153
210
|
if (!session) {
|
|
154
211
|
return { error: 'Browser not running. Use browser_navigate first.' };
|
|
155
212
|
}
|
|
156
213
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
214
|
+
const btn = button || 'left';
|
|
215
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
216
|
+
await session.page.mouse.click(x, y, { button: btn });
|
|
217
|
+
}
|
|
218
|
+
else if (text) {
|
|
219
|
+
await session.page.getByText(text, { exact: false }).first().click({ button: btn, timeout: 10_000 });
|
|
159
220
|
}
|
|
160
221
|
else if (selector) {
|
|
161
|
-
await session.page.click(selector, { button:
|
|
222
|
+
await session.page.click(selector, { button: btn, timeout: 10_000 });
|
|
162
223
|
}
|
|
163
224
|
else {
|
|
164
|
-
return { error: 'Provide
|
|
225
|
+
return { error: 'Provide selector, text, or x/y coordinates to click' };
|
|
165
226
|
}
|
|
166
227
|
await session.page.waitForTimeout(500);
|
|
167
|
-
|
|
228
|
+
const target = typeof x === 'number' ? `coords:(${x},${y})` : selector || `text:"${text}"`;
|
|
229
|
+
return { clicked: target, url: session.page.url() };
|
|
168
230
|
}
|
|
169
231
|
catch (error) {
|
|
170
232
|
return { error: `Click failed: ${error.message}` };
|
|
171
233
|
}
|
|
172
234
|
},
|
|
173
235
|
});
|
|
236
|
+
export const browserMouseTool = tool({
|
|
237
|
+
name: 'browser_mouse',
|
|
238
|
+
description: 'Perform mouse operations at specific pixel coordinates. Use after taking a screenshot to identify target positions. Supports hover, double-click, drag, and scroll.',
|
|
239
|
+
inputSchema: z.object({
|
|
240
|
+
action: z.enum(['hover', 'doubleclick', 'drag', 'scroll']).describe('Mouse action to perform'),
|
|
241
|
+
x: z.number().describe('X pixel coordinate'),
|
|
242
|
+
y: z.number().describe('Y pixel coordinate'),
|
|
243
|
+
toX: z.number().optional().describe('Drag destination X coordinate'),
|
|
244
|
+
toY: z.number().optional().describe('Drag destination Y coordinate'),
|
|
245
|
+
scrollX: z.number().optional().describe('Horizontal scroll amount in pixels'),
|
|
246
|
+
scrollY: z.number().optional().describe('Vertical scroll amount in pixels'),
|
|
247
|
+
}),
|
|
248
|
+
execute: async ({ action, x, y, toX, toY, scrollX, scrollY }) => {
|
|
249
|
+
const session = await ensureBrowser();
|
|
250
|
+
if (!session) {
|
|
251
|
+
return { error: 'Browser not running. Use browser_navigate first.' };
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
switch (action) {
|
|
255
|
+
case 'hover':
|
|
256
|
+
await session.page.mouse.move(x, y);
|
|
257
|
+
break;
|
|
258
|
+
case 'doubleclick':
|
|
259
|
+
await session.page.mouse.dblclick(x, y);
|
|
260
|
+
await session.page.waitForTimeout(500);
|
|
261
|
+
break;
|
|
262
|
+
case 'drag': {
|
|
263
|
+
if (typeof toX !== 'number' || typeof toY !== 'number') {
|
|
264
|
+
return { error: 'Drag requires toX and toY coordinates' };
|
|
265
|
+
}
|
|
266
|
+
await session.page.mouse.move(x, y);
|
|
267
|
+
await session.page.mouse.down();
|
|
268
|
+
await session.page.mouse.move(toX, toY, { steps: 10 });
|
|
269
|
+
await session.page.mouse.up();
|
|
270
|
+
await session.page.waitForTimeout(500);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case 'scroll':
|
|
274
|
+
await session.page.mouse.move(x, y);
|
|
275
|
+
await session.page.mouse.wheel(scrollX ?? 0, scrollY ?? 0);
|
|
276
|
+
await session.page.waitForTimeout(300);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
return { action, x, y, url: session.page.url() };
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return { error: `Mouse ${action} failed: ${error.message}` };
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
});
|
|
174
286
|
export const browserTypeTool = tool({
|
|
175
287
|
name: 'browser_type',
|
|
176
288
|
description: 'Type text into an input field on the current browser page.',
|
|
@@ -253,21 +365,170 @@ export const browserGetTextTool = tool({
|
|
|
253
365
|
}
|
|
254
366
|
},
|
|
255
367
|
});
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
368
|
+
const SESSION_DIR = '.scalix/browser-sessions';
|
|
369
|
+
async function saveSessionState(name) {
|
|
370
|
+
if (!browserContext || !browserPage) {
|
|
371
|
+
throw new Error('No active browser session to save');
|
|
372
|
+
}
|
|
373
|
+
const storageState = await browserContext.storageState();
|
|
374
|
+
const url = browserPage.url();
|
|
375
|
+
const sessionDir = path.join(process.cwd(), SESSION_DIR);
|
|
376
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
377
|
+
const sessionPath = path.join(sessionDir, `${name}.json`);
|
|
378
|
+
let existing = null;
|
|
379
|
+
try {
|
|
380
|
+
existing = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
|
|
381
|
+
}
|
|
382
|
+
catch { }
|
|
383
|
+
const session = {
|
|
384
|
+
name,
|
|
385
|
+
url,
|
|
386
|
+
storageState,
|
|
387
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
388
|
+
updatedAt: Date.now(),
|
|
389
|
+
viewport: { width: 1280, height: 720 },
|
|
390
|
+
};
|
|
391
|
+
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
392
|
+
return session;
|
|
393
|
+
}
|
|
394
|
+
export const browserSessionSaveTool = tool({
|
|
395
|
+
name: 'browser_session_save',
|
|
396
|
+
description: 'Save the current browser session (cookies, localStorage, URL) to disk for later restoration.',
|
|
397
|
+
inputSchema: z.object({
|
|
398
|
+
name: z.string().max(100).describe('Session name (used to restore later)'),
|
|
399
|
+
}),
|
|
400
|
+
execute: async ({ name }) => {
|
|
401
|
+
try {
|
|
402
|
+
const session = await saveSessionState(name);
|
|
403
|
+
return { saved: name, url: session.url, updatedAt: session.updatedAt };
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
return { error: `Session save failed: ${error.message}` };
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
export const browserSessionRestoreTool = tool({
|
|
411
|
+
name: 'browser_session_restore',
|
|
412
|
+
description: 'Restore a previously saved browser session. Recreates the browser context with saved cookies/localStorage and navigates to the saved URL.',
|
|
413
|
+
inputSchema: z.object({
|
|
414
|
+
name: z.string().max(100).describe('Session name to restore'),
|
|
415
|
+
}),
|
|
416
|
+
execute: async ({ name }) => {
|
|
417
|
+
const pw = await getPlaywright();
|
|
418
|
+
if (!pw) {
|
|
419
|
+
return { error: 'Playwright not available. Install it: npm install playwright && npx playwright install chromium' };
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const sessionPath = path.join(process.cwd(), SESSION_DIR, `${name}.json`);
|
|
423
|
+
const raw = await fs.readFile(sessionPath, 'utf-8');
|
|
424
|
+
let session;
|
|
425
|
+
try {
|
|
426
|
+
session = JSON.parse(raw);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return { error: `Session restore failed: corrupt session file for "${name}"` };
|
|
430
|
+
}
|
|
431
|
+
if (!session.storageState || typeof session.storageState !== 'object' ||
|
|
432
|
+
!session.viewport || typeof session.viewport !== 'object' ||
|
|
433
|
+
typeof session.viewport.width !== 'number' || typeof session.viewport.height !== 'number') {
|
|
434
|
+
return { error: `Session restore failed: invalid session data for "${name}" (missing or malformed storageState/viewport)` };
|
|
435
|
+
}
|
|
436
|
+
if (browserContext) {
|
|
437
|
+
try {
|
|
438
|
+
await browserContext.close();
|
|
439
|
+
}
|
|
440
|
+
catch { }
|
|
441
|
+
browserContext = null;
|
|
442
|
+
browserPage = null;
|
|
443
|
+
}
|
|
444
|
+
if (!browserInstance) {
|
|
445
|
+
browserInstance = await pw.chromium.launch({ headless: true });
|
|
446
|
+
}
|
|
447
|
+
browserContext = await browserInstance.newContext({
|
|
448
|
+
viewport: session.viewport,
|
|
449
|
+
storageState: session.storageState,
|
|
450
|
+
});
|
|
451
|
+
browserPage = await browserContext.newPage();
|
|
452
|
+
if (session.url && session.url !== 'about:blank') {
|
|
453
|
+
await browserPage.goto(session.url, { waitUntil: 'load', timeout: 30_000 });
|
|
454
|
+
}
|
|
455
|
+
const title = await browserPage.title();
|
|
456
|
+
return {
|
|
457
|
+
restored: name,
|
|
458
|
+
url: browserPage.url(),
|
|
459
|
+
title,
|
|
460
|
+
savedAt: session.updatedAt,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return { error: `Session restore failed: ${error.message}` };
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
export const browserSessionListTool = tool({
|
|
469
|
+
name: 'browser_session_list',
|
|
470
|
+
description: 'List all saved browser sessions.',
|
|
259
471
|
inputSchema: z.object({}),
|
|
260
472
|
execute: async () => {
|
|
473
|
+
try {
|
|
474
|
+
const sessionDir = path.join(process.cwd(), SESSION_DIR);
|
|
475
|
+
let files;
|
|
476
|
+
try {
|
|
477
|
+
files = await fs.readdir(sessionDir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return { sessions: [] };
|
|
481
|
+
}
|
|
482
|
+
const sessions = [];
|
|
483
|
+
for (const file of files) {
|
|
484
|
+
if (!file.endsWith('.json'))
|
|
485
|
+
continue;
|
|
486
|
+
try {
|
|
487
|
+
const raw = await fs.readFile(path.join(sessionDir, file), 'utf-8');
|
|
488
|
+
const session = JSON.parse(raw);
|
|
489
|
+
sessions.push({
|
|
490
|
+
name: session.name,
|
|
491
|
+
url: session.url,
|
|
492
|
+
createdAt: session.createdAt,
|
|
493
|
+
updatedAt: session.updatedAt,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
catch { }
|
|
497
|
+
}
|
|
498
|
+
return { sessions };
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
return { error: `Session list failed: ${error.message}` };
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
export const browserCloseTool = tool({
|
|
506
|
+
name: 'browser_close',
|
|
507
|
+
description: 'Close the browser instance and free resources. Optionally save the session before closing.',
|
|
508
|
+
inputSchema: z.object({
|
|
509
|
+
saveSession: z.string().max(100).optional().describe('Save session state under this name before closing'),
|
|
510
|
+
}),
|
|
511
|
+
execute: async ({ saveSession } = {}) => {
|
|
512
|
+
try {
|
|
513
|
+
if (saveSession && browserContext && browserPage) {
|
|
514
|
+
await saveSessionState(saveSession);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch { }
|
|
261
518
|
await closeBrowser();
|
|
262
|
-
return { status: 'closed' };
|
|
519
|
+
return { status: 'closed', sessionSaved: saveSession || undefined };
|
|
263
520
|
},
|
|
264
521
|
});
|
|
265
522
|
export const browserTools = [
|
|
266
523
|
browserNavigateTool,
|
|
267
524
|
browserScreenshotTool,
|
|
268
525
|
browserClickTool,
|
|
526
|
+
browserMouseTool,
|
|
269
527
|
browserTypeTool,
|
|
270
528
|
browserEvalTool,
|
|
271
529
|
browserGetTextTool,
|
|
530
|
+
browserSessionSaveTool,
|
|
531
|
+
browserSessionRestoreTool,
|
|
532
|
+
browserSessionListTool,
|
|
272
533
|
browserCloseTool,
|
|
273
534
|
];
|
package/dist/tools-extra.js
CHANGED
|
@@ -172,6 +172,13 @@ export const searchFilesTool = tool({
|
|
|
172
172
|
try {
|
|
173
173
|
const flags = `${caseInsensitive ? 'i' : ''}${multiline ? 'ms' : ''}`;
|
|
174
174
|
regex = new RegExp(pattern, flags);
|
|
175
|
+
// Probe with a short string to catch catastrophic patterns early
|
|
176
|
+
const probe = 'a'.repeat(50);
|
|
177
|
+
const start = performance.now();
|
|
178
|
+
regex.test(probe);
|
|
179
|
+
if (performance.now() - start > 100) {
|
|
180
|
+
return { error: 'Regex pattern is too complex (possible catastrophic backtracking). Simplify the pattern.' };
|
|
181
|
+
}
|
|
175
182
|
}
|
|
176
183
|
catch (e) {
|
|
177
184
|
return { error: `Invalid regex pattern: ${e.message}` };
|
package/dist/tools-vision.js
CHANGED
|
@@ -2,7 +2,7 @@ import { tool } from './tool-helper.js';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { sandboxPath } from './sandbox.js';
|
|
5
|
+
import { sandboxPath, getMaxImageSize } from './sandbox.js';
|
|
6
6
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
|
|
7
7
|
const MIME_TYPES = {
|
|
8
8
|
'.png': 'image/png',
|
|
@@ -38,8 +38,9 @@ export const readImageTool = tool({
|
|
|
38
38
|
return { error: `Not a supported image format. Supported: ${[...IMAGE_EXTENSIONS].join(', ')}` };
|
|
39
39
|
}
|
|
40
40
|
const stat = await fs.stat(safePath);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const maxSize = getMaxImageSize();
|
|
42
|
+
if (stat.size > maxSize) {
|
|
43
|
+
return { error: `Image too large (${Math.round(stat.size / 1024 / 1024)}MB). Max ${Math.round(maxSize / 1024 / 1024)}MB.` };
|
|
43
44
|
}
|
|
44
45
|
const dataUrl = await encodeImageToDataUrl(safePath);
|
|
45
46
|
return {
|
package/dist/tools.js
CHANGED
|
@@ -134,7 +134,13 @@ export const runCommandTool = tool({
|
|
|
134
134
|
child.stderr?.on('error', () => { });
|
|
135
135
|
child.on('error', (err) => {
|
|
136
136
|
entry.exitCode = -1;
|
|
137
|
-
|
|
137
|
+
const errMsg = `\nProcess error: ${err.message}`;
|
|
138
|
+
if (entry.stderr.length + errMsg.length <= _limits.bgBufferMax) {
|
|
139
|
+
entry.stderr += errMsg;
|
|
140
|
+
}
|
|
141
|
+
else if (entry.stderr.length < _limits.bgBufferMax) {
|
|
142
|
+
entry.stderr += errMsg.slice(0, _limits.bgBufferMax - entry.stderr.length);
|
|
143
|
+
}
|
|
138
144
|
});
|
|
139
145
|
child.on('close', (code) => {
|
|
140
146
|
entry.exitCode = code ?? 1;
|
|
@@ -211,8 +217,12 @@ export const readFileTool = tool({
|
|
|
211
217
|
return { error: `File too large (${Math.round(stat.size / 1024)}KB, max ${Math.round(_limits.readFileMaxSize / 1024)}KB). Use read_file_lines to read specific sections.` };
|
|
212
218
|
const fd = await fs.open(safePath, 'r');
|
|
213
219
|
const header = Buffer.alloc(BINARY_DETECT_BUFFER_SIZE);
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
try {
|
|
221
|
+
await fd.read(header, 0, Math.min(BINARY_DETECT_BUFFER_SIZE, stat.size), 0);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await fd.close();
|
|
225
|
+
}
|
|
216
226
|
if (header.slice(0, Math.min(BINARY_DETECT_BUFFER_SIZE, stat.size)).includes(0x00)) {
|
|
217
227
|
return { error: 'File appears to be binary. Use a specialized tool to inspect binary files.' };
|
|
218
228
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { VERSION, UPDATE_DEFAULTS, USER_CONFIG_DIR } from './constants.js';
|
|
6
|
+
function semverGt(a, b) {
|
|
7
|
+
const pa = a.replace(/^v/, '').split('.').map(Number);
|
|
8
|
+
const pb = b.replace(/^v/, '').split('.').map(Number);
|
|
9
|
+
for (let i = 0; i < 3; i++) {
|
|
10
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
11
|
+
return true;
|
|
12
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function isValidSemver(v) {
|
|
18
|
+
return /^\d+\.\d+\.\d+/.test(v.replace(/^v/, ''));
|
|
19
|
+
}
|
|
20
|
+
function getCacheFile() {
|
|
21
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR, 'update-check.json');
|
|
22
|
+
}
|
|
23
|
+
function readCacheSync() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(getCacheFile(), 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (typeof parsed.lastCheck === 'number' && typeof parsed.latestVersion === 'string') {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async function writeCache(cache) {
|
|
35
|
+
const dir = path.dirname(getCacheFile());
|
|
36
|
+
await fs.mkdir(dir, { recursive: true });
|
|
37
|
+
await fs.writeFile(getCacheFile(), JSON.stringify(cache), { encoding: 'utf-8', mode: 0o600 });
|
|
38
|
+
}
|
|
39
|
+
export async function checkForUpdate() {
|
|
40
|
+
if (process.env.NO_UPDATE_NOTIFIER)
|
|
41
|
+
return null;
|
|
42
|
+
const cached = readCacheSync();
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (cached && (now - cached.lastCheck) < UPDATE_DEFAULTS.checkIntervalMs) {
|
|
45
|
+
return {
|
|
46
|
+
current: VERSION,
|
|
47
|
+
latest: cached.latestVersion,
|
|
48
|
+
updateAvailable: semverGt(cached.latestVersion, VERSION),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(UPDATE_DEFAULTS.registryUrl, {
|
|
53
|
+
signal: AbortSignal.timeout(UPDATE_DEFAULTS.fetchTimeoutMs),
|
|
54
|
+
headers: { 'Accept': 'application/json' },
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok)
|
|
57
|
+
return null;
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
const latest = data.version;
|
|
60
|
+
if (!latest || !isValidSemver(latest))
|
|
61
|
+
return null;
|
|
62
|
+
await writeCache({ lastCheck: now, latestVersion: latest }).catch(() => { });
|
|
63
|
+
return {
|
|
64
|
+
current: VERSION,
|
|
65
|
+
latest,
|
|
66
|
+
updateAvailable: semverGt(latest, VERSION),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function getUpdateBanner() {
|
|
74
|
+
if (process.env.NO_UPDATE_NOTIFIER)
|
|
75
|
+
return null;
|
|
76
|
+
const cached = readCacheSync();
|
|
77
|
+
if (!cached)
|
|
78
|
+
return null;
|
|
79
|
+
if (!semverGt(cached.latestVersion, VERSION))
|
|
80
|
+
return null;
|
|
81
|
+
const current = VERSION;
|
|
82
|
+
const latest = cached.latestVersion;
|
|
83
|
+
const line1 = ` Update available: ${current} → ${latest}`;
|
|
84
|
+
const line2 = ` Run /update or:`;
|
|
85
|
+
const line3 = ` npm install -g ${UPDATE_DEFAULTS.npmPackageName}@latest`;
|
|
86
|
+
const maxLen = Math.max(line1.length, line2.length, line3.length) + 2;
|
|
87
|
+
const pad = (s) => s + ' '.repeat(Math.max(0, maxLen - s.length));
|
|
88
|
+
return [
|
|
89
|
+
`╭${'─'.repeat(maxLen)}╮`,
|
|
90
|
+
`│${pad(line1)}│`,
|
|
91
|
+
`│${pad(line2)}│`,
|
|
92
|
+
`│${pad(line3)}│`,
|
|
93
|
+
`╰${'─'.repeat(maxLen)}╯`,
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
export function performUpdate() {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
99
|
+
execFile(npmCmd, ['install', '-g', `${UPDATE_DEFAULTS.npmPackageName}@latest`], {
|
|
100
|
+
timeout: 120_000,
|
|
101
|
+
env: { ...process.env },
|
|
102
|
+
}, (error, stdout, stderr) => {
|
|
103
|
+
if (error) {
|
|
104
|
+
resolve({ success: false, output: stderr || error.message });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
resolve({ success: true, output: stdout.trim() });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|