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.
Files changed (112) hide show
  1. package/dist/agent/index.d.ts +1 -1
  2. package/dist/agent/index.js +661 -39
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +1996 -225
  5. package/dist/cli.js.map +1 -1
  6. package/dist/{index-OhuTM4a0.d.ts → index-BvIissiB.d.ts} +9 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +1683 -199
  9. package/dist/index.js.map +1 -1
  10. package/dist/server/index.js +1683 -199
  11. package/dist/server/index.js.map +1 -1
  12. package/dist/skills/default/computer-use.md +150 -0
  13. package/dist/tools/index.d.ts +167 -1
  14. package/dist/tools/index.js +604 -10
  15. package/dist/tools/index.js.map +1 -1
  16. package/package.json +2 -1
  17. package/src/skills/default/computer-use.md +150 -0
  18. package/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  20. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  21. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  22. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  23. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  38. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  67. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
  75. package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ecd2bdca._.js → 2374f_317b1fef._.js} +1 -1
  85. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9adc1edb._.js → 2374f_37dd9702._.js} +1 -1
  86. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8dc0f9aa._.js → 2374f_4d44e4ed._.js} +1 -1
  87. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_cc6c6363._.js → 2374f_54ac917f._.js} +1 -1
  88. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_00f7fe07._.js → 2374f_86585101._.js} +1 -1
  89. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_369747ce._.js → 2374f_a383a4d9._.js} +1 -1
  90. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d58d0276._.js → 2374f_c59a35bb._.js} +1 -1
  91. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__25b25c9d._.js → [root-of-the-server]__9a826344._.js} +2 -2
  92. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  93. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  94. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  95. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  96. package/web/.next/standalone/web/.next/static/chunks/275e8268daf318b2.js +7 -0
  97. package/web/.next/standalone/web/.next/static/static/chunks/275e8268daf318b2.js +7 -0
  98. package/web/.next/standalone/web/src/app/embed/[id]/page.tsx +12 -0
  99. package/web/.next/standalone/web/src/lib/embed-bootstrap.ts +108 -0
  100. package/web/.next/static/chunks/275e8268daf318b2.js +7 -0
  101. package/web/.next/standalone/web/.next/static/chunks/5383c5717758f575.js +0 -7
  102. package/web/.next/standalone/web/.next/static/static/chunks/5383c5717758f575.js +0 -7
  103. package/web/.next/static/chunks/5383c5717758f575.js +0 -7
  104. /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
  105. /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
  106. /package/web/.next/standalone/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → static/uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
  107. /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
  108. /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
  109. /package/web/.next/standalone/web/.next/static/{static/Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_ssgManifest.js +0 -0
  110. /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_buildManifest.js +0 -0
  111. /package/web/.next/static/{Pt6kwIO6lniM7h7I5E6kk → uUaN7Xe5kF_pP6zhfaeYi}/_clientMiddlewareManifest.json +0 -0
  112. /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, type, name) {
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: type,
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: type,
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: readdirSync3 } = await import("fs");
2405
- const { join: join14, relative: relative10 } = await import("path");
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 = readdirSync3(currentDir, { withFileTypes: true });
2425
+ entries = readdirSync4(currentDir, { withFileTypes: true });
2411
2426
  } catch {
2412
2427
  return;
2413
2428
  }
