sparkecoder 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +666 -40
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +2001 -226
- package/dist/cli.js.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/{index-OhuTM4a0.d.ts → index-BvIissiB.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1688 -200
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +1688 -200
- package/dist/server/index.js.map +1 -1
- package/dist/skills/default/computer-use.md +150 -0
- package/dist/tools/index.d.ts +167 -1
- package/dist/tools/index.js +609 -11
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -1
- package/src/skills/default/computer-use.md +150 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ecd2bdca._.js → 2374f_317b1fef._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9adc1edb._.js → 2374f_37dd9702._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8dc0f9aa._.js → 2374f_4d44e4ed._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_cc6c6363._.js → 2374f_54ac917f._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_00f7fe07._.js → 2374f_86585101._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_369747ce._.js → 2374f_a383a4d9._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d58d0276._.js → 2374f_c59a35bb._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__25b25c9d._.js → [root-of-the-server]__9a826344._.js} +2 -2
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/package-lock.json +3 -3
- package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
- package/web/.next/standalone/web/src/lib/embed-bootstrap.ts +108 -0
- package/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/static/chunks/5383c5717758f575.js +0 -7
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
package/dist/server/index.js
CHANGED
|
@@ -447,7 +447,11 @@ var init_remote = __esm({
|
|
|
447
447
|
return result.files;
|
|
448
448
|
},
|
|
449
449
|
async getDownloadUrl(fileId) {
|
|
450
|
-
|
|
450
|
+
const result = await storageApi(`/download/${fileId}`);
|
|
451
|
+
return {
|
|
452
|
+
downloadUrl: result.shortUrl || result.downloadUrl,
|
|
453
|
+
expiresAt: result.expiresAt
|
|
454
|
+
};
|
|
451
455
|
},
|
|
452
456
|
async deleteFile(fileId) {
|
|
453
457
|
await storageApi(`/files/${fileId}`, { method: "DELETE" });
|
|
@@ -512,7 +516,12 @@ var init_types = __esm({
|
|
|
512
516
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
513
517
|
alwaysApply: z.boolean().optional().default(false),
|
|
514
518
|
// Glob patterns - auto-inject when working with matching files
|
|
515
|
-
globs: z.array(z.string()).optional().default([])
|
|
519
|
+
globs: z.array(z.string()).optional().default([]),
|
|
520
|
+
// Platform requirements — skill is hidden from the model on platforms
|
|
521
|
+
// not listed here. Values match `process.platform`
|
|
522
|
+
// (darwin, linux, win32, freebsd, ...). If omitted or empty, the skill is
|
|
523
|
+
// available on all platforms.
|
|
524
|
+
platforms: z.array(z.string()).optional().default([])
|
|
516
525
|
});
|
|
517
526
|
TaskConfigSchema = z.object({
|
|
518
527
|
enabled: z.boolean(),
|
|
@@ -530,7 +539,13 @@ var init_types = __esm({
|
|
|
530
539
|
approvalWebhook: z.string().url().optional(),
|
|
531
540
|
skillsDirectory: z.string().optional(),
|
|
532
541
|
maxContextChars: z.number().optional().default(2e5),
|
|
533
|
-
task: TaskConfigSchema.optional()
|
|
542
|
+
task: TaskConfigSchema.optional(),
|
|
543
|
+
// Anthropic computer use tool — opt-in. When true, the `computer` tool is
|
|
544
|
+
// included in the toolset for Anthropic models. Default false.
|
|
545
|
+
computerUseEnabled: z.boolean().optional(),
|
|
546
|
+
// Display dimensions for the computer use tool (defaults: 1280x800).
|
|
547
|
+
computerUseDisplayWidth: z.number().int().positive().optional(),
|
|
548
|
+
computerUseDisplayHeight: z.number().int().positive().optional()
|
|
534
549
|
});
|
|
535
550
|
VectorGatewayConfigSchema = z.object({
|
|
536
551
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -1531,7 +1546,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1531
1546
|
globs: parsed.metadata.globs,
|
|
1532
1547
|
loadType,
|
|
1533
1548
|
priority,
|
|
1534
|
-
sourceDir: directory
|
|
1549
|
+
sourceDir: directory,
|
|
1550
|
+
platforms: parsed.metadata.platforms
|
|
1535
1551
|
});
|
|
1536
1552
|
} else {
|
|
1537
1553
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1544,11 +1560,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1544
1560
|
globs: [],
|
|
1545
1561
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1546
1562
|
priority,
|
|
1547
|
-
sourceDir: directory
|
|
1563
|
+
sourceDir: directory,
|
|
1564
|
+
platforms: []
|
|
1548
1565
|
});
|
|
1549
1566
|
}
|
|
1550
1567
|
}
|
|
1551
|
-
return skills
|
|
1568
|
+
return skills.filter(
|
|
1569
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1570
|
+
);
|
|
1552
1571
|
}
|
|
1553
1572
|
async function loadAllSkills(directories) {
|
|
1554
1573
|
const allSkills = [];
|
|
@@ -2145,6 +2164,7 @@ __export(webhook_exports, {
|
|
|
2145
2164
|
sendWebhook: () => sendWebhook
|
|
2146
2165
|
});
|
|
2147
2166
|
async function sendWebhook(url, event) {
|
|
2167
|
+
const t0 = Date.now();
|
|
2148
2168
|
try {
|
|
2149
2169
|
const controller = new AbortController();
|
|
2150
2170
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2158,17 +2178,36 @@ async function sendWebhook(url, event) {
|
|
|
2158
2178
|
signal: controller.signal
|
|
2159
2179
|
});
|
|
2160
2180
|
clearTimeout(timeout);
|
|
2181
|
+
const ms = Date.now() - t0;
|
|
2161
2182
|
if (!response.ok) {
|
|
2162
|
-
|
|
2183
|
+
const body = await response.text().catch(() => "");
|
|
2184
|
+
console.warn(
|
|
2185
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> HTTP ${response.status} in ${ms}ms${body ? ` (${body.slice(0, 200)})` : ""}`
|
|
2186
|
+
);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (!QUIET || TERMINAL_EVENTS.has(event.type)) {
|
|
2190
|
+
console.log(
|
|
2191
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> 200 in ${ms}ms`
|
|
2192
|
+
);
|
|
2163
2193
|
}
|
|
2164
2194
|
} catch (err) {
|
|
2165
2195
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2166
|
-
console.warn(
|
|
2196
|
+
console.warn(
|
|
2197
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
2198
|
+
);
|
|
2167
2199
|
}
|
|
2168
2200
|
}
|
|
2201
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2169
2202
|
var init_webhook = __esm({
|
|
2170
2203
|
"src/utils/webhook.ts"() {
|
|
2171
2204
|
"use strict";
|
|
2205
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set([
|
|
2206
|
+
"task.started",
|
|
2207
|
+
"task.completed",
|
|
2208
|
+
"task.failed"
|
|
2209
|
+
]);
|
|
2210
|
+
QUIET = process.env.SPARKECODER_QUIET_WEBHOOKS === "1";
|
|
2172
2211
|
}
|
|
2173
2212
|
});
|
|
2174
2213
|
|
|
@@ -2378,15 +2417,15 @@ var recorder_exports = {};
|
|
|
2378
2417
|
__export(recorder_exports, {
|
|
2379
2418
|
FrameRecorder: () => FrameRecorder
|
|
2380
2419
|
});
|
|
2381
|
-
import { exec as
|
|
2382
|
-
import { promisify as
|
|
2420
|
+
import { exec as exec6 } from "child_process";
|
|
2421
|
+
import { promisify as promisify6 } from "util";
|
|
2383
2422
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
2384
|
-
import { join as
|
|
2385
|
-
import { tmpdir } from "os";
|
|
2386
|
-
import { nanoid as
|
|
2423
|
+
import { join as join9 } from "path";
|
|
2424
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2425
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2387
2426
|
async function checkFfmpeg() {
|
|
2388
2427
|
try {
|
|
2389
|
-
await
|
|
2428
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
2390
2429
|
return true;
|
|
2391
2430
|
} catch {
|
|
2392
2431
|
return false;
|
|
@@ -2398,11 +2437,11 @@ async function cleanup(dir) {
|
|
|
2398
2437
|
} catch {
|
|
2399
2438
|
}
|
|
2400
2439
|
}
|
|
2401
|
-
var
|
|
2440
|
+
var execAsync6, FrameRecorder;
|
|
2402
2441
|
var init_recorder = __esm({
|
|
2403
2442
|
"src/browser/recorder.ts"() {
|
|
2404
2443
|
"use strict";
|
|
2405
|
-
|
|
2444
|
+
execAsync6 = promisify6(exec6);
|
|
2406
2445
|
FrameRecorder = class {
|
|
2407
2446
|
frames = [];
|
|
2408
2447
|
startTime = null;
|
|
@@ -2438,21 +2477,21 @@ var init_recorder = __esm({
|
|
|
2438
2477
|
*/
|
|
2439
2478
|
async encode() {
|
|
2440
2479
|
if (this.frames.length === 0) return null;
|
|
2441
|
-
const workDir =
|
|
2480
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid4(8)}`);
|
|
2442
2481
|
await mkdir4(workDir, { recursive: true });
|
|
2443
2482
|
try {
|
|
2444
2483
|
for (let i = 0; i < this.frames.length; i++) {
|
|
2445
|
-
const framePath =
|
|
2484
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
2446
2485
|
await writeFile5(framePath, this.frames[i].data);
|
|
2447
2486
|
}
|
|
2448
2487
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
2449
2488
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
2450
2489
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
2451
|
-
const outputPath =
|
|
2490
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
2452
2491
|
const hasFfmpeg = await checkFfmpeg();
|
|
2453
2492
|
if (hasFfmpeg) {
|
|
2454
|
-
await
|
|
2455
|
-
`ffmpeg -y -framerate ${clampedFps} -i "${
|
|
2493
|
+
await execAsync6(
|
|
2494
|
+
`ffmpeg -y -framerate ${clampedFps} -i "${join9(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
|
|
2456
2495
|
{ timeout: 12e4 }
|
|
2457
2496
|
);
|
|
2458
2497
|
} else {
|
|
@@ -2464,7 +2503,7 @@ var init_recorder = __esm({
|
|
|
2464
2503
|
const files = await readdir5(workDir);
|
|
2465
2504
|
for (const f of files) {
|
|
2466
2505
|
if (f.startsWith("frame_")) {
|
|
2467
|
-
await unlink2(
|
|
2506
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
2468
2507
|
});
|
|
2469
2508
|
}
|
|
2470
2509
|
}
|
|
@@ -2487,12 +2526,12 @@ var init_recorder = __esm({
|
|
|
2487
2526
|
|
|
2488
2527
|
// src/server/index.ts
|
|
2489
2528
|
import "dotenv/config";
|
|
2490
|
-
import { Hono as
|
|
2529
|
+
import { Hono as Hono7 } from "hono";
|
|
2491
2530
|
import { serve } from "@hono/node-server";
|
|
2492
2531
|
import { cors } from "hono/cors";
|
|
2493
2532
|
import { logger } from "hono/logger";
|
|
2494
|
-
import { existsSync as
|
|
2495
|
-
import { resolve as resolve10, dirname as dirname7, join as
|
|
2533
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2534
|
+
import { resolve as resolve10, dirname as dirname7, join as join13 } from "path";
|
|
2496
2535
|
import { spawn as spawn2 } from "child_process";
|
|
2497
2536
|
import { createServer as createNetServer } from "net";
|
|
2498
2537
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -2501,17 +2540,17 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
2501
2540
|
init_db();
|
|
2502
2541
|
import { Hono } from "hono";
|
|
2503
2542
|
import { zValidator } from "@hono/zod-validator";
|
|
2504
|
-
import { z as
|
|
2505
|
-
import { existsSync as
|
|
2543
|
+
import { z as z16 } from "zod";
|
|
2544
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
2506
2545
|
import { readdir as readdir6 } from "fs/promises";
|
|
2507
|
-
import { join as
|
|
2508
|
-
import { nanoid as
|
|
2546
|
+
import { join as join10, basename as basename5, extname as extname8, relative as relative9 } from "path";
|
|
2547
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2509
2548
|
|
|
2510
2549
|
// src/agent/index.ts
|
|
2511
2550
|
import {
|
|
2512
2551
|
streamText as streamText2,
|
|
2513
2552
|
generateText as generateText3,
|
|
2514
|
-
tool as
|
|
2553
|
+
tool as tool14,
|
|
2515
2554
|
stepCountIs as stepCountIs2
|
|
2516
2555
|
} from "ai";
|
|
2517
2556
|
|
|
@@ -2702,8 +2741,8 @@ var SUBAGENT_MODELS = {
|
|
|
2702
2741
|
// src/agent/index.ts
|
|
2703
2742
|
init_db();
|
|
2704
2743
|
init_config();
|
|
2705
|
-
import { z as
|
|
2706
|
-
import { nanoid as
|
|
2744
|
+
import { z as z15 } from "zod";
|
|
2745
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2707
2746
|
|
|
2708
2747
|
// src/tools/bash.ts
|
|
2709
2748
|
import { tool } from "ai";
|
|
@@ -3770,12 +3809,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
3770
3809
|
}
|
|
3771
3810
|
async function commandExists(cmd) {
|
|
3772
3811
|
try {
|
|
3773
|
-
const { exec:
|
|
3774
|
-
const { promisify:
|
|
3775
|
-
const
|
|
3812
|
+
const { exec: exec8 } = await import("child_process");
|
|
3813
|
+
const { promisify: promisify8 } = await import("util");
|
|
3814
|
+
const execAsync8 = promisify8(exec8);
|
|
3776
3815
|
const isWindows = process.platform === "win32";
|
|
3777
3816
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
3778
|
-
await
|
|
3817
|
+
await execAsync8(checkCmd);
|
|
3779
3818
|
return true;
|
|
3780
3819
|
} catch {
|
|
3781
3820
|
return false;
|
|
@@ -6166,6 +6205,568 @@ function createUploadFileTool(options) {
|
|
|
6166
6205
|
});
|
|
6167
6206
|
}
|
|
6168
6207
|
|
|
6208
|
+
// src/tools/computer-use.ts
|
|
6209
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
6210
|
+
import { exec as exec5 } from "child_process";
|
|
6211
|
+
import { promisify as promisify5 } from "util";
|
|
6212
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync15, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6213
|
+
import { join as join8 } from "path";
|
|
6214
|
+
import { tmpdir } from "os";
|
|
6215
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
6216
|
+
var execAsync5 = promisify5(exec5);
|
|
6217
|
+
var DEFAULT_WIDTH = 1280;
|
|
6218
|
+
var DEFAULT_HEIGHT = 800;
|
|
6219
|
+
function isMacOs() {
|
|
6220
|
+
return process.platform === "darwin";
|
|
6221
|
+
}
|
|
6222
|
+
async function isCliclickInstalled() {
|
|
6223
|
+
try {
|
|
6224
|
+
await execAsync5("command -v cliclick", { timeout: 2e3 });
|
|
6225
|
+
return true;
|
|
6226
|
+
} catch {
|
|
6227
|
+
return false;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
async function runJxa(script) {
|
|
6231
|
+
try {
|
|
6232
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
6233
|
+
const { stdout } = await execAsync5(`osascript -l JavaScript -e '${escaped}'`, {
|
|
6234
|
+
timeout: 5e3
|
|
6235
|
+
});
|
|
6236
|
+
return JSON.parse(stdout.trim());
|
|
6237
|
+
} catch {
|
|
6238
|
+
return null;
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
async function hasAccessibilityPermissions() {
|
|
6242
|
+
try {
|
|
6243
|
+
const { stderr } = await execAsync5("cliclick p:.", { timeout: 3e3 });
|
|
6244
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6245
|
+
return { ok: false, error: stderr.trim().split("\n")[0] };
|
|
6246
|
+
}
|
|
6247
|
+
return { ok: true };
|
|
6248
|
+
} catch (err) {
|
|
6249
|
+
return { ok: false, error: err?.message || String(err) };
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
async function hasScreenRecordingPermissions() {
|
|
6253
|
+
const result = await runJxa(
|
|
6254
|
+
`ObjC.import("Cocoa");
|
|
6255
|
+
ObjC.import("CoreGraphics");
|
|
6256
|
+
ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]);
|
|
6257
|
+
JSON.stringify({ hasAccess: !!$.CGPreflightScreenCaptureAccess() });`
|
|
6258
|
+
);
|
|
6259
|
+
return result?.hasAccess ?? false;
|
|
6260
|
+
}
|
|
6261
|
+
async function requestAccessibilityPrompt() {
|
|
6262
|
+
const result = await runJxa(
|
|
6263
|
+
`ObjC.import("ApplicationServices");
|
|
6264
|
+
var key = $.kAXTrustedCheckOptionPrompt;
|
|
6265
|
+
var dict = $.NSDictionary.dictionaryWithObjectForKey($.kCFBooleanTrue, key);
|
|
6266
|
+
var trusted = $.AXIsProcessTrustedWithOptions(dict);
|
|
6267
|
+
JSON.stringify({ trusted: !!trusted });`
|
|
6268
|
+
);
|
|
6269
|
+
return result?.trusted ?? false;
|
|
6270
|
+
}
|
|
6271
|
+
async function requestScreenRecordingPrompt() {
|
|
6272
|
+
const result = await runJxa(
|
|
6273
|
+
`ObjC.import("Cocoa");
|
|
6274
|
+
ObjC.import("CoreGraphics");
|
|
6275
|
+
ObjC.bindFunction("CGRequestScreenCaptureAccess", ["bool", []]);
|
|
6276
|
+
JSON.stringify({ granted: !!$.CGRequestScreenCaptureAccess() });`
|
|
6277
|
+
);
|
|
6278
|
+
return result?.granted ?? false;
|
|
6279
|
+
}
|
|
6280
|
+
async function openSystemSettings(pane) {
|
|
6281
|
+
const url = pane === "accessibility" ? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" : "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6282
|
+
try {
|
|
6283
|
+
await execAsync5(`open '${url}'`, { timeout: 3e3 });
|
|
6284
|
+
} catch {
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
async function detectScreenSize() {
|
|
6288
|
+
try {
|
|
6289
|
+
const { stdout } = await execAsync5(
|
|
6290
|
+
`osascript -e 'tell application "Finder" to get bounds of window of desktop'`,
|
|
6291
|
+
{ timeout: 3e3 }
|
|
6292
|
+
);
|
|
6293
|
+
const parts = stdout.trim().split(",").map((s) => parseInt(s.trim(), 10));
|
|
6294
|
+
if (parts.length >= 4 && parts.every((n) => Number.isFinite(n))) {
|
|
6295
|
+
const [x1, y1, x2, y2] = parts;
|
|
6296
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
6297
|
+
}
|
|
6298
|
+
} catch {
|
|
6299
|
+
}
|
|
6300
|
+
return null;
|
|
6301
|
+
}
|
|
6302
|
+
async function runCliclick(args) {
|
|
6303
|
+
const quoted = args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
6304
|
+
const { stdout, stderr } = await execAsync5(`cliclick ${quoted}`, {
|
|
6305
|
+
timeout: 15e3,
|
|
6306
|
+
maxBuffer: 1024 * 1024
|
|
6307
|
+
});
|
|
6308
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6309
|
+
throw new Error(
|
|
6310
|
+
"Accessibility permissions not granted to cliclick. Open System Settings \u2192 Privacy & Security \u2192 Accessibility, add cliclick (or the agent runtime), and toggle it on."
|
|
6311
|
+
);
|
|
6312
|
+
}
|
|
6313
|
+
if (stderr && !stdout) throw new Error(stderr.trim());
|
|
6314
|
+
return (stdout || "").trim();
|
|
6315
|
+
}
|
|
6316
|
+
async function runScreencapture(path) {
|
|
6317
|
+
await execAsync5(`screencapture -x -t png '${path.replace(/'/g, `'\\''`)}'`, {
|
|
6318
|
+
timeout: 5e3
|
|
6319
|
+
});
|
|
6320
|
+
}
|
|
6321
|
+
async function resizeScreenshotToPoints(path, targetWidth, targetHeight) {
|
|
6322
|
+
const sharpModule = await import("sharp");
|
|
6323
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6324
|
+
const meta = await sharp2(path).metadata();
|
|
6325
|
+
if (meta.width === targetWidth && meta.height === targetHeight) {
|
|
6326
|
+
return readFileSync7(path);
|
|
6327
|
+
}
|
|
6328
|
+
return await sharp2(path).resize(targetWidth, targetHeight, { fit: "fill" }).png().toBuffer();
|
|
6329
|
+
}
|
|
6330
|
+
async function runScroll(dx, dy) {
|
|
6331
|
+
const wheelY = -Math.round(dy);
|
|
6332
|
+
const wheelX = -Math.round(dx);
|
|
6333
|
+
const script = `ObjC.import('CoreGraphics');var ev = $.CGEventCreateScrollWheelEvent(null, 0, 2, ${wheelY}, ${wheelX});$.CGEventPost(0, ev);`;
|
|
6334
|
+
await execAsync5(
|
|
6335
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6336
|
+
{ timeout: 5e3 }
|
|
6337
|
+
);
|
|
6338
|
+
}
|
|
6339
|
+
function translateKeyForCliclick(key) {
|
|
6340
|
+
if (!key) return [];
|
|
6341
|
+
const parts = key.split("+").map((p) => p.trim()).filter(Boolean);
|
|
6342
|
+
if (parts.length === 0) return [];
|
|
6343
|
+
const modMap = {
|
|
6344
|
+
ctrl: "ctrl",
|
|
6345
|
+
control: "ctrl",
|
|
6346
|
+
alt: "alt",
|
|
6347
|
+
option: "alt",
|
|
6348
|
+
shift: "shift",
|
|
6349
|
+
cmd: "cmd",
|
|
6350
|
+
super: "cmd",
|
|
6351
|
+
meta: "cmd",
|
|
6352
|
+
win: "cmd",
|
|
6353
|
+
fn: "fn"
|
|
6354
|
+
};
|
|
6355
|
+
const keyMap = {
|
|
6356
|
+
return: "enter",
|
|
6357
|
+
enter: "enter",
|
|
6358
|
+
esc: "esc",
|
|
6359
|
+
escape: "esc",
|
|
6360
|
+
backspace: "delete",
|
|
6361
|
+
back_space: "delete",
|
|
6362
|
+
delete: "fwd-delete",
|
|
6363
|
+
fwd_delete: "fwd-delete",
|
|
6364
|
+
forward_delete: "fwd-delete",
|
|
6365
|
+
tab: "tab",
|
|
6366
|
+
space: "space",
|
|
6367
|
+
up: "arrow-up",
|
|
6368
|
+
arrow_up: "arrow-up",
|
|
6369
|
+
down: "arrow-down",
|
|
6370
|
+
arrow_down: "arrow-down",
|
|
6371
|
+
left: "arrow-left",
|
|
6372
|
+
arrow_left: "arrow-left",
|
|
6373
|
+
right: "arrow-right",
|
|
6374
|
+
arrow_right: "arrow-right",
|
|
6375
|
+
page_up: "page-up",
|
|
6376
|
+
pageup: "page-up",
|
|
6377
|
+
page_down: "page-down",
|
|
6378
|
+
pagedown: "page-down",
|
|
6379
|
+
home: "home",
|
|
6380
|
+
end: "end",
|
|
6381
|
+
f1: "f1",
|
|
6382
|
+
f2: "f2",
|
|
6383
|
+
f3: "f3",
|
|
6384
|
+
f4: "f4",
|
|
6385
|
+
f5: "f5",
|
|
6386
|
+
f6: "f6",
|
|
6387
|
+
f7: "f7",
|
|
6388
|
+
f8: "f8",
|
|
6389
|
+
f9: "f9",
|
|
6390
|
+
f10: "f10",
|
|
6391
|
+
f11: "f11",
|
|
6392
|
+
f12: "f12"
|
|
6393
|
+
};
|
|
6394
|
+
const modifiers = [];
|
|
6395
|
+
let mainKey = null;
|
|
6396
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6397
|
+
const lower = parts[i].toLowerCase().replace(/-/g, "_");
|
|
6398
|
+
if (i < parts.length - 1 && modMap[lower]) {
|
|
6399
|
+
modifiers.push(modMap[lower]);
|
|
6400
|
+
} else {
|
|
6401
|
+
mainKey = keyMap[lower] || lower;
|
|
6402
|
+
}
|
|
6403
|
+
}
|
|
6404
|
+
const args = [];
|
|
6405
|
+
if (modifiers.length > 0) args.push(`kd:${modifiers.join(",")}`);
|
|
6406
|
+
if (mainKey) {
|
|
6407
|
+
const isNamedKey = Object.values(keyMap).includes(mainKey) || /^f([1-9]|1[0-9]|20)$/.test(mainKey) || /^num-/.test(mainKey);
|
|
6408
|
+
if (isNamedKey) {
|
|
6409
|
+
args.push(`kp:${mainKey}`);
|
|
6410
|
+
} else {
|
|
6411
|
+
args.push(`t:${mainKey}`);
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
if (modifiers.length > 0) args.push(`ku:${modifiers.join(",")}`);
|
|
6415
|
+
return args;
|
|
6416
|
+
}
|
|
6417
|
+
function modifierStringToCliclick(text) {
|
|
6418
|
+
return text.split("+").map((p) => p.trim().toLowerCase()).map((p) => {
|
|
6419
|
+
if (p === "ctrl" || p === "control") return "ctrl";
|
|
6420
|
+
if (p === "alt" || p === "option") return "alt";
|
|
6421
|
+
if (p === "shift") return "shift";
|
|
6422
|
+
if (p === "super" || p === "meta" || p === "cmd") return "cmd";
|
|
6423
|
+
return "";
|
|
6424
|
+
}).filter(Boolean);
|
|
6425
|
+
}
|
|
6426
|
+
function createComputerUseTool(options) {
|
|
6427
|
+
const displayWidth = options.displayWidth ?? DEFAULT_WIDTH;
|
|
6428
|
+
const displayHeight = options.displayHeight ?? DEFAULT_HEIGHT;
|
|
6429
|
+
return anthropic.tools.computer_20251124({
|
|
6430
|
+
displayWidthPx: displayWidth,
|
|
6431
|
+
displayHeightPx: displayHeight,
|
|
6432
|
+
enableZoom: true,
|
|
6433
|
+
execute: async (input) => {
|
|
6434
|
+
try {
|
|
6435
|
+
switch (input.action) {
|
|
6436
|
+
case "screenshot": {
|
|
6437
|
+
const path = join8(tmpdir(), `cu-${nanoid3(8)}.png`);
|
|
6438
|
+
await runScreencapture(path);
|
|
6439
|
+
const resized = await resizeScreenshotToPoints(path, displayWidth, displayHeight);
|
|
6440
|
+
try {
|
|
6441
|
+
unlinkSync2(path);
|
|
6442
|
+
} catch {
|
|
6443
|
+
}
|
|
6444
|
+
return { type: "image", data: resized.toString("base64") };
|
|
6445
|
+
}
|
|
6446
|
+
case "left_click": {
|
|
6447
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6448
|
+
if (input.text) {
|
|
6449
|
+
const mods = modifierStringToCliclick(input.text);
|
|
6450
|
+
if (mods.length > 0) {
|
|
6451
|
+
await runCliclick([`kd:${mods.join(",")}`, `c:${x},${y}`, `ku:${mods.join(",")}`]);
|
|
6452
|
+
} else {
|
|
6453
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6454
|
+
}
|
|
6455
|
+
} else {
|
|
6456
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6457
|
+
}
|
|
6458
|
+
return `clicked at (${x}, ${y})${input.text ? ` with ${input.text}` : ""}`;
|
|
6459
|
+
}
|
|
6460
|
+
case "right_click": {
|
|
6461
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6462
|
+
await runCliclick([`rc:${x},${y}`]);
|
|
6463
|
+
return `right-clicked at (${x}, ${y})`;
|
|
6464
|
+
}
|
|
6465
|
+
case "middle_click": {
|
|
6466
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6467
|
+
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);`;
|
|
6468
|
+
await execAsync5(
|
|
6469
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6470
|
+
{ timeout: 3e3 }
|
|
6471
|
+
);
|
|
6472
|
+
return `middle-clicked at (${x}, ${y})`;
|
|
6473
|
+
}
|
|
6474
|
+
case "double_click": {
|
|
6475
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6476
|
+
await runCliclick([`dc:${x},${y}`]);
|
|
6477
|
+
return `double-clicked at (${x}, ${y})`;
|
|
6478
|
+
}
|
|
6479
|
+
case "triple_click": {
|
|
6480
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6481
|
+
await runCliclick([`tc:${x},${y}`]);
|
|
6482
|
+
return `triple-clicked at (${x}, ${y})`;
|
|
6483
|
+
}
|
|
6484
|
+
case "mouse_move": {
|
|
6485
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6486
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6487
|
+
return `moved cursor to (${x}, ${y})`;
|
|
6488
|
+
}
|
|
6489
|
+
case "left_mouse_down": {
|
|
6490
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6491
|
+
await runCliclick([`dd:${x},${y}`]);
|
|
6492
|
+
return `left mouse button pressed at (${x}, ${y})`;
|
|
6493
|
+
}
|
|
6494
|
+
case "left_mouse_up": {
|
|
6495
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6496
|
+
await runCliclick([`du:${x},${y}`]);
|
|
6497
|
+
return `left mouse button released at (${x}, ${y})`;
|
|
6498
|
+
}
|
|
6499
|
+
case "left_click_drag": {
|
|
6500
|
+
const [sx, sy] = input.start_coordinate ?? [0, 0];
|
|
6501
|
+
const [ex, ey] = input.coordinate ?? [0, 0];
|
|
6502
|
+
await runCliclick([`dd:${sx},${sy}`, `m:${ex},${ey}`, `du:${ex},${ey}`]);
|
|
6503
|
+
return `dragged from (${sx}, ${sy}) to (${ex}, ${ey})`;
|
|
6504
|
+
}
|
|
6505
|
+
case "type": {
|
|
6506
|
+
const text = input.text ?? "";
|
|
6507
|
+
await runCliclick([`t:${text}`]);
|
|
6508
|
+
return `typed ${text.length} character(s)`;
|
|
6509
|
+
}
|
|
6510
|
+
case "key": {
|
|
6511
|
+
const args = translateKeyForCliclick(input.text ?? "");
|
|
6512
|
+
if (args.length === 0) return "no key specified";
|
|
6513
|
+
await runCliclick(args);
|
|
6514
|
+
return `pressed ${input.text}`;
|
|
6515
|
+
}
|
|
6516
|
+
case "hold_key": {
|
|
6517
|
+
const text = (input.text ?? "").toLowerCase();
|
|
6518
|
+
const duration = input.duration ?? 1;
|
|
6519
|
+
const modMap = {
|
|
6520
|
+
ctrl: "ctrl",
|
|
6521
|
+
control: "ctrl",
|
|
6522
|
+
alt: "alt",
|
|
6523
|
+
option: "alt",
|
|
6524
|
+
shift: "shift",
|
|
6525
|
+
cmd: "cmd",
|
|
6526
|
+
super: "cmd",
|
|
6527
|
+
meta: "cmd",
|
|
6528
|
+
fn: "fn"
|
|
6529
|
+
};
|
|
6530
|
+
const cliName = modMap[text] || text;
|
|
6531
|
+
await runCliclick([`kd:${cliName}`]);
|
|
6532
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6533
|
+
await runCliclick([`ku:${cliName}`]);
|
|
6534
|
+
return `held ${text} for ${duration}s`;
|
|
6535
|
+
}
|
|
6536
|
+
case "scroll": {
|
|
6537
|
+
const direction = input.scroll_direction ?? "down";
|
|
6538
|
+
const amount = input.scroll_amount ?? 3;
|
|
6539
|
+
const px = amount * 100;
|
|
6540
|
+
const dx = direction === "left" ? -px : direction === "right" ? px : 0;
|
|
6541
|
+
const dy = direction === "up" ? -px : direction === "down" ? px : 0;
|
|
6542
|
+
if (input.coordinate) {
|
|
6543
|
+
const [x, y] = input.coordinate;
|
|
6544
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6545
|
+
}
|
|
6546
|
+
const mods = input.text ? modifierStringToCliclick(input.text) : [];
|
|
6547
|
+
if (mods.length > 0) {
|
|
6548
|
+
await runCliclick([`kd:${mods.join(",")}`]);
|
|
6549
|
+
}
|
|
6550
|
+
await runScroll(dx, dy);
|
|
6551
|
+
if (mods.length > 0) {
|
|
6552
|
+
await runCliclick([`ku:${mods.join(",")}`]);
|
|
6553
|
+
}
|
|
6554
|
+
return `scrolled ${direction} by ${amount}`;
|
|
6555
|
+
}
|
|
6556
|
+
case "wait": {
|
|
6557
|
+
const duration = input.duration ?? 1;
|
|
6558
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6559
|
+
return `waited ${duration}s`;
|
|
6560
|
+
}
|
|
6561
|
+
case "cursor_position": {
|
|
6562
|
+
const out = await runCliclick(["p:."]);
|
|
6563
|
+
return `cursor at ${out}`;
|
|
6564
|
+
}
|
|
6565
|
+
case "zoom": {
|
|
6566
|
+
const region = input.region ?? [0, 0, displayWidth, displayHeight];
|
|
6567
|
+
const [x1, y1, x2, y2] = region;
|
|
6568
|
+
const tmpPath = join8(tmpdir(), `cu-zoom-${nanoid3(8)}.png`);
|
|
6569
|
+
await runScreencapture(tmpPath);
|
|
6570
|
+
const sharpModule = await import("sharp");
|
|
6571
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6572
|
+
const meta = await sharp2(tmpPath).metadata();
|
|
6573
|
+
const scaleX = (meta.width || displayWidth) / displayWidth;
|
|
6574
|
+
const scaleY = (meta.height || displayHeight) / displayHeight;
|
|
6575
|
+
const px = {
|
|
6576
|
+
left: Math.max(0, Math.round(x1 * scaleX)),
|
|
6577
|
+
top: Math.max(0, Math.round(y1 * scaleY)),
|
|
6578
|
+
width: Math.max(1, Math.round((x2 - x1) * scaleX)),
|
|
6579
|
+
height: Math.max(1, Math.round((y2 - y1) * scaleY))
|
|
6580
|
+
};
|
|
6581
|
+
const buf = await sharp2(tmpPath).extract(px).png().toBuffer();
|
|
6582
|
+
try {
|
|
6583
|
+
unlinkSync2(tmpPath);
|
|
6584
|
+
} catch {
|
|
6585
|
+
}
|
|
6586
|
+
return { type: "image", data: buf.toString("base64") };
|
|
6587
|
+
}
|
|
6588
|
+
default: {
|
|
6589
|
+
const exhaustive = input.action;
|
|
6590
|
+
return `unsupported action: ${String(exhaustive)}`;
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
} catch (err) {
|
|
6594
|
+
const msg = err?.message || String(err);
|
|
6595
|
+
let hint = "";
|
|
6596
|
+
if (/accessibility|not authorized|tcc|operation not permitted/i.test(msg)) {
|
|
6597
|
+
hint = " (Hint: call enable_computer_use to (re-)check permissions and open System Settings)";
|
|
6598
|
+
} else if (/command not found/i.test(msg)) {
|
|
6599
|
+
hint = " (Hint: install cliclick with `brew install cliclick`)";
|
|
6600
|
+
}
|
|
6601
|
+
return `Error: ${msg}${hint}`;
|
|
6602
|
+
}
|
|
6603
|
+
},
|
|
6604
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6605
|
+
toModelOutput({ output }) {
|
|
6606
|
+
if (typeof output === "string") {
|
|
6607
|
+
return { type: "content", value: [{ type: "text", text: output }] };
|
|
6608
|
+
}
|
|
6609
|
+
return {
|
|
6610
|
+
type: "content",
|
|
6611
|
+
value: [{ type: "media", data: output.data, mediaType: "image/png" }]
|
|
6612
|
+
};
|
|
6613
|
+
}
|
|
6614
|
+
});
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
// src/tools/enable-computer-use.ts
|
|
6618
|
+
init_db();
|
|
6619
|
+
import { tool as tool13 } from "ai";
|
|
6620
|
+
import { z as z14 } from "zod";
|
|
6621
|
+
var inputSchema = z14.object({
|
|
6622
|
+
display_width: z14.number().int().positive().optional().describe("Display width in pixels (defaults to detected primary display, fallback 1280)"),
|
|
6623
|
+
display_height: z14.number().int().positive().optional().describe("Display height in pixels (defaults to detected primary display, fallback 800)"),
|
|
6624
|
+
request_permissions: z14.boolean().optional().default(true).describe(
|
|
6625
|
+
"When true (default), proactively trigger macOS permission prompts and open System Settings panes for any missing permissions."
|
|
6626
|
+
)
|
|
6627
|
+
});
|
|
6628
|
+
var ACCESSIBILITY_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
6629
|
+
var SCREEN_RECORDING_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6630
|
+
function createEnableComputerUseTool(options) {
|
|
6631
|
+
return tool13({
|
|
6632
|
+
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.",
|
|
6633
|
+
inputSchema,
|
|
6634
|
+
execute: async ({ display_width, display_height, request_permissions }) => {
|
|
6635
|
+
try {
|
|
6636
|
+
if (!isMacOs()) {
|
|
6637
|
+
return {
|
|
6638
|
+
success: false,
|
|
6639
|
+
error: "Computer use is currently only supported on macOS.",
|
|
6640
|
+
platform: process.platform
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
if (!await isCliclickInstalled()) {
|
|
6644
|
+
return {
|
|
6645
|
+
success: false,
|
|
6646
|
+
error: "`cliclick` is not installed. It is required for mouse/keyboard control on macOS.",
|
|
6647
|
+
installCommand: "brew install cliclick",
|
|
6648
|
+
fixSteps: [
|
|
6649
|
+
"In a terminal on this Mac, run: brew install cliclick",
|
|
6650
|
+
"(If Homebrew is not installed, install it first from https://brew.sh)",
|
|
6651
|
+
"Then call enable_computer_use again"
|
|
6652
|
+
]
|
|
6653
|
+
};
|
|
6654
|
+
}
|
|
6655
|
+
const acc = await hasAccessibilityPermissions();
|
|
6656
|
+
const screen = await hasScreenRecordingPermissions();
|
|
6657
|
+
const missing = [];
|
|
6658
|
+
if (!acc.ok) {
|
|
6659
|
+
let prompted = false;
|
|
6660
|
+
let panelOpened = false;
|
|
6661
|
+
if (request_permissions) {
|
|
6662
|
+
prompted = await requestAccessibilityPrompt().then(() => true).catch(() => false);
|
|
6663
|
+
await openSystemSettings("accessibility").then(() => {
|
|
6664
|
+
panelOpened = true;
|
|
6665
|
+
}).catch(() => void 0);
|
|
6666
|
+
}
|
|
6667
|
+
missing.push({
|
|
6668
|
+
name: "Accessibility",
|
|
6669
|
+
reason: "cliclick failed: " + (acc.error?.split("\n")[0] || "no permission"),
|
|
6670
|
+
pane: "accessibility",
|
|
6671
|
+
settingsUrl: ACCESSIBILITY_URL,
|
|
6672
|
+
fixSteps: [
|
|
6673
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Accessibility pane that opened",
|
|
6674
|
+
"Click the + button",
|
|
6675
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6676
|
+
"Toggle the switch ON",
|
|
6677
|
+
"Restart the agent process so the new permission takes effect",
|
|
6678
|
+
"Then call enable_computer_use again"
|
|
6679
|
+
],
|
|
6680
|
+
prompted,
|
|
6681
|
+
panelOpened
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
if (!screen) {
|
|
6685
|
+
let prompted = false;
|
|
6686
|
+
let panelOpened = false;
|
|
6687
|
+
if (request_permissions) {
|
|
6688
|
+
prompted = await requestScreenRecordingPrompt().then(() => true).catch(() => false);
|
|
6689
|
+
await openSystemSettings("screen-recording").then(() => {
|
|
6690
|
+
panelOpened = true;
|
|
6691
|
+
}).catch(() => void 0);
|
|
6692
|
+
}
|
|
6693
|
+
missing.push({
|
|
6694
|
+
name: "Screen Recording",
|
|
6695
|
+
reason: "CGPreflightScreenCaptureAccess returned false",
|
|
6696
|
+
pane: "screen-recording",
|
|
6697
|
+
settingsUrl: SCREEN_RECORDING_URL,
|
|
6698
|
+
fixSteps: [
|
|
6699
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Screen Recording pane that opened",
|
|
6700
|
+
"Click the + button",
|
|
6701
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6702
|
+
"Toggle the switch ON",
|
|
6703
|
+
"Restart the agent process so the new permission takes effect",
|
|
6704
|
+
"Then call enable_computer_use again"
|
|
6705
|
+
],
|
|
6706
|
+
prompted,
|
|
6707
|
+
panelOpened
|
|
6708
|
+
});
|
|
6709
|
+
}
|
|
6710
|
+
if (missing.length > 0) {
|
|
6711
|
+
return {
|
|
6712
|
+
success: false,
|
|
6713
|
+
error: `Missing permission${missing.length > 1 ? "s" : ""}: ` + missing.map((m) => m.name).join(" and ") + ".",
|
|
6714
|
+
missingPermissions: missing,
|
|
6715
|
+
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."
|
|
6716
|
+
};
|
|
6717
|
+
}
|
|
6718
|
+
let width = display_width;
|
|
6719
|
+
let height = display_height;
|
|
6720
|
+
let detected = null;
|
|
6721
|
+
if (width === void 0 || height === void 0) {
|
|
6722
|
+
detected = await detectScreenSize();
|
|
6723
|
+
width = width ?? detected?.width ?? 1280;
|
|
6724
|
+
height = height ?? detected?.height ?? 800;
|
|
6725
|
+
}
|
|
6726
|
+
const session = await sessionQueries.getById(options.sessionId);
|
|
6727
|
+
if (!session) {
|
|
6728
|
+
return { success: false, error: "Session not found" };
|
|
6729
|
+
}
|
|
6730
|
+
const config = session.config || {};
|
|
6731
|
+
if (config.computerUseEnabled === true && config.computerUseDisplayWidth === width && config.computerUseDisplayHeight === height) {
|
|
6732
|
+
return {
|
|
6733
|
+
success: true,
|
|
6734
|
+
alreadyEnabled: true,
|
|
6735
|
+
message: "Computer use was already enabled for this session.",
|
|
6736
|
+
displayWidth: width,
|
|
6737
|
+
displayHeight: height
|
|
6738
|
+
};
|
|
6739
|
+
}
|
|
6740
|
+
const updated = {
|
|
6741
|
+
...config,
|
|
6742
|
+
computerUseEnabled: true,
|
|
6743
|
+
computerUseDisplayWidth: width,
|
|
6744
|
+
computerUseDisplayHeight: height
|
|
6745
|
+
};
|
|
6746
|
+
await sessionQueries.update(options.sessionId, { config: updated });
|
|
6747
|
+
return {
|
|
6748
|
+
success: true,
|
|
6749
|
+
enabled: true,
|
|
6750
|
+
platform: "darwin",
|
|
6751
|
+
displayWidth: width,
|
|
6752
|
+
displayHeight: height,
|
|
6753
|
+
detectedScreenSize: detected || void 0,
|
|
6754
|
+
permissions: {
|
|
6755
|
+
accessibility: "granted",
|
|
6756
|
+
screenRecording: "granted"
|
|
6757
|
+
},
|
|
6758
|
+
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.`
|
|
6759
|
+
};
|
|
6760
|
+
} catch (err) {
|
|
6761
|
+
return {
|
|
6762
|
+
success: false,
|
|
6763
|
+
error: err?.message || String(err)
|
|
6764
|
+
};
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
});
|
|
6768
|
+
}
|
|
6769
|
+
|
|
6169
6770
|
// src/tools/index.ts
|
|
6170
6771
|
init_semantic();
|
|
6171
6772
|
init_remote();
|
|
@@ -6214,6 +6815,20 @@ async function createTools(options) {
|
|
|
6214
6815
|
sessionId: options.sessionId
|
|
6215
6816
|
});
|
|
6216
6817
|
}
|
|
6818
|
+
if (process.platform === "darwin") {
|
|
6819
|
+
if (options.enableComputerUse) {
|
|
6820
|
+
tools.computer = createComputerUseTool({
|
|
6821
|
+
workingDirectory: options.workingDirectory,
|
|
6822
|
+
sessionId: options.sessionId,
|
|
6823
|
+
displayWidth: options.computerUseDisplayWidth,
|
|
6824
|
+
displayHeight: options.computerUseDisplayHeight
|
|
6825
|
+
});
|
|
6826
|
+
} else {
|
|
6827
|
+
tools.enable_computer_use = createEnableComputerUseTool({
|
|
6828
|
+
sessionId: options.sessionId
|
|
6829
|
+
});
|
|
6830
|
+
}
|
|
6831
|
+
}
|
|
6217
6832
|
if (options.enableSemanticSearch !== false) {
|
|
6218
6833
|
try {
|
|
6219
6834
|
if (isVectorGatewayConfigured()) {
|
|
@@ -6244,11 +6859,11 @@ init_db();
|
|
|
6244
6859
|
init_todo();
|
|
6245
6860
|
import os from "os";
|
|
6246
6861
|
function getSearchInstructions() {
|
|
6247
|
-
const
|
|
6862
|
+
const platform5 = process.platform;
|
|
6248
6863
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
6249
6864
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
6250
6865
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
6251
|
-
if (
|
|
6866
|
+
if (platform5 === "win32") {
|
|
6252
6867
|
return `${common}
|
|
6253
6868
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
6254
6869
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -6295,13 +6910,13 @@ async function buildSystemPrompt(options) {
|
|
|
6295
6910
|
);
|
|
6296
6911
|
const hasNoTodos = todos.length === 0;
|
|
6297
6912
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6298
|
-
const
|
|
6913
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6299
6914
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6300
6915
|
const searchInstructions = getSearchInstructions();
|
|
6301
6916
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
6302
6917
|
|
|
6303
6918
|
## Environment
|
|
6304
|
-
- **Platform**: ${
|
|
6919
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
6305
6920
|
- **Date**: ${currentDate}
|
|
6306
6921
|
- **Working Directory**: ${workingDirectory}
|
|
6307
6922
|
|
|
@@ -7239,10 +7854,14 @@ var Agent = class _Agent {
|
|
|
7239
7854
|
*/
|
|
7240
7855
|
async createToolsWithCallbacks(options) {
|
|
7241
7856
|
const config = getConfig();
|
|
7857
|
+
const sessionConfig = this.session.config || {};
|
|
7242
7858
|
return createTools({
|
|
7243
7859
|
sessionId: this.session.id,
|
|
7244
7860
|
workingDirectory: this.session.workingDirectory,
|
|
7245
7861
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7862
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7863
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7864
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
7246
7865
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
7247
7866
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
7248
7867
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -7275,10 +7894,14 @@ var Agent = class _Agent {
|
|
|
7275
7894
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
7276
7895
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
7277
7896
|
});
|
|
7897
|
+
const sessionConfig = session.config || {};
|
|
7278
7898
|
const tools = await createTools({
|
|
7279
7899
|
sessionId: session.id,
|
|
7280
7900
|
workingDirectory: session.workingDirectory,
|
|
7281
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
7901
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7902
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7903
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7904
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
7282
7905
|
});
|
|
7283
7906
|
return new _Agent(session, context, tools);
|
|
7284
7907
|
}
|
|
@@ -7451,10 +8074,10 @@ ${prompt}` });
|
|
|
7451
8074
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
7452
8075
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
7453
8076
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
7454
|
-
const fireWebhook = (
|
|
8077
|
+
const fireWebhook = (type2, data) => {
|
|
7455
8078
|
if (!webhookUrl) return;
|
|
7456
8079
|
sendWebhook(webhookUrl, {
|
|
7457
|
-
type,
|
|
8080
|
+
type: type2,
|
|
7458
8081
|
taskId: this.session.id,
|
|
7459
8082
|
sessionId: this.session.id,
|
|
7460
8083
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -7497,10 +8120,14 @@ ${prompt}` });
|
|
|
7497
8120
|
});
|
|
7498
8121
|
}
|
|
7499
8122
|
};
|
|
8123
|
+
const taskSessionConfig = this.session.config || {};
|
|
7500
8124
|
const taskTools = await createTools({
|
|
7501
8125
|
sessionId: this.session.id,
|
|
7502
8126
|
workingDirectory: this.session.workingDirectory,
|
|
7503
8127
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8128
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
8129
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
8130
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
7504
8131
|
onBashProgress: bashProgressHandler,
|
|
7505
8132
|
onWriteFileProgress: (progress) => {
|
|
7506
8133
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -7783,11 +8410,11 @@ ${taskAddendum}`;
|
|
|
7783
8410
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7784
8411
|
if (!isRemoteConfigured2()) return [];
|
|
7785
8412
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
7786
|
-
const { join:
|
|
8413
|
+
const { join: join14, basename: basename6 } = await import("path");
|
|
7787
8414
|
const urls = [];
|
|
7788
8415
|
for (const filePath of filePaths) {
|
|
7789
8416
|
try {
|
|
7790
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
8417
|
+
const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
|
|
7791
8418
|
const fileName = basename6(fullPath);
|
|
7792
8419
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
7793
8420
|
const mimeMap = {
|
|
@@ -7845,11 +8472,11 @@ ${taskAddendum}`;
|
|
|
7845
8472
|
wrappedTools[name] = originalTool;
|
|
7846
8473
|
continue;
|
|
7847
8474
|
}
|
|
7848
|
-
wrappedTools[name] =
|
|
8475
|
+
wrappedTools[name] = tool14({
|
|
7849
8476
|
description: originalTool.description || "",
|
|
7850
|
-
inputSchema: originalTool.inputSchema ||
|
|
8477
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
7851
8478
|
execute: async (input, toolOptions) => {
|
|
7852
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
8479
|
+
const toolCallId = toolOptions.toolCallId || nanoid5();
|
|
7853
8480
|
const execution = toolExecutionQueries.create({
|
|
7854
8481
|
sessionId: this.session.id,
|
|
7855
8482
|
toolName: name,
|
|
@@ -7867,10 +8494,10 @@ ${taskAddendum}`;
|
|
|
7867
8494
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
7868
8495
|
approvalResolvers.delete(toolCallId);
|
|
7869
8496
|
this.pendingApprovals.delete(toolCallId);
|
|
7870
|
-
const
|
|
8497
|
+
const exec8 = await execution;
|
|
7871
8498
|
if (!approved) {
|
|
7872
8499
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
7873
|
-
await toolExecutionQueries.reject(
|
|
8500
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
7874
8501
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7875
8502
|
return {
|
|
7876
8503
|
status: "rejected",
|
|
@@ -7880,14 +8507,14 @@ ${taskAddendum}`;
|
|
|
7880
8507
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
7881
8508
|
};
|
|
7882
8509
|
}
|
|
7883
|
-
await toolExecutionQueries.approve(
|
|
8510
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
7884
8511
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7885
8512
|
try {
|
|
7886
8513
|
const result = await originalTool.execute(input, toolOptions);
|
|
7887
|
-
await toolExecutionQueries.complete(
|
|
8514
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
7888
8515
|
return result;
|
|
7889
8516
|
} catch (error) {
|
|
7890
|
-
await toolExecutionQueries.complete(
|
|
8517
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
7891
8518
|
throw error;
|
|
7892
8519
|
}
|
|
7893
8520
|
}
|
|
@@ -7988,18 +8615,20 @@ function cleanupPendingInputs() {
|
|
|
7988
8615
|
}
|
|
7989
8616
|
}
|
|
7990
8617
|
}
|
|
7991
|
-
var createSessionSchema =
|
|
7992
|
-
name:
|
|
7993
|
-
workingDirectory:
|
|
7994
|
-
model:
|
|
7995
|
-
toolApprovals:
|
|
8618
|
+
var createSessionSchema = z16.object({
|
|
8619
|
+
name: z16.string().optional(),
|
|
8620
|
+
workingDirectory: z16.string().optional(),
|
|
8621
|
+
model: z16.string().optional(),
|
|
8622
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional(),
|
|
8623
|
+
// Optional full session-config passthrough (computerUseEnabled, etc.)
|
|
8624
|
+
config: z16.record(z16.string(), z16.unknown()).optional()
|
|
7996
8625
|
});
|
|
7997
|
-
var paginationQuerySchema =
|
|
7998
|
-
limit:
|
|
7999
|
-
offset:
|
|
8626
|
+
var paginationQuerySchema = z16.object({
|
|
8627
|
+
limit: z16.string().optional(),
|
|
8628
|
+
offset: z16.string().optional()
|
|
8000
8629
|
});
|
|
8001
|
-
var messagesQuerySchema =
|
|
8002
|
-
limit:
|
|
8630
|
+
var messagesQuerySchema = z16.object({
|
|
8631
|
+
limit: z16.string().optional()
|
|
8003
8632
|
});
|
|
8004
8633
|
sessions.get(
|
|
8005
8634
|
"/",
|
|
@@ -8037,11 +8666,20 @@ sessions.post(
|
|
|
8037
8666
|
async (c) => {
|
|
8038
8667
|
const body = c.req.valid("json");
|
|
8039
8668
|
const config = getConfig();
|
|
8669
|
+
const cuDefault = process.env.SPARKECODER_COMPUTER_USE === "1";
|
|
8670
|
+
const baseConfig = body.config || {};
|
|
8671
|
+
const mergedConfig = {
|
|
8672
|
+
...baseConfig,
|
|
8673
|
+
...body.toolApprovals ? { toolApprovals: body.toolApprovals } : {},
|
|
8674
|
+
// Turn on computer use by default if the server was launched with --enable-computer-use,
|
|
8675
|
+
// unless the client explicitly provided a value.
|
|
8676
|
+
...cuDefault && baseConfig.computerUseEnabled === void 0 ? { computerUseEnabled: true } : {}
|
|
8677
|
+
};
|
|
8040
8678
|
const agent = await Agent.create({
|
|
8041
8679
|
name: body.name,
|
|
8042
8680
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8043
8681
|
model: body.model || config.defaultModel,
|
|
8044
|
-
sessionConfig:
|
|
8682
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8045
8683
|
});
|
|
8046
8684
|
const session = agent.getSession();
|
|
8047
8685
|
return c.json({
|
|
@@ -8138,10 +8776,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8138
8776
|
count: executions.length
|
|
8139
8777
|
});
|
|
8140
8778
|
});
|
|
8141
|
-
var updateSessionSchema =
|
|
8142
|
-
model:
|
|
8143
|
-
name:
|
|
8144
|
-
toolApprovals:
|
|
8779
|
+
var updateSessionSchema = z16.object({
|
|
8780
|
+
model: z16.string().optional(),
|
|
8781
|
+
name: z16.string().optional(),
|
|
8782
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
|
|
8145
8783
|
});
|
|
8146
8784
|
sessions.patch(
|
|
8147
8785
|
"/:id",
|
|
@@ -8211,8 +8849,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8211
8849
|
await agent.clearContext();
|
|
8212
8850
|
return c.json({ success: true, sessionId: id });
|
|
8213
8851
|
});
|
|
8214
|
-
var pendingInputSchema =
|
|
8215
|
-
text:
|
|
8852
|
+
var pendingInputSchema = z16.object({
|
|
8853
|
+
text: z16.string()
|
|
8216
8854
|
});
|
|
8217
8855
|
sessions.post(
|
|
8218
8856
|
"/:id/pending-input",
|
|
@@ -8243,13 +8881,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
8243
8881
|
createdAt: pending.createdAt.toISOString()
|
|
8244
8882
|
});
|
|
8245
8883
|
});
|
|
8246
|
-
var devtoolsContextSchema =
|
|
8247
|
-
url:
|
|
8248
|
-
path:
|
|
8249
|
-
pageName:
|
|
8250
|
-
screenWidth:
|
|
8251
|
-
screenHeight:
|
|
8252
|
-
devicePixelRatio:
|
|
8884
|
+
var devtoolsContextSchema = z16.object({
|
|
8885
|
+
url: z16.string(),
|
|
8886
|
+
path: z16.string(),
|
|
8887
|
+
pageName: z16.string().optional(),
|
|
8888
|
+
screenWidth: z16.number().optional(),
|
|
8889
|
+
screenHeight: z16.number().optional(),
|
|
8890
|
+
devicePixelRatio: z16.number().optional()
|
|
8253
8891
|
});
|
|
8254
8892
|
sessions.post(
|
|
8255
8893
|
"/:id/devtools-context",
|
|
@@ -8435,12 +9073,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
8435
9073
|
});
|
|
8436
9074
|
function getAttachmentsDir(sessionId) {
|
|
8437
9075
|
const appDataDir = getAppDataDirectory();
|
|
8438
|
-
return
|
|
9076
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
8439
9077
|
}
|
|
8440
9078
|
function ensureAttachmentsDir(sessionId) {
|
|
8441
9079
|
const dir = getAttachmentsDir(sessionId);
|
|
8442
|
-
if (!
|
|
8443
|
-
|
|
9080
|
+
if (!existsSync16(dir)) {
|
|
9081
|
+
mkdirSync6(dir, { recursive: true });
|
|
8444
9082
|
}
|
|
8445
9083
|
return dir;
|
|
8446
9084
|
}
|
|
@@ -8451,12 +9089,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
8451
9089
|
return c.json({ error: "Session not found" }, 404);
|
|
8452
9090
|
}
|
|
8453
9091
|
const dir = getAttachmentsDir(sessionId);
|
|
8454
|
-
if (!
|
|
9092
|
+
if (!existsSync16(dir)) {
|
|
8455
9093
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
8456
9094
|
}
|
|
8457
9095
|
const files = readdirSync2(dir);
|
|
8458
9096
|
const attachments = files.map((filename) => {
|
|
8459
|
-
const filePath =
|
|
9097
|
+
const filePath = join10(dir, filename);
|
|
8460
9098
|
const stats = statSync2(filePath);
|
|
8461
9099
|
return {
|
|
8462
9100
|
id: filename.split("_")[0],
|
|
@@ -8488,10 +9126,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8488
9126
|
return c.json({ error: "No file provided" }, 400);
|
|
8489
9127
|
}
|
|
8490
9128
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8491
|
-
const id =
|
|
9129
|
+
const id = nanoid6(10);
|
|
8492
9130
|
const ext = extname8(file.name) || "";
|
|
8493
9131
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8494
|
-
const filePath =
|
|
9132
|
+
const filePath = join10(dir, safeFilename);
|
|
8495
9133
|
const arrayBuffer = await file.arrayBuffer();
|
|
8496
9134
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
8497
9135
|
return c.json({
|
|
@@ -8514,10 +9152,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8514
9152
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
8515
9153
|
}
|
|
8516
9154
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8517
|
-
const id =
|
|
9155
|
+
const id = nanoid6(10);
|
|
8518
9156
|
const ext = extname8(body.filename) || "";
|
|
8519
9157
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8520
|
-
const filePath =
|
|
9158
|
+
const filePath = join10(dir, safeFilename);
|
|
8521
9159
|
let base64Data = body.data;
|
|
8522
9160
|
if (base64Data.includes(",")) {
|
|
8523
9161
|
base64Data = base64Data.split(",")[1];
|
|
@@ -8546,7 +9184,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8546
9184
|
return c.json({ error: "Session not found" }, 404);
|
|
8547
9185
|
}
|
|
8548
9186
|
const dir = getAttachmentsDir(sessionId);
|
|
8549
|
-
if (!
|
|
9187
|
+
if (!existsSync16(dir)) {
|
|
8550
9188
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8551
9189
|
}
|
|
8552
9190
|
const files = readdirSync2(dir);
|
|
@@ -8554,14 +9192,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8554
9192
|
if (!file) {
|
|
8555
9193
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8556
9194
|
}
|
|
8557
|
-
const filePath =
|
|
8558
|
-
|
|
9195
|
+
const filePath = join10(dir, file);
|
|
9196
|
+
unlinkSync3(filePath);
|
|
8559
9197
|
return c.json({ success: true, id: attachmentId });
|
|
8560
9198
|
});
|
|
8561
|
-
var filesQuerySchema =
|
|
8562
|
-
query:
|
|
9199
|
+
var filesQuerySchema = z16.object({
|
|
9200
|
+
query: z16.string().optional(),
|
|
8563
9201
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
8564
|
-
limit:
|
|
9202
|
+
limit: z16.string().optional()
|
|
8565
9203
|
// Max results (default 50)
|
|
8566
9204
|
});
|
|
8567
9205
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -8637,7 +9275,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
8637
9275
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
8638
9276
|
for (const entry of entries) {
|
|
8639
9277
|
if (results.length >= limit * 2) break;
|
|
8640
|
-
const fullPath =
|
|
9278
|
+
const fullPath = join10(currentDir, entry.name);
|
|
8641
9279
|
const relativePath = relative9(baseDir, fullPath);
|
|
8642
9280
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
8643
9281
|
continue;
|
|
@@ -8685,7 +9323,7 @@ sessions.get(
|
|
|
8685
9323
|
return c.json({ error: "Session not found" }, 404);
|
|
8686
9324
|
}
|
|
8687
9325
|
const workingDirectory = session.workingDirectory;
|
|
8688
|
-
if (!
|
|
9326
|
+
if (!existsSync16(workingDirectory)) {
|
|
8689
9327
|
return c.json({
|
|
8690
9328
|
sessionId,
|
|
8691
9329
|
workingDirectory,
|
|
@@ -8795,9 +9433,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
8795
9433
|
init_db();
|
|
8796
9434
|
import { Hono as Hono2 } from "hono";
|
|
8797
9435
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
8798
|
-
import { z as
|
|
8799
|
-
import { existsSync as
|
|
8800
|
-
import { join as
|
|
9436
|
+
import { z as z17 } from "zod";
|
|
9437
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
9438
|
+
import { join as join11 } from "path";
|
|
8801
9439
|
init_config();
|
|
8802
9440
|
|
|
8803
9441
|
// src/server/resumable-stream.ts
|
|
@@ -8884,7 +9522,7 @@ var streamContext = createResumableStreamContext({
|
|
|
8884
9522
|
});
|
|
8885
9523
|
|
|
8886
9524
|
// src/server/routes/agents.ts
|
|
8887
|
-
import { nanoid as
|
|
9525
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
8888
9526
|
init_stream_proxy();
|
|
8889
9527
|
init_recorder();
|
|
8890
9528
|
init_remote();
|
|
@@ -8975,40 +9613,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
8975
9613
|
${prompt}`;
|
|
8976
9614
|
}
|
|
8977
9615
|
var agents = new Hono2();
|
|
8978
|
-
var attachmentSchema =
|
|
8979
|
-
type:
|
|
8980
|
-
data:
|
|
9616
|
+
var attachmentSchema = z17.object({
|
|
9617
|
+
type: z17.enum(["image", "file"]),
|
|
9618
|
+
data: z17.string(),
|
|
8981
9619
|
// base64 data URL or raw base64
|
|
8982
|
-
mediaType:
|
|
8983
|
-
filename:
|
|
9620
|
+
mediaType: z17.string().optional(),
|
|
9621
|
+
filename: z17.string().optional()
|
|
8984
9622
|
});
|
|
8985
|
-
var runPromptSchema =
|
|
8986
|
-
prompt:
|
|
9623
|
+
var runPromptSchema = z17.object({
|
|
9624
|
+
prompt: z17.string(),
|
|
8987
9625
|
// Can be empty if attachments are provided
|
|
8988
|
-
attachments:
|
|
9626
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
8989
9627
|
}).refine(
|
|
8990
9628
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
8991
9629
|
{ message: "Either prompt or attachments must be provided" }
|
|
8992
9630
|
);
|
|
8993
|
-
var quickStartSchema =
|
|
8994
|
-
prompt:
|
|
8995
|
-
name:
|
|
8996
|
-
workingDirectory:
|
|
8997
|
-
model:
|
|
8998
|
-
toolApprovals:
|
|
9631
|
+
var quickStartSchema = z17.object({
|
|
9632
|
+
prompt: z17.string().min(1),
|
|
9633
|
+
name: z17.string().optional(),
|
|
9634
|
+
workingDirectory: z17.string().optional(),
|
|
9635
|
+
model: z17.string().optional(),
|
|
9636
|
+
toolApprovals: z17.record(z17.string(), z17.boolean()).optional()
|
|
8999
9637
|
});
|
|
9000
|
-
var rejectSchema =
|
|
9001
|
-
reason:
|
|
9638
|
+
var rejectSchema = z17.object({
|
|
9639
|
+
reason: z17.string().optional()
|
|
9002
9640
|
}).optional();
|
|
9003
9641
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9004
9642
|
function getAttachmentsDirectory(sessionId) {
|
|
9005
9643
|
const appDataDir = getAppDataDirectory();
|
|
9006
|
-
return
|
|
9644
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9007
9645
|
}
|
|
9008
9646
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9009
9647
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9010
|
-
if (!
|
|
9011
|
-
|
|
9648
|
+
if (!existsSync17(attachmentsDir)) {
|
|
9649
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9012
9650
|
}
|
|
9013
9651
|
let filename = attachment.filename;
|
|
9014
9652
|
if (!filename) {
|
|
@@ -9026,7 +9664,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9026
9664
|
attachment.mediaType = resized.mediaType;
|
|
9027
9665
|
attachment.data = buffer.toString("base64");
|
|
9028
9666
|
}
|
|
9029
|
-
const filePath =
|
|
9667
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9030
9668
|
writeFileSync4(filePath, buffer);
|
|
9031
9669
|
return filePath;
|
|
9032
9670
|
}
|
|
@@ -9037,9 +9675,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9037
9675
|
}
|
|
9038
9676
|
return data;
|
|
9039
9677
|
}
|
|
9040
|
-
function getExtensionFromMediaType(mediaType,
|
|
9678
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9041
9679
|
if (!mediaType) {
|
|
9042
|
-
return
|
|
9680
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9043
9681
|
}
|
|
9044
9682
|
const mimeToExt = {
|
|
9045
9683
|
"image/png": ".png",
|
|
@@ -9443,7 +10081,7 @@ ${prompt}` });
|
|
|
9443
10081
|
userMessageContent = prompt;
|
|
9444
10082
|
}
|
|
9445
10083
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
9446
|
-
const streamId = `stream_${id}_${
|
|
10084
|
+
const streamId = `stream_${id}_${nanoid7(10)}`;
|
|
9447
10085
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
9448
10086
|
await activeStreamQueries.create(id, streamId);
|
|
9449
10087
|
const stream = await streamContext.resumableStream(
|
|
@@ -9648,7 +10286,7 @@ agents.post(
|
|
|
9648
10286
|
});
|
|
9649
10287
|
const session = agent.getSession();
|
|
9650
10288
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
9651
|
-
const streamId = `stream_${session.id}_${
|
|
10289
|
+
const streamId = `stream_${session.id}_${nanoid7(10)}`;
|
|
9652
10290
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
9653
10291
|
await activeStreamQueries.create(session.id, streamId);
|
|
9654
10292
|
const createQuickStreamProducer = () => {
|
|
@@ -9915,23 +10553,23 @@ agents.post(
|
|
|
9915
10553
|
});
|
|
9916
10554
|
}
|
|
9917
10555
|
);
|
|
9918
|
-
var browserInputSchema =
|
|
9919
|
-
type:
|
|
9920
|
-
eventType:
|
|
9921
|
-
x:
|
|
9922
|
-
y:
|
|
9923
|
-
button:
|
|
9924
|
-
clickCount:
|
|
9925
|
-
deltaX:
|
|
9926
|
-
deltaY:
|
|
9927
|
-
key:
|
|
9928
|
-
code:
|
|
9929
|
-
text:
|
|
9930
|
-
modifiers:
|
|
9931
|
-
touchPoints:
|
|
9932
|
-
x:
|
|
9933
|
-
y:
|
|
9934
|
-
id:
|
|
10556
|
+
var browserInputSchema = z17.object({
|
|
10557
|
+
type: z17.enum(["input_mouse", "input_keyboard", "input_touch"]),
|
|
10558
|
+
eventType: z17.string(),
|
|
10559
|
+
x: z17.number().optional(),
|
|
10560
|
+
y: z17.number().optional(),
|
|
10561
|
+
button: z17.string().optional(),
|
|
10562
|
+
clickCount: z17.number().optional(),
|
|
10563
|
+
deltaX: z17.number().optional(),
|
|
10564
|
+
deltaY: z17.number().optional(),
|
|
10565
|
+
key: z17.string().optional(),
|
|
10566
|
+
code: z17.string().optional(),
|
|
10567
|
+
text: z17.string().optional(),
|
|
10568
|
+
modifiers: z17.number().optional(),
|
|
10569
|
+
touchPoints: z17.array(z17.object({
|
|
10570
|
+
x: z17.number(),
|
|
10571
|
+
y: z17.number(),
|
|
10572
|
+
id: z17.number().optional()
|
|
9935
10573
|
})).optional()
|
|
9936
10574
|
});
|
|
9937
10575
|
agents.post(
|
|
@@ -9966,27 +10604,279 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
9966
10604
|
init_config();
|
|
9967
10605
|
import { Hono as Hono3 } from "hono";
|
|
9968
10606
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
9969
|
-
import { z as
|
|
9970
|
-
import { readFileSync as
|
|
10607
|
+
import { z as z18 } from "zod";
|
|
10608
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
9971
10609
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9972
|
-
import { dirname as dirname6, join as
|
|
10610
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10611
|
+
|
|
10612
|
+
// src/personal-agent/heartbeat.ts
|
|
10613
|
+
import { execSync as execSync3 } from "child_process";
|
|
10614
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10615
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
10616
|
+
|
|
10617
|
+
// src/personal-agent/system-metrics.ts
|
|
10618
|
+
import { execSync as execSync2 } from "child_process";
|
|
10619
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
10620
|
+
import {
|
|
10621
|
+
arch,
|
|
10622
|
+
cpus,
|
|
10623
|
+
freemem,
|
|
10624
|
+
hostname,
|
|
10625
|
+
loadavg,
|
|
10626
|
+
networkInterfaces,
|
|
10627
|
+
platform as platform2,
|
|
10628
|
+
release,
|
|
10629
|
+
totalmem,
|
|
10630
|
+
type,
|
|
10631
|
+
uptime,
|
|
10632
|
+
userInfo
|
|
10633
|
+
} from "os";
|
|
10634
|
+
var _lastSample = null;
|
|
10635
|
+
function snapshotCpuTimes() {
|
|
10636
|
+
const sum = { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
|
|
10637
|
+
for (const c of cpus()) {
|
|
10638
|
+
sum.user += c.times.user;
|
|
10639
|
+
sum.nice += c.times.nice;
|
|
10640
|
+
sum.sys += c.times.sys;
|
|
10641
|
+
sum.idle += c.times.idle;
|
|
10642
|
+
sum.irq += c.times.irq;
|
|
10643
|
+
}
|
|
10644
|
+
return sum;
|
|
10645
|
+
}
|
|
10646
|
+
function readCpuUsage() {
|
|
10647
|
+
const now = snapshotCpuTimes();
|
|
10648
|
+
if (!_lastSample) {
|
|
10649
|
+
_lastSample = now;
|
|
10650
|
+
return 0;
|
|
10651
|
+
}
|
|
10652
|
+
const dUser = now.user - _lastSample.user;
|
|
10653
|
+
const dNice = now.nice - _lastSample.nice;
|
|
10654
|
+
const dSys = now.sys - _lastSample.sys;
|
|
10655
|
+
const dIdle = now.idle - _lastSample.idle;
|
|
10656
|
+
const dIrq = now.irq - _lastSample.irq;
|
|
10657
|
+
const total = dUser + dNice + dSys + dIdle + dIrq;
|
|
10658
|
+
_lastSample = now;
|
|
10659
|
+
if (total <= 0) return 0;
|
|
10660
|
+
return Math.max(0, Math.min(1, (total - dIdle) / total));
|
|
10661
|
+
}
|
|
10662
|
+
snapshotCpuTimes();
|
|
10663
|
+
function readCpuTempC() {
|
|
10664
|
+
try {
|
|
10665
|
+
if (platform2() === "linux") {
|
|
10666
|
+
let hottest = -Infinity;
|
|
10667
|
+
try {
|
|
10668
|
+
for (const entry of readdirSync3("/sys/class/thermal")) {
|
|
10669
|
+
if (!entry.startsWith("thermal_zone")) continue;
|
|
10670
|
+
try {
|
|
10671
|
+
const v = Number(
|
|
10672
|
+
readFileSync8(`/sys/class/thermal/${entry}/temp`, "utf8").trim()
|
|
10673
|
+
);
|
|
10674
|
+
if (Number.isFinite(v) && v > hottest) hottest = v;
|
|
10675
|
+
} catch {
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
} catch {
|
|
10679
|
+
}
|
|
10680
|
+
if (hottest > -Infinity) return hottest / 1e3;
|
|
10681
|
+
}
|
|
10682
|
+
const overrideCmd = process.env.PERSONAL_AGENT_TEMP_CMD;
|
|
10683
|
+
if (overrideCmd) {
|
|
10684
|
+
const out = execSync2(overrideCmd, { encoding: "utf8", timeout: 1500 }).trim();
|
|
10685
|
+
const v = Number(out);
|
|
10686
|
+
if (Number.isFinite(v)) return v;
|
|
10687
|
+
}
|
|
10688
|
+
} catch {
|
|
10689
|
+
}
|
|
10690
|
+
return void 0;
|
|
10691
|
+
}
|
|
10692
|
+
function readDisks() {
|
|
10693
|
+
try {
|
|
10694
|
+
const p = platform2();
|
|
10695
|
+
if (p === "darwin" || p === "linux") {
|
|
10696
|
+
const raw = execSync2("df -kP", { encoding: "utf8", timeout: 2e3 });
|
|
10697
|
+
const lines = raw.trim().split("\n").slice(1);
|
|
10698
|
+
const out = [];
|
|
10699
|
+
for (const line of lines) {
|
|
10700
|
+
const parts = line.trim().split(/\s+/);
|
|
10701
|
+
if (parts.length < 6) continue;
|
|
10702
|
+
const filesystem = parts[0];
|
|
10703
|
+
const total1k = Number(parts[1]);
|
|
10704
|
+
const used1k = Number(parts[2]);
|
|
10705
|
+
const free1k = Number(parts[3]);
|
|
10706
|
+
const mount = parts.slice(5).join(" ");
|
|
10707
|
+
if (!Number.isFinite(total1k) || total1k <= 0) continue;
|
|
10708
|
+
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")) {
|
|
10709
|
+
if (mount !== "/") continue;
|
|
10710
|
+
}
|
|
10711
|
+
out.push({
|
|
10712
|
+
mount,
|
|
10713
|
+
filesystem,
|
|
10714
|
+
totalBytes: total1k * 1024,
|
|
10715
|
+
usedBytes: used1k * 1024,
|
|
10716
|
+
freeBytes: free1k * 1024,
|
|
10717
|
+
usage: total1k > 0 ? used1k / total1k : 0
|
|
10718
|
+
});
|
|
10719
|
+
}
|
|
10720
|
+
out.sort((a, b) => {
|
|
10721
|
+
const score = (m) => m === "/" ? 0 : m.startsWith("/Users") || m.startsWith("/home") ? 1 : 2;
|
|
10722
|
+
return score(a.mount) - score(b.mount);
|
|
10723
|
+
});
|
|
10724
|
+
return out.slice(0, 6);
|
|
10725
|
+
}
|
|
10726
|
+
if (p === "win32") {
|
|
10727
|
+
const raw = execSync2(
|
|
10728
|
+
"wmic logicaldisk get DeviceID,Size,FreeSpace /format:csv",
|
|
10729
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
10730
|
+
);
|
|
10731
|
+
const out = [];
|
|
10732
|
+
for (const line of raw.trim().split(/\r?\n/).slice(1)) {
|
|
10733
|
+
const cols = line.split(",");
|
|
10734
|
+
if (cols.length < 4) continue;
|
|
10735
|
+
const [, deviceId, freeStr, sizeStr] = cols;
|
|
10736
|
+
const total = Number(sizeStr);
|
|
10737
|
+
const free = Number(freeStr);
|
|
10738
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
10739
|
+
const used = Math.max(0, total - free);
|
|
10740
|
+
out.push({
|
|
10741
|
+
mount: deviceId,
|
|
10742
|
+
totalBytes: total,
|
|
10743
|
+
usedBytes: used,
|
|
10744
|
+
freeBytes: free,
|
|
10745
|
+
usage: used / total
|
|
10746
|
+
});
|
|
10747
|
+
}
|
|
10748
|
+
return out;
|
|
10749
|
+
}
|
|
10750
|
+
} catch {
|
|
10751
|
+
}
|
|
10752
|
+
return void 0;
|
|
10753
|
+
}
|
|
10754
|
+
function readNetwork() {
|
|
10755
|
+
try {
|
|
10756
|
+
const out = [];
|
|
10757
|
+
const ifaces = networkInterfaces();
|
|
10758
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
10759
|
+
if (!addrs) continue;
|
|
10760
|
+
for (const a of addrs) {
|
|
10761
|
+
if (a.internal) continue;
|
|
10762
|
+
out.push({
|
|
10763
|
+
iface: name,
|
|
10764
|
+
family: a.family,
|
|
10765
|
+
address: a.address,
|
|
10766
|
+
mac: a.mac,
|
|
10767
|
+
internal: a.internal
|
|
10768
|
+
});
|
|
10769
|
+
}
|
|
10770
|
+
}
|
|
10771
|
+
return out;
|
|
10772
|
+
} catch {
|
|
10773
|
+
return void 0;
|
|
10774
|
+
}
|
|
10775
|
+
}
|
|
10776
|
+
function readSystemMetrics() {
|
|
10777
|
+
const cpuList = cpus();
|
|
10778
|
+
const usage = readCpuUsage();
|
|
10779
|
+
const tot = totalmem();
|
|
10780
|
+
const free = freemem();
|
|
10781
|
+
const metrics = {
|
|
10782
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10783
|
+
hostname: hostname(),
|
|
10784
|
+
platform: platform2(),
|
|
10785
|
+
arch: arch(),
|
|
10786
|
+
kernelRelease: release(),
|
|
10787
|
+
osType: type(),
|
|
10788
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
10789
|
+
systemUptimeSec: Math.round(uptime()),
|
|
10790
|
+
user: safeUser(),
|
|
10791
|
+
cpu: cpuList[0] ? {
|
|
10792
|
+
model: cpuList[0].model,
|
|
10793
|
+
count: cpuList.length,
|
|
10794
|
+
speedMhz: cpuList[0].speed,
|
|
10795
|
+
loadAvg1: loadavg()[0],
|
|
10796
|
+
loadAvg5: loadavg()[1],
|
|
10797
|
+
loadAvg15: loadavg()[2],
|
|
10798
|
+
usage,
|
|
10799
|
+
tempC: readCpuTempC()
|
|
10800
|
+
} : void 0,
|
|
10801
|
+
memory: {
|
|
10802
|
+
totalBytes: tot,
|
|
10803
|
+
freeBytes: free,
|
|
10804
|
+
usedBytes: Math.max(0, tot - free),
|
|
10805
|
+
usage: tot > 0 ? (tot - free) / tot : 0
|
|
10806
|
+
},
|
|
10807
|
+
disks: readDisks(),
|
|
10808
|
+
network: readNetwork()
|
|
10809
|
+
};
|
|
10810
|
+
return metrics;
|
|
10811
|
+
}
|
|
10812
|
+
function safeUser() {
|
|
10813
|
+
try {
|
|
10814
|
+
return userInfo().username;
|
|
10815
|
+
} catch {
|
|
10816
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
10817
|
+
}
|
|
10818
|
+
}
|
|
10819
|
+
|
|
10820
|
+
// src/personal-agent/heartbeat.ts
|
|
10821
|
+
var _cachedHwid = null;
|
|
10822
|
+
function getHardwareIdCached() {
|
|
10823
|
+
if (_cachedHwid !== null) return _cachedHwid;
|
|
10824
|
+
_cachedHwid = getHardwareId();
|
|
10825
|
+
return _cachedHwid;
|
|
10826
|
+
}
|
|
10827
|
+
function getHardwareId() {
|
|
10828
|
+
const p = platform3();
|
|
10829
|
+
try {
|
|
10830
|
+
if (p === "darwin") {
|
|
10831
|
+
const out = execSync3(
|
|
10832
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/ {print $4}'`,
|
|
10833
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
10834
|
+
).trim();
|
|
10835
|
+
if (out) return normalize2(out);
|
|
10836
|
+
} else if (p === "linux") {
|
|
10837
|
+
try {
|
|
10838
|
+
return normalize2(readFileSync9("/etc/machine-id", "utf8").trim());
|
|
10839
|
+
} catch {
|
|
10840
|
+
return normalize2(readFileSync9("/var/lib/dbus/machine-id", "utf8").trim());
|
|
10841
|
+
}
|
|
10842
|
+
} else if (p === "win32") {
|
|
10843
|
+
const out = execSync3("wmic csproduct get uuid /value", {
|
|
10844
|
+
encoding: "utf8",
|
|
10845
|
+
timeout: 3e3
|
|
10846
|
+
});
|
|
10847
|
+
const m = out.match(/UUID=([\w-]+)/i);
|
|
10848
|
+
if (m && m[1]) return normalize2(m[1]);
|
|
10849
|
+
}
|
|
10850
|
+
} catch (e) {
|
|
10851
|
+
console.warn(`[personal-agent] could not read hardware UUID: ${e.message}`);
|
|
10852
|
+
}
|
|
10853
|
+
console.warn(
|
|
10854
|
+
"[personal-agent] falling back to hostname for HWID; this is NOT stable across rename/reinstall"
|
|
10855
|
+
);
|
|
10856
|
+
return `host-${hostname2()}`;
|
|
10857
|
+
}
|
|
10858
|
+
function normalize2(raw) {
|
|
10859
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
10860
|
+
}
|
|
10861
|
+
|
|
10862
|
+
// src/server/routes/health.ts
|
|
9973
10863
|
var __filename = fileURLToPath3(import.meta.url);
|
|
9974
10864
|
var __dirname = dirname6(__filename);
|
|
9975
10865
|
var possiblePaths = [
|
|
9976
|
-
|
|
10866
|
+
join12(__dirname, "../package.json"),
|
|
9977
10867
|
// From dist/server -> dist/../package.json
|
|
9978
|
-
|
|
10868
|
+
join12(__dirname, "../../package.json"),
|
|
9979
10869
|
// From dist/server (if nested differently)
|
|
9980
|
-
|
|
10870
|
+
join12(__dirname, "../../../package.json"),
|
|
9981
10871
|
// From src/server/routes (development)
|
|
9982
|
-
|
|
10872
|
+
join12(process.cwd(), "package.json")
|
|
9983
10873
|
// From current working directory
|
|
9984
10874
|
];
|
|
9985
10875
|
var currentVersion = "0.0.0";
|
|
9986
10876
|
var packageName = "sparkecoder";
|
|
9987
10877
|
for (const packageJsonPath of possiblePaths) {
|
|
9988
10878
|
try {
|
|
9989
|
-
const packageJson = JSON.parse(
|
|
10879
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
9990
10880
|
if (packageJson.name === "sparkecoder") {
|
|
9991
10881
|
currentVersion = packageJson.version || "0.0.0";
|
|
9992
10882
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10001,11 +10891,17 @@ health.get("/", async (c) => {
|
|
|
10001
10891
|
const apiKeyStatus = getApiKeyStatus();
|
|
10002
10892
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10003
10893
|
const hasApiKey = gatewayKey?.configured ?? false;
|
|
10894
|
+
let hwid;
|
|
10895
|
+
try {
|
|
10896
|
+
hwid = getHardwareIdCached();
|
|
10897
|
+
} catch {
|
|
10898
|
+
}
|
|
10004
10899
|
return c.json({
|
|
10005
10900
|
status: "ok",
|
|
10006
10901
|
version: currentVersion,
|
|
10007
10902
|
uptime: process.uptime(),
|
|
10008
10903
|
apiKeyConfigured: hasApiKey,
|
|
10904
|
+
hwid,
|
|
10009
10905
|
config: {
|
|
10010
10906
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10011
10907
|
defaultModel: config.defaultModel,
|
|
@@ -10076,9 +10972,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10076
10972
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10077
10973
|
});
|
|
10078
10974
|
});
|
|
10079
|
-
var setApiKeySchema =
|
|
10080
|
-
provider:
|
|
10081
|
-
apiKey:
|
|
10975
|
+
var setApiKeySchema = z18.object({
|
|
10976
|
+
provider: z18.string(),
|
|
10977
|
+
apiKey: z18.string().min(1)
|
|
10082
10978
|
});
|
|
10083
10979
|
health.post(
|
|
10084
10980
|
"/api-keys",
|
|
@@ -10117,13 +11013,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10117
11013
|
// src/server/routes/terminals.ts
|
|
10118
11014
|
import { Hono as Hono4 } from "hono";
|
|
10119
11015
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10120
|
-
import { z as
|
|
11016
|
+
import { z as z19 } from "zod";
|
|
10121
11017
|
init_db();
|
|
10122
11018
|
var terminals = new Hono4();
|
|
10123
|
-
var spawnSchema =
|
|
10124
|
-
command:
|
|
10125
|
-
cwd:
|
|
10126
|
-
name:
|
|
11019
|
+
var spawnSchema = z19.object({
|
|
11020
|
+
command: z19.string(),
|
|
11021
|
+
cwd: z19.string().optional(),
|
|
11022
|
+
name: z19.string().optional()
|
|
10127
11023
|
});
|
|
10128
11024
|
terminals.post(
|
|
10129
11025
|
"/:sessionId/terminals",
|
|
@@ -10204,8 +11100,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10204
11100
|
// We don't track exit codes in tmux mode
|
|
10205
11101
|
});
|
|
10206
11102
|
});
|
|
10207
|
-
var logsQuerySchema =
|
|
10208
|
-
tail:
|
|
11103
|
+
var logsQuerySchema = z19.object({
|
|
11104
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10209
11105
|
});
|
|
10210
11106
|
terminals.get(
|
|
10211
11107
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -10229,8 +11125,8 @@ terminals.get(
|
|
|
10229
11125
|
});
|
|
10230
11126
|
}
|
|
10231
11127
|
);
|
|
10232
|
-
var killSchema =
|
|
10233
|
-
signal:
|
|
11128
|
+
var killSchema = z19.object({
|
|
11129
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
10234
11130
|
});
|
|
10235
11131
|
terminals.post(
|
|
10236
11132
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -10244,8 +11140,8 @@ terminals.post(
|
|
|
10244
11140
|
return c.json({ success: true, message: "Terminal killed" });
|
|
10245
11141
|
}
|
|
10246
11142
|
);
|
|
10247
|
-
var writeSchema =
|
|
10248
|
-
input:
|
|
11143
|
+
var writeSchema = z19.object({
|
|
11144
|
+
input: z19.string()
|
|
10249
11145
|
});
|
|
10250
11146
|
terminals.post(
|
|
10251
11147
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -10430,20 +11326,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
10430
11326
|
init_db();
|
|
10431
11327
|
import { Hono as Hono5 } from "hono";
|
|
10432
11328
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
10433
|
-
import { z as
|
|
10434
|
-
import { nanoid as
|
|
11329
|
+
import { z as z20 } from "zod";
|
|
11330
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
10435
11331
|
init_config();
|
|
10436
11332
|
var tasks = new Hono5();
|
|
10437
11333
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
10438
|
-
var createTaskSchema =
|
|
10439
|
-
prompt:
|
|
10440
|
-
outputSchema:
|
|
10441
|
-
webhookUrl:
|
|
10442
|
-
model:
|
|
10443
|
-
workingDirectory:
|
|
10444
|
-
name:
|
|
10445
|
-
maxIterations:
|
|
10446
|
-
parentTaskId:
|
|
11334
|
+
var createTaskSchema = z20.object({
|
|
11335
|
+
prompt: z20.string().min(1),
|
|
11336
|
+
outputSchema: z20.record(z20.string(), z20.unknown()),
|
|
11337
|
+
webhookUrl: z20.string().url().optional(),
|
|
11338
|
+
model: z20.string().optional(),
|
|
11339
|
+
workingDirectory: z20.string().optional(),
|
|
11340
|
+
name: z20.string().optional(),
|
|
11341
|
+
maxIterations: z20.number().int().min(1).max(500).optional(),
|
|
11342
|
+
parentTaskId: z20.string().optional()
|
|
10447
11343
|
});
|
|
10448
11344
|
tasks.post(
|
|
10449
11345
|
"/",
|
|
@@ -10505,7 +11401,7 @@ tasks.post(
|
|
|
10505
11401
|
const taskId = agent.sessionId;
|
|
10506
11402
|
const abortController = new AbortController();
|
|
10507
11403
|
taskAbortControllers.set(taskId, abortController);
|
|
10508
|
-
const streamId = `stream_${taskId}_${
|
|
11404
|
+
const streamId = `stream_${taskId}_${nanoid8(10)}`;
|
|
10509
11405
|
await activeStreamQueries.create(taskId, streamId);
|
|
10510
11406
|
const taskStreamProducer = () => {
|
|
10511
11407
|
const { readable, writable } = new TransformStream();
|
|
@@ -10655,17 +11551,581 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10655
11551
|
});
|
|
10656
11552
|
var tasks_default = tasks;
|
|
10657
11553
|
|
|
11554
|
+
// src/server/routes/system.ts
|
|
11555
|
+
import { Hono as Hono6 } from "hono";
|
|
11556
|
+
import { streamSSE } from "hono/streaming";
|
|
11557
|
+
var system = new Hono6();
|
|
11558
|
+
system.get("/metrics", (c) => {
|
|
11559
|
+
return c.json(readSystemMetrics());
|
|
11560
|
+
});
|
|
11561
|
+
system.get("/metrics/stream", (c) => {
|
|
11562
|
+
return streamSSE(c, async (stream) => {
|
|
11563
|
+
const intervalMs = Number(c.req.query("intervalMs") ?? 2e3);
|
|
11564
|
+
const safeMs = Number.isFinite(intervalMs) ? Math.max(500, Math.min(6e4, intervalMs)) : 2e3;
|
|
11565
|
+
let id = 0;
|
|
11566
|
+
let aborted = false;
|
|
11567
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
11568
|
+
aborted = true;
|
|
11569
|
+
});
|
|
11570
|
+
while (!aborted) {
|
|
11571
|
+
try {
|
|
11572
|
+
const snap = readSystemMetrics();
|
|
11573
|
+
await stream.writeSSE({
|
|
11574
|
+
id: String(id++),
|
|
11575
|
+
event: "metrics",
|
|
11576
|
+
data: JSON.stringify(snap)
|
|
11577
|
+
});
|
|
11578
|
+
} catch (e) {
|
|
11579
|
+
await stream.writeSSE({
|
|
11580
|
+
id: String(id++),
|
|
11581
|
+
event: "error",
|
|
11582
|
+
data: JSON.stringify({ error: e.message })
|
|
11583
|
+
});
|
|
11584
|
+
}
|
|
11585
|
+
await stream.sleep(safeMs);
|
|
11586
|
+
}
|
|
11587
|
+
});
|
|
11588
|
+
});
|
|
11589
|
+
|
|
11590
|
+
// src/personal-agent/hwid-middleware.ts
|
|
11591
|
+
var SKIP_PATHS = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11592
|
+
function personalAgentConfigured() {
|
|
11593
|
+
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);
|
|
11594
|
+
}
|
|
11595
|
+
function hwidMiddleware() {
|
|
11596
|
+
const enabled = personalAgentConfigured();
|
|
11597
|
+
let warnedMissing = false;
|
|
11598
|
+
return async (c, next) => {
|
|
11599
|
+
if (!enabled) return next();
|
|
11600
|
+
const path = c.req.path;
|
|
11601
|
+
if (SKIP_PATHS.has(path) || path.startsWith("/health/api-keys")) {
|
|
11602
|
+
return next();
|
|
11603
|
+
}
|
|
11604
|
+
const got = c.req.header("x-device-hwid");
|
|
11605
|
+
if (!got) {
|
|
11606
|
+
if (!warnedMissing) {
|
|
11607
|
+
warnedMissing = true;
|
|
11608
|
+
console.warn(
|
|
11609
|
+
`[personal-agent] request to ${path} arrived without X-Device-Hwid; allowing (backwards compat)`
|
|
11610
|
+
);
|
|
11611
|
+
}
|
|
11612
|
+
return next();
|
|
11613
|
+
}
|
|
11614
|
+
const expected = getHardwareIdCached();
|
|
11615
|
+
if (got !== expected) {
|
|
11616
|
+
console.warn(
|
|
11617
|
+
`[personal-agent] HWID mismatch on ${path}: got=${got.slice(0, 12)}\u2026, expected=${expected.slice(0, 12)}\u2026`
|
|
11618
|
+
);
|
|
11619
|
+
return c.json(
|
|
11620
|
+
{
|
|
11621
|
+
error: "hwid mismatch",
|
|
11622
|
+
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.",
|
|
11623
|
+
expected: expected.slice(0, 12) + "\u2026",
|
|
11624
|
+
got: got.slice(0, 12) + "\u2026"
|
|
11625
|
+
},
|
|
11626
|
+
409
|
|
11627
|
+
);
|
|
11628
|
+
}
|
|
11629
|
+
return next();
|
|
11630
|
+
};
|
|
11631
|
+
}
|
|
11632
|
+
|
|
11633
|
+
// src/personal-agent/signature-verify.ts
|
|
11634
|
+
import { createHash as createHash3, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
11635
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
11636
|
+
var REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
11637
|
+
var _cachedKey = null;
|
|
11638
|
+
var _cachedFromInput = null;
|
|
11639
|
+
function loadPublicKey(input) {
|
|
11640
|
+
if (_cachedFromInput === input && _cachedKey) return _cachedKey;
|
|
11641
|
+
let pem = input;
|
|
11642
|
+
if (!input.includes("BEGIN") && existsSync18(input)) {
|
|
11643
|
+
pem = readFileSync11(input, "utf8");
|
|
11644
|
+
}
|
|
11645
|
+
const key = createPublicKey({ key: pem, format: "pem" });
|
|
11646
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
11647
|
+
throw new Error(
|
|
11648
|
+
`expected an ed25519 public key, got ${key.asymmetricKeyType}. Generate with personal-agents/scripts/generate-signing-keys.mjs.`
|
|
11649
|
+
);
|
|
11650
|
+
}
|
|
11651
|
+
_cachedKey = key;
|
|
11652
|
+
_cachedFromInput = input;
|
|
11653
|
+
return key;
|
|
11654
|
+
}
|
|
11655
|
+
function bodyHashB64(body) {
|
|
11656
|
+
const hash = createHash3("sha256");
|
|
11657
|
+
if (body == null || body === "") {
|
|
11658
|
+
} else if (typeof body === "string") {
|
|
11659
|
+
hash.update(body, "utf8");
|
|
11660
|
+
} else if (Buffer.isBuffer(body)) {
|
|
11661
|
+
hash.update(body);
|
|
11662
|
+
} else {
|
|
11663
|
+
hash.update(Buffer.from(body));
|
|
11664
|
+
}
|
|
11665
|
+
return hash.digest("base64");
|
|
11666
|
+
}
|
|
11667
|
+
function canonicalSigningString(args) {
|
|
11668
|
+
return [
|
|
11669
|
+
args.method.toUpperCase(),
|
|
11670
|
+
args.path,
|
|
11671
|
+
String(args.timestamp),
|
|
11672
|
+
args.bodyHashB64
|
|
11673
|
+
].join("\n");
|
|
11674
|
+
}
|
|
11675
|
+
function fromBase64Url(s) {
|
|
11676
|
+
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
11677
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
11678
|
+
return Buffer.from(std, "base64");
|
|
11679
|
+
}
|
|
11680
|
+
function verifyEmbedToken(args) {
|
|
11681
|
+
const dot = args.token.indexOf(".");
|
|
11682
|
+
if (dot < 1 || dot >= args.token.length - 1) {
|
|
11683
|
+
return { ok: false, reason: "malformed" };
|
|
11684
|
+
}
|
|
11685
|
+
const payloadB64 = args.token.slice(0, dot);
|
|
11686
|
+
const sigB64 = args.token.slice(dot + 1);
|
|
11687
|
+
let sigBuf;
|
|
11688
|
+
try {
|
|
11689
|
+
sigBuf = fromBase64Url(sigB64);
|
|
11690
|
+
} catch {
|
|
11691
|
+
return { ok: false, reason: "bad-encoding" };
|
|
11692
|
+
}
|
|
11693
|
+
const sigOk = cryptoVerify(
|
|
11694
|
+
null,
|
|
11695
|
+
Buffer.from(payloadB64, "utf8"),
|
|
11696
|
+
args.publicKey,
|
|
11697
|
+
sigBuf
|
|
11698
|
+
);
|
|
11699
|
+
if (!sigOk) return { ok: false, reason: "signature-mismatch" };
|
|
11700
|
+
let payload;
|
|
11701
|
+
try {
|
|
11702
|
+
const json = JSON.parse(fromBase64Url(payloadB64).toString("utf8"));
|
|
11703
|
+
if (!json || typeof json !== "object" || typeof json.sid !== "string" || typeof json.exp !== "number") {
|
|
11704
|
+
return { ok: false, reason: "bad-payload" };
|
|
11705
|
+
}
|
|
11706
|
+
payload = { sid: json.sid, exp: json.exp };
|
|
11707
|
+
} catch (e) {
|
|
11708
|
+
return { ok: false, reason: "bad-payload", detail: e.message };
|
|
11709
|
+
}
|
|
11710
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11711
|
+
if (payload.exp < now) {
|
|
11712
|
+
return {
|
|
11713
|
+
ok: false,
|
|
11714
|
+
reason: "expired",
|
|
11715
|
+
detail: `${now - payload.exp}s past expiry`
|
|
11716
|
+
};
|
|
11717
|
+
}
|
|
11718
|
+
return { ok: true, payload };
|
|
11719
|
+
}
|
|
11720
|
+
function verifyRequest(args) {
|
|
11721
|
+
if (!args.signatureB64 || !args.timestampSeconds || !args.algorithm) {
|
|
11722
|
+
return { ok: false, reason: "missing-headers" };
|
|
11723
|
+
}
|
|
11724
|
+
if (args.algorithm.toLowerCase() !== "ed25519") {
|
|
11725
|
+
return { ok: false, reason: "bad-algorithm", detail: args.algorithm };
|
|
11726
|
+
}
|
|
11727
|
+
const ts = Number(args.timestampSeconds);
|
|
11728
|
+
if (!Number.isFinite(ts)) {
|
|
11729
|
+
return { ok: false, reason: "bad-timestamp", detail: args.timestampSeconds };
|
|
11730
|
+
}
|
|
11731
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11732
|
+
if (Math.abs(now - ts) > REPLAY_WINDOW_SECONDS) {
|
|
11733
|
+
return {
|
|
11734
|
+
ok: false,
|
|
11735
|
+
reason: "stale-timestamp",
|
|
11736
|
+
detail: `${Math.abs(now - ts)}s outside the ${REPLAY_WINDOW_SECONDS}s window`
|
|
11737
|
+
};
|
|
11738
|
+
}
|
|
11739
|
+
let sigBuf;
|
|
11740
|
+
try {
|
|
11741
|
+
sigBuf = Buffer.from(args.signatureB64, "base64");
|
|
11742
|
+
} catch {
|
|
11743
|
+
return { ok: false, reason: "bad-signature-encoding" };
|
|
11744
|
+
}
|
|
11745
|
+
const canonical = canonicalSigningString({
|
|
11746
|
+
method: args.method,
|
|
11747
|
+
path: args.path,
|
|
11748
|
+
timestamp: ts,
|
|
11749
|
+
bodyHashB64: bodyHashB64(args.body)
|
|
11750
|
+
});
|
|
11751
|
+
const ok = cryptoVerify(null, Buffer.from(canonical, "utf8"), args.publicKey, sigBuf);
|
|
11752
|
+
return ok ? { ok: true } : { ok: false, reason: "signature-mismatch" };
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11755
|
+
// src/personal-agent/signature-middleware.ts
|
|
11756
|
+
var SKIP_PATHS2 = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11757
|
+
function isSkipped(path) {
|
|
11758
|
+
return SKIP_PATHS2.has(path) || path.startsWith("/health/api-keys");
|
|
11759
|
+
}
|
|
11760
|
+
function pathBindsSessionId(path, sid) {
|
|
11761
|
+
const cleanPath = path.split("?")[0];
|
|
11762
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
11763
|
+
return segments.includes(sid);
|
|
11764
|
+
}
|
|
11765
|
+
function signatureMiddleware(opts = {}) {
|
|
11766
|
+
const acceptSignedOnly = opts.acceptSignedOnly ?? process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
11767
|
+
const publicKeyInput = opts.publicKeyInput ?? process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
11768
|
+
if (acceptSignedOnly && !publicKeyInput) {
|
|
11769
|
+
return async (c) => c.json(
|
|
11770
|
+
{
|
|
11771
|
+
error: "signature middleware misconfigured",
|
|
11772
|
+
message: "started with --accept-signed-only but no --personal-agent-public-key / PERSONAL_AGENT_PUBLIC_KEY supplied. Refusing all non-/health traffic."
|
|
11773
|
+
},
|
|
11774
|
+
500
|
|
11775
|
+
);
|
|
11776
|
+
}
|
|
11777
|
+
if (!publicKeyInput) {
|
|
11778
|
+
return async (_c, next) => next();
|
|
11779
|
+
}
|
|
11780
|
+
const publicKey = loadPublicKey(publicKeyInput);
|
|
11781
|
+
let warnedUnsigned = false;
|
|
11782
|
+
return async (c, next) => {
|
|
11783
|
+
const path = c.req.path;
|
|
11784
|
+
if (isSkipped(path)) return next();
|
|
11785
|
+
const sig = c.req.header("x-signature");
|
|
11786
|
+
const ts = c.req.header("x-signature-timestamp");
|
|
11787
|
+
const alg = c.req.header("x-signature-algorithm");
|
|
11788
|
+
if (!sig && (c.req.method === "GET" || c.req.method === "HEAD")) {
|
|
11789
|
+
const embedTok = c.req.header("x-embed-token") ?? c.req.query("embed_token");
|
|
11790
|
+
if (embedTok) {
|
|
11791
|
+
const result2 = verifyEmbedToken({ publicKey, token: embedTok });
|
|
11792
|
+
if (!result2.ok) {
|
|
11793
|
+
return c.json(
|
|
11794
|
+
{
|
|
11795
|
+
error: "embed token verification failed",
|
|
11796
|
+
reason: result2.reason,
|
|
11797
|
+
detail: result2.detail
|
|
11798
|
+
},
|
|
11799
|
+
401
|
|
11800
|
+
);
|
|
11801
|
+
}
|
|
11802
|
+
if (!pathBindsSessionId(path, result2.payload.sid)) {
|
|
11803
|
+
return c.json(
|
|
11804
|
+
{
|
|
11805
|
+
error: "embed token scoped to a different session",
|
|
11806
|
+
detail: `token sid=${result2.payload.sid} but request path=${path}`
|
|
11807
|
+
},
|
|
11808
|
+
403
|
|
11809
|
+
);
|
|
11810
|
+
}
|
|
11811
|
+
return next();
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
if (!sig) {
|
|
11815
|
+
if (acceptSignedOnly) {
|
|
11816
|
+
return c.json(
|
|
11817
|
+
{
|
|
11818
|
+
error: "signature required",
|
|
11819
|
+
message: "this sparkecoder is started with --accept-signed-only; every request must carry X-Signature, X-Signature-Timestamp, X-Signature-Algorithm."
|
|
11820
|
+
},
|
|
11821
|
+
401
|
|
11822
|
+
);
|
|
11823
|
+
}
|
|
11824
|
+
if (!warnedUnsigned) {
|
|
11825
|
+
warnedUnsigned = true;
|
|
11826
|
+
console.warn(
|
|
11827
|
+
`[personal-agent] inbound ${c.req.method} ${path} arrived without X-Signature; allowed because --accept-signed-only is off`
|
|
11828
|
+
);
|
|
11829
|
+
}
|
|
11830
|
+
return next();
|
|
11831
|
+
}
|
|
11832
|
+
let body;
|
|
11833
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
11834
|
+
body = Buffer.from(await c.req.raw.clone().arrayBuffer());
|
|
11835
|
+
}
|
|
11836
|
+
const result = verifyRequest({
|
|
11837
|
+
publicKey,
|
|
11838
|
+
method: c.req.method,
|
|
11839
|
+
path,
|
|
11840
|
+
body,
|
|
11841
|
+
signatureB64: sig,
|
|
11842
|
+
timestampSeconds: ts,
|
|
11843
|
+
algorithm: alg
|
|
11844
|
+
});
|
|
11845
|
+
if (!result.ok) {
|
|
11846
|
+
console.warn(
|
|
11847
|
+
`[personal-agent] signature verification failed on ${c.req.method} ${path}: ${result.reason}${result.detail ? ` (${result.detail})` : ""}`
|
|
11848
|
+
);
|
|
11849
|
+
return c.json(
|
|
11850
|
+
{
|
|
11851
|
+
error: "signature verification failed",
|
|
11852
|
+
reason: result.reason,
|
|
11853
|
+
detail: result.detail
|
|
11854
|
+
},
|
|
11855
|
+
401
|
|
11856
|
+
);
|
|
11857
|
+
}
|
|
11858
|
+
return next();
|
|
11859
|
+
};
|
|
11860
|
+
}
|
|
11861
|
+
|
|
11862
|
+
// src/personal-agent/pty-server.ts
|
|
11863
|
+
import { hostname as hostname3 } from "os";
|
|
11864
|
+
import { WebSocketServer } from "ws";
|
|
11865
|
+
var RESIZE_RE = /\x1b\[RESIZE:(\d+);(\d+)\]/;
|
|
11866
|
+
var _ptyMod = null;
|
|
11867
|
+
async function loadPty() {
|
|
11868
|
+
if (_ptyMod) return _ptyMod;
|
|
11869
|
+
try {
|
|
11870
|
+
const mod = await import("node-pty");
|
|
11871
|
+
_ptyMod = mod;
|
|
11872
|
+
return mod;
|
|
11873
|
+
} catch (e) {
|
|
11874
|
+
console.warn(
|
|
11875
|
+
`[personal-agent] node-pty failed to load; /pty WebSocket disabled. (${e.message})`
|
|
11876
|
+
);
|
|
11877
|
+
return null;
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
function defaultShell() {
|
|
11881
|
+
if (process.platform === "win32") {
|
|
11882
|
+
return { file: process.env.COMSPEC || "cmd.exe", args: [] };
|
|
11883
|
+
}
|
|
11884
|
+
return { file: process.env.SHELL || "/bin/zsh", args: ["-l"] };
|
|
11885
|
+
}
|
|
11886
|
+
function cleanEnv() {
|
|
11887
|
+
const env = {};
|
|
11888
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
11889
|
+
if (typeof v === "string") env[k] = v;
|
|
11890
|
+
}
|
|
11891
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
11892
|
+
if (!env.LANG) env.LANG = "en_US.UTF-8";
|
|
11893
|
+
return env;
|
|
11894
|
+
}
|
|
11895
|
+
function parseUpgrade(req) {
|
|
11896
|
+
const url = new URL(req.url || "/", "http://placeholder");
|
|
11897
|
+
const cols = clampInt(url.searchParams.get("cols"), 80, 10, 500);
|
|
11898
|
+
const rows = clampInt(url.searchParams.get("rows"), 24, 5, 500);
|
|
11899
|
+
const cwd = url.searchParams.get("cwd") || void 0;
|
|
11900
|
+
const shell = url.searchParams.get("shell") || void 0;
|
|
11901
|
+
return {
|
|
11902
|
+
cols,
|
|
11903
|
+
rows,
|
|
11904
|
+
cwd,
|
|
11905
|
+
shell,
|
|
11906
|
+
hwidHeader: headerStr(req, "x-device-hwid") ?? url.searchParams.get("hwid") ?? void 0,
|
|
11907
|
+
sigHeader: headerStr(req, "x-signature") ?? url.searchParams.get("sig") ?? void 0,
|
|
11908
|
+
tsHeader: headerStr(req, "x-signature-timestamp") ?? url.searchParams.get("ts") ?? void 0,
|
|
11909
|
+
algHeader: headerStr(req, "x-signature-algorithm") ?? url.searchParams.get("alg") ?? void 0
|
|
11910
|
+
};
|
|
11911
|
+
}
|
|
11912
|
+
function clampInt(v, dflt, lo, hi) {
|
|
11913
|
+
if (!v) return dflt;
|
|
11914
|
+
const n = parseInt(v, 10);
|
|
11915
|
+
if (!Number.isFinite(n)) return dflt;
|
|
11916
|
+
return Math.max(lo, Math.min(hi, n));
|
|
11917
|
+
}
|
|
11918
|
+
function headerStr(req, name) {
|
|
11919
|
+
const v = req.headers[name];
|
|
11920
|
+
if (Array.isArray(v)) return v[0];
|
|
11921
|
+
return v;
|
|
11922
|
+
}
|
|
11923
|
+
function authenticate(parsed, path, pubKey, signedOnly) {
|
|
11924
|
+
if (parsed.hwidHeader) {
|
|
11925
|
+
const expected = getHardwareIdCached();
|
|
11926
|
+
if (parsed.hwidHeader !== expected) {
|
|
11927
|
+
return {
|
|
11928
|
+
ok: false,
|
|
11929
|
+
status: 409,
|
|
11930
|
+
reason: `hwid mismatch: got ${parsed.hwidHeader.slice(0, 12)}\u2026, expected ${expected.slice(0, 12)}\u2026`
|
|
11931
|
+
};
|
|
11932
|
+
}
|
|
11933
|
+
}
|
|
11934
|
+
if (!pubKey) {
|
|
11935
|
+
if (signedOnly) {
|
|
11936
|
+
return { ok: false, status: 500, reason: "signature required but no public key configured" };
|
|
11937
|
+
}
|
|
11938
|
+
return { ok: true };
|
|
11939
|
+
}
|
|
11940
|
+
if (!parsed.sigHeader) {
|
|
11941
|
+
if (signedOnly) {
|
|
11942
|
+
return { ok: false, status: 401, reason: "missing X-Signature on upgrade request" };
|
|
11943
|
+
}
|
|
11944
|
+
return { ok: true };
|
|
11945
|
+
}
|
|
11946
|
+
const result = verifyRequest({
|
|
11947
|
+
publicKey: pubKey,
|
|
11948
|
+
method: "GET",
|
|
11949
|
+
path,
|
|
11950
|
+
body: void 0,
|
|
11951
|
+
signatureB64: parsed.sigHeader,
|
|
11952
|
+
timestampSeconds: parsed.tsHeader,
|
|
11953
|
+
algorithm: parsed.algHeader
|
|
11954
|
+
});
|
|
11955
|
+
if (!result.ok) {
|
|
11956
|
+
return { ok: false, status: 401, reason: `signature verification failed: ${result.reason}` };
|
|
11957
|
+
}
|
|
11958
|
+
return { ok: true };
|
|
11959
|
+
}
|
|
11960
|
+
function rejectUpgrade(socket, status, reason) {
|
|
11961
|
+
const body = JSON.stringify({ error: reason });
|
|
11962
|
+
socket.write(
|
|
11963
|
+
`HTTP/1.1 ${status} ${reasonText(status)}\r
|
|
11964
|
+
Content-Type: application/json\r
|
|
11965
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
11966
|
+
Connection: close\r
|
|
11967
|
+
\r
|
|
11968
|
+
` + body
|
|
11969
|
+
);
|
|
11970
|
+
socket.destroy();
|
|
11971
|
+
}
|
|
11972
|
+
function reasonText(status) {
|
|
11973
|
+
switch (status) {
|
|
11974
|
+
case 401:
|
|
11975
|
+
return "Unauthorized";
|
|
11976
|
+
case 409:
|
|
11977
|
+
return "Conflict";
|
|
11978
|
+
case 500:
|
|
11979
|
+
return "Internal Server Error";
|
|
11980
|
+
case 503:
|
|
11981
|
+
return "Service Unavailable";
|
|
11982
|
+
default:
|
|
11983
|
+
return "Error";
|
|
11984
|
+
}
|
|
11985
|
+
}
|
|
11986
|
+
function attachPtyServer(httpServer, opts = {}) {
|
|
11987
|
+
const path = opts.path ?? "/pty";
|
|
11988
|
+
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
11989
|
+
const acceptSignedOnly = process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
11990
|
+
const publicKeyInput = process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
11991
|
+
let pubKey = null;
|
|
11992
|
+
if (publicKeyInput) {
|
|
11993
|
+
try {
|
|
11994
|
+
pubKey = loadPublicKey(publicKeyInput);
|
|
11995
|
+
} catch (e) {
|
|
11996
|
+
console.warn(
|
|
11997
|
+
`[personal-agent] /pty signature verification disabled \u2014 failed to load public key: ${e.message}`
|
|
11998
|
+
);
|
|
11999
|
+
}
|
|
12000
|
+
}
|
|
12001
|
+
const handler = (req, socket, head) => {
|
|
12002
|
+
let pathname = "/";
|
|
12003
|
+
try {
|
|
12004
|
+
pathname = new URL(req.url || "/", "http://placeholder").pathname;
|
|
12005
|
+
} catch {
|
|
12006
|
+
}
|
|
12007
|
+
if (pathname !== path) return;
|
|
12008
|
+
const parsed = parseUpgrade(req);
|
|
12009
|
+
const auth = authenticate(parsed, pathname, pubKey, acceptSignedOnly);
|
|
12010
|
+
if (!auth.ok) {
|
|
12011
|
+
console.warn(`[personal-agent] rejecting /pty upgrade: ${auth.reason}`);
|
|
12012
|
+
rejectUpgrade(socket, auth.status ?? 401, auth.reason ?? "unauthorized");
|
|
12013
|
+
return;
|
|
12014
|
+
}
|
|
12015
|
+
void loadPty().then((pty) => {
|
|
12016
|
+
if (!pty) {
|
|
12017
|
+
rejectUpgrade(
|
|
12018
|
+
socket,
|
|
12019
|
+
503,
|
|
12020
|
+
"node-pty is not available on this device (failed to load native module)"
|
|
12021
|
+
);
|
|
12022
|
+
return;
|
|
12023
|
+
}
|
|
12024
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
12025
|
+
spawnPty(ws, pty, parsed, opts);
|
|
12026
|
+
});
|
|
12027
|
+
});
|
|
12028
|
+
};
|
|
12029
|
+
httpServer.on("upgrade", handler);
|
|
12030
|
+
if (!opts.quiet) {
|
|
12031
|
+
console.log(
|
|
12032
|
+
`[personal-agent] WebSocket PTY server attached at ${path} (host: ${hostname3()}, sigCheck: ${pubKey ? "on" : "off"})`
|
|
12033
|
+
);
|
|
12034
|
+
}
|
|
12035
|
+
return {
|
|
12036
|
+
close: () => {
|
|
12037
|
+
httpServer.off("upgrade", handler);
|
|
12038
|
+
wss.close();
|
|
12039
|
+
}
|
|
12040
|
+
};
|
|
12041
|
+
}
|
|
12042
|
+
function spawnPty(ws, pty, parsed, opts) {
|
|
12043
|
+
const { file, args } = (() => {
|
|
12044
|
+
if (parsed.shell || opts.shell) {
|
|
12045
|
+
const s = parsed.shell ?? opts.shell;
|
|
12046
|
+
return { file: s, args: process.platform === "win32" ? [] : ["-l"] };
|
|
12047
|
+
}
|
|
12048
|
+
return defaultShell();
|
|
12049
|
+
})();
|
|
12050
|
+
const cwd = parsed.cwd ?? opts.cwd ?? process.env.HOME ?? process.cwd();
|
|
12051
|
+
let proc;
|
|
12052
|
+
try {
|
|
12053
|
+
proc = pty.spawn(file, args, {
|
|
12054
|
+
name: "xterm-256color",
|
|
12055
|
+
cols: parsed.cols,
|
|
12056
|
+
rows: parsed.rows,
|
|
12057
|
+
cwd,
|
|
12058
|
+
env: cleanEnv()
|
|
12059
|
+
});
|
|
12060
|
+
} catch (e) {
|
|
12061
|
+
const msg = e.message;
|
|
12062
|
+
safeSend(ws, `\r
|
|
12063
|
+
\x1B[31mFailed to spawn shell: ${msg}\x1B[0m\r
|
|
12064
|
+
`);
|
|
12065
|
+
try {
|
|
12066
|
+
ws.close();
|
|
12067
|
+
} catch {
|
|
12068
|
+
}
|
|
12069
|
+
return;
|
|
12070
|
+
}
|
|
12071
|
+
const banner = `\x1B[90m[sparkecoder pty] ${file} on ${hostname3()} pid=${proc.pid} ${parsed.cols}x${parsed.rows}\x1B[0m\r
|
|
12072
|
+
`;
|
|
12073
|
+
safeSend(ws, banner);
|
|
12074
|
+
proc.onData((data) => safeSend(ws, data));
|
|
12075
|
+
proc.onExit(({ exitCode }) => {
|
|
12076
|
+
safeSend(ws, `\r
|
|
12077
|
+
\x1B[90m[exit ${exitCode}]\x1B[0m\r
|
|
12078
|
+
`);
|
|
12079
|
+
try {
|
|
12080
|
+
ws.close();
|
|
12081
|
+
} catch {
|
|
12082
|
+
}
|
|
12083
|
+
});
|
|
12084
|
+
ws.on("message", (msg, isBinary) => {
|
|
12085
|
+
const input = typeof msg === "string" ? msg : Buffer.isBuffer(msg) ? msg.toString(isBinary ? "utf8" : "utf8") : Array.isArray(msg) ? Buffer.concat(msg).toString("utf8") : "";
|
|
12086
|
+
if (!input) return;
|
|
12087
|
+
const m = input.match(RESIZE_RE);
|
|
12088
|
+
if (m) {
|
|
12089
|
+
const cols = clampInt(m[1], 80, 10, 500);
|
|
12090
|
+
const rows = clampInt(m[2], 24, 5, 500);
|
|
12091
|
+
try {
|
|
12092
|
+
proc.resize(cols, rows);
|
|
12093
|
+
} catch {
|
|
12094
|
+
}
|
|
12095
|
+
if (input.replace(RESIZE_RE, "").length === 0) return;
|
|
12096
|
+
proc.write(input.replace(RESIZE_RE, ""));
|
|
12097
|
+
return;
|
|
12098
|
+
}
|
|
12099
|
+
proc.write(input);
|
|
12100
|
+
});
|
|
12101
|
+
const onClose = () => {
|
|
12102
|
+
try {
|
|
12103
|
+
proc.kill();
|
|
12104
|
+
} catch {
|
|
12105
|
+
}
|
|
12106
|
+
};
|
|
12107
|
+
ws.on("close", onClose);
|
|
12108
|
+
ws.on("error", onClose);
|
|
12109
|
+
}
|
|
12110
|
+
function safeSend(ws, data) {
|
|
12111
|
+
if (ws.readyState !== 1) return;
|
|
12112
|
+
try {
|
|
12113
|
+
ws.send(data);
|
|
12114
|
+
} catch {
|
|
12115
|
+
}
|
|
12116
|
+
}
|
|
12117
|
+
|
|
10658
12118
|
// src/server/index.ts
|
|
10659
12119
|
init_config();
|
|
10660
12120
|
init_db();
|
|
10661
12121
|
|
|
10662
12122
|
// src/utils/dependencies.ts
|
|
10663
|
-
import { exec as
|
|
10664
|
-
import { promisify as
|
|
10665
|
-
import { platform as
|
|
10666
|
-
var
|
|
12123
|
+
import { exec as exec7 } from "child_process";
|
|
12124
|
+
import { promisify as promisify7 } from "util";
|
|
12125
|
+
import { platform as platform4 } from "os";
|
|
12126
|
+
var execAsync7 = promisify7(exec7);
|
|
10667
12127
|
function getInstallInstructions() {
|
|
10668
|
-
const os2 =
|
|
12128
|
+
const os2 = platform4();
|
|
10669
12129
|
if (os2 === "darwin") {
|
|
10670
12130
|
return `
|
|
10671
12131
|
Install tmux on macOS:
|
|
@@ -10696,7 +12156,7 @@ Install tmux:
|
|
|
10696
12156
|
}
|
|
10697
12157
|
async function checkTmux() {
|
|
10698
12158
|
try {
|
|
10699
|
-
const { stdout } = await
|
|
12159
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
10700
12160
|
const version = stdout.trim();
|
|
10701
12161
|
return {
|
|
10702
12162
|
available: true,
|
|
@@ -10745,11 +12205,11 @@ function getWebDirectory() {
|
|
|
10745
12205
|
try {
|
|
10746
12206
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
10747
12207
|
const webDir = resolve10(currentDir, "..", "web");
|
|
10748
|
-
if (
|
|
12208
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
10749
12209
|
return webDir;
|
|
10750
12210
|
}
|
|
10751
12211
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
10752
|
-
if (
|
|
12212
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
10753
12213
|
return altWebDir;
|
|
10754
12214
|
}
|
|
10755
12215
|
return null;
|
|
@@ -10807,23 +12267,23 @@ async function findWebPort(preferredPort) {
|
|
|
10807
12267
|
return { port: preferredPort, alreadyRunning: false };
|
|
10808
12268
|
}
|
|
10809
12269
|
function hasProductionBuild(webDir) {
|
|
10810
|
-
const buildIdPath =
|
|
10811
|
-
return
|
|
12270
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
12271
|
+
return existsSync19(buildIdPath);
|
|
10812
12272
|
}
|
|
10813
12273
|
function hasSourceFiles(webDir) {
|
|
10814
|
-
const appDir =
|
|
10815
|
-
const pagesDir =
|
|
10816
|
-
const rootAppDir =
|
|
10817
|
-
const rootPagesDir =
|
|
10818
|
-
return
|
|
12274
|
+
const appDir = join13(webDir, "src", "app");
|
|
12275
|
+
const pagesDir = join13(webDir, "src", "pages");
|
|
12276
|
+
const rootAppDir = join13(webDir, "app");
|
|
12277
|
+
const rootPagesDir = join13(webDir, "pages");
|
|
12278
|
+
return existsSync19(appDir) || existsSync19(pagesDir) || existsSync19(rootAppDir) || existsSync19(rootPagesDir);
|
|
10819
12279
|
}
|
|
10820
12280
|
function getStandaloneServerPath(webDir) {
|
|
10821
12281
|
const possiblePaths2 = [
|
|
10822
|
-
|
|
10823
|
-
|
|
12282
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
12283
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
10824
12284
|
];
|
|
10825
12285
|
for (const serverPath of possiblePaths2) {
|
|
10826
|
-
if (
|
|
12286
|
+
if (existsSync19(serverPath)) {
|
|
10827
12287
|
return serverPath;
|
|
10828
12288
|
}
|
|
10829
12289
|
}
|
|
@@ -10863,13 +12323,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10863
12323
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
10864
12324
|
return { process: null, port: actualPort };
|
|
10865
12325
|
}
|
|
10866
|
-
const usePnpm =
|
|
10867
|
-
const useNpm = !usePnpm &&
|
|
12326
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
12327
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
10868
12328
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
10869
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
12329
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
10870
12330
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
10871
12331
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
10872
|
-
const runtimeConfigPath =
|
|
12332
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
10873
12333
|
try {
|
|
10874
12334
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
10875
12335
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -10877,7 +12337,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10877
12337
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
10878
12338
|
}
|
|
10879
12339
|
const webEnv = {
|
|
10880
|
-
...
|
|
12340
|
+
...cleanEnv2,
|
|
10881
12341
|
PORT: String(actualPort)
|
|
10882
12342
|
// Next.js respects PORT env var
|
|
10883
12343
|
};
|
|
@@ -10991,12 +12451,28 @@ function stopWebUI() {
|
|
|
10991
12451
|
}
|
|
10992
12452
|
}
|
|
10993
12453
|
async function createApp(options = {}) {
|
|
10994
|
-
const app = new
|
|
12454
|
+
const app = new Hono7();
|
|
10995
12455
|
app.use("*", cors({
|
|
10996
12456
|
origin: "*",
|
|
10997
12457
|
// Allow all origins
|
|
10998
12458
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
10999
|
-
allowHeaders: [
|
|
12459
|
+
allowHeaders: [
|
|
12460
|
+
"Content-Type",
|
|
12461
|
+
"Authorization",
|
|
12462
|
+
"X-Requested-With",
|
|
12463
|
+
// Personal-agent dashboard signs every request to the device with
|
|
12464
|
+
// these. Without them whitelisted the browser preflight strips the
|
|
12465
|
+
// headers and the signature middleware 401s.
|
|
12466
|
+
"X-Signature",
|
|
12467
|
+
"X-Signature-Timestamp",
|
|
12468
|
+
"X-Signature-Algorithm",
|
|
12469
|
+
"X-Device-Hwid",
|
|
12470
|
+
// Short-lived embed token used by the iframed SparkECoder web UI
|
|
12471
|
+
// when it's hosted inside the personal-agents dashboard. The
|
|
12472
|
+
// bootstrap in `web/src/lib/embed-bootstrap.ts` adds this header
|
|
12473
|
+
// to every API call.
|
|
12474
|
+
"X-Embed-Token"
|
|
12475
|
+
],
|
|
11000
12476
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11001
12477
|
maxAge: 86400
|
|
11002
12478
|
// 24 hours
|
|
@@ -11004,12 +12480,15 @@ async function createApp(options = {}) {
|
|
|
11004
12480
|
if (!options.quiet) {
|
|
11005
12481
|
app.use("*", logger());
|
|
11006
12482
|
}
|
|
12483
|
+
app.use("*", hwidMiddleware());
|
|
12484
|
+
app.use("*", signatureMiddleware());
|
|
11007
12485
|
app.route("/health", health);
|
|
11008
12486
|
app.route("/sessions", sessions);
|
|
11009
12487
|
app.route("/agents", agents);
|
|
11010
12488
|
app.route("/sessions", terminals);
|
|
11011
12489
|
app.route("/terminals", terminals);
|
|
11012
12490
|
app.route("/tasks", tasks_default);
|
|
12491
|
+
app.route("/system", system);
|
|
11013
12492
|
app.get("/openapi.json", async (c) => {
|
|
11014
12493
|
return c.json(generateOpenAPISpec());
|
|
11015
12494
|
});
|
|
@@ -11062,8 +12541,8 @@ async function startServer(options = {}) {
|
|
|
11062
12541
|
if (options.workingDirectory) {
|
|
11063
12542
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11064
12543
|
}
|
|
11065
|
-
if (!
|
|
11066
|
-
|
|
12544
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
12545
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11067
12546
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11068
12547
|
}
|
|
11069
12548
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11098,6 +12577,15 @@ async function startServer(options = {}) {
|
|
|
11098
12577
|
port,
|
|
11099
12578
|
hostname: host
|
|
11100
12579
|
});
|
|
12580
|
+
try {
|
|
12581
|
+
attachPtyServer(serverInstance, {
|
|
12582
|
+
quiet: options.quiet
|
|
12583
|
+
});
|
|
12584
|
+
} catch (e) {
|
|
12585
|
+
if (!options.quiet) {
|
|
12586
|
+
console.warn(` \u26A0 Failed to attach /pty WebSocket server: ${e.message}`);
|
|
12587
|
+
}
|
|
12588
|
+
}
|
|
11101
12589
|
let webPort;
|
|
11102
12590
|
let webStarted;
|
|
11103
12591
|
if (options.webUI !== false) {
|