sparkecoder 0.1.86 → 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 +661 -39
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +1996 -225
- package/dist/cli.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 +1683 -199
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +1683 -199
- 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 +604 -10
- 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/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/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → 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)
|
|
@@ -1535,7 +1546,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1535
1546
|
globs: parsed.metadata.globs,
|
|
1536
1547
|
loadType,
|
|
1537
1548
|
priority,
|
|
1538
|
-
sourceDir: directory
|
|
1549
|
+
sourceDir: directory,
|
|
1550
|
+
platforms: parsed.metadata.platforms
|
|
1539
1551
|
});
|
|
1540
1552
|
} else {
|
|
1541
1553
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1548,11 +1560,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1548
1560
|
globs: [],
|
|
1549
1561
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1550
1562
|
priority,
|
|
1551
|
-
sourceDir: directory
|
|
1563
|
+
sourceDir: directory,
|
|
1564
|
+
platforms: []
|
|
1552
1565
|
});
|
|
1553
1566
|
}
|
|
1554
1567
|
}
|
|
1555
|
-
return skills
|
|
1568
|
+
return skills.filter(
|
|
1569
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1570
|
+
);
|
|
1556
1571
|
}
|
|
1557
1572
|
async function loadAllSkills(directories) {
|
|
1558
1573
|
const allSkills = [];
|
|
@@ -2149,6 +2164,7 @@ __export(webhook_exports, {
|
|
|
2149
2164
|
sendWebhook: () => sendWebhook
|
|
2150
2165
|
});
|
|
2151
2166
|
async function sendWebhook(url, event) {
|
|
2167
|
+
const t0 = Date.now();
|
|
2152
2168
|
try {
|
|
2153
2169
|
const controller = new AbortController();
|
|
2154
2170
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2162,17 +2178,36 @@ async function sendWebhook(url, event) {
|
|
|
2162
2178
|
signal: controller.signal
|
|
2163
2179
|
});
|
|
2164
2180
|
clearTimeout(timeout);
|
|
2181
|
+
const ms = Date.now() - t0;
|
|
2165
2182
|
if (!response.ok) {
|
|
2166
|
-
|
|
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
|
+
);
|
|
2167
2193
|
}
|
|
2168
2194
|
} catch (err) {
|
|
2169
2195
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2170
|
-
console.warn(
|
|
2196
|
+
console.warn(
|
|
2197
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
2198
|
+
);
|
|
2171
2199
|
}
|
|
2172
2200
|
}
|
|
2201
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2173
2202
|
var init_webhook = __esm({
|
|
2174
2203
|
"src/utils/webhook.ts"() {
|
|
2175
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";
|
|
2176
2211
|
}
|
|
2177
2212
|
});
|
|
2178
2213
|
|
|
@@ -2382,15 +2417,15 @@ var recorder_exports = {};
|
|
|
2382
2417
|
__export(recorder_exports, {
|
|
2383
2418
|
FrameRecorder: () => FrameRecorder
|
|
2384
2419
|
});
|
|
2385
|
-
import { exec as
|
|
2386
|
-
import { promisify as
|
|
2420
|
+
import { exec as exec6 } from "child_process";
|
|
2421
|
+
import { promisify as promisify6 } from "util";
|
|
2387
2422
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
2388
|
-
import { join as
|
|
2389
|
-
import { tmpdir } from "os";
|
|
2390
|
-
import { nanoid as
|
|
2423
|
+
import { join as join9 } from "path";
|
|
2424
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2425
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2391
2426
|
async function checkFfmpeg() {
|
|
2392
2427
|
try {
|
|
2393
|
-
await
|
|
2428
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
2394
2429
|
return true;
|
|
2395
2430
|
} catch {
|
|
2396
2431
|
return false;
|
|
@@ -2402,11 +2437,11 @@ async function cleanup(dir) {
|
|
|
2402
2437
|
} catch {
|
|
2403
2438
|
}
|
|
2404
2439
|
}
|
|
2405
|
-
var
|
|
2440
|
+
var execAsync6, FrameRecorder;
|
|
2406
2441
|
var init_recorder = __esm({
|
|
2407
2442
|
"src/browser/recorder.ts"() {
|
|
2408
2443
|
"use strict";
|
|
2409
|
-
|
|
2444
|
+
execAsync6 = promisify6(exec6);
|
|
2410
2445
|
FrameRecorder = class {
|
|
2411
2446
|
frames = [];
|
|
2412
2447
|
startTime = null;
|
|
@@ -2442,21 +2477,21 @@ var init_recorder = __esm({
|
|
|
2442
2477
|
*/
|
|
2443
2478
|
async encode() {
|
|
2444
2479
|
if (this.frames.length === 0) return null;
|
|
2445
|
-
const workDir =
|
|
2480
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid4(8)}`);
|
|
2446
2481
|
await mkdir4(workDir, { recursive: true });
|
|
2447
2482
|
try {
|
|
2448
2483
|
for (let i = 0; i < this.frames.length; i++) {
|
|
2449
|
-
const framePath =
|
|
2484
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
2450
2485
|
await writeFile5(framePath, this.frames[i].data);
|
|
2451
2486
|
}
|
|
2452
2487
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
2453
2488
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
2454
2489
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
2455
|
-
const outputPath =
|
|
2490
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
2456
2491
|
const hasFfmpeg = await checkFfmpeg();
|
|
2457
2492
|
if (hasFfmpeg) {
|
|
2458
|
-
await
|
|
2459
|
-
`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}"`,
|
|
2460
2495
|
{ timeout: 12e4 }
|
|
2461
2496
|
);
|
|
2462
2497
|
} else {
|
|
@@ -2468,7 +2503,7 @@ var init_recorder = __esm({
|
|
|
2468
2503
|
const files = await readdir5(workDir);
|
|
2469
2504
|
for (const f of files) {
|
|
2470
2505
|
if (f.startsWith("frame_")) {
|
|
2471
|
-
await unlink2(
|
|
2506
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
2472
2507
|
});
|
|
2473
2508
|
}
|
|
2474
2509
|
}
|
|
@@ -2493,7 +2528,7 @@ var init_recorder = __esm({
|
|
|
2493
2528
|
import {
|
|
2494
2529
|
streamText as streamText2,
|
|
2495
2530
|
generateText as generateText3,
|
|
2496
|
-
tool as
|
|
2531
|
+
tool as tool14,
|
|
2497
2532
|
stepCountIs as stepCountIs2
|
|
2498
2533
|
} from "ai";
|
|
2499
2534
|
|
|
@@ -2684,8 +2719,8 @@ var SUBAGENT_MODELS = {
|
|
|
2684
2719
|
// src/agent/index.ts
|
|
2685
2720
|
init_db();
|
|
2686
2721
|
init_config();
|
|
2687
|
-
import { z as
|
|
2688
|
-
import { nanoid as
|
|
2722
|
+
import { z as z15 } from "zod";
|
|
2723
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2689
2724
|
|
|
2690
2725
|
// src/tools/bash.ts
|
|
2691
2726
|
import { tool } from "ai";
|
|
@@ -3769,12 +3804,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
3769
3804
|
}
|
|
3770
3805
|
async function commandExists(cmd) {
|
|
3771
3806
|
try {
|
|
3772
|
-
const { exec:
|
|
3773
|
-
const { promisify:
|
|
3774
|
-
const
|
|
3807
|
+
const { exec: exec8 } = await import("child_process");
|
|
3808
|
+
const { promisify: promisify8 } = await import("util");
|
|
3809
|
+
const execAsync8 = promisify8(exec8);
|
|
3775
3810
|
const isWindows = process.platform === "win32";
|
|
3776
3811
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
3777
|
-
await
|
|
3812
|
+
await execAsync8(checkCmd);
|
|
3778
3813
|
return true;
|
|
3779
3814
|
} catch {
|
|
3780
3815
|
return false;
|
|
@@ -6165,6 +6200,568 @@ function createUploadFileTool(options) {
|
|
|
6165
6200
|
});
|
|
6166
6201
|
}
|
|
6167
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
|
+
|
|
6168
6765
|
// src/tools/index.ts
|
|
6169
6766
|
init_semantic();
|
|
6170
6767
|
init_remote();
|
|
@@ -6213,6 +6810,20 @@ async function createTools(options) {
|
|
|
6213
6810
|
sessionId: options.sessionId
|
|
6214
6811
|
});
|
|
6215
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
|
+
}
|
|
6216
6827
|
if (options.enableSemanticSearch !== false) {
|
|
6217
6828
|
try {
|
|
6218
6829
|
if (isVectorGatewayConfigured()) {
|
|
@@ -6243,11 +6854,11 @@ init_db();
|
|
|
6243
6854
|
init_todo();
|
|
6244
6855
|
import os from "os";
|
|
6245
6856
|
function getSearchInstructions() {
|
|
6246
|
-
const
|
|
6857
|
+
const platform5 = process.platform;
|
|
6247
6858
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
6248
6859
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
6249
6860
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
6250
|
-
if (
|
|
6861
|
+
if (platform5 === "win32") {
|
|
6251
6862
|
return `${common}
|
|
6252
6863
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
6253
6864
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -6294,13 +6905,13 @@ async function buildSystemPrompt(options) {
|
|
|
6294
6905
|
);
|
|
6295
6906
|
const hasNoTodos = todos.length === 0;
|
|
6296
6907
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6297
|
-
const
|
|
6908
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6298
6909
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6299
6910
|
const searchInstructions = getSearchInstructions();
|
|
6300
6911
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
6301
6912
|
|
|
6302
6913
|
## Environment
|
|
6303
|
-
- **Platform**: ${
|
|
6914
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
6304
6915
|
- **Date**: ${currentDate}
|
|
6305
6916
|
- **Working Directory**: ${workingDirectory}
|
|
6306
6917
|
|
|
@@ -7238,10 +7849,14 @@ var Agent = class _Agent {
|
|
|
7238
7849
|
*/
|
|
7239
7850
|
async createToolsWithCallbacks(options) {
|
|
7240
7851
|
const config = getConfig();
|
|
7852
|
+
const sessionConfig = this.session.config || {};
|
|
7241
7853
|
return createTools({
|
|
7242
7854
|
sessionId: this.session.id,
|
|
7243
7855
|
workingDirectory: this.session.workingDirectory,
|
|
7244
7856
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7857
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7858
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7859
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
7245
7860
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
7246
7861
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
7247
7862
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -7274,10 +7889,14 @@ var Agent = class _Agent {
|
|
|
7274
7889
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
7275
7890
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
7276
7891
|
});
|
|
7892
|
+
const sessionConfig = session.config || {};
|
|
7277
7893
|
const tools = await createTools({
|
|
7278
7894
|
sessionId: session.id,
|
|
7279
7895
|
workingDirectory: session.workingDirectory,
|
|
7280
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
7896
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7897
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7898
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7899
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
7281
7900
|
});
|
|
7282
7901
|
return new _Agent(session, context, tools);
|
|
7283
7902
|
}
|
|
@@ -7450,10 +8069,10 @@ ${prompt}` });
|
|
|
7450
8069
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
7451
8070
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
7452
8071
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
7453
|
-
const fireWebhook = (
|
|
8072
|
+
const fireWebhook = (type2, data) => {
|
|
7454
8073
|
if (!webhookUrl) return;
|
|
7455
8074
|
sendWebhook(webhookUrl, {
|
|
7456
|
-
type,
|
|
8075
|
+
type: type2,
|
|
7457
8076
|
taskId: this.session.id,
|
|
7458
8077
|
sessionId: this.session.id,
|
|
7459
8078
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -7496,10 +8115,14 @@ ${prompt}` });
|
|
|
7496
8115
|
});
|
|
7497
8116
|
}
|
|
7498
8117
|
};
|
|
8118
|
+
const taskSessionConfig = this.session.config || {};
|
|
7499
8119
|
const taskTools = await createTools({
|
|
7500
8120
|
sessionId: this.session.id,
|
|
7501
8121
|
workingDirectory: this.session.workingDirectory,
|
|
7502
8122
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8123
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
8124
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
8125
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
7503
8126
|
onBashProgress: bashProgressHandler,
|
|
7504
8127
|
onWriteFileProgress: (progress) => {
|
|
7505
8128
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -7782,11 +8405,11 @@ ${taskAddendum}`;
|
|
|
7782
8405
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7783
8406
|
if (!isRemoteConfigured2()) return [];
|
|
7784
8407
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
7785
|
-
const { join:
|
|
8408
|
+
const { join: join14, basename: basename6 } = await import("path");
|
|
7786
8409
|
const urls = [];
|
|
7787
8410
|
for (const filePath of filePaths) {
|
|
7788
8411
|
try {
|
|
7789
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
8412
|
+
const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
|
|
7790
8413
|
const fileName = basename6(fullPath);
|
|
7791
8414
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
7792
8415
|
const mimeMap = {
|
|
@@ -7844,11 +8467,11 @@ ${taskAddendum}`;
|
|
|
7844
8467
|
wrappedTools[name] = originalTool;
|
|
7845
8468
|
continue;
|
|
7846
8469
|
}
|
|
7847
|
-
wrappedTools[name] =
|
|
8470
|
+
wrappedTools[name] = tool14({
|
|
7848
8471
|
description: originalTool.description || "",
|
|
7849
|
-
inputSchema: originalTool.inputSchema ||
|
|
8472
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
7850
8473
|
execute: async (input, toolOptions) => {
|
|
7851
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
8474
|
+
const toolCallId = toolOptions.toolCallId || nanoid5();
|
|
7852
8475
|
const execution = toolExecutionQueries.create({
|
|
7853
8476
|
sessionId: this.session.id,
|
|
7854
8477
|
toolName: name,
|
|
@@ -7866,10 +8489,10 @@ ${taskAddendum}`;
|
|
|
7866
8489
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
7867
8490
|
approvalResolvers.delete(toolCallId);
|
|
7868
8491
|
this.pendingApprovals.delete(toolCallId);
|
|
7869
|
-
const
|
|
8492
|
+
const exec8 = await execution;
|
|
7870
8493
|
if (!approved) {
|
|
7871
8494
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
7872
|
-
await toolExecutionQueries.reject(
|
|
8495
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
7873
8496
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7874
8497
|
return {
|
|
7875
8498
|
status: "rejected",
|
|
@@ -7879,14 +8502,14 @@ ${taskAddendum}`;
|
|
|
7879
8502
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
7880
8503
|
};
|
|
7881
8504
|
}
|
|
7882
|
-
await toolExecutionQueries.approve(
|
|
8505
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
7883
8506
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7884
8507
|
try {
|
|
7885
8508
|
const result = await originalTool.execute(input, toolOptions);
|
|
7886
|
-
await toolExecutionQueries.complete(
|
|
8509
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
7887
8510
|
return result;
|
|
7888
8511
|
} catch (error) {
|
|
7889
|
-
await toolExecutionQueries.complete(
|
|
8512
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
7890
8513
|
throw error;
|
|
7891
8514
|
}
|
|
7892
8515
|
}
|
|
@@ -7957,12 +8580,12 @@ ${taskAddendum}`;
|
|
|
7957
8580
|
|
|
7958
8581
|
// src/server/index.ts
|
|
7959
8582
|
import "dotenv/config";
|
|
7960
|
-
import { Hono as
|
|
8583
|
+
import { Hono as Hono7 } from "hono";
|
|
7961
8584
|
import { serve } from "@hono/node-server";
|
|
7962
8585
|
import { cors } from "hono/cors";
|
|
7963
8586
|
import { logger } from "hono/logger";
|
|
7964
|
-
import { existsSync as
|
|
7965
|
-
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";
|
|
7966
8589
|
import { spawn as spawn2 } from "child_process";
|
|
7967
8590
|
import { createServer as createNetServer } from "net";
|
|
7968
8591
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -7971,11 +8594,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
7971
8594
|
init_db();
|
|
7972
8595
|
import { Hono } from "hono";
|
|
7973
8596
|
import { zValidator } from "@hono/zod-validator";
|
|
7974
|
-
import { z as
|
|
7975
|
-
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";
|
|
7976
8599
|
import { readdir as readdir6 } from "fs/promises";
|
|
7977
|
-
import { join as
|
|
7978
|
-
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";
|
|
7979
8602
|
init_config();
|
|
7980
8603
|
|
|
7981
8604
|
// src/server/devtools-store.ts
|
|
@@ -8007,18 +8630,20 @@ function cleanupPendingInputs() {
|
|
|
8007
8630
|
}
|
|
8008
8631
|
}
|
|
8009
8632
|
}
|
|
8010
|
-
var createSessionSchema =
|
|
8011
|
-
name:
|
|
8012
|
-
workingDirectory:
|
|
8013
|
-
model:
|
|
8014
|
-
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()
|
|
8015
8640
|
});
|
|
8016
|
-
var paginationQuerySchema =
|
|
8017
|
-
limit:
|
|
8018
|
-
offset:
|
|
8641
|
+
var paginationQuerySchema = z16.object({
|
|
8642
|
+
limit: z16.string().optional(),
|
|
8643
|
+
offset: z16.string().optional()
|
|
8019
8644
|
});
|
|
8020
|
-
var messagesQuerySchema =
|
|
8021
|
-
limit:
|
|
8645
|
+
var messagesQuerySchema = z16.object({
|
|
8646
|
+
limit: z16.string().optional()
|
|
8022
8647
|
});
|
|
8023
8648
|
sessions.get(
|
|
8024
8649
|
"/",
|
|
@@ -8056,11 +8681,20 @@ sessions.post(
|
|
|
8056
8681
|
async (c) => {
|
|
8057
8682
|
const body = c.req.valid("json");
|
|
8058
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
|
+
};
|
|
8059
8693
|
const agent = await Agent.create({
|
|
8060
8694
|
name: body.name,
|
|
8061
8695
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8062
8696
|
model: body.model || config.defaultModel,
|
|
8063
|
-
sessionConfig:
|
|
8697
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8064
8698
|
});
|
|
8065
8699
|
const session = agent.getSession();
|
|
8066
8700
|
return c.json({
|
|
@@ -8157,10 +8791,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8157
8791
|
count: executions.length
|
|
8158
8792
|
});
|
|
8159
8793
|
});
|
|
8160
|
-
var updateSessionSchema =
|
|
8161
|
-
model:
|
|
8162
|
-
name:
|
|
8163
|
-
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()
|
|
8164
8798
|
});
|
|
8165
8799
|
sessions.patch(
|
|
8166
8800
|
"/:id",
|
|
@@ -8230,8 +8864,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8230
8864
|
await agent.clearContext();
|
|
8231
8865
|
return c.json({ success: true, sessionId: id });
|
|
8232
8866
|
});
|
|
8233
|
-
var pendingInputSchema =
|
|
8234
|
-
text:
|
|
8867
|
+
var pendingInputSchema = z16.object({
|
|
8868
|
+
text: z16.string()
|
|
8235
8869
|
});
|
|
8236
8870
|
sessions.post(
|
|
8237
8871
|
"/:id/pending-input",
|
|
@@ -8262,13 +8896,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
8262
8896
|
createdAt: pending.createdAt.toISOString()
|
|
8263
8897
|
});
|
|
8264
8898
|
});
|
|
8265
|
-
var devtoolsContextSchema =
|
|
8266
|
-
url:
|
|
8267
|
-
path:
|
|
8268
|
-
pageName:
|
|
8269
|
-
screenWidth:
|
|
8270
|
-
screenHeight:
|
|
8271
|
-
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()
|
|
8272
8906
|
});
|
|
8273
8907
|
sessions.post(
|
|
8274
8908
|
"/:id/devtools-context",
|
|
@@ -8454,12 +9088,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
8454
9088
|
});
|
|
8455
9089
|
function getAttachmentsDir(sessionId) {
|
|
8456
9090
|
const appDataDir = getAppDataDirectory();
|
|
8457
|
-
return
|
|
9091
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
8458
9092
|
}
|
|
8459
9093
|
function ensureAttachmentsDir(sessionId) {
|
|
8460
9094
|
const dir = getAttachmentsDir(sessionId);
|
|
8461
|
-
if (!
|
|
8462
|
-
|
|
9095
|
+
if (!existsSync16(dir)) {
|
|
9096
|
+
mkdirSync6(dir, { recursive: true });
|
|
8463
9097
|
}
|
|
8464
9098
|
return dir;
|
|
8465
9099
|
}
|
|
@@ -8470,12 +9104,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
8470
9104
|
return c.json({ error: "Session not found" }, 404);
|
|
8471
9105
|
}
|
|
8472
9106
|
const dir = getAttachmentsDir(sessionId);
|
|
8473
|
-
if (!
|
|
9107
|
+
if (!existsSync16(dir)) {
|
|
8474
9108
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
8475
9109
|
}
|
|
8476
9110
|
const files = readdirSync2(dir);
|
|
8477
9111
|
const attachments = files.map((filename) => {
|
|
8478
|
-
const filePath =
|
|
9112
|
+
const filePath = join10(dir, filename);
|
|
8479
9113
|
const stats = statSync2(filePath);
|
|
8480
9114
|
return {
|
|
8481
9115
|
id: filename.split("_")[0],
|
|
@@ -8507,10 +9141,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8507
9141
|
return c.json({ error: "No file provided" }, 400);
|
|
8508
9142
|
}
|
|
8509
9143
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8510
|
-
const id =
|
|
9144
|
+
const id = nanoid6(10);
|
|
8511
9145
|
const ext = extname8(file.name) || "";
|
|
8512
9146
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8513
|
-
const filePath =
|
|
9147
|
+
const filePath = join10(dir, safeFilename);
|
|
8514
9148
|
const arrayBuffer = await file.arrayBuffer();
|
|
8515
9149
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
8516
9150
|
return c.json({
|
|
@@ -8533,10 +9167,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8533
9167
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
8534
9168
|
}
|
|
8535
9169
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8536
|
-
const id =
|
|
9170
|
+
const id = nanoid6(10);
|
|
8537
9171
|
const ext = extname8(body.filename) || "";
|
|
8538
9172
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8539
|
-
const filePath =
|
|
9173
|
+
const filePath = join10(dir, safeFilename);
|
|
8540
9174
|
let base64Data = body.data;
|
|
8541
9175
|
if (base64Data.includes(",")) {
|
|
8542
9176
|
base64Data = base64Data.split(",")[1];
|
|
@@ -8565,7 +9199,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8565
9199
|
return c.json({ error: "Session not found" }, 404);
|
|
8566
9200
|
}
|
|
8567
9201
|
const dir = getAttachmentsDir(sessionId);
|
|
8568
|
-
if (!
|
|
9202
|
+
if (!existsSync16(dir)) {
|
|
8569
9203
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8570
9204
|
}
|
|
8571
9205
|
const files = readdirSync2(dir);
|
|
@@ -8573,14 +9207,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8573
9207
|
if (!file) {
|
|
8574
9208
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8575
9209
|
}
|
|
8576
|
-
const filePath =
|
|
8577
|
-
|
|
9210
|
+
const filePath = join10(dir, file);
|
|
9211
|
+
unlinkSync3(filePath);
|
|
8578
9212
|
return c.json({ success: true, id: attachmentId });
|
|
8579
9213
|
});
|
|
8580
|
-
var filesQuerySchema =
|
|
8581
|
-
query:
|
|
9214
|
+
var filesQuerySchema = z16.object({
|
|
9215
|
+
query: z16.string().optional(),
|
|
8582
9216
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
8583
|
-
limit:
|
|
9217
|
+
limit: z16.string().optional()
|
|
8584
9218
|
// Max results (default 50)
|
|
8585
9219
|
});
|
|
8586
9220
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -8656,7 +9290,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
8656
9290
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
8657
9291
|
for (const entry of entries) {
|
|
8658
9292
|
if (results.length >= limit * 2) break;
|
|
8659
|
-
const fullPath =
|
|
9293
|
+
const fullPath = join10(currentDir, entry.name);
|
|
8660
9294
|
const relativePath = relative9(baseDir, fullPath);
|
|
8661
9295
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
8662
9296
|
continue;
|
|
@@ -8704,7 +9338,7 @@ sessions.get(
|
|
|
8704
9338
|
return c.json({ error: "Session not found" }, 404);
|
|
8705
9339
|
}
|
|
8706
9340
|
const workingDirectory = session.workingDirectory;
|
|
8707
|
-
if (!
|
|
9341
|
+
if (!existsSync16(workingDirectory)) {
|
|
8708
9342
|
return c.json({
|
|
8709
9343
|
sessionId,
|
|
8710
9344
|
workingDirectory,
|
|
@@ -8814,9 +9448,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
8814
9448
|
init_db();
|
|
8815
9449
|
import { Hono as Hono2 } from "hono";
|
|
8816
9450
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
8817
|
-
import { z as
|
|
8818
|
-
import { existsSync as
|
|
8819
|
-
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";
|
|
8820
9454
|
init_config();
|
|
8821
9455
|
|
|
8822
9456
|
// src/server/resumable-stream.ts
|
|
@@ -8903,7 +9537,7 @@ var streamContext = createResumableStreamContext({
|
|
|
8903
9537
|
});
|
|
8904
9538
|
|
|
8905
9539
|
// src/server/routes/agents.ts
|
|
8906
|
-
import { nanoid as
|
|
9540
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
8907
9541
|
init_stream_proxy();
|
|
8908
9542
|
init_recorder();
|
|
8909
9543
|
init_remote();
|
|
@@ -8994,40 +9628,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
8994
9628
|
${prompt}`;
|
|
8995
9629
|
}
|
|
8996
9630
|
var agents = new Hono2();
|
|
8997
|
-
var attachmentSchema =
|
|
8998
|
-
type:
|
|
8999
|
-
data:
|
|
9631
|
+
var attachmentSchema = z17.object({
|
|
9632
|
+
type: z17.enum(["image", "file"]),
|
|
9633
|
+
data: z17.string(),
|
|
9000
9634
|
// base64 data URL or raw base64
|
|
9001
|
-
mediaType:
|
|
9002
|
-
filename:
|
|
9635
|
+
mediaType: z17.string().optional(),
|
|
9636
|
+
filename: z17.string().optional()
|
|
9003
9637
|
});
|
|
9004
|
-
var runPromptSchema =
|
|
9005
|
-
prompt:
|
|
9638
|
+
var runPromptSchema = z17.object({
|
|
9639
|
+
prompt: z17.string(),
|
|
9006
9640
|
// Can be empty if attachments are provided
|
|
9007
|
-
attachments:
|
|
9641
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
9008
9642
|
}).refine(
|
|
9009
9643
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
9010
9644
|
{ message: "Either prompt or attachments must be provided" }
|
|
9011
9645
|
);
|
|
9012
|
-
var quickStartSchema =
|
|
9013
|
-
prompt:
|
|
9014
|
-
name:
|
|
9015
|
-
workingDirectory:
|
|
9016
|
-
model:
|
|
9017
|
-
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()
|
|
9018
9652
|
});
|
|
9019
|
-
var rejectSchema =
|
|
9020
|
-
reason:
|
|
9653
|
+
var rejectSchema = z17.object({
|
|
9654
|
+
reason: z17.string().optional()
|
|
9021
9655
|
}).optional();
|
|
9022
9656
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9023
9657
|
function getAttachmentsDirectory(sessionId) {
|
|
9024
9658
|
const appDataDir = getAppDataDirectory();
|
|
9025
|
-
return
|
|
9659
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9026
9660
|
}
|
|
9027
9661
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9028
9662
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9029
|
-
if (!
|
|
9030
|
-
|
|
9663
|
+
if (!existsSync17(attachmentsDir)) {
|
|
9664
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9031
9665
|
}
|
|
9032
9666
|
let filename = attachment.filename;
|
|
9033
9667
|
if (!filename) {
|
|
@@ -9045,7 +9679,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9045
9679
|
attachment.mediaType = resized.mediaType;
|
|
9046
9680
|
attachment.data = buffer.toString("base64");
|
|
9047
9681
|
}
|
|
9048
|
-
const filePath =
|
|
9682
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9049
9683
|
writeFileSync4(filePath, buffer);
|
|
9050
9684
|
return filePath;
|
|
9051
9685
|
}
|
|
@@ -9056,9 +9690,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9056
9690
|
}
|
|
9057
9691
|
return data;
|
|
9058
9692
|
}
|
|
9059
|
-
function getExtensionFromMediaType(mediaType,
|
|
9693
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9060
9694
|
if (!mediaType) {
|
|
9061
|
-
return
|
|
9695
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9062
9696
|
}
|
|
9063
9697
|
const mimeToExt = {
|
|
9064
9698
|
"image/png": ".png",
|
|
@@ -9462,7 +10096,7 @@ ${prompt}` });
|
|
|
9462
10096
|
userMessageContent = prompt;
|
|
9463
10097
|
}
|
|
9464
10098
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
9465
|
-
const streamId = `stream_${id}_${
|
|
10099
|
+
const streamId = `stream_${id}_${nanoid7(10)}`;
|
|
9466
10100
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
9467
10101
|
await activeStreamQueries.create(id, streamId);
|
|
9468
10102
|
const stream = await streamContext.resumableStream(
|
|
@@ -9667,7 +10301,7 @@ agents.post(
|
|
|
9667
10301
|
});
|
|
9668
10302
|
const session = agent.getSession();
|
|
9669
10303
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
9670
|
-
const streamId = `stream_${session.id}_${
|
|
10304
|
+
const streamId = `stream_${session.id}_${nanoid7(10)}`;
|
|
9671
10305
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
9672
10306
|
await activeStreamQueries.create(session.id, streamId);
|
|
9673
10307
|
const createQuickStreamProducer = () => {
|
|
@@ -9934,23 +10568,23 @@ agents.post(
|
|
|
9934
10568
|
});
|
|
9935
10569
|
}
|
|
9936
10570
|
);
|
|
9937
|
-
var browserInputSchema =
|
|
9938
|
-
type:
|
|
9939
|
-
eventType:
|
|
9940
|
-
x:
|
|
9941
|
-
y:
|
|
9942
|
-
button:
|
|
9943
|
-
clickCount:
|
|
9944
|
-
deltaX:
|
|
9945
|
-
deltaY:
|
|
9946
|
-
key:
|
|
9947
|
-
code:
|
|
9948
|
-
text:
|
|
9949
|
-
modifiers:
|
|
9950
|
-
touchPoints:
|
|
9951
|
-
x:
|
|
9952
|
-
y:
|
|
9953
|
-
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()
|
|
9954
10588
|
})).optional()
|
|
9955
10589
|
});
|
|
9956
10590
|
agents.post(
|
|
@@ -9985,27 +10619,279 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
9985
10619
|
init_config();
|
|
9986
10620
|
import { Hono as Hono3 } from "hono";
|
|
9987
10621
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
9988
|
-
import { z as
|
|
9989
|
-
import { readFileSync as
|
|
10622
|
+
import { z as z18 } from "zod";
|
|
10623
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
9990
10624
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9991
|
-
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
|
|
9992
10878
|
var __filename = fileURLToPath3(import.meta.url);
|
|
9993
10879
|
var __dirname = dirname6(__filename);
|
|
9994
10880
|
var possiblePaths = [
|
|
9995
|
-
|
|
10881
|
+
join12(__dirname, "../package.json"),
|
|
9996
10882
|
// From dist/server -> dist/../package.json
|
|
9997
|
-
|
|
10883
|
+
join12(__dirname, "../../package.json"),
|
|
9998
10884
|
// From dist/server (if nested differently)
|
|
9999
|
-
|
|
10885
|
+
join12(__dirname, "../../../package.json"),
|
|
10000
10886
|
// From src/server/routes (development)
|
|
10001
|
-
|
|
10887
|
+
join12(process.cwd(), "package.json")
|
|
10002
10888
|
// From current working directory
|
|
10003
10889
|
];
|
|
10004
10890
|
var currentVersion = "0.0.0";
|
|
10005
10891
|
var packageName = "sparkecoder";
|
|
10006
10892
|
for (const packageJsonPath of possiblePaths) {
|
|
10007
10893
|
try {
|
|
10008
|
-
const packageJson = JSON.parse(
|
|
10894
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
10009
10895
|
if (packageJson.name === "sparkecoder") {
|
|
10010
10896
|
currentVersion = packageJson.version || "0.0.0";
|
|
10011
10897
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10020,11 +10906,17 @@ health.get("/", async (c) => {
|
|
|
10020
10906
|
const apiKeyStatus = getApiKeyStatus();
|
|
10021
10907
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10022
10908
|
const hasApiKey = gatewayKey?.configured ?? false;
|
|
10909
|
+
let hwid;
|
|
10910
|
+
try {
|
|
10911
|
+
hwid = getHardwareIdCached();
|
|
10912
|
+
} catch {
|
|
10913
|
+
}
|
|
10023
10914
|
return c.json({
|
|
10024
10915
|
status: "ok",
|
|
10025
10916
|
version: currentVersion,
|
|
10026
10917
|
uptime: process.uptime(),
|
|
10027
10918
|
apiKeyConfigured: hasApiKey,
|
|
10919
|
+
hwid,
|
|
10028
10920
|
config: {
|
|
10029
10921
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10030
10922
|
defaultModel: config.defaultModel,
|
|
@@ -10095,9 +10987,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10095
10987
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10096
10988
|
});
|
|
10097
10989
|
});
|
|
10098
|
-
var setApiKeySchema =
|
|
10099
|
-
provider:
|
|
10100
|
-
apiKey:
|
|
10990
|
+
var setApiKeySchema = z18.object({
|
|
10991
|
+
provider: z18.string(),
|
|
10992
|
+
apiKey: z18.string().min(1)
|
|
10101
10993
|
});
|
|
10102
10994
|
health.post(
|
|
10103
10995
|
"/api-keys",
|
|
@@ -10136,13 +11028,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10136
11028
|
// src/server/routes/terminals.ts
|
|
10137
11029
|
import { Hono as Hono4 } from "hono";
|
|
10138
11030
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10139
|
-
import { z as
|
|
11031
|
+
import { z as z19 } from "zod";
|
|
10140
11032
|
init_db();
|
|
10141
11033
|
var terminals = new Hono4();
|
|
10142
|
-
var spawnSchema =
|
|
10143
|
-
command:
|
|
10144
|
-
cwd:
|
|
10145
|
-
name:
|
|
11034
|
+
var spawnSchema = z19.object({
|
|
11035
|
+
command: z19.string(),
|
|
11036
|
+
cwd: z19.string().optional(),
|
|
11037
|
+
name: z19.string().optional()
|
|
10146
11038
|
});
|
|
10147
11039
|
terminals.post(
|
|
10148
11040
|
"/:sessionId/terminals",
|
|
@@ -10223,8 +11115,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10223
11115
|
// We don't track exit codes in tmux mode
|
|
10224
11116
|
});
|
|
10225
11117
|
});
|
|
10226
|
-
var logsQuerySchema =
|
|
10227
|
-
tail:
|
|
11118
|
+
var logsQuerySchema = z19.object({
|
|
11119
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10228
11120
|
});
|
|
10229
11121
|
terminals.get(
|
|
10230
11122
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -10248,8 +11140,8 @@ terminals.get(
|
|
|
10248
11140
|
});
|
|
10249
11141
|
}
|
|
10250
11142
|
);
|
|
10251
|
-
var killSchema =
|
|
10252
|
-
signal:
|
|
11143
|
+
var killSchema = z19.object({
|
|
11144
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
10253
11145
|
});
|
|
10254
11146
|
terminals.post(
|
|
10255
11147
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -10263,8 +11155,8 @@ terminals.post(
|
|
|
10263
11155
|
return c.json({ success: true, message: "Terminal killed" });
|
|
10264
11156
|
}
|
|
10265
11157
|
);
|
|
10266
|
-
var writeSchema =
|
|
10267
|
-
input:
|
|
11158
|
+
var writeSchema = z19.object({
|
|
11159
|
+
input: z19.string()
|
|
10268
11160
|
});
|
|
10269
11161
|
terminals.post(
|
|
10270
11162
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -10449,20 +11341,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
10449
11341
|
init_db();
|
|
10450
11342
|
import { Hono as Hono5 } from "hono";
|
|
10451
11343
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
10452
|
-
import { z as
|
|
10453
|
-
import { nanoid as
|
|
11344
|
+
import { z as z20 } from "zod";
|
|
11345
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
10454
11346
|
init_config();
|
|
10455
11347
|
var tasks = new Hono5();
|
|
10456
11348
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
10457
|
-
var createTaskSchema =
|
|
10458
|
-
prompt:
|
|
10459
|
-
outputSchema:
|
|
10460
|
-
webhookUrl:
|
|
10461
|
-
model:
|
|
10462
|
-
workingDirectory:
|
|
10463
|
-
name:
|
|
10464
|
-
maxIterations:
|
|
10465
|
-
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()
|
|
10466
11358
|
});
|
|
10467
11359
|
tasks.post(
|
|
10468
11360
|
"/",
|
|
@@ -10524,7 +11416,7 @@ tasks.post(
|
|
|
10524
11416
|
const taskId = agent.sessionId;
|
|
10525
11417
|
const abortController = new AbortController();
|
|
10526
11418
|
taskAbortControllers.set(taskId, abortController);
|
|
10527
|
-
const streamId = `stream_${taskId}_${
|
|
11419
|
+
const streamId = `stream_${taskId}_${nanoid8(10)}`;
|
|
10528
11420
|
await activeStreamQueries.create(taskId, streamId);
|
|
10529
11421
|
const taskStreamProducer = () => {
|
|
10530
11422
|
const { readable, writable } = new TransformStream();
|
|
@@ -10674,17 +11566,581 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10674
11566
|
});
|
|
10675
11567
|
var tasks_default = tasks;
|
|
10676
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
|
+
|
|
10677
12133
|
// src/server/index.ts
|
|
10678
12134
|
init_config();
|
|
10679
12135
|
init_db();
|
|
10680
12136
|
|
|
10681
12137
|
// src/utils/dependencies.ts
|
|
10682
|
-
import { exec as
|
|
10683
|
-
import { promisify as
|
|
10684
|
-
import { platform as
|
|
10685
|
-
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);
|
|
10686
12142
|
function getInstallInstructions() {
|
|
10687
|
-
const os2 =
|
|
12143
|
+
const os2 = platform4();
|
|
10688
12144
|
if (os2 === "darwin") {
|
|
10689
12145
|
return `
|
|
10690
12146
|
Install tmux on macOS:
|
|
@@ -10715,7 +12171,7 @@ Install tmux:
|
|
|
10715
12171
|
}
|
|
10716
12172
|
async function checkTmux() {
|
|
10717
12173
|
try {
|
|
10718
|
-
const { stdout } = await
|
|
12174
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
10719
12175
|
const version = stdout.trim();
|
|
10720
12176
|
return {
|
|
10721
12177
|
available: true,
|
|
@@ -10764,11 +12220,11 @@ function getWebDirectory() {
|
|
|
10764
12220
|
try {
|
|
10765
12221
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
10766
12222
|
const webDir = resolve10(currentDir, "..", "web");
|
|
10767
|
-
if (
|
|
12223
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
10768
12224
|
return webDir;
|
|
10769
12225
|
}
|
|
10770
12226
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
10771
|
-
if (
|
|
12227
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
10772
12228
|
return altWebDir;
|
|
10773
12229
|
}
|
|
10774
12230
|
return null;
|
|
@@ -10826,23 +12282,23 @@ async function findWebPort(preferredPort) {
|
|
|
10826
12282
|
return { port: preferredPort, alreadyRunning: false };
|
|
10827
12283
|
}
|
|
10828
12284
|
function hasProductionBuild(webDir) {
|
|
10829
|
-
const buildIdPath =
|
|
10830
|
-
return
|
|
12285
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
12286
|
+
return existsSync19(buildIdPath);
|
|
10831
12287
|
}
|
|
10832
12288
|
function hasSourceFiles(webDir) {
|
|
10833
|
-
const appDir =
|
|
10834
|
-
const pagesDir =
|
|
10835
|
-
const rootAppDir =
|
|
10836
|
-
const rootPagesDir =
|
|
10837
|
-
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);
|
|
10838
12294
|
}
|
|
10839
12295
|
function getStandaloneServerPath(webDir) {
|
|
10840
12296
|
const possiblePaths2 = [
|
|
10841
|
-
|
|
10842
|
-
|
|
12297
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
12298
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
10843
12299
|
];
|
|
10844
12300
|
for (const serverPath of possiblePaths2) {
|
|
10845
|
-
if (
|
|
12301
|
+
if (existsSync19(serverPath)) {
|
|
10846
12302
|
return serverPath;
|
|
10847
12303
|
}
|
|
10848
12304
|
}
|
|
@@ -10882,13 +12338,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10882
12338
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
10883
12339
|
return { process: null, port: actualPort };
|
|
10884
12340
|
}
|
|
10885
|
-
const usePnpm =
|
|
10886
|
-
const useNpm = !usePnpm &&
|
|
12341
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
12342
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
10887
12343
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
10888
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
12344
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
10889
12345
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
10890
12346
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
10891
|
-
const runtimeConfigPath =
|
|
12347
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
10892
12348
|
try {
|
|
10893
12349
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
10894
12350
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -10896,7 +12352,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10896
12352
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
10897
12353
|
}
|
|
10898
12354
|
const webEnv = {
|
|
10899
|
-
...
|
|
12355
|
+
...cleanEnv2,
|
|
10900
12356
|
PORT: String(actualPort)
|
|
10901
12357
|
// Next.js respects PORT env var
|
|
10902
12358
|
};
|
|
@@ -11010,12 +12466,28 @@ function stopWebUI() {
|
|
|
11010
12466
|
}
|
|
11011
12467
|
}
|
|
11012
12468
|
async function createApp(options = {}) {
|
|
11013
|
-
const app = new
|
|
12469
|
+
const app = new Hono7();
|
|
11014
12470
|
app.use("*", cors({
|
|
11015
12471
|
origin: "*",
|
|
11016
12472
|
// Allow all origins
|
|
11017
12473
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11018
|
-
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
|
+
],
|
|
11019
12491
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11020
12492
|
maxAge: 86400
|
|
11021
12493
|
// 24 hours
|
|
@@ -11023,12 +12495,15 @@ async function createApp(options = {}) {
|
|
|
11023
12495
|
if (!options.quiet) {
|
|
11024
12496
|
app.use("*", logger());
|
|
11025
12497
|
}
|
|
12498
|
+
app.use("*", hwidMiddleware());
|
|
12499
|
+
app.use("*", signatureMiddleware());
|
|
11026
12500
|
app.route("/health", health);
|
|
11027
12501
|
app.route("/sessions", sessions);
|
|
11028
12502
|
app.route("/agents", agents);
|
|
11029
12503
|
app.route("/sessions", terminals);
|
|
11030
12504
|
app.route("/terminals", terminals);
|
|
11031
12505
|
app.route("/tasks", tasks_default);
|
|
12506
|
+
app.route("/system", system);
|
|
11032
12507
|
app.get("/openapi.json", async (c) => {
|
|
11033
12508
|
return c.json(generateOpenAPISpec());
|
|
11034
12509
|
});
|
|
@@ -11081,8 +12556,8 @@ async function startServer(options = {}) {
|
|
|
11081
12556
|
if (options.workingDirectory) {
|
|
11082
12557
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11083
12558
|
}
|
|
11084
|
-
if (!
|
|
11085
|
-
|
|
12559
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
12560
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11086
12561
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11087
12562
|
}
|
|
11088
12563
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11117,6 +12592,15 @@ async function startServer(options = {}) {
|
|
|
11117
12592
|
port,
|
|
11118
12593
|
hostname: host
|
|
11119
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
|
+
}
|
|
11120
12604
|
let webPort;
|
|
11121
12605
|
let webStarted;
|
|
11122
12606
|
if (options.webUI !== false) {
|