2414
2429
  for (const entry of entries) {
2415
- const fullPath = join14(currentDir, entry.name);
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
- console.warn(`[WEBHOOK] ${event.type} to ${url} returned HTTP ${response.status}`);
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(`[WEBHOOK] ${event.type} to ${url} failed: ${reason}`);
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 exec5 } from "child_process";
3157
- import { promisify as promisify5 } from "util";
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 join8 } from "path";
3160
- import { tmpdir } from "os";
3161
- import { nanoid as nanoid3 } from "nanoid";
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 execAsync5("ffmpeg -version", { timeout: 5e3 });
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 execAsync5, FrameRecorder;
3638
+ var execAsync6, FrameRecorder;
3177
3639
  var init_recorder = __esm({
3178
3640
  "src/browser/recorder.ts"() {
3179
3641
  "use strict";
3180
- execAsync5 = promisify5(exec5);
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 = join8(tmpdir(), `sparkecoder-recording-${nanoid3(8)}`);
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 = join8(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
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 = join8(workDir, `recording_${this.sessionId}.mp4`);
3688
+ const outputPath = join9(workDir, `recording_${this.sessionId}.mp4`);
3227
3689
  const hasFfmpeg = await checkFfmpeg();
3228
3690
  if (hasFfmpeg) {
3229
- await execAsync5(
3230
- `ffmpeg -y -framerate ${clampedFps} -i "${join8(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
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(join8(workDir, f)).catch(() => {
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 Hono6 } from "hono";
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 existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
3277
- import { resolve as resolve10, dirname as dirname7, join as join12 } from "path";
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 z15 } from "zod";
3287
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
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 join9, basename as basename5, extname as extname8, relative as relative9 } from "path";
3290
- import { nanoid as nanoid5 } from "nanoid";
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 tool13,
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 z14 } from "zod";
3488
- import { nanoid as nanoid4 } from "nanoid";
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: exec7 } = await import("child_process");
4556
- const { promisify: promisify7 } = await import("util");
4557
- const execAsync7 = promisify7(exec7);
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 execAsync7(checkCmd);
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 platform3 = process.platform;
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 (platform3 === "win32") {
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 platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
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**: ${platform3} (${os.release()})
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 = (type, data) => {
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: join14, basename: basename6 } = await import("path");
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 : join14(this.session.workingDirectory, 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] = tool13({
9657
+ wrappedTools[name] = tool14({
8631
9658
  description: originalTool.description || "",
8632
- inputSchema: originalTool.inputSchema || z14.object({}),
9659
+ inputSchema: originalTool.inputSchema || z15.object({}),
8633
9660
  execute: async (input, toolOptions) => {
8634
- const toolCallId = toolOptions.toolCallId || nanoid4();
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 exec7 = await execution;
9679
+ const exec8 = await execution;
8653
9680
  if (!approved) {
8654
9681
  const reason = resolverData?.reason || "User rejected the tool execution";
8655
- await toolExecutionQueries.reject(exec7.id);
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(exec7.id);
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(exec7.id, result);
9696
+ await toolExecutionQueries.complete(exec8.id, result);
8670
9697
  return result;
8671
9698
  } catch (error) {
8672
- await toolExecutionQueries.complete(exec7.id, null, error.message);
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 = z15.object({
8774
- name: z15.string().optional(),
8775
- workingDirectory: z15.string().optional(),
8776
- model: z15.string().optional(),
8777
- toolApprovals: z15.record(z15.string(), z15.boolean()).optional()
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 = z15.object({
8780
- limit: z15.string().optional(),
8781
- offset: z15.string().optional()
9808
+ var paginationQuerySchema = z16.object({
9809
+ limit: z16.string().optional(),
9810
+ offset: z16.string().optional()
8782
9811
  });
8783
- var messagesQuerySchema = z15.object({
8784
- limit: z15.string().optional()
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: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
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 = z15.object({
8924
- model: z15.string().optional(),
8925
- name: z15.string().optional(),
8926
- toolApprovals: z15.record(z15.string(), z15.boolean()).optional()
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 = z15.object({
8997
- text: z15.string()
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 = z15.object({
9029
- url: z15.string(),
9030
- path: z15.string(),
9031
- pageName: z15.string().optional(),
9032
- screenWidth: z15.number().optional(),
9033
- screenHeight: z15.number().optional(),
9034
- devicePixelRatio: z15.number().optional()
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 join9(appDataDir, "attachments", sessionId);
10258
+ return join10(appDataDir, "attachments", sessionId);
9221
10259
  }
9222
10260
  function ensureAttachmentsDir(sessionId) {
9223
10261
  const dir = getAttachmentsDir(sessionId);
9224
- if (!existsSync15(dir)) {
9225
- mkdirSync5(dir, { recursive: true });
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 (!existsSync15(dir)) {
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 = join9(dir, filename);
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 = nanoid5(10);
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 = join9(dir, safeFilename);
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 = nanoid5(10);
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 = join9(dir, safeFilename);
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 (!existsSync15(dir)) {
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 = join9(dir, file);
9340
- unlinkSync2(filePath);
10377
+ const filePath = join10(dir, file);
10378
+ unlinkSync3(filePath);
9341
10379
  return c.json({ success: true, id: attachmentId });
9342
10380
  });
9343
- var filesQuerySchema = z15.object({
9344
- query: z15.string().optional(),
10381
+ var filesQuerySchema = z16.object({
10382
+ query: z16.string().optional(),
9345
10383
  // Filter query (e.g., "src/com" to match "src/components")
9346
- limit: z15.string().optional()
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 = join9(currentDir, entry.name);
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 (!existsSync15(workingDirectory)) {
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 z16 } from "zod";
9581
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
9582
- import { join as join10 } from "path";
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 nanoid6 } from "nanoid";
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 = z16.object({
9761
- type: z16.enum(["image", "file"]),
9762
- data: z16.string(),
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: z16.string().optional(),
9765
- filename: z16.string().optional()
10802
+ mediaType: z17.string().optional(),
10803
+ filename: z17.string().optional()
9766
10804
  });
9767
- var runPromptSchema = z16.object({
9768
- prompt: z16.string(),
10805
+ var runPromptSchema = z17.object({
10806
+ prompt: z17.string(),
9769
10807
  // Can be empty if attachments are provided
9770
- attachments: z16.array(attachmentSchema).optional()
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 = z16.object({
9776
- prompt: z16.string().min(1),
9777
- name: z16.string().optional(),
9778
- workingDirectory: z16.string().optional(),
9779
- model: z16.string().optional(),
9780
- toolApprovals: z16.record(z16.string(), z16.boolean()).optional()
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 = z16.object({
9783
- reason: z16.string().optional()
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 join10(appDataDir, "attachments", sessionId);
10826
+ return join11(appDataDir, "attachments", sessionId);
9789
10827
  }
9790
10828
  async function saveAttachmentToDisk(sessionId, attachment, index) {
9791
10829
  const attachmentsDir = getAttachmentsDirectory(sessionId);
9792
- if (!existsSync16(attachmentsDir)) {
9793
- mkdirSync6(attachmentsDir, { recursive: true });
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 = join10(attachmentsDir, filename);
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, type) {
10860
+ function getExtensionFromMediaType(mediaType, type2) {
9823
10861
  if (!mediaType) {
9824
- return type === "image" ? ".png" : ".bin";
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}_${nanoid6(10)}`;
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}_${nanoid6(10)}`;
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 = z16.object({
10701
- type: z16.enum(["input_mouse", "input_keyboard", "input_touch"]),
10702
- eventType: z16.string(),
10703
- x: z16.number().optional(),
10704
- y: z16.number().optional(),
10705
- button: z16.string().optional(),
10706
- clickCount: z16.number().optional(),
10707
- deltaX: z16.number().optional(),
10708
- deltaY: z16.number().optional(),
10709
- key: z16.string().optional(),
10710
- code: z16.string().optional(),
10711
- text: z16.string().optional(),
10712
- modifiers: z16.number().optional(),
10713
- touchPoints: z16.array(z16.object({
10714
- x: z16.number(),
10715
- y: z16.number(),
10716
- id: z16.number().optional()
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 z17 } from "zod";
10752
- import { readFileSync as readFileSync7 } from "fs";
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 join11 } from "path";
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
- join11(__dirname, "../package.json"),
11797
+ join12(__dirname, "../package.json"),
10759
11798
  // From dist/server -> dist/../package.json
10760
- join11(__dirname, "../../package.json"),
11799
+ join12(__dirname, "../../package.json"),
10761
11800
  // From dist/server (if nested differently)
10762
- join11(__dirname, "../../../package.json"),
11801
+ join12(__dirname, "../../../package.json"),
10763
11802
  // From src/server/routes (development)
10764
- join11(process.cwd(), "package.json")
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(readFileSync7(packageJsonPath, "utf-8"));
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 = z17.object({
10862
- provider: z17.string(),
10863
- apiKey: z17.string().min(1)
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 z18 } from "zod";
11947
+ import { z as z19 } from "zod";
10903
11948
  init_db();
10904
11949
  var terminals = new Hono4();
10905
- var spawnSchema = z18.object({
10906
- command: z18.string(),
10907
- cwd: z18.string().optional(),
10908
- name: z18.string().optional()
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 = z18.object({
10990
- tail: z18.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
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 = z18.object({
11015
- signal: z18.enum(["SIGTERM", "SIGKILL"]).optional()
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 = z18.object({
11030
- input: z18.string()
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 z19 } from "zod";
11216
- import { nanoid as nanoid7 } from "nanoid";
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 = z19.object({
11221
- prompt: z19.string().min(1),
11222
- outputSchema: z19.record(z19.string(), z19.unknown()),
11223
- webhookUrl: z19.string().url().optional(),
11224
- model: z19.string().optional(),
11225
- workingDirectory: z19.string().optional(),
11226
- name: z19.string().optional(),
11227
- maxIterations: z19.number().int().min(1).max(500).optional(),
11228
- parentTaskId: z19.string().optional()
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}_${nanoid7(10)}`;
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 exec6 } from "child_process";
11446
- import { promisify as promisify6 } from "util";
11447
- import { platform as platform2 } from "os";
11448
- var execAsync6 = promisify6(exec6);
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 = platform2();
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 execAsync6("tmux -V", { timeout: 5e3 });
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 execAsync6("agent-browser --version", { timeout: 1e4 });
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 execAsync6("npm install -g agent-browser", { timeout: 12e4 });
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 execAsync6("agent-browser install", { timeout: 12e4 });
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 = platform2();
13170
+ const os2 = platform4();
11559
13171
  try {
11560
13172
  if (os2 === "darwin") {
11561
13173
  try {
11562
- await execAsync6("which brew", { timeout: 5e3 });
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 execAsync6("brew install tmux", { timeout: 3e5 });
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 execAsync6("which apt-get", { timeout: 5e3 });
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 execAsync6("sudo apt-get update && sudo apt-get install -y tmux", {
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 execAsync6("which dnf", { timeout: 5e3 });
13196
+ await execAsync7("which dnf", { timeout: 5e3 });
11585
13197
  console.log("\u{1F4E6} Installing tmux via dnf...");
11586
- await execAsync6("sudo dnf install -y tmux", { timeout: 3e5 });
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 (existsSync17(webDir) && existsSync17(join12(webDir, "package.json"))) {
13238
+ if (existsSync19(webDir) && existsSync19(join13(webDir, "package.json"))) {
11627
13239
  return webDir;
11628
13240
  }
11629
13241
  const altWebDir = resolve10(currentDir, "..", "..", "web");
11630
- if (existsSync17(altWebDir) && existsSync17(join12(altWebDir, "package.json"))) {
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 = join12(webDir, ".next", "BUILD_ID");
11689
- return existsSync17(buildIdPath);
13300
+ const buildIdPath = join13(webDir, ".next", "BUILD_ID");
13301
+ return existsSync19(buildIdPath);
11690
13302
  }
11691
13303
  function hasSourceFiles(webDir) {
11692
- const appDir = join12(webDir, "src", "app");
11693
- const pagesDir = join12(webDir, "src", "pages");
11694
- const rootAppDir = join12(webDir, "app");
11695
- const rootPagesDir = join12(webDir, "pages");
11696
- return existsSync17(appDir) || existsSync17(pagesDir) || existsSync17(rootAppDir) || existsSync17(rootPagesDir);
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
- join12(webDir, ".next", "standalone", "server.js"),
11701
- join12(webDir, ".next", "standalone", "web", "server.js")
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 (existsSync17(serverPath)) {
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 = existsSync17(join12(webDir, "pnpm-lock.yaml"));
11745
- const useNpm = !usePnpm && existsSync17(join12(webDir, "package-lock.json"));
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, ...cleanEnv } = process.env;
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 = join12(webDir, "runtime-config.json");
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
- ...cleanEnv,
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 Hono6();
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: ["Content-Type", "Authorization", "X-Requested-With"],
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 (!existsSync17(config.resolvedWorkingDirectory)) {
11944
- mkdirSync7(config.resolvedWorkingDirectory, { recursive: true });
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 readFileSync8, existsSync as existsSync18 } from "fs";
12461
- import { resolve as resolve11, join as join13 } from "path";
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 (existsSync18(schemaStr)) {
13097
- outputSchema = JSON.parse(readFileSync8(schemaStr, "utf-8"));
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 = join13(appDataDir, "sparkecoder.config.json");
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 (existsSync18(configPath) && !options.force) {
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