mixdog 0.7.17 → 0.8.0

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 (836) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/cost.mjs +66 -0
  179. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  180. package/src/runtime/shared/open-url.mjs +37 -0
  181. package/src/runtime/shared/plugin-paths.mjs +25 -0
  182. package/src/runtime/shared/process-shutdown.mjs +147 -0
  183. package/src/runtime/shared/schedules-store.mjs +70 -0
  184. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  185. package/src/runtime/shared/tool-surface.mjs +950 -0
  186. package/src/runtime/shared/user-cwd.mjs +221 -0
  187. package/src/runtime/shared/user-data-guard.mjs +232 -0
  188. package/src/runtime/shared/workspace-router.mjs +259 -0
  189. package/src/standalone/bridge-tool.mjs +1414 -0
  190. package/src/standalone/channel-admin.mjs +366 -0
  191. package/src/standalone/channel-worker-preload.cjs +3 -0
  192. package/src/standalone/channel-worker.mjs +353 -0
  193. package/src/standalone/explore-tool.mjs +233 -0
  194. package/src/standalone/hook-bus.mjs +246 -0
  195. package/src/standalone/plugin-admin.mjs +247 -0
  196. package/src/standalone/provider-admin.mjs +338 -0
  197. package/src/standalone/seeds.mjs +94 -0
  198. package/src/standalone/usage-dashboard.mjs +510 -0
  199. package/src/tui/App.jsx +5438 -0
  200. package/src/tui/components/AnsiText.jsx +199 -0
  201. package/src/tui/components/ContextPanel.jsx +217 -0
  202. package/src/tui/components/Markdown.jsx +205 -0
  203. package/src/tui/components/MarkdownTable.jsx +204 -0
  204. package/src/tui/components/Message.jsx +103 -0
  205. package/src/tui/components/Picker.jsx +317 -0
  206. package/src/tui/components/PromptInput.jsx +584 -0
  207. package/src/tui/components/QueuedCommands.jsx +47 -0
  208. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  209. package/src/tui/components/Spinner.jsx +317 -0
  210. package/src/tui/components/StatusLine.jsx +87 -0
  211. package/src/tui/components/TextEntryPanel.jsx +323 -0
  212. package/src/tui/components/ToolExecution.jsx +772 -0
  213. package/src/tui/components/TurnDone.jsx +78 -0
  214. package/src/tui/components/UsagePanel.jsx +331 -0
  215. package/src/tui/dist/index.mjs +12359 -0
  216. package/src/tui/engine.mjs +2410 -0
  217. package/src/tui/figures.mjs +50 -0
  218. package/src/tui/hooks/useEngine.mjs +16 -0
  219. package/src/tui/index.jsx +254 -0
  220. package/src/tui/input-editing.mjs +242 -0
  221. package/src/tui/markdown/format-token.mjs +194 -0
  222. package/src/tui/paste-attachments.mjs +198 -0
  223. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  224. package/src/tui/spinner-verbs.mjs +45 -0
  225. package/src/tui/theme.mjs +67 -0
  226. package/src/tui/time-format.mjs +53 -0
  227. package/src/ui/ansi.mjs +115 -0
  228. package/src/ui/markdown.mjs +195 -0
  229. package/src/ui/statusline.mjs +730 -0
  230. package/src/ui/tool-card.mjs +101 -0
  231. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  232. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  233. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  234. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  235. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  236. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  237. package/src/workflows/default/WORKFLOW.md +7 -0
  238. package/src/workflows/default/workflow.json +14 -0
  239. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  240. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  241. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  242. package/vendor/ink/build/colorize.d.ts +3 -0
  243. package/vendor/ink/build/colorize.js +48 -0
  244. package/vendor/ink/build/colorize.js.map +1 -0
  245. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  247. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  248. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  249. package/vendor/ink/build/components/AnimationContext.js +13 -0
  250. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  251. package/vendor/ink/build/components/App.d.ts +24 -0
  252. package/vendor/ink/build/components/App.js +554 -0
  253. package/vendor/ink/build/components/App.js.map +1 -0
  254. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  255. package/vendor/ink/build/components/AppContext.js +25 -0
  256. package/vendor/ink/build/components/AppContext.js.map +1 -0
  257. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  258. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  259. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  260. package/vendor/ink/build/components/Box.d.ts +130 -0
  261. package/vendor/ink/build/components/Box.js +34 -0
  262. package/vendor/ink/build/components/Box.js.map +1 -0
  263. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  264. package/vendor/ink/build/components/CursorContext.js +8 -0
  265. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  266. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  268. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  269. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  270. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  271. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  272. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  273. package/vendor/ink/build/components/FocusContext.js +17 -0
  274. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  275. package/vendor/ink/build/components/Newline.d.ts +13 -0
  276. package/vendor/ink/build/components/Newline.js +8 -0
  277. package/vendor/ink/build/components/Newline.js.map +1 -0
  278. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  279. package/vendor/ink/build/components/Spacer.js +11 -0
  280. package/vendor/ink/build/components/Spacer.js.map +1 -0
  281. package/vendor/ink/build/components/Static.d.ts +24 -0
  282. package/vendor/ink/build/components/Static.js +28 -0
  283. package/vendor/ink/build/components/Static.js.map +1 -0
  284. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  285. package/vendor/ink/build/components/StderrContext.js +13 -0
  286. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  287. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  288. package/vendor/ink/build/components/StdinContext.js +20 -0
  289. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  290. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  291. package/vendor/ink/build/components/StdoutContext.js +13 -0
  292. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  293. package/vendor/ink/build/components/Text.d.ts +55 -0
  294. package/vendor/ink/build/components/Text.js +50 -0
  295. package/vendor/ink/build/components/Text.js.map +1 -0
  296. package/vendor/ink/build/components/Transform.d.ts +16 -0
  297. package/vendor/ink/build/components/Transform.js +15 -0
  298. package/vendor/ink/build/components/Transform.js.map +1 -0
  299. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  300. package/vendor/ink/build/cursor-helpers.js +62 -0
  301. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  304. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  305. package/vendor/ink/build/devtools.d.ts +1 -0
  306. package/vendor/ink/build/devtools.js +36 -0
  307. package/vendor/ink/build/devtools.js.map +1 -0
  308. package/vendor/ink/build/dom.d.ts +62 -0
  309. package/vendor/ink/build/dom.js +143 -0
  310. package/vendor/ink/build/dom.js.map +1 -0
  311. package/vendor/ink/build/get-max-width.d.ts +3 -0
  312. package/vendor/ink/build/get-max-width.js +10 -0
  313. package/vendor/ink/build/get-max-width.js.map +1 -0
  314. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  315. package/vendor/ink/build/hooks/use-animation.js +87 -0
  316. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  317. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  318. package/vendor/ink/build/hooks/use-app.js +8 -0
  319. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  322. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  323. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  324. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  325. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  328. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  329. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  330. package/vendor/ink/build/hooks/use-focus.js +43 -0
  331. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  332. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  333. package/vendor/ink/build/hooks/use-input.js +126 -0
  334. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  337. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  338. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  339. package/vendor/ink/build/hooks/use-paste.js +62 -0
  340. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  341. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  342. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  343. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  344. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  345. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  346. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  347. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  348. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  349. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  350. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  351. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  352. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  353. package/vendor/ink/build/index.d.ts +42 -0
  354. package/vendor/ink/build/index.js +24 -0
  355. package/vendor/ink/build/index.js.map +1 -0
  356. package/vendor/ink/build/ink.d.ts +146 -0
  357. package/vendor/ink/build/ink.js +1022 -0
  358. package/vendor/ink/build/ink.js.map +1 -0
  359. package/vendor/ink/build/input-parser.d.ts +10 -0
  360. package/vendor/ink/build/input-parser.js +194 -0
  361. package/vendor/ink/build/input-parser.js.map +1 -0
  362. package/vendor/ink/build/instances.d.ts +3 -0
  363. package/vendor/ink/build/instances.js +8 -0
  364. package/vendor/ink/build/instances.js.map +1 -0
  365. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  366. package/vendor/ink/build/kitty-keyboard.js +32 -0
  367. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  368. package/vendor/ink/build/log-update.d.ts +20 -0
  369. package/vendor/ink/build/log-update.js +261 -0
  370. package/vendor/ink/build/log-update.js.map +1 -0
  371. package/vendor/ink/build/measure-element.d.ts +20 -0
  372. package/vendor/ink/build/measure-element.js +13 -0
  373. package/vendor/ink/build/measure-element.js.map +1 -0
  374. package/vendor/ink/build/measure-text.d.ts +6 -0
  375. package/vendor/ink/build/measure-text.js +21 -0
  376. package/vendor/ink/build/measure-text.js.map +1 -0
  377. package/vendor/ink/build/output.d.ts +35 -0
  378. package/vendor/ink/build/output.js +328 -0
  379. package/vendor/ink/build/output.js.map +1 -0
  380. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  381. package/vendor/ink/build/parse-keypress.js +495 -0
  382. package/vendor/ink/build/parse-keypress.js.map +1 -0
  383. package/vendor/ink/build/reconciler.d.ts +4 -0
  384. package/vendor/ink/build/reconciler.js +306 -0
  385. package/vendor/ink/build/reconciler.js.map +1 -0
  386. package/vendor/ink/build/render-background.d.ts +4 -0
  387. package/vendor/ink/build/render-background.js +25 -0
  388. package/vendor/ink/build/render-background.js.map +1 -0
  389. package/vendor/ink/build/render-border.d.ts +4 -0
  390. package/vendor/ink/build/render-border.js +84 -0
  391. package/vendor/ink/build/render-border.js.map +1 -0
  392. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  393. package/vendor/ink/build/render-node-to-output.js +162 -0
  394. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  395. package/vendor/ink/build/render-to-string.d.ts +38 -0
  396. package/vendor/ink/build/render-to-string.js +116 -0
  397. package/vendor/ink/build/render-to-string.js.map +1 -0
  398. package/vendor/ink/build/render.d.ts +176 -0
  399. package/vendor/ink/build/render.js +71 -0
  400. package/vendor/ink/build/render.js.map +1 -0
  401. package/vendor/ink/build/renderer.d.ts +8 -0
  402. package/vendor/ink/build/renderer.js +64 -0
  403. package/vendor/ink/build/renderer.js.map +1 -0
  404. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  405. package/vendor/ink/build/sanitize-ansi.js +27 -0
  406. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  407. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  408. package/vendor/ink/build/squash-text-nodes.js +36 -0
  409. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  410. package/vendor/ink/build/styles.d.ts +302 -0
  411. package/vendor/ink/build/styles.js +303 -0
  412. package/vendor/ink/build/styles.js.map +1 -0
  413. package/vendor/ink/build/utils.d.ts +9 -0
  414. package/vendor/ink/build/utils.js +19 -0
  415. package/vendor/ink/build/utils.js.map +1 -0
  416. package/vendor/ink/build/wrap-text.d.ts +3 -0
  417. package/vendor/ink/build/wrap-text.js +38 -0
  418. package/vendor/ink/build/wrap-text.js.map +1 -0
  419. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  420. package/vendor/ink/build/write-synchronized.js +9 -0
  421. package/vendor/ink/build/write-synchronized.js.map +1 -0
  422. package/vendor/ink/license +10 -0
  423. package/vendor/ink/package.json +137 -0
  424. package/.claude-plugin/marketplace.json +0 -34
  425. package/.claude-plugin/plugin.json +0 -20
  426. package/.gitattributes +0 -34
  427. package/.mcp.json +0 -14
  428. package/ARCHITECTURE.md +0 -77
  429. package/CHANGELOG.md +0 -30
  430. package/CONTRIBUTING.md +0 -45
  431. package/DATA-FLOW.md +0 -79
  432. package/LICENSE +0 -21
  433. package/SECURITY.md +0 -138
  434. package/UNINSTALL.md +0 -112
  435. package/agents/maintenance.md +0 -5
  436. package/agents/memory-classification.md +0 -30
  437. package/agents/scheduler-task.md +0 -18
  438. package/agents/webhook-handler.md +0 -27
  439. package/agents/worker.md +0 -24
  440. package/bin/bridge +0 -133
  441. package/bin/statusline-launcher.mjs +0 -82
  442. package/bin/statusline-lib.mjs +0 -558
  443. package/bin/statusline.mjs +0 -615
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/setup.md +0 -17
  448. package/defaults/hidden-roles.json +0 -68
  449. package/defaults/memory-chunk-prompt.md +0 -63
  450. package/defaults/mixdog-config.template.json +0 -27
  451. package/defaults/user-workflow.json +0 -8
  452. package/defaults/user-workflow.md +0 -17
  453. package/hooks/hooks.json +0 -73
  454. package/hooks/lib/active-instance.cjs +0 -77
  455. package/hooks/lib/permission-evaluator.cjs +0 -411
  456. package/hooks/lib/permission-route.cjs +0 -63
  457. package/hooks/lib/settings-loader.cjs +0 -117
  458. package/hooks/post-tool-use.cjs +0 -84
  459. package/hooks/pre-mcp-sandbox.cjs +0 -158
  460. package/hooks/pre-tool-subagent.cjs +0 -258
  461. package/hooks/session-start.cjs +0 -1479
  462. package/hooks/shim-launcher.cjs +0 -65
  463. package/hooks/turn-timer.cjs +0 -82
  464. package/lib/claude-md-writer.cjs +0 -386
  465. package/lib/keychain-cjs.cjs +0 -290
  466. package/lib/plugin-paths.cjs +0 -69
  467. package/lib/rules-builder.cjs +0 -241
  468. package/native/README.md +0 -117
  469. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  470. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  471. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  473. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  474. package/prompts/code-review.txt +0 -16
  475. package/prompts/security-audit.txt +0 -17
  476. package/rules/bridge/00-common.md +0 -39
  477. package/rules/bridge/20-skip-protocol.md +0 -18
  478. package/rules/bridge/30-explorer.md +0 -33
  479. package/rules/bridge/40-cycle1-agent.md +0 -52
  480. package/rules/bridge/41-cycle2-agent.md +0 -62
  481. package/rules/lead/00-tool-lead.md +0 -61
  482. package/rules/lead/01-general.md +0 -23
  483. package/rules/lead/02-channels.md +0 -49
  484. package/rules/lead/03-team.md +0 -27
  485. package/rules/lead/04-workflow.md +0 -20
  486. package/rules/shared/00-language.md +0 -14
  487. package/rules/shared/01-tool.md +0 -138
  488. package/scripts/bootstrap.mjs +0 -130
  489. package/scripts/bridge-unify-smoke.mjs +0 -308
  490. package/scripts/build-runtime-linux.sh +0 -348
  491. package/scripts/build-runtime-macos.sh +0 -217
  492. package/scripts/build-runtime-windows.ps1 +0 -242
  493. package/scripts/builtin-utils-smoke.mjs +0 -398
  494. package/scripts/bump.mjs +0 -80
  495. package/scripts/check-json.mjs +0 -45
  496. package/scripts/check-syntax-changed.mjs +0 -102
  497. package/scripts/check-syntax.mjs +0 -58
  498. package/scripts/code-graph-batch.test.mjs +0 -33
  499. package/scripts/config-preserve-smoke.mjs +0 -180
  500. package/scripts/doctor.mjs +0 -489
  501. package/scripts/edit-normalize-fuzz.mjs +0 -130
  502. package/scripts/edit-normalize-smoke.mjs +0 -401
  503. package/scripts/edit-operation-smoke.mjs +0 -369
  504. package/scripts/edit2-smoke.mjs +0 -63
  505. package/scripts/ensure-deps.mjs +0 -259
  506. package/scripts/fuzzy-e2e.mjs +0 -28
  507. package/scripts/fuzzy-smoke.mjs +0 -26
  508. package/scripts/generate-runtime-manifest.mjs +0 -166
  509. package/scripts/guard-smoke.mjs +0 -66
  510. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  511. package/scripts/hook-routing-smoke.mjs +0 -29
  512. package/scripts/inject-input.ps1 +0 -204
  513. package/scripts/io-complex-smoke.mjs +0 -667
  514. package/scripts/io-explore-bench.mjs +0 -424
  515. package/scripts/io-guardrails-smoke.mjs +0 -205
  516. package/scripts/io-mini-bench-baseline.json +0 -11
  517. package/scripts/io-mini-bench.mjs +0 -216
  518. package/scripts/io-route-harness.mjs +0 -933
  519. package/scripts/io-telemetry-report.mjs +0 -691
  520. package/scripts/mutation-bench.mjs +0 -564
  521. package/scripts/mutation-io-smoke.mjs +0 -1097
  522. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  523. package/scripts/native-patch-smoke.mjs +0 -304
  524. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  525. package/scripts/patch-interior-context-smoke.mjs +0 -49
  526. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  527. package/scripts/perf-hook-smoke.mjs +0 -71
  528. package/scripts/permission-eval-smoke.mjs +0 -443
  529. package/scripts/prep-patch.mjs +0 -53
  530. package/scripts/prep-shim.mjs +0 -96
  531. package/scripts/provider-cache-smoke.mjs +0 -687
  532. package/scripts/report-runtime-health.mjs +0 -132
  533. package/scripts/resolve-bun.mjs +0 -60
  534. package/scripts/run-mcp.mjs +0 -1448
  535. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  536. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  537. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  538. package/scripts/smoke-runtime-negative.ps1 +0 -100
  539. package/scripts/smoke-runtime-negative.sh +0 -95
  540. package/scripts/stall-policy-smoke.mjs +0 -50
  541. package/scripts/start-memory-worker.mjs +0 -23
  542. package/scripts/statusline-launcher-smoke.mjs +0 -82
  543. package/scripts/stress-atomic-write.mjs +0 -1028
  544. package/scripts/test-fault-inject.mjs +0 -164
  545. package/scripts/test-large-file.mjs +0 -174
  546. package/scripts/tool-edge-smoke.mjs +0 -209
  547. package/scripts/uninstall.mjs +0 -201
  548. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  549. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  550. package/server-main.mjs +0 -3109
  551. package/server.mjs +0 -468
  552. package/setup/config-merge.mjs +0 -246
  553. package/setup/install.mjs +0 -574
  554. package/setup/launch-core.mjs +0 -617
  555. package/setup/launch.mjs +0 -101
  556. package/setup/locate-claude.mjs +0 -56
  557. package/setup/mixdog-cli.mjs +0 -122
  558. package/setup/setup-server.mjs +0 -3305
  559. package/setup/setup.html +0 -3740
  560. package/setup/tui.mjs +0 -325
  561. package/skills/retro-skill-proposer/SKILL.md +0 -92
  562. package/skills/schedule-add/SKILL.md +0 -77
  563. package/skills/setup/SKILL.md +0 -356
  564. package/skills/webhook-add/SKILL.md +0 -81
  565. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  566. package/src/agent/index.mjs +0 -2138
  567. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  568. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  569. package/src/agent/orchestrator/bridge-trace.mjs +0 -583
  570. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  571. package/src/agent/orchestrator/config.mjs +0 -405
  572. package/src/agent/orchestrator/context/collect.mjs +0 -651
  573. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  574. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  575. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  576. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  577. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  578. package/src/agent/orchestrator/jobs.mjs +0 -116
  579. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  580. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1881
  581. package/src/agent/orchestrator/providers/anthropic.mjs +0 -594
  582. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  583. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  584. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  585. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  586. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  587. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  588. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  589. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  590. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  591. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  592. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  593. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  594. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  595. package/src/agent/orchestrator/session/loop.mjs +0 -1478
  596. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  597. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  598. package/src/agent/orchestrator/session/store.mjs +0 -632
  599. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  600. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  601. package/src/agent/orchestrator/session/trim.mjs +0 -491
  602. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  603. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  604. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  605. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  606. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  607. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  608. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  609. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  610. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  611. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  612. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -455
  613. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  614. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  615. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  616. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  617. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  618. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  619. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  620. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  621. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  622. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  623. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  624. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  625. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  626. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  627. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  628. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  629. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  630. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  631. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  632. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  633. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  634. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  635. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  636. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  637. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  638. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  639. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  640. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  641. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  642. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  643. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  644. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  645. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  646. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  647. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  648. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  649. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  650. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  651. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  652. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  653. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  654. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  655. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  656. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  657. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  658. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  659. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  660. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  661. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  662. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  663. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  664. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  665. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  666. package/src/agent/tool-defs.mjs +0 -103
  667. package/src/channels/backends/discord.mjs +0 -784
  668. package/src/channels/data/voice-runtime-manifest.json +0 -138
  669. package/src/channels/index.mjs +0 -3268
  670. package/src/channels/lib/config.mjs +0 -292
  671. package/src/channels/lib/drop-trace.mjs +0 -71
  672. package/src/channels/lib/event-pipeline.mjs +0 -81
  673. package/src/channels/lib/holidays.mjs +0 -138
  674. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  675. package/src/channels/lib/output-forwarder.mjs +0 -765
  676. package/src/channels/lib/runtime-paths.mjs +0 -517
  677. package/src/channels/lib/scheduler.mjs +0 -723
  678. package/src/channels/lib/session-discovery.mjs +0 -103
  679. package/src/channels/lib/state-file.mjs +0 -68
  680. package/src/channels/lib/status-snapshot.mjs +0 -219
  681. package/src/channels/lib/tool-format.mjs +0 -140
  682. package/src/channels/lib/transcript-discovery.mjs +0 -195
  683. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  684. package/src/channels/lib/webhook.mjs +0 -1318
  685. package/src/channels/tool-defs.mjs +0 -170
  686. package/src/daemon/host.mjs +0 -118
  687. package/src/daemon/mcp-transport.mjs +0 -47
  688. package/src/daemon/session.mjs +0 -100
  689. package/src/daemon/thin-client.mjs +0 -71
  690. package/src/daemon/transport.mjs +0 -163
  691. package/src/memory/data/runtime-manifest.json +0 -40
  692. package/src/memory/index.mjs +0 -3332
  693. package/src/memory/lib/core-memory-store.mjs +0 -330
  694. package/src/memory/lib/embedding-provider.mjs +0 -269
  695. package/src/memory/lib/embedding-worker.mjs +0 -323
  696. package/src/memory/lib/memory-cycle1.mjs +0 -645
  697. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  698. package/src/memory/lib/memory-cycle3.mjs +0 -540
  699. package/src/memory/lib/memory-embed.mjs +0 -299
  700. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  701. package/src/memory/lib/memory-recall-store.mjs +0 -638
  702. package/src/memory/lib/memory.mjs +0 -412
  703. package/src/memory/lib/pg/adapter.mjs +0 -308
  704. package/src/memory/lib/pg/process.mjs +0 -360
  705. package/src/memory/lib/pg/supervisor.mjs +0 -396
  706. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  707. package/src/memory/lib/trace-store.mjs +0 -728
  708. package/src/memory/tool-defs.mjs +0 -79
  709. package/src/search/index.mjs +0 -1173
  710. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  711. package/src/search/lib/backends/exa.mjs +0 -50
  712. package/src/search/lib/backends/firecrawl.mjs +0 -61
  713. package/src/search/lib/backends/gemini-api.mjs +0 -83
  714. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  715. package/src/search/lib/backends/index.mjs +0 -150
  716. package/src/search/lib/backends/openai-api.mjs +0 -144
  717. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  718. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  719. package/src/search/lib/backends/tavily.mjs +0 -55
  720. package/src/search/lib/backends/xai-api.mjs +0 -113
  721. package/src/search/lib/config.mjs +0 -192
  722. package/src/search/lib/provider-usage.mjs +0 -67
  723. package/src/search/lib/providers.mjs +0 -47
  724. package/src/search/lib/search-intent.mjs +0 -109
  725. package/src/search/lib/setup-handler.mjs +0 -261
  726. package/src/search/lib/web-tools.mjs +0 -1219
  727. package/src/search/tool-defs.mjs +0 -83
  728. package/src/setup/defender-exclusion.mjs +0 -183
  729. package/src/shared/atomic-file.mjs +0 -436
  730. package/src/shared/config.mjs +0 -372
  731. package/src/shared/daemon-recycle.mjs +0 -108
  732. package/src/shared/disable-claude-builtins.mjs +0 -91
  733. package/src/shared/err-text.mjs +0 -12
  734. package/src/shared/llm/cost.mjs +0 -66
  735. package/src/shared/llm/http-agent.mjs +0 -123
  736. package/src/shared/open-url.mjs +0 -62
  737. package/src/shared/plugin-paths.mjs +0 -58
  738. package/src/shared/schedules-store.mjs +0 -70
  739. package/src/shared/seed.mjs +0 -136
  740. package/src/shared/user-cwd.mjs +0 -225
  741. package/src/shared/user-data-guard.mjs +0 -244
  742. package/src/status/aggregator.mjs +0 -584
  743. package/src/status/server.mjs +0 -413
  744. package/tools.json +0 -1653
  745. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  746. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  747. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  748. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  749. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  750. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  751. /package/{lib → src/lib}/text-utils.cjs +0 -0
  752. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  753. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  754. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  755. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  756. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  757. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  758. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  759. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  805. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  806. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  807. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  808. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  809. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  810. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  811. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  815. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  816. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  817. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  818. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  819. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  820. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  821. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  829. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  830. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  831. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  832. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  833. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  834. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  835. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  836. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -1,3305 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { exec, execSync, spawn, spawnSync } from 'child_process';
