sparkecoder 0.1.86 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +661 -39
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +1996 -225
- package/dist/cli.js.map +1 -1
- package/dist/{index-OhuTM4a0.d.ts → index-BvIissiB.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1683 -199
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +1683 -199
- package/dist/server/index.js.map +1 -1
- package/dist/skills/default/computer-use.md +150 -0
- package/dist/tools/index.d.ts +167 -1
- package/dist/tools/index.js +604 -10
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -1
- package/src/skills/default/computer-use.md +150 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ecd2bdca._.js → 2374f_317b1fef._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9adc1edb._.js → 2374f_37dd9702._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8dc0f9aa._.js → 2374f_4d44e4ed._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_cc6c6363._.js → 2374f_54ac917f._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_00f7fe07._.js → 2374f_86585101._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_369747ce._.js → 2374f_a383a4d9._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d58d0276._.js → 2374f_c59a35bb._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__25b25c9d._.js → [root-of-the-server]__9a826344._.js} +2 -2
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
- package/web/.next/standalone/web/src/lib/embed-bootstrap.ts +108 -0
- package/web/.next/static/chunks/275e8268daf318b2.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
- package/web/.next/static/chunks/5383c5717758f575.js +0 -7
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -518,7 +518,12 @@ var init_types = __esm({
|
|
|
518
518
|
// Whether to always inject this skill into context (vs on-demand loading)
|
|
519
519
|
alwaysApply: z.boolean().optional().default(false),
|
|
520
520
|
// Glob patterns - auto-inject when working with matching files
|
|
521
|
-
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([])
|
|
522
527
|
});
|
|
523
528
|
TaskConfigSchema = z.object({
|
|
524
529
|
enabled: z.boolean(),
|
|
@@ -536,7 +541,13 @@ var init_types = __esm({
|
|
|
536
541
|
approvalWebhook: z.string().url().optional(),
|
|
537
542
|
skillsDirectory: z.string().optional(),
|
|
538
543
|
maxContextChars: z.number().optional().default(2e5),
|
|
539
|
-
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()
|
|
540
551
|
});
|
|
541
552
|
VectorGatewayConfigSchema = z.object({
|
|
542
553
|
// Redis cluster nodes URL for Vector Gateway (or use REDIS_CLUSTER_NODES env var)
|
|
@@ -1564,7 +1575,8 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1564
1575
|
globs: parsed.metadata.globs,
|
|
1565
1576
|
loadType,
|
|
1566
1577
|
priority,
|
|
1567
|
-
sourceDir: directory
|
|
1578
|
+
sourceDir: directory,
|
|
1579
|
+
platforms: parsed.metadata.platforms
|
|
1568
1580
|
});
|
|
1569
1581
|
} else {
|
|
1570
1582
|
const name = getSkillNameFromPath(filePath);
|
|
@@ -1577,11 +1589,14 @@ async function loadSkillsFromDirectory(directory, options = {}) {
|
|
|
1577
1589
|
globs: [],
|
|
1578
1590
|
loadType: forceAlwaysApply ? "always" : defaultLoadType,
|
|
1579
1591
|
priority,
|
|
1580
|
-
sourceDir: directory
|
|
1592
|
+
sourceDir: directory,
|
|
1593
|
+
platforms: []
|
|
1581
1594
|
});
|
|
1582
1595
|
}
|
|
1583
1596
|
}
|
|
1584
|
-
return skills
|
|
1597
|
+
return skills.filter(
|
|
1598
|
+
(s) => s.platforms.length === 0 || s.platforms.includes(process.platform)
|
|
1599
|
+
);
|
|
1585
1600
|
}
|
|
1586
1601
|
async function loadAllSkills(directories) {
|
|
1587
1602
|
const allSkills = [];
|
|
@@ -2042,7 +2057,7 @@ function mergeSmallBlocks(blocks, lines) {
|
|
|
2042
2057
|
merged.push(current);
|
|
2043
2058
|
return merged;
|
|
2044
2059
|
}
|
|
2045
|
-
function splitLargeBlock(filePath, content, startLine, language,
|
|
2060
|
+
function splitLargeBlock(filePath, content, startLine, language, type2, name) {
|
|
2046
2061
|
const chunks = [];
|
|
2047
2062
|
const lines = content.split("\n");
|
|
2048
2063
|
let currentStart = 0;
|
|
@@ -2062,7 +2077,7 @@ function splitLargeBlock(filePath, content, startLine, language, type, name) {
|
|
|
2062
2077
|
startLine: startLine + currentStart + 1,
|
|
2063
2078
|
endLine: startLine + i,
|
|
2064
2079
|
language,
|
|
2065
|
-
chunkType:
|
|
2080
|
+
chunkType: type2,
|
|
2066
2081
|
symbolName: name
|
|
2067
2082
|
}
|
|
2068
2083
|
});
|
|
@@ -2084,7 +2099,7 @@ function splitLargeBlock(filePath, content, startLine, language, type, name) {
|
|
|
2084
2099
|
startLine: startLine + currentStart + 1,
|
|
2085
2100
|
endLine: startLine + lines.length,
|
|
2086
2101
|
language,
|
|
2087
|
-
chunkType:
|
|
2102
|
+
chunkType: type2,
|
|
2088
2103
|
symbolName: name
|
|
2089
2104
|
}
|
|
2090
2105
|
});
|
|
@@ -2401,18 +2416,18 @@ function isPathExcluded(relativePath, exclude) {
|
|
|
2401
2416
|
});
|
|
2402
2417
|
}
|
|
2403
2418
|
async function walkDirectory(dir, include, exclude, baseDir) {
|
|
2404
|
-
const { readdirSync:
|
|
2405
|
-
const { join:
|
|
2419
|
+
const { readdirSync: readdirSync4 } = await import("fs");
|
|
2420
|
+
const { join: join15, relative: relative10 } = await import("path");
|
|
2406
2421
|
const files = [];
|
|
2407
2422
|
function walk(currentDir) {
|
|
2408
2423
|
let entries;
|
|
2409
2424
|
try {
|
|
2410
|
-
entries =
|
|
2425
|
+
entries = readdirSync4(currentDir, { withFileTypes: true });
|
|
2411
2426
|
} catch {
|
|
2412
2427
|
return;
|
|
2413
2428
|
}
|
|
2414
2429
|
for (const entry of entries) {
|
|
2415
|
-
const fullPath =
|
|
2430
|
+
const fullPath = join15(currentDir, entry.name);
|
|
2416
2431
|
const relativePath = relative10(baseDir, fullPath);
|
|
2417
2432
|
if (isPathExcluded(relativePath, exclude)) {
|
|
2418
2433
|
continue;
|
|
@@ -2914,12 +2929,440 @@ var init_semantic_search = __esm({
|
|
|
2914
2929
|
}
|
|
2915
2930
|
});
|
|
2916
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
|
+
|
|
2917
3359
|
// src/utils/webhook.ts
|
|
2918
3360
|
var webhook_exports = {};
|
|
2919
3361
|
__export(webhook_exports, {
|
|
2920
3362
|
sendWebhook: () => sendWebhook
|
|
2921
3363
|
});
|
|
2922
3364
|
async function sendWebhook(url, event) {
|
|
3365
|
+
const t0 = Date.now();
|
|
2923
3366
|
try {
|
|
2924
3367
|
const controller = new AbortController();
|
|
2925
3368
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -2933,17 +3376,36 @@ async function sendWebhook(url, event) {
|
|
|
2933
3376
|
signal: controller.signal
|
|
2934
3377
|
});
|
|
2935
3378
|
clearTimeout(timeout);
|
|
3379
|
+
const ms = Date.now() - t0;
|
|
2936
3380
|
if (!response.ok) {
|
|
2937
|
-
|
|
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
|
+
);
|
|
2938
3391
|
}
|
|
2939
3392
|
} catch (err) {
|
|
2940
3393
|
const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
|
|
2941
|
-
console.warn(
|
|
3394
|
+
console.warn(
|
|
3395
|
+
`[WEBHOOK] ${event.type} task=${event.taskId} -> failed: ${reason}`
|
|
3396
|
+
);
|
|
2942
3397
|
}
|
|
2943
3398
|
}
|
|
3399
|
+
var TERMINAL_EVENTS, QUIET;
|
|
2944
3400
|
var init_webhook = __esm({
|
|
2945
3401
|
"src/utils/webhook.ts"() {
|
|
2946
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";
|
|
2947
3409
|
}
|
|
2948
3410
|
});
|
|
2949
3411
|
|
|
@@ -3153,15 +3615,15 @@ var recorder_exports = {};
|
|
|
3153
3615
|
__export(recorder_exports, {
|
|
3154
3616
|
FrameRecorder: () => FrameRecorder
|
|
3155
3617
|
});
|
|
3156
|
-
import { exec as
|
|
3157
|
-
import { promisify as
|
|
3618
|
+
import { exec as exec6 } from "child_process";
|
|
3619
|
+
import { promisify as promisify6 } from "util";
|
|
3158
3620
|
import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
|
|
3159
|
-
import { join as
|
|
3160
|
-
import { tmpdir } from "os";
|
|
3161
|
-
import { nanoid as
|
|
3621
|
+
import { join as join9 } from "path";
|
|
3622
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3623
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
3162
3624
|
async function checkFfmpeg() {
|
|
3163
3625
|
try {
|
|
3164
|
-
await
|
|
3626
|
+
await execAsync6("ffmpeg -version", { timeout: 5e3 });
|
|
3165
3627
|
return true;
|
|
3166
3628
|
} catch {
|
|
3167
3629
|
return false;
|
|
@@ -3173,11 +3635,11 @@ async function cleanup(dir) {
|
|
|
3173
3635
|
} catch {
|
|
3174
3636
|
}
|
|
3175
3637
|
}
|
|
3176
|
-
var
|
|
3638
|
+
var execAsync6, FrameRecorder;
|
|
3177
3639
|
var init_recorder = __esm({
|
|
3178
3640
|
"src/browser/recorder.ts"() {
|
|
3179
3641
|
"use strict";
|
|
3180
|
-
|
|
3642
|
+
execAsync6 = promisify6(exec6);
|
|
3181
3643
|
FrameRecorder = class {
|
|
3182
3644
|
frames = [];
|
|
3183
3645
|
startTime = null;
|
|
@@ -3213,21 +3675,21 @@ var init_recorder = __esm({
|
|
|
3213
3675
|
*/
|
|
3214
3676
|
async encode() {
|
|
3215
3677
|
if (this.frames.length === 0) return null;
|
|
3216
|
-
const workDir =
|
|
3678
|
+
const workDir = join9(tmpdir2(), `sparkecoder-recording-${nanoid4(8)}`);
|
|
3217
3679
|
await mkdir4(workDir, { recursive: true });
|
|
3218
3680
|
try {
|
|
3219
3681
|
for (let i = 0; i < this.frames.length; i++) {
|
|
3220
|
-
const framePath =
|
|
3682
|
+
const framePath = join9(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
|
|
3221
3683
|
await writeFile5(framePath, this.frames[i].data);
|
|
3222
3684
|
}
|
|
3223
3685
|
const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
|
|
3224
3686
|
const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
|
|
3225
3687
|
const clampedFps = Math.max(1, Math.min(fps, 30));
|
|
3226
|
-
const outputPath =
|
|
3688
|
+
const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
|
|
3227
3689
|
const hasFfmpeg = await checkFfmpeg();
|
|
3228
3690
|
if (hasFfmpeg) {
|
|
3229
|
-
await
|
|
3230
|
-
`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}"`,
|
|
3231
3693
|
{ timeout: 12e4 }
|
|
3232
3694
|
);
|
|
3233
3695
|
} else {
|
|
@@ -3239,7 +3701,7 @@ var init_recorder = __esm({
|
|
|
3239
3701
|
const files = await readdir5(workDir);
|
|
3240
3702
|
for (const f of files) {
|
|
3241
3703
|
if (f.startsWith("frame_")) {
|
|
3242
|
-
await unlink2(
|
|
3704
|
+
await unlink2(join9(workDir, f)).catch(() => {
|
|
3243
3705
|
});
|
|
3244
3706
|
}
|
|
3245
3707
|
}
|
|
@@ -3260,6 +3722,387 @@ var init_recorder = __esm({
|
|
|
3260
3722
|
}
|
|
3261
3723
|
});
|
|
3262
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
|
+
|
|
3263
4106
|
// src/cli.ts
|
|
3264
4107
|
import { Command } from "commander";
|
|
3265
4108
|
import chalk from "chalk";
|
|
@@ -3269,12 +4112,12 @@ import { createInterface } from "readline";
|
|
|
3269
4112
|
|
|
3270
4113
|
// src/server/index.ts
|
|
3271
4114
|
import "dotenv/config";
|
|
3272
|
-
import { Hono as
|
|
4115
|
+
import { Hono as Hono7 } from "hono";
|
|
3273
4116
|
import { serve } from "@hono/node-server";
|
|
3274
4117
|
import { cors } from "hono/cors";
|
|
3275
4118
|
import { logger } from "hono/logger";
|
|
3276
|
-
import { existsSync as
|
|
3277
|
-
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";
|
|
3278
4121
|
import { spawn as spawn2 } from "child_process";
|
|
3279
4122
|
import { createServer as createNetServer } from "net";
|
|
3280
4123
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -3283,17 +4126,17 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
3283
4126
|
init_db();
|
|
3284
4127
|
import { Hono } from "hono";
|
|
3285
4128
|
import { zValidator } from "@hono/zod-validator";
|
|
3286
|
-
import { z as
|
|
3287
|
-
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";
|
|
3288
4131
|
import { readdir as readdir6 } from "fs/promises";
|
|
3289
|
-
import { join as
|
|
3290
|
-
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";
|
|
3291
4134
|
|
|
3292
4135
|
// src/agent/index.ts
|
|
3293
4136
|
import {
|
|
3294
4137
|
streamText as streamText2,
|
|
3295
4138
|
generateText as generateText3,
|
|
3296
|
-
tool as
|
|
4139
|
+
tool as tool14,
|
|
3297
4140
|
stepCountIs as stepCountIs2
|
|
3298
4141
|
} from "ai";
|
|
3299
4142
|
|
|
@@ -3484,8 +4327,8 @@ var SUBAGENT_MODELS = {
|
|
|
3484
4327
|
// src/agent/index.ts
|
|
3485
4328
|
init_db();
|
|
3486
4329
|
init_config();
|
|
3487
|
-
import { z as
|
|
3488
|
-
import { nanoid as
|
|
4330
|
+
import { z as z15 } from "zod";
|
|
4331
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
3489
4332
|
|
|
3490
4333
|
// src/tools/bash.ts
|
|
3491
4334
|
import { tool } from "ai";
|
|
@@ -4552,12 +5395,12 @@ function findNearestRoot(startDir, markers) {
|
|
|
4552
5395
|
}
|
|
4553
5396
|
async function commandExists(cmd) {
|
|
4554
5397
|
try {
|
|
4555
|
-
const { exec:
|
|
4556
|
-
const { promisify:
|
|
4557
|
-
const
|
|
5398
|
+
const { exec: exec8 } = await import("child_process");
|
|
5399
|
+
const { promisify: promisify8 } = await import("util");
|
|
5400
|
+
const execAsync8 = promisify8(exec8);
|
|
4558
5401
|
const isWindows = process.platform === "win32";
|
|
4559
5402
|
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
4560
|
-
await
|
|
5403
|
+
await execAsync8(checkCmd);
|
|
4561
5404
|
return true;
|
|
4562
5405
|
} catch {
|
|
4563
5406
|
return false;
|
|
@@ -6948,11 +7791,169 @@ function createUploadFileTool(options) {
|
|
|
6948
7791
|
});
|
|
6949
7792
|
}
|
|
6950
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
|
+
|
|
6951
7951
|
// src/tools/index.ts
|
|
6952
7952
|
init_semantic();
|
|
6953
7953
|
init_remote();
|
|
6954
7954
|
init_todo();
|
|
6955
7955
|
init_semantic_search();
|
|
7956
|
+
init_computer_use();
|
|
6956
7957
|
async function createTools(options) {
|
|
6957
7958
|
const tools = {
|
|
6958
7959
|
bash: createBashTool({
|
|
@@ -6996,6 +7997,20 @@ async function createTools(options) {
|
|
|
6996
7997
|
sessionId: options.sessionId
|
|
6997
7998
|
});
|
|
6998
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
|
+
}
|
|
6999
8014
|
if (options.enableSemanticSearch !== false) {
|
|
7000
8015
|
try {
|
|
7001
8016
|
if (isVectorGatewayConfigured()) {
|
|
@@ -7026,11 +8041,11 @@ init_db();
|
|
|
7026
8041
|
init_todo();
|
|
7027
8042
|
import os from "os";
|
|
7028
8043
|
function getSearchInstructions() {
|
|
7029
|
-
const
|
|
8044
|
+
const platform5 = process.platform;
|
|
7030
8045
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
7031
8046
|
- **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
|
|
7032
8047
|
- **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
|
|
7033
|
-
if (
|
|
8048
|
+
if (platform5 === "win32") {
|
|
7034
8049
|
return `${common}
|
|
7035
8050
|
- **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
|
|
7036
8051
|
- **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
|
|
@@ -7077,13 +8092,13 @@ async function buildSystemPrompt(options) {
|
|
|
7077
8092
|
);
|
|
7078
8093
|
const hasNoTodos = todos.length === 0;
|
|
7079
8094
|
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
7080
|
-
const
|
|
8095
|
+
const platform5 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
7081
8096
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
7082
8097
|
const searchInstructions = getSearchInstructions();
|
|
7083
8098
|
const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
7084
8099
|
|
|
7085
8100
|
## Environment
|
|
7086
|
-
- **Platform**: ${
|
|
8101
|
+
- **Platform**: ${platform5} (${os.release()})
|
|
7087
8102
|
- **Date**: ${currentDate}
|
|
7088
8103
|
- **Working Directory**: ${workingDirectory}
|
|
7089
8104
|
|
|
@@ -8021,10 +9036,14 @@ var Agent = class _Agent {
|
|
|
8021
9036
|
*/
|
|
8022
9037
|
async createToolsWithCallbacks(options) {
|
|
8023
9038
|
const config = getConfig();
|
|
9039
|
+
const sessionConfig = this.session.config || {};
|
|
8024
9040
|
return createTools({
|
|
8025
9041
|
sessionId: this.session.id,
|
|
8026
9042
|
workingDirectory: this.session.workingDirectory,
|
|
8027
9043
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9044
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
9045
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
9046
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight,
|
|
8028
9047
|
onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0,
|
|
8029
9048
|
onWriteFileProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "write_file", data: progress }) : void 0,
|
|
8030
9049
|
onSearchProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "explore_agent", data: progress }) : void 0
|
|
@@ -8057,10 +9076,14 @@ var Agent = class _Agent {
|
|
|
8057
9076
|
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
8058
9077
|
autoSummarize: config.context?.autoSummarize ?? true
|
|
8059
9078
|
});
|
|
9079
|
+
const sessionConfig = session.config || {};
|
|
8060
9080
|
const tools = await createTools({
|
|
8061
9081
|
sessionId: session.id,
|
|
8062
9082
|
workingDirectory: session.workingDirectory,
|
|
8063
|
-
skillsDirectories: config.resolvedSkillsDirectories
|
|
9083
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9084
|
+
enableComputerUse: sessionConfig.computerUseEnabled === true,
|
|
9085
|
+
computerUseDisplayWidth: sessionConfig.computerUseDisplayWidth,
|
|
9086
|
+
computerUseDisplayHeight: sessionConfig.computerUseDisplayHeight
|
|
8064
9087
|
});
|
|
8065
9088
|
return new _Agent(session, context, tools);
|
|
8066
9089
|
}
|
|
@@ -8233,10 +9256,10 @@ ${prompt}` });
|
|
|
8233
9256
|
const maxIterations = options.taskConfig.maxIterations ?? 50;
|
|
8234
9257
|
const webhookUrl = options.taskConfig.webhookUrl;
|
|
8235
9258
|
const parentTaskId = options.taskConfig.parentTaskId;
|
|
8236
|
-
const fireWebhook = (
|
|
9259
|
+
const fireWebhook = (type2, data) => {
|
|
8237
9260
|
if (!webhookUrl) return;
|
|
8238
9261
|
sendWebhook(webhookUrl, {
|
|
8239
|
-
type,
|
|
9262
|
+
type: type2,
|
|
8240
9263
|
taskId: this.session.id,
|
|
8241
9264
|
sessionId: this.session.id,
|
|
8242
9265
|
...parentTaskId ? { parentTaskId } : {},
|
|
@@ -8279,10 +9302,14 @@ ${prompt}` });
|
|
|
8279
9302
|
});
|
|
8280
9303
|
}
|
|
8281
9304
|
};
|
|
9305
|
+
const taskSessionConfig = this.session.config || {};
|
|
8282
9306
|
const taskTools = await createTools({
|
|
8283
9307
|
sessionId: this.session.id,
|
|
8284
9308
|
workingDirectory: this.session.workingDirectory,
|
|
8285
9309
|
skillsDirectories: config.resolvedSkillsDirectories,
|
|
9310
|
+
enableComputerUse: taskSessionConfig.computerUseEnabled === true,
|
|
9311
|
+
computerUseDisplayWidth: taskSessionConfig.computerUseDisplayWidth,
|
|
9312
|
+
computerUseDisplayHeight: taskSessionConfig.computerUseDisplayHeight,
|
|
8286
9313
|
onBashProgress: bashProgressHandler,
|
|
8287
9314
|
onWriteFileProgress: (progress) => {
|
|
8288
9315
|
options.onToolProgress?.({ toolName: "write_file", data: progress });
|
|
@@ -8565,11 +9592,11 @@ ${taskAddendum}`;
|
|
|
8565
9592
|
const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
8566
9593
|
if (!isRemoteConfigured2()) return [];
|
|
8567
9594
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
8568
|
-
const { join:
|
|
9595
|
+
const { join: join15, basename: basename6 } = await import("path");
|
|
8569
9596
|
const urls = [];
|
|
8570
9597
|
for (const filePath of filePaths) {
|
|
8571
9598
|
try {
|
|
8572
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
9599
|
+
const fullPath = filePath.startsWith("/") ? filePath : join15(this.session.workingDirectory, filePath);
|
|
8573
9600
|
const fileName = basename6(fullPath);
|
|
8574
9601
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
8575
9602
|
const mimeMap = {
|
|
@@ -8627,11 +9654,11 @@ ${taskAddendum}`;
|
|
|
8627
9654
|
wrappedTools[name] = originalTool;
|
|
8628
9655
|
continue;
|
|
8629
9656
|
}
|
|
8630
|
-
wrappedTools[name] =
|
|
9657
|
+
wrappedTools[name] = tool14({
|
|
8631
9658
|
description: originalTool.description || "",
|
|
8632
|
-
inputSchema: originalTool.inputSchema ||
|
|
9659
|
+
inputSchema: originalTool.inputSchema || z15.object({}),
|
|
8633
9660
|
execute: async (input, toolOptions) => {
|
|
8634
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
9661
|
+
const toolCallId = toolOptions.toolCallId || nanoid5();
|
|
8635
9662
|
const execution = toolExecutionQueries.create({
|
|
8636
9663
|
sessionId: this.session.id,
|
|
8637
9664
|
toolName: name,
|
|
@@ -8649,10 +9676,10 @@ ${taskAddendum}`;
|
|
|
8649
9676
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
8650
9677
|
approvalResolvers.delete(toolCallId);
|
|
8651
9678
|
this.pendingApprovals.delete(toolCallId);
|
|
8652
|
-
const
|
|
9679
|
+
const exec8 = await execution;
|
|
8653
9680
|
if (!approved) {
|
|
8654
9681
|
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
8655
|
-
await toolExecutionQueries.reject(
|
|
9682
|
+
await toolExecutionQueries.reject(exec8.id);
|
|
8656
9683
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
8657
9684
|
return {
|
|
8658
9685
|
status: "rejected",
|
|
@@ -8662,14 +9689,14 @@ ${taskAddendum}`;
|
|
|
8662
9689
|
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
8663
9690
|
};
|
|
8664
9691
|
}
|
|
8665
|
-
await toolExecutionQueries.approve(
|
|
9692
|
+
await toolExecutionQueries.approve(exec8.id);
|
|
8666
9693
|
await sessionQueries.updateStatus(this.session.id, "active");
|
|
8667
9694
|
try {
|
|
8668
9695
|
const result = await originalTool.execute(input, toolOptions);
|
|
8669
|
-
await toolExecutionQueries.complete(
|
|
9696
|
+
await toolExecutionQueries.complete(exec8.id, result);
|
|
8670
9697
|
return result;
|
|
8671
9698
|
} catch (error) {
|
|
8672
|
-
await toolExecutionQueries.complete(
|
|
9699
|
+
await toolExecutionQueries.complete(exec8.id, null, error.message);
|
|
8673
9700
|
throw error;
|
|
8674
9701
|
}
|
|
8675
9702
|
}
|
|
@@ -8770,18 +9797,20 @@ function cleanupPendingInputs() {
|
|
|
8770
9797
|
}
|
|
8771
9798
|
}
|
|
8772
9799
|
}
|
|
8773
|
-
var createSessionSchema =
|
|
8774
|
-
name:
|
|
8775
|
-
workingDirectory:
|
|
8776
|
-
model:
|
|
8777
|
-
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()
|
|
8778
9807
|
});
|
|
8779
|
-
var paginationQuerySchema =
|
|
8780
|
-
limit:
|
|
8781
|
-
offset:
|
|
9808
|
+
var paginationQuerySchema = z16.object({
|
|
9809
|
+
limit: z16.string().optional(),
|
|
9810
|
+
offset: z16.string().optional()
|
|
8782
9811
|
});
|
|
8783
|
-
var messagesQuerySchema =
|
|
8784
|
-
limit:
|
|
9812
|
+
var messagesQuerySchema = z16.object({
|
|
9813
|
+
limit: z16.string().optional()
|
|
8785
9814
|
});
|
|
8786
9815
|
sessions.get(
|
|
8787
9816
|
"/",
|
|
@@ -8819,11 +9848,20 @@ sessions.post(
|
|
|
8819
9848
|
async (c) => {
|
|
8820
9849
|
const body = c.req.valid("json");
|
|
8821
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
|
+
};
|
|
8822
9860
|
const agent = await Agent.create({
|
|
8823
9861
|
name: body.name,
|
|
8824
9862
|
workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
|
|
8825
9863
|
model: body.model || config.defaultModel,
|
|
8826
|
-
sessionConfig:
|
|
9864
|
+
sessionConfig: Object.keys(mergedConfig).length > 0 ? mergedConfig : void 0
|
|
8827
9865
|
});
|
|
8828
9866
|
const session = agent.getSession();
|
|
8829
9867
|
return c.json({
|
|
@@ -8920,10 +9958,10 @@ sessions.get("/:id/tools", async (c) => {
|
|
|
8920
9958
|
count: executions.length
|
|
8921
9959
|
});
|
|
8922
9960
|
});
|
|
8923
|
-
var updateSessionSchema =
|
|
8924
|
-
model:
|
|
8925
|
-
name:
|
|
8926
|
-
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()
|
|
8927
9965
|
});
|
|
8928
9966
|
sessions.patch(
|
|
8929
9967
|
"/:id",
|
|
@@ -8993,8 +10031,8 @@ sessions.post("/:id/clear", async (c) => {
|
|
|
8993
10031
|
await agent.clearContext();
|
|
8994
10032
|
return c.json({ success: true, sessionId: id });
|
|
8995
10033
|
});
|
|
8996
|
-
var pendingInputSchema =
|
|
8997
|
-
text:
|
|
10034
|
+
var pendingInputSchema = z16.object({
|
|
10035
|
+
text: z16.string()
|
|
8998
10036
|
});
|
|
8999
10037
|
sessions.post(
|
|
9000
10038
|
"/:id/pending-input",
|
|
@@ -9025,13 +10063,13 @@ sessions.get("/:id/pending-input", async (c) => {
|
|
|
9025
10063
|
createdAt: pending.createdAt.toISOString()
|
|
9026
10064
|
});
|
|
9027
10065
|
});
|
|
9028
|
-
var devtoolsContextSchema =
|
|
9029
|
-
url:
|
|
9030
|
-
path:
|
|
9031
|
-
pageName:
|
|
9032
|
-
screenWidth:
|
|
9033
|
-
screenHeight:
|
|
9034
|
-
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()
|
|
9035
10073
|
});
|
|
9036
10074
|
sessions.post(
|
|
9037
10075
|
"/:id/devtools-context",
|
|
@@ -9217,12 +10255,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
|
|
|
9217
10255
|
});
|
|
9218
10256
|
function getAttachmentsDir(sessionId) {
|
|
9219
10257
|
const appDataDir = getAppDataDirectory();
|
|
9220
|
-
return
|
|
10258
|
+
return join10(appDataDir, "attachments", sessionId);
|
|
9221
10259
|
}
|
|
9222
10260
|
function ensureAttachmentsDir(sessionId) {
|
|
9223
10261
|
const dir = getAttachmentsDir(sessionId);
|
|
9224
|
-
if (!
|
|
9225
|
-
|
|
10262
|
+
if (!existsSync16(dir)) {
|
|
10263
|
+
mkdirSync6(dir, { recursive: true });
|
|
9226
10264
|
}
|
|
9227
10265
|
return dir;
|
|
9228
10266
|
}
|
|
@@ -9233,12 +10271,12 @@ sessions.get("/:id/attachments", async (c) => {
|
|
|
9233
10271
|
return c.json({ error: "Session not found" }, 404);
|
|
9234
10272
|
}
|
|
9235
10273
|
const dir = getAttachmentsDir(sessionId);
|
|
9236
|
-
if (!
|
|
10274
|
+
if (!existsSync16(dir)) {
|
|
9237
10275
|
return c.json({ sessionId, attachments: [], count: 0 });
|
|
9238
10276
|
}
|
|
9239
10277
|
const files = readdirSync2(dir);
|
|
9240
10278
|
const attachments = files.map((filename) => {
|
|
9241
|
-
const filePath =
|
|
10279
|
+
const filePath = join10(dir, filename);
|
|
9242
10280
|
const stats = statSync2(filePath);
|
|
9243
10281
|
return {
|
|
9244
10282
|
id: filename.split("_")[0],
|
|
@@ -9270,10 +10308,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
9270
10308
|
return c.json({ error: "No file provided" }, 400);
|
|
9271
10309
|
}
|
|
9272
10310
|
const dir = ensureAttachmentsDir(sessionId);
|
|
9273
|
-
const id =
|
|
10311
|
+
const id = nanoid6(10);
|
|
9274
10312
|
const ext = extname8(file.name) || "";
|
|
9275
10313
|
const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
9276
|
-
const filePath =
|
|
10314
|
+
const filePath = join10(dir, safeFilename);
|
|
9277
10315
|
const arrayBuffer = await file.arrayBuffer();
|
|
9278
10316
|
writeFileSync3(filePath, Buffer.from(arrayBuffer));
|
|
9279
10317
|
return c.json({
|
|
@@ -9296,10 +10334,10 @@ sessions.post("/:id/attachments", async (c) => {
|
|
|
9296
10334
|
return c.json({ error: "Missing filename or data" }, 400);
|
|
9297
10335
|
}
|
|
9298
10336
|
const dir = ensureAttachmentsDir(sessionId);
|
|
9299
|
-
const id =
|
|
10337
|
+
const id = nanoid6(10);
|
|
9300
10338
|
const ext = extname8(body.filename) || "";
|
|
9301
10339
|
const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
9302
|
-
const filePath =
|
|
10340
|
+
const filePath = join10(dir, safeFilename);
|
|
9303
10341
|
let base64Data = body.data;
|
|
9304
10342
|
if (base64Data.includes(",")) {
|
|
9305
10343
|
base64Data = base64Data.split(",")[1];
|
|
@@ -9328,7 +10366,7 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
9328
10366
|
return c.json({ error: "Session not found" }, 404);
|
|
9329
10367
|
}
|
|
9330
10368
|
const dir = getAttachmentsDir(sessionId);
|
|
9331
|
-
if (!
|
|
10369
|
+
if (!existsSync16(dir)) {
|
|
9332
10370
|
return c.json({ error: "Attachment not found" }, 404);
|
|
9333
10371
|
}
|
|
9334
10372
|
const files = readdirSync2(dir);
|
|
@@ -9336,14 +10374,14 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
|
|
|
9336
10374
|
if (!file) {
|
|
9337
10375
|
return c.json({ error: "Attachment not found" }, 404);
|
|
9338
10376
|
}
|
|
9339
|
-
const filePath =
|
|
9340
|
-
|
|
10377
|
+
const filePath = join10(dir, file);
|
|
10378
|
+
unlinkSync3(filePath);
|
|
9341
10379
|
return c.json({ success: true, id: attachmentId });
|
|
9342
10380
|
});
|
|
9343
|
-
var filesQuerySchema =
|
|
9344
|
-
query:
|
|
10381
|
+
var filesQuerySchema = z16.object({
|
|
10382
|
+
query: z16.string().optional(),
|
|
9345
10383
|
// Filter query (e.g., "src/com" to match "src/components")
|
|
9346
|
-
limit:
|
|
10384
|
+
limit: z16.string().optional()
|
|
9347
10385
|
// Max results (default 50)
|
|
9348
10386
|
});
|
|
9349
10387
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -9419,7 +10457,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
|
|
|
9419
10457
|
const entries = await readdir6(currentDir, { withFileTypes: true });
|
|
9420
10458
|
for (const entry of entries) {
|
|
9421
10459
|
if (results.length >= limit * 2) break;
|
|
9422
|
-
const fullPath =
|
|
10460
|
+
const fullPath = join10(currentDir, entry.name);
|
|
9423
10461
|
const relativePath = relative9(baseDir, fullPath);
|
|
9424
10462
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
|
|
9425
10463
|
continue;
|
|
@@ -9467,7 +10505,7 @@ sessions.get(
|
|
|
9467
10505
|
return c.json({ error: "Session not found" }, 404);
|
|
9468
10506
|
}
|
|
9469
10507
|
const workingDirectory = session.workingDirectory;
|
|
9470
|
-
if (!
|
|
10508
|
+
if (!existsSync16(workingDirectory)) {
|
|
9471
10509
|
return c.json({
|
|
9472
10510
|
sessionId,
|
|
9473
10511
|
workingDirectory,
|
|
@@ -9577,9 +10615,9 @@ sessions.get("/:id/browser-recording", async (c) => {
|
|
|
9577
10615
|
init_db();
|
|
9578
10616
|
import { Hono as Hono2 } from "hono";
|
|
9579
10617
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
9580
|
-
import { z as
|
|
9581
|
-
import { existsSync as
|
|
9582
|
-
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";
|
|
9583
10621
|
init_config();
|
|
9584
10622
|
|
|
9585
10623
|
// src/server/resumable-stream.ts
|
|
@@ -9666,7 +10704,7 @@ var streamContext = createResumableStreamContext({
|
|
|
9666
10704
|
});
|
|
9667
10705
|
|
|
9668
10706
|
// src/server/routes/agents.ts
|
|
9669
|
-
import { nanoid as
|
|
10707
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
9670
10708
|
init_stream_proxy();
|
|
9671
10709
|
init_recorder();
|
|
9672
10710
|
init_remote();
|
|
@@ -9757,40 +10795,40 @@ function enrichPromptWithDevtoolsContext(sessionId, prompt) {
|
|
|
9757
10795
|
${prompt}`;
|
|
9758
10796
|
}
|
|
9759
10797
|
var agents = new Hono2();
|
|
9760
|
-
var attachmentSchema =
|
|
9761
|
-
type:
|
|
9762
|
-
data:
|
|
10798
|
+
var attachmentSchema = z17.object({
|
|
10799
|
+
type: z17.enum(["image", "file"]),
|
|
10800
|
+
data: z17.string(),
|
|
9763
10801
|
// base64 data URL or raw base64
|
|
9764
|
-
mediaType:
|
|
9765
|
-
filename:
|
|
10802
|
+
mediaType: z17.string().optional(),
|
|
10803
|
+
filename: z17.string().optional()
|
|
9766
10804
|
});
|
|
9767
|
-
var runPromptSchema =
|
|
9768
|
-
prompt:
|
|
10805
|
+
var runPromptSchema = z17.object({
|
|
10806
|
+
prompt: z17.string(),
|
|
9769
10807
|
// Can be empty if attachments are provided
|
|
9770
|
-
attachments:
|
|
10808
|
+
attachments: z17.array(attachmentSchema).optional()
|
|
9771
10809
|
}).refine(
|
|
9772
10810
|
(data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
|
|
9773
10811
|
{ message: "Either prompt or attachments must be provided" }
|
|
9774
10812
|
);
|
|
9775
|
-
var quickStartSchema =
|
|
9776
|
-
prompt:
|
|
9777
|
-
name:
|
|
9778
|
-
workingDirectory:
|
|
9779
|
-
model:
|
|
9780
|
-
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()
|
|
9781
10819
|
});
|
|
9782
|
-
var rejectSchema =
|
|
9783
|
-
reason:
|
|
10820
|
+
var rejectSchema = z17.object({
|
|
10821
|
+
reason: z17.string().optional()
|
|
9784
10822
|
}).optional();
|
|
9785
10823
|
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
9786
10824
|
function getAttachmentsDirectory(sessionId) {
|
|
9787
10825
|
const appDataDir = getAppDataDirectory();
|
|
9788
|
-
return
|
|
10826
|
+
return join11(appDataDir, "attachments", sessionId);
|
|
9789
10827
|
}
|
|
9790
10828
|
async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
9791
10829
|
const attachmentsDir = getAttachmentsDirectory(sessionId);
|
|
9792
|
-
if (!
|
|
9793
|
-
|
|
10830
|
+
if (!existsSync17(attachmentsDir)) {
|
|
10831
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
9794
10832
|
}
|
|
9795
10833
|
let filename = attachment.filename;
|
|
9796
10834
|
if (!filename) {
|
|
@@ -9808,7 +10846,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
|
|
|
9808
10846
|
attachment.mediaType = resized.mediaType;
|
|
9809
10847
|
attachment.data = buffer.toString("base64");
|
|
9810
10848
|
}
|
|
9811
|
-
const filePath =
|
|
10849
|
+
const filePath = join11(attachmentsDir, filename);
|
|
9812
10850
|
writeFileSync4(filePath, buffer);
|
|
9813
10851
|
return filePath;
|
|
9814
10852
|
}
|
|
@@ -9819,9 +10857,9 @@ function stripDataUrlPrefix2(data) {
|
|
|
9819
10857
|
}
|
|
9820
10858
|
return data;
|
|
9821
10859
|
}
|
|
9822
|
-
function getExtensionFromMediaType(mediaType,
|
|
10860
|
+
function getExtensionFromMediaType(mediaType, type2) {
|
|
9823
10861
|
if (!mediaType) {
|
|
9824
|
-
return
|
|
10862
|
+
return type2 === "image" ? ".png" : ".bin";
|
|
9825
10863
|
}
|
|
9826
10864
|
const mimeToExt = {
|
|
9827
10865
|
"image/png": ".png",
|
|
@@ -10225,7 +11263,7 @@ ${prompt}` });
|
|
|
10225
11263
|
userMessageContent = prompt;
|
|
10226
11264
|
}
|
|
10227
11265
|
await messageQueries.create(id, { role: "user", content: userMessageContent });
|
|
10228
|
-
const streamId = `stream_${id}_${
|
|
11266
|
+
const streamId = `stream_${id}_${nanoid7(10)}`;
|
|
10229
11267
|
console.log(`[STREAM] Creating stream ${streamId} for session ${id}`);
|
|
10230
11268
|
await activeStreamQueries.create(id, streamId);
|
|
10231
11269
|
const stream = await streamContext.resumableStream(
|
|
@@ -10430,7 +11468,7 @@ agents.post(
|
|
|
10430
11468
|
});
|
|
10431
11469
|
const session = agent.getSession();
|
|
10432
11470
|
const enrichedPrompt = enrichPromptWithDevtoolsContext(session.id, body.prompt);
|
|
10433
|
-
const streamId = `stream_${session.id}_${
|
|
11471
|
+
const streamId = `stream_${session.id}_${nanoid7(10)}`;
|
|
10434
11472
|
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
10435
11473
|
await activeStreamQueries.create(session.id, streamId);
|
|
10436
11474
|
const createQuickStreamProducer = () => {
|
|
@@ -10697,23 +11735,23 @@ agents.post(
|
|
|
10697
11735
|
});
|
|
10698
11736
|
}
|
|
10699
11737
|
);
|
|
10700
|
-
var browserInputSchema =
|
|
10701
|
-
type:
|
|
10702
|
-
eventType:
|
|
10703
|
-
x:
|
|
10704
|
-
y:
|
|
10705
|
-
button:
|
|
10706
|
-
clickCount:
|
|
10707
|
-
deltaX:
|
|
10708
|
-
deltaY:
|
|
10709
|
-
key:
|
|
10710
|
-
code:
|
|
10711
|
-
text:
|
|
10712
|
-
modifiers:
|
|
10713
|
-
touchPoints:
|
|
10714
|
-
x:
|
|
10715
|
-
y:
|
|
10716
|
-
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()
|
|
10717
11755
|
})).optional()
|
|
10718
11756
|
});
|
|
10719
11757
|
agents.post(
|
|
@@ -10746,29 +11784,30 @@ agents.get("/:id/browser-stream", async (c) => {
|
|
|
10746
11784
|
|
|
10747
11785
|
// src/server/routes/health.ts
|
|
10748
11786
|
init_config();
|
|
11787
|
+
init_heartbeat();
|
|
10749
11788
|
import { Hono as Hono3 } from "hono";
|
|
10750
11789
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
10751
|
-
import { z as
|
|
10752
|
-
import { readFileSync as
|
|
11790
|
+
import { z as z18 } from "zod";
|
|
11791
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
10753
11792
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10754
|
-
import { dirname as dirname6, join as
|
|
11793
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
10755
11794
|
var __filename = fileURLToPath3(import.meta.url);
|
|
10756
11795
|
var __dirname = dirname6(__filename);
|
|
10757
11796
|
var possiblePaths = [
|
|
10758
|
-
|
|
11797
|
+
join12(__dirname, "../package.json"),
|
|
10759
11798
|
// From dist/server -> dist/../package.json
|
|
10760
|
-
|
|
11799
|
+
join12(__dirname, "../../package.json"),
|
|
10761
11800
|
// From dist/server (if nested differently)
|
|
10762
|
-
|
|
11801
|
+
join12(__dirname, "../../../package.json"),
|
|
10763
11802
|
// From src/server/routes (development)
|
|
10764
|
-
|
|
11803
|
+
join12(process.cwd(), "package.json")
|
|
10765
11804
|
// From current working directory
|
|
10766
11805
|
];
|
|
10767
11806
|
var currentVersion = "0.0.0";
|
|
10768
11807
|
var packageName = "sparkecoder";
|
|
10769
11808
|
for (const packageJsonPath of possiblePaths) {
|
|
10770
11809
|
try {
|
|
10771
|
-
const packageJson = JSON.parse(
|
|
11810
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
10772
11811
|
if (packageJson.name === "sparkecoder") {
|
|
10773
11812
|
currentVersion = packageJson.version || "0.0.0";
|
|
10774
11813
|
packageName = packageJson.name || "sparkecoder";
|
|
@@ -10783,11 +11822,17 @@ health.get("/", async (c) => {
|
|
|
10783
11822
|
const apiKeyStatus = getApiKeyStatus();
|
|
10784
11823
|
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
10785
11824
|
const hasApiKey = gatewayKey?.configured ?? false;
|
|
11825
|
+
let hwid;
|
|
11826
|
+
try {
|
|
11827
|
+
hwid = getHardwareIdCached();
|
|
11828
|
+
} catch {
|
|
11829
|
+
}
|
|
10786
11830
|
return c.json({
|
|
10787
11831
|
status: "ok",
|
|
10788
11832
|
version: currentVersion,
|
|
10789
11833
|
uptime: process.uptime(),
|
|
10790
11834
|
apiKeyConfigured: hasApiKey,
|
|
11835
|
+
hwid,
|
|
10791
11836
|
config: {
|
|
10792
11837
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
10793
11838
|
defaultModel: config.defaultModel,
|
|
@@ -10858,9 +11903,9 @@ health.get("/api-keys", async (c) => {
|
|
|
10858
11903
|
supportedProviders: SUPPORTED_PROVIDERS
|
|
10859
11904
|
});
|
|
10860
11905
|
});
|
|
10861
|
-
var setApiKeySchema =
|
|
10862
|
-
provider:
|
|
10863
|
-
apiKey:
|
|
11906
|
+
var setApiKeySchema = z18.object({
|
|
11907
|
+
provider: z18.string(),
|
|
11908
|
+
apiKey: z18.string().min(1)
|
|
10864
11909
|
});
|
|
10865
11910
|
health.post(
|
|
10866
11911
|
"/api-keys",
|
|
@@ -10899,13 +11944,13 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
10899
11944
|
// src/server/routes/terminals.ts
|
|
10900
11945
|
import { Hono as Hono4 } from "hono";
|
|
10901
11946
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
10902
|
-
import { z as
|
|
11947
|
+
import { z as z19 } from "zod";
|
|
10903
11948
|
init_db();
|
|
10904
11949
|
var terminals = new Hono4();
|
|
10905
|
-
var spawnSchema =
|
|
10906
|
-
command:
|
|
10907
|
-
cwd:
|
|
10908
|
-
name:
|
|
11950
|
+
var spawnSchema = z19.object({
|
|
11951
|
+
command: z19.string(),
|
|
11952
|
+
cwd: z19.string().optional(),
|
|
11953
|
+
name: z19.string().optional()
|
|
10909
11954
|
});
|
|
10910
11955
|
terminals.post(
|
|
10911
11956
|
"/:sessionId/terminals",
|
|
@@ -10986,8 +12031,8 @@ terminals.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
|
10986
12031
|
// We don't track exit codes in tmux mode
|
|
10987
12032
|
});
|
|
10988
12033
|
});
|
|
10989
|
-
var logsQuerySchema =
|
|
10990
|
-
tail:
|
|
12034
|
+
var logsQuerySchema = z19.object({
|
|
12035
|
+
tail: z19.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
10991
12036
|
});
|
|
10992
12037
|
terminals.get(
|
|
10993
12038
|
"/:sessionId/terminals/:terminalId/logs",
|
|
@@ -11011,8 +12056,8 @@ terminals.get(
|
|
|
11011
12056
|
});
|
|
11012
12057
|
}
|
|
11013
12058
|
);
|
|
11014
|
-
var killSchema =
|
|
11015
|
-
signal:
|
|
12059
|
+
var killSchema = z19.object({
|
|
12060
|
+
signal: z19.enum(["SIGTERM", "SIGKILL"]).optional()
|
|
11016
12061
|
});
|
|
11017
12062
|
terminals.post(
|
|
11018
12063
|
"/:sessionId/terminals/:terminalId/kill",
|
|
@@ -11026,8 +12071,8 @@ terminals.post(
|
|
|
11026
12071
|
return c.json({ success: true, message: "Terminal killed" });
|
|
11027
12072
|
}
|
|
11028
12073
|
);
|
|
11029
|
-
var writeSchema =
|
|
11030
|
-
input:
|
|
12074
|
+
var writeSchema = z19.object({
|
|
12075
|
+
input: z19.string()
|
|
11031
12076
|
});
|
|
11032
12077
|
terminals.post(
|
|
11033
12078
|
"/:sessionId/terminals/:terminalId/write",
|
|
@@ -11212,20 +12257,20 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
11212
12257
|
init_db();
|
|
11213
12258
|
import { Hono as Hono5 } from "hono";
|
|
11214
12259
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
11215
|
-
import { z as
|
|
11216
|
-
import { nanoid as
|
|
12260
|
+
import { z as z20 } from "zod";
|
|
12261
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
11217
12262
|
init_config();
|
|
11218
12263
|
var tasks = new Hono5();
|
|
11219
12264
|
var taskAbortControllers = /* @__PURE__ */ new Map();
|
|
11220
|
-
var createTaskSchema =
|
|
11221
|
-
prompt:
|
|
11222
|
-
outputSchema:
|
|
11223
|
-
webhookUrl:
|
|
11224
|
-
model:
|
|
11225
|
-
workingDirectory:
|
|
11226
|
-
name:
|
|
11227
|
-
maxIterations:
|
|
11228
|
-
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()
|
|
11229
12274
|
});
|
|
11230
12275
|
tasks.post(
|
|
11231
12276
|
"/",
|
|
@@ -11287,7 +12332,7 @@ tasks.post(
|
|
|
11287
12332
|
const taskId = agent.sessionId;
|
|
11288
12333
|
const abortController = new AbortController();
|
|
11289
12334
|
taskAbortControllers.set(taskId, abortController);
|
|
11290
|
-
const streamId = `stream_${taskId}_${
|
|
12335
|
+
const streamId = `stream_${taskId}_${nanoid8(10)}`;
|
|
11291
12336
|
await activeStreamQueries.create(taskId, streamId);
|
|
11292
12337
|
const taskStreamProducer = () => {
|
|
11293
12338
|
const { readable, writable } = new TransformStream();
|
|
@@ -11437,17 +12482,584 @@ tasks.post("/:id/cancel", async (c) => {
|
|
|
11437
12482
|
});
|
|
11438
12483
|
var tasks_default = tasks;
|
|
11439
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
|
+
|
|
11440
13052
|
// src/server/index.ts
|
|
11441
13053
|
init_config();
|
|
11442
13054
|
init_db();
|
|
11443
13055
|
|
|
11444
13056
|
// src/utils/dependencies.ts
|
|
11445
|
-
import { exec as
|
|
11446
|
-
import { promisify as
|
|
11447
|
-
import { platform as
|
|
11448
|
-
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);
|
|
11449
13061
|
function getInstallInstructions() {
|
|
11450
|
-
const os2 =
|
|
13062
|
+
const os2 = platform4();
|
|
11451
13063
|
if (os2 === "darwin") {
|
|
11452
13064
|
return `
|
|
11453
13065
|
Install tmux on macOS:
|
|
@@ -11478,7 +13090,7 @@ Install tmux:
|
|
|
11478
13090
|
}
|
|
11479
13091
|
async function checkTmux() {
|
|
11480
13092
|
try {
|
|
11481
|
-
const { stdout } = await
|
|
13093
|
+
const { stdout } = await execAsync7("tmux -V", { timeout: 5e3 });
|
|
11482
13094
|
const version = stdout.trim();
|
|
11483
13095
|
return {
|
|
11484
13096
|
available: true,
|
|
@@ -11519,7 +13131,7 @@ async function checkDependencies(options = {}) {
|
|
|
11519
13131
|
}
|
|
11520
13132
|
async function checkAgentBrowser() {
|
|
11521
13133
|
try {
|
|
11522
|
-
const { stdout } = await
|
|
13134
|
+
const { stdout } = await execAsync7("agent-browser --version", { timeout: 1e4 });
|
|
11523
13135
|
const version = stdout.trim();
|
|
11524
13136
|
return { available: true, version };
|
|
11525
13137
|
} catch {
|
|
@@ -11535,12 +13147,12 @@ async function tryInstallAgentBrowser(options = {}) {
|
|
|
11535
13147
|
if (!options.quiet) {
|
|
11536
13148
|
console.log("\u{1F4E6} Installing agent-browser globally...");
|
|
11537
13149
|
}
|
|
11538
|
-
await
|
|
13150
|
+
await execAsync7("npm install -g agent-browser", { timeout: 12e4 });
|
|
11539
13151
|
try {
|
|
11540
13152
|
if (!options.quiet) {
|
|
11541
13153
|
console.log("\u{1F4E6} Installing Chromium for browser automation...");
|
|
11542
13154
|
}
|
|
11543
|
-
await
|
|
13155
|
+
await execAsync7("agent-browser install", { timeout: 12e4 });
|
|
11544
13156
|
} catch {
|
|
11545
13157
|
}
|
|
11546
13158
|
if (!options.quiet) {
|
|
@@ -11555,25 +13167,25 @@ async function tryInstallAgentBrowser(options = {}) {
|
|
|
11555
13167
|
}
|
|
11556
13168
|
}
|
|
11557
13169
|
async function tryAutoInstallTmux() {
|
|
11558
|
-
const os2 =
|
|
13170
|
+
const os2 = platform4();
|
|
11559
13171
|
try {
|
|
11560
13172
|
if (os2 === "darwin") {
|
|
11561
13173
|
try {
|
|
11562
|
-
await
|
|
13174
|
+
await execAsync7("which brew", { timeout: 5e3 });
|
|
11563
13175
|
} catch {
|
|
11564
13176
|
return false;
|
|
11565
13177
|
}
|
|
11566
13178
|
console.log("\u{1F4E6} Installing tmux via Homebrew...");
|
|
11567
|
-
await
|
|
13179
|
+
await execAsync7("brew install tmux", { timeout: 3e5 });
|
|
11568
13180
|
console.log("\u2705 tmux installed successfully");
|
|
11569
13181
|
return true;
|
|
11570
13182
|
}
|
|
11571
13183
|
if (os2 === "linux") {
|
|
11572
13184
|
try {
|
|
11573
|
-
await
|
|
13185
|
+
await execAsync7("which apt-get", { timeout: 5e3 });
|
|
11574
13186
|
console.log("\u{1F4E6} Installing tmux via apt-get...");
|
|
11575
13187
|
console.log(" (This may require sudo password)");
|
|
11576
|
-
await
|
|
13188
|
+
await execAsync7("sudo apt-get update && sudo apt-get install -y tmux", {
|
|
11577
13189
|
timeout: 3e5
|
|
11578
13190
|
});
|
|
11579
13191
|
console.log("\u2705 tmux installed successfully");
|
|
@@ -11581,9 +13193,9 @@ async function tryAutoInstallTmux() {
|
|
|
11581
13193
|
} catch {
|
|
11582
13194
|
}
|
|
11583
13195
|
try {
|
|
11584
|
-
await
|
|
13196
|
+
await execAsync7("which dnf", { timeout: 5e3 });
|
|
11585
13197
|
console.log("\u{1F4E6} Installing tmux via dnf...");
|
|
11586
|
-
await
|
|
13198
|
+
await execAsync7("sudo dnf install -y tmux", { timeout: 3e5 });
|
|
11587
13199
|
console.log("\u2705 tmux installed successfully");
|
|
11588
13200
|
return true;
|
|
11589
13201
|
} catch {
|
|
@@ -11623,11 +13235,11 @@ function getWebDirectory() {
|
|
|
11623
13235
|
try {
|
|
11624
13236
|
const currentDir = dirname7(fileURLToPath4(import.meta.url));
|
|
11625
13237
|
const webDir = resolve10(currentDir, "..", "web");
|
|
11626
|
-
if (
|
|
13238
|
+
if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
|
|
11627
13239
|
return webDir;
|
|
11628
13240
|
}
|
|
11629
13241
|
const altWebDir = resolve10(currentDir, "..", "..", "web");
|
|
11630
|
-
if (
|
|
13242
|
+
if (existsSync19(altWebDir) && existsSync19(join13(altWebDir, "package.json"))) {
|
|
11631
13243
|
return altWebDir;
|
|
11632
13244
|
}
|
|
11633
13245
|
return null;
|
|
@@ -11685,23 +13297,23 @@ async function findWebPort(preferredPort) {
|
|
|
11685
13297
|
return { port: preferredPort, alreadyRunning: false };
|
|
11686
13298
|
}
|
|
11687
13299
|
function hasProductionBuild(webDir) {
|
|
11688
|
-
const buildIdPath =
|
|
11689
|
-
return
|
|
13300
|
+
const buildIdPath = join13(webDir, ".next", "BUILD_ID");
|
|
13301
|
+
return existsSync19(buildIdPath);
|
|
11690
13302
|
}
|
|
11691
13303
|
function hasSourceFiles(webDir) {
|
|
11692
|
-
const appDir =
|
|
11693
|
-
const pagesDir =
|
|
11694
|
-
const rootAppDir =
|
|
11695
|
-
const rootPagesDir =
|
|
11696
|
-
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);
|
|
11697
13309
|
}
|
|
11698
13310
|
function getStandaloneServerPath(webDir) {
|
|
11699
13311
|
const possiblePaths2 = [
|
|
11700
|
-
|
|
11701
|
-
|
|
13312
|
+
join13(webDir, ".next", "standalone", "server.js"),
|
|
13313
|
+
join13(webDir, ".next", "standalone", "web", "server.js")
|
|
11702
13314
|
];
|
|
11703
13315
|
for (const serverPath of possiblePaths2) {
|
|
11704
|
-
if (
|
|
13316
|
+
if (existsSync19(serverPath)) {
|
|
11705
13317
|
return serverPath;
|
|
11706
13318
|
}
|
|
11707
13319
|
}
|
|
@@ -11741,13 +13353,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
11741
13353
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
11742
13354
|
return { process: null, port: actualPort };
|
|
11743
13355
|
}
|
|
11744
|
-
const usePnpm =
|
|
11745
|
-
const useNpm = !usePnpm &&
|
|
13356
|
+
const usePnpm = existsSync19(join13(webDir, "pnpm-lock.yaml"));
|
|
13357
|
+
const useNpm = !usePnpm && existsSync19(join13(webDir, "package-lock.json"));
|
|
11746
13358
|
const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
|
|
11747
|
-
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...
|
|
13359
|
+
const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv2 } = process.env;
|
|
11748
13360
|
const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
|
|
11749
13361
|
const runtimeConfig = { apiBaseUrl: apiUrl };
|
|
11750
|
-
const runtimeConfigPath =
|
|
13362
|
+
const runtimeConfigPath = join13(webDir, "runtime-config.json");
|
|
11751
13363
|
try {
|
|
11752
13364
|
writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
11753
13365
|
if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
|
|
@@ -11755,7 +13367,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
|
|
|
11755
13367
|
if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
|
|
11756
13368
|
}
|
|
11757
13369
|
const webEnv = {
|
|
11758
|
-
...
|
|
13370
|
+
...cleanEnv2,
|
|
11759
13371
|
PORT: String(actualPort)
|
|
11760
13372
|
// Next.js respects PORT env var
|
|
11761
13373
|
};
|
|
@@ -11869,12 +13481,28 @@ function stopWebUI() {
|
|
|
11869
13481
|
}
|
|
11870
13482
|
}
|
|
11871
13483
|
async function createApp(options = {}) {
|
|
11872
|
-
const app = new
|
|
13484
|
+
const app = new Hono7();
|
|
11873
13485
|
app.use("*", cors({
|
|
11874
13486
|
origin: "*",
|
|
11875
13487
|
// Allow all origins
|
|
11876
13488
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
11877
|
-
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
|
+
],
|
|
11878
13506
|
exposeHeaders: ["X-Stream-Id", "x-stream-id"],
|
|
11879
13507
|
maxAge: 86400
|
|
11880
13508
|
// 24 hours
|
|
@@ -11882,12 +13510,15 @@ async function createApp(options = {}) {
|
|
|
11882
13510
|
if (!options.quiet) {
|
|
11883
13511
|
app.use("*", logger());
|
|
11884
13512
|
}
|
|
13513
|
+
app.use("*", hwidMiddleware());
|
|
13514
|
+
app.use("*", signatureMiddleware());
|
|
11885
13515
|
app.route("/health", health);
|
|
11886
13516
|
app.route("/sessions", sessions);
|
|
11887
13517
|
app.route("/agents", agents);
|
|
11888
13518
|
app.route("/sessions", terminals);
|
|
11889
13519
|
app.route("/terminals", terminals);
|
|
11890
13520
|
app.route("/tasks", tasks_default);
|
|
13521
|
+
app.route("/system", system);
|
|
11891
13522
|
app.get("/openapi.json", async (c) => {
|
|
11892
13523
|
return c.json(generateOpenAPISpec());
|
|
11893
13524
|
});
|
|
@@ -11940,8 +13571,8 @@ async function startServer(options = {}) {
|
|
|
11940
13571
|
if (options.workingDirectory) {
|
|
11941
13572
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
11942
13573
|
}
|
|
11943
|
-
if (!
|
|
11944
|
-
|
|
13574
|
+
if (!existsSync19(config.resolvedWorkingDirectory)) {
|
|
13575
|
+
mkdirSync8(config.resolvedWorkingDirectory, { recursive: true });
|
|
11945
13576
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
11946
13577
|
}
|
|
11947
13578
|
if (!config.resolvedRemoteServer.url) {
|
|
@@ -11976,6 +13607,15 @@ async function startServer(options = {}) {
|
|
|
11976
13607
|
port,
|
|
11977
13608
|
hostname: host
|
|
11978
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
|
+
}
|
|
11979
13619
|
let webPort;
|
|
11980
13620
|
let webStarted;
|
|
11981
13621
|
if (options.webUI !== false) {
|
|
@@ -12457,8 +14097,8 @@ function generateOpenAPISpec() {
|
|
|
12457
14097
|
init_config();
|
|
12458
14098
|
init_semantic();
|
|
12459
14099
|
init_db();
|
|
12460
|
-
import { writeFileSync as writeFileSync6, readFileSync as
|
|
12461
|
-
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";
|
|
12462
14102
|
async function apiRequest(baseUrl, path, options = {}) {
|
|
12463
14103
|
const url = `${baseUrl}${path}`;
|
|
12464
14104
|
const init = {
|
|
@@ -13021,10 +14661,26 @@ Unexpected error: ${outerError.message}`));
|
|
|
13021
14661
|
}
|
|
13022
14662
|
}
|
|
13023
14663
|
var program = new Command();
|
|
13024
|
-
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
|
+
}
|
|
13025
14669
|
await runChat(options);
|
|
13026
14670
|
});
|
|
13027
|
-
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
|
+
}
|
|
13028
14684
|
await ensureDependencies({ quiet: false });
|
|
13029
14685
|
const spinner = ora("Starting SparkECoder server...").start();
|
|
13030
14686
|
try {
|
|
@@ -13047,6 +14703,8 @@ program.command("server").description("Start the SparkECoder server (API + Web U
|
|
|
13047
14703
|
console.log("");
|
|
13048
14704
|
console.log(chalk.dim(` API: http://${host}:${port}`));
|
|
13049
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));
|
|
13050
14708
|
console.log("");
|
|
13051
14709
|
console.log(chalk.dim("Press Ctrl+C to stop"));
|
|
13052
14710
|
const cleanup2 = () => {
|
|
@@ -13061,7 +14719,13 @@ program.command("server").description("Start the SparkECoder server (API + Web U
|
|
|
13061
14719
|
process.exit(1);
|
|
13062
14720
|
}
|
|
13063
14721
|
});
|
|
13064
|
-
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
|
+
}
|
|
13065
14729
|
await runChat(options);
|
|
13066
14730
|
});
|
|
13067
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) => {
|
|
@@ -13093,8 +14757,8 @@ program.command("task").description("Run an autonomous task that completes witho
|
|
|
13093
14757
|
let outputSchema;
|
|
13094
14758
|
try {
|
|
13095
14759
|
const schemaStr = options.schema;
|
|
13096
|
-
if (
|
|
13097
|
-
outputSchema = JSON.parse(
|
|
14760
|
+
if (existsSync20(schemaStr)) {
|
|
14761
|
+
outputSchema = JSON.parse(readFileSync12(schemaStr, "utf-8"));
|
|
13098
14762
|
} else {
|
|
13099
14763
|
outputSchema = JSON.parse(schemaStr);
|
|
13100
14764
|
}
|
|
@@ -13161,13 +14825,13 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
|
|
|
13161
14825
|
let configLocation;
|
|
13162
14826
|
if (options.global) {
|
|
13163
14827
|
const appDataDir = ensureAppDataDirectory();
|
|
13164
|
-
configPath =
|
|
14828
|
+
configPath = join14(appDataDir, "sparkecoder.config.json");
|
|
13165
14829
|
configLocation = "global";
|
|
13166
14830
|
} else {
|
|
13167
14831
|
configPath = resolve11(process.cwd(), "sparkecoder.config.json");
|
|
13168
14832
|
configLocation = "local";
|
|
13169
14833
|
}
|
|
13170
|
-
if (
|
|
14834
|
+
if (existsSync20(configPath) && !options.force) {
|
|
13171
14835
|
console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
|
|
13172
14836
|
console.log(chalk.dim(` ${configPath}`));
|
|
13173
14837
|
return;
|
|
@@ -13489,5 +15153,112 @@ ${providerName} API Key:
|
|
|
13489
15153
|
process.exit(1);
|
|
13490
15154
|
}
|
|
13491
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
|
+
});
|
|
13492
15263
|
program.parse();
|
|
13493
15264
|
//# sourceMappingURL=cli.js.map
|