sparkecoder 0.1.86 → 0.1.93
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/README.md +1 -1
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +809 -97
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +2395 -316
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/{index-OhuTM4a0.d.ts → index-Bn0Zox8f.d.ts} +32 -23
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1932 -273
- package/dist/index.js.map +1 -1
- package/dist/{schema-CohdIL13.d.ts → schema-EmpbnQeQ.d.ts} +3 -3
- package/dist/{search-CCffrVJE.d.ts → search-BRnGaIl-.d.ts} +7 -7
- package/dist/server/index.js +1932 -273
- package/dist/server/index.js.map +1 -1
- package/dist/skills/default/computer-use.md +150 -0
- package/dist/tools/index.d.ts +196 -3
- package/dist/tools/index.js +639 -11
- package/dist/tools/index.js.map +1 -1
- package/package.json +6 -5
- 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/(main)/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
- 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/page_client-reference-manifest.js +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 +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- 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 +2 -2
- 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/page_client-reference-manifest.js +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 +3 -3
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +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 +3 -3
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- 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 +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +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 +3 -3
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- 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 +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +2 -2
- 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 +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__36edac7c._.js +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/chunks/ssr/[root-of-the-server]__be5e2967._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_components_sessions-sidebar_tsx_92510070._.js +1 -1
- 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/26eb5fab5216f3cc.js → chunks/58fd0aaa2746b444.js} +1 -1
- package/web/.next/standalone/web/.next/static/chunks/9fce2ce79c4c834e.js +1 -0
- package/web/.next/{static/chunks/8d3efc76109d2efc.js → standalone/web/.next/static/chunks/a888d448ceab1abe.js} +1 -1
- package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/{static/chunks/26eb5fab5216f3cc.js → standalone/web/.next/static/static/chunks/58fd0aaa2746b444.js} +1 -1
- package/web/.next/standalone/web/.next/static/static/chunks/9fce2ce79c4c834e.js +1 -0
- package/web/.next/standalone/web/.next/static/{chunks/8d3efc76109d2efc.js → static/chunks/a888d448ceab1abe.js} +1 -1
- package/web/.next/standalone/web/package-lock.json +27 -27
- package/web/.next/standalone/web/package.json +1 -1
- package/web/.next/standalone/web/src/app/(main)/page.tsx +2 -2
- package/web/.next/standalone/web/src/app/api/config/route.ts +3 -2
- package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
- package/web/.next/standalone/web/src/components/sessions-sidebar.tsx +1 -1
- package/web/.next/standalone/web/src/lib/config.ts +2 -1
- 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/26eb5fab5216f3cc.js → static/chunks/58fd0aaa2746b444.js} +1 -1
- package/web/.next/static/chunks/9fce2ce79c4c834e.js +1 -0
- package/web/.next/{standalone/web/.next/static/static/chunks/8d3efc76109d2efc.js → static/chunks/a888d448ceab1abe.js} +1 -1
- package/web/package.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/chunks/b31b0765abe0c427.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/b31b0765abe0c427.js +0 -1
- package/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/static/chunks/b31b0765abe0c427.js +0 -1
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_ssgManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_buildManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → -ax3tJNqBGukss29jpu41}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -27,7 +27,12 @@ var init_types = __esm({
|
|
|
27
27
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
28
28
|
alwaysApply: z.boolean().optional().default(false),
|
|
29
29
|
// Glob patterns - auto-inject when working with matching files
|
|
30
|
-
globs: z.array(z.string()).optional().default([])
|
|
30
|
+
globs: z.array(z.string()).optional().default([]),
|
|
31
|
+
// Platform requirements — skill is hidden from the model on platforms
|
|
32
|
+
// not listed here. Values match `process.platform`
|
|
33
|
+
// (darwin, linux, win32, freebsd, ...). If omitted or empty, the skill is
|
|
34
|
+
// available on all platforms.
|
|
35
|
+
platforms: z.array(z.string()).optional().default([])
|
|
31
36
|
});
|
|
32
37
|
TaskConfigSchema = z.object({
|
|
33
38
|
enabled: z.boolean(),
|
|
@@ -45,7 +50,13 @@ var init_types = __esm({
|
|
|
45
50
|
approvalWebhook: z.string().url().optional(),
|
|
46
51
|
skillsDirectory: z.string().optional(),
|
|
47
52
|
maxContextChars: z.number().optional().default(2e5),
|
|
48
|
-
task: TaskConfigSchema.optional()
|
|
53
|
+
task: TaskConfigSchema.optional(),
|
|
54
|
+
// Anthropic computer use tool — opt-in. When true, the `computer` tool is
|
|
55
|
+
// included in the toolset for Anthropic models. Default false.
|
|
56
|
+
computerUseEnabled: z.boolean().optional(),
|
|
57
|
+
// Display dimensions for the computer use tool (defaults: 1280x800).
|
|
58
|
+
computerUseDisplayWidth: z.number().int().positive().optional(),
|
|
59
|
+
computerUseDisplayHeight: z.number().int().positive().optional()
|
|
49
60
|
});
|
|
50
61
|
VectorGatewayConfigSchema = z.object({
|
|
51
62
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -96,7 +107,7 @@ var init_types = __esm({
|
|
|
96
107
|
}).optional();
|
|
97
108
|
SparkcoderConfigSchema = z.object({
|
|
98
109
|
// Default model to use (Vercel AI Gateway format)
|
|
99
|
-
defaultModel: z.string().default("anthropic/claude-opus-4
|
|
110
|
+
defaultModel: z.string().default("anthropic/claude-opus-4.7"),
|
|
100
111
|
// Working directory for file operations
|
|
101
112
|
workingDirectory: z.string().optional(),
|
|
102
113
|
// Tool approval settings
|
|
@@ -455,6 +466,14 @@ function loadApiKeysIntoEnv() {
|
|
|
455
466
|
}
|
|
456
467
|
}
|
|
457
468
|
}
|
|
469
|
+
function isRemoteInferenceConfigured() {
|
|
470
|
+
try {
|
|
471
|
+
const config = getConfig();
|
|
472
|
+
return config.resolvedRemoteServer.isConfigured;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
458
477
|
function setApiKey(provider, apiKey) {
|
|
459
478
|
const normalizedProvider = provider.toLowerCase();
|
|
460
479
|
const envVar = PROVIDER_ENV_MAP[normalizedProvider];
|
|
@@ -504,11 +523,11 @@ function getApiKeyStatus() {
|
|
|
504
523
|
};
|
|
505
524
|
});
|
|
506
525
|
}
|
|
507
|
-
function maskApiKey(
|
|
508
|
-
if (
|
|
509
|
-
return "****" +
|
|
526
|
+
function maskApiKey(key2) {
|
|
527
|
+
if (key2.length <= 12) {
|
|
528
|
+
return "****" + key2.slice(-4);
|
|
510
529
|
}
|
|
511
|
-
return
|
|
530
|
+
return key2.slice(0, 4) + "..." + key2.slice(-4);
|
|
512
531
|
}
|
|
513
532
|
var CONFIG_FILE_NAMES, cachedConfig, AUTH_KEY_FILE, API_KEYS_FILE, PROVIDER_ENV_MAP, SUPPORTED_PROVIDERS;
|
|
514
533
|
var init_config = __esm({
|
|
@@ -555,9 +574,9 @@ __export(remote_exports, {
|
|
|
555
574
|
remoteToolExecutionQueries: () => remoteToolExecutionQueries,
|
|
556
575
|
storageQueries: () => storageQueries
|
|
557
576
|
});
|
|
558
|
-
function initRemoteDatabase(serverUrl,
|
|
577
|
+
function initRemoteDatabase(serverUrl, key2) {
|
|
559
578
|
remoteServerUrl = serverUrl.replace(/\/$/, "");
|
|
560
|
-
authKey =
|
|
579
|
+
authKey = key2;
|
|
561
580
|
}
|
|
562
581
|
function closeRemoteDatabase() {
|
|
563
582
|
remoteServerUrl = null;
|
|
@@ -571,14 +590,14 @@ function parseDates(obj) {
|
|
|
571
590
|
if (Array.isArray(obj)) return obj.map(parseDates);
|
|
572
591
|
if (typeof obj !== "object" || obj instanceof Date) return obj;
|
|
573
592
|
const result = { ...obj };
|
|
574
|
-
for (const
|
|
575
|
-
if (MODEL_MESSAGE_FIELDS.includes(
|
|
593
|
+
for (const key2 of Object.keys(result)) {
|
|
594
|
+
if (MODEL_MESSAGE_FIELDS.includes(key2)) {
|
|
576
595
|
continue;
|
|
577
596
|
}
|
|
578
|
-
if (DATE_FIELDS.includes(
|
|
579
|
-
result[
|
|
580
|
-
} else if (typeof result[
|
|
581
|
-
result[
|
|
597
|
+
if (DATE_FIELDS.includes(key2) && typeof result[key2] === "string") {
|
|
598
|
+
result[key2] = new Date(result[key2]);
|
|
599
|
+
} else if (typeof result[key2] === "object") {
|
|
600
|
+
result[key2] = parseDates(result[key2]);
|
|
582
601
|
}
|
|
583
602
|
}
|
|
584
603
|
return result;
|
|
@@ -1451,10 +1470,10 @@ function parseSkillFrontmatter(content) {
|
|
|
1451
1470
|
}
|
|
1452
1471
|
const colonIndex = line.indexOf(":");
|
|
1453
1472
|
if (colonIndex > 0) {
|
|
1454
|
-
const
|
|
1473
|
+
const key2 = line.slice(0, colonIndex).trim();
|
|
1455
1474
|
let value = line.slice(colonIndex + 1).trim();
|
|
1456
1475
|
if (value === "" || value === "[]") {
|
|
1457
|
-
currentArrayKey =
|
|
1476
|
+
currentArrayKey = key2;
|
|
1458
1477
|
currentArray = [];
|
|
1459
1478
|
continue;
|
|
1460
1479
|
}
|
|
@@ -1467,18 +1486,18 @@ function parseSkillFrontmatter(content) {
|
|
|
1467
1486
|
}
|
|
1468
1487
|
return trimmed;
|
|
1469
1488
|
}).filter((item) => item.length > 0);
|
|
1470
|
-
data[
|
|
1489
|
+
data[key2] = items;
|
|
1471
1490
|
continue;
|
|
1472
1491
|
}
|
|
1473
1492
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1474
1493
|
value = value.slice(1, -1);
|
|
1475
1494
|
}
|
|
1476
1495
|
if (value === "true") {
|
|
1477
|
-
data[
|
|
1496
|
+
data[key2] = true;
|
|
1478
1497
|
} else if (value === "false") {
|
|
1479
|
-
data[
|
|
1498
|
+
data[key2] = false;
|
|
1480
1499
|
} else {
|
|
1481
|
-
data[
|
|
1500
|
+
data[key2] = value;
|
|
1482
1501
|
}
|
|
1483
1502
|
}
|
|
1484
1503
|
}
|
|
@@ -1535,7 +1554,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1535
1554
|
globs: parsed.metadata.globs,
|
|
1536
1555
|
loadType,
|
|
1537
1556
|
priority,
|
|
1538
|
-
sourceDir: directory
|
|
1557
|
+
sourceDir: directory,
|
|
1558
|
+
platforms: parsed.metadata.platforms
|
|
1539
1559
|
});
|
|
1540
1560
|
} else {
|
|
1541
1561
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1548,11 +1568,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1548
1568
|
globs: [],
|
|
1549
1569
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1550
1570
|
priority,
|
|
1551
|
-
sourceDir: directory
|
|
1571
|
+
sourceDir: directory,
|
|
1572
|
+
platforms: []
|
|
1552
1573
|
});
|
|
1553
1574
|
}
|
|
1554
1575
|
}
|
|
1555
|
-
return skills
|
|
1576
|
+
return skills.filter(
|
|
1577
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1578
|
+
);
|
|
1556
1579
|
}
|
|
1557
1580
|
async function loadAllSkills(directories) {
|
|
1558
1581
|
const allSkills = [];
|
|
@@ -1806,9 +1829,9 @@ var init_chunker = __esm({
|
|
|
1806
1829
|
});
|
|
1807
1830
|
|
|
1808
1831
|
// src/semantic/client.ts
|
|
1809
|
-
function initVectorClient(serverUrl,
|
|
1832
|
+
function initVectorClient(serverUrl, key2) {
|
|
1810
1833
|
remoteServerUrl2 = serverUrl.replace(/\/$/, "");
|
|
1811
|
-
authKey2 =
|
|
1834
|
+
authKey2 = key2;
|
|
1812
1835
|
}
|
|
1813
1836
|
function isVectorClientConfigured() {
|
|
1814
1837
|
return !!remoteServerUrl2 && !!authKey2;
|
|
@@ -2149,6 +2172,7 @@ __export(webhook_exports, {
|
|
|
2149
2172
|
sendWebhook: () => sendWebhook
|
|
2150
2173
|
});
|
|
2151
2174
|
async function sendWebhook(url, event) {
|
|
2175
|
+
const t0 = Date.now();
|
|
2152
2176
|
try {
|
|
2153
2177
|
const controller = new AbortController();
|
|
2154
2178
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2162,17 +2186,36 @@ async function sendWebhook(url, event) {
|
|
|
2162
2186
|
signal: controller.signal
|
|
2163
2187
|
});
|
|
2164
2188
|
clearTimeout(timeout);
|
|
2189
|
+
const ms = Date.now() - t0;
|
|
2165
2190
|
if (!response.ok) {
|
|
2166
|
-
|
|
2191
|
+
const body = await response.text().catch(() => "");
|
|
2192
|
+
console.warn(
|
|
2193
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> HTTP ${response.status} in ${ms}ms${body ? ` (${body.slice(0, 200)})` : ""}`
|
|
2194
|
+
);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (!QUIET || TERMINAL_EVENTS.has(event.type)) {
|
|
2198
|
+
console.log(
|
|
2199
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> 200 in ${ms}ms`
|
|
2200
|
+
);
|
|
2167
2201
|
}
|
|
2168
2202
|
} catch (err) {
|
|
2169
2203
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2170
|
-
console.warn(
|
|
2204
|
+
console.warn(
|
|
2205
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
2206
|
+
);
|
|
2171
2207
|
}
|
|
2172
2208
|
}
|
|
2209
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2173
2210
|
var init_webhook = __esm({
|
|
2174
2211
|
"src/utils/webhook.ts"() {
|
|
2175
2212
|
"use strict";
|
|
2213
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set([
|
|
2214
|
+
"task.started",
|
|
2215
|
+
"task.completed",
|
|
2216
|
+
"task.failed"
|
|
2217
|
+
]);
|
|
2218
|
+
QUIET = process.env.SPARKECODER_QUIET_WEBHOOKS === "1";
|
|
2176
2219
|
}
|
|
2177
2220
|
});
|
|
2178
2221
|
|
|
@@ -2382,15 +2425,15 @@ var recorder_exports = {};
|
|
|
2382
2425
|
__export(recorder_exports, {
|
|
2383
2426
|
FrameRecorder: () => FrameRecorder
|
|
2384
2427
|
});
|
|
2385
|
-
import { exec as
|
|
2386
|
-
import { promisify as
|
|
2428
|
+
import { exec as exec6 } from "child_process";
|
|
2429
|
+
import { promisify as promisify6 } from "util";
|
|
2387
2430
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
2388
|
-
import { join as
|
|
2389
|
-
import { tmpdir } from "os";
|
|
2390
|
-
import { nanoid as
|
|
2431
|
+
import { join as join9 } from "path";
|
|
2432
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2433
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2391
2434
|
async function checkFfmpeg() {
|
|
2392
2435
|
try {
|
|
2393
|
-
await
|
|
2436
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
2394
2437
|
return true;
|
|
2395
2438
|
} catch {
|
|
2396
2439
|
return false;
|
|
@@ -2402,11 +2445,11 @@ async function cleanup(dir) {
|
|
|
2402
2445
|
} catch {
|
|
2403
2446
|
}
|
|
2404
2447
|
}
|
|
2405
|
-
var
|
|
2448
|
+
var execAsync6, FrameRecorder;
|
|
2406
2449
|
var init_recorder = __esm({
|
|
2407
2450
|
"src/browser/recorder.ts"() {
|
|
2408
2451
|
"use strict";
|
|
2409
|
-
|
|
2452
|
+
execAsync6 = promisify6(exec6);
|
|
2410
2453
|
FrameRecorder = class {
|
|
2411
2454
|
frames = [];
|
|
2412
2455
|
startTime = null;
|
|
@@ -2442,21 +2485,21 @@ var init_recorder = __esm({
|
|
|
2442
2485
|
*/
|
|
2443
2486
|
async encode() {
|
|
2444
2487
|
if (this.frames.length === 0) return null;
|
|
2445
|
-
const workDir =
|
|
2488
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid5(8)}`);
|
|
2446
2489
|
await mkdir4(workDir, { recursive: true });
|
|
2447
2490
|
try {
|
|
2448
2491
|
for (let i = 0; i < this.frames.length; i++) {
|
|
2449
|
-
const framePath =
|
|
2492
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
2450
2493
|
await writeFile5(framePath, this.frames[i].data);
|
|
2451
2494
|
}
|
|
2452
2495
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
2453
2496
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
2454
2497
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
2455
|
-
const outputPath =
|
|
2498
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
2456
2499
|
const hasFfmpeg = await checkFfmpeg();
|
|
2457
2500
|
if (hasFfmpeg) {
|
|
2458
|
-
await
|
|
2459
|
-
`ffmpeg -y -framerate ${clampedFps} -i "${
|
|
2501
|
+
await execAsync6(
|
|
2502
|
+
`ffmpeg -y -framerate ${clampedFps} -i "${join9(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
|
|
2460
2503
|
{ timeout: 12e4 }
|
|
2461
2504
|
);
|
|
2462
2505
|
} else {
|
|
@@ -2468,7 +2511,7 @@ var init_recorder = __esm({
|
|
|
2468
2511
|
const files = await readdir5(workDir);
|
|
2469
2512
|
for (const f of files) {
|
|
2470
2513
|
if (f.startsWith("frame_")) {
|
|
2471
|
-
await unlink2(
|
|
2514
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
2472
2515
|
});
|
|
2473
2516
|
}
|
|
2474
2517
|
}
|
|
@@ -2493,7 +2536,7 @@ var init_recorder = __esm({
|
|
|
2493
2536
|
import {
|
|
2494
2537
|
streamText as streamText2,
|
|
2495
2538
|
generateText as generateText3,
|
|
2496
|
-
tool as
|
|
2539
|
+
tool as tool14,
|
|
2497
2540
|
stepCountIs as stepCountIs2
|
|
2498
2541
|
} from "ai";
|
|
2499
2542
|
|
|
@@ -2662,6 +2705,23 @@ function isAnthropicModel(modelId) {
|
|
|
2662
2705
|
const normalized = modelId.trim().toLowerCase();
|
|
2663
2706
|
return normalized.startsWith(ANTHROPIC_PREFIX) || normalized.startsWith("claude-");
|
|
2664
2707
|
}
|
|
2708
|
+
function requiresAdaptiveThinking(modelId) {
|
|
2709
|
+
const m = modelId.toLowerCase().match(/claude-(?:opus|sonnet|haiku)-(\d+)[.-](\d{1,2})(?!\d)/);
|
|
2710
|
+
if (!m) return false;
|
|
2711
|
+
const major = Number(m[1]);
|
|
2712
|
+
const minor = Number(m[2]);
|
|
2713
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) return false;
|
|
2714
|
+
if (major > 4) return true;
|
|
2715
|
+
if (major === 4 && minor >= 6) return true;
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
function getAnthropicProviderOptions(modelId, opts = {}) {
|
|
2719
|
+
const { toolStreaming, budgetTokens = 1e4 } = opts;
|
|
2720
|
+
const thinking = requiresAdaptiveThinking(modelId) ? { type: "adaptive" } : { type: "enabled", budgetTokens };
|
|
2721
|
+
const out = { thinking };
|
|
2722
|
+
if (toolStreaming) out.toolStreaming = true;
|
|
2723
|
+
return out;
|
|
2724
|
+
}
|
|
2665
2725
|
function resolveModel(modelId) {
|
|
2666
2726
|
try {
|
|
2667
2727
|
const config = getConfig();
|
|
@@ -2684,8 +2744,8 @@ var SUBAGENT_MODELS = {
|
|
|
2684
2744
|
// src/agent/index.ts
|
|
2685
2745
|
init_db();
|
|
2686
2746
|
init_config();
|
|
2687
|
-
import { z as
|
|
2688
|
-
import { nanoid as
|
|
2747
|
+
import { z as z15 } from "zod";
|
|
2748
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2689
2749
|
|
|
2690
2750
|
// src/tools/bash.ts
|
|
2691
2751
|
import { tool } from "ai";
|
|
@@ -3035,11 +3095,11 @@ async function sendInput(terminalId, input, options = {}) {
|
|
|
3035
3095
|
return false;
|
|
3036
3096
|
}
|
|
3037
3097
|
}
|
|
3038
|
-
async function sendKey(terminalId,
|
|
3098
|
+
async function sendKey(terminalId, key2) {
|
|
3039
3099
|
const session = getSessionName(terminalId);
|
|
3040
3100
|
try {
|
|
3041
3101
|
await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
|
|
3042
|
-
await execAsync(`tmux send-keys -t ${session} ${
|
|
3102
|
+
await execAsync(`tmux send-keys -t ${session} ${key2}`, { timeout: 1e3 });
|
|
3043
3103
|
return true;
|
|
3044
3104
|
} catch {
|
|
3045
3105
|
return false;
|
|
@@ -3178,7 +3238,7 @@ bash({ id: "abc123", input: "my text" }) // send text input
|
|
|
3178
3238
|
Terminal output is stored in the global SparkECoder data directory. Use the \`tail\` option to read recent output.`,
|
|
3179
3239
|
inputSchema: bashInputSchema,
|
|
3180
3240
|
execute: async (inputArgs) => {
|
|
3181
|
-
const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
|
|
3241
|
+
const { command, background, id, kill, tail, input: textInput, key: key2 } = inputArgs;
|
|
3182
3242
|
if (id) {
|
|
3183
3243
|
if (kill) {
|
|
3184
3244
|
const success = await killTerminal(id);
|
|
@@ -3209,8 +3269,8 @@ Terminal output is stored in the global SparkECoder data directory. Use the \`ta
|
|
|
3209
3269
|
message: `Sent input "${textInput}" to terminal`
|
|
3210
3270
|
};
|
|
3211
3271
|
}
|
|
3212
|
-
if (
|
|
3213
|
-
const success = await sendKey(id,
|
|
3272
|
+
if (key2) {
|
|
3273
|
+
const success = await sendKey(id, key2);
|
|
3214
3274
|
if (!success) {
|
|
3215
3275
|
return {
|
|
3216
3276
|
success: false,
|
|
@@ -3226,7 +3286,7 @@ Terminal output is stored in the global SparkECoder data directory. Use the \`ta
|
|
|
3226
3286
|
id,
|
|
3227
3287
|
output: truncatedOutput2,
|
|
3228
3288
|
status: status2,
|
|
3229
|
-
message: `Sent key "${
|
|
3289
|
+
message: `Sent key "${key2}" to terminal`
|
|
3230
3290
|
};
|
|
3231
3291
|
}
|
|
3232
3292
|
const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
|
|
@@ -3368,13 +3428,13 @@ async function resizeImageIfNeeded(buffer, mediaType) {
|
|
|
3368
3428
|
const needsResize = longEdge > MAX_LONG_EDGE;
|
|
3369
3429
|
const needsShrink = buffer.length > MAX_FILE_BYTES;
|
|
3370
3430
|
if (!needsResize && !needsShrink) return { buffer, mediaType: inputMediaType };
|
|
3371
|
-
const
|
|
3431
|
+
const key2 = cacheKey(buffer);
|
|
3372
3432
|
const cacheDir = getCacheDir();
|
|
3373
3433
|
const isPng = inputMediaType.includes("png");
|
|
3374
3434
|
const willConvertToJpeg = isPng && (needsShrink || buffer.length > 2 * 1024 * 1024);
|
|
3375
3435
|
const outputMediaType = willConvertToJpeg || !isPng ? "image/jpeg" : "image/png";
|
|
3376
3436
|
const ext = outputMediaType === "image/png" ? ".png" : ".jpg";
|
|
3377
|
-
const cachePath = join3(cacheDir,
|
|
3437
|
+
const cachePath = join3(cacheDir, key2 + ext);
|
|
3378
3438
|
if (existsSync3(cachePath)) {
|
|
3379
3439
|
console.log(`[image-resize] Cache hit for ${width}x${height} image`);
|
|
3380
3440
|
return { buffer: readFileSync2(cachePath), mediaType: outputMediaType };
|
|
@@ -3769,12 +3829,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
3769
3829
|
}
|
|
3770
3830
|
async function commandExists(cmd) {
|
|
3771
3831
|
try {
|
|
3772
|
-
const { exec:
|
|
3773
|
-
const { promisify:
|
|
3774
|
-
const
|
|
3832
|
+
const { exec: exec8 } = await import("child_process");
|
|
3833
|
+
const { promisify: promisify8 } = await import("util");
|
|
3834
|
+
const execAsync8 = promisify8(exec8);
|
|
3775
3835
|
const isWindows = process.platform === "win32";
|
|
3776
3836
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
3777
|
-
await
|
|
3837
|
+
await execAsync8(checkCmd);
|
|
3778
3838
|
return true;
|
|
3779
3839
|
} catch {
|
|
3780
3840
|
return false;
|
|
@@ -4265,31 +4325,31 @@ async function getClientForFile(filePath) {
|
|
|
4265
4325
|
return null;
|
|
4266
4326
|
}
|
|
4267
4327
|
const root = dirname4(normalized);
|
|
4268
|
-
const
|
|
4269
|
-
const existing = state.clients.get(
|
|
4328
|
+
const key2 = `${serverDef.id}:${root}`;
|
|
4329
|
+
const existing = state.clients.get(key2);
|
|
4270
4330
|
if (existing) {
|
|
4271
4331
|
return existing;
|
|
4272
4332
|
}
|
|
4273
|
-
if (state.broken.has(
|
|
4333
|
+
if (state.broken.has(key2)) {
|
|
4274
4334
|
return null;
|
|
4275
4335
|
}
|
|
4276
4336
|
try {
|
|
4277
4337
|
const handle = await serverDef.spawn(root);
|
|
4278
4338
|
if (!handle) {
|
|
4279
|
-
state.broken.add(
|
|
4339
|
+
state.broken.add(key2);
|
|
4280
4340
|
return null;
|
|
4281
4341
|
}
|
|
4282
4342
|
console.log(`[lsp] Started ${serverDef.name} for ${root}`);
|
|
4283
4343
|
const client = await createClient(serverDef.id, handle, root);
|
|
4284
|
-
state.clients.set(
|
|
4344
|
+
state.clients.set(key2, client);
|
|
4285
4345
|
handle.process.on("exit", (code) => {
|
|
4286
4346
|
console.log(`[lsp] ${serverDef.name} exited with code ${code}`);
|
|
4287
|
-
state.clients.delete(
|
|
4347
|
+
state.clients.delete(key2);
|
|
4288
4348
|
});
|
|
4289
4349
|
return client;
|
|
4290
4350
|
} catch (error) {
|
|
4291
4351
|
console.error(`[lsp] Failed to start ${serverDef.name}:`, error);
|
|
4292
|
-
state.broken.add(
|
|
4352
|
+
state.broken.add(key2);
|
|
4293
4353
|
return null;
|
|
4294
4354
|
}
|
|
4295
4355
|
}
|
|
@@ -6021,6 +6081,7 @@ init_semantic_search();
|
|
|
6021
6081
|
import { tool as tool11 } from "ai";
|
|
6022
6082
|
import { z as z12 } from "zod";
|
|
6023
6083
|
import Ajv from "ajv";
|
|
6084
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
6024
6085
|
var ajv = new Ajv({ allErrors: true });
|
|
6025
6086
|
function createCompleteTaskTool(options) {
|
|
6026
6087
|
const validate = ajv.compile(options.outputSchema);
|
|
@@ -6067,6 +6128,37 @@ function createTaskFailedTool(options) {
|
|
|
6067
6128
|
}
|
|
6068
6129
|
});
|
|
6069
6130
|
}
|
|
6131
|
+
function createAskQuestionToUserTool(options) {
|
|
6132
|
+
return tool11({
|
|
6133
|
+
description: "Ask the user a blocking clarification question when you cannot safely continue without more information. Use this only after trying to infer the answer from the available context. The task will pause until the orchestrator or user answers.",
|
|
6134
|
+
inputSchema: z12.object({
|
|
6135
|
+
question: z12.string().min(1).describe("The concise question you need answered."),
|
|
6136
|
+
context: z12.string().optional().describe("Brief context explaining why this answer is needed and what you already tried."),
|
|
6137
|
+
choices: z12.array(z12.string().min(1)).max(10).optional().describe("Optional suggested answer choices when the question is multiple choice.")
|
|
6138
|
+
}),
|
|
6139
|
+
execute: async (input) => {
|
|
6140
|
+
if (!options.onQuestion) {
|
|
6141
|
+
return {
|
|
6142
|
+
status: "unavailable",
|
|
6143
|
+
message: "Question routing is not configured for this task. Continue with the best safe assumption or call task_failed if blocked."
|
|
6144
|
+
};
|
|
6145
|
+
}
|
|
6146
|
+
const questionId = `q_${nanoid3(12)}`;
|
|
6147
|
+
const answer = await options.onQuestion({
|
|
6148
|
+
questionId,
|
|
6149
|
+
question: input.question,
|
|
6150
|
+
context: input.context,
|
|
6151
|
+
choices: input.choices
|
|
6152
|
+
});
|
|
6153
|
+
return {
|
|
6154
|
+
status: "answered",
|
|
6155
|
+
questionId,
|
|
6156
|
+
answer: answer.answer,
|
|
6157
|
+
answeredBy: answer.answeredBy ?? "unknown"
|
|
6158
|
+
};
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
}
|
|
6070
6162
|
|
|
6071
6163
|
// src/tools/upload-file.ts
|
|
6072
6164
|
import { tool as tool12 } from "ai";
|
|
@@ -6165,6 +6257,568 @@ function createUploadFileTool(options) {
|
|
|
6165
6257
|
});
|
|
6166
6258
|
}
|
|
6167
6259
|
|
|
6260
|
+
// src/tools/computer-use.ts
|
|
6261
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
6262
|
+
import { exec as exec5 } from "child_process";
|
|
6263
|
+
import { promisify as promisify5 } from "util";
|
|
6264
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync15, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6265
|
+
import { join as join8 } from "path";
|
|
6266
|
+
import { tmpdir } from "os";
|
|
6267
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
6268
|
+
var execAsync5 = promisify5(exec5);
|
|
6269
|
+
var DEFAULT_WIDTH = 1280;
|
|
6270
|
+
var DEFAULT_HEIGHT = 800;
|
|
6271
|
+
function isMacOs() {
|
|
6272
|
+
return process.platform === "darwin";
|
|
6273
|
+
}
|
|
6274
|
+
async function isCliclickInstalled() {
|
|
6275
|
+
try {
|
|
6276
|
+
await execAsync5("command -v cliclick", { timeout: 2e3 });
|
|
6277
|
+
return true;
|
|
6278
|
+
} catch {
|
|
6279
|
+
return false;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
async function runJxa(script) {
|
|
6283
|
+
try {
|
|
6284
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
6285
|
+
const { stdout } = await execAsync5(`osascript -l JavaScript -e '${escaped}'`, {
|
|
6286
|
+
timeout: 5e3
|
|
6287
|
+
});
|
|
6288
|
+
return JSON.parse(stdout.trim());
|
|
6289
|
+
} catch {
|
|
6290
|
+
return null;
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
async function hasAccessibilityPermissions() {
|
|
6294
|
+
try {
|
|
6295
|
+
const { stderr } = await execAsync5("cliclick p:.", { timeout: 3e3 });
|
|
6296
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6297
|
+
return { ok: false, error: stderr.trim().split("\n")[0] };
|
|
6298
|
+
}
|
|
6299
|
+
return { ok: true };
|
|
6300
|
+
} catch (err) {
|
|
6301
|
+
return { ok: false, error: err?.message || String(err) };
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
async function hasScreenRecordingPermissions() {
|
|
6305
|
+
const result = await runJxa(
|
|
6306
|
+
`ObjC.import("Cocoa");
|
|
6307
|
+
ObjC.import("CoreGraphics");
|
|
6308
|
+
ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]);
|
|
6309
|
+
JSON.stringify({ hasAccess: !!$.CGPreflightScreenCaptureAccess() });`
|
|
6310
|
+
);
|
|
6311
|
+
return result?.hasAccess ?? false;
|
|
6312
|
+
}
|
|
6313
|
+
async function requestAccessibilityPrompt() {
|
|
6314
|
+
const result = await runJxa(
|
|
6315
|
+
`ObjC.import("ApplicationServices");
|
|
6316
|
+
var key = $.kAXTrustedCheckOptionPrompt;
|
|
6317
|
+
var dict = $.NSDictionary.dictionaryWithObjectForKey($.kCFBooleanTrue, key);
|
|
6318
|
+
var trusted = $.AXIsProcessTrustedWithOptions(dict);
|
|
6319
|
+
JSON.stringify({ trusted: !!trusted });`
|
|
6320
|
+
);
|
|
6321
|
+
return result?.trusted ?? false;
|
|
6322
|
+
}
|
|
6323
|
+
async function requestScreenRecordingPrompt() {
|
|
6324
|
+
const result = await runJxa(
|
|
6325
|
+
`ObjC.import("Cocoa");
|
|
6326
|
+
ObjC.import("CoreGraphics");
|
|
6327
|
+
ObjC.bindFunction("CGRequestScreenCaptureAccess", ["bool", []]);
|
|
6328
|
+
JSON.stringify({ granted: !!$.CGRequestScreenCaptureAccess() });`
|
|
6329
|
+
);
|
|
6330
|
+
return result?.granted ?? false;
|
|
6331
|
+
}
|
|
6332
|
+
async function openSystemSettings(pane) {
|
|
6333
|
+
const url = pane === "accessibility" ? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" : "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6334
|
+
try {
|
|
6335
|
+
await execAsync5(`open '${url}'`, { timeout: 3e3 });
|
|
6336
|
+
} catch {
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
async function detectScreenSize() {
|
|
6340
|
+
try {
|
|
6341
|
+
const { stdout } = await execAsync5(
|
|
6342
|
+
`osascript -e 'tell application "Finder" to get bounds of window of desktop'`,
|
|
6343
|
+
{ timeout: 3e3 }
|
|
6344
|
+
);
|
|
6345
|
+
const parts = stdout.trim().split(",").map((s) => parseInt(s.trim(), 10));
|
|
6346
|
+
if (parts.length >= 4 && parts.every((n) => Number.isFinite(n))) {
|
|
6347
|
+
const [x1, y1, x2, y2] = parts;
|
|
6348
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
6349
|
+
}
|
|
6350
|
+
} catch {
|
|
6351
|
+
}
|
|
6352
|
+
return null;
|
|
6353
|
+
}
|
|
6354
|
+
async function runCliclick(args) {
|
|
6355
|
+
const quoted = args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
6356
|
+
const { stdout, stderr } = await execAsync5(`cliclick ${quoted}`, {
|
|
6357
|
+
timeout: 15e3,
|
|
6358
|
+
maxBuffer: 1024 * 1024
|
|
6359
|
+
});
|
|
6360
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6361
|
+
throw new Error(
|
|
6362
|
+
"Accessibility permissions not granted to cliclick. Open System Settings \u2192 Privacy & Security \u2192 Accessibility, add cliclick (or the agent runtime), and toggle it on."
|
|
6363
|
+
);
|
|
6364
|
+
}
|
|
6365
|
+
if (stderr && !stdout) throw new Error(stderr.trim());
|
|
6366
|
+
return (stdout || "").trim();
|
|
6367
|
+
}
|
|
6368
|
+
async function runScreencapture(path) {
|
|
6369
|
+
await execAsync5(`screencapture -x -t png '${path.replace(/'/g, `'\\''`)}'`, {
|
|
6370
|
+
timeout: 5e3
|
|
6371
|
+
});
|
|
6372
|
+
}
|
|
6373
|
+
async function resizeScreenshotToPoints(path, targetWidth, targetHeight) {
|
|
6374
|
+
const sharpModule = await import("sharp");
|
|
6375
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6376
|
+
const meta = await sharp2(path).metadata();
|
|
6377
|
+
if (meta.width === targetWidth && meta.height === targetHeight) {
|
|
6378
|
+
return readFileSync7(path);
|
|
6379
|
+
}
|
|
6380
|
+
return await sharp2(path).resize(targetWidth, targetHeight, { fit: "fill" }).png().toBuffer();
|
|
6381
|
+
}
|
|
6382
|
+
async function runScroll(dx, dy) {
|
|
6383
|
+
const wheelY = -Math.round(dy);
|
|
6384
|
+
const wheelX = -Math.round(dx);
|
|
6385
|
+
const script = `ObjC.import('CoreGraphics');var ev = $.CGEventCreateScrollWheelEvent(null, 0, 2, ${wheelY}, ${wheelX});$.CGEventPost(0, ev);`;
|
|
6386
|
+
await execAsync5(
|
|
6387
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6388
|
+
{ timeout: 5e3 }
|
|
6389
|
+
);
|
|
6390
|
+
}
|
|
6391
|
+
function translateKeyForCliclick(key2) {
|
|
6392
|
+
if (!key2) return [];
|
|
6393
|
+
const parts = key2.split("+").map((p) => p.trim()).filter(Boolean);
|
|
6394
|
+
if (parts.length === 0) return [];
|
|
6395
|
+
const modMap = {
|
|
6396
|
+
ctrl: "ctrl",
|
|
6397
|
+
control: "ctrl",
|
|
6398
|
+
alt: "alt",
|
|
6399
|
+
option: "alt",
|
|
6400
|
+
shift: "shift",
|
|
6401
|
+
cmd: "cmd",
|
|
6402
|
+
super: "cmd",
|
|
6403
|
+
meta: "cmd",
|
|
6404
|
+
win: "cmd",
|
|
6405
|
+
fn: "fn"
|
|
6406
|
+
};
|
|
6407
|
+
const keyMap = {
|
|
6408
|
+
return: "enter",
|
|
6409
|
+
enter: "enter",
|
|
6410
|
+
esc: "esc",
|
|
6411
|
+
escape: "esc",
|
|
6412
|
+
backspace: "delete",
|
|
6413
|
+
back_space: "delete",
|
|
6414
|
+
delete: "fwd-delete",
|
|
6415
|
+
fwd_delete: "fwd-delete",
|
|
6416
|
+
forward_delete: "fwd-delete",
|
|
6417
|
+
tab: "tab",
|
|
6418
|
+
space: "space",
|
|
6419
|
+
up: "arrow-up",
|
|
6420
|
+
arrow_up: "arrow-up",
|
|
6421
|
+
down: "arrow-down",
|
|
6422
|
+
arrow_down: "arrow-down",
|
|
6423
|
+
left: "arrow-left",
|
|
6424
|
+
arrow_left: "arrow-left",
|
|
6425
|
+
right: "arrow-right",
|
|
6426
|
+
arrow_right: "arrow-right",
|
|
6427
|
+
page_up: "page-up",
|
|
6428
|
+
pageup: "page-up",
|
|
6429
|
+
page_down: "page-down",
|
|
6430
|
+
pagedown: "page-down",
|
|
6431
|
+
home: "home",
|
|
6432
|
+
end: "end",
|
|
6433
|
+
f1: "f1",
|
|
6434
|
+
f2: "f2",
|
|
6435
|
+
f3: "f3",
|
|
6436
|
+
f4: "f4",
|
|
6437
|
+
f5: "f5",
|
|
6438
|
+
f6: "f6",
|
|
6439
|
+
f7: "f7",
|
|
6440
|
+
f8: "f8",
|
|
6441
|
+
f9: "f9",
|
|
6442
|
+
f10: "f10",
|
|
6443
|
+
f11: "f11",
|
|
6444
|
+
f12: "f12"
|
|
6445
|
+
};
|
|
6446
|
+
const modifiers = [];
|
|
6447
|
+
let mainKey = null;
|
|
6448
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6449
|
+
const lower = parts[i].toLowerCase().replace(/-/g, "_");
|
|
6450
|
+
if (i < parts.length - 1 && modMap[lower]) {
|
|
6451
|
+
modifiers.push(modMap[lower]);
|
|
6452
|
+
} else {
|
|
6453
|
+
mainKey = keyMap[lower] || lower;
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
const args = [];
|
|
6457
|
+
if (modifiers.length > 0) args.push(`kd:${modifiers.join(",")}`);
|
|
6458
|
+
if (mainKey) {
|
|
6459
|
+
const isNamedKey = Object.values(keyMap).includes(mainKey) || /^f([1-9]|1[0-9]|20)$/.test(mainKey) || /^num-/.test(mainKey);
|
|
6460
|
+
if (isNamedKey) {
|
|
6461
|
+
args.push(`kp:${mainKey}`);
|
|
6462
|
+
} else {
|
|
6463
|
+
args.push(`t:${mainKey}`);
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
if (modifiers.length > 0) args.push(`ku:${modifiers.join(",")}`);
|
|
6467
|
+
return args;
|
|
6468
|
+
}
|
|
6469
|
+
function modifierStringToCliclick(text) {
|
|
6470
|
+
return text.split("+").map((p) => p.trim().toLowerCase()).map((p) => {
|
|
6471
|
+
if (p === "ctrl" || p === "control") return "ctrl";
|
|
6472
|
+
if (p === "alt" || p === "option") return "alt";
|
|
6473
|
+
if (p === "shift") return "shift";
|
|
6474
|
+
if (p === "super" || p === "meta" || p === "cmd") return "cmd";
|
|
6475
|
+
return "";
|
|
6476
|
+
}).filter(Boolean);
|
|
6477
|
+
}
|
|
6478
|
+
function createComputerUseTool(options) {
|
|
6479
|
+
const displayWidth = options.displayWidth ?? DEFAULT_WIDTH;
|
|
6480
|
+
const displayHeight = options.displayHeight ?? DEFAULT_HEIGHT;
|
|
6481
|
+
return anthropic.tools.computer_20251124({
|
|
6482
|
+
displayWidthPx: displayWidth,
|
|
6483
|
+
displayHeightPx: displayHeight,
|
|
6484
|
+
enableZoom: true,
|
|
6485
|
+
execute: async (input) => {
|
|
6486
|
+
try {
|
|
6487
|
+
switch (input.action) {
|
|
6488
|
+
case "screenshot": {
|
|
6489
|
+
const path = join8(tmpdir(), `cu-${nanoid4(8)}.png`);
|
|
6490
|
+
await runScreencapture(path);
|
|
6491
|
+
const resized = await resizeScreenshotToPoints(path, displayWidth, displayHeight);
|
|
6492
|
+
try {
|
|
6493
|
+
unlinkSync2(path);
|
|
6494
|
+
} catch {
|
|
6495
|
+
}
|
|
6496
|
+
return { type: "image", data: resized.toString("base64") };
|
|
6497
|
+
}
|
|
6498
|
+
case "left_click": {
|
|
6499
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6500
|
+
if (input.text) {
|
|
6501
|
+
const mods = modifierStringToCliclick(input.text);
|
|
6502
|
+
if (mods.length > 0) {
|
|
6503
|
+
await runCliclick([`kd:${mods.join(",")}`, `c:${x},${y}`, `ku:${mods.join(",")}`]);
|
|
6504
|
+
} else {
|
|
6505
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6506
|
+
}
|
|
6507
|
+
} else {
|
|
6508
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6509
|
+
}
|
|
6510
|
+
return `clicked at (${x}, ${y})${input.text ? ` with ${input.text}` : ""}`;
|
|
6511
|
+
}
|
|
6512
|
+
case "right_click": {
|
|
6513
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6514
|
+
await runCliclick([`rc:${x},${y}`]);
|
|
6515
|
+
return `right-clicked at (${x}, ${y})`;
|
|
6516
|
+
}
|
|
6517
|
+
case "middle_click": {
|
|
6518
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6519
|
+
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);`;
|
|
6520
|
+
await execAsync5(
|
|
6521
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6522
|
+
{ timeout: 3e3 }
|
|
6523
|
+
);
|
|
6524
|
+
return `middle-clicked at (${x}, ${y})`;
|
|
6525
|
+
}
|
|
6526
|
+
case "double_click": {
|
|
6527
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6528
|
+
await runCliclick([`dc:${x},${y}`]);
|
|
6529
|
+
return `double-clicked at (${x}, ${y})`;
|
|
6530
|
+
}
|
|
6531
|
+
case "triple_click": {
|
|
6532
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6533
|
+
await runCliclick([`tc:${x},${y}`]);
|
|
6534
|
+
return `triple-clicked at (${x}, ${y})`;
|
|
6535
|
+
}
|
|
6536
|
+
case "mouse_move": {
|
|
6537
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6538
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6539
|
+
return `moved cursor to (${x}, ${y})`;
|
|
6540
|
+
}
|
|
6541
|
+
case "left_mouse_down": {
|
|
6542
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6543
|
+
await runCliclick([`dd:${x},${y}`]);
|
|
6544
|
+
return `left mouse button pressed at (${x}, ${y})`;
|
|
6545
|
+
}
|
|
6546
|
+
case "left_mouse_up": {
|
|
6547
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6548
|
+
await runCliclick([`du:${x},${y}`]);
|
|
6549
|
+
return `left mouse button released at (${x}, ${y})`;
|
|
6550
|
+
}
|
|
6551
|
+
case "left_click_drag": {
|
|
6552
|
+
const [sx, sy] = input.start_coordinate ?? [0, 0];
|
|
6553
|
+
const [ex, ey] = input.coordinate ?? [0, 0];
|
|
6554
|
+
await runCliclick([`dd:${sx},${sy}`, `m:${ex},${ey}`, `du:${ex},${ey}`]);
|
|
6555
|
+
return `dragged from (${sx}, ${sy}) to (${ex}, ${ey})`;
|
|
6556
|
+
}
|
|
6557
|
+
case "type": {
|
|
6558
|
+
const text = input.text ?? "";
|
|
6559
|
+
await runCliclick([`t:${text}`]);
|
|
6560
|
+
return `typed ${text.length} character(s)`;
|
|
6561
|
+
}
|
|
6562
|
+
case "key": {
|
|
6563
|
+
const args = translateKeyForCliclick(input.text ?? "");
|
|
6564
|
+
if (args.length === 0) return "no key specified";
|
|
6565
|
+
await runCliclick(args);
|
|
6566
|
+
return `pressed ${input.text}`;
|
|
6567
|
+
}
|
|
6568
|
+
case "hold_key": {
|
|
6569
|
+
const text = (input.text ?? "").toLowerCase();
|
|
6570
|
+
const duration = input.duration ?? 1;
|
|
6571
|
+
const modMap = {
|
|
6572
|
+
ctrl: "ctrl",
|
|
6573
|
+
control: "ctrl",
|
|
6574
|
+
alt: "alt",
|
|
6575
|
+
option: "alt",
|
|
6576
|
+
shift: "shift",
|
|
6577
|
+
cmd: "cmd",
|
|
6578
|
+
super: "cmd",
|
|
6579
|
+
meta: "cmd",
|
|
6580
|
+
fn: "fn"
|
|
6581
|
+
};
|
|
6582
|
+
const cliName = modMap[text] || text;
|
|
6583
|
+
await runCliclick([`kd:${cliName}`]);
|
|
6584
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6585
|
+
await runCliclick([`ku:${cliName}`]);
|
|
6586
|
+
return `held ${text} for ${duration}s`;
|
|
6587
|
+
}
|
|
6588
|
+
case "scroll": {
|
|
6589
|
+
const direction = input.scroll_direction ?? "down";
|
|
6590
|
+
const amount = input.scroll_amount ?? 3;
|
|
6591
|
+
const px = amount * 100;
|
|
6592
|
+
const dx = direction === "left" ? -px : direction === "right" ? px : 0;
|
|
6593
|
+
const dy = direction === "up" ? -px : direction === "down" ? px : 0;
|
|
6594
|
+
if (input.coordinate) {
|
|
6595
|
+
const [x, y] = input.coordinate;
|
|
6596
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6597
|
+
}
|
|
6598
|
+
const mods = input.text ? modifierStringToCliclick(input.text) : [];
|
|
6599
|
+
if (mods.length > 0) {
|
|
6600
|
+
await runCliclick([`kd:${mods.join(",")}`]);
|
|
6601
|
+
}
|
|
6602
|
+
await runScroll(dx, dy);
|
|
6603
|
+
if (mods.length > 0) {
|
|
6604
|
+
await runCliclick([`ku:${mods.join(",")}`]);
|
|
6605
|
+
}
|
|
6606
|
+
return `scrolled ${direction} by ${amount}`;
|
|
6607
|
+
}
|
|
6608
|
+
case "wait": {
|
|
6609
|
+
const duration = input.duration ?? 1;
|
|
6610
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6611
|
+
return `waited ${duration}s`;
|
|
6612
|
+
}
|
|
6613
|
+
case "cursor_position": {
|
|
6614
|
+
const out = await runCliclick(["p:."]);
|
|
6615
|
+
return `cursor at ${out}`;
|
|
6616
|
+
}
|
|
6617
|
+
case "zoom": {
|
|
6618
|
+
const region = input.region ?? [0, 0, displayWidth, displayHeight];
|
|
6619
|
+
const [x1, y1, x2, y2] = region;
|
|
6620
|
+
const tmpPath = join8(tmpdir(), `cu-zoom-${nanoid4(8)}.png`);
|
|
6621
|
+
await runScreencapture(tmpPath);
|
|
6622
|
+
const sharpModule = await import("sharp");
|
|
6623
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6624
|
+
const meta = await sharp2(tmpPath).metadata();
|
|
6625
|
+
const scaleX = (meta.width || displayWidth) / displayWidth;
|
|
6626
|
+
const scaleY = (meta.height || displayHeight) / displayHeight;
|
|
6627
|
+
const px = {
|
|
6628
|
+
left: Math.max(0, Math.round(x1 * scaleX)),
|
|
6629
|
+
top: Math.max(0, Math.round(y1 * scaleY)),
|
|
6630
|
+
width: Math.max(1, Math.round((x2 - x1) * scaleX)),
|
|
6631
|
+
height: Math.max(1, Math.round((y2 - y1) * scaleY))
|
|
6632
|
+
};
|
|
6633
|
+
const buf = await sharp2(tmpPath).extract(px).png().toBuffer();
|
|
6634
|
+
try {
|
|
6635
|
+
unlinkSync2(tmpPath);
|
|
6636
|
+
} catch {
|
|
6637
|
+
}
|
|
6638
|
+
return { type: "image", data: buf.toString("base64") };
|
|
6639
|
+
}
|
|
6640
|
+
default: {
|
|
6641
|
+
const exhaustive = input.action;
|
|
6642
|
+
return `unsupported action: ${String(exhaustive)}`;
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
} catch (err) {
|
|
6646
|
+
const msg = err?.message || String(err);
|
|
6647
|
+
let hint = "";
|
|
6648
|
+
if (/accessibility|not authorized|tcc|operation not permitted/i.test(msg)) {
|
|
6649
|
+
hint = " (Hint: call enable_computer_use to (re-)check permissions and open System Settings)";
|
|
6650
|
+
} else if (/command not found/i.test(msg)) {
|
|
6651
|
+
hint = " (Hint: install cliclick with `brew install cliclick`)";
|
|
6652
|
+
}
|
|
6653
|
+
return `Error: ${msg}${hint}`;
|
|
6654
|
+
}
|
|
6655
|
+
},
|
|
6656
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6657
|
+
toModelOutput({ output }) {
|
|
6658
|
+
if (typeof output === "string") {
|
|
6659
|
+
return { type: "content", value: [{ type: "text", text: output }] };
|
|
6660
|
+
}
|
|
6661
|
+
return {
|
|
6662
|
+
type: "content",
|
|
6663
|
+
value: [{ type: "media", data: output.data, mediaType: "image/png" }]
|
|
6664
|
+
};
|
|
6665
|
+
}
|
|
6666
|
+
});
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
// src/tools/enable-computer-use.ts
|
|
6670
|
+
init_db();
|
|
6671
|
+
import { tool as tool13 } from "ai";
|
|
6672
|
+
import { z as z14 } from "zod";
|
|
6673
|
+
var inputSchema = z14.object({
|
|
6674
|
+
display_width: z14.number().int().positive().optional().describe("Display width in pixels (defaults to detected primary display, fallback 1280)"),
|
|
6675
|
+
display_height: z14.number().int().positive().optional().describe("Display height in pixels (defaults to detected primary display, fallback 800)"),
|
|
6676
|
+
request_permissions: z14.boolean().optional().default(true).describe(
|
|
6677
|
+
"When true (default), proactively trigger macOS permission prompts and open System Settings panes for any missing permissions."
|
|
6678
|
+
)
|
|
6679
|
+
});
|
|
6680
|
+
var ACCESSIBILITY_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
6681
|
+
var SCREEN_RECORDING_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6682
|
+
function createEnableComputerUseTool(options) {
|
|
6683
|
+
return tool13({
|
|
6684
|
+
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.",
|
|
6685
|
+
inputSchema,
|
|
6686
|
+
execute: async ({ display_width, display_height, request_permissions }) => {
|
|
6687
|
+
try {
|
|
6688
|
+
if (!isMacOs()) {
|
|
6689
|
+
return {
|
|
6690
|
+
success: false,
|
|
6691
|
+
error: "Computer use is currently only supported on macOS.",
|
|
6692
|
+
platform: process.platform
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
if (!await isCliclickInstalled()) {
|
|
6696
|
+
return {
|
|
6697
|
+
success: false,
|
|
6698
|
+
error: "`cliclick` is not installed. It is required for mouse/keyboard control on macOS.",
|
|
6699
|
+
installCommand: "brew install cliclick",
|
|
6700
|
+
fixSteps: [
|
|
6701
|
+
"In a terminal on this Mac, run: brew install cliclick",
|
|
6702
|
+
"(If Homebrew is not installed, install it first from https://brew.sh)",
|
|
6703
|
+
"Then call enable_computer_use again"
|
|
6704
|
+
]
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
const acc = await hasAccessibilityPermissions();
|
|
6708
|
+
const screen = await hasScreenRecordingPermissions();
|
|
6709
|
+
const missing = [];
|
|
6710
|
+
if (!acc.ok) {
|
|
6711
|
+
let prompted = false;
|
|
6712
|
+
let panelOpened = false;
|
|
6713
|
+
if (request_permissions) {
|
|
6714
|
+
prompted = await requestAccessibilityPrompt().then(() => true).catch(() => false);
|
|
6715
|
+
await openSystemSettings("accessibility").then(() => {
|
|
6716
|
+
panelOpened = true;
|
|
6717
|
+
}).catch(() => void 0);
|
|
6718
|
+
}
|
|
6719
|
+
missing.push({
|
|
6720
|
+
name: "Accessibility",
|
|
6721
|
+
reason: "cliclick failed: " + (acc.error?.split("\n")[0] || "no permission"),
|
|
6722
|
+
pane: "accessibility",
|
|
6723
|
+
settingsUrl: ACCESSIBILITY_URL,
|
|
6724
|
+
fixSteps: [
|
|
6725
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Accessibility pane that opened",
|
|
6726
|
+
"Click the + button",
|
|
6727
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6728
|
+
"Toggle the switch ON",
|
|
6729
|
+
"Restart the agent process so the new permission takes effect",
|
|
6730
|
+
"Then call enable_computer_use again"
|
|
6731
|
+
],
|
|
6732
|
+
prompted,
|
|
6733
|
+
panelOpened
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6736
|
+
if (!screen) {
|
|
6737
|
+
let prompted = false;
|
|
6738
|
+
let panelOpened = false;
|
|
6739
|
+
if (request_permissions) {
|
|
6740
|
+
prompted = await requestScreenRecordingPrompt().then(() => true).catch(() => false);
|
|
6741
|
+
await openSystemSettings("screen-recording").then(() => {
|
|
6742
|
+
panelOpened = true;
|
|
6743
|
+
}).catch(() => void 0);
|
|
6744
|
+
}
|
|
6745
|
+
missing.push({
|
|
6746
|
+
name: "Screen Recording",
|
|
6747
|
+
reason: "CGPreflightScreenCaptureAccess returned false",
|
|
6748
|
+
pane: "screen-recording",
|
|
6749
|
+
settingsUrl: SCREEN_RECORDING_URL,
|
|
6750
|
+
fixSteps: [
|
|
6751
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Screen Recording pane that opened",
|
|
6752
|
+
"Click the + button",
|
|
6753
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6754
|
+
"Toggle the switch ON",
|
|
6755
|
+
"Restart the agent process so the new permission takes effect",
|
|
6756
|
+
"Then call enable_computer_use again"
|
|
6757
|
+
],
|
|
6758
|
+
prompted,
|
|
6759
|
+
panelOpened
|
|
6760
|
+
});
|
|
6761
|
+
}
|
|
6762
|
+
if (missing.length > 0) {
|
|
6763
|
+
return {
|
|
6764
|
+
success: false,
|
|
6765
|
+
error: `Missing permission${missing.length > 1 ? "s" : ""}: ` + missing.map((m) => m.name).join(" and ") + ".",
|
|
6766
|
+
missingPermissions: missing,
|
|
6767
|
+
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."
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6770
|
+
let width = display_width;
|
|
6771
|
+
let height = display_height;
|
|
6772
|
+
let detected = null;
|
|
6773
|
+
if (width === void 0 || height === void 0) {
|
|
6774
|
+
detected = await detectScreenSize();
|
|
6775
|
+
width = width ?? detected?.width ?? 1280;
|
|
6776
|
+
height = height ?? detected?.height ?? 800;
|
|
6777
|
+
}
|
|
6778
|
+
const session = await sessionQueries.getById(options.sessionId);
|
|
6779
|
+
if (!session) {
|
|
6780
|
+
return { success: false, error: "Session not found" };
|
|
6781
|
+
}
|
|
6782
|
+
const config = session.config || {};
|
|
6783
|
+
if (config.computerUseEnabled === true && config.computerUseDisplayWidth === width && config.computerUseDisplayHeight === height) {
|
|
6784
|
+
return {
|
|
6785
|
+
success: true,
|
|
6786
|
+
alreadyEnabled: true,
|
|
6787
|
+
message: "Computer use was already enabled for this session.",
|
|
6788
|
+
displayWidth: width,
|
|
6789
|
+
displayHeight: height
|
|
6790
|
+
};
|
|
6791
|
+
}
|
|
6792
|
+
const updated = {
|
|
6793
|
+
...config,
|
|
6794
|
+
computerUseEnabled: true,
|
|
6795
|
+
computerUseDisplayWidth: width,
|
|
6796
|
+
computerUseDisplayHeight: height
|
|
6797
|
+
};
|
|
6798
|
+
await sessionQueries.update(options.sessionId, { config: updated });
|
|
6799
|
+
return {
|
|
6800
|
+
success: true,
|
|
6801
|
+
enabled: true,
|
|
6802
|
+
platform: "darwin",
|
|
6803
|
+
displayWidth: width,
|
|
6804
|
+
displayHeight: height,
|
|
6805
|
+
detectedScreenSize: detected || void 0,
|
|
6806
|
+
permissions: {
|
|
6807
|
+
accessibility: "granted",
|
|
6808
|
+
screenRecording: "granted"
|
|
6809
|
+
},
|
|
6810
|
+
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.`
|
|
6811
|
+
};
|
|
6812
|
+
} catch (err) {
|
|
6813
|
+
return {
|
|
6814
|
+
success: false,
|
|
6815
|
+
error: err?.message || String(err)
|
|
6816
|
+
};
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
|
|
6168
6822
|
// src/tools/index.ts
|
|
6169
6823
|
init_semantic();
|
|
6170
6824
|
init_remote();
|
|
@@ -6213,6 +6867,20 @@ async function createTools(options) {
|
|
|
6213
6867
|
sessionId: options.sessionId
|
|
6214
6868
|
});
|
|
6215
6869
|
}
|
|
6870
|
+
if (process.platform === "darwin") {
|
|
6871
|
+
if (options.enableComputerUse) {
|
|
6872
|
+
tools.computer = createComputerUseTool({
|
|
6873
|
+
workingDirectory: options.workingDirectory,
|
|
6874
|
+
sessionId: options.sessionId,
|
|
6875
|
+
displayWidth: options.computerUseDisplayWidth,
|
|
6876
|
+
displayHeight: options.computerUseDisplayHeight
|
|
6877
|
+
});
|
|
6878
|
+
} else {
|
|
6879
|
+
tools.enable_computer_use = createEnableComputerUseTool({
|
|
6880
|
+
sessionId: options.sessionId
|
|
6881
|
+
});
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6216
6884
|
if (options.enableSemanticSearch !== false) {
|
|
6217
6885
|
try {
|
|
6218
6886
|
if (isVectorGatewayConfigured()) {
|
|
@@ -6229,6 +6897,7 @@ async function createTools(options) {
|
|
|
6229
6897
|
if (options.taskTools) {
|
|
6230
6898
|
tools.complete_task = createCompleteTaskTool(options.taskTools);
|
|
6231
6899
|
tools.task_failed = createTaskFailedTool(options.taskTools);
|
|
6900
|
+
tools.ask_question_to_user = createAskQuestionToUserTool(options.taskTools);
|
|
6232
6901
|
}
|
|
6233
6902
|
return tools;
|
|
6234
6903
|
}
|
|
@@ -6243,11 +6912,11 @@ init_db();
|
|
|
6243
6912
|
init_todo();
|
|
6244
6913
|
import os from "os";
|
|
6245
6914
|
function getSearchInstructions() {
|
|
6246
|
-
const
|
|
6915
|
+
const platform5 = process.platform;
|
|
6247
6916
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
6248
6917
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
6249
6918
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
6250
|
-
if (
|
|
6919
|
+
if (platform5 === "win32") {
|
|
6251
6920
|
return `${common}
|
|
6252
6921
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
6253
6922
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -6294,13 +6963,13 @@ async function buildSystemPrompt(options) {
|
|
|
6294
6963
|
);
|
|
6295
6964
|
const hasNoTodos = todos.length === 0;
|
|
6296
6965
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6297
|
-
const
|
|
6966
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6298
6967
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6299
6968
|
const searchInstructions = getSearchInstructions();
|
|
6300
6969
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
6301
6970
|
|
|
6302
6971
|
## Environment
|
|
6303
|
-
- **Platform**: ${
|
|
6972
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
6304
6973
|
- **Date**: ${currentDate}
|
|
6305
6974
|
- **Working Directory**: ${workingDirectory}
|
|
6306
6975
|
|
|
@@ -6636,9 +7305,10 @@ If you need to give the user a downloadable file (report, image, export, etc.),
|
|
|
6636
7305
|
### Rules
|
|
6637
7306
|
1. Work independently \u2014 no human will approve tool calls. All tools run without approval.
|
|
6638
7307
|
2. Keep working until the task is fully complete \u2014 and then VERIFY it is complete before finishing.
|
|
6639
|
-
3.
|
|
6640
|
-
4.
|
|
6641
|
-
5.
|
|
7308
|
+
3. If you are blocked by missing information, call \`ask_question_to_user\` with a concise question. The run will pause until the orchestrator or user answers, then you should continue from that answer.
|
|
7309
|
+
4. When done, call the \`complete_task\` tool with a JSON result matching the output schema below.
|
|
7310
|
+
5. If you determine the task is impossible or encounter an unrecoverable error, call the \`task_failed\` tool with a clear reason.
|
|
7311
|
+
6. Do NOT stop without calling \`complete_task\`, \`task_failed\`, or \`ask_question_to_user\` when blocked.
|
|
6642
7312
|
|
|
6643
7313
|
### Verification \u2014 BE EXTREMELY THOROUGH
|
|
6644
7314
|
Before calling \`complete_task\`, you MUST verify your work completely. Do not just assume it worked. Actually check.
|
|
@@ -6709,6 +7379,7 @@ ${JSON.stringify(outputSchema, null, 2)}
|
|
|
6709
7379
|
### Completion Tools
|
|
6710
7380
|
- **\`complete_task({ result: ... })\`** \u2014 Call ONLY after thorough verification. The result is validated against the schema above. If validation fails you will get errors back \u2014 fix and retry.
|
|
6711
7381
|
- **\`task_failed({ reason: "..." })\`** \u2014 Call only if the task truly cannot be completed.
|
|
7382
|
+
- **\`ask_question_to_user({ question, context?, choices? })\`** \u2014 Call only when you need information that is not available in the repo, task prompt, files, logs, or tools. Ask one clear question; after the answer is returned, continue working.
|
|
6712
7383
|
`;
|
|
6713
7384
|
}
|
|
6714
7385
|
function createSummaryPrompt(conversationHistory) {
|
|
@@ -6864,6 +7535,7 @@ function sanitizeModelMessages(messages) {
|
|
|
6864
7535
|
|
|
6865
7536
|
// src/agent/model-limits.ts
|
|
6866
7537
|
var MODEL_LIMITS = {
|
|
7538
|
+
"anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6867
7539
|
"anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6868
7540
|
"anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6869
7541
|
"anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
@@ -7200,17 +7872,65 @@ function repairToolPairing(messages) {
|
|
|
7200
7872
|
|
|
7201
7873
|
// src/agent/index.ts
|
|
7202
7874
|
init_webhook();
|
|
7875
|
+
|
|
7876
|
+
// src/tasks/questions.ts
|
|
7877
|
+
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
7878
|
+
var answeredQuestions = /* @__PURE__ */ new Map();
|
|
7879
|
+
function key(taskId, questionId) {
|
|
7880
|
+
return `${taskId}:${questionId}`;
|
|
7881
|
+
}
|
|
7882
|
+
function waitForTaskQuestionAnswer(question) {
|
|
7883
|
+
const k = key(question.taskId, question.questionId);
|
|
7884
|
+
if (pendingQuestions.has(k)) {
|
|
7885
|
+
return Promise.reject(new Error(`Question already pending: ${question.questionId}`));
|
|
7886
|
+
}
|
|
7887
|
+
return new Promise((resolve11, reject) => {
|
|
7888
|
+
pendingQuestions.set(k, {
|
|
7889
|
+
...question,
|
|
7890
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
7891
|
+
resolve: resolve11,
|
|
7892
|
+
reject
|
|
7893
|
+
});
|
|
7894
|
+
});
|
|
7895
|
+
}
|
|
7896
|
+
function answerTaskQuestion(taskId, questionId, answer) {
|
|
7897
|
+
const k = key(taskId, questionId);
|
|
7898
|
+
const pending = pendingQuestions.get(k);
|
|
7899
|
+
if (!pending) return answeredQuestions.has(k) ? "already_answered" : "not_found";
|
|
7900
|
+
pendingQuestions.delete(k);
|
|
7901
|
+
answeredQuestions.set(k, answer);
|
|
7902
|
+
pending.resolve(answer);
|
|
7903
|
+
return "answered";
|
|
7904
|
+
}
|
|
7905
|
+
function rejectTaskQuestions(taskId, reason) {
|
|
7906
|
+
for (const [k, pending] of pendingQuestions) {
|
|
7907
|
+
if (pending.taskId !== taskId) continue;
|
|
7908
|
+
pendingQuestions.delete(k);
|
|
7909
|
+
pending.reject(new Error(reason));
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
function getPendingTaskQuestion(taskId, questionId) {
|
|
7913
|
+
const pending = pendingQuestions.get(key(taskId, questionId));
|
|
7914
|
+
if (!pending) return null;
|
|
7915
|
+
const { resolve: _resolve, reject: _reject, ...question } = pending;
|
|
7916
|
+
return question;
|
|
7917
|
+
}
|
|
7918
|
+
function getAnsweredTaskQuestion(taskId, questionId) {
|
|
7919
|
+
return answeredQuestions.get(key(taskId, questionId)) ?? null;
|
|
7920
|
+
}
|
|
7921
|
+
|
|
7922
|
+
// src/agent/index.ts
|
|
7203
7923
|
var MAX_SSE_FIELD_LENGTH = 8 * 1024;
|
|
7204
7924
|
var SSE_PREVIEW_LENGTH = 2 * 1024;
|
|
7205
7925
|
function truncateWriteFileInput(input) {
|
|
7206
7926
|
const out = { ...input };
|
|
7207
|
-
for (const
|
|
7208
|
-
const val = out[
|
|
7927
|
+
for (const key2 of ["content", "old_string", "new_string"]) {
|
|
7928
|
+
const val = out[key2];
|
|
7209
7929
|
if (typeof val === "string" && val.length > MAX_SSE_FIELD_LENGTH) {
|
|
7210
|
-
out[
|
|
7930
|
+
out[key2] = `${val.slice(0, SSE_PREVIEW_LENGTH)}
|
|
7211
7931
|
... (truncated)`;
|
|
7212
|
-
out[`${
|
|
7213
|
-
out[`${
|
|
7932
|
+
out[`${key2}Truncated`] = true;
|
|
7933
|
+
out[`${key2}Length`] = val.length;
|
|
7214
7934
|
}
|
|
7215
7935
|
}
|
|
7216
7936
|
return out;
|
|
@@ -7238,10 +7958,14 @@ var Agent = class _Agent {
|
|
|
7238
7958
|
*/
|
|
7239
7959
|
async createToolsWithCallbacks(options) {
|
|
7240
7960
|
const config = getConfig();
|
|
7961
|
+
const sessionConfig = this.session.config || {};
|
|
7241
7962
|
return createTools({
|
|
7242
7963
|
sessionId: this.session.id,
|
|
7243
7964
|
workingDirectory: this.session.workingDirectory,
|
|
7244
7965
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7966
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7967
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7968
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
7245
7969
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
7246
7970
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
7247
7971
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -7274,10 +7998,14 @@ var Agent = class _Agent {
|
|
|
7274
7998
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
7275
7999
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
7276
8000
|
});
|
|
8001
|
+
const sessionConfig = session.config || {};
|
|
7277
8002
|
const tools = await createTools({
|
|
7278
8003
|
sessionId: session.id,
|
|
7279
8004
|
workingDirectory: session.workingDirectory,
|
|
7280
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
8005
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8006
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
8007
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
8008
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
7281
8009
|
});
|
|
7282
8010
|
return new _Agent(session, context, tools);
|
|
7283
8011
|
}
|
|
@@ -7372,13 +8100,7 @@ ${prompt}` });
|
|
|
7372
8100
|
abortSignal: options.abortSignal,
|
|
7373
8101
|
// Enable extended thinking/reasoning for models that support it
|
|
7374
8102
|
providerOptions: useAnthropic ? {
|
|
7375
|
-
anthropic: {
|
|
7376
|
-
toolStreaming: true,
|
|
7377
|
-
thinking: {
|
|
7378
|
-
type: "enabled",
|
|
7379
|
-
budgetTokens: 1e4
|
|
7380
|
-
}
|
|
7381
|
-
}
|
|
8103
|
+
anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
|
|
7382
8104
|
} : void 0,
|
|
7383
8105
|
onStepFinish: async (step) => {
|
|
7384
8106
|
options.onStepFinish?.(step);
|
|
@@ -7425,12 +8147,7 @@ ${prompt}` });
|
|
|
7425
8147
|
stopWhen: stepCountIs2(500),
|
|
7426
8148
|
// Enable extended thinking/reasoning for models that support it
|
|
7427
8149
|
providerOptions: useAnthropic ? {
|
|
7428
|
-
anthropic:
|
|
7429
|
-
thinking: {
|
|
7430
|
-
type: "enabled",
|
|
7431
|
-
budgetTokens: 1e4
|
|
7432
|
-
}
|
|
7433
|
-
}
|
|
8150
|
+
anthropic: getAnthropicProviderOptions(this.session.model)
|
|
7434
8151
|
} : void 0
|
|
7435
8152
|
});
|
|
7436
8153
|
const responseMessages = result.response.messages;
|
|
@@ -7450,10 +8167,10 @@ ${prompt}` });
|
|
|
7450
8167
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
7451
8168
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
7452
8169
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
7453
|
-
const fireWebhook = (
|
|
8170
|
+
const fireWebhook = (type2, data) => {
|
|
7454
8171
|
if (!webhookUrl) return;
|
|
7455
8172
|
sendWebhook(webhookUrl, {
|
|
7456
|
-
type,
|
|
8173
|
+
type: type2,
|
|
7457
8174
|
taskId: this.session.id,
|
|
7458
8175
|
sessionId: this.session.id,
|
|
7459
8176
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -7496,10 +8213,14 @@ ${prompt}` });
|
|
|
7496
8213
|
});
|
|
7497
8214
|
}
|
|
7498
8215
|
};
|
|
8216
|
+
const taskSessionConfig = this.session.config || {};
|
|
7499
8217
|
const taskTools = await createTools({
|
|
7500
8218
|
sessionId: this.session.id,
|
|
7501
8219
|
workingDirectory: this.session.workingDirectory,
|
|
7502
8220
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8221
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
8222
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
8223
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
7503
8224
|
onBashProgress: bashProgressHandler,
|
|
7504
8225
|
onWriteFileProgress: (progress) => {
|
|
7505
8226
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -7513,7 +8234,38 @@ ${prompt}` });
|
|
|
7513
8234
|
},
|
|
7514
8235
|
taskTools: {
|
|
7515
8236
|
outputSchema: options.taskConfig.outputSchema,
|
|
7516
|
-
onComplete
|
|
8237
|
+
onComplete,
|
|
8238
|
+
onQuestion: async (question) => {
|
|
8239
|
+
const payload = {
|
|
8240
|
+
questionId: question.questionId,
|
|
8241
|
+
question: question.question,
|
|
8242
|
+
context: question.context,
|
|
8243
|
+
choices: question.choices,
|
|
8244
|
+
status: "pending"
|
|
8245
|
+
};
|
|
8246
|
+
const answerPromise = waitForTaskQuestionAnswer({
|
|
8247
|
+
taskId: this.session.id,
|
|
8248
|
+
questionId: question.questionId,
|
|
8249
|
+
question: question.question,
|
|
8250
|
+
context: question.context,
|
|
8251
|
+
choices: question.choices
|
|
8252
|
+
});
|
|
8253
|
+
fireWebhook("task.question", payload);
|
|
8254
|
+
if (emit) {
|
|
8255
|
+
await emit(JSON.stringify({ type: "task-question", data: payload }));
|
|
8256
|
+
}
|
|
8257
|
+
const answer = await answerPromise;
|
|
8258
|
+
const answeredPayload = {
|
|
8259
|
+
questionId: question.questionId,
|
|
8260
|
+
answer: answer.answer,
|
|
8261
|
+
answeredBy: answer.answeredBy
|
|
8262
|
+
};
|
|
8263
|
+
fireWebhook("task.question_answered", answeredPayload);
|
|
8264
|
+
if (emit) {
|
|
8265
|
+
await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
|
|
8266
|
+
}
|
|
8267
|
+
return answer;
|
|
8268
|
+
}
|
|
7517
8269
|
}
|
|
7518
8270
|
});
|
|
7519
8271
|
const baseSystemPrompt = await buildSystemPrompt({
|
|
@@ -7558,10 +8310,7 @@ ${taskAddendum}`;
|
|
|
7558
8310
|
stopWhen: stepCountIs2(500),
|
|
7559
8311
|
abortSignal: options.abortSignal,
|
|
7560
8312
|
providerOptions: useAnthropic ? {
|
|
7561
|
-
anthropic: {
|
|
7562
|
-
toolStreaming: true,
|
|
7563
|
-
thinking: { type: "enabled", budgetTokens: 1e4 }
|
|
7564
|
-
}
|
|
8313
|
+
anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
|
|
7565
8314
|
} : void 0,
|
|
7566
8315
|
onStepFinish: async (step) => {
|
|
7567
8316
|
options.onStepFinish?.(step);
|
|
@@ -7782,11 +8531,11 @@ ${taskAddendum}`;
|
|
|
7782
8531
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7783
8532
|
if (!isRemoteConfigured2()) return [];
|
|
7784
8533
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
7785
|
-
const { join:
|
|
8534
|
+
const { join: join14, basename: basename6 } = await import("path");
|
|
7786
8535
|
const urls = [];
|
|
7787
8536
|
for (const filePath of filePaths) {
|
|
7788
8537
|
try {
|
|
7789
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
8538
|
+
const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
|
|
7790
8539
|
const fileName = basename6(fullPath);
|
|
7791
8540
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
7792
8541
|
const mimeMap = {
|
|
@@ -7844,11 +8593,11 @@ ${taskAddendum}`;
|
|
|
7844
8593
|
wrappedTools[name] = originalTool;
|
|
7845
8594
|
continue;
|
|
7846
8595
|
}
|
|
7847
|
-
wrappedTools[name] =
|
|
8596
|
+
wrappedTools[name] = tool14({
|
|
7848
8597
|
description: originalTool.description || "",
|
|
7849
|
-
inputSchema: originalTool.inputSchema ||
|
|
8598
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
7850
8599
|
execute: async (input, toolOptions) => {
|
|
7851
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
8600
|
+
const toolCallId = toolOptions.toolCallId || nanoid6();
|
|
7852
8601
|
const execution = toolExecutionQueries.create({
|
|
7853
8602
|
sessionId: this.session.id,
|
|
7854
8603
|
toolName: name,
|
|
@@ -7866,10 +8615,10 @@ ${taskAddendum}`;
|
|
|
7866
8615
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
7867
8616
|
approvalResolvers.delete(toolCallId);
|
|
7868
8617
|
this.pendingApprovals.delete(toolCallId);
|
|
7869
|
-
const
|
|
8618
|
+
const exec8 = await execution;
|
|
7870
8619
|
if (!approved) {
|
|
7871
8620
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
7872
|
-
await toolExecutionQueries.reject(
|
|
8621
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
7873
8622
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7874
8623
|
return {
|
|
7875
8624
|
status: "rejected",
|
|
@@ -7879,14 +8628,14 @@ ${taskAddendum}`;
|
|
|
7879
8628
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
7880
8629
|
};
|
|
7881
8630
|
}
|
|
7882
|
-
await toolExecutionQueries.approve(
|
|
8631
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
7883
8632
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7884
8633
|
try {
|
|
7885
8634
|
const result = await originalTool.execute(input, toolOptions);
|
|
7886
|
-
await toolExecutionQueries.complete(
|
|
8635
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
7887
8636
|
return result;
|
|
7888
8637
|
} catch (error) {
|
|
7889
|
-
await toolExecutionQueries.complete(
|
|
8638
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
7890
8639
|
throw error;
|
|
7891
8640
|
}
|
|
7892
8641
|
}
|
|
@@ -7957,12 +8706,12 @@ ${taskAddendum}`;
|
|
|
7957
8706
|
|
|
7958
8707
|
// src/server/index.ts
|
|
7959
8708
|
import "dotenv/config";
|
|
7960
|
-
import { Hono as
|
|
8709
|
+
import { Hono as Hono7 } from "hono";
|
|
7961
8710
|
import { serve } from "@hono/node-server";
|
|
7962
8711
|
import { cors } from "hono/cors";
|
|
7963
8712
|
import { logger } from "hono/logger";
|
|
7964
|
-
import { existsSync as
|
|
7965
|
-
import { resolve as resolve10, dirname as dirname7, join as
|
|
8713
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
8714
|
+
import { resolve as resolve10, dirname as dirname7, join as join13 } from "path";
|
|
7966
8715
|
import { spawn as spawn2 } from "child_process";
|
|
7967
8716
|
import { createServer as createNetServer } from "net";
|
|
7968
8717
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -7971,11 +8720,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
7971
8720
|
init_db();
|
|
7972
8721
|
import { Hono } from "hono";
|
|
7973
8722
|
import { zValidator } from "@hono/zod-validator";
|
|
7974
|
-
import { z as
|
|
7975
|
-
import { existsSync as
|
|
8723
|
+
import { z as z16 } from "zod";
|
|
8724
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
7976
8725
|
import { readdir as readdir6 } from "fs/promises";
|
|
7977
|
-
import { join as
|
|
7978
|
-
import { nanoid as
|
|
8726
|
+
import { join as join10, basename as basename5, extname as extname8, relative as relative9 } from "path";
|
|
8727
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
7979
8728
|
init_config();
|
|
7980
8729
|
|
|
7981
8730
|
// src/server/devtools-store.ts
|
|
@@ -8007,18 +8756,20 @@ function cleanupPendingInputs() {
|
|
|
8007
8756
|
}
|
|
8008
8757
|
}
|
|
8009
8758
|
}
|
|
8010
|
-
var createSessionSchema =
|
|
8011
|
-
name:
|
|
8012
|
-
workingDirectory:
|
|
8013
|
-
model:
|
|
8014
|
-
toolApprovals:
|
|
8759
|
+
var createSessionSchema = z16.object({
|
|
8760
|
+
name: z16.string().optional(),
|
|
8761
|
+
workingDirectory: z16.string().optional(),
|
|
8762
|
+
model: z16.string().optional(),
|
|
8763
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional(),
|
|
8764
|
+
// Optional full session-config passthrough (computerUseEnabled, etc.)
|
|
8765
|
+
config: z16.record(z16.string(), z16.unknown()).optional()
|
|
8015
8766
|
});
|
|
8016
|
-
var paginationQuerySchema =
|
|
8017
|
-
limit:
|
|
8018
|
-
offset:
|
|
8767
|
+
var paginationQuerySchema = z16.object({
|
|
8768
|
+
limit: z16.string().optional(),
|
|
8769
|
+
offset: z16.string().optional()
|
|
8019
8770
|
});
|
|
8020
|
-
var messagesQuerySchema =
|
|
8021
|
-
limit:
|
|
8771
|
+
var messagesQuerySchema = z16.object({
|
|
8772
|
+
limit: z16.string().optional()
|
|
8022
8773
|
});
|
|
8023
8774
|
sessions.get(
|
|
8024
8775
|
"/",
|
|
@@ -8056,11 +8807,20 @@ sessions.post(
|
|
|
8056
8807
|
async (c) => {
|
|
8057
8808
|
const body = c.req.valid("json");
|
|
8058
8809
|
const config = getConfig();
|
|
8810
|
+
const cuDefault = process.env.SPARKECODER_COMPUTER_USE === "1";
|
|
8811
|
+
const baseConfig = body.config || {};
|
|
8812
|
+
const mergedConfig = {
|
|
8813
|
+
...baseConfig,
|
|
8814
|
+
...body.toolApprovals ? { toolApprovals: body.toolApprovals } : {},
|
|
8815
|
+
// Turn on computer use by default if the server was launched with --enable-computer-use,
|
|
8816
|
+
// unless the client explicitly provided a value.
|
|
8817
|
+
...cuDefault && baseConfig.computerUseEnabled === void 0 ? { computerUseEnabled: true } : {}
|
|
8818
|
+
};
|
|
8059
8819
|
const agent = await Agent.create({
|
|
8060
8820
|
name: body.name,
|
|
8061
8821
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8062
8822
|
model: body.model || config.defaultModel,
|
|
8063
|
-
sessionConfig:
|
|
8823
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8064
8824
|
});
|
|
8065
8825
|
const session = agent.getSession();
|
|
8066
8826
|
return c.json({
|
|
@@ -8157,10 +8917,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8157
8917
|
count: executions.length
|
|
8158
8918
|
});
|
|
8159
8919
|
});
|
|
8160
|
-
var updateSessionSchema =
|
|
8161
|
-
model:
|
|
8162
|
-
name:
|
|
8163
|
-
toolApprovals:
|
|
8920
|
+
var updateSessionSchema = z16.object({
|
|
8921
|
+
model: z16.string().optional(),
|
|
8922
|
+
name: z16.string().optional(),
|
|
8923
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
|
|
8164
8924
|
});
|
|
8165
8925
|
sessions.patch(
|
|
8166
8926
|
"/:id",
|
|
@@ -8230,8 +8990,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8230
8990
|
await agent.clearContext();
|
|
8231
8991
|
return c.json({ success: true, sessionId: id });
|
|
8232
8992
|
});
|
|
8233
|
-
var pendingInputSchema =
|
|
8234
|
-
text:
|
|
8993
|
+
var pendingInputSchema = z16.object({
|
|
8994
|
+
text: z16.string()
|
|
8235
8995
|
});
|
|
8236
8996
|
sessions.post(
|
|
8237
8997
|
"/:id/pending-input",
|
|
@@ -8262,13 +9022,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
8262
9022
|
createdAt: pending.createdAt.toISOString()
|
|
8263
9023
|
});
|
|
8264
9024
|
});
|
|
8265
|
-
var devtoolsContextSchema =
|
|
8266
|
-
url:
|
|
8267
|
-
path:
|
|
8268
|
-
pageName:
|
|
8269
|
-
screenWidth:
|
|
8270
|
-
screenHeight:
|
|
8271
|
-
devicePixelRatio:
|
|
9025
|
+
var devtoolsContextSchema = z16.object({
|
|
9026
|
+
url: z16.string(),
|
|
9027
|
+
path: z16.string(),
|
|
9028
|
+
pageName: z16.string().optional(),
|
|
9029
|
+
screenWidth: z16.number().optional(),
|
|
9030
|
+
screenHeight: z16.number().optional(),
|
|
9031
|
+
devicePixelRatio: z16.number().optional()
|
|
8272
9032
|
});
|
|
8273
9033
|
sessions.post(
|
|
8274
9034
|
"/:id/devtools-context",
|
|
@@ -8454,12 +9214,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
8454
9214
|
});
|
|
8455
9215
|
function getAttachmentsDir(sessionId) {
|
|
8456
9216
|
const appDataDir = getAppDataDirectory();
|
|
8457
|
-
return
|
|
9217
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
8458
9218
|
}
|
|
8459
9219
|
function ensureAttachmentsDir(sessionId) {
|
|
8460
9220
|
const dir = getAttachmentsDir(sessionId);
|
|
8461
|
-
if (!
|
|
8462
|
-
|
|
9221
|
+
if (!existsSync16(dir)) {
|
|
9222
|
+
mkdirSync6(dir, { recursive: true });
|
|
8463
9223
|
}
|
|
8464
9224
|
return dir;
|
|
8465
9225
|
}
|
|
@@ -8470,12 +9230,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
8470
9230
|
return c.json({ error: "Session not found" }, 404);
|
|
8471
9231
|
}
|
|
8472
9232
|
const dir = getAttachmentsDir(sessionId);
|
|
8473
|
-
if (!
|
|
9233
|
+
if (!existsSync16(dir)) {
|
|
8474
9234
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
8475
9235
|
}
|
|
8476
9236
|
const files = readdirSync2(dir);
|
|
8477
9237
|
const attachments = files.map((filename) => {
|
|
8478
|
-
const filePath =
|
|
9238
|
+
const filePath = join10(dir, filename);
|
|
8479
9239
|
const stats = statSync2(filePath);
|
|
8480
9240
|
return {
|
|
8481
9241
|
id: filename.split("_")[0],
|
|
@@ -8507,10 +9267,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8507
9267
|
return c.json({ error: "No file provided" }, 400);
|
|
8508
9268
|
}
|
|
8509
9269
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8510
|
-
const id =
|
|
9270
|
+
const id = nanoid7(10);
|
|
8511
9271
|
const ext = extname8(file.name) || "";
|
|
8512
9272
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8513
|
-
const filePath =
|
|
9273
|
+
const filePath = join10(dir, safeFilename);
|
|
8514
9274
|
const arrayBuffer = await file.arrayBuffer();
|
|
8515
9275
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
8516
9276
|
return c.json({
|
|
@@ -8533,10 +9293,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8533
9293
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
8534
9294
|
}
|
|
8535
9295
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8536
|
-
const id =
|
|
9296
|
+
const id = nanoid7(10);
|
|
8537
9297
|
const ext = extname8(body.filename) || "";
|
|
8538
9298
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8539
|
-
const filePath =
|
|
9299
|
+
const filePath = join10(dir, safeFilename);
|
|
8540
9300
|
let base64Data = body.data;
|
|
8541
9301
|
if (base64Data.includes(",")) {
|
|
8542
9302
|
base64Data = base64Data.split(",")[1];
|
|
@@ -8565,7 +9325,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8565
9325
|
return c.json({ error: "Session not found" }, 404);
|
|
8566
9326
|
}
|
|
8567
9327
|
const dir = getAttachmentsDir(sessionId);
|
|
8568
|
-
if (!
|
|
9328
|
+
if (!existsSync16(dir)) {
|
|
8569
9329
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8570
9330
|
}
|
|
8571
9331
|
const files = readdirSync2(dir);
|
|
@@ -8573,14 +9333,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8573
9333
|
if (!file) {
|
|
8574
9334
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8575
9335
|
}
|
|
8576
|
-
const filePath =
|
|
8577
|
-
|
|
9336
|
+
const filePath = join10(dir, file);
|
|
9337
|
+
unlinkSync3(filePath);
|
|
8578
9338
|
return c.json({ success: true, id: attachmentId });
|
|
8579
9339
|
});
|
|
8580
|
-
var filesQuerySchema =
|
|
8581
|
-
query:
|
|
9340
|
+
var filesQuerySchema = z16.object({
|
|
9341
|
+
query: z16.string().optional(),
|
|
8582
9342
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
8583
|
-
limit:
|
|
9343
|
+
limit: z16.string().optional()
|
|
8584
9344
|
// Max results (default 50)
|
|
8585
9345
|
});
|
|
8586
9346
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -8656,7 +9416,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
8656
9416
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
8657
9417
|
for (const entry of entries) {
|
|
8658
9418
|
if (results.length >= limit * 2) break;
|
|
8659
|
-
const fullPath =
|
|
9419
|
+
const fullPath = join10(currentDir, entry.name);
|
|
8660
9420
|
const relativePath = relative9(baseDir, fullPath);
|
|
8661
9421
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
8662
9422
|
continue;
|
|
@@ -8704,7 +9464,7 @@ sessions.get(
|
|
|
8704
9464
|
return c.json({ error: "Session not found" }, 404);
|
|
8705
9465
|
}
|
|
8706
9466
|
const workingDirectory = session.workingDirectory;
|
|
8707
|
-
if (!
|
|
9467
|
+
if (!existsSync16(workingDirectory)) {
|
|
8708
9468
|
return c.json({
|
|
8709
9469
|
sessionId,
|
|
8710
9470
|
workingDirectory,
|
|
@@ -8814,9 +9574,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
8814
9574
|
init_db();
|
|
8815
9575
|
import { Hono as Hono2 } from "hono";
|
|
8816
9576
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
8817
|
-
import { z as
|
|
8818
|
-
import { existsSync as
|
|
8819
|
-
import { join as
|
|
9577
|
+
import { z as z17 } from "zod";
|
|
9578
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
9579
|
+
import { join as join11 } from "path";
|
|
8820
9580
|
init_config();
|
|
8821
9581
|
|
|
8822
9582
|
// src/server/resumable-stream.ts
|
|
@@ -8825,9 +9585,9 @@ var store = /* @__PURE__ */ new Map();
|
|
|
8825
9585
|
var channels = /* @__PURE__ */ new Map();
|
|
8826
9586
|
var cleanupInterval = setInterval(() => {
|
|
8827
9587
|
const now = Date.now();
|
|
8828
|
-
for (const [
|
|
9588
|
+
for (const [key2, data] of store.entries()) {
|
|
8829
9589
|
if (data.expiresAt && data.expiresAt < now) {
|
|
8830
|
-
store.delete(
|
|
9590
|
+
store.delete(key2);
|
|
8831
9591
|
}
|
|
8832
9592
|
}
|
|
8833
9593
|
}, 6e4);
|
|
@@ -8851,27 +9611,27 @@ var publisher = {
|
|
|
8851
9611
|
}
|
|
8852
9612
|
}
|
|
8853
9613
|
},
|
|
8854
|
-
set: async (
|
|
9614
|
+
set: async (key2, value, options) => {
|
|
8855
9615
|
const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
|
|
8856
|
-
store.set(
|
|
9616
|
+
store.set(key2, { value, expiresAt });
|
|
8857
9617
|
if (options?.EX) {
|
|
8858
|
-
setTimeout(() => store.delete(
|
|
9618
|
+
setTimeout(() => store.delete(key2), options.EX * 1e3);
|
|
8859
9619
|
}
|
|
8860
9620
|
},
|
|
8861
|
-
get: async (
|
|
8862
|
-
const data = store.get(
|
|
9621
|
+
get: async (key2) => {
|
|
9622
|
+
const data = store.get(key2);
|
|
8863
9623
|
if (!data) return null;
|
|
8864
9624
|
if (data.expiresAt && data.expiresAt < Date.now()) {
|
|
8865
|
-
store.delete(
|
|
9625
|
+
store.delete(key2);
|
|
8866
9626
|
return null;
|
|
8867
9627
|
}
|
|
8868
9628
|
return data.value;
|
|
8869
9629
|
},
|
|
8870
|
-
incr: async (
|
|
8871
|
-
const data = store.get(
|
|
9630
|
+
incr: async (key2) => {
|
|
9631
|
+
const data = store.get(key2);
|
|
8872
9632
|
const current = data ? parseInt(data.value, 10) : 0;
|
|
8873
9633
|
const next = (isNaN(current) ? 0 : current) + 1;
|
|
8874
|
-
store.set(
|
|
9634
|
+
store.set(key2, { value: String(next), expiresAt: data?.expiresAt });
|
|
8875
9635
|
return next;
|
|
8876
9636
|
}
|
|
8877
9637
|
};
|
|
@@ -8903,7 +9663,7 @@ var streamContext = createResumableStreamContext({
|
|
|
8903
9663
|
});
|
|
8904
9664
|
|
|
8905
9665
|
// src/server/routes/agents.ts
|
|
8906
|
-
import { nanoid as
|
|
9666
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
8907
9667
|
init_stream_proxy();
|
|
8908
9668
|
init_recorder();
|
|
8909
9669
|
init_remote();
|
|
@@ -8994,40 +9754,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
8994
9754
|
${prompt}`;
|
|
8995
9755
|
}
|
|
8996
9756
|
var agents = new Hono2();
|
|
8997
|
-
var attachmentSchema =
|
|
8998
|
-
type:
|
|
8999
|
-
data:
|
|
9757
|
+
var attachmentSchema = z17.object({
|
|
9758
|
+
type: z17.enum(["image", "file"]),
|
|
9759
|
+
data: z17.string(),
|
|
9000
9760
|
// base64 data URL or raw base64
|
|
9001
|
-
mediaType:
|
|
9002
|
-
filename:
|
|
9761
|
+
mediaType: z17.string().optional(),
|
|
9762
|
+
filename: z17.string().optional()
|
|
9003
9763
|
});
|
|
9004
|
-
var runPromptSchema =
|
|
9005
|
-
prompt:
|
|
9764
|
+
var runPromptSchema = z17.object({
|
|
9765
|
+
prompt: z17.string(),
|
|
9006
9766
|
// Can be empty if attachments are provided
|
|
9007
|
-
attachments:
|
|
9767
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
9008
9768
|
}).refine(
|
|
9009
9769
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
9010
9770
|
{ message: "Either prompt or attachments must be provided" }
|
|
9011
9771
|
);
|
|
9012
|
-
var quickStartSchema =
|
|
9013
|
-
prompt:
|
|
9014
|
-
name:
|
|
9015
|
-
workingDirectory:
|
|
9016
|
-
model:
|
|
9017
|
-
toolApprovals:
|
|
9772
|
+
var quickStartSchema = z17.object({
|
|
9773
|
+
prompt: z17.string().min(1),
|
|
9774
|
+
name: z17.string().optional(),
|
|
9775
|
+
workingDirectory: z17.string().optional(),
|
|
9776
|
+
model: z17.string().optional(),
|
|
9777
|
+
toolApprovals: z17.record(z17.string(), z17.boolean()).optional()
|
|
9018
9778
|
});
|
|
9019
|
-
var rejectSchema =
|
|
9020
|
-
reason:
|
|
9779
|
+
var rejectSchema = z17.object({
|
|
9780
|
+
reason: z17.string().optional()
|
|
9021
9781
|
}).optional();
|
|
9022
9782
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9023
9783
|
function getAttachmentsDirectory(sessionId) {
|
|
9024
9784
|
const appDataDir = getAppDataDirectory();
|
|
9025
|
-
return
|
|
9785
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9026
9786
|
}
|
|
9027
9787
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9028
9788
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9029
|
-
if (!
|
|
9030
|
-
|
|
9789
|
+
if (!existsSync17(attachmentsDir)) {
|
|
9790
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9031
9791
|
}
|
|
9032
9792
|
let filename = attachment.filename;
|
|
9033
9793
|
if (!filename) {
|
|
@@ -9045,7 +9805,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9045
9805
|
attachment.mediaType = resized.mediaType;
|
|
9046
9806
|
attachment.data = buffer.toString("base64");
|
|
9047
9807
|
}
|
|
9048
|
-
const filePath =
|
|
9808
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9049
9809
|
writeFileSync4(filePath, buffer);
|
|
9050
9810
|
return filePath;
|
|
9051
9811
|
}
|
|
@@ -9056,9 +9816,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9056
9816
|
}
|
|
9057
9817
|
return data;
|
|
9058
9818
|
}
|
|
9059
|
-
function getExtensionFromMediaType(mediaType,
|
|
9819
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9060
9820
|
if (!mediaType) {
|
|
9061
|
-
return
|
|
9821
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9062
9822
|
}
|
|
9063
9823
|
const mimeToExt = {
|
|
9064
9824
|
"image/png": ".png",
|
|
@@ -9462,7 +10222,7 @@ ${prompt}` });
|
|
|
9462
10222
|
userMessageContent = prompt;
|
|
9463
10223
|
}
|
|
9464
10224
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
9465
|
-
const streamId = `stream_${id}_${
|
|
10225
|
+
const streamId = `stream_${id}_${nanoid8(10)}`;
|
|
9466
10226
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
9467
10227
|
await activeStreamQueries.create(id, streamId);
|
|
9468
10228
|
const stream = await streamContext.resumableStream(
|
|
@@ -9667,7 +10427,7 @@ agents.post(
|
|
|
9667
10427
|
});
|
|
9668
10428
|
const session = agent.getSession();
|
|
9669
10429
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
9670
|
-
const streamId = `stream_${session.id}_${
|
|
10430
|
+
const streamId = `stream_${session.id}_${nanoid8(10)}`;
|
|
9671
10431
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
9672
10432
|
await activeStreamQueries.create(session.id, streamId);
|
|
9673
10433
|
const createQuickStreamProducer = () => {
|
|
@@ -9934,23 +10694,23 @@ agents.post(
|
|
|
9934
10694
|
});
|
|
9935
10695
|
}
|
|
9936
10696
|
);
|
|
9937
|
-
var browserInputSchema =
|
|
9938
|
-
type:
|
|
9939
|
-
eventType:
|
|
9940
|
-
x:
|
|
9941
|
-
y:
|
|
9942
|
-
button:
|
|
9943
|
-
clickCount:
|
|
9944
|
-
deltaX:
|
|
9945
|
-
deltaY:
|
|
9946
|
-
key:
|
|
9947
|
-
code:
|
|
9948
|
-
text:
|
|
9949
|
-
modifiers:
|
|
9950
|
-
touchPoints:
|
|
9951
|
-
x:
|
|
9952
|
-
y:
|
|
9953
|
-
id:
|
|
10697
|
+
var browserInputSchema = z17.object({
|
|
10698
|
+
type: z17.enum(["input_mouse", "input_keyboard", "input_touch"]),
|
|
10699
|
+
eventType: z17.string(),
|
|
10700
|
+
x: z17.number().optional(),
|
|
10701
|
+
y: z17.number().optional(),
|
|
10702
|
+
button: z17.string().optional(),
|
|
10703
|
+
clickCount: z17.number().optional(),
|
|
10704
|
+
deltaX: z17.number().optional(),
|
|
10705
|
+
deltaY: z17.number().optional(),
|
|
10706
|
+
key: z17.string().optional(),
|
|
10707
|
+
code: z17.string().optional(),
|
|
10708
|
+
text: z17.string().optional(),
|
|
10709
|
+
modifiers: z17.number().optional(),
|
|
10710
|
+
touchPoints: z17.array(z17.object({
|
|
10711
|
+
x: z17.number(),
|
|
10712
|
+
y: z17.number(),
|
|
10713
|
+
id: z17.number().optional()
|
|
9954
10714
|
})).optional()
|
|
9955
10715
|
});
|
|
9956
10716
|
agents.post(
|
|
@@ -9985,27 +10745,279 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
9985
10745
|
init_config();
|
|
9986
10746
|
import { Hono as Hono3 } from "hono";
|
|
9987
10747
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
9988
|
-
import { z as
|
|
9989
|
-
import { readFileSync as
|
|
10748
|
+
import { z as z18 } from "zod";
|
|
10749
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
9990
10750
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9991
|
-
import { dirname as dirname6, join as
|
|
10751
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10752
|
+
|
|
10753
|
+
// src/personal-agent/heartbeat.ts
|
|
10754
|
+
import { execSync as execSync3 } from "child_process";
|
|
10755
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10756
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
10757
|
+
|
|
10758
|
+
// src/personal-agent/system-metrics.ts
|
|
10759
|
+
import { execSync as execSync2 } from "child_process";
|
|
10760
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
10761
|
+
import {
|
|
10762
|
+
arch,
|
|
10763
|
+
cpus,
|
|
10764
|
+
freemem,
|
|
10765
|
+
hostname,
|
|
10766
|
+
loadavg,
|
|
10767
|
+
networkInterfaces,
|
|
10768
|
+
platform as platform2,
|
|
10769
|
+
release,
|
|
10770
|
+
totalmem,
|
|
10771
|
+
type,
|
|
10772
|
+
uptime,
|
|
10773
|
+
userInfo
|
|
10774
|
+
} from "os";
|
|
10775
|
+
var _lastSample = null;
|
|
10776
|
+
function snapshotCpuTimes() {
|
|
10777
|
+
const sum = { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
|
|
10778
|
+
for (const c of cpus()) {
|
|
10779
|
+
sum.user += c.times.user;
|
|
10780
|
+
sum.nice += c.times.nice;
|
|
10781
|
+
sum.sys += c.times.sys;
|
|
10782
|
+
sum.idle += c.times.idle;
|
|
10783
|
+
sum.irq += c.times.irq;
|
|
10784
|
+
}
|
|
10785
|
+
return sum;
|
|
10786
|
+
}
|
|
10787
|
+
function readCpuUsage() {
|
|
10788
|
+
const now = snapshotCpuTimes();
|
|
10789
|
+
if (!_lastSample) {
|
|
10790
|
+
_lastSample = now;
|
|
10791
|
+
return 0;
|
|
10792
|
+
}
|
|
10793
|
+
const dUser = now.user - _lastSample.user;
|
|
10794
|
+
const dNice = now.nice - _lastSample.nice;
|
|
10795
|
+
const dSys = now.sys - _lastSample.sys;
|
|
10796
|
+
const dIdle = now.idle - _lastSample.idle;
|
|
10797
|
+
const dIrq = now.irq - _lastSample.irq;
|
|
10798
|
+
const total = dUser + dNice + dSys + dIdle + dIrq;
|
|
10799
|
+
_lastSample = now;
|
|
10800
|
+
if (total <= 0) return 0;
|
|
10801
|
+
return Math.max(0, Math.min(1, (total - dIdle) / total));
|
|
10802
|
+
}
|
|
10803
|
+
snapshotCpuTimes();
|
|
10804
|
+
function readCpuTempC() {
|
|
10805
|
+
try {
|
|
10806
|
+
if (platform2() === "linux") {
|
|
10807
|
+
let hottest = -Infinity;
|
|
10808
|
+
try {
|
|
10809
|
+
for (const entry of readdirSync3("/sys/class/thermal")) {
|
|
10810
|
+
if (!entry.startsWith("thermal_zone")) continue;
|
|
10811
|
+
try {
|
|
10812
|
+
const v = Number(
|
|
10813
|
+
readFileSync8(`/sys/class/thermal/${entry}/temp`, "utf8").trim()
|
|
10814
|
+
);
|
|
10815
|
+
if (Number.isFinite(v) && v > hottest) hottest = v;
|
|
10816
|
+
} catch {
|
|
10817
|
+
}
|
|
10818
|
+
}
|
|
10819
|
+
} catch {
|
|
10820
|
+
}
|
|
10821
|
+
if (hottest > -Infinity) return hottest / 1e3;
|
|
10822
|
+
}
|
|
10823
|
+
const overrideCmd = process.env.PERSONAL_AGENT_TEMP_CMD;
|
|
10824
|
+
if (overrideCmd) {
|
|
10825
|
+
const out = execSync2(overrideCmd, { encoding: "utf8", timeout: 1500 }).trim();
|
|
10826
|
+
const v = Number(out);
|
|
10827
|
+
if (Number.isFinite(v)) return v;
|
|
10828
|
+
}
|
|
10829
|
+
} catch {
|
|
10830
|
+
}
|
|
10831
|
+
return void 0;
|
|
10832
|
+
}
|
|
10833
|
+
function readDisks() {
|
|
10834
|
+
try {
|
|
10835
|
+
const p = platform2();
|
|
10836
|
+
if (p === "darwin" || p === "linux") {
|
|
10837
|
+
const raw = execSync2("df -kP", { encoding: "utf8", timeout: 2e3 });
|
|
10838
|
+
const lines = raw.trim().split("\n").slice(1);
|
|
10839
|
+
const out = [];
|
|
10840
|
+
for (const line of lines) {
|
|
10841
|
+
const parts = line.trim().split(/\s+/);
|
|
10842
|
+
if (parts.length < 6) continue;
|
|
10843
|
+
const filesystem = parts[0];
|
|
10844
|
+
const total1k = Number(parts[1]);
|
|
10845
|
+
const used1k = Number(parts[2]);
|
|
10846
|
+
const free1k = Number(parts[3]);
|
|
10847
|
+
const mount = parts.slice(5).join(" ");
|
|
10848
|
+
if (!Number.isFinite(total1k) || total1k <= 0) continue;
|
|
10849
|
+
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")) {
|
|
10850
|
+
if (mount !== "/") continue;
|
|
10851
|
+
}
|
|
10852
|
+
out.push({
|
|
10853
|
+
mount,
|
|
10854
|
+
filesystem,
|
|
10855
|
+
totalBytes: total1k * 1024,
|
|
10856
|
+
usedBytes: used1k * 1024,
|
|
10857
|
+
freeBytes: free1k * 1024,
|
|
10858
|
+
usage: total1k > 0 ? used1k / total1k : 0
|
|
10859
|
+
});
|
|
10860
|
+
}
|
|
10861
|
+
out.sort((a, b) => {
|
|
10862
|
+
const score = (m) => m === "/" ? 0 : m.startsWith("/Users") || m.startsWith("/home") ? 1 : 2;
|
|
10863
|
+
return score(a.mount) - score(b.mount);
|
|
10864
|
+
});
|
|
10865
|
+
return out.slice(0, 6);
|
|
10866
|
+
}
|
|
10867
|
+
if (p === "win32") {
|
|
10868
|
+
const raw = execSync2(
|
|
10869
|
+
"wmic logicaldisk get DeviceID,Size,FreeSpace /format:csv",
|
|
10870
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
10871
|
+
);
|
|
10872
|
+
const out = [];
|
|
10873
|
+
for (const line of raw.trim().split(/\r?\n/).slice(1)) {
|
|
10874
|
+
const cols = line.split(",");
|
|
10875
|
+
if (cols.length < 4) continue;
|
|
10876
|
+
const [, deviceId, freeStr, sizeStr] = cols;
|
|
10877
|
+
const total = Number(sizeStr);
|
|
10878
|
+
const free = Number(freeStr);
|
|
10879
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
10880
|
+
const used = Math.max(0, total - free);
|
|
10881
|
+
out.push({
|
|
10882
|
+
mount: deviceId,
|
|
10883
|
+
totalBytes: total,
|
|
10884
|
+
usedBytes: used,
|
|
10885
|
+
freeBytes: free,
|
|
10886
|
+
usage: used / total
|
|
10887
|
+
});
|
|
10888
|
+
}
|
|
10889
|
+
return out;
|
|
10890
|
+
}
|
|
10891
|
+
} catch {
|
|
10892
|
+
}
|
|
10893
|
+
return void 0;
|
|
10894
|
+
}
|
|
10895
|
+
function readNetwork() {
|
|
10896
|
+
try {
|
|
10897
|
+
const out = [];
|
|
10898
|
+
const ifaces = networkInterfaces();
|
|
10899
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
10900
|
+
if (!addrs) continue;
|
|
10901
|
+
for (const a of addrs) {
|
|
10902
|
+
if (a.internal) continue;
|
|
10903
|
+
out.push({
|
|
10904
|
+
iface: name,
|
|
10905
|
+
family: a.family,
|
|
10906
|
+
address: a.address,
|
|
10907
|
+
mac: a.mac,
|
|
10908
|
+
internal: a.internal
|
|
10909
|
+
});
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
return out;
|
|
10913
|
+
} catch {
|
|
10914
|
+
return void 0;
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
function readSystemMetrics() {
|
|
10918
|
+
const cpuList = cpus();
|
|
10919
|
+
const usage = readCpuUsage();
|
|
10920
|
+
const tot = totalmem();
|
|
10921
|
+
const free = freemem();
|
|
10922
|
+
const metrics = {
|
|
10923
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10924
|
+
hostname: hostname(),
|
|
10925
|
+
platform: platform2(),
|
|
10926
|
+
arch: arch(),
|
|
10927
|
+
kernelRelease: release(),
|
|
10928
|
+
osType: type(),
|
|
10929
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
10930
|
+
systemUptimeSec: Math.round(uptime()),
|
|
10931
|
+
user: safeUser(),
|
|
10932
|
+
cpu: cpuList[0] ? {
|
|
10933
|
+
model: cpuList[0].model,
|
|
10934
|
+
count: cpuList.length,
|
|
10935
|
+
speedMhz: cpuList[0].speed,
|
|
10936
|
+
loadAvg1: loadavg()[0],
|
|
10937
|
+
loadAvg5: loadavg()[1],
|
|
10938
|
+
loadAvg15: loadavg()[2],
|
|
10939
|
+
usage,
|
|
10940
|
+
tempC: readCpuTempC()
|
|
10941
|
+
} : void 0,
|
|
10942
|
+
memory: {
|
|
10943
|
+
totalBytes: tot,
|
|
10944
|
+
freeBytes: free,
|
|
10945
|
+
usedBytes: Math.max(0, tot - free),
|
|
10946
|
+
usage: tot > 0 ? (tot - free) / tot : 0
|
|
10947
|
+
},
|
|
10948
|
+
disks: readDisks(),
|
|
10949
|
+
network: readNetwork()
|
|
10950
|
+
};
|
|
10951
|
+
return metrics;
|
|
10952
|
+
}
|
|
10953
|
+
function safeUser() {
|
|
10954
|
+
try {
|
|
10955
|
+
return userInfo().username;
|
|
10956
|
+
} catch {
|
|
10957
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
|
|
10961
|
+
// src/personal-agent/heartbeat.ts
|
|
10962
|
+
var _cachedHwid = null;
|
|
10963
|
+
function getHardwareIdCached() {
|
|
10964
|
+
if (_cachedHwid !== null) return _cachedHwid;
|
|
10965
|
+
_cachedHwid = getHardwareId();
|
|
10966
|
+
return _cachedHwid;
|
|
10967
|
+
}
|
|
10968
|
+
function getHardwareId() {
|
|
10969
|
+
const p = platform3();
|
|
10970
|
+
try {
|
|
10971
|
+
if (p === "darwin") {
|
|
10972
|
+
const out = execSync3(
|
|
10973
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/ {print $4}'`,
|
|
10974
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
10975
|
+
).trim();
|
|
10976
|
+
if (out) return normalize2(out);
|
|
10977
|
+
} else if (p === "linux") {
|
|
10978
|
+
try {
|
|
10979
|
+
return normalize2(readFileSync9("/etc/machine-id", "utf8").trim());
|
|
10980
|
+
} catch {
|
|
10981
|
+
return normalize2(readFileSync9("/var/lib/dbus/machine-id", "utf8").trim());
|
|
10982
|
+
}
|
|
10983
|
+
} else if (p === "win32") {
|
|
10984
|
+
const out = execSync3("wmic csproduct get uuid /value", {
|
|
10985
|
+
encoding: "utf8",
|
|
10986
|
+
timeout: 3e3
|
|
10987
|
+
});
|
|
10988
|
+
const m = out.match(/UUID=([\w-]+)/i);
|
|
10989
|
+
if (m && m[1]) return normalize2(m[1]);
|
|
10990
|
+
}
|
|
10991
|
+
} catch (e) {
|
|
10992
|
+
console.warn(`[personal-agent] could not read hardware UUID: ${e.message}`);
|
|
10993
|
+
}
|
|
10994
|
+
console.warn(
|
|
10995
|
+
"[personal-agent] falling back to hostname for HWID; this is NOT stable across rename/reinstall"
|
|
10996
|
+
);
|
|
10997
|
+
return `host-${hostname2()}`;
|
|
10998
|
+
}
|
|
10999
|
+
function normalize2(raw) {
|
|
11000
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
11001
|
+
}
|
|
11002
|
+
|
|
11003
|
+
// src/server/routes/health.ts
|
|
9992
11004
|
var __filename = fileURLToPath3(import.meta.url);
|
|
9993
11005
|
var __dirname = dirname6(__filename);
|
|
9994
11006
|
var possiblePaths = [
|
|
9995
|
-
|
|
11007
|
+
join12(__dirname, "../package.json"),
|
|
9996
11008
|
// From dist/server -> dist/../package.json
|
|
9997
|
-
|
|
11009
|
+
join12(__dirname, "../../package.json"),
|
|
9998
11010
|
// From dist/server (if nested differently)
|
|
9999
|
-
|
|
11011
|
+
join12(__dirname, "../../../package.json"),
|
|
10000
11012
|
// From src/server/routes (development)
|
|
10001
|
-
|
|
11013
|
+
join12(process.cwd(), "package.json")
|
|
10002
11014
|
// From current working directory
|
|
10003
11015
|
];
|
|
10004
11016
|
var currentVersion = "0.0.0";
|
|
10005
11017
|
var packageName = "sparkecoder";
|
|
10006
11018
|
for (const packageJsonPath of possiblePaths) {
|
|
10007
11019
|
try {
|
|
10008
|
-
const packageJson = JSON.parse(
|
|
11020
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
10009
11021
|
if (packageJson.name === "sparkecoder") {
|
|
10010
11022
|
currentVersion = packageJson.version || "0.0.0";
|
|
10011
11023
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10019,12 +11031,20 @@ health.get("/", async (c) => {
|
|
|
10019
11031
|
const config = getConfig();
|
|
10020
11032
|
const apiKeyStatus = getApiKeyStatus();
|
|
10021
11033
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10022
|
-
const
|
|
11034
|
+
const remoteInference = isRemoteInferenceConfigured();
|
|
11035
|
+
const hasApiKey = remoteInference || (gatewayKey?.configured ?? false);
|
|
11036
|
+
let hwid;
|
|
11037
|
+
try {
|
|
11038
|
+
hwid = getHardwareIdCached();
|
|
11039
|
+
} catch {
|
|
11040
|
+
}
|
|
10023
11041
|
return c.json({
|
|
10024
11042
|
status: "ok",
|
|
10025
11043
|
version: currentVersion,
|
|
10026
11044
|
uptime: process.uptime(),
|
|
10027
11045
|
apiKeyConfigured: hasApiKey,
|
|
11046
|
+
inferenceMode: remoteInference ? "remote" : "local",
|
|
11047
|
+
hwid,
|
|
10028
11048
|
config: {
|
|
10029
11049
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10030
11050
|
defaultModel: config.defaultModel,
|
|
@@ -10095,9 +11115,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10095
11115
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10096
11116
|
});
|
|
10097
11117
|
});
|
|
10098
|
-
var setApiKeySchema =
|
|
10099
|
-
provider:
|
|
10100
|
-
apiKey:
|
|
11118
|
+
var setApiKeySchema = z18.object({
|
|
11119
|
+
provider: z18.string(),
|
|
11120
|
+
apiKey: z18.string().min(1)
|
|
10101
11121
|
});
|
|
10102
11122
|
health.post(
|
|
10103
11123
|
"/api-keys",
|
|
@@ -10136,13 +11156,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10136
11156
|
// src/server/routes/terminals.ts
|
|
10137
11157
|
import { Hono as Hono4 } from "hono";
|
|
10138
11158
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10139
|
-
import { z as
|
|
11159
|
+
import { z as z19 } from "zod";
|
|
10140
11160
|
init_db();
|
|
10141
11161
|
var terminals = new Hono4();
|
|
10142
|
-
var spawnSchema =
|
|
10143
|
-
command:
|
|
10144
|
-
cwd:
|
|
10145
|
-
name:
|
|
11162
|
+
var spawnSchema = z19.object({
|
|
11163
|
+
command: z19.string(),
|
|
11164
|
+
cwd: z19.string().optional(),
|
|
11165
|
+
name: z19.string().optional()
|
|
10146
11166
|
});
|
|
10147
11167
|
terminals.post(
|
|
10148
11168
|
"/:sessionId/terminals",
|
|
@@ -10223,8 +11243,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10223
11243
|
// We don't track exit codes in tmux mode
|
|
10224
11244
|
});
|
|
10225
11245
|
});
|
|
10226
|
-
var logsQuerySchema =
|
|
10227
|
-
tail:
|
|
11246
|
+
var logsQuerySchema = z19.object({
|
|
11247
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10228
11248
|
});
|
|
10229
11249
|
terminals.get(
|
|
10230
11250
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -10248,8 +11268,8 @@ terminals.get(
|
|
|
10248
11268
|
});
|
|
10249
11269
|
}
|
|
10250
11270
|
);
|
|
10251
|
-
var killSchema =
|
|
10252
|
-
signal:
|
|
11271
|
+
var killSchema = z19.object({
|
|
11272
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
10253
11273
|
});
|
|
10254
11274
|
terminals.post(
|
|
10255
11275
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -10263,8 +11283,8 @@ terminals.post(
|
|
|
10263
11283
|
return c.json({ success: true, message: "Terminal killed" });
|
|
10264
11284
|
}
|
|
10265
11285
|
);
|
|
10266
|
-
var writeSchema =
|
|
10267
|
-
input:
|
|
11286
|
+
var writeSchema = z19.object({
|
|
11287
|
+
input: z19.string()
|
|
10268
11288
|
});
|
|
10269
11289
|
terminals.post(
|
|
10270
11290
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -10449,20 +11469,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
10449
11469
|
init_db();
|
|
10450
11470
|
import { Hono as Hono5 } from "hono";
|
|
10451
11471
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
10452
|
-
import { z as
|
|
10453
|
-
import { nanoid as
|
|
11472
|
+
import { z as z20 } from "zod";
|
|
11473
|
+
import { nanoid as nanoid9 } from "nanoid";
|
|
10454
11474
|
init_config();
|
|
10455
11475
|
var tasks = new Hono5();
|
|
10456
11476
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
10457
|
-
var createTaskSchema =
|
|
10458
|
-
prompt:
|
|
10459
|
-
outputSchema:
|
|
10460
|
-
webhookUrl:
|
|
10461
|
-
model:
|
|
10462
|
-
workingDirectory:
|
|
10463
|
-
name:
|
|
10464
|
-
maxIterations:
|
|
10465
|
-
parentTaskId:
|
|
11477
|
+
var createTaskSchema = z20.object({
|
|
11478
|
+
prompt: z20.string().min(1),
|
|
11479
|
+
outputSchema: z20.record(z20.string(), z20.unknown()),
|
|
11480
|
+
webhookUrl: z20.string().url().optional(),
|
|
11481
|
+
model: z20.string().optional(),
|
|
11482
|
+
workingDirectory: z20.string().optional(),
|
|
11483
|
+
name: z20.string().optional(),
|
|
11484
|
+
maxIterations: z20.number().int().min(1).max(500).optional(),
|
|
11485
|
+
parentTaskId: z20.string().optional()
|
|
10466
11486
|
});
|
|
10467
11487
|
tasks.post(
|
|
10468
11488
|
"/",
|
|
@@ -10524,7 +11544,7 @@ tasks.post(
|
|
|
10524
11544
|
const taskId = agent.sessionId;
|
|
10525
11545
|
const abortController = new AbortController();
|
|
10526
11546
|
taskAbortControllers.set(taskId, abortController);
|
|
10527
|
-
const streamId = `stream_${taskId}_${
|
|
11547
|
+
const streamId = `stream_${taskId}_${nanoid9(10)}`;
|
|
10528
11548
|
await activeStreamQueries.create(taskId, streamId);
|
|
10529
11549
|
const taskStreamProducer = () => {
|
|
10530
11550
|
const { readable, writable } = new TransformStream();
|
|
@@ -10651,6 +11671,7 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10651
11671
|
abortController.abort();
|
|
10652
11672
|
taskAbortControllers.delete(id);
|
|
10653
11673
|
}
|
|
11674
|
+
rejectTaskQuestions(id, "Task cancelled by user");
|
|
10654
11675
|
const cancelledTask = {
|
|
10655
11676
|
...task,
|
|
10656
11677
|
status: "failed",
|
|
@@ -10672,19 +11693,629 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10672
11693
|
}
|
|
10673
11694
|
return c.json({ taskId: id, status: "failed", error: "Task cancelled by user" });
|
|
10674
11695
|
});
|
|
11696
|
+
var answerQuestionSchema = z20.object({
|
|
11697
|
+
answer: z20.string().min(1),
|
|
11698
|
+
answeredBy: z20.enum(["orchestrator", "user", "system"]).optional()
|
|
11699
|
+
});
|
|
11700
|
+
tasks.post(
|
|
11701
|
+
"/:id/questions/:questionId/answer",
|
|
11702
|
+
zValidator5("json", answerQuestionSchema),
|
|
11703
|
+
async (c) => {
|
|
11704
|
+
const id = c.req.param("id");
|
|
11705
|
+
const questionId = c.req.param("questionId");
|
|
11706
|
+
const body = c.req.valid("json");
|
|
11707
|
+
const session = await sessionQueries.getById(id);
|
|
11708
|
+
if (!session) {
|
|
11709
|
+
return c.json({ error: "Task not found" }, 404);
|
|
11710
|
+
}
|
|
11711
|
+
const task = session.config?.task;
|
|
11712
|
+
if (!task?.enabled) {
|
|
11713
|
+
return c.json({ error: "Session is not a task" }, 400);
|
|
11714
|
+
}
|
|
11715
|
+
const pending = getPendingTaskQuestion(id, questionId);
|
|
11716
|
+
if (!pending) {
|
|
11717
|
+
if (getAnsweredTaskQuestion(id, questionId)) {
|
|
11718
|
+
return c.json({
|
|
11719
|
+
taskId: id,
|
|
11720
|
+
questionId,
|
|
11721
|
+
status: "answered",
|
|
11722
|
+
alreadyAnswered: true
|
|
11723
|
+
});
|
|
11724
|
+
}
|
|
11725
|
+
return c.json({ error: "Question is not pending" }, 404);
|
|
11726
|
+
}
|
|
11727
|
+
const answerStatus = answerTaskQuestion(id, questionId, {
|
|
11728
|
+
answer: body.answer,
|
|
11729
|
+
answeredBy: body.answeredBy
|
|
11730
|
+
});
|
|
11731
|
+
if (answerStatus === "not_found") {
|
|
11732
|
+
return c.json({ error: "Question is not pending" }, 404);
|
|
11733
|
+
}
|
|
11734
|
+
return c.json({
|
|
11735
|
+
taskId: id,
|
|
11736
|
+
questionId,
|
|
11737
|
+
status: "answered",
|
|
11738
|
+
alreadyAnswered: answerStatus === "already_answered"
|
|
11739
|
+
});
|
|
11740
|
+
}
|
|
11741
|
+
);
|
|
10675
11742
|
var tasks_default = tasks;
|
|
10676
11743
|
|
|
11744
|
+
// src/server/routes/system.ts
|
|
11745
|
+
import { Hono as Hono6 } from "hono";
|
|
11746
|
+
import { streamSSE } from "hono/streaming";
|
|
11747
|
+
var system = new Hono6();
|
|
11748
|
+
system.get("/metrics", (c) => {
|
|
11749
|
+
return c.json(readSystemMetrics());
|
|
11750
|
+
});
|
|
11751
|
+
system.get("/metrics/stream", (c) => {
|
|
11752
|
+
return streamSSE(c, async (stream) => {
|
|
11753
|
+
const intervalMs = Number(c.req.query("intervalMs") ?? 2e3);
|
|
11754
|
+
const safeMs = Number.isFinite(intervalMs) ? Math.max(500, Math.min(6e4, intervalMs)) : 2e3;
|
|
11755
|
+
let id = 0;
|
|
11756
|
+
let aborted = false;
|
|
11757
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
11758
|
+
aborted = true;
|
|
11759
|
+
});
|
|
11760
|
+
while (!aborted) {
|
|
11761
|
+
try {
|
|
11762
|
+
const snap = readSystemMetrics();
|
|
11763
|
+
await stream.writeSSE({
|
|
11764
|
+
id: String(id++),
|
|
11765
|
+
event: "metrics",
|
|
11766
|
+
data: JSON.stringify(snap)
|
|
11767
|
+
});
|
|
11768
|
+
} catch (e) {
|
|
11769
|
+
await stream.writeSSE({
|
|
11770
|
+
id: String(id++),
|
|
11771
|
+
event: "error",
|
|
11772
|
+
data: JSON.stringify({ error: e.message })
|
|
11773
|
+
});
|
|
11774
|
+
}
|
|
11775
|
+
await stream.sleep(safeMs);
|
|
11776
|
+
}
|
|
11777
|
+
});
|
|
11778
|
+
});
|
|
11779
|
+
|
|
11780
|
+
// src/personal-agent/hwid-middleware.ts
|
|
11781
|
+
var SKIP_PATHS = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11782
|
+
function personalAgentConfigured() {
|
|
11783
|
+
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);
|
|
11784
|
+
}
|
|
11785
|
+
function hwidMiddleware() {
|
|
11786
|
+
const enabled = personalAgentConfigured();
|
|
11787
|
+
let warnedMissing = false;
|
|
11788
|
+
return async (c, next) => {
|
|
11789
|
+
if (!enabled) return next();
|
|
11790
|
+
const path = c.req.path;
|
|
11791
|
+
if (SKIP_PATHS.has(path) || path.startsWith("/health/api-keys")) {
|
|
11792
|
+
return next();
|
|
11793
|
+
}
|
|
11794
|
+
const got = c.req.header("x-device-hwid");
|
|
11795
|
+
if (!got) {
|
|
11796
|
+
if (!warnedMissing) {
|
|
11797
|
+
warnedMissing = true;
|
|
11798
|
+
console.warn(
|
|
11799
|
+
`[personal-agent] request to ${path} arrived without X-Device-Hwid; allowing (backwards compat)`
|
|
11800
|
+
);
|
|
11801
|
+
}
|
|
11802
|
+
return next();
|
|
11803
|
+
}
|
|
11804
|
+
const expected = getHardwareIdCached();
|
|
11805
|
+
if (got !== expected) {
|
|
11806
|
+
console.warn(
|
|
11807
|
+
`[personal-agent] HWID mismatch on ${path}: got=${got.slice(0, 12)}\u2026, expected=${expected.slice(0, 12)}\u2026`
|
|
11808
|
+
);
|
|
11809
|
+
return c.json(
|
|
11810
|
+
{
|
|
11811
|
+
error: "hwid mismatch",
|
|
11812
|
+
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.",
|
|
11813
|
+
expected: expected.slice(0, 12) + "\u2026",
|
|
11814
|
+
got: got.slice(0, 12) + "\u2026"
|
|
11815
|
+
},
|
|
11816
|
+
409
|
|
11817
|
+
);
|
|
11818
|
+
}
|
|
11819
|
+
return next();
|
|
11820
|
+
};
|
|
11821
|
+
}
|
|
11822
|
+
|
|
11823
|
+
// src/personal-agent/signature-verify.ts
|
|
11824
|
+
import { createHash as createHash3, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
11825
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
11826
|
+
var REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
11827
|
+
var _cachedKey = null;
|
|
11828
|
+
var _cachedFromInput = null;
|
|
11829
|
+
function loadPublicKey(input) {
|
|
11830
|
+
if (_cachedFromInput === input && _cachedKey) return _cachedKey;
|
|
11831
|
+
let pem = input;
|
|
11832
|
+
if (!input.includes("BEGIN") && existsSync18(input)) {
|
|
11833
|
+
pem = readFileSync11(input, "utf8");
|
|
11834
|
+
}
|
|
11835
|
+
const key2 = createPublicKey({ key: pem, format: "pem" });
|
|
11836
|
+
if (key2.asymmetricKeyType !== "ed25519") {
|
|
11837
|
+
throw new Error(
|
|
11838
|
+
`expected an ed25519 public key, got ${key2.asymmetricKeyType}. Generate with personal-agents/scripts/generate-signing-keys.mjs.`
|
|
11839
|
+
);
|
|
11840
|
+
}
|
|
11841
|
+
_cachedKey = key2;
|
|
11842
|
+
_cachedFromInput = input;
|
|
11843
|
+
return key2;
|
|
11844
|
+
}
|
|
11845
|
+
function bodyHashB64(body) {
|
|
11846
|
+
const hash = createHash3("sha256");
|
|
11847
|
+
if (body == null || body === "") {
|
|
11848
|
+
} else if (typeof body === "string") {
|
|
11849
|
+
hash.update(body, "utf8");
|
|
11850
|
+
} else if (Buffer.isBuffer(body)) {
|
|
11851
|
+
hash.update(body);
|
|
11852
|
+
} else {
|
|
11853
|
+
hash.update(Buffer.from(body));
|
|
11854
|
+
}
|
|
11855
|
+
return hash.digest("base64");
|
|
11856
|
+
}
|
|
11857
|
+
function canonicalSigningString(args) {
|
|
11858
|
+
return [
|
|
11859
|
+
args.method.toUpperCase(),
|
|
11860
|
+
args.path,
|
|
11861
|
+
String(args.timestamp),
|
|
11862
|
+
args.bodyHashB64
|
|
11863
|
+
].join("\n");
|
|
11864
|
+
}
|
|
11865
|
+
function fromBase64Url(s) {
|
|
11866
|
+
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
11867
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
11868
|
+
return Buffer.from(std, "base64");
|
|
11869
|
+
}
|
|
11870
|
+
function verifyEmbedToken(args) {
|
|
11871
|
+
const dot = args.token.indexOf(".");
|
|
11872
|
+
if (dot < 1 || dot >= args.token.length - 1) {
|
|
11873
|
+
return { ok: false, reason: "malformed" };
|
|
11874
|
+
}
|
|
11875
|
+
const payloadB64 = args.token.slice(0, dot);
|
|
11876
|
+
const sigB64 = args.token.slice(dot + 1);
|
|
11877
|
+
let sigBuf;
|
|
11878
|
+
try {
|
|
11879
|
+
sigBuf = fromBase64Url(sigB64);
|
|
11880
|
+
} catch {
|
|
11881
|
+
return { ok: false, reason: "bad-encoding" };
|
|
11882
|
+
}
|
|
11883
|
+
const sigOk = cryptoVerify(
|
|
11884
|
+
null,
|
|
11885
|
+
Buffer.from(payloadB64, "utf8"),
|
|
11886
|
+
args.publicKey,
|
|
11887
|
+
sigBuf
|
|
11888
|
+
);
|
|
11889
|
+
if (!sigOk) return { ok: false, reason: "signature-mismatch" };
|
|
11890
|
+
let payload;
|
|
11891
|
+
try {
|
|
11892
|
+
const json = JSON.parse(fromBase64Url(payloadB64).toString("utf8"));
|
|
11893
|
+
if (!json || typeof json !== "object" || typeof json.sid !== "string" || typeof json.exp !== "number") {
|
|
11894
|
+
return { ok: false, reason: "bad-payload" };
|
|
11895
|
+
}
|
|
11896
|
+
payload = { sid: json.sid, exp: json.exp };
|
|
11897
|
+
} catch (e) {
|
|
11898
|
+
return { ok: false, reason: "bad-payload", detail: e.message };
|
|
11899
|
+
}
|
|
11900
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11901
|
+
if (payload.exp < now) {
|
|
11902
|
+
return {
|
|
11903
|
+
ok: false,
|
|
11904
|
+
reason: "expired",
|
|
11905
|
+
detail: `${now - payload.exp}s past expiry`
|
|
11906
|
+
};
|
|
11907
|
+
}
|
|
11908
|
+
return { ok: true, payload };
|
|
11909
|
+
}
|
|
11910
|
+
function verifyRequest(args) {
|
|
11911
|
+
if (!args.signatureB64 || !args.timestampSeconds || !args.algorithm) {
|
|
11912
|
+
return { ok: false, reason: "missing-headers" };
|
|
11913
|
+
}
|
|
11914
|
+
if (args.algorithm.toLowerCase() !== "ed25519") {
|
|
11915
|
+
return { ok: false, reason: "bad-algorithm", detail: args.algorithm };
|
|
11916
|
+
}
|
|
11917
|
+
const ts = Number(args.timestampSeconds);
|
|
11918
|
+
if (!Number.isFinite(ts)) {
|
|
11919
|
+
return { ok: false, reason: "bad-timestamp", detail: args.timestampSeconds };
|
|
11920
|
+
}
|
|
11921
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11922
|
+
if (Math.abs(now - ts) > REPLAY_WINDOW_SECONDS) {
|
|
11923
|
+
return {
|
|
11924
|
+
ok: false,
|
|
11925
|
+
reason: "stale-timestamp",
|
|
11926
|
+
detail: `${Math.abs(now - ts)}s outside the ${REPLAY_WINDOW_SECONDS}s window`
|
|
11927
|
+
};
|
|
11928
|
+
}
|
|
11929
|
+
let sigBuf;
|
|
11930
|
+
try {
|
|
11931
|
+
sigBuf = Buffer.from(args.signatureB64, "base64");
|
|
11932
|
+
} catch {
|
|
11933
|
+
return { ok: false, reason: "bad-signature-encoding" };
|
|
11934
|
+
}
|
|
11935
|
+
const canonical = canonicalSigningString({
|
|
11936
|
+
method: args.method,
|
|
11937
|
+
path: args.path,
|
|
11938
|
+
timestamp: ts,
|
|
11939
|
+
bodyHashB64: bodyHashB64(args.body)
|
|
11940
|
+
});
|
|
11941
|
+
const ok = cryptoVerify(null, Buffer.from(canonical, "utf8"), args.publicKey, sigBuf);
|
|
11942
|
+
return ok ? { ok: true } : { ok: false, reason: "signature-mismatch" };
|
|
11943
|
+
}
|
|
11944
|
+
|
|
11945
|
+
// src/personal-agent/signature-middleware.ts
|
|
11946
|
+
var SKIP_PATHS2 = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11947
|
+
function isSkipped(path) {
|
|
11948
|
+
return SKIP_PATHS2.has(path) || path.startsWith("/health/api-keys");
|
|
11949
|
+
}
|
|
11950
|
+
function pathBindsSessionId(path, sid) {
|
|
11951
|
+
const cleanPath = path.split("?")[0];
|
|
11952
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
11953
|
+
return segments.includes(sid);
|
|
11954
|
+
}
|
|
11955
|
+
function signatureMiddleware(opts = {}) {
|
|
11956
|
+
const acceptSignedOnly = opts.acceptSignedOnly ?? process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
11957
|
+
const publicKeyInput = opts.publicKeyInput ?? process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
11958
|
+
if (acceptSignedOnly && !publicKeyInput) {
|
|
11959
|
+
return async (c) => c.json(
|
|
11960
|
+
{
|
|
11961
|
+
error: "signature middleware misconfigured",
|
|
11962
|
+
message: "started with --accept-signed-only but no --personal-agent-public-key / PERSONAL_AGENT_PUBLIC_KEY supplied. Refusing all non-/health traffic."
|
|
11963
|
+
},
|
|
11964
|
+
500
|
|
11965
|
+
);
|
|
11966
|
+
}
|
|
11967
|
+
if (!publicKeyInput) {
|
|
11968
|
+
return async (_c, next) => next();
|
|
11969
|
+
}
|
|
11970
|
+
const publicKey = loadPublicKey(publicKeyInput);
|
|
11971
|
+
let warnedUnsigned = false;
|
|
11972
|
+
return async (c, next) => {
|
|
11973
|
+
const path = c.req.path;
|
|
11974
|
+
if (isSkipped(path)) return next();
|
|
11975
|
+
const sig = c.req.header("x-signature");
|
|
11976
|
+
const ts = c.req.header("x-signature-timestamp");
|
|
11977
|
+
const alg = c.req.header("x-signature-algorithm");
|
|
11978
|
+
if (!sig && (c.req.method === "GET" || c.req.method === "HEAD")) {
|
|
11979
|
+
const embedTok = c.req.header("x-embed-token") ?? c.req.query("embed_token");
|
|
11980
|
+
if (embedTok) {
|
|
11981
|
+
const result2 = verifyEmbedToken({ publicKey, token: embedTok });
|
|
11982
|
+
if (!result2.ok) {
|
|
11983
|
+
return c.json(
|
|
11984
|
+
{
|
|
11985
|
+
error: "embed token verification failed",
|
|
11986
|
+
reason: result2.reason,
|
|
11987
|
+
detail: result2.detail
|
|
11988
|
+
},
|
|
11989
|
+
401
|
|
11990
|
+
);
|
|
11991
|
+
}
|
|
11992
|
+
if (!pathBindsSessionId(path, result2.payload.sid)) {
|
|
11993
|
+
return c.json(
|
|
11994
|
+
{
|
|
11995
|
+
error: "embed token scoped to a different session",
|
|
11996
|
+
detail: `token sid=${result2.payload.sid} but request path=${path}`
|
|
11997
|
+
},
|
|
11998
|
+
403
|
|
11999
|
+
);
|
|
12000
|
+
}
|
|
12001
|
+
return next();
|
|
12002
|
+
}
|
|
12003
|
+
}
|
|
12004
|
+
if (!sig) {
|
|
12005
|
+
if (acceptSignedOnly) {
|
|
12006
|
+
return c.json(
|
|
12007
|
+
{
|
|
12008
|
+
error: "signature required",
|
|
12009
|
+
message: "this sparkecoder is started with --accept-signed-only; every request must carry X-Signature, X-Signature-Timestamp, X-Signature-Algorithm."
|
|
12010
|
+
},
|
|
12011
|
+
401
|
|
12012
|
+
);
|
|
12013
|
+
}
|
|
12014
|
+
if (!warnedUnsigned) {
|
|
12015
|
+
warnedUnsigned = true;
|
|
12016
|
+
console.warn(
|
|
12017
|
+
`[personal-agent] inbound ${c.req.method} ${path} arrived without X-Signature; allowed because --accept-signed-only is off`
|
|
12018
|
+
);
|
|
12019
|
+
}
|
|
12020
|
+
return next();
|
|
12021
|
+
}
|
|
12022
|
+
let body;
|
|
12023
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
12024
|
+
body = Buffer.from(await c.req.raw.clone().arrayBuffer());
|
|
12025
|
+
}
|
|
12026
|
+
const result = verifyRequest({
|
|
12027
|
+
publicKey,
|
|
12028
|
+
method: c.req.method,
|
|
12029
|
+
path,
|
|
12030
|
+
body,
|
|
12031
|
+
signatureB64: sig,
|
|
12032
|
+
timestampSeconds: ts,
|
|
12033
|
+
algorithm: alg
|
|
12034
|
+
});
|
|
12035
|
+
if (!result.ok) {
|
|
12036
|
+
console.warn(
|
|
12037
|
+
`[personal-agent] signature verification failed on ${c.req.method} ${path}: ${result.reason}${result.detail ? ` (${result.detail})` : ""}`
|
|
12038
|
+
);
|
|
12039
|
+
return c.json(
|
|
12040
|
+
{
|
|
12041
|
+
error: "signature verification failed",
|
|
12042
|
+
reason: result.reason,
|
|
12043
|
+
detail: result.detail
|
|
12044
|
+
},
|
|
12045
|
+
401
|
|
12046
|
+
);
|
|
12047
|
+
}
|
|
12048
|
+
return next();
|
|
12049
|
+
};
|
|
12050
|
+
}
|
|
12051
|
+
|
|
12052
|
+
// src/personal-agent/pty-server.ts
|
|
12053
|
+
import { hostname as hostname3 } from "os";
|
|
12054
|
+
import { WebSocketServer } from "ws";
|
|
12055
|
+
var RESIZE_RE = /\x1b\[RESIZE:(\d+);(\d+)\]/;
|
|
12056
|
+
var _ptyMod = null;
|
|
12057
|
+
async function loadPty() {
|
|
12058
|
+
if (_ptyMod) return _ptyMod;
|
|
12059
|
+
try {
|
|
12060
|
+
const mod = await import("node-pty");
|
|
12061
|
+
_ptyMod = mod;
|
|
12062
|
+
return mod;
|
|
12063
|
+
} catch (e) {
|
|
12064
|
+
console.warn(
|
|
12065
|
+
`[personal-agent] node-pty failed to load; /pty WebSocket disabled. (${e.message})`
|
|
12066
|
+
);
|
|
12067
|
+
return null;
|
|
12068
|
+
}
|
|
12069
|
+
}
|
|
12070
|
+
function defaultShell() {
|
|
12071
|
+
if (process.platform === "win32") {
|
|
12072
|
+
return { file: process.env.COMSPEC || "cmd.exe", args: [] };
|
|
12073
|
+
}
|
|
12074
|
+
return { file: process.env.SHELL || "/bin/zsh", args: ["-l"] };
|
|
12075
|
+
}
|
|
12076
|
+
function cleanEnv() {
|
|
12077
|
+
const env = {};
|
|
12078
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
12079
|
+
if (typeof v === "string") env[k] = v;
|
|
12080
|
+
}
|
|
12081
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
12082
|
+
if (!env.LANG) env.LANG = "en_US.UTF-8";
|
|
12083
|
+
return env;
|
|
12084
|
+
}
|
|
12085
|
+
function parseUpgrade(req) {
|
|
12086
|
+
const url = new URL(req.url || "/", "http://placeholder");
|
|
12087
|
+
const cols = clampInt(url.searchParams.get("cols"), 80, 10, 500);
|
|
12088
|
+
const rows = clampInt(url.searchParams.get("rows"), 24, 5, 500);
|
|
12089
|
+
const cwd = url.searchParams.get("cwd") || void 0;
|
|
12090
|
+
const shell = url.searchParams.get("shell") || void 0;
|
|
12091
|
+
return {
|
|
12092
|
+
cols,
|
|
12093
|
+
rows,
|
|
12094
|
+
cwd,
|
|
12095
|
+
shell,
|
|
12096
|
+
hwidHeader: headerStr(req, "x-device-hwid") ?? url.searchParams.get("hwid") ?? void 0,
|
|
12097
|
+
sigHeader: headerStr(req, "x-signature") ?? url.searchParams.get("sig") ?? void 0,
|
|
12098
|
+
tsHeader: headerStr(req, "x-signature-timestamp") ?? url.searchParams.get("ts") ?? void 0,
|
|
12099
|
+
algHeader: headerStr(req, "x-signature-algorithm") ?? url.searchParams.get("alg") ?? void 0
|
|
12100
|
+
};
|
|
12101
|
+
}
|
|
12102
|
+
function clampInt(v, dflt, lo, hi) {
|
|
12103
|
+
if (!v) return dflt;
|
|
12104
|
+
const n = parseInt(v, 10);
|
|
12105
|
+
if (!Number.isFinite(n)) return dflt;
|
|
12106
|
+
return Math.max(lo, Math.min(hi, n));
|
|
12107
|
+
}
|
|
12108
|
+
function headerStr(req, name) {
|
|
12109
|
+
const v = req.headers[name];
|
|
12110
|
+
if (Array.isArray(v)) return v[0];
|
|
12111
|
+
return v;
|
|
12112
|
+
}
|
|
12113
|
+
function authenticate(parsed, path, pubKey, signedOnly) {
|
|
12114
|
+
if (parsed.hwidHeader) {
|
|
12115
|
+
const expected = getHardwareIdCached();
|
|
12116
|
+
if (parsed.hwidHeader !== expected) {
|
|
12117
|
+
return {
|
|
12118
|
+
ok: false,
|
|
12119
|
+
status: 409,
|
|
12120
|
+
reason: `hwid mismatch: got ${parsed.hwidHeader.slice(0, 12)}\u2026, expected ${expected.slice(0, 12)}\u2026`
|
|
12121
|
+
};
|
|
12122
|
+
}
|
|
12123
|
+
}
|
|
12124
|
+
if (!pubKey) {
|
|
12125
|
+
if (signedOnly) {
|
|
12126
|
+
return { ok: false, status: 500, reason: "signature required but no public key configured" };
|
|
12127
|
+
}
|
|
12128
|
+
return { ok: true };
|
|
12129
|
+
}
|
|
12130
|
+
if (!parsed.sigHeader) {
|
|
12131
|
+
if (signedOnly) {
|
|
12132
|
+
return { ok: false, status: 401, reason: "missing X-Signature on upgrade request" };
|
|
12133
|
+
}
|
|
12134
|
+
return { ok: true };
|
|
12135
|
+
}
|
|
12136
|
+
const result = verifyRequest({
|
|
12137
|
+
publicKey: pubKey,
|
|
12138
|
+
method: "GET",
|
|
12139
|
+
path,
|
|
12140
|
+
body: void 0,
|
|
12141
|
+
signatureB64: parsed.sigHeader,
|
|
12142
|
+
timestampSeconds: parsed.tsHeader,
|
|
12143
|
+
algorithm: parsed.algHeader
|
|
12144
|
+
});
|
|
12145
|
+
if (!result.ok) {
|
|
12146
|
+
return { ok: false, status: 401, reason: `signature verification failed: ${result.reason}` };
|
|
12147
|
+
}
|
|
12148
|
+
return { ok: true };
|
|
12149
|
+
}
|
|
12150
|
+
function rejectUpgrade(socket, status, reason) {
|
|
12151
|
+
const body = JSON.stringify({ error: reason });
|
|
12152
|
+
socket.write(
|
|
12153
|
+
`HTTP/1.1 ${status} ${reasonText(status)}\r
|
|
12154
|
+
Content-Type: application/json\r
|
|
12155
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
12156
|
+
Connection: close\r
|
|
12157
|
+
\r
|
|
12158
|
+
` + body
|
|
12159
|
+
);
|
|
12160
|
+
socket.destroy();
|
|
12161
|
+
}
|
|
12162
|
+
function reasonText(status) {
|
|
12163
|
+
switch (status) {
|
|
12164
|
+
case 401:
|
|
12165
|
+
return "Unauthorized";
|
|
12166
|
+
case 409:
|
|
12167
|
+
return "Conflict";
|
|
12168
|
+
case 500:
|
|
12169
|
+
return "Internal Server Error";
|
|
12170
|
+
case 503:
|
|
12171
|
+
return "Service Unavailable";
|
|
12172
|
+
default:
|
|
12173
|
+
return "Error";
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
12176
|
+
function attachPtyServer(httpServer, opts = {}) {
|
|
12177
|
+
const path = opts.path ?? "/pty";
|
|
12178
|
+
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
12179
|
+
const acceptSignedOnly = process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
12180
|
+
const publicKeyInput = process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
12181
|
+
let pubKey = null;
|
|
12182
|
+
if (publicKeyInput) {
|
|
12183
|
+
try {
|
|
12184
|
+
pubKey = loadPublicKey(publicKeyInput);
|
|
12185
|
+
} catch (e) {
|
|
12186
|
+
console.warn(
|
|
12187
|
+
`[personal-agent] /pty signature verification disabled \u2014 failed to load public key: ${e.message}`
|
|
12188
|
+
);
|
|
12189
|
+
}
|
|
12190
|
+
}
|
|
12191
|
+
const handler = (req, socket, head) => {
|
|
12192
|
+
let pathname = "/";
|
|
12193
|
+
try {
|
|
12194
|
+
pathname = new URL(req.url || "/", "http://placeholder").pathname;
|
|
12195
|
+
} catch {
|
|
12196
|
+
}
|
|
12197
|
+
if (pathname !== path) return;
|
|
12198
|
+
const parsed = parseUpgrade(req);
|
|
12199
|
+
const auth = authenticate(parsed, pathname, pubKey, acceptSignedOnly);
|
|
12200
|
+
if (!auth.ok) {
|
|
12201
|
+
console.warn(`[personal-agent] rejecting /pty upgrade: ${auth.reason}`);
|
|
12202
|
+
rejectUpgrade(socket, auth.status ?? 401, auth.reason ?? "unauthorized");
|
|
12203
|
+
return;
|
|
12204
|
+
}
|
|
12205
|
+
void loadPty().then((pty) => {
|
|
12206
|
+
if (!pty) {
|
|
12207
|
+
rejectUpgrade(
|
|
12208
|
+
socket,
|
|
12209
|
+
503,
|
|
12210
|
+
"node-pty is not available on this device (failed to load native module)"
|
|
12211
|
+
);
|
|
12212
|
+
return;
|
|
12213
|
+
}
|
|
12214
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
12215
|
+
spawnPty(ws, pty, parsed, opts);
|
|
12216
|
+
});
|
|
12217
|
+
});
|
|
12218
|
+
};
|
|
12219
|
+
httpServer.on("upgrade", handler);
|
|
12220
|
+
if (!opts.quiet) {
|
|
12221
|
+
console.log(
|
|
12222
|
+
`[personal-agent] WebSocket PTY server attached at ${path} (host: ${hostname3()}, sigCheck: ${pubKey ? "on" : "off"})`
|
|
12223
|
+
);
|
|
12224
|
+
}
|
|
12225
|
+
return {
|
|
12226
|
+
close: () => {
|
|
12227
|
+
httpServer.off("upgrade", handler);
|
|
12228
|
+
wss.close();
|
|
12229
|
+
}
|
|
12230
|
+
};
|
|
12231
|
+
}
|
|
12232
|
+
function spawnPty(ws, pty, parsed, opts) {
|
|
12233
|
+
const { file, args } = (() => {
|
|
12234
|
+
if (parsed.shell || opts.shell) {
|
|
12235
|
+
const s = parsed.shell ?? opts.shell;
|
|
12236
|
+
return { file: s, args: process.platform === "win32" ? [] : ["-l"] };
|
|
12237
|
+
}
|
|
12238
|
+
return defaultShell();
|
|
12239
|
+
})();
|
|
12240
|
+
const cwd = parsed.cwd ?? opts.cwd ?? process.env.HOME ?? process.cwd();
|
|
12241
|
+
let proc;
|
|
12242
|
+
try {
|
|
12243
|
+
proc = pty.spawn(file, args, {
|
|
12244
|
+
name: "xterm-256color",
|
|
12245
|
+
cols: parsed.cols,
|
|
12246
|
+
rows: parsed.rows,
|
|
12247
|
+
cwd,
|
|
12248
|
+
env: cleanEnv()
|
|
12249
|
+
});
|
|
12250
|
+
} catch (e) {
|
|
12251
|
+
const msg = e.message;
|
|
12252
|
+
safeSend(ws, `\r
|
|
12253
|
+
\x1B[31mFailed to spawn shell: ${msg}\x1B[0m\r
|
|
12254
|
+
`);
|
|
12255
|
+
try {
|
|
12256
|
+
ws.close();
|
|
12257
|
+
} catch {
|
|
12258
|
+
}
|
|
12259
|
+
return;
|
|
12260
|
+
}
|
|
12261
|
+
const banner = `\x1B[90m[sparkecoder pty] ${file} on ${hostname3()} pid=${proc.pid} ${parsed.cols}x${parsed.rows}\x1B[0m\r
|
|
12262
|
+
`;
|
|
12263
|
+
safeSend(ws, banner);
|
|
12264
|
+
proc.onData((data) => safeSend(ws, data));
|
|
12265
|
+
proc.onExit(({ exitCode }) => {
|
|
12266
|
+
safeSend(ws, `\r
|
|
12267
|
+
\x1B[90m[exit ${exitCode}]\x1B[0m\r
|
|
12268
|
+
`);
|
|
12269
|
+
try {
|
|
12270
|
+
ws.close();
|
|
12271
|
+
} catch {
|
|
12272
|
+
}
|
|
12273
|
+
});
|
|
12274
|
+
ws.on("message", (msg, isBinary) => {
|
|
12275
|
+
const input = typeof msg === "string" ? msg : Buffer.isBuffer(msg) ? msg.toString(isBinary ? "utf8" : "utf8") : Array.isArray(msg) ? Buffer.concat(msg).toString("utf8") : "";
|
|
12276
|
+
if (!input) return;
|
|
12277
|
+
const m = input.match(RESIZE_RE);
|
|
12278
|
+
if (m) {
|
|
12279
|
+
const cols = clampInt(m[1], 80, 10, 500);
|
|
12280
|
+
const rows = clampInt(m[2], 24, 5, 500);
|
|
12281
|
+
try {
|
|
12282
|
+
proc.resize(cols, rows);
|
|
12283
|
+
} catch {
|
|
12284
|
+
}
|
|
12285
|
+
if (input.replace(RESIZE_RE, "").length === 0) return;
|
|
12286
|
+
proc.write(input.replace(RESIZE_RE, ""));
|
|
12287
|
+
return;
|
|
12288
|
+
}
|
|
12289
|
+
proc.write(input);
|
|
12290
|
+
});
|
|
12291
|
+
const onClose = () => {
|
|
12292
|
+
try {
|
|
12293
|
+
proc.kill();
|
|
12294
|
+
} catch {
|
|
12295
|
+
}
|
|
12296
|
+
};
|
|
12297
|
+
ws.on("close", onClose);
|
|
12298
|
+
ws.on("error", onClose);
|
|
12299
|
+
}
|
|
12300
|
+
function safeSend(ws, data) {
|
|
12301
|
+
if (ws.readyState !== 1) return;
|
|
12302
|
+
try {
|
|
12303
|
+
ws.send(data);
|
|
12304
|
+
} catch {
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12307
|
+
|
|
10677
12308
|
// src/server/index.ts
|
|
10678
12309
|
init_config();
|
|
10679
12310
|
init_db();
|
|
10680
12311
|
|
|
10681
12312
|
// src/utils/dependencies.ts
|
|
10682
|
-
import { exec as
|
|
10683
|
-
import { promisify as
|
|
10684
|
-
import { platform as
|
|
10685
|
-
var
|
|
12313
|
+
import { exec as exec7 } from "child_process";
|
|
12314
|
+
import { promisify as promisify7 } from "util";
|
|
12315
|
+
import { platform as platform4 } from "os";
|
|
12316
|
+
var execAsync7 = promisify7(exec7);
|
|
10686
12317
|
function getInstallInstructions() {
|
|
10687
|
-
const os2 =
|
|
12318
|
+
const os2 = platform4();
|
|
10688
12319
|
if (os2 === "darwin") {
|
|
10689
12320
|
return `
|
|
10690
12321
|
Install tmux on macOS:
|
|
@@ -10715,7 +12346,7 @@ Install tmux:
|
|
|
10715
12346
|
}
|
|
10716
12347
|
async function checkTmux() {
|
|
10717
12348
|
try {
|
|
10718
|
-
const { stdout } = await
|
|
12349
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
10719
12350
|
const version = stdout.trim();
|
|
10720
12351
|
return {
|
|
10721
12352
|
available: true,
|
|
@@ -10764,11 +12395,11 @@ function getWebDirectory() {
|
|
|
10764
12395
|
try {
|
|
10765
12396
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
10766
12397
|
const webDir = resolve10(currentDir, "..", "web");
|
|
10767
|
-
if (
|
|
12398
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
10768
12399
|
return webDir;
|
|
10769
12400
|
}
|
|
10770
12401
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
10771
|
-
if (
|
|
12402
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
10772
12403
|
return altWebDir;
|
|
10773
12404
|
}
|
|
10774
12405
|
return null;
|
|
@@ -10826,23 +12457,23 @@ async function findWebPort(preferredPort) {
|
|
|
10826
12457
|
return { port: preferredPort, alreadyRunning: false };
|
|
10827
12458
|
}
|
|
10828
12459
|
function hasProductionBuild(webDir) {
|
|
10829
|
-
const buildIdPath =
|
|
10830
|
-
return
|
|
12460
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
12461
|
+
return existsSync19(buildIdPath);
|
|
10831
12462
|
}
|
|
10832
12463
|
function hasSourceFiles(webDir) {
|
|
10833
|
-
const appDir =
|
|
10834
|
-
const pagesDir =
|
|
10835
|
-
const rootAppDir =
|
|
10836
|
-
const rootPagesDir =
|
|
10837
|
-
return
|
|
12464
|
+
const appDir = join13(webDir, "src", "app");
|
|
12465
|
+
const pagesDir = join13(webDir, "src", "pages");
|
|
12466
|
+
const rootAppDir = join13(webDir, "app");
|
|
12467
|
+
const rootPagesDir = join13(webDir, "pages");
|
|
12468
|
+
return existsSync19(appDir) || existsSync19(pagesDir) || existsSync19(rootAppDir) || existsSync19(rootPagesDir);
|
|
10838
12469
|
}
|
|
10839
12470
|
function getStandaloneServerPath(webDir) {
|
|
10840
12471
|
const possiblePaths2 = [
|
|
10841
|
-
|
|
10842
|
-
|
|
12472
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
12473
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
10843
12474
|
];
|
|
10844
12475
|
for (const serverPath of possiblePaths2) {
|
|
10845
|
-
if (
|
|
12476
|
+
if (existsSync19(serverPath)) {
|
|
10846
12477
|
return serverPath;
|
|
10847
12478
|
}
|
|
10848
12479
|
}
|
|
@@ -10882,13 +12513,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10882
12513
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
10883
12514
|
return { process: null, port: actualPort };
|
|
10884
12515
|
}
|
|
10885
|
-
const usePnpm =
|
|
10886
|
-
const useNpm = !usePnpm &&
|
|
12516
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
12517
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
10887
12518
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
10888
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
12519
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
10889
12520
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
10890
12521
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
10891
|
-
const runtimeConfigPath =
|
|
12522
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
10892
12523
|
try {
|
|
10893
12524
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
10894
12525
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -10896,7 +12527,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10896
12527
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
10897
12528
|
}
|
|
10898
12529
|
const webEnv = {
|
|
10899
|
-
...
|
|
12530
|
+
...cleanEnv2,
|
|
10900
12531
|
PORT: String(actualPort)
|
|
10901
12532
|
// Next.js respects PORT env var
|
|
10902
12533
|
};
|
|
@@ -11010,12 +12641,28 @@ function stopWebUI() {
|
|
|
11010
12641
|
}
|
|
11011
12642
|
}
|
|
11012
12643
|
async function createApp(options = {}) {
|
|
11013
|
-
const app = new
|
|
12644
|
+
const app = new Hono7();
|
|
11014
12645
|
app.use("*", cors({
|
|
11015
12646
|
origin: "*",
|
|
11016
12647
|
// Allow all origins
|
|
11017
12648
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11018
|
-
allowHeaders: [
|
|
12649
|
+
allowHeaders: [
|
|
12650
|
+
"Content-Type",
|
|
12651
|
+
"Authorization",
|
|
12652
|
+
"X-Requested-With",
|
|
12653
|
+
// Personal-agent dashboard signs every request to the device with
|
|
12654
|
+
// these. Without them whitelisted the browser preflight strips the
|
|
12655
|
+
// headers and the signature middleware 401s.
|
|
12656
|
+
"X-Signature",
|
|
12657
|
+
"X-Signature-Timestamp",
|
|
12658
|
+
"X-Signature-Algorithm",
|
|
12659
|
+
"X-Device-Hwid",
|
|
12660
|
+
// Short-lived embed token used by the iframed SparkECoder web UI
|
|
12661
|
+
// when it's hosted inside the personal-agents dashboard. The
|
|
12662
|
+
// bootstrap in `web/src/lib/embed-bootstrap.ts` adds this header
|
|
12663
|
+
// to every API call.
|
|
12664
|
+
"X-Embed-Token"
|
|
12665
|
+
],
|
|
11019
12666
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11020
12667
|
maxAge: 86400
|
|
11021
12668
|
// 24 hours
|
|
@@ -11023,12 +12670,15 @@ async function createApp(options = {}) {
|
|
|
11023
12670
|
if (!options.quiet) {
|
|
11024
12671
|
app.use("*", logger());
|
|
11025
12672
|
}
|
|
12673
|
+
app.use("*", hwidMiddleware());
|
|
12674
|
+
app.use("*", signatureMiddleware());
|
|
11026
12675
|
app.route("/health", health);
|
|
11027
12676
|
app.route("/sessions", sessions);
|
|
11028
12677
|
app.route("/agents", agents);
|
|
11029
12678
|
app.route("/sessions", terminals);
|
|
11030
12679
|
app.route("/terminals", terminals);
|
|
11031
12680
|
app.route("/tasks", tasks_default);
|
|
12681
|
+
app.route("/system", system);
|
|
11032
12682
|
app.get("/openapi.json", async (c) => {
|
|
11033
12683
|
return c.json(generateOpenAPISpec());
|
|
11034
12684
|
});
|
|
@@ -11081,8 +12731,8 @@ async function startServer(options = {}) {
|
|
|
11081
12731
|
if (options.workingDirectory) {
|
|
11082
12732
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11083
12733
|
}
|
|
11084
|
-
if (!
|
|
11085
|
-
|
|
12734
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
12735
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11086
12736
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11087
12737
|
}
|
|
11088
12738
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11117,6 +12767,15 @@ async function startServer(options = {}) {
|
|
|
11117
12767
|
port,
|
|
11118
12768
|
hostname: host
|
|
11119
12769
|
});
|
|
12770
|
+
try {
|
|
12771
|
+
attachPtyServer(serverInstance, {
|
|
12772
|
+
quiet: options.quiet
|
|
12773
|
+
});
|
|
12774
|
+
} catch (e) {
|
|
12775
|
+
if (!options.quiet) {
|
|
12776
|
+
console.warn(` \u26A0 Failed to attach /pty WebSocket server: ${e.message}`);
|
|
12777
|
+
}
|
|
12778
|
+
}
|
|
11120
12779
|
let webPort;
|
|
11121
12780
|
let webStarted;
|
|
11122
12781
|
if (options.webUI !== false) {
|