sparkecoder 0.1.86 → 0.1.93

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