sparkecoder 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +666 -40
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +2001 -226
- package/dist/cli.js.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/{index-OhuTM4a0.d.ts → index-BvIissiB.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1688 -200
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +1688 -200
- package/dist/server/index.js.map +1 -1
- package/dist/skills/default/computer-use.md +150 -0
- package/dist/tools/index.d.ts +167 -1
- package/dist/tools/index.js +609 -11
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -1
- package/src/skills/default/computer-use.md +150 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ecd2bdca._.js → 2374f_317b1fef._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9adc1edb._.js → 2374f_37dd9702._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8dc0f9aa._.js → 2374f_4d44e4ed._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_cc6c6363._.js → 2374f_54ac917f._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_00f7fe07._.js → 2374f_86585101._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_369747ce._.js → 2374f_a383a4d9._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d58d0276._.js → 2374f_c59a35bb._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__25b25c9d._.js → [root-of-the-server]__9a826344._.js} +2 -2
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/package-lock.json +3 -3
- package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
- package/web/.next/standalone/web/src/lib/embed-bootstrap.ts +108 -0
- package/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/static/chunks/5383c5717758f575.js +0 -7
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{J0gen1p9aNjUNIU1NDO5h → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -448,7 +448,11 @@ var init_remote = __esm({
|
|
|
448
448
|
return result.files;
|
|
449
449
|
},
|
|
450
450
|
async getDownloadUrl(fileId) {
|
|
451
|
-
|
|
451
|
+
const result = await storageApi(`/download/${fileId}`);
|
|
452
|
+
return {
|
|
453
|
+
downloadUrl: result.shortUrl || result.downloadUrl,
|
|
454
|
+
expiresAt: result.expiresAt
|
|
455
|
+
};
|
|
452
456
|
},
|
|
453
457
|
async deleteFile(fileId) {
|
|
454
458
|
await storageApi(`/files/${fileId}`, { method: "DELETE" });
|
|
@@ -514,7 +518,12 @@ var init_types = __esm({
|
|
|
514
518
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
515
519
|
alwaysApply: z.boolean().optional().default(false),
|
|
516
520
|
// Glob patterns - auto-inject when working with matching files
|
|
517
|
-
globs: z.array(z.string()).optional().default([])
|
|
521
|
+
globs: z.array(z.string()).optional().default([]),
|
|
522
|
+
// Platform requirements — skill is hidden from the model on platforms
|
|
523
|
+
// not listed here. Values match `process.platform`
|
|
524
|
+
// (darwin, linux, win32, freebsd, ...). If omitted or empty, the skill is
|
|
525
|
+
// available on all platforms.
|
|
526
|
+
platforms: z.array(z.string()).optional().default([])
|
|
518
527
|
});
|
|
519
528
|
TaskConfigSchema = z.object({
|
|
520
529
|
enabled: z.boolean(),
|
|
@@ -532,7 +541,13 @@ var init_types = __esm({
|
|
|
532
541
|
approvalWebhook: z.string().url().optional(),
|
|
533
542
|
skillsDirectory: z.string().optional(),
|
|
534
543
|
maxContextChars: z.number().optional().default(2e5),
|
|
535
|
-
task: TaskConfigSchema.optional()
|
|
544
|
+
task: TaskConfigSchema.optional(),
|
|
545
|
+
// Anthropic computer use tool — opt-in. When true, the `computer` tool is
|
|
546
|
+
// included in the toolset for Anthropic models. Default false.
|
|
547
|
+
computerUseEnabled: z.boolean().optional(),
|
|
548
|
+
// Display dimensions for the computer use tool (defaults: 1280x800).
|
|
549
|
+
computerUseDisplayWidth: z.number().int().positive().optional(),
|
|
550
|
+
computerUseDisplayHeight: z.number().int().positive().optional()
|
|
536
551
|
});
|
|
537
552
|
VectorGatewayConfigSchema = z.object({
|
|
538
553
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -1560,7 +1575,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1560
1575
|
globs: parsed.metadata.globs,
|
|
1561
1576
|
loadType,
|
|
1562
1577
|
priority,
|
|
1563
|
-
sourceDir: directory
|
|
1578
|
+
sourceDir: directory,
|
|
1579
|
+
platforms: parsed.metadata.platforms
|
|
1564
1580
|
});
|
|
1565
1581
|
} else {
|
|
1566
1582
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1573,11 +1589,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1573
1589
|
globs: [],
|
|
1574
1590
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1575
1591
|
priority,
|
|
1576
|
-
sourceDir: directory
|
|
1592
|
+
sourceDir: directory,
|
|
1593
|
+
platforms: []
|
|
1577
1594
|
});
|
|
1578
1595
|
}
|
|
1579
1596
|
}
|
|
1580
|
-
return skills
|
|
1597
|
+
return skills.filter(
|
|
1598
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1599
|
+
);
|
|
1581
1600
|
}
|
|
1582
1601
|
async function loadAllSkills(directories) {
|
|
1583
1602
|
const allSkills = [];
|
|
@@ -2038,7 +2057,7 @@ function mergeSmallBlocks(blocks, lines) {
|
|
|
2038
2057
|
merged.push(current);
|
|
2039
2058
|
return merged;
|
|
2040
2059
|
}
|
|
2041
|
-
function splitLargeBlock(filePath, content, startLine, language,
|
|
2060
|
+
function splitLargeBlock(filePath, content, startLine, language, type2, name) {
|
|
2042
2061
|
const chunks = [];
|
|
2043
2062
|
const lines = content.split("\n");
|
|
2044
2063
|
let currentStart = 0;
|
|
@@ -2058,7 +2077,7 @@ function splitLargeBlock(filePath, content, startLine, language, type, name) {
|
|
|
2058
2077
|
startLine: startLine + currentStart + 1,
|
|
2059
2078
|
endLine: startLine + i,
|
|
2060
2079
|
language,
|
|
2061
|
-
chunkType:
|
|
2080
|
+
chunkType: type2,
|
|
2062
2081
|
symbolName: name
|
|
2063
2082
|
}
|
|
2064
2083
|
});
|
|
@@ -2080,7 +2099,7 @@ function splitLargeBlock(filePath, content, startLine, language, type, name) {
|
|
|
2080
2099
|
startLine: startLine + currentStart + 1,
|
|
2081
2100
|
endLine: startLine + lines.length,
|
|
2082
2101
|
language,
|
|
2083
|
-
chunkType:
|
|
2102
|
+
chunkType: type2,
|
|
2084
2103
|
symbolName: name
|
|
2085
2104
|
}
|
|
2086
2105
|
});
|
|
@@ -2397,18 +2416,18 @@ function isPathExcluded(relativePath, exclude) {
|
|
|
2397
2416
|
});
|
|
2398
2417
|
}
|
|
2399
2418
|
async function walkDirectory(dir, include, exclude, baseDir) {
|
|
2400
|
-
const { readdirSync:
|
|
2401
|
-
const { join:
|
|
2419
|
+
const { readdirSync: readdirSync4 } = await import("fs");
|
|
2420
|
+
const { join: join15, relative: relative10 } = await import("path");
|
|
2402
2421
|
const files = [];
|
|
2403
2422
|
function walk(currentDir) {
|
|
2404
2423
|
let entries;
|
|
2405
2424
|
try {
|
|
2406
|
-
entries =
|
|
2425
|
+
entries = readdirSync4(currentDir, { withFileTypes: true });
|
|
2407
2426
|
} catch {
|
|
2408
2427
|
return;
|
|
2409
2428
|
}
|
|
2410
2429
|
for (const entry of entries) {
|
|
2411
|
-
const fullPath =
|
|
2430
|
+
const fullPath = join15(currentDir, entry.name);
|
|
2412
2431
|
const relativePath = relative10(baseDir, fullPath);
|
|
2413
2432
|
if (isPathExcluded(relativePath, exclude)) {
|
|
2414
2433
|
continue;
|
|
@@ -2910,12 +2929,440 @@ var init_semantic_search = __esm({
|
|
|
2910
2929
|
}
|
|
2911
2930
|
});
|
|
2912
2931
|
|
|
2932
|
+
// src/tools/computer-use.ts
|
|
2933
|
+
var computer_use_exports = {};
|
|
2934
|
+
__export(computer_use_exports, {
|
|
2935
|
+
createComputerUseTool: () => createComputerUseTool,
|
|
2936
|
+
detectScreenSize: () => detectScreenSize,
|
|
2937
|
+
hasAccessibilityPermissions: () => hasAccessibilityPermissions,
|
|
2938
|
+
hasScreenRecordingPermissions: () => hasScreenRecordingPermissions,
|
|
2939
|
+
isCliclickInstalled: () => isCliclickInstalled,
|
|
2940
|
+
isMacOs: () => isMacOs,
|
|
2941
|
+
openSystemSettings: () => openSystemSettings,
|
|
2942
|
+
requestAccessibilityPrompt: () => requestAccessibilityPrompt,
|
|
2943
|
+
requestScreenRecordingPrompt: () => requestScreenRecordingPrompt
|
|
2944
|
+
});
|
|
2945
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
2946
|
+
import { exec as exec5 } from "child_process";
|
|
2947
|
+
import { promisify as promisify5 } from "util";
|
|
2948
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync15, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
2949
|
+
import { join as join8 } from "path";
|
|
2950
|
+
import { tmpdir } from "os";
|
|
2951
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
2952
|
+
function isMacOs() {
|
|
2953
|
+
return process.platform === "darwin";
|
|
2954
|
+
}
|
|
2955
|
+
async function isCliclickInstalled() {
|
|
2956
|
+
try {
|
|
2957
|
+
await execAsync5("command -v cliclick", { timeout: 2e3 });
|
|
2958
|
+
return true;
|
|
2959
|
+
} catch {
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
async function runJxa(script) {
|
|
2964
|
+
try {
|
|
2965
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
2966
|
+
const { stdout } = await execAsync5(`osascript -l JavaScript -e '${escaped}'`, {
|
|
2967
|
+
timeout: 5e3
|
|
2968
|
+
});
|
|
2969
|
+
return JSON.parse(stdout.trim());
|
|
2970
|
+
} catch {
|
|
2971
|
+
return null;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
async function hasAccessibilityPermissions() {
|
|
2975
|
+
try {
|
|
2976
|
+
const { stderr } = await execAsync5("cliclick p:.", { timeout: 3e3 });
|
|
2977
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
2978
|
+
return { ok: false, error: stderr.trim().split("\n")[0] };
|
|
2979
|
+
}
|
|
2980
|
+
return { ok: true };
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
return { ok: false, error: err?.message || String(err) };
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
async function hasScreenRecordingPermissions() {
|
|
2986
|
+
const result = await runJxa(
|
|
2987
|
+
`ObjC.import("Cocoa");
|
|
2988
|
+
ObjC.import("CoreGraphics");
|
|
2989
|
+
ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]);
|
|
2990
|
+
JSON.stringify({ hasAccess: !!$.CGPreflightScreenCaptureAccess() });`
|
|
2991
|
+
);
|
|
2992
|
+
return result?.hasAccess ?? false;
|
|
2993
|
+
}
|
|
2994
|
+
async function requestAccessibilityPrompt() {
|
|
2995
|
+
const result = await runJxa(
|
|
2996
|
+
`ObjC.import("ApplicationServices");
|
|
2997
|
+
var key = $.kAXTrustedCheckOptionPrompt;
|
|
2998
|
+
var dict = $.NSDictionary.dictionaryWithObjectForKey($.kCFBooleanTrue, key);
|
|
2999
|
+
var trusted = $.AXIsProcessTrustedWithOptions(dict);
|
|
3000
|
+
JSON.stringify({ trusted: !!trusted });`
|
|
3001
|
+
);
|
|
3002
|
+
return result?.trusted ?? false;
|
|
3003
|
+
}
|
|
3004
|
+
async function requestScreenRecordingPrompt() {
|
|
3005
|
+
const result = await runJxa(
|
|
3006
|
+
`ObjC.import("Cocoa");
|
|
3007
|
+
ObjC.import("CoreGraphics");
|
|
3008
|
+
ObjC.bindFunction("CGRequestScreenCaptureAccess", ["bool", []]);
|
|
3009
|
+
JSON.stringify({ granted: !!$.CGRequestScreenCaptureAccess() });`
|
|
3010
|
+
);
|
|
3011
|
+
return result?.granted ?? false;
|
|
3012
|
+
}
|
|
3013
|
+
async function openSystemSettings(pane) {
|
|
3014
|
+
const url = pane === "accessibility" ? "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" : "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
3015
|
+
try {
|
|
3016
|
+
await execAsync5(`open '${url}'`, { timeout: 3e3 });
|
|
3017
|
+
} catch {
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function detectScreenSize() {
|
|
3021
|
+
try {
|
|
3022
|
+
const { stdout } = await execAsync5(
|
|
3023
|
+
`osascript -e 'tell application "Finder" to get bounds of window of desktop'`,
|
|
3024
|
+
{ timeout: 3e3 }
|
|
3025
|
+
);
|
|
3026
|
+
const parts = stdout.trim().split(",").map((s) => parseInt(s.trim(), 10));
|
|
3027
|
+
if (parts.length >= 4 && parts.every((n) => Number.isFinite(n))) {
|
|
3028
|
+
const [x1, y1, x2, y2] = parts;
|
|
3029
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
3030
|
+
}
|
|
3031
|
+
} catch {
|
|
3032
|
+
}
|
|
3033
|
+
return null;
|
|
3034
|
+
}
|
|
3035
|
+
async function runCliclick(args) {
|
|
3036
|
+
const quoted = args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
3037
|
+
const { stdout, stderr } = await execAsync5(`cliclick ${quoted}`, {
|
|
3038
|
+
timeout: 15e3,
|
|
3039
|
+
maxBuffer: 1024 * 1024
|
|
3040
|
+
});
|
|
3041
|
+
if (/accessibility privileges not enabled/i.test(stderr)) {
|
|
3042
|
+
throw new Error(
|
|
3043
|
+
"Accessibility permissions not granted to cliclick. Open System Settings \u2192 Privacy & Security \u2192 Accessibility, add cliclick (or the agent runtime), and toggle it on."
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
if (stderr && !stdout) throw new Error(stderr.trim());
|
|
3047
|
+
return (stdout || "").trim();
|
|
3048
|
+
}
|
|
3049
|
+
async function runScreencapture(path) {
|
|
3050
|
+
await execAsync5(`screencapture -x -t png '${path.replace(/'/g, `'\\''`)}'`, {
|
|
3051
|
+
timeout: 5e3
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
async function resizeScreenshotToPoints(path, targetWidth, targetHeight) {
|
|
3055
|
+
const sharpModule = await import("sharp");
|
|
3056
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
3057
|
+
const meta = await sharp2(path).metadata();
|
|
3058
|
+
if (meta.width === targetWidth && meta.height === targetHeight) {
|
|
3059
|
+
return readFileSync7(path);
|
|
3060
|
+
}
|
|
3061
|
+
return await sharp2(path).resize(targetWidth, targetHeight, { fit: "fill" }).png().toBuffer();
|
|
3062
|
+
}
|
|
3063
|
+
async function runScroll(dx, dy) {
|
|
3064
|
+
const wheelY = -Math.round(dy);
|
|
3065
|
+
const wheelX = -Math.round(dx);
|
|
3066
|
+
const script = `ObjC.import('CoreGraphics');var ev = $.CGEventCreateScrollWheelEvent(null, 0, 2, ${wheelY}, ${wheelX});$.CGEventPost(0, ev);`;
|
|
3067
|
+
await execAsync5(
|
|
3068
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
3069
|
+
{ timeout: 5e3 }
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
function translateKeyForCliclick(key) {
|
|
3073
|
+
if (!key) return [];
|
|
3074
|
+
const parts = key.split("+").map((p) => p.trim()).filter(Boolean);
|
|
3075
|
+
if (parts.length === 0) return [];
|
|
3076
|
+
const modMap = {
|
|
3077
|
+
ctrl: "ctrl",
|
|
3078
|
+
control: "ctrl",
|
|
3079
|
+
alt: "alt",
|
|
3080
|
+
option: "alt",
|
|
3081
|
+
shift: "shift",
|
|
3082
|
+
cmd: "cmd",
|
|
3083
|
+
super: "cmd",
|
|
3084
|
+
meta: "cmd",
|
|
3085
|
+
win: "cmd",
|
|
3086
|
+
fn: "fn"
|
|
3087
|
+
};
|
|
3088
|
+
const keyMap = {
|
|
3089
|
+
return: "enter",
|
|
3090
|
+
enter: "enter",
|
|
3091
|
+
esc: "esc",
|
|
3092
|
+
escape: "esc",
|
|
3093
|
+
backspace: "delete",
|
|
3094
|
+
back_space: "delete",
|
|
3095
|
+
delete: "fwd-delete",
|
|
3096
|
+
fwd_delete: "fwd-delete",
|
|
3097
|
+
forward_delete: "fwd-delete",
|
|
3098
|
+
tab: "tab",
|
|
3099
|
+
space: "space",
|
|
3100
|
+
up: "arrow-up",
|
|
3101
|
+
arrow_up: "arrow-up",
|
|
3102
|
+
down: "arrow-down",
|
|
3103
|
+
arrow_down: "arrow-down",
|
|
3104
|
+
left: "arrow-left",
|
|
3105
|
+
arrow_left: "arrow-left",
|
|
3106
|
+
right: "arrow-right",
|
|
3107
|
+
arrow_right: "arrow-right",
|
|
3108
|
+
page_up: "page-up",
|
|
3109
|
+
pageup: "page-up",
|
|
3110
|
+
page_down: "page-down",
|
|
3111
|
+
pagedown: "page-down",
|
|
3112
|
+
home: "home",
|
|
3113
|
+
end: "end",
|
|
3114
|
+
f1: "f1",
|
|
3115
|
+
f2: "f2",
|
|
3116
|
+
f3: "f3",
|
|
3117
|
+
f4: "f4",
|
|
3118
|
+
f5: "f5",
|
|
3119
|
+
f6: "f6",
|
|
3120
|
+
f7: "f7",
|
|
3121
|
+
f8: "f8",
|
|
3122
|
+
f9: "f9",
|
|
3123
|
+
f10: "f10",
|
|
3124
|
+
f11: "f11",
|
|
3125
|
+
f12: "f12"
|
|
3126
|
+
};
|
|
3127
|
+
const modifiers = [];
|
|
3128
|
+
let mainKey = null;
|
|
3129
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3130
|
+
const lower = parts[i].toLowerCase().replace(/-/g, "_");
|
|
3131
|
+
if (i < parts.length - 1 && modMap[lower]) {
|
|
3132
|
+
modifiers.push(modMap[lower]);
|
|
3133
|
+
} else {
|
|
3134
|
+
mainKey = keyMap[lower] || lower;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const args = [];
|
|
3138
|
+
if (modifiers.length > 0) args.push(`kd:${modifiers.join(",")}`);
|
|
3139
|
+
if (mainKey) {
|
|
3140
|
+
const isNamedKey = Object.values(keyMap).includes(mainKey) || /^f([1-9]|1[0-9]|20)$/.test(mainKey) || /^num-/.test(mainKey);
|
|
3141
|
+
if (isNamedKey) {
|
|
3142
|
+
args.push(`kp:${mainKey}`);
|
|
3143
|
+
} else {
|
|
3144
|
+
args.push(`t:${mainKey}`);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
if (modifiers.length > 0) args.push(`ku:${modifiers.join(",")}`);
|
|
3148
|
+
return args;
|
|
3149
|
+
}
|
|
3150
|
+
function modifierStringToCliclick(text) {
|
|
3151
|
+
return text.split("+").map((p) => p.trim().toLowerCase()).map((p) => {
|
|
3152
|
+
if (p === "ctrl" || p === "control") return "ctrl";
|
|
3153
|
+
if (p === "alt" || p === "option") return "alt";
|
|
3154
|
+
if (p === "shift") return "shift";
|
|
3155
|
+
if (p === "super" || p === "meta" || p === "cmd") return "cmd";
|
|
3156
|
+
return "";
|
|
3157
|
+
}).filter(Boolean);
|
|
3158
|
+
}
|
|
3159
|
+
function createComputerUseTool(options) {
|
|
3160
|
+
const displayWidth = options.displayWidth ?? DEFAULT_WIDTH;
|
|
3161
|
+
const displayHeight = options.displayHeight ?? DEFAULT_HEIGHT;
|
|
3162
|
+
return anthropic.tools.computer_20251124({
|
|
3163
|
+
displayWidthPx: displayWidth,
|
|
3164
|
+
displayHeightPx: displayHeight,
|
|
3165
|
+
enableZoom: true,
|
|
3166
|
+
execute: async (input) => {
|
|
3167
|
+
try {
|
|
3168
|
+
switch (input.action) {
|
|
3169
|
+
case "screenshot": {
|
|
3170
|
+
const path = join8(tmpdir(), `cu-${nanoid3(8)}.png`);
|
|
3171
|
+
await runScreencapture(path);
|
|
3172
|
+
const resized = await resizeScreenshotToPoints(path, displayWidth, displayHeight);
|
|
3173
|
+
try {
|
|
3174
|
+
unlinkSync2(path);
|
|
3175
|
+
} catch {
|
|
3176
|
+
}
|
|
3177
|
+
return { type: "image", data: resized.toString("base64") };
|
|
3178
|
+
}
|
|
3179
|
+
case "left_click": {
|
|
3180
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3181
|
+
if (input.text) {
|
|
3182
|
+
const mods = modifierStringToCliclick(input.text);
|
|
3183
|
+
if (mods.length > 0) {
|
|
3184
|
+
await runCliclick([`kd:${mods.join(",")}`, `c:${x},${y}`, `ku:${mods.join(",")}`]);
|
|
3185
|
+
} else {
|
|
3186
|
+
await runCliclick([`c:${x},${y}`]);
|
|
3187
|
+
}
|
|
3188
|
+
} else {
|
|
3189
|
+
await runCliclick([`c:${x},${y}`]);
|
|
3190
|
+
}
|
|
3191
|
+
return `clicked at (${x}, ${y})${input.text ? ` with ${input.text}` : ""}`;
|
|
3192
|
+
}
|
|
3193
|
+
case "right_click": {
|
|
3194
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3195
|
+
await runCliclick([`rc:${x},${y}`]);
|
|
3196
|
+
return `right-clicked at (${x}, ${y})`;
|
|
3197
|
+
}
|
|
3198
|
+
case "middle_click": {
|
|
3199
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3200
|
+
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);`;
|
|
3201
|
+
await execAsync5(
|
|
3202
|
+
`osascript -l JavaScript -e '${script.replace(/'/g, `'\\''`)}'`,
|
|
3203
|
+
{ timeout: 3e3 }
|
|
3204
|
+
);
|
|
3205
|
+
return `middle-clicked at (${x}, ${y})`;
|
|
3206
|
+
}
|
|
3207
|
+
case "double_click": {
|
|
3208
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3209
|
+
await runCliclick([`dc:${x},${y}`]);
|
|
3210
|
+
return `double-clicked at (${x}, ${y})`;
|
|
3211
|
+
}
|
|
3212
|
+
case "triple_click": {
|
|
3213
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3214
|
+
await runCliclick([`tc:${x},${y}`]);
|
|
3215
|
+
return `triple-clicked at (${x}, ${y})`;
|
|
3216
|
+
}
|
|
3217
|
+
case "mouse_move": {
|
|
3218
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3219
|
+
await runCliclick([`m:${x},${y}`]);
|
|
3220
|
+
return `moved cursor to (${x}, ${y})`;
|
|
3221
|
+
}
|
|
3222
|
+
case "left_mouse_down": {
|
|
3223
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3224
|
+
await runCliclick([`dd:${x},${y}`]);
|
|
3225
|
+
return `left mouse button pressed at (${x}, ${y})`;
|
|
3226
|
+
}
|
|
3227
|
+
case "left_mouse_up": {
|
|
3228
|
+
const [x, y] = input.coordinate ?? [0, 0];
|
|
3229
|
+
await runCliclick([`du:${x},${y}`]);
|
|
3230
|
+
return `left mouse button released at (${x}, ${y})`;
|
|
3231
|
+
}
|
|
3232
|
+
case "left_click_drag": {
|
|
3233
|
+
const [sx, sy] = input.start_coordinate ?? [0, 0];
|
|
3234
|
+
const [ex, ey] = input.coordinate ?? [0, 0];
|
|
3235
|
+
await runCliclick([`dd:${sx},${sy}`, `m:${ex},${ey}`, `du:${ex},${ey}`]);
|
|
3236
|
+
return `dragged from (${sx}, ${sy}) to (${ex}, ${ey})`;
|
|
3237
|
+
}
|
|
3238
|
+
case "type": {
|
|
3239
|
+
const text = input.text ?? "";
|
|
3240
|
+
await runCliclick([`t:${text}`]);
|
|
3241
|
+
return `typed ${text.length} character(s)`;
|
|
3242
|
+
}
|
|
3243
|
+
case "key": {
|
|
3244
|
+
const args = translateKeyForCliclick(input.text ?? "");
|
|
3245
|
+
if (args.length === 0) return "no key specified";
|
|
3246
|
+
await runCliclick(args);
|
|
3247
|
+
return `pressed ${input.text}`;
|
|
3248
|
+
}
|
|
3249
|
+
case "hold_key": {
|
|
3250
|
+
const text = (input.text ?? "").toLowerCase();
|
|
3251
|
+
const duration = input.duration ?? 1;
|
|
3252
|
+
const modMap = {
|
|
3253
|
+
ctrl: "ctrl",
|
|
3254
|
+
control: "ctrl",
|
|
3255
|
+
alt: "alt",
|
|
3256
|
+
option: "alt",
|
|
3257
|
+
shift: "shift",
|
|
3258
|
+
cmd: "cmd",
|
|
3259
|
+
super: "cmd",
|
|
3260
|
+
meta: "cmd",
|
|
3261
|
+
fn: "fn"
|
|
3262
|
+
};
|
|
3263
|
+
const cliName = modMap[text] || text;
|
|
3264
|
+
await runCliclick([`kd:${cliName}`]);
|
|
3265
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
3266
|
+
await runCliclick([`ku:${cliName}`]);
|
|
3267
|
+
return `held ${text} for ${duration}s`;
|
|
3268
|
+
}
|
|
3269
|
+
case "scroll": {
|
|
3270
|
+
const direction = input.scroll_direction ?? "down";
|
|
3271
|
+
const amount = input.scroll_amount ?? 3;
|
|
3272
|
+
const px = amount * 100;
|
|
3273
|
+
const dx = direction === "left" ? -px : direction === "right" ? px : 0;
|
|
3274
|
+
const dy = direction === "up" ? -px : direction === "down" ? px : 0;
|
|
3275
|
+
if (input.coordinate) {
|
|
3276
|
+
const [x, y] = input.coordinate;
|
|
3277
|
+
await runCliclick([`m:${x},${y}`]);
|
|
3278
|
+
}
|
|
3279
|
+
const mods = input.text ? modifierStringToCliclick(input.text) : [];
|
|
3280
|
+
if (mods.length > 0) {
|
|
3281
|
+
await runCliclick([`kd:${mods.join(",")}`]);
|
|
3282
|
+
}
|
|
3283
|
+
await runScroll(dx, dy);
|
|
3284
|
+
if (mods.length > 0) {
|
|
3285
|
+
await runCliclick([`ku:${mods.join(",")}`]);
|
|
3286
|
+
}
|
|
3287
|
+
return `scrolled ${direction} by ${amount}`;
|
|
3288
|
+
}
|
|
3289
|
+
case "wait": {
|
|
3290
|
+
const duration = input.duration ?? 1;
|
|
3291
|
+
await new Promise((r) => setTimeout(r, duration * 1e3));
|
|
3292
|
+
return `waited ${duration}s`;
|
|
3293
|
+
}
|
|
3294
|
+
case "cursor_position": {
|
|
3295
|
+
const out = await runCliclick(["p:."]);
|
|
3296
|
+
return `cursor at ${out}`;
|
|
3297
|
+
}
|
|
3298
|
+
case "zoom": {
|
|
3299
|
+
const region = input.region ?? [0, 0, displayWidth, displayHeight];
|
|
3300
|
+
const [x1, y1, x2, y2] = region;
|
|
3301
|
+
const tmpPath = join8(tmpdir(), `cu-zoom-${nanoid3(8)}.png`);
|
|
3302
|
+
await runScreencapture(tmpPath);
|
|
3303
|
+
const sharpModule = await import("sharp");
|
|
3304
|
+
const sharp2 = sharpModule.default || sharpModule;
|
|
3305
|
+
const meta = await sharp2(tmpPath).metadata();
|
|
3306
|
+
const scaleX = (meta.width || displayWidth) / displayWidth;
|
|
3307
|
+
const scaleY = (meta.height || displayHeight) / displayHeight;
|
|
3308
|
+
const px = {
|
|
3309
|
+
left: Math.max(0, Math.round(x1 * scaleX)),
|
|
3310
|
+
top: Math.max(0, Math.round(y1 * scaleY)),
|
|
3311
|
+
width: Math.max(1, Math.round((x2 - x1) * scaleX)),
|
|
3312
|
+
height: Math.max(1, Math.round((y2 - y1) * scaleY))
|
|
3313
|
+
};
|
|
3314
|
+
const buf = await sharp2(tmpPath).extract(px).png().toBuffer();
|
|
3315
|
+
try {
|
|
3316
|
+
unlinkSync2(tmpPath);
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
return { type: "image", data: buf.toString("base64") };
|
|
3320
|
+
}
|
|
3321
|
+
default: {
|
|
3322
|
+
const exhaustive = input.action;
|
|
3323
|
+
return `unsupported action: ${String(exhaustive)}`;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
} catch (err) {
|
|
3327
|
+
const msg = err?.message || String(err);
|
|
3328
|
+
let hint = "";
|
|
3329
|
+
if (/accessibility|not authorized|tcc|operation not permitted/i.test(msg)) {
|
|
3330
|
+
hint = " (Hint: call enable_computer_use to (re-)check permissions and open System Settings)";
|
|
3331
|
+
} else if (/command not found/i.test(msg)) {
|
|
3332
|
+
hint = " (Hint: install cliclick with `brew install cliclick`)";
|
|
3333
|
+
}
|
|
3334
|
+
return `Error: ${msg}${hint}`;
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
3337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3338
|
+
toModelOutput({ output }) {
|
|
3339
|
+
if (typeof output === "string") {
|
|
3340
|
+
return { type: "content", value: [{ type: "text", text: output }] };
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
type: "content",
|
|
3344
|
+
value: [{ type: "media", data: output.data, mediaType: "image/png" }]
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
var execAsync5, DEFAULT_WIDTH, DEFAULT_HEIGHT;
|
|
3350
|
+
var init_computer_use = __esm({
|
|
3351
|
+
"src/tools/computer-use.ts"() {
|
|
3352
|
+
"use strict";
|
|
3353
|
+
execAsync5 = promisify5(exec5);
|
|
3354
|
+
DEFAULT_WIDTH = 1280;
|
|
3355
|
+
DEFAULT_HEIGHT = 800;
|
|
3356
|
+
}
|
|
3357
|
+
});
|
|
3358
|
+
|
|
2913
3359
|
// src/utils/webhook.ts
|
|
2914
3360
|
var webhook_exports = {};
|
|
2915
3361
|
__export(webhook_exports, {
|
|
2916
3362
|
sendWebhook: () => sendWebhook
|
|
2917
3363
|
});
|
|
2918
3364
|
async function sendWebhook(url, event) {
|
|
3365
|
+
const t0 = Date.now();
|
|
2919
3366
|
try {
|
|
2920
3367
|
const controller = new AbortController();
|
|
2921
3368
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2929,17 +3376,36 @@ async function sendWebhook(url, event) {
|
|
|
2929
3376
|
signal: controller.signal
|
|
2930
3377
|
});
|
|
2931
3378
|
clearTimeout(timeout);
|
|
3379
|
+
const ms = Date.now() - t0;
|
|
2932
3380
|
if (!response.ok) {
|
|
2933
|
-
|
|
3381
|
+
const body = await response.text().catch(() => "");
|
|
3382
|
+
console.warn(
|
|
3383
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> HTTP ${response.status} in ${ms}ms${body ? ` (${body.slice(0, 200)})` : ""}`
|
|
3384
|
+
);
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (!QUIET || TERMINAL_EVENTS.has(event.type)) {
|
|
3388
|
+
console.log(
|
|
3389
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> 200 in ${ms}ms`
|
|
3390
|
+
);
|
|
2934
3391
|
}
|
|
2935
3392
|
} catch (err) {
|
|
2936
3393
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2937
|
-
console.warn(
|
|
3394
|
+
console.warn(
|
|
3395
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
3396
|
+
);
|
|
2938
3397
|
}
|
|
2939
3398
|
}
|
|
3399
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2940
3400
|
var init_webhook = __esm({
|
|
2941
3401
|
"src/utils/webhook.ts"() {
|
|
2942
3402
|
"use strict";
|
|
3403
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set([
|
|
3404
|
+
"task.started",
|
|
3405
|
+
"task.completed",
|
|
3406
|
+
"task.failed"
|
|
3407
|
+
]);
|
|
3408
|
+
QUIET = process.env.SPARKECODER_QUIET_WEBHOOKS === "1";
|
|
2943
3409
|
}
|
|
2944
3410
|
});
|
|
2945
3411
|
|
|
@@ -3149,15 +3615,15 @@ var recorder_exports = {};
|
|
|
3149
3615
|
__export(recorder_exports, {
|
|
3150
3616
|
FrameRecorder: () => FrameRecorder
|
|
3151
3617
|
});
|
|
3152
|
-
import { exec as
|
|
3153
|
-
import { promisify as
|
|
3618
|
+
import { exec as exec6 } from "child_process";
|
|
3619
|
+
import { promisify as promisify6 } from "util";
|
|
3154
3620
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
3155
|
-
import { join as
|
|
3156
|
-
import { tmpdir } from "os";
|
|
3157
|
-
import { nanoid as
|
|
3621
|
+
import { join as join9 } from "path";
|
|
3622
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3623
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
3158
3624
|
async function checkFfmpeg() {
|
|
3159
3625
|
try {
|
|
3160
|
-
await
|
|
3626
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
3161
3627
|
return true;
|
|
3162
3628
|
} catch {
|
|
3163
3629
|
return false;
|
|
@@ -3169,11 +3635,11 @@ async function cleanup(dir) {
|
|
|
3169
3635
|
} catch {
|
|
3170
3636
|
}
|
|
3171
3637
|
}
|
|
3172
|
-
var
|
|
3638
|
+
var execAsync6, FrameRecorder;
|
|
3173
3639
|
var init_recorder = __esm({
|
|
3174
3640
|
"src/browser/recorder.ts"() {
|
|
3175
3641
|
"use strict";
|
|
3176
|
-
|
|
3642
|
+
execAsync6 = promisify6(exec6);
|
|
3177
3643
|
FrameRecorder = class {
|
|
3178
3644
|
frames = [];
|
|
3179
3645
|
startTime = null;
|
|
@@ -3209,21 +3675,21 @@ var init_recorder = __esm({
|
|
|
3209
3675
|
*/
|
|
3210
3676
|
async encode() {
|
|
3211
3677
|
if (this.frames.length === 0) return null;
|
|
3212
|
-
const workDir =
|
|
3678
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid4(8)}`);
|
|
3213
3679
|
await mkdir4(workDir, { recursive: true });
|
|
3214
3680
|
try {
|
|
3215
3681
|
for (let i = 0; i < this.frames.length; i++) {
|
|
3216
|
-
const framePath =
|
|
3682
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
3217
3683
|
await writeFile5(framePath, this.frames[i].data);
|
|
3218
3684
|
}
|
|
3219
3685
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
3220
3686
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
3221
3687
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
3222
|
-
const outputPath =
|
|
3688
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
3223
3689
|
const hasFfmpeg = await checkFfmpeg();
|
|
3224
3690
|
if (hasFfmpeg) {
|
|
3225
|
-
await
|
|
3226
|
-
`ffmpeg -y -framerate ${clampedFps} -i "${
|
|
3691
|
+
await execAsync6(
|
|
3692
|
+
`ffmpeg -y -framerate ${clampedFps} -i "${join9(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
|
|
3227
3693
|
{ timeout: 12e4 }
|
|
3228
3694
|
);
|
|
3229
3695
|
} else {
|
|
@@ -3235,7 +3701,7 @@ var init_recorder = __esm({
|
|
|
3235
3701
|
const files = await readdir5(workDir);
|
|
3236
3702
|
for (const f of files) {
|
|
3237
3703
|
if (f.startsWith("frame_")) {
|
|
3238
|
-
await unlink2(
|
|
3704
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
3239
3705
|
});
|
|
3240
3706
|
}
|
|
3241
3707
|
}
|
|
@@ -3256,6 +3722,387 @@ var init_recorder = __esm({
|
|
|
3256
3722
|
}
|
|
3257
3723
|
});
|
|
3258
3724
|
|
|
3725
|
+
// src/personal-agent/system-metrics.ts
|
|
3726
|
+
import { execSync as execSync2 } from "child_process";
|
|
3727
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
3728
|
+
import {
|
|
3729
|
+
arch,
|
|
3730
|
+
cpus,
|
|
3731
|
+
freemem,
|
|
3732
|
+
hostname,
|
|
3733
|
+
loadavg,
|
|
3734
|
+
networkInterfaces,
|
|
3735
|
+
platform as platform2,
|
|
3736
|
+
release,
|
|
3737
|
+
totalmem,
|
|
3738
|
+
type,
|
|
3739
|
+
uptime,
|
|
3740
|
+
userInfo
|
|
3741
|
+
} from "os";
|
|
3742
|
+
function snapshotCpuTimes() {
|
|
3743
|
+
const sum = { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 };
|
|
3744
|
+
for (const c of cpus()) {
|
|
3745
|
+
sum.user += c.times.user;
|
|
3746
|
+
sum.nice += c.times.nice;
|
|
3747
|
+
sum.sys += c.times.sys;
|
|
3748
|
+
sum.idle += c.times.idle;
|
|
3749
|
+
sum.irq += c.times.irq;
|
|
3750
|
+
}
|
|
3751
|
+
return sum;
|
|
3752
|
+
}
|
|
3753
|
+
function readCpuUsage() {
|
|
3754
|
+
const now = snapshotCpuTimes();
|
|
3755
|
+
if (!_lastSample) {
|
|
3756
|
+
_lastSample = now;
|
|
3757
|
+
return 0;
|
|
3758
|
+
}
|
|
3759
|
+
const dUser = now.user - _lastSample.user;
|
|
3760
|
+
const dNice = now.nice - _lastSample.nice;
|
|
3761
|
+
const dSys = now.sys - _lastSample.sys;
|
|
3762
|
+
const dIdle = now.idle - _lastSample.idle;
|
|
3763
|
+
const dIrq = now.irq - _lastSample.irq;
|
|
3764
|
+
const total = dUser + dNice + dSys + dIdle + dIrq;
|
|
3765
|
+
_lastSample = now;
|
|
3766
|
+
if (total <= 0) return 0;
|
|
3767
|
+
return Math.max(0, Math.min(1, (total - dIdle) / total));
|
|
3768
|
+
}
|
|
3769
|
+
function readCpuTempC() {
|
|
3770
|
+
try {
|
|
3771
|
+
if (platform2() === "linux") {
|
|
3772
|
+
let hottest = -Infinity;
|
|
3773
|
+
try {
|
|
3774
|
+
for (const entry of readdirSync3("/sys/class/thermal")) {
|
|
3775
|
+
if (!entry.startsWith("thermal_zone")) continue;
|
|
3776
|
+
try {
|
|
3777
|
+
const v = Number(
|
|
3778
|
+
readFileSync8(`/sys/class/thermal/${entry}/temp`, "utf8").trim()
|
|
3779
|
+
);
|
|
3780
|
+
if (Number.isFinite(v) && v > hottest) hottest = v;
|
|
3781
|
+
} catch {
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
if (hottest > -Infinity) return hottest / 1e3;
|
|
3787
|
+
}
|
|
3788
|
+
const overrideCmd = process.env.PERSONAL_AGENT_TEMP_CMD;
|
|
3789
|
+
if (overrideCmd) {
|
|
3790
|
+
const out = execSync2(overrideCmd, { encoding: "utf8", timeout: 1500 }).trim();
|
|
3791
|
+
const v = Number(out);
|
|
3792
|
+
if (Number.isFinite(v)) return v;
|
|
3793
|
+
}
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
return void 0;
|
|
3797
|
+
}
|
|
3798
|
+
function readDisks() {
|
|
3799
|
+
try {
|
|
3800
|
+
const p = platform2();
|
|
3801
|
+
if (p === "darwin" || p === "linux") {
|
|
3802
|
+
const raw = execSync2("df -kP", { encoding: "utf8", timeout: 2e3 });
|
|
3803
|
+
const lines = raw.trim().split("\n").slice(1);
|
|
3804
|
+
const out = [];
|
|
3805
|
+
for (const line of lines) {
|
|
3806
|
+
const parts = line.trim().split(/\s+/);
|
|
3807
|
+
if (parts.length < 6) continue;
|
|
3808
|
+
const filesystem = parts[0];
|
|
3809
|
+
const total1k = Number(parts[1]);
|
|
3810
|
+
const used1k = Number(parts[2]);
|
|
3811
|
+
const free1k = Number(parts[3]);
|
|
3812
|
+
const mount = parts.slice(5).join(" ");
|
|
3813
|
+
if (!Number.isFinite(total1k) || total1k <= 0) continue;
|
|
3814
|
+
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")) {
|
|
3815
|
+
if (mount !== "/") continue;
|
|
3816
|
+
}
|
|
3817
|
+
out.push({
|
|
3818
|
+
mount,
|
|
3819
|
+
filesystem,
|
|
3820
|
+
totalBytes: total1k * 1024,
|
|
3821
|
+
usedBytes: used1k * 1024,
|
|
3822
|
+
freeBytes: free1k * 1024,
|
|
3823
|
+
usage: total1k > 0 ? used1k / total1k : 0
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
out.sort((a, b) => {
|
|
3827
|
+
const score = (m) => m === "/" ? 0 : m.startsWith("/Users") || m.startsWith("/home") ? 1 : 2;
|
|
3828
|
+
return score(a.mount) - score(b.mount);
|
|
3829
|
+
});
|
|
3830
|
+
return out.slice(0, 6);
|
|
3831
|
+
}
|
|
3832
|
+
if (p === "win32") {
|
|
3833
|
+
const raw = execSync2(
|
|
3834
|
+
"wmic logicaldisk get DeviceID,Size,FreeSpace /format:csv",
|
|
3835
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
3836
|
+
);
|
|
3837
|
+
const out = [];
|
|
3838
|
+
for (const line of raw.trim().split(/\r?\n/).slice(1)) {
|
|
3839
|
+
const cols = line.split(",");
|
|
3840
|
+
if (cols.length < 4) continue;
|
|
3841
|
+
const [, deviceId, freeStr, sizeStr] = cols;
|
|
3842
|
+
const total = Number(sizeStr);
|
|
3843
|
+
const free = Number(freeStr);
|
|
3844
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
3845
|
+
const used = Math.max(0, total - free);
|
|
3846
|
+
out.push({
|
|
3847
|
+
mount: deviceId,
|
|
3848
|
+
totalBytes: total,
|
|
3849
|
+
usedBytes: used,
|
|
3850
|
+
freeBytes: free,
|
|
3851
|
+
usage: used / total
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
return out;
|
|
3855
|
+
}
|
|
3856
|
+
} catch {
|
|
3857
|
+
}
|
|
3858
|
+
return void 0;
|
|
3859
|
+
}
|
|
3860
|
+
function readNetwork() {
|
|
3861
|
+
try {
|
|
3862
|
+
const out = [];
|
|
3863
|
+
const ifaces = networkInterfaces();
|
|
3864
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
3865
|
+
if (!addrs) continue;
|
|
3866
|
+
for (const a of addrs) {
|
|
3867
|
+
if (a.internal) continue;
|
|
3868
|
+
out.push({
|
|
3869
|
+
iface: name,
|
|
3870
|
+
family: a.family,
|
|
3871
|
+
address: a.address,
|
|
3872
|
+
mac: a.mac,
|
|
3873
|
+
internal: a.internal
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
return out;
|
|
3878
|
+
} catch {
|
|
3879
|
+
return void 0;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function readSystemMetrics() {
|
|
3883
|
+
const cpuList = cpus();
|
|
3884
|
+
const usage = readCpuUsage();
|
|
3885
|
+
const tot = totalmem();
|
|
3886
|
+
const free = freemem();
|
|
3887
|
+
const metrics = {
|
|
3888
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3889
|
+
hostname: hostname(),
|
|
3890
|
+
platform: platform2(),
|
|
3891
|
+
arch: arch(),
|
|
3892
|
+
kernelRelease: release(),
|
|
3893
|
+
osType: type(),
|
|
3894
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
3895
|
+
systemUptimeSec: Math.round(uptime()),
|
|
3896
|
+
user: safeUser(),
|
|
3897
|
+
cpu: cpuList[0] ? {
|
|
3898
|
+
model: cpuList[0].model,
|
|
3899
|
+
count: cpuList.length,
|
|
3900
|
+
speedMhz: cpuList[0].speed,
|
|
3901
|
+
loadAvg1: loadavg()[0],
|
|
3902
|
+
loadAvg5: loadavg()[1],
|
|
3903
|
+
loadAvg15: loadavg()[2],
|
|
3904
|
+
usage,
|
|
3905
|
+
tempC: readCpuTempC()
|
|
3906
|
+
} : void 0,
|
|
3907
|
+
memory: {
|
|
3908
|
+
totalBytes: tot,
|
|
3909
|
+
freeBytes: free,
|
|
3910
|
+
usedBytes: Math.max(0, tot - free),
|
|
3911
|
+
usage: tot > 0 ? (tot - free) / tot : 0
|
|
3912
|
+
},
|
|
3913
|
+
disks: readDisks(),
|
|
3914
|
+
network: readNetwork()
|
|
3915
|
+
};
|
|
3916
|
+
return metrics;
|
|
3917
|
+
}
|
|
3918
|
+
function safeUser() {
|
|
3919
|
+
try {
|
|
3920
|
+
return userInfo().username;
|
|
3921
|
+
} catch {
|
|
3922
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
var _lastSample;
|
|
3926
|
+
var init_system_metrics = __esm({
|
|
3927
|
+
"src/personal-agent/system-metrics.ts"() {
|
|
3928
|
+
"use strict";
|
|
3929
|
+
_lastSample = null;
|
|
3930
|
+
snapshotCpuTimes();
|
|
3931
|
+
}
|
|
3932
|
+
});
|
|
3933
|
+
|
|
3934
|
+
// src/personal-agent/heartbeat.ts
|
|
3935
|
+
var heartbeat_exports = {};
|
|
3936
|
+
__export(heartbeat_exports, {
|
|
3937
|
+
getHardwareId: () => getHardwareId,
|
|
3938
|
+
getHardwareIdCached: () => getHardwareIdCached,
|
|
3939
|
+
readConfig: () => readConfig,
|
|
3940
|
+
startPersonalAgent: () => startPersonalAgent
|
|
3941
|
+
});
|
|
3942
|
+
import { execSync as execSync3 } from "child_process";
|
|
3943
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3944
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
3945
|
+
function readConfig(opts) {
|
|
3946
|
+
const enabled = Boolean(opts.personalAgentMode) || process.env.PERSONAL_AGENT_MODE === "1" || process.env.PERSONAL_AGENT_MODE === "true";
|
|
3947
|
+
return {
|
|
3948
|
+
enabled,
|
|
3949
|
+
dashboardUrl: opts.personalAgentDashboard ?? process.env.PERSONAL_AGENT_DASHBOARD,
|
|
3950
|
+
name: opts.personalAgentName ?? process.env.PERSONAL_AGENT_NAME ?? hostname2(),
|
|
3951
|
+
publicUrl: opts.personalAgentPublicUrl ?? process.env.PERSONAL_AGENT_PUBLIC_URL,
|
|
3952
|
+
webUrl: opts.personalAgentWebUrl ?? process.env.PERSONAL_AGENT_WEB_URL,
|
|
3953
|
+
ingestToken: opts.personalAgentIngestToken ?? process.env.PERSONAL_AGENT_INGEST_TOKEN
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
function getHardwareIdCached() {
|
|
3957
|
+
if (_cachedHwid !== null) return _cachedHwid;
|
|
3958
|
+
_cachedHwid = getHardwareId();
|
|
3959
|
+
return _cachedHwid;
|
|
3960
|
+
}
|
|
3961
|
+
function getHardwareId() {
|
|
3962
|
+
const p = platform3();
|
|
3963
|
+
try {
|
|
3964
|
+
if (p === "darwin") {
|
|
3965
|
+
const out = execSync3(
|
|
3966
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/ {print $4}'`,
|
|
3967
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
3968
|
+
).trim();
|
|
3969
|
+
if (out) return normalize2(out);
|
|
3970
|
+
} else if (p === "linux") {
|
|
3971
|
+
try {
|
|
3972
|
+
return normalize2(readFileSync9("/etc/machine-id", "utf8").trim());
|
|
3973
|
+
} catch {
|
|
3974
|
+
return normalize2(readFileSync9("/var/lib/dbus/machine-id", "utf8").trim());
|
|
3975
|
+
}
|
|
3976
|
+
} else if (p === "win32") {
|
|
3977
|
+
const out = execSync3("wmic csproduct get uuid /value", {
|
|
3978
|
+
encoding: "utf8",
|
|
3979
|
+
timeout: 3e3
|
|
3980
|
+
});
|
|
3981
|
+
const m = out.match(/UUID=([\w-]+)/i);
|
|
3982
|
+
if (m && m[1]) return normalize2(m[1]);
|
|
3983
|
+
}
|
|
3984
|
+
} catch (e) {
|
|
3985
|
+
console.warn(`[personal-agent] could not read hardware UUID: ${e.message}`);
|
|
3986
|
+
}
|
|
3987
|
+
console.warn(
|
|
3988
|
+
"[personal-agent] falling back to hostname for HWID; this is NOT stable across rename/reinstall"
|
|
3989
|
+
);
|
|
3990
|
+
return `host-${hostname2()}`;
|
|
3991
|
+
}
|
|
3992
|
+
function normalize2(raw) {
|
|
3993
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
3994
|
+
}
|
|
3995
|
+
function startPersonalAgent(cfg) {
|
|
3996
|
+
if (!cfg.enabled) return;
|
|
3997
|
+
if (!cfg.dashboardUrl) {
|
|
3998
|
+
console.warn(
|
|
3999
|
+
"[personal-agent] --personal-agent-mode is on but no --personal-agent-dashboard set; skipping heartbeat"
|
|
4000
|
+
);
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (!cfg.publicUrl) {
|
|
4004
|
+
console.warn(
|
|
4005
|
+
"[personal-agent] no --personal-agent-public-url set; the dashboard won't know how to call this sparkecoder"
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
if (!cfg.ingestToken) {
|
|
4009
|
+
console.warn(
|
|
4010
|
+
"[personal-agent] no --personal-agent-ingest-token set; the dashboard's heartbeat endpoint will reject us with 401"
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
const hwid = getHardwareId();
|
|
4014
|
+
const startedAt = Date.now();
|
|
4015
|
+
console.log("");
|
|
4016
|
+
console.log(`Personal Agent: ${cfg.name}`);
|
|
4017
|
+
console.log(`HWID: ${hwid}`);
|
|
4018
|
+
if (cfg.publicUrl) console.log(`Public URL: ${cfg.publicUrl}`);
|
|
4019
|
+
if (cfg.webUrl) console.log(`Web URL: ${cfg.webUrl}`);
|
|
4020
|
+
console.log(`Dashboard: ${cfg.dashboardUrl}`);
|
|
4021
|
+
console.log("");
|
|
4022
|
+
console.log(`Ask an admin to assign this HWID to your user from`);
|
|
4023
|
+
console.log(`${cfg.dashboardUrl.replace(/\/$/, "")}/admin/devices`);
|
|
4024
|
+
console.log("");
|
|
4025
|
+
const warned = /* @__PURE__ */ new Set();
|
|
4026
|
+
function warnOnce(key, msg) {
|
|
4027
|
+
if (warned.has(key)) return;
|
|
4028
|
+
warned.add(key);
|
|
4029
|
+
console.warn(msg);
|
|
4030
|
+
}
|
|
4031
|
+
async function tick() {
|
|
4032
|
+
if (!cfg.publicUrl) {
|
|
4033
|
+
warnOnce(
|
|
4034
|
+
"no-public-url",
|
|
4035
|
+
"[personal-agent] skipping heartbeat: no --personal-agent-public-url / PERSONAL_AGENT_PUBLIC_URL set"
|
|
4036
|
+
);
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
let metrics;
|
|
4040
|
+
try {
|
|
4041
|
+
metrics = readSystemMetrics();
|
|
4042
|
+
} catch (e) {
|
|
4043
|
+
warnOnce(
|
|
4044
|
+
"metrics-error",
|
|
4045
|
+
`[personal-agent] failed to collect system metrics for heartbeat: ${e.message}`
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4048
|
+
const body = {
|
|
4049
|
+
hwid,
|
|
4050
|
+
name: cfg.name,
|
|
4051
|
+
sparkecoderUrl: cfg.publicUrl,
|
|
4052
|
+
...cfg.webUrl ? { webUrl: cfg.webUrl } : {},
|
|
4053
|
+
status: "online",
|
|
4054
|
+
uptimeMs: Date.now() - startedAt,
|
|
4055
|
+
os: platform3(),
|
|
4056
|
+
...metrics ? { metrics } : {}
|
|
4057
|
+
};
|
|
4058
|
+
try {
|
|
4059
|
+
const res = await fetch(
|
|
4060
|
+
`${cfg.dashboardUrl.replace(/\/$/, "")}/api/devices/heartbeat`,
|
|
4061
|
+
{
|
|
4062
|
+
method: "POST",
|
|
4063
|
+
headers: {
|
|
4064
|
+
"Content-Type": "application/json",
|
|
4065
|
+
...cfg.ingestToken ? { Authorization: `Bearer ${cfg.ingestToken}` } : {}
|
|
4066
|
+
},
|
|
4067
|
+
body: JSON.stringify(body)
|
|
4068
|
+
}
|
|
4069
|
+
);
|
|
4070
|
+
if (res.status === 401) {
|
|
4071
|
+
warnOnce(
|
|
4072
|
+
"401",
|
|
4073
|
+
"[personal-agent] heartbeat got 401 from dashboard \u2014 check PERSONAL_AGENT_INGEST_TOKEN matches both sides"
|
|
4074
|
+
);
|
|
4075
|
+
} else if (res.status === 400) {
|
|
4076
|
+
const text = await res.text().catch(() => "");
|
|
4077
|
+
warnOnce(
|
|
4078
|
+
"400",
|
|
4079
|
+
`[personal-agent] heartbeat got 400 from dashboard \u2014 ${text.slice(0, 200)}`
|
|
4080
|
+
);
|
|
4081
|
+
} else if (!res.ok) {
|
|
4082
|
+
warnOnce(
|
|
4083
|
+
`non2xx-${res.status}`,
|
|
4084
|
+
`[personal-agent] heartbeat got ${res.status} from dashboard`
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
} catch (e) {
|
|
4088
|
+
warnOnce(
|
|
4089
|
+
"fetch-error",
|
|
4090
|
+
`[personal-agent] heartbeat network error: ${e.message}`
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
void tick();
|
|
4095
|
+
setInterval(tick, 15e3).unref();
|
|
4096
|
+
}
|
|
4097
|
+
var _cachedHwid;
|
|
4098
|
+
var init_heartbeat = __esm({
|
|
4099
|
+
"src/personal-agent/heartbeat.ts"() {
|
|
4100
|
+
"use strict";
|
|
4101
|
+
init_system_metrics();
|
|
4102
|
+
_cachedHwid = null;
|
|
4103
|
+
}
|
|
4104
|
+
});
|
|
4105
|
+
|
|
3259
4106
|
// src/cli.ts
|
|
3260
4107
|
import { Command } from "commander";
|
|
3261
4108
|
import chalk from "chalk";
|
|
@@ -3265,12 +4112,12 @@ import { createInterface } from "readline";
|
|
|
3265
4112
|
|
|
3266
4113
|
// src/server/index.ts
|
|
3267
4114
|
import "dotenv/config";
|
|
3268
|
-
import { Hono as
|
|
4115
|
+
import { Hono as Hono7 } from "hono";
|
|
3269
4116
|
import { serve } from "@hono/node-server";
|
|
3270
4117
|
import { cors } from "hono/cors";
|
|
3271
4118
|
import { logger } from "hono/logger";
|
|
3272
|
-
import { existsSync as
|
|
3273
|
-
import { resolve as resolve10, dirname as dirname7, join as
|
|
4119
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
4120
|
+
import { resolve as resolve10, dirname as dirname7, join as join13 } from "path";
|
|
3274
4121
|
import { spawn as spawn2 } from "child_process";
|
|
3275
4122
|
import { createServer as createNetServer } from "net";
|
|
3276
4123
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -3279,17 +4126,17 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
3279
4126
|
init_db();
|
|
3280
4127
|
import { Hono } from "hono";
|
|
3281
4128
|
import { zValidator } from "@hono/zod-validator";
|
|
3282
|
-
import { z as
|
|
3283
|
-
import { existsSync as
|
|
4129
|
+
import { z as z16 } from "zod";
|
|
4130
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
3284
4131
|
import { readdir as readdir6 } from "fs/promises";
|
|
3285
|
-
import { join as
|
|
3286
|
-
import { nanoid as
|
|
4132
|
+
import { join as join10, basename as basename5, extname as extname8, relative as relative9 } from "path";
|
|
4133
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
3287
4134
|
|
|
3288
4135
|
// src/agent/index.ts
|
|
3289
4136
|
import {
|
|
3290
4137
|
streamText as streamText2,
|
|
3291
4138
|
generateText as generateText3,
|
|
3292
|
-
tool as
|
|
4139
|
+
tool as tool14,
|
|
3293
4140
|
stepCountIs as stepCountIs2
|
|
3294
4141
|
} from "ai";
|
|
3295
4142
|
|
|
@@ -3480,8 +4327,8 @@ var SUBAGENT_MODELS = {
|
|
|
3480
4327
|
// src/agent/index.ts
|
|
3481
4328
|
init_db();
|
|
3482
4329
|
init_config();
|
|
3483
|
-
import { z as
|
|
3484
|
-
import { nanoid as
|
|
4330
|
+
import { z as z15 } from "zod";
|
|
4331
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
3485
4332
|
|
|
3486
4333
|
// src/tools/bash.ts
|
|
3487
4334
|
import { tool } from "ai";
|
|
@@ -4548,12 +5395,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
4548
5395
|
}
|
|
4549
5396
|
async function commandExists(cmd) {
|
|
4550
5397
|
try {
|
|
4551
|
-
const { exec:
|
|
4552
|
-
const { promisify:
|
|
4553
|
-
const
|
|
5398
|
+
const { exec: exec8 } = await import("child_process");
|
|
5399
|
+
const { promisify: promisify8 } = await import("util");
|
|
5400
|
+
const execAsync8 = promisify8(exec8);
|
|
4554
5401
|
const isWindows = process.platform === "win32";
|
|
4555
5402
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
4556
|
-
await
|
|
5403
|
+
await execAsync8(checkCmd);
|
|
4557
5404
|
return true;
|
|
4558
5405
|
} catch {
|
|
4559
5406
|
return false;
|
|
@@ -6944,11 +7791,169 @@ function createUploadFileTool(options) {
|
|
|
6944
7791
|
});
|
|
6945
7792
|
}
|
|
6946
7793
|
|
|
7794
|
+
// src/tools/index.ts
|
|
7795
|
+
init_computer_use();
|
|
7796
|
+
|
|
7797
|
+
// src/tools/enable-computer-use.ts
|
|
7798
|
+
init_db();
|
|
7799
|
+
init_computer_use();
|
|
7800
|
+
import { tool as tool13 } from "ai";
|
|
7801
|
+
import { z as z14 } from "zod";
|
|
7802
|
+
var inputSchema = z14.object({
|
|
7803
|
+
display_width: z14.number().int().positive().optional().describe("Display width in pixels (defaults to detected primary display, fallback 1280)"),
|
|
7804
|
+
display_height: z14.number().int().positive().optional().describe("Display height in pixels (defaults to detected primary display, fallback 800)"),
|
|
7805
|
+
request_permissions: z14.boolean().optional().default(true).describe(
|
|
7806
|
+
"When true (default), proactively trigger macOS permission prompts and open System Settings panes for any missing permissions."
|
|
7807
|
+
)
|
|
7808
|
+
});
|
|
7809
|
+
var ACCESSIBILITY_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
7810
|
+
var SCREEN_RECORDING_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
|
|
7811
|
+
function createEnableComputerUseTool(options) {
|
|
7812
|
+
return tool13({
|
|
7813
|
+
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.",
|
|
7814
|
+
inputSchema,
|
|
7815
|
+
execute: async ({ display_width, display_height, request_permissions }) => {
|
|
7816
|
+
try {
|
|
7817
|
+
if (!isMacOs()) {
|
|
7818
|
+
return {
|
|
7819
|
+
success: false,
|
|
7820
|
+
error: "Computer use is currently only supported on macOS.",
|
|
7821
|
+
platform: process.platform
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
if (!await isCliclickInstalled()) {
|
|
7825
|
+
return {
|
|
7826
|
+
success: false,
|
|
7827
|
+
error: "`cliclick` is not installed. It is required for mouse/keyboard control on macOS.",
|
|
7828
|
+
installCommand: "brew install cliclick",
|
|
7829
|
+
fixSteps: [
|
|
7830
|
+
"In a terminal on this Mac, run: brew install cliclick",
|
|
7831
|
+
"(If Homebrew is not installed, install it first from https://brew.sh)",
|
|
7832
|
+
"Then call enable_computer_use again"
|
|
7833
|
+
]
|
|
7834
|
+
};
|
|
7835
|
+
}
|
|
7836
|
+
const acc = await hasAccessibilityPermissions();
|
|
7837
|
+
const screen = await hasScreenRecordingPermissions();
|
|
7838
|
+
const missing = [];
|
|
7839
|
+
if (!acc.ok) {
|
|
7840
|
+
let prompted = false;
|
|
7841
|
+
let panelOpened = false;
|
|
7842
|
+
if (request_permissions) {
|
|
7843
|
+
prompted = await requestAccessibilityPrompt().then(() => true).catch(() => false);
|
|
7844
|
+
await openSystemSettings("accessibility").then(() => {
|
|
7845
|
+
panelOpened = true;
|
|
7846
|
+
}).catch(() => void 0);
|
|
7847
|
+
}
|
|
7848
|
+
missing.push({
|
|
7849
|
+
name: "Accessibility",
|
|
7850
|
+
reason: "cliclick failed: " + (acc.error?.split("\n")[0] || "no permission"),
|
|
7851
|
+
pane: "accessibility",
|
|
7852
|
+
settingsUrl: ACCESSIBILITY_URL,
|
|
7853
|
+
fixSteps: [
|
|
7854
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Accessibility pane that opened",
|
|
7855
|
+
"Click the + button",
|
|
7856
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
7857
|
+
"Toggle the switch ON",
|
|
7858
|
+
"Restart the agent process so the new permission takes effect",
|
|
7859
|
+
"Then call enable_computer_use again"
|
|
7860
|
+
],
|
|
7861
|
+
prompted,
|
|
7862
|
+
panelOpened
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
if (!screen) {
|
|
7866
|
+
let prompted = false;
|
|
7867
|
+
let panelOpened = false;
|
|
7868
|
+
if (request_permissions) {
|
|
7869
|
+
prompted = await requestScreenRecordingPrompt().then(() => true).catch(() => false);
|
|
7870
|
+
await openSystemSettings("screen-recording").then(() => {
|
|
7871
|
+
panelOpened = true;
|
|
7872
|
+
}).catch(() => void 0);
|
|
7873
|
+
}
|
|
7874
|
+
missing.push({
|
|
7875
|
+
name: "Screen Recording",
|
|
7876
|
+
reason: "CGPreflightScreenCaptureAccess returned false",
|
|
7877
|
+
pane: "screen-recording",
|
|
7878
|
+
settingsUrl: SCREEN_RECORDING_URL,
|
|
7879
|
+
fixSteps: [
|
|
7880
|
+
"In the System Settings \u2192 Privacy & Security \u2192 Screen Recording pane that opened",
|
|
7881
|
+
"Click the + button",
|
|
7882
|
+
"Add the application running the agent (Terminal, iTerm, your IDE, or `node`)",
|
|
7883
|
+
"Toggle the switch ON",
|
|
7884
|
+
"Restart the agent process so the new permission takes effect",
|
|
7885
|
+
"Then call enable_computer_use again"
|
|
7886
|
+
],
|
|
7887
|
+
prompted,
|
|
7888
|
+
panelOpened
|
|
7889
|
+
});
|
|
7890
|
+
}
|
|
7891
|
+
if (missing.length > 0) {
|
|
7892
|
+
return {
|
|
7893
|
+
success: false,
|
|
7894
|
+
error: `Missing permission${missing.length > 1 ? "s" : ""}: ` + missing.map((m) => m.name).join(" and ") + ".",
|
|
7895
|
+
missingPermissions: missing,
|
|
7896
|
+
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."
|
|
7897
|
+
};
|
|
7898
|
+
}
|
|
7899
|
+
let width = display_width;
|
|
7900
|
+
let height = display_height;
|
|
7901
|
+
let detected = null;
|
|
7902
|
+
if (width === void 0 || height === void 0) {
|
|
7903
|
+
detected = await detectScreenSize();
|
|
7904
|
+
width = width ?? detected?.width ?? 1280;
|
|
7905
|
+
height = height ?? detected?.height ?? 800;
|
|
7906
|
+
}
|
|
7907
|
+
const session = await sessionQueries.getById(options.sessionId);
|
|
7908
|
+
if (!session) {
|
|
7909
|
+
return { success: false, error: "Session not found" };
|
|
7910
|
+
}
|
|
7911
|
+
const config = session.config || {};
|
|
7912
|
+
if (config.computerUseEnabled === true && config.computerUseDisplayWidth === width && config.computerUseDisplayHeight === height) {
|
|
7913
|
+
return {
|
|
7914
|
+
success: true,
|
|
7915
|
+
alreadyEnabled: true,
|
|
7916
|
+
message: "Computer use was already enabled for this session.",
|
|
7917
|
+
displayWidth: width,
|
|
7918
|
+
displayHeight: height
|
|
7919
|
+
};
|
|
7920
|
+
}
|
|
7921
|
+
const updated = {
|
|
7922
|
+
...config,
|
|
7923
|
+
computerUseEnabled: true,
|
|
7924
|
+
computerUseDisplayWidth: width,
|
|
7925
|
+
computerUseDisplayHeight: height
|
|
7926
|
+
};
|
|
7927
|
+
await sessionQueries.update(options.sessionId, { config: updated });
|
|
7928
|
+
return {
|
|
7929
|
+
success: true,
|
|
7930
|
+
enabled: true,
|
|
7931
|
+
platform: "darwin",
|
|
7932
|
+
displayWidth: width,
|
|
7933
|
+
displayHeight: height,
|
|
7934
|
+
detectedScreenSize: detected || void 0,
|
|
7935
|
+
permissions: {
|
|
7936
|
+
accessibility: "granted",
|
|
7937
|
+
screenRecording: "granted"
|
|
7938
|
+
},
|
|
7939
|
+
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.`
|
|
7940
|
+
};
|
|
7941
|
+
} catch (err) {
|
|
7942
|
+
return {
|
|
7943
|
+
success: false,
|
|
7944
|
+
error: err?.message || String(err)
|
|
7945
|
+
};
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
});
|
|
7949
|
+
}
|
|
7950
|
+
|
|
6947
7951
|
// src/tools/index.ts
|
|
6948
7952
|
init_semantic();
|
|
6949
7953
|
init_remote();
|
|
6950
7954
|
init_todo();
|
|
6951
7955
|
init_semantic_search();
|
|
7956
|
+
init_computer_use();
|
|
6952
7957
|
async function createTools(options) {
|
|
6953
7958
|
const tools = {
|
|
6954
7959
|
bash: createBashTool({
|
|
@@ -6992,6 +7997,20 @@ async function createTools(options) {
|
|
|
6992
7997
|
sessionId: options.sessionId
|
|
6993
7998
|
});
|
|
6994
7999
|
}
|
|
8000
|
+
if (process.platform === "darwin") {
|
|
8001
|
+
if (options.enableComputerUse) {
|
|
8002
|
+
tools.computer = createComputerUseTool({
|
|
8003
|
+
workingDirectory: options.workingDirectory,
|
|
8004
|
+
sessionId: options.sessionId,
|
|
8005
|
+
displayWidth: options.computerUseDisplayWidth,
|
|
8006
|
+
displayHeight: options.computerUseDisplayHeight
|
|
8007
|
+
});
|
|
8008
|
+
} else {
|
|
8009
|
+
tools.enable_computer_use = createEnableComputerUseTool({
|
|
8010
|
+
sessionId: options.sessionId
|
|
8011
|
+
});
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
6995
8014
|
if (options.enableSemanticSearch !== false) {
|
|
6996
8015
|
try {
|
|
6997
8016
|
if (isVectorGatewayConfigured()) {
|
|
@@ -7022,11 +8041,11 @@ init_db();
|
|
|
7022
8041
|
init_todo();
|
|
7023
8042
|
import os from "os";
|
|
7024
8043
|
function getSearchInstructions() {
|
|
7025
|
-
const
|
|
8044
|
+
const platform5 = process.platform;
|
|
7026
8045
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
7027
8046
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
7028
8047
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
7029
|
-
if (
|
|
8048
|
+
if (platform5 === "win32") {
|
|
7030
8049
|
return `${common}
|
|
7031
8050
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
7032
8051
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -7073,13 +8092,13 @@ async function buildSystemPrompt(options) {
|
|
|
7073
8092
|
);
|
|
7074
8093
|
const hasNoTodos = todos.length === 0;
|
|
7075
8094
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
7076
|
-
const
|
|
8095
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
7077
8096
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
7078
8097
|
const searchInstructions = getSearchInstructions();
|
|
7079
8098
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
7080
8099
|
|
|
7081
8100
|
## Environment
|
|
7082
|
-
- **Platform**: ${
|
|
8101
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
7083
8102
|
- **Date**: ${currentDate}
|
|
7084
8103
|
- **Working Directory**: ${workingDirectory}
|
|
7085
8104
|
|
|
@@ -8017,10 +9036,14 @@ var Agent = class _Agent {
|
|
|
8017
9036
|
*/
|
|
8018
9037
|
async createToolsWithCallbacks(options) {
|
|
8019
9038
|
const config = getConfig();
|
|
9039
|
+
const sessionConfig = this.session.config || {};
|
|
8020
9040
|
return createTools({
|
|
8021
9041
|
sessionId: this.session.id,
|
|
8022
9042
|
workingDirectory: this.session.workingDirectory,
|
|
8023
9043
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9044
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
9045
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
9046
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
8024
9047
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
8025
9048
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
8026
9049
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -8053,10 +9076,14 @@ var Agent = class _Agent {
|
|
|
8053
9076
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
8054
9077
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
8055
9078
|
});
|
|
9079
|
+
const sessionConfig = session.config || {};
|
|
8056
9080
|
const tools = await createTools({
|
|
8057
9081
|
sessionId: session.id,
|
|
8058
9082
|
workingDirectory: session.workingDirectory,
|
|
8059
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
9083
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9084
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
9085
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
9086
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
8060
9087
|
});
|
|
8061
9088
|
return new _Agent(session, context, tools);
|
|
8062
9089
|
}
|
|
@@ -8229,10 +9256,10 @@ ${prompt}` });
|
|
|
8229
9256
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
8230
9257
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
8231
9258
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
8232
|
-
const fireWebhook = (
|
|
9259
|
+
const fireWebhook = (type2, data) => {
|
|
8233
9260
|
if (!webhookUrl) return;
|
|
8234
9261
|
sendWebhook(webhookUrl, {
|
|
8235
|
-
type,
|
|
9262
|
+
type: type2,
|
|
8236
9263
|
taskId: this.session.id,
|
|
8237
9264
|
sessionId: this.session.id,
|
|
8238
9265
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -8275,10 +9302,14 @@ ${prompt}` });
|
|
|
8275
9302
|
});
|
|
8276
9303
|
}
|
|
8277
9304
|
};
|
|
9305
|
+
const taskSessionConfig = this.session.config || {};
|
|
8278
9306
|
const taskTools = await createTools({
|
|
8279
9307
|
sessionId: this.session.id,
|
|
8280
9308
|
workingDirectory: this.session.workingDirectory,
|
|
8281
9309
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9310
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
9311
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
9312
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
8282
9313
|
onBashProgress: bashProgressHandler,
|
|
8283
9314
|
onWriteFileProgress: (progress) => {
|
|
8284
9315
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -8561,11 +9592,11 @@ ${taskAddendum}`;
|
|
|
8561
9592
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
8562
9593
|
if (!isRemoteConfigured2()) return [];
|
|
8563
9594
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
8564
|
-
const { join:
|
|
9595
|
+
const { join: join15, basename: basename6 } = await import("path");
|
|
8565
9596
|
const urls = [];
|
|
8566
9597
|
for (const filePath of filePaths) {
|
|
8567
9598
|
try {
|
|
8568
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
9599
|
+
const fullPath = filePath.startsWith("/") ? filePath : join15(this.session.workingDirectory, filePath);
|
|
8569
9600
|
const fileName = basename6(fullPath);
|
|
8570
9601
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
8571
9602
|
const mimeMap = {
|
|
@@ -8623,11 +9654,11 @@ ${taskAddendum}`;
|
|
|
8623
9654
|
wrappedTools[name] = originalTool;
|
|
8624
9655
|
continue;
|
|
8625
9656
|
}
|
|
8626
|
-
wrappedTools[name] =
|
|
9657
|
+
wrappedTools[name] = tool14({
|
|
8627
9658
|
description: originalTool.description || "",
|
|
8628
|
-
inputSchema: originalTool.inputSchema ||
|
|
9659
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
8629
9660
|
execute: async (input, toolOptions) => {
|
|
8630
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
9661
|
+
const toolCallId = toolOptions.toolCallId || nanoid5();
|
|
8631
9662
|
const execution = toolExecutionQueries.create({
|
|
8632
9663
|
sessionId: this.session.id,
|
|
8633
9664
|
toolName: name,
|
|
@@ -8645,10 +9676,10 @@ ${taskAddendum}`;
|
|
|
8645
9676
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
8646
9677
|
approvalResolvers.delete(toolCallId);
|
|
8647
9678
|
this.pendingApprovals.delete(toolCallId);
|
|
8648
|
-
const
|
|
9679
|
+
const exec8 = await execution;
|
|
8649
9680
|
if (!approved) {
|
|
8650
9681
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
8651
|
-
await toolExecutionQueries.reject(
|
|
9682
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
8652
9683
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
8653
9684
|
return {
|
|
8654
9685
|
status: "rejected",
|
|
@@ -8658,14 +9689,14 @@ ${taskAddendum}`;
|
|
|
8658
9689
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
8659
9690
|
};
|
|
8660
9691
|
}
|
|
8661
|
-
await toolExecutionQueries.approve(
|
|
9692
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
8662
9693
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
8663
9694
|
try {
|
|
8664
9695
|
const result = await originalTool.execute(input, toolOptions);
|
|
8665
|
-
await toolExecutionQueries.complete(
|
|
9696
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
8666
9697
|
return result;
|
|
8667
9698
|
} catch (error) {
|
|
8668
|
-
await toolExecutionQueries.complete(
|
|
9699
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
8669
9700
|
throw error;
|
|
8670
9701
|
}
|
|
8671
9702
|
}
|
|
@@ -8766,18 +9797,20 @@ function cleanupPendingInputs() {
|
|
|
8766
9797
|
}
|
|
8767
9798
|
}
|
|
8768
9799
|
}
|
|
8769
|
-
var createSessionSchema =
|
|
8770
|
-
name:
|
|
8771
|
-
workingDirectory:
|
|
8772
|
-
model:
|
|
8773
|
-
toolApprovals:
|
|
9800
|
+
var createSessionSchema = z16.object({
|
|
9801
|
+
name: z16.string().optional(),
|
|
9802
|
+
workingDirectory: z16.string().optional(),
|
|
9803
|
+
model: z16.string().optional(),
|
|
9804
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional(),
|
|
9805
|
+
// Optional full session-config passthrough (computerUseEnabled, etc.)
|
|
9806
|
+
config: z16.record(z16.string(), z16.unknown()).optional()
|
|
8774
9807
|
});
|
|
8775
|
-
var paginationQuerySchema =
|
|
8776
|
-
limit:
|
|
8777
|
-
offset:
|
|
9808
|
+
var paginationQuerySchema = z16.object({
|
|
9809
|
+
limit: z16.string().optional(),
|
|
9810
|
+
offset: z16.string().optional()
|
|
8778
9811
|
});
|
|
8779
|
-
var messagesQuerySchema =
|
|
8780
|
-
limit:
|
|
9812
|
+
var messagesQuerySchema = z16.object({
|
|
9813
|
+
limit: z16.string().optional()
|
|
8781
9814
|
});
|
|
8782
9815
|
sessions.get(
|
|
8783
9816
|
"/",
|
|
@@ -8815,11 +9848,20 @@ sessions.post(
|
|
|
8815
9848
|
async (c) => {
|
|
8816
9849
|
const body = c.req.valid("json");
|
|
8817
9850
|
const config = getConfig();
|
|
9851
|
+
const cuDefault = process.env.SPARKECODER_COMPUTER_USE === "1";
|
|
9852
|
+
const baseConfig = body.config || {};
|
|
9853
|
+
const mergedConfig = {
|
|
9854
|
+
...baseConfig,
|
|
9855
|
+
...body.toolApprovals ? { toolApprovals: body.toolApprovals } : {},
|
|
9856
|
+
// Turn on computer use by default if the server was launched with --enable-computer-use,
|
|
9857
|
+
// unless the client explicitly provided a value.
|
|
9858
|
+
...cuDefault && baseConfig.computerUseEnabled === void 0 ? { computerUseEnabled: true } : {}
|
|
9859
|
+
};
|
|
8818
9860
|
const agent = await Agent.create({
|
|
8819
9861
|
name: body.name,
|
|
8820
9862
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8821
9863
|
model: body.model || config.defaultModel,
|
|
8822
|
-
sessionConfig:
|
|
9864
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8823
9865
|
});
|
|
8824
9866
|
const session = agent.getSession();
|
|
8825
9867
|
return c.json({
|
|
@@ -8916,10 +9958,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8916
9958
|
count: executions.length
|
|
8917
9959
|
});
|
|
8918
9960
|
});
|
|
8919
|
-
var updateSessionSchema =
|
|
8920
|
-
model:
|
|
8921
|
-
name:
|
|
8922
|
-
toolApprovals:
|
|
9961
|
+
var updateSessionSchema = z16.object({
|
|
9962
|
+
model: z16.string().optional(),
|
|
9963
|
+
name: z16.string().optional(),
|
|
9964
|
+
toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
|
|
8923
9965
|
});
|
|
8924
9966
|
sessions.patch(
|
|
8925
9967
|
"/:id",
|
|
@@ -8989,8 +10031,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8989
10031
|
await agent.clearContext();
|
|
8990
10032
|
return c.json({ success: true, sessionId: id });
|
|
8991
10033
|
});
|
|
8992
|
-
var pendingInputSchema =
|
|
8993
|
-
text:
|
|
10034
|
+
var pendingInputSchema = z16.object({
|
|
10035
|
+
text: z16.string()
|
|
8994
10036
|
});
|
|
8995
10037
|
sessions.post(
|
|
8996
10038
|
"/:id/pending-input",
|
|
@@ -9021,13 +10063,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
9021
10063
|
createdAt: pending.createdAt.toISOString()
|
|
9022
10064
|
});
|
|
9023
10065
|
});
|
|
9024
|
-
var devtoolsContextSchema =
|
|
9025
|
-
url:
|
|
9026
|
-
path:
|
|
9027
|
-
pageName:
|
|
9028
|
-
screenWidth:
|
|
9029
|
-
screenHeight:
|
|
9030
|
-
devicePixelRatio:
|
|
10066
|
+
var devtoolsContextSchema = z16.object({
|
|
10067
|
+
url: z16.string(),
|
|
10068
|
+
path: z16.string(),
|
|
10069
|
+
pageName: z16.string().optional(),
|
|
10070
|
+
screenWidth: z16.number().optional(),
|
|
10071
|
+
screenHeight: z16.number().optional(),
|
|
10072
|
+
devicePixelRatio: z16.number().optional()
|
|
9031
10073
|
});
|
|
9032
10074
|
sessions.post(
|
|
9033
10075
|
"/:id/devtools-context",
|
|
@@ -9213,12 +10255,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
9213
10255
|
});
|
|
9214
10256
|
function getAttachmentsDir(sessionId) {
|
|
9215
10257
|
const appDataDir = getAppDataDirectory();
|
|
9216
|
-
return
|
|
10258
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
9217
10259
|
}
|
|
9218
10260
|
function ensureAttachmentsDir(sessionId) {
|
|
9219
10261
|
const dir = getAttachmentsDir(sessionId);
|
|
9220
|
-
if (!
|
|
9221
|
-
|
|
10262
|
+
if (!existsSync16(dir)) {
|
|
10263
|
+
mkdirSync6(dir, { recursive: true });
|
|
9222
10264
|
}
|
|
9223
10265
|
return dir;
|
|
9224
10266
|
}
|
|
@@ -9229,12 +10271,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
9229
10271
|
return c.json({ error: "Session not found" }, 404);
|
|
9230
10272
|
}
|
|
9231
10273
|
const dir = getAttachmentsDir(sessionId);
|
|
9232
|
-
if (!
|
|
10274
|
+
if (!existsSync16(dir)) {
|
|
9233
10275
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
9234
10276
|
}
|
|
9235
10277
|
const files = readdirSync2(dir);
|
|
9236
10278
|
const attachments = files.map((filename) => {
|
|
9237
|
-
const filePath =
|
|
10279
|
+
const filePath = join10(dir, filename);
|
|
9238
10280
|
const stats = statSync2(filePath);
|
|
9239
10281
|
return {
|
|
9240
10282
|
id: filename.split("_")[0],
|
|
@@ -9266,10 +10308,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
9266
10308
|
return c.json({ error: "No file provided" }, 400);
|
|
9267
10309
|
}
|
|
9268
10310
|
const dir = ensureAttachmentsDir(sessionId);
|
|
9269
|
-
const id =
|
|
10311
|
+
const id = nanoid6(10);
|
|
9270
10312
|
const ext = extname8(file.name) || "";
|
|
9271
10313
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
9272
|
-
const filePath =
|
|
10314
|
+
const filePath = join10(dir, safeFilename);
|
|
9273
10315
|
const arrayBuffer = await file.arrayBuffer();
|
|
9274
10316
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
9275
10317
|
return c.json({
|
|
@@ -9292,10 +10334,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
9292
10334
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
9293
10335
|
}
|
|
9294
10336
|
const dir = ensureAttachmentsDir(sessionId);
|
|
9295
|
-
const id =
|
|
10337
|
+
const id = nanoid6(10);
|
|
9296
10338
|
const ext = extname8(body.filename) || "";
|
|
9297
10339
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
9298
|
-
const filePath =
|
|
10340
|
+
const filePath = join10(dir, safeFilename);
|
|
9299
10341
|
let base64Data = body.data;
|
|
9300
10342
|
if (base64Data.includes(",")) {
|
|
9301
10343
|
base64Data = base64Data.split(",")[1];
|
|
@@ -9324,7 +10366,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
9324
10366
|
return c.json({ error: "Session not found" }, 404);
|
|
9325
10367
|
}
|
|
9326
10368
|
const dir = getAttachmentsDir(sessionId);
|
|
9327
|
-
if (!
|
|
10369
|
+
if (!existsSync16(dir)) {
|
|
9328
10370
|
return c.json({ error: "Attachment not found" }, 404);
|
|
9329
10371
|
}
|
|
9330
10372
|
const files = readdirSync2(dir);
|
|
@@ -9332,14 +10374,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
9332
10374
|
if (!file) {
|
|
9333
10375
|
return c.json({ error: "Attachment not found" }, 404);
|
|
9334
10376
|
}
|
|
9335
|
-
const filePath =
|
|
9336
|
-
|
|
10377
|
+
const filePath = join10(dir, file);
|
|
10378
|
+
unlinkSync3(filePath);
|
|
9337
10379
|
return c.json({ success: true, id: attachmentId });
|
|
9338
10380
|
});
|
|
9339
|
-
var filesQuerySchema =
|
|
9340
|
-
query:
|
|
10381
|
+
var filesQuerySchema = z16.object({
|
|
10382
|
+
query: z16.string().optional(),
|
|
9341
10383
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
9342
|
-
limit:
|
|
10384
|
+
limit: z16.string().optional()
|
|
9343
10385
|
// Max results (default 50)
|
|
9344
10386
|
});
|
|
9345
10387
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -9415,7 +10457,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
9415
10457
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
9416
10458
|
for (const entry of entries) {
|
|
9417
10459
|
if (results.length >= limit * 2) break;
|
|
9418
|
-
const fullPath =
|
|
10460
|
+
const fullPath = join10(currentDir, entry.name);
|
|
9419
10461
|
const relativePath = relative9(baseDir, fullPath);
|
|
9420
10462
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
9421
10463
|
continue;
|
|
@@ -9463,7 +10505,7 @@ sessions.get(
|
|
|
9463
10505
|
return c.json({ error: "Session not found" }, 404);
|
|
9464
10506
|
}
|
|
9465
10507
|
const workingDirectory = session.workingDirectory;
|
|
9466
|
-
if (!
|
|
10508
|
+
if (!existsSync16(workingDirectory)) {
|
|
9467
10509
|
return c.json({
|
|
9468
10510
|
sessionId,
|
|
9469
10511
|
workingDirectory,
|
|
@@ -9573,9 +10615,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
9573
10615
|
init_db();
|
|
9574
10616
|
import { Hono as Hono2 } from "hono";
|
|
9575
10617
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
9576
|
-
import { z as
|
|
9577
|
-
import { existsSync as
|
|
9578
|
-
import { join as
|
|
10618
|
+
import { z as z17 } from "zod";
|
|
10619
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
10620
|
+
import { join as join11 } from "path";
|
|
9579
10621
|
init_config();
|
|
9580
10622
|
|
|
9581
10623
|
// src/server/resumable-stream.ts
|
|
@@ -9662,7 +10704,7 @@ var streamContext = createResumableStreamContext({
|
|
|
9662
10704
|
});
|
|
9663
10705
|
|
|
9664
10706
|
// src/server/routes/agents.ts
|
|
9665
|
-
import { nanoid as
|
|
10707
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
9666
10708
|
init_stream_proxy();
|
|
9667
10709
|
init_recorder();
|
|
9668
10710
|
init_remote();
|
|
@@ -9753,40 +10795,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
9753
10795
|
${prompt}`;
|
|
9754
10796
|
}
|
|
9755
10797
|
var agents = new Hono2();
|
|
9756
|
-
var attachmentSchema =
|
|
9757
|
-
type:
|
|
9758
|
-
data:
|
|
10798
|
+
var attachmentSchema = z17.object({
|
|
10799
|
+
type: z17.enum(["image", "file"]),
|
|
10800
|
+
data: z17.string(),
|
|
9759
10801
|
// base64 data URL or raw base64
|
|
9760
|
-
mediaType:
|
|
9761
|
-
filename:
|
|
10802
|
+
mediaType: z17.string().optional(),
|
|
10803
|
+
filename: z17.string().optional()
|
|
9762
10804
|
});
|
|
9763
|
-
var runPromptSchema =
|
|
9764
|
-
prompt:
|
|
10805
|
+
var runPromptSchema = z17.object({
|
|
10806
|
+
prompt: z17.string(),
|
|
9765
10807
|
// Can be empty if attachments are provided
|
|
9766
|
-
attachments:
|
|
10808
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
9767
10809
|
}).refine(
|
|
9768
10810
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
9769
10811
|
{ message: "Either prompt or attachments must be provided" }
|
|
9770
10812
|
);
|
|
9771
|
-
var quickStartSchema =
|
|
9772
|
-
prompt:
|
|
9773
|
-
name:
|
|
9774
|
-
workingDirectory:
|
|
9775
|
-
model:
|
|
9776
|
-
toolApprovals:
|
|
10813
|
+
var quickStartSchema = z17.object({
|
|
10814
|
+
prompt: z17.string().min(1),
|
|
10815
|
+
name: z17.string().optional(),
|
|
10816
|
+
workingDirectory: z17.string().optional(),
|
|
10817
|
+
model: z17.string().optional(),
|
|
10818
|
+
toolApprovals: z17.record(z17.string(), z17.boolean()).optional()
|
|
9777
10819
|
});
|
|
9778
|
-
var rejectSchema =
|
|
9779
|
-
reason:
|
|
10820
|
+
var rejectSchema = z17.object({
|
|
10821
|
+
reason: z17.string().optional()
|
|
9780
10822
|
}).optional();
|
|
9781
10823
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9782
10824
|
function getAttachmentsDirectory(sessionId) {
|
|
9783
10825
|
const appDataDir = getAppDataDirectory();
|
|
9784
|
-
return
|
|
10826
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9785
10827
|
}
|
|
9786
10828
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9787
10829
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9788
|
-
if (!
|
|
9789
|
-
|
|
10830
|
+
if (!existsSync17(attachmentsDir)) {
|
|
10831
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9790
10832
|
}
|
|
9791
10833
|
let filename = attachment.filename;
|
|
9792
10834
|
if (!filename) {
|
|
@@ -9804,7 +10846,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9804
10846
|
attachment.mediaType = resized.mediaType;
|
|
9805
10847
|
attachment.data = buffer.toString("base64");
|
|
9806
10848
|
}
|
|
9807
|
-
const filePath =
|
|
10849
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9808
10850
|
writeFileSync4(filePath, buffer);
|
|
9809
10851
|
return filePath;
|
|
9810
10852
|
}
|
|
@@ -9815,9 +10857,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9815
10857
|
}
|
|
9816
10858
|
return data;
|
|
9817
10859
|
}
|
|
9818
|
-
function getExtensionFromMediaType(mediaType,
|
|
10860
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9819
10861
|
if (!mediaType) {
|
|
9820
|
-
return
|
|
10862
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9821
10863
|
}
|
|
9822
10864
|
const mimeToExt = {
|
|
9823
10865
|
"image/png": ".png",
|
|
@@ -10221,7 +11263,7 @@ ${prompt}` });
|
|
|
10221
11263
|
userMessageContent = prompt;
|
|
10222
11264
|
}
|
|
10223
11265
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
10224
|
-
const streamId = `stream_${id}_${
|
|
11266
|
+
const streamId = `stream_${id}_${nanoid7(10)}`;
|
|
10225
11267
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
10226
11268
|
await activeStreamQueries.create(id, streamId);
|
|
10227
11269
|
const stream = await streamContext.resumableStream(
|
|
@@ -10426,7 +11468,7 @@ agents.post(
|
|
|
10426
11468
|
});
|
|
10427
11469
|
const session = agent.getSession();
|
|
10428
11470
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
10429
|
-
const streamId = `stream_${session.id}_${
|
|
11471
|
+
const streamId = `stream_${session.id}_${nanoid7(10)}`;
|
|
10430
11472
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
10431
11473
|
await activeStreamQueries.create(session.id, streamId);
|
|
10432
11474
|
const createQuickStreamProducer = () => {
|
|
@@ -10693,23 +11735,23 @@ agents.post(
|
|
|
10693
11735
|
});
|
|
10694
11736
|
}
|
|
10695
11737
|
);
|
|
10696
|
-
var browserInputSchema =
|
|
10697
|
-
type:
|
|
10698
|
-
eventType:
|
|
10699
|
-
x:
|
|
10700
|
-
y:
|
|
10701
|
-
button:
|
|
10702
|
-
clickCount:
|
|
10703
|
-
deltaX:
|
|
10704
|
-
deltaY:
|
|
10705
|
-
key:
|
|
10706
|
-
code:
|
|
10707
|
-
text:
|
|
10708
|
-
modifiers:
|
|
10709
|
-
touchPoints:
|
|
10710
|
-
x:
|
|
10711
|
-
y:
|
|
10712
|
-
id:
|
|
11738
|
+
var browserInputSchema = z17.object({
|
|
11739
|
+
type: z17.enum(["input_mouse", "input_keyboard", "input_touch"]),
|
|
11740
|
+
eventType: z17.string(),
|
|
11741
|
+
x: z17.number().optional(),
|
|
11742
|
+
y: z17.number().optional(),
|
|
11743
|
+
button: z17.string().optional(),
|
|
11744
|
+
clickCount: z17.number().optional(),
|
|
11745
|
+
deltaX: z17.number().optional(),
|
|
11746
|
+
deltaY: z17.number().optional(),
|
|
11747
|
+
key: z17.string().optional(),
|
|
11748
|
+
code: z17.string().optional(),
|
|
11749
|
+
text: z17.string().optional(),
|
|
11750
|
+
modifiers: z17.number().optional(),
|
|
11751
|
+
touchPoints: z17.array(z17.object({
|
|
11752
|
+
x: z17.number(),
|
|
11753
|
+
y: z17.number(),
|
|
11754
|
+
id: z17.number().optional()
|
|
10713
11755
|
})).optional()
|
|
10714
11756
|
});
|
|
10715
11757
|
agents.post(
|
|
@@ -10742,29 +11784,30 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
10742
11784
|
|
|
10743
11785
|
// src/server/routes/health.ts
|
|
10744
11786
|
init_config();
|
|
11787
|
+
init_heartbeat();
|
|
10745
11788
|
import { Hono as Hono3 } from "hono";
|
|
10746
11789
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
10747
|
-
import { z as
|
|
10748
|
-
import { readFileSync as
|
|
11790
|
+
import { z as z18 } from "zod";
|
|
11791
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
10749
11792
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10750
|
-
import { dirname as dirname6, join as
|
|
11793
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10751
11794
|
var __filename = fileURLToPath3(import.meta.url);
|
|
10752
11795
|
var __dirname = dirname6(__filename);
|
|
10753
11796
|
var possiblePaths = [
|
|
10754
|
-
|
|
11797
|
+
join12(__dirname, "../package.json"),
|
|
10755
11798
|
// From dist/server -> dist/../package.json
|
|
10756
|
-
|
|
11799
|
+
join12(__dirname, "../../package.json"),
|
|
10757
11800
|
// From dist/server (if nested differently)
|
|
10758
|
-
|
|
11801
|
+
join12(__dirname, "../../../package.json"),
|
|
10759
11802
|
// From src/server/routes (development)
|
|
10760
|
-
|
|
11803
|
+
join12(process.cwd(), "package.json")
|
|
10761
11804
|
// From current working directory
|
|
10762
11805
|
];
|
|
10763
11806
|
var currentVersion = "0.0.0";
|
|
10764
11807
|
var packageName = "sparkecoder";
|
|
10765
11808
|
for (const packageJsonPath of possiblePaths) {
|
|
10766
11809
|
try {
|
|
10767
|
-
const packageJson = JSON.parse(
|
|
11810
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
10768
11811
|
if (packageJson.name === "sparkecoder") {
|
|
10769
11812
|
currentVersion = packageJson.version || "0.0.0";
|
|
10770
11813
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10779,11 +11822,17 @@ health.get("/", async (c) => {
|
|
|
10779
11822
|
const apiKeyStatus = getApiKeyStatus();
|
|
10780
11823
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10781
11824
|
const hasApiKey = gatewayKey?.configured ?? false;
|
|
11825
|
+
let hwid;
|
|
11826
|
+
try {
|
|
11827
|
+
hwid = getHardwareIdCached();
|
|
11828
|
+
} catch {
|
|
11829
|
+
}
|
|
10782
11830
|
return c.json({
|
|
10783
11831
|
status: "ok",
|
|
10784
11832
|
version: currentVersion,
|
|
10785
11833
|
uptime: process.uptime(),
|
|
10786
11834
|
apiKeyConfigured: hasApiKey,
|
|
11835
|
+
hwid,
|
|
10787
11836
|
config: {
|
|
10788
11837
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10789
11838
|
defaultModel: config.defaultModel,
|
|
@@ -10854,9 +11903,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10854
11903
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10855
11904
|
});
|
|
10856
11905
|
});
|
|
10857
|
-
var setApiKeySchema =
|
|
10858
|
-
provider:
|
|
10859
|
-
apiKey:
|
|
11906
|
+
var setApiKeySchema = z18.object({
|
|
11907
|
+
provider: z18.string(),
|
|
11908
|
+
apiKey: z18.string().min(1)
|
|
10860
11909
|
});
|
|
10861
11910
|
health.post(
|
|
10862
11911
|
"/api-keys",
|
|
@@ -10895,13 +11944,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10895
11944
|
// src/server/routes/terminals.ts
|
|
10896
11945
|
import { Hono as Hono4 } from "hono";
|
|
10897
11946
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10898
|
-
import { z as
|
|
11947
|
+
import { z as z19 } from "zod";
|
|
10899
11948
|
init_db();
|
|
10900
11949
|
var terminals = new Hono4();
|
|
10901
|
-
var spawnSchema =
|
|
10902
|
-
command:
|
|
10903
|
-
cwd:
|
|
10904
|
-
name:
|
|
11950
|
+
var spawnSchema = z19.object({
|
|
11951
|
+
command: z19.string(),
|
|
11952
|
+
cwd: z19.string().optional(),
|
|
11953
|
+
name: z19.string().optional()
|
|
10905
11954
|
});
|
|
10906
11955
|
terminals.post(
|
|
10907
11956
|
"/:sessionId/terminals",
|
|
@@ -10982,8 +12031,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10982
12031
|
// We don't track exit codes in tmux mode
|
|
10983
12032
|
});
|
|
10984
12033
|
});
|
|
10985
|
-
var logsQuerySchema =
|
|
10986
|
-
tail:
|
|
12034
|
+
var logsQuerySchema = z19.object({
|
|
12035
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10987
12036
|
});
|
|
10988
12037
|
terminals.get(
|
|
10989
12038
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -11007,8 +12056,8 @@ terminals.get(
|
|
|
11007
12056
|
});
|
|
11008
12057
|
}
|
|
11009
12058
|
);
|
|
11010
|
-
var killSchema =
|
|
11011
|
-
signal:
|
|
12059
|
+
var killSchema = z19.object({
|
|
12060
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
11012
12061
|
});
|
|
11013
12062
|
terminals.post(
|
|
11014
12063
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -11022,8 +12071,8 @@ terminals.post(
|
|
|
11022
12071
|
return c.json({ success: true, message: "Terminal killed" });
|
|
11023
12072
|
}
|
|
11024
12073
|
);
|
|
11025
|
-
var writeSchema =
|
|
11026
|
-
input:
|
|
12074
|
+
var writeSchema = z19.object({
|
|
12075
|
+
input: z19.string()
|
|
11027
12076
|
});
|
|
11028
12077
|
terminals.post(
|
|
11029
12078
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -11208,20 +12257,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
11208
12257
|
init_db();
|
|
11209
12258
|
import { Hono as Hono5 } from "hono";
|
|
11210
12259
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
11211
|
-
import { z as
|
|
11212
|
-
import { nanoid as
|
|
12260
|
+
import { z as z20 } from "zod";
|
|
12261
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
11213
12262
|
init_config();
|
|
11214
12263
|
var tasks = new Hono5();
|
|
11215
12264
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
11216
|
-
var createTaskSchema =
|
|
11217
|
-
prompt:
|
|
11218
|
-
outputSchema:
|
|
11219
|
-
webhookUrl:
|
|
11220
|
-
model:
|
|
11221
|
-
workingDirectory:
|
|
11222
|
-
name:
|
|
11223
|
-
maxIterations:
|
|
11224
|
-
parentTaskId:
|
|
12265
|
+
var createTaskSchema = z20.object({
|
|
12266
|
+
prompt: z20.string().min(1),
|
|
12267
|
+
outputSchema: z20.record(z20.string(), z20.unknown()),
|
|
12268
|
+
webhookUrl: z20.string().url().optional(),
|
|
12269
|
+
model: z20.string().optional(),
|
|
12270
|
+
workingDirectory: z20.string().optional(),
|
|
12271
|
+
name: z20.string().optional(),
|
|
12272
|
+
maxIterations: z20.number().int().min(1).max(500).optional(),
|
|
12273
|
+
parentTaskId: z20.string().optional()
|
|
11225
12274
|
});
|
|
11226
12275
|
tasks.post(
|
|
11227
12276
|
"/",
|
|
@@ -11283,7 +12332,7 @@ tasks.post(
|
|
|
11283
12332
|
const taskId = agent.sessionId;
|
|
11284
12333
|
const abortController = new AbortController();
|
|
11285
12334
|
taskAbortControllers.set(taskId, abortController);
|
|
11286
|
-
const streamId = `stream_${taskId}_${
|
|
12335
|
+
const streamId = `stream_${taskId}_${nanoid8(10)}`;
|
|
11287
12336
|
await activeStreamQueries.create(taskId, streamId);
|
|
11288
12337
|
const taskStreamProducer = () => {
|
|
11289
12338
|
const { readable, writable } = new TransformStream();
|
|
@@ -11433,17 +12482,584 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
11433
12482
|
});
|
|
11434
12483
|
var tasks_default = tasks;
|
|
11435
12484
|
|
|
12485
|
+
// src/server/routes/system.ts
|
|
12486
|
+
init_system_metrics();
|
|
12487
|
+
import { Hono as Hono6 } from "hono";
|
|
12488
|
+
import { streamSSE } from "hono/streaming";
|
|
12489
|
+
var system = new Hono6();
|
|
12490
|
+
system.get("/metrics", (c) => {
|
|
12491
|
+
return c.json(readSystemMetrics());
|
|
12492
|
+
});
|
|
12493
|
+
system.get("/metrics/stream", (c) => {
|
|
12494
|
+
return streamSSE(c, async (stream) => {
|
|
12495
|
+
const intervalMs = Number(c.req.query("intervalMs") ?? 2e3);
|
|
12496
|
+
const safeMs = Number.isFinite(intervalMs) ? Math.max(500, Math.min(6e4, intervalMs)) : 2e3;
|
|
12497
|
+
let id = 0;
|
|
12498
|
+
let aborted = false;
|
|
12499
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
12500
|
+
aborted = true;
|
|
12501
|
+
});
|
|
12502
|
+
while (!aborted) {
|
|
12503
|
+
try {
|
|
12504
|
+
const snap = readSystemMetrics();
|
|
12505
|
+
await stream.writeSSE({
|
|
12506
|
+
id: String(id++),
|
|
12507
|
+
event: "metrics",
|
|
12508
|
+
data: JSON.stringify(snap)
|
|
12509
|
+
});
|
|
12510
|
+
} catch (e) {
|
|
12511
|
+
await stream.writeSSE({
|
|
12512
|
+
id: String(id++),
|
|
12513
|
+
event: "error",
|
|
12514
|
+
data: JSON.stringify({ error: e.message })
|
|
12515
|
+
});
|
|
12516
|
+
}
|
|
12517
|
+
await stream.sleep(safeMs);
|
|
12518
|
+
}
|
|
12519
|
+
});
|
|
12520
|
+
});
|
|
12521
|
+
|
|
12522
|
+
// src/personal-agent/hwid-middleware.ts
|
|
12523
|
+
init_heartbeat();
|
|
12524
|
+
var SKIP_PATHS = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
12525
|
+
function personalAgentConfigured() {
|
|
12526
|
+
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);
|
|
12527
|
+
}
|
|
12528
|
+
function hwidMiddleware() {
|
|
12529
|
+
const enabled = personalAgentConfigured();
|
|
12530
|
+
let warnedMissing = false;
|
|
12531
|
+
return async (c, next) => {
|
|
12532
|
+
if (!enabled) return next();
|
|
12533
|
+
const path = c.req.path;
|
|
12534
|
+
if (SKIP_PATHS.has(path) || path.startsWith("/health/api-keys")) {
|
|
12535
|
+
return next();
|
|
12536
|
+
}
|
|
12537
|
+
const got = c.req.header("x-device-hwid");
|
|
12538
|
+
if (!got) {
|
|
12539
|
+
if (!warnedMissing) {
|
|
12540
|
+
warnedMissing = true;
|
|
12541
|
+
console.warn(
|
|
12542
|
+
`[personal-agent] request to ${path} arrived without X-Device-Hwid; allowing (backwards compat)`
|
|
12543
|
+
);
|
|
12544
|
+
}
|
|
12545
|
+
return next();
|
|
12546
|
+
}
|
|
12547
|
+
const expected = getHardwareIdCached();
|
|
12548
|
+
if (got !== expected) {
|
|
12549
|
+
console.warn(
|
|
12550
|
+
`[personal-agent] HWID mismatch on ${path}: got=${got.slice(0, 12)}\u2026, expected=${expected.slice(0, 12)}\u2026`
|
|
12551
|
+
);
|
|
12552
|
+
return c.json(
|
|
12553
|
+
{
|
|
12554
|
+
error: "hwid mismatch",
|
|
12555
|
+
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.",
|
|
12556
|
+
expected: expected.slice(0, 12) + "\u2026",
|
|
12557
|
+
got: got.slice(0, 12) + "\u2026"
|
|
12558
|
+
},
|
|
12559
|
+
409
|
|
12560
|
+
);
|
|
12561
|
+
}
|
|
12562
|
+
return next();
|
|
12563
|
+
};
|
|
12564
|
+
}
|
|
12565
|
+
|
|
12566
|
+
// src/personal-agent/signature-verify.ts
|
|
12567
|
+
import { createHash as createHash3, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
12568
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
12569
|
+
var REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
12570
|
+
var _cachedKey = null;
|
|
12571
|
+
var _cachedFromInput = null;
|
|
12572
|
+
function loadPublicKey(input) {
|
|
12573
|
+
if (_cachedFromInput === input && _cachedKey) return _cachedKey;
|
|
12574
|
+
let pem = input;
|
|
12575
|
+
if (!input.includes("BEGIN") && existsSync18(input)) {
|
|
12576
|
+
pem = readFileSync11(input, "utf8");
|
|
12577
|
+
}
|
|
12578
|
+
const key = createPublicKey({ key: pem, format: "pem" });
|
|
12579
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
12580
|
+
throw new Error(
|
|
12581
|
+
`expected an ed25519 public key, got ${key.asymmetricKeyType}. Generate with personal-agents/scripts/generate-signing-keys.mjs.`
|
|
12582
|
+
);
|
|
12583
|
+
}
|
|
12584
|
+
_cachedKey = key;
|
|
12585
|
+
_cachedFromInput = input;
|
|
12586
|
+
return key;
|
|
12587
|
+
}
|
|
12588
|
+
function bodyHashB64(body) {
|
|
12589
|
+
const hash = createHash3("sha256");
|
|
12590
|
+
if (body == null || body === "") {
|
|
12591
|
+
} else if (typeof body === "string") {
|
|
12592
|
+
hash.update(body, "utf8");
|
|
12593
|
+
} else if (Buffer.isBuffer(body)) {
|
|
12594
|
+
hash.update(body);
|
|
12595
|
+
} else {
|
|
12596
|
+
hash.update(Buffer.from(body));
|
|
12597
|
+
}
|
|
12598
|
+
return hash.digest("base64");
|
|
12599
|
+
}
|
|
12600
|
+
function canonicalSigningString(args) {
|
|
12601
|
+
return [
|
|
12602
|
+
args.method.toUpperCase(),
|
|
12603
|
+
args.path,
|
|
12604
|
+
String(args.timestamp),
|
|
12605
|
+
args.bodyHashB64
|
|
12606
|
+
].join("\n");
|
|
12607
|
+
}
|
|
12608
|
+
function fromBase64Url(s) {
|
|
12609
|
+
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
12610
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
12611
|
+
return Buffer.from(std, "base64");
|
|
12612
|
+
}
|
|
12613
|
+
function verifyEmbedToken(args) {
|
|
12614
|
+
const dot = args.token.indexOf(".");
|
|
12615
|
+
if (dot < 1 || dot >= args.token.length - 1) {
|
|
12616
|
+
return { ok: false, reason: "malformed" };
|
|
12617
|
+
}
|
|
12618
|
+
const payloadB64 = args.token.slice(0, dot);
|
|
12619
|
+
const sigB64 = args.token.slice(dot + 1);
|
|
12620
|
+
let sigBuf;
|
|
12621
|
+
try {
|
|
12622
|
+
sigBuf = fromBase64Url(sigB64);
|
|
12623
|
+
} catch {
|
|
12624
|
+
return { ok: false, reason: "bad-encoding" };
|
|
12625
|
+
}
|
|
12626
|
+
const sigOk = cryptoVerify(
|
|
12627
|
+
null,
|
|
12628
|
+
Buffer.from(payloadB64, "utf8"),
|
|
12629
|
+
args.publicKey,
|
|
12630
|
+
sigBuf
|
|
12631
|
+
);
|
|
12632
|
+
if (!sigOk) return { ok: false, reason: "signature-mismatch" };
|
|
12633
|
+
let payload;
|
|
12634
|
+
try {
|
|
12635
|
+
const json = JSON.parse(fromBase64Url(payloadB64).toString("utf8"));
|
|
12636
|
+
if (!json || typeof json !== "object" || typeof json.sid !== "string" || typeof json.exp !== "number") {
|
|
12637
|
+
return { ok: false, reason: "bad-payload" };
|
|
12638
|
+
}
|
|
12639
|
+
payload = { sid: json.sid, exp: json.exp };
|
|
12640
|
+
} catch (e) {
|
|
12641
|
+
return { ok: false, reason: "bad-payload", detail: e.message };
|
|
12642
|
+
}
|
|
12643
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
12644
|
+
if (payload.exp < now) {
|
|
12645
|
+
return {
|
|
12646
|
+
ok: false,
|
|
12647
|
+
reason: "expired",
|
|
12648
|
+
detail: `${now - payload.exp}s past expiry`
|
|
12649
|
+
};
|
|
12650
|
+
}
|
|
12651
|
+
return { ok: true, payload };
|
|
12652
|
+
}
|
|
12653
|
+
function verifyRequest(args) {
|
|
12654
|
+
if (!args.signatureB64 || !args.timestampSeconds || !args.algorithm) {
|
|
12655
|
+
return { ok: false, reason: "missing-headers" };
|
|
12656
|
+
}
|
|
12657
|
+
if (args.algorithm.toLowerCase() !== "ed25519") {
|
|
12658
|
+
return { ok: false, reason: "bad-algorithm", detail: args.algorithm };
|
|
12659
|
+
}
|
|
12660
|
+
const ts = Number(args.timestampSeconds);
|
|
12661
|
+
if (!Number.isFinite(ts)) {
|
|
12662
|
+
return { ok: false, reason: "bad-timestamp", detail: args.timestampSeconds };
|
|
12663
|
+
}
|
|
12664
|
+
const now = args.now ?? Math.floor(Date.now() / 1e3);
|
|
12665
|
+
if (Math.abs(now - ts) > REPLAY_WINDOW_SECONDS) {
|
|
12666
|
+
return {
|
|
12667
|
+
ok: false,
|
|
12668
|
+
reason: "stale-timestamp",
|
|
12669
|
+
detail: `${Math.abs(now - ts)}s outside the ${REPLAY_WINDOW_SECONDS}s window`
|
|
12670
|
+
};
|
|
12671
|
+
}
|
|
12672
|
+
let sigBuf;
|
|
12673
|
+
try {
|
|
12674
|
+
sigBuf = Buffer.from(args.signatureB64, "base64");
|
|
12675
|
+
} catch {
|
|
12676
|
+
return { ok: false, reason: "bad-signature-encoding" };
|
|
12677
|
+
}
|
|
12678
|
+
const canonical = canonicalSigningString({
|
|
12679
|
+
method: args.method,
|
|
12680
|
+
path: args.path,
|
|
12681
|
+
timestamp: ts,
|
|
12682
|
+
bodyHashB64: bodyHashB64(args.body)
|
|
12683
|
+
});
|
|
12684
|
+
const ok = cryptoVerify(null, Buffer.from(canonical, "utf8"), args.publicKey, sigBuf);
|
|
12685
|
+
return ok ? { ok: true } : { ok: false, reason: "signature-mismatch" };
|
|
12686
|
+
}
|
|
12687
|
+
|
|
12688
|
+
// src/personal-agent/signature-middleware.ts
|
|
12689
|
+
var SKIP_PATHS2 = /* @__PURE__ */ new Set(["/health", "/health/", "/health/ready", "/health/version"]);
|
|
12690
|
+
function isSkipped(path) {
|
|
12691
|
+
return SKIP_PATHS2.has(path) || path.startsWith("/health/api-keys");
|
|
12692
|
+
}
|
|
12693
|
+
function pathBindsSessionId(path, sid) {
|
|
12694
|
+
const cleanPath = path.split("?")[0];
|
|
12695
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
12696
|
+
return segments.includes(sid);
|
|
12697
|
+
}
|
|
12698
|
+
function signatureMiddleware(opts = {}) {
|
|
12699
|
+
const acceptSignedOnly = opts.acceptSignedOnly ?? process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
12700
|
+
const publicKeyInput = opts.publicKeyInput ?? process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
12701
|
+
if (acceptSignedOnly && !publicKeyInput) {
|
|
12702
|
+
return async (c) => c.json(
|
|
12703
|
+
{
|
|
12704
|
+
error: "signature middleware misconfigured",
|
|
12705
|
+
message: "started with --accept-signed-only but no --personal-agent-public-key / PERSONAL_AGENT_PUBLIC_KEY supplied. Refusing all non-/health traffic."
|
|
12706
|
+
},
|
|
12707
|
+
500
|
|
12708
|
+
);
|
|
12709
|
+
}
|
|
12710
|
+
if (!publicKeyInput) {
|
|
12711
|
+
return async (_c, next) => next();
|
|
12712
|
+
}
|
|
12713
|
+
const publicKey = loadPublicKey(publicKeyInput);
|
|
12714
|
+
let warnedUnsigned = false;
|
|
12715
|
+
return async (c, next) => {
|
|
12716
|
+
const path = c.req.path;
|
|
12717
|
+
if (isSkipped(path)) return next();
|
|
12718
|
+
const sig = c.req.header("x-signature");
|
|
12719
|
+
const ts = c.req.header("x-signature-timestamp");
|
|
12720
|
+
const alg = c.req.header("x-signature-algorithm");
|
|
12721
|
+
if (!sig && (c.req.method === "GET" || c.req.method === "HEAD")) {
|
|
12722
|
+
const embedTok = c.req.header("x-embed-token") ?? c.req.query("embed_token");
|
|
12723
|
+
if (embedTok) {
|
|
12724
|
+
const result2 = verifyEmbedToken({ publicKey, token: embedTok });
|
|
12725
|
+
if (!result2.ok) {
|
|
12726
|
+
return c.json(
|
|
12727
|
+
{
|
|
12728
|
+
error: "embed token verification failed",
|
|
12729
|
+
reason: result2.reason,
|
|
12730
|
+
detail: result2.detail
|
|
12731
|
+
},
|
|
12732
|
+
401
|
|
12733
|
+
);
|
|
12734
|
+
}
|
|
12735
|
+
if (!pathBindsSessionId(path, result2.payload.sid)) {
|
|
12736
|
+
return c.json(
|
|
12737
|
+
{
|
|
12738
|
+
error: "embed token scoped to a different session",
|
|
12739
|
+
detail: `token sid=${result2.payload.sid} but request path=${path}`
|
|
12740
|
+
},
|
|
12741
|
+
403
|
|
12742
|
+
);
|
|
12743
|
+
}
|
|
12744
|
+
return next();
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
if (!sig) {
|
|
12748
|
+
if (acceptSignedOnly) {
|
|
12749
|
+
return c.json(
|
|
12750
|
+
{
|
|
12751
|
+
error: "signature required",
|
|
12752
|
+
message: "this sparkecoder is started with --accept-signed-only; every request must carry X-Signature, X-Signature-Timestamp, X-Signature-Algorithm."
|
|
12753
|
+
},
|
|
12754
|
+
401
|
|
12755
|
+
);
|
|
12756
|
+
}
|
|
12757
|
+
if (!warnedUnsigned) {
|
|
12758
|
+
warnedUnsigned = true;
|
|
12759
|
+
console.warn(
|
|
12760
|
+
`[personal-agent] inbound ${c.req.method} ${path} arrived without X-Signature; allowed because --accept-signed-only is off`
|
|
12761
|
+
);
|
|
12762
|
+
}
|
|
12763
|
+
return next();
|
|
12764
|
+
}
|
|
12765
|
+
let body;
|
|
12766
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
12767
|
+
body = Buffer.from(await c.req.raw.clone().arrayBuffer());
|
|
12768
|
+
}
|
|
12769
|
+
const result = verifyRequest({
|
|
12770
|
+
publicKey,
|
|
12771
|
+
method: c.req.method,
|
|
12772
|
+
path,
|
|
12773
|
+
body,
|
|
12774
|
+
signatureB64: sig,
|
|
12775
|
+
timestampSeconds: ts,
|
|
12776
|
+
algorithm: alg
|
|
12777
|
+
});
|
|
12778
|
+
if (!result.ok) {
|
|
12779
|
+
console.warn(
|
|
12780
|
+
`[personal-agent] signature verification failed on ${c.req.method} ${path}: ${result.reason}${result.detail ? ` (${result.detail})` : ""}`
|
|
12781
|
+
);
|
|
12782
|
+
return c.json(
|
|
12783
|
+
{
|
|
12784
|
+
error: "signature verification failed",
|
|
12785
|
+
reason: result.reason,
|
|
12786
|
+
detail: result.detail
|
|
12787
|
+
},
|
|
12788
|
+
401
|
|
12789
|
+
);
|
|
12790
|
+
}
|
|
12791
|
+
return next();
|
|
12792
|
+
};
|
|
12793
|
+
}
|
|
12794
|
+
|
|
12795
|
+
// src/personal-agent/pty-server.ts
|
|
12796
|
+
init_heartbeat();
|
|
12797
|
+
import { hostname as hostname3 } from "os";
|
|
12798
|
+
import { WebSocketServer } from "ws";
|
|
12799
|
+
var RESIZE_RE = /\x1b\[RESIZE:(\d+);(\d+)\]/;
|
|
12800
|
+
var _ptyMod = null;
|
|
12801
|
+
async function loadPty() {
|
|
12802
|
+
if (_ptyMod) return _ptyMod;
|
|
12803
|
+
try {
|
|
12804
|
+
const mod = await import("node-pty");
|
|
12805
|
+
_ptyMod = mod;
|
|
12806
|
+
return mod;
|
|
12807
|
+
} catch (e) {
|
|
12808
|
+
console.warn(
|
|
12809
|
+
`[personal-agent] node-pty failed to load; /pty WebSocket disabled. (${e.message})`
|
|
12810
|
+
);
|
|
12811
|
+
return null;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12814
|
+
function defaultShell() {
|
|
12815
|
+
if (process.platform === "win32") {
|
|
12816
|
+
return { file: process.env.COMSPEC || "cmd.exe", args: [] };
|
|
12817
|
+
}
|
|
12818
|
+
return { file: process.env.SHELL || "/bin/zsh", args: ["-l"] };
|
|
12819
|
+
}
|
|
12820
|
+
function cleanEnv() {
|
|
12821
|
+
const env = {};
|
|
12822
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
12823
|
+
if (typeof v === "string") env[k] = v;
|
|
12824
|
+
}
|
|
12825
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
12826
|
+
if (!env.LANG) env.LANG = "en_US.UTF-8";
|
|
12827
|
+
return env;
|
|
12828
|
+
}
|
|
12829
|
+
function parseUpgrade(req) {
|
|
12830
|
+
const url = new URL(req.url || "/", "http://placeholder");
|
|
12831
|
+
const cols = clampInt(url.searchParams.get("cols"), 80, 10, 500);
|
|
12832
|
+
const rows = clampInt(url.searchParams.get("rows"), 24, 5, 500);
|
|
12833
|
+
const cwd = url.searchParams.get("cwd") || void 0;
|
|
12834
|
+
const shell = url.searchParams.get("shell") || void 0;
|
|
12835
|
+
return {
|
|
12836
|
+
cols,
|
|
12837
|
+
rows,
|
|
12838
|
+
cwd,
|
|
12839
|
+
shell,
|
|
12840
|
+
hwidHeader: headerStr(req, "x-device-hwid") ?? url.searchParams.get("hwid") ?? void 0,
|
|
12841
|
+
sigHeader: headerStr(req, "x-signature") ?? url.searchParams.get("sig") ?? void 0,
|
|
12842
|
+
tsHeader: headerStr(req, "x-signature-timestamp") ?? url.searchParams.get("ts") ?? void 0,
|
|
12843
|
+
algHeader: headerStr(req, "x-signature-algorithm") ?? url.searchParams.get("alg") ?? void 0
|
|
12844
|
+
};
|
|
12845
|
+
}
|
|
12846
|
+
function clampInt(v, dflt, lo, hi) {
|
|
12847
|
+
if (!v) return dflt;
|
|
12848
|
+
const n = parseInt(v, 10);
|
|
12849
|
+
if (!Number.isFinite(n)) return dflt;
|
|
12850
|
+
return Math.max(lo, Math.min(hi, n));
|
|
12851
|
+
}
|
|
12852
|
+
function headerStr(req, name) {
|
|
12853
|
+
const v = req.headers[name];
|
|
12854
|
+
if (Array.isArray(v)) return v[0];
|
|
12855
|
+
return v;
|
|
12856
|
+
}
|
|
12857
|
+
function authenticate(parsed, path, pubKey, signedOnly) {
|
|
12858
|
+
if (parsed.hwidHeader) {
|
|
12859
|
+
const expected = getHardwareIdCached();
|
|
12860
|
+
if (parsed.hwidHeader !== expected) {
|
|
12861
|
+
return {
|
|
12862
|
+
ok: false,
|
|
12863
|
+
status: 409,
|
|
12864
|
+
reason: `hwid mismatch: got ${parsed.hwidHeader.slice(0, 12)}\u2026, expected ${expected.slice(0, 12)}\u2026`
|
|
12865
|
+
};
|
|
12866
|
+
}
|
|
12867
|
+
}
|
|
12868
|
+
if (!pubKey) {
|
|
12869
|
+
if (signedOnly) {
|
|
12870
|
+
return { ok: false, status: 500, reason: "signature required but no public key configured" };
|
|
12871
|
+
}
|
|
12872
|
+
return { ok: true };
|
|
12873
|
+
}
|
|
12874
|
+
if (!parsed.sigHeader) {
|
|
12875
|
+
if (signedOnly) {
|
|
12876
|
+
return { ok: false, status: 401, reason: "missing X-Signature on upgrade request" };
|
|
12877
|
+
}
|
|
12878
|
+
return { ok: true };
|
|
12879
|
+
}
|
|
12880
|
+
const result = verifyRequest({
|
|
12881
|
+
publicKey: pubKey,
|
|
12882
|
+
method: "GET",
|
|
12883
|
+
path,
|
|
12884
|
+
body: void 0,
|
|
12885
|
+
signatureB64: parsed.sigHeader,
|
|
12886
|
+
timestampSeconds: parsed.tsHeader,
|
|
12887
|
+
algorithm: parsed.algHeader
|
|
12888
|
+
});
|
|
12889
|
+
if (!result.ok) {
|
|
12890
|
+
return { ok: false, status: 401, reason: `signature verification failed: ${result.reason}` };
|
|
12891
|
+
}
|
|
12892
|
+
return { ok: true };
|
|
12893
|
+
}
|
|
12894
|
+
function rejectUpgrade(socket, status, reason) {
|
|
12895
|
+
const body = JSON.stringify({ error: reason });
|
|
12896
|
+
socket.write(
|
|
12897
|
+
`HTTP/1.1 ${status} ${reasonText(status)}\r
|
|
12898
|
+
Content-Type: application/json\r
|
|
12899
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
12900
|
+
Connection: close\r
|
|
12901
|
+
\r
|
|
12902
|
+
` + body
|
|
12903
|
+
);
|
|
12904
|
+
socket.destroy();
|
|
12905
|
+
}
|
|
12906
|
+
function reasonText(status) {
|
|
12907
|
+
switch (status) {
|
|
12908
|
+
case 401:
|
|
12909
|
+
return "Unauthorized";
|
|
12910
|
+
case 409:
|
|
12911
|
+
return "Conflict";
|
|
12912
|
+
case 500:
|
|
12913
|
+
return "Internal Server Error";
|
|
12914
|
+
case 503:
|
|
12915
|
+
return "Service Unavailable";
|
|
12916
|
+
default:
|
|
12917
|
+
return "Error";
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
function attachPtyServer(httpServer, opts = {}) {
|
|
12921
|
+
const path = opts.path ?? "/pty";
|
|
12922
|
+
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
12923
|
+
const acceptSignedOnly = process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY === "1";
|
|
12924
|
+
const publicKeyInput = process.env.PERSONAL_AGENT_PUBLIC_KEY ?? "";
|
|
12925
|
+
let pubKey = null;
|
|
12926
|
+
if (publicKeyInput) {
|
|
12927
|
+
try {
|
|
12928
|
+
pubKey = loadPublicKey(publicKeyInput);
|
|
12929
|
+
} catch (e) {
|
|
12930
|
+
console.warn(
|
|
12931
|
+
`[personal-agent] /pty signature verification disabled \u2014 failed to load public key: ${e.message}`
|
|
12932
|
+
);
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
const handler = (req, socket, head) => {
|
|
12936
|
+
let pathname = "/";
|
|
12937
|
+
try {
|
|
12938
|
+
pathname = new URL(req.url || "/", "http://placeholder").pathname;
|
|
12939
|
+
} catch {
|
|
12940
|
+
}
|
|
12941
|
+
if (pathname !== path) return;
|
|
12942
|
+
const parsed = parseUpgrade(req);
|
|
12943
|
+
const auth = authenticate(parsed, pathname, pubKey, acceptSignedOnly);
|
|
12944
|
+
if (!auth.ok) {
|
|
12945
|
+
console.warn(`[personal-agent] rejecting /pty upgrade: ${auth.reason}`);
|
|
12946
|
+
rejectUpgrade(socket, auth.status ?? 401, auth.reason ?? "unauthorized");
|
|
12947
|
+
return;
|
|
12948
|
+
}
|
|
12949
|
+
void loadPty().then((pty) => {
|
|
12950
|
+
if (!pty) {
|
|
12951
|
+
rejectUpgrade(
|
|
12952
|
+
socket,
|
|
12953
|
+
503,
|
|
12954
|
+
"node-pty is not available on this device (failed to load native module)"
|
|
12955
|
+
);
|
|
12956
|
+
return;
|
|
12957
|
+
}
|
|
12958
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
12959
|
+
spawnPty(ws, pty, parsed, opts);
|
|
12960
|
+
});
|
|
12961
|
+
});
|
|
12962
|
+
};
|
|
12963
|
+
httpServer.on("upgrade", handler);
|
|
12964
|
+
if (!opts.quiet) {
|
|
12965
|
+
console.log(
|
|
12966
|
+
`[personal-agent] WebSocket PTY server attached at ${path} (host: ${hostname3()}, sigCheck: ${pubKey ? "on" : "off"})`
|
|
12967
|
+
);
|
|
12968
|
+
}
|
|
12969
|
+
return {
|
|
12970
|
+
close: () => {
|
|
12971
|
+
httpServer.off("upgrade", handler);
|
|
12972
|
+
wss.close();
|
|
12973
|
+
}
|
|
12974
|
+
};
|
|
12975
|
+
}
|
|
12976
|
+
function spawnPty(ws, pty, parsed, opts) {
|
|
12977
|
+
const { file, args } = (() => {
|
|
12978
|
+
if (parsed.shell || opts.shell) {
|
|
12979
|
+
const s = parsed.shell ?? opts.shell;
|
|
12980
|
+
return { file: s, args: process.platform === "win32" ? [] : ["-l"] };
|
|
12981
|
+
}
|
|
12982
|
+
return defaultShell();
|
|
12983
|
+
})();
|
|
12984
|
+
const cwd = parsed.cwd ?? opts.cwd ?? process.env.HOME ?? process.cwd();
|
|
12985
|
+
let proc;
|
|
12986
|
+
try {
|
|
12987
|
+
proc = pty.spawn(file, args, {
|
|
12988
|
+
name: "xterm-256color",
|
|
12989
|
+
cols: parsed.cols,
|
|
12990
|
+
rows: parsed.rows,
|
|
12991
|
+
cwd,
|
|
12992
|
+
env: cleanEnv()
|
|
12993
|
+
});
|
|
12994
|
+
} catch (e) {
|
|
12995
|
+
const msg = e.message;
|
|
12996
|
+
safeSend(ws, `\r
|
|
12997
|
+
\x1B[31mFailed to spawn shell: ${msg}\x1B[0m\r
|
|
12998
|
+
`);
|
|
12999
|
+
try {
|
|
13000
|
+
ws.close();
|
|
13001
|
+
} catch {
|
|
13002
|
+
}
|
|
13003
|
+
return;
|
|
13004
|
+
}
|
|
13005
|
+
const banner = `\x1B[90m[sparkecoder pty] ${file} on ${hostname3()} pid=${proc.pid} ${parsed.cols}x${parsed.rows}\x1B[0m\r
|
|
13006
|
+
`;
|
|
13007
|
+
safeSend(ws, banner);
|
|
13008
|
+
proc.onData((data) => safeSend(ws, data));
|
|
13009
|
+
proc.onExit(({ exitCode }) => {
|
|
13010
|
+
safeSend(ws, `\r
|
|
13011
|
+
\x1B[90m[exit ${exitCode}]\x1B[0m\r
|
|
13012
|
+
`);
|
|
13013
|
+
try {
|
|
13014
|
+
ws.close();
|
|
13015
|
+
} catch {
|
|
13016
|
+
}
|
|
13017
|
+
});
|
|
13018
|
+
ws.on("message", (msg, isBinary) => {
|
|
13019
|
+
const input = typeof msg === "string" ? msg : Buffer.isBuffer(msg) ? msg.toString(isBinary ? "utf8" : "utf8") : Array.isArray(msg) ? Buffer.concat(msg).toString("utf8") : "";
|
|
13020
|
+
if (!input) return;
|
|
13021
|
+
const m = input.match(RESIZE_RE);
|
|
13022
|
+
if (m) {
|
|
13023
|
+
const cols = clampInt(m[1], 80, 10, 500);
|
|
13024
|
+
const rows = clampInt(m[2], 24, 5, 500);
|
|
13025
|
+
try {
|
|
13026
|
+
proc.resize(cols, rows);
|
|
13027
|
+
} catch {
|
|
13028
|
+
}
|
|
13029
|
+
if (input.replace(RESIZE_RE, "").length === 0) return;
|
|
13030
|
+
proc.write(input.replace(RESIZE_RE, ""));
|
|
13031
|
+
return;
|
|
13032
|
+
}
|
|
13033
|
+
proc.write(input);
|
|
13034
|
+
});
|
|
13035
|
+
const onClose = () => {
|
|
13036
|
+
try {
|
|
13037
|
+
proc.kill();
|
|
13038
|
+
} catch {
|
|
13039
|
+
}
|
|
13040
|
+
};
|
|
13041
|
+
ws.on("close", onClose);
|
|
13042
|
+
ws.on("error", onClose);
|
|
13043
|
+
}
|
|
13044
|
+
function safeSend(ws, data) {
|
|
13045
|
+
if (ws.readyState !== 1) return;
|
|
13046
|
+
try {
|
|
13047
|
+
ws.send(data);
|
|
13048
|
+
} catch {
|
|
13049
|
+
}
|
|
13050
|
+
}
|
|
13051
|
+
|
|
11436
13052
|
// src/server/index.ts
|
|
11437
13053
|
init_config();
|
|
11438
13054
|
init_db();
|
|
11439
13055
|
|
|
11440
13056
|
// src/utils/dependencies.ts
|
|
11441
|
-
import { exec as
|
|
11442
|
-
import { promisify as
|
|
11443
|
-
import { platform as
|
|
11444
|
-
var
|
|
13057
|
+
import { exec as exec7 } from "child_process";
|
|
13058
|
+
import { promisify as promisify7 } from "util";
|
|
13059
|
+
import { platform as platform4 } from "os";
|
|
13060
|
+
var execAsync7 = promisify7(exec7);
|
|
11445
13061
|
function getInstallInstructions() {
|
|
11446
|
-
const os2 =
|
|
13062
|
+
const os2 = platform4();
|
|
11447
13063
|
if (os2 === "darwin") {
|
|
11448
13064
|
return `
|
|
11449
13065
|
Install tmux on macOS:
|
|
@@ -11474,7 +13090,7 @@ Install tmux:
|
|
|
11474
13090
|
}
|
|
11475
13091
|
async function checkTmux() {
|
|
11476
13092
|
try {
|
|
11477
|
-
const { stdout } = await
|
|
13093
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
11478
13094
|
const version = stdout.trim();
|
|
11479
13095
|
return {
|
|
11480
13096
|
available: true,
|
|
@@ -11515,7 +13131,7 @@ async function checkDependencies(options = {}) {
|
|
|
11515
13131
|
}
|
|
11516
13132
|
async function checkAgentBrowser() {
|
|
11517
13133
|
try {
|
|
11518
|
-
const { stdout } = await
|
|
13134
|
+
const { stdout } = await execAsync7("agent-browser --version", { timeout: 1e4 });
|
|
11519
13135
|
const version = stdout.trim();
|
|
11520
13136
|
return { available: true, version };
|
|
11521
13137
|
} catch {
|
|
@@ -11531,12 +13147,12 @@ async function tryInstallAgentBrowser(options = {}) {
|
|
|
11531
13147
|
if (!options.quiet) {
|
|
11532
13148
|
console.log("\u{1F4E6} Installing agent-browser globally...");
|
|
11533
13149
|
}
|
|
11534
|
-
await
|
|
13150
|
+
await execAsync7("npm install -g agent-browser", { timeout: 12e4 });
|
|
11535
13151
|
try {
|
|
11536
13152
|
if (!options.quiet) {
|
|
11537
13153
|
console.log("\u{1F4E6} Installing Chromium for browser automation...");
|
|
11538
13154
|
}
|
|
11539
|
-
await
|
|
13155
|
+
await execAsync7("agent-browser install", { timeout: 12e4 });
|
|
11540
13156
|
} catch {
|
|
11541
13157
|
}
|
|
11542
13158
|
if (!options.quiet) {
|
|
@@ -11551,25 +13167,25 @@ async function tryInstallAgentBrowser(options = {}) {
|
|
|
11551
13167
|
}
|
|
11552
13168
|
}
|
|
11553
13169
|
async function tryAutoInstallTmux() {
|
|
11554
|
-
const os2 =
|
|
13170
|
+
const os2 = platform4();
|
|
11555
13171
|
try {
|
|
11556
13172
|
if (os2 === "darwin") {
|
|
11557
13173
|
try {
|
|
11558
|
-
await
|
|
13174
|
+
await execAsync7("which brew", { timeout: 5e3 });
|
|
11559
13175
|
} catch {
|
|
11560
13176
|
return false;
|
|
11561
13177
|
}
|
|
11562
13178
|
console.log("\u{1F4E6} Installing tmux via Homebrew...");
|
|
11563
|
-
await
|
|
13179
|
+
await execAsync7("brew install tmux", { timeout: 3e5 });
|
|
11564
13180
|
console.log("\u2705 tmux installed successfully");
|
|
11565
13181
|
return true;
|
|
11566
13182
|
}
|
|
11567
13183
|
if (os2 === "linux") {
|
|
11568
13184
|
try {
|
|
11569
|
-
await
|
|
13185
|
+
await execAsync7("which apt-get", { timeout: 5e3 });
|
|
11570
13186
|
console.log("\u{1F4E6} Installing tmux via apt-get...");
|
|
11571
13187
|
console.log(" (This may require sudo password)");
|
|
11572
|
-
await
|
|
13188
|
+
await execAsync7("sudo apt-get update && sudo apt-get install -y tmux", {
|
|
11573
13189
|
timeout: 3e5
|
|
11574
13190
|
});
|
|
11575
13191
|
console.log("\u2705 tmux installed successfully");
|
|
@@ -11577,9 +13193,9 @@ async function tryAutoInstallTmux() {
|
|
|
11577
13193
|
} catch {
|
|
11578
13194
|
}
|
|
11579
13195
|
try {
|
|
11580
|
-
await
|
|
13196
|
+
await execAsync7("which dnf", { timeout: 5e3 });
|
|
11581
13197
|
console.log("\u{1F4E6} Installing tmux via dnf...");
|
|
11582
|
-
await
|
|
13198
|
+
await execAsync7("sudo dnf install -y tmux", { timeout: 3e5 });
|
|
11583
13199
|
console.log("\u2705 tmux installed successfully");
|
|
11584
13200
|
return true;
|
|
11585
13201
|
} catch {
|
|
@@ -11619,11 +13235,11 @@ function getWebDirectory() {
|
|
|
11619
13235
|
try {
|
|
11620
13236
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
11621
13237
|
const webDir = resolve10(currentDir, "..", "web");
|
|
11622
|
-
if (
|
|
13238
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
11623
13239
|
return webDir;
|
|
11624
13240
|
}
|
|
11625
13241
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
11626
|
-
if (
|
|
13242
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
11627
13243
|
return altWebDir;
|
|
11628
13244
|
}
|
|
11629
13245
|
return null;
|
|
@@ -11681,23 +13297,23 @@ async function findWebPort(preferredPort) {
|
|
|
11681
13297
|
return { port: preferredPort, alreadyRunning: false };
|
|
11682
13298
|
}
|
|
11683
13299
|
function hasProductionBuild(webDir) {
|
|
11684
|
-
const buildIdPath =
|
|
11685
|
-
return
|
|
13300
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
13301
|
+
return existsSync19(buildIdPath);
|
|
11686
13302
|
}
|
|
11687
13303
|
function hasSourceFiles(webDir) {
|
|
11688
|
-
const appDir =
|
|
11689
|
-
const pagesDir =
|
|
11690
|
-
const rootAppDir =
|
|
11691
|
-
const rootPagesDir =
|
|
11692
|
-
return
|
|
13304
|
+
const appDir = join13(webDir, "src", "app");
|
|
13305
|
+
const pagesDir = join13(webDir, "src", "pages");
|
|
13306
|
+
const rootAppDir = join13(webDir, "app");
|
|
13307
|
+
const rootPagesDir = join13(webDir, "pages");
|
|
13308
|
+
return existsSync19(appDir) || existsSync19(pagesDir) || existsSync19(rootAppDir) || existsSync19(rootPagesDir);
|
|
11693
13309
|
}
|
|
11694
13310
|
function getStandaloneServerPath(webDir) {
|
|
11695
13311
|
const possiblePaths2 = [
|
|
11696
|
-
|
|
11697
|
-
|
|
13312
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
13313
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
11698
13314
|
];
|
|
11699
13315
|
for (const serverPath of possiblePaths2) {
|
|
11700
|
-
if (
|
|
13316
|
+
if (existsSync19(serverPath)) {
|
|
11701
13317
|
return serverPath;
|
|
11702
13318
|
}
|
|
11703
13319
|
}
|
|
@@ -11737,13 +13353,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
11737
13353
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
11738
13354
|
return { process: null, port: actualPort };
|
|
11739
13355
|
}
|
|
11740
|
-
const usePnpm =
|
|
11741
|
-
const useNpm = !usePnpm &&
|
|
13356
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
13357
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
11742
13358
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
11743
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
13359
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
11744
13360
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
11745
13361
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
11746
|
-
const runtimeConfigPath =
|
|
13362
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
11747
13363
|
try {
|
|
11748
13364
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
11749
13365
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -11751,7 +13367,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
11751
13367
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
11752
13368
|
}
|
|
11753
13369
|
const webEnv = {
|
|
11754
|
-
...
|
|
13370
|
+
...cleanEnv2,
|
|
11755
13371
|
PORT: String(actualPort)
|
|
11756
13372
|
// Next.js respects PORT env var
|
|
11757
13373
|
};
|
|
@@ -11865,12 +13481,28 @@ function stopWebUI() {
|
|
|
11865
13481
|
}
|
|
11866
13482
|
}
|
|
11867
13483
|
async function createApp(options = {}) {
|
|
11868
|
-
const app = new
|
|
13484
|
+
const app = new Hono7();
|
|
11869
13485
|
app.use("*", cors({
|
|
11870
13486
|
origin: "*",
|
|
11871
13487
|
// Allow all origins
|
|
11872
13488
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11873
|
-
allowHeaders: [
|
|
13489
|
+
allowHeaders: [
|
|
13490
|
+
"Content-Type",
|
|
13491
|
+
"Authorization",
|
|
13492
|
+
"X-Requested-With",
|
|
13493
|
+
// Personal-agent dashboard signs every request to the device with
|
|
13494
|
+
// these. Without them whitelisted the browser preflight strips the
|
|
13495
|
+
// headers and the signature middleware 401s.
|
|
13496
|
+
"X-Signature",
|
|
13497
|
+
"X-Signature-Timestamp",
|
|
13498
|
+
"X-Signature-Algorithm",
|
|
13499
|
+
"X-Device-Hwid",
|
|
13500
|
+
// Short-lived embed token used by the iframed SparkECoder web UI
|
|
13501
|
+
// when it's hosted inside the personal-agents dashboard. The
|
|
13502
|
+
// bootstrap in `web/src/lib/embed-bootstrap.ts` adds this header
|
|
13503
|
+
// to every API call.
|
|
13504
|
+
"X-Embed-Token"
|
|
13505
|
+
],
|
|
11874
13506
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11875
13507
|
maxAge: 86400
|
|
11876
13508
|
// 24 hours
|
|
@@ -11878,12 +13510,15 @@ async function createApp(options = {}) {
|
|
|
11878
13510
|
if (!options.quiet) {
|
|
11879
13511
|
app.use("*", logger());
|
|
11880
13512
|
}
|
|
13513
|
+
app.use("*", hwidMiddleware());
|
|
13514
|
+
app.use("*", signatureMiddleware());
|
|
11881
13515
|
app.route("/health", health);
|
|
11882
13516
|
app.route("/sessions", sessions);
|
|
11883
13517
|
app.route("/agents", agents);
|
|
11884
13518
|
app.route("/sessions", terminals);
|
|
11885
13519
|
app.route("/terminals", terminals);
|
|
11886
13520
|
app.route("/tasks", tasks_default);
|
|
13521
|
+
app.route("/system", system);
|
|
11887
13522
|
app.get("/openapi.json", async (c) => {
|
|
11888
13523
|
return c.json(generateOpenAPISpec());
|
|
11889
13524
|
});
|
|
@@ -11936,8 +13571,8 @@ async function startServer(options = {}) {
|
|
|
11936
13571
|
if (options.workingDirectory) {
|
|
11937
13572
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11938
13573
|
}
|
|
11939
|
-
if (!
|
|
11940
|
-
|
|
13574
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
13575
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11941
13576
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11942
13577
|
}
|
|
11943
13578
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11972,6 +13607,15 @@ async function startServer(options = {}) {
|
|
|
11972
13607
|
port,
|
|
11973
13608
|
hostname: host
|
|
11974
13609
|
});
|
|
13610
|
+
try {
|
|
13611
|
+
attachPtyServer(serverInstance, {
|
|
13612
|
+
quiet: options.quiet
|
|
13613
|
+
});
|
|
13614
|
+
} catch (e) {
|
|
13615
|
+
if (!options.quiet) {
|
|
13616
|
+
console.warn(` \u26A0 Failed to attach /pty WebSocket server: ${e.message}`);
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
11975
13619
|
let webPort;
|
|
11976
13620
|
let webStarted;
|
|
11977
13621
|
if (options.webUI !== false) {
|
|
@@ -12453,8 +14097,8 @@ function generateOpenAPISpec() {
|
|
|
12453
14097
|
init_config();
|
|
12454
14098
|
init_semantic();
|
|
12455
14099
|
init_db();
|
|
12456
|
-
import { writeFileSync as writeFileSync6, readFileSync as
|
|
12457
|
-
import { resolve as resolve11, join as
|
|
14100
|
+
import { writeFileSync as writeFileSync6, readFileSync as readFileSync12, existsSync as existsSync20 } from "fs";
|
|
14101
|
+
import { resolve as resolve11, join as join14 } from "path";
|
|
12458
14102
|
async function apiRequest(baseUrl, path, options = {}) {
|
|
12459
14103
|
const url = `${baseUrl}${path}`;
|
|
12460
14104
|
const init = {
|
|
@@ -13017,10 +14661,26 @@ Unexpected error: ${outerError.message}`));
|
|
|
13017
14661
|
}
|
|
13018
14662
|
}
|
|
13019
14663
|
var program = new Command();
|
|
13020
|
-
program.name("sparkecoder").description("AI coding agent - just type sparkecoder to start chatting").version("0.1.0").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").action(async (options) => {
|
|
14664
|
+
program.name("sparkecoder").description("AI coding agent - just type sparkecoder to start chatting").version("0.1.0").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").option("--enable-computer-use", "Enable the Anthropic computer use tool for all new sessions by default (macOS only)").action(async (options) => {
|
|
14665
|
+
if (options.enableComputerUse) {
|
|
14666
|
+
process.env.SPARKECODER_COMPUTER_USE = "1";
|
|
14667
|
+
console.log(chalk.cyan("\u2192 Computer use enabled by default for new sessions."));
|
|
14668
|
+
}
|
|
13021
14669
|
await runChat(options);
|
|
13022
14670
|
});
|
|
13023
|
-
program.command("server").description("Start the SparkECoder server (API + Web UI)").option("-p, --port <port>", "API server port", "3141").option("-h, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("-w, --working-dir <path>", "Working directory").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").action(async (options) => {
|
|
14671
|
+
program.command("server").description("Start the SparkECoder server (API + Web UI)").option("-p, --port <port>", "API server port", "3141").option("-h, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("-w, --working-dir <path>", "Working directory").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--enable-computer-use", "Enable the Anthropic computer use tool for all new sessions by default (macOS only)").option("--personal-agent-mode", "Heartbeat to a personal-agents dashboard so it can dispatch tasks here. Reads $PERSONAL_AGENT_* env vars for the rest.").option("--personal-agent-dashboard <url>", "Dashboard URL to heartbeat to (env: PERSONAL_AGENT_DASHBOARD)").option("--personal-agent-name <name>", "Friendly device name shown on the dashboard (env: PERSONAL_AGENT_NAME, default: hostname)").option("--personal-agent-public-url <url>", "Public URL the dashboard should call to reach this sparkecoder API (env: PERSONAL_AGENT_PUBLIC_URL)").option("--personal-agent-web-url <url>", "Public URL of this sparkecoder's web UI, typically port 6970. When set, the dashboard can iframe `${webUrl}/embed/${sparkTaskId}` to show the live session inside its TaskDrawer (env: PERSONAL_AGENT_WEB_URL)").option("--personal-agent-ingest-token <token>", "Shared bearer token that auths heartbeats with the dashboard (env: PERSONAL_AGENT_INGEST_TOKEN)").option("--personal-agent-public-key <pem-or-path>", "Ed25519 PUBLIC key the dashboard signs with (PEM string or path to .pem). env: PERSONAL_AGENT_PUBLIC_KEY").option("--accept-signed-only", "Reject any inbound non-/health request that lacks a valid Ed25519 signature from the dashboard. Requires --personal-agent-public-key.").action(async (options) => {
|
|
14672
|
+
const globalOpts = program.opts();
|
|
14673
|
+
const enableCU = options.enableComputerUse || globalOpts.enableComputerUse;
|
|
14674
|
+
if (enableCU) {
|
|
14675
|
+
process.env.SPARKECODER_COMPUTER_USE = "1";
|
|
14676
|
+
console.log(chalk.cyan("\u2192 Computer use enabled by default for new sessions."));
|
|
14677
|
+
}
|
|
14678
|
+
if (options.personalAgentPublicKey) {
|
|
14679
|
+
process.env.PERSONAL_AGENT_PUBLIC_KEY = options.personalAgentPublicKey;
|
|
14680
|
+
}
|
|
14681
|
+
if (options.acceptSignedOnly) {
|
|
14682
|
+
process.env.PERSONAL_AGENT_ACCEPT_SIGNED_ONLY = "1";
|
|
14683
|
+
}
|
|
13024
14684
|
await ensureDependencies({ quiet: false });
|
|
13025
14685
|
const spinner = ora("Starting SparkECoder server...").start();
|
|
13026
14686
|
try {
|
|
@@ -13043,6 +14703,8 @@ program.command("server").description("Start the SparkECoder server (API + Web U
|
|
|
13043
14703
|
console.log("");
|
|
13044
14704
|
console.log(chalk.dim(` API: http://${host}:${port}`));
|
|
13045
14705
|
console.log(chalk.dim(` Swagger: http://${host}:${port}/swagger`));
|
|
14706
|
+
const { readConfig: readPaCfg, startPersonalAgent: startPersonalAgent2 } = await Promise.resolve().then(() => (init_heartbeat(), heartbeat_exports));
|
|
14707
|
+
startPersonalAgent2(readPaCfg(options));
|
|
13046
14708
|
console.log("");
|
|
13047
14709
|
console.log(chalk.dim("Press Ctrl+C to stop"));
|
|
13048
14710
|
const cleanup2 = () => {
|
|
@@ -13057,7 +14719,13 @@ program.command("server").description("Start the SparkECoder server (API + Web U
|
|
|
13057
14719
|
process.exit(1);
|
|
13058
14720
|
}
|
|
13059
14721
|
});
|
|
13060
|
-
program.command("chat").description("Start an interactive chat session").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").action(async (options) => {
|
|
14722
|
+
program.command("chat").description("Start an interactive chat session").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").option("--enable-computer-use", "Enable the Anthropic computer use tool for all new sessions by default (macOS only)").action(async (options) => {
|
|
14723
|
+
const globalOpts = program.opts();
|
|
14724
|
+
const enableCU = options.enableComputerUse || globalOpts.enableComputerUse;
|
|
14725
|
+
if (enableCU) {
|
|
14726
|
+
process.env.SPARKECODER_COMPUTER_USE = "1";
|
|
14727
|
+
console.log(chalk.cyan("\u2192 Computer use enabled by default for new sessions."));
|
|
14728
|
+
}
|
|
13061
14729
|
await runChat(options);
|
|
13062
14730
|
});
|
|
13063
14731
|
program.command("task").description("Run an autonomous task that completes without human interaction").requiredOption("--prompt <prompt>", "Task prompt describing what to do").requiredOption("--schema <schema>", "JSON Schema for the output (file path or inline JSON string)").option("--webhook <url>", "Webhook URL to receive task events").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-n, --name <name>", "Name for the task").option("--max-iterations <n>", "Maximum agent loop iterations", "50").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("--no-auto-start", "Do not auto-start server if not running").option("--parent-task <id>", "Continue from a previous task (inherits its full conversation context)").option("--wait", "Block and poll until task completes").option("-v, --verbose", "Enable verbose logging").action(async (options) => {
|
|
@@ -13089,8 +14757,8 @@ program.command("task").description("Run an autonomous task that completes witho
|
|
|
13089
14757
|
let outputSchema;
|
|
13090
14758
|
try {
|
|
13091
14759
|
const schemaStr = options.schema;
|
|
13092
|
-
if (
|
|
13093
|
-
outputSchema = JSON.parse(
|
|
14760
|
+
if (existsSync20(schemaStr)) {
|
|
14761
|
+
outputSchema = JSON.parse(readFileSync12(schemaStr, "utf-8"));
|
|
13094
14762
|
} else {
|
|
13095
14763
|
outputSchema = JSON.parse(schemaStr);
|
|
13096
14764
|
}
|
|
@@ -13157,13 +14825,13 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
|
|
|
13157
14825
|
let configLocation;
|
|
13158
14826
|
if (options.global) {
|
|
13159
14827
|
const appDataDir = ensureAppDataDirectory();
|
|
13160
|
-
configPath =
|
|
14828
|
+
configPath = join14(appDataDir, "sparkecoder.config.json");
|
|
13161
14829
|
configLocation = "global";
|
|
13162
14830
|
} else {
|
|
13163
14831
|
configPath = resolve11(process.cwd(), "sparkecoder.config.json");
|
|
13164
14832
|
configLocation = "local";
|
|
13165
14833
|
}
|
|
13166
|
-
if (
|
|
14834
|
+
if (existsSync20(configPath) && !options.force) {
|
|
13167
14835
|
console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
|
|
13168
14836
|
console.log(chalk.dim(` ${configPath}`));
|
|
13169
14837
|
return;
|
|
@@ -13485,5 +15153,112 @@ ${providerName} API Key:
|
|
|
13485
15153
|
process.exit(1);
|
|
13486
15154
|
}
|
|
13487
15155
|
});
|
|
15156
|
+
program.command("check-permissions").description("Check macOS permissions required for the computer use tool (Accessibility + Screen Recording)").action(async () => {
|
|
15157
|
+
if (process.platform !== "darwin") {
|
|
15158
|
+
console.log(chalk.yellow(`Computer use is only supported on macOS. Current platform: ${process.platform}`));
|
|
15159
|
+
process.exit(0);
|
|
15160
|
+
}
|
|
15161
|
+
const {
|
|
15162
|
+
isCliclickInstalled: isCliclickInstalled2,
|
|
15163
|
+
hasAccessibilityPermissions: hasAccessibilityPermissions2,
|
|
15164
|
+
hasScreenRecordingPermissions: hasScreenRecordingPermissions2,
|
|
15165
|
+
detectScreenSize: detectScreenSize2
|
|
15166
|
+
} = await Promise.resolve().then(() => (init_computer_use(), computer_use_exports));
|
|
15167
|
+
console.log(chalk.bold("\nComputer use prerequisites:\n"));
|
|
15168
|
+
const cliclick = await isCliclickInstalled2();
|
|
15169
|
+
if (cliclick) {
|
|
15170
|
+
console.log(` ${chalk.green("\u2713")} cliclick installed`);
|
|
15171
|
+
} else {
|
|
15172
|
+
console.log(` ${chalk.red("\u2717")} cliclick not installed`);
|
|
15173
|
+
console.log(` ${chalk.dim("Install with: brew install cliclick")}`);
|
|
15174
|
+
}
|
|
15175
|
+
const acc = cliclick ? await hasAccessibilityPermissions2() : { ok: false, error: "cliclick not installed" };
|
|
15176
|
+
if (acc.ok) {
|
|
15177
|
+
console.log(` ${chalk.green("\u2713")} Accessibility permissions granted`);
|
|
15178
|
+
} else {
|
|
15179
|
+
console.log(` ${chalk.red("\u2717")} Accessibility permissions missing`);
|
|
15180
|
+
console.log(` ${chalk.dim(acc.error?.split("\n")[0] || "")}`);
|
|
15181
|
+
}
|
|
15182
|
+
const screen = await hasScreenRecordingPermissions2();
|
|
15183
|
+
if (screen) {
|
|
15184
|
+
console.log(` ${chalk.green("\u2713")} Screen Recording permissions granted`);
|
|
15185
|
+
} else {
|
|
15186
|
+
console.log(` ${chalk.red("\u2717")} Screen Recording permissions missing`);
|
|
15187
|
+
}
|
|
15188
|
+
const size = await detectScreenSize2();
|
|
15189
|
+
if (size) {
|
|
15190
|
+
console.log(` ${chalk.dim("\u2022")} Detected display: ${size.width}x${size.height} points`);
|
|
15191
|
+
}
|
|
15192
|
+
const allOk = cliclick && acc.ok && screen;
|
|
15193
|
+
console.log();
|
|
15194
|
+
if (allOk) {
|
|
15195
|
+
console.log(chalk.green("All checks passed. The agent can use computer use."));
|
|
15196
|
+
} else {
|
|
15197
|
+
console.log(chalk.yellow("Some prerequisites are missing."));
|
|
15198
|
+
console.log(chalk.dim("Run: sparkecoder request-permissions"));
|
|
15199
|
+
}
|
|
15200
|
+
console.log();
|
|
15201
|
+
process.exit(allOk ? 0 : 1);
|
|
15202
|
+
});
|
|
15203
|
+
program.command("request-permissions").description("Request macOS permissions for the computer use tool \u2014 triggers system prompts and opens System Settings").action(async () => {
|
|
15204
|
+
if (process.platform !== "darwin") {
|
|
15205
|
+
console.log(chalk.yellow(`Computer use is only supported on macOS. Current platform: ${process.platform}`));
|
|
15206
|
+
process.exit(0);
|
|
15207
|
+
}
|
|
15208
|
+
const {
|
|
15209
|
+
isCliclickInstalled: isCliclickInstalled2,
|
|
15210
|
+
hasAccessibilityPermissions: hasAccessibilityPermissions2,
|
|
15211
|
+
hasScreenRecordingPermissions: hasScreenRecordingPermissions2,
|
|
15212
|
+
requestAccessibilityPrompt: requestAccessibilityPrompt2,
|
|
15213
|
+
requestScreenRecordingPrompt: requestScreenRecordingPrompt2,
|
|
15214
|
+
openSystemSettings: openSystemSettings2,
|
|
15215
|
+
detectScreenSize: detectScreenSize2
|
|
15216
|
+
} = await Promise.resolve().then(() => (init_computer_use(), computer_use_exports));
|
|
15217
|
+
console.log(chalk.bold("\nRequesting computer use permissions...\n"));
|
|
15218
|
+
if (!await isCliclickInstalled2()) {
|
|
15219
|
+
console.log(` ${chalk.red("\u2717")} cliclick is not installed`);
|
|
15220
|
+
console.log(` ${chalk.dim("Run: brew install cliclick")}`);
|
|
15221
|
+
console.log(` ${chalk.dim("Then re-run: sparkecoder request-permissions")}`);
|
|
15222
|
+
console.log();
|
|
15223
|
+
process.exit(1);
|
|
15224
|
+
}
|
|
15225
|
+
const acc = await hasAccessibilityPermissions2();
|
|
15226
|
+
const screen = await hasScreenRecordingPermissions2();
|
|
15227
|
+
let needsRestart = false;
|
|
15228
|
+
if (!acc.ok) {
|
|
15229
|
+
console.log(` ${chalk.yellow("\u26A0")} Accessibility permission missing \u2014 triggering prompt and opening System Settings...`);
|
|
15230
|
+
await requestAccessibilityPrompt2();
|
|
15231
|
+
await openSystemSettings2("accessibility");
|
|
15232
|
+
console.log(` ${chalk.dim("1. In Privacy & Security \u2192 Accessibility, click +")}`);
|
|
15233
|
+
console.log(` ${chalk.dim("2. Add the application running the agent (Terminal, iTerm, your IDE, or `node`)")}`);
|
|
15234
|
+
console.log(` ${chalk.dim("3. Toggle the switch ON")}`);
|
|
15235
|
+
needsRestart = true;
|
|
15236
|
+
} else {
|
|
15237
|
+
console.log(` ${chalk.green("\u2713")} Accessibility already granted`);
|
|
15238
|
+
}
|
|
15239
|
+
if (!screen) {
|
|
15240
|
+
console.log(` ${chalk.yellow("\u26A0")} Screen Recording permission missing \u2014 triggering prompt and opening System Settings...`);
|
|
15241
|
+
await requestScreenRecordingPrompt2();
|
|
15242
|
+
await openSystemSettings2("screen-recording");
|
|
15243
|
+
console.log(` ${chalk.dim("1. In Privacy & Security \u2192 Screen Recording, click +")}`);
|
|
15244
|
+
console.log(` ${chalk.dim("2. Add the application running the agent (Terminal, iTerm, your IDE, or `node`)")}`);
|
|
15245
|
+
console.log(` ${chalk.dim("3. Toggle the switch ON")}`);
|
|
15246
|
+
needsRestart = true;
|
|
15247
|
+
} else {
|
|
15248
|
+
console.log(` ${chalk.green("\u2713")} Screen Recording already granted`);
|
|
15249
|
+
}
|
|
15250
|
+
const size = await detectScreenSize2();
|
|
15251
|
+
if (size) {
|
|
15252
|
+
console.log(` ${chalk.dim("\u2022")} Detected display: ${size.width}x${size.height} points`);
|
|
15253
|
+
}
|
|
15254
|
+
console.log();
|
|
15255
|
+
if (needsRestart) {
|
|
15256
|
+
console.log(chalk.yellow("After granting permissions, RESTART the agent process for the new TCC entries to take effect."));
|
|
15257
|
+
console.log(chalk.dim("Then run: sparkecoder check-permissions"));
|
|
15258
|
+
} else {
|
|
15259
|
+
console.log(chalk.green("All permissions are already granted. Computer use is ready."));
|
|
15260
|
+
}
|
|
15261
|
+
console.log();
|
|
15262
|
+
});
|
|
13488
15263
|
program.parse();
|
|
13489
15264
|
//# sourceMappingURL=cli.js.map
|