sparkecoder 0.1.130 → 0.1.132

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 (216) hide show
  1. package/README.md +3 -3
  2. package/dist/agent/index.d.ts +3 -3
  3. package/dist/agent/index.js +1480 -638
  4. package/dist/agent/index.js.map +1 -1
  5. package/dist/cli.js +2281 -808
  6. package/dist/cli.js.map +1 -1
  7. package/dist/db/index.d.ts +2 -2
  8. package/dist/db/index.js.map +1 -1
  9. package/dist/{index-Bcz0aCAR.d.ts → index-BM99kjgq.d.ts} +177 -103
  10. package/dist/index.d.ts +5 -5
  11. package/dist/index.js +2215 -780
  12. package/dist/index.js.map +1 -1
  13. package/dist/{schema-BWbWmfDQ.d.ts → schema-Dz-wABVY.d.ts} +27 -4
  14. package/dist/{search-DOzC4ojH.d.ts → search-CVVfuBPZ.d.ts} +4 -4
  15. package/dist/server/index.js +2215 -780
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/skills/default/build-context-and-solve-issue.md +74 -0
  18. package/dist/skills/default/doublecheck.md +95 -0
  19. package/dist/tools/index.d.ts +3 -3
  20. package/dist/tools/index.js +11 -2
  21. package/dist/tools/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/skills/default/build-context-and-solve-issue.md +74 -0
  24. package/src/skills/default/doublecheck.md +95 -0
  25. package/web/.next/BUILD_ID +1 -1
  26. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  27. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  28. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  29. package/web/.next/standalone/web/.next/server/app/(main)/agents/page/next-font-manifest.json +1 -1
  30. package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
  31. package/web/.next/standalone/web/.next/server/app/(main)/page/next-font-manifest.json +1 -1
  32. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  33. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page/next-font-manifest.json +1 -1
  34. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  35. package/web/.next/standalone/web/.next/server/app/(main)/settings/page/next-font-manifest.json +1 -1
  36. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  38. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
  45. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  47. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +3 -3
  48. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  49. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  51. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  55. package/web/.next/standalone/web/.next/server/app/agents.rsc +6 -6
  56. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +2 -2
  57. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +2 -2
  59. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +6 -6
  60. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +3 -3
  62. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +3 -3
  63. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  64. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/installation/page/next-font-manifest.json +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/installation.html +4 -4
  68. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +6 -6
  69. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +6 -6
  70. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +3 -3
  72. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +20 -21
  74. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +2 -2
  76. package/web/.next/standalone/web/.next/server/app/docs/page/next-font-manifest.json +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs/skills/page/next-font-manifest.json +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  80. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  81. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +4 -4
  82. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +4 -4
  83. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +3 -3
  85. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  86. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +2 -2
  89. package/web/.next/standalone/web/.next/server/app/docs/tools/page/next-font-manifest.json +1 -1
  90. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  91. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  92. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +4 -4
  93. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +4 -4
  94. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +3 -3
  96. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  97. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +2 -2
  98. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  99. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +2 -2
  100. package/web/.next/standalone/web/.next/server/app/docs.html +3 -3
  101. package/web/.next/standalone/web/.next/server/app/docs.rsc +5 -5
  102. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +5 -5
  103. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  104. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +3 -3
  105. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  106. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +3 -3
  107. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +2 -2
  108. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  109. package/web/.next/standalone/web/.next/server/app/index.rsc +6 -6
  110. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  111. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  112. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +6 -6
  113. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  114. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  115. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +3 -3
  116. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  117. package/web/.next/standalone/web/.next/server/app/settings.rsc +6 -6
  118. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  119. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  120. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +2 -2
  121. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +6 -6
  122. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  123. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +3 -3
  124. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +3 -3
  125. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__36edac7c._.js +1 -1
  126. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__397fadd4._.js +1 -1
  127. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__70cecda8._.js +1 -1
  128. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__be5e2967._.js +1 -1
  129. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_layout_tsx_453f6492._.js +1 -1
  130. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +1 -1
  131. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +2 -2
  132. package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
  133. package/web/.next/standalone/web/.next/server/next-font-manifest.json +9 -9
  134. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  135. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  136. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  137. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  138. package/web/.next/standalone/web/.next/static/chunks/185f69f6478ba713.js +1 -0
  139. package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +3 -0
  140. package/web/.next/standalone/web/.next/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  141. package/web/.next/standalone/web/.next/static/chunks/7549a5b7c7f6786e.js +1 -0
  142. package/web/.next/standalone/web/.next/static/{static/chunks/c5dd884b71007965.js → chunks/a839c83078c56476.js} +1 -1
  143. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  144. package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  145. package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  146. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  147. package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  148. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  149. package/web/.next/standalone/web/.next/static/static/chunks/185f69f6478ba713.js +1 -0
  150. package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +3 -0
  151. package/web/.next/standalone/web/.next/static/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  152. package/web/.next/standalone/web/.next/static/static/chunks/7549a5b7c7f6786e.js +1 -0
  153. package/web/.next/{static/chunks/c5dd884b71007965.js → standalone/web/.next/static/static/chunks/a839c83078c56476.js} +1 -1
  154. package/web/.next/standalone/web/.next/static/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  155. package/web/.next/standalone/web/.next/static/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  156. package/web/.next/standalone/web/.next/static/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  157. package/web/.next/standalone/web/.next/static/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  158. package/web/.next/standalone/web/.next/static/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  159. package/web/.next/standalone/web/.next/static/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  160. package/web/.next/standalone/web/package-lock.json +21 -21
  161. package/web/.next/standalone/web/runtime-config.json +2 -1
  162. package/web/.next/standalone/web/src/app/(main)/page.tsx +2 -2
  163. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +111 -11
  164. package/web/.next/standalone/web/src/app/__sfapi/[...path]/route.ts +96 -0
  165. package/web/.next/standalone/web/src/app/api/config/route.ts +5 -12
  166. package/web/.next/standalone/web/src/app/docs/installation/page.mdx +2 -2
  167. package/web/.next/standalone/web/src/app/docs/page.mdx +1 -1
  168. package/web/.next/standalone/web/src/components/sessions-sidebar.tsx +1 -1
  169. package/web/.next/standalone/web/src/lib/config.ts +26 -16
  170. package/web/.next/static/chunks/185f69f6478ba713.js +1 -0
  171. package/web/.next/static/chunks/20ca4e35e9bb3e94.js +3 -0
  172. package/web/.next/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  173. package/web/.next/static/chunks/7549a5b7c7f6786e.js +1 -0
  174. package/web/.next/{standalone/web/.next/static/chunks/c5dd884b71007965.js → static/chunks/a839c83078c56476.js} +1 -1
  175. package/web/.next/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  176. package/web/.next/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  177. package/web/.next/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  178. package/web/.next/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  179. package/web/.next/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  180. package/web/.next/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  181. package/web/.next/standalone/web/.next/static/chunks/9b88f148788e4504.js +0 -3
  182. package/web/.next/standalone/web/.next/static/chunks/b203b9aa975135d3.js +0 -1
  183. package/web/.next/standalone/web/.next/static/chunks/ea89ca7892d8c557.js +0 -1
  184. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  185. package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  186. package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  187. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  188. package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  189. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  190. package/web/.next/standalone/web/.next/static/static/chunks/9b88f148788e4504.js +0 -3
  191. package/web/.next/standalone/web/.next/static/static/chunks/b203b9aa975135d3.js +0 -1
  192. package/web/.next/standalone/web/.next/static/static/chunks/ea89ca7892d8c557.js +0 -1
  193. package/web/.next/standalone/web/.next/static/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  194. package/web/.next/standalone/web/.next/static/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  195. package/web/.next/standalone/web/.next/static/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  196. package/web/.next/standalone/web/.next/static/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  197. package/web/.next/standalone/web/.next/static/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  198. package/web/.next/standalone/web/.next/static/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  199. package/web/.next/static/chunks/9b88f148788e4504.js +0 -3
  200. package/web/.next/static/chunks/b203b9aa975135d3.js +0 -1
  201. package/web/.next/static/chunks/ea89ca7892d8c557.js +0 -1
  202. package/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  203. package/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  204. package/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  205. package/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  206. package/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  207. package/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  208. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  209. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  210. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
  211. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  212. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  213. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
  214. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  215. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  216. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -819,8 +819,8 @@ var init_types = __esm({
819
819
  authKey: z.string().optional()
820
820
  }).optional();
821
821
  SparkcoderConfigSchema = z.object({
822
- // Default model to use (Vercel AI Gateway format)
823
- defaultModel: z.string().default("anthropic/claude-opus-4.7"),
822
+ // Default model to use (LiteLLM model id)
823
+ defaultModel: z.string().default("gpt-5.5"),
824
824
  // Working directory for file operations
825
825
  workingDirectory: z.string().optional(),
826
826
  // Tool approval settings
@@ -859,6 +859,14 @@ var init_types = __esm({
859
859
  webhooks: z.object({
860
860
  token: z.string().optional()
861
861
  }).optional(),
862
+ // Self-update: when running as the managed service, periodically check
863
+ // npm for a newer published version and, if found, re-run the hosted
864
+ // installer (full upgrade + restart). Disabled automatically when not
865
+ // running from a global install (e.g. dev/source checkouts).
866
+ autoUpdate: z.object({
867
+ enabled: z.boolean().optional().default(true),
868
+ intervalHours: z.number().positive().optional().default(6)
869
+ }).optional().default({}),
862
870
  // Database path (used for local SQLite - ignored if remoteServer is configured)
863
871
  databasePath: z.string().optional().default("./sparkecoder.db"),
864
872
  // Remote server configuration (for centralized storage)
@@ -966,6 +974,7 @@ __export(config_exports, {
966
974
  requiresApproval: () => requiresApproval,
967
975
  saveAuthKey: () => saveAuthKey,
968
976
  setApiKey: () => setApiKey,
977
+ setCfAccessConfig: () => setCfAccessConfig,
969
978
  setMcpServers: () => setMcpServers,
970
979
  setPublicUrl: () => setPublicUrl,
971
980
  setSkillsAdditionalDirectories: () => setSkillsAdditionalDirectories,
@@ -1154,12 +1163,12 @@ function loadConfig(configPath, workingDirectory) {
1154
1163
  ]
1155
1164
  };
1156
1165
  const DEFAULT_REMOTE_URL = "https://agent-remote-server.sparkecode.com";
1157
- const remoteUrl = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
1166
+ const remoteUrl2 = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
1158
1167
  const remoteAuthKey = process.env.SPARKECODER_AUTH_KEY || config.remoteServer?.authKey || loadStoredAuthKey();
1159
1168
  const resolvedRemoteServer = {
1160
- url: remoteUrl,
1169
+ url: remoteUrl2,
1161
1170
  authKey: remoteAuthKey,
1162
- isConfigured: !!remoteUrl && !!remoteAuthKey
1171
+ isConfigured: !!remoteUrl2 && !!remoteAuthKey
1163
1172
  };
1164
1173
  const resolved = {
1165
1174
  ...config,
@@ -1315,6 +1324,40 @@ function setPublicUrl(publicUrl) {
1315
1324
  console.warn("[config] failed to persist publicUrl:", err?.message || err);
1316
1325
  }
1317
1326
  }
1327
+ function setCfAccessConfig(input) {
1328
+ const applyToAuth = (auth) => {
1329
+ const curAuth = auth || {};
1330
+ const curCf = curAuth.cfAccess || {};
1331
+ const nextCf = { ...curCf };
1332
+ if (input.enabled !== void 0) nextCf.enabled = input.enabled;
1333
+ if (input.teamDomain !== void 0) nextCf.teamDomain = input.teamDomain;
1334
+ if (input.audTag !== void 0) nextCf.audTag = input.audTag;
1335
+ const nextAuth = { ...curAuth, cfAccess: nextCf };
1336
+ if (input.allowedEmails !== void 0) nextAuth.allowedEmails = input.allowedEmails;
1337
+ return nextAuth;
1338
+ };
1339
+ if (cachedConfig) {
1340
+ cachedConfig.auth = applyToAuth(cachedConfig.auth);
1341
+ }
1342
+ try {
1343
+ const cwdPath = resolve(process.cwd(), "sparkecoder.config.json");
1344
+ const target = existsSync(cwdPath) ? cwdPath : join(ensureAppDataDirectory(), "sparkecoder.config.json");
1345
+ let raw = {};
1346
+ if (existsSync(target)) {
1347
+ try {
1348
+ raw = JSON.parse(readFileSync(target, "utf-8"));
1349
+ } catch {
1350
+ raw = {};
1351
+ }
1352
+ } else {
1353
+ raw = createDefaultConfig();
1354
+ }
1355
+ raw.auth = applyToAuth(raw.auth);
1356
+ writeFileSync(target, JSON.stringify(raw, null, 2));
1357
+ } catch (err) {
1358
+ console.warn("[config] failed to persist cf-access config:", err?.message || err);
1359
+ }
1360
+ }
1318
1361
  function clearSlackConfig() {
1319
1362
  if (cachedConfig) cachedConfig.slack = {};
1320
1363
  try {
@@ -1356,7 +1399,7 @@ function autoApproveAllTools(sessionConfig) {
1356
1399
  }
1357
1400
  function createDefaultConfig() {
1358
1401
  return {
1359
- defaultModel: "anthropic/claude-opus-4.7",
1402
+ defaultModel: "gpt-5.5",
1360
1403
  // workingDirectory is intentionally not set - defaults to where CLI is run
1361
1404
  toolApprovals: {
1362
1405
  bash: true,
@@ -1378,6 +1421,10 @@ function createDefaultConfig() {
1378
1421
  port: 3141,
1379
1422
  host: "0.0.0.0"
1380
1423
  },
1424
+ autoUpdate: {
1425
+ enabled: true,
1426
+ intervalHours: 6
1427
+ },
1381
1428
  databasePath: "./sparkecoder.db"
1382
1429
  };
1383
1430
  }
@@ -1604,6 +1651,7 @@ var init_config = __esm({
1604
1651
  openai: "OPENAI_API_KEY",
1605
1652
  google: "GOOGLE_GENERATIVE_AI_API_KEY",
1606
1653
  xai: "XAI_API_KEY",
1654
+ litellm: "LITELLM_API_KEY",
1607
1655
  "ai-gateway": "AI_GATEWAY_API_KEY"
1608
1656
  };
1609
1657
  SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
@@ -4846,11 +4894,11 @@ async function getRepoNamespace(workingDirectory, configuredNamespace) {
4846
4894
  if (configuredNamespace) {
4847
4895
  return configuredNamespace;
4848
4896
  }
4849
- const remoteUrl = getGitRemoteUrl(workingDirectory);
4850
- if (!remoteUrl) {
4897
+ const remoteUrl2 = getGitRemoteUrl(workingDirectory);
4898
+ if (!remoteUrl2) {
4851
4899
  return null;
4852
4900
  }
4853
- const parsed = parseGitRemoteUrl(remoteUrl);
4901
+ const parsed = parseGitRemoteUrl(remoteUrl2);
4854
4902
  if (!parsed) {
4855
4903
  return null;
4856
4904
  }
@@ -5449,7 +5497,7 @@ function isPathExcluded(relativePath, exclude) {
5449
5497
  }
5450
5498
  async function walkDirectory(dir, include, exclude, baseDir) {
5451
5499
  const { readdirSync: readdirSync4 } = await import("fs");
5452
- const { join: join19, relative: relative10 } = await import("path");
5500
+ const { join: join21, relative: relative10 } = await import("path");
5453
5501
  const files = [];
5454
5502
  function walk(currentDir) {
5455
5503
  let entries;
@@ -5459,7 +5507,7 @@ async function walkDirectory(dir, include, exclude, baseDir) {
5459
5507
  return;
5460
5508
  }
5461
5509
  for (const entry2 of entries) {
5462
- const fullPath = join19(currentDir, entry2.name);
5510
+ const fullPath = join21(currentDir, entry2.name);
5463
5511
  const relativePath = relative10(baseDir, fullPath);
5464
5512
  if (isPathExcluded(relativePath, exclude)) {
5465
5513
  continue;
@@ -7289,7 +7337,8 @@ async function buildSystemPrompt(options) {
7289
7337
  sessionId,
7290
7338
  discoveredSkills,
7291
7339
  activeFiles = [],
7292
- customInstructions
7340
+ customInstructions,
7341
+ taskScopedSkills
7293
7342
  } = options;
7294
7343
  let alwaysLoadedContent = "";
7295
7344
  let globMatchedContent = "";
@@ -7310,6 +7359,22 @@ async function buildSystemPrompt(options) {
7310
7359
  const skills2 = await loadAllSkills2(skillsDirectories);
7311
7360
  onDemandSkillsContext = formatSkillsForContext(skills2);
7312
7361
  }
7362
+ let taskScopedSkillsBlock = "";
7363
+ if (taskScopedSkills && (taskScopedSkills.always.length > 0 || taskScopedSkills.onDemand.length > 0)) {
7364
+ const parts = ["<task_provided_skills>"];
7365
+ parts.push("These skills were supplied with this task and are available for this run only.");
7366
+ if (taskScopedSkills.always.length > 0) {
7367
+ parts.push(formatAlwaysLoadedSkills(taskScopedSkills.always));
7368
+ }
7369
+ if (taskScopedSkills.onDemand.length > 0) {
7370
+ parts.push("Load any of these on demand with the load_skill tool:");
7371
+ for (const s of taskScopedSkills.onDemand) {
7372
+ parts.push(`- ${s.name}: ${s.description}`);
7373
+ }
7374
+ }
7375
+ parts.push("</task_provided_skills>");
7376
+ taskScopedSkillsBlock = parts.join("\n");
7377
+ }
7313
7378
  const todos = await todoQueries.getBySession(sessionId);
7314
7379
  const todosContext = formatTodosForContext(todos);
7315
7380
  const plans = await readSessionPlans(workingDirectory, sessionId);
@@ -7602,6 +7667,8 @@ ${globMatchedContent}
7602
7667
  ${onDemandSkillsContext}
7603
7668
  </on_demand_skills>
7604
7669
 
7670
+ ${taskScopedSkillsBlock}
7671
+
7605
7672
  <current_task_list>
7606
7673
  ${todosContext}
7607
7674
  </current_task_list>
@@ -8223,6 +8290,111 @@ var init_sanitize_messages = __esm({
8223
8290
  }
8224
8291
  });
8225
8292
 
8293
+ // src/utils/cap-image-count.ts
8294
+ function isImagePart(part) {
8295
+ if (!part || typeof part !== "object") return false;
8296
+ const t = part.type;
8297
+ if (t === "image") return true;
8298
+ if (t === "image-data") return true;
8299
+ if (t === "media") {
8300
+ const data = part.data;
8301
+ const mt = part.mediaType;
8302
+ if (typeof data === "string" && typeof mt === "string" && mt.startsWith("image/")) {
8303
+ return true;
8304
+ }
8305
+ }
8306
+ return false;
8307
+ }
8308
+ function makePlaceholder() {
8309
+ return { type: "text", text: IMAGE_TRUNCATED_PLACEHOLDER };
8310
+ }
8311
+ function countImages(messages) {
8312
+ let n = 0;
8313
+ for (const msg of messages) {
8314
+ if (!Array.isArray(msg.content)) continue;
8315
+ for (const part of msg.content) {
8316
+ if (isImagePart(part)) {
8317
+ n++;
8318
+ continue;
8319
+ }
8320
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
8321
+ for (const sub of part.output.value) {
8322
+ if (isImagePart(sub)) n++;
8323
+ }
8324
+ }
8325
+ }
8326
+ }
8327
+ return n;
8328
+ }
8329
+ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
8330
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
8331
+ if (max < 0) throw new Error("capImageCount: max must be >= 0");
8332
+ const total = countImages(messages);
8333
+ if (total <= max) return messages;
8334
+ let toDrop = total - max;
8335
+ let mutated = false;
8336
+ const out = messages.slice();
8337
+ for (let i = 0; i < out.length && toDrop > 0; i++) {
8338
+ const msg = out[i];
8339
+ if (!Array.isArray(msg.content)) continue;
8340
+ let contentCloned = false;
8341
+ const ensureContentCloned = () => {
8342
+ if (contentCloned) return;
8343
+ out[i] = { ...msg, content: [...msg.content] };
8344
+ contentCloned = true;
8345
+ };
8346
+ const content = () => out[i].content;
8347
+ for (let j = 0; j < content().length && toDrop > 0; j++) {
8348
+ const part = content()[j];
8349
+ if (isImagePart(part)) {
8350
+ ensureContentCloned();
8351
+ out[i].content[j] = makePlaceholder();
8352
+ toDrop--;
8353
+ mutated = true;
8354
+ continue;
8355
+ }
8356
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
8357
+ const innerImages = [];
8358
+ const innerValue = part.output.value;
8359
+ for (let k = 0; k < innerValue.length; k++) {
8360
+ if (isImagePart(innerValue[k])) innerImages.push(k);
8361
+ }
8362
+ if (innerImages.length === 0) continue;
8363
+ const dropHere = Math.min(innerImages.length, toDrop);
8364
+ ensureContentCloned();
8365
+ const newOutputValue = [...innerValue];
8366
+ for (let d = 0; d < dropHere; d++) {
8367
+ newOutputValue[innerImages[d]] = makePlaceholder();
8368
+ }
8369
+ const newPart = {
8370
+ ...part,
8371
+ output: {
8372
+ ...part.output,
8373
+ value: newOutputValue
8374
+ }
8375
+ };
8376
+ out[i].content[j] = newPart;
8377
+ toDrop -= dropHere;
8378
+ mutated = true;
8379
+ }
8380
+ }
8381
+ }
8382
+ if (mutated) {
8383
+ console.warn(
8384
+ `[cap-image-count] Replaced ${total - max} oldest image(s) with text placeholder (total=${total}, kept=${max}). This prevents request-too-large errors at the model / tunnel layer.`
8385
+ );
8386
+ }
8387
+ return mutated ? out : messages;
8388
+ }
8389
+ var MAX_IMAGES_IN_CONTEXT, IMAGE_TRUNCATED_PLACEHOLDER;
8390
+ var init_cap_image_count = __esm({
8391
+ "src/utils/cap-image-count.ts"() {
8392
+ "use strict";
8393
+ MAX_IMAGES_IN_CONTEXT = 11;
8394
+ IMAGE_TRUNCATED_PLACEHOLDER = "[image truncated due to length of conversation]";
8395
+ }
8396
+ });
8397
+
8226
8398
  // src/agent/model-limits.ts
8227
8399
  function getModelLimits(modelId) {
8228
8400
  const normalized = modelId.trim().toLowerCase();
@@ -8238,18 +8410,9 @@ var init_model_limits = __esm({
8238
8410
  "src/agent/model-limits.ts"() {
8239
8411
  "use strict";
8240
8412
  MODEL_LIMITS = {
8241
- "anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
8242
- "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
8243
- "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
8244
- "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
8245
- "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
8246
- "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
8247
- "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
8248
- "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
8249
- "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
8250
- "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
8251
- "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
8252
- "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
8413
+ "claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
8414
+ "gpt-5.5": { contextWindow: 35e4, rollingTarget: 15e4 },
8415
+ "claude-fable-5": { contextWindow: 2e5, rollingTarget: 15e4 }
8253
8416
  };
8254
8417
  DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
8255
8418
  PREFIX_DEFAULTS = {
@@ -8323,6 +8486,32 @@ var init_conversation_archive = __esm({
8323
8486
 
8324
8487
  // src/agent/context.ts
8325
8488
  import { generateText as generateText2 } from "ai";
8489
+ function stripBinaryContentForSummary(value) {
8490
+ if (Array.isArray(value)) return value.map(stripBinaryContentForSummary);
8491
+ if (!value || typeof value !== "object") return value;
8492
+ const record = value;
8493
+ const type = record.type;
8494
+ if ((type === "image-data" || type === "file-data" || type === "media") && typeof record.data === "string") {
8495
+ const mediaType = typeof record.mediaType === "string" ? record.mediaType : "unknown media type";
8496
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
8497
+ return {
8498
+ ...record,
8499
+ data: `[${type}${filename}; ${mediaType}; ${record.data.length} base64 chars omitted for summary]`
8500
+ };
8501
+ }
8502
+ if (type === "image" && typeof record.image === "string") {
8503
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
8504
+ return {
8505
+ ...record,
8506
+ image: `[image${filename}; ${record.image.length} base64 chars omitted for summary]`
8507
+ };
8508
+ }
8509
+ const out = {};
8510
+ for (const [key2, nested] of Object.entries(record)) {
8511
+ out[key2] = stripBinaryContentForSummary(nested);
8512
+ }
8513
+ return out;
8514
+ }
8326
8515
  function stripOrphanedToolResults(msg, removedIds) {
8327
8516
  if (!Array.isArray(msg.content)) return msg;
8328
8517
  const parts = msg.content.filter((part) => {
@@ -8483,6 +8672,7 @@ var init_context = __esm({
8483
8672
  init_tokens();
8484
8673
  init_prompts();
8485
8674
  init_sanitize_messages();
8675
+ init_cap_image_count();
8486
8676
  init_model_limits();
8487
8677
  TOOL_OUTPUT_TRIM_CHARS = 400;
8488
8678
  COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
@@ -8532,6 +8722,7 @@ ${summaryContent}`
8532
8722
  messages = repairToolPairing(messages);
8533
8723
  messages = ensureToolResultsFollowCalls(messages);
8534
8724
  messages = ensureEndsWithUserOrTool(messages);
8725
+ messages = capImageCount(messages);
8535
8726
  return messages;
8536
8727
  }
8537
8728
  // ---------------------------------------------------------------------------
@@ -8649,7 +8840,7 @@ ${summaryContent}`
8649
8840
  }
8650
8841
  async summarizeChunk(chunk) {
8651
8842
  const historyText = chunk.map((msg) => {
8652
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
8843
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(stripBinaryContentForSummary(msg.content));
8653
8844
  return `[${msg.role}]: ${content}`;
8654
8845
  }).join("\n\n");
8655
8846
  try {
@@ -8897,6 +9088,127 @@ var init_persistence = __esm({
8897
9088
  });
8898
9089
 
8899
9090
  // src/integrations/slack/client.ts
9091
+ var client_exports = {};
9092
+ __export(client_exports, {
9093
+ LOADING_REACTION: () => LOADING_REACTION,
9094
+ RESULT_REACTIONS: () => RESULT_REACTIONS,
9095
+ addLoadingReaction: () => addLoadingReaction,
9096
+ addResultReaction: () => addResultReaction,
9097
+ botParticipatedInThread: () => botParticipatedInThread,
9098
+ ensureSlackSelfIdentity: () => ensureSlackSelfIdentity,
9099
+ getCachedSlackSelfIdentity: () => getCachedSlackSelfIdentity,
9100
+ getDefaultOrchestratorName: () => getDefaultOrchestratorName,
9101
+ getSlackAdapter: () => getSlackAdapter,
9102
+ getSlackAllowlistPolicy: () => getSlackAllowlistPolicy,
9103
+ getSlackBotToken: () => getSlackBotToken,
9104
+ getSlackDeniedReplyPolicy: () => getSlackDeniedReplyPolicy,
9105
+ getSlackSigningSecret: () => getSlackSigningSecret,
9106
+ isSlackConfigured: () => isSlackConfigured,
9107
+ normalizeSlackMentions: () => normalizeSlackMentions,
9108
+ noteBotPostedInThread: () => noteBotPostedInThread,
9109
+ postThreadMessage: () => postThreadMessage,
9110
+ removeLoadingReaction: () => removeLoadingReaction,
9111
+ resolveSlackUserInfo: () => resolveSlackUserInfo,
9112
+ resolveSlackUserName: () => resolveSlackUserName
9113
+ });
9114
+ function slackBackoffMs(attempt) {
9115
+ const expo = SLACK_BACKOFF_BASE_MS * 2 ** attempt;
9116
+ const jitter = Math.floor(Math.random() * SLACK_BACKOFF_BASE_MS);
9117
+ return Math.min(expo + jitter, SLACK_BACKOFF_CAP_MS);
9118
+ }
9119
+ async function slackFetchWithRetry(url, init, attempts = SLACK_FETCH_ATTEMPTS) {
9120
+ let lastErr;
9121
+ for (let i = 0; i < attempts; i++) {
9122
+ const isLast = i === attempts - 1;
9123
+ try {
9124
+ const res = await fetch(url, init);
9125
+ if ((res.status === 429 || res.status >= 500) && !isLast) {
9126
+ const ra = Number(res.headers.get("retry-after"));
9127
+ const waitMs = Number.isFinite(ra) && ra > 0 ? Math.min(ra * 1e3, SLACK_BACKOFF_CAP_MS) : slackBackoffMs(i);
9128
+ await new Promise((r) => setTimeout(r, waitMs));
9129
+ continue;
9130
+ }
9131
+ return res;
9132
+ } catch (err) {
9133
+ lastErr = err;
9134
+ if (isLast) throw err;
9135
+ await new Promise((r) => setTimeout(r, slackBackoffMs(i)));
9136
+ }
9137
+ }
9138
+ throw lastErr ?? new Error("slack fetch failed");
9139
+ }
9140
+ function reactionKey(channel, ts) {
9141
+ return `${channel}\u241F${ts}`;
9142
+ }
9143
+ async function addLoadingReaction(channel, timestamp) {
9144
+ const adapter = getSlackAdapter();
9145
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
9146
+ const key2 = reactionKey(channel, timestamp);
9147
+ const inFlight = (async () => {
9148
+ try {
9149
+ const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
9150
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
9151
+ console.warn(`[slack] addReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
9152
+ }
9153
+ return res;
9154
+ } catch (err) {
9155
+ console.warn(`[slack] addReaction threw on ${channel}/${timestamp}:`, err?.message || err);
9156
+ return { ok: false, error: err?.message || "unknown" };
9157
+ }
9158
+ })();
9159
+ pendingAdds.set(key2, inFlight);
9160
+ void inFlight.finally(() => {
9161
+ if (pendingAdds.get(key2) === inFlight) pendingAdds.delete(key2);
9162
+ });
9163
+ return inFlight;
9164
+ }
9165
+ async function removeLoadingReaction(channel, timestamp) {
9166
+ const adapter = getSlackAdapter();
9167
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
9168
+ const pending = pendingAdds.get(reactionKey(channel, timestamp));
9169
+ if (pending) {
9170
+ try {
9171
+ await pending;
9172
+ } catch {
9173
+ }
9174
+ }
9175
+ try {
9176
+ const res = await adapter.removeReaction({ channel, timestamp, name: LOADING_REACTION });
9177
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
9178
+ console.warn(`[slack] removeReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
9179
+ }
9180
+ return res;
9181
+ } catch (err) {
9182
+ console.warn(`[slack] removeReaction threw on ${channel}/${timestamp}:`, err?.message || err);
9183
+ return { ok: false, error: err?.message || "unknown" };
9184
+ }
9185
+ }
9186
+ async function addResultReaction(channel, timestamp, state2) {
9187
+ const name = RESULT_REACTIONS[state2];
9188
+ if (!name) return { ok: false, error: `no_reaction_for_state:${state2}` };
9189
+ const adapter = getSlackAdapter();
9190
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
9191
+ try {
9192
+ const res = await adapter.addReaction({ channel, timestamp, name });
9193
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
9194
+ console.warn(`[slack] addReaction ${name} failed on ${channel}/${timestamp}: ${res.error}`);
9195
+ }
9196
+ return res;
9197
+ } catch (err) {
9198
+ console.warn(`[slack] addResultReaction threw on ${channel}/${timestamp}:`, err?.message || err);
9199
+ return { ok: false, error: err?.message || "unknown" };
9200
+ }
9201
+ }
9202
+ async function postThreadMessage(channel, threadTs, text) {
9203
+ const adapter = getSlackAdapter();
9204
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
9205
+ try {
9206
+ return await adapter.postMessage({ channel, text, threadTs });
9207
+ } catch (err) {
9208
+ console.warn(`[slack] postThreadMessage threw on ${channel}/${threadTs}:`, err?.message || err);
9209
+ return { ok: false, error: err?.message || "unknown" };
9210
+ }
9211
+ }
8900
9212
  function readSlackConfig() {
8901
9213
  try {
8902
9214
  const cfg = getConfig();
@@ -8914,9 +9226,25 @@ function readSlackConfig() {
8914
9226
  function getSlackAdapter() {
8915
9227
  const cfg = readSlackConfig();
8916
9228
  if (!cfg) return void 0;
9229
+ const slackForm = async (endpoint, params) => {
9230
+ const body = new URLSearchParams(params).toString();
9231
+ const res = await fetch(`https://slack.com/api/${endpoint}`, {
9232
+ method: "POST",
9233
+ headers: {
9234
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
9235
+ Authorization: `Bearer ${cfg.botToken}`
9236
+ },
9237
+ body
9238
+ });
9239
+ const data = await res.json().catch(() => ({}));
9240
+ if (!res.ok || data?.ok === false) {
9241
+ return { ok: false, error: data?.error || `HTTP ${res.status}` };
9242
+ }
9243
+ return { ok: true };
9244
+ };
8917
9245
  return {
8918
9246
  async postMessage({ channel, text, threadTs }) {
8919
- const res = await fetch("https://slack.com/api/chat.postMessage", {
9247
+ const res = await slackFetchWithRetry("https://slack.com/api/chat.postMessage", {
8920
9248
  method: "POST",
8921
9249
  headers: {
8922
9250
  "Content-Type": "application/json; charset=utf-8",
@@ -8929,6 +9257,12 @@ function getSlackAdapter() {
8929
9257
  return { ok: false, error: data?.error || `HTTP ${res.status}` };
8930
9258
  }
8931
9259
  return { ok: true, ts: data?.ts };
9260
+ },
9261
+ addReaction({ channel, timestamp, name }) {
9262
+ return slackForm("reactions.add", { channel, timestamp, name });
9263
+ },
9264
+ removeReaction({ channel, timestamp, name }) {
9265
+ return slackForm("reactions.remove", { channel, timestamp, name });
8932
9266
  }
8933
9267
  };
8934
9268
  }
@@ -9129,12 +9463,31 @@ function getSlackDeniedReplyPolicy() {
9129
9463
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
9130
9464
  }
9131
9465
  }
9132
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
9466
+ var LOADING_REACTION, RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, pendingAdds, cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
9133
9467
  var init_client3 = __esm({
9134
9468
  "src/integrations/slack/client.ts"() {
9135
9469
  "use strict";
9136
9470
  init_config();
9137
9471
  init_persistence();
9472
+ LOADING_REACTION = "hourglass_flowing_sand";
9473
+ RESULT_REACTIONS = {
9474
+ responded: "white_check_mark",
9475
+ skipped: "zzz",
9476
+ handed_off: "eyes",
9477
+ failed: "warning"
9478
+ };
9479
+ SLACK_FETCH_ATTEMPTS = 3;
9480
+ SLACK_BACKOFF_BASE_MS = 400;
9481
+ SLACK_BACKOFF_CAP_MS = 3e4;
9482
+ REACTION_SOFT_ERRORS = /* @__PURE__ */ new Set([
9483
+ "already_reacted",
9484
+ // add: someone (or we) already added this emoji
9485
+ "no_reaction",
9486
+ // remove: the emoji isn't on the message
9487
+ "message_not_found"
9488
+ // remove/add: original message deleted
9489
+ ]);
9490
+ pendingAdds = /* @__PURE__ */ new Map();
9138
9491
  cachedSelf = null;
9139
9492
  selfInflight = null;
9140
9493
  USER_TTL_MS = 60 * 60 * 1e3;
@@ -9147,83 +9500,541 @@ var init_client3 = __esm({
9147
9500
  }
9148
9501
  });
9149
9502
 
9150
- // src/integrations/channels/slack.ts
9151
- function threadKey(channel, threadTs) {
9152
- return `${channel}\u241F${threadTs}`;
9153
- }
9154
- function markThreadOwned(channel, threadTs) {
9155
- ownedThreads.add(threadKey(channel, threadTs));
9156
- }
9157
- function isThreadOwned(channel, threadTs) {
9158
- return ownedThreads.has(threadKey(channel, threadTs));
9503
+ // src/agent/session-lock.ts
9504
+ async function withSessionLock(sessionId, fn) {
9505
+ let state2 = locks.get(sessionId);
9506
+ if (!state2) {
9507
+ state2 = { tail: Promise.resolve(), pending: 0 };
9508
+ locks.set(sessionId, state2);
9509
+ }
9510
+ state2.pending++;
9511
+ const prev = state2.tail;
9512
+ let release;
9513
+ const next = new Promise((resolve14) => {
9514
+ release = resolve14;
9515
+ });
9516
+ state2.tail = prev.then(() => next);
9517
+ await prev;
9518
+ try {
9519
+ return await fn();
9520
+ } finally {
9521
+ release();
9522
+ state2.pending--;
9523
+ if (state2.pending === 0 && locks.get(sessionId) === state2) {
9524
+ locks.delete(sessionId);
9525
+ }
9526
+ }
9159
9527
  }
9160
- function isSelfAuthored(event, self) {
9161
- if (!self) return true;
9162
- if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
9163
- if (self.botUserId && event.user && event.user === self.botUserId) return true;
9164
- return false;
9528
+ function isSessionLocked(sessionId) {
9529
+ const s = locks.get(sessionId);
9530
+ return !!s && s.pending > 0;
9165
9531
  }
9166
- function slackEventToInboundResult(event, opts = {}) {
9167
- if (!event) return { event: null, dropReason: "empty_text" };
9168
- const self = opts.self ?? getCachedSlackSelfIdentity();
9169
- const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
9170
- if (isBotAuthored && isSelfAuthored(event, self)) {
9171
- return { event: null, dropReason: "bot_message" };
9172
- }
9173
- if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
9174
- return { event: null, dropReason: "ignored_subtype" };
9532
+ var locks;
9533
+ var init_session_lock = __esm({
9534
+ "src/agent/session-lock.ts"() {
9535
+ "use strict";
9536
+ locks = /* @__PURE__ */ new Map();
9175
9537
  }
9176
- const isDm = event.type === "message" && event.channel_type === "im";
9177
- const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
9178
- const isNonThreadChannelMsg = event.type === "message" && !isDm && !isThreadReply && (event.channel_type === "channel" || event.channel_type === "group" || event.channel_type === "mpim" || // Some payload shapes omit channel_type for channel messages.
9179
- typeof event.channel === "string");
9180
- if (event.type !== "app_mention" && !isDm && !isThreadReply) {
9181
- if (isNonThreadChannelMsg) {
9182
- return { event: null, dropReason: "non_thread_channel_msg" };
9538
+ });
9539
+
9540
+ // src/orchestrator/webhook-events.ts
9541
+ import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
9542
+ import { dirname as dirname7, join as join10 } from "path";
9543
+ import { nanoid as nanoid4 } from "nanoid";
9544
+ function logFilePath() {
9545
+ return join10(getAppDataDirectory(), "webhook-events.jsonl");
9546
+ }
9547
+ function ensureLoaded() {
9548
+ if (cache !== null) return cache;
9549
+ cache = [];
9550
+ try {
9551
+ const p = logFilePath();
9552
+ if (!existsSync17(p)) return cache;
9553
+ const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
9554
+ for (const line of lines) {
9555
+ try {
9556
+ cache.push(JSON.parse(line));
9557
+ } catch {
9558
+ }
9183
9559
  }
9184
- if (event.type !== "message") {
9185
- return { event: null, dropReason: "non_message_event" };
9560
+ if (cache.length > MAX_EVENTS) {
9561
+ cache = cache.slice(-MAX_EVENTS);
9562
+ try {
9563
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
9564
+ } catch {
9565
+ }
9186
9566
  }
9187
- return { event: null, dropReason: "unsupported_type" };
9567
+ } catch {
9188
9568
  }
9189
- const text = (event.text ?? "").trim();
9190
- if (!text) return { event: null, dropReason: "empty_text" };
9191
- const policy = getSlackAllowlistPolicy();
9192
- const userAllowlistActive = policy.allowedUsers.length > 0;
9193
- const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
9194
- if (isDm) {
9195
- if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
9196
- return { event: null, dropReason: "dm_blocked" };
9197
- }
9198
- } else {
9199
- const channelAllowlistActive = policy.allowedChannels.length > 0;
9200
- if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
9201
- return { event: null, dropReason: "channel_not_allowed" };
9202
- }
9203
- if (!userOk) {
9204
- return { event: null, dropReason: "user_not_allowed" };
9205
- }
9569
+ return cache;
9570
+ }
9571
+ function appendEvent(ev) {
9572
+ const list = ensureLoaded();
9573
+ list.push(ev);
9574
+ if (list.length > MAX_EVENTS) list.shift();
9575
+ try {
9576
+ const p = logFilePath();
9577
+ mkdirSync7(dirname7(p), { recursive: true });
9578
+ appendFileSync3(p, JSON.stringify(ev) + "\n");
9579
+ } catch {
9206
9580
  }
9207
- const ref = {
9208
- channel: "slack",
9209
- slackChannel: event.channel,
9210
- threadTs: event.thread_ts || event.ts,
9211
- teamId: event.team,
9212
- user: event.user
9213
- };
9214
- const label = slackChannel.displayLabel(ref);
9215
- return {
9216
- event: {
9217
- ref,
9218
- content: `[${label}] ${text}`,
9219
- wake: "now",
9220
- enqueuedAt: /* @__PURE__ */ new Date()
9221
- }
9222
- };
9223
9581
  }
9224
- var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
9225
- var init_slack = __esm({
9226
- "src/integrations/channels/slack.ts"() {
9582
+ function recordEvent(ev) {
9583
+ const full = {
9584
+ id: ev.id ?? nanoid4(),
9585
+ ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
9586
+ source: ev.source,
9587
+ status: ev.status,
9588
+ subtype: ev.subtype,
9589
+ channel: ev.channel,
9590
+ user: ev.user,
9591
+ textSnippet: ev.textSnippet?.slice(0, 200),
9592
+ dropReason: ev.dropReason,
9593
+ error: ev.error,
9594
+ sessionId: ev.sessionId,
9595
+ durationMs: ev.durationMs,
9596
+ meta: ev.meta
9597
+ };
9598
+ appendEvent(full);
9599
+ return full.id;
9600
+ }
9601
+ function updateEvent(id, patch) {
9602
+ const list = ensureLoaded();
9603
+ const i = list.findIndex((e) => e.id === id);
9604
+ if (i < 0) return;
9605
+ list[i] = { ...list[i], ...patch };
9606
+ try {
9607
+ const p = logFilePath();
9608
+ mkdirSync7(dirname7(p), { recursive: true });
9609
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
9610
+ } catch {
9611
+ }
9612
+ }
9613
+ function listEvents(filter = {}) {
9614
+ const list = ensureLoaded();
9615
+ const q = filter.q?.toLowerCase();
9616
+ const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
9617
+ const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
9618
+ const matched = list.filter((e) => {
9619
+ if (filter.source && e.source !== filter.source) return false;
9620
+ if (filter.status && e.status !== filter.status) return false;
9621
+ const t = Date.parse(e.ts);
9622
+ if (t < sinceTs) return false;
9623
+ if (t >= beforeTs) return false;
9624
+ if (q) {
9625
+ const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
9626
+ if (!hay.includes(q)) return false;
9627
+ }
9628
+ return true;
9629
+ });
9630
+ matched.reverse();
9631
+ const offset = Math.max(0, filter.offset ?? 0);
9632
+ const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
9633
+ return {
9634
+ events: matched.slice(offset, offset + limit),
9635
+ total: matched.length
9636
+ };
9637
+ }
9638
+ function clearAllEvents() {
9639
+ cache = [];
9640
+ try {
9641
+ writeFileSync4(logFilePath(), "");
9642
+ } catch {
9643
+ }
9644
+ }
9645
+ var MAX_EVENTS, cache;
9646
+ var init_webhook_events = __esm({
9647
+ "src/orchestrator/webhook-events.ts"() {
9648
+ "use strict";
9649
+ init_config();
9650
+ MAX_EVENTS = 1e3;
9651
+ cache = null;
9652
+ }
9653
+ });
9654
+
9655
+ // src/orchestrator/inbox.ts
9656
+ var inbox_exports = {};
9657
+ __export(inbox_exports, {
9658
+ clearInbox: () => clearInbox,
9659
+ flush: () => flush,
9660
+ peekInbox: () => peekInbox,
9661
+ pushToInbox: () => pushToInbox,
9662
+ setFlushHandler: () => setFlushHandler
9663
+ });
9664
+ function setFlushHandler(fn) {
9665
+ flushHandler = fn;
9666
+ }
9667
+ function entryFor(sessionId) {
9668
+ let e = inboxes.get(sessionId);
9669
+ if (!e) {
9670
+ e = { pending: [] };
9671
+ inboxes.set(sessionId, e);
9672
+ }
9673
+ return e;
9674
+ }
9675
+ function pushToInbox(orchestratorSessionId, event) {
9676
+ const e = entryFor(orchestratorSessionId);
9677
+ e.pending.push(event);
9678
+ try {
9679
+ trackInbound(orchestratorSessionId, event);
9680
+ } catch {
9681
+ }
9682
+ if (event.wake === "now") {
9683
+ scheduleFlush(orchestratorSessionId);
9684
+ }
9685
+ }
9686
+ function scheduleFlush(sessionId) {
9687
+ const e = inboxes.get(sessionId);
9688
+ if (!e) return;
9689
+ if (e.timer) clearTimeout(e.timer);
9690
+ e.timer = setTimeout(() => {
9691
+ void flush(sessionId);
9692
+ }, FLUSH_DEBOUNCE_MS);
9693
+ }
9694
+ async function flush(sessionId) {
9695
+ const e = inboxes.get(sessionId);
9696
+ if (!e) return;
9697
+ if (e.timer) {
9698
+ clearTimeout(e.timer);
9699
+ e.timer = void 0;
9700
+ }
9701
+ const events = e.pending.splice(0);
9702
+ if (events.length === 0) return;
9703
+ if (!flushHandler) {
9704
+ console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
9705
+ return;
9706
+ }
9707
+ try {
9708
+ await flushHandler(sessionId, events);
9709
+ } catch (err) {
9710
+ console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
9711
+ }
9712
+ }
9713
+ function peekInbox(sessionId) {
9714
+ return inboxes.get(sessionId)?.pending.slice() ?? [];
9715
+ }
9716
+ function clearInbox(sessionId) {
9717
+ const e = inboxes.get(sessionId);
9718
+ if (!e) return;
9719
+ if (e.timer) {
9720
+ clearTimeout(e.timer);
9721
+ e.timer = void 0;
9722
+ }
9723
+ e.pending.length = 0;
9724
+ }
9725
+ var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
9726
+ var init_inbox = __esm({
9727
+ "src/orchestrator/inbox.ts"() {
9728
+ "use strict";
9729
+ init_inbox_acks();
9730
+ inboxes = /* @__PURE__ */ new Map();
9731
+ FLUSH_DEBOUNCE_MS = 200;
9732
+ flushHandler = null;
9733
+ }
9734
+ });
9735
+
9736
+ // src/orchestrator/inbox-acks.ts
9737
+ var inbox_acks_exports = {};
9738
+ __export(inbox_acks_exports, {
9739
+ MAX_ATTEMPTS: () => MAX_ATTEMPTS,
9740
+ RECONCILE_EVERY_MS: () => RECONCILE_EVERY_MS,
9741
+ REPLAY_AFTER_MS: () => REPLAY_AFTER_MS,
9742
+ __getAck: () => __getAck,
9743
+ __listAcks: () => __listAcks,
9744
+ __resetAcks: () => __resetAcks,
9745
+ eventKey: () => eventKey,
9746
+ markRespondedForThread: () => markRespondedForThread,
9747
+ markState: () => markState,
9748
+ reconcileOnce: () => reconcileOnce,
9749
+ resolveBatchOnTurnEnd: () => resolveBatchOnTurnEnd,
9750
+ startReconciler: () => startReconciler,
9751
+ stopReconciler: () => stopReconciler,
9752
+ trackInbound: () => trackInbound
9753
+ });
9754
+ function eventKey(event) {
9755
+ const ref = event.ref;
9756
+ const ch = ref?.channel ?? "unknown";
9757
+ switch (ch) {
9758
+ case "slack":
9759
+ return `slack${SEP}${ref.slackChannel}${SEP}${ref.messageTs ?? ref.threadTs ?? ""}`;
9760
+ case "system":
9761
+ return `system${SEP}${ref.workerId}${SEP}${ref.kind}`;
9762
+ case "webhook":
9763
+ return `webhook${SEP}${ref.webhookId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9764
+ case "schedule":
9765
+ return `schedule${SEP}${ref.scheduleId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9766
+ default:
9767
+ return `${ch}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}${SEP}${event.content.slice(0, 40)}`;
9768
+ }
9769
+ }
9770
+ function trackInbound(sessionId, event) {
9771
+ if (event.wake === "never") return null;
9772
+ const key2 = eventKey(event);
9773
+ const existing = ledger.get(key2);
9774
+ if (existing) return key2;
9775
+ const ref = event.ref;
9776
+ const now = Date.now();
9777
+ const entry2 = {
9778
+ key: key2,
9779
+ sessionId,
9780
+ event,
9781
+ channel: ref?.channel ?? "unknown",
9782
+ state: "working",
9783
+ attempts: 0,
9784
+ trackedAt: now,
9785
+ updatedAt: now
9786
+ };
9787
+ if (ref?.channel === "slack") {
9788
+ entry2.slackChannel = ref.slackChannel;
9789
+ entry2.threadTs = ref.threadTs;
9790
+ entry2.messageTs = ref.messageTs;
9791
+ }
9792
+ ledger.set(key2, entry2);
9793
+ if (ledger.size > MAX_ENTRIES) pruneOldest();
9794
+ return key2;
9795
+ }
9796
+ function markState(key2, state2) {
9797
+ const entry2 = ledger.get(key2);
9798
+ if (!entry2) return;
9799
+ if (TERMINAL.has(entry2.state)) return;
9800
+ entry2.state = state2;
9801
+ entry2.updatedAt = Date.now();
9802
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9803
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, state2);
9804
+ }
9805
+ }
9806
+ function markRespondedForThread(slackChannel2, threadTs) {
9807
+ if (!slackChannel2 || !threadTs) return;
9808
+ for (const entry2 of ledger.values()) {
9809
+ if (entry2.channel === "slack" && entry2.state === "working" && entry2.slackChannel === slackChannel2 && entry2.threadTs === threadTs) {
9810
+ markState(entry2.key, "responded");
9811
+ }
9812
+ }
9813
+ }
9814
+ function resolveBatchOnTurnEnd(events, ok) {
9815
+ if (!ok) return;
9816
+ for (const ev of events) {
9817
+ const key2 = eventKey(ev);
9818
+ const entry2 = ledger.get(key2);
9819
+ if (!entry2 || entry2.state !== "working") continue;
9820
+ if (entry2.channel === "slack") continue;
9821
+ markState(key2, "responded");
9822
+ }
9823
+ }
9824
+ async function reconcileOnce(now = Date.now()) {
9825
+ let pushToInbox2 = null;
9826
+ const toReplay = [];
9827
+ for (const entry2 of ledger.values()) {
9828
+ if (TERMINAL.has(entry2.state)) {
9829
+ if (now - entry2.updatedAt > PRUNE_AFTER_MS) ledger.delete(entry2.key);
9830
+ continue;
9831
+ }
9832
+ if (isSessionLocked(entry2.sessionId)) continue;
9833
+ if (now - entry2.updatedAt < REPLAY_AFTER_MS) continue;
9834
+ if (entry2.attempts >= MAX_ATTEMPTS) {
9835
+ failEntry(entry2);
9836
+ continue;
9837
+ }
9838
+ toReplay.push(entry2);
9839
+ }
9840
+ if (toReplay.length === 0) return;
9841
+ try {
9842
+ ({ pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports)));
9843
+ } catch {
9844
+ return;
9845
+ }
9846
+ for (const entry2 of toReplay) {
9847
+ entry2.attempts += 1;
9848
+ entry2.updatedAt = Date.now();
9849
+ const nudged = {
9850
+ ...entry2.event,
9851
+ content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply, you can ignore it.]
9852
+ ${entry2.event.content}`,
9853
+ wake: "now"
9854
+ };
9855
+ try {
9856
+ pushToInbox2(entry2.sessionId, nudged);
9857
+ } catch {
9858
+ }
9859
+ }
9860
+ }
9861
+ function failEntry(entry2) {
9862
+ entry2.state = "failed";
9863
+ entry2.updatedAt = Date.now();
9864
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9865
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, "failed");
9866
+ if (entry2.threadTs) {
9867
+ fireFallback(
9868
+ entry2.slackChannel,
9869
+ entry2.threadTs,
9870
+ `:warning: I wasn't able to handle this after ${entry2.attempts} attempt(s). It may need a human \u2014 flagging it here so it isn't lost.`
9871
+ );
9872
+ }
9873
+ }
9874
+ recordEvent({
9875
+ source: "daemon",
9876
+ status: "failed",
9877
+ channel: entry2.channel,
9878
+ sessionId: entry2.sessionId,
9879
+ error: `unacknowledged after ${entry2.attempts} replay attempt(s)`,
9880
+ textSnippet: entry2.event.content.slice(0, 200),
9881
+ meta: { ackKey: entry2.key, ackState: "failed" }
9882
+ });
9883
+ }
9884
+ function pruneOldest() {
9885
+ const terminal = [];
9886
+ for (const e of ledger.values()) if (TERMINAL.has(e.state)) terminal.push(e);
9887
+ terminal.sort((a, b) => a.updatedAt - b.updatedAt);
9888
+ for (const e of terminal) {
9889
+ if (ledger.size <= MAX_ENTRIES) break;
9890
+ ledger.delete(e.key);
9891
+ }
9892
+ while (ledger.size > MAX_ENTRIES) {
9893
+ const oldest = ledger.keys().next().value;
9894
+ if (!oldest) break;
9895
+ ledger.delete(oldest);
9896
+ }
9897
+ }
9898
+ function fireResultReaction(channel, ts, state2) {
9899
+ if (typeof addResultReaction !== "function") return;
9900
+ try {
9901
+ void Promise.resolve(addResultReaction(channel, ts, state2)).catch(() => {
9902
+ });
9903
+ } catch {
9904
+ }
9905
+ }
9906
+ function fireFallback(channel, threadTs, text) {
9907
+ if (typeof postThreadMessage !== "function") return;
9908
+ try {
9909
+ void Promise.resolve(postThreadMessage(channel, threadTs, text)).catch(() => {
9910
+ });
9911
+ } catch {
9912
+ }
9913
+ }
9914
+ function startReconciler() {
9915
+ if (reconcileTimer) return;
9916
+ reconcileTimer = setInterval(() => {
9917
+ void reconcileOnce();
9918
+ }, RECONCILE_EVERY_MS);
9919
+ if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
9920
+ }
9921
+ function stopReconciler() {
9922
+ if (reconcileTimer) {
9923
+ clearInterval(reconcileTimer);
9924
+ reconcileTimer = null;
9925
+ }
9926
+ }
9927
+ function __getAck(key2) {
9928
+ return ledger.get(key2);
9929
+ }
9930
+ function __listAcks() {
9931
+ return [...ledger.values()];
9932
+ }
9933
+ function __resetAcks() {
9934
+ ledger.clear();
9935
+ }
9936
+ var REPLAY_AFTER_MS, RECONCILE_EVERY_MS, MAX_ATTEMPTS, PRUNE_AFTER_MS, MAX_ENTRIES, TERMINAL, SEP, ledger, reconcileTimer;
9937
+ var init_inbox_acks = __esm({
9938
+ "src/orchestrator/inbox-acks.ts"() {
9939
+ "use strict";
9940
+ init_session_lock();
9941
+ init_webhook_events();
9942
+ init_client3();
9943
+ REPLAY_AFTER_MS = 3 * 6e4;
9944
+ RECONCILE_EVERY_MS = 6e4;
9945
+ MAX_ATTEMPTS = 2;
9946
+ PRUNE_AFTER_MS = 60 * 6e4;
9947
+ MAX_ENTRIES = 5e3;
9948
+ TERMINAL = /* @__PURE__ */ new Set(["responded", "skipped", "handed_off", "failed"]);
9949
+ SEP = "\u241F";
9950
+ ledger = /* @__PURE__ */ new Map();
9951
+ reconcileTimer = null;
9952
+ }
9953
+ });
9954
+
9955
+ // src/integrations/channels/slack.ts
9956
+ function threadKey(channel, threadTs) {
9957
+ return `${channel}\u241F${threadTs}`;
9958
+ }
9959
+ function markThreadOwned(channel, threadTs) {
9960
+ ownedThreads.add(threadKey(channel, threadTs));
9961
+ }
9962
+ function isThreadOwned(channel, threadTs) {
9963
+ return ownedThreads.has(threadKey(channel, threadTs));
9964
+ }
9965
+ function isSelfAuthored(event, self) {
9966
+ if (!self) return true;
9967
+ if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
9968
+ if (self.botUserId && event.user && event.user === self.botUserId) return true;
9969
+ return false;
9970
+ }
9971
+ function slackEventToInboundResult(event, opts = {}) {
9972
+ if (!event) return { event: null, dropReason: "empty_text" };
9973
+ const self = opts.self ?? getCachedSlackSelfIdentity();
9974
+ const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
9975
+ if (isBotAuthored && isSelfAuthored(event, self)) {
9976
+ return { event: null, dropReason: "bot_message" };
9977
+ }
9978
+ if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
9979
+ return { event: null, dropReason: "ignored_subtype" };
9980
+ }
9981
+ const isDm = event.type === "message" && event.channel_type === "im";
9982
+ const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
9983
+ const isNonThreadChannelMsg = event.type === "message" && !isDm && !isThreadReply && (event.channel_type === "channel" || event.channel_type === "group" || event.channel_type === "mpim" || // Some payload shapes omit channel_type for channel messages.
9984
+ typeof event.channel === "string");
9985
+ if (event.type !== "app_mention" && !isDm && !isThreadReply) {
9986
+ if (isNonThreadChannelMsg) {
9987
+ return { event: null, dropReason: "non_thread_channel_msg" };
9988
+ }
9989
+ if (event.type !== "message") {
9990
+ return { event: null, dropReason: "non_message_event" };
9991
+ }
9992
+ return { event: null, dropReason: "unsupported_type" };
9993
+ }
9994
+ const text = (event.text ?? "").trim();
9995
+ const hasFiles = Array.isArray(event.files) && event.files.length > 0;
9996
+ if (!text && !hasFiles) return { event: null, dropReason: "empty_text" };
9997
+ const policy = getSlackAllowlistPolicy();
9998
+ const userAllowlistActive = policy.allowedUsers.length > 0;
9999
+ const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
10000
+ if (isDm) {
10001
+ if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
10002
+ return { event: null, dropReason: "dm_blocked" };
10003
+ }
10004
+ } else {
10005
+ const channelAllowlistActive = policy.allowedChannels.length > 0;
10006
+ if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
10007
+ return { event: null, dropReason: "channel_not_allowed" };
10008
+ }
10009
+ if (!userOk) {
10010
+ return { event: null, dropReason: "user_not_allowed" };
10011
+ }
10012
+ }
10013
+ const ref = {
10014
+ channel: "slack",
10015
+ slackChannel: event.channel,
10016
+ // For thread replies, threadTs points at the parent (so our reply
10017
+ // continues the thread). messageTs is the inbound message's own ts —
10018
+ // used by reaction add/remove (which target the message itself, not
10019
+ // its parent) and any future "edit this message" operations.
10020
+ threadTs: event.thread_ts || event.ts,
10021
+ messageTs: event.ts,
10022
+ teamId: event.team,
10023
+ user: event.user
10024
+ };
10025
+ const label = slackChannel.displayLabel(ref);
10026
+ return {
10027
+ event: {
10028
+ ref,
10029
+ content: `[${label}] ${text}`,
10030
+ wake: "now",
10031
+ enqueuedAt: /* @__PURE__ */ new Date()
10032
+ }
10033
+ };
10034
+ }
10035
+ var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
10036
+ var init_slack = __esm({
10037
+ "src/integrations/channels/slack.ts"() {
9227
10038
  "use strict";
9228
10039
  init_client3();
9229
10040
  ownedThreads = /* @__PURE__ */ new Set();
@@ -9243,6 +10054,8 @@ var init_slack = __esm({
9243
10054
  if (r.slackChannel && r.threadTs) {
9244
10055
  markThreadOwned(r.slackChannel, r.threadTs);
9245
10056
  noteBotPostedInThread(r.slackChannel, r.threadTs);
10057
+ void Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports)).then((m) => m.markRespondedForThread(r.slackChannel, r.threadTs)).catch(() => {
10058
+ });
9246
10059
  }
9247
10060
  },
9248
10061
  displayLabel(ref) {
@@ -9275,8 +10088,14 @@ var init_slack = __esm({
9275
10088
  // also-broadcast-to-channel replies; the regular thread reply already fires
9276
10089
  "message_replied",
9277
10090
  // legacy parent-thread bump
9278
- "file_share",
9279
- // we'd handle these later; for now skip to avoid double-handling
10091
+ // NOTE: `file_share` is intentionally NOT ignored. It's the subtype Slack
10092
+ // attaches to a normal user message that includes file uploads — the
10093
+ // event still has `text`, `user`, `channel`, and `files: [...]`. The
10094
+ // route handler (src/server/routes/slack.ts) hands the `files[]` array
10095
+ // to `ingestSlackFiles` (src/integrations/slack/files.ts), which
10096
+ // downloads each file using the bot token, re-uploads it to GCS via
10097
+ // the remote-server's /storage/upload-url endpoint, and appends the
10098
+ // resulting short public URLs to the inbound message content.
9280
10099
  "reply_broadcast",
9281
10100
  "tombstone",
9282
10101
  "huddle_thread"
@@ -9485,7 +10304,7 @@ var init_messenger = __esm({
9485
10304
  });
9486
10305
 
9487
10306
  // src/orchestrator/schedules-store.ts
9488
- import { nanoid as nanoid4 } from "nanoid";
10307
+ import { nanoid as nanoid5 } from "nanoid";
9489
10308
  async function readOrch(orchestratorSessionId) {
9490
10309
  const s = await sessionQueries.getById(orchestratorSessionId);
9491
10310
  if (!s) return null;
@@ -9500,7 +10319,7 @@ async function createSchedule(orchestratorSessionId, input) {
9500
10319
  const data = await readOrch(orchestratorSessionId);
9501
10320
  if (!data) throw new Error("orchestrator session not found");
9502
10321
  const row = {
9503
- id: `sch_${nanoid4(10)}`,
10322
+ id: `sch_${nanoid5(10)}`,
9504
10323
  name: input.name,
9505
10324
  cron: input.cron,
9506
10325
  prompt: input.prompt,
@@ -9537,7 +10356,7 @@ var init_schedules_store = __esm({
9537
10356
 
9538
10357
  // src/orchestrator/webhooks-store.ts
9539
10358
  import { randomBytes } from "crypto";
9540
- import { nanoid as nanoid5 } from "nanoid";
10359
+ import { nanoid as nanoid6 } from "nanoid";
9541
10360
  function newToken() {
9542
10361
  return randomBytes(24).toString("base64url");
9543
10362
  }
@@ -9554,7 +10373,7 @@ async function createWebhook(orchestratorSessionId, input) {
9554
10373
  const data = await readOrch2(orchestratorSessionId);
9555
10374
  if (!data) throw new Error("orchestrator session not found");
9556
10375
  const row = {
9557
- id: `whk_${nanoid5(10)}`,
10376
+ id: `whk_${nanoid6(10)}`,
9558
10377
  name: input.name,
9559
10378
  token: newToken(),
9560
10379
  wake: input.wake ?? "now",
@@ -9699,7 +10518,9 @@ function buildAgentTool(opts) {
9699
10518
  workingDirectory: input.workingDirectory ?? opts.defaultWorkingDirectory,
9700
10519
  name: input.name,
9701
10520
  maxIterations: input.maxIterations ?? 100,
9702
- orchestratorSessionId: opts.orchestratorSessionId
10521
+ orchestratorSessionId: opts.orchestratorSessionId,
10522
+ ...input.mcpServers ? { mcpServers: input.mcpServers } : {},
10523
+ ...input.skills ? { skills: input.skills } : {}
9703
10524
  }
9704
10525
  });
9705
10526
  return {
@@ -9872,6 +10693,26 @@ var init_orchestrator_actions = __esm({
9872
10693
  model: z14.string().optional().describe("spawn only: model override."),
9873
10694
  workingDirectory: z14.string().optional().describe("spawn only: working directory override."),
9874
10695
  maxIterations: z14.number().int().min(1).max(500).optional().describe("spawn only."),
10696
+ mcpServers: z14.array(
10697
+ z14.object({
10698
+ name: z14.string(),
10699
+ transport: z14.enum(["http", "sse", "stdio"]),
10700
+ url: z14.string().optional(),
10701
+ headers: z14.record(z14.string(), z14.string()).optional(),
10702
+ command: z14.string().optional(),
10703
+ args: z14.array(z14.string()).optional(),
10704
+ env: z14.record(z14.string(), z14.string()).optional()
10705
+ })
10706
+ ).optional().describe("spawn only: task-scoped MCP servers (with auth headers) connected for this worker only, tools exposed as mcp_<name>_<tool>."),
10707
+ skills: z14.array(
10708
+ z14.object({
10709
+ name: z14.string(),
10710
+ description: z14.string().optional(),
10711
+ content: z14.string(),
10712
+ alwaysApply: z14.boolean().optional(),
10713
+ globs: z14.array(z14.string()).optional()
10714
+ })
10715
+ ).optional().describe("spawn only: task-scoped skills (inline markdown) available to this worker only."),
9875
10716
  // message
9876
10717
  text: z14.string().optional().describe("message only: the text to deliver to the worker."),
9877
10718
  force: z14.boolean().optional().describe("message only: soft-interrupt the current step."),
@@ -9911,9 +10752,9 @@ var init_orchestrator_actions = __esm({
9911
10752
  });
9912
10753
 
9913
10754
  // src/integrations/mcp/store.ts
9914
- import { nanoid as nanoid6 } from "nanoid";
9915
- import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
9916
- import { resolve as resolve10, join as join10 } from "path";
10755
+ import { nanoid as nanoid7 } from "nanoid";
10756
+ import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
10757
+ import { resolve as resolve10, join as join11 } from "path";
9917
10758
  function readServers() {
9918
10759
  try {
9919
10760
  const cfg = getConfig();
@@ -9925,12 +10766,12 @@ function readServers() {
9925
10766
  function refreshMcpServersFromDisk() {
9926
10767
  const candidates = [
9927
10768
  resolve10(process.cwd(), "sparkecoder.config.json"),
9928
- join10(ensureAppDataDirectory(), "sparkecoder.config.json")
10769
+ join11(ensureAppDataDirectory(), "sparkecoder.config.json")
9929
10770
  ];
9930
10771
  for (const path of candidates) {
9931
- if (!existsSync17(path)) continue;
10772
+ if (!existsSync18(path)) continue;
9932
10773
  try {
9933
- const raw = JSON.parse(readFileSync8(path, "utf-8"));
10774
+ const raw = JSON.parse(readFileSync9(path, "utf-8"));
9934
10775
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
9935
10776
  setMcpServers(servers2);
9936
10777
  return servers2;
@@ -9949,7 +10790,7 @@ function createMcpServer(input) {
9949
10790
  const all = readServers();
9950
10791
  validateInput(input);
9951
10792
  const row = {
9952
- id: `mcp_${nanoid6(10)}`,
10793
+ id: `mcp_${nanoid7(10)}`,
9953
10794
  name: sanitizeName(input.name),
9954
10795
  transport: input.transport,
9955
10796
  url: input.url,
@@ -10120,6 +10961,159 @@ var init_pool = __esm({
10120
10961
  }
10121
10962
  });
10122
10963
 
10964
+ // src/integrations/mcp/task-scoped.ts
10965
+ import { createMCPClient as createMCPClient2 } from "@ai-sdk/mcp";
10966
+ function sanitizeName2(raw) {
10967
+ return raw.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/_+/g, "_");
10968
+ }
10969
+ function buildHttpLikeTransport(server) {
10970
+ if (!server.url) {
10971
+ throw new Error(`${server.transport} transport requires a url`);
10972
+ }
10973
+ return {
10974
+ type: server.transport,
10975
+ url: server.url,
10976
+ headers: server.headers
10977
+ };
10978
+ }
10979
+ async function buildStdioTransport2(server) {
10980
+ if (!server.command) {
10981
+ throw new Error("stdio transport requires a command");
10982
+ }
10983
+ const mod = await import("@ai-sdk/mcp/mcp-stdio");
10984
+ const Cls = mod.Experimental_StdioMCPTransport || mod.StdioClientTransport;
10985
+ if (!Cls) throw new Error("@ai-sdk/mcp/mcp-stdio is missing the stdio transport class");
10986
+ return new Cls({
10987
+ command: server.command,
10988
+ args: server.args ?? [],
10989
+ env: server.env
10990
+ });
10991
+ }
10992
+ async function buildTransport(server) {
10993
+ return server.transport === "stdio" ? await buildStdioTransport2(server) : buildHttpLikeTransport(server);
10994
+ }
10995
+ async function connectTaskMcpServers(servers2, opts = {}) {
10996
+ const tools = {};
10997
+ const connected = [];
10998
+ const errors = [];
10999
+ const clients2 = [];
11000
+ for (const raw of servers2 ?? []) {
11001
+ const name = sanitizeName2(raw.name || "");
11002
+ if (!name) {
11003
+ errors.push({ name: raw.name || "(unnamed)", error: "server name is required" });
11004
+ continue;
11005
+ }
11006
+ let client = null;
11007
+ try {
11008
+ const transport = await buildTransport(raw);
11009
+ client = await createMCPClient2({ transport });
11010
+ clients2.push(client);
11011
+ const serverTools = await client.tools();
11012
+ for (const [toolName, t] of Object.entries(serverTools)) {
11013
+ tools[`mcp_${name}_${toolName}`] = t;
11014
+ }
11015
+ connected.push(name);
11016
+ } catch (err) {
11017
+ const message = err?.message || String(err);
11018
+ errors.push({ name, error: message });
11019
+ if (!opts.quiet) {
11020
+ console.warn(`[mcp:task] connecting "${name}" failed: ${message}`);
11021
+ }
11022
+ if (client) {
11023
+ try {
11024
+ await client.close();
11025
+ } catch {
11026
+ }
11027
+ const idx = clients2.indexOf(client);
11028
+ if (idx >= 0) clients2.splice(idx, 1);
11029
+ }
11030
+ }
11031
+ }
11032
+ let closed = false;
11033
+ const close = async () => {
11034
+ if (closed) return;
11035
+ closed = true;
11036
+ await Promise.all(
11037
+ clients2.map(async (c) => {
11038
+ try {
11039
+ await c.close();
11040
+ } catch {
11041
+ }
11042
+ })
11043
+ );
11044
+ };
11045
+ return { tools, connected, errors, close };
11046
+ }
11047
+ var init_task_scoped = __esm({
11048
+ "src/integrations/mcp/task-scoped.ts"() {
11049
+ "use strict";
11050
+ }
11051
+ });
11052
+
11053
+ // src/skills/task-scoped.ts
11054
+ import { mkdtemp, writeFile as writeFile5, rm } from "fs/promises";
11055
+ import { tmpdir } from "os";
11056
+ import { join as join12 } from "path";
11057
+ function safeFileName(name, index) {
11058
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
11059
+ return `${base || `skill-${index + 1}`}.md`;
11060
+ }
11061
+ function escapeFrontmatterValue(value) {
11062
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11063
+ }
11064
+ function buildSkillFile(skill) {
11065
+ const lines = ["---"];
11066
+ lines.push(`name: ${escapeFrontmatterValue(skill.name)}`);
11067
+ lines.push(`description: ${escapeFrontmatterValue(skill.description || skill.name)}`);
11068
+ if (skill.alwaysApply) lines.push("alwaysApply: true");
11069
+ if (skill.globs && skill.globs.length > 0) {
11070
+ lines.push(`globs: [${skill.globs.map((g) => escapeFrontmatterValue(g)).join(", ")}]`);
11071
+ }
11072
+ lines.push("---");
11073
+ lines.push("");
11074
+ lines.push(skill.content);
11075
+ return lines.join("\n");
11076
+ }
11077
+ async function materializeTaskSkills(skills2, taskId) {
11078
+ if (!skills2 || skills2.length === 0) return null;
11079
+ const safeTaskId = taskId.replace(/[^a-zA-Z0-9_-]+/g, "_");
11080
+ const dir = await mkdtemp(join12(tmpdir(), `sparkecoder-task-skills-${safeTaskId}-`));
11081
+ const seen = /* @__PURE__ */ new Set();
11082
+ await Promise.all(
11083
+ skills2.map(async (skill, i) => {
11084
+ let fileName = safeFileName(skill.name, i);
11085
+ while (seen.has(fileName)) fileName = `dup-${i}-${fileName}`;
11086
+ seen.add(fileName);
11087
+ await writeFile5(join12(dir, fileName), buildSkillFile(skill), "utf-8");
11088
+ })
11089
+ );
11090
+ const loaded2 = await loadSkillsFromDirectory(dir, { priority: 1, defaultLoadType: "on_demand" });
11091
+ const alwaysSkills = loaded2.filter((s) => s.alwaysApply || s.loadType === "always");
11092
+ const onDemand = loaded2.filter((s) => !(s.alwaysApply || s.loadType === "always"));
11093
+ const always = (await Promise.all(
11094
+ alwaysSkills.map(async (s) => {
11095
+ const withContent = await loadSkillContent(s.name, [dir]);
11096
+ return withContent ? { ...s, content: withContent.content } : null;
11097
+ })
11098
+ )).filter((s) => s !== null);
11099
+ let cleaned = false;
11100
+ const cleanup2 = async () => {
11101
+ if (cleaned) return;
11102
+ cleaned = true;
11103
+ try {
11104
+ await rm(dir, { recursive: true, force: true });
11105
+ } catch {
11106
+ }
11107
+ };
11108
+ return { dir, always, onDemand, cleanup: cleanup2 };
11109
+ }
11110
+ var init_task_scoped2 = __esm({
11111
+ "src/skills/task-scoped.ts"() {
11112
+ "use strict";
11113
+ init_skills();
11114
+ }
11115
+ });
11116
+
10123
11117
  // src/utils/webhook.ts
10124
11118
  var webhook_exports = {};
10125
11119
  __export(webhook_exports, {
@@ -10278,79 +11272,57 @@ var init_pending_input = __esm({
10278
11272
  }
10279
11273
  });
10280
11274
 
10281
- // src/orchestrator/inbox.ts
10282
- var inbox_exports = {};
10283
- __export(inbox_exports, {
10284
- clearInbox: () => clearInbox,
10285
- flush: () => flush,
10286
- peekInbox: () => peekInbox,
10287
- pushToInbox: () => pushToInbox,
10288
- setFlushHandler: () => setFlushHandler
10289
- });
10290
- function setFlushHandler(fn) {
10291
- flushHandler = fn;
10292
- }
10293
- function entryFor(sessionId) {
10294
- let e = inboxes.get(sessionId);
10295
- if (!e) {
10296
- e = { pending: [] };
10297
- inboxes.set(sessionId, e);
10298
- }
10299
- return e;
11275
+ // src/utils/local-device-time.ts
11276
+ function formatLocalDeviceTimeLine(now = /* @__PURE__ */ new Date()) {
11277
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
11278
+ const formatted = now.toLocaleString("en-US", {
11279
+ weekday: "long",
11280
+ year: "numeric",
11281
+ month: "long",
11282
+ day: "numeric",
11283
+ hour: "numeric",
11284
+ minute: "2-digit",
11285
+ second: "2-digit",
11286
+ timeZoneName: "short"
11287
+ });
11288
+ return `${LOCAL_TIME_MARKER} ${formatted} (${timeZone})]`;
10300
11289
  }
10301
- function pushToInbox(orchestratorSessionId, event) {
10302
- const e = entryFor(orchestratorSessionId);
10303
- e.pending.push(event);
10304
- if (event.wake === "now") {
10305
- scheduleFlush(orchestratorSessionId);
10306
- }
11290
+ function hasLocalDeviceTimeLine(text) {
11291
+ return text.includes(LOCAL_TIME_MARKER);
10307
11292
  }
10308
- function scheduleFlush(sessionId) {
10309
- const e = inboxes.get(sessionId);
10310
- if (!e) return;
10311
- if (e.timer) clearTimeout(e.timer);
10312
- e.timer = setTimeout(() => {
10313
- void flush(sessionId);
10314
- }, FLUSH_DEBOUNCE_MS);
11293
+ function prependLocalDeviceTimeToUserMessage(text, now) {
11294
+ const trimmed = text.trim();
11295
+ if (!trimmed || hasLocalDeviceTimeLine(text)) return text;
11296
+ return `${formatLocalDeviceTimeLine(now)}
11297
+ ${text}`;
10315
11298
  }
10316
- async function flush(sessionId) {
10317
- const e = inboxes.get(sessionId);
10318
- if (!e) return;
10319
- if (e.timer) {
10320
- clearTimeout(e.timer);
10321
- e.timer = void 0;
10322
- }
10323
- const events = e.pending.splice(0);
10324
- if (events.length === 0) return;
10325
- if (!flushHandler) {
10326
- console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
10327
- return;
11299
+ function prependLocalDeviceTimeToUserContent(content, now) {
11300
+ if (typeof content === "string") {
11301
+ return prependLocalDeviceTimeToUserMessage(content, now);
10328
11302
  }
10329
- try {
10330
- await flushHandler(sessionId, events);
10331
- } catch (err) {
10332
- console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
11303
+ const line = formatLocalDeviceTimeLine(now);
11304
+ if (content.some((p) => p.type === "text" && p.text && hasLocalDeviceTimeLine(p.text))) {
11305
+ return content;
10333
11306
  }
10334
- }
10335
- function peekInbox(sessionId) {
10336
- return inboxes.get(sessionId)?.pending.slice() ?? [];
10337
- }
10338
- function clearInbox(sessionId) {
10339
- const e = inboxes.get(sessionId);
10340
- if (!e) return;
10341
- if (e.timer) {
10342
- clearTimeout(e.timer);
10343
- e.timer = void 0;
11307
+ const userIdx = content.findIndex(
11308
+ (p) => p.type === "text" && p.text?.includes("[USER MESSAGE]")
11309
+ );
11310
+ if (userIdx >= 0 && content[userIdx].text) {
11311
+ const copy = content.map((p) => ({ ...p }));
11312
+ copy[userIdx] = {
11313
+ ...copy[userIdx],
11314
+ text: `${line}
11315
+ ${copy[userIdx].text}`
11316
+ };
11317
+ return copy;
10344
11318
  }
10345
- e.pending.length = 0;
11319
+ return [{ type: "text", text: line }, ...content];
10346
11320
  }
10347
- var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
10348
- var init_inbox = __esm({
10349
- "src/orchestrator/inbox.ts"() {
11321
+ var LOCAL_TIME_MARKER;
11322
+ var init_local_device_time = __esm({
11323
+ "src/utils/local-device-time.ts"() {
10350
11324
  "use strict";
10351
- inboxes = /* @__PURE__ */ new Map();
10352
- FLUSH_DEBOUNCE_MS = 200;
10353
- flushHandler = null;
11325
+ LOCAL_TIME_MARKER = "[Local device time:";
10354
11326
  }
10355
11327
  });
10356
11328
 
@@ -10562,10 +11534,10 @@ __export(recorder_exports, {
10562
11534
  });
10563
11535
  import { exec as exec5 } from "child_process";
10564
11536
  import { promisify as promisify5 } from "util";
10565
- import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
10566
- import { join as join11 } from "path";
10567
- import { tmpdir } from "os";
10568
- import { nanoid as nanoid7 } from "nanoid";
11537
+ import { writeFile as writeFile6, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm as rm2 } from "fs/promises";
11538
+ import { join as join13 } from "path";
11539
+ import { tmpdir as tmpdir2 } from "os";
11540
+ import { nanoid as nanoid8 } from "nanoid";
10569
11541
  async function checkFfmpeg() {
10570
11542
  try {
10571
11543
  await execAsync5("ffmpeg -version", { timeout: 5e3 });
@@ -10576,7 +11548,7 @@ async function checkFfmpeg() {
10576
11548
  }
10577
11549
  async function cleanup(dir) {
10578
11550
  try {
10579
- await rm(dir, { recursive: true, force: true });
11551
+ await rm2(dir, { recursive: true, force: true });
10580
11552
  } catch {
10581
11553
  }
10582
11554
  }
@@ -10620,21 +11592,21 @@ var init_recorder = __esm({
10620
11592
  */
10621
11593
  async encode() {
10622
11594
  if (this.frames.length === 0) return null;
10623
- const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
11595
+ const workDir = join13(tmpdir2(), `sparkecoder-recording-${nanoid8(8)}`);
10624
11596
  await mkdir4(workDir, { recursive: true });
10625
11597
  try {
10626
11598
  for (let i = 0; i < this.frames.length; i++) {
10627
- const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
10628
- await writeFile5(framePath, this.frames[i].data);
11599
+ const framePath = join13(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
11600
+ await writeFile6(framePath, this.frames[i].data);
10629
11601
  }
10630
11602
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
10631
11603
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
10632
11604
  const clampedFps = Math.max(1, Math.min(fps, 30));
10633
- const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
11605
+ const outputPath = join13(workDir, `recording_${this.sessionId}.mp4`);
10634
11606
  const hasFfmpeg = await checkFfmpeg();
10635
11607
  if (hasFfmpeg) {
10636
11608
  await execAsync5(
10637
- `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
11609
+ `ffmpeg -y -framerate ${clampedFps} -i "${join13(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
10638
11610
  { timeout: 12e4 }
10639
11611
  );
10640
11612
  } else {
@@ -10646,7 +11618,7 @@ var init_recorder = __esm({
10646
11618
  const files = await readdir5(workDir);
10647
11619
  for (const f of files) {
10648
11620
  if (f.startsWith("frame_")) {
10649
- await unlink2(join11(workDir, f)).catch(() => {
11621
+ await unlink2(join13(workDir, f)).catch(() => {
10650
11622
  });
10651
11623
  }
10652
11624
  }
@@ -10675,7 +11647,7 @@ import {
10675
11647
  stepCountIs as stepCountIs2
10676
11648
  } from "ai";
10677
11649
  import { z as z15 } from "zod";
10678
- import { nanoid as nanoid8 } from "nanoid";
11650
+ import { nanoid as nanoid9 } from "nanoid";
10679
11651
  function anySignal(signals) {
10680
11652
  const ctrl = new AbortController();
10681
11653
  for (const s of signals) {
@@ -10719,10 +11691,13 @@ var init_agent = __esm({
10719
11691
  init_prompts();
10720
11692
  init_orchestrator_actions();
10721
11693
  init_pool();
11694
+ init_task_scoped();
11695
+ init_task_scoped2();
10722
11696
  init_webhook2();
10723
11697
  init_questions();
10724
11698
  init_pending_input();
10725
11699
  init_inbox();
11700
+ init_local_device_time();
10726
11701
  init_system();
10727
11702
  init_context();
10728
11703
  init_prompts();
@@ -10885,9 +11860,11 @@ ${prompt}` });
10885
11860
  */
10886
11861
  async stream(options) {
10887
11862
  const config = getConfig();
10888
- const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
11863
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11864
+ const userContent = this.buildUserMessageContent(prompt, options.attachments);
11865
+ const persistedUserContent = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserContent(userContent) : userContent;
10889
11866
  if (!options.skipSaveUserMessage) {
10890
- await this.context.addUserMessage(userContent);
11867
+ await this.context.addUserMessage(persistedUserContent);
10891
11868
  }
10892
11869
  await sessionQueries.updateStatus(this.session.id, "active");
10893
11870
  let systemPrompt = await buildSystemPrompt({
@@ -10965,7 +11942,8 @@ ${personality.trim()}
10965
11942
  */
10966
11943
  async run(options) {
10967
11944
  const config = getConfig();
10968
- await this.context.addUserMessage(options.prompt);
11945
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11946
+ await this.context.addUserMessage(prompt);
10969
11947
  const systemPrompt = await buildSystemPrompt({
10970
11948
  workingDirectory: this.session.workingDirectory,
10971
11949
  skillsDirectories: config.resolvedSkillsDirectories,
@@ -11009,355 +11987,387 @@ ${personality.trim()}
11009
11987
  */
11010
11988
  async runTask(options) {
11011
11989
  const config = getConfig();
11012
- const maxIterations = options.taskConfig.maxIterations ?? 50;
11013
- const webhookUrl = options.taskConfig.webhookUrl;
11014
- const parentTaskId = options.taskConfig.parentTaskId;
11015
- const fireWebhook = (type, data) => {
11016
- if (!webhookUrl) return;
11017
- sendWebhook(webhookUrl, {
11018
- type,
11019
- taskId: this.session.id,
11020
- sessionId: this.session.id,
11021
- ...parentTaskId ? { parentTaskId } : {},
11022
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11023
- data
11024
- });
11025
- };
11026
- const completion = { signal: null };
11027
- const onComplete = (signal) => {
11028
- completion.signal = signal;
11029
- };
11030
- let taskRecorder = null;
11031
- const sessionId = this.session.id;
11032
- const emit = options.writeSSE;
11033
- const bashProgressHandler = (progress) => {
11034
- options.onToolProgress?.({ toolName: "bash", data: progress });
11035
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
11036
- });
11037
- const port = progress.browserStreamPort;
11038
- if (port && progress.status === "started") {
11039
- Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
11040
- const proxy = getOrCreateProxy2(sessionId, port);
11041
- if (!taskRecorder) {
11042
- Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
11043
- taskRecorder = new FrameRecorder2(sessionId);
11044
- taskRecorder.start();
11045
- });
11046
- }
11047
- if (proxy.listenerCount("frame") === 0) {
11048
- proxy.on("frame", (frame) => {
11049
- taskRecorder?.addFrame(frame);
11050
- if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
11051
- });
11052
- });
11053
- proxy.on("status", (s) => {
11054
- if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
11055
- });
11056
- });
11057
- }
11990
+ const taskScopedCleanups = [];
11991
+ try {
11992
+ const maxIterations = options.taskConfig.maxIterations ?? 50;
11993
+ const webhookUrl = options.taskConfig.webhookUrl;
11994
+ const parentTaskId = options.taskConfig.parentTaskId;
11995
+ const fireWebhook = (type, data) => {
11996
+ if (!webhookUrl) return;
11997
+ sendWebhook(webhookUrl, {
11998
+ type,
11999
+ taskId: this.session.id,
12000
+ sessionId: this.session.id,
12001
+ ...parentTaskId ? { parentTaskId } : {},
12002
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12003
+ data
11058
12004
  });
12005
+ };
12006
+ const completion = { signal: null };
12007
+ const onComplete = (signal) => {
12008
+ completion.signal = signal;
12009
+ };
12010
+ let taskMcpTools = {};
12011
+ if (options.mcpServers && options.mcpServers.length > 0) {
12012
+ const mcpConnection = await connectTaskMcpServers(options.mcpServers, { quiet: true });
12013
+ taskScopedCleanups.push(mcpConnection.close);
12014
+ taskMcpTools = mcpConnection.tools;
12015
+ if (mcpConnection.connected.length > 0) {
12016
+ console.log(`[TASK] connected ${mcpConnection.connected.length} task-scoped MCP server(s): ${mcpConnection.connected.join(", ")}`);
12017
+ }
12018
+ for (const e of mcpConnection.errors) {
12019
+ console.warn(`[TASK] task-scoped MCP server "${e.name}" failed to connect: ${e.error}`);
12020
+ if (options.writeSSE) await options.writeSSE(JSON.stringify({ type: "task-mcp-error", data: { name: e.name, error: e.error } }));
12021
+ }
11059
12022
  }
11060
- };
11061
- const taskTools = await createTools({
11062
- sessionId: this.session.id,
11063
- workingDirectory: this.session.workingDirectory,
11064
- skillsDirectories: config.resolvedSkillsDirectories,
11065
- onBashProgress: bashProgressHandler,
11066
- onWriteFileProgress: (progress) => {
11067
- options.onToolProgress?.({ toolName: "write_file", data: progress });
11068
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
11069
- });
11070
- },
11071
- onSearchProgress: (progress) => {
11072
- options.onToolProgress?.({ toolName: "explore_agent", data: progress });
11073
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
12023
+ const materializedSkills = await materializeTaskSkills(options.skills, this.session.id);
12024
+ if (materializedSkills) taskScopedCleanups.push(materializedSkills.cleanup);
12025
+ const taskSkillsDir = materializedSkills?.dir;
12026
+ let taskRecorder = null;
12027
+ const sessionId = this.session.id;
12028
+ const emit = options.writeSSE;
12029
+ const bashProgressHandler = (progress) => {
12030
+ options.onToolProgress?.({ toolName: "bash", data: progress });
12031
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
11074
12032
  });
11075
- },
11076
- taskTools: {
11077
- outputSchema: options.taskConfig.outputSchema,
11078
- onComplete,
11079
- onQuestion: async (question) => {
11080
- const payload = {
11081
- questionId: question.questionId,
11082
- question: question.question,
11083
- context: question.context,
11084
- choices: question.choices,
11085
- status: "pending"
11086
- };
11087
- const answerPromise = waitForTaskQuestionAnswer({
11088
- taskId: this.session.id,
11089
- questionId: question.questionId,
11090
- question: question.question,
11091
- context: question.context,
11092
- choices: question.choices
12033
+ const port = progress.browserStreamPort;
12034
+ if (port && progress.status === "started") {
12035
+ Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
12036
+ const proxy = getOrCreateProxy2(sessionId, port);
12037
+ if (!taskRecorder) {
12038
+ Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
12039
+ taskRecorder = new FrameRecorder2(sessionId);
12040
+ taskRecorder.start();
12041
+ });
12042
+ }
12043
+ if (proxy.listenerCount("frame") === 0) {
12044
+ proxy.on("frame", (frame) => {
12045
+ taskRecorder?.addFrame(frame);
12046
+ if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
12047
+ });
12048
+ });
12049
+ proxy.on("status", (s) => {
12050
+ if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
12051
+ });
12052
+ });
12053
+ }
11093
12054
  });
11094
- fireWebhook("task.question", payload);
11095
- if (emit) {
11096
- await emit(JSON.stringify({ type: "task-question", data: payload }));
11097
- }
11098
- const orchId = this.session.config?.orchestratorSessionId;
11099
- if (orchId) {
11100
- pushToInbox(orchId, workerQuestionEvent(
11101
- this.session.id,
11102
- this.session.name || "worker",
11103
- question.question,
11104
- question.questionId
11105
- ));
11106
- }
11107
- const answer = await answerPromise;
11108
- const answeredPayload = {
11109
- questionId: question.questionId,
11110
- answer: answer.answer,
11111
- answeredBy: answer.answeredBy
11112
- };
11113
- fireWebhook("task.question_answered", answeredPayload);
11114
- if (emit) {
11115
- await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
12055
+ }
12056
+ };
12057
+ const taskTools = await createTools({
12058
+ sessionId: this.session.id,
12059
+ workingDirectory: this.session.workingDirectory,
12060
+ onBashProgress: bashProgressHandler,
12061
+ onWriteFileProgress: (progress) => {
12062
+ options.onToolProgress?.({ toolName: "write_file", data: progress });
12063
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
12064
+ });
12065
+ },
12066
+ onSearchProgress: (progress) => {
12067
+ options.onToolProgress?.({ toolName: "explore_agent", data: progress });
12068
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
12069
+ });
12070
+ },
12071
+ // Add the task-scoped skills temp dir (if any) so load_skill can list
12072
+ // and load the inline skills supplied with this task.
12073
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
12074
+ taskTools: {
12075
+ outputSchema: options.taskConfig.outputSchema,
12076
+ onComplete,
12077
+ onQuestion: async (question) => {
12078
+ const payload = {
12079
+ questionId: question.questionId,
12080
+ question: question.question,
12081
+ context: question.context,
12082
+ choices: question.choices,
12083
+ status: "pending"
12084
+ };
12085
+ const answerPromise = waitForTaskQuestionAnswer({
12086
+ taskId: this.session.id,
12087
+ questionId: question.questionId,
12088
+ question: question.question,
12089
+ context: question.context,
12090
+ choices: question.choices
12091
+ });
12092
+ fireWebhook("task.question", payload);
12093
+ if (emit) {
12094
+ await emit(JSON.stringify({ type: "task-question", data: payload }));
12095
+ }
12096
+ const orchId = this.session.config?.orchestratorSessionId;
12097
+ if (orchId) {
12098
+ pushToInbox(orchId, workerQuestionEvent(
12099
+ this.session.id,
12100
+ this.session.name || "worker",
12101
+ question.question,
12102
+ question.questionId
12103
+ ));
12104
+ }
12105
+ const answer = await answerPromise;
12106
+ const answeredPayload = {
12107
+ questionId: question.questionId,
12108
+ answer: answer.answer,
12109
+ answeredBy: answer.answeredBy
12110
+ };
12111
+ fireWebhook("task.question_answered", answeredPayload);
12112
+ if (emit) {
12113
+ await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
12114
+ }
12115
+ return answer;
11116
12116
  }
11117
- return answer;
11118
12117
  }
12118
+ });
12119
+ for (const [name, t] of Object.entries(taskMcpTools)) {
12120
+ taskTools[name] = t;
11119
12121
  }
11120
- });
11121
- const baseSystemPrompt = await buildSystemPrompt({
11122
- workingDirectory: this.session.workingDirectory,
11123
- skillsDirectories: config.resolvedSkillsDirectories,
11124
- sessionId: this.session.id,
11125
- discoveredSkills: config.discoveredSkills,
11126
- activeFiles: []
11127
- });
11128
- const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
11129
- const systemPrompt = `${baseSystemPrompt}
12122
+ const baseSystemPrompt = await buildSystemPrompt({
12123
+ workingDirectory: this.session.workingDirectory,
12124
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
12125
+ sessionId: this.session.id,
12126
+ discoveredSkills: config.discoveredSkills,
12127
+ activeFiles: [],
12128
+ taskScopedSkills: materializedSkills ? { always: materializedSkills.always, onDemand: materializedSkills.onDemand } : void 0
12129
+ });
12130
+ const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
12131
+ const systemPrompt = `${baseSystemPrompt}
11130
12132
 
11131
12133
  ${taskAddendum}`;
11132
- fireWebhook("task.started", { prompt: options.prompt });
11133
- if (emit) {
11134
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
11135
- }
11136
- await this.context.addUserMessage(options.prompt);
11137
- let iteration = 0;
11138
- while (iteration < maxIterations) {
11139
- iteration++;
11140
- if (options.abortSignal?.aborted) {
11141
- const cancelError = "Task was cancelled";
11142
- fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
11143
- clearInterruptController(this.session.id);
11144
- return { status: "failed", error: cancelError, iterations: iteration };
11145
- }
11146
- const pending = drainInputs(this.session.id);
11147
- for (const p of pending) {
11148
- const labelled = p.source === "orchestrator" ? `[message from orchestrator]
11149
- ${p.text}` : p.source === "system" ? `[system note]
11150
- ${p.text}` : p.text;
11151
- if (emit) {
11152
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
11153
- }
11154
- await this.context.addUserMessage(labelled);
11155
- }
11156
- const interruptController = new AbortController();
11157
- registerInterruptController(this.session.id, interruptController);
11158
- const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
11159
- const messages = await this.context.getMessages();
11160
- const useAnthropic = isAnthropicModel(this.session.model);
12134
+ fireWebhook("task.started", { prompt: options.prompt });
11161
12135
  if (emit) {
11162
- await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
12136
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
11163
12137
  }
11164
- let textStarted = false;
11165
- let textId = `text_${Date.now()}`;
11166
- let reasoningId = `reasoning_${Date.now()}`;
11167
- let reasoningStarted = false;
11168
- const toolCallStarts = /* @__PURE__ */ new Set();
11169
- const iterStream = streamText2({
11170
- model: resolveModel(this.session.model),
11171
- system: systemPrompt,
11172
- messages,
11173
- tools: wrapToolsNeverThrow(taskTools),
11174
- stopWhen: stepCountIs2(500),
11175
- abortSignal: combinedAbort,
11176
- providerOptions: useAnthropic ? {
11177
- anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
11178
- } : void 0,
11179
- // See the matching note in `stream()` — repair tool pairing before
11180
- // every step so we never feed the model an orphan tool-call.
11181
- prepareStep: async ({ messages: stepMessages }) => {
11182
- const paired = repairToolPairing(stepMessages);
11183
- const ordered = ensureToolResultsFollowCalls(paired);
11184
- if (ordered === stepMessages) return {};
11185
- return { messages: ordered };
11186
- },
11187
- onStepFinish: async (step) => {
11188
- options.onStepFinish?.(step);
11189
- fireWebhook("task.step_finished", { iteration, text: step.text });
11190
- if (emit) {
11191
- if (textStarted) {
11192
- await emit(JSON.stringify({ type: "text-end", id: textId }));
11193
- textStarted = false;
11194
- textId = `text_${Date.now()}`;
11195
- }
11196
- await emit(JSON.stringify({ type: "finish-step" }));
11197
- }
11198
- }
11199
- });
11200
- for await (const part of iterStream.fullStream) {
11201
- if (part.type === "text-delta") {
11202
- if (emit) {
11203
- if (!textStarted) {
11204
- await emit(JSON.stringify({ type: "text-start", id: textId }));
11205
- textStarted = true;
11206
- }
11207
- await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
11208
- }
11209
- } else if (part.type === "reasoning-start") {
12138
+ await this.context.addUserMessage(options.prompt);
12139
+ let iteration = 0;
12140
+ while (iteration < maxIterations) {
12141
+ iteration++;
12142
+ if (options.abortSignal?.aborted) {
12143
+ const cancelError = "Task was cancelled";
12144
+ fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
12145
+ clearInterruptController(this.session.id);
12146
+ return { status: "failed", error: cancelError, iterations: iteration };
12147
+ }
12148
+ const pending = drainInputs(this.session.id);
12149
+ for (const p of pending) {
12150
+ const labelled = p.source === "orchestrator" ? `[message from orchestrator]
12151
+ ${p.text}` : p.source === "system" ? `[system note]
12152
+ ${p.text}` : p.text;
11210
12153
  if (emit) {
11211
- await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
11212
- reasoningStarted = true;
12154
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
11213
12155
  }
11214
- } else if (part.type === "reasoning-delta") {
11215
- if (emit) {
11216
- await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
12156
+ await this.context.addUserMessage(labelled);
12157
+ }
12158
+ const interruptController = new AbortController();
12159
+ registerInterruptController(this.session.id, interruptController);
12160
+ const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
12161
+ const messages = await this.context.getMessages();
12162
+ const useAnthropic = isAnthropicModel(this.session.model);
12163
+ if (emit) {
12164
+ await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
12165
+ }
12166
+ let textStarted = false;
12167
+ let textId = `text_${Date.now()}`;
12168
+ let reasoningId = `reasoning_${Date.now()}`;
12169
+ let reasoningStarted = false;
12170
+ const toolCallStarts = /* @__PURE__ */ new Set();
12171
+ const iterStream = streamText2({
12172
+ model: resolveModel(this.session.model),
12173
+ system: systemPrompt,
12174
+ messages,
12175
+ tools: wrapToolsNeverThrow(taskTools),
12176
+ stopWhen: stepCountIs2(500),
12177
+ abortSignal: combinedAbort,
12178
+ providerOptions: useAnthropic ? {
12179
+ anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
12180
+ } : void 0,
12181
+ // See the matching note in `stream()` — repair tool pairing before
12182
+ // every step so we never feed the model an orphan tool-call.
12183
+ prepareStep: async ({ messages: stepMessages }) => {
12184
+ const paired = repairToolPairing(stepMessages);
12185
+ const ordered = ensureToolResultsFollowCalls(paired);
12186
+ if (ordered === stepMessages) return {};
12187
+ return { messages: ordered };
12188
+ },
12189
+ onStepFinish: async (step) => {
12190
+ options.onStepFinish?.(step);
12191
+ fireWebhook("task.step_finished", { iteration, text: step.text });
12192
+ if (emit) {
12193
+ if (textStarted) {
12194
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
12195
+ textStarted = false;
12196
+ textId = `text_${Date.now()}`;
12197
+ }
12198
+ await emit(JSON.stringify({ type: "finish-step" }));
12199
+ }
11217
12200
  }
11218
- } else if (part.type === "reasoning-end") {
11219
- if (emit && reasoningStarted) {
11220
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11221
- reasoningStarted = false;
11222
- reasoningId = `reasoning_${Date.now()}`;
12201
+ });
12202
+ for await (const part of iterStream.fullStream) {
12203
+ if (part.type === "text-delta") {
12204
+ if (emit) {
12205
+ if (!textStarted) {
12206
+ await emit(JSON.stringify({ type: "text-start", id: textId }));
12207
+ textStarted = true;
12208
+ }
12209
+ await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
12210
+ }
12211
+ } else if (part.type === "reasoning-start") {
12212
+ if (emit) {
12213
+ await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
12214
+ reasoningStarted = true;
12215
+ }
12216
+ } else if (part.type === "reasoning-delta") {
12217
+ if (emit) {
12218
+ await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
12219
+ }
12220
+ } else if (part.type === "reasoning-end") {
12221
+ if (emit && reasoningStarted) {
12222
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
12223
+ reasoningStarted = false;
12224
+ reasoningId = `reasoning_${Date.now()}`;
12225
+ }
12226
+ } else if (part.type === "tool-call-streaming-start") {
12227
+ if (emit) {
12228
+ const p = part;
12229
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
12230
+ toolCallStarts.add(p.toolCallId);
12231
+ }
12232
+ } else if (part.type === "tool-call-delta") {
12233
+ if (emit) {
12234
+ const p = part;
12235
+ await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
12236
+ }
12237
+ } else if (part.type === "tool-call") {
12238
+ if (emit) {
12239
+ if (!toolCallStarts.has(part.toolCallId)) {
12240
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
12241
+ toolCallStarts.add(part.toolCallId);
12242
+ }
12243
+ const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
12244
+ await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
12245
+ }
12246
+ } else if (part.type === "tool-result") {
12247
+ if (emit) {
12248
+ await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
12249
+ }
12250
+ } else if (part.type === "error") {
12251
+ console.error("Task stream error:", part.error);
12252
+ if (emit) {
12253
+ await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
12254
+ }
11223
12255
  }
11224
- } else if (part.type === "tool-call-streaming-start") {
11225
- if (emit) {
11226
- const p = part;
11227
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
11228
- toolCallStarts.add(p.toolCallId);
12256
+ }
12257
+ if (emit && textStarted) {
12258
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
12259
+ }
12260
+ if (emit && reasoningStarted) {
12261
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
12262
+ }
12263
+ const interrupted = interruptController.signal.aborted;
12264
+ clearInterruptController(this.session.id);
12265
+ const iterResponse = await iterStream.response;
12266
+ const responseMessages = iterResponse.messages;
12267
+ await this.context.addResponseMessages(responseMessages);
12268
+ const resultText = await iterStream.text;
12269
+ const resultSteps = await iterStream.steps;
12270
+ if (resultText) {
12271
+ options.onText?.(resultText);
12272
+ fireWebhook("task.message", { iteration, text: resultText });
12273
+ }
12274
+ for (const step of resultSteps) {
12275
+ if (step.toolCalls) {
12276
+ for (const tc of step.toolCalls) {
12277
+ options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
12278
+ fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
12279
+ }
11229
12280
  }
11230
- } else if (part.type === "tool-call-delta") {
11231
- if (emit) {
11232
- const p = part;
11233
- await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
12281
+ if (step.toolResults) {
12282
+ for (const tr of step.toolResults) {
12283
+ options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
12284
+ fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
12285
+ }
11234
12286
  }
11235
- } else if (part.type === "tool-call") {
11236
- if (emit) {
11237
- if (!toolCallStarts.has(part.toolCallId)) {
11238
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
11239
- toolCallStarts.add(part.toolCallId);
12287
+ }
12288
+ if (completion.signal) {
12289
+ const sig = completion.signal;
12290
+ const finalStatus = sig.status;
12291
+ let fileUrls;
12292
+ if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
12293
+ const resultObj = sig.result;
12294
+ const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
12295
+ if (filePaths.length > 0) {
12296
+ fileUrls = await this.uploadTaskFiles(filePaths);
11240
12297
  }
11241
- const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
11242
- await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
11243
12298
  }
11244
- } else if (part.type === "tool-result") {
11245
- if (emit) {
11246
- await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
12299
+ const recordingUrls = await this.finishTaskRecording(taskRecorder);
12300
+ const allFileUrls = [...fileUrls || [], ...recordingUrls];
12301
+ const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
12302
+ fireWebhook(eventType, {
12303
+ status: finalStatus,
12304
+ result: sig.result,
12305
+ error: sig.error,
12306
+ iterations: iteration,
12307
+ fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
12308
+ browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
12309
+ });
12310
+ const updatedTask2 = {
12311
+ ...options.taskConfig,
12312
+ status: finalStatus,
12313
+ result: sig.result,
12314
+ error: sig.error,
12315
+ iterations: iteration
12316
+ };
12317
+ await sessionQueries.update(this.session.id, {
12318
+ config: { ...this.session.config, task: updatedTask2 }
12319
+ });
12320
+ const orchId = this.session.config?.orchestratorSessionId;
12321
+ if (orchId) {
12322
+ const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
12323
+ pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
11247
12324
  }
11248
- } else if (part.type === "error") {
11249
- console.error("Task stream error:", part.error);
12325
+ return {
12326
+ status: finalStatus,
12327
+ result: sig.result,
12328
+ error: sig.error,
12329
+ iterations: iteration
12330
+ };
12331
+ }
12332
+ if (!interrupted) {
12333
+ const continuationPrompt = "Continue working on the task. Before calling `complete_task`, VERIFY your work is correct \u2014 re-read edited files, run the linter, run tests if applicable, and check the browser/server if you made UI or API changes. Make sure you searched the right directories and found everything relevant. When fully verified, call `complete_task` with the result. If you cannot complete it, call `task_failed` with a reason.";
11250
12334
  if (emit) {
11251
- await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
12335
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
11252
12336
  }
12337
+ await this.context.addUserMessage(continuationPrompt);
11253
12338
  }
11254
12339
  }
11255
- if (emit && textStarted) {
11256
- await emit(JSON.stringify({ type: "text-end", id: textId }));
11257
- }
11258
- if (emit && reasoningStarted) {
11259
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11260
- }
11261
- const interrupted = interruptController.signal.aborted;
11262
12340
  clearInterruptController(this.session.id);
11263
- const iterResponse = await iterStream.response;
11264
- const responseMessages = iterResponse.messages;
11265
- await this.context.addResponseMessages(responseMessages);
11266
- const resultText = await iterStream.text;
11267
- const resultSteps = await iterStream.steps;
11268
- if (resultText) {
11269
- options.onText?.(resultText);
11270
- fireWebhook("task.message", { iteration, text: resultText });
11271
- }
11272
- for (const step of resultSteps) {
11273
- if (step.toolCalls) {
11274
- for (const tc of step.toolCalls) {
11275
- options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
11276
- fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
11277
- }
11278
- }
11279
- if (step.toolResults) {
11280
- for (const tr of step.toolResults) {
11281
- options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
11282
- fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
11283
- }
11284
- }
11285
- }
11286
- if (completion.signal) {
11287
- const sig = completion.signal;
11288
- const finalStatus = sig.status;
11289
- let fileUrls;
11290
- if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
11291
- const resultObj = sig.result;
11292
- const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
11293
- if (filePaths.length > 0) {
11294
- fileUrls = await this.uploadTaskFiles(filePaths);
11295
- }
11296
- }
11297
- const recordingUrls = await this.finishTaskRecording(taskRecorder);
11298
- const allFileUrls = [...fileUrls || [], ...recordingUrls];
11299
- const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
11300
- fireWebhook(eventType, {
11301
- status: finalStatus,
11302
- result: sig.result,
11303
- error: sig.error,
11304
- iterations: iteration,
11305
- fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
11306
- browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
11307
- });
11308
- const updatedTask2 = {
11309
- ...options.taskConfig,
11310
- status: finalStatus,
11311
- result: sig.result,
11312
- error: sig.error,
11313
- iterations: iteration
11314
- };
11315
- await sessionQueries.update(this.session.id, {
11316
- config: { ...this.session.config, task: updatedTask2 }
11317
- });
11318
- const orchId = this.session.config?.orchestratorSessionId;
11319
- if (orchId) {
11320
- const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
11321
- pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
11322
- }
11323
- return {
11324
- status: finalStatus,
11325
- result: sig.result,
11326
- error: sig.error,
11327
- iterations: iteration
11328
- };
12341
+ const timeoutError = `Task did not complete within ${maxIterations} iterations`;
12342
+ const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
12343
+ fireWebhook("task.failed", {
12344
+ status: "failed",
12345
+ error: timeoutError,
12346
+ iterations: iteration,
12347
+ browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
12348
+ });
12349
+ const updatedTask = {
12350
+ ...options.taskConfig,
12351
+ status: "failed",
12352
+ error: timeoutError,
12353
+ iterations: iteration
12354
+ };
12355
+ await sessionQueries.update(this.session.id, {
12356
+ config: { ...this.session.config, task: updatedTask }
12357
+ });
12358
+ const orchIdTimeout = this.session.config?.orchestratorSessionId;
12359
+ if (orchIdTimeout) {
12360
+ pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
11329
12361
  }
11330
- if (!interrupted) {
11331
- const continuationPrompt = "Continue working on the task. Before calling `complete_task`, VERIFY your work is correct \u2014 re-read edited files, run the linter, run tests if applicable, and check the browser/server if you made UI or API changes. Make sure you searched the right directories and found everything relevant. When fully verified, call `complete_task` with the result. If you cannot complete it, call `task_failed` with a reason.";
11332
- if (emit) {
11333
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
12362
+ return { status: "failed", error: timeoutError, iterations: iteration };
12363
+ } finally {
12364
+ for (const cleanup2 of taskScopedCleanups) {
12365
+ try {
12366
+ await cleanup2();
12367
+ } catch {
11334
12368
  }
11335
- await this.context.addUserMessage(continuationPrompt);
11336
12369
  }
11337
12370
  }
11338
- clearInterruptController(this.session.id);
11339
- const timeoutError = `Task did not complete within ${maxIterations} iterations`;
11340
- const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
11341
- fireWebhook("task.failed", {
11342
- status: "failed",
11343
- error: timeoutError,
11344
- iterations: iteration,
11345
- browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
11346
- });
11347
- const updatedTask = {
11348
- ...options.taskConfig,
11349
- status: "failed",
11350
- error: timeoutError,
11351
- iterations: iteration
11352
- };
11353
- await sessionQueries.update(this.session.id, {
11354
- config: { ...this.session.config, task: updatedTask }
11355
- });
11356
- const orchIdTimeout = this.session.config?.orchestratorSessionId;
11357
- if (orchIdTimeout) {
11358
- pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
11359
- }
11360
- return { status: "failed", error: timeoutError, iterations: iteration };
11361
12371
  }
11362
12372
  /**
11363
12373
  * Stop a task-mode browser recording, encode to MP4, upload to GCS.
@@ -11417,11 +12427,11 @@ ${p.text}` : p.text;
11417
12427
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
11418
12428
  if (!isRemoteConfigured2()) return [];
11419
12429
  const { readFile: readFile13 } = await import("fs/promises");
11420
- const { join: join19, basename: basename7 } = await import("path");
12430
+ const { join: join21, basename: basename7 } = await import("path");
11421
12431
  const urls = [];
11422
12432
  for (const filePath of filePaths) {
11423
12433
  try {
11424
- const fullPath = filePath.startsWith("/") ? filePath : join19(this.session.workingDirectory, filePath);
12434
+ const fullPath = filePath.startsWith("/") ? filePath : join21(this.session.workingDirectory, filePath);
11425
12435
  const fileName = basename7(fullPath);
11426
12436
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
11427
12437
  const mimeMap = {
@@ -11483,7 +12493,7 @@ ${p.text}` : p.text;
11483
12493
  description: originalTool.description || "",
11484
12494
  inputSchema: originalTool.inputSchema || z15.object({}),
11485
12495
  execute: async (input, toolOptions) => {
11486
- const toolCallId = toolOptions.toolCallId || nanoid8();
12496
+ const toolCallId = toolOptions.toolCallId || nanoid9();
11487
12497
  const execution = toolExecutionQueries.create({
11488
12498
  sessionId: this.session.id,
11489
12499
  toolName: name,
@@ -11555,188 +12565,40 @@ ${p.text}` : p.text;
11555
12565
  /**
11556
12566
  * Reject a pending tool execution
11557
12567
  */
11558
- async reject(toolCallId, reason) {
11559
- const resolver = approvalResolvers.get(toolCallId);
11560
- if (resolver) {
11561
- resolver.reason = reason;
11562
- resolver.resolve(false);
11563
- return { rejected: true };
11564
- }
11565
- const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
11566
- const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
11567
- if (!execution) {
11568
- throw new Error(`No pending approval for tool call: ${toolCallId}`);
11569
- }
11570
- await toolExecutionQueries.reject(execution.id);
11571
- return { rejected: true };
11572
- }
11573
- /**
11574
- * Get pending approvals
11575
- */
11576
- async getPendingApprovals() {
11577
- return toolExecutionQueries.getPendingApprovals(this.session.id);
11578
- }
11579
- /**
11580
- * Get context statistics
11581
- */
11582
- getContextStats() {
11583
- return this.context.getStats();
11584
- }
11585
- /**
11586
- * Clear conversation context (start fresh)
11587
- */
11588
- clearContext() {
11589
- this.context.clear();
11590
- }
11591
- };
11592
- }
11593
- });
11594
-
11595
- // src/agent/session-lock.ts
11596
- async function withSessionLock(sessionId, fn) {
11597
- let state2 = locks.get(sessionId);
11598
- if (!state2) {
11599
- state2 = { tail: Promise.resolve(), pending: 0 };
11600
- locks.set(sessionId, state2);
11601
- }
11602
- state2.pending++;
11603
- const prev = state2.tail;
11604
- let release;
11605
- const next = new Promise((resolve14) => {
11606
- release = resolve14;
11607
- });
11608
- state2.tail = prev.then(() => next);
11609
- await prev;
11610
- try {
11611
- return await fn();
11612
- } finally {
11613
- release();
11614
- state2.pending--;
11615
- if (state2.pending === 0 && locks.get(sessionId) === state2) {
11616
- locks.delete(sessionId);
11617
- }
11618
- }
11619
- }
11620
- var locks;
11621
- var init_session_lock = __esm({
11622
- "src/agent/session-lock.ts"() {
11623
- "use strict";
11624
- locks = /* @__PURE__ */ new Map();
11625
- }
11626
- });
11627
-
11628
- // src/orchestrator/webhook-events.ts
11629
- import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
11630
- import { dirname as dirname7, join as join12 } from "path";
11631
- import { nanoid as nanoid9 } from "nanoid";
11632
- function logFilePath() {
11633
- return join12(getAppDataDirectory(), "webhook-events.jsonl");
11634
- }
11635
- function ensureLoaded() {
11636
- if (cache !== null) return cache;
11637
- cache = [];
11638
- try {
11639
- const p = logFilePath();
11640
- if (!existsSync18(p)) return cache;
11641
- const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
11642
- for (const line of lines) {
11643
- try {
11644
- cache.push(JSON.parse(line));
11645
- } catch {
11646
- }
11647
- }
11648
- if (cache.length > MAX_EVENTS) {
11649
- cache = cache.slice(-MAX_EVENTS);
11650
- try {
11651
- writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
11652
- } catch {
11653
- }
11654
- }
11655
- } catch {
11656
- }
11657
- return cache;
11658
- }
11659
- function appendEvent(ev) {
11660
- const list = ensureLoaded();
11661
- list.push(ev);
11662
- if (list.length > MAX_EVENTS) list.shift();
11663
- try {
11664
- const p = logFilePath();
11665
- mkdirSync7(dirname7(p), { recursive: true });
11666
- appendFileSync3(p, JSON.stringify(ev) + "\n");
11667
- } catch {
11668
- }
11669
- }
11670
- function recordEvent(ev) {
11671
- const full = {
11672
- id: ev.id ?? nanoid9(),
11673
- ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
11674
- source: ev.source,
11675
- status: ev.status,
11676
- subtype: ev.subtype,
11677
- channel: ev.channel,
11678
- user: ev.user,
11679
- textSnippet: ev.textSnippet?.slice(0, 200),
11680
- dropReason: ev.dropReason,
11681
- error: ev.error,
11682
- sessionId: ev.sessionId,
11683
- durationMs: ev.durationMs,
11684
- meta: ev.meta
11685
- };
11686
- appendEvent(full);
11687
- return full.id;
11688
- }
11689
- function updateEvent(id, patch) {
11690
- const list = ensureLoaded();
11691
- const i = list.findIndex((e) => e.id === id);
11692
- if (i < 0) return;
11693
- list[i] = { ...list[i], ...patch };
11694
- try {
11695
- const p = logFilePath();
11696
- mkdirSync7(dirname7(p), { recursive: true });
11697
- writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
11698
- } catch {
11699
- }
11700
- }
11701
- function listEvents(filter = {}) {
11702
- const list = ensureLoaded();
11703
- const q = filter.q?.toLowerCase();
11704
- const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
11705
- const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
11706
- const matched = list.filter((e) => {
11707
- if (filter.source && e.source !== filter.source) return false;
11708
- if (filter.status && e.status !== filter.status) return false;
11709
- const t = Date.parse(e.ts);
11710
- if (t < sinceTs) return false;
11711
- if (t >= beforeTs) return false;
11712
- if (q) {
11713
- const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
11714
- if (!hay.includes(q)) return false;
11715
- }
11716
- return true;
11717
- });
11718
- matched.reverse();
11719
- const offset = Math.max(0, filter.offset ?? 0);
11720
- const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
11721
- return {
11722
- events: matched.slice(offset, offset + limit),
11723
- total: matched.length
11724
- };
11725
- }
11726
- function clearAllEvents() {
11727
- cache = [];
11728
- try {
11729
- writeFileSync4(logFilePath(), "");
11730
- } catch {
11731
- }
11732
- }
11733
- var MAX_EVENTS, cache;
11734
- var init_webhook_events = __esm({
11735
- "src/orchestrator/webhook-events.ts"() {
11736
- "use strict";
11737
- init_config();
11738
- MAX_EVENTS = 1e3;
11739
- cache = null;
12568
+ async reject(toolCallId, reason) {
12569
+ const resolver = approvalResolvers.get(toolCallId);
12570
+ if (resolver) {
12571
+ resolver.reason = reason;
12572
+ resolver.resolve(false);
12573
+ return { rejected: true };
12574
+ }
12575
+ const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
12576
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
12577
+ if (!execution) {
12578
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
12579
+ }
12580
+ await toolExecutionQueries.reject(execution.id);
12581
+ return { rejected: true };
12582
+ }
12583
+ /**
12584
+ * Get pending approvals
12585
+ */
12586
+ async getPendingApprovals() {
12587
+ return toolExecutionQueries.getPendingApprovals(this.session.id);
12588
+ }
12589
+ /**
12590
+ * Get context statistics
12591
+ */
12592
+ getContextStats() {
12593
+ return this.context.getStats();
12594
+ }
12595
+ /**
12596
+ * Clear conversation context (start fresh)
12597
+ */
12598
+ clearContext() {
12599
+ this.context.clear();
12600
+ }
12601
+ };
11740
12602
  }
11741
12603
  });
11742
12604
 
@@ -11812,7 +12674,24 @@ async function runDaemonTurn(sessionId, events) {
11812
12674
  durationMs: finishedAt.getTime() - startedAt.getTime(),
11813
12675
  meta: { triggeredBy: events.map((e) => e.content?.slice(0, 80)) }
11814
12676
  });
12677
+ try {
12678
+ resolveBatchOnTurnEnd(events, !error);
12679
+ } catch (err) {
12680
+ console.error("[daemon] ack bookkeeping threw:", err?.message || err);
12681
+ }
11815
12682
  broadcast({ sessionId, text: trimmed, triggeredBy: events, startedAt, finishedAt, error });
12683
+ const seen = /* @__PURE__ */ new Set();
12684
+ for (const ev of events) {
12685
+ if (ev.ref?.channel !== "slack") continue;
12686
+ const ref = ev.ref;
12687
+ const channel = ref.slackChannel;
12688
+ const ts = ref.messageTs;
12689
+ if (!channel || !ts) continue;
12690
+ const key2 = `${channel}\u241F${ts}`;
12691
+ if (seen.has(key2)) continue;
12692
+ seen.add(key2);
12693
+ void removeLoadingReaction(channel, ts);
12694
+ }
11816
12695
  }
11817
12696
  var listeners;
11818
12697
  var init_daemon = __esm({
@@ -11823,6 +12702,8 @@ var init_daemon = __esm({
11823
12702
  init_db();
11824
12703
  init_inbox();
11825
12704
  init_webhook_events();
12705
+ init_inbox_acks();
12706
+ init_client3();
11826
12707
  listeners = /* @__PURE__ */ new Map();
11827
12708
  }
11828
12709
  });
@@ -11921,6 +12802,233 @@ var init_ensure_orchestrator = __esm({
11921
12802
  }
11922
12803
  });
11923
12804
 
12805
+ // src/orchestrator/self-update.ts
12806
+ var self_update_exports = {};
12807
+ __export(self_update_exports, {
12808
+ __test: () => __test,
12809
+ startSelfUpdater: () => startSelfUpdater,
12810
+ stopSelfUpdater: () => stopSelfUpdater
12811
+ });
12812
+ import { spawn as spawn2, execFile } from "child_process";
12813
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10 } from "fs";
12814
+ import { dirname as dirname10, join as join18 } from "path";
12815
+ import { fileURLToPath as fileURLToPath4 } from "url";
12816
+ function currentVersion2() {
12817
+ const here = dirname10(fileURLToPath4(import.meta.url));
12818
+ const candidates = [
12819
+ join18(here, "..", "..", "package.json"),
12820
+ join18(here, "..", "package.json"),
12821
+ join18(process.cwd(), "package.json")
12822
+ ];
12823
+ for (const p of candidates) {
12824
+ try {
12825
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
12826
+ if (pkg.name === "sparkecoder" && pkg.version) return pkg.version;
12827
+ } catch {
12828
+ }
12829
+ }
12830
+ return "0.0.0";
12831
+ }
12832
+ function isLikelyGlobalInstall() {
12833
+ const here = dirname10(fileURLToPath4(import.meta.url));
12834
+ return here.includes("/node_modules/sparkecoder/") || here.includes("\\node_modules\\sparkecoder\\");
12835
+ }
12836
+ function isEnabled() {
12837
+ if (process.env.SPARKECODER_AUTO_UPDATE === "false" || process.env.SPARKECODER_AUTO_UPDATE === "0") return false;
12838
+ try {
12839
+ const cfg = getConfig();
12840
+ if (cfg?.autoUpdate?.enabled === false) return false;
12841
+ } catch {
12842
+ }
12843
+ return true;
12844
+ }
12845
+ function remoteUrl() {
12846
+ try {
12847
+ const cfg = getConfig();
12848
+ const url = cfg?.remoteServer?.url;
12849
+ return typeof url === "string" && url.length > 0 ? url.replace(/\/+$/, "") : null;
12850
+ } catch {
12851
+ return null;
12852
+ }
12853
+ }
12854
+ function intervalMs() {
12855
+ try {
12856
+ const h = getConfig()?.autoUpdate?.intervalHours;
12857
+ if (typeof h === "number" && h > 0) return h * 60 * 6e4;
12858
+ } catch {
12859
+ }
12860
+ return DEFAULT_INTERVAL_HOURS * 60 * 6e4;
12861
+ }
12862
+ function semverGt(a, b) {
12863
+ const parse = (v) => v.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
12864
+ const pa = parse(a);
12865
+ const pb = parse(b);
12866
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
12867
+ const x = pa[i] ?? 0;
12868
+ const y = pb[i] ?? 0;
12869
+ if (x > y) return true;
12870
+ if (x < y) return false;
12871
+ }
12872
+ return false;
12873
+ }
12874
+ function statePath() {
12875
+ try {
12876
+ return join18(getAppDataDirectory(), "self-update-state.json");
12877
+ } catch {
12878
+ return null;
12879
+ }
12880
+ }
12881
+ function readState() {
12882
+ const p = statePath();
12883
+ if (!p) return {};
12884
+ try {
12885
+ return JSON.parse(readFileSync11(p, "utf8"));
12886
+ } catch {
12887
+ return {};
12888
+ }
12889
+ }
12890
+ function writeState(s) {
12891
+ const p = statePath();
12892
+ if (!p) return;
12893
+ try {
12894
+ mkdirSync10(dirname10(p), { recursive: true });
12895
+ writeFileSync7(p, JSON.stringify(s));
12896
+ } catch {
12897
+ }
12898
+ }
12899
+ function attemptedRecently(target, now) {
12900
+ const s = readState();
12901
+ return s.lastTarget === target && typeof s.lastAttemptAt === "number" && now - s.lastAttemptAt < RETRY_COOLDOWN_MS;
12902
+ }
12903
+ function latestPublishedVersion() {
12904
+ return new Promise((resolve14) => {
12905
+ execFile("npm", ["view", "sparkecoder", "version"], { timeout: 3e4 }, (err, stdout) => {
12906
+ if (err) {
12907
+ resolve14(null);
12908
+ return;
12909
+ }
12910
+ const v = String(stdout).trim();
12911
+ resolve14(/^\d+\.\d+\.\d+/.test(v) ? v : null);
12912
+ });
12913
+ });
12914
+ }
12915
+ function runInstaller(url) {
12916
+ const secret = process.env.SPARKECODER_SETUP_SECRET || process.env.SPARKECODER_TUNNEL_SECRET || "";
12917
+ const query = secret ? `?secret=${encodeURIComponent(secret)}` : "";
12918
+ const oneLiner = `bash -c "$(curl -fsSL '${url}/install.sh${query}')" >/tmp/sparkecoder-selfupdate.log 2>&1`;
12919
+ const child = spawn2("bash", ["-lc", oneLiner], {
12920
+ detached: true,
12921
+ stdio: "ignore"
12922
+ });
12923
+ child.unref();
12924
+ }
12925
+ async function checkAndUpdate() {
12926
+ if (upgrading || !isEnabled()) return;
12927
+ const url = remoteUrl();
12928
+ if (!url) return;
12929
+ const latest = await latestPublishedVersion();
12930
+ if (!latest) return;
12931
+ const current = currentVersion2();
12932
+ if (!semverGt(latest, current)) return;
12933
+ const now = Date.now();
12934
+ if (attemptedRecently(latest, now)) {
12935
+ console.log(`[self-update] v${latest} already attempted recently; skipping until cooldown elapses`);
12936
+ return;
12937
+ }
12938
+ upgrading = true;
12939
+ const announced = await announceUpdate(latest);
12940
+ const delay = announced ? ANNOUNCE_GRACE_MS : 0;
12941
+ if (announced) {
12942
+ console.log(`[self-update] announced v${latest} in Slack; updating in ${Math.round(delay / 6e4)}m`);
12943
+ }
12944
+ const t = setTimeout(() => doInstall(latest, url, current), delay);
12945
+ if (typeof t.unref === "function") t.unref();
12946
+ }
12947
+ function doInstall(latest, url, current) {
12948
+ const prev = readState();
12949
+ writeState({
12950
+ lastTarget: latest,
12951
+ lastAttemptAt: Date.now(),
12952
+ attempts: prev.lastTarget === latest ? (prev.attempts ?? 0) + 1 : 1
12953
+ });
12954
+ console.log(`[self-update] newer version available: v${current} \u2192 v${latest}; re-running installer`);
12955
+ try {
12956
+ runInstaller(url);
12957
+ } catch (err) {
12958
+ upgrading = false;
12959
+ console.warn("[self-update] failed to launch installer:", err?.message || err);
12960
+ }
12961
+ }
12962
+ async function findOrchestratorId() {
12963
+ try {
12964
+ const { sessionQueries: sessionQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
12965
+ const all = await sessionQueries2.list(500, 0);
12966
+ const orch = all.find((s) => s?.config?.role === "orchestrator");
12967
+ return orch?.id ?? null;
12968
+ } catch {
12969
+ return null;
12970
+ }
12971
+ }
12972
+ async function announceUpdate(target) {
12973
+ try {
12974
+ const { isSlackConfigured: isSlackConfigured2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
12975
+ if (!isSlackConfigured2()) return false;
12976
+ const orchId = await findOrchestratorId();
12977
+ if (!orchId) return false;
12978
+ const { pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports));
12979
+ pushToInbox2(orchId, {
12980
+ ref: { channel: "system", kind: "worker.completed", workerId: "self-update", workerName: "self-update" },
12981
+ content: `[SYSTEM self-update] A software update to v${target} will begin in about 5 minutes and the service will restart briefly (expect ~1\u20132 minutes of downtime). Send a short Slack heads-up to whoever you normally talk to / the bot owner that you'll be offline for a quick update, then carry on. Use the messenger tool to deliver it. If you genuinely don't know who to tell, post in the most relevant channel you've recently been active in; if Slack isn't reachable, skip silently \u2014 the update will proceed regardless.`,
12982
+ wake: "now",
12983
+ enqueuedAt: /* @__PURE__ */ new Date()
12984
+ });
12985
+ return true;
12986
+ } catch {
12987
+ return false;
12988
+ }
12989
+ }
12990
+ function startSelfUpdater() {
12991
+ if (started) return;
12992
+ started = true;
12993
+ if (!isEnabled()) {
12994
+ console.log("[self-update] disabled");
12995
+ return;
12996
+ }
12997
+ if (!isLikelyGlobalInstall()) {
12998
+ console.log("[self-update] skipped (not a global install)");
12999
+ return;
13000
+ }
13001
+ const kickoff = setTimeout(() => {
13002
+ void checkAndUpdate();
13003
+ timer = setInterval(() => {
13004
+ void checkAndUpdate();
13005
+ }, intervalMs());
13006
+ if (typeof timer.unref === "function") timer.unref();
13007
+ }, INITIAL_DELAY_MS);
13008
+ if (typeof kickoff.unref === "function") kickoff.unref();
13009
+ }
13010
+ function stopSelfUpdater() {
13011
+ if (timer) {
13012
+ clearInterval(timer);
13013
+ timer = null;
13014
+ }
13015
+ }
13016
+ var INITIAL_DELAY_MS, DEFAULT_INTERVAL_HOURS, ANNOUNCE_GRACE_MS, RETRY_COOLDOWN_MS, timer, started, upgrading, __test;
13017
+ var init_self_update = __esm({
13018
+ "src/orchestrator/self-update.ts"() {
13019
+ "use strict";
13020
+ init_config();
13021
+ INITIAL_DELAY_MS = 5 * 6e4;
13022
+ DEFAULT_INTERVAL_HOURS = 6;
13023
+ ANNOUNCE_GRACE_MS = 5 * 6e4;
13024
+ RETRY_COOLDOWN_MS = 24 * 60 * 6e4;
13025
+ timer = null;
13026
+ started = false;
13027
+ upgrading = false;
13028
+ __test = { currentVersion: currentVersion2, semverGt, isLikelyGlobalInstall };
13029
+ }
13030
+ });
13031
+
11924
13032
  // src/tasks/scheduler.ts
11925
13033
  var scheduler_exports = {};
11926
13034
  __export(scheduler_exports, {
@@ -12223,8 +13331,8 @@ import chalk from "chalk";
12223
13331
  import ora from "ora";
12224
13332
  import "dotenv/config";
12225
13333
  import { createInterface } from "readline";
12226
- import { dirname as dirname11 } from "path";
12227
- import { fileURLToPath as fileURLToPath5 } from "url";
13334
+ import { dirname as dirname12 } from "path";
13335
+ import { fileURLToPath as fileURLToPath6 } from "url";
12228
13336
 
12229
13337
  // src/server/index.ts
12230
13338
  import "dotenv/config";
@@ -12232,11 +13340,11 @@ import { Hono as Hono10 } from "hono";
12232
13340
  import { serve } from "@hono/node-server";
12233
13341
  import { cors } from "hono/cors";
12234
13342
  import { logger } from "hono/logger";
12235
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
12236
- import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
12237
- import { spawn as spawn2 } from "child_process";
13343
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync8 } from "fs";
13344
+ import { resolve as resolve12, dirname as dirname11, join as join19 } from "path";
13345
+ import { spawn as spawn3 } from "child_process";
12238
13346
  import { createServer as createNetServer } from "net";
12239
- import { fileURLToPath as fileURLToPath4 } from "url";
13347
+ import { fileURLToPath as fileURLToPath5 } from "url";
12240
13348
 
12241
13349
  // src/server/routes/sessions.ts
12242
13350
  init_db();
@@ -12249,7 +13357,7 @@ import { zValidator } from "@hono/zod-validator";
12249
13357
  import { z as z16 } from "zod";
12250
13358
  import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
12251
13359
  import { readdir as readdir6 } from "fs/promises";
12252
- import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
13360
+ import { join as join14, basename as basename5, extname as extname8, relative as relative9 } from "path";
12253
13361
  import { nanoid as nanoid10 } from "nanoid";
12254
13362
 
12255
13363
  // src/tasks/agent-status.ts
@@ -12890,7 +13998,7 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
12890
13998
  });
12891
13999
  function getAttachmentsDir(sessionId) {
12892
14000
  const appDataDir = getAppDataDirectory();
12893
- return join13(appDataDir, "attachments", sessionId);
14001
+ return join14(appDataDir, "attachments", sessionId);
12894
14002
  }
12895
14003
  function ensureAttachmentsDir(sessionId) {
12896
14004
  const dir = getAttachmentsDir(sessionId);
@@ -12911,7 +14019,7 @@ sessions2.get("/:id/attachments", async (c) => {
12911
14019
  }
12912
14020
  const files = readdirSync3(dir);
12913
14021
  const attachments = files.map((filename) => {
12914
- const filePath = join13(dir, filename);
14022
+ const filePath = join14(dir, filename);
12915
14023
  const stats = statSync2(filePath);
12916
14024
  return {
12917
14025
  id: filename.split("_")[0],
@@ -12946,7 +14054,7 @@ sessions2.post("/:id/attachments", async (c) => {
12946
14054
  const id = nanoid10(10);
12947
14055
  const ext = extname8(file.name) || "";
12948
14056
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12949
- const filePath = join13(dir, safeFilename);
14057
+ const filePath = join14(dir, safeFilename);
12950
14058
  const arrayBuffer = await file.arrayBuffer();
12951
14059
  writeFileSync5(filePath, Buffer.from(arrayBuffer));
12952
14060
  return c.json({
@@ -12972,7 +14080,7 @@ sessions2.post("/:id/attachments", async (c) => {
12972
14080
  const id = nanoid10(10);
12973
14081
  const ext = extname8(body.filename) || "";
12974
14082
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12975
- const filePath = join13(dir, safeFilename);
14083
+ const filePath = join14(dir, safeFilename);
12976
14084
  let base64Data = body.data;
12977
14085
  if (base64Data.includes(",")) {
12978
14086
  base64Data = base64Data.split(",")[1];
@@ -13009,7 +14117,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
13009
14117
  if (!file) {
13010
14118
  return c.json({ error: "Attachment not found" }, 404);
13011
14119
  }
13012
- const filePath = join13(dir, file);
14120
+ const filePath = join14(dir, file);
13013
14121
  unlinkSync2(filePath);
13014
14122
  return c.json({ success: true, id: attachmentId });
13015
14123
  });
@@ -13092,7 +14200,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
13092
14200
  const entries = await readdir6(currentDir, { withFileTypes: true });
13093
14201
  for (const entry2 of entries) {
13094
14202
  if (results.length >= limit * 2) break;
13095
- const fullPath = join13(currentDir, entry2.name);
14203
+ const fullPath = join14(currentDir, entry2.name);
13096
14204
  const relativePath = relative9(baseDir, fullPath);
13097
14205
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
13098
14206
  continue;
@@ -13252,7 +14360,7 @@ import { Hono as Hono2 } from "hono";
13252
14360
  import { zValidator as zValidator2 } from "@hono/zod-validator";
13253
14361
  import { z as z17 } from "zod";
13254
14362
  import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
13255
- import { join as join14 } from "path";
14363
+ import { join as join15 } from "path";
13256
14364
 
13257
14365
  // src/agent/missing-tool-recovery.ts
13258
14366
  init_db();
@@ -13519,6 +14627,7 @@ init_stream_proxy();
13519
14627
  init_recorder();
13520
14628
  init_remote();
13521
14629
  init_resize_image();
14630
+ init_local_device_time();
13522
14631
  var sessionRecorders = /* @__PURE__ */ new Map();
13523
14632
  var MAX_TOOL_INPUT_LENGTH = 8 * 1024;
13524
14633
  var MAX_TOOL_INPUT_PREVIEW = 2 * 1024;
@@ -13634,7 +14743,7 @@ var rejectSchema = z17.object({
13634
14743
  var streamAbortControllers = /* @__PURE__ */ new Map();
13635
14744
  function getAttachmentsDirectory(sessionId) {
13636
14745
  const appDataDir = getAppDataDirectory();
13637
- return join14(appDataDir, "attachments", sessionId);
14746
+ return join15(appDataDir, "attachments", sessionId);
13638
14747
  }
13639
14748
  async function saveAttachmentToDisk(sessionId, attachment, index) {
13640
14749
  const attachmentsDir = getAttachmentsDirectory(sessionId);
@@ -13657,7 +14766,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
13657
14766
  attachment.mediaType = resized.mediaType;
13658
14767
  attachment.data = buffer.toString("base64");
13659
14768
  }
13660
- const filePath = join14(attachmentsDir, filename);
14769
+ const filePath = join15(attachmentsDir, filename);
13661
14770
  writeFileSync6(filePath, buffer);
13662
14771
  return filePath;
13663
14772
  }
@@ -14034,9 +15143,12 @@ agents.post(
14034
15143
  if (!session) {
14035
15144
  return c.json({ error: "Session not found" }, 404);
14036
15145
  }
14037
- if (session.config?.role === "orchestrator" && !/^\[\w+/.test(prompt)) {
15146
+ if (session.config?.role === "orchestrator" && !/^\[(WEB|SLACK|SYSTEM|SCHEDULE|WEBHOOK)\b/.test(prompt)) {
14038
15147
  prompt = `[WEB] ${prompt}`;
14039
15148
  }
15149
+ if (session.config?.role === "orchestrator") {
15150
+ prompt = prependLocalDeviceTimeToUserMessage(prompt);
15151
+ }
14040
15152
  const nextSequence = await messageQueries.getNextSequence(id);
14041
15153
  await createCheckpoint(id, session.workingDirectory, nextSequence);
14042
15154
  let userMessageContent;
@@ -14646,17 +15758,17 @@ import { zValidator as zValidator3 } from "@hono/zod-validator";
14646
15758
  import { z as z18 } from "zod";
14647
15759
  import { readFileSync as readFileSync10 } from "fs";
14648
15760
  import { fileURLToPath as fileURLToPath3 } from "url";
14649
- import { dirname as dirname8, join as join15 } from "path";
15761
+ import { dirname as dirname8, join as join16 } from "path";
14650
15762
  var __filename = fileURLToPath3(import.meta.url);
14651
15763
  var __dirname = dirname8(__filename);
14652
15764
  var possiblePaths = [
14653
- join15(__dirname, "../package.json"),
15765
+ join16(__dirname, "../package.json"),
14654
15766
  // From dist/server -> dist/../package.json
14655
- join15(__dirname, "../../package.json"),
15767
+ join16(__dirname, "../../package.json"),
14656
15768
  // From dist/server (if nested differently)
14657
- join15(__dirname, "../../../package.json"),
15769
+ join16(__dirname, "../../../package.json"),
14658
15770
  // From src/server/routes (development)
14659
- join15(process.cwd(), "package.json")
15771
+ join16(process.cwd(), "package.json")
14660
15772
  // From current working directory
14661
15773
  ];
14662
15774
  var currentVersion = "0.0.0";
@@ -15117,6 +16229,25 @@ import { nanoid as nanoid12 } from "nanoid";
15117
16229
  init_questions();
15118
16230
  var tasks = new Hono5();
15119
16231
  var taskAbortControllers = /* @__PURE__ */ new Map();
16232
+ var taskMcpServerSchema = z20.object({
16233
+ name: z20.string().min(1).describe("Tool prefix + display name."),
16234
+ transport: z20.enum(["http", "sse", "stdio"]),
16235
+ url: z20.string().url().optional().describe("http/sse transports."),
16236
+ headers: z20.record(z20.string(), z20.string()).optional().describe("Auth / custom headers for http/sse."),
16237
+ command: z20.string().optional().describe("stdio transport."),
16238
+ args: z20.array(z20.string()).optional(),
16239
+ env: z20.record(z20.string(), z20.string()).optional().describe("Env vars for stdio child process.")
16240
+ }).refine(
16241
+ (s) => s.transport === "stdio" ? !!s.command : !!s.url,
16242
+ { message: 'http/sse require "url"; stdio requires "command".' }
16243
+ );
16244
+ var taskSkillSchema = z20.object({
16245
+ name: z20.string().min(1),
16246
+ description: z20.string().optional(),
16247
+ content: z20.string().min(1).describe("Full markdown body of the skill."),
16248
+ alwaysApply: z20.boolean().optional().describe("Inject into the system prompt up-front (vs load on demand)."),
16249
+ globs: z20.array(z20.string()).optional()
16250
+ });
15120
16251
  var createTaskSchema = z20.object({
15121
16252
  prompt: z20.string().min(1),
15122
16253
  outputSchema: z20.record(z20.string(), z20.unknown()),
@@ -15128,8 +16259,30 @@ var createTaskSchema = z20.object({
15128
16259
  parentTaskId: z20.string().optional(),
15129
16260
  /** When set, the spawning orchestrator's session id. Stamped on the
15130
16261
  * worker's config so terminal events can wake the orchestrator. */
15131
- orchestratorSessionId: z20.string().optional()
16262
+ orchestratorSessionId: z20.string().optional(),
16263
+ /** Task-scoped MCP servers — auto-connected for this task only. */
16264
+ mcpServers: z20.array(taskMcpServerSchema).optional(),
16265
+ /** Task-scoped skills — available to this task only. */
16266
+ skills: z20.array(taskSkillSchema).optional()
15132
16267
  });
16268
+ function redactMcpServers(servers2) {
16269
+ if (!servers2 || servers2.length === 0) return void 0;
16270
+ return servers2.map((s) => ({
16271
+ name: s.name,
16272
+ transport: s.transport,
16273
+ url: s.url,
16274
+ hasHeaders: !!(s.headers && Object.keys(s.headers).length > 0),
16275
+ command: s.command
16276
+ }));
16277
+ }
16278
+ function redactSkills(skills2) {
16279
+ if (!skills2 || skills2.length === 0) return void 0;
16280
+ return skills2.map((s) => ({
16281
+ name: s.name,
16282
+ description: s.description,
16283
+ alwaysApply: s.alwaysApply
16284
+ }));
16285
+ }
15133
16286
  tasks.post(
15134
16287
  "/",
15135
16288
  zValidator5("json", createTaskSchema),
@@ -15142,7 +16295,9 @@ tasks.post(
15142
16295
  webhookUrl: body.webhookUrl,
15143
16296
  maxIterations: body.maxIterations ?? 50,
15144
16297
  status: "running",
15145
- parentTaskId: body.parentTaskId
16298
+ parentTaskId: body.parentTaskId,
16299
+ mcpServers: redactMcpServers(body.mcpServers),
16300
+ skills: redactSkills(body.skills)
15146
16301
  };
15147
16302
  let agent;
15148
16303
  if (body.parentTaskId) {
@@ -15219,7 +16374,9 @@ tasks.post(
15219
16374
  prompt: body.prompt,
15220
16375
  taskConfig,
15221
16376
  abortSignal: abortController.signal,
15222
- writeSSE
16377
+ writeSSE,
16378
+ mcpServers: body.mcpServers,
16379
+ skills: body.skills
15223
16380
  });
15224
16381
  await writeSSE(JSON.stringify({ type: "finish" }));
15225
16382
  } catch (err) {
@@ -15313,6 +16470,8 @@ tasks.get("/:id", async (c) => {
15313
16470
  model: session.model,
15314
16471
  name: session.name,
15315
16472
  parentTaskId: task.parentTaskId,
16473
+ mcpServers: task.mcpServers,
16474
+ skills: task.skills,
15316
16475
  createdAt: session.createdAt.toISOString(),
15317
16476
  updatedAt: session.updatedAt.toISOString(),
15318
16477
  browserRecordings: browserRecordings.length > 0 ? browserRecordings : void 0
@@ -15435,6 +16594,204 @@ function verifySlackSignature(opts) {
15435
16594
  // src/server/routes/slack.ts
15436
16595
  init_client3();
15437
16596
  init_slack();
16597
+
16598
+ // src/integrations/slack/files.ts
16599
+ init_client3();
16600
+ var MAX_BYTES = 100 * 1024 * 1024;
16601
+ var INGEST_TIMEOUT_MS = 2500;
16602
+ function inferFileName(file) {
16603
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
16604
+ }
16605
+ function inferContentType(file) {
16606
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
16607
+ return "application/octet-stream";
16608
+ }
16609
+ function formatBytes(n) {
16610
+ if (!Number.isFinite(n) || n <= 0) return "?";
16611
+ if (n < 1024) return `${n} B`;
16612
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
16613
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
16614
+ }
16615
+ function withTimeout(p, ms, label) {
16616
+ return new Promise((resolve14, reject) => {
16617
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
16618
+ p.then(
16619
+ (v) => {
16620
+ clearTimeout(t);
16621
+ resolve14(v);
16622
+ },
16623
+ (e) => {
16624
+ clearTimeout(t);
16625
+ reject(e);
16626
+ }
16627
+ );
16628
+ });
16629
+ }
16630
+ async function ingestOne(file, sessionId, botToken) {
16631
+ const fileName = inferFileName(file);
16632
+ const contentType = inferContentType(file);
16633
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
16634
+ const base = {
16635
+ slackFileId: file.id,
16636
+ fileName,
16637
+ contentType,
16638
+ sizeBytes: declaredSize
16639
+ };
16640
+ const sourceUrl = file.url_private_download || file.url_private;
16641
+ if (!sourceUrl || typeof sourceUrl !== "string") {
16642
+ return { ...base, shortUrl: null, error: "no_source_url" };
16643
+ }
16644
+ if (declaredSize > MAX_BYTES) {
16645
+ return { ...base, shortUrl: null, error: "size_exceeded" };
16646
+ }
16647
+ let bytes;
16648
+ try {
16649
+ const res = await fetch(sourceUrl, {
16650
+ headers: { Authorization: `Bearer ${botToken}` }
16651
+ });
16652
+ if (!res.ok) {
16653
+ return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
16654
+ }
16655
+ const ab = await res.arrayBuffer();
16656
+ if (ab.byteLength > MAX_BYTES) {
16657
+ return { ...base, shortUrl: null, error: "size_exceeded" };
16658
+ }
16659
+ bytes = Buffer.from(ab);
16660
+ } catch (err) {
16661
+ return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
16662
+ }
16663
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
16664
+ let upload;
16665
+ try {
16666
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
16667
+ } catch (err) {
16668
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
16669
+ }
16670
+ try {
16671
+ const putRes = await fetch(upload.uploadUrl, {
16672
+ method: "PUT",
16673
+ headers: { "Content-Type": contentType },
16674
+ body: bytes
16675
+ });
16676
+ if (!putRes.ok) {
16677
+ return {
16678
+ ...base,
16679
+ sizeBytes: bytes.length,
16680
+ shortUrl: null,
16681
+ error: `gcs_put_${putRes.status}`
16682
+ };
16683
+ }
16684
+ } catch (err) {
16685
+ return {
16686
+ ...base,
16687
+ sizeBytes: bytes.length,
16688
+ shortUrl: null,
16689
+ error: `gcs_put_error:${err?.message || "unknown"}`
16690
+ };
16691
+ }
16692
+ try {
16693
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
16694
+ } catch (err) {
16695
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
16696
+ }
16697
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
16698
+ // server somehow forgot to return it (older remote-server versions).
16699
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
16700
+ return {
16701
+ ...base,
16702
+ sizeBytes: bytes.length,
16703
+ shortUrl
16704
+ };
16705
+ }
16706
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
16707
+ try {
16708
+ const u = new URL(uploadUrl);
16709
+ if (u.hostname.endsWith(".googleapis.com")) return null;
16710
+ return `${u.origin}/f/${fileId}`;
16711
+ } catch {
16712
+ return null;
16713
+ }
16714
+ }
16715
+ async function ingestSlackFiles(files, sessionId, options = {}) {
16716
+ if (!Array.isArray(files) || files.length === 0) return [];
16717
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
16718
+ if (!isRemoteConfigured2()) {
16719
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
16720
+ return files.map((f) => ({
16721
+ slackFileId: f.id,
16722
+ fileName: inferFileName(f),
16723
+ contentType: inferContentType(f),
16724
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
16725
+ shortUrl: null,
16726
+ error: "storage_unconfigured"
16727
+ }));
16728
+ }
16729
+ const botToken = getSlackBotToken();
16730
+ if (!botToken) {
16731
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
16732
+ return files.map((f) => ({
16733
+ slackFileId: f.id,
16734
+ fileName: inferFileName(f),
16735
+ contentType: inferContentType(f),
16736
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
16737
+ shortUrl: null,
16738
+ error: "no_bot_token"
16739
+ }));
16740
+ }
16741
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
16742
+ const startedAt = Date.now();
16743
+ const pipeline = Promise.allSettled(
16744
+ files.map((f) => ingestOne(f, sessionId, botToken))
16745
+ );
16746
+ let settled;
16747
+ try {
16748
+ settled = await withTimeout(pipeline, timeoutMs, "ingest");
16749
+ } catch (err) {
16750
+ console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
16751
+ return files.map((f) => ({
16752
+ slackFileId: f.id,
16753
+ fileName: inferFileName(f),
16754
+ contentType: inferContentType(f),
16755
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
16756
+ shortUrl: null,
16757
+ error: "timeout"
16758
+ }));
16759
+ }
16760
+ const results = settled.map((s, i) => {
16761
+ if (s.status === "fulfilled") return s.value;
16762
+ const f = files[i];
16763
+ return {
16764
+ slackFileId: f.id,
16765
+ fileName: inferFileName(f),
16766
+ contentType: inferContentType(f),
16767
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
16768
+ shortUrl: null,
16769
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
16770
+ };
16771
+ });
16772
+ const okCount = results.filter((r) => r.shortUrl).length;
16773
+ console.log(
16774
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
16775
+ );
16776
+ return results;
16777
+ }
16778
+ function formatFileBlock(files) {
16779
+ if (!files || files.length === 0) return "";
16780
+ const lines = ["[files]"];
16781
+ for (const f of files) {
16782
+ const sizeLabel = formatBytes(f.sizeBytes);
16783
+ if (f.shortUrl) {
16784
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
16785
+ } else {
16786
+ lines.push(
16787
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
16788
+ );
16789
+ }
16790
+ }
16791
+ return lines.join("\n");
16792
+ }
16793
+
16794
+ // src/server/routes/slack.ts
15438
16795
  init_webhook_events();
15439
16796
  init_inbox();
15440
16797
  var recentlyHandled = /* @__PURE__ */ new Map();
@@ -15521,9 +16878,43 @@ slack.post("/events", async (c) => {
15521
16878
  inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
15522
16879
  }
15523
16880
  }
15524
- pushToInbox(orchestratorId, inbound);
16881
+ const slackFiles = Array.isArray(ev.files) ? ev.files : [];
15525
16882
  markHandled(ev.channel, ev.ts);
15526
- updateEvent(auditId, { status: "routed", sessionId: orchestratorId });
16883
+ if (ev.channel && ev.ts) {
16884
+ void addLoadingReaction(String(ev.channel), String(ev.ts));
16885
+ }
16886
+ let ingestedCount = 0;
16887
+ if (slackFiles.length > 0) {
16888
+ try {
16889
+ const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
16890
+ const block = formatFileBlock(ingested);
16891
+ if (block) inbound.content = `${inbound.content}
16892
+ ${block}`;
16893
+ ingestedCount = ingested.filter((f) => f.shortUrl).length;
16894
+ } catch (err) {
16895
+ console.warn("[slack-files] ingestion threw:", err?.message || err);
16896
+ inbound.content = `${inbound.content}
16897
+ [files] (ingestion failed: ${err?.message || "unknown"})`;
16898
+ }
16899
+ }
16900
+ pushToInbox(orchestratorId, inbound);
16901
+ updateEvent(auditId, {
16902
+ status: "routed",
16903
+ sessionId: orchestratorId,
16904
+ ...slackFiles.length > 0 ? {
16905
+ // Preserve the original meta (ts, thread_ts, team,
16906
+ // event_subtype) from recordEvent above — updateEvent does a
16907
+ // shallow merge, so we have to re-include them.
16908
+ meta: {
16909
+ ts: ev.ts,
16910
+ thread_ts: ev.thread_ts,
16911
+ team: ev.team,
16912
+ event_subtype: ev.subtype,
16913
+ fileCount: slackFiles.length,
16914
+ ingestedCount
16915
+ }
16916
+ } : {}
16917
+ });
15527
16918
  } else {
15528
16919
  updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
15529
16920
  }
@@ -15743,10 +17134,35 @@ integrations.get("/", async (c) => {
15743
17134
  cfAccess: {
15744
17135
  enabled: !!cfg?.auth?.cfAccess?.enabled,
15745
17136
  teamDomain: cfg?.auth?.cfAccess?.teamDomain || null,
17137
+ audTag: cfg?.auth?.cfAccess?.audTag || null,
15746
17138
  allowedEmails: cfg?.auth?.allowedEmails || []
15747
17139
  }
15748
17140
  });
15749
17141
  });
17142
+ var cfAccessSchema = z21.object({
17143
+ enabled: z21.boolean().optional(),
17144
+ teamDomain: z21.string().optional(),
17145
+ audTag: z21.string().optional(),
17146
+ // Email allowlist for the public (cloudflared) surface. Empty array = allow
17147
+ // any email that passes the Cloudflare Access policy (no extra filtering).
17148
+ allowedEmails: z21.array(z21.string().trim().toLowerCase()).optional()
17149
+ });
17150
+ integrations.post("/cf-access", zValidator6("json", cfAccessSchema), async (c) => {
17151
+ const body = c.req.valid("json");
17152
+ if (body.enabled) {
17153
+ const cfg = getConfig();
17154
+ const teamDomain = body.teamDomain ?? cfg?.auth?.cfAccess?.teamDomain;
17155
+ const audTag = body.audTag ?? cfg?.auth?.cfAccess?.audTag;
17156
+ if (!teamDomain || !audTag) {
17157
+ return c.json(
17158
+ { error: "teamDomain and audTag are required to enable Cloudflare Access" },
17159
+ 400
17160
+ );
17161
+ }
17162
+ }
17163
+ setCfAccessConfig(body);
17164
+ return c.json({ ok: true });
17165
+ });
15750
17166
  var slackConfigSchema = z21.object({
15751
17167
  botToken: z21.string().optional(),
15752
17168
  signingSecret: z21.string().optional(),
@@ -15929,8 +17345,8 @@ import { Hono as Hono9 } from "hono";
15929
17345
  import { zValidator as zValidator7 } from "@hono/zod-validator";
15930
17346
  import { z as z22 } from "zod";
15931
17347
  import { existsSync as existsSync21, statSync as statSync3 } from "fs";
15932
- import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
15933
- import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
17348
+ import { readFile as readFile12, writeFile as writeFile7, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
17349
+ import { resolve as resolve11, join as join17, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
15934
17350
  var skills = new Hono9();
15935
17351
  function encodeId(filePath) {
15936
17352
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -16078,13 +17494,13 @@ skills.post(
16078
17494
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
16079
17495
  const ext = extname9(safeName).toLowerCase();
16080
17496
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
16081
- const filePath = join16(targetDir, finalName);
17497
+ const filePath = join17(targetDir, finalName);
16082
17498
  if (existsSync21(filePath)) {
16083
17499
  return c.json({ error: `file already exists: ${finalName}` }, 409);
16084
17500
  }
16085
17501
  try {
16086
17502
  await mkdir5(targetDir, { recursive: true });
16087
- await writeFile6(filePath, content, "utf-8");
17503
+ await writeFile7(filePath, content, "utf-8");
16088
17504
  } catch (err) {
16089
17505
  return c.json({ error: err?.message || "write failed" }, 500);
16090
17506
  }
@@ -16100,7 +17516,7 @@ skills.put(
16100
17516
  if (filePath.includes("/skills/default")) {
16101
17517
  return c.json({ error: "built-in skills are read-only" }, 400);
16102
17518
  }
16103
- await writeFile6(filePath, c.req.valid("json").content, "utf-8");
17519
+ await writeFile7(filePath, c.req.valid("json").content, "utf-8");
16104
17520
  return c.json({ ok: true });
16105
17521
  }
16106
17522
  );
@@ -16144,6 +17560,14 @@ skills.delete("/directories", (c) => {
16144
17560
  init_config();
16145
17561
  import { createRemoteJWKSet, jwtVerify } from "jose";
16146
17562
  var EXEMPT_PATH_PREFIXES = ["/health", "/api/slack/events", "/api/inbox/", "/w/"];
17563
+ function isExemptPath(path) {
17564
+ return EXEMPT_PATH_PREFIXES.some((p) => {
17565
+ if (p.endsWith("/")) {
17566
+ return path === p.slice(0, -1) || path === p || path.startsWith(p);
17567
+ }
17568
+ return path === p || path.startsWith(p + "/") || path.startsWith(p + "?");
17569
+ });
17570
+ }
16147
17571
  var cachedJWKS = null;
16148
17572
  var cachedJWKSUrl = null;
16149
17573
  function getOrCreateJWKS(teamDomain) {
@@ -16173,12 +17597,13 @@ function cfAccessMiddleware() {
16173
17597
  return next();
16174
17598
  }
16175
17599
  const path = c.req.path;
16176
- if (EXEMPT_PATH_PREFIXES.some((p) => path === p || path.startsWith(p + "/") || path.startsWith(p + "?"))) {
17600
+ if (isExemptPath(path)) {
16177
17601
  return next();
16178
17602
  }
16179
17603
  const host = c.req.header("host");
16180
17604
  const remote = c.req.raw?.socket?.remoteAddress;
16181
- if (isLoopback(host, remote)) {
17605
+ const hasCfJwt = !!c.req.header("cf-access-jwt-assertion");
17606
+ if (!hasCfJwt && isLoopback(host, remote)) {
16182
17607
  return next();
16183
17608
  }
16184
17609
  const teamDomain = cfg.teamDomain;
@@ -16197,8 +17622,10 @@ function cfAccessMiddleware() {
16197
17622
  audience: aud
16198
17623
  });
16199
17624
  const email = String(payload.email || "").toLowerCase();
16200
- const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase());
16201
- if (allowed.length > 0 && !allowed.includes(email)) {
17625
+ const emailDomain = email.split("@")[1] || "";
17626
+ const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase().trim().replace(/^@/, "")).filter(Boolean);
17627
+ const isAllowed = allowed.length === 0 || allowed.some((entry2) => entry2.includes("@") ? entry2 === email : entry2 === emailDomain);
17628
+ if (!isAllowed) {
16202
17629
  console.warn(`[cf-access] rejected ${email}: not in allowlist`);
16203
17630
  return c.json({ error: "Email not allowed" }, 403);
16204
17631
  }
@@ -16396,13 +17823,13 @@ var DEFAULT_WEB_PORT = 6969;
16396
17823
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
16397
17824
  function getWebDirectory() {
16398
17825
  try {
16399
- const currentDir = dirname10(fileURLToPath4(import.meta.url));
17826
+ const currentDir = dirname11(fileURLToPath5(import.meta.url));
16400
17827
  const webDir = resolve12(currentDir, "..", "web");
16401
- if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
17828
+ if (existsSync22(webDir) && existsSync22(join19(webDir, "package.json"))) {
16402
17829
  return webDir;
16403
17830
  }
16404
17831
  const altWebDir = resolve12(currentDir, "..", "..", "web");
16405
- if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
17832
+ if (existsSync22(altWebDir) && existsSync22(join19(altWebDir, "package.json"))) {
16406
17833
  return altWebDir;
16407
17834
  }
16408
17835
  return null;
@@ -16460,20 +17887,20 @@ async function findWebPort(preferredPort) {
16460
17887
  return { port: preferredPort, alreadyRunning: false };
16461
17888
  }
16462
17889
  function hasProductionBuild(webDir) {
16463
- const buildIdPath = join17(webDir, ".next", "BUILD_ID");
17890
+ const buildIdPath = join19(webDir, ".next", "BUILD_ID");
16464
17891
  return existsSync22(buildIdPath);
16465
17892
  }
16466
17893
  function hasSourceFiles(webDir) {
16467
- const appDir = join17(webDir, "src", "app");
16468
- const pagesDir = join17(webDir, "src", "pages");
16469
- const rootAppDir = join17(webDir, "app");
16470
- const rootPagesDir = join17(webDir, "pages");
17894
+ const appDir = join19(webDir, "src", "app");
17895
+ const pagesDir = join19(webDir, "src", "pages");
17896
+ const rootAppDir = join19(webDir, "app");
17897
+ const rootPagesDir = join19(webDir, "pages");
16471
17898
  return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
16472
17899
  }
16473
17900
  function getStandaloneServerPath(webDir) {
16474
17901
  const possiblePaths2 = [
16475
- join17(webDir, ".next", "standalone", "server.js"),
16476
- join17(webDir, ".next", "standalone", "web", "server.js")
17902
+ join19(webDir, ".next", "standalone", "server.js"),
17903
+ join19(webDir, ".next", "standalone", "web", "server.js")
16477
17904
  ];
16478
17905
  for (const serverPath of possiblePaths2) {
16479
17906
  if (existsSync22(serverPath)) {
@@ -16484,7 +17911,7 @@ function getStandaloneServerPath(webDir) {
16484
17911
  }
16485
17912
  function runCommand(command, args, cwd, env) {
16486
17913
  return new Promise((resolve14) => {
16487
- const child = spawn2(command, args, {
17914
+ const child = spawn3(command, args, {
16488
17915
  cwd,
16489
17916
  stdio: ["ignore", "pipe", "pipe"],
16490
17917
  env,
@@ -16516,15 +17943,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16516
17943
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
16517
17944
  return { process: null, port: actualPort };
16518
17945
  }
16519
- const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
16520
- const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
17946
+ const usePnpm = existsSync22(join19(webDir, "pnpm-lock.yaml"));
17947
+ const useNpm = !usePnpm && existsSync22(join19(webDir, "package-lock.json"));
16521
17948
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
16522
17949
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
16523
17950
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
16524
- const runtimeConfig = { apiBaseUrl: apiUrl };
16525
- const runtimeConfigPath = join17(webDir, "runtime-config.json");
17951
+ const runtimeConfig = { apiBaseUrl: apiUrl, localApiBaseUrl: `http://127.0.0.1:${apiPort}` };
17952
+ const runtimeConfigPath = join19(webDir, "runtime-config.json");
16526
17953
  try {
16527
- writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
17954
+ writeFileSync8(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
16528
17955
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
16529
17956
  } catch (err) {
16530
17957
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -16544,7 +17971,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16544
17971
  if (standaloneServerPath) {
16545
17972
  command = "node";
16546
17973
  args = ["server.js"];
16547
- cwd = dirname10(standaloneServerPath);
17974
+ cwd = dirname11(standaloneServerPath);
16548
17975
  webEnv.PORT = String(actualPort);
16549
17976
  webEnv.HOSTNAME = "0.0.0.0";
16550
17977
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -16574,7 +18001,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16574
18001
  }
16575
18002
  return { process: null, port: actualPort };
16576
18003
  }
16577
- const child = spawn2(command, args, {
18004
+ const child = spawn3(command, args, {
16578
18005
  cwd,
16579
18006
  stdio: ["ignore", "pipe", "pipe"],
16580
18007
  env: webEnv,
@@ -16582,12 +18009,12 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16582
18009
  shell: true
16583
18010
  });
16584
18011
  const startupTimeout = 3e4;
16585
- let started = false;
18012
+ let started2 = false;
16586
18013
  let exited = false;
16587
18014
  let exitCode = null;
16588
18015
  const startedPromise = new Promise((resolve14) => {
16589
18016
  const timeout = setTimeout(() => {
16590
- if (!started && !exited) {
18017
+ if (!started2 && !exited) {
16591
18018
  resolve14(false);
16592
18019
  }
16593
18020
  }, startupTimeout);
@@ -16599,8 +18026,8 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16599
18026
  console.log(` Web UI: ${line}`);
16600
18027
  }
16601
18028
  }
16602
- if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
16603
- started = true;
18029
+ if (!started2 && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
18030
+ started2 = true;
16604
18031
  clearTimeout(timeout);
16605
18032
  resolve14(true);
16606
18033
  }
@@ -16619,7 +18046,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
16619
18046
  child.on("exit", (code) => {
16620
18047
  exited = true;
16621
18048
  exitCode = code;
16622
- if (!started) {
18049
+ if (!started2) {
16623
18050
  clearTimeout(timeout);
16624
18051
  resolve14(false);
16625
18052
  }
@@ -16739,7 +18166,7 @@ async function startServer(options = {}) {
16739
18166
  config.resolvedWorkingDirectory = options.workingDirectory;
16740
18167
  }
16741
18168
  if (!existsSync22(config.resolvedWorkingDirectory)) {
16742
- mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
18169
+ mkdirSync11(config.resolvedWorkingDirectory, { recursive: true });
16743
18170
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
16744
18171
  }
16745
18172
  if (!config.resolvedRemoteServer.url) {
@@ -16770,9 +18197,17 @@ async function startServer(options = {}) {
16770
18197
  try {
16771
18198
  const { startOrchestratorDaemon: startOrchestratorDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
16772
18199
  startOrchestratorDaemon2();
18200
+ const { startReconciler: startReconciler2 } = await Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports));
18201
+ startReconciler2();
16773
18202
  } catch (err) {
16774
18203
  if (!options.quiet) console.warn(`[daemon] start skipped: ${err.message}`);
16775
18204
  }
18205
+ try {
18206
+ const { startSelfUpdater: startSelfUpdater2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
18207
+ startSelfUpdater2();
18208
+ } catch (err) {
18209
+ if (!options.quiet) console.warn(`[self-update] start skipped: ${err.message}`);
18210
+ }
16776
18211
  try {
16777
18212
  const { startScheduler: startScheduler2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
16778
18213
  startScheduler2({ quiet: options.quiet });
@@ -17297,18 +18732,18 @@ function generateOpenAPISpec() {
17297
18732
  init_config();
17298
18733
  init_semantic();
17299
18734
  init_db();
17300
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync8, readFileSync as readFileSync11, existsSync as existsSync23, statSync as statSync4, unlinkSync as unlinkSync3 } from "fs";
17301
- import { resolve as resolve13, join as join18 } from "path";
18735
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync9, readFileSync as readFileSync12, existsSync as existsSync23, statSync as statSync4, unlinkSync as unlinkSync3 } from "fs";
18736
+ import { resolve as resolve13, join as join20 } from "path";
17302
18737
  function getCliVersion() {
17303
- const here = dirname11(fileURLToPath5(import.meta.url));
18738
+ const here = dirname12(fileURLToPath6(import.meta.url));
17304
18739
  const candidates = [
17305
- join18(here, "..", "package.json"),
17306
- join18(here, "..", "..", "package.json"),
17307
- join18(process.cwd(), "package.json")
18740
+ join20(here, "..", "package.json"),
18741
+ join20(here, "..", "..", "package.json"),
18742
+ join20(process.cwd(), "package.json")
17308
18743
  ];
17309
18744
  for (const p of candidates) {
17310
18745
  try {
17311
- const pkg = JSON.parse(readFileSync11(p, "utf8"));
18746
+ const pkg = JSON.parse(readFileSync12(p, "utf8"));
17312
18747
  if (pkg.name === "sparkecoder" && pkg.version) return pkg.version;
17313
18748
  } catch {
17314
18749
  }
@@ -17926,7 +19361,7 @@ program.command("server").description("Start the SparkECoder server (API + Web U
17926
19361
  program.command("chat").description("Start an interactive chat session").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").action(async (options) => {
17927
19362
  await runChat(options);
17928
19363
  });
17929
- program.command("task").description("Run an autonomous task that completes without human interaction").requiredOption("--prompt <prompt>", "Task prompt describing what to do").requiredOption("--schema <schema>", "JSON Schema for the output (file path or inline JSON string)").option("--webhook <url>", "Webhook URL to receive task events").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-n, --name <name>", "Name for the task").option("--max-iterations <n>", "Maximum agent loop iterations", "50").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("--no-auto-start", "Do not auto-start server if not running").option("--parent-task <id>", "Continue from a previous task (inherits its full conversation context)").option("--wait", "Block and poll until task completes").option("-v, --verbose", "Enable verbose logging").action(async (options) => {
19364
+ program.command("task").description("Run an autonomous task that completes without human interaction").requiredOption("--prompt <prompt>", "Task prompt describing what to do").requiredOption("--schema <schema>", "JSON Schema for the output (file path or inline JSON string)").option("--webhook <url>", "Webhook URL to receive task events").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-n, --name <name>", "Name for the task").option("--max-iterations <n>", "Maximum agent loop iterations", "50").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("--no-auto-start", "Do not auto-start server if not running").option("--parent-task <id>", "Continue from a previous task (inherits its full conversation context)").option("--mcp <servers>", "Task-scoped MCP servers as a JSON array (file path or inline JSON). Each: {name, transport, url?, headers?, command?, args?, env?}").option("--skills <skills>", "Task-scoped skills as a JSON array (file path or inline JSON). Each: {name, description?, content, alwaysApply?, globs?}").option("--wait", "Block and poll until task completes").option("-v, --verbose", "Enable verbose logging").action(async (options) => {
17930
19365
  await ensureDependencies({ quiet: true });
17931
19366
  loadApiKeysIntoEnv();
17932
19367
  const baseUrl = `http://${options.host}:${options.port}`;
@@ -17956,7 +19391,7 @@ program.command("task").description("Run an autonomous task that completes witho
17956
19391
  try {
17957
19392
  const schemaStr = options.schema;
17958
19393
  if (existsSync23(schemaStr)) {
17959
- outputSchema = JSON.parse(readFileSync11(schemaStr, "utf-8"));
19394
+ outputSchema = JSON.parse(readFileSync12(schemaStr, "utf-8"));
17960
19395
  } else {
17961
19396
  outputSchema = JSON.parse(schemaStr);
17962
19397
  }
@@ -17975,6 +19410,30 @@ program.command("task").description("Run an autonomous task that completes witho
17975
19410
  if (options.workingDir) body.workingDirectory = options.workingDir;
17976
19411
  if (options.name) body.name = options.name;
17977
19412
  if (options.parentTask) body.parentTaskId = options.parentTask;
19413
+ const parseJsonArrayOption = (raw, label) => {
19414
+ const text = existsSync23(raw) ? readFileSync12(raw, "utf-8") : raw;
19415
+ const parsed = JSON.parse(text);
19416
+ if (!Array.isArray(parsed)) {
19417
+ throw new Error(`${label} must be a JSON array`);
19418
+ }
19419
+ return parsed;
19420
+ };
19421
+ if (options.mcp) {
19422
+ try {
19423
+ body.mcpServers = parseJsonArrayOption(options.mcp, "--mcp");
19424
+ } catch (err) {
19425
+ console.error(chalk.red(`Invalid --mcp value: ${err.message}`));
19426
+ process.exit(1);
19427
+ }
19428
+ }
19429
+ if (options.skills) {
19430
+ try {
19431
+ body.skills = parseJsonArrayOption(options.skills, "--skills");
19432
+ } catch (err) {
19433
+ console.error(chalk.red(`Invalid --skills value: ${err.message}`));
19434
+ process.exit(1);
19435
+ }
19436
+ }
17978
19437
  const spinner = ora("Creating task...").start();
17979
19438
  const response = await apiRequest(baseUrl, "/tasks", {
17980
19439
  method: "POST",
@@ -18023,7 +19482,7 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
18023
19482
  let configLocation;
18024
19483
  if (options.global) {
18025
19484
  const appDataDir = ensureAppDataDirectory();
18026
- configPath = join18(appDataDir, "sparkecoder.config.json");
19485
+ configPath = join20(appDataDir, "sparkecoder.config.json");
18027
19486
  configLocation = "global";
18028
19487
  } else {
18029
19488
  configPath = resolve13(process.cwd(), "sparkecoder.config.json");
@@ -18035,7 +19494,7 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
18035
19494
  return;
18036
19495
  }
18037
19496
  const config = createDefaultConfig();
18038
- writeFileSync8(configPath, JSON.stringify(config, null, 2));
19497
+ writeFileSync9(configPath, JSON.stringify(config, null, 2));
18039
19498
  console.log(chalk.green(`\u2713 Created ${configLocation} config`));
18040
19499
  console.log(chalk.dim(` ${configPath}`));
18041
19500
  console.log(chalk.dim("Set AI_GATEWAY_API_KEY and run sparkecoder to start"));
@@ -18054,11 +19513,11 @@ program.command("slack-setup").description("Interactively configure Slack integr
18054
19513
  console.error(chalk.red("Both bot token and signing secret are required."));
18055
19514
  process.exit(1);
18056
19515
  }
18057
- const configPath = options.global ? join18(ensureAppDataDirectory(), "sparkecoder.config.json") : resolve13(process.cwd(), "sparkecoder.config.json");
19516
+ const configPath = options.global ? join20(ensureAppDataDirectory(), "sparkecoder.config.json") : resolve13(process.cwd(), "sparkecoder.config.json");
18058
19517
  let existing = {};
18059
19518
  if (existsSync23(configPath)) {
18060
19519
  try {
18061
- existing = JSON.parse(readFileSync11(configPath, "utf-8"));
19520
+ existing = JSON.parse(readFileSync12(configPath, "utf-8"));
18062
19521
  } catch {
18063
19522
  }
18064
19523
  } else {
@@ -18070,7 +19529,7 @@ program.command("slack-setup").description("Interactively configure Slack integr
18070
19529
  signingSecret,
18071
19530
  defaultOrchestratorName: existing.slack?.defaultOrchestratorName ?? "orchestrator"
18072
19531
  };
18073
- writeFileSync8(configPath, JSON.stringify(existing, null, 2));
19532
+ writeFileSync9(configPath, JSON.stringify(existing, null, 2));
18074
19533
  console.log(chalk.green(`
18075
19534
  \u2713 Slack configured`));
18076
19535
  console.log(chalk.dim(` ${configPath}`));
@@ -18138,15 +19597,15 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18138
19597
  if (options.remote) {
18139
19598
  try {
18140
19599
  let config = loadConfig(options.config, process.cwd());
18141
- let remoteUrl = config.resolvedRemoteServer.url;
18142
- if (!remoteUrl) {
19600
+ let remoteUrl2 = config.resolvedRemoteServer.url;
19601
+ if (!remoteUrl2) {
18143
19602
  console.log(chalk.red("No remoteServer.url configured. Run `sparkecoder login` (or set remoteServer.url in your config) first."));
18144
19603
  process.exitCode = 1;
18145
19604
  return;
18146
19605
  }
18147
19606
  let authKey3 = config.resolvedRemoteServer.authKey;
18148
19607
  if (!authKey3) {
18149
- authKey3 = await ensureRemoteAuthKey(remoteUrl);
19608
+ authKey3 = await ensureRemoteAuthKey(remoteUrl2);
18150
19609
  }
18151
19610
  const setupSecret = options.setupSecret || process.env.SPARKECODER_SETUP_SECRET || process.env.SPARKECODER_TUNNEL_SECRET;
18152
19611
  const hostname = options.hostname;
@@ -18160,7 +19619,7 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18160
19619
  name: tunnelName
18161
19620
  };
18162
19621
  if (hostname) reqBody.hostname = hostname;
18163
- const res = await fetch(`${remoteUrl.replace(/\/$/, "")}${path}`, {
19622
+ const res = await fetch(`${remoteUrl2.replace(/\/$/, "")}${path}`, {
18164
19623
  method: "POST",
18165
19624
  headers: {
18166
19625
  "Content-Type": "application/json",
@@ -18175,7 +19634,7 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18175
19634
  process.exitCode = 1;
18176
19635
  return;
18177
19636
  }
18178
- const { tunnelToken, hostname: provisionedHost, apiHostname, reused, webhookToken, webhookBaseUrl } = await res.json();
19637
+ const { tunnelToken, hostname: provisionedHost, apiHostname, reused, webhookToken, webhookBaseUrl, cfAccess } = await res.json();
18179
19638
  if (reused) {
18180
19639
  console.log(chalk.dim("\u2713 Re-using existing tunnel \u2014 no new tunnel created on Cloudflare."));
18181
19640
  }
@@ -18193,6 +19652,20 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18193
19652
  console.warn(chalk.yellow(`Could not persist webhook token: ${err?.message ?? err}`));
18194
19653
  }
18195
19654
  }
19655
+ if (cfAccess?.teamDomain && cfAccess?.audTag) {
19656
+ try {
19657
+ setCfAccessConfig({
19658
+ enabled: true,
19659
+ teamDomain: cfAccess.teamDomain,
19660
+ audTag: cfAccess.audTag,
19661
+ allowedEmails: cfAccess.allowedEmails ?? []
19662
+ });
19663
+ const n = (cfAccess.allowedEmails ?? []).length;
19664
+ console.log(chalk.dim(`\u2713 Enabled Cloudflare Access locally (allowlist: ${n} email${n === 1 ? "" : "s"}).`));
19665
+ } catch (err) {
19666
+ console.warn(chalk.yellow(`Could not persist Cloudflare Access config: ${err?.message ?? err}`));
19667
+ }
19668
+ }
18196
19669
  const cfPath = await installCloudflaredIfNeeded();
18197
19670
  if (!cfPath) {
18198
19671
  console.log(chalk.yellow("cloudflared not installed; run this once installed:"));
@@ -18322,8 +19795,8 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18322
19795
  }
18323
19796
  const verOut = run("cloudflared", ["--version"]).stdout?.toString().split("\n")[0] || "installed";
18324
19797
  console.log(chalk.green("\u2713"), "cloudflared:", chalk.dim(verOut));
18325
- const cfDir = join18(homedir2(), ".cloudflared");
18326
- const certPath = join18(cfDir, "cert.pem");
19798
+ const cfDir = join20(homedir2(), ".cloudflared");
19799
+ const certPath = join20(cfDir, "cert.pem");
18327
19800
  if (!existsSync23(certPath)) {
18328
19801
  console.log(chalk.yellow("No Cloudflare login cert found."));
18329
19802
  if (await confirm("Run `cloudflared tunnel login` now (opens a browser)?", true)) {
@@ -18368,7 +19841,7 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18368
19841
  return;
18369
19842
  }
18370
19843
  }
18371
- const credsFile = tunnel.credentials_file || join18(cfDir, `${tunnel.id}.json`);
19844
+ const credsFile = tunnel.credentials_file || join20(cfDir, `${tunnel.id}.json`);
18372
19845
  if (!existsSync23(credsFile)) {
18373
19846
  console.log(chalk.yellow(`Credentials file not found at ${credsFile}. The tunnel may still work via cert.pem.`));
18374
19847
  }
@@ -18392,7 +19865,7 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
18392
19865
  console.log(chalk.yellow("DNS route warning:"), err.trim().split("\n").slice(-2).join(" "));
18393
19866
  }
18394
19867
  }
18395
- const configPath = join18(cfDir, "config.yml");
19868
+ const configPath = join20(cfDir, "config.yml");
18396
19869
  const configBody = `tunnel: ${tunnel.id}
18397
19870
  credentials-file: ${credsFile}
18398
19871
  ingress:
@@ -18404,13 +19877,13 @@ ingress:
18404
19877
  `;
18405
19878
  let wroteConfig = false;
18406
19879
  if (existsSync23(configPath)) {
18407
- const existing = readFileSync11(configPath, "utf8");
19880
+ const existing = readFileSync12(configPath, "utf8");
18408
19881
  if (existing.trim() === configBody.trim()) {
18409
19882
  console.log(chalk.green("\u2713"), `config.yml already up to date: ${configPath}`);
18410
19883
  wroteConfig = true;
18411
19884
  } else if (await confirm(`A different ${configPath} exists. Overwrite (a backup will be saved)?`, false)) {
18412
19885
  copyFileSync(configPath, `${configPath}.bak.${Date.now()}`);
18413
- writeFileSync8(configPath, configBody);
19886
+ writeFileSync9(configPath, configBody);
18414
19887
  console.log(chalk.green("\u2713"), `wrote ${configPath} (previous saved as .bak.*)`);
18415
19888
  wroteConfig = true;
18416
19889
  } else {
@@ -18418,7 +19891,7 @@ ingress:
18418
19891
  console.log(chalk.cyan(configBody));
18419
19892
  }
18420
19893
  } else {
18421
- writeFileSync8(configPath, configBody);
19894
+ writeFileSync9(configPath, configBody);
18422
19895
  console.log(chalk.green("\u2713"), `wrote ${configPath}`);
18423
19896
  wroteConfig = true;
18424
19897
  }
@@ -18521,8 +19994,8 @@ program.command("index").description("Index repository for semantic search").opt
18521
19994
  try {
18522
19995
  const workingDir = options.workingDir ? resolve13(options.workingDir) : process.cwd();
18523
19996
  let config = loadConfig(options.config, workingDir);
18524
- const remoteUrl = config.resolvedRemoteServer.url;
18525
- if (!remoteUrl) {
19997
+ const remoteUrl2 = config.resolvedRemoteServer.url;
19998
+ if (!remoteUrl2) {
18526
19999
  console.error(chalk.red("Error: Remote server not configured"));
18527
20000
  console.log(chalk.dim("Set REMOTE_SERVER_URL environment variable or remoteServer.url in config"));
18528
20001
  process.exit(1);
@@ -18530,7 +20003,7 @@ program.command("index").description("Index repository for semantic search").opt
18530
20003
  let authKey3 = config.resolvedRemoteServer.authKey;
18531
20004
  if (!authKey3) {
18532
20005
  console.log(chalk.dim("Registering with remote server..."));
18533
- authKey3 = await ensureRemoteAuthKey(remoteUrl);
20006
+ authKey3 = await ensureRemoteAuthKey(remoteUrl2);
18534
20007
  config = loadConfig(options.config, workingDir);
18535
20008
  authKey3 = config.resolvedRemoteServer.authKey || authKey3;
18536
20009
  }
@@ -18539,7 +20012,7 @@ program.command("index").description("Index repository for semantic search").opt
18539
20012
  console.log(chalk.dim("Set SPARKECODER_AUTH_KEY or run sparkecoder to register with the remote server"));
18540
20013
  process.exit(1);
18541
20014
  }
18542
- initDatabase({ url: remoteUrl, authKey: authKey3 });
20015
+ initDatabase({ url: remoteUrl2, authKey: authKey3 });
18543
20016
  if (!isGitRepository(workingDir)) {
18544
20017
  console.error(chalk.red("Error: Not a git repository"));
18545
20018
  console.log(chalk.dim("Semantic indexing requires a git repository with a remote configured."));
@@ -18655,20 +20128,20 @@ program.command("search").description("Search indexed repository using semantic
18655
20128
  try {
18656
20129
  const workingDir = options.workingDir ? resolve13(options.workingDir) : process.cwd();
18657
20130
  const config = loadConfig(options.config, workingDir);
18658
- const remoteUrl = config.resolvedRemoteServer.url;
18659
- if (!remoteUrl) {
20131
+ const remoteUrl2 = config.resolvedRemoteServer.url;
20132
+ if (!remoteUrl2) {
18660
20133
  console.error(chalk.red("Error: Remote server not configured"));
18661
20134
  process.exit(1);
18662
20135
  }
18663
20136
  let authKey3 = config.resolvedRemoteServer.authKey;
18664
20137
  if (!authKey3) {
18665
- authKey3 = await ensureRemoteAuthKey(remoteUrl);
20138
+ authKey3 = await ensureRemoteAuthKey(remoteUrl2);
18666
20139
  }
18667
20140
  if (!authKey3) {
18668
20141
  console.error(chalk.red("Error: Remote auth key not available"));
18669
20142
  process.exit(1);
18670
20143
  }
18671
- initDatabase({ url: remoteUrl, authKey: authKey3 });
20144
+ initDatabase({ url: remoteUrl2, authKey: authKey3 });
18672
20145
  const namespace = await getRepoNamespace(workingDir, config.resolvedVectorGateway.namespace);
18673
20146
  if (!namespace) {
18674
20147
  console.error(chalk.red("Error: Could not determine repository namespace"));
@@ -18910,17 +20383,17 @@ program.command("request-permissions").description("Open System Settings to the
18910
20383
  let shellEscape2 = function(str) {
18911
20384
  return `'${str.replace(/'/g, "'\\''")}'`;
18912
20385
  }, stateFilePath = function() {
18913
- return join18(ensureAppDataDirectory(), "recordings.json");
18914
- }, readState = function() {
20386
+ return join20(ensureAppDataDirectory(), "recordings.json");
20387
+ }, readState2 = function() {
18915
20388
  const p = stateFilePath();
18916
20389
  if (!existsSync23(p)) return [];
18917
20390
  try {
18918
- return JSON.parse(readFileSync11(p, "utf-8"));
20391
+ return JSON.parse(readFileSync12(p, "utf-8"));
18919
20392
  } catch {
18920
20393
  return [];
18921
20394
  }
18922
- }, writeState = function(rows) {
18923
- writeFileSync8(stateFilePath(), JSON.stringify(rows, null, 2), { mode: 384 });
20395
+ }, writeState2 = function(rows) {
20396
+ writeFileSync9(stateFilePath(), JSON.stringify(rows, null, 2), { mode: 384 });
18924
20397
  }, isAlive = function(pid) {
18925
20398
  try {
18926
20399
  process.kill(pid, 0);
@@ -18931,7 +20404,7 @@ program.command("request-permissions").description("Open System Settings to the
18931
20404
  }, pruneDead = function(rows) {
18932
20405
  return rows.filter((r) => isAlive(r.pid));
18933
20406
  };
18934
- shellEscape3 = shellEscape2, stateFilePath2 = stateFilePath, readState2 = readState, writeState2 = writeState, isAlive2 = isAlive, pruneDead2 = pruneDead;
20407
+ shellEscape3 = shellEscape2, stateFilePath2 = stateFilePath, readState3 = readState2, writeState3 = writeState2, isAlive2 = isAlive, pruneDead2 = pruneDead;
18935
20408
  async function stopRecorderPid(pid) {
18936
20409
  if (!isAlive(pid)) return;
18937
20410
  try {
@@ -18993,12 +20466,12 @@ program.command("request-permissions").description("Open System Settings to the
18993
20466
  const record = program.command("record").description("Start/stop screen recordings");
18994
20467
  record.command("start").description("Start a screen recording (returns id, path, pid as JSON)").option("--name <slug>", "Optional human-readable label for the recording").option("--dir <path>", "Output directory (default: ~/recordings)").action(async (opts) => {
18995
20468
  const { homedir: homedir2, platform: osPlatform } = await import("os");
18996
- const outDir = opts.dir ? resolve13(opts.dir.replace(/^~/, homedir2())) : join18(homedir2(), "recordings");
18997
- mkdirSync11(outDir, { recursive: true });
20469
+ const outDir = opts.dir ? resolve13(opts.dir.replace(/^~/, homedir2())) : join20(homedir2(), "recordings");
20470
+ mkdirSync12(outDir, { recursive: true });
18998
20471
  const id = `rec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
18999
20472
  const ext = "mp4";
19000
20473
  const filename = `${id}${opts.name ? `-${String(opts.name).replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}` : ""}.${ext}`;
19001
- const path = join18(outDir, filename);
20474
+ const path = join20(outDir, filename);
19002
20475
  let cmd;
19003
20476
  let args;
19004
20477
  let capturePath;
@@ -19080,13 +20553,13 @@ program.command("request-permissions").description("Open System Settings to the
19080
20553
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
19081
20554
  platform: osPlatform()
19082
20555
  };
19083
- const rows = pruneDead(readState());
20556
+ const rows = pruneDead(readState2());
19084
20557
  rows.push(row);
19085
- writeState(rows);
20558
+ writeState2(rows);
19086
20559
  console.log(JSON.stringify({ id, path, pid: child.pid, platform: osPlatform() }));
19087
20560
  });
19088
20561
  record.command("stop <id>").description("Stop a recording started by `sparkecoder record start`").action(async (id) => {
19089
- const rows = readState();
20562
+ const rows = readState2();
19090
20563
  const row = rows.find((r) => r.id === id);
19091
20564
  if (!row) {
19092
20565
  console.error(JSON.stringify({ error: `No recording found with id ${id}` }));
@@ -19094,7 +20567,7 @@ program.command("request-permissions").description("Open System Settings to the
19094
20567
  }
19095
20568
  const startedAt = new Date(row.startedAt).getTime();
19096
20569
  const { path: finalPath, ok, sizeMb, warning } = await finalizeRecording(row);
19097
- writeState(rows.filter((r) => r.id !== id));
20570
+ writeState2(rows.filter((r) => r.id !== id));
19098
20571
  console.log(JSON.stringify({
19099
20572
  id,
19100
20573
  path: finalPath,
@@ -19105,25 +20578,25 @@ program.command("request-permissions").description("Open System Settings to the
19105
20578
  }));
19106
20579
  });
19107
20580
  record.command("list").description("List active recordings").action(() => {
19108
- const rows = pruneDead(readState());
19109
- writeState(rows);
20581
+ const rows = pruneDead(readState2());
20582
+ writeState2(rows);
19110
20583
  console.log(JSON.stringify(rows, null, 2));
19111
20584
  });
19112
20585
  record.command("stop-all").description("Stop every active recording").action(async () => {
19113
- const rows = readState();
20586
+ const rows = readState2();
19114
20587
  const stopped = [];
19115
20588
  for (const r of rows) {
19116
20589
  const { path: finalPath, ok, warning } = await finalizeRecording(r);
19117
20590
  stopped.push({ id: r.id, path: finalPath, ok, ...warning ? { warning } : {} });
19118
20591
  }
19119
- writeState([]);
20592
+ writeState2([]);
19120
20593
  console.log(JSON.stringify({ stopped }));
19121
20594
  });
19122
20595
  }
19123
20596
  var shellEscape3;
19124
20597
  var stateFilePath2;
19125
- var readState2;
19126
- var writeState2;
20598
+ var readState3;
20599
+ var writeState3;
19127
20600
  var isAlive2;
19128
20601
  var pruneDead2;
19129
20602
  program.parse();