3
- import { existsSync, readFileSync, writeFileSync, createWriteStream, mkdirSync, renameSync, unlinkSync, readdirSync, rmSync, statSync, openSync, readSync, closeSync } from 'fs';
4
- import { join, dirname, basename } from 'path';
5
- import { homedir, arch, platform } from 'os';
6
- import { fileURLToPath } from 'url';
7
- import http from 'http';
8
- import https from 'https';
9
- import { DEFAULT_MAINTENANCE, MAINTENANCE_SLOTS, DEFAULT_PRESETS, getPluginData } from '../src/agent/orchestrator/config.mjs';
10
- import { getOpenAIOAuthModelCatalogError, hasOpenAIOAuthCredentials, loginOAuth as loginOpenAIOAuth } from '../src/agent/orchestrator/providers/openai-oauth.mjs';
11
- import { hasAnthropicOAuthCredentials, loginOAuth as loginAnthropicOAuth } from '../src/agent/orchestrator/providers/anthropic-oauth.mjs';
12
- import { hasGrokOAuthCredentials, loginOAuth as loginGrokOAuth } from '../src/agent/orchestrator/providers/grok-oauth.mjs';
13
- import { resolvePluginData } from '../src/shared/plugin-paths.mjs';
14
- import { listSchedules } from '../src/shared/schedules-store.mjs';
15
- import { ensureDataSeeds } from '../src/shared/seed.mjs';
16
- import { backupUserData, markUserDataInitialized, shouldSeedMissingUserData } from '../src/shared/user-data-guard.mjs';
17
- import { tmpdir } from 'os';
18
- import { readSection, writeSection, updateSection, saveSecret, deleteSecret, hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey, getAgentApiKey, getDiscordToken, getWebhookAuthtoken, diagnoseDiscordTokenValue, AGENT_PROVIDER_ENV, AGENT_PROVIDER_ENV_ALIASES } from '../src/shared/config.mjs';
19
- import { applyDefaults as applyChannelsDefaults } from '../src/channels/lib/config.mjs';
20
- import { validateCronExpression } from '../src/channels/lib/scheduler.mjs';
21
- import { mergeAgentConfig, mergeMemoryConfig, mergeSearchConfig, mergeConfig, mergeEndpointConfig, mergeWebhookEndpointConfig } from './config-merge.mjs';
22
- import { isWSL } from '../src/shared/wsl.mjs';
23
-
24
- // C2 — Origin/Referer guard for mutating routes.
25
- // Returns true when the request is safe to handle (same-origin loopback UI,
26
- // or direct curl/native-client that sends no browser Origin or Referer).
27
- // Empty Origin alone is no longer trusted — browsers always send Origin on
28
- // cross-origin requests; same-origin browser requests may omit Origin but
29
- // will carry a matching Referer, handled by the loopback regex below.
30
- function isAllowedOrigin(req) {
31
- const origin = req.headers.origin || '';
32
- const referer = req.headers.referer || '';
33
- // No Origin AND no Referer → direct curl / native client → allow.
34
- if (!origin && !referer) return true;
35
- // Origin present → must match our loopback UI port.
36
- if (origin) return /^http:\/\/(localhost|127\.0\.0\.1):3458(\/|$)/.test(origin);
37
- // Origin absent but Referer present → allow only if Referer is loopback UI.
38
- return /^http:\/\/(localhost|127\.0\.0\.1):3458(\/|$)/.test(referer);
39
- }
40
-
41
- // sanitizeName — reject path-traversal in user-supplied names used as
42
- // directory/filename components (schedules, webhooks, presets, etc.).
43
- function sanitizeName(n) {
44
- if (!n || typeof n !== 'string') return null;
45
- if (n !== basename(n)) return null;
46
- if (n.includes('..') || n.startsWith('.')) return null;
47
- return n;
48
- }
49
-
50
- const __dirname = dirname(fileURLToPath(import.meta.url));
51
- const isWin = process.platform === 'win32';
52
-
53
- // MIXDOG_DEBUG_SETUP=1 gates verbose tracing for the chrome launcher and the
54
- // /open / /req HTTP path. The previous unconditional console.error calls
55
- // produced a wall of [open-debug] / [req-trace] noise on every config-UI
56
- // session even when nothing was wrong; gating preserves the diagnostic
57
- // power without filling supervisor.log on the happy path.
58
- function debugSetup(msg) {
59
- if (!process.env.MIXDOG_DEBUG_SETUP) return;
60
- try { console.error(`${new Date().toISOString()} ${msg}`); } catch { /* best-effort */ }
61
- }
62
- const home = homedir();
63
-
64
- // -- Channels paths --
65
- const DATA_DIR = resolvePluginData();
66
- const MIXDOG_CONFIG_PATH = join(DATA_DIR, 'mixdog-config.json');
67
- const STATUS_SNAPSHOT_PATH = join(DATA_DIR, 'channels', 'status-snapshot.json');
68
-
69
- // -- Workflow paths --
70
- const USER_WORKFLOW_PATH = join(DATA_DIR, 'user-workflow.json');
71
- const USER_WORKFLOW_MD_PATH = join(DATA_DIR, 'user-workflow.md');
72
-
73
- // Plugin-shipped defaults loaded from <plugin-root>/defaults/. Keeps the
74
- // canonical user-facing seed templates editable as plain files instead of
75
- // inline string constants. See defaults/user-workflow.{json,md}.
76
- const DEFAULTS_DIR = join(__dirname, '..', 'defaults');
77
- const DEFAULT_USER_WORKFLOW = JSON.parse(readFileSync(join(DEFAULTS_DIR, 'user-workflow.json'), 'utf8'));
78
- const DEFAULT_USER_WORKFLOW_MD = readFileSync(join(DEFAULTS_DIR, 'user-workflow.md'), 'utf8');
79
-
80
- const PORT = 3458;
81
- const APP_WIDTH = 950;
82
- const APP_HEIGHT = 900;
83
- const HTML_PATH = join(__dirname, 'setup.html');
84
-
85
- // Drop runtime-provider model caches on boot and after provider saves so the
86
- // Config UI re-fetches fresh catalogs. Caches can get stuck on partial/stale
87
- // responses (e.g. Codex /backend-api/codex/models returning just one model).
88
- function dropRuntimeModelCaches() {
89
- for (const name of ['openai-oauth-models.json', 'anthropic-oauth-models.json', 'grok-oauth-models.json']) {
90
- try { rmSync(join(getPluginData(), name), { force: true }); } catch {}
91
- }
92
- }
93
- dropRuntimeModelCaches();
94
-
95
- // First-install seeding SSOT. ensureDataSeeds seeds mixdog-config.json AND
96
- // user-workflow.json / user-workflow.md together as one fresh-install set, so
97
- // the role→preset mapping always lands regardless of which boot path (this
98
- // server, MCP boot, or agent index) runs first. It must NOT be split: seeding
99
- // user-workflow here separately would set the init marker early and make
100
- // ensureDataSeeds refuse the remaining first-time seeds. Idempotent — existing
101
- // files are left untouched.
102
- ensureDataSeeds(DATA_DIR);
103
-
104
- // -- Helpers --
105
-
106
- function readJsonFile(path) {
107
- try { return JSON.parse(readFileSync(path, 'utf8')); }
108
- catch { return {}; }
109
- }
110
-
111
- function writeJsonFile(path, data) {
112
- mkdirSync(dirname(path), { recursive: true });
113
- if (path === MIXDOG_CONFIG_PATH || path === USER_WORKFLOW_PATH) {
114
- try { backupUserData(DATA_DIR, path === USER_WORKFLOW_PATH ? 'pre-workflow-write' : 'pre-config-write'); } catch {}
115
- }
116
- const tmp = path + '.tmp';
117
- writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
118
- renameSync(tmp, path);
119
- if (path === MIXDOG_CONFIG_PATH || path === USER_WORKFLOW_PATH) {
120
- try { markUserDataInitialized(DATA_DIR); } catch {}
121
- try { backupUserData(DATA_DIR, path === USER_WORKFLOW_PATH ? 'post-workflow-write' : 'post-config-write'); } catch {}
122
- }
123
- }
124
-
125
- function readConfig() { return readSection('channels'); }
126
- function writeConfig(data) { writeSection('channels', data); }
127
-
128
- function readAgentConfig() { return readSection('agent'); }
129
- function writeAgentConfig(data) { writeSection('agent', data); }
130
-
131
- function readMemoryConfig() { return readSection('memory'); }
132
- function writeMemoryConfig(data) { writeSection('memory', data); }
133
-
134
- function readSearchConfig() { return readSection('search'); }
135
- function writeSearchConfig(data) { writeSection('search', data); }
136
-
137
- // Provider availability — credential resolution follows the invariant declared
138
- // in Core Memory: `search.credentials → agent.providers.<x>-oauth →
139
- // agent.providers.<x>.apiKey`, first match. Returns the subset of the 9
140
- // supported providers whose credentials are already registered, so the Setup
141
- // UI dropdown can show only selectable options.
142
- const SUPPORTED_PROVIDERS = [
143
- 'anthropic-oauth', 'openai-oauth', 'openai-api', 'gemini-api', 'xai-api', 'grok-oauth',
144
- 'tavily', 'firecrawl', 'exa',
145
- ];
146
- // AGENT_PROVIDER_ENV / AGENT_PROVIDER_ENV_ALIASES are the SSOT (src/shared/config.mjs);
147
- // derive the id list so a provider added there flows through every detection path
148
- // (keyStored, envKeys, secret presence) here automatically.
149
- const AGENT_KEY_PROVIDER_IDS = Object.keys(AGENT_PROVIDER_ENV);
150
- const SEARCH_KEY_PROVIDER_IDS = ['firecrawl', 'tavily', 'exa'];
151
-
152
- function envSecretPresent(account) {
153
- const key = 'MIXDOG_' + String(account || '').replace(/[.\s]+/g, '_').toUpperCase();
154
- if (process.env[key]) return true;
155
- const agentMatch = String(account || '').match(/^agent\.([^.]+)\.apiKey$/);
156
- if (agentMatch) {
157
- const std = AGENT_PROVIDER_ENV[agentMatch[1]];
158
- if (std && process.env[std]) return true;
159
- }
160
- return false;
161
- }
162
-
163
- function getSecretByAccount(account) {
164
- if (account === SECRET_ACCOUNTS.discordToken) return getDiscordToken();
165
- if (account === SECRET_ACCOUNTS.webhookAuth) return getWebhookAuthtoken();
166
- const searchMatch = String(account || '').match(/^search\.([^.]+)\.apiKey$/);
167
- if (searchMatch) return getSearchApiKey(searchMatch[1]);
168
- const agentMatch = String(account || '').match(/^agent\.([^.]+)\.apiKey$/);
169
- if (agentMatch) return getAgentApiKey(agentMatch[1]);
170
- return null;
171
- }
172
-
173
- function keychainSecretPresent(account) {
174
- if (hasStoredSecret(account)) return true;
175
- // Cross-check through the canonical getters. On Windows the setup server can
176
- // occasionally see a false negative from the low-level presence check while
177
- // the normal read path succeeds; the UI cares whether the credential is
178
- // actually available to runtime.
179
- if (envSecretPresent(account)) return false;
180
- return !!getSecretByAccount(account);
181
- }
182
-
183
- function secretPresence(account) {
184
- return {
185
- keychain: keychainSecretPresent(account),
186
- env: envSecretPresent(account),
187
- };
188
- }
189
-
190
- function fullSecretStatus(channelsConfig = null) {
191
- const agent = {};
192
- for (const id of AGENT_KEY_PROVIDER_IDS) agent[id] = secretPresence(SECRET_ACCOUNTS.agentApiKey(id));
193
- const search = {};
194
- for (const id of SEARCH_KEY_PROVIDER_IDS) search[id] = secretPresence(SECRET_ACCOUNTS.searchApiKey(id));
195
- const discordToken = secretPresence(SECRET_ACCOUNTS.discordToken);
196
- const discordProblem = diagnoseDiscordTokenValue(getDiscordToken(), channelsConfig || {});
197
- if (discordProblem) {
198
- discordToken.invalid = true;
199
- discordToken.problem = discordProblem;
200
- }
201
- return {
202
- channels: {
203
- discordToken,
204
- webhookAuth: secretPresence(SECRET_ACCOUNTS.webhookAuth),
205
- },
206
- agent,
207
- memory: {
208
- // Memory provider keys reuse the same provider keychain accounts as Agent.
209
- agent,
210
- },
211
- search,
212
- };
213
- }
214
-
215
- function saveSecretChecked(account, rawValue, label) {
216
- const value = typeof rawValue === 'string' ? rawValue.trim() : '';
217
- if (!value) {
218
- return { attempted: false, stored: keychainSecretPresent(account), env: envSecretPresent(account), inputLength: 0 };
219
- }
220
- saveSecret(account, value);
221
- const stored = keychainSecretPresent(account);
222
- if (!stored && !envSecretPresent(account)) {
223
- throw new Error(`${label}: keychain write completed but secret is not readable`);
224
- }
225
- return { attempted: true, stored, env: envSecretPresent(account), inputLength: value.length };
226
- }
227
-
228
- function deleteSecretIfCurrentValue(account, value) {
229
- if (!value) return false;
230
- try {
231
- if (getSecretByAccount(account) !== value) return false;
232
- deleteSecret(account);
233
- return true;
234
- } catch {
235
- return false;
236
- }
237
- }
238
-
239
- function saveDiscordTokenChecked(rawValue, config) {
240
- const value = typeof rawValue === 'string' ? rawValue.trim() : '';
241
- if (!value) {
242
- return { attempted: false, stored: keychainSecretPresent(SECRET_ACCOUNTS.discordToken), env: envSecretPresent(SECRET_ACCOUNTS.discordToken), inputLength: 0 };
243
- }
244
- const problem = diagnoseDiscordTokenValue(value, config || {});
245
- if (problem) {
246
- const removed = deleteSecretIfCurrentValue(SECRET_ACCOUNTS.discordToken, value);
247
- throw new Error(`channels.discord.token: ${problem}${removed ? ' Removed the invalid saved value.' : ''}`);
248
- }
249
- return saveSecretChecked(SECRET_ACCOUNTS.discordToken, value, 'channels.discord.token');
250
- }
251
-
252
- function pruneInvalidDiscordToken(config) {
253
- const value = getDiscordToken();
254
- const problem = diagnoseDiscordTokenValue(value, config || {});
255
- if (!problem) return null;
256
- const removed = deleteSecretIfCurrentValue(SECRET_ACCOUNTS.discordToken, value);
257
- return { invalid: true, removed, problem };
258
- }
259
-
260
- function computeAvailableProviders() {
261
- const isAvailable = (id) => {
262
- if (id === 'anthropic-oauth') return hasAnthropicOAuthCredentials();
263
- if (id === 'openai-oauth') return hasOpenAIOAuthCredentials();
264
- if (id === 'grok-oauth') return hasGrokOAuthCredentials();
265
- if (id === 'openai-api') return !!getAgentApiKey('openai');
266
- if (id === 'gemini-api') return !!getAgentApiKey('gemini');
267
- if (id === 'xai-api') return !!getAgentApiKey('xai');
268
- if (id === 'firecrawl' || id === 'tavily' || id === 'exa') return !!getSearchApiKey(id);
269
- return false;
270
- };
271
- return SUPPORTED_PROVIDERS.filter(isAvailable);
272
- }
273
-
274
- function readUserWorkflow() {
275
- if (!existsSync(USER_WORKFLOW_PATH)) return DEFAULT_USER_WORKFLOW;
276
- try { return JSON.parse(readFileSync(USER_WORKFLOW_PATH, 'utf8')); }
277
- catch { return DEFAULT_USER_WORKFLOW; }
278
- }
279
- // Phase C Ship 3 — the `worker` role is reserved and non-deletable. Smart
280
- // Bridge's router dispatches any request with `role: "worker"` to the
281
- // `worker-full` profile; if the role goes missing the router has nowhere to
282
- // send Worker calls. Every persist path funnels through here, so reinstating
283
- // the role on save keeps the contract intact regardless of how the caller
284
- // mutated the roster (UI drag-delete, raw MD edit, direct JSON PUT).
285
- function writeUserWorkflow(data) {
286
- const roles = Array.isArray(data?.roles) ? data.roles.slice() : [];
287
- if (!roles.some(r => r?.name === 'worker')) {
288
- const existing = readUserWorkflow();
289
- const preservedWorker = existing?.roles?.find(r => r?.name === 'worker');
290
- const seedWorker = DEFAULT_USER_WORKFLOW.roles.find(r => r?.name === 'worker');
291
- roles.unshift(preservedWorker || seedWorker);
292
- }
293
- const sanitizedRoles = roles.map(r => {
294
- if (!r || typeof r !== "object") return r;
295
- const name = sanitizeName(r.name);
296
- if (name == null) throw new Error('invalid role name: ' + r.name);
297
- return { ...r, name };
298
- });
299
- // Intentional full replace: POST /workflow/save sends the complete workflow
300
- // document; no unmanaged top-level sidecar fields on user-workflow.json.
301
- writeJsonFile(USER_WORKFLOW_PATH, { ...data, roles: sanitizedRoles });
302
- }
303
-
304
- function readUserWorkflowMd() {
305
- if (!existsSync(USER_WORKFLOW_MD_PATH)) return DEFAULT_USER_WORKFLOW_MD;
306
- try { return readFileSync(USER_WORKFLOW_MD_PATH, 'utf8'); }
307
- catch { return DEFAULT_USER_WORKFLOW_MD; }
308
- }
309
- function writeUserWorkflowMd(content) {
310
- // Intentional full replace: the UI owns the entire user-workflow.md body.
311
- mkdirSync(dirname(USER_WORKFLOW_MD_PATH), { recursive: true });
312
- try { backupUserData(DATA_DIR, 'pre-workflow-md-write'); } catch {}
313
- const tmp = USER_WORKFLOW_MD_PATH + '.tmp';
314
- writeFileSync(tmp, content, 'utf8');
315
- renameSync(tmp, USER_WORKFLOW_MD_PATH);
316
- try { markUserDataInitialized(DATA_DIR); } catch {}
317
- try { backupUserData(DATA_DIR, 'post-workflow-md-write'); } catch {}
318
- }
319
-
320
- // -- HTTPS helpers --
321
-
322
- function httpGetJson(url, headers) {
323
- return new Promise((resolve, reject) => {
324
- const u = new URL(url);
325
- const lib = u.protocol === 'https:' ? https : http;
326
- const req = lib.request({
327
- hostname: u.hostname, port: u.port, path: u.pathname + u.search,
328
- method: 'GET', headers, timeout: 10000,
329
- }, res => {
330
- let body = '';
331
- res.on('data', c => { body += c; });
332
- res.on('end', () => { res.statusCode < 400 ? resolve(JSON.parse(body)) : reject(); });
333
- });
334
- req.on('error', reject);
335
- req.on('timeout', () => { req.destroy(); reject(); });
336
- req.end();
337
- });
338
- }
339
-
340
- function httpPostJson(url, data, headers) {
341
- return new Promise((resolve, reject) => {
342
- const u = new URL(url);
343
- const body = JSON.stringify(data);
344
- const lib = u.protocol === 'https:' ? https : http;
345
- const req = lib.request({
346
- hostname: u.hostname, port: u.port, path: u.pathname,
347
- method: 'POST',
348
- headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
349
- timeout: 10000,
350
- }, res => {
351
- let buf = '';
352
- res.on('data', c => { buf += c; });
353
- res.on('end', () => { res.statusCode < 400 ? resolve(JSON.parse(buf)) : reject(); });
354
- });
355
- req.on('error', reject);
356
- req.on('timeout', () => { req.destroy(); reject(); });
357
- req.write(body);
358
- req.end();
359
- });
360
- }
361
-
362
- function pingLocalHttp(url, timeoutMs = 1500) {
363
- return new Promise((resolve) => {
364
- try {
365
- const u = new URL(url);
366
- const req = http.request({
367
- hostname: u.hostname, port: u.port,
368
- path: u.pathname + u.search,
369
- method: 'GET', timeout: timeoutMs,
370
- }, res => { res.resume(); resolve(res.statusCode > 0 && res.statusCode < 500); });
371
- req.on('error', () => resolve(false));
372
- req.on('timeout', () => { req.destroy(); resolve(false); });
373
- req.end();
374
- } catch { resolve(false); }
375
- });
376
- }
377
-
378
- // -- Agent key validation --
379
-
380
- async function validateAgentKey(provider, key) {
381
- if (!key) return 'empty';
382
- try {
383
- switch (provider) {
384
- case 'openai':
385
- await httpGetJson('https://api.openai.com/v1/models', { 'Authorization': `Bearer ${key}` });
386
- return 'valid';
387
- case 'anthropic':
388
- await httpPostJson('https://api.anthropic.com/v1/messages',
389
- { model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] },
390
- { 'x-api-key': key, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' });
391
- return 'valid';
392
- case 'gemini':
393
- await httpGetJson(`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`, {});
394
- return 'valid';
395
- case 'groq':
396
- await httpGetJson('https://api.groq.com/openai/v1/models', { 'Authorization': `Bearer ${key}` });
397
- return 'valid';
398
- case 'openrouter':
399
- await httpGetJson('https://openrouter.ai/api/v1/models', { 'Authorization': `Bearer ${key}` });
400
- return 'valid';
401
- case 'xai':
402
- await httpPostJson('https://api.x.ai/v1/chat/completions',
403
- { model: 'grok-3-mini-fast', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
404
- { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' });
405
- return 'valid';
406
- default: return 'valid';
407
- }
408
- } catch { return 'invalid'; }
409
- }
410
-
411
- // -- Search key validation --
412
-
413
- async function validateSearchKey(provider, key) {
414
- if (!key) return 'empty';
415
- try {
416
- switch (provider) {
417
- case 'serper':
418
- await httpPostJson('https://google.serper.dev/search', { q: 'test' },
419
- { 'X-API-KEY': key, 'Content-Type': 'application/json' });
420
- return 'valid';
421
- case 'brave':
422
- await httpGetJson('https://api.search.brave.com/res/v1/web/search?q=test&count=1',
423
- { 'X-Subscription-Token': key });
424
- return 'valid';
425
- case 'xai':
426
- await httpPostJson('https://api.x.ai/v1/chat/completions',
427
- { model: 'grok-3-mini-fast', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
428
- { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' });
429
- return 'valid';
430
- case 'perplexity':
431
- await httpPostJson('https://api.perplexity.ai/chat/completions',
432
- { model: 'sonar', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
433
- { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' });
434
- return 'valid';
435
- case 'firecrawl':
436
- await httpGetJson('https://api.firecrawl.dev/v1/crawl', { 'Authorization': `Bearer ${key}` });
437
- return 'valid';
438
- case 'tavily':
439
- await httpPostJson('https://api.tavily.com/search',
440
- { api_key: key, query: 'test', max_results: 1 },
441
- { 'Content-Type': 'application/json' });
442
- return 'valid';
443
- default: return 'valid';
444
- }
445
- } catch { return 'invalid'; }
446
- }
447
-
448
- // -- Auth detection (shared by agent & memory) --
449
-
450
- async function detectAuth(config = {}) {
451
- const result = {};
452
- // Single source of truth: same predicate the runtime uses to decide
453
- // whether to register the provider. Avoids the UI showing "Set" while
454
- // runtime disables the provider (or vice versa) when only one of the
455
- // candidate paths is populated.
456
- result.codexOAuth = hasOpenAIOAuthCredentials();
457
- result.anthropicOAuth = hasAnthropicOAuthCredentials();
458
- result.grokOAuth = hasGrokOAuthCredentials();
459
- const configDir = isWin
460
- ? (process.env.LOCALAPPDATA || join(home, 'AppData', 'Local'))
461
- : join(home, '.config');
462
- result.copilot = existsSync(join(configDir, 'github-copilot', 'hosts.json'))
463
- || existsSync(join(configDir, 'github-copilot', 'apps.json'));
464
- result.envKeys = {};
465
- for (const [id, env] of Object.entries(AGENT_PROVIDER_ENV)) result.envKeys[id] = !!process.env[env];
466
- // Honor the shared last-resort env aliases (e.g. GROK_API_KEY for xai) so a
467
- // GROK_API_KEY-only env still shows xAI as available, matching getAgentApiKey.
468
- // keyStored semantics (keychain-only) stay unchanged.
469
- for (const [id, aliases] of Object.entries(AGENT_PROVIDER_ENV_ALIASES)) {
470
- if (!result.envKeys[id]) result.envKeys[id] = aliases.some((a) => !!process.env[a]);
471
- }
472
- // Keychain-stored provider keys. Only this boolean is sent to the browser —
473
- // the secret value never leaves the server, so the UI can show "Set" without
474
- // exposing the key.
475
- result.keyStored = {};
476
- for (const id of AGENT_KEY_PROVIDER_IDS) {
477
- result.keyStored[id] = hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(id));
478
- }
479
- const ollamaUrl = config?.providers?.ollama?.baseURL || 'http://localhost:11434/v1';
480
- const lmstudioUrl = config?.providers?.lmstudio?.baseURL || 'http://localhost:1234/v1';
481
- const [ollamaUp, lmstudioUp] = await Promise.all([
482
- pingLocalHttp(ollamaUrl + '/models'),
483
- pingLocalHttp(lmstudioUrl + '/models'),
484
- ]);
485
- result.ollama = ollamaUp;
486
- result.lmstudio = lmstudioUp;
487
- return result;
488
- }
489
-
490
- // -- Provider model listing --
491
-
492
- // Static model fallback removed: model lists must come from the live
493
- // provider (registry.mjs `provider.listModels()` or the provider's REST
494
- // endpoint). Hard-coded lists drift behind real releases (e.g. opus-4-6
495
- // vs opus-4-7) and silently mis-resolve saved presets. Empty result on
496
- // missing credentials is the correct invariant — only show models the
497
- // user can actually use.
498
-
499
- // Try the live provider registry first (dynamic catalog via /v1/models,
500
- // Codex /backend-api/codex/models, Gemini /v1beta/models). Returns null on
501
- // any failure so the caller uses the direct HTTP endpoint handlers below.
502
- // Preserves full metadata (tier, family, latest,
503
- // contextWindow, reasoningLevels, pricing) so the UI can build tier-grouped
504
- // dropdowns and adapt effort options per model.
505
- //
506
- // registry.mjs populates its provider Map only after initProviders(cfg) is
507
- // called, so setup-server (which never runs the agent's normal boot path)
508
- // must force-init before querying — otherwise getProvider() returns
509
- // undefined and listModels() never runs.
510
- // Provider IDs registry.mjs actually knows. Listing anything else makes
511
- // initProviders throw `unknown enabled provider: …`, which the outer catch
512
- // swallows — silently nuking every model lookup for every provider. Keep
513
- // in sync with src/agent/orchestrator/providers/registry.mjs.
514
- const _RUNTIME_PROVIDER_NAMES = [
515
- 'anthropic', 'anthropic-oauth', 'openai', 'openai-oauth',
516
- 'gemini', 'deepseek', 'xai', 'opencode-go', 'grok-oauth',
517
- 'ollama', 'lmstudio',
518
- ];
519
-
520
- async function getRuntimeProviderModels(providerId, cfg, opts = {}) {
521
- try {
522
- const { initProviders, getProvider } = await import('../src/agent/orchestrator/providers/registry.mjs');
523
- const initCfg = {};
524
- for (const name of _RUNTIME_PROVIDER_NAMES) {
525
- initCfg[name] = { ...(cfg?.providers?.[name] || {}), enabled: true };
526
- }
527
- await initProviders(initCfg);
528
- const provider = getProvider(providerId);
529
- if (!provider) return null;
530
- let models = null;
531
- if (opts.forceRefresh && typeof provider._refreshModelCache === 'function') {
532
- models = await provider._refreshModelCache();
533
- }
534
- if (!Array.isArray(models) || models.length === 0) models = await provider.listModels();
535
- if (!Array.isArray(models) || models.length === 0) return null;
536
- return models
537
- .map(m => {
538
- if (typeof m === 'string') return { id: m };
539
- const id = m?.id || m?.name;
540
- if (!id) return null;
541
- return { ...m, id: String(id) };
542
- })
543
- .filter(Boolean);
544
- } catch { return null; }
545
- }
546
-
547
- function _idOnly(id) { return id ? { id: String(id) } : null; }
548
-
549
- // Per-provider id blocklist applied to dynamic and direct-HTTP catalogs.
550
- // Pro tier models are surfaced by /v1/models but not usable through the
551
- // standard chat/responses paths we support, so they get filtered out at
552
- // catalog level rather than per-UI.
553
- const _MODEL_ID_BLOCKLIST = {
554
- openai: [/^gpt-\d+(\.\d+)?-pro(-|$)/i, /^o\d+-pro(-|$)/i, /^sora-\d+-pro(-|$)/i],
555
- 'openai-oauth': [/^gpt-\d+(\.\d+)?-pro(-|$)/i],
556
- };
557
- function _applyModelBlocklist(providerId, models) {
558
- const rules = _MODEL_ID_BLOCKLIST[providerId];
559
- if (!rules || !Array.isArray(models)) return models;
560
- return models.filter(m => {
561
- const id = typeof m === 'string' ? m : m?.id;
562
- if (!id) return true;
563
- return !rules.some(re => re.test(id));
564
- });
565
- }
566
-
567
- function _configuredProviderModels(providerId, cfg) {
568
- const provider = normalizeAgentProviderId(providerId);
569
- const presets = Array.isArray(cfg?.presets) ? cfg.presets : [];
570
- const seen = new Set();
571
- const out = [];
572
- for (const p of presets) {
573
- const modelId = String(p?.model || '').trim();
574
- if (!modelId || seen.has(modelId)) continue;
575
- if (normalizeAgentProviderId(p?.provider) !== provider) continue;
576
- seen.add(modelId);
577
- out.push(_modelFromConfiguredId(modelId, provider));
578
- }
579
- return out;
580
- }
581
-
582
- function _modelFromConfiguredId(id, provider) {
583
- const family = _familyFromModelId(id);
584
- return {
585
- id,
586
- display: _displayFromModelId(id),
587
- provider,
588
- family,
589
- tier: /-\d{8}$/.test(id) ? 'dated' : /-\d+-\d+/.test(id) ? 'version' : undefined,
590
- latest: true,
591
- contextWindow: _contextWindowFromModelId(id),
592
- mode: 'chat',
593
- };
594
- }
595
-
596
- function _familyFromModelId(id) {
597
- const s = String(id || '').toLowerCase();
598
- const claude = s.match(/^claude-(opus|sonnet|haiku)/i);
599
- if (claude) return claude[1].toLowerCase();
600
- if (s.includes('nano')) return 'gpt-nano';
601
- if (s.includes('mini')) return 'gpt-mini';
602
- if (s.includes('codex')) return 'gpt-codex';
603
- if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
604
- if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
605
- if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
606
- if (s.startsWith('gpt-5')) return 'gpt-5';
607
- const gpt = s.match(/^(gpt-\d+(?:\.\d+)?)/i);
608
- if (gpt) return gpt[1].toLowerCase();
609
- return undefined;
610
- }
611
-
612
- function _displayFromModelId(id) {
613
- const m = String(id || '').match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)(?:-\d{8})?$/i);
614
- if (!m) return id;
615
- const family = m[1].charAt(0).toUpperCase() + m[1].slice(1).toLowerCase();
616
- return `Claude ${family} ${m[2]}.${m[3]}`;
617
- }
618
-
619
- function _contextWindowFromModelId(id) {
620
- const v = String(id || '').toLowerCase();
621
- if (/^claude-opus-4-(6|7|8)(?:$|-)/.test(v)) return 1000000;
622
- if (/^claude-sonnet-4-6(?:$|-)/.test(v)) return 1000000;
623
- if (/^claude-haiku-4-5/.test(v)) return 200000;
624
- if (/^gpt-5(?:\.|-|$)/.test(v)) return 1000000;
625
- return null;
626
- }
627
-
628
- function _hasAllConfiguredModels(providerId, cfg, models) {
629
- const ids = new Set((models || []).map(m => typeof m === 'string' ? m : m?.id).filter(Boolean).map(String));
630
- return _configuredProviderModels(providerId, cfg).every(m => ids.has(m.id));
631
- }
632
-
633
- function _mergeConfiguredModels(providerId, cfg, models) {
634
- const out = Array.isArray(models) ? models.slice() : [];
635
- const ids = new Set(out.map(m => typeof m === 'string' ? m : m?.id).filter(Boolean).map(String));
636
- for (const model of _configuredProviderModels(providerId, cfg).reverse()) {
637
- if (ids.has(model.id)) continue;
638
- out.unshift(model);
639
- ids.add(model.id);
640
- }
641
- return out;
642
- }
643
-
644
- async function listProviderModels(providerId, cfg) {
645
- // Search backends use suffix-tagged IDs (openai-api / gemini-api / xai-api)
646
- // that share their model catalog with the bare provider. Alias them ONLY
647
- // for the direct-HTTP fallback path. The runtime registry has its own
648
- // provider entry per auth shape (`anthropic-oauth` ≠ `anthropic`), so the
649
- // runtime call must keep the original ID.
650
- const HTTP_ALIAS = {
651
- 'openai-api': 'openai',
652
- 'gemini-api': 'gemini',
653
- 'xai-api': 'xai',
654
- };
655
- const httpLookupId = HTTP_ALIAS[providerId] || providerId;
656
- const pcfg = cfg?.providers?.[providerId] || cfg?.providers?.[httpLookupId] || {};
657
- // 1. Runtime provider (dynamic catalog, cached 24h). Try the original ID
658
- // first (oauth providers expose their own model catalog), then the bare
659
- // alias (gemini-api → gemini etc. where the registry only knows the
660
- // unsuffixed entry).
661
- let runtime = await getRuntimeProviderModels(providerId, cfg);
662
- if ((!runtime || runtime.length === 0) && httpLookupId !== providerId) {
663
- runtime = await getRuntimeProviderModels(httpLookupId, cfg);
664
- }
665
- if (runtime && runtime.length > 0 && !_hasAllConfiguredModels(providerId, cfg, runtime)) {
666
- const refreshed = await getRuntimeProviderModels(providerId, cfg, { forceRefresh: true });
667
- if (refreshed && refreshed.length > 0) runtime = refreshed;
668
- }
669
- if (runtime && runtime.length > 0) {
670
- return _applyModelBlocklist(providerId, _mergeConfiguredModels(providerId, cfg, runtime));
671
- }
672
- // 2. Direct HTTP model list for key-based providers.
673
- const KNOWN_ENDPOINTS = {
674
- openai: { url: 'https://api.openai.com/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
675
- xai: { url: 'https://api.x.ai/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
676
- deepseek: { url: 'https://api.deepseek.com/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
677
- };
678
- const ep = KNOWN_ENDPOINTS[httpLookupId];
679
- if (ep && pcfg.apiKey) {
680
- try {
681
- const json = await httpGetJson(ep.url, ep.auth(pcfg.apiKey));
682
- const data = Array.isArray(json?.data) ? json.data : [];
683
- // Preserve `created` (UNIX timestamp) so the UI can apply its 6-month
684
- // freshness cutoff. Without it the dropdown silently includes legacy
685
- // generations (gpt-3.5-turbo, gpt-4-0613, …) because the filter has
686
- // no date to compare against.
687
- const mapped = data
688
- .map(m => {
689
- const id = m?.id || m?.name;
690
- if (!id) return null;
691
- const entry = { id: String(id) };
692
- if (typeof m.created === 'number' && m.created > 0) entry.created = m.created;
693
- return entry;
694
- })
695
- .filter(Boolean)
696
- .sort((a, b) => (b.created || 0) - (a.created || 0) || a.id.localeCompare(b.id));
697
- return _applyModelBlocklist(providerId, mapped);
698
- } catch { /* runtime+HTTP both unavailable → fall through to [] */ }
699
- }
700
-
701
- const LOCAL_DEFAULTS = { ollama: 'http://localhost:11434/v1/models', lmstudio: 'http://localhost:1234/v1/models' };
702
- if (LOCAL_DEFAULTS[providerId]) {
703
- const baseURL = pcfg.baseURL || LOCAL_DEFAULTS[providerId].replace(/\/models$/, '');
704
- const url = `${baseURL.replace(/\/$/, '')}/models`;
705
- try {
706
- const json = await httpGetJson(url, {});
707
- const data = Array.isArray(json?.data) ? json.data : [];
708
- return data
709
- .map(m => _idOnly(m.id || m.name))
710
- .filter(Boolean)
711
- .sort((a, b) => a.id.localeCompare(b.id));
712
- } catch { return []; }
713
- }
714
- return [];
715
- }
716
-
717
- // -- Presets (shared logic for agent & memory) --
718
-
719
- const VALID_TOOLS = new Set(['full', 'readonly', 'mcp']);
720
- const VALID_EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
721
- const AGENT_PROVIDER_ALIASES = Object.freeze({
722
- 'openai-api': 'openai',
723
- 'gemini-api': 'gemini',
724
- 'xai-api': 'xai',
725
- });
726
- const FAST_CAPABLE_PRESET_PROVIDERS = new Set([
727
- 'anthropic',
728
- 'anthropic-oauth',
729
- 'openai',
730
- 'openai-oauth',
731
- ]);
732
- function normalizeAgentProviderId(provider) {
733
- const id = String(provider || '').trim();
734
- return AGENT_PROVIDER_ALIASES[id] || id;
735
- }
736
-
737
- function normalizePreset(input) {
738
- if (!input || typeof input !== 'object') throw new Error('preset must be an object');
739
- const id = String(input.id || '').trim();
740
- if (!id) throw new Error('preset.id is required');
741
- if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error('preset.id must be alphanumeric (._- allowed)');
742
- const model = String(input.model || '').trim();
743
- if (!model) throw new Error('preset.model is required');
744
- const provider = normalizeAgentProviderId(input.provider);
745
- if (!provider) throw new Error('preset.provider is required');
746
- const tools = String(input.tools || 'full');
747
- if (!VALID_TOOLS.has(tools)) throw new Error(`preset.tools must be one of ${[...VALID_TOOLS].join(', ')}`);
748
- const out = { id, type: 'bridge', model, provider, tools };
749
- if (typeof input.name === 'string' && input.name.trim()) out.name = input.name.trim();
750
- if (input.effort != null && input.effort !== '') {
751
- const effort = String(input.effort);
752
- if (!VALID_EFFORTS.has(effort)) throw new Error(`preset.effort must be one of ${[...VALID_EFFORTS].join(', ')}`);
753
- out.effort = effort;
754
- }
755
- if (input.fast === true && FAST_CAPABLE_PRESET_PROVIDERS.has(provider)) out.fast = true;
756
- return out;
757
- }
758
-
759
- function readAgentPresets() {
760
- const cfg = readAgentConfig();
761
- // Migrated/unified configs may have no agent.presets key (vs an explicit
762
- // empty array, which the user may have intentionally cleared). Fall back
763
- // to the seeded defaults only when the key is absent so the Custom
764
- // Workflow dropdowns render real options on first load. An explicit
765
- // empty array stays empty — matches the "No presets yet" UI path.
766
- const raw = Array.isArray(cfg.presets)
767
- ? cfg.presets
768
- : DEFAULT_PRESETS.map((p) => ({ ...p }));
769
- return raw.map((p) => {
770
- try { return normalizePreset(p); }
771
- catch { return p; }
772
- });
773
- }
774
-
775
- function writeAgentPresets(list) {
776
- const cfg = readAgentConfig();
777
- // Intentional presets-array replace inside read-modify-write agent section
778
- // (other agent keys preserved via writeAgentConfig).
779
- cfg.presets = Array.isArray(list) ? list.map((p) => normalizePreset(p)) : [];
780
- if ('defaultPreset' in cfg) delete cfg.defaultPreset;
781
- const validKeys = cfg.presets.map(p => p.id || p.name).filter(Boolean);
782
- if (!cfg.default || !validKeys.includes(cfg.default)) cfg.default = validKeys[0] || null;
783
- writeAgentConfig(cfg);
784
- }
785
-
786
- function readMemoryPresets() {
787
- const cfg = readMemoryConfig();
788
- return Array.isArray(cfg.presets) ? cfg.presets : [];
789
- }
790
-
791
- function writeMemoryPresets(list) {
792
- const cfg = readMemoryConfig();
793
- // Intentional presets-array replace inside read-modify-write memory section.
794
- cfg.presets = list;
795
- writeMemoryConfig(cfg);
796
- }
797
-
798
- function getMemoryServicePort() {
799
- const active = JSON.parse(readFileSync(join(tmpdir(), 'mixdog', 'active-instance.json'), 'utf8'));
800
- const port = Number(active && active.memory_port);
801
- if (!Number.isFinite(port) || port <= 0) {
802
- throw new Error('active-instance.json missing memory_port');
803
- }
804
- return port;
805
- }
806
-
807
- function memoryServiceCall(method, urlPath, body, timeoutMs = 600000) {
808
- return new Promise((resolve, reject) => {
809
- const port = getMemoryServicePort();
810
- const payload = body ? JSON.stringify(body) : null;
811
- const req = http.request({
812
- hostname: '127.0.0.1',
813
- port,
814
- path: urlPath,
815
- method,
816
- headers: payload
817
- ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
818
- : {},
819
- timeout: Math.max(1, timeoutMs),
820
- }, (res) => {
821
- const chunks = [];
822
- res.on('data', (c) => chunks.push(c));
823
- res.on('end', () => {
824
- const text = Buffer.concat(chunks).toString('utf8');
825
- let parsed;
826
- try { parsed = JSON.parse(text); }
827
- catch (e) { reject(new Error(`memory-service ${urlPath} invalid JSON: ${e.message}`)); return; }
828
- resolve({ statusCode: res.statusCode, body: parsed });
829
- });
830
- });
831
- req.on('error', reject);
832
- req.on('timeout', () => { req.destroy(new Error('memory-service timeout')); });
833
- if (payload) req.write(payload);
834
- req.end();
835
- });
836
- }
837
-
838
- const WINDOWS_BROWSER_CANDIDATES = [
839
- { label: 'Chrome (user)', env: 'LOCALAPPDATA', parts: ['Google', 'Chrome', 'Application', 'chrome.exe'] },
840
- { label: 'Chrome (Program Files)', env: 'PROGRAMFILES', parts: ['Google', 'Chrome', 'Application', 'chrome.exe'] },
841
- { label: 'Chrome (Program Files x86)', env: 'PROGRAMFILES(X86)', parts: ['Google', 'Chrome', 'Application', 'chrome.exe'] },
842
- { label: 'Edge (user)', env: 'LOCALAPPDATA', parts: ['Microsoft', 'Edge', 'Application', 'msedge.exe'] },
843
- { label: 'Edge (Program Files)', env: 'PROGRAMFILES', parts: ['Microsoft', 'Edge', 'Application', 'msedge.exe'] },
844
- { label: 'Edge (Program Files x86)', env: 'PROGRAMFILES(X86)', parts: ['Microsoft', 'Edge', 'Application', 'msedge.exe'] },
845
- { label: 'Brave (user)', env: 'LOCALAPPDATA', parts: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'] },
846
- { label: 'Brave (Program Files)', env: 'PROGRAMFILES', parts: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'] },
847
- { label: 'Brave (Program Files x86)', env: 'PROGRAMFILES(X86)', parts: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'] },
848
- { label: 'Vivaldi (user)', env: 'LOCALAPPDATA', parts: ['Vivaldi', 'Application', 'vivaldi.exe'] },
849
- { label: 'Vivaldi (Program Files)', env: 'PROGRAMFILES', parts: ['Vivaldi', 'Application', 'vivaldi.exe'] },
850
- { label: 'Vivaldi (Program Files x86)', env: 'PROGRAMFILES(X86)', parts: ['Vivaldi', 'Application', 'vivaldi.exe'] },
851
- ];
852
-
853
- function getBrowserPath() {
854
- const checked = [];
855
- const missingEnv = new Set();
856
- const seenPaths = new Set();
857
-
858
- for (const candidate of WINDOWS_BROWSER_CANDIDATES) {
859
- const base = process.env[candidate.env];
860
- if (!base) {
861
- missingEnv.add(candidate.env);
862
- continue;
863
- }
864
-
865
- const browserPath = join(base, ...candidate.parts);
866
- if (seenPaths.has(browserPath)) continue;
867
- seenPaths.add(browserPath);
868
- checked.push({ label: candidate.label, path: browserPath });
869
- if (existsSync(browserPath)) return browserPath;
870
- }
871
-
872
- const checkedText = checked.length
873
- ? checked.map(item => `${item.label}: ${item.path}`).join('; ')
874
- : 'no candidate paths because required environment variables were missing';
875
- const missingText = missingEnv.size ? ` Missing env vars: ${[...missingEnv].join(', ')}.` : '';
876
- console.error(`[setup] No supported Chromium browser found for Config UI app mode. Checked ${checked.length} path(s): ${checkedText}.${missingText}`);
877
- return null;
878
- }
879
-
880
- // One stable chrome profile dir reused across /mixdog:config invocations.
881
- //
882
- // Previous design created a fresh `chrome-app-<unix-ms>` per call to sidestep
883
- // the singleton-lock issue, but that forced chrome through its full first-run
884
- // path every time: many short-lived helper subprocesses (component update,
885
- // crash handler, network service, gpu, renderer warmups, etc.), each
886
- // allocating a conhost window even with the cmd.exe console-inherit trick.
887
- // Users saw this as repeated terminal flashes throughout the loading
888
- // spinner. Stable profile means first-run happens once; subsequent launches
889
- // open the popup directly with no helper churn.
890
- //
891
- // Singleton-lock collision is handled in the launch path: the vbs that
892
- // ShellExecutes chrome first taskkill-s any existing `MIXDOG CONFIG` window,
893
- // so the profile lock is released before the new chrome boots. The kill
894
- // runs hidden+synchronous via WScript.Shell.Run, no extra spawn flash.
895
- //
896
- // Sweep legacy `chrome-app-<unix-ms>` dirs (from the fresh-profile era) so
897
- // they don't bloat the data dir.
898
- const CHROME_PROFILE_DIR = 'chrome-app-profile';
899
- const CHROME_PROFILE_LEGACY_PREFIX = 'chrome-app-';
900
- function ensureStableChromeProfileDir() {
901
- const root = DATA_DIR;
902
- // Sweep old `chrome-app-<digits>` directories (legacy, not the stable one).
903
- try {
904
- const entries = readdirSync(root, { withFileTypes: true });
905
- for (const e of entries) {
906
- if (!e.isDirectory()) continue;
907
- if (e.name === CHROME_PROFILE_DIR) continue;
908
- if (!e.name.startsWith(CHROME_PROFILE_LEGACY_PREFIX)) continue;
909
- const suffix = e.name.slice(CHROME_PROFILE_LEGACY_PREFIX.length);
910
- if (!/^\d+$/.test(suffix)) continue;
911
- try { rmSync(join(root, e.name), { recursive: true, force: true }); } catch {}
912
- }
913
- } catch {}
914
- const profileDir = join(root, CHROME_PROFILE_DIR);
915
- try { mkdirSync(profileDir, { recursive: true }); } catch {}
916
- // Suppress chrome's password-save prompt and autofill prompts inside the
917
- // config UI popup. The setup window only renders local form fields (API
918
- // keys, tokens) that chrome would otherwise treat as login fields and pop
919
- // a "save password?" bubble on every edit. Writing the Preferences file
920
- // before chrome's first launch with this profile dir is the only reliable
921
- // way to disable the password manager — command-line flags alone don't
922
- // suppress the bubble in current chrome versions. Only seed on first
923
- // launch (file absent); preserve user-mutated state otherwise.
924
- try {
925
- const defaultDir = join(profileDir, 'Default');
926
- mkdirSync(defaultDir, { recursive: true });
927
- const prefsPath = join(defaultDir, 'Preferences');
928
- if (!existsSync(prefsPath)) {
929
- writeFileSync(
930
- prefsPath,
931
- JSON.stringify({
932
- credentials_enable_service: false,
933
- credentials_enable_autosignin: false,
934
- profile: { password_manager_enabled: false },
935
- autofill: { enabled: false, profile_enabled: false, credit_card_enabled: false },
936
- }),
937
- 'utf8',
938
- );
939
- }
940
- } catch {}
941
- return profileDir;
942
- }
943
-
944
- // killChromesUsingProfile() and the makeFreshChromeProfileDir alias were
945
- // removed: with per-launch profile dirs there is no singleton contention
946
- // to clean up, and the alias only ever pointed at ensureStableChromeProfileDir
947
- // from a single call site.
948
-
949
- // Terminate every chrome.exe whose command line references a mixdog
950
- // per-launch profile dir (matched by prefix `chrome-app-`). Runs before
951
- // each new `--app=` spawn so old popups die and release their keepalive
952
- // HTTP connections to setup-server; without this the connections pile up
953
- // in ESTABLISHED state and exhaust the listener's accept queue (visible
954
- // to the user as `/mixdog:config` printing "Config UI" but every
955
- // subsequent fetch — including F5 — hanging forever).
956
- function killAllMixdogChromes() {
957
- if (!isWin) return;
958
- debugSetup(`[open-debug] killAllMixdogChromes spawnSync powershell`);
959
- const script =
960
- "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" -ErrorAction SilentlyContinue | " +
961
- "Where-Object { $_.CommandLine -and $_.CommandLine -match 'chrome-app-' } | " +
962
- "ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }";
963
- try {
964
- spawnSync(
965
- 'powershell.exe',
966
- ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script],
967
- { encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'ignore', 'ignore'], timeout: 6000 },
968
- );
969
- } catch {
970
- // Best-effort. If chrome stays alive the next spawn may still succeed
971
- // (different profile dir), at worst the user gets a duplicate window.
972
- }
973
- }
974
-
975
- // Bring the most recently spawned MIXDOG CONFIG window to the foreground.
976
- // Bring the newest MIXDOG CONFIG window to the foreground. WScript.Shell
977
- // AppActivate only — no Add-Type / P-Invoke. Add-Type compiles C# at runtime
978
- // via csc.exe, which flashes a conhost window and adds ~1-2s; AppActivate is a
979
- // script-context activation Windows allows with no compile. A SendKeys('%')
980
- // first synthesizes an Alt input event from this process, lifting the
981
- // foreground restriction so AppActivate reliably promotes the window. Detached
982
- // PowerShell — does not block the /open response. Polls up to ~3s (Chrome may
983
- // take 400-900ms to bind its window title after launch).
984
- function focusNewestMixdogWindow() {
985
- if (!isWin) return;
986
- debugSetup(`[open-debug] focusNewestMixdogWindow spawn powershell`);
987
- const psScript =
988
- "$sh = New-Object -ComObject WScript.Shell;\n" +
989
- "for ($i = 0; $i -lt 12; $i++) {\n" +
990
- " Start-Sleep -Milliseconds 250;\n" +
991
- " $p = Get-Process | Where-Object { $_.MainWindowTitle -eq 'MIXDOG CONFIG' } | Sort-Object StartTime -Descending | Select-Object -First 1;\n" +
992
- " if ($p -and $p.MainWindowHandle -ne 0) {\n" +
993
- " try { $sh.SendKeys('%') } catch {}\n" +
994
- " $sh.AppActivate($p.Id) | Out-Null;\n" +
995
- " break;\n" +
996
- " }\n" +
997
- "}";
998
- try {
999
- const child = spawn(
1000
- 'powershell.exe',
1001
- ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', psScript],
1002
- { detached: true, stdio: 'ignore', windowsHide: true },
1003
- );
1004
- child.unref();
1005
- } catch {
1006
- // best-effort — window still opens, just may stay buried
1007
- }
1008
- }
1009
-
1010
- function getCenteredWindowPosition() {
1011
- if (!isWin) return null;
1012
- debugSetup(`[open-debug] getCenteredWindowPosition spawnSync powershell`);
1013
- // Center on whichever monitor the cursor is on, not unconditionally on
1014
- // PrimaryScreen. With a two-monitor setup the popup used to land on the
1015
- // primary at (805, 246) even when the user was working on the secondary —
1016
- // so they reported the window as "not showing" while it was actually
1017
- // visible on the other monitor. Pick the active monitor via
1018
- // Cursor.Position → Screen.FromPoint.
1019
- const script = [
1020
- "[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')",
1021
- "$p=[System.Windows.Forms.Cursor]::Position",
1022
- "$s=[System.Windows.Forms.Screen]::FromPoint($p)",
1023
- "$a=$s.WorkingArea",
1024
- 'Write-Output "$($a.X),$($a.Y),$($a.Width),$($a.Height)"',
1025
- ].join(';');
1026
- try {
1027
- const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], {
1028
- encoding: 'utf8',
1029
- windowsHide: true,
1030
- stdio: ['ignore', 'pipe', 'ignore'],
1031
- });
1032
- if (result.status !== 0) return null;
1033
- const [x, y, width, height] = (result.stdout || '').trim().split(',').map(Number);
1034
- if ([x, y, width, height].some(Number.isNaN)) return null;
1035
- return {
1036
- x: Math.max(0, Math.round(x + ((width - APP_WIDTH) / 2))),
1037
- y: Math.max(0, Math.round(y + ((height - APP_HEIGHT) / 2))),
1038
- };
1039
- } catch {
1040
- return null;
1041
- }
1042
- }
1043
-
1044
- function formatOpenError(error) {
1045
- return error instanceof Error ? error.message : String(error);
1046
- }
1047
-
1048
- function describeSpawnSyncResult(result) {
1049
- if (result.error) return formatOpenError(result.error);
1050
- const details = [];
1051
- if (typeof result.status === 'number') details.push(`exit status ${result.status}`);
1052
- if (result.signal) details.push(`signal ${result.signal}`);
1053
- const stderr = (result.stderr || '').toString().trim();
1054
- const stdout = (result.stdout || '').toString().trim();
1055
- if (stderr) details.push(`stderr: ${stderr}`);
1056
- if (stdout) details.push(`stdout: ${stdout}`);
1057
- return details.join('; ') || 'unknown launch failure';
1058
- }
1059
-
1060
- function logOpenFailure(method, message) {
1061
- console.error(`[setup] Failed to open Config UI window via ${method}: ${message}`);
1062
- }
1063
-
1064
- function tryDetachedOpen(method, command, args, attempts) {
1065
- return new Promise(resolve => {
1066
- let child;
1067
- let settled = false;
1068
- const finish = ok => {
1069
- if (settled) return;
1070
- settled = true;
1071
- resolve(ok);
1072
- };
1073
-
1074
- try {
1075
- child = spawn(command, args, {
1076
- detached: true,
1077
- stdio: 'ignore',
1078
- windowsHide: true,
1079
- });
1080
- } catch (error) {
1081
- const message = formatOpenError(error);
1082
- attempts.push({ method, ok: false, error: message });
1083
- logOpenFailure(method, message);
1084
- finish(false);
1085
- return;
1086
- }
1087
-
1088
- child.once('error', error => {
1089
- const message = formatOpenError(error);
1090
- attempts.push({ method, ok: false, error: message });
1091
- logOpenFailure(method, message);
1092
- finish(false);
1093
- });
1094
- child.once('spawn', () => {
1095
- child.unref();
1096
- attempts.push({ method, ok: true });
1097
- finish(true);
1098
- });
1099
- });
1100
- }
1101
-
1102
- function trySyncOpen(method, command, args, attempts) {
1103
- debugSetup(`[open-debug] trySyncOpen method=${method} command=${command}`);
1104
- let result;
1105
- try {
1106
- result = spawnSync(command, args, {
1107
- encoding: 'utf8',
1108
- windowsHide: true,
1109
- stdio: ['ignore', 'pipe', 'pipe'],
1110
- });
1111
- } catch (error) {
1112
- const message = formatOpenError(error);
1113
- attempts.push({ method, ok: false, error: message });
1114
- logOpenFailure(method, message);
1115
- return false;
1116
- }
1117
-
1118
- if (!result.error && result.status === 0) {
1119
- attempts.push({ method, ok: true });
1120
- return true;
1121
- }
1122
-
1123
- const message = describeSpawnSyncResult(result);
1124
- attempts.push({ method, ok: false, error: message });
1125
- logOpenFailure(method, message);
1126
- return false;
1127
- }
1128
-
1129
- function quotePowerShellString(value) {
1130
- return `'${String(value).replace(/'/g, "''")}'`;
1131
- }
1132
-
1133
- // Serialize every window-open sequence through one in-process chain. All
1134
- // /open requests (and the open-on-start path) funnel through this single
1135
- // server process, so concurrent opens would otherwise race the FindChromePid
1136
- // existence gate: open #2 runs while open #1's browser has spawned but is not
1137
- // yet discoverable, sees pid=0, and deletes the LIVE owner's Singleton* files
1138
- // then respawns. The lock holds (see openAppWindowSequence's win32 path) until
1139
- // the spawned browser is discoverable or its launcher child exits, so by the
1140
- // time the next sequence runs the existence check is stable.
1141
- let openWindowChain = Promise.resolve();
1142
- function openAppWindow() {
1143
- // Run after the previous sequence settles (success OR failure), ignoring its
1144
- // result; keep the chain alive with a rejection-swallowing tail so one
1145
- // failed open never poisons later opens.
1146
- const run = openWindowChain.then(openAppWindowSequence, openAppWindowSequence);
1147
- openWindowChain = run.then(() => {}, () => {});
1148
- return run;
1149
- }
1150
-
1151
- async function openAppWindowSequence() {
1152
- const appUrl = `http://localhost:${PORT}`;
1153
- const attempts = [];
1154
-
1155
- if (isWin) {
1156
- const browser = getBrowserPath();
1157
- if (browser) {
1158
- // Chrome `--app=` mode renders the frameless standalone popup the
1159
- // user wants. Two pitfalls handled:
1160
- // 1. Singleton lock: shared user-data-dir + stale chrome session →
1161
- // IPC reuse → silent no-op when prior window was closed. Sidestep
1162
- // via unique per-launch profile dir.
1163
- // 2. Socket-leak deadlock: each chrome holds persistent keepalive
1164
- // HTTP connections to setup-server. Repeated launches without
1165
- // closing earlier popups accumulate ESTABLISHED/CLOSE_WAIT entries
1166
- // until the server can't accept new requests (F5 hangs forever).
1167
- // Sweep any mixdog-owned chrome before spawning so connections
1168
- // are released and only one popup is ever live.
1169
- // killAllMixdogChromes / getCenteredWindowPosition / focusNewestMixdogWindow
1170
- // were each spawning a PowerShell child (3 console-flash sources per
1171
- // /open). Removing them eliminates the conhost flashes. Trade-offs: old
1172
- // popups stay alive until the user closes them, the new window opens at
1173
- // chrome's default position rather than centered on the cursor's
1174
- // monitor, and it spawns behind whatever was foregrounded. Use a fresh
1175
- // per-launch profile dir to guarantee a new window even without the
1176
- // singleton kill.
1177
- const chromeProfile = ensureStableChromeProfileDir();
1178
- debugSetup(`[open-debug] chromeProfile=${chromeProfile}`);
1179
- const args = [
1180
- `--app=${appUrl}`,
1181
- `--user-data-dir=${chromeProfile}`,
1182
- `--window-size=${APP_WIDTH},${APP_HEIGHT}`,
1183
- '--no-first-run',
1184
- '--no-default-browser-check',
1185
- // Password/autofill suppression defensive belt to the Preferences
1186
- // file suspenders above (some code paths key off the flag instead).
1187
- '--password-store=basic',
1188
- // Helper-subprocess minimization (verified safe). --no-zygote and
1189
- // the other startup-stripping flags broke renderer init (blank
1190
- // page), so this set is the maximum that keeps chrome rendering:
1191
- // Audio in-process via --disable-features below.
1192
- // --disable-logging / --log-level=3: suppress chrome's stderr
1193
- // pipe so helpers don't allocate a console.
1194
- // --enable-features=...:disable trims a few background services.
1195
- // Removed in this revision:
1196
- // --in-process-gpu: produced a white screen on the --app window
1197
- // (renderer JS executed, .main.ready class landed, /generation
1198
- // polled; but the compositor never painted). The cmd.exe show=0
1199
- // wrapper above already hides every helper conhost, so this
1200
- // optimization saved one process at the cost of breaking
1201
- // rendering — keep the default out-of-process GPU.
1202
- // --no-sandbox: did not reduce helper count (sandbox is per-
1203
- // renderer, not a separate process). It only triggered chrome's
1204
- // "unsupported command-line flag" warning bar on top of the
1205
- // --app window. Zero ergonomic benefit, real UX regression.
1206
- '--disable-logging',
1207
- '--log-level=3',
1208
- '--disable-features=PasswordManagerOnboarding,AutofillServerCommunication,PasswordCheck,AudioServiceOutOfProcess,RendererCodeIntegrity,CalculateNativeWinOcclusion',
1209
- ];
1210
-
1211
- // Direct chrome.exe spawn with detached + ignored stdio + windowsHide.
1212
- // The previous `cmd /c start` path made chrome inherit cmd's console
1213
- // handle; its --type=renderer/gpu helpers then attached conhost.exe
1214
- // windows that flashed visibly on screen during open (5-10 flashes
1215
- // observed). Direct spawn keeps the entire chrome process tree
1216
- // consoleless. detached:true puts chrome in its own process group so
1217
- // it stays alive after this wrapper exits.
1218
- debugSetup(`[open-debug] chrome via wscript: ${browser}`);
1219
- let chromeSpawnOk = false;
1220
- try {
1221
- // Bun 1.3.13 spawn() on Windows lets CreateProcess flash a console
1222
- // for the child (and chrome's helpers) before windowsHide takes
1223
- // effect, even with stdio:'ignore'. wscript.exe is a GUI-mode
1224
- // scripting host with no console of its own, but Win11 can ignore
1225
- // Shell.Application.ShellExecute(..., show=0) for cmd.exe and leave
1226
- // the empty conhost visible. Use WMI Win32_Process.Create instead:
1227
- // Win32_ProcessStartup.ShowWindow=0 is applied to the actual
1228
- // CreateProcess call that creates cmd.exe, so cmd's console is born
1229
- // hidden rather than hidden after ShellExecute creates it. Do NOT
1230
- // set CREATE_NO_WINDOW; chrome helpers need a real hidden console to
1231
- // inherit so they do not allocate their own conhost instances.
1232
- const escVbs = s => String(s).replace(/"/g, '""');
1233
- const argsStr = args.join(' ');
1234
- // Hybrid warm-focus / ghost-respawn. A chrome process can outlive its
1235
- // window (closed / crashed / IPC-forwarded over the singleton socket),
1236
- // so "a process exists" never proves "a window is visible" — the old
1237
- // focus-if-found path then opened nothing (the "URL printed, no window"
1238
- // cold-open bug), while killing+respawning on EVERY open cold-boots
1239
- // chrome each time (slow). Balance: TryFocus the existing MAIN with a
1240
- // short bound. If it activates → healthy window, just focus it (fast,
1241
- // no respawn). If it can't (ghost) or none exists → KillMixdogChromes
1242
- // for this profile (--user-data-dir match, MAIN + helpers), clear the
1243
- // stale Singleton* locks, spawn one fresh --app window. The VBS finally
1244
- // re-checks FindChromePid and exits 0 only if a MAIN is live (else 2 →
1245
- // default-browser fallback in JS), so /open's ok reflects a real window.
1246
- //
1247
- // The taskkill → chrome chain runs under one hidden cmd.exe (one
1248
- // cmd.exe per /open). `&` runs both regardless of exit code (taskkill
1249
- // exits 128 when nothing matches — must not abort the chain). cmd /c
1250
- // waits until chrome exits, then exits with it.
1251
- const cmdArg = `/d /c taskkill /F /FI "WINDOWTITLE eq MIXDOG CONFIG" >NUL 2>&1 & "${browser}" ${argsStr} >NUL 2>&1`;
1252
- const appNeedle = `--app=${appUrl}`;
1253
- const profileNeedle = `--user-data-dir=${chromeProfile}`;
1254
- // Process-name needle derived from the ACTUAL chosen browser exe
1255
- // (chrome.exe / msedge.exe / brave.exe / vivaldi.exe). Hardcoding
1256
- // chrome.exe made FindChromePid always return 0 under Edge/Brave/
1257
- // Vivaldi, so the cold branch deleted a LIVE owner's Singleton* files
1258
- // and respawned instead of focusing.
1259
- const procName = basename(browser);
1260
- const vbsLines = [
1261
- 'Option Explicit',
1262
- 'Const HIDDEN_WINDOW = 0',
1263
- 'Dim Wmi, Startup, Wsh, cmdLine, cmdPid, rc, appNeedle, profileNeedle, existingPid, profileDir, procName, focused',
1264
- 'Set Wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2")',
1265
- 'Set Wsh = CreateObject("WScript.Shell")',
1266
- `appNeedle = "${escVbs(appNeedle)}"`,
1267
- `profileNeedle = "${escVbs(profileNeedle)}"`,
1268
- `profileDir = "${escVbs(chromeProfile)}"`,
1269
- `procName = "${escVbs(procName)}"`,
1270
- 'existingPid = FindChromePid(Wmi, appNeedle, profileNeedle)',
1271
- 'focused = False',
1272
- 'If existingPid <> 0 Then focused = TryFocus(Wsh, existingPid, 8)',
1273
- 'If Not focused Then',
1274
- ' Call KillMixdogChromes(Wmi, profileNeedle)',
1275
- ' WScript.Sleep 200',
1276
- ' Call ClearSingletonLocks(profileDir)',
1277
- ' Set Startup = Wmi.Get("Win32_ProcessStartup").SpawnInstance_',
1278
- ' Startup.ShowWindow = HIDDEN_WINDOW',
1279
- ` cmdLine = "cmd.exe ${escVbs(cmdArg)}"`,
1280
- ' rc = Wmi.Get("Win32_Process").Create(cmdLine, Null, Startup, cmdPid)',
1281
- ' If rc <> 0 Then WScript.Quit rc',
1282
- ' Call FocusMixdogWindow(Wmi, Wsh, appNeedle, profileNeedle)',
1283
- 'End If',
1284
- 'existingPid = FindChromePid(Wmi, appNeedle, profileNeedle)',
1285
- 'If existingPid = 0 Then WScript.Quit 2',
1286
- 'WScript.Quit 0',
1287
- '',
1288
- 'Sub ClearSingletonLocks(profileDir)',
1289
- ' Dim Fso, names, i, p',
1290
- ' On Error Resume Next',
1291
- ' Set Fso = CreateObject("Scripting.FileSystemObject")',
1292
- ' names = Array("SingletonLock", "SingletonSocket", "SingletonCookie")',
1293
- ' For i = 0 To UBound(names)',
1294
- ' p = Fso.BuildPath(profileDir, names(i))',
1295
- ' If Fso.FileExists(p) Then Fso.DeleteFile p, True',
1296
- ' Next',
1297
- ' On Error GoTo 0',
1298
- 'End Sub',
1299
- '',
1300
- 'Sub KillMixdogChromes(Wmi, profileNeedle)',
1301
- ' Dim proc, commandLine',
1302
- ' On Error Resume Next',
1303
- ' For Each proc In Wmi.ExecQuery("SELECT ProcessId,CommandLine FROM Win32_Process WHERE Name = \'" & procName & "\'")',
1304
- ' commandLine = ""',
1305
- ' If Not IsNull(proc.CommandLine) Then commandLine = CStr(proc.CommandLine)',
1306
- ' If InStr(1, commandLine, profileNeedle, vbTextCompare) > 0 Then proc.Terminate',
1307
- ' Next',
1308
- ' On Error GoTo 0',
1309
- 'End Sub',
1310
- '',
1311
- 'Function TryFocus(Wsh, pid, maxTicks)',
1312
- ' Dim i, ok',
1313
- ' ok = False',
1314
- ' On Error Resume Next',
1315
- ' For i = 1 To maxTicks',
1316
- ' Wsh.SendKeys "%"',
1317
- ' WScript.Sleep 25',
1318
- ' ok = Wsh.AppActivate(CLng(pid))',
1319
- ' If ok Then Exit For',
1320
- ' WScript.Sleep 120',
1321
- ' Next',
1322
- ' On Error GoTo 0',
1323
- ' TryFocus = ok',
1324
- 'End Function',
1325
- '',
1326
- 'Sub FocusMixdogWindow(Wmi, Wsh, appNeedle, profileNeedle)',
1327
- ' Dim i, pid, activated',
1328
- ' On Error Resume Next',
1329
- ' For i = 1 To 40',
1330
- ' WScript.Sleep 150',
1331
- ' pid = FindChromePid(Wmi, appNeedle, profileNeedle)',
1332
- ' activated = False',
1333
- ' If pid <> 0 Then',
1334
- ' Wsh.SendKeys "%"',
1335
- ' WScript.Sleep 25',
1336
- ' activated = Wsh.AppActivate(CLng(pid))',
1337
- ' End If',
1338
- ' If Not activated And (pid <> 0 Or i > 8) Then',
1339
- ' Wsh.SendKeys "%"',
1340
- ' WScript.Sleep 25',
1341
- ' activated = Wsh.AppActivate("MIXDOG CONFIG")',
1342
- ' End If',
1343
- ' If activated Then Exit For',
1344
- ' Next',
1345
- ' On Error GoTo 0',
1346
- 'End Sub',
1347
- '',
1348
- 'Function FindChromePid(Wmi, appNeedle, profileNeedle)',
1349
- ' Dim proc, newestPid, newestCreated, commandLine',
1350
- ' newestPid = 0',
1351
- ' newestCreated = ""',
1352
- ' For Each proc In Wmi.ExecQuery("SELECT ProcessId,CommandLine,CreationDate FROM Win32_Process WHERE Name = \'" & procName & "\'")',
1353
- ' commandLine = ""',
1354
- ' If Not IsNull(proc.CommandLine) Then commandLine = CStr(proc.CommandLine)',
1355
- ' If InStr(1, commandLine, appNeedle, vbTextCompare) > 0 Then',
1356
- ' If InStr(1, commandLine, profileNeedle, vbTextCompare) > 0 Then',
1357
- ' If InStr(1, commandLine, "--type=", vbTextCompare) = 0 Then',
1358
- ' If newestCreated = "" Or CStr(proc.CreationDate) > newestCreated Then',
1359
- ' newestCreated = CStr(proc.CreationDate)',
1360
- ' newestPid = CLng(proc.ProcessId)',
1361
- ' End If',
1362
- ' End If',
1363
- ' End If',
1364
- ' End If',
1365
- ' Next',
1366
- ' FindChromePid = newestPid',
1367
- 'End Function',
1368
- ];
1369
- const vbsPath = join(tmpdir(), `mixdog-chrome-launch-${Date.now()}.vbs`);
1370
- writeFileSync(vbsPath, vbsLines.join('\r\n'), 'utf8');
1371
- // Hold the open mutex until this launcher exits. The wscript host
1372
- // runs ClearSingletonLocks→spawn→FocusMixdogWindow, whose loop polls
1373
- // FindChromePid until the browser is discoverable (or its bounded
1374
- // ~6s/40-tick loop expires), then exits. Awaiting it guarantees the
1375
- // next queued open sees a stable existence check — the spawned
1376
- // browser is discoverable (so it focuses) rather than racing the
1377
- // pid=0 gate into a stale-Singleton delete of a live owner.
1378
- //
1379
- // Bounded await: if wscript/WMI/CreateProcess hangs before exiting,
1380
- // an unbounded wait would leave openAppWindowSequence() pending →
1381
- // openWindowChain stuck → every future /open queued forever (the
1382
- // client-side requestOpen timeout never settles the server chain).
1383
- // Race the child's exit against a server-side deadline a few seconds
1384
- // above the VBS focus loop's own ~6s bound; on deadline, kill the
1385
- // wscript child (tree) and report a failed open so the chain advances.
1386
- // Killing wscript cannot touch the browser: the VBS spawns it via WMI
1387
- // Win32_Process.Create (cmd.exe → browser), an independent process not
1388
- // parented to wscript, so a tree-kill of wscript reaps only the focus
1389
- // loop.
1390
- const WSCRIPT_OPEN_DEADLINE_MS = 12000;
1391
- const outcome = await new Promise(resolve => {
1392
- const wscriptChild = spawn('wscript.exe', ['//B', '//NoLogo', vbsPath], {
1393
- stdio: 'ignore', windowsHide: true,
1394
- });
1395
- let settled = false;
1396
- const finish = v => { if (settled) return; settled = true; clearTimeout(timer); resolve(v); };
1397
- const timer = setTimeout(() => {
1398
- // Tree-kill the wscript launcher only; the detached browser lives on.
1399
- try { spawnSync('taskkill', ['/F', '/T', '/PID', String(wscriptChild.pid)], { windowsHide: true, stdio: 'ignore', timeout: 4000 }); } catch {}
1400
- try { wscriptChild.kill(); } catch {}
1401
- finish({ timedOut: true });
1402
- }, WSCRIPT_OPEN_DEADLINE_MS);
1403
- if (typeof timer.unref === 'function') timer.unref();
1404
- wscriptChild.once('error', () => finish({ timedOut: false, code: -1 }));
1405
- wscriptChild.once('exit', code => finish({ timedOut: false, code }));
1406
- });
1407
- if (outcome.timedOut) {
1408
- const err = `wscript launcher did not exit within ${WSCRIPT_OPEN_DEADLINE_MS}ms; killed launcher (browser left running)`;
1409
- attempts.push({ method: 'browser app mode (wscript)', ok: false, error: err });
1410
- logOpenFailure('browser app mode (wscript)', err);
1411
- } else if (outcome.code === 0) {
1412
- // VBS exits 0 only after it re-checks FindChromePid post-spawn and a
1413
- // MAIN (non-helper) chrome for this profile is live — an honest
1414
- // "window materialized" signal, not just "the launcher exited". Any
1415
- // other code (esp. 2 = no window) falls through to the default browser.
1416
- chromeSpawnOk = true;
1417
- attempts.push({ method: 'browser app mode (wscript)', ok: true });
1418
- } else {
1419
- const err = `wscript exited ${outcome.code}; config window did not materialize`;
1420
- attempts.push({ method: 'browser app mode (wscript)', ok: false, error: err });
1421
- logOpenFailure('browser app mode (wscript)', err);
1422
- }
1423
- } catch (error) {
1424
- attempts.push({ method: 'browser app mode (wscript)', ok: false, error: formatOpenError(error) });
1425
- logOpenFailure('browser app mode (wscript)', formatOpenError(error));
1426
- }
1427
- if (chromeSpawnOk) {
1428
- return { ok: true, method: 'browser app mode (wscript)', attempts };
1429
- }
1430
- } else {
1431
- attempts.push({ method: 'browser app mode', ok: false, error: 'No supported Chromium browser path found' });
1432
- }
1433
-
1434
- if (trySyncOpen('cmd start', 'cmd', ['/c', 'start', '', appUrl], attempts)) {
1435
- return {
1436
- ok: true,
1437
- method: 'cmd start',
1438
- warning: browser ? undefined : 'Supported Chromium browser not found; opened with the default browser instead of app mode.',
1439
- attempts,
1440
- };
1441
- }
1442
-
1443
- const psCommand = `Start-Process -FilePath ${quotePowerShellString(appUrl)}`;
1444
- if (trySyncOpen('PowerShell Start-Process', 'powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', psCommand], attempts)) {
1445
- return { ok: true, method: 'PowerShell Start-Process', attempts };
1446
- }
1447
-
1448
- return { ok: false, error: 'Failed to launch Config UI window', attempts };
1449
- }
1450
-
1451
- if (process.platform === 'darwin') {
1452
- const macResult = await new Promise(resolve => {
1453
- const child = spawn('open', [appUrl], { stdio: 'ignore' });
1454
- let timer = setTimeout(() => {
1455
- try { child.kill(); } catch {}
1456
- resolve({ ok: false, error: 'open-timeout' });
1457
- }, 5000);
1458
- child.once('error', err => resolve({ ok: false, error: err.message }));
1459
- child.once('close', code => {
1460
- clearTimeout(timer);
1461
- resolve(code === 0 ? { ok: true } : { ok: false, error: `exit ${code}` });
1462
- });
1463
- });
1464
- const macAttempt = { method: 'macOS open', ...macResult };
1465
- if (!macResult.ok) logOpenFailure('macOS open', macResult.error);
1466
- return { ...macResult, method: 'macOS open', attempts: [macAttempt] };
1467
- }
1468
-
1469
- // WSL: process.platform is 'linux' but xdg-open targets a (usually absent)
1470
- // Linux GUI. Reach the Windows-HOST browser instead, mirroring open-url.mjs:
1471
- // prefer wslview (wslu), then PowerShell Start-Process (cmd.exe is avoided —
1472
- // see the note below). Each is a sync spawn through trySyncOpen so a missing
1473
- // opener advances to the next.
1474
- if (isWSL()) {
1475
- if (trySyncOpen('wslview', 'wslview', [appUrl], attempts)) {
1476
- return { ok: true, method: 'wslview', attempts };
1477
- }
1478
- // cmd.exe `start` is intentionally NOT used: cmd re-parses URL
1479
- // metacharacters, so a query-string `&` is treated as a command separator
1480
- // and the URL truncates. Start-Process takes the URL as a single argument
1481
- // and does not reparse `&`; quotePowerShellString single-quotes the URL
1482
- // (doubling embedded quotes) so it is injection-safe.
1483
- const wslPsCommand = `Start-Process -FilePath ${quotePowerShellString(appUrl)}`;
1484
- if (trySyncOpen('PowerShell Start-Process', 'powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', wslPsCommand], attempts)) {
1485
- return { ok: true, method: 'PowerShell Start-Process', attempts };
1486
- }
1487
- return { ok: false, error: 'Failed to open Windows browser from WSL', attempts };
1488
- }
1489
-
1490
- const xdgResult = await new Promise(resolve => {
1491
- const child = spawn('xdg-open', [appUrl], { stdio: 'ignore' });
1492
- let timer = setTimeout(() => {
1493
- try { child.kill(); } catch {}
1494
- resolve({ ok: false, error: 'open-timeout' });
1495
- }, 5000);
1496
- child.once('error', err => resolve({ ok: false, error: err.message }));
1497
- child.once('close', code => {
1498
- clearTimeout(timer);
1499
- resolve(code === 0 ? { ok: true } : { ok: false, error: `exit ${code}` });
1500
- });
1501
- });
1502
- const xdgAttempt = { method: 'xdg-open', ...xdgResult };
1503
- if (!xdgResult.ok) logOpenFailure('xdg-open', xdgResult.error);
1504
- return { ...xdgResult, method: 'xdg-open', attempts: [xdgAttempt] };
1505
- }
1506
-
1507
- // -- CLI check --
1508
-
1509
- // Direct spawn of `where.exe` / `which` (shell:false) skips Node's default
1510
- // cmd.exe-wrapped exec on Windows. The cmd.exe wrapper allocates a console
1511
- // that flashed conhost on /cli-check during config-UI boot even with
1512
- // windowsHide:true. Direct spawn keeps the lookup consoleless.
1513
- //
1514
- // 6s timeout + auto-resolve {installed:false} guards against a where.exe
1515
- // PATH-resolution hang. The /cli-check route is one of the 9 loaders the
1516
- // setup-html boot path Promise.allSettled-s on before flipping .main.ready;
1517
- // without a timeout, a hung lookup would leave the page on a white screen
1518
- // indefinitely (loader never settles → spinner never hides → no UI render).
1519
- // 2s was too tight: on cold-cache page-boot the bun spawn → where.exe →
1520
- // close-event roundtrip was empirically 1.5-2.4s on Windows, so the timer
1521
- // raced the close handler and intermittently returned `installed:false`
1522
- // for an actually-installed binary (user-visible "ngrok not found" after
1523
- // fresh /mixdog:config). 6s keeps the hang-guard intact while clearing
1524
- // the spawn-overhead band by ~3x.
1525
- function checkCli(name) {
1526
- return new Promise(resolve => {
1527
- const tool = isWin ? 'where.exe' : 'which';
1528
- let settled = false;
1529
- const finish = result => { if (!settled) { settled = true; resolve(result); } };
1530
- let child;
1531
- try {
1532
- child = spawn(tool, [name], {
1533
- windowsHide: true,
1534
- stdio: ['ignore', 'pipe', 'ignore'],
1535
- shell: false,
1536
- });
1537
- } catch {
1538
- finish({ installed: false });
1539
- return;
1540
- }
1541
- const timer = setTimeout(() => {
1542
- try { child.kill('SIGKILL'); } catch {}
1543
- finish({ installed: false });
1544
- }, 6000);
1545
- let out = '';
1546
- if (child.stdout) child.stdout.on('data', chunk => { out += chunk; });
1547
- child.once('error', () => { clearTimeout(timer); finish({ installed: false }); });
1548
- child.once('close', code => {
1549
- clearTimeout(timer);
1550
- if (code !== 0 || !out.trim()) finish({ installed: false });
1551
- else finish({ installed: true, path: out.trim().split(/\r?\n/)[0] });
1552
- });
1553
- });
1554
- }
1555
-
1556
- // -- HTTP body reader --
1557
- // An empty/whitespace body resolves to {} (action endpoints POST with no
1558
- // body). A NON-empty body that fails JSON.parse is rejected with a tagged
1559
- // error instead of being silently coerced to {} — previously a truncated or
1560
- // malformed payload parsed as {} and the save handler still returned success,
1561
- // masking a failed save (defaults written, real data lost). The request
1562
- // handler converts BadJsonError into a 400 so the client sees the failure.
1563
- class BadJsonError extends Error {
1564
- constructor(message) { super(message); this.name = 'BadJsonError'; this.statusCode = 400; }
1565
- }
1566
- function readBody(req) {
1567
- return new Promise((resolve, reject) => {
1568
- let body = '';
1569
- req.on('data', c => { body += c; });
1570
- req.on('end', () => {
1571
- if (!body.trim()) { resolve({}); return; }
1572
- try { resolve(JSON.parse(body)); }
1573
- catch (e) { reject(new BadJsonError(`malformed JSON body: ${e.message}`)); }
1574
- });
1575
- req.on('error', reject);
1576
- });
1577
- }
1578
-
1579
- // -- Server --
1580
- let openGeneration = 0;
1581
- let windowOpen = false;
1582
-
1583
- const server = http.createServer(async (req, res) => {
1584
- // Outer guard: most handlers await readBody() outside their own try/catch,
1585
- // so a rejected body parse (BadJsonError) would otherwise be an unhandled
1586
- // rejection that leaves the request hanging. Map it to a JSON error response
1587
- // (400 for malformed input, 500 otherwise) so malformed saves fail loudly.
1588
- try {
1589
- await handleRequest(req, res);
1590
- } catch (e) {
1591
- const code = Number.isInteger(e?.statusCode) ? e.statusCode : 500;
1592
- if (!res.headersSent) {
1593
- res.writeHead(code, { 'Content-Type': 'application/json' });
1594
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
1595
- } else {
1596
- try { res.end(); } catch {}
1597
- }
1598
- }
1599
- });
1600
-
1601
- async function handleRequest(req, res) {
1602
- const url = new URL(req.url, `http://localhost:${PORT}`);
1603
- const path = url.pathname;
1604
-
1605
- // Reflective CORS — echo Origin only when it is our loopback UI port;
1606
- // otherwise omit the header so browsers block the cross-origin response.
1607
- const _reqOrigin = req.headers.origin || '';
1608
- if (_reqOrigin && /^http:\/\/(localhost|127\.0\.0\.1):3458(\/|$)/.test(_reqOrigin)) {
1609
- res.setHeader('Access-Control-Allow-Origin', _reqOrigin);
1610
- res.setHeader('Access-Control-Allow-Credentials', 'true');
1611
- }
1612
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1613
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1614
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
1615
-
1616
- // Global CSRF guard — POST/PUT/DELETE require an allowed origin.
1617
- if ((req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') && !isAllowedOrigin(req)) {
1618
- res.writeHead(403, { 'Content-Type': 'application/json' });
1619
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1620
- return;
1621
- }
1622
-
1623
- if (req.method === 'GET' && path === '/') {
1624
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1625
- res.end(readFileSync(HTML_PATH, 'utf8'));
1626
- return;
1627
- }
1628
-
1629
- // Phase D-2 — Smart Bridge cache dashboard (provider × profile matrix).
1630
- // Each workflow role exposes one `shards` map keyed by provider so callers
1631
- // can show, for example, that a role is warm on anthropic-oauth but cold on
1632
- // openai-oauth without either row overwriting the other.
1633
- if (req.method === 'GET' && path === '/bridge/stats') {
1634
- try {
1635
- const { CacheRegistry } = await import('../src/agent/orchestrator/smart-bridge/registry.mjs');
1636
- const registry = CacheRegistry.shared();
1637
- const stats = registry.getStats();
1638
- const profiles = {};
1639
- let warmShards = 0;
1640
- for (const [profileId, providers] of Object.entries(stats.profiles || {})) {
1641
- const shards = {};
1642
- for (const [provider, entry] of Object.entries(providers)) {
1643
- const hit = entry.hitCount || 0;
1644
- const miss = entry.missCount || 0;
1645
- const total = hit + miss;
1646
- const expiresInMs = Math.max(0, entry.expiresIn || 0);
1647
- const warm = expiresInMs > 0 && entry.observedOnly !== true;
1648
- if (warm) warmShards += 1;
1649
- shards[provider] = {
1650
- prefixHash: entry.prefixHash || null,
1651
- hitCount: hit,
1652
- missCount: miss,
1653
- hitRate: total > 0 ? hit / total : 0,
1654
- warm,
1655
- expiresInMs,
1656
- createdAt: entry.createdAt ? new Date(entry.createdAt).toISOString() : null,
1657
- observedOnly: entry.observedOnly === true,
1658
- };
1659
- }
1660
- profiles[profileId] = {
1661
- id: profileId,
1662
- taskType: null,
1663
- behavior: null,
1664
- fallbackPreset: null,
1665
- description: '(workflow role)',
1666
- shards,
1667
- };
1668
- }
1669
- res.writeHead(200, { 'Content-Type': 'application/json' });
1670
- res.end(JSON.stringify({
1671
- profileCount: stats.profileCount || Object.keys(profiles).length,
1672
- shardCount: stats.shardCount || 0,
1673
- warmShardCount: warmShards,
1674
- openaiKeyCount: stats.openaiKeyCount || 0,
1675
- updatedAt: registry.data.updatedAt,
1676
- profiles,
1677
- }));
1678
- } catch (e) {
1679
- res.writeHead(500, { 'Content-Type': 'application/json' });
1680
- res.end(JSON.stringify({ error: String(e?.message || e) }));
1681
- }
1682
- return;
1683
- }
1684
-
1685
- // ── GET /bridge/status ──────────────────────────────────────────────
1686
- // Cheap, read-only, loopback-only. Returns mixdog runtime state as
1687
- // either a JSON object (?format=json) or a single-line statusline
1688
- // string (?format=text or Accept: text/plain).
1689
- // No Origin guard needed — read-only endpoint (C2 convention, v0.1.14).
1690
- // 0.1.26: aggregation logic lives in src/status/aggregator.mjs so the
1691
- // MCP-embedded status server shares the same implementation.
1692
- if (req.method === 'GET' && path === '/bridge/status') {
1693
- try {
1694
- const { buildBridgeStatus, renderBridgeStatusText } = await import('../src/status/aggregator.mjs');
1695
- const wantText = url.searchParams.get('format') === 'text'
1696
- || (req.headers['accept'] || '').includes('text/plain');
1697
- const payload = await buildBridgeStatus(DATA_DIR);
1698
- if (wantText) {
1699
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
1700
- res.end(renderBridgeStatusText(payload));
1701
- } else {
1702
- res.writeHead(200, { 'Content-Type': 'application/json' });
1703
- res.end(JSON.stringify(payload));
1704
- }
1705
- } catch (e) {
1706
- res.writeHead(500, { 'Content-Type': 'application/json' });
1707
- res.end(JSON.stringify({ error: String(e?.message || e) }));
1708
- }
1709
- return;
1710
- }
1711
-
1712
-
1713
- // ── GET /api/plugin-path ─────────────────────────────────────────────────
1714
- // Returns the absolute directory of the plugin install (parent of setup/).
1715
- // Used by setup.html to render the correct statusline.sh path in the snippet.
1716
- if (req.method === 'GET' && path === '/api/plugin-path') {
1717
- const pluginRoot = join(__dirname, '..');
1718
- res.writeHead(200, { 'Content-Type': 'application/json' });
1719
- res.end(JSON.stringify({ path: pluginRoot }));
1720
- return;
1721
- }
1722
-
1723
- if (req.method === 'GET' && path === '/config') {
1724
- const raw = readConfig();
1725
- const config = applyChannelsDefaults(raw);
1726
- const invalidDiscordToken = pruneInvalidDiscordToken(config);
1727
- // Re-hydrate secrets from the keychain so the UI password inputs round-
1728
- // trip. mergeConfig() routes discord.token / webhook.authtoken to the
1729
- // keychain on save and deletes them from the JSON (keychain is the
1730
- // canonical source of truth); without re-hydration here, every reload
1731
- // shows blank password fields and users assume the save failed.
1732
- const dToken = getDiscordToken();
1733
- if (dToken && !invalidDiscordToken) config.discord = { ...(config.discord || {}), token: dToken };
1734
- const wAuth = getWebhookAuthtoken();
1735
- if (wAuth) config.webhook = { ...(config.webhook || {}), authtoken: wAuth };
1736
- if (invalidDiscordToken) config._secretDiagnostics = { discordToken: invalidDiscordToken };
1737
- res.writeHead(200, { 'Content-Type': 'application/json' });
1738
- res.end(JSON.stringify(config));
1739
- return;
1740
- }
1741
-
1742
- if (req.method === 'GET' && path === '/config/secrets') {
1743
- const channelsConfig = applyChannelsDefaults(readConfig());
1744
- res.writeHead(200, { 'Content-Type': 'application/json' });
1745
- res.end(JSON.stringify(fullSecretStatus(channelsConfig)));
1746
- return;
1747
- }
1748
-
1749
- if (req.method === 'POST' && path === '/config') {
1750
- if (!isAllowedOrigin(req)) {
1751
- res.writeHead(403, { 'Content-Type': 'application/json' });
1752
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1753
- return;
1754
- }
1755
- const data = await readBody(req);
1756
- const secrets = {};
1757
- try {
1758
- // RMW inside the config lock: compute mergeConfig() against the value
1759
- // read under the same lock that guards the write, so overlapping saves
1760
- // can't clobber each other (read-then-write outside the lock raced).
1761
- let merged;
1762
- updateSection('channels', (current) => {
1763
- merged = mergeConfig(current, data, secrets);
1764
- return merged;
1765
- });
1766
- console.log(` Config saved: channels secrets=${JSON.stringify(secrets)}`);
1767
-
1768
- res.writeHead(200, { 'Content-Type': 'application/json' });
1769
- res.end(JSON.stringify({ ok: true, secrets, secretStatus: fullSecretStatus(merged) }));
1770
- } catch (e) {
1771
- process.stderr.write('[setup] /config failed: ' + (e?.stack || e?.message || String(e)) + '\n');
1772
- res.writeHead(500, { 'Content-Type': 'application/json' });
1773
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e), secrets }));
1774
- }
1775
- return;
1776
- }
1777
-
1778
- // -- B6 General module toggles (channels / memory / search / agent) --
1779
- // Stored as a top-level `modules` section inside mixdog-config.json.
1780
- // Missing keys default to enabled:true so pre-B6 configs keep all
1781
- // modules on. Changes require a plugin restart to take effect.
1782
- if (req.method === 'GET' && path === '/modules') {
1783
- const raw = readSection('modules');
1784
- const out = {};
1785
- for (const name of ['channels', 'memory', 'search', 'agent']) {
1786
- const entry = raw && typeof raw === 'object' ? raw[name] : null;
1787
- const enabled = entry && typeof entry === 'object' && entry.enabled === false ? false : true;
1788
- out[name] = { enabled };
1789
- }
1790
- res.writeHead(200, { 'Content-Type': 'application/json' });
1791
- res.end(JSON.stringify(out));
1792
- return;
1793
- }
1794
-
1795
- if (req.method === 'POST' && path === '/modules') {
1796
- if (!isAllowedOrigin(req)) {
1797
- res.writeHead(403, { 'Content-Type': 'application/json' });
1798
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1799
- return;
1800
- }
1801
- const data = await readBody(req);
1802
- const sanitized = {};
1803
- for (const name of ['channels', 'memory', 'search', 'agent']) {
1804
- const entry = data && typeof data === 'object' ? data[name] : null;
1805
- const enabled = entry && typeof entry === 'object' && entry.enabled === false ? false : true;
1806
- sanitized[name] = { enabled };
1807
- }
1808
- // Serialize through updateSection (file lock + atomic RMW + backup
1809
- // restore) instead of read→merge→whole-file write. A bare
1810
- // readJsonFile()→{} on a transient read failure here used to drop
1811
- // every other section when the thin object was written back.
1812
- // Intentional full replace: the modules section schema is only {enabled}
1813
- // per module; the UI owns the complete map.
1814
- updateSection('modules', () => sanitized);
1815
- console.log(' Config saved: modules');
1816
- res.writeHead(200, { 'Content-Type': 'application/json' });
1817
- res.end(JSON.stringify({ ok: true, modules: sanitized }));
1818
- return;
1819
- }
1820
-
1821
- // -- B2 Security capabilities (homeAccess) ---------------------------
1822
- // Stored as a top-level `capabilities` section inside mixdog-config.json.
1823
- // Missing keys default to `false` so out-of-the-box installs stay
1824
- // cwd-only; flipping a toggle takes effect on the next tool call
1825
- // (capability is re-read per invocation in builtin.mjs/patch.mjs).
1826
- if (req.method === 'GET' && path === '/capabilities') {
1827
- const raw = readSection('capabilities');
1828
- const out = { homeAccess: !!(raw && typeof raw === 'object' && raw.homeAccess === true) };
1829
- res.writeHead(200, { 'Content-Type': 'application/json' });
1830
- res.end(JSON.stringify(out));
1831
- return;
1832
- }
1833
-
1834
- if (req.method === 'POST' && path === '/capabilities') {
1835
- if (!isAllowedOrigin(req)) {
1836
- res.writeHead(403, { 'Content-Type': 'application/json' });
1837
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1838
- return;
1839
- }
1840
- const data = await readBody(req);
1841
- const sanitized = { homeAccess: !!(data && typeof data === 'object' && data.homeAccess === true) };
1842
- // Serialize through updateSection (file lock + atomic RMW + backup
1843
- // restore); see /modules POST above for why the read→merge→whole-file
1844
- // write was unsafe.
1845
- // Intentional full replace: capabilities is a flat toggle map (homeAccess);
1846
- // no unmanaged sidecar fields today.
1847
- updateSection('capabilities', () => sanitized);
1848
- console.log(' Config saved: capabilities');
1849
- res.writeHead(200, { 'Content-Type': 'application/json' });
1850
- res.end(JSON.stringify({ ok: true, capabilities: sanitized }));
1851
- return;
1852
- }
1853
-
1854
- // -- Schedules CRUD --
1855
- const SCHEDULES_DIR = join(DATA_DIR, 'schedules');
1856
-
1857
- if (req.method === 'GET' && path === '/schedules') {
1858
- res.writeHead(200, { 'Content-Type': 'application/json' });
1859
- res.end(JSON.stringify(listSchedules()));
1860
- return;
1861
- }
1862
-
1863
- if (req.method === 'POST' && path === '/schedules') {
1864
- if (!isAllowedOrigin(req)) {
1865
- res.writeHead(403, { 'Content-Type': 'application/json' });
1866
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1867
- return;
1868
- }
1869
- const sc = await readBody(req);
1870
- const name = sanitizeName(sc.name);
1871
- if (!name) { res.writeHead(400); res.end('name required or invalid'); return; }
1872
- if (!sc.time) {
1873
- res.writeHead(400, { 'Content-Type': 'application/json' });
1874
- res.end(JSON.stringify({ ok: false, error: 'time required' }));
1875
- return;
1876
- }
1877
- const isCronLike = sc.time.trim().split(/\s+/).length >= 5;
1878
- const isHHMM = /^([01]?\d|2[0-3]):[0-5]\d$/.test(sc.time);
1879
- const isLegacy = /^(every\d+m|hourly|daily)$/.test(sc.time);
1880
- if (isCronLike) {
1881
- try { validateCronExpression(sc.time); } catch (e) {
1882
- res.writeHead(400, { 'Content-Type': 'application/json' });
1883
- res.end(JSON.stringify({ ok: false, error: e.message }));
1884
- return;
1885
- }
1886
- } else if (!isHHMM && !isLegacy) {
1887
- res.writeHead(400, { 'Content-Type': 'application/json' });
1888
- res.end(JSON.stringify({ ok: false, error: 'invalid time format: must be HH:MM, cron expression, or every<N>m|hourly|daily' }));
1889
- return;
1890
- }
1891
- if (isHHMM) {
1892
- // The scheduler registers cron expressions only — convert on save so
1893
- // UI-created entries actually fire instead of being skipped at reload.
1894
- const [h, m] = sc.time.split(':');
1895
- // Encode the legacy `days` field into the day-of-week slot.
1896
- const dow = sc.days === 'weekday' ? '1-5' : (sc.days === 'weekend' ? '0,6' : '*');
1897
- sc.time = `${Number(m)} ${Number(h)} * * ${dow}`;
1898
- } else if (isLegacy) {
1899
- const everyM = sc.time.match(/^every(\d+)m$/);
1900
- if (everyM) {
1901
- const n = Math.min(Math.max(Number(everyM[1]), 1), 59);
1902
- sc.time = `*/${n} * * * *`;
1903
- } else if (sc.time === 'hourly') {
1904
- sc.time = '0 * * * *';
1905
- } else {
1906
- sc.time = '0 0 * * *';
1907
- }
1908
- }
1909
- const dir = join(SCHEDULES_DIR, name);
1910
- mkdirSync(dir, { recursive: true });
1911
- const prompt = sc.prompt || '';
1912
- delete sc.prompt;
1913
- delete sc.name;
1914
- const configPath = join(dir, 'config.json');
1915
- const merged = mergeEndpointConfig(readJsonFile(configPath), sc);
1916
- writeFileSync(configPath, JSON.stringify(merged, null, 2));
1917
- writeFileSync(join(dir, 'instructions.md'), prompt);
1918
- console.log(' Schedule saved:', name);
1919
- res.writeHead(200, { 'Content-Type': 'application/json' });
1920
- res.end(JSON.stringify({ ok: true }));
1921
- return;
1922
- }
1923
-
1924
- if (req.method === 'DELETE' && path === '/schedules') {
1925
- if (!isAllowedOrigin(req)) {
1926
- res.writeHead(403, { 'Content-Type': 'application/json' });
1927
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1928
- return;
1929
- }
1930
- const name = sanitizeName(url.searchParams.get('name'));
1931
- if (!name) { res.writeHead(400); res.end('name required or invalid'); return; }
1932
- const dir = join(SCHEDULES_DIR, name);
1933
- if (existsSync(dir)) { rmSync(dir, { recursive: true }); console.log(' Schedule deleted:', name); }
1934
- res.writeHead(200, { 'Content-Type': 'application/json' });
1935
- res.end(JSON.stringify({ ok: true }));
1936
- return;
1937
- }
1938
-
1939
- if (req.method === 'GET' && path.startsWith('/schedules/file/')) {
1940
- if (!isAllowedOrigin(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' })); return; }
1941
- const name = sanitizeName(decodeURIComponent(path.slice('/schedules/file/'.length)));
1942
- if (!name) { res.writeHead(400); res.end('invalid schedule name'); return; }
1943
- const filePath = join(SCHEDULES_DIR, name, 'instructions.md');
1944
- if (!existsSync(filePath)) { mkdirSync(join(SCHEDULES_DIR, name), { recursive: true }); writeFileSync(filePath, '', 'utf8'); }
1945
- if (isWin) { spawn('cmd', ['/c', 'start', '""', filePath.replace(/[&^"<>|]/g, '^$&')], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: false }).unref(); }
1946
- else { spawn('open', [filePath], { detached: true, stdio: 'ignore' }).unref(); }
1947
- res.writeHead(200, { 'Content-Type': 'application/json' });
1948
- res.end(JSON.stringify({ ok: true }));
1949
- return;
1950
- }
1951
-
1952
- // -- Webhooks CRUD --
1953
- const WEBHOOKS_DIR = join(DATA_DIR, 'webhooks');
1954
-
1955
- if (req.method === 'GET' && path === '/webhooks') {
1956
- const result = [];
1957
- if (existsSync(WEBHOOKS_DIR)) {
1958
- for (const name of readdirSync(WEBHOOKS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)) {
1959
- const cfg = readJsonFile(join(WEBHOOKS_DIR, name, 'config.json')) || {};
1960
- let instructions = '';
1961
- try { instructions = readFileSync(join(WEBHOOKS_DIR, name, 'instructions.md'), 'utf8'); } catch {}
1962
- result.push({ name, ...cfg, instructions });
1963
- }
1964
- }
1965
- res.writeHead(200, { 'Content-Type': 'application/json' });
1966
- res.end(JSON.stringify(result));
1967
- return;
1968
- }
1969
-
1970
- if (req.method === 'POST' && path === '/webhooks') {
1971
- if (!isAllowedOrigin(req)) {
1972
- res.writeHead(403, { 'Content-Type': 'application/json' });
1973
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
1974
- return;
1975
- }
1976
- const wh = await readBody(req);
1977
- const name = sanitizeName(wh.name);
1978
- if (!name) { res.writeHead(400); res.end('name required or invalid'); return; }
1979
- const dir = join(WEBHOOKS_DIR, name);
1980
- mkdirSync(dir, { recursive: true });
1981
- const instructions = wh.instructions || '';
1982
- delete wh.instructions;
1983
- delete wh.name;
1984
- // Invariant: webhook delegate dispatch is bound to the internal
1985
- // `webhook-handler` hidden role. The UI no longer exposes a role
1986
- // picker, and any role value posted by a third-party client is
1987
- // overwritten here so dispatch always lands on the plugin-managed
1988
- // hidden role rather than a user-workflow role.
1989
- const configPath = join(dir, 'config.json');
1990
- const merged = mergeWebhookEndpointConfig(readJsonFile(configPath), wh);
1991
- writeFileSync(configPath, JSON.stringify(merged, null, 2));
1992
- writeFileSync(join(dir, 'instructions.md'), instructions);
1993
- console.log(' Webhook saved:', name);
1994
- res.writeHead(200, { 'Content-Type': 'application/json' });
1995
- res.end(JSON.stringify({ ok: true }));
1996
- return;
1997
- }
1998
-
1999
- if (req.method === 'DELETE' && path === '/webhooks') {
2000
- if (!isAllowedOrigin(req)) {
2001
- res.writeHead(403, { 'Content-Type': 'application/json' });
2002
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2003
- return;
2004
- }
2005
- const name = sanitizeName(url.searchParams.get('name'));
2006
- if (!name) { res.writeHead(400); res.end('name required or invalid'); return; }
2007
- const dir = join(WEBHOOKS_DIR, name);
2008
- if (existsSync(dir)) { rmSync(dir, { recursive: true }); console.log(' Webhook deleted:', name); }
2009
- res.writeHead(200, { 'Content-Type': 'application/json' });
2010
- res.end(JSON.stringify({ ok: true }));
2011
- return;
2012
- }
2013
-
2014
- if (req.method === 'GET' && path.startsWith('/webhooks/file/')) {
2015
- if (!isAllowedOrigin(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' })); return; }
2016
- const name = sanitizeName(decodeURIComponent(path.slice('/webhooks/file/'.length)));
2017
- if (!name) { res.writeHead(400); res.end('invalid webhook name'); return; }
2018
- const filePath = join(WEBHOOKS_DIR, name, 'instructions.md');
2019
- if (!existsSync(filePath)) { mkdirSync(join(WEBHOOKS_DIR, name), { recursive: true }); writeFileSync(filePath, '', 'utf8'); }
2020
- if (isWin) { spawn('cmd', ['/c', 'start', '""', filePath.replace(/[&^"<>|]/g, '^$&')], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: false }).unref(); }
2021
- else { spawn('open', [filePath], { detached: true, stdio: 'ignore' }).unref(); }
2022
- res.writeHead(200, { 'Content-Type': 'application/json' });
2023
- res.end(JSON.stringify({ ok: true }));
2024
- return;
2025
- }
2026
-
2027
- // -- Delivery log --
2028
- // Each endpoint keeps an append-only JSONL under its folder. Lists are
2029
- // latest-wins merged by id, filtered by ?name= / ?status=, sorted ts desc.
2030
- if (req.method === 'GET' && path === '/webhooks/deliveries') {
2031
- const name = url.searchParams.get('name') || null;
2032
- const status = url.searchParams.get('status') || null;
2033
- const limitRaw = parseInt(url.searchParams.get('limit') || '100', 10);
2034
- const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 500) : 100;
2035
- try {
2036
- const mod = await import('../src/channels/lib/webhook.mjs');
2037
- const list = mod.listAllDeliveries({ endpoint: name, status, limit });
2038
- res.writeHead(200, { 'Content-Type': 'application/json' });
2039
- res.end(JSON.stringify(list));
2040
- } catch (err) {
2041
- res.writeHead(500, { 'Content-Type': 'application/json' });
2042
- res.end(JSON.stringify({ error: String(err?.message || err) }));
2043
- }
2044
- return;
2045
- }
2046
-
2047
- // Retry: payload is only preserved as a 512-char preview, so a silent
2048
- // replay would be misleading. Return 400 and ask the sender to redeliver.
2049
- if (req.method === 'POST' && path.startsWith('/webhooks/deliveries/') && path.endsWith('/retry')) {
2050
- if (!isAllowedOrigin(req)) {
2051
- res.writeHead(403, { 'Content-Type': 'application/json' });
2052
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2053
- return;
2054
- }
2055
- const id = decodeURIComponent(path.slice('/webhooks/deliveries/'.length, -'/retry'.length));
2056
- res.writeHead(400, { 'Content-Type': 'application/json' });
2057
- res.end(JSON.stringify({
2058
- ok: false,
2059
- id,
2060
- error: 'payload not retained — use the upstream Redeliver action (GitHub webhooks UI → Recent Deliveries → Redeliver)',
2061
- }));
2062
- return;
2063
- }
2064
-
2065
- if (req.method === 'GET' && path === '/cli-check') {
2066
- let whisperInstalled = false;
2067
- let voiceMeta = null;
2068
- let binaryReady = false;
2069
- let modelReady = false;
2070
- let ffmpegReady = false;
2071
- try {
2072
- const { resolveVoiceRuntime } = await import('../src/channels/lib/voice-runtime-fetcher.mjs');
2073
- const runtime = resolveVoiceRuntime(DATA_DIR);
2074
- const whisperCmd = runtime?.whisperCmd || '';
2075
- const modelPath = runtime?.modelPath || '';
2076
- const ffmpegPath = runtime?.ffmpegPath || '';
2077
- binaryReady = !!runtime?.binary;
2078
- modelReady = !!runtime?.model;
2079
- ffmpegReady = !!runtime?.ffmpeg;
2080
- if (whisperCmd || modelPath || ffmpegPath || runtime?.kind) {
2081
- voiceMeta = {
2082
- kind: runtime?.kind || '',
2083
- label: runtime?.label || '',
2084
- commandName: whisperCmd ? basename(whisperCmd) : '',
2085
- commandPath: whisperCmd || '',
2086
- modelName: modelPath ? basename(modelPath) : '',
2087
- modelPath: modelPath || '',
2088
- ffmpegName: ffmpegPath ? basename(ffmpegPath) : '',
2089
- ffmpegPath: ffmpegPath || '',
2090
- };
2091
- }
2092
- } catch {}
2093
- whisperInstalled = binaryReady && modelReady && ffmpegReady;
2094
- const ngrok = await checkCli('ngrok');
2095
- const cliPayload = {
2096
- whisper: { installed: whisperInstalled, binary: binaryReady, model: modelReady, ffmpeg: ffmpegReady },
2097
- ngrok,
2098
- };
2099
- if (voiceMeta) cliPayload.voice = voiceMeta;
2100
- res.writeHead(200, { 'Content-Type': 'application/json' });
2101
- res.end(JSON.stringify(cliPayload));
2102
- return;
2103
- }
2104
-
2105
- // ============================================================
2106
- // AGENT MODULE ROUTES
2107
- // ============================================================
2108
-
2109
- if (req.method === 'GET' && path === '/agent/config') {
2110
- const config = readAgentConfig();
2111
- const auth = await detectAuth(config);
2112
- res.writeHead(200, { 'Content-Type': 'application/json' });
2113
- res.end(JSON.stringify({ config, auth }));
2114
- return;
2115
- }
2116
-
2117
- if (req.method === 'POST' && path === '/agent/config') {
2118
- if (!isAllowedOrigin(req)) {
2119
- res.writeHead(403, { 'Content-Type': 'application/json' });
2120
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2121
- return;
2122
- }
2123
- const data = await readBody(req);
2124
- const secrets = {};
2125
- try {
2126
- // RMW inside the config lock (see /config) so concurrent agent saves
2127
- // serialize through updateSection instead of racing a read→merge→write.
2128
- let merged;
2129
- updateSection('agent', (current) => {
2130
- merged = mergeAgentConfig(current, data, secrets);
2131
- return merged;
2132
- });
2133
- if (data?.providers && typeof data.providers === 'object') dropRuntimeModelCaches();
2134
- console.log(` Config saved: agent secrets=${JSON.stringify(secrets)}`);
2135
- res.writeHead(200, { 'Content-Type': 'application/json' });
2136
- res.end(JSON.stringify({ ok: true, secrets, secretStatus: fullSecretStatus() }));
2137
- } catch (e) {
2138
- process.stderr.write('[setup] /agent/config failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2139
- res.writeHead(500, { 'Content-Type': 'application/json' });
2140
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e), secrets }));
2141
- }
2142
- return;
2143
- }
2144
-
2145
- // Grok CLI OAuth login ("Grok Build"). An existing `grok` CLI login under
2146
- // ~/.grok/auth.json is auto-detected; this route is for signing in directly
2147
- // from Setup when no CLI login exists. Blocking: waits (up to 5 min) for the
2148
- // loopback callback on 127.0.0.1:56121, then persists to the own token store.
2149
- if (req.method === 'POST' && path === '/agent/grok-oauth/login') {
2150
- if (!isAllowedOrigin(req)) {
2151
- res.writeHead(403, { 'Content-Type': 'application/json' });
2152
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2153
- return;
2154
- }
2155
- try {
2156
- const tokens = await loginGrokOAuth();
2157
- if (!tokens?.access_token) {
2158
- res.writeHead(200, { 'Content-Type': 'application/json' });
2159
- res.end(JSON.stringify({ ok: false, error: 'login cancelled or timed out' }));
2160
- return;
2161
- }
2162
- dropRuntimeModelCaches();
2163
- res.writeHead(200, { 'Content-Type': 'application/json' });
2164
- res.end(JSON.stringify({ ok: true }));
2165
- } catch (e) {
2166
- process.stderr.write('[setup] /agent/grok-oauth/login failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2167
- res.writeHead(500, { 'Content-Type': 'application/json' });
2168
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
2169
- }
2170
- return;
2171
- }
2172
-
2173
- if (req.method === 'POST' && path === '/agent/openai-oauth/login') {
2174
- if (!isAllowedOrigin(req)) {
2175
- res.writeHead(403, { 'Content-Type': 'application/json' });
2176
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2177
- return;
2178
- }
2179
- try {
2180
- const tokens = await loginOpenAIOAuth();
2181
- if (!tokens?.access_token) {
2182
- res.writeHead(200, { 'Content-Type': 'application/json' });
2183
- res.end(JSON.stringify({ ok: false, error: 'login cancelled or timed out' }));
2184
- return;
2185
- }
2186
- dropRuntimeModelCaches();
2187
- res.writeHead(200, { 'Content-Type': 'application/json' });
2188
- res.end(JSON.stringify({ ok: true }));
2189
- } catch (e) {
2190
- process.stderr.write('[setup] /agent/openai-oauth/login failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2191
- res.writeHead(500, { 'Content-Type': 'application/json' });
2192
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
2193
- }
2194
- return;
2195
- }
2196
-
2197
- if (req.method === 'POST' && path === '/agent/anthropic-oauth/login') {
2198
- if (!isAllowedOrigin(req)) {
2199
- res.writeHead(403, { 'Content-Type': 'application/json' });
2200
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2201
- return;
2202
- }
2203
- try {
2204
- const tokens = await loginAnthropicOAuth();
2205
- if (!tokens?.accessToken) {
2206
- res.writeHead(200, { 'Content-Type': 'application/json' });
2207
- res.end(JSON.stringify({ ok: false, error: 'login cancelled or timed out' }));
2208
- return;
2209
- }
2210
- dropRuntimeModelCaches();
2211
- res.writeHead(200, { 'Content-Type': 'application/json' });
2212
- res.end(JSON.stringify({ ok: true }));
2213
- } catch (e) {
2214
- process.stderr.write('[setup] /agent/anthropic-oauth/login failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2215
- res.writeHead(500, { 'Content-Type': 'application/json' });
2216
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
2217
- }
2218
- return;
2219
- }
2220
-
2221
- if (req.method === 'GET' && path === '/agent/presets') {
2222
- res.writeHead(200, { 'Content-Type': 'application/json' });
2223
- res.end(JSON.stringify({ presets: readAgentPresets() }));
2224
- return;
2225
- }
2226
-
2227
- if (req.method === 'POST' && path === '/agent/presets') {
2228
- if (!isAllowedOrigin(req)) {
2229
- res.writeHead(403, { 'Content-Type': 'application/json' });
2230
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2231
- return;
2232
- }
2233
- const data = await readBody(req);
2234
- let preset;
2235
- try { preset = normalizePreset(data); }
2236
- catch (err) {
2237
- res.writeHead(400, { 'Content-Type': 'application/json' });
2238
- res.end(JSON.stringify({ ok: false, error: err.message }));
2239
- return;
2240
- }
2241
- const list = readAgentPresets();
2242
- const idx = list.findIndex(p => p.id === preset.id);
2243
- if (idx >= 0) list[idx] = preset; else list.push(preset);
2244
- writeAgentPresets(list);
2245
- console.log(` Agent preset saved: ${preset.id}`);
2246
- res.writeHead(200, { 'Content-Type': 'application/json' });
2247
- res.end(JSON.stringify({ ok: true, preset }));
2248
- return;
2249
- }
2250
-
2251
- if (req.method === 'DELETE' && path === '/agent/presets') {
2252
- const id = url.searchParams.get('id');
2253
- if (!id) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'id required' })); return; }
2254
- const list = readAgentPresets().filter(p => p.id !== id);
2255
- writeAgentPresets(list);
2256
- console.log(` Agent preset deleted: ${id}`);
2257
- res.writeHead(200, { 'Content-Type': 'application/json' });
2258
- res.end(JSON.stringify({ ok: true }));
2259
- return;
2260
- }
2261
-
2262
- // -- Agent maintenance presets --
2263
- if (req.method === 'GET' && path === '/agent/maintenance') {
2264
- const cfg = readAgentConfig();
2265
- const rawMaint = cfg.maintenance || {};
2266
- // Strip legacy keys that no longer belong in maintenance
2267
- // (classification/recap were retired with the cycle1 split;
2268
- // scheduler/webhook keep their model per-entry; the three memory-cycle
2269
- // MODEL presets collapsed into a single `memory` key — fold the first
2270
- // present legacy cycle value into `memory` before stripping).
2271
- // Persist back when the stored config carried any of them so the Setup
2272
- // panel and the runtime resolver stop having to dual-match name vs id.
2273
- const allowedKeys = new Set([...Object.keys(DEFAULT_MAINTENANCE), ...MAINTENANCE_SLOTS]);
2274
- const cleanMaint = {};
2275
- let changed = false;
2276
- const legacyCycleKeys = ['cycle1', 'cycle2', 'cycle3'];
2277
- if (!('memory' in rawMaint) && legacyCycleKeys.some(k => k in rawMaint)) {
2278
- cleanMaint.memory = rawMaint.cycle1 ?? rawMaint.cycle2 ?? rawMaint.cycle3 ?? DEFAULT_MAINTENANCE.memory;
2279
- changed = true;
2280
- }
2281
- for (const [k, v] of Object.entries(rawMaint)) {
2282
- if (allowedKeys.has(k)) cleanMaint[k] = v;
2283
- else changed = true;
2284
- }
2285
- if (changed) {
2286
- cfg.maintenance = cleanMaint;
2287
- try { writeAgentConfig(cfg); }
2288
- catch (e) { process.stderr.write(`[setup] maintenance legacy-key cleanup write failed: ${e.message}\n`); }
2289
- }
2290
- const merged = { ...DEFAULT_MAINTENANCE, ...cleanMaint };
2291
- res.writeHead(200, { 'Content-Type': 'application/json' });
2292
- res.end(JSON.stringify({ maintenance: merged, defaults: { ...DEFAULT_MAINTENANCE } }));
2293
- return;
2294
- }
2295
-
2296
- if (req.method === 'POST' && path === '/agent/maintenance') {
2297
- if (!isAllowedOrigin(req)) {
2298
- res.writeHead(403, { 'Content-Type': 'application/json' });
2299
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2300
- return;
2301
- }
2302
- const data = await readBody(req);
2303
- const cfg = readAgentConfig();
2304
- const validIds = new Set([
2305
- ...(cfg.presets || []).map(p => p.id),
2306
- ...DEFAULT_PRESETS.map(p => p.id),
2307
- ]);
2308
- const allowedKeys = new Set([...Object.keys(DEFAULT_MAINTENANCE), ...MAINTENANCE_SLOTS]);
2309
- const unknownKeys = Object.keys(data).filter(k => !allowedKeys.has(k));
2310
- if (unknownKeys.length) {
2311
- res.writeHead(400, { 'Content-Type': 'application/json' });
2312
- res.end(JSON.stringify({ ok: false, error: `Unknown maintenance task(s): ${unknownKeys.join(', ')} (per-entry model required for scheduler/webhook)` }));
2313
- return;
2314
- }
2315
- const invalid = Object.entries(data)
2316
- .filter(([k, v]) => v && !validIds.has(v))
2317
- .map(([k, v]) => `${k}: ${v}`);
2318
- if (invalid.length) {
2319
- res.writeHead(400, { 'Content-Type': 'application/json' });
2320
- res.end(JSON.stringify({ ok: false, error: `Unknown preset(s): ${invalid.join(', ')}` }));
2321
- return;
2322
- }
2323
- const nextMaint = { ...(cfg.maintenance || {}) };
2324
- // Migrate any stored legacy cycle1/2/3 model keys into `memory` (one-time
2325
- // schema collapse) so the persisted config never carries them forward.
2326
- {
2327
- const legacyCycleKeys = ['cycle1', 'cycle2', 'cycle3'];
2328
- if (!('memory' in nextMaint) && legacyCycleKeys.some(k => k in nextMaint)) {
2329
- nextMaint.memory = nextMaint.cycle1 ?? nextMaint.cycle2 ?? nextMaint.cycle3 ?? DEFAULT_MAINTENANCE.memory;
2330
- }
2331
- for (const k of legacyCycleKeys) delete nextMaint[k];
2332
- }
2333
- for (const [k, v] of Object.entries(data)) {
2334
- if (v == null || v === '') delete nextMaint[k]; // inherit → remove override
2335
- else nextMaint[k] = v;
2336
- }
2337
- cfg.maintenance = nextMaint;
2338
- writeAgentConfig(cfg);
2339
- console.log(' Maintenance presets saved');
2340
- res.writeHead(200, { 'Content-Type': 'application/json' });
2341
- res.end(JSON.stringify({ ok: true }));
2342
- return;
2343
- }
2344
-
2345
- if (req.method === 'GET' && path === '/agent/models') {
2346
- const provider = url.searchParams.get('provider');
2347
- if (!provider) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'provider required' })); return; }
2348
- const cfg = readAgentConfig();
2349
- const models = await listProviderModels(provider, cfg);
2350
- if (provider === 'openai-oauth' && (!Array.isArray(models) || models.length === 0) && hasOpenAIOAuthCredentials()) {
2351
- const detail = getOpenAIOAuthModelCatalogError();
2352
- const error = detail
2353
- ? `OpenAI OAuth model catalog unavailable: ${detail}`
2354
- : 'OpenAI OAuth model catalog unavailable. Run codex login, then reopen this setup page.';
2355
- res.writeHead(200, { 'Content-Type': 'application/json' });
2356
- res.end(JSON.stringify({ ok: false, provider, models: [], error }));
2357
- return;
2358
- }
2359
- res.writeHead(200, { 'Content-Type': 'application/json' });
2360
- res.end(JSON.stringify({ ok: true, provider, models }));
2361
- return;
2362
- }
2363
-
2364
- if (req.method === 'POST' && path === '/agent/validate') {
2365
- if (!isAllowedOrigin(req)) {
2366
- res.writeHead(403, { 'Content-Type': 'application/json' });
2367
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2368
- return;
2369
- }
2370
- const data = await readBody(req);
2371
- const validation = {};
2372
- const checks = [];
2373
- for (const [id, key] of Object.entries(data.keys || {})) {
2374
- if (key) checks.push(validateAgentKey(id, key).then(r => { validation[id] = r; }));
2375
- }
2376
- await Promise.all(checks);
2377
- res.writeHead(200, { 'Content-Type': 'application/json' });
2378
- res.end(JSON.stringify({ ok: true, validation }));
2379
- return;
2380
- }
2381
-
2382
- // ============================================================
2383
- // MEMORY MODULE ROUTES
2384
- // ============================================================
2385
-
2386
- if (req.method === 'GET' && path === '/memory/config') {
2387
- const config = readMemoryConfig();
2388
- res.writeHead(200, { 'Content-Type': 'application/json' });
2389
- res.end(JSON.stringify(config));
2390
- return;
2391
- }
2392
-
2393
- if (req.method === 'POST' && path === '/memory/config') {
2394
- if (!isAllowedOrigin(req)) {
2395
- res.writeHead(403, { 'Content-Type': 'application/json' });
2396
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2397
- return;
2398
- }
2399
- const data = await readBody(req);
2400
- const secrets = {};
2401
- try {
2402
- // RMW inside the config lock (see /config) so concurrent memory saves
2403
- // serialize through updateSection instead of racing a read→merge→write.
2404
- let merged;
2405
- updateSection('memory', (current) => {
2406
- merged = mergeMemoryConfig(current, data, secrets);
2407
- return merged;
2408
- });
2409
- console.log(` Config saved: memory secrets=${JSON.stringify(secrets)}`);
2410
- res.writeHead(200, { 'Content-Type': 'application/json' });
2411
- res.end(JSON.stringify({ ok: true, secrets, secretStatus: fullSecretStatus() }));
2412
- } catch (e) {
2413
- process.stderr.write('[setup] /memory/config failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2414
- res.writeHead(500, { 'Content-Type': 'application/json' });
2415
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e), secrets }));
2416
- }
2417
- return;
2418
- }
2419
-
2420
- if (req.method === 'GET' && path === '/memory/auth') {
2421
- const cfg = readMemoryConfig();
2422
- const result = await detectAuth(cfg);
2423
- res.writeHead(200, { 'Content-Type': 'application/json' });
2424
- res.end(JSON.stringify(result));
2425
- return;
2426
- }
2427
-
2428
- if (req.method === 'GET' && path === '/memory/presets') {
2429
- res.writeHead(200, { 'Content-Type': 'application/json' });
2430
- res.end(JSON.stringify({ presets: readMemoryPresets() }));
2431
- return;
2432
- }
2433
-
2434
- if (req.method === 'POST' && path === '/memory/presets') {
2435
- if (!isAllowedOrigin(req)) {
2436
- res.writeHead(403, { 'Content-Type': 'application/json' });
2437
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2438
- return;
2439
- }
2440
- const data = await readBody(req);
2441
- let preset;
2442
- try { preset = normalizePreset(data); }
2443
- catch (err) {
2444
- res.writeHead(400, { 'Content-Type': 'application/json' });
2445
- res.end(JSON.stringify({ ok: false, error: err.message }));
2446
- return;
2447
- }
2448
- const list = readMemoryPresets();
2449
- const idx = list.findIndex(p => p.id === preset.id);
2450
- if (idx >= 0) list[idx] = preset; else list.push(preset);
2451
- writeMemoryPresets(list);
2452
- console.log(` Memory preset saved: ${preset.id}`);
2453
- res.writeHead(200, { 'Content-Type': 'application/json' });
2454
- res.end(JSON.stringify({ ok: true, preset }));
2455
- return;
2456
- }
2457
-
2458
- if (req.method === 'PUT' && path === '/memory/presets') {
2459
- const data = await readBody(req);
2460
- if (!Array.isArray(data.presets)) {
2461
- res.writeHead(400, { 'Content-Type': 'application/json' });
2462
- res.end(JSON.stringify({ ok: false, error: 'presets array required' }));
2463
- return;
2464
- }
2465
- const normalized = data.presets.map(p => normalizePreset(p));
2466
- writeMemoryPresets(normalized);
2467
- console.log(` Memory presets reordered: ${normalized.length} items`);
2468
- res.writeHead(200, { 'Content-Type': 'application/json' });
2469
- res.end(JSON.stringify({ ok: true }));
2470
- return;
2471
- }
2472
-
2473
- if (req.method === 'DELETE' && path === '/memory/presets') {
2474
- const id = url.searchParams.get('id');
2475
- if (!id) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'id required' })); return; }
2476
- const list = readMemoryPresets().filter(p => p.id !== id);
2477
- writeMemoryPresets(list);
2478
- console.log(` Memory preset deleted: ${id}`);
2479
- res.writeHead(200, { 'Content-Type': 'application/json' });
2480
- res.end(JSON.stringify({ ok: true }));
2481
- return;
2482
- }
2483
-
2484
- if (req.method === 'GET' && path === '/memory/models') {
2485
- const provider = url.searchParams.get('provider');
2486
- if (!provider) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'provider required' })); return; }
2487
- const cfg = readMemoryConfig();
2488
- const models = await listProviderModels(provider, cfg);
2489
- res.writeHead(200, { 'Content-Type': 'application/json' });
2490
- res.end(JSON.stringify({ ok: true, provider, models }));
2491
- return;
2492
- }
2493
- const MEMORY_FILE_WHITELIST = ['user.md', 'bot.md'];
2494
- const HISTORY_DIR = join(DATA_DIR, 'history');
2495
-
2496
- if (req.method === 'GET' && path === '/memory/files') {
2497
- const result = {};
2498
- for (const name of MEMORY_FILE_WHITELIST) {
2499
- const filePath = join(HISTORY_DIR, name);
2500
- try { result[name] = readFileSync(filePath, 'utf8'); }
2501
- catch { result[name] = ''; }
2502
- }
2503
- res.writeHead(200, { 'Content-Type': 'application/json' });
2504
- res.end(JSON.stringify(result));
2505
- return;
2506
- }
2507
-
2508
- if (req.method === 'POST' && path === '/memory/files') {
2509
- const data = await readBody(req);
2510
- const keys = Object.keys(data);
2511
- for (const key of keys) {
2512
- if (!MEMORY_FILE_WHITELIST.includes(key)) {
2513
- res.writeHead(400, { 'Content-Type': 'application/json' });
2514
- res.end(JSON.stringify({ ok: false, error: `disallowed file name: ${key}` }));
2515
- return;
2516
- }
2517
- }
2518
- mkdirSync(HISTORY_DIR, { recursive: true });
2519
- for (const name of MEMORY_FILE_WHITELIST) {
2520
- if (!(name in data)) continue;
2521
- const filePath = join(HISTORY_DIR, name);
2522
- const tmp = filePath + '.tmp';
2523
- writeFileSync(tmp, String(data[name]), 'utf8');
2524
- renameSync(tmp, filePath);
2525
- }
2526
- res.writeHead(200, { 'Content-Type': 'application/json' });
2527
- res.end(JSON.stringify({ ok: true }));
2528
- return;
2529
- }
2530
-
2531
- {
2532
- const fileNameMatch = path.match(/^\/memory\/file\/([^/]+)$/);
2533
- if (req.method === 'GET' && fileNameMatch) {
2534
- const name = fileNameMatch[1];
2535
- if (!MEMORY_FILE_WHITELIST.includes(name)) {
2536
- res.writeHead(400, { 'Content-Type': 'application/json' });
2537
- res.end(JSON.stringify({ ok: false, error: `disallowed file name: ${name}` }));
2538
- return;
2539
- }
2540
- const filePath = join(HISTORY_DIR, name);
2541
- if (!existsSync(filePath)) {
2542
- res.writeHead(404, { 'Content-Type': 'application/json' });
2543
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
2544
- return;
2545
- }
2546
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
2547
- res.end(readFileSync(filePath, 'utf8'));
2548
- return;
2549
- }
2550
- }
2551
-
2552
- if (req.method === 'GET' && path === '/api/memory/entries/active') {
2553
- try {
2554
- const r = await memoryServiceCall('GET', '/admin/entries/active', null, 30000);
2555
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2556
- res.end(JSON.stringify(r.body));
2557
- } catch (e) {
2558
- res.writeHead(500, { 'Content-Type': 'application/json' });
2559
- res.end(JSON.stringify({ ok: false, error: e.message }));
2560
- }
2561
- return;
2562
- }
2563
-
2564
- if (req.method === 'GET' && path === '/api/memory/core') {
2565
- try {
2566
- const r = await memoryServiceCall('GET', '/admin/core/entries', null, 30000);
2567
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2568
- res.end(JSON.stringify(r.body));
2569
- } catch (e) {
2570
- res.writeHead(500, { 'Content-Type': 'application/json' });
2571
- res.end(JSON.stringify({ ok: false, error: e.message }));
2572
- }
2573
- return;
2574
- }
2575
-
2576
- if (req.method === 'POST' && path === '/api/memory/core') {
2577
- if (!isAllowedOrigin(req)) {
2578
- res.writeHead(403, { 'Content-Type': 'application/json' });
2579
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2580
- return;
2581
- }
2582
- const data = await readBody(req);
2583
- try {
2584
- const r = await memoryServiceCall('POST', '/admin/core/entries', {
2585
- element: data.element,
2586
- summary: data.summary,
2587
- category: data.category,
2588
- project_id: data.project_id,
2589
- }, 30000);
2590
- if (r.body?.ok) console.log(` Core memory saved: ${r.body.item?.id ?? '?'}`);
2591
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2592
- res.end(JSON.stringify(r.body));
2593
- } catch (e) {
2594
- res.writeHead(500, { 'Content-Type': 'application/json' });
2595
- res.end(JSON.stringify({ ok: false, error: e.message }));
2596
- }
2597
- return;
2598
- }
2599
-
2600
- {
2601
- const coreDeleteMatch = req.method === 'POST' && path.match(/^\/api\/memory\/core\/(\d+)\/delete$/);
2602
- if (coreDeleteMatch && !isAllowedOrigin(req)) {
2603
- res.writeHead(403, { 'Content-Type': 'application/json' });
2604
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2605
- return;
2606
- }
2607
- if (coreDeleteMatch) {
2608
- const id = Number(coreDeleteMatch[1]);
2609
- try {
2610
- const r = await memoryServiceCall('POST', '/admin/core/entries/delete', { id }, 30000);
2611
- console.log(` Core memory #${id} deleted`);
2612
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2613
- res.end(JSON.stringify(r.body));
2614
- } catch (e) {
2615
- res.writeHead(500, { 'Content-Type': 'application/json' });
2616
- res.end(JSON.stringify({ ok: false, error: e.message }));
2617
- }
2618
- return;
2619
- }
2620
- }
2621
-
2622
- {
2623
- const statusMatch = req.method === 'POST' && path.match(/^\/api\/memory\/entries\/(\d+)\/status$/);
2624
- if (statusMatch && !isAllowedOrigin(req)) {
2625
- res.writeHead(403, { 'Content-Type': 'application/json' });
2626
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2627
- return;
2628
- }
2629
- if (statusMatch) {
2630
- const id = Number(statusMatch[1]);
2631
- const data = await readBody(req);
2632
- const VALID = ['pending', 'active', 'archived'];
2633
- const status = String(data.status ?? '').trim().toLowerCase();
2634
- if (!Number.isInteger(id) || id <= 0 || !VALID.includes(status)) {
2635
- res.writeHead(400, { 'Content-Type': 'application/json' });
2636
- res.end(JSON.stringify({ ok: false, error: 'valid id and status required' }));
2637
- return;
2638
- }
2639
- try {
2640
- const r = await memoryServiceCall('POST', '/admin/entries/status', { id, status }, 30000);
2641
- console.log(` Entry #${id} → ${status} (changes=${r.body?.changes ?? '?'})`);
2642
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2643
- res.end(JSON.stringify(r.body));
2644
- } catch (e) {
2645
- res.writeHead(500, { 'Content-Type': 'application/json' });
2646
- res.end(JSON.stringify({ ok: false, error: e.message }));
2647
- }
2648
- return;
2649
- }
2650
- }
2651
-
2652
- if (req.method === 'POST' && path === '/api/memory/entries') {
2653
- if (!isAllowedOrigin(req)) {
2654
- res.writeHead(403, { 'Content-Type': 'application/json' });
2655
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2656
- return;
2657
- }
2658
- const data = await readBody(req);
2659
- try {
2660
- const r = await memoryServiceCall('POST', '/admin/entries/add', {
2661
- element: data.element,
2662
- summary: data.summary,
2663
- category: data.category,
2664
- }, 30000);
2665
- if (r.body?.ok) console.log(` Remembered entry #${r.body.id}: ${r.body.text || ''}`);
2666
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2667
- res.end(JSON.stringify(r.body));
2668
- } catch (e) {
2669
- res.writeHead(500, { 'Content-Type': 'application/json' });
2670
- res.end(JSON.stringify({ ok: false, error: e.message }));
2671
- }
2672
- return;
2673
- }
2674
-
2675
- if (req.method === 'POST' && path === '/memory/backfill') {
2676
- if (!isAllowedOrigin(req)) {
2677
- res.writeHead(403, { 'Content-Type': 'application/json' });
2678
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2679
- return;
2680
- }
2681
- const data = await readBody(req);
2682
- const requestedWindow = data.window || '7d';
2683
- try {
2684
- console.log(`[backfill] start window=${requestedWindow}`);
2685
- // Backfill iterates transcripts and runs cycle1/cycle2 — long, no fixed
2686
- // upper bound. Pass a generous timeout (1h) and let memory-service's
2687
- // _cycle1InFlight guard serialise overlapping requests.
2688
- const r = await memoryServiceCall('POST', '/admin/backfill', {
2689
- window: requestedWindow,
2690
- scope: 'all',
2691
- }, 3_600_000);
2692
- console.log(`[backfill] ${r.body?.text || JSON.stringify(r.body)}`);
2693
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2694
- res.end(JSON.stringify(r.body));
2695
- } catch (err) {
2696
- console.error(`[backfill] failed: ${err.message}`);
2697
- res.writeHead(500, { 'Content-Type': 'application/json' });
2698
- res.end(JSON.stringify({ ok: false, error: err.message }));
2699
- }
2700
- return;
2701
- }
2702
-
2703
- if (req.method === 'POST' && path === '/memory/delete') {
2704
- if (!isAllowedOrigin(req)) {
2705
- res.writeHead(403, { 'Content-Type': 'application/json' });
2706
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2707
- return;
2708
- }
2709
- const data = await readBody(req);
2710
- try {
2711
- const r = await memoryServiceCall('POST', '/admin/purge', {
2712
- confirm: data?.confirm,
2713
- }, 60000);
2714
- res.writeHead(r.statusCode, { 'Content-Type': 'application/json' });
2715
- res.end(JSON.stringify(r.body));
2716
- } catch (err) {
2717
- console.error(`[memory delete] failed: ${err.message}`);
2718
- res.writeHead(500, { 'Content-Type': 'application/json' });
2719
- res.end(JSON.stringify({ ok: false, error: err.message }));
2720
- }
2721
- return;
2722
- }
2723
-
2724
- if (req.method === 'POST' && path === '/memory/validate') {
2725
- if (!isAllowedOrigin(req)) {
2726
- res.writeHead(403, { 'Content-Type': 'application/json' });
2727
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2728
- return;
2729
- }
2730
- const data = await readBody(req);
2731
- const validation = {};
2732
- const checks = [];
2733
- for (const [id, key] of Object.entries(data.keys || {})) {
2734
- if (key) checks.push(validateAgentKey(id, key).then(r => { validation[id] = r; }));
2735
- }
2736
- await Promise.all(checks);
2737
- res.writeHead(200, { 'Content-Type': 'application/json' });
2738
- res.end(JSON.stringify({ ok: true, validation }));
2739
- return;
2740
- }
2741
-
2742
- // ============================================================
2743
- // SEARCH MODULE ROUTES
2744
- // ============================================================
2745
-
2746
- if (req.method === 'GET' && path === '/search/config') {
2747
- const config = readSearchConfig();
2748
- if (config.rawSearch && config.rawSearch.credentials) {
2749
- for (const [id, cred] of Object.entries(config.rawSearch.credentials)) {
2750
- const secret = getSearchApiKey(id);
2751
- if (secret) {
2752
- config.rawSearch.credentials[id] = { ...(cred || {}), apiKey: secret };
2753
- }
2754
- }
2755
- }
2756
- config.availableProviders = computeAvailableProviders();
2757
- res.writeHead(200, { 'Content-Type': 'application/json' });
2758
- res.end(JSON.stringify(config));
2759
- return;
2760
- }
2761
-
2762
- if (req.method === 'POST' && path === '/search/config') {
2763
- if (!isAllowedOrigin(req)) {
2764
- res.writeHead(403, { 'Content-Type': 'application/json' });
2765
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2766
- return;
2767
- }
2768
- const data = await readBody(req);
2769
- const secrets = {};
2770
- try {
2771
- // RMW inside the config lock (see /config) so concurrent search saves
2772
- // serialize through updateSection instead of racing a read→merge→write.
2773
- let merged;
2774
- updateSection('search', (current) => {
2775
- merged = mergeSearchConfig(current, data, secrets);
2776
- return merged;
2777
- });
2778
- console.log(` Config saved: search secrets=${JSON.stringify(secrets)}`);
2779
- res.writeHead(200, { 'Content-Type': 'application/json' });
2780
- res.end(JSON.stringify({ ok: true, secrets, secretStatus: fullSecretStatus() }));
2781
- } catch (e) {
2782
- process.stderr.write('[setup] /search/config failed: ' + (e?.stack || e?.message || String(e)) + '\n');
2783
- res.writeHead(500, { 'Content-Type': 'application/json' });
2784
- res.end(JSON.stringify({ ok: false, error: e?.message || String(e), secrets }));
2785
- }
2786
- return;
2787
- }
2788
-
2789
- if (req.method === 'POST' && path === '/search/validate') {
2790
- if (!isAllowedOrigin(req)) {
2791
- res.writeHead(403, { 'Content-Type': 'application/json' });
2792
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2793
- return;
2794
- }
2795
- const data = await readBody(req);
2796
- const validation = {};
2797
- const checks = [];
2798
- for (const [id, val] of Object.entries(data.searchProviders || {})) {
2799
- const key = typeof val === 'object' ? val.key : val;
2800
- if (key) checks.push(validateSearchKey(id, key).then(r => { validation[id] = r; }));
2801
- }
2802
- for (const [id, val] of Object.entries(data.aiProviders || {})) {
2803
- if (val && val !== 'cli') checks.push(validateSearchKey(id, val).then(r => { validation[id] = r; }));
2804
- }
2805
- await Promise.all(checks);
2806
- res.writeHead(200, { 'Content-Type': 'application/json' });
2807
- res.end(JSON.stringify({ ok: true, validation }));
2808
- return;
2809
- }
2810
-
2811
- if (req.method === 'GET' && path === '/search/cli-check') {
2812
- // Previously three serial `execSync(`${cmd} --version`)` calls — each
2813
- // wrapped by Node in cmd.exe which flashed a conhost window even with
2814
- // windowsHide:true, and each blocking the server thread for up to 5s
2815
- // (15s worst case if all three CLIs are missing). Switch to non-shell
2816
- // existence-only checks via `where.exe` / `which`, in parallel: no
2817
- // cmd.exe wrapper → no flash, and the request returns in one round-trip.
2818
- const check = (cmd) => new Promise(resolve => {
2819
- const tool = isWin ? 'where.exe' : 'which';
2820
- const child = spawn(tool, [cmd], {
2821
- windowsHide: true,
2822
- stdio: 'ignore',
2823
- shell: false,
2824
- });
2825
- child.once('error', () => resolve(false));
2826
- child.once('close', code => resolve(code === 0));
2827
- });
2828
- const [codex, claude, gemini] = await Promise.all([
2829
- check('codex'), check('claude'), check('gemini'),
2830
- ]);
2831
- res.writeHead(200, { 'Content-Type': 'application/json' });
2832
- res.end(JSON.stringify({ codex, claude, gemini }));
2833
- return;
2834
- }
2835
-
2836
- // ============================================================
2837
- // CHANNELS MODULE ROUTES (continued)
2838
- // ============================================================
2839
-
2840
- if (req.method === 'POST' && path === '/install') {
2841
- if (!isAllowedOrigin(req)) {
2842
- res.writeHead(403, { 'Content-Type': 'application/json' });
2843
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2844
- return;
2845
- }
2846
- const data = await readBody(req);
2847
- const tool = data.tool;
2848
- if (!tool || !['ngrok'].includes(tool)) {
2849
- res.writeHead(400, { 'Content-Type': 'application/json' });
2850
- res.end(JSON.stringify({ ok: false, error: 'Invalid tool' }));
2851
- return;
2852
- }
2853
- try {
2854
- const { stdout } = await new Promise((resolve, reject) => {
2855
- exec('npm install -g ngrok', { timeout: 120000, windowsHide: true }, (err, stdout, stderr) => {
2856
- if (err) reject(err);
2857
- else resolve({ stdout, stderr });
2858
- });
2859
- });
2860
- console.log(` Installed ${tool}`);
2861
- res.writeHead(200, { 'Content-Type': 'application/json' });
2862
- res.end(JSON.stringify({ ok: true, tool, output: stdout.trim() }));
2863
- } catch (e) {
2864
- console.log(` Install ${tool} failed: ${e.message}`);
2865
- res.writeHead(200, { 'Content-Type': 'application/json' });
2866
- res.end(JSON.stringify({ ok: false, tool, error: e.message }));
2867
- }
2868
- return;
2869
- }
2870
-
2871
- // POST /install/voice-runtime — single-shot voice install: binary + model.
2872
- // Sequentially fetches the platform-matched whisper.cpp runtime and the
2873
- // large-v3-turbo model from the managed manifest. Both are idempotent: if
2874
- // the cached binary exists and the model's sha256 matches, the call returns
2875
- // without re-downloading. The endpoint completes only after both are ready.
2876
- if (req.method === 'POST' && path === '/install/voice-runtime') {
2877
- if (!isAllowedOrigin(req)) {
2878
- res.writeHead(403, { 'Content-Type': 'application/json' });
2879
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2880
- return;
2881
- }
2882
- res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'Cache-Control': 'no-cache' });
2883
- const send = (obj) => { try { res.write(JSON.stringify(obj) + '\n'); } catch {} };
2884
- try {
2885
- const { ensureWhisperRuntime, ensureWhisperModel, ensureFfmpegRuntime } = await import(new URL('../src/channels/lib/voice-runtime-fetcher.mjs', import.meta.url).href);
2886
- const onProgress = (p) => send({ type: 'progress', ...p });
2887
- const runtime = await ensureWhisperRuntime(DATA_DIR, onProgress);
2888
- const model = await ensureWhisperModel(DATA_DIR, onProgress);
2889
- const ffmpeg = await ensureFfmpegRuntime(DATA_DIR, onProgress);
2890
- send({ type: 'done', ok: true, runtime, model, ffmpeg });
2891
- } catch (e) {
2892
- send({ type: 'error', ok: false, error: e?.message || String(e) });
2893
- } finally {
2894
- res.end();
2895
- }
2896
- return;
2897
- }
2898
-
2899
- if (req.method === 'GET' && path === '/general/config') {
2900
- const config = readConfig();
2901
- const pi = (config && typeof config.promptInjection === 'object' && config.promptInjection) || {};
2902
- res.writeHead(200, { 'Content-Type': 'application/json' });
2903
- res.end(JSON.stringify({
2904
- promptInjection: {
2905
- mode: pi.mode === 'hook' ? 'hook' : 'claude_md',
2906
- targetPath: typeof pi.targetPath === 'string' && pi.targetPath ? pi.targetPath : '~/.claude/CLAUDE.md',
2907
- },
2908
- }));
2909
- return;
2910
- }
2911
-
2912
- if (req.method === 'POST' && path === '/general/save') {
2913
- if (!isAllowedOrigin(req)) {
2914
- res.writeHead(403, { 'Content-Type': 'application/json' });
2915
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2916
- return;
2917
- }
2918
- const data = await readBody(req);
2919
- const existing = readConfig();
2920
- const next = { ...existing };
2921
- const prev = (existing && typeof existing.promptInjection === 'object' && existing.promptInjection) || {};
2922
- const merged = { ...prev };
2923
- if (data && (data.mode === 'hook' || data.mode === 'claude_md')) {
2924
- merged.mode = data.mode;
2925
- }
2926
- if (data && typeof data.targetPath === 'string' && data.targetPath.trim()) {
2927
- merged.targetPath = data.targetPath.trim();
2928
- }
2929
- if (!merged.mode) merged.mode = 'claude_md';
2930
- if (!merged.targetPath) merged.targetPath = '~/.claude/CLAUDE.md';
2931
- next.promptInjection = merged;
2932
- writeConfig(next);
2933
- console.log(' Config saved: general/promptInjection');
2934
- // Update CLAUDE.md managed block when mode is claude_md
2935
- let claudeMdResult = null;
2936
- if (merged.mode === 'claude_md') {
2937
- claudeMdResult = { ok: false, error: 'generateClaudeMdBlock not available' };
2938
- }
2939
- res.writeHead(200, { 'Content-Type': 'application/json' });
2940
- res.end(JSON.stringify({ ok: true, promptInjection: merged, claudeMd: claudeMdResult }));
2941
- return;
2942
- }
2943
-
2944
- // ============================================================
2945
- // MD LIBRARY ROUTES — Project MD + per-role MD (Common MD moved to
2946
- // plugin rules/agent.md and is no longer user-editable).
2947
- // ============================================================
2948
-
2949
- if (req.method === 'GET' && path === '/md/project') {
2950
- const indexPath = join(getPluginData(), 'project-md-index.json');
2951
- let registry = { paths: [] };
2952
- try { registry = JSON.parse(readFileSync(indexPath, 'utf8')); } catch {}
2953
- const items = [];
2954
- for (const cwd of registry.paths || []) {
2955
- let content = '';
2956
- try { content = readFileSync(join(cwd, 'PROJECT.md'), 'utf8'); } catch {}
2957
- items.push({ path: cwd, content });
2958
- }
2959
- res.writeHead(200, { 'Content-Type': 'application/json' });
2960
- res.end(JSON.stringify({ items }));
2961
- return;
2962
- }
2963
-
2964
- if (req.method === 'POST' && path === '/md/project') {
2965
- if (!isAllowedOrigin(req)) {
2966
- res.writeHead(403, { 'Content-Type': 'application/json' });
2967
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
2968
- return;
2969
- }
2970
- const body = await readBody(req);
2971
- const cwd = String(body?.path || '').trim();
2972
- const content = String(body?.content ?? '');
2973
- if (!cwd) {
2974
- res.writeHead(400, { 'Content-Type': 'application/json' });
2975
- res.end(JSON.stringify({ ok: false, error: 'path required' }));
2976
- return;
2977
- }
2978
- try {
2979
- mkdirSync(cwd, { recursive: true });
2980
- const _pTmp = join(cwd, 'PROJECT.md.tmp');
2981
- writeFileSync(_pTmp, content, 'utf8');
2982
- renameSync(_pTmp, join(cwd, 'PROJECT.md'));
2983
- } catch (err) {
2984
- res.writeHead(500, { 'Content-Type': 'application/json' });
2985
- res.end(JSON.stringify({ ok: false, error: `Cannot write PROJECT.md: ${err.message}` }));
2986
- return;
2987
- }
2988
- // Update registry
2989
- const indexPath = join(getPluginData(), 'project-md-index.json');
2990
- let registry = { paths: [] };
2991
- try { registry = JSON.parse(readFileSync(indexPath, 'utf8')); } catch {}
2992
- if (!registry.paths.includes(cwd)) registry.paths.push(cwd);
2993
- mkdirSync(dirname(indexPath), { recursive: true });
2994
- // Intentional full replace of project-md-index.json (paths registry only).
2995
- try {
2996
- const _regTmp = indexPath + '.tmp';
2997
- writeFileSync(_regTmp, JSON.stringify(registry, null, 2), 'utf8');
2998
- renameSync(_regTmp, indexPath);
2999
- } catch (err) {
3000
- res.writeHead(500, { 'Content-Type': 'application/json' });
3001
- res.end(JSON.stringify({ ok: false, error: `Cannot write registry: ${err.message}` }));
3002
- return;
3003
- }
3004
- console.log(` Config saved: project MD (${cwd})`);
3005
- res.writeHead(200, { 'Content-Type': 'application/json' });
3006
- res.end(JSON.stringify({ ok: true }));
3007
- return;
3008
- }
3009
-
3010
- if (req.method === 'DELETE' && path === '/md/project') {
3011
- const qs = new URL(req.url, 'http://x').searchParams;
3012
- const cwd = String(qs.get('path') || '').trim();
3013
- if (!cwd) {
3014
- res.writeHead(400, { 'Content-Type': 'application/json' });
3015
- res.end(JSON.stringify({ ok: false, error: 'path required' }));
3016
- return;
3017
- }
3018
- const indexPath = join(getPluginData(), 'project-md-index.json');
3019
- let registry = { paths: [] };
3020
- try { registry = JSON.parse(readFileSync(indexPath, 'utf8')); } catch {}
3021
- registry.paths = (registry.paths || []).filter(p => p !== cwd);
3022
- mkdirSync(dirname(indexPath), { recursive: true });
3023
- // Intentional full replace of project-md-index.json (paths registry only).
3024
- try {
3025
- const _regTmp = indexPath + '.tmp';
3026
- writeFileSync(_regTmp, JSON.stringify(registry, null, 2), 'utf8');
3027
- renameSync(_regTmp, indexPath);
3028
- } catch (err) {
3029
- res.writeHead(500, { 'Content-Type': 'application/json' });
3030
- res.end(JSON.stringify({ ok: false, error: `Cannot write registry: ${err.message}` }));
3031
- return;
3032
- }
3033
- console.log(` Config removed from registry: ${cwd} (PROJECT.md file kept)`);
3034
- res.writeHead(200, { 'Content-Type': 'application/json' });
3035
- res.end(JSON.stringify({ ok: true }));
3036
- return;
3037
- }
3038
-
3039
- // ROLE MD ROUTES (Phase B §4) — UI-managed agent role files.
3040
- // Each role lives at <data>/roles/<name>.md with frontmatter
3041
- // (name, description, permission) + optional body. Permission is one of
3042
- // "read" | "read-write" | "mcp".
3043
-
3044
- if (req.method === 'GET' && path === '/md/role') {
3045
- const rolesDir = join(getPluginData(), 'roles');
3046
- const items = [];
3047
- try {
3048
- mkdirSync(rolesDir, { recursive: true });
3049
- const files = (await import('fs')).readdirSync(rolesDir).filter(f => f.endsWith('.md'));
3050
- for (const f of files) {
3051
- const name = f.replace(/\.md$/, '');
3052
- let raw = '';
3053
- try { raw = readFileSync(join(rolesDir, f), 'utf8'); } catch {}
3054
- const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n*/);
3055
- const fm = fmMatch ? fmMatch[1] : '';
3056
- const body = fmMatch ? raw.slice(fmMatch[0].length).trim() : raw.trim();
3057
- const description = (fm.match(/^description:\s*["']?(.+?)["']?\s*$/m)?.[1] || '').trim();
3058
- const permission = (fm.match(/^permission:\s*["']?(.+?)["']?\s*$/m)?.[1] || '').trim().toLowerCase();
3059
- items.push({ name, description, permission, body });
3060
- }
3061
- } catch {}
3062
- res.writeHead(200, { 'Content-Type': 'application/json' });
3063
- res.end(JSON.stringify({ items }));
3064
- return;
3065
- }
3066
-
3067
- if (req.method === 'POST' && path === '/md/role') {
3068
- if (!isAllowedOrigin(req)) {
3069
- res.writeHead(403, { 'Content-Type': 'application/json' });
3070
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
3071
- return;
3072
- }
3073
- const body = await readBody(req);
3074
- const name = String(body?.name || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
3075
- const description = String(body?.description ?? '').trim();
3076
- const permission = String(body?.permission ?? '').trim().toLowerCase();
3077
- const note = String(body?.body ?? '').trim();
3078
- if (!name) {
3079
- res.writeHead(400, { 'Content-Type': 'application/json' });
3080
- res.end(JSON.stringify({ ok: false, error: 'name required' }));
3081
- return;
3082
- }
3083
- if (permission && permission !== 'read' && permission !== 'read-write' && permission !== 'mcp') {
3084
- res.writeHead(400, { 'Content-Type': 'application/json' });
3085
- res.end(JSON.stringify({ ok: false, error: 'permission must be "read", "read-write", or "mcp"' }));
3086
- return;
3087
- }
3088
- const fmLines = [`name: ${name}`];
3089
- if (description) fmLines.push(`description: ${description.replace(/\n/g, ' ')}`);
3090
- if (permission) fmLines.push(`permission: ${permission}`);
3091
- const content = `---\n${fmLines.join('\n')}\n---\n${note ? `\n${note}\n` : ''}`;
3092
- const p = join(getPluginData(), 'roles', `${name}.md`);
3093
- // Intentional full replace: POST /md/role owns the entire role markdown file.
3094
- try {
3095
- mkdirSync(dirname(p), { recursive: true });
3096
- const _rTmp = p + '.tmp';
3097
- writeFileSync(_rTmp, content, 'utf8');
3098
- renameSync(_rTmp, p);
3099
- } catch (err) {
3100
- res.writeHead(500, { 'Content-Type': 'application/json' });
3101
- res.end(JSON.stringify({ ok: false, error: 'Cannot write role: ' + err.message }));
3102
- return;
3103
- }
3104
- console.log(` Config saved: role MD (${name})`);
3105
- res.writeHead(200, { 'Content-Type': 'application/json' });
3106
- res.end(JSON.stringify({ ok: true }));
3107
- return;
3108
- }
3109
-
3110
- if (req.method === 'DELETE' && path === '/md/role') {
3111
- const qs = new URL(req.url, 'http://x').searchParams;
3112
- const name = String(qs.get('name') || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
3113
- if (!name) {
3114
- res.writeHead(400, { 'Content-Type': 'application/json' });
3115
- res.end(JSON.stringify({ ok: false, error: 'name required' }));
3116
- return;
3117
- }
3118
- const p = join(getPluginData(), 'roles', `${name}.md`);
3119
- try { (await import('fs')).unlinkSync(p); } catch (err) {
3120
- if (err.code !== 'ENOENT') {
3121
- res.writeHead(500, { 'Content-Type': 'application/json' });
3122
- res.end(JSON.stringify({ ok: false, error: 'Cannot delete role: ' + err.message }));
3123
- return;
3124
- }
3125
- }
3126
- console.log(` Config removed: role MD (${name})`);
3127
- res.writeHead(200, { 'Content-Type': 'application/json' });
3128
- res.end(JSON.stringify({ ok: true }));
3129
- return;
3130
- }
3131
-
3132
- // ============================================================
3133
- // WORKFLOW MODULE ROUTES
3134
- // ============================================================
3135
-
3136
- if (req.method === 'GET' && path === '/workflow/load') {
3137
- res.writeHead(200, { 'Content-Type': 'application/json' });
3138
- res.end(JSON.stringify(readUserWorkflow()));
3139
- return;
3140
- }
3141
-
3142
- if (req.method === 'POST' && path === '/workflow/save') {
3143
- if (!isAllowedOrigin(req)) {
3144
- res.writeHead(403, { 'Content-Type': 'application/json' });
3145
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
3146
- return;
3147
- }
3148
- const data = await readBody(req);
3149
- writeUserWorkflow(data);
3150
- console.log(' Config saved: user-workflow');
3151
- res.writeHead(200, { 'Content-Type': 'application/json' });
3152
- res.end(JSON.stringify({ ok: true }));
3153
- return;
3154
- }
3155
-
3156
- if (req.method === 'GET' && path === '/workflow/md') {
3157
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
3158
- res.end(readUserWorkflowMd());
3159
- return;
3160
- }
3161
-
3162
- if (req.method === 'POST' && path === '/workflow/md') {
3163
- if (!isAllowedOrigin(req)) {
3164
- res.writeHead(403, { 'Content-Type': 'application/json' });
3165
- res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
3166
- return;
3167
- }
3168
- let body = '';
3169
- await new Promise((resolve, reject) => {
3170
- req.on('data', c => { body += c; });
3171
- req.on('end', resolve);
3172
- req.on('error', reject);
3173
- });
3174
- writeUserWorkflowMd(body);
3175
- console.log(' Config saved: user-workflow.md');
3176
- res.writeHead(200, { 'Content-Type': 'application/json' });
3177
- res.end(JSON.stringify({ ok: true }));
3178
- return;
3179
- }
3180
-
3181
- if (req.method === 'GET' && path === '/workflow/file') {
3182
- if (!existsSync(USER_WORKFLOW_MD_PATH)) {
3183
- if (!shouldSeedMissingUserData(DATA_DIR, 'user-workflow.md')) {
3184
- res.writeHead(409, { 'Content-Type': 'application/json' });
3185
- res.end(JSON.stringify({ ok: false, error: 'user-workflow.md missing; restore from backup or intentionally reset user data' }));
3186
- return;
3187
- }
3188
- mkdirSync(dirname(USER_WORKFLOW_MD_PATH), { recursive: true });
3189
- writeFileSync(USER_WORKFLOW_MD_PATH, DEFAULT_USER_WORKFLOW_MD, 'utf8');
3190
- markUserDataInitialized(DATA_DIR);
3191
- }
3192
- if (isWin) { spawn('cmd', ['/c', 'start', '', USER_WORKFLOW_MD_PATH], { detached: true, stdio: 'ignore', windowsHide: true }).unref(); }
3193
- else { spawn('open', [USER_WORKFLOW_MD_PATH], { detached: true, stdio: 'ignore' }).unref(); }
3194
- res.writeHead(200, { 'Content-Type': 'application/json' });
3195
- res.end(JSON.stringify({ ok: true }));
3196
- return;
3197
- }
3198
-
3199
- if (path === '/close') {
3200
- windowOpen = false;
3201
- res.writeHead(200);
3202
- res.end();
3203
- console.log(' Window closed');
3204
- return;
3205
- }
3206
-
3207
- debugSetup(`[req-trace] ${req.method} ${path}`);
3208
- if (path === '/open') {
3209
- debugSetup(`[open-debug] /open request received`);
3210
- const result = await openAppWindow();
3211
- debugSetup(`[open-debug] /open result=${JSON.stringify(result)}`);
3212
- if (!result.ok) {
3213
- windowOpen = false;
3214
- res.writeHead(500, { 'Content-Type': 'application/json' });
3215
- res.end(JSON.stringify(result));
3216
- return;
3217
- }
3218
-
3219
- windowOpen = true;
3220
- res.writeHead(200, { 'Content-Type': 'application/json' });
3221
- res.end(JSON.stringify(result));
3222
- return;
3223
- }
3224
-
3225
- if (req.method === 'GET' && path === '/generation') {
3226
- res.writeHead(200, { 'Content-Type': 'application/json' });
3227
- res.end(JSON.stringify({ generation: openGeneration }));
3228
- return;
3229
- }
3230
-
3231
- res.writeHead(404);
3232
- res.end('Not found');
3233
- }
3234
-
3235
- // Fire-and-forget warm-up of the model catalogs the Config UI will request.
3236
- // Collects every provider referenced by the current agent config's presets,
3237
- // then drives each through listProviderModels — the exact path /agent/models
3238
- // serves — so the 24h runtime cache is populated by the time the UI asks.
3239
- // Errors per provider are logged and swallowed; this never throws.
3240
- function prefetchModelCatalogs() {
3241
- // Config reads can rethrow non-ENOENT I/O failures (EACCES/EIO) through
3242
- // readJsonFile; this runs unguarded inside the listen callback, so contain
3243
- // them here — a failed prefetch must never take the server down.
3244
- let cfg;
3245
- const providers = new Set();
3246
- try {
3247
- cfg = readAgentConfig();
3248
- for (const preset of readAgentPresets()) {
3249
- const id = normalizeAgentProviderId(preset?.provider);
3250
- if (id) providers.add(id);
3251
- }
3252
- } catch (err) {
3253
- console.error(`[setup] model catalog prefetch skipped: ${err?.message || err}`);
3254
- return;
3255
- }
3256
- for (const id of providers) {
3257
- listProviderModels(id, cfg).catch(err => {
3258
- console.error(`[setup] model catalog prefetch failed for ${id}: ${err?.message || err}`);
3259
- });
3260
- }
3261
- }
3262
-
3263
- server.listen(PORT, '127.0.0.1', () => { // localhost-only — config UI holds secrets
3264
- console.log(`\n MIXDOG CONFIG`);
3265
- console.log(` http://localhost:${PORT}\n`);
3266
- // Warm model catalogs in the background so the Config UI's provider
3267
- // dropdowns fill without a cold network round-trip. Boot drops the runtime
3268
- // caches (dropRuntimeModelCaches above), so the first /agent/models request
3269
- // would otherwise pay full fetch latency while the user waits on a disabled
3270
- // dropdown. Fire-and-forget through the same listProviderModels path the UI
3271
- // uses; errors are logged and never thrown.
3272
- prefetchModelCatalogs();
3273
- if (process.env.MIXDOG_SETUP_OPEN_ON_START === '1') {
3274
- openGeneration++;
3275
- windowOpen = true;
3276
- openAppWindow().then(result => {
3277
- if (!result?.ok) {
3278
- windowOpen = false;
3279
- console.error(`[setup] openAppWindow failed: ${result?.error || JSON.stringify(result?.attempts)}`);
3280
- }
3281
- }).catch(err => {
3282
- windowOpen = false;
3283
- console.error(`[setup] openAppWindow threw: ${err?.message || err}`);
3284
- });
3285
- }
3286
- });
3287
-
3288
- // Parent-PID watchdog: setup-server is launched detached/unref'd (see
3289
- // setup/launch.mjs), so losing Claude Code does not reap it. Poll the
3290
- // launcher's parent PID (the Claude Code CLI) and exit when it dies. This is
3291
- // the detached-process equivalent of the run-mcp.mjs stdin-close pattern
3292
- // applied to memory/channels workers in v0.6.0.
3293
- (() => {
3294
- const parentPid = parseInt(process.env.MIXDOG_SETUP_PARENT_PID || '', 10);
3295
- if (!Number.isFinite(parentPid) || parentPid <= 0) return;
3296
- const tick = () => {
3297
- try {
3298
- process.kill(parentPid, 0);
3299
- } catch {
3300
- process.exit(0);
3301
- }
3302
- };
3303
- const timer = setInterval(tick, 5000);
3304
- if (typeof timer.unref === 'function') timer.unref();
3305
- })();