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/server/index.js
CHANGED
|
@@ -28,9 +28,9 @@ __export(remote_exports, {
|
|
|
28
28
|
remoteToolExecutionQueries: () => remoteToolExecutionQueries,
|
|
29
29
|
storageQueries: () => storageQueries
|
|
30
30
|
});
|
|
31
|
-
function initRemoteDatabase(serverUrl,
|
|
31
|
+
function initRemoteDatabase(serverUrl, key2) {
|
|
32
32
|
remoteServerUrl = serverUrl.replace(/\/$/, "");
|
|
33
|
-
authKey =
|
|
33
|
+
authKey = key2;
|
|
34
34
|
}
|
|
35
35
|
function closeRemoteDatabase() {
|
|
36
36
|
remoteServerUrl = null;
|
|
@@ -44,14 +44,14 @@ function parseDates(obj) {
|
|
|
44
44
|
if (Array.isArray(obj)) return obj.map(parseDates);
|
|
45
45
|
if (typeof obj !== "object" || obj instanceof Date) return obj;
|
|
46
46
|
const result = { ...obj };
|
|
47
|
-
for (const
|
|
48
|
-
if (MODEL_MESSAGE_FIELDS.includes(
|
|
47
|
+
for (const key2 of Object.keys(result)) {
|
|
48
|
+
if (MODEL_MESSAGE_FIELDS.includes(key2)) {
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
if (DATE_FIELDS.includes(
|
|
52
|
-
result[
|
|
53
|
-
} else if (typeof result[
|
|
54
|
-
result[
|
|
51
|
+
if (DATE_FIELDS.includes(key2) && typeof result[key2] === "string") {
|
|
52
|
+
result[key2] = new Date(result[key2]);
|
|
53
|
+
} else if (typeof result[key2] === "object") {
|
|
54
|
+
result[key2] = parseDates(result[key2]);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
return result;
|
|
@@ -516,7 +516,12 @@ var init_types = __esm({
|
|
|
516
516
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
517
517
|
alwaysApply: z.boolean().optional().default(false),
|
|
518
518
|
// Glob patterns - auto-inject when working with matching files
|
|
519
|
-
globs: z.array(z.string()).optional().default([])
|
|
519
|
+
globs: z.array(z.string()).optional().default([]),
|
|
520
|
+
// Platform requirements — skill is hidden from the model on platforms
|
|
521
|
+
// not listed here. Values match `process.platform`
|
|
522
|
+
// (darwin, linux, win32, freebsd, ...). If omitted or empty, the skill is
|
|
523
|
+
// available on all platforms.
|
|
524
|
+
platforms: z.array(z.string()).optional().default([])
|
|
520
525
|
});
|
|
521
526
|
TaskConfigSchema = z.object({
|
|
522
527
|
enabled: z.boolean(),
|
|
@@ -534,7 +539,13 @@ var init_types = __esm({
|
|
|
534
539
|
approvalWebhook: z.string().url().optional(),
|
|
535
540
|
skillsDirectory: z.string().optional(),
|
|
536
541
|
maxContextChars: z.number().optional().default(2e5),
|
|
537
|
-
task: TaskConfigSchema.optional()
|
|
542
|
+
task: TaskConfigSchema.optional(),
|
|
543
|
+
// Anthropic computer use tool — opt-in. When true, the `computer` tool is
|
|
544
|
+
// included in the toolset for Anthropic models. Default false.
|
|
545
|
+
computerUseEnabled: z.boolean().optional(),
|
|
546
|
+
// Display dimensions for the computer use tool (defaults: 1280x800).
|
|
547
|
+
computerUseDisplayWidth: z.number().int().positive().optional(),
|
|
548
|
+
computerUseDisplayHeight: z.number().int().positive().optional()
|
|
538
549
|
});
|
|
539
550
|
VectorGatewayConfigSchema = z.object({
|
|
540
551
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -585,7 +596,7 @@ var init_types = __esm({
|
|
|
585
596
|
}).optional();
|
|
586
597
|
SparkcoderConfigSchema = z.object({
|
|
587
598
|
// Default model to use (Vercel AI Gateway format)
|
|
588
|
-
defaultModel: z.string().default("anthropic/claude-opus-4
|
|
599
|
+
defaultModel: z.string().default("anthropic/claude-opus-4.7"),
|
|
589
600
|
// Working directory for file operations
|
|
590
601
|
workingDirectory: z.string().optional(),
|
|
591
602
|
// Tool approval settings
|
|
@@ -944,6 +955,14 @@ function loadApiKeysIntoEnv() {
|
|
|
944
955
|
}
|
|
945
956
|
}
|
|
946
957
|
}
|
|
958
|
+
function isRemoteInferenceConfigured() {
|
|
959
|
+
try {
|
|
960
|
+
const config = getConfig();
|
|
961
|
+
return config.resolvedRemoteServer.isConfigured;
|
|
962
|
+
} catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
947
966
|
function setApiKey(provider, apiKey) {
|
|
948
967
|
const normalizedProvider = provider.toLowerCase();
|
|
949
968
|
const envVar = PROVIDER_ENV_MAP[normalizedProvider];
|
|
@@ -993,11 +1012,11 @@ function getApiKeyStatus() {
|
|
|
993
1012
|
};
|
|
994
1013
|
});
|
|
995
1014
|
}
|
|
996
|
-
function maskApiKey(
|
|
997
|
-
if (
|
|
998
|
-
return "****" +
|
|
1015
|
+
function maskApiKey(key2) {
|
|
1016
|
+
if (key2.length <= 12) {
|
|
1017
|
+
return "****" + key2.slice(-4);
|
|
999
1018
|
}
|
|
1000
|
-
return
|
|
1019
|
+
return key2.slice(0, 4) + "..." + key2.slice(-4);
|
|
1001
1020
|
}
|
|
1002
1021
|
var CONFIG_FILE_NAMES, cachedConfig, AUTH_KEY_FILE, API_KEYS_FILE, PROVIDER_ENV_MAP, SUPPORTED_PROVIDERS;
|
|
1003
1022
|
var init_config = __esm({
|
|
@@ -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
|
}
|
|
@@ -2491,12 +2534,12 @@ var init_recorder = __esm({
|
|
|
2491
2534
|
|
|
2492
2535
|
// src/server/index.ts
|
|
2493
2536
|
import "dotenv/config";
|
|
2494
|
-
import { Hono as
|
|
2537
|
+
import { Hono as Hono7 } from "hono";
|
|
2495
2538
|
import { serve } from "@hono/node-server";
|
|
2496
2539
|
import { cors } from "hono/cors";
|
|
2497
2540
|
import { logger } from "hono/logger";
|
|
2498
|
-
import { existsSync as
|
|
2499
|
-
import { resolve as resolve10, dirname as dirname7, join as
|
|
2541
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2542
|
+
import { resolve as resolve10, dirname as dirname7, join as join13 } from "path";
|
|
2500
2543
|
import { spawn as spawn2 } from "child_process";
|
|
2501
2544
|
import { createServer as createNetServer } from "net";
|
|
2502
2545
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -2505,17 +2548,17 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
2505
2548
|
init_db();
|
|
2506
2549
|
import { Hono } from "hono";
|
|
2507
2550
|
import { zValidator } from "@hono/zod-validator";
|
|
2508
|
-
import { z as
|
|
2509
|
-
import { existsSync as
|
|
2551
|
+
import { z as z16 } from "zod";
|
|
2552
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
2510
2553
|
import { readdir as readdir6 } from "fs/promises";
|
|
2511
|
-
import { join as
|
|
2512
|
-
import { nanoid as
|
|
2554
|
+
import { join as join10, basename as basename5, extname as extname8, relative as relative9 } from "path";
|
|
2555
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
2513
2556
|
|
|
2514
2557
|
// src/agent/index.ts
|
|
2515
2558
|
import {
|
|
2516
2559
|
streamText as streamText2,
|
|
2517
2560
|
generateText as generateText3,
|
|
2518
|
-
tool as
|
|
2561
|
+
tool as tool14,
|
|
2519
2562
|
stepCountIs as stepCountIs2
|
|
2520
2563
|
} from "ai";
|
|
2521
2564
|
|
|
@@ -2684,6 +2727,23 @@ function isAnthropicModel(modelId) {
|
|
|
2684
2727
|
const normalized = modelId.trim().toLowerCase();
|
|
2685
2728
|
return normalized.startsWith(ANTHROPIC_PREFIX) || normalized.startsWith("claude-");
|
|
2686
2729
|
}
|
|
2730
|
+
function requiresAdaptiveThinking(modelId) {
|
|
2731
|
+
const m = modelId.toLowerCase().match(/claude-(?:opus|sonnet|haiku)-(\d+)[.-](\d{1,2})(?!\d)/);
|
|
2732
|
+
if (!m) return false;
|
|
2733
|
+
const major = Number(m[1]);
|
|
2734
|
+
const minor = Number(m[2]);
|
|
2735
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) return false;
|
|
2736
|
+
if (major > 4) return true;
|
|
2737
|
+
if (major === 4 && minor >= 6) return true;
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
function getAnthropicProviderOptions(modelId, opts = {}) {
|
|
2741
|
+
const { toolStreaming, budgetTokens = 1e4 } = opts;
|
|
2742
|
+
const thinking = requiresAdaptiveThinking(modelId) ? { type: "adaptive" } : { type: "enabled", budgetTokens };
|
|
2743
|
+
const out = { thinking };
|
|
2744
|
+
if (toolStreaming) out.toolStreaming = true;
|
|
2745
|
+
return out;
|
|
2746
|
+
}
|
|
2687
2747
|
function resolveModel(modelId) {
|
|
2688
2748
|
try {
|
|
2689
2749
|
const config = getConfig();
|
|
@@ -2706,8 +2766,8 @@ var SUBAGENT_MODELS = {
|
|
|
2706
2766
|
// src/agent/index.ts
|
|
2707
2767
|
init_db();
|
|
2708
2768
|
init_config();
|
|
2709
|
-
import { z as
|
|
2710
|
-
import { nanoid as
|
|
2769
|
+
import { z as z15 } from "zod";
|
|
2770
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2711
2771
|
|
|
2712
2772
|
// src/tools/bash.ts
|
|
2713
2773
|
import { tool } from "ai";
|
|
@@ -3040,11 +3100,11 @@ async function sendInput(terminalId, input, options = {}) {
|
|
|
3040
3100
|
return false;
|
|
3041
3101
|
}
|
|
3042
3102
|
}
|
|
3043
|
-
async function sendKey(terminalId,
|
|
3103
|
+
async function sendKey(terminalId, key2) {
|
|
3044
3104
|
const session = getSessionName(terminalId);
|
|
3045
3105
|
try {
|
|
3046
3106
|
await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
|
|
3047
|
-
await execAsync(`tmux send-keys -t ${session} ${
|
|
3107
|
+
await execAsync(`tmux send-keys -t ${session} ${key2}`, { timeout: 1e3 });
|
|
3048
3108
|
return true;
|
|
3049
3109
|
} catch {
|
|
3050
3110
|
return false;
|
|
@@ -3183,7 +3243,7 @@ bash({ id: "abc123", input: "my text" }) // send text input
|
|
|
3183
3243
|
Terminal output is stored in the global SparkECoder data directory. Use the \`tail\` option to read recent output.`,
|
|
3184
3244
|
inputSchema: bashInputSchema,
|
|
3185
3245
|
execute: async (inputArgs) => {
|
|
3186
|
-
const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
|
|
3246
|
+
const { command, background, id, kill, tail, input: textInput, key: key2 } = inputArgs;
|
|
3187
3247
|
if (id) {
|
|
3188
3248
|
if (kill) {
|
|
3189
3249
|
const success = await killTerminal(id);
|
|
@@ -3214,8 +3274,8 @@ Terminal output is stored in the global SparkECoder data directory. Use the \`ta
|
|
|
3214
3274
|
message: `Sent input "${textInput}" to terminal`
|
|
3215
3275
|
};
|
|
3216
3276
|
}
|
|
3217
|
-
if (
|
|
3218
|
-
const success = await sendKey(id,
|
|
3277
|
+
if (key2) {
|
|
3278
|
+
const success = await sendKey(id, key2);
|
|
3219
3279
|
if (!success) {
|
|
3220
3280
|
return {
|
|
3221
3281
|
success: false,
|
|
@@ -3231,7 +3291,7 @@ Terminal output is stored in the global SparkECoder data directory. Use the \`ta
|
|
|
3231
3291
|
id,
|
|
3232
3292
|
output: truncatedOutput2,
|
|
3233
3293
|
status: status2,
|
|
3234
|
-
message: `Sent key "${
|
|
3294
|
+
message: `Sent key "${key2}" to terminal`
|
|
3235
3295
|
};
|
|
3236
3296
|
}
|
|
3237
3297
|
const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
|
|
@@ -3373,13 +3433,13 @@ async function resizeImageIfNeeded(buffer, mediaType) {
|
|
|
3373
3433
|
const needsResize = longEdge > MAX_LONG_EDGE;
|
|
3374
3434
|
const needsShrink = buffer.length > MAX_FILE_BYTES;
|
|
3375
3435
|
if (!needsResize && !needsShrink) return { buffer, mediaType: inputMediaType };
|
|
3376
|
-
const
|
|
3436
|
+
const key2 = cacheKey(buffer);
|
|
3377
3437
|
const cacheDir = getCacheDir();
|
|
3378
3438
|
const isPng = inputMediaType.includes("png");
|
|
3379
3439
|
const willConvertToJpeg = isPng && (needsShrink || buffer.length > 2 * 1024 * 1024);
|
|
3380
3440
|
const outputMediaType = willConvertToJpeg || !isPng ? "image/jpeg" : "image/png";
|
|
3381
3441
|
const ext = outputMediaType === "image/png" ? ".png" : ".jpg";
|
|
3382
|
-
const cachePath = join3(cacheDir,
|
|
3442
|
+
const cachePath = join3(cacheDir, key2 + ext);
|
|
3383
3443
|
if (existsSync3(cachePath)) {
|
|
3384
3444
|
console.log(`[image-resize] Cache hit for ${width}x${height} image`);
|
|
3385
3445
|
return { buffer: readFileSync2(cachePath), mediaType: outputMediaType };
|
|
@@ -3774,12 +3834,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
3774
3834
|
}
|
|
3775
3835
|
async function commandExists(cmd) {
|
|
3776
3836
|
try {
|
|
3777
|
-
const { exec:
|
|
3778
|
-
const { promisify:
|
|
3779
|
-
const
|
|
3837
|
+
const { exec: exec8 } = await import("child_process");
|
|
3838
|
+
const { promisify: promisify8 } = await import("util");
|
|
3839
|
+
const execAsync8 = promisify8(exec8);
|
|
3780
3840
|
const isWindows = process.platform === "win32";
|
|
3781
3841
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
3782
|
-
await
|
|
3842
|
+
await execAsync8(checkCmd);
|
|
3783
3843
|
return true;
|
|
3784
3844
|
} catch {
|
|
3785
3845
|
return false;
|
|
@@ -4270,31 +4330,31 @@ async function getClientForFile(filePath) {
|
|
|
4270
4330
|
return null;
|
|
4271
4331
|
}
|
|
4272
4332
|
const root = dirname4(normalized);
|
|
4273
|
-
const
|
|
4274
|
-
const existing = state.clients.get(
|
|
4333
|
+
const key2 = `${serverDef.id}:${root}`;
|
|
4334
|
+
const existing = state.clients.get(key2);
|
|
4275
4335
|
if (existing) {
|
|
4276
4336
|
return existing;
|
|
4277
4337
|
}
|
|
4278
|
-
if (state.broken.has(
|
|
4338
|
+
if (state.broken.has(key2)) {
|
|
4279
4339
|
return null;
|
|
4280
4340
|
}
|
|
4281
4341
|
try {
|
|
4282
4342
|
const handle = await serverDef.spawn(root);
|
|
4283
4343
|
if (!handle) {
|
|
4284
|
-
state.broken.add(
|
|
4344
|
+
state.broken.add(key2);
|
|
4285
4345
|
return null;
|
|
4286
4346
|
}
|
|
4287
4347
|
console.log(`[lsp] Started ${serverDef.name} for ${root}`);
|
|
4288
4348
|
const client = await createClient(serverDef.id, handle, root);
|
|
4289
|
-
state.clients.set(
|
|
4349
|
+
state.clients.set(key2, client);
|
|
4290
4350
|
handle.process.on("exit", (code) => {
|
|
4291
4351
|
console.log(`[lsp] ${serverDef.name} exited with code ${code}`);
|
|
4292
|
-
state.clients.delete(
|
|
4352
|
+
state.clients.delete(key2);
|
|
4293
4353
|
});
|
|
4294
4354
|
return client;
|
|
4295
4355
|
} catch (error) {
|
|
4296
4356
|
console.error(`[lsp] Failed to start ${serverDef.name}:`, error);
|
|
4297
|
-
state.broken.add(
|
|
4357
|
+
state.broken.add(key2);
|
|
4298
4358
|
return null;
|
|
4299
4359
|
}
|
|
4300
4360
|
}
|
|
@@ -6026,6 +6086,7 @@ init_semantic_search();
|
|
|
6026
6086
|
import { tool as tool11 } from "ai";
|
|
6027
6087
|
import { z as z12 } from "zod";
|
|
6028
6088
|
import Ajv from "ajv";
|
|
6089
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
6029
6090
|
var ajv = new Ajv({ allErrors: true });
|
|
6030
6091
|
function createCompleteTaskTool(options) {
|
|
6031
6092
|
const validate = ajv.compile(options.outputSchema);
|
|
@@ -6072,6 +6133,37 @@ function createTaskFailedTool(options) {
|
|
|
6072
6133
|
}
|
|
6073
6134
|
});
|
|
6074
6135
|
}
|
|
6136
|
+
function createAskQuestionToUserTool(options) {
|
|
6137
|
+
return tool11({
|
|
6138
|
+
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.",
|
|
6139
|
+
inputSchema: z12.object({
|
|
6140
|
+
question: z12.string().min(1).describe("The concise question you need answered."),
|
|
6141
|
+
context: z12.string().optional().describe("Brief context explaining why this answer is needed and what you already tried."),
|
|
6142
|
+
choices: z12.array(z12.string().min(1)).max(10).optional().describe("Optional suggested answer choices when the question is multiple choice.")
|
|
6143
|
+
}),
|
|
6144
|
+
execute: async (input) => {
|
|
6145
|
+
if (!options.onQuestion) {
|
|
6146
|
+
return {
|
|
6147
|
+
status: "unavailable",
|
|
6148
|
+
message: "Question routing is not configured for this task. Continue with the best safe assumption or call task_failed if blocked."
|
|
6149
|
+
};
|
|
6150
|
+
}
|
|
6151
|
+
const questionId = `q_${nanoid3(12)}`;
|
|
6152
|
+
const answer = await options.onQuestion({
|
|
6153
|
+
questionId,
|
|
6154
|
+
question: input.question,
|
|
6155
|
+
context: input.context,
|
|
6156
|
+
choices: input.choices
|
|
6157
|
+
});
|
|
6158
|
+
return {
|
|
6159
|
+
status: "answered",
|
|
6160
|
+
questionId,
|
|
6161
|
+
answer: answer.answer,
|
|
6162
|
+
answeredBy: answer.answeredBy ?? "unknown"
|
|
6163
|
+
};
|
|
6164
|
+
}
|
|
6165
|
+
});
|
|
6166
|
+
}
|
|
6075
6167
|
|
|
6076
6168
|
// src/tools/upload-file.ts
|
|
6077
6169
|
import { tool as tool12 } from "ai";
|
|
@@ -6170,6 +6262,568 @@ function createUploadFileTool(options) {
|
|
|
6170
6262
|
});
|
|
6171
6263
|
}
|
|
6172
6264
|
|
|
6265
|
+
// src/tools/computer-use.ts
|
|
6266
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
6267
|
+
import { exec as exec5 } from "child_process";
|
|
6268
|
+
import { promisify as promisify5 } from "util";
|
|
6269
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync15, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6270
|
+
import { join as join8 } from "path";
|
|
6271
|
+
import { tmpdir } from "os";
|
|
6272
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
6273
|
+
var execAsync5 = promisify5(exec5);
|
|
6274
|
+
var DEFAULT_WIDTH = 1280;
|
|
6275
|
+
var DEFAULT_HEIGHT = 800;
|
|
6276
|
+
function isMacOs() {
|
|
6277
|
+
return process.platform === "darwin";
|
|
6278
|
+
}
|
|
6279
|
+
async function isCliclickInstalled() {
|
|
6280
|
+
try {
|
|
6281
|
+
await execAsync5("command -v cliclick", { timeout: 2e3 });
|
|
6282
|
+
return true;
|
|
6283
|
+
} catch {
|
|
6284
|
+
return false;
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
async function runJxa(script) {
|
|
6288
|
+
try {
|
|
6289
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
6290
|
+
const { stdout } = await execAsync5(`osascript -l JavaScript -e '${escaped}'`, {
|
|
6291
|
+
timeout: 5e3
|
|
6292
|
+
});
|
|
6293
|
+
return JSON.parse(stdout.trim());
|
|
6294
|
+
} catch {
|
|
6295
|
+
return null;
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
async function hasAccessibilityPermissions() {
|
|
6299
|
+
try {
|
|
6300
|
+
const { stderr } = await execAsync5("cliclick p:.", { timeout: 3e3 });
|
|
6301
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6302
|
+
return { ok: false, error: stderr.trim().split("\n")[0] };
|
|
6303
|
+
}
|
|
6304
|
+
return { ok: true };
|
|
6305
|
+
} catch (err) {
|
|
6306
|
+
return { ok: false, error: err?.message || String(err) };
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
async function hasScreenRecordingPermissions() {
|
|
6310
|
+
const result = await runJxa(
|
|
6311
|
+
`ObjC.import("Cocoa");
|
|
6312
|
+
ObjC.import("CoreGraphics");
|
|
6313
|
+
ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]);
|
|
6314
|
+
JSON.stringify({ hasAccess: !!$.CGPreflightScreenCaptureAccess() });`
|
|
6315
|
+
);
|
|
6316
|
+
return result?.hasAccess ?? false;
|
|
6317
|
+
}
|
|
6318
|
+
async function requestAccessibilityPrompt() {
|
|
6319
|
+
const result = await runJxa(
|
|
6320
|
+
`ObjC.import("ApplicationServices");
|
|
6321
|
+
var key = $.kAXTrustedCheckOptionPrompt;
|
|
6322
|
+
var dict = $.NSDictionary.dictionaryWithObjectForKey($.kCFBooleanTrue, key);
|
|
6323
|
+
var trusted = $.AXIsProcessTrustedWithOptions(dict);
|
|
6324
|
+
JSON.stringify({ trusted: !!trusted });`
|
|
6325
|
+
);
|
|
6326
|
+
return result?.trusted ?? false;
|
|
6327
|
+
}
|
|
6328
|
+
async function requestScreenRecordingPrompt() {
|
|
6329
|
+
const result = await runJxa(
|
|
6330
|
+
`ObjC.import("Cocoa");
|
|
6331
|
+
ObjC.import("CoreGraphics");
|
|
6332
|
+
ObjC.bindFunction("CGRequestScreenCaptureAccess", ["bool", []]);
|
|
6333
|
+
JSON.stringify({ granted: !!$.CGRequestScreenCaptureAccess() });`
|
|
6334
|
+
);
|
|
6335
|
+
return result?.granted ?? false;
|
|
6336
|
+
}
|
|
6337
|
+
async function openSystemSettings(pane) {
|
|
6338
|
+
const url = pane === "accessibility" ? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" : "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6339
|
+
try {
|
|
6340
|
+
await execAsync5(`open '${url}'`, { timeout: 3e3 });
|
|
6341
|
+
} catch {
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
async function detectScreenSize() {
|
|
6345
|
+
try {
|
|
6346
|
+
const { stdout } = await execAsync5(
|
|
6347
|
+
`osascript -e 'tell application "Finder" to get bounds of window of desktop'`,
|
|
6348
|
+
{ timeout: 3e3 }
|
|
6349
|
+
);
|
|
6350
|
+
const parts = stdout.trim().split(",").map((s) => parseInt(s.trim(), 10));
|
|
6351
|
+
if (parts.length >= 4 && parts.every((n) => Number.isFinite(n))) {
|
|
6352
|
+
const [x1, y1, x2, y2] = parts;
|
|
6353
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
6354
|
+
}
|
|
6355
|
+
} catch {
|
|
6356
|
+
}
|
|
6357
|
+
return null;
|
|
6358
|
+
}
|
|
6359
|
+
async function runCliclick(args) {
|
|
6360
|
+
const quoted = args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
6361
|
+
const { stdout, stderr } = await execAsync5(`cliclick ${quoted}`, {
|
|
6362
|
+
timeout: 15e3,
|
|
6363
|
+
maxBuffer: 1024 * 1024
|
|
6364
|
+
});
|
|
6365
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
6366
|
+
throw new Error(
|
|
6367
|
+
"Accessibility permissions not granted to cliclick. Open System Settings \u2192 Privacy & Security \u2192 Accessibility, add cliclick (or the agent runtime), and toggle it on."
|
|
6368
|
+
);
|
|
6369
|
+
}
|
|
6370
|
+
if (stderr && !stdout) throw new Error(stderr.trim());
|
|
6371
|
+
return (stdout || "").trim();
|
|
6372
|
+
}
|
|
6373
|
+
async function runScreencapture(path) {
|
|
6374
|
+
await execAsync5(`screencapture -x -t png '${path.replace(/'/g, `'\\''`)}'`, {
|
|
6375
|
+
timeout: 5e3
|
|
6376
|
+
});
|
|
6377
|
+
}
|
|
6378
|
+
async function resizeScreenshotToPoints(path, targetWidth, targetHeight) {
|
|
6379
|
+
const sharpModule = await import("sharp");
|
|
6380
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6381
|
+
const meta = await sharp2(path).metadata();
|
|
6382
|
+
if (meta.width === targetWidth && meta.height === targetHeight) {
|
|
6383
|
+
return readFileSync7(path);
|
|
6384
|
+
}
|
|
6385
|
+
return await sharp2(path).resize(targetWidth, targetHeight, { fit: "fill" }).png().toBuffer();
|
|
6386
|
+
}
|
|
6387
|
+
async function runScroll(dx, dy) {
|
|
6388
|
+
const wheelY = -Math.round(dy);
|
|
6389
|
+
const wheelX = -Math.round(dx);
|
|
6390
|
+
const script = `ObjC.import('CoreGraphics');var ev = $.CGEventCreateScrollWheelEvent(null, 0, 2, ${wheelY}, ${wheelX});$.CGEventPost(0, ev);`;
|
|
6391
|
+
await execAsync5(
|
|
6392
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6393
|
+
{ timeout: 5e3 }
|
|
6394
|
+
);
|
|
6395
|
+
}
|
|
6396
|
+
function translateKeyForCliclick(key2) {
|
|
6397
|
+
if (!key2) return [];
|
|
6398
|
+
const parts = key2.split("+").map((p) => p.trim()).filter(Boolean);
|
|
6399
|
+
if (parts.length === 0) return [];
|
|
6400
|
+
const modMap = {
|
|
6401
|
+
ctrl: "ctrl",
|
|
6402
|
+
control: "ctrl",
|
|
6403
|
+
alt: "alt",
|
|
6404
|
+
option: "alt",
|
|
6405
|
+
shift: "shift",
|
|
6406
|
+
cmd: "cmd",
|
|
6407
|
+
super: "cmd",
|
|
6408
|
+
meta: "cmd",
|
|
6409
|
+
win: "cmd",
|
|
6410
|
+
fn: "fn"
|
|
6411
|
+
};
|
|
6412
|
+
const keyMap = {
|
|
6413
|
+
return: "enter",
|
|
6414
|
+
enter: "enter",
|
|
6415
|
+
esc: "esc",
|
|
6416
|
+
escape: "esc",
|
|
6417
|
+
backspace: "delete",
|
|
6418
|
+
back_space: "delete",
|
|
6419
|
+
delete: "fwd-delete",
|
|
6420
|
+
fwd_delete: "fwd-delete",
|
|
6421
|
+
forward_delete: "fwd-delete",
|
|
6422
|
+
tab: "tab",
|
|
6423
|
+
space: "space",
|
|
6424
|
+
up: "arrow-up",
|
|
6425
|
+
arrow_up: "arrow-up",
|
|
6426
|
+
down: "arrow-down",
|
|
6427
|
+
arrow_down: "arrow-down",
|
|
6428
|
+
left: "arrow-left",
|
|
6429
|
+
arrow_left: "arrow-left",
|
|
6430
|
+
right: "arrow-right",
|
|
6431
|
+
arrow_right: "arrow-right",
|
|
6432
|
+
page_up: "page-up",
|
|
6433
|
+
pageup: "page-up",
|
|
6434
|
+
page_down: "page-down",
|
|
6435
|
+
pagedown: "page-down",
|
|
6436
|
+
home: "home",
|
|
6437
|
+
end: "end",
|
|
6438
|
+
f1: "f1",
|
|
6439
|
+
f2: "f2",
|
|
6440
|
+
f3: "f3",
|
|
6441
|
+
f4: "f4",
|
|
6442
|
+
f5: "f5",
|
|
6443
|
+
f6: "f6",
|
|
6444
|
+
f7: "f7",
|
|
6445
|
+
f8: "f8",
|
|
6446
|
+
f9: "f9",
|
|
6447
|
+
f10: "f10",
|
|
6448
|
+
f11: "f11",
|
|
6449
|
+
f12: "f12"
|
|
6450
|
+
};
|
|
6451
|
+
const modifiers = [];
|
|
6452
|
+
let mainKey = null;
|
|
6453
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6454
|
+
const lower = parts[i].toLowerCase().replace(/-/g, "_");
|
|
6455
|
+
if (i < parts.length - 1 && modMap[lower]) {
|
|
6456
|
+
modifiers.push(modMap[lower]);
|
|
6457
|
+
} else {
|
|
6458
|
+
mainKey = keyMap[lower] || lower;
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
const args = [];
|
|
6462
|
+
if (modifiers.length > 0) args.push(`kd:${modifiers.join(",")}`);
|
|
6463
|
+
if (mainKey) {
|
|
6464
|
+
const isNamedKey = Object.values(keyMap).includes(mainKey) || /^f([1-9]|1[0-9]|20)$/.test(mainKey) || /^num-/.test(mainKey);
|
|
6465
|
+
if (isNamedKey) {
|
|
6466
|
+
args.push(`kp:${mainKey}`);
|
|
6467
|
+
} else {
|
|
6468
|
+
args.push(`t:${mainKey}`);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
if (modifiers.length > 0) args.push(`ku:${modifiers.join(",")}`);
|
|
6472
|
+
return args;
|
|
6473
|
+
}
|
|
6474
|
+
function modifierStringToCliclick(text) {
|
|
6475
|
+
return text.split("+").map((p) => p.trim().toLowerCase()).map((p) => {
|
|
6476
|
+
if (p === "ctrl" || p === "control") return "ctrl";
|
|
6477
|
+
if (p === "alt" || p === "option") return "alt";
|
|
6478
|
+
if (p === "shift") return "shift";
|
|
6479
|
+
if (p === "super" || p === "meta" || p === "cmd") return "cmd";
|
|
6480
|
+
return "";
|
|
6481
|
+
}).filter(Boolean);
|
|
6482
|
+
}
|
|
6483
|
+
function createComputerUseTool(options) {
|
|
6484
|
+
const displayWidth = options.displayWidth ?? DEFAULT_WIDTH;
|
|
6485
|
+
const displayHeight = options.displayHeight ?? DEFAULT_HEIGHT;
|
|
6486
|
+
return anthropic.tools.computer_20251124({
|
|
6487
|
+
displayWidthPx: displayWidth,
|
|
6488
|
+
displayHeightPx: displayHeight,
|
|
6489
|
+
enableZoom: true,
|
|
6490
|
+
execute: async (input) => {
|
|
6491
|
+
try {
|
|
6492
|
+
switch (input.action) {
|
|
6493
|
+
case "screenshot": {
|
|
6494
|
+
const path = join8(tmpdir(), `cu-${nanoid4(8)}.png`);
|
|
6495
|
+
await runScreencapture(path);
|
|
6496
|
+
const resized = await resizeScreenshotToPoints(path, displayWidth, displayHeight);
|
|
6497
|
+
try {
|
|
6498
|
+
unlinkSync2(path);
|
|
6499
|
+
} catch {
|
|
6500
|
+
}
|
|
6501
|
+
return { type: "image", data: resized.toString("base64") };
|
|
6502
|
+
}
|
|
6503
|
+
case "left_click": {
|
|
6504
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6505
|
+
if (input.text) {
|
|
6506
|
+
const mods = modifierStringToCliclick(input.text);
|
|
6507
|
+
if (mods.length > 0) {
|
|
6508
|
+
await runCliclick([`kd:${mods.join(",")}`, `c:${x},${y}`, `ku:${mods.join(",")}`]);
|
|
6509
|
+
} else {
|
|
6510
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6511
|
+
}
|
|
6512
|
+
} else {
|
|
6513
|
+
await runCliclick([`c:${x},${y}`]);
|
|
6514
|
+
}
|
|
6515
|
+
return `clicked at (${x}, ${y})${input.text ? ` with ${input.text}` : ""}`;
|
|
6516
|
+
}
|
|
6517
|
+
case "right_click": {
|
|
6518
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6519
|
+
await runCliclick([`rc:${x},${y}`]);
|
|
6520
|
+
return `right-clicked at (${x}, ${y})`;
|
|
6521
|
+
}
|
|
6522
|
+
case "middle_click": {
|
|
6523
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6524
|
+
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);`;
|
|
6525
|
+
await execAsync5(
|
|
6526
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
6527
|
+
{ timeout: 3e3 }
|
|
6528
|
+
);
|
|
6529
|
+
return `middle-clicked at (${x}, ${y})`;
|
|
6530
|
+
}
|
|
6531
|
+
case "double_click": {
|
|
6532
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6533
|
+
await runCliclick([`dc:${x},${y}`]);
|
|
6534
|
+
return `double-clicked at (${x}, ${y})`;
|
|
6535
|
+
}
|
|
6536
|
+
case "triple_click": {
|
|
6537
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6538
|
+
await runCliclick([`tc:${x},${y}`]);
|
|
6539
|
+
return `triple-clicked at (${x}, ${y})`;
|
|
6540
|
+
}
|
|
6541
|
+
case "mouse_move": {
|
|
6542
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6543
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6544
|
+
return `moved cursor to (${x}, ${y})`;
|
|
6545
|
+
}
|
|
6546
|
+
case "left_mouse_down": {
|
|
6547
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6548
|
+
await runCliclick([`dd:${x},${y}`]);
|
|
6549
|
+
return `left mouse button pressed at (${x}, ${y})`;
|
|
6550
|
+
}
|
|
6551
|
+
case "left_mouse_up": {
|
|
6552
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
6553
|
+
await runCliclick([`du:${x},${y}`]);
|
|
6554
|
+
return `left mouse button released at (${x}, ${y})`;
|
|
6555
|
+
}
|
|
6556
|
+
case "left_click_drag": {
|
|
6557
|
+
const [sx, sy] = input.start_coordinate ?? [0, 0];
|
|
6558
|
+
const [ex, ey] = input.coordinate ?? [0, 0];
|
|
6559
|
+
await runCliclick([`dd:${sx},${sy}`, `m:${ex},${ey}`, `du:${ex},${ey}`]);
|
|
6560
|
+
return `dragged from (${sx}, ${sy}) to (${ex}, ${ey})`;
|
|
6561
|
+
}
|
|
6562
|
+
case "type": {
|
|
6563
|
+
const text = input.text ?? "";
|
|
6564
|
+
await runCliclick([`t:${text}`]);
|
|
6565
|
+
return `typed ${text.length} character(s)`;
|
|
6566
|
+
}
|
|
6567
|
+
case "key": {
|
|
6568
|
+
const args = translateKeyForCliclick(input.text ?? "");
|
|
6569
|
+
if (args.length === 0) return "no key specified";
|
|
6570
|
+
await runCliclick(args);
|
|
6571
|
+
return `pressed ${input.text}`;
|
|
6572
|
+
}
|
|
6573
|
+
case "hold_key": {
|
|
6574
|
+
const text = (input.text ?? "").toLowerCase();
|
|
6575
|
+
const duration = input.duration ?? 1;
|
|
6576
|
+
const modMap = {
|
|
6577
|
+
ctrl: "ctrl",
|
|
6578
|
+
control: "ctrl",
|
|
6579
|
+
alt: "alt",
|
|
6580
|
+
option: "alt",
|
|
6581
|
+
shift: "shift",
|
|
6582
|
+
cmd: "cmd",
|
|
6583
|
+
super: "cmd",
|
|
6584
|
+
meta: "cmd",
|
|
6585
|
+
fn: "fn"
|
|
6586
|
+
};
|
|
6587
|
+
const cliName = modMap[text] || text;
|
|
6588
|
+
await runCliclick([`kd:${cliName}`]);
|
|
6589
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6590
|
+
await runCliclick([`ku:${cliName}`]);
|
|
6591
|
+
return `held ${text} for ${duration}s`;
|
|
6592
|
+
}
|
|
6593
|
+
case "scroll": {
|
|
6594
|
+
const direction = input.scroll_direction ?? "down";
|
|
6595
|
+
const amount = input.scroll_amount ?? 3;
|
|
6596
|
+
const px = amount * 100;
|
|
6597
|
+
const dx = direction === "left" ? -px : direction === "right" ? px : 0;
|
|
6598
|
+
const dy = direction === "up" ? -px : direction === "down" ? px : 0;
|
|
6599
|
+
if (input.coordinate) {
|
|
6600
|
+
const [x, y] = input.coordinate;
|
|
6601
|
+
await runCliclick([`m:${x},${y}`]);
|
|
6602
|
+
}
|
|
6603
|
+
const mods = input.text ? modifierStringToCliclick(input.text) : [];
|
|
6604
|
+
if (mods.length > 0) {
|
|
6605
|
+
await runCliclick([`kd:${mods.join(",")}`]);
|
|
6606
|
+
}
|
|
6607
|
+
await runScroll(dx, dy);
|
|
6608
|
+
if (mods.length > 0) {
|
|
6609
|
+
await runCliclick([`ku:${mods.join(",")}`]);
|
|
6610
|
+
}
|
|
6611
|
+
return `scrolled ${direction} by ${amount}`;
|
|
6612
|
+
}
|
|
6613
|
+
case "wait": {
|
|
6614
|
+
const duration = input.duration ?? 1;
|
|
6615
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
6616
|
+
return `waited ${duration}s`;
|
|
6617
|
+
}
|
|
6618
|
+
case "cursor_position": {
|
|
6619
|
+
const out = await runCliclick(["p:."]);
|
|
6620
|
+
return `cursor at ${out}`;
|
|
6621
|
+
}
|
|
6622
|
+
case "zoom": {
|
|
6623
|
+
const region = input.region ?? [0, 0, displayWidth, displayHeight];
|
|
6624
|
+
const [x1, y1, x2, y2] = region;
|
|
6625
|
+
const tmpPath = join8(tmpdir(), `cu-zoom-${nanoid4(8)}.png`);
|
|
6626
|
+
await runScreencapture(tmpPath);
|
|
6627
|
+
const sharpModule = await import("sharp");
|
|
6628
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
6629
|
+
const meta = await sharp2(tmpPath).metadata();
|
|
6630
|
+
const scaleX = (meta.width || displayWidth) / displayWidth;
|
|
6631
|
+
const scaleY = (meta.height || displayHeight) / displayHeight;
|
|
6632
|
+
const px = {
|
|
6633
|
+
left: Math.max(0, Math.round(x1 * scaleX)),
|
|
6634
|
+
top: Math.max(0, Math.round(y1 * scaleY)),
|
|
6635
|
+
width: Math.max(1, Math.round((x2 - x1) * scaleX)),
|
|
6636
|
+
height: Math.max(1, Math.round((y2 - y1) * scaleY))
|
|
6637
|
+
};
|
|
6638
|
+
const buf = await sharp2(tmpPath).extract(px).png().toBuffer();
|
|
6639
|
+
try {
|
|
6640
|
+
unlinkSync2(tmpPath);
|
|
6641
|
+
} catch {
|
|
6642
|
+
}
|
|
6643
|
+
return { type: "image", data: buf.toString("base64") };
|
|
6644
|
+
}
|
|
6645
|
+
default: {
|
|
6646
|
+
const exhaustive = input.action;
|
|
6647
|
+
return `unsupported action: ${String(exhaustive)}`;
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
} catch (err) {
|
|
6651
|
+
const msg = err?.message || String(err);
|
|
6652
|
+
let hint = "";
|
|
6653
|
+
if (/accessibility|not authorized|tcc|operation not permitted/i.test(msg)) {
|
|
6654
|
+
hint = " (Hint: call enable_computer_use to (re-)check permissions and open System Settings)";
|
|
6655
|
+
} else if (/command not found/i.test(msg)) {
|
|
6656
|
+
hint = " (Hint: install cliclick with `brew install cliclick`)";
|
|
6657
|
+
}
|
|
6658
|
+
return `Error: ${msg}${hint}`;
|
|
6659
|
+
}
|
|
6660
|
+
},
|
|
6661
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6662
|
+
toModelOutput({ output }) {
|
|
6663
|
+
if (typeof output === "string") {
|
|
6664
|
+
return { type: "content", value: [{ type: "text", text: output }] };
|
|
6665
|
+
}
|
|
6666
|
+
return {
|
|
6667
|
+
type: "content",
|
|
6668
|
+
value: [{ type: "media", data: output.data, mediaType: "image/png" }]
|
|
6669
|
+
};
|
|
6670
|
+
}
|
|
6671
|
+
});
|
|
6672
|
+
}
|
|
6673
|
+
|
|
6674
|
+
// src/tools/enable-computer-use.ts
|
|
6675
|
+
init_db();
|
|
6676
|
+
import { tool as tool13 } from "ai";
|
|
6677
|
+
import { z as z14 } from "zod";
|
|
6678
|
+
var inputSchema = z14.object({
|
|
6679
|
+
display_width: z14.number().int().positive().optional().describe("Display width in pixels (defaults to detected primary display, fallback 1280)"),
|
|
6680
|
+
display_height: z14.number().int().positive().optional().describe("Display height in pixels (defaults to detected primary display, fallback 800)"),
|
|
6681
|
+
request_permissions: z14.boolean().optional().default(true).describe(
|
|
6682
|
+
"When true (default), proactively trigger macOS permission prompts and open System Settings panes for any missing permissions."
|
|
6683
|
+
)
|
|
6684
|
+
});
|
|
6685
|
+
var ACCESSIBILITY_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
6686
|
+
var SCREEN_RECORDING_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
6687
|
+
function createEnableComputerUseTool(options) {
|
|
6688
|
+
return tool13({
|
|
6689
|
+
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.",
|
|
6690
|
+
inputSchema,
|
|
6691
|
+
execute: async ({ display_width, display_height, request_permissions }) => {
|
|
6692
|
+
try {
|
|
6693
|
+
if (!isMacOs()) {
|
|
6694
|
+
return {
|
|
6695
|
+
success: false,
|
|
6696
|
+
error: "Computer use is currently only supported on macOS.",
|
|
6697
|
+
platform: process.platform
|
|
6698
|
+
};
|
|
6699
|
+
}
|
|
6700
|
+
if (!await isCliclickInstalled()) {
|
|
6701
|
+
return {
|
|
6702
|
+
success: false,
|
|
6703
|
+
error: "`cliclick` is not installed. It is required for mouse/keyboard control on macOS.",
|
|
6704
|
+
installCommand: "brew install cliclick",
|
|
6705
|
+
fixSteps: [
|
|
6706
|
+
"In a terminal on this Mac, run: brew install cliclick",
|
|
6707
|
+
"(If Homebrew is not installed, install it first from https://brew.sh)",
|
|
6708
|
+
"Then call enable_computer_use again"
|
|
6709
|
+
]
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6712
|
+
const acc = await hasAccessibilityPermissions();
|
|
6713
|
+
const screen = await hasScreenRecordingPermissions();
|
|
6714
|
+
const missing = [];
|
|
6715
|
+
if (!acc.ok) {
|
|
6716
|
+
let prompted = false;
|
|
6717
|
+
let panelOpened = false;
|
|
6718
|
+
if (request_permissions) {
|
|
6719
|
+
prompted = await requestAccessibilityPrompt().then(() => true).catch(() => false);
|
|
6720
|
+
await openSystemSettings("accessibility").then(() => {
|
|
6721
|
+
panelOpened = true;
|
|
6722
|
+
}).catch(() => void 0);
|
|
6723
|
+
}
|
|
6724
|
+
missing.push({
|
|
6725
|
+
name: "Accessibility",
|
|
6726
|
+
reason: "cliclick failed: " + (acc.error?.split("\n")[0] || "no permission"),
|
|
6727
|
+
pane: "accessibility",
|
|
6728
|
+
settingsUrl: ACCESSIBILITY_URL,
|
|
6729
|
+
fixSteps: [
|
|
6730
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Accessibility pane that opened",
|
|
6731
|
+
"Click the + button",
|
|
6732
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6733
|
+
"Toggle the switch ON",
|
|
6734
|
+
"Restart the agent process so the new permission takes effect",
|
|
6735
|
+
"Then call enable_computer_use again"
|
|
6736
|
+
],
|
|
6737
|
+
prompted,
|
|
6738
|
+
panelOpened
|
|
6739
|
+
});
|
|
6740
|
+
}
|
|
6741
|
+
if (!screen) {
|
|
6742
|
+
let prompted = false;
|
|
6743
|
+
let panelOpened = false;
|
|
6744
|
+
if (request_permissions) {
|
|
6745
|
+
prompted = await requestScreenRecordingPrompt().then(() => true).catch(() => false);
|
|
6746
|
+
await openSystemSettings("screen-recording").then(() => {
|
|
6747
|
+
panelOpened = true;
|
|
6748
|
+
}).catch(() => void 0);
|
|
6749
|
+
}
|
|
6750
|
+
missing.push({
|
|
6751
|
+
name: "Screen Recording",
|
|
6752
|
+
reason: "CGPreflightScreenCaptureAccess returned false",
|
|
6753
|
+
pane: "screen-recording",
|
|
6754
|
+
settingsUrl: SCREEN_RECORDING_URL,
|
|
6755
|
+
fixSteps: [
|
|
6756
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Screen Recording pane that opened",
|
|
6757
|
+
"Click the + button",
|
|
6758
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
6759
|
+
"Toggle the switch ON",
|
|
6760
|
+
"Restart the agent process so the new permission takes effect",
|
|
6761
|
+
"Then call enable_computer_use again"
|
|
6762
|
+
],
|
|
6763
|
+
prompted,
|
|
6764
|
+
panelOpened
|
|
6765
|
+
});
|
|
6766
|
+
}
|
|
6767
|
+
if (missing.length > 0) {
|
|
6768
|
+
return {
|
|
6769
|
+
success: false,
|
|
6770
|
+
error: `Missing permission${missing.length > 1 ? "s" : ""}: ` + missing.map((m) => m.name).join(" and ") + ".",
|
|
6771
|
+
missingPermissions: missing,
|
|
6772
|
+
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."
|
|
6773
|
+
};
|
|
6774
|
+
}
|
|
6775
|
+
let width = display_width;
|
|
6776
|
+
let height = display_height;
|
|
6777
|
+
let detected = null;
|
|
6778
|
+
if (width === void 0 || height === void 0) {
|
|
6779
|
+
detected = await detectScreenSize();
|
|
6780
|
+
width = width ?? detected?.width ?? 1280;
|
|
6781
|
+
height = height ?? detected?.height ?? 800;
|
|
6782
|
+
}
|
|
6783
|
+
const session = await sessionQueries.getById(options.sessionId);
|
|
6784
|
+
if (!session) {
|
|
6785
|
+
return { success: false, error: "Session not found" };
|
|
6786
|
+
}
|
|
6787
|
+
const config = session.config || {};
|
|
6788
|
+
if (config.computerUseEnabled === true && config.computerUseDisplayWidth === width && config.computerUseDisplayHeight === height) {
|
|
6789
|
+
return {
|
|
6790
|
+
success: true,
|
|
6791
|
+
alreadyEnabled: true,
|
|
6792
|
+
message: "Computer use was already enabled for this session.",
|
|
6793
|
+
displayWidth: width,
|
|
6794
|
+
displayHeight: height
|
|
6795
|
+
};
|
|
6796
|
+
}
|
|
6797
|
+
const updated = {
|
|
6798
|
+
...config,
|
|
6799
|
+
computerUseEnabled: true,
|
|
6800
|
+
computerUseDisplayWidth: width,
|
|
6801
|
+
computerUseDisplayHeight: height
|
|
6802
|
+
};
|
|
6803
|
+
await sessionQueries.update(options.sessionId, { config: updated });
|
|
6804
|
+
return {
|
|
6805
|
+
success: true,
|
|
6806
|
+
enabled: true,
|
|
6807
|
+
platform: "darwin",
|
|
6808
|
+
displayWidth: width,
|
|
6809
|
+
displayHeight: height,
|
|
6810
|
+
detectedScreenSize: detected || void 0,
|
|
6811
|
+
permissions: {
|
|
6812
|
+
accessibility: "granted",
|
|
6813
|
+
screenRecording: "granted"
|
|
6814
|
+
},
|
|
6815
|
+
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.`
|
|
6816
|
+
};
|
|
6817
|
+
} catch (err) {
|
|
6818
|
+
return {
|
|
6819
|
+
success: false,
|
|
6820
|
+
error: err?.message || String(err)
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6173
6827
|
// src/tools/index.ts
|
|
6174
6828
|
init_semantic();
|
|
6175
6829
|
init_remote();
|
|
@@ -6218,6 +6872,20 @@ async function createTools(options) {
|
|
|
6218
6872
|
sessionId: options.sessionId
|
|
6219
6873
|
});
|
|
6220
6874
|
}
|
|
6875
|
+
if (process.platform === "darwin") {
|
|
6876
|
+
if (options.enableComputerUse) {
|
|
6877
|
+
tools.computer = createComputerUseTool({
|
|
6878
|
+
workingDirectory: options.workingDirectory,
|
|
6879
|
+
sessionId: options.sessionId,
|
|
6880
|
+
displayWidth: options.computerUseDisplayWidth,
|
|
6881
|
+
displayHeight: options.computerUseDisplayHeight
|
|
6882
|
+
});
|
|
6883
|
+
} else {
|
|
6884
|
+
tools.enable_computer_use = createEnableComputerUseTool({
|
|
6885
|
+
sessionId: options.sessionId
|
|
6886
|
+
});
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6221
6889
|
if (options.enableSemanticSearch !== false) {
|
|
6222
6890
|
try {
|
|
6223
6891
|
if (isVectorGatewayConfigured()) {
|
|
@@ -6234,6 +6902,7 @@ async function createTools(options) {
|
|
|
6234
6902
|
if (options.taskTools) {
|
|
6235
6903
|
tools.complete_task = createCompleteTaskTool(options.taskTools);
|
|
6236
6904
|
tools.task_failed = createTaskFailedTool(options.taskTools);
|
|
6905
|
+
tools.ask_question_to_user = createAskQuestionToUserTool(options.taskTools);
|
|
6237
6906
|
}
|
|
6238
6907
|
return tools;
|
|
6239
6908
|
}
|
|
@@ -6248,11 +6917,11 @@ init_db();
|
|
|
6248
6917
|
init_todo();
|
|
6249
6918
|
import os from "os";
|
|
6250
6919
|
function getSearchInstructions() {
|
|
6251
|
-
const
|
|
6920
|
+
const platform5 = process.platform;
|
|
6252
6921
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
6253
6922
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
6254
6923
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
6255
|
-
if (
|
|
6924
|
+
if (platform5 === "win32") {
|
|
6256
6925
|
return `${common}
|
|
6257
6926
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
6258
6927
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -6299,13 +6968,13 @@ async function buildSystemPrompt(options) {
|
|
|
6299
6968
|
);
|
|
6300
6969
|
const hasNoTodos = todos.length === 0;
|
|
6301
6970
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6302
|
-
const
|
|
6971
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6303
6972
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6304
6973
|
const searchInstructions = getSearchInstructions();
|
|
6305
6974
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
6306
6975
|
|
|
6307
6976
|
## Environment
|
|
6308
|
-
- **Platform**: ${
|
|
6977
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
6309
6978
|
- **Date**: ${currentDate}
|
|
6310
6979
|
- **Working Directory**: ${workingDirectory}
|
|
6311
6980
|
|
|
@@ -6641,9 +7310,10 @@ If you need to give the user a downloadable file (report, image, export, etc.),
|
|
|
6641
7310
|
### Rules
|
|
6642
7311
|
1. Work independently \u2014 no human will approve tool calls. All tools run without approval.
|
|
6643
7312
|
2. Keep working until the task is fully complete \u2014 and then VERIFY it is complete before finishing.
|
|
6644
|
-
3.
|
|
6645
|
-
4.
|
|
6646
|
-
5.
|
|
7313
|
+
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.
|
|
7314
|
+
4. When done, call the \`complete_task\` tool with a JSON result matching the output schema below.
|
|
7315
|
+
5. If you determine the task is impossible or encounter an unrecoverable error, call the \`task_failed\` tool with a clear reason.
|
|
7316
|
+
6. Do NOT stop without calling \`complete_task\`, \`task_failed\`, or \`ask_question_to_user\` when blocked.
|
|
6647
7317
|
|
|
6648
7318
|
### Verification \u2014 BE EXTREMELY THOROUGH
|
|
6649
7319
|
Before calling \`complete_task\`, you MUST verify your work completely. Do not just assume it worked. Actually check.
|
|
@@ -6714,6 +7384,7 @@ ${JSON.stringify(outputSchema, null, 2)}
|
|
|
6714
7384
|
### Completion Tools
|
|
6715
7385
|
- **\`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.
|
|
6716
7386
|
- **\`task_failed({ reason: "..." })\`** \u2014 Call only if the task truly cannot be completed.
|
|
7387
|
+
- **\`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.
|
|
6717
7388
|
`;
|
|
6718
7389
|
}
|
|
6719
7390
|
function createSummaryPrompt(conversationHistory) {
|
|
@@ -6869,6 +7540,7 @@ function sanitizeModelMessages(messages) {
|
|
|
6869
7540
|
|
|
6870
7541
|
// src/agent/model-limits.ts
|
|
6871
7542
|
var MODEL_LIMITS = {
|
|
7543
|
+
"anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6872
7544
|
"anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6873
7545
|
"anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
6874
7546
|
"anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
@@ -7205,17 +7877,65 @@ function repairToolPairing(messages) {
|
|
|
7205
7877
|
|
|
7206
7878
|
// src/agent/index.ts
|
|
7207
7879
|
init_webhook();
|
|
7880
|
+
|
|
7881
|
+
// src/tasks/questions.ts
|
|
7882
|
+
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
7883
|
+
var answeredQuestions = /* @__PURE__ */ new Map();
|
|
7884
|
+
function key(taskId, questionId) {
|
|
7885
|
+
return `${taskId}:${questionId}`;
|
|
7886
|
+
}
|
|
7887
|
+
function waitForTaskQuestionAnswer(question) {
|
|
7888
|
+
const k = key(question.taskId, question.questionId);
|
|
7889
|
+
if (pendingQuestions.has(k)) {
|
|
7890
|
+
return Promise.reject(new Error(`Question already pending: ${question.questionId}`));
|
|
7891
|
+
}
|
|
7892
|
+
return new Promise((resolve11, reject) => {
|
|
7893
|
+
pendingQuestions.set(k, {
|
|
7894
|
+
...question,
|
|
7895
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
7896
|
+
resolve: resolve11,
|
|
7897
|
+
reject
|
|
7898
|
+
});
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
function answerTaskQuestion(taskId, questionId, answer) {
|
|
7902
|
+
const k = key(taskId, questionId);
|
|
7903
|
+
const pending = pendingQuestions.get(k);
|
|
7904
|
+
if (!pending) return answeredQuestions.has(k) ? "already_answered" : "not_found";
|
|
7905
|
+
pendingQuestions.delete(k);
|
|
7906
|
+
answeredQuestions.set(k, answer);
|
|
7907
|
+
pending.resolve(answer);
|
|
7908
|
+
return "answered";
|
|
7909
|
+
}
|
|
7910
|
+
function rejectTaskQuestions(taskId, reason) {
|
|
7911
|
+
for (const [k, pending] of pendingQuestions) {
|
|
7912
|
+
if (pending.taskId !== taskId) continue;
|
|
7913
|
+
pendingQuestions.delete(k);
|
|
7914
|
+
pending.reject(new Error(reason));
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
function getPendingTaskQuestion(taskId, questionId) {
|
|
7918
|
+
const pending = pendingQuestions.get(key(taskId, questionId));
|
|
7919
|
+
if (!pending) return null;
|
|
7920
|
+
const { resolve: _resolve, reject: _reject, ...question } = pending;
|
|
7921
|
+
return question;
|
|
7922
|
+
}
|
|
7923
|
+
function getAnsweredTaskQuestion(taskId, questionId) {
|
|
7924
|
+
return answeredQuestions.get(key(taskId, questionId)) ?? null;
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7927
|
+
// src/agent/index.ts
|
|
7208
7928
|
var MAX_SSE_FIELD_LENGTH = 8 * 1024;
|
|
7209
7929
|
var SSE_PREVIEW_LENGTH = 2 * 1024;
|
|
7210
7930
|
function truncateWriteFileInput(input) {
|
|
7211
7931
|
const out = { ...input };
|
|
7212
|
-
for (const
|
|
7213
|
-
const val = out[
|
|
7932
|
+
for (const key2 of ["content", "old_string", "new_string"]) {
|
|
7933
|
+
const val = out[key2];
|
|
7214
7934
|
if (typeof val === "string" && val.length > MAX_SSE_FIELD_LENGTH) {
|
|
7215
|
-
out[
|
|
7935
|
+
out[key2] = `${val.slice(0, SSE_PREVIEW_LENGTH)}
|
|
7216
7936
|
... (truncated)`;
|
|
7217
|
-
out[`${
|
|
7218
|
-
out[`${
|
|
7937
|
+
out[`${key2}Truncated`] = true;
|
|
7938
|
+
out[`${key2}Length`] = val.length;
|
|
7219
7939
|
}
|
|
7220
7940
|
}
|
|
7221
7941
|
return out;
|
|
@@ -7243,10 +7963,14 @@ var Agent = class _Agent {
|
|
|
7243
7963
|
*/
|
|
7244
7964
|
async createToolsWithCallbacks(options) {
|
|
7245
7965
|
const config = getConfig();
|
|
7966
|
+
const sessionConfig = this.session.config || {};
|
|
7246
7967
|
return createTools({
|
|
7247
7968
|
sessionId: this.session.id,
|
|
7248
7969
|
workingDirectory: this.session.workingDirectory,
|
|
7249
7970
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
7971
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
7972
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
7973
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
7250
7974
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
7251
7975
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
7252
7976
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -7279,10 +8003,14 @@ var Agent = class _Agent {
|
|
|
7279
8003
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
7280
8004
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
7281
8005
|
});
|
|
8006
|
+
const sessionConfig = session.config || {};
|
|
7282
8007
|
const tools = await createTools({
|
|
7283
8008
|
sessionId: session.id,
|
|
7284
8009
|
workingDirectory: session.workingDirectory,
|
|
7285
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
8010
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8011
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
8012
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
8013
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
7286
8014
|
});
|
|
7287
8015
|
return new _Agent(session, context, tools);
|
|
7288
8016
|
}
|
|
@@ -7377,13 +8105,7 @@ ${prompt}` });
|
|
|
7377
8105
|
abortSignal: options.abortSignal,
|
|
7378
8106
|
// Enable extended thinking/reasoning for models that support it
|
|
7379
8107
|
providerOptions: useAnthropic ? {
|
|
7380
|
-
anthropic: {
|
|
7381
|
-
toolStreaming: true,
|
|
7382
|
-
thinking: {
|
|
7383
|
-
type: "enabled",
|
|
7384
|
-
budgetTokens: 1e4
|
|
7385
|
-
}
|
|
7386
|
-
}
|
|
8108
|
+
anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
|
|
7387
8109
|
} : void 0,
|
|
7388
8110
|
onStepFinish: async (step) => {
|
|
7389
8111
|
options.onStepFinish?.(step);
|
|
@@ -7430,12 +8152,7 @@ ${prompt}` });
|
|
|
7430
8152
|
stopWhen: stepCountIs2(500),
|
|
7431
8153
|
// Enable extended thinking/reasoning for models that support it
|
|
7432
8154
|
providerOptions: useAnthropic ? {
|
|
7433
|
-
anthropic:
|
|
7434
|
-
thinking: {
|
|
7435
|
-
type: "enabled",
|
|
7436
|
-
budgetTokens: 1e4
|
|
7437
|
-
}
|
|
7438
|
-
}
|
|
8155
|
+
anthropic: getAnthropicProviderOptions(this.session.model)
|
|
7439
8156
|
} : void 0
|
|
7440
8157
|
});
|
|
7441
8158
|
const responseMessages = result.response.messages;
|
|
@@ -7455,10 +8172,10 @@ ${prompt}` });
|
|
|
7455
8172
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
7456
8173
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
7457
8174
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
7458
|
-
const fireWebhook = (
|
|
8175
|
+
const fireWebhook = (type2, data) => {
|
|
7459
8176
|
if (!webhookUrl) return;
|
|
7460
8177
|
sendWebhook(webhookUrl, {
|
|
7461
|
-
type,
|
|
8178
|
+
type: type2,
|
|
7462
8179
|
taskId: this.session.id,
|
|
7463
8180
|
sessionId: this.session.id,
|
|
7464
8181
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -7501,10 +8218,14 @@ ${prompt}` });
|
|
|
7501
8218
|
});
|
|
7502
8219
|
}
|
|
7503
8220
|
};
|
|
8221
|
+
const taskSessionConfig = this.session.config || {};
|
|
7504
8222
|
const taskTools = await createTools({
|
|
7505
8223
|
sessionId: this.session.id,
|
|
7506
8224
|
workingDirectory: this.session.workingDirectory,
|
|
7507
8225
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
8226
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
8227
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
8228
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
7508
8229
|
onBashProgress: bashProgressHandler,
|
|
7509
8230
|
onWriteFileProgress: (progress) => {
|
|
7510
8231
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -7518,7 +8239,38 @@ ${prompt}` });
|
|
|
7518
8239
|
},
|
|
7519
8240
|
taskTools: {
|
|
7520
8241
|
outputSchema: options.taskConfig.outputSchema,
|
|
7521
|
-
onComplete
|
|
8242
|
+
onComplete,
|
|
8243
|
+
onQuestion: async (question) => {
|
|
8244
|
+
const payload = {
|
|
8245
|
+
questionId: question.questionId,
|
|
8246
|
+
question: question.question,
|
|
8247
|
+
context: question.context,
|
|
8248
|
+
choices: question.choices,
|
|
8249
|
+
status: "pending"
|
|
8250
|
+
};
|
|
8251
|
+
const answerPromise = waitForTaskQuestionAnswer({
|
|
8252
|
+
taskId: this.session.id,
|
|
8253
|
+
questionId: question.questionId,
|
|
8254
|
+
question: question.question,
|
|
8255
|
+
context: question.context,
|
|
8256
|
+
choices: question.choices
|
|
8257
|
+
});
|
|
8258
|
+
fireWebhook("task.question", payload);
|
|
8259
|
+
if (emit) {
|
|
8260
|
+
await emit(JSON.stringify({ type: "task-question", data: payload }));
|
|
8261
|
+
}
|
|
8262
|
+
const answer = await answerPromise;
|
|
8263
|
+
const answeredPayload = {
|
|
8264
|
+
questionId: question.questionId,
|
|
8265
|
+
answer: answer.answer,
|
|
8266
|
+
answeredBy: answer.answeredBy
|
|
8267
|
+
};
|
|
8268
|
+
fireWebhook("task.question_answered", answeredPayload);
|
|
8269
|
+
if (emit) {
|
|
8270
|
+
await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
|
|
8271
|
+
}
|
|
8272
|
+
return answer;
|
|
8273
|
+
}
|
|
7522
8274
|
}
|
|
7523
8275
|
});
|
|
7524
8276
|
const baseSystemPrompt = await buildSystemPrompt({
|
|
@@ -7563,10 +8315,7 @@ ${taskAddendum}`;
|
|
|
7563
8315
|
stopWhen: stepCountIs2(500),
|
|
7564
8316
|
abortSignal: options.abortSignal,
|
|
7565
8317
|
providerOptions: useAnthropic ? {
|
|
7566
|
-
anthropic: {
|
|
7567
|
-
toolStreaming: true,
|
|
7568
|
-
thinking: { type: "enabled", budgetTokens: 1e4 }
|
|
7569
|
-
}
|
|
8318
|
+
anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
|
|
7570
8319
|
} : void 0,
|
|
7571
8320
|
onStepFinish: async (step) => {
|
|
7572
8321
|
options.onStepFinish?.(step);
|
|
@@ -7787,11 +8536,11 @@ ${taskAddendum}`;
|
|
|
7787
8536
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7788
8537
|
if (!isRemoteConfigured2()) return [];
|
|
7789
8538
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
7790
|
-
const { join:
|
|
8539
|
+
const { join: join14, basename: basename6 } = await import("path");
|
|
7791
8540
|
const urls = [];
|
|
7792
8541
|
for (const filePath of filePaths) {
|
|
7793
8542
|
try {
|
|
7794
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
8543
|
+
const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
|
|
7795
8544
|
const fileName = basename6(fullPath);
|
|
7796
8545
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
7797
8546
|
const mimeMap = {
|
|
@@ -7849,11 +8598,11 @@ ${taskAddendum}`;
|
|
|
7849
8598
|
wrappedTools[name] = originalTool;
|
|
7850
8599
|
continue;
|
|
7851
8600
|
}
|
|
7852
|
-
wrappedTools[name] =
|
|
8601
|
+
wrappedTools[name] = tool14({
|
|
7853
8602
|
description: originalTool.description || "",
|
|
7854
|
-
inputSchema: originalTool.inputSchema ||
|
|
8603
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
7855
8604
|
execute: async (input, toolOptions) => {
|
|
7856
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
8605
|
+
const toolCallId = toolOptions.toolCallId || nanoid6();
|
|
7857
8606
|
const execution = toolExecutionQueries.create({
|
|
7858
8607
|
sessionId: this.session.id,
|
|
7859
8608
|
toolName: name,
|
|
@@ -7871,10 +8620,10 @@ ${taskAddendum}`;
|
|
|
7871
8620
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
7872
8621
|
approvalResolvers.delete(toolCallId);
|
|
7873
8622
|
this.pendingApprovals.delete(toolCallId);
|
|
7874
|
-
const
|
|
8623
|
+
const exec8 = await execution;
|
|
7875
8624
|
if (!approved) {
|
|
7876
8625
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
7877
|
-
await toolExecutionQueries.reject(
|
|
8626
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
7878
8627
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7879
8628
|
return {
|
|
7880
8629
|
status: "rejected",
|
|
@@ -7884,14 +8633,14 @@ ${taskAddendum}`;
|
|
|
7884
8633
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
7885
8634
|
};
|
|
7886
8635
|
}
|
|
7887
|
-
await toolExecutionQueries.approve(
|
|
8636
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
7888
8637
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
7889
8638
|
try {
|
|
7890
8639
|
const result = await originalTool.execute(input, toolOptions);
|
|
7891
|
-
await toolExecutionQueries.complete(
|
|
8640
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
7892
8641
|
return result;
|
|
7893
8642
|
} catch (error) {
|
|
7894
|
-
await toolExecutionQueries.complete(
|
|
8643
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
7895
8644
|
throw error;
|
|
7896
8645
|
}
|
|
7897
8646
|
}
|
|
@@ -7992,18 +8741,20 @@ function cleanupPendingInputs() {
|
|
|
7992
8741
|
}
|
|
7993
8742
|
}
|
|
7994
8743
|
}
|
|
7995
|
-
var createSessionSchema =
|
|
7996
|
-
name:
|
|
7997
|
-
workingDirectory:
|
|
7998
|
-
model:
|
|
7999
|
-
toolApprovals:
|
|
8744
|
+
var createSessionSchema = z16.object({
|
|
8745
|
+
name: z16.string().optional(),
|
|
8746
|
+
workingDirectory: z16.string().optional(),
|
|
8747
|
+
model: z16.string().optional(),
|
|
8748
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional(),
|
|
8749
|
+
// Optional full session-config passthrough (computerUseEnabled, etc.)
|
|
8750
|
+
config: z16.record(z16.string(), z16.unknown()).optional()
|
|
8000
8751
|
});
|
|
8001
|
-
var paginationQuerySchema =
|
|
8002
|
-
limit:
|
|
8003
|
-
offset:
|
|
8752
|
+
var paginationQuerySchema = z16.object({
|
|
8753
|
+
limit: z16.string().optional(),
|
|
8754
|
+
offset: z16.string().optional()
|
|
8004
8755
|
});
|
|
8005
|
-
var messagesQuerySchema =
|
|
8006
|
-
limit:
|
|
8756
|
+
var messagesQuerySchema = z16.object({
|
|
8757
|
+
limit: z16.string().optional()
|
|
8007
8758
|
});
|
|
8008
8759
|
sessions.get(
|
|
8009
8760
|
"/",
|
|
@@ -8041,11 +8792,20 @@ sessions.post(
|
|
|
8041
8792
|
async (c) => {
|
|
8042
8793
|
const body = c.req.valid("json");
|
|
8043
8794
|
const config = getConfig();
|
|
8795
|
+
const cuDefault = process.env.SPARKECODER_COMPUTER_USE === "1";
|
|
8796
|
+
const baseConfig = body.config || {};
|
|
8797
|
+
const mergedConfig = {
|
|
8798
|
+
...baseConfig,
|
|
8799
|
+
...body.toolApprovals ? { toolApprovals: body.toolApprovals } : {},
|
|
8800
|
+
// Turn on computer use by default if the server was launched with --enable-computer-use,
|
|
8801
|
+
// unless the client explicitly provided a value.
|
|
8802
|
+
...cuDefault && baseConfig.computerUseEnabled === void 0 ? { computerUseEnabled: true } : {}
|
|
8803
|
+
};
|
|
8044
8804
|
const agent = await Agent.create({
|
|
8045
8805
|
name: body.name,
|
|
8046
8806
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8047
8807
|
model: body.model || config.defaultModel,
|
|
8048
|
-
sessionConfig:
|
|
8808
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8049
8809
|
});
|
|
8050
8810
|
const session = agent.getSession();
|
|
8051
8811
|
return c.json({
|
|
@@ -8142,10 +8902,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8142
8902
|
count: executions.length
|
|
8143
8903
|
});
|
|
8144
8904
|
});
|
|
8145
|
-
var updateSessionSchema =
|
|
8146
|
-
model:
|
|
8147
|
-
name:
|
|
8148
|
-
toolApprovals:
|
|
8905
|
+
var updateSessionSchema = z16.object({
|
|
8906
|
+
model: z16.string().optional(),
|
|
8907
|
+
name: z16.string().optional(),
|
|
8908
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
|
|
8149
8909
|
});
|
|
8150
8910
|
sessions.patch(
|
|
8151
8911
|
"/:id",
|
|
@@ -8215,8 +8975,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8215
8975
|
await agent.clearContext();
|
|
8216
8976
|
return c.json({ success: true, sessionId: id });
|
|
8217
8977
|
});
|
|
8218
|
-
var pendingInputSchema =
|
|
8219
|
-
text:
|
|
8978
|
+
var pendingInputSchema = z16.object({
|
|
8979
|
+
text: z16.string()
|
|
8220
8980
|
});
|
|
8221
8981
|
sessions.post(
|
|
8222
8982
|
"/:id/pending-input",
|
|
@@ -8247,13 +9007,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
8247
9007
|
createdAt: pending.createdAt.toISOString()
|
|
8248
9008
|
});
|
|
8249
9009
|
});
|
|
8250
|
-
var devtoolsContextSchema =
|
|
8251
|
-
url:
|
|
8252
|
-
path:
|
|
8253
|
-
pageName:
|
|
8254
|
-
screenWidth:
|
|
8255
|
-
screenHeight:
|
|
8256
|
-
devicePixelRatio:
|
|
9010
|
+
var devtoolsContextSchema = z16.object({
|
|
9011
|
+
url: z16.string(),
|
|
9012
|
+
path: z16.string(),
|
|
9013
|
+
pageName: z16.string().optional(),
|
|
9014
|
+
screenWidth: z16.number().optional(),
|
|
9015
|
+
screenHeight: z16.number().optional(),
|
|
9016
|
+
devicePixelRatio: z16.number().optional()
|
|
8257
9017
|
});
|
|
8258
9018
|
sessions.post(
|
|
8259
9019
|
"/:id/devtools-context",
|
|
@@ -8439,12 +9199,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
8439
9199
|
});
|
|
8440
9200
|
function getAttachmentsDir(sessionId) {
|
|
8441
9201
|
const appDataDir = getAppDataDirectory();
|
|
8442
|
-
return
|
|
9202
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
8443
9203
|
}
|
|
8444
9204
|
function ensureAttachmentsDir(sessionId) {
|
|
8445
9205
|
const dir = getAttachmentsDir(sessionId);
|
|
8446
|
-
if (!
|
|
8447
|
-
|
|
9206
|
+
if (!existsSync16(dir)) {
|
|
9207
|
+
mkdirSync6(dir, { recursive: true });
|
|
8448
9208
|
}
|
|
8449
9209
|
return dir;
|
|
8450
9210
|
}
|
|
@@ -8455,12 +9215,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
8455
9215
|
return c.json({ error: "Session not found" }, 404);
|
|
8456
9216
|
}
|
|
8457
9217
|
const dir = getAttachmentsDir(sessionId);
|
|
8458
|
-
if (!
|
|
9218
|
+
if (!existsSync16(dir)) {
|
|
8459
9219
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
8460
9220
|
}
|
|
8461
9221
|
const files = readdirSync2(dir);
|
|
8462
9222
|
const attachments = files.map((filename) => {
|
|
8463
|
-
const filePath =
|
|
9223
|
+
const filePath = join10(dir, filename);
|
|
8464
9224
|
const stats = statSync2(filePath);
|
|
8465
9225
|
return {
|
|
8466
9226
|
id: filename.split("_")[0],
|
|
@@ -8492,10 +9252,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8492
9252
|
return c.json({ error: "No file provided" }, 400);
|
|
8493
9253
|
}
|
|
8494
9254
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8495
|
-
const id =
|
|
9255
|
+
const id = nanoid7(10);
|
|
8496
9256
|
const ext = extname8(file.name) || "";
|
|
8497
9257
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8498
|
-
const filePath =
|
|
9258
|
+
const filePath = join10(dir, safeFilename);
|
|
8499
9259
|
const arrayBuffer = await file.arrayBuffer();
|
|
8500
9260
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
8501
9261
|
return c.json({
|
|
@@ -8518,10 +9278,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
8518
9278
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
8519
9279
|
}
|
|
8520
9280
|
const dir = ensureAttachmentsDir(sessionId);
|
|
8521
|
-
const id =
|
|
9281
|
+
const id = nanoid7(10);
|
|
8522
9282
|
const ext = extname8(body.filename) || "";
|
|
8523
9283
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8524
|
-
const filePath =
|
|
9284
|
+
const filePath = join10(dir, safeFilename);
|
|
8525
9285
|
let base64Data = body.data;
|
|
8526
9286
|
if (base64Data.includes(",")) {
|
|
8527
9287
|
base64Data = base64Data.split(",")[1];
|
|
@@ -8550,7 +9310,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8550
9310
|
return c.json({ error: "Session not found" }, 404);
|
|
8551
9311
|
}
|
|
8552
9312
|
const dir = getAttachmentsDir(sessionId);
|
|
8553
|
-
if (!
|
|
9313
|
+
if (!existsSync16(dir)) {
|
|
8554
9314
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8555
9315
|
}
|
|
8556
9316
|
const files = readdirSync2(dir);
|
|
@@ -8558,14 +9318,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
8558
9318
|
if (!file) {
|
|
8559
9319
|
return c.json({ error: "Attachment not found" }, 404);
|
|
8560
9320
|
}
|
|
8561
|
-
const filePath =
|
|
8562
|
-
|
|
9321
|
+
const filePath = join10(dir, file);
|
|
9322
|
+
unlinkSync3(filePath);
|
|
8563
9323
|
return c.json({ success: true, id: attachmentId });
|
|
8564
9324
|
});
|
|
8565
|
-
var filesQuerySchema =
|
|
8566
|
-
query:
|
|
9325
|
+
var filesQuerySchema = z16.object({
|
|
9326
|
+
query: z16.string().optional(),
|
|
8567
9327
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
8568
|
-
limit:
|
|
9328
|
+
limit: z16.string().optional()
|
|
8569
9329
|
// Max results (default 50)
|
|
8570
9330
|
});
|
|
8571
9331
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -8641,7 +9401,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
8641
9401
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
8642
9402
|
for (const entry of entries) {
|
|
8643
9403
|
if (results.length >= limit * 2) break;
|
|
8644
|
-
const fullPath =
|
|
9404
|
+
const fullPath = join10(currentDir, entry.name);
|
|
8645
9405
|
const relativePath = relative9(baseDir, fullPath);
|
|
8646
9406
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
8647
9407
|
continue;
|
|
@@ -8689,7 +9449,7 @@ sessions.get(
|
|
|
8689
9449
|
return c.json({ error: "Session not found" }, 404);
|
|
8690
9450
|
}
|
|
8691
9451
|
const workingDirectory = session.workingDirectory;
|
|
8692
|
-
if (!
|
|
9452
|
+
if (!existsSync16(workingDirectory)) {
|
|
8693
9453
|
return c.json({
|
|
8694
9454
|
sessionId,
|
|
8695
9455
|
workingDirectory,
|
|
@@ -8799,9 +9559,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
8799
9559
|
init_db();
|
|
8800
9560
|
import { Hono as Hono2 } from "hono";
|
|
8801
9561
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
8802
|
-
import { z as
|
|
8803
|
-
import { existsSync as
|
|
8804
|
-
import { join as
|
|
9562
|
+
import { z as z17 } from "zod";
|
|
9563
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
9564
|
+
import { join as join11 } from "path";
|
|
8805
9565
|
init_config();
|
|
8806
9566
|
|
|
8807
9567
|
// src/server/resumable-stream.ts
|
|
@@ -8810,9 +9570,9 @@ var store = /* @__PURE__ */ new Map();
|
|
|
8810
9570
|
var channels = /* @__PURE__ */ new Map();
|
|
8811
9571
|
var cleanupInterval = setInterval(() => {
|
|
8812
9572
|
const now = Date.now();
|
|
8813
|
-
for (const [
|
|
9573
|
+
for (const [key2, data] of store.entries()) {
|
|
8814
9574
|
if (data.expiresAt && data.expiresAt < now) {
|
|
8815
|
-
store.delete(
|
|
9575
|
+
store.delete(key2);
|
|
8816
9576
|
}
|
|
8817
9577
|
}
|
|
8818
9578
|
}, 6e4);
|
|
@@ -8836,27 +9596,27 @@ var publisher = {
|
|
|
8836
9596
|
}
|
|
8837
9597
|
}
|
|
8838
9598
|
},
|
|
8839
|
-
set: async (
|
|
9599
|
+
set: async (key2, value, options) => {
|
|
8840
9600
|
const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
|
|
8841
|
-
store.set(
|
|
9601
|
+
store.set(key2, { value, expiresAt });
|
|
8842
9602
|
if (options?.EX) {
|
|
8843
|
-
setTimeout(() => store.delete(
|
|
9603
|
+
setTimeout(() => store.delete(key2), options.EX * 1e3);
|
|
8844
9604
|
}
|
|
8845
9605
|
},
|
|
8846
|
-
get: async (
|
|
8847
|
-
const data = store.get(
|
|
9606
|
+
get: async (key2) => {
|
|
9607
|
+
const data = store.get(key2);
|
|
8848
9608
|
if (!data) return null;
|
|
8849
9609
|
if (data.expiresAt && data.expiresAt < Date.now()) {
|
|
8850
|
-
store.delete(
|
|
9610
|
+
store.delete(key2);
|
|
8851
9611
|
return null;
|
|
8852
9612
|
}
|
|
8853
9613
|
return data.value;
|
|
8854
9614
|
},
|
|
8855
|
-
incr: async (
|
|
8856
|
-
const data = store.get(
|
|
9615
|
+
incr: async (key2) => {
|
|
9616
|
+
const data = store.get(key2);
|
|
8857
9617
|
const current = data ? parseInt(data.value, 10) : 0;
|
|
8858
9618
|
const next = (isNaN(current) ? 0 : current) + 1;
|
|
8859
|
-
store.set(
|
|
9619
|
+
store.set(key2, { value: String(next), expiresAt: data?.expiresAt });
|
|
8860
9620
|
return next;
|
|
8861
9621
|
}
|
|
8862
9622
|
};
|
|
@@ -8888,7 +9648,7 @@ var streamContext = createResumableStreamContext({
|
|
|
8888
9648
|
});
|
|
8889
9649
|
|
|
8890
9650
|
// src/server/routes/agents.ts
|
|
8891
|
-
import { nanoid as
|
|
9651
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
8892
9652
|
init_stream_proxy();
|
|
8893
9653
|
init_recorder();
|
|
8894
9654
|
init_remote();
|
|
@@ -8979,40 +9739,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
8979
9739
|
${prompt}`;
|
|
8980
9740
|
}
|
|
8981
9741
|
var agents = new Hono2();
|
|
8982
|
-
var attachmentSchema =
|
|
8983
|
-
type:
|
|
8984
|
-
data:
|
|
9742
|
+
var attachmentSchema = z17.object({
|
|
9743
|
+
type: z17.enum(["image", "file"]),
|
|
9744
|
+
data: z17.string(),
|
|
8985
9745
|
// base64 data URL or raw base64
|
|
8986
|
-
mediaType:
|
|
8987
|
-
filename:
|
|
9746
|
+
mediaType: z17.string().optional(),
|
|
9747
|
+
filename: z17.string().optional()
|
|
8988
9748
|
});
|
|
8989
|
-
var runPromptSchema =
|
|
8990
|
-
prompt:
|
|
9749
|
+
var runPromptSchema = z17.object({
|
|
9750
|
+
prompt: z17.string(),
|
|
8991
9751
|
// Can be empty if attachments are provided
|
|
8992
|
-
attachments:
|
|
9752
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
8993
9753
|
}).refine(
|
|
8994
9754
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
8995
9755
|
{ message: "Either prompt or attachments must be provided" }
|
|
8996
9756
|
);
|
|
8997
|
-
var quickStartSchema =
|
|
8998
|
-
prompt:
|
|
8999
|
-
name:
|
|
9000
|
-
workingDirectory:
|
|
9001
|
-
model:
|
|
9002
|
-
toolApprovals:
|
|
9757
|
+
var quickStartSchema = z17.object({
|
|
9758
|
+
prompt: z17.string().min(1),
|
|
9759
|
+
name: z17.string().optional(),
|
|
9760
|
+
workingDirectory: z17.string().optional(),
|
|
9761
|
+
model: z17.string().optional(),
|
|
9762
|
+
toolApprovals: z17.record(z17.string(), z17.boolean()).optional()
|
|
9003
9763
|
});
|
|
9004
|
-
var rejectSchema =
|
|
9005
|
-
reason:
|
|
9764
|
+
var rejectSchema = z17.object({
|
|
9765
|
+
reason: z17.string().optional()
|
|
9006
9766
|
}).optional();
|
|
9007
9767
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9008
9768
|
function getAttachmentsDirectory(sessionId) {
|
|
9009
9769
|
const appDataDir = getAppDataDirectory();
|
|
9010
|
-
return
|
|
9770
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9011
9771
|
}
|
|
9012
9772
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9013
9773
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9014
|
-
if (!
|
|
9015
|
-
|
|
9774
|
+
if (!existsSync17(attachmentsDir)) {
|
|
9775
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9016
9776
|
}
|
|
9017
9777
|
let filename = attachment.filename;
|
|
9018
9778
|
if (!filename) {
|
|
@@ -9030,7 +9790,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9030
9790
|
attachment.mediaType = resized.mediaType;
|
|
9031
9791
|
attachment.data = buffer.toString("base64");
|
|
9032
9792
|
}
|
|
9033
|
-
const filePath =
|
|
9793
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9034
9794
|
writeFileSync4(filePath, buffer);
|
|
9035
9795
|
return filePath;
|
|
9036
9796
|
}
|
|
@@ -9041,9 +9801,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9041
9801
|
}
|
|
9042
9802
|
return data;
|
|
9043
9803
|
}
|
|
9044
|
-
function getExtensionFromMediaType(mediaType,
|
|
9804
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9045
9805
|
if (!mediaType) {
|
|
9046
|
-
return
|
|
9806
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9047
9807
|
}
|
|
9048
9808
|
const mimeToExt = {
|
|
9049
9809
|
"image/png": ".png",
|
|
@@ -9447,7 +10207,7 @@ ${prompt}` });
|
|
|
9447
10207
|
userMessageContent = prompt;
|
|
9448
10208
|
}
|
|
9449
10209
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
9450
|
-
const streamId = `stream_${id}_${
|
|
10210
|
+
const streamId = `stream_${id}_${nanoid8(10)}`;
|
|
9451
10211
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
9452
10212
|
await activeStreamQueries.create(id, streamId);
|
|
9453
10213
|
const stream = await streamContext.resumableStream(
|
|
@@ -9652,7 +10412,7 @@ agents.post(
|
|
|
9652
10412
|
});
|
|
9653
10413
|
const session = agent.getSession();
|
|
9654
10414
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
9655
|
-
const streamId = `stream_${session.id}_${
|
|
10415
|
+
const streamId = `stream_${session.id}_${nanoid8(10)}`;
|
|
9656
10416
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
9657
10417
|
await activeStreamQueries.create(session.id, streamId);
|
|
9658
10418
|
const createQuickStreamProducer = () => {
|
|
@@ -9919,23 +10679,23 @@ agents.post(
|
|
|
9919
10679
|
});
|
|
9920
10680
|
}
|
|
9921
10681
|
);
|
|
9922
|
-
var browserInputSchema =
|
|
9923
|
-
type:
|
|
9924
|
-
eventType:
|
|
9925
|
-
x:
|
|
9926
|
-
y:
|
|
9927
|
-
button:
|
|
9928
|
-
clickCount:
|
|
9929
|
-
deltaX:
|
|
9930
|
-
deltaY:
|
|
9931
|
-
key:
|
|
9932
|
-
code:
|
|
9933
|
-
text:
|
|
9934
|
-
modifiers:
|
|
9935
|
-
touchPoints:
|
|
9936
|
-
x:
|
|
9937
|
-
y:
|
|
9938
|
-
id:
|
|
10682
|
+
var browserInputSchema = z17.object({
|
|
10683
|
+
type: z17.enum(["input_mouse", "input_keyboard", "input_touch"]),
|
|
10684
|
+
eventType: z17.string(),
|
|
10685
|
+
x: z17.number().optional(),
|
|
10686
|
+
y: z17.number().optional(),
|
|
10687
|
+
button: z17.string().optional(),
|
|
10688
|
+
clickCount: z17.number().optional(),
|
|
10689
|
+
deltaX: z17.number().optional(),
|
|
10690
|
+
deltaY: z17.number().optional(),
|
|
10691
|
+
key: z17.string().optional(),
|
|
10692
|
+
code: z17.string().optional(),
|
|
10693
|
+
text: z17.string().optional(),
|
|
10694
|
+
modifiers: z17.number().optional(),
|
|
10695
|
+
touchPoints: z17.array(z17.object({
|
|
10696
|
+
x: z17.number(),
|
|
10697
|
+
y: z17.number(),
|
|
10698
|
+
id: z17.number().optional()
|
|
9939
10699
|
})).optional()
|
|
9940
10700
|
});
|
|
9941
10701
|
agents.post(
|
|
@@ -9970,27 +10730,279 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
9970
10730
|
init_config();
|
|
9971
10731
|
import { Hono as Hono3 } from "hono";
|
|
9972
10732
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
9973
|
-
import { z as
|
|
9974
|
-
import { readFileSync as
|
|
10733
|
+
import { z as z18 } from "zod";
|
|
10734
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
9975
10735
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9976
|
-
import { dirname as dirname6, join as
|
|
10736
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10737
|
+
|
|
10738
|
+
// src/personal-agent/heartbeat.ts
|
|
10739
|
+
import { execSync as execSync3 } from "child_process";
|
|
10740
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10741
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
10742
|
+
|
|
10743
|
+
// src/personal-agent/system-metrics.ts
|
|
10744
|
+
import { execSync as execSync2 } from "child_process";
|
|
10745
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
10746
|
+
import {
|
|
10747
|
+
arch,
|
|
10748
|
+
cpus,
|
|
10749
|
+
freemem,
|
|
10750
|
+
hostname,
|
|
10751
|
+
loadavg,
|
|
10752
|
+
networkInterfaces,
|
|
10753
|
+
platform as platform2,
|
|
10754
|
+
release,
|
|
10755
|
+
totalmem,
|
|
10756
|
+
type,
|
|
10757
|
+
uptime,
|
|
10758
|
+
userInfo
|
|
10759
|
+
} from "os";
|
|
10760
|
+
var _lastSample = null;
|
|
10761
|
+
function snapshotCpuTimes() {
|
|
10762
|
+
const sum = { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
|
|
10763
|
+
for (const c of cpus()) {
|
|
10764
|
+
sum.user += c.times.user;
|
|
10765
|
+
sum.nice += c.times.nice;
|
|
10766
|
+
sum.sys += c.times.sys;
|
|
10767
|
+
sum.idle += c.times.idle;
|
|
10768
|
+
sum.irq += c.times.irq;
|
|
10769
|
+
}
|
|
10770
|
+
return sum;
|
|
10771
|
+
}
|
|
10772
|
+
function readCpuUsage() {
|
|
10773
|
+
const now = snapshotCpuTimes();
|
|
10774
|
+
if (!_lastSample) {
|
|
10775
|
+
_lastSample = now;
|
|
10776
|
+
return 0;
|
|
10777
|
+
}
|
|
10778
|
+
const dUser = now.user - _lastSample.user;
|
|
10779
|
+
const dNice = now.nice - _lastSample.nice;
|
|
10780
|
+
const dSys = now.sys - _lastSample.sys;
|
|
10781
|
+
const dIdle = now.idle - _lastSample.idle;
|
|
10782
|
+
const dIrq = now.irq - _lastSample.irq;
|
|
10783
|
+
const total = dUser + dNice + dSys + dIdle + dIrq;
|
|
10784
|
+
_lastSample = now;
|
|
10785
|
+
if (total <= 0) return 0;
|
|
10786
|
+
return Math.max(0, Math.min(1, (total - dIdle) / total));
|
|
10787
|
+
}
|
|
10788
|
+
snapshotCpuTimes();
|
|
10789
|
+
function readCpuTempC() {
|
|
10790
|
+
try {
|
|
10791
|
+
if (platform2() === "linux") {
|
|
10792
|
+
let hottest = -Infinity;
|
|
10793
|
+
try {
|
|
10794
|
+
for (const entry of readdirSync3("/sys/class/thermal")) {
|
|
10795
|
+
if (!entry.startsWith("thermal_zone")) continue;
|
|
10796
|
+
try {
|
|
10797
|
+
const v = Number(
|
|
10798
|
+
readFileSync8(`/sys/class/thermal/${entry}/temp`, "utf8").trim()
|
|
10799
|
+
);
|
|
10800
|
+
if (Number.isFinite(v) && v > hottest) hottest = v;
|
|
10801
|
+
} catch {
|
|
10802
|
+
}
|
|
10803
|
+
}
|
|
10804
|
+
} catch {
|
|
10805
|
+
}
|
|
10806
|
+
if (hottest > -Infinity) return hottest / 1e3;
|
|
10807
|
+
}
|
|
10808
|
+
const overrideCmd = process.env.PERSONAL_AGENT_TEMP_CMD;
|
|
10809
|
+
if (overrideCmd) {
|
|
10810
|
+
const out = execSync2(overrideCmd, { encoding: "utf8", timeout: 1500 }).trim();
|
|
10811
|
+
const v = Number(out);
|
|
10812
|
+
if (Number.isFinite(v)) return v;
|
|
10813
|
+
}
|
|
10814
|
+
} catch {
|
|
10815
|
+
}
|
|
10816
|
+
return void 0;
|
|
10817
|
+
}
|
|
10818
|
+
function readDisks() {
|
|
10819
|
+
try {
|
|
10820
|
+
const p = platform2();
|
|
10821
|
+
if (p === "darwin" || p === "linux") {
|
|
10822
|
+
const raw = execSync2("df -kP", { encoding: "utf8", timeout: 2e3 });
|
|
10823
|
+
const lines = raw.trim().split("\n").slice(1);
|
|
10824
|
+
const out = [];
|
|
10825
|
+
for (const line of lines) {
|
|
10826
|
+
const parts = line.trim().split(/\s+/);
|
|
10827
|
+
if (parts.length < 6) continue;
|
|
10828
|
+
const filesystem = parts[0];
|
|
10829
|
+
const total1k = Number(parts[1]);
|
|
10830
|
+
const used1k = Number(parts[2]);
|
|
10831
|
+
const free1k = Number(parts[3]);
|
|
10832
|
+
const mount = parts.slice(5).join(" ");
|
|
10833
|
+
if (!Number.isFinite(total1k) || total1k <= 0) continue;
|
|
10834
|
+
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")) {
|
|
10835
|
+
if (mount !== "/") continue;
|
|
10836
|
+
}
|
|
10837
|
+
out.push({
|
|
10838
|
+
mount,
|
|
10839
|
+
filesystem,
|
|
10840
|
+
totalBytes: total1k * 1024,
|
|
10841
|
+
usedBytes: used1k * 1024,
|
|
10842
|
+
freeBytes: free1k * 1024,
|
|
10843
|
+
usage: total1k > 0 ? used1k / total1k : 0
|
|
10844
|
+
});
|
|
10845
|
+
}
|
|
10846
|
+
out.sort((a, b) => {
|
|
10847
|
+
const score = (m) => m === "/" ? 0 : m.startsWith("/Users") || m.startsWith("/home") ? 1 : 2;
|
|
10848
|
+
return score(a.mount) - score(b.mount);
|
|
10849
|
+
});
|
|
10850
|
+
return out.slice(0, 6);
|
|
10851
|
+
}
|
|
10852
|
+
if (p === "win32") {
|
|
10853
|
+
const raw = execSync2(
|
|
10854
|
+
"wmic logicaldisk get DeviceID,Size,FreeSpace /format:csv",
|
|
10855
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
10856
|
+
);
|
|
10857
|
+
const out = [];
|
|
10858
|
+
for (const line of raw.trim().split(/\r?\n/).slice(1)) {
|
|
10859
|
+
const cols = line.split(",");
|
|
10860
|
+
if (cols.length < 4) continue;
|
|
10861
|
+
const [, deviceId, freeStr, sizeStr] = cols;
|
|
10862
|
+
const total = Number(sizeStr);
|
|
10863
|
+
const free = Number(freeStr);
|
|
10864
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
10865
|
+
const used = Math.max(0, total - free);
|
|
10866
|
+
out.push({
|
|
10867
|
+
mount: deviceId,
|
|
10868
|
+
totalBytes: total,
|
|
10869
|
+
usedBytes: used,
|
|
10870
|
+
freeBytes: free,
|
|
10871
|
+
usage: used / total
|
|
10872
|
+
});
|
|
10873
|
+
}
|
|
10874
|
+
return out;
|
|
10875
|
+
}
|
|
10876
|
+
} catch {
|
|
10877
|
+
}
|
|
10878
|
+
return void 0;
|
|
10879
|
+
}
|
|
10880
|
+
function readNetwork() {
|
|
10881
|
+
try {
|
|
10882
|
+
const out = [];
|
|
10883
|
+
const ifaces = networkInterfaces();
|
|
10884
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
10885
|
+
if (!addrs) continue;
|
|
10886
|
+
for (const a of addrs) {
|
|
10887
|
+
if (a.internal) continue;
|
|
10888
|
+
out.push({
|
|
10889
|
+
iface: name,
|
|
10890
|
+
family: a.family,
|
|
10891
|
+
address: a.address,
|
|
10892
|
+
mac: a.mac,
|
|
10893
|
+
internal: a.internal
|
|
10894
|
+
});
|
|
10895
|
+
}
|
|
10896
|
+
}
|
|
10897
|
+
return out;
|
|
10898
|
+
} catch {
|
|
10899
|
+
return void 0;
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
function readSystemMetrics() {
|
|
10903
|
+
const cpuList = cpus();
|
|
10904
|
+
const usage = readCpuUsage();
|
|
10905
|
+
const tot = totalmem();
|
|
10906
|
+
const free = freemem();
|
|
10907
|
+
const metrics = {
|
|
10908
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10909
|
+
hostname: hostname(),
|
|
10910
|
+
platform: platform2(),
|
|
10911
|
+
arch: arch(),
|
|
10912
|
+
kernelRelease: release(),
|
|
10913
|
+
osType: type(),
|
|
10914
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
10915
|
+
systemUptimeSec: Math.round(uptime()),
|
|
10916
|
+
user: safeUser(),
|
|
10917
|
+
cpu: cpuList[0] ? {
|
|
10918
|
+
model: cpuList[0].model,
|
|
10919
|
+
count: cpuList.length,
|
|
10920
|
+
speedMhz: cpuList[0].speed,
|
|
10921
|
+
loadAvg1: loadavg()[0],
|
|
10922
|
+
loadAvg5: loadavg()[1],
|
|
10923
|
+
loadAvg15: loadavg()[2],
|
|
10924
|
+
usage,
|
|
10925
|
+
tempC: readCpuTempC()
|
|
10926
|
+
} : void 0,
|
|
10927
|
+
memory: {
|
|
10928
|
+
totalBytes: tot,
|
|
10929
|
+
freeBytes: free,
|
|
10930
|
+
usedBytes: Math.max(0, tot - free),
|
|
10931
|
+
usage: tot > 0 ? (tot - free) / tot : 0
|
|
10932
|
+
},
|
|
10933
|
+
disks: readDisks(),
|
|
10934
|
+
network: readNetwork()
|
|
10935
|
+
};
|
|
10936
|
+
return metrics;
|
|
10937
|
+
}
|
|
10938
|
+
function safeUser() {
|
|
10939
|
+
try {
|
|
10940
|
+
return userInfo().username;
|
|
10941
|
+
} catch {
|
|
10942
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
10943
|
+
}
|
|
10944
|
+
}
|
|
10945
|
+
|
|
10946
|
+
// src/personal-agent/heartbeat.ts
|
|
10947
|
+
var _cachedHwid = null;
|
|
10948
|
+
function getHardwareIdCached() {
|
|
10949
|
+
if (_cachedHwid !== null) return _cachedHwid;
|
|
10950
|
+
_cachedHwid = getHardwareId();
|
|
10951
|
+
return _cachedHwid;
|
|
10952
|
+
}
|
|
10953
|
+
function getHardwareId() {
|
|
10954
|
+
const p = platform3();
|
|
10955
|
+
try {
|
|
10956
|
+
if (p === "darwin") {
|
|
10957
|
+
const out = execSync3(
|
|
10958
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/ {print $4}'`,
|
|
10959
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
10960
|
+
).trim();
|
|
10961
|
+
if (out) return normalize2(out);
|
|
10962
|
+
} else if (p === "linux") {
|
|
10963
|
+
try {
|
|
10964
|
+
return normalize2(readFileSync9("/etc/machine-id", "utf8").trim());
|
|
10965
|
+
} catch {
|
|
10966
|
+
return normalize2(readFileSync9("/var/lib/dbus/machine-id", "utf8").trim());
|
|
10967
|
+
}
|
|
10968
|
+
} else if (p === "win32") {
|
|
10969
|
+
const out = execSync3("wmic csproduct get uuid /value", {
|
|
10970
|
+
encoding: "utf8",
|
|
10971
|
+
timeout: 3e3
|
|
10972
|
+
});
|
|
10973
|
+
const m = out.match(/UUID=([\w-]+)/i);
|
|
10974
|
+
if (m && m[1]) return normalize2(m[1]);
|
|
10975
|
+
}
|
|
10976
|
+
} catch (e) {
|
|
10977
|
+
console.warn(`[personal-agent] could not read hardware UUID: ${e.message}`);
|
|
10978
|
+
}
|
|
10979
|
+
console.warn(
|
|
10980
|
+
"[personal-agent] falling back to hostname for HWID; this is NOT stable across rename/reinstall"
|
|
10981
|
+
);
|
|
10982
|
+
return `host-${hostname2()}`;
|
|
10983
|
+
}
|
|
10984
|
+
function normalize2(raw) {
|
|
10985
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
10986
|
+
}
|
|
10987
|
+
|
|
10988
|
+
// src/server/routes/health.ts
|
|
9977
10989
|
var __filename = fileURLToPath3(import.meta.url);
|
|
9978
10990
|
var __dirname = dirname6(__filename);
|
|
9979
10991
|
var possiblePaths = [
|
|
9980
|
-
|
|
10992
|
+
join12(__dirname, "../package.json"),
|
|
9981
10993
|
// From dist/server -> dist/../package.json
|
|
9982
|
-
|
|
10994
|
+
join12(__dirname, "../../package.json"),
|
|
9983
10995
|
// From dist/server (if nested differently)
|
|
9984
|
-
|
|
10996
|
+
join12(__dirname, "../../../package.json"),
|
|
9985
10997
|
// From src/server/routes (development)
|
|
9986
|
-
|
|
10998
|
+
join12(process.cwd(), "package.json")
|
|
9987
10999
|
// From current working directory
|
|
9988
11000
|
];
|
|
9989
11001
|
var currentVersion = "0.0.0";
|
|
9990
11002
|
var packageName = "sparkecoder";
|
|
9991
11003
|
for (const packageJsonPath of possiblePaths) {
|
|
9992
11004
|
try {
|
|
9993
|
-
const packageJson = JSON.parse(
|
|
11005
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
9994
11006
|
if (packageJson.name === "sparkecoder") {
|
|
9995
11007
|
currentVersion = packageJson.version || "0.0.0";
|
|
9996
11008
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10004,12 +11016,20 @@ health.get("/", async (c) => {
|
|
|
10004
11016
|
const config = getConfig();
|
|
10005
11017
|
const apiKeyStatus = getApiKeyStatus();
|
|
10006
11018
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10007
|
-
const
|
|
11019
|
+
const remoteInference = isRemoteInferenceConfigured();
|
|
11020
|
+
const hasApiKey = remoteInference || (gatewayKey?.configured ?? false);
|
|
11021
|
+
let hwid;
|
|
11022
|
+
try {
|
|
11023
|
+
hwid = getHardwareIdCached();
|
|
11024
|
+
} catch {
|
|
11025
|
+
}
|
|
10008
11026
|
return c.json({
|
|
10009
11027
|
status: "ok",
|
|
10010
11028
|
version: currentVersion,
|
|
10011
11029
|
uptime: process.uptime(),
|
|
10012
11030
|
apiKeyConfigured: hasApiKey,
|
|
11031
|
+
inferenceMode: remoteInference ? "remote" : "local",
|
|
11032
|
+
hwid,
|
|
10013
11033
|
config: {
|
|
10014
11034
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10015
11035
|
defaultModel: config.defaultModel,
|
|
@@ -10080,9 +11100,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10080
11100
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10081
11101
|
});
|
|
10082
11102
|
});
|
|
10083
|
-
var setApiKeySchema =
|
|
10084
|
-
provider:
|
|
10085
|
-
apiKey:
|
|
11103
|
+
var setApiKeySchema = z18.object({
|
|
11104
|
+
provider: z18.string(),
|
|
11105
|
+
apiKey: z18.string().min(1)
|
|
10086
11106
|
});
|
|
10087
11107
|
health.post(
|
|
10088
11108
|
"/api-keys",
|
|
@@ -10121,13 +11141,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10121
11141
|
// src/server/routes/terminals.ts
|
|
10122
11142
|
import { Hono as Hono4 } from "hono";
|
|
10123
11143
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10124
|
-
import { z as
|
|
11144
|
+
import { z as z19 } from "zod";
|
|
10125
11145
|
init_db();
|
|
10126
11146
|
var terminals = new Hono4();
|
|
10127
|
-
var spawnSchema =
|
|
10128
|
-
command:
|
|
10129
|
-
cwd:
|
|
10130
|
-
name:
|
|
11147
|
+
var spawnSchema = z19.object({
|
|
11148
|
+
command: z19.string(),
|
|
11149
|
+
cwd: z19.string().optional(),
|
|
11150
|
+
name: z19.string().optional()
|
|
10131
11151
|
});
|
|
10132
11152
|
terminals.post(
|
|
10133
11153
|
"/:sessionId/terminals",
|
|
@@ -10208,8 +11228,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10208
11228
|
// We don't track exit codes in tmux mode
|
|
10209
11229
|
});
|
|
10210
11230
|
});
|
|
10211
|
-
var logsQuerySchema =
|
|
10212
|
-
tail:
|
|
11231
|
+
var logsQuerySchema = z19.object({
|
|
11232
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10213
11233
|
});
|
|
10214
11234
|
terminals.get(
|
|
10215
11235
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -10233,8 +11253,8 @@ terminals.get(
|
|
|
10233
11253
|
});
|
|
10234
11254
|
}
|
|
10235
11255
|
);
|
|
10236
|
-
var killSchema =
|
|
10237
|
-
signal:
|
|
11256
|
+
var killSchema = z19.object({
|
|
11257
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
10238
11258
|
});
|
|
10239
11259
|
terminals.post(
|
|
10240
11260
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -10248,8 +11268,8 @@ terminals.post(
|
|
|
10248
11268
|
return c.json({ success: true, message: "Terminal killed" });
|
|
10249
11269
|
}
|
|
10250
11270
|
);
|
|
10251
|
-
var writeSchema =
|
|
10252
|
-
input:
|
|
11271
|
+
var writeSchema = z19.object({
|
|
11272
|
+
input: z19.string()
|
|
10253
11273
|
});
|
|
10254
11274
|
terminals.post(
|
|
10255
11275
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -10434,20 +11454,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
10434
11454
|
init_db();
|
|
10435
11455
|
import { Hono as Hono5 } from "hono";
|
|
10436
11456
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
10437
|
-
import { z as
|
|
10438
|
-
import { nanoid as
|
|
11457
|
+
import { z as z20 } from "zod";
|
|
11458
|
+
import { nanoid as nanoid9 } from "nanoid";
|
|
10439
11459
|
init_config();
|
|
10440
11460
|
var tasks = new Hono5();
|
|
10441
11461
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
10442
|
-
var createTaskSchema =
|
|
10443
|
-
prompt:
|
|
10444
|
-
outputSchema:
|
|
10445
|
-
webhookUrl:
|
|
10446
|
-
model:
|
|
10447
|
-
workingDirectory:
|
|
10448
|
-
name:
|
|
10449
|
-
maxIterations:
|
|
10450
|
-
parentTaskId:
|
|
11462
|
+
var createTaskSchema = z20.object({
|
|
11463
|
+
prompt: z20.string().min(1),
|
|
11464
|
+
outputSchema: z20.record(z20.string(), z20.unknown()),
|
|
11465
|
+
webhookUrl: z20.string().url().optional(),
|
|
11466
|
+
model: z20.string().optional(),
|
|
11467
|
+
workingDirectory: z20.string().optional(),
|
|
11468
|
+
name: z20.string().optional(),
|
|
11469
|
+
maxIterations: z20.number().int().min(1).max(500).optional(),
|
|
11470
|
+
parentTaskId: z20.string().optional()
|
|
10451
11471
|
});
|
|
10452
11472
|
tasks.post(
|
|
10453
11473
|
"/",
|
|
@@ -10509,7 +11529,7 @@ tasks.post(
|
|
|
10509
11529
|
const taskId = agent.sessionId;
|
|
10510
11530
|
const abortController = new AbortController();
|
|
10511
11531
|
taskAbortControllers.set(taskId, abortController);
|
|
10512
|
-
const streamId = `stream_${taskId}_${
|
|
11532
|
+
const streamId = `stream_${taskId}_${nanoid9(10)}`;
|
|
10513
11533
|
await activeStreamQueries.create(taskId, streamId);
|
|
10514
11534
|
const taskStreamProducer = () => {
|
|
10515
11535
|
const { readable, writable } = new TransformStream();
|
|
@@ -10636,6 +11656,7 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10636
11656
|
abortController.abort();
|
|
10637
11657
|
taskAbortControllers.delete(id);
|
|
10638
11658
|
}
|
|
11659
|
+
rejectTaskQuestions(id, "Task cancelled by user");
|
|
10639
11660
|
const cancelledTask = {
|
|
10640
11661
|
...task,
|
|
10641
11662
|
status: "failed",
|
|
@@ -10657,19 +11678,629 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
10657
11678
|
}
|
|
10658
11679
|
return c.json({ taskId: id, status: "failed", error: "Task cancelled by user" });
|
|
10659
11680
|
});
|
|
11681
|
+
var answerQuestionSchema = z20.object({
|
|
11682
|
+
answer: z20.string().min(1),
|
|
11683
|
+
answeredBy: z20.enum(["orchestrator", "user", "system"]).optional()
|
|
11684
|
+
});
|
|
11685
|
+
tasks.post(
|
|
11686
|
+
"/:id/questions/:questionId/answer",
|
|
11687
|
+
zValidator5("json", answerQuestionSchema),
|
|
11688
|
+
async (c) => {
|
|
11689
|
+
const id = c.req.param("id");
|
|
11690
|
+
const questionId = c.req.param("questionId");
|
|
11691
|
+
const body = c.req.valid("json");
|
|
11692
|
+
const session = await sessionQueries.getById(id);
|
|
11693
|
+
if (!session) {
|
|
11694
|
+
return c.json({ error: "Task not found" }, 404);
|
|
11695
|
+
}
|
|
11696
|
+
const task = session.config?.task;
|
|
11697
|
+
if (!task?.enabled) {
|
|
11698
|
+
return c.json({ error: "Session is not a task" }, 400);
|
|
11699
|
+
}
|
|
11700
|
+
const pending = getPendingTaskQuestion(id, questionId);
|
|
11701
|
+
if (!pending) {
|
|
11702
|
+
if (getAnsweredTaskQuestion(id, questionId)) {
|
|
11703
|
+
return c.json({
|
|
11704
|
+
taskId: id,
|
|
11705
|
+
questionId,
|
|
11706
|
+
status: "answered",
|
|
11707
|
+
alreadyAnswered: true
|
|
11708
|
+
});
|
|
11709
|
+
}
|
|
11710
|
+
return c.json({ error: "Question is not pending" }, 404);
|
|
11711
|
+
}
|
|
11712
|
+
const answerStatus = answerTaskQuestion(id, questionId, {
|
|
11713
|
+
answer: body.answer,
|
|
11714
|
+
answeredBy: body.answeredBy
|
|
11715
|
+
});
|
|
11716
|
+
if (answerStatus === "not_found") {
|
|
11717
|
+
return c.json({ error: "Question is not pending" }, 404);
|
|
11718
|
+
}
|
|
11719
|
+
return c.json({
|
|
11720
|
+
taskId: id,
|
|
11721
|
+
questionId,
|
|
11722
|
+
status: "answered",
|
|
11723
|
+
alreadyAnswered: answerStatus === "already_answered"
|
|
11724
|
+
});
|
|
11725
|
+
}
|
|
11726
|
+
);
|
|
10660
11727
|
var tasks_default = tasks;
|
|
10661
11728
|
|
|
11729
|
+
// src/server/routes/system.ts
|
|
11730
|
+
import { Hono as Hono6 } from "hono";
|
|
11731
|
+
import { streamSSE } from "hono/streaming";
|
|
11732
|
+
var system = new Hono6();
|
|
11733
|
+
system.get("/metrics", (c) => {
|
|
11734
|
+
return c.json(readSystemMetrics());
|
|
11735
|
+
});
|
|
11736
|
+
system.get("/metrics/stream", (c) => {
|
|
11737
|
+
return streamSSE(c, async (stream) => {
|
|
11738
|
+
const intervalMs = Number(c.req.query("intervalMs") ?? 2e3);
|
|
11739
|
+
const safeMs = Number.isFinite(intervalMs) ? Math.max(500, Math.min(6e4, intervalMs)) : 2e3;
|
|
11740
|
+
let id = 0;
|
|
11741
|
+
let aborted = false;
|
|
11742
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
11743
|
+
aborted = true;
|
|
11744
|
+
});
|
|
11745
|
+
while (!aborted) {
|
|
11746
|
+
try {
|
|
11747
|
+
const snap = readSystemMetrics();
|
|
11748
|
+
await stream.writeSSE({
|
|
11749
|
+
id: String(id++),
|
|
11750
|
+
event: "metrics",
|
|
11751
|
+
data: JSON.stringify(snap)
|
|
11752
|
+
});
|
|
11753
|
+
} catch (e) {
|
|
11754
|
+
await stream.writeSSE({
|
|
11755
|
+
id: String(id++),
|
|
11756
|
+
event: "error",
|
|
11757
|
+
data: JSON.stringify({ error: e.message })
|
|
11758
|
+
});
|
|
11759
|
+
}
|
|
11760
|
+
await stream.sleep(safeMs);
|
|
11761
|
+
}
|
|
11762
|
+
});
|
|
11763
|
+
});
|
|
11764
|
+
|
|
11765
|
+
// src/personal-agent/hwid-middleware.ts
|
|
11766
|
+
var SKIP_PATHS = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11767
|
+
function personalAgentConfigured() {
|
|
11768
|
+
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);
|
|
11769
|
+
}
|
|
11770
|
+
function hwidMiddleware() {
|
|
11771
|
+
const enabled = personalAgentConfigured();
|
|
11772
|
+
let warnedMissing = false;
|
|
11773
|
+
return async (c, next) => {
|
|
11774
|
+
if (!enabled) return next();
|
|
11775
|
+
const path = c.req.path;
|
|
11776
|
+
if (SKIP_PATHS.has(path) || path.startsWith("/health/api-keys")) {
|
|
11777
|
+
return next();
|
|
11778
|
+
}
|
|
11779
|
+
const got = c.req.header("x-device-hwid");
|
|
11780
|
+
if (!got) {
|
|
11781
|
+
if (!warnedMissing) {
|
|
11782
|
+
warnedMissing = true;
|
|
11783
|
+
console.warn(
|
|
11784
|
+
`[personal-agent] request to ${path} arrived without X-Device-Hwid; allowing (backwards compat)`
|
|
11785
|
+
);
|
|
11786
|
+
}
|
|
11787
|
+
return next();
|
|
11788
|
+
}
|
|
11789
|
+
const expected = getHardwareIdCached();
|
|
11790
|
+
if (got !== expected) {
|
|
11791
|
+
console.warn(
|
|
11792
|
+
`[personal-agent] HWID mismatch on ${path}: got=${got.slice(0, 12)}\u2026, expected=${expected.slice(0, 12)}\u2026`
|
|
11793
|
+
);
|
|
11794
|
+
return c.json(
|
|
11795
|
+
{
|
|
11796
|
+
error: "hwid mismatch",
|
|
11797
|
+
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.",
|
|
11798
|
+
expected: expected.slice(0, 12) + "\u2026",
|
|
11799
|
+
got: got.slice(0, 12) + "\u2026"
|
|
11800
|
+
},
|
|
11801
|
+
409
|
|
11802
|
+
);
|
|
11803
|
+
}
|
|
11804
|
+
return next();
|
|
11805
|
+
};
|
|
11806
|
+
}
|
|
11807
|
+
|
|
11808
|
+
// src/personal-agent/signature-verify.ts
|
|
11809
|
+
import { createHash as createHash3, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
11810
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
11811
|
+
var REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
11812
|
+
var _cachedKey = null;
|
|
11813
|
+
var _cachedFromInput = null;
|
|
11814
|
+
function loadPublicKey(input) {
|
|
11815
|
+
if (_cachedFromInput === input && _cachedKey) return _cachedKey;
|
|
11816
|
+
let pem = input;
|
|
11817
|
+
if (!input.includes("BEGIN") && existsSync18(input)) {
|
|
11818
|
+
pem = readFileSync11(input, "utf8");
|
|
11819
|
+
}
|
|
11820
|
+
const key2 = createPublicKey({ key: pem, format: "pem" });
|
|
11821
|
+
if (key2.asymmetricKeyType !== "ed25519") {
|
|
11822
|
+
throw new Error(
|
|
11823
|
+
`expected an ed25519 public key, got ${key2.asymmetricKeyType}. Generate with personal-agents/scripts/generate-signing-keys.mjs.`
|
|
11824
|
+
);
|
|
11825
|
+
}
|
|
11826
|
+
_cachedKey = key2;
|
|
11827
|
+
_cachedFromInput = input;
|
|
11828
|
+
return key2;
|
|
11829
|
+
}
|
|
11830
|
+
function bodyHashB64(body) {
|
|
11831
|
+
const hash = createHash3("sha256");
|
|
11832
|
+
if (body == null || body === "") {
|
|
11833
|
+
} else if (typeof body === "string") {
|
|
11834
|
+
hash.update(body, "utf8");
|
|
11835
|
+
} else if (Buffer.isBuffer(body)) {
|
|
11836
|
+
hash.update(body);
|
|
11837
|
+
} else {
|
|
11838
|
+
hash.update(Buffer.from(body));
|
|
11839
|
+
}
|
|
11840
|
+
return hash.digest("base64");
|
|
11841
|
+
}
|
|
11842
|
+
function canonicalSigningString(args) {
|
|
11843
|
+
return [
|
|
11844
|
+
args.method.toUpperCase(),
|
|
11845
|
+
args.path,
|
|
11846
|
+
String(args.timestamp),
|
|
11847
|
+
args.bodyHashB64
|
|
11848
|
+
].join("\n");
|
|
11849
|
+
}
|
|
11850
|
+
function fromBase64Url(s) {
|
|
11851
|
+
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
11852
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
11853
|
+
return Buffer.from(std, "base64");
|
|
11854
|
+
}
|
|
11855
|
+
function verifyEmbedToken(args) {
|
|
11856
|
+
const dot = args.token.indexOf(".");
|
|
11857
|
+
if (dot < 1 || dot >= args.token.length - 1) {
|
|
11858
|
+
return { ok: false, reason: "malformed" };
|
|
11859
|
+
}
|
|
11860
|
+
const payloadB64 = args.token.slice(0, dot);
|
|
11861
|
+
const sigB64 = args.token.slice(dot + 1);
|
|
11862
|
+
let sigBuf;
|
|
11863
|
+
try {
|
|
11864
|
+
sigBuf = fromBase64Url(sigB64);
|
|
11865
|
+
} catch {
|
|
11866
|
+
return { ok: false, reason: "bad-encoding" };
|
|
11867
|
+
}
|
|
11868
|
+
const sigOk = cryptoVerify(
|
|
11869
|
+
null,
|
|
11870
|
+
Buffer.from(payloadB64, "utf8"),
|
|
11871
|
+
args.publicKey,
|
|
11872
|
+
sigBuf
|
|
11873
|
+
);
|
|
11874
|
+
if (!sigOk) return { ok: false, reason: "signature-mismatch" };
|
|
11875
|
+
let payload;
|
|
11876
|
+
try {
|
|
11877
|
+
const json = JSON.parse(fromBase64Url(payloadB64).toString("utf8"));
|
|
11878
|
+
if (!json || typeof json !== "object" || typeof json.sid !== "string" || typeof json.exp !== "number") {
|
|
11879
|
+
return { ok: false, reason: "bad-payload" };
|
|
11880
|
+
}
|
|
11881
|
+
payload = { sid: json.sid, exp: json.exp };
|
|
11882
|
+
} catch (e) {
|
|
11883
|
+
return { ok: false, reason: "bad-payload", detail: e.message };
|
|
11884
|
+
}
|
|
11885
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11886
|
+
if (payload.exp < now) {
|
|
11887
|
+
return {
|
|
11888
|
+
ok: false,
|
|
11889
|
+
reason: "expired",
|
|
11890
|
+
detail: `${now - payload.exp}s past expiry`
|
|
11891
|
+
};
|
|
11892
|
+
}
|
|
11893
|
+
return { ok: true, payload };
|
|
11894
|
+
}
|
|
11895
|
+
function verifyRequest(args) {
|
|
11896
|
+
if (!args.signatureB64 || !args.timestampSeconds || !args.algorithm) {
|
|
11897
|
+
return { ok: false, reason: "missing-headers" };
|
|
11898
|
+
}
|
|
11899
|
+
if (args.algorithm.toLowerCase() !== "ed25519") {
|
|
11900
|
+
return { ok: false, reason: "bad-algorithm", detail: args.algorithm };
|
|
11901
|
+
}
|
|
11902
|
+
const ts = Number(args.timestampSeconds);
|
|
11903
|
+
if (!Number.isFinite(ts)) {
|
|
11904
|
+
return { ok: false, reason: "bad-timestamp", detail: args.timestampSeconds };
|
|
11905
|
+
}
|
|
11906
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
11907
|
+
if (Math.abs(now - ts) > REPLAY_WINDOW_SECONDS) {
|
|
11908
|
+
return {
|
|
11909
|
+
ok: false,
|
|
11910
|
+
reason: "stale-timestamp",
|
|
11911
|
+
detail: `${Math.abs(now - ts)}s outside the ${REPLAY_WINDOW_SECONDS}s window`
|
|
11912
|
+
};
|
|
11913
|
+
}
|
|
11914
|
+
let sigBuf;
|
|
11915
|
+
try {
|
|
11916
|
+
sigBuf = Buffer.from(args.signatureB64, "base64");
|
|
11917
|
+
} catch {
|
|
11918
|
+
return { ok: false, reason: "bad-signature-encoding" };
|
|
11919
|
+
}
|
|
11920
|
+
const canonical = canonicalSigningString({
|
|
11921
|
+
method: args.method,
|
|
11922
|
+
path: args.path,
|
|
11923
|
+
timestamp: ts,
|
|
11924
|
+
bodyHashB64: bodyHashB64(args.body)
|
|
11925
|
+
});
|
|
11926
|
+
const ok = cryptoVerify(null, Buffer.from(canonical, "utf8"), args.publicKey, sigBuf);
|
|
11927
|
+
return ok ? { ok: true } : { ok: false, reason: "signature-mismatch" };
|
|
11928
|
+
}
|
|
11929
|
+
|
|
11930
|
+
// src/personal-agent/signature-middleware.ts
|
|
11931
|
+
var SKIP_PATHS2 = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
11932
|
+
function isSkipped(path) {
|
|
11933
|
+
return SKIP_PATHS2.has(path) || path.startsWith("/health/api-keys");
|
|
11934
|
+
}
|
|
11935
|
+
function pathBindsSessionId(path, sid) {
|
|
11936
|
+
const cleanPath = path.split("?")[0];
|
|
11937
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
11938
|
+
return segments.includes(sid);
|
|
11939
|
+
}
|
|
11940
|
+
function signatureMiddleware(opts = {}) {
|
|
11941
|
+
const acceptSignedOnly = opts.acceptSignedOnly ?? process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
11942
|
+
const publicKeyInput = opts.publicKeyInput ?? process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
11943
|
+
if (acceptSignedOnly && !publicKeyInput) {
|
|
11944
|
+
return async (c) => c.json(
|
|
11945
|
+
{
|
|
11946
|
+
error: "signature middleware misconfigured",
|
|
11947
|
+
message: "started with --accept-signed-only but no --personal-agent-public-key / PERSONAL_AGENT_PUBLIC_KEY supplied. Refusing all non-/health traffic."
|
|
11948
|
+
},
|
|
11949
|
+
500
|
|
11950
|
+
);
|
|
11951
|
+
}
|
|
11952
|
+
if (!publicKeyInput) {
|
|
11953
|
+
return async (_c, next) => next();
|
|
11954
|
+
}
|
|
11955
|
+
const publicKey = loadPublicKey(publicKeyInput);
|
|
11956
|
+
let warnedUnsigned = false;
|
|
11957
|
+
return async (c, next) => {
|
|
11958
|
+
const path = c.req.path;
|
|
11959
|
+
if (isSkipped(path)) return next();
|
|
11960
|
+
const sig = c.req.header("x-signature");
|
|
11961
|
+
const ts = c.req.header("x-signature-timestamp");
|
|
11962
|
+
const alg = c.req.header("x-signature-algorithm");
|
|
11963
|
+
if (!sig && (c.req.method === "GET" || c.req.method === "HEAD")) {
|
|
11964
|
+
const embedTok = c.req.header("x-embed-token") ?? c.req.query("embed_token");
|
|
11965
|
+
if (embedTok) {
|
|
11966
|
+
const result2 = verifyEmbedToken({ publicKey, token: embedTok });
|
|
11967
|
+
if (!result2.ok) {
|
|
11968
|
+
return c.json(
|
|
11969
|
+
{
|
|
11970
|
+
error: "embed token verification failed",
|
|
11971
|
+
reason: result2.reason,
|
|
11972
|
+
detail: result2.detail
|
|
11973
|
+
},
|
|
11974
|
+
401
|
|
11975
|
+
);
|
|
11976
|
+
}
|
|
11977
|
+
if (!pathBindsSessionId(path, result2.payload.sid)) {
|
|
11978
|
+
return c.json(
|
|
11979
|
+
{
|
|
11980
|
+
error: "embed token scoped to a different session",
|
|
11981
|
+
detail: `token sid=${result2.payload.sid} but request path=${path}`
|
|
11982
|
+
},
|
|
11983
|
+
403
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
11986
|
+
return next();
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
if (!sig) {
|
|
11990
|
+
if (acceptSignedOnly) {
|
|
11991
|
+
return c.json(
|
|
11992
|
+
{
|
|
11993
|
+
error: "signature required",
|
|
11994
|
+
message: "this sparkecoder is started with --accept-signed-only; every request must carry X-Signature, X-Signature-Timestamp, X-Signature-Algorithm."
|
|
11995
|
+
},
|
|
11996
|
+
401
|
|
11997
|
+
);
|
|
11998
|
+
}
|
|
11999
|
+
if (!warnedUnsigned) {
|
|
12000
|
+
warnedUnsigned = true;
|
|
12001
|
+
console.warn(
|
|
12002
|
+
`[personal-agent] inbound ${c.req.method} ${path} arrived without X-Signature; allowed because --accept-signed-only is off`
|
|
12003
|
+
);
|
|
12004
|
+
}
|
|
12005
|
+
return next();
|
|
12006
|
+
}
|
|
12007
|
+
let body;
|
|
12008
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
12009
|
+
body = Buffer.from(await c.req.raw.clone().arrayBuffer());
|
|
12010
|
+
}
|
|
12011
|
+
const result = verifyRequest({
|
|
12012
|
+
publicKey,
|
|
12013
|
+
method: c.req.method,
|
|
12014
|
+
path,
|
|
12015
|
+
body,
|
|
12016
|
+
signatureB64: sig,
|
|
12017
|
+
timestampSeconds: ts,
|
|
12018
|
+
algorithm: alg
|
|
12019
|
+
});
|
|
12020
|
+
if (!result.ok) {
|
|
12021
|
+
console.warn(
|
|
12022
|
+
`[personal-agent] signature verification failed on ${c.req.method} ${path}: ${result.reason}${result.detail ? ` (${result.detail})` : ""}`
|
|
12023
|
+
);
|
|
12024
|
+
return c.json(
|
|
12025
|
+
{
|
|
12026
|
+
error: "signature verification failed",
|
|
12027
|
+
reason: result.reason,
|
|
12028
|
+
detail: result.detail
|
|
12029
|
+
},
|
|
12030
|
+
401
|
|
12031
|
+
);
|
|
12032
|
+
}
|
|
12033
|
+
return next();
|
|
12034
|
+
};
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
// src/personal-agent/pty-server.ts
|
|
12038
|
+
import { hostname as hostname3 } from "os";
|
|
12039
|
+
import { WebSocketServer } from "ws";
|
|
12040
|
+
var RESIZE_RE = /\x1b\[RESIZE:(\d+);(\d+)\]/;
|
|
12041
|
+
var _ptyMod = null;
|
|
12042
|
+
async function loadPty() {
|
|
12043
|
+
if (_ptyMod) return _ptyMod;
|
|
12044
|
+
try {
|
|
12045
|
+
const mod = await import("node-pty");
|
|
12046
|
+
_ptyMod = mod;
|
|
12047
|
+
return mod;
|
|
12048
|
+
} catch (e) {
|
|
12049
|
+
console.warn(
|
|
12050
|
+
`[personal-agent] node-pty failed to load; /pty WebSocket disabled. (${e.message})`
|
|
12051
|
+
);
|
|
12052
|
+
return null;
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
function defaultShell() {
|
|
12056
|
+
if (process.platform === "win32") {
|
|
12057
|
+
return { file: process.env.COMSPEC || "cmd.exe", args: [] };
|
|
12058
|
+
}
|
|
12059
|
+
return { file: process.env.SHELL || "/bin/zsh", args: ["-l"] };
|
|
12060
|
+
}
|
|
12061
|
+
function cleanEnv() {
|
|
12062
|
+
const env = {};
|
|
12063
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
12064
|
+
if (typeof v === "string") env[k] = v;
|
|
12065
|
+
}
|
|
12066
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
12067
|
+
if (!env.LANG) env.LANG = "en_US.UTF-8";
|
|
12068
|
+
return env;
|
|
12069
|
+
}
|
|
12070
|
+
function parseUpgrade(req) {
|
|
12071
|
+
const url = new URL(req.url || "/", "http://placeholder");
|
|
12072
|
+
const cols = clampInt(url.searchParams.get("cols"), 80, 10, 500);
|
|
12073
|
+
const rows = clampInt(url.searchParams.get("rows"), 24, 5, 500);
|
|
12074
|
+
const cwd = url.searchParams.get("cwd") || void 0;
|
|
12075
|
+
const shell = url.searchParams.get("shell") || void 0;
|
|
12076
|
+
return {
|
|
12077
|
+
cols,
|
|
12078
|
+
rows,
|
|
12079
|
+
cwd,
|
|
12080
|
+
shell,
|
|
12081
|
+
hwidHeader: headerStr(req, "x-device-hwid") ?? url.searchParams.get("hwid") ?? void 0,
|
|
12082
|
+
sigHeader: headerStr(req, "x-signature") ?? url.searchParams.get("sig") ?? void 0,
|
|
12083
|
+
tsHeader: headerStr(req, "x-signature-timestamp") ?? url.searchParams.get("ts") ?? void 0,
|
|
12084
|
+
algHeader: headerStr(req, "x-signature-algorithm") ?? url.searchParams.get("alg") ?? void 0
|
|
12085
|
+
};
|
|
12086
|
+
}
|
|
12087
|
+
function clampInt(v, dflt, lo, hi) {
|
|
12088
|
+
if (!v) return dflt;
|
|
12089
|
+
const n = parseInt(v, 10);
|
|
12090
|
+
if (!Number.isFinite(n)) return dflt;
|
|
12091
|
+
return Math.max(lo, Math.min(hi, n));
|
|
12092
|
+
}
|
|
12093
|
+
function headerStr(req, name) {
|
|
12094
|
+
const v = req.headers[name];
|
|
12095
|
+
if (Array.isArray(v)) return v[0];
|
|
12096
|
+
return v;
|
|
12097
|
+
}
|
|
12098
|
+
function authenticate(parsed, path, pubKey, signedOnly) {
|
|
12099
|
+
if (parsed.hwidHeader) {
|
|
12100
|
+
const expected = getHardwareIdCached();
|
|
12101
|
+
if (parsed.hwidHeader !== expected) {
|
|
12102
|
+
return {
|
|
12103
|
+
ok: false,
|
|
12104
|
+
status: 409,
|
|
12105
|
+
reason: `hwid mismatch: got ${parsed.hwidHeader.slice(0, 12)}\u2026, expected ${expected.slice(0, 12)}\u2026`
|
|
12106
|
+
};
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
if (!pubKey) {
|
|
12110
|
+
if (signedOnly) {
|
|
12111
|
+
return { ok: false, status: 500, reason: "signature required but no public key configured" };
|
|
12112
|
+
}
|
|
12113
|
+
return { ok: true };
|
|
12114
|
+
}
|
|
12115
|
+
if (!parsed.sigHeader) {
|
|
12116
|
+
if (signedOnly) {
|
|
12117
|
+
return { ok: false, status: 401, reason: "missing X-Signature on upgrade request" };
|
|
12118
|
+
}
|
|
12119
|
+
return { ok: true };
|
|
12120
|
+
}
|
|
12121
|
+
const result = verifyRequest({
|
|
12122
|
+
publicKey: pubKey,
|
|
12123
|
+
method: "GET",
|
|
12124
|
+
path,
|
|
12125
|
+
body: void 0,
|
|
12126
|
+
signatureB64: parsed.sigHeader,
|
|
12127
|
+
timestampSeconds: parsed.tsHeader,
|
|
12128
|
+
algorithm: parsed.algHeader
|
|
12129
|
+
});
|
|
12130
|
+
if (!result.ok) {
|
|
12131
|
+
return { ok: false, status: 401, reason: `signature verification failed: ${result.reason}` };
|
|
12132
|
+
}
|
|
12133
|
+
return { ok: true };
|
|
12134
|
+
}
|
|
12135
|
+
function rejectUpgrade(socket, status, reason) {
|
|
12136
|
+
const body = JSON.stringify({ error: reason });
|
|
12137
|
+
socket.write(
|
|
12138
|
+
`HTTP/1.1 ${status} ${reasonText(status)}\r
|
|
12139
|
+
Content-Type: application/json\r
|
|
12140
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
12141
|
+
Connection: close\r
|
|
12142
|
+
\r
|
|
12143
|
+
` + body
|
|
12144
|
+
);
|
|
12145
|
+
socket.destroy();
|
|
12146
|
+
}
|
|
12147
|
+
function reasonText(status) {
|
|
12148
|
+
switch (status) {
|
|
12149
|
+
case 401:
|
|
12150
|
+
return "Unauthorized";
|
|
12151
|
+
case 409:
|
|
12152
|
+
return "Conflict";
|
|
12153
|
+
case 500:
|
|
12154
|
+
return "Internal Server Error";
|
|
12155
|
+
case 503:
|
|
12156
|
+
return "Service Unavailable";
|
|
12157
|
+
default:
|
|
12158
|
+
return "Error";
|
|
12159
|
+
}
|
|
12160
|
+
}
|
|
12161
|
+
function attachPtyServer(httpServer, opts = {}) {
|
|
12162
|
+
const path = opts.path ?? "/pty";
|
|
12163
|
+
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
12164
|
+
const acceptSignedOnly = process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
12165
|
+
const publicKeyInput = process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
12166
|
+
let pubKey = null;
|
|
12167
|
+
if (publicKeyInput) {
|
|
12168
|
+
try {
|
|
12169
|
+
pubKey = loadPublicKey(publicKeyInput);
|
|
12170
|
+
} catch (e) {
|
|
12171
|
+
console.warn(
|
|
12172
|
+
`[personal-agent] /pty signature verification disabled \u2014 failed to load public key: ${e.message}`
|
|
12173
|
+
);
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
12176
|
+
const handler = (req, socket, head) => {
|
|
12177
|
+
let pathname = "/";
|
|
12178
|
+
try {
|
|
12179
|
+
pathname = new URL(req.url || "/", "http://placeholder").pathname;
|
|
12180
|
+
} catch {
|
|
12181
|
+
}
|
|
12182
|
+
if (pathname !== path) return;
|
|
12183
|
+
const parsed = parseUpgrade(req);
|
|
12184
|
+
const auth = authenticate(parsed, pathname, pubKey, acceptSignedOnly);
|
|
12185
|
+
if (!auth.ok) {
|
|
12186
|
+
console.warn(`[personal-agent] rejecting /pty upgrade: ${auth.reason}`);
|
|
12187
|
+
rejectUpgrade(socket, auth.status ?? 401, auth.reason ?? "unauthorized");
|
|
12188
|
+
return;
|
|
12189
|
+
}
|
|
12190
|
+
void loadPty().then((pty) => {
|
|
12191
|
+
if (!pty) {
|
|
12192
|
+
rejectUpgrade(
|
|
12193
|
+
socket,
|
|
12194
|
+
503,
|
|
12195
|
+
"node-pty is not available on this device (failed to load native module)"
|
|
12196
|
+
);
|
|
12197
|
+
return;
|
|
12198
|
+
}
|
|
12199
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
12200
|
+
spawnPty(ws, pty, parsed, opts);
|
|
12201
|
+
});
|
|
12202
|
+
});
|
|
12203
|
+
};
|
|
12204
|
+
httpServer.on("upgrade", handler);
|
|
12205
|
+
if (!opts.quiet) {
|
|
12206
|
+
console.log(
|
|
12207
|
+
`[personal-agent] WebSocket PTY server attached at ${path} (host: ${hostname3()}, sigCheck: ${pubKey ? "on" : "off"})`
|
|
12208
|
+
);
|
|
12209
|
+
}
|
|
12210
|
+
return {
|
|
12211
|
+
close: () => {
|
|
12212
|
+
httpServer.off("upgrade", handler);
|
|
12213
|
+
wss.close();
|
|
12214
|
+
}
|
|
12215
|
+
};
|
|
12216
|
+
}
|
|
12217
|
+
function spawnPty(ws, pty, parsed, opts) {
|
|
12218
|
+
const { file, args } = (() => {
|
|
12219
|
+
if (parsed.shell || opts.shell) {
|
|
12220
|
+
const s = parsed.shell ?? opts.shell;
|
|
12221
|
+
return { file: s, args: process.platform === "win32" ? [] : ["-l"] };
|
|
12222
|
+
}
|
|
12223
|
+
return defaultShell();
|
|
12224
|
+
})();
|
|
12225
|
+
const cwd = parsed.cwd ?? opts.cwd ?? process.env.HOME ?? process.cwd();
|
|
12226
|
+
let proc;
|
|
12227
|
+
try {
|
|
12228
|
+
proc = pty.spawn(file, args, {
|
|
12229
|
+
name: "xterm-256color",
|
|
12230
|
+
cols: parsed.cols,
|
|
12231
|
+
rows: parsed.rows,
|
|
12232
|
+
cwd,
|
|
12233
|
+
env: cleanEnv()
|
|
12234
|
+
});
|
|
12235
|
+
} catch (e) {
|
|
12236
|
+
const msg = e.message;
|
|
12237
|
+
safeSend(ws, `\r
|
|
12238
|
+
\x1B[31mFailed to spawn shell: ${msg}\x1B[0m\r
|
|
12239
|
+
`);
|
|
12240
|
+
try {
|
|
12241
|
+
ws.close();
|
|
12242
|
+
} catch {
|
|
12243
|
+
}
|
|
12244
|
+
return;
|
|
12245
|
+
}
|
|
12246
|
+
const banner = `\x1B[90m[sparkecoder pty] ${file} on ${hostname3()} pid=${proc.pid} ${parsed.cols}x${parsed.rows}\x1B[0m\r
|
|
12247
|
+
`;
|
|
12248
|
+
safeSend(ws, banner);
|
|
12249
|
+
proc.onData((data) => safeSend(ws, data));
|
|
12250
|
+
proc.onExit(({ exitCode }) => {
|
|
12251
|
+
safeSend(ws, `\r
|
|
12252
|
+
\x1B[90m[exit ${exitCode}]\x1B[0m\r
|
|
12253
|
+
`);
|
|
12254
|
+
try {
|
|
12255
|
+
ws.close();
|
|
12256
|
+
} catch {
|
|
12257
|
+
}
|
|
12258
|
+
});
|
|
12259
|
+
ws.on("message", (msg, isBinary) => {
|
|
12260
|
+
const input = typeof msg === "string" ? msg : Buffer.isBuffer(msg) ? msg.toString(isBinary ? "utf8" : "utf8") : Array.isArray(msg) ? Buffer.concat(msg).toString("utf8") : "";
|
|
12261
|
+
if (!input) return;
|
|
12262
|
+
const m = input.match(RESIZE_RE);
|
|
12263
|
+
if (m) {
|
|
12264
|
+
const cols = clampInt(m[1], 80, 10, 500);
|
|
12265
|
+
const rows = clampInt(m[2], 24, 5, 500);
|
|
12266
|
+
try {
|
|
12267
|
+
proc.resize(cols, rows);
|
|
12268
|
+
} catch {
|
|
12269
|
+
}
|
|
12270
|
+
if (input.replace(RESIZE_RE, "").length === 0) return;
|
|
12271
|
+
proc.write(input.replace(RESIZE_RE, ""));
|
|
12272
|
+
return;
|
|
12273
|
+
}
|
|
12274
|
+
proc.write(input);
|
|
12275
|
+
});
|
|
12276
|
+
const onClose = () => {
|
|
12277
|
+
try {
|
|
12278
|
+
proc.kill();
|
|
12279
|
+
} catch {
|
|
12280
|
+
}
|
|
12281
|
+
};
|
|
12282
|
+
ws.on("close", onClose);
|
|
12283
|
+
ws.on("error", onClose);
|
|
12284
|
+
}
|
|
12285
|
+
function safeSend(ws, data) {
|
|
12286
|
+
if (ws.readyState !== 1) return;
|
|
12287
|
+
try {
|
|
12288
|
+
ws.send(data);
|
|
12289
|
+
} catch {
|
|
12290
|
+
}
|
|
12291
|
+
}
|
|
12292
|
+
|
|
10662
12293
|
// src/server/index.ts
|
|
10663
12294
|
init_config();
|
|
10664
12295
|
init_db();
|
|
10665
12296
|
|
|
10666
12297
|
// src/utils/dependencies.ts
|
|
10667
|
-
import { exec as
|
|
10668
|
-
import { promisify as
|
|
10669
|
-
import { platform as
|
|
10670
|
-
var
|
|
12298
|
+
import { exec as exec7 } from "child_process";
|
|
12299
|
+
import { promisify as promisify7 } from "util";
|
|
12300
|
+
import { platform as platform4 } from "os";
|
|
12301
|
+
var execAsync7 = promisify7(exec7);
|
|
10671
12302
|
function getInstallInstructions() {
|
|
10672
|
-
const os2 =
|
|
12303
|
+
const os2 = platform4();
|
|
10673
12304
|
if (os2 === "darwin") {
|
|
10674
12305
|
return `
|
|
10675
12306
|
Install tmux on macOS:
|
|
@@ -10700,7 +12331,7 @@ Install tmux:
|
|
|
10700
12331
|
}
|
|
10701
12332
|
async function checkTmux() {
|
|
10702
12333
|
try {
|
|
10703
|
-
const { stdout } = await
|
|
12334
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
10704
12335
|
const version = stdout.trim();
|
|
10705
12336
|
return {
|
|
10706
12337
|
available: true,
|
|
@@ -10749,11 +12380,11 @@ function getWebDirectory() {
|
|
|
10749
12380
|
try {
|
|
10750
12381
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
10751
12382
|
const webDir = resolve10(currentDir, "..", "web");
|
|
10752
|
-
if (
|
|
12383
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
10753
12384
|
return webDir;
|
|
10754
12385
|
}
|
|
10755
12386
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
10756
|
-
if (
|
|
12387
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
10757
12388
|
return altWebDir;
|
|
10758
12389
|
}
|
|
10759
12390
|
return null;
|
|
@@ -10811,23 +12442,23 @@ async function findWebPort(preferredPort) {
|
|
|
10811
12442
|
return { port: preferredPort, alreadyRunning: false };
|
|
10812
12443
|
}
|
|
10813
12444
|
function hasProductionBuild(webDir) {
|
|
10814
|
-
const buildIdPath =
|
|
10815
|
-
return
|
|
12445
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
12446
|
+
return existsSync19(buildIdPath);
|
|
10816
12447
|
}
|
|
10817
12448
|
function hasSourceFiles(webDir) {
|
|
10818
|
-
const appDir =
|
|
10819
|
-
const pagesDir =
|
|
10820
|
-
const rootAppDir =
|
|
10821
|
-
const rootPagesDir =
|
|
10822
|
-
return
|
|
12449
|
+
const appDir = join13(webDir, "src", "app");
|
|
12450
|
+
const pagesDir = join13(webDir, "src", "pages");
|
|
12451
|
+
const rootAppDir = join13(webDir, "app");
|
|
12452
|
+
const rootPagesDir = join13(webDir, "pages");
|
|
12453
|
+
return existsSync19(appDir) || existsSync19(pagesDir) || existsSync19(rootAppDir) || existsSync19(rootPagesDir);
|
|
10823
12454
|
}
|
|
10824
12455
|
function getStandaloneServerPath(webDir) {
|
|
10825
12456
|
const possiblePaths2 = [
|
|
10826
|
-
|
|
10827
|
-
|
|
12457
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
12458
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
10828
12459
|
];
|
|
10829
12460
|
for (const serverPath of possiblePaths2) {
|
|
10830
|
-
if (
|
|
12461
|
+
if (existsSync19(serverPath)) {
|
|
10831
12462
|
return serverPath;
|
|
10832
12463
|
}
|
|
10833
12464
|
}
|
|
@@ -10867,13 +12498,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10867
12498
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
10868
12499
|
return { process: null, port: actualPort };
|
|
10869
12500
|
}
|
|
10870
|
-
const usePnpm =
|
|
10871
|
-
const useNpm = !usePnpm &&
|
|
12501
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
12502
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
10872
12503
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
10873
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
12504
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
10874
12505
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
10875
12506
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
10876
|
-
const runtimeConfigPath =
|
|
12507
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
10877
12508
|
try {
|
|
10878
12509
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
10879
12510
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -10881,7 +12512,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
10881
12512
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
10882
12513
|
}
|
|
10883
12514
|
const webEnv = {
|
|
10884
|
-
...
|
|
12515
|
+
...cleanEnv2,
|
|
10885
12516
|
PORT: String(actualPort)
|
|
10886
12517
|
// Next.js respects PORT env var
|
|
10887
12518
|
};
|
|
@@ -10995,12 +12626,28 @@ function stopWebUI() {
|
|
|
10995
12626
|
}
|
|
10996
12627
|
}
|
|
10997
12628
|
async function createApp(options = {}) {
|
|
10998
|
-
const app = new
|
|
12629
|
+
const app = new Hono7();
|
|
10999
12630
|
app.use("*", cors({
|
|
11000
12631
|
origin: "*",
|
|
11001
12632
|
// Allow all origins
|
|
11002
12633
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11003
|
-
allowHeaders: [
|
|
12634
|
+
allowHeaders: [
|
|
12635
|
+
"Content-Type",
|
|
12636
|
+
"Authorization",
|
|
12637
|
+
"X-Requested-With",
|
|
12638
|
+
// Personal-agent dashboard signs every request to the device with
|
|
12639
|
+
// these. Without them whitelisted the browser preflight strips the
|
|
12640
|
+
// headers and the signature middleware 401s.
|
|
12641
|
+
"X-Signature",
|
|
12642
|
+
"X-Signature-Timestamp",
|
|
12643
|
+
"X-Signature-Algorithm",
|
|
12644
|
+
"X-Device-Hwid",
|
|
12645
|
+
// Short-lived embed token used by the iframed SparkECoder web UI
|
|
12646
|
+
// when it's hosted inside the personal-agents dashboard. The
|
|
12647
|
+
// bootstrap in `web/src/lib/embed-bootstrap.ts` adds this header
|
|
12648
|
+
// to every API call.
|
|
12649
|
+
"X-Embed-Token"
|
|
12650
|
+
],
|
|
11004
12651
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11005
12652
|
maxAge: 86400
|
|
11006
12653
|
// 24 hours
|
|
@@ -11008,12 +12655,15 @@ async function createApp(options = {}) {
|
|
|
11008
12655
|
if (!options.quiet) {
|
|
11009
12656
|
app.use("*", logger());
|
|
11010
12657
|
}
|
|
12658
|
+
app.use("*", hwidMiddleware());
|
|
12659
|
+
app.use("*", signatureMiddleware());
|
|
11011
12660
|
app.route("/health", health);
|
|
11012
12661
|
app.route("/sessions", sessions);
|
|
11013
12662
|
app.route("/agents", agents);
|
|
11014
12663
|
app.route("/sessions", terminals);
|
|
11015
12664
|
app.route("/terminals", terminals);
|
|
11016
12665
|
app.route("/tasks", tasks_default);
|
|
12666
|
+
app.route("/system", system);
|
|
11017
12667
|
app.get("/openapi.json", async (c) => {
|
|
11018
12668
|
return c.json(generateOpenAPISpec());
|
|
11019
12669
|
});
|
|
@@ -11066,8 +12716,8 @@ async function startServer(options = {}) {
|
|
|
11066
12716
|
if (options.workingDirectory) {
|
|
11067
12717
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11068
12718
|
}
|
|
11069
|
-
if (!
|
|
11070
|
-
|
|
12719
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
12720
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11071
12721
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11072
12722
|
}
|
|
11073
12723
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11102,6 +12752,15 @@ async function startServer(options = {}) {
|
|
|
11102
12752
|
port,
|
|
11103
12753
|
hostname: host
|
|
11104
12754
|
});
|
|
12755
|
+
try {
|
|
12756
|
+
attachPtyServer(serverInstance, {
|
|
12757
|
+
quiet: options.quiet
|
|
12758
|
+
});
|
|
12759
|
+
} catch (e) {
|
|
12760
|
+
if (!options.quiet) {
|
|
12761
|
+
console.warn(` \u26A0 Failed to attach /pty WebSocket server: ${e.message}`);
|
|
12762
|
+
}
|
|
12763
|
+
}
|
|
11105
12764
|
let webPort;
|
|
11106
12765
|
let webStarted;
|
|
11107
12766
|
if (options.webUI !== false) {
|