sparkecoder 0.1.85 → 0.1.87
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/agent/index.d.ts +1 -1
- package/dist/agent/index.js +666 -40
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +2001 -226
- package/dist/cli.js.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/{index-OhuTM4a0.d.ts → index-BvIissiB.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1688 -200
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +1688 -200
- package/dist/server/index.js.map +1 -1
- package/dist/skills/default/computer-use.md +150 -0
- package/dist/tools/index.d.ts +167 -1
- package/dist/tools/index.js +609 -11
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -1
- package/src/skills/default/computer-use.md +150 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ecd2bdca._.js → 2374f_317b1fef._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9adc1edb._.js → 2374f_37dd9702._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8dc0f9aa._.js → 2374f_4d44e4ed._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_cc6c6363._.js → 2374f_54ac917f._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_00f7fe07._.js → 2374f_86585101._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_369747ce._.js → 2374f_a383a4d9._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d58d0276._.js → 2374f_c59a35bb._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__25b25c9d._.js → [root-of-the-server]__9a826344._.js} +2 -2
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/package-lock.json +3 -3
- package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
- package/web/.next/standalone/web/src/lib/embed-bootstrap.ts +108 -0
- package/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/static/chunks/5383c5717758f575.js +0 -7
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -27,7 +27,12 @@ var init_types = __esm({
|
|
|
27
27
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
28
28
|
alwaysApply: z.boolean().optional().default(false),
|
|
29
29
|
// Glob patterns - auto-inject when working with matching files
|
|
30
|
-
globs: z.array(z.string()).optional().default([])
|
|
30
|
+
globs: z.array(z.string()).optional().default([]),
|
|
31
|
+
// Platform requirements — skill is hidden from the model on platforms
|
|
32
|
+
// not listed here. Values match `process.platform`
|
|
33
|
+
// (darwin, linux, win32, freebsd, ...). If omitted or empty, the skill is
|
|
34
|
+
// available on all platforms.
|
|
35
|
+
platforms: z.array(z.string()).optional().default([])
|
|
31
36
|
});
|
|
32
37
|
TaskConfigSchema = z.object({
|
|
33
38
|
enabled: z.boolean(),
|
|
@@ -45,7 +50,13 @@ var init_types = __esm({
|
|
|
45
50
|
approvalWebhook: z.string().url().optional(),
|
|
46
51
|
skillsDirectory: z.string().optional(),
|
|
47
52
|
maxContextChars: z.number().optional().default(2e5),
|
|
48
|
-
task: TaskConfigSchema.optional()
|
|
53
|
+
task: TaskConfigSchema.optional(),
|
|
54
|
+
// Anthropic computer use tool — opt-in. When true, the `computer` tool is
|
|
55
|
+
// included in the toolset for Anthropic models. Default false.
|
|
56
|
+
computerUseEnabled: z.boolean().optional(),
|
|
57
|
+
// Display dimensions for the computer use tool (defaults: 1280x800).
|
|
58
|
+
computerUseDisplayWidth: z.number().int().positive().optional(),
|
|
59
|
+
computerUseDisplayHeight: z.number().int().positive().optional()
|
|
49
60
|
});
|
|
50
61
|
VectorGatewayConfigSchema = z.object({
|
|
51
62
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -974,7 +985,11 @@ var init_remote = __esm({
|
|
|
974
985
|
return result.files;
|
|
975
986
|
},
|
|
976
987
|
async getDownloadUrl(fileId) {
|
|
977
|
-
|
|
988
|
+
const result = await storageApi(`/download/${fileId}`);
|
|
989
|
+
return {
|
|
990
|
+
downloadUrl: result.shortUrl || result.downloadUrl,
|
|
991
|
+
expiresAt: result.expiresAt
|
|
992
|
+
};
|
|
978
993
|
},
|
|
979
994
|
async deleteFile(fileId) {
|
|
980
995
|
await storageApi(`/files/${fileId}`, { method: "DELETE" });
|
|
@@ -1531,7 +1546,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1531
1546
|
globs: parsed.metadata.globs,
|
|
1532
1547
|
loadType,
|
|
1533
1548
|
priority,
|
|
1534
|
-
sourceDir: directory
|
|
1549
|
+
sourceDir: directory,
|
|
1550
|
+
platforms: parsed.metadata.platforms
|
|
1535
1551
|
});
|
|
1536
1552
|
} else {
|
|
1537
1553
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1544,11 +1560,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1544
1560
|
globs: [],
|
|
1545
1561
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1546
1562
|
priority,
|
|
1547
|
-
sourceDir: directory
|
|
1563
|
+
sourceDir: directory,
|
|
1564
|
+
platforms: []
|
|
1548
1565
|
});
|
|
1549
1566
|
}
|
|
1550
1567
|
}
|
|
1551
|
-
return skills
|
|
1568
|
+
return skills.filter(
|
|
1569
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1570
|
+
);
|
|
1552
1571
|
}
|
|
1553
1572
|
async function loadAllSkills(directories) {
|
|
1554
1573
|
const allSkills = [];
|
|
@@ -2145,6 +2164,7 @@ __export(webhook_exports, {
|
|
|
2145
2164
|
sendWebhook: () => sendWebhook
|
|
2146
2165
|
});
|
|
2147
2166
|
async function sendWebhook(url, event) {
|
|
2167
|
+
const t0 = Date.now();
|
|
2148
2168
|
try {
|
|
2149
2169
|
const controller = new AbortController();
|
|
2150
2170
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2158,17 +2178,36 @@ async function sendWebhook(url, event) {
|
|
|
2158
2178
|
signal: controller.signal
|
|
2159
2179
|
});
|
|
2160
2180
|
clearTimeout(timeout);
|
|
2181
|
+
const ms = Date.now() - t0;
|
|
2161
2182
|
if (!response.ok) {
|
|
2162
|
-
|
|
2183
|
+
const body = await response.text().catch(() => "");
|
|
2184
|
+
console.warn(
|
|
2185
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> HTTP ${response.status} in ${ms}ms${body ? ` (${body.slice(0, 200)})` : ""}`
|
|
2186
|
+
);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (!QUIET || TERMINAL_EVENTS.has(event.type)) {
|
|
2190
|
+
console.log(
|
|
2191
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> 200 in ${ms}ms`
|
|
2192
|
+
);
|
|
2163
2193
|
}
|
|
2164
2194
|
} catch (err) {
|
|
2165
2195
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2166
|
-
console.warn(
|
|
2196
|
+
console.warn(
|
|
2197
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
2198
|
+
);
|
|
2167
2199
|
}
|
|
2168
2200
|
}
|
|
2201
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2169
2202
|
var init_webhook = __esm({
|
|
2170
2203
|
"src/utils/webhook.ts"() {
|
|
2171
2204
|
"use strict";
|
|
2205
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set([
|
|
2206
|
+
"task.started",
|
|
2207
|
+
"task.completed",
|
|
2208
|
+
"task.failed"
|
|
2209
|
+
]);
|
|
2210
|
+
QUIET = process.env.SPARKECODER_QUIET_WEBHOOKS === "1";
|
|
2172
2211
|
}
|
|
2173
2212
|
});
|
|
2174
2213
|
|
|
@@ -2378,15 +2417,15 @@ var recorder_exports = {};
|
|
|
2378
2417
|
__export(recorder_exports, {
|
|
2379
2418
|
FrameRecorder: () => FrameRecorder
|
|
2380
2419
|
});
|
|
2381
|
-
import { exec as
|
|
2382
|
-
import { promisify as
|
|
2420
|
+
import { exec as exec6 } from "child_process";
|
|
2421
|
+
import { promisify as promisify6 } from "util";
|
|
2383
2422
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
2384
|
-
import { join as
|
|
2385
|
-
import { tmpdir } from "os";
|
|
2386
|
-
import { nanoid as
|
|
2423
|
+
import { join as join9 } from "path";
|
|
2424
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2425
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2387
2426
|
async function checkFfmpeg() {
|
|
2388
2427
|
try {
|
|
2389
|
-
await
|
|
2428
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
2390
2429
|
return true;
|
|
2391
2430
|
} catch {
|
|
2392
2431
|
return false;
|
|
@@ -2398,11 +2437,11 @@ async function cleanup(dir) {
|
|
|
2398
2437
|
} catch {
|
|
2399
2438
|
}
|
|
2400
2439
|
}
|
|
2401
|
-
var
|
|
2440
|
+
var execAsync6, FrameRecorder;
|
|
2402
2441
|
var init_recorder = __esm({
|
|
2403
2442
|
"src/browser/recorder.ts"() {
|
|
2404
2443
|
"use strict";
|
|
2405
|
-
|
|
2444
|
+
execAsync6 = promisify6(exec6);
|
|
2406
2445
|
FrameRecorder = class {
|
|
2407
2446
|
frames = [];
|
|
2408
2447
|
startTime = null;
|
|
@@ -2438,21 +2477,21 @@ var init_recorder = __esm({
|
|
|
2438
2477
|
*/
|
|
2439
2478
|
async encode() {
|
|
2440
2479
|
if (this.frames.length === 0) return null;
|
|
2441
|
-
const workDir =
|
|
2480
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid4(8)}`);
|
|
2442
2481
|
await mkdir4(workDir, { recursive: true });
|
|
2443
2482
|
try {
|
|
2444
2483
|
for (let i = 0; i < this.frames.length; i++) {
|
|
2445
|
-
const framePath =
|
|
2484
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
2446
2485
|
await writeFile5(framePath, this.frames[i].data);
|
|
2447
2486
|
}
|
|
2448
2487
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
2449
2488
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
2450
2489
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
2451
|
-
const outputPath =
|
|
2490
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
2452
2491
|
const hasFfmpeg = await checkFfmpeg();
|
|
2453
2492
|
if (hasFfmpeg) {
|
|
2454
|
-
await
|
|
2455
|
-
`ffmpeg -y -framerate ${clampedFps} -i "${
|
|
2493
|
+
await execAsync6(
|
|
2494
|
+
`ffmpeg -y -framerate ${clampedFps} -i "${join9(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
|
|
2456
2495
|
{ timeout: 12e4 }
|
|
2457
2496
|
);
|
|
2458
2497
|
} else {
|
|
@@ -2464,7 +2503,7 @@ var init_recorder = __esm({
|
|
|
2464
2503
|
const files = await readdir5(workDir);
|
|
2465
2504
|
for (const f of files) {
|
|
2466
2505
|
if (f.startsWith("frame_")) {
|
|
2467
|
-
await unlink2(
|
|
2506
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
2468
2507
|
});
|
|
2469
2508
|
}
|
|
2470
2509
|
}
|
|
@@ -2489,7 +2528,7 @@ var init_recorder = __esm({
|
|
|
2489
2528
|
import {
|
|
2490
2529
|
streamText as streamText2,
|
|
2491
2530
|
generateText as generateText3,
|
|
2492
|
-
tool as
|
|
2531
|
+
tool as tool14,
|
|
2493
2532
|
stepCountIs as stepCountIs2
|
|
2494
2533
|
} from "ai";
|
|
2495
2534
|
|
|
@@ -2680,8 +2719,8 @@ var SUBAGENT_MODELS = {
|
|
|
2680
2719
|
// src/agent/index.ts
|
|
2681
2720
|
init_db();
|
|
2682
2721
|
init_config();
|
|
2683
|
-
import { z as
|
|
2684
|
-
import { nanoid as
|
|
2722
|
+
import { z as z15 } from "zod";
|
|
2723
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2685
2724
|
|
|
2686
2725
|
// src/tools/bash.ts
|
|
2687
2726
|
import { tool } from "ai";
|
|
@@ -3765,12 +3804,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
3765
3804
|
}
|
|
3766
3805
|
async function commandExists(cmd) {
|
|
3767
3806
|
try {
|
|
3768
|
-
const { exec:
|
|
3769
|
-
const { promisify:
|
|
3770
|
-
const
|
|
3807
|
+
const { exec: exec8 } = await import("child_process");
|
|
3808
|
+
const { promisify: promisify8 } = await import("util");
|
|
3809
|
+
const execAsync8 = promisify8(exec8);
|
|
3771
3810
|
const isWindows = process.platform === "win32";
|
|
3772
3811
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
3773
|
-
await
|
|
3812
|
+
await execAsync8(checkCmd);
|
|
3774
3813
|
return true;
|
|
3775
3814
|
} catch {
|
|
3776
3815
|
return false;
|
|
@@ -6161,6 +6200,568 @@ function createUploadFileTool(options) {
|
|
|
6161
6200
|
});
|
|
6162
6201
|
}
|
|
6163
6202
|
|
|
6203
|
+
// src/tools/computer-use.ts
|
|
6204
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
6205
|
+
import { exec as exec5 } from "child_process";
|
|
6206
|
+
import { promisify as promisify5 } from "util";
|
|
6207
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync15, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6208
|
+
import { join as join8 } from "path";
|
|
6209
|
+
import { tmpdir } from "os";
|
|
6210
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
6211
|
+
var execAsync5 = promisify5(exec5);
|
|
6212
|
+
var DEFAULT_WIDTH = 1280;
|
|
6213
|
+
var DEFAULT_HEIGHT = 800;
|
|
6214
|
+
function isMacOs() {
|
|
6215
|
+
return process.platform === "darwin";
|
|
6216
|
+
}
|
|
6217
|
+
async function isCliclickInstalled() {
|
|
6218
|
+
try {
|
|
6219
|
+
await execAsync5("command -v cliclick", { timeout: 2e3 });
|
|
6220
|
+
return true;
|
|
6221
|
+
} catch {
|
|
6222
|
+
return false;
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
async function runJxa(script) {
|
|
6226
|
+
try {
|
|
6227
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
6228
|
+
const { stdout } = await execAsync5(`osascript -l JavaScript -e '${escaped}'`, {
|
|
6229
|
+
timeout: 5e3
|
|
6230
|
+
});
|
|
6231
|
+
return JSON.parse(stdout.trim());
|
|
6232
|
+
} catch {
|
|
6233
|
+
return null;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
async function hasAccessibilityPermissions() {
|
|
6237
|
+
try {
|
|
6238
|
+
const { stderr } = await execAsync5("cliclick p:.", { timeout: 3e3 });
|
|
6239
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6240
|
+
return { ok: false, error: stderr.trim().split("\n")[0] };
|
|
6241
|
+
}
|
|
6242
|
+
return { ok: true };
|
|
6243
|
+
} catch (err) {
|
|
6244
|
+
return { ok: false, error: err?.message || String(err) };
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
async function hasScreenRecordingPermissions() {
|
|
6248
|
+
const result = await runJxa(
|
|
6249
|
+
`ObjC.import("Cocoa");
|
|
6250
|
+
ObjC.import("CoreGraphics");
|
|
6251
|
+
ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]);
|
|
6252
|
+
JSON.stringify({ hasAccess: !!$.CGPreflightScreenCaptureAccess() });`
|
|
6253
|
+
);
|
|
6254
|
+
return result?.hasAccess ?? false;
|
|
6255
|
+
}
|
|
6256
|
+
async function requestAccessibilityPrompt() {
|
|
6257
|
+
const result = await runJxa(
|
|
6258
|
+
`ObjC.import("ApplicationServices");
|
|
6259
|
+
var key = $.kAXTrustedCheckOptionPrompt;
|
|
6260
|
+
var dict = $.NSDictionary.dictionaryWithObjectForKey($.kCFBooleanTrue, key);
|
|
6261
|
+
var trusted = $.AXIsProcessTrustedWithOptions(dict);
|
|
6262
|
+
JSON.stringify({ trusted: !!trusted });`
|
|
6263
|
+
);
|
|
6264
|
+
return result?.trusted ?? false;
|
|
6265
|
+
}
|
|
6266
|
+
async function requestScreenRecordingPrompt() {
|
|
6267
|
+
const result = await runJxa(
|
|
6268
|
+
`ObjC.import("Cocoa");
|
|
6269
|
+
ObjC.import("CoreGraphics");
|
|
6270
|
+
ObjC.bindFunction("CGRequestScreenCaptureAccess", ["bool", []]);
|
|
6271
|
+
JSON.stringify({ granted: !!$.CGRequestScreenCaptureAccess() });`
|
|
6272
|
+
);
|
|
6273
|
+
return result?.granted ?? false;
|
|
6274
|
+
}
|
|
6275
|
+
async function openSystemSettings(pane) {
|
|
6276
|
+
const url = pane === "accessibility" ? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" : "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6277
|
+
try {
|
|
6278
|
+
await execAsync5(`open '${url}'`, { timeout: 3e3 });
|
|
6279
|
+
} catch {
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
async function detectScreenSize() {
|
|
6283
|
+
try {
|
|
6284
|
+
const { stdout } = await execAsync5(
|
|
6285
|
+
`osascript -e 'tell application "Finder" to get bounds of window of desktop'`,
|
|
6286
|
+
{ timeout: 3e3 }
|
|
6287
|
+
);
|
|
6288
|
+
const parts = stdout.trim().split(",").map((s) => parseInt(s.trim(), 10));
|
|
6289
|
+
if (parts.length >= 4 && parts.every((n) => Number.isFinite(n))) {
|
|
6290
|
+
const [x1, y1, x2, y2] = parts;
|
|
6291
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
6292
|
+
}
|
|
6293
|
+
} catch {
|
|
6294
|
+
}
|
|
6295
|
+
return null;
|
|
6296
|
+
}
|
|
6297
|
+
async function runCliclick(args) {
|
|
6298
|
+
const quoted = args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
6299
|
+
const { stdout, stderr } = await execAsync5(`cliclick ${quoted}`, {
|
|
6300
|
+
timeout: 15e3,
|
|
6301
|
+
maxBuffer: 1024 * 1024
|
|
6302
|
+
});
|
|
6303
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6304
|
+
throw new Error(
|
|
6305
|
+
"Accessibility permissions not granted to cliclick. Open System Settings \u2192 Privacy & Security \u2192 Accessibility, add cliclick (or the agent runtime), and toggle it on."
|
|
6306
|
+
);
|
|
6307
|
+
}
|
|
6308
|
+
if (stderr && !stdout) throw new Error(stderr.trim());
|
|
6309
|
+
return (stdout || "").trim();
|
|
6310
|
+
}
|
|
6311
|
+
async function runScreencapture(path) {
|
|
6312
|
+
await execAsync5(`screencapture -x -t png '${path.replace(/'/g, `'\\''`)}'`, {
|
|
6313
|
+
timeout: 5e3
|
|
6314
|
+
});
|
|
6315
|
+
}
|
|
6316
|
+
async function resizeScreenshotToPoints(path, targetWidth, targetHeight) {
|
|
6317
|
+
const sharpModule = await import("sharp");
|
|
6318
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6319
|
+
const meta = await sharp2(path).metadata();
|
|
6320
|
+
if (meta.width === targetWidth && meta.height === targetHeight) {
|
|
6321
|
+
return readFileSync7(path);
|
|
6322
|
+
}
|
|
6323
|
+
return await sharp2(path).resize(targetWidth, targetHeight, { fit: "fill" }).png().toBuffer();
|
|
6324
|
+
}
|
|
6325
|
+
async function runScroll(dx, dy) {
|
|
6326
|
+
const wheelY = -Math.round(dy);
|
|
6327
|
+
const wheelX = -Math.round(dx);
|
|
6328
|
+
const script = `ObjC.import('CoreGraphics');var ev = $.CGEventCreateScrollWheelEvent(null, 0, 2, ${wheelY}, ${wheelX});$.CGEventPost(0, ev);`;
|
|
6329
|
+
await execAsync5(
|
|
6330
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6331
|
+
{ timeout: 5e3 }
|
|
6332
|
+
);
|
|
6333
|
+
}
|
|
6334
|
+
function translateKeyForCliclick(key) {
|
|
6335
|
+
if (!key) return [];
|
|
6336
|
+
const parts = key.split("+").map((p) => p.trim()).filter(Boolean);
|
|
6337
|
+
if (parts.length === 0) return [];
|
|
6338
|
+
const modMap = {
|
|
6339
|
+
ctrl: "ctrl",
|
|
6340
|
+
control: "ctrl",
|
|
6341
|
+
alt: "alt",
|
|
6342
|
+
option: "alt",
|
|
6343
|
+
shift: "shift",
|
|
6344
|
+
cmd: "cmd",
|
|
6345
|
+
super: "cmd",
|
|
6346
|
+
meta: "cmd",
|
|
6347
|
+
win: "cmd",
|
|
6348
|
+
fn: "fn"
|
|
6349
|
+
};
|
|
6350
|
+
const keyMap = {
|
|
6351
|
+
return: "enter",
|
|
6352
|
+
enter: "enter",
|
|
6353
|
+
esc: "esc",
|
|
6354
|
+
escape: "esc",
|
|
6355
|
+
backspace: "delete",
|
|
6356
|
+
back_space: "delete",
|
|
6357
|
+
delete: "fwd-delete",
|
|
6358
|
+
fwd_delete: "fwd-delete",
|
|
6359
|
+
forward_delete: "fwd-delete",
|
|
6360
|
+
tab: "tab",
|
|
6361
|
+
space: "space",
|
|
6362
|
+
up: "arrow-up",
|
|
6363
|
+
arrow_up: "arrow-up",
|
|
6364
|
+
down: "arrow-down",
|
|
6365
|
+
arrow_down: "arrow-down",
|
|
6366
|
+
left: "arrow-left",
|
|
6367
|
+
arrow_left: "arrow-left",
|
|
6368
|
+
right: "arrow-right",
|
|
6369
|
+
arrow_right: "arrow-right",
|
|
6370
|
+
page_up: "page-up",
|
|
6371
|
+
pageup: "page-up",
|
|
6372
|
+
page_down: "page-down",
|
|
6373
|
+
pagedown: "page-down",
|
|
6374
|
+
home: "home",
|
|
6375
|
+
end: "end",
|
|
6376
|
+
f1: "f1",
|
|
6377
|
+
f2: "f2",
|
|
6378
|
+
f3: "f3",
|
|
6379
|
+
f4: "f4",
|
|
6380
|
+
f5: "f5",
|
|
6381
|
+
f6: "f6",
|
|
6382
|
+
f7: "f7",
|
|
6383
|
+
f8: "f8",
|
|
6384
|
+
f9: "f9",
|
|
6385
|
+
f10: "f10",
|
|
6386
|
+
f11: "f11",
|
|
6387
|
+
f12: "f12"
|
|
6388
|
+
};
|
|
6389
|
+
const modifiers = [];
|
|
6390
|
+
let mainKey = null;
|
|
6391
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6392
|
+
const lower = parts[i].toLowerCase().replace(/-/g, "_");
|
|
6393
|
+
if (i < parts.length - 1 && modMap[lower]) {
|
|
6394
|
+
modifiers.push(modMap[lower]);
|
|
6395
|
+
} else {
|
|
6396
|
+
mainKey = keyMap[lower] || lower;
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
const args = [];
|
|
6400
|
+
if (modifiers.length > 0) args.push(`kd:${modifiers.join(",")}`);
|
|
6401
|
+
if (mainKey) {
|
|
6402
|
+
const isNamedKey = Object.values(keyMap).includes(mainKey) || /^f([1-9]|1[0-9]|20)$/.test(mainKey) || /^num-/.test(mainKey);
|
|
6403
|
+
if (isNamedKey) {
|
|
6404
|
+
args.push(`kp:${mainKey}`);
|
|
6405
|
+
} else {
|
|
6406
|
+
args.push(`t:${mainKey}`);
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
if (modifiers.length > 0) args.push(`ku:${modifiers.join(",")}`);
|
|
6410
|
+
return args;
|
|
6411
|
+
}
|
|
6412
|
+
function modifierStringToCliclick(text) {
|
|
6413
|
+
return text.split("+").map((p) => p.trim().toLowerCase()).map((p) => {
|
|
6414
|
+
if (p === "ctrl" || p === "control") return "ctrl";
|
|
6415
|
+
if (p === "alt" || p === "option") return "alt";
|
|
6416
|
+
if (p === "shift") return "shift";
|
|
6417
|
+
if (p === "super" || p === "meta" || p === "cmd") return "cmd";
|
|
6418
|
+
return "";
|
|
6419
|
+
}).filter(Boolean);
|
|
6420
|
+
}
|
|
6421
|
+
function createComputerUseTool(options) {
|
|
6422
|
+
const displayWidth = options.displayWidth ?? DEFAULT_WIDTH;
|
|
6423
|
+
const displayHeight = options.displayHeight ?? DEFAULT_HEIGHT;
|
|
6424
|
+
return anthropic.tools.computer_20251124({
|
|
6425
|
+
displayWidthPx: displayWidth,
|
|
6426
|
+
displayHeightPx: displayHeight,
|
|
6427
|
+
enableZoom: true,
|
|
6428
|
+
execute: async (input) => {
|
|
6429
|
+
try {
|
|
6430
|
+
switch (input.action) {
|
|
6431
|
+
case "screenshot": {
|
|
6432
|
+
const path = join8(tmpdir(), `cu-${nanoid3(8)}.png`);
|
|
6433
|
+
await runScreencapture(path);
|
|
6434
|
+
const resized = await resizeScreenshotToPoints(path, displayWidth, displayHeight);
|
|
6435
|
+
try {
|
|
6436
|
+
unlinkSync2(path);
|
|
6437
|
+
} catch {
|
|
6438
|
+
}
|
|
6439
|
+
return { type: "image", data: resized.toString("base64") };
|
|
6440
|
+
}
|
|
6441
|
+
case "left_click": {
|
|
6442
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6443
|
+
if (input.text) {
|
|
6444
|
+
const mods = modifierStringToCliclick(input.text);
|
|
6445
|
+
if (mods.length > 0) {
|
|
6446
|
+
await runCliclick([`kd:${mods.join(",")}`, `c:${x},${y}`, `ku:${mods.join(",")}`]);
|
|
6447
|
+
} else {
|
|
6448
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6449
|
+
}
|
|
6450
|
+
} else {
|
|
6451
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6452
|
+
}
|
|
6453
|
+
return `clicked at (${x}, ${y})${input.text ? ` with ${input.text}` : ""}`;
|
|
6454
|
+
}
|
|
6455
|
+
case "right_click": {
|
|
6456
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6457
|
+
await runCliclick([`rc:${x},${y}`]);
|
|
6458
|
+
return `right-clicked at (${x}, ${y})`;
|
|
6459
|
+
}
|
|
6460
|
+
case "middle_click": {
|
|
6461
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6462
|
+
const script = `ObjC.import('CoreGraphics');var loc = $.CGPointMake(${x}, ${y});var down = $.CGEventCreateMouseEvent(null, 25, loc, 2);var up = $.CGEventCreateMouseEvent(null, 26, loc, 2);$.CGEventPost(0, down); $.CGEventPost(0, up);`;
|
|
6463
|
+
await execAsync5(
|
|
6464
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6465
|
+
{ timeout: 3e3 }
|
|
6466
|
+
);
|
|
6467
|
+
return `middle-clicked at (${x}, ${y})`;
|
|
6468
|
+
}
|
|
6469
|
+
case "double_click": {
|
|
6470
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6471
|
+
await runCliclick([`dc:${x},${y}`]);
|
|
6472
|
+
return `double-clicked at (${x}, ${y})`;
|
|
6473
|
+
}
|
|
6474
|
+
case "triple_click": {
|
|
6475
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6476
|
+
await runCliclick([`tc:${x},${y}`]);
|
|
6477
|
+
return `triple-clicked at (${x}, ${y})`;
|
|
6478
|
+
}
|
|
6479
|
+
case "mouse_move": {
|
|
6480
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6481
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6482
|
+
return `moved cursor to (${x}, ${y})`;
|
|
6483
|
+
}
|
|
6484
|
+
case "left_mouse_down": {
|
|
6485
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6486
|
+
await runCliclick([`dd:${x},${y}`]);
|
|
6487
|
+
return `left mouse button pressed at (${x}, ${y})`;
|
|
6488
|
+
}
|
|
6489
|
+
case "left_mouse_up": {
|
|
6490
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6491
|
+
await runCliclick([`du:${x},${y}`]);
|
|
6492
|
+
return `left mouse button released at (${x}, ${y})`;
|
|
6493
|
+
}
|
|
6494
|
+
case "left_click_drag": {
|
|
6495
|
+
const [sx, sy] = input.start_coordinate ?? [0, 0];
|
|
6496
|
+
const [ex, ey] = input.coordinate ?? [0, 0];
|
|
6497
|
+
await runCliclick([`dd:${sx},${sy}`, `m:${ex},${ey}`, `du:${ex},${ey}`]);
|
|
6498
|
+
return `dragged from (${sx}, ${sy}) to (${ex}, ${ey})`;
|
|
6499
|
+
}
|
|
6500
|
+
case "type": {
|
|
6501
|
+
const text = input.text ?? "";
|
|
6502
|
+
await runCliclick([`t:${text}`]);
|
|
6503
|
+
return `typed ${text.length} character(s)`;
|
|
6504
|
+
}
|
|
6505
|
+
case "key": {
|
|
6506
|
+
const args = translateKeyForCliclick(input.text ?? "");
|
|
6507
|
+
if (args.length === 0) return "no key specified";
|
|
6508
|
+
await runCliclick(args);
|
|
6509
|
+
return `pressed ${input.text}`;
|
|
6510
|
+
}
|
|
6511
|
+
case "hold_key": {
|
|
6512
|
+
const text = (input.text ?? "").toLowerCase();
|
|
6513
|
+
const duration = input.duration ?? 1;
|
|
6514
|
+
const modMap = {
|
|
6515
|
+
ctrl: "ctrl",
|
|
6516
|
+
control: "ctrl",
|
|
6517
|
+
alt: "alt",
|
|
6518
|
+
option: "alt",
|
|
6519
|
+
shift: "shift",
|
|
6520
|
+
cmd: "cmd",
|
|
6521
|
+
super: "cmd",
|
|
6522
|
+
meta: "cmd",
|
|
6523
|
+
fn: "fn"
|
|
6524
|
+
};
|
|
6525
|
+
const cliName = modMap[text] || text;
|
|
6526
|
+
await runCliclick([`kd:${cliName}`]);
|
|
6527
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6528
|
+
await runCliclick([`ku:${cliName}`]);
|
|
6529
|
+
return `held ${text} for ${duration}s`;
|
|
6530
|
+
}
|
|
6531
|
+
case "scroll": {
|
|
6532
|
+
const direction = input.scroll_direction ?? "down";
|
|
6533
|
+
const amount = input.scroll_amount ?? 3;
|
|
6534
|
+
const px = amount * 100;
|
|
6535
|
+
const dx = direction === "left" ? -px : direction === "right" ? px : 0;
|
|
6536
|
+
const dy = direction === "up" ? -px : direction === "down" ? px : 0;
|
|
6537
|
+
if (input.coordinate) {
|
|
6538
|
+
const [x, y] = input.coordinate;
|
|
6539
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6540
|
+
}
|
|
6541
|
+
const mods = input.text ? modifierStringToCliclick(input.text) : [];
|
|
6542
|
+
if (mods.length > 0) {
|
|
6543
|
+
await runCliclick([`kd:${mods.join(",")}`]);
|
|
6544
|
+
}
|
|
6545
|
+
await runScroll(dx, dy);
|
|
6546
|
+
if (mods.length > 0) {
|
|
6547
|
+
await runCliclick([`ku:${mods.join(",")}`]);
|
|
6548
|
+
}
|
|
6549
|
+
return `scrolled ${direction} by ${amount}`;
|
|
6550
|
+
}
|
|
6551
|
+
case "wait": {
|
|
6552
|
+
const duration = input.duration ?? 1;
|
|
6553
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6554
|
+
return `waited ${duration}s`;
|
|
6555
|
+
}
|
|
6556
|
+
case "cursor_position": {
|
|
6557
|
+
const out = await runCliclick(["p:."]);
|
|
6558
|
+
return `cursor at ${out}`;
|
|
6559
|
+
}
|
|
6560
|
+
case "zoom": {
|
|
6561
|
+
const region = input.region ?? [0, 0, displayWidth, displayHeight];
|
|
6562
|
+
const [x1, y1, x2, y2] = region;
|
|
6563
|
+
const tmpPath = join8(tmpdir(), `cu-zoom-${nanoid3(8)}.png`);
|
|
6564
|
+
await runScreencapture(tmpPath);
|
|
6565
|
+
const sharpModule = await import("sharp");
|
|
6566
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6567
|
+
const meta = await sharp2(tmpPath).metadata();
|
|
6568
|
+
const scaleX = (meta.width || displayWidth) / displayWidth;
|
|
6569
|
+
const scaleY = (meta.height || displayHeight) / displayHeight;
|
|
6570
|
+
const px = {
|
|
6571
|
+
left: Math.max(0, Math.round(x1 * scaleX)),
|
|
6572
|
+
top: Math.max(0, Math.round(y1 * scaleY)),
|
|
6573
|
+
width: Math.max(1, Math.round((x2 - x1) * scaleX)),
|
|
6574
|
+
height: Math.max(1, Math.round((y2 - y1) * scaleY))
|
|
6575
|
+
};
|
|
6576
|
+
const buf = await sharp2(tmpPath).extract(px).png().toBuffer();
|
|
6577
|
+
try {
|
|
6578
|
+
unlinkSync2(tmpPath);
|
|
6579
|
+
} catch {
|
|
6580
|
+
}
|
|
6581
|
+
return { type: "image", data: buf.toString("base64") };
|
|
6582
|
+
}
|
|
6583
|
+
default: {
|
|
6584
|
+
const exhaustive = input.action;
|
|
6585
|
+
return `unsupported action: ${String(exhaustive)}`;
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
} catch (err) {
|
|
6589
|
+
const msg = err?.message || String(err);
|
|
6590
|
+
let hint = "";
|
|
6591
|
+
if (/accessibility|not authorized|tcc|operation not permitted/i.test(msg)) {
|
|
6592
|
+
hint = " (Hint: call enable_computer_use to (re-)check permissions and open System Settings)";
|
|
6593
|
+
} else if (/command not found/i.test(msg)) {
|
|
6594
|
+
hint = " (Hint: install cliclick with `brew install cliclick`)";
|
|
6595
|
+
}
|
|
6596
|
+
return `Error: ${msg}${hint}`;
|
|
6597
|
+
}
|
|
6598
|
+
},
|
|
6599
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6600
|
+
toModelOutput({ output }) {
|
|
6601
|
+
if (typeof output === "string") {
|
|
6602
|
+
return { type: "content", value: [{ type: "text", text: output }] };
|
|
6603
|
+
}
|
|
6604
|
+
return {
|
|
6605
|
+
type: "content",
|
|
6606
|
+
value: [{ type: "media", data: output.data, mediaType: "image/png" }]
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6612
|
+
// src/tools/enable-computer-use.ts
|
|
6613
|
+
init_db();
|
|
6614
|
+
import { tool as tool13 } from "ai";
|
|
6615
|
+
import { z as z14 } from "zod";
|
|
6616
|
+
var inputSchema = z14.object({
|
|
6617
|
+
display_width: z14.number().int().positive().optional().describe("Display width in pixels (defaults to detected primary display, fallback 1280)"),
|
|
6618
|
+
display_height: z14.number().int().positive().optional().describe("Display height in pixels (defaults to detected primary display, fallback 800)"),
|
|
6619
|
+
request_permissions: z14.boolean().optional().default(true).describe(
|
|
6620
|
+
"When true (default), proactively trigger macOS permission prompts and open System Settings panes for any missing permissions."
|
|
6621
|
+
)
|
|
6622
|
+
});
|
|
6623
|
+
var ACCESSIBILITY_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
6624
|
+
var SCREEN_RECORDING_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6625
|
+
function createEnableComputerUseTool(options) {
|
|
6626
|
+
return tool13({
|
|
6627
|
+
description: "Enable Anthropic's computer use beta tool for this session. macOS only. Drives the actual desktop (mouse, keyboard, screenshots) at pixel coordinates. Requires `cliclick` (brew install cliclick), Accessibility permissions, and Screen Recording permissions. When called, this tool will automatically request any missing permissions and open System Settings to the right pane. Only works on Anthropic Claude models. After this tool succeeds, you MUST stop the current turn and ask the user to send another message \u2014 the `computer` tool only becomes available on the NEXT message because the toolset is fixed for the current turn.",
|
|
6628
|
+
inputSchema,
|
|
6629
|
+
execute: async ({ display_width, display_height, request_permissions }) => {
|
|
6630
|
+
try {
|
|
6631
|
+
if (!isMacOs()) {
|
|
6632
|
+
return {
|
|
6633
|
+
success: false,
|
|
6634
|
+
error: "Computer use is currently only supported on macOS.",
|
|
6635
|
+
platform: process.platform
|
|
6636
|
+
};
|
|
6637
|
+
}
|
|
6638
|
+
if (!await isCliclickInstalled()) {
|
|
6639
|
+
return {
|
|
6640
|
+
success: false,
|
|
6641
|
+
error: "`cliclick` is not installed. It is required for mouse/keyboard control on macOS.",
|
|
6642
|
+
installCommand: "brew install cliclick",
|
|
6643
|
+
fixSteps: [
|
|
6644
|
+
"In a terminal on this Mac, run: brew install cliclick",
|
|
6645
|
+
"(If Homebrew is not installed, install it first from https://brew.sh)",
|
|
6646
|
+
"Then call enable_computer_use again"
|
|
6647
|
+
]
|
|
6648
|
+
};
|
|
6649
|
+
}
|
|
6650
|
+
const acc = await hasAccessibilityPermissions();
|
|
6651
|
+
const screen = await hasScreenRecordingPermissions();
|
|
6652
|
+
const missing = [];
|
|
6653
|
+
if (!acc.ok) {
|
|
6654
|
+
let prompted = false;
|
|
6655
|
+
let panelOpened = false;
|
|
6656
|
+
if (request_permissions) {
|
|
6657
|
+
prompted = await requestAccessibilityPrompt().then(() => true).catch(() => false);
|
|
6658
|
+
await openSystemSettings("accessibility").then(() => {
|
|
6659
|
+
panelOpened = true;
|
|
6660
|
+
}).catch(() => void 0);
|
|
6661
|
+
}
|
|
6662
|
+
missing.push({
|
|
6663
|
+
name: "Accessibility",
|
|
6664
|
+
reason: "cliclick failed: " + (acc.error?.split("\n")[0] || "no permission"),
|
|
6665
|
+
pane: "accessibility",
|
|
6666
|
+
settingsUrl: ACCESSIBILITY_URL,
|
|
6667
|
+
fixSteps: [
|
|
6668
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Accessibility pane that opened",
|
|
6669
|
+
"Click the + button",
|
|
6670
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6671
|
+
"Toggle the switch ON",
|
|
6672
|
+
"Restart the agent process so the new permission takes effect",
|
|
6673
|
+
"Then call enable_computer_use again"
|
|
6674
|
+
],
|
|
6675
|
+
prompted,
|
|
6676
|
+
panelOpened
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
6679
|
+
if (!screen) {
|
|
6680
|
+
let prompted = false;
|
|
6681
|
+
let panelOpened = false;
|
|
6682
|
+
if (request_permissions) {
|
|
6683
|
+
prompted = await requestScreenRecordingPrompt().then(() => true).catch(() => false);
|
|
6684
|
+
await openSystemSettings("screen-recording").then(() => {
|
|
6685
|
+
panelOpened = true;
|
|
6686
|
+
}).catch(() => void 0);
|
|
6687
|
+
}
|
|
6688
|
+
missing.push({
|
|
6689
|
+
name: "Screen Recording",
|
|
6690
|
+
reason: "CGPreflightScreenCaptureAccess returned false",
|
|
6691
|
+
pane: "screen-recording",
|
|
6692
|
+
settingsUrl: SCREEN_RECORDING_URL,
|
|
6693
|
+
fixSteps: [
|
|
6694
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Screen Recording pane that opened",
|
|
6695
|
+
"Click the + button",
|
|
6696
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6697
|
+
"Toggle the switch ON",
|
|
6698
|
+
"Restart the agent process so the new permission takes effect",
|
|
6699
|
+
"Then call enable_computer_use again"
|
|
6700
|
+
],
|
|
6701
|
+
prompted,
|
|
6702
|
+
panelOpened
|
|
6703
|
+
});
|
|
6704
|
+
}
|
|
6705
|
+
if (missing.length > 0) {
|
|
6706
|
+
return {
|
|
6707
|
+
success: false,
|
|
6708
|
+
error: `Missing permission${missing.length > 1 ? "s" : ""}: ` + missing.map((m) => m.name).join(" and ") + ".",
|
|
6709
|
+
missingPermissions: missing,
|
|
6710
|
+
note: request_permissions ? "System permission prompts have been triggered (best-effort) and System Settings has been opened to the relevant pane(s). After granting and restarting the agent, call enable_computer_use again." : "Re-run with request_permissions: true to auto-open System Settings."
|
|
6711
|
+
};
|
|
6712
|
+
}
|
|
6713
|
+
let width = display_width;
|
|
6714
|
+
let height = display_height;
|
|
6715
|
+
let detected = null;
|
|
6716
|
+
if (width === void 0 || height === void 0) {
|
|
6717
|
+
detected = await detectScreenSize();
|
|
6718
|
+
width = width ?? detected?.width ?? 1280;
|
|
6719
|
+
height = height ?? detected?.height ?? 800;
|
|
6720
|
+
}
|
|
6721
|
+
const session = await sessionQueries.getById(options.sessionId);
|
|
6722
|
+
if (!session) {
|
|
6723
|
+
return { success: false, error: "Session not found" };
|
|
6724
|
+
}
|
|
6725
|
+
const config = session.config || {};
|
|
6726
|
+
if (config.computerUseEnabled === true && config.computerUseDisplayWidth === width && config.computerUseDisplayHeight === height) {
|
|
6727
|
+
return {
|
|
6728
|
+
success: true,
|
|
6729
|
+
alreadyEnabled: true,
|
|
6730
|
+
message: "Computer use was already enabled for this session.",
|
|
6731
|
+
displayWidth: width,
|
|
6732
|
+
displayHeight: height
|
|
6733
|
+
};
|
|
6734
|
+
}
|
|
6735
|
+
const updated = {
|
|
6736
|
+
...config,
|
|
6737
|
+
computerUseEnabled: true,
|
|
6738
|
+
computerUseDisplayWidth: width,
|
|
6739
|
+
computerUseDisplayHeight: height
|
|
6740
|
+
};
|
|
6741
|
+
await sessionQueries.update(options.sessionId, { config: updated });
|
|
6742
|
+
return {
|
|
6743
|
+
success: true,
|
|
6744
|
+
enabled: true,
|
|
6745
|
+
platform: "darwin",
|
|
6746
|
+
displayWidth: width,
|
|
6747
|
+
displayHeight: height,
|
|
6748
|
+
detectedScreenSize: detected || void 0,
|
|
6749
|
+
permissions: {
|
|
6750
|
+
accessibility: "granted",
|
|
6751
|
+
screenRecording: "granted"
|
|
6752
|
+
},
|
|
6753
|
+
message: `Computer use is now enabled for this session. IMPORTANT: The \`computer\` tool is NOT yet available in this turn. Stop here, send a brief message to the user telling them computer use is enabled (display: ${width}x${height}), and ask them to send their next message to begin using it.`
|
|
6754
|
+
};
|
|
6755
|
+
} catch (err) {
|
|
6756
|
+
return {
|
|
6757
|
+
success: false,
|
|
6758
|
+
error: err?.message || String(err)
|
|
6759
|
+
};
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
});
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6164
6765
|
// src/tools/index.ts
|
|
6165
6766
|
init_semantic();
|
|
6166
6767
|
init_remote();
|
|
@@ -6209,6 +6810,20 @@ async function createTools(options) {
|
|
|
6209
6810
|
sessionId: options.sessionId
|
|
6210
6811
|
});
|
|
6211
6812
|
}
|
|
6813
|
+
if (process.platform === "darwin") {
|
|
6814
|
+
if (options.enableComputerUse) {
|
|
6815
|
+
tools.computer = createComputerUseTool({
|
|
6816
|
+
workingDirectory: options.workingDirectory,
|
|
6817
|
+
sessionId: options.sessionId,
|
|
6818
|
+
displayWidth: options.computerUseDisplayWidth,
|
|
6819
|
+
displayHeight: options.computerUseDisplayHeight
|
|
6820
|
+
});
|
|
6821
|
+
} else {
|
|
6822
|
+
tools.enable_computer_use = createEnableComputerUseTool({
|
|
6823
|
+
sessionId: options.sessionId
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6212
6827
|
if (options.enableSemanticSearch !== false) {
|
|
6213
6828
|
try {
|
|
6214
6829
|
if (isVectorGatewayConfigured()) {
|
|
@@ -6239,11 +6854,11 @@ init_db();
|
|
|
6239
6854
|
init_todo();
|
|
6240
6855
|
import os from "os";
|
|
6241
6856
|
function getSearchInstructions() {
|
|
6242
|
-
const
|
|
6857
|
+
const platform5 = process.platform;
|
|
6243
6858
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
6244
6859
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
6245
6860
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
6246
|
-
if (
|
|
6861
|
+
if (platform5 === "win32") {
|
|
6247
6862
|
return `${common}
|
|
6248
6863
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
6249
6864
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -6290,13 +6905,13 @@ async function buildSystemPrompt(options) {
|
|
|
6290
6905
|
);
|
|
6291
6906
|
const hasNoTodos = todos.length === 0;
|
|
6292
6907
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6293
|
-
const
|
|
6908
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6294
6909
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6295
6910
|
const searchInstructions = getSearchInstructions();
|
|
6296
6911
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
6297
6912
|
|
|
6298
6913
|
## Environment
|
|
6299
|
-
- **Platform**: ${
|
|
6914
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
6300
6915
|
- **Date**: ${currentDate}
|
|
6301
6916
|
- **Working Directory**: ${workingDirectory}
|
|
6302
6917
|
|
|
@@ -7234,10 +7849,14 @@ var Agent = class _Agent {
|
|
|
7234
7849
|
*/
|
|
7235
7850
|
async createToolsWithCallbacks(options) {
|
|
7236
7851
|
const config = getConfig();
|
|
7852
|
+
const sessionConfig = this.session.config || {};
|
|
7237
7853
|
return createTools({
|
|
7238
7854
|
sessionId: this.session.id,
|
|
7239
7855
|
workingDirectory: this.session.workingDirectory,
|
|
7240
7856
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7857
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7858
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7859
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
7241
7860
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
7242
7861
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
7243
7862
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -7270,10 +7889,14 @@ var Agent = class _Agent {
|
|
|
7270
7889
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
7271
7890
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
7272
7891
|
});
|
|
7892
|
+
const sessionConfig = session.config || {};
|
|
7273
7893
|
const tools = await createTools({
|
|
7274
7894
|
sessionId: session.id,
|
|
7275
7895
|
workingDirectory: session.workingDirectory,
|
|
7276
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
7896
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7897
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7898
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7899
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
7277
7900
|
});
|
|
7278
7901
|
return new _Agent(session, context, tools);
|
|
7279
7902
|
}
|
|
@@ -7446,10 +8069,10 @@ ${prompt}` });
|
|
|
7446
8069
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
7447
8070
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
7448
8071
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
7449
|
-
const fireWebhook = (
|
|
8072
|
+
const fireWebhook = (type2, data) => {
|
|
7450
8073
|
if (!webhookUrl) return;
|
|
7451
8074
|
sendWebhook(webhookUrl, {
|
|
7452
|
-
type,
|
|
8075
|
+
type: type2,
|
|
7453
8076
|
taskId: this.session.id,
|
|
7454
8077
|
sessionId: this.session.id,
|
|
7455
8078
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -7492,10 +8115,14 @@ ${prompt}` });
|
|
|
7492
8115
|
});
|
|
7493
8116
|
}
|
|
7494
8117
|
};
|
|
8118
|
+
const taskSessionConfig = this.session.config || {};
|
|
7495
8119
|
const taskTools = await createTools({
|
|
7496
8120
|
sessionId: this.session.id,
|
|
7497
8121
|
workingDirectory: this.session.workingDirectory,
|
|
7498
8122
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8123
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
8124
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
8125
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
7499
8126
|
onBashProgress: bashProgressHandler,
|
|
7500
8127
|
onWriteFileProgress: (progress) => {
|
|
7501
8128
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -7778,11 +8405,11 @@ ${taskAddendum}`;
|
|
|
7778
8405
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7779
8406
|
if (!isRemoteConfigured2()) return [];
|
|
7780
8407
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
7781
|
-
const { join:
|
|
8408
|
+
const { join: join14, basename: basename6 } = await import("path");
|
|
7782
8409
|
const urls = [];
|
|
7783
8410
|
for (const filePath of filePaths) {
|
|
7784
8411
|
try {
|
|
7785
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
8412
|
+
const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
|
|
7786
8413
|
const fileName = basename6(fullPath);
|
|
7787
8414
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
7788
8415
|
const mimeMap = {
|
|
@@ -7840,11 +8467,11 @@ ${taskAddendum}`;
|
|
|
7840
8467
|
wrappedTools[name] = originalTool;
|
|
7841
8468
|
continue;
|
|
7842
8469
|
}
|
|
7843
|
-
wrappedTools[name] =
|
|
8470
|
+
wrappedTools[name] = tool14({
|
|
7844
8471
|
description: originalTool.description || "",
|
|
7845
|
-
inputSchema: originalTool.inputSchema ||
|
|
8472
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
7846
8473
|
execute: async (input, toolOptions) => {
|
|
7847
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
8474
|
+
const toolCallId = toolOptions.toolCallId || nanoid5();
|
|
7848
8475
|
const execution = toolExecutionQueries.create({
|
|
7849
8476
|
sessionId: this.session.id,
|
|
7850
8477
|
toolName: name,
|
|
@@ -7862,10 +8489,10 @@ ${taskAddendum}`;
|
|
|
7862
8489
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
7863
8490
|
approvalResolvers.delete(toolCallId);
|
|
7864
8491
|
this.pendingApprovals.delete(toolCallId);
|
|
7865
|
-
const
|
|
8492
|
+
const exec8 = await execution;
|
|
7866
8493
|
if (!approved) {
|
|
7867
8494
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
7868
|
-
await toolExecutionQueries.reject(
|
|
8495
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
7869
8496
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7870
8497
|
return {
|
|
7871
8498
|
status: "rejected",
|
|
@@ -7875,14 +8502,14 @@ ${taskAddendum}`;
|
|
|
7875
8502
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
7876
8503
|
};
|
|
7877
8504
|
}
|
|
7878
|
-
await toolExecutionQueries.approve(
|
|
8505
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
7879
8506
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7880
8507
|
try {
|
|
7881
8508
|
const result = await originalTool.execute(input, toolOptions);
|
|
7882
|
-
await toolExecutionQueries.complete(
|
|
8509
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
7883
8510
|
return result;
|
|
7884
8511
|
} catch (error) {
|
|
7885
|
-
await toolExecutionQueries.complete(
|
|
8512
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
7886
8513
|
throw error;
|
|
7887
8514
|
}
|
|
7888
8515
|
}
|
|
@@ -7953,12 +8580,12 @@ ${taskAddendum}`;
|
|
|
7953
8580
|
|
|
7954
8581
|
// src/server/index.ts
|
|
7955
8582
|
import "dotenv/config";
|
|
7956
|
-
import { Hono as
|
|
8583
|
+
import { Hono as Hono7 } from "hono";
|
|
7957
8584
|
import { serve } from "@hono/node-server";
|
|
7958
8585
|
import { cors } from "hono/cors";
|
|
7959
8586
|
import { logger } from "hono/logger";
|
|
7960
|
-
import { existsSync as
|
|
7961
|
-
import { resolve as resolve10, dirname as dirname7, join as
|
|
8587
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
8588
|
+
import { resolve as resolve10, dirname as dirname7, join as join13 } from "path";
|
|
7962
8589
|
import { spawn as spawn2 } from "child_process";
|
|
7963
8590
|
import { createServer as createNetServer } from "net";
|
|
7964
8591
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -7967,11 +8594,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
7967
8594
|
init_db();
|
|
7968
8595
|
import { Hono } from "hono";
|
|
7969
8596
|
import { zValidator } from "@hono/zod-validator";
|
|
7970
|
-
import { z as
|
|
7971
|
-
import { existsSync as
|
|
8597
|
+
import { z as z16 } from "zod";
|
|
8598
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
7972
8599
|
import { readdir as readdir6 } from "fs/promises";
|
|
7973
|
-
import { join as
|
|
7974
|
-
import { nanoid as
|
|
8600
|
+
import { join as join10, basename as basename5, extname as extname8, relative as relative9 } from "path";
|
|
8601
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
7975
8602
|
init_config();
|
|
7976
8603
|
|
|
7977
8604
|
// src/server/devtools-store.ts
|
|
@@ -8003,18 +8630,20 @@ function cleanupPendingInputs() {
|
|
|
8003
8630
|
}
|
|
8004
8631
|
}
|
|
8005
8632
|
}
|
|
8006
|
-
var createSessionSchema =
|
|
8007
|
-
name:
|
|
8008
|
-
workingDirectory:
|
|
8009
|
-
model:
|
|
8010
|
-
toolApprovals:
|
|
8633
|
+
var createSessionSchema = z16.object({
|
|
8634
|
+
name: z16.string().optional(),
|
|
8635
|
+
workingDirectory: z16.string().optional(),
|
|
8636
|
+
model: z16.string().optional(),
|
|
8637
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional(),
|
|
8638
|
+
// Optional full session-config passthrough (computerUseEnabled, etc.)
|
|
8639
|
+
config: z16.record(z16.string(), z16.unknown()).optional()
|
|
8011
8640
|
});
|
|
8012
|
-
var paginationQuerySchema =
|
|
8013
|
-
limit:
|
|
8014
|
-
offset:
|
|
8641
|
+
var paginationQuerySchema = z16.object({
|
|
8642
|
+
limit: z16.string().optional(),
|
|
8643
|
+
offset: z16.string().optional()
|
|
8015
8644
|
});
|
|
8016
|
-
var messagesQuerySchema =
|
|
8017
|
-
limit:
|
|
8645
|
+
var messagesQuerySchema = z16.object({
|
|
8646
|
+
limit: z16.string().optional()
|
|
8018
8647
|
});
|
|
8019
8648
|
sessions.get(
|
|
8020
8649
|
"/",
|
|
@@ -8052,11 +8681,20 @@ sessions.post(
|
|
|
8052
8681
|
async (c) => {
|
|
8053
8682
|
const body = c.req.valid("json");
|
|
8054
8683
|
const config = getConfig();
|
|
8684
|
+
const cuDefault = process.env.SPARKECODER_COMPUTER_USE === "1";
|
|
8685
|
+
const baseConfig = body.config || {};
|
|
8686
|
+
const mergedConfig = {
|
|
8687
|
+
...baseConfig,
|
|
8688
|
+
...body.toolApprovals ? { toolApprovals: body.toolApprovals } : {},
|
|
8689
|
+
// Turn on computer use by default if the server was launched with --enable-computer-use,
|
|
8690
|
+
// unless the client explicitly provided a value.
|
|
8691
|
+
...cuDefault && baseConfig.computerUseEnabled === void 0 ? { computerUseEnabled: true } : {}
|
|
8692
|
+
};
|
|
8055
8693
|
const agent = await Agent.create({
|
|
8056
8694
|
name: body.name,
|
|
8057
8695
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8058
8696
|
model: body.model || config.defaultModel,
|
|
8059
|
-
sessionConfig:
|
|
8697
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8060
8698
|
});
|
|
8061
8699
|
const session = agent.getSession();
|
|
8062
8700
|
return c.json({
|
|
@@ -8153,10 +8791,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8153
8791
|
count: executions.length
|
|
8154
8792
|
});
|
|
8155
8793
|
});
|
|
8156
|
-
var updateSessionSchema =
|
|
8157
|
-
model:
|
|
8158
|
-
name:
|
|
8159
|
-
toolApprovals:
|
|
8794
|
+
var updateSessionSchema = z16.object({
|
|
8795
|
+
model: z16.string().optional(),
|
|
8796
|
+
name: z16.string().optional(),
|
|
8797
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
|
|
8160
8798
|
});
|
|
8161
8799
|
sessions.patch(
|
|
8162
8800
|
"/:id",
|
|
@@ -8226,8 +8864,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8226
8864
|
await agent.clearContext();
|
|
8227
8865
|
return c.json({ success: true, sessionId: id });
|
|
8228
8866
|
});
|
|
8229
|
-
var pendingInputSchema =
|
|
8230
|
-
text:
|
|
8867
|
+
var pendingInputSchema = z16.object({
|
|
8868
|
+
text: z16.string()
|
|
8231
8869
|
});
|
|
8232
8870
|
sessions.post(
|
|
8233
8871
|
"/:id/pending-input",
|
|
@@ -8258,13 +8896,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
8258
8896
|
createdAt: pending.createdAt.toISOString()
|
|
8259
8897
|
});
|
|
8260
8898
|
});
|
|
8261
|
-
var devtoolsContextSchema =
|
|
8262
|
-
url:
|
|
8263
|
-
path:
|
|
8264
|
-
pageName:
|
|
8265
|
-
screenWidth:
|
|
8266
|
-
screenHeight:
|
|
8267
|
-
devicePixelRatio:
|
|
8899
|
+
var devtoolsContextSchema = z16.object({
|
|
8900
|
+
url: z16.string(),
|
|
8901
|
+
path: z16.string(),
|
|
8902
|
+
pageName: z16.string().optional(),
|
|
8903
|
+
screenWidth: z16.number().optional(),
|
|
8904
|
+
screenHeight: z16.number().optional(),
|
|
8905
|
+
devicePixelRatio: z16.number().optional()
|
|
8268
8906
|
});
|
|
8269
8907
|
sessions.post(
|
|
8270
8908
|
"/:id/devtools-context",
|
|
@@ -8450,12 +9088,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
8450
9088
|
});
|
|
8451
9089
|
function getAttachmentsDir(sessionId) {
|
|
8452
9090
|
const appDataDir = getAppDataDirectory();
|
|
8453
|
-
return
|
|
9091
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
8454
9092
|
}
|
|
8455
9093
|
function ensureAttachmentsDir(sessionId) {
|
|
8456
9094
|
const dir = getAttachmentsDir(sessionId);
|
|
8457
|
-
if (!
|
|
8458
|
-
|
|
9095
|
+
if (!existsSync16(dir)) {
|
|
9096
|
+
mkdirSync6(dir, { recursive: true });
|
|
8459
9097
|
}
|
|
8460
9098
|
return dir;
|
|
8461
9099
|
}
|
|
@@ -8466,12 +9104,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
8466
9104
|
return c.json({ error: "Session not found" }, 404);
|
|
8467
9105
|
}
|
|
8468
9106
|
const dir = getAttachmentsDir(sessionId);
|
|
8469
|
-
if (!
|
|
9107
|
+
if (!existsSync16(dir)) {
|
|
8470
9108
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
8471
9109
|
}
|
|
8472
9110
|
const files = readdirSync2(dir);
|
|
8473
9111
|
const attachments = files.map((filename) => {
|
|
8474
|
-
const filePath =
|
|
9112
|
+
const filePath = join10(dir, filename);
|
|
8475
9113
|
const stats = statSync2(filePath);
|
|
8476
9114
|
return {
|
|
8477
9115
|
id: filename.split("_")[0],
|
|
@@ -8503,10 +9141,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8503
9141
|
return c.json({ error: "No file provided" }, 400);
|
|
8504
9142
|
}
|
|
8505
9143
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8506
|
-
const id =
|
|
9144
|
+
const id = nanoid6(10);
|
|
8507
9145
|
const ext = extname8(file.name) || "";
|
|
8508
9146
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8509
|
-
const filePath =
|
|
9147
|
+
const filePath = join10(dir, safeFilename);
|
|
8510
9148
|
const arrayBuffer = await file.arrayBuffer();
|
|
8511
9149
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
8512
9150
|
return c.json({
|
|
@@ -8529,10 +9167,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8529
9167
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
8530
9168
|
}
|
|
8531
9169
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8532
|
-
const id =
|
|
9170
|
+
const id = nanoid6(10);
|
|
8533
9171
|
const ext = extname8(body.filename) || "";
|
|
8534
9172
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8535
|
-
const filePath =
|
|
9173
|
+
const filePath = join10(dir, safeFilename);
|
|
8536
9174
|
let base64Data = body.data;
|
|
8537
9175
|
if (base64Data.includes(",")) {
|
|
8538
9176
|
base64Data = base64Data.split(",")[1];
|
|
@@ -8561,7 +9199,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8561
9199
|
return c.json({ error: "Session not found" }, 404);
|
|
8562
9200
|
}
|
|
8563
9201
|
const dir = getAttachmentsDir(sessionId);
|
|
8564
|
-
if (!
|
|
9202
|
+
if (!existsSync16(dir)) {
|
|
8565
9203
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8566
9204
|
}
|
|
8567
9205
|
const files = readdirSync2(dir);
|
|
@@ -8569,14 +9207,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8569
9207
|
if (!file) {
|
|
8570
9208
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8571
9209
|
}
|
|
8572
|
-
const filePath =
|
|
8573
|
-
|
|
9210
|
+
const filePath = join10(dir, file);
|
|
9211
|
+
unlinkSync3(filePath);
|
|
8574
9212
|
return c.json({ success: true, id: attachmentId });
|
|
8575
9213
|
});
|
|
8576
|
-
var filesQuerySchema =
|
|
8577
|
-
query:
|
|
9214
|
+
var filesQuerySchema = z16.object({
|
|
9215
|
+
query: z16.string().optional(),
|
|
8578
9216
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
8579
|
-
limit:
|
|
9217
|
+
limit: z16.string().optional()
|
|
8580
9218
|
// Max results (default 50)
|
|
8581
9219
|
});
|
|
8582
9220
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -8652,7 +9290,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
8652
9290
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
8653
9291
|
for (const entry of entries) {
|
|
8654
9292
|
if (results.length >= limit * 2) break;
|
|
8655
|
-
const fullPath =
|
|
9293
|
+
const fullPath = join10(currentDir, entry.name);
|
|
8656
9294
|
const relativePath = relative9(baseDir, fullPath);
|
|
8657
9295
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
8658
9296
|
continue;
|
|
@@ -8700,7 +9338,7 @@ sessions.get(
|
|
|
8700
9338
|
return c.json({ error: "Session not found" }, 404);
|
|
8701
9339
|
}
|
|
8702
9340
|
const workingDirectory = session.workingDirectory;
|
|
8703
|
-
if (!
|
|
9341
|
+
if (!existsSync16(workingDirectory)) {
|
|
8704
9342
|
return c.json({
|
|
8705
9343
|
sessionId,
|
|
8706
9344
|
workingDirectory,
|
|
@@ -8810,9 +9448,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
8810
9448
|
init_db();
|
|
8811
9449
|
import { Hono as Hono2 } from "hono";
|
|
8812
9450
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
8813
|
-
import { z as
|
|
8814
|
-
import { existsSync as
|
|
8815
|
-
import { join as
|
|
9451
|
+
import { z as z17 } from "zod";
|
|
9452
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
9453
|
+
import { join as join11 } from "path";
|
|
8816
9454
|
init_config();
|
|
8817
9455
|
|
|
8818
9456
|
// src/server/resumable-stream.ts
|
|
@@ -8899,7 +9537,7 @@ var streamContext = createResumableStreamContext({
|
|
|
8899
9537
|
});
|
|
8900
9538
|
|
|
8901
9539
|
// src/server/routes/agents.ts
|
|
8902
|
-
import { nanoid as
|
|
9540
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
8903
9541
|
init_stream_proxy();
|
|
8904
9542
|
init_recorder();
|
|
8905
9543
|
init_remote();
|
|
@@ -8990,40 +9628,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
8990
9628
|
${prompt}`;
|
|
8991
9629
|
}
|
|
8992
9630
|
var agents = new Hono2();
|
|
8993
|
-
var attachmentSchema =
|
|
8994
|
-
type:
|
|
8995
|
-
data:
|
|
9631
|
+
var attachmentSchema = z17.object({
|
|
9632
|
+
type: z17.enum(["image", "file"]),
|
|
9633
|
+
data: z17.string(),
|
|
8996
9634
|
// base64 data URL or raw base64
|
|
8997
|
-
mediaType:
|
|
8998
|
-
filename:
|
|
9635
|
+
mediaType: z17.string().optional(),
|
|
9636
|
+
filename: z17.string().optional()
|
|
8999
9637
|
});
|
|
9000
|
-
var runPromptSchema =
|
|
9001
|
-
prompt:
|
|
9638
|
+
var runPromptSchema = z17.object({
|
|
9639
|
+
prompt: z17.string(),
|
|
9002
9640
|
// Can be empty if attachments are provided
|
|
9003
|
-
attachments:
|
|
9641
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
9004
9642
|
}).refine(
|
|
9005
9643
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
9006
9644
|
{ message: "Either prompt or attachments must be provided" }
|
|
9007
9645
|
);
|
|
9008
|
-
var quickStartSchema =
|
|
9009
|
-
prompt:
|
|
9010
|
-
name:
|
|
9011
|
-
workingDirectory:
|
|
9012
|
-
model:
|
|
9013
|
-
toolApprovals:
|
|
9646
|
+
var quickStartSchema = z17.object({
|
|
9647
|
+
prompt: z17.string().min(1),
|
|
9648
|
+
name: z17.string().optional(),
|
|
9649
|
+
workingDirectory: z17.string().optional(),
|
|
9650
|
+
model: z17.string().optional(),
|
|
9651
|
+
toolApprovals: z17.record(z17.string(), z17.boolean()).optional()
|
|
9014
9652
|
});
|
|
9015
|
-
var rejectSchema =
|
|
9016
|
-
reason:
|
|
9653
|
+
var rejectSchema = z17.object({
|
|
9654
|
+
reason: z17.string().optional()
|
|
9017
9655
|
}).optional();
|
|
9018
9656
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9019
9657
|
function getAttachmentsDirectory(sessionId) {
|
|
9020
9658
|
const appDataDir = getAppDataDirectory();
|
|
9021
|
-
return
|
|
9659
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9022
9660
|
}
|
|
9023
9661
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9024
9662
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9025
|
-
if (!
|
|
9026
|
-
|
|
9663
|
+
if (!existsSync17(attachmentsDir)) {
|
|
9664
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9027
9665
|
}
|
|
9028
9666
|
let filename = attachment.filename;
|
|
9029
9667
|
if (!filename) {
|
|
@@ -9041,7 +9679,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9041
9679
|
attachment.mediaType = resized.mediaType;
|
|
9042
9680
|
attachment.data = buffer.toString("base64");
|
|
9043
9681
|
}
|
|
9044
|
-
const filePath =
|
|
9682
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9045
9683
|
writeFileSync4(filePath, buffer);
|
|
9046
9684
|
return filePath;
|
|
9047
9685
|
}
|
|
@@ -9052,9 +9690,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9052
9690
|
}
|
|
9053
9691
|
return data;
|
|
9054
9692
|
}
|
|
9055
|
-
function getExtensionFromMediaType(mediaType,
|
|
9693
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9056
9694
|
if (!mediaType) {
|
|
9057
|
-
return
|
|
9695
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9058
9696
|
}
|
|
9059
9697
|
const mimeToExt = {
|
|
9060
9698
|
"image/png": ".png",
|
|
@@ -9458,7 +10096,7 @@ ${prompt}` });
|
|
|
9458
10096
|
userMessageContent = prompt;
|
|
9459
10097
|
}
|
|
9460
10098
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
9461
|
-
const streamId = `stream_${id}_${
|
|
10099
|
+
const streamId = `stream_${id}_${nanoid7(10)}`;
|
|
9462
10100
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
9463
10101
|
await activeStreamQueries.create(id, streamId);
|
|
9464
10102
|
const stream = await streamContext.resumableStream(
|
|
@@ -9663,7 +10301,7 @@ agents.post(
|
|
|
9663
10301
|
});
|
|
9664
10302
|
const session = agent.getSession();
|
|
9665
10303
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
9666
|
-
const streamId = `stream_${session.id}_${
|
|
10304
|
+
const streamId = `stream_${session.id}_${nanoid7(10)}`;
|
|
9667
10305
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
9668
10306
|
await activeStreamQueries.create(session.id, streamId);
|
|
9669
10307
|
const createQuickStreamProducer = () => {
|
|
@@ -9930,23 +10568,23 @@ agents.post(
|
|
|
9930
10568
|
});
|
|
9931
10569
|
}
|
|
9932
10570
|
);
|
|
9933
|
-
var browserInputSchema =
|
|
9934
|
-
type:
|
|
9935
|
-
eventType:
|
|
9936
|
-
x:
|
|
9937
|
-
y:
|
|
9938
|
-
button:
|
|
9939
|
-
clickCount:
|
|
9940
|
-
deltaX:
|
|
9941
|
-
deltaY:
|
|
9942
|
-
key:
|
|
9943
|
-
code:
|
|
9944
|
-
text:
|
|
9945
|
-
modifiers:
|
|
9946
|
-
touchPoints:
|
|
9947
|
-
x:
|
|
9948
|
-
y:
|
|
9949
|
-
id:
|
|
10571
|
+
var browserInputSchema = z17.object({
|
|
10572
|
+
type: z17.enum(["input_mouse", "input_keyboard", "input_touch"]),
|
|
10573
|
+
eventType: z17.string(),
|
|
10574
|
+
x: z17.number().optional(),
|
|
10575
|
+
y: z17.number().optional(),
|
|
10576
|
+
button: z17.string().optional(),
|
|
10577
|
+
clickCount: z17.number().optional(),
|
|
10578
|
+
deltaX: z17.number().optional(),
|
|
10579
|
+
deltaY: z17.number().optional(),
|
|
10580
|
+
key: z17.string().optional(),
|
|
10581
|
+
code: z17.string().optional(),
|
|
10582
|
+
text: z17.string().optional(),
|
|
10583
|
+
modifiers: z17.number().optional(),
|
|
10584
|
+
touchPoints: z17.array(z17.object({
|
|
10585
|
+
x: z17.number(),
|
|
10586
|
+
y: z17.number(),
|
|
10587
|
+
id: z17.number().optional()
|
|
9950
10588
|
})).optional()
|
|
9951
10589
|
});
|
|
9952
10590
|
agents.post(
|
|
@@ -9981,27 +10619,279 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
9981
10619
|
init_config();
|
|
9982
10620
|
import { Hono as Hono3 } from "hono";
|
|
9983
10621
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
9984
|
-
import { z as
|
|
9985
|
-
import { readFileSync as
|
|
10622
|
+
import { z as z18 } from "zod";
|
|
10623
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
9986
10624
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9987
|
-
import { dirname as dirname6, join as
|
|
10625
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10626
|
+
|
|
10627
|
+
// src/personal-agent/heartbeat.ts
|
|
10628
|
+
import { execSync as execSync3 } from "child_process";
|
|
10629
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10630
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
10631
|
+
|
|
10632
|
+
// src/personal-agent/system-metrics.ts
|
|
10633
|
+
import { execSync as execSync2 } from "child_process";
|
|
10634
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
10635
|
+
import {
|
|
10636
|
+
arch,
|
|
10637
|
+
cpus,
|
|
10638
|
+
freemem,
|
|
10639
|
+
hostname,
|
|
10640
|
+
loadavg,
|
|
10641
|
+
networkInterfaces,
|
|
10642
|
+
platform as platform2,
|
|
10643
|
+
release,
|
|
10644
|
+
totalmem,
|
|
10645
|
+
type,
|
|
10646
|
+
uptime,
|
|
10647
|
+
userInfo
|
|
10648
|
+
} from "os";
|
|
10649
|
+
var _lastSample = null;
|
|
10650
|
+
function snapshotCpuTimes() {
|
|
10651
|
+
const sum = { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
|
|
10652
|
+
for (const c of cpus()) {
|
|
10653
|
+
sum.user += c.times.user;
|
|
10654
|
+
sum.nice += c.times.nice;
|
|
10655
|
+
sum.sys += c.times.sys;
|
|
10656
|
+
sum.idle += c.times.idle;
|
|
10657
|
+
sum.irq += c.times.irq;
|
|
10658
|
+
}
|
|
10659
|
+
return sum;
|
|
10660
|
+
}
|
|
10661
|
+
function readCpuUsage() {
|
|
10662
|
+
const now = snapshotCpuTimes();
|
|
10663
|
+
if (!_lastSample) {
|
|
10664
|
+
_lastSample = now;
|
|
10665
|
+
return 0;
|
|
10666
|
+
}
|
|
10667
|
+
const dUser = now.user - _lastSample.user;
|
|
10668
|
+
const dNice = now.nice - _lastSample.nice;
|
|
10669
|
+
const dSys = now.sys - _lastSample.sys;
|
|
10670
|
+
const dIdle = now.idle - _lastSample.idle;
|
|
10671
|
+
const dIrq = now.irq - _lastSample.irq;
|
|
10672
|
+
const total = dUser + dNice + dSys + dIdle + dIrq;
|
|
10673
|
+
_lastSample = now;
|
|
10674
|
+
if (total <= 0) return 0;
|
|
10675
|
+
return Math.max(0, Math.min(1, (total - dIdle) / total));
|
|
10676
|
+
}
|
|
10677
|
+
snapshotCpuTimes();
|
|
10678
|
+
function readCpuTempC() {
|
|
10679
|
+
try {
|
|
10680
|
+
if (platform2() === "linux") {
|
|
10681
|
+
let hottest = -Infinity;
|
|
10682
|
+
try {
|
|
10683
|
+
for (const entry of readdirSync3("/sys/class/thermal")) {
|
|
10684
|
+
if (!entry.startsWith("thermal_zone")) continue;
|
|
10685
|
+
try {
|
|
10686
|
+
const v = Number(
|
|
10687
|
+
readFileSync8(`/sys/class/thermal/${entry}/temp`, "utf8").trim()
|
|
10688
|
+
);
|
|
10689
|
+
if (Number.isFinite(v) && v > hottest) hottest = v;
|
|
10690
|
+
} catch {
|
|
10691
|
+
}
|
|
10692
|
+
}
|
|
10693
|
+
} catch {
|
|
10694
|
+
}
|
|
10695
|
+
if (hottest > -Infinity) return hottest / 1e3;
|
|
10696
|
+
}
|
|
10697
|
+
const overrideCmd = process.env.PERSONAL_AGENT_TEMP_CMD;
|
|
10698
|
+
if (overrideCmd) {
|
|
10699
|
+
const out = execSync2(overrideCmd, { encoding: "utf8", timeout: 1500 }).trim();
|
|
10700
|
+
const v = Number(out);
|
|
10701
|
+
if (Number.isFinite(v)) return v;
|
|
10702
|
+
}
|
|
10703
|
+
} catch {
|
|
10704
|
+
}
|
|
10705
|
+
return void 0;
|
|
10706
|
+
}
|
|
10707
|
+
function readDisks() {
|
|
10708
|
+
try {
|
|
10709
|
+
const p = platform2();
|
|
10710
|
+
if (p === "darwin" || p === "linux") {
|
|
10711
|
+
const raw = execSync2("df -kP", { encoding: "utf8", timeout: 2e3 });
|
|
10712
|
+
const lines = raw.trim().split("\n").slice(1);
|
|
10713
|
+
const out = [];
|
|
10714
|
+
for (const line of lines) {
|
|
10715
|
+
const parts = line.trim().split(/\s+/);
|
|
10716
|
+
if (parts.length < 6) continue;
|
|
10717
|
+
const filesystem = parts[0];
|
|
10718
|
+
const total1k = Number(parts[1]);
|
|
10719
|
+
const used1k = Number(parts[2]);
|
|
10720
|
+
const free1k = Number(parts[3]);
|
|
10721
|
+
const mount = parts.slice(5).join(" ");
|
|
10722
|
+
if (!Number.isFinite(total1k) || total1k <= 0) continue;
|
|
10723
|
+
if (filesystem === "tmpfs" || filesystem === "devfs" || filesystem === "map" || filesystem.startsWith("/dev/loop") || mount.startsWith("/System/Volumes/") || mount.startsWith("/private/var/vm") || mount.startsWith("/proc") || mount.startsWith("/sys") || mount.startsWith("/run") || mount.startsWith("/snap")) {
|
|
10724
|
+
if (mount !== "/") continue;
|
|
10725
|
+
}
|
|
10726
|
+
out.push({
|
|
10727
|
+
mount,
|
|
10728
|
+
filesystem,
|
|
10729
|
+
totalBytes: total1k * 1024,
|
|
10730
|
+
usedBytes: used1k * 1024,
|
|
10731
|
+
freeBytes: free1k * 1024,
|
|
10732
|
+
usage: total1k > 0 ? used1k / total1k : 0
|
|
10733
|
+
});
|
|
10734
|
+
}
|
|
10735
|
+
out.sort((a, b) => {
|
|
10736
|
+
const score = (m) => m === "/" ? 0 : m.startsWith("/Users") || m.startsWith("/home") ? 1 : 2;
|
|
10737
|
+
return score(a.mount) - score(b.mount);
|
|
10738
|
+
});
|
|
10739
|
+
return out.slice(0, 6);
|
|
10740
|
+
}
|
|
10741
|
+
if (p === "win32") {
|
|
10742
|
+
const raw = execSync2(
|
|
10743
|
+
"wmic logicaldisk get DeviceID,Size,FreeSpace /format:csv",
|
|
10744
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
10745
|
+
);
|
|
10746
|
+
const out = [];
|
|
10747
|
+
for (const line of raw.trim().split(/\r?\n/).slice(1)) {
|
|
10748
|
+
const cols = line.split(",");
|
|
10749
|
+
if (cols.length < 4) continue;
|
|
10750
|
+
const [, deviceId, freeStr, sizeStr] = cols;
|
|
10751
|
+
const total = Number(sizeStr);
|
|
10752
|
+
const free = Number(freeStr);
|
|
10753
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
10754
|
+
const used = Math.max(0, total - free);
|
|
10755
|
+
out.push({
|
|
10756
|
+
mount: deviceId,
|
|
10757
|
+
totalBytes: total,
|
|
10758
|
+
usedBytes: used,
|
|
10759
|
+
freeBytes: free,
|
|
10760
|
+
usage: used / total
|
|
10761
|
+
});
|
|
10762
|
+
}
|
|
10763
|
+
return out;
|
|
10764
|
+
}
|
|
10765
|
+
} catch {
|
|
10766
|
+
}
|
|
10767
|
+
return void 0;
|
|
10768
|
+
}
|
|
10769
|
+
function readNetwork() {
|
|
10770
|
+
try {
|
|
10771
|
+
const out = [];
|
|
10772
|
+
const ifaces = networkInterfaces();
|
|
10773
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
10774
|
+
if (!addrs) continue;
|
|
10775
|
+
for (const a of addrs) {
|
|
10776
|
+
if (a.internal) continue;
|
|
10777
|
+
out.push({
|
|
10778
|
+
iface: name,
|
|
10779
|
+
family: a.family,
|
|
10780
|
+
address: a.address,
|
|
10781
|
+
mac: a.mac,
|
|
10782
|
+
internal: a.internal
|
|
10783
|
+
});
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
return out;
|
|
10787
|
+
} catch {
|
|
10788
|
+
return void 0;
|
|
10789
|
+
}
|
|
10790
|
+
}
|
|
10791
|
+
function readSystemMetrics() {
|
|
10792
|
+
const cpuList = cpus();
|
|
10793
|
+
const usage = readCpuUsage();
|
|
10794
|
+
const tot = totalmem();
|
|
10795
|
+
const free = freemem();
|
|
10796
|
+
const metrics = {
|
|
10797
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10798
|
+
hostname: hostname(),
|
|
10799
|
+
platform: platform2(),
|
|
10800
|
+
arch: arch(),
|
|
10801
|
+
kernelRelease: release(),
|
|
10802
|
+
osType: type(),
|
|
10803
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
10804
|
+
systemUptimeSec: Math.round(uptime()),
|
|
10805
|
+
user: safeUser(),
|
|
10806
|
+
cpu: cpuList[0] ? {
|
|
10807
|
+
model: cpuList[0].model,
|
|
10808
|
+
count: cpuList.length,
|
|
10809
|
+
speedMhz: cpuList[0].speed,
|
|
10810
|
+
loadAvg1: loadavg()[0],
|
|
10811
|
+
loadAvg5: loadavg()[1],
|
|
10812
|
+
loadAvg15: loadavg()[2],
|
|
10813
|
+
usage,
|
|
10814
|
+
tempC: readCpuTempC()
|
|
10815
|
+
} : void 0,
|
|
10816
|
+
memory: {
|
|
10817
|
+
totalBytes: tot,
|
|
10818
|
+
freeBytes: free,
|
|
10819
|
+
usedBytes: Math.max(0, tot - free),
|
|
10820
|
+
usage: tot > 0 ? (tot - free) / tot : 0
|
|
10821
|
+
},
|
|
10822
|
+
disks: readDisks(),
|
|
10823
|
+
network: readNetwork()
|
|
10824
|
+
};
|
|
10825
|
+
return metrics;
|
|
10826
|
+
}
|
|
10827
|
+
function safeUser() {
|
|
10828
|
+
try {
|
|
10829
|
+
return userInfo().username;
|
|
10830
|
+
} catch {
|
|
10831
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
10832
|
+
}
|
|
10833
|
+
}
|
|
10834
|
+
|
|
10835
|
+
// src/personal-agent/heartbeat.ts
|
|
10836
|
+
var _cachedHwid = null;
|
|
10837
|
+
function getHardwareIdCached() {
|
|
10838
|
+
if (_cachedHwid !== null) return _cachedHwid;
|
|
10839
|
+
_cachedHwid = getHardwareId();
|
|
10840
|
+
return _cachedHwid;
|
|
10841
|
+
}
|
|
10842
|
+
function getHardwareId() {
|
|
10843
|
+
const p = platform3();
|
|
10844
|
+
try {
|
|
10845
|
+
if (p === "darwin") {
|
|
10846
|
+
const out = execSync3(
|
|
10847
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/ {print $4}'`,
|
|
10848
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
10849
|
+
).trim();
|
|
10850
|
+
if (out) return normalize2(out);
|
|
10851
|
+
} else if (p === "linux") {
|
|
10852
|
+
try {
|
|
10853
|
+
return normalize2(readFileSync9("/etc/machine-id", "utf8").trim());
|
|
10854
|
+
} catch {
|
|
10855
|
+
return normalize2(readFileSync9("/var/lib/dbus/machine-id", "utf8").trim());
|
|
10856
|
+
}
|
|
10857
|
+
} else if (p === "win32") {
|
|
10858
|
+
const out = execSync3("wmic csproduct get uuid /value", {
|
|
10859
|
+
encoding: "utf8",
|
|
10860
|
+
timeout: 3e3
|
|
10861
|
+
});
|
|
10862
|
+
const m = out.match(/UUID=([\w-]+)/i);
|
|
10863
|
+
if (m && m[1]) return normalize2(m[1]);
|
|
10864
|
+
}
|
|
10865
|
+
} catch (e) {
|
|
10866
|
+
console.warn(`[personal-agent] could not read hardware UUID: ${e.message}`);
|
|
10867
|
+
}
|
|
10868
|
+
console.warn(
|
|
10869
|
+
"[personal-agent] falling back to hostname for HWID; this is NOT stable across rename/reinstall"
|
|
10870
|
+
);
|
|
10871
|
+
return `host-${hostname2()}`;
|
|
10872
|
+
}
|
|
10873
|
+
function normalize2(raw) {
|
|
10874
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
10875
|
+
}
|
|
10876
|
+
|
|
10877
|
+
// src/server/routes/health.ts
|
|
9988
10878
|
var __filename = fileURLToPath3(import.meta.url);
|
|
9989
10879
|
var __dirname = dirname6(__filename);
|
|
9990
10880
|
var possiblePaths = [
|
|
9991
|
-
|
|
10881
|
+
join12(__dirname, "../package.json"),
|
|
9992
10882
|
// From dist/server -> dist/../package.json
|
|
9993
|
-
|
|
10883
|
+
join12(__dirname, "../../package.json"),
|
|
9994
10884
|
// From dist/server (if nested differently)
|
|
9995
|
-
|
|
10885
|
+
join12(__dirname, "../../../package.json"),
|
|
9996
10886
|
// From src/server/routes (development)
|
|
9997
|
-
|
|
10887
|
+
join12(process.cwd(), "package.json")
|
|
9998
10888
|
// From current working directory
|
|
9999
10889
|
];
|
|
10000
10890
|
var currentVersion = "0.0.0";
|
|
10001
10891
|
var packageName = "sparkecoder";
|
|
10002
10892
|
for (const packageJsonPath of possiblePaths) {
|
|
10003
10893
|
try {
|
|
10004
|
-
const packageJson = JSON.parse(
|
|
10894
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
10005
10895
|
if (packageJson.name === "sparkecoder") {
|
|
10006
10896
|
currentVersion = packageJson.version || "0.0.0";
|
|
10007
10897
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10016,11 +10906,17 @@ health.get("/", async (c) => {
|
|
|
10016
10906
|
const apiKeyStatus = getApiKeyStatus();
|
|
10017
10907
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10018
10908
|
const hasApiKey = gatewayKey?.configured ?? false;
|
|
10909
|
+
let hwid;
|
|
10910
|
+
try {
|
|
10911
|
+
hwid = getHardwareIdCached();
|
|
10912
|
+
} catch {
|
|
10913
|
+
}
|
|
10019
10914
|
return c.json({
|
|
10020
10915
|
status: "ok",
|
|
10021
10916
|
version: currentVersion,
|
|
10022
10917
|
uptime: process.uptime(),
|
|
10023
10918
|
apiKeyConfigured: hasApiKey,
|
|
10919
|
+
hwid,
|
|
10024
10920
|
config: {
|
|
10025
10921
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10026
10922
|
defaultModel: config.defaultModel,
|
|
@@ -10091,9 +10987,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10091
10987
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10092
10988
|
});
|
|
10093
10989
|
});
|
|
10094
|
-
var setApiKeySchema =
|
|
10095
|
-
provider:
|
|
10096
|
-
apiKey:
|
|
10990
|
+
var setApiKeySchema = z18.object({
|
|
10991
|
+
provider: z18.string(),
|
|
10992
|
+
apiKey: z18.string().min(1)
|
|
10097
10993
|
});
|
|
10098
10994
|
health.post(
|
|
10099
10995
|
"/api-keys",
|
|
@@ -10132,13 +11028,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10132
11028
|
// src/server/routes/terminals.ts
|
|
10133
11029
|
import { Hono as Hono4 } from "hono";
|
|
10134
11030
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10135
|
-
import { z as
|
|
11031
|
+
import { z as z19 } from "zod";
|
|
10136
11032
|
init_db();
|
|
10137
11033
|
var terminals = new Hono4();
|
|
10138
|
-
var spawnSchema =
|
|
10139
|
-
command:
|
|
10140
|
-
cwd:
|
|
10141
|
-
name:
|
|
11034
|
+
var spawnSchema = z19.object({
|
|
11035
|
+
command: z19.string(),
|
|
11036
|
+
cwd: z19.string().optional(),
|
|
11037
|
+
name: z19.string().optional()
|
|
10142
11038
|
});
|
|
10143
11039
|
terminals.post(
|
|
10144
11040
|
"/:sessionId/terminals",
|
|
@@ -10219,8 +11115,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10219
11115
|
// We don't track exit codes in tmux mode
|
|
10220
11116
|
});
|
|
10221
11117
|
});
|
|
10222
|
-
var logsQuerySchema =
|
|
10223
|
-
tail:
|
|
11118
|
+
var logsQuerySchema = z19.object({
|
|
11119
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10224
11120
|
});
|
|
10225
11121
|
terminals.get(
|
|
10226
11122
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -10244,8 +11140,8 @@ terminals.get(
|
|
|
10244
11140
|
});
|
|
10245
11141
|
}
|
|
10246
11142
|
);
|
|
10247
|
-
var killSchema =
|
|
10248
|
-
signal:
|
|
11143
|
+
var killSchema = z19.object({
|
|
11144
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
10249
11145
|
});
|
|
10250
11146
|
terminals.post(
|
|
10251
11147
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -10259,8 +11155,8 @@ terminals.post(
|
|
|
10259
11155
|
return c.json({ success: true, message: "Terminal killed" });
|
|
10260
11156
|
}
|
|
10261
11157
|
);
|
|
10262
|
-
var writeSchema =
|
|
10263
|
-
input:
|
|
11158
|
+
var writeSchema = z19.object({
|
|
11159
|
+
input: z19.string()
|
|
10264
11160
|
});
|
|
10265
11161
|
terminals.post(
|
|
10266
11162
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -10445,20 +11341,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
10445
11341
|
init_db();
|
|
10446
11342
|
import { Hono as Hono5 } from "hono";
|
|
10447
11343
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
10448
|
-
import { z as
|
|
10449
|
-
import { nanoid as
|
|
11344
|
+
import { z as z20 } from "zod";
|
|
11345
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
10450
11346
|
init_config();
|
|
10451
11347
|
var tasks = new Hono5();
|
|
10452
11348
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
10453
|
-
var createTaskSchema =
|
|
10454
|
-
prompt:
|
|
10455
|
-
outputSchema:
|
|
10456
|
-
webhookUrl:
|
|
10457
|
-
model:
|
|
10458
|
-
workingDirectory:
|
|
10459
|
-
name:
|
|
10460
|
-
maxIterations:
|
|
10461
|
-
parentTaskId:
|
|
11349
|
+
var createTaskSchema = z20.object({
|
|
11350
|
+
prompt: z20.string().min(1),
|
|
11351
|
+
outputSchema: z20.record(z20.string(), z20.unknown()),
|
|
11352
|
+
webhookUrl: z20.string().url().optional(),
|
|
11353
|
+
model: z20.string().optional(),
|
|
11354
|
+
workingDirectory: z20.string().optional(),
|
|
11355
|
+
name: z20.string().optional(),
|
|
11356
|
+
maxIterations: z20.number().int().min(1).max(500).optional(),
|
|
11357
|
+
parentTaskId: z20.string().optional()
|
|
10462
11358
|
});
|
|
10463
11359
|
tasks.post(
|
|
10464
11360
|
"/",
|
|
@@ -10520,7 +11416,7 @@ tasks.post(
|
|
|
10520
11416
|
const taskId = agent.sessionId;
|
|
10521
11417
|
const abortController = new AbortController();
|
|
10522
11418
|
taskAbortControllers.set(taskId, abortController);
|
|
10523
|
-
const streamId = `stream_${taskId}_${
|
|
11419
|
+
const streamId = `stream_${taskId}_${nanoid8(10)}`;
|
|
10524
11420
|
await activeStreamQueries.create(taskId, streamId);
|
|
10525
11421
|
const taskStreamProducer = () => {
|
|
10526
11422
|
const { readable, writable } = new TransformStream();
|
|
@@ -10670,17 +11566,581 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10670
11566
|
});
|
|
10671
11567
|
var tasks_default = tasks;
|
|
10672
11568
|
|
|
11569
|
+
// src/server/routes/system.ts
|
|
11570
|
+
import { Hono as Hono6 } from "hono";
|
|
11571
|
+
import { streamSSE } from "hono/streaming";
|
|
11572
|
+
var system = new Hono6();
|
|
11573
|
+
system.get("/metrics", (c) => {
|
|
11574
|
+
return c.json(readSystemMetrics());
|
|
11575
|
+
});
|
|
11576
|
+
system.get("/metrics/stream", (c) => {
|
|
11577
|
+
return streamSSE(c, async (stream) => {
|
|
11578
|
+
const intervalMs = Number(c.req.query("intervalMs") ?? 2e3);
|
|
11579
|
+
const safeMs = Number.isFinite(intervalMs) ? Math.max(500, Math.min(6e4, intervalMs)) : 2e3;
|
|
11580
|
+
let id = 0;
|
|
11581
|
+
let aborted = false;
|
|
11582
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
11583
|
+
aborted = true;
|
|
11584
|
+
});
|
|
11585
|
+
while (!aborted) {
|
|
11586
|
+
try {
|
|
11587
|
+
const snap = readSystemMetrics();
|
|
11588
|
+
await stream.writeSSE({
|
|
11589
|
+
id: String(id++),
|
|
11590
|
+
event: "metrics",
|
|
11591
|
+
data: JSON.stringify(snap)
|
|
11592
|
+
});
|
|
11593
|
+
} catch (e) {
|
|
11594
|
+
await stream.writeSSE({
|
|
11595
|
+
id: String(id++),
|
|
11596
|
+
event: "error",
|
|
11597
|
+
data: JSON.stringify({ error: e.message })
|
|
11598
|
+
});
|
|
11599
|
+
}
|
|
11600
|
+
await stream.sleep(safeMs);
|
|
11601
|
+
}
|
|
11602
|
+
});
|
|
11603
|
+
});
|
|
11604
|
+
|
|
11605
|
+
// src/personal-agent/hwid-middleware.ts
|
|
11606
|
+
var SKIP_PATHS = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11607
|
+
function personalAgentConfigured() {
|
|
11608
|
+
return process.env.PERSONAL_AGENT_MODE === "1" || process.env.PERSONAL_AGENT_MODE === "true" || Boolean(process.env.PERSONAL_AGENT_PUBLIC_KEY) || Boolean(process.env.PERSONAL_AGENT_DASHBOARD);
|
|
11609
|
+
}
|
|
11610
|
+
function hwidMiddleware() {
|
|
11611
|
+
const enabled = personalAgentConfigured();
|
|
11612
|
+
let warnedMissing = false;
|
|
11613
|
+
return async (c, next) => {
|
|
11614
|
+
if (!enabled) return next();
|
|
11615
|
+
const path = c.req.path;
|
|
11616
|
+
if (SKIP_PATHS.has(path) || path.startsWith("/health/api-keys")) {
|
|
11617
|
+
return next();
|
|
11618
|
+
}
|
|
11619
|
+
const got = c.req.header("x-device-hwid");
|
|
11620
|
+
if (!got) {
|
|
11621
|
+
if (!warnedMissing) {
|
|
11622
|
+
warnedMissing = true;
|
|
11623
|
+
console.warn(
|
|
11624
|
+
`[personal-agent] request to ${path} arrived without X-Device-Hwid; allowing (backwards compat)`
|
|
11625
|
+
);
|
|
11626
|
+
}
|
|
11627
|
+
return next();
|
|
11628
|
+
}
|
|
11629
|
+
const expected = getHardwareIdCached();
|
|
11630
|
+
if (got !== expected) {
|
|
11631
|
+
console.warn(
|
|
11632
|
+
`[personal-agent] HWID mismatch on ${path}: got=${got.slice(0, 12)}\u2026, expected=${expected.slice(0, 12)}\u2026`
|
|
11633
|
+
);
|
|
11634
|
+
return c.json(
|
|
11635
|
+
{
|
|
11636
|
+
error: "hwid mismatch",
|
|
11637
|
+
message: "This sparkecoder's hardware UUID does not match what the dashboard expected. Likely cause: a Cloudflare tunnel hostname is pointing at the wrong machine.",
|
|
11638
|
+
expected: expected.slice(0, 12) + "\u2026",
|
|
11639
|
+
got: got.slice(0, 12) + "\u2026"
|
|
11640
|
+
},
|
|
11641
|
+
409
|
|
11642
|
+
);
|
|
11643
|
+
}
|
|
11644
|
+
return next();
|
|
11645
|
+
};
|
|
11646
|
+
}
|
|
11647
|
+
|
|
11648
|
+
// src/personal-agent/signature-verify.ts
|
|
11649
|
+
import { createHash as createHash3, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
11650
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
11651
|
+
var REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
11652
|
+
var _cachedKey = null;
|
|
11653
|
+
var _cachedFromInput = null;
|
|
11654
|
+
function loadPublicKey(input) {
|
|
11655
|
+
if (_cachedFromInput === input && _cachedKey) return _cachedKey;
|
|
11656
|
+
let pem = input;
|
|
11657
|
+
if (!input.includes("BEGIN") && existsSync18(input)) {
|
|
11658
|
+
pem = readFileSync11(input, "utf8");
|
|
11659
|
+
}
|
|
11660
|
+
const key = createPublicKey({ key: pem, format: "pem" });
|
|
11661
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
11662
|
+
throw new Error(
|
|
11663
|
+
`expected an ed25519 public key, got ${key.asymmetricKeyType}. Generate with personal-agents/scripts/generate-signing-keys.mjs.`
|
|
11664
|
+
);
|
|
11665
|
+
}
|
|
11666
|
+
_cachedKey = key;
|
|
11667
|
+
_cachedFromInput = input;
|
|
11668
|
+
return key;
|
|
11669
|
+
}
|
|
11670
|
+
function bodyHashB64(body) {
|
|
11671
|
+
const hash = createHash3("sha256");
|
|
11672
|
+
if (body == null || body === "") {
|
|
11673
|
+
} else if (typeof body === "string") {
|
|
11674
|
+
hash.update(body, "utf8");
|
|
11675
|
+
} else if (Buffer.isBuffer(body)) {
|
|
11676
|
+
hash.update(body);
|
|
11677
|
+
} else {
|
|
11678
|
+
hash.update(Buffer.from(body));
|
|
11679
|
+
}
|
|
11680
|
+
return hash.digest("base64");
|
|
11681
|
+
}
|
|
11682
|
+
function canonicalSigningString(args) {
|
|
11683
|
+
return [
|
|
11684
|
+
args.method.toUpperCase(),
|
|
11685
|
+
args.path,
|
|
11686
|
+
String(args.timestamp),
|
|
11687
|
+
args.bodyHashB64
|
|
11688
|
+
].join("\n");
|
|
11689
|
+
}
|
|
11690
|
+
function fromBase64Url(s) {
|
|
11691
|
+
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
11692
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
11693
|
+
return Buffer.from(std, "base64");
|
|
11694
|
+
}
|
|
11695
|
+
function verifyEmbedToken(args) {
|
|
11696
|
+
const dot = args.token.indexOf(".");
|
|
11697
|
+
if (dot < 1 || dot >= args.token.length - 1) {
|
|
11698
|
+
return { ok: false, reason: "malformed" };
|
|
11699
|
+
}
|
|
11700
|
+
const payloadB64 = args.token.slice(0, dot);
|
|
11701
|
+
const sigB64 = args.token.slice(dot + 1);
|
|
11702
|
+
let sigBuf;
|
|
11703
|
+
try {
|
|
11704
|
+
sigBuf = fromBase64Url(sigB64);
|
|
11705
|
+
} catch {
|
|
11706
|
+
return { ok: false, reason: "bad-encoding" };
|
|
11707
|
+
}
|
|
11708
|
+
const sigOk = cryptoVerify(
|
|
11709
|
+
null,
|
|
11710
|
+
Buffer.from(payloadB64, "utf8"),
|
|
11711
|
+
args.publicKey,
|
|
11712
|
+
sigBuf
|
|
11713
|
+
);
|
|
11714
|
+
if (!sigOk) return { ok: false, reason: "signature-mismatch" };
|
|
11715
|
+
let payload;
|
|
11716
|
+
try {
|
|
11717
|
+
const json = JSON.parse(fromBase64Url(payloadB64).toString("utf8"));
|
|
11718
|
+
if (!json || typeof json !== "object" || typeof json.sid !== "string" || typeof json.exp !== "number") {
|
|
11719
|
+
return { ok: false, reason: "bad-payload" };
|
|
11720
|
+
}
|
|
11721
|
+
payload = { sid: json.sid, exp: json.exp };
|
|
11722
|
+
} catch (e) {
|
|
11723
|
+
return { ok: false, reason: "bad-payload", detail: e.message };
|
|
11724
|
+
}
|
|
11725
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11726
|
+
if (payload.exp < now) {
|
|
11727
|
+
return {
|
|
11728
|
+
ok: false,
|
|
11729
|
+
reason: "expired",
|
|
11730
|
+
detail: `${now - payload.exp}s past expiry`
|
|
11731
|
+
};
|
|
11732
|
+
}
|
|
11733
|
+
return { ok: true, payload };
|
|
11734
|
+
}
|
|
11735
|
+
function verifyRequest(args) {
|
|
11736
|
+
if (!args.signatureB64 || !args.timestampSeconds || !args.algorithm) {
|
|
11737
|
+
return { ok: false, reason: "missing-headers" };
|
|
11738
|
+
}
|
|
11739
|
+
if (args.algorithm.toLowerCase() !== "ed25519") {
|
|
11740
|
+
return { ok: false, reason: "bad-algorithm", detail: args.algorithm };
|
|
11741
|
+
}
|
|
11742
|
+
const ts = Number(args.timestampSeconds);
|
|
11743
|
+
if (!Number.isFinite(ts)) {
|
|
11744
|
+
return { ok: false, reason: "bad-timestamp", detail: args.timestampSeconds };
|
|
11745
|
+
}
|
|
11746
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11747
|
+
if (Math.abs(now - ts) > REPLAY_WINDOW_SECONDS) {
|
|
11748
|
+
return {
|
|
11749
|
+
ok: false,
|
|
11750
|
+
reason: "stale-timestamp",
|
|
11751
|
+
detail: `${Math.abs(now - ts)}s outside the ${REPLAY_WINDOW_SECONDS}s window`
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11754
|
+
let sigBuf;
|
|
11755
|
+
try {
|
|
11756
|
+
sigBuf = Buffer.from(args.signatureB64, "base64");
|
|
11757
|
+
} catch {
|
|
11758
|
+
return { ok: false, reason: "bad-signature-encoding" };
|
|
11759
|
+
}
|
|
11760
|
+
const canonical = canonicalSigningString({
|
|
11761
|
+
method: args.method,
|
|
11762
|
+
path: args.path,
|
|
11763
|
+
timestamp: ts,
|
|
11764
|
+
bodyHashB64: bodyHashB64(args.body)
|
|
11765
|
+
});
|
|
11766
|
+
const ok = cryptoVerify(null, Buffer.from(canonical, "utf8"), args.publicKey, sigBuf);
|
|
11767
|
+
return ok ? { ok: true } : { ok: false, reason: "signature-mismatch" };
|
|
11768
|
+
}
|
|
11769
|
+
|
|
11770
|
+
// src/personal-agent/signature-middleware.ts
|
|
11771
|
+
var SKIP_PATHS2 = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11772
|
+
function isSkipped(path) {
|
|
11773
|
+
return SKIP_PATHS2.has(path) || path.startsWith("/health/api-keys");
|
|
11774
|
+
}
|
|
11775
|
+
function pathBindsSessionId(path, sid) {
|
|
11776
|
+
const cleanPath = path.split("?")[0];
|
|
11777
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
11778
|
+
return segments.includes(sid);
|
|
11779
|
+
}
|
|
11780
|
+
function signatureMiddleware(opts = {}) {
|
|
11781
|
+
const acceptSignedOnly = opts.acceptSignedOnly ?? process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
11782
|
+
const publicKeyInput = opts.publicKeyInput ?? process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
11783
|
+
if (acceptSignedOnly && !publicKeyInput) {
|
|
11784
|
+
return async (c) => c.json(
|
|
11785
|
+
{
|
|
11786
|
+
error: "signature middleware misconfigured",
|
|
11787
|
+
message: "started with --accept-signed-only but no --personal-agent-public-key / PERSONAL_AGENT_PUBLIC_KEY supplied. Refusing all non-/health traffic."
|
|
11788
|
+
},
|
|
11789
|
+
500
|
|
11790
|
+
);
|
|
11791
|
+
}
|
|
11792
|
+
if (!publicKeyInput) {
|
|
11793
|
+
return async (_c, next) => next();
|
|
11794
|
+
}
|
|
11795
|
+
const publicKey = loadPublicKey(publicKeyInput);
|
|
11796
|
+
let warnedUnsigned = false;
|
|
11797
|
+
return async (c, next) => {
|
|
11798
|
+
const path = c.req.path;
|
|
11799
|
+
if (isSkipped(path)) return next();
|
|
11800
|
+
const sig = c.req.header("x-signature");
|
|
11801
|
+
const ts = c.req.header("x-signature-timestamp");
|
|
11802
|
+
const alg = c.req.header("x-signature-algorithm");
|
|
11803
|
+
if (!sig && (c.req.method === "GET" || c.req.method === "HEAD")) {
|
|
11804
|
+
const embedTok = c.req.header("x-embed-token") ?? c.req.query("embed_token");
|
|
11805
|
+
if (embedTok) {
|
|
11806
|
+
const result2 = verifyEmbedToken({ publicKey, token: embedTok });
|
|
11807
|
+
if (!result2.ok) {
|
|
11808
|
+
return c.json(
|
|
11809
|
+
{
|
|
11810
|
+
error: "embed token verification failed",
|
|
11811
|
+
reason: result2.reason,
|
|
11812
|
+
detail: result2.detail
|
|
11813
|
+
},
|
|
11814
|
+
401
|
|
11815
|
+
);
|
|
11816
|
+
}
|
|
11817
|
+
if (!pathBindsSessionId(path, result2.payload.sid)) {
|
|
11818
|
+
return c.json(
|
|
11819
|
+
{
|
|
11820
|
+
error: "embed token scoped to a different session",
|
|
11821
|
+
detail: `token sid=${result2.payload.sid} but request path=${path}`
|
|
11822
|
+
},
|
|
11823
|
+
403
|
|
11824
|
+
);
|
|
11825
|
+
}
|
|
11826
|
+
return next();
|
|
11827
|
+
}
|
|
11828
|
+
}
|
|
11829
|
+
if (!sig) {
|
|
11830
|
+
if (acceptSignedOnly) {
|
|
11831
|
+
return c.json(
|
|
11832
|
+
{
|
|
11833
|
+
error: "signature required",
|
|
11834
|
+
message: "this sparkecoder is started with --accept-signed-only; every request must carry X-Signature, X-Signature-Timestamp, X-Signature-Algorithm."
|
|
11835
|
+
},
|
|
11836
|
+
401
|
|
11837
|
+
);
|
|
11838
|
+
}
|
|
11839
|
+
if (!warnedUnsigned) {
|
|
11840
|
+
warnedUnsigned = true;
|
|
11841
|
+
console.warn(
|
|
11842
|
+
`[personal-agent] inbound ${c.req.method} ${path} arrived without X-Signature; allowed because --accept-signed-only is off`
|
|
11843
|
+
);
|
|
11844
|
+
}
|
|
11845
|
+
return next();
|
|
11846
|
+
}
|
|
11847
|
+
let body;
|
|
11848
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
11849
|
+
body = Buffer.from(await c.req.raw.clone().arrayBuffer());
|
|
11850
|
+
}
|
|
11851
|
+
const result = verifyRequest({
|
|
11852
|
+
publicKey,
|
|
11853
|
+
method: c.req.method,
|
|
11854
|
+
path,
|
|
11855
|
+
body,
|
|
11856
|
+
signatureB64: sig,
|
|
11857
|
+
timestampSeconds: ts,
|
|
11858
|
+
algorithm: alg
|
|
11859
|
+
});
|
|
11860
|
+
if (!result.ok) {
|
|
11861
|
+
console.warn(
|
|
11862
|
+
`[personal-agent] signature verification failed on ${c.req.method} ${path}: ${result.reason}${result.detail ? ` (${result.detail})` : ""}`
|
|
11863
|
+
);
|
|
11864
|
+
return c.json(
|
|
11865
|
+
{
|
|
11866
|
+
error: "signature verification failed",
|
|
11867
|
+
reason: result.reason,
|
|
11868
|
+
detail: result.detail
|
|
11869
|
+
},
|
|
11870
|
+
401
|
|
11871
|
+
);
|
|
11872
|
+
}
|
|
11873
|
+
return next();
|
|
11874
|
+
};
|
|
11875
|
+
}
|
|
11876
|
+
|
|
11877
|
+
// src/personal-agent/pty-server.ts
|
|
11878
|
+
import { hostname as hostname3 } from "os";
|
|
11879
|
+
import { WebSocketServer } from "ws";
|
|
11880
|
+
var RESIZE_RE = /\x1b\[RESIZE:(\d+);(\d+)\]/;
|
|
11881
|
+
var _ptyMod = null;
|
|
11882
|
+
async function loadPty() {
|
|
11883
|
+
if (_ptyMod) return _ptyMod;
|
|
11884
|
+
try {
|
|
11885
|
+
const mod = await import("node-pty");
|
|
11886
|
+
_ptyMod = mod;
|
|
11887
|
+
return mod;
|
|
11888
|
+
} catch (e) {
|
|
11889
|
+
console.warn(
|
|
11890
|
+
`[personal-agent] node-pty failed to load; /pty WebSocket disabled. (${e.message})`
|
|
11891
|
+
);
|
|
11892
|
+
return null;
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
function defaultShell() {
|
|
11896
|
+
if (process.platform === "win32") {
|
|
11897
|
+
return { file: process.env.COMSPEC || "cmd.exe", args: [] };
|
|
11898
|
+
}
|
|
11899
|
+
return { file: process.env.SHELL || "/bin/zsh", args: ["-l"] };
|
|
11900
|
+
}
|
|
11901
|
+
function cleanEnv() {
|
|
11902
|
+
const env = {};
|
|
11903
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
11904
|
+
if (typeof v === "string") env[k] = v;
|
|
11905
|
+
}
|
|
11906
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
11907
|
+
if (!env.LANG) env.LANG = "en_US.UTF-8";
|
|
11908
|
+
return env;
|
|
11909
|
+
}
|
|
11910
|
+
function parseUpgrade(req) {
|
|
11911
|
+
const url = new URL(req.url || "/", "http://placeholder");
|
|
11912
|
+
const cols = clampInt(url.searchParams.get("cols"), 80, 10, 500);
|
|
11913
|
+
const rows = clampInt(url.searchParams.get("rows"), 24, 5, 500);
|
|
11914
|
+
const cwd = url.searchParams.get("cwd") || void 0;
|
|
11915
|
+
const shell = url.searchParams.get("shell") || void 0;
|
|
11916
|
+
return {
|
|
11917
|
+
cols,
|
|
11918
|
+
rows,
|
|
11919
|
+
cwd,
|
|
11920
|
+
shell,
|
|
11921
|
+
hwidHeader: headerStr(req, "x-device-hwid") ?? url.searchParams.get("hwid") ?? void 0,
|
|
11922
|
+
sigHeader: headerStr(req, "x-signature") ?? url.searchParams.get("sig") ?? void 0,
|
|
11923
|
+
tsHeader: headerStr(req, "x-signature-timestamp") ?? url.searchParams.get("ts") ?? void 0,
|
|
11924
|
+
algHeader: headerStr(req, "x-signature-algorithm") ?? url.searchParams.get("alg") ?? void 0
|
|
11925
|
+
};
|
|
11926
|
+
}
|
|
11927
|
+
function clampInt(v, dflt, lo, hi) {
|
|
11928
|
+
if (!v) return dflt;
|
|
11929
|
+
const n = parseInt(v, 10);
|
|
11930
|
+
if (!Number.isFinite(n)) return dflt;
|
|
11931
|
+
return Math.max(lo, Math.min(hi, n));
|
|
11932
|
+
}
|
|
11933
|
+
function headerStr(req, name) {
|
|
11934
|
+
const v = req.headers[name];
|
|
11935
|
+
if (Array.isArray(v)) return v[0];
|
|
11936
|
+
return v;
|
|
11937
|
+
}
|
|
11938
|
+
function authenticate(parsed, path, pubKey, signedOnly) {
|
|
11939
|
+
if (parsed.hwidHeader) {
|
|
11940
|
+
const expected = getHardwareIdCached();
|
|
11941
|
+
if (parsed.hwidHeader !== expected) {
|
|
11942
|
+
return {
|
|
11943
|
+
ok: false,
|
|
11944
|
+
status: 409,
|
|
11945
|
+
reason: `hwid mismatch: got ${parsed.hwidHeader.slice(0, 12)}\u2026, expected ${expected.slice(0, 12)}\u2026`
|
|
11946
|
+
};
|
|
11947
|
+
}
|
|
11948
|
+
}
|
|
11949
|
+
if (!pubKey) {
|
|
11950
|
+
if (signedOnly) {
|
|
11951
|
+
return { ok: false, status: 500, reason: "signature required but no public key configured" };
|
|
11952
|
+
}
|
|
11953
|
+
return { ok: true };
|
|
11954
|
+
}
|
|
11955
|
+
if (!parsed.sigHeader) {
|
|
11956
|
+
if (signedOnly) {
|
|
11957
|
+
return { ok: false, status: 401, reason: "missing X-Signature on upgrade request" };
|
|
11958
|
+
}
|
|
11959
|
+
return { ok: true };
|
|
11960
|
+
}
|
|
11961
|
+
const result = verifyRequest({
|
|
11962
|
+
publicKey: pubKey,
|
|
11963
|
+
method: "GET",
|
|
11964
|
+
path,
|
|
11965
|
+
body: void 0,
|
|
11966
|
+
signatureB64: parsed.sigHeader,
|
|
11967
|
+
timestampSeconds: parsed.tsHeader,
|
|
11968
|
+
algorithm: parsed.algHeader
|
|
11969
|
+
});
|
|
11970
|
+
if (!result.ok) {
|
|
11971
|
+
return { ok: false, status: 401, reason: `signature verification failed: ${result.reason}` };
|
|
11972
|
+
}
|
|
11973
|
+
return { ok: true };
|
|
11974
|
+
}
|
|
11975
|
+
function rejectUpgrade(socket, status, reason) {
|
|
11976
|
+
const body = JSON.stringify({ error: reason });
|
|
11977
|
+
socket.write(
|
|
11978
|
+
`HTTP/1.1 ${status} ${reasonText(status)}\r
|
|
11979
|
+
Content-Type: application/json\r
|
|
11980
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
11981
|
+
Connection: close\r
|
|
11982
|
+
\r
|
|
11983
|
+
` + body
|
|
11984
|
+
);
|
|
11985
|
+
socket.destroy();
|
|
11986
|
+
}
|
|
11987
|
+
function reasonText(status) {
|
|
11988
|
+
switch (status) {
|
|
11989
|
+
case 401:
|
|
11990
|
+
return "Unauthorized";
|
|
11991
|
+
case 409:
|
|
11992
|
+
return "Conflict";
|
|
11993
|
+
case 500:
|
|
11994
|
+
return "Internal Server Error";
|
|
11995
|
+
case 503:
|
|
11996
|
+
return "Service Unavailable";
|
|
11997
|
+
default:
|
|
11998
|
+
return "Error";
|
|
11999
|
+
}
|
|
12000
|
+
}
|
|
12001
|
+
function attachPtyServer(httpServer, opts = {}) {
|
|
12002
|
+
const path = opts.path ?? "/pty";
|
|
12003
|
+
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
12004
|
+
const acceptSignedOnly = process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
12005
|
+
const publicKeyInput = process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
12006
|
+
let pubKey = null;
|
|
12007
|
+
if (publicKeyInput) {
|
|
12008
|
+
try {
|
|
12009
|
+
pubKey = loadPublicKey(publicKeyInput);
|
|
12010
|
+
} catch (e) {
|
|
12011
|
+
console.warn(
|
|
12012
|
+
`[personal-agent] /pty signature verification disabled \u2014 failed to load public key: ${e.message}`
|
|
12013
|
+
);
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
const handler = (req, socket, head) => {
|
|
12017
|
+
let pathname = "/";
|
|
12018
|
+
try {
|
|
12019
|
+
pathname = new URL(req.url || "/", "http://placeholder").pathname;
|
|
12020
|
+
} catch {
|
|
12021
|
+
}
|
|
12022
|
+
if (pathname !== path) return;
|
|
12023
|
+
const parsed = parseUpgrade(req);
|
|
12024
|
+
const auth = authenticate(parsed, pathname, pubKey, acceptSignedOnly);
|
|
12025
|
+
if (!auth.ok) {
|
|
12026
|
+
console.warn(`[personal-agent] rejecting /pty upgrade: ${auth.reason}`);
|
|
12027
|
+
rejectUpgrade(socket, auth.status ?? 401, auth.reason ?? "unauthorized");
|
|
12028
|
+
return;
|
|
12029
|
+
}
|
|
12030
|
+
void loadPty().then((pty) => {
|
|
12031
|
+
if (!pty) {
|
|
12032
|
+
rejectUpgrade(
|
|
12033
|
+
socket,
|
|
12034
|
+
503,
|
|
12035
|
+
"node-pty is not available on this device (failed to load native module)"
|
|
12036
|
+
);
|
|
12037
|
+
return;
|
|
12038
|
+
}
|
|
12039
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
12040
|
+
spawnPty(ws, pty, parsed, opts);
|
|
12041
|
+
});
|
|
12042
|
+
});
|
|
12043
|
+
};
|
|
12044
|
+
httpServer.on("upgrade", handler);
|
|
12045
|
+
if (!opts.quiet) {
|
|
12046
|
+
console.log(
|
|
12047
|
+
`[personal-agent] WebSocket PTY server attached at ${path} (host: ${hostname3()}, sigCheck: ${pubKey ? "on" : "off"})`
|
|
12048
|
+
);
|
|
12049
|
+
}
|
|
12050
|
+
return {
|
|
12051
|
+
close: () => {
|
|
12052
|
+
httpServer.off("upgrade", handler);
|
|
12053
|
+
wss.close();
|
|
12054
|
+
}
|
|
12055
|
+
};
|
|
12056
|
+
}
|
|
12057
|
+
function spawnPty(ws, pty, parsed, opts) {
|
|
12058
|
+
const { file, args } = (() => {
|
|
12059
|
+
if (parsed.shell || opts.shell) {
|
|
12060
|
+
const s = parsed.shell ?? opts.shell;
|
|
12061
|
+
return { file: s, args: process.platform === "win32" ? [] : ["-l"] };
|
|
12062
|
+
}
|
|
12063
|
+
return defaultShell();
|
|
12064
|
+
})();
|
|
12065
|
+
const cwd = parsed.cwd ?? opts.cwd ?? process.env.HOME ?? process.cwd();
|
|
12066
|
+
let proc;
|
|
12067
|
+
try {
|
|
12068
|
+
proc = pty.spawn(file, args, {
|
|
12069
|
+
name: "xterm-256color",
|
|
12070
|
+
cols: parsed.cols,
|
|
12071
|
+
rows: parsed.rows,
|
|
12072
|
+
cwd,
|
|
12073
|
+
env: cleanEnv()
|
|
12074
|
+
});
|
|
12075
|
+
} catch (e) {
|
|
12076
|
+
const msg = e.message;
|
|
12077
|
+
safeSend(ws, `\r
|
|
12078
|
+
\x1B[31mFailed to spawn shell: ${msg}\x1B[0m\r
|
|
12079
|
+
`);
|
|
12080
|
+
try {
|
|
12081
|
+
ws.close();
|
|
12082
|
+
} catch {
|
|
12083
|
+
}
|
|
12084
|
+
return;
|
|
12085
|
+
}
|
|
12086
|
+
const banner = `\x1B[90m[sparkecoder pty] ${file} on ${hostname3()} pid=${proc.pid} ${parsed.cols}x${parsed.rows}\x1B[0m\r
|
|
12087
|
+
`;
|
|
12088
|
+
safeSend(ws, banner);
|
|
12089
|
+
proc.onData((data) => safeSend(ws, data));
|
|
12090
|
+
proc.onExit(({ exitCode }) => {
|
|
12091
|
+
safeSend(ws, `\r
|
|
12092
|
+
\x1B[90m[exit ${exitCode}]\x1B[0m\r
|
|
12093
|
+
`);
|
|
12094
|
+
try {
|
|
12095
|
+
ws.close();
|
|
12096
|
+
} catch {
|
|
12097
|
+
}
|
|
12098
|
+
});
|
|
12099
|
+
ws.on("message", (msg, isBinary) => {
|
|
12100
|
+
const input = typeof msg === "string" ? msg : Buffer.isBuffer(msg) ? msg.toString(isBinary ? "utf8" : "utf8") : Array.isArray(msg) ? Buffer.concat(msg).toString("utf8") : "";
|
|
12101
|
+
if (!input) return;
|
|
12102
|
+
const m = input.match(RESIZE_RE);
|
|
12103
|
+
if (m) {
|
|
12104
|
+
const cols = clampInt(m[1], 80, 10, 500);
|
|
12105
|
+
const rows = clampInt(m[2], 24, 5, 500);
|
|
12106
|
+
try {
|
|
12107
|
+
proc.resize(cols, rows);
|
|
12108
|
+
} catch {
|
|
12109
|
+
}
|
|
12110
|
+
if (input.replace(RESIZE_RE, "").length === 0) return;
|
|
12111
|
+
proc.write(input.replace(RESIZE_RE, ""));
|
|
12112
|
+
return;
|
|
12113
|
+
}
|
|
12114
|
+
proc.write(input);
|
|
12115
|
+
});
|
|
12116
|
+
const onClose = () => {
|
|
12117
|
+
try {
|
|
12118
|
+
proc.kill();
|
|
12119
|
+
} catch {
|
|
12120
|
+
}
|
|
12121
|
+
};
|
|
12122
|
+
ws.on("close", onClose);
|
|
12123
|
+
ws.on("error", onClose);
|
|
12124
|
+
}
|
|
12125
|
+
function safeSend(ws, data) {
|
|
12126
|
+
if (ws.readyState !== 1) return;
|
|
12127
|
+
try {
|
|
12128
|
+
ws.send(data);
|
|
12129
|
+
} catch {
|
|
12130
|
+
}
|
|
12131
|
+
}
|
|
12132
|
+
|
|
10673
12133
|
// src/server/index.ts
|
|
10674
12134
|
init_config();
|
|
10675
12135
|
init_db();
|
|
10676
12136
|
|
|
10677
12137
|
// src/utils/dependencies.ts
|
|
10678
|
-
import { exec as
|
|
10679
|
-
import { promisify as
|
|
10680
|
-
import { platform as
|
|
10681
|
-
var
|
|
12138
|
+
import { exec as exec7 } from "child_process";
|
|
12139
|
+
import { promisify as promisify7 } from "util";
|
|
12140
|
+
import { platform as platform4 } from "os";
|
|
12141
|
+
var execAsync7 = promisify7(exec7);
|
|
10682
12142
|
function getInstallInstructions() {
|
|
10683
|
-
const os2 =
|
|
12143
|
+
const os2 = platform4();
|
|
10684
12144
|
if (os2 === "darwin") {
|
|
10685
12145
|
return `
|
|
10686
12146
|
Install tmux on macOS:
|
|
@@ -10711,7 +12171,7 @@ Install tmux:
|
|
|
10711
12171
|
}
|
|
10712
12172
|
async function checkTmux() {
|
|
10713
12173
|
try {
|
|
10714
|
-
const { stdout } = await
|
|
12174
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
10715
12175
|
const version = stdout.trim();
|
|
10716
12176
|
return {
|
|
10717
12177
|
available: true,
|
|
@@ -10760,11 +12220,11 @@ function getWebDirectory() {
|
|
|
10760
12220
|
try {
|
|
10761
12221
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
10762
12222
|
const webDir = resolve10(currentDir, "..", "web");
|
|
10763
|
-
if (
|
|
12223
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
10764
12224
|
return webDir;
|
|
10765
12225
|
}
|
|
10766
12226
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
10767
|
-
if (
|
|
12227
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
10768
12228
|
return altWebDir;
|
|
10769
12229
|
}
|
|
10770
12230
|
return null;
|
|
@@ -10822,23 +12282,23 @@ async function findWebPort(preferredPort) {
|
|
|
10822
12282
|
return { port: preferredPort, alreadyRunning: false };
|
|
10823
12283
|
}
|
|
10824
12284
|
function hasProductionBuild(webDir) {
|
|
10825
|
-
const buildIdPath =
|
|
10826
|
-
return
|
|
12285
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
12286
|
+
return existsSync19(buildIdPath);
|
|
10827
12287
|
}
|
|
10828
12288
|
function hasSourceFiles(webDir) {
|
|
10829
|
-
const appDir =
|
|
10830
|
-
const pagesDir =
|
|
10831
|
-
const rootAppDir =
|
|
10832
|
-
const rootPagesDir =
|
|
10833
|
-
return
|
|
12289
|
+
const appDir = join13(webDir, "src", "app");
|
|
12290
|
+
const pagesDir = join13(webDir, "src", "pages");
|
|
12291
|
+
const rootAppDir = join13(webDir, "app");
|
|
12292
|
+
const rootPagesDir = join13(webDir, "pages");
|
|
12293
|
+
return existsSync19(appDir) || existsSync19(pagesDir) || existsSync19(rootAppDir) || existsSync19(rootPagesDir);
|
|
10834
12294
|
}
|
|
10835
12295
|
function getStandaloneServerPath(webDir) {
|
|
10836
12296
|
const possiblePaths2 = [
|
|
10837
|
-
|
|
10838
|
-
|
|
12297
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
12298
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
10839
12299
|
];
|
|
10840
12300
|
for (const serverPath of possiblePaths2) {
|
|
10841
|
-
if (
|
|
12301
|
+
if (existsSync19(serverPath)) {
|
|
10842
12302
|
return serverPath;
|
|
10843
12303
|
}
|
|
10844
12304
|
}
|
|
@@ -10878,13 +12338,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10878
12338
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
10879
12339
|
return { process: null, port: actualPort };
|
|
10880
12340
|
}
|
|
10881
|
-
const usePnpm =
|
|
10882
|
-
const useNpm = !usePnpm &&
|
|
12341
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
12342
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
10883
12343
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
10884
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
12344
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
10885
12345
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
10886
12346
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
10887
|
-
const runtimeConfigPath =
|
|
12347
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
10888
12348
|
try {
|
|
10889
12349
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
10890
12350
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -10892,7 +12352,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10892
12352
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
10893
12353
|
}
|
|
10894
12354
|
const webEnv = {
|
|
10895
|
-
...
|
|
12355
|
+
...cleanEnv2,
|
|
10896
12356
|
PORT: String(actualPort)
|
|
10897
12357
|
// Next.js respects PORT env var
|
|
10898
12358
|
};
|
|
@@ -11006,12 +12466,28 @@ function stopWebUI() {
|
|
|
11006
12466
|
}
|
|
11007
12467
|
}
|
|
11008
12468
|
async function createApp(options = {}) {
|
|
11009
|
-
const app = new
|
|
12469
|
+
const app = new Hono7();
|
|
11010
12470
|
app.use("*", cors({
|
|
11011
12471
|
origin: "*",
|
|
11012
12472
|
// Allow all origins
|
|
11013
12473
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11014
|
-
allowHeaders: [
|
|
12474
|
+
allowHeaders: [
|
|
12475
|
+
"Content-Type",
|
|
12476
|
+
"Authorization",
|
|
12477
|
+
"X-Requested-With",
|
|
12478
|
+
// Personal-agent dashboard signs every request to the device with
|
|
12479
|
+
// these. Without them whitelisted the browser preflight strips the
|
|
12480
|
+
// headers and the signature middleware 401s.
|
|
12481
|
+
"X-Signature",
|
|
12482
|
+
"X-Signature-Timestamp",
|
|
12483
|
+
"X-Signature-Algorithm",
|
|
12484
|
+
"X-Device-Hwid",
|
|
12485
|
+
// Short-lived embed token used by the iframed SparkECoder web UI
|
|
12486
|
+
// when it's hosted inside the personal-agents dashboard. The
|
|
12487
|
+
// bootstrap in `web/src/lib/embed-bootstrap.ts` adds this header
|
|
12488
|
+
// to every API call.
|
|
12489
|
+
"X-Embed-Token"
|
|
12490
|
+
],
|
|
11015
12491
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11016
12492
|
maxAge: 86400
|
|
11017
12493
|
// 24 hours
|
|
@@ -11019,12 +12495,15 @@ async function createApp(options = {}) {
|
|
|
11019
12495
|
if (!options.quiet) {
|
|
11020
12496
|
app.use("*", logger());
|
|
11021
12497
|
}
|
|
12498
|
+
app.use("*", hwidMiddleware());
|
|
12499
|
+
app.use("*", signatureMiddleware());
|
|
11022
12500
|
app.route("/health", health);
|
|
11023
12501
|
app.route("/sessions", sessions);
|
|
11024
12502
|
app.route("/agents", agents);
|
|
11025
12503
|
app.route("/sessions", terminals);
|
|
11026
12504
|
app.route("/terminals", terminals);
|
|
11027
12505
|
app.route("/tasks", tasks_default);
|
|
12506
|
+
app.route("/system", system);
|
|
11028
12507
|
app.get("/openapi.json", async (c) => {
|
|
11029
12508
|
return c.json(generateOpenAPISpec());
|
|
11030
12509
|
});
|
|
@@ -11077,8 +12556,8 @@ async function startServer(options = {}) {
|
|
|
11077
12556
|
if (options.workingDirectory) {
|
|
11078
12557
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11079
12558
|
}
|
|
11080
|
-
if (!
|
|
11081
|
-
|
|
12559
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
12560
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11082
12561
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11083
12562
|
}
|
|
11084
12563
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11113,6 +12592,15 @@ async function startServer(options = {}) {
|
|
|
11113
12592
|
port,
|
|
11114
12593
|
hostname: host
|
|
11115
12594
|
});
|
|
12595
|
+
try {
|
|
12596
|
+
attachPtyServer(serverInstance, {
|
|
12597
|
+
quiet: options.quiet
|
|
12598
|
+
});
|
|
12599
|
+
} catch (e) {
|
|
12600
|
+
if (!options.quiet) {
|
|
12601
|
+
console.warn(` \u26A0 Failed to attach /pty WebSocket server: ${e.message}`);
|
|
12602
|
+
}
|
|
12603
|
+
}
|
|
11116
12604
|
let webPort;
|
|
11117
12605
|
let webStarted;
|
|
11118
12606
|
if (options.webUI !== false) {
|