sparkecoder 0.1.85 → 0.1.87

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