synabun 1.3.0 → 2.0.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 (1163) hide show
  1. package/CHANGELOG.md +205 -662
  2. package/LICENSE-COMMERCIAL.md +33 -0
  3. package/NOTICE +17 -0
  4. package/README.md +175 -853
  5. package/THIRD-PARTY-LICENSES.md +136 -0
  6. package/benchmarks/browser-live-results.json +79 -0
  7. package/benchmarks/browser-v2-results.json +258 -0
  8. package/benchmarks/fixtures/browser/README.md +57 -0
  9. package/benchmarks/fixtures/browser/page-state.json +132 -0
  10. package/benchmarks/fixtures/browser/social.json +92 -0
  11. package/benchmarks/fixtures/browser/target.json +319 -0
  12. package/benchmarks/fixtures/desktop/README.md +58 -0
  13. package/benchmarks/fixtures/desktop/abstention.json +59 -0
  14. package/benchmarks/fixtures/desktop/adversarial.json +22 -0
  15. package/benchmarks/fixtures/desktop/forbidden.json +93 -0
  16. package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
  17. package/benchmarks/fixtures/desktop/target.json +136 -0
  18. package/bin/synabun.js +30 -0
  19. package/docs/whatsapp.md +325 -0
  20. package/hooks/claude-code/post-plan.mjs +613 -391
  21. package/hooks/claude-code/post-remember.mjs +246 -122
  22. package/hooks/claude-code/pre-compact.mjs +171 -110
  23. package/hooks/claude-code/pre-task.mjs +133 -0
  24. package/hooks/claude-code/pre-websearch.mjs +15 -4
  25. package/hooks/claude-code/prompt-origin.mjs +94 -0
  26. package/hooks/claude-code/prompt-submit.mjs +1244 -861
  27. package/hooks/claude-code/session-start.mjs +114 -156
  28. package/hooks/claude-code/shared.mjs +1228 -368
  29. package/hooks/claude-code/state.mjs +167 -0
  30. package/hooks/claude-code/stop.mjs +515 -140
  31. package/hooks/claude-code/subagent-start.mjs +154 -0
  32. package/hooks/claude-code/subagent-stop.mjs +102 -0
  33. package/hooks/claude-code/task-gate.mjs +150 -0
  34. package/launcher.mjs +40 -0
  35. package/lib/claude-hooks.js +137 -0
  36. package/lib/claude-usage-audit.js +141 -0
  37. package/lib/client-config-repair.js +145 -0
  38. package/lib/data-home-migration.js +502 -0
  39. package/lib/desktop-pwa.js +206 -0
  40. package/lib/external-tools.js +221 -0
  41. package/lib/first-launch-protection.js +245 -0
  42. package/lib/packaged-runtime.js +110 -0
  43. package/lib/paths.d.ts +21 -0
  44. package/lib/paths.js +103 -0
  45. package/lib/start-launcher.js +1309 -0
  46. package/mcp-server/dist/config.d.ts +22 -0
  47. package/mcp-server/dist/config.d.ts.map +1 -0
  48. package/mcp-server/dist/config.js +109 -0
  49. package/mcp-server/dist/config.js.map +1 -0
  50. package/mcp-server/dist/http.d.ts +30 -0
  51. package/mcp-server/dist/http.d.ts.map +1 -0
  52. package/mcp-server/dist/http.js +276 -0
  53. package/mcp-server/dist/http.js.map +1 -0
  54. package/mcp-server/dist/index.d.ts +48 -0
  55. package/mcp-server/dist/index.d.ts.map +1 -0
  56. package/mcp-server/dist/index.js +538 -0
  57. package/mcp-server/dist/index.js.map +1 -0
  58. package/mcp-server/dist/preload-http.d.ts +3 -0
  59. package/mcp-server/dist/preload-http.d.ts.map +1 -0
  60. package/mcp-server/dist/preload-http.js +28 -0
  61. package/mcp-server/dist/preload-http.js.map +1 -0
  62. package/mcp-server/dist/preload.d.ts +3 -0
  63. package/mcp-server/dist/preload.d.ts.map +1 -0
  64. package/mcp-server/dist/preload.js +32 -0
  65. package/mcp-server/dist/preload.js.map +1 -0
  66. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
  67. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
  68. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
  69. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
  70. package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
  71. package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
  72. package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
  73. package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
  74. package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
  75. package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
  76. package/mcp-server/dist/services/assistant-judgments.js +128 -0
  77. package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
  78. package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
  79. package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
  80. package/mcp-server/dist/services/browser-assist-gate.js +344 -0
  81. package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
  82. package/mcp-server/dist/services/browser-assist.d.ts +104 -0
  83. package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
  84. package/mcp-server/dist/services/browser-assist.js +282 -0
  85. package/mcp-server/dist/services/browser-assist.js.map +1 -0
  86. package/mcp-server/dist/services/browser-bench.d.ts +772 -0
  87. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
  88. package/mcp-server/dist/services/browser-bench.js +424 -0
  89. package/mcp-server/dist/services/browser-bench.js.map +1 -0
  90. package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
  91. package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
  92. package/mcp-server/dist/services/browser-judgments.js +363 -0
  93. package/mcp-server/dist/services/browser-judgments.js.map +1 -0
  94. package/mcp-server/dist/services/browser-risk.d.ts +166 -0
  95. package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
  96. package/mcp-server/dist/services/browser-risk.js +458 -0
  97. package/mcp-server/dist/services/browser-risk.js.map +1 -0
  98. package/mcp-server/dist/services/categories.d.ts +61 -0
  99. package/mcp-server/dist/services/categories.d.ts.map +1 -0
  100. package/mcp-server/dist/services/categories.js +377 -0
  101. package/mcp-server/dist/services/categories.js.map +1 -0
  102. package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
  103. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
  104. package/mcp-server/dist/services/codex-config-heal.js +346 -0
  105. package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
  106. package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
  107. package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
  108. package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
  109. package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
  110. package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
  111. package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
  112. package/mcp-server/dist/services/desktop-assist.js +266 -0
  113. package/mcp-server/dist/services/desktop-assist.js.map +1 -0
  114. package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
  115. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
  116. package/mcp-server/dist/services/desktop-bench.js +311 -0
  117. package/mcp-server/dist/services/desktop-bench.js.map +1 -0
  118. package/mcp-server/dist/services/desktop-client.d.ts +23 -0
  119. package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
  120. package/mcp-server/dist/services/desktop-client.js +79 -0
  121. package/mcp-server/dist/services/desktop-client.js.map +1 -0
  122. package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
  123. package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
  124. package/mcp-server/dist/services/desktop-judgments.js +177 -0
  125. package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
  126. package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
  127. package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
  128. package/mcp-server/dist/services/desktop-risk.js +605 -0
  129. package/mcp-server/dist/services/desktop-risk.js.map +1 -0
  130. package/mcp-server/dist/services/discord.d.ts +112 -0
  131. package/mcp-server/dist/services/discord.d.ts.map +1 -0
  132. package/mcp-server/dist/services/discord.js +477 -0
  133. package/mcp-server/dist/services/discord.js.map +1 -0
  134. package/mcp-server/dist/services/edit-stale.d.ts +81 -0
  135. package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
  136. package/mcp-server/dist/services/edit-stale.js +358 -0
  137. package/mcp-server/dist/services/edit-stale.js.map +1 -0
  138. package/mcp-server/dist/services/embedding-service.d.ts +16 -0
  139. package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
  140. package/mcp-server/dist/services/embedding-service.js +126 -0
  141. package/mcp-server/dist/services/embedding-service.js.map +1 -0
  142. package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
  143. package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
  144. package/mcp-server/dist/services/embedding-worker.js +52 -0
  145. package/mcp-server/dist/services/embedding-worker.js.map +1 -0
  146. package/mcp-server/dist/services/fb-regions.d.ts +71 -0
  147. package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
  148. package/mcp-server/dist/services/fb-regions.js +211 -0
  149. package/mcp-server/dist/services/fb-regions.js.map +1 -0
  150. package/mcp-server/dist/services/file-checksums.d.ts +22 -0
  151. package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
  152. package/mcp-server/dist/services/file-checksums.js +55 -0
  153. package/mcp-server/dist/services/file-checksums.js.map +1 -0
  154. package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
  155. package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
  156. package/mcp-server/dist/services/file-excerpt.js +65 -0
  157. package/mcp-server/dist/services/file-excerpt.js.map +1 -0
  158. package/mcp-server/dist/services/identity.d.ts +124 -0
  159. package/mcp-server/dist/services/identity.d.ts.map +1 -0
  160. package/mcp-server/dist/services/identity.js +241 -0
  161. package/mcp-server/dist/services/identity.js.map +1 -0
  162. package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
  163. package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
  164. package/mcp-server/dist/services/local-embeddings.js +3 -0
  165. package/mcp-server/dist/services/local-embeddings.js.map +1 -0
  166. package/mcp-server/dist/services/memory-client.d.ts +4 -0
  167. package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
  168. package/mcp-server/dist/services/memory-client.js +27 -0
  169. package/mcp-server/dist/services/memory-client.js.map +1 -0
  170. package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
  171. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
  172. package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
  173. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
  174. package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
  175. package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
  176. package/mcp-server/dist/services/memory-judgments.js +674 -0
  177. package/mcp-server/dist/services/memory-judgments.js.map +1 -0
  178. package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
  179. package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
  180. package/mcp-server/dist/services/memory-maintenance.js +403 -0
  181. package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
  182. package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
  183. package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
  184. package/mcp-server/dist/services/memory-retrieval.js +412 -0
  185. package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
  186. package/mcp-server/dist/services/memory-schema.d.ts +16 -0
  187. package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
  188. package/mcp-server/dist/services/memory-schema.js +282 -0
  189. package/mcp-server/dist/services/memory-schema.js.map +1 -0
  190. package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
  191. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
  192. package/mcp-server/dist/services/memory-sessions.js +110 -0
  193. package/mcp-server/dist/services/memory-sessions.js.map +1 -0
  194. package/mcp-server/dist/services/memory-triage.d.ts +60 -0
  195. package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
  196. package/mcp-server/dist/services/memory-triage.js +189 -0
  197. package/mcp-server/dist/services/memory-triage.js.map +1 -0
  198. package/mcp-server/dist/services/memory-verification.d.ts +24 -0
  199. package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
  200. package/mcp-server/dist/services/memory-verification.js +92 -0
  201. package/mcp-server/dist/services/memory-verification.js.map +1 -0
  202. package/mcp-server/dist/services/memory-writer.d.ts +26 -0
  203. package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
  204. package/mcp-server/dist/services/memory-writer.js +126 -0
  205. package/mcp-server/dist/services/memory-writer.js.map +1 -0
  206. package/mcp-server/dist/services/neural-interface.d.ts +439 -0
  207. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
  208. package/mcp-server/dist/services/neural-interface.js +1135 -0
  209. package/mcp-server/dist/services/neural-interface.js.map +1 -0
  210. package/mcp-server/dist/services/profiles.d.ts +120 -0
  211. package/mcp-server/dist/services/profiles.d.ts.map +1 -0
  212. package/mcp-server/dist/services/profiles.js +387 -0
  213. package/mcp-server/dist/services/profiles.js.map +1 -0
  214. package/mcp-server/dist/services/secret-gate.d.ts +29 -0
  215. package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
  216. package/mcp-server/dist/services/secret-gate.js +43 -0
  217. package/mcp-server/dist/services/secret-gate.js.map +1 -0
  218. package/mcp-server/dist/services/server-instructions.d.ts +10 -0
  219. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
  220. package/mcp-server/dist/services/server-instructions.js +101 -0
  221. package/mcp-server/dist/services/server-instructions.js.map +1 -0
  222. package/mcp-server/dist/services/sqlite.d.ts +235 -0
  223. package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
  224. package/mcp-server/dist/services/sqlite.js +1185 -0
  225. package/mcp-server/dist/services/sqlite.js.map +1 -0
  226. package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
  227. package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
  228. package/mcp-server/dist/services/typesafe-bench.js +218 -0
  229. package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
  230. package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
  231. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
  232. package/mcp-server/dist/services/typesafe-config.js +746 -0
  233. package/mcp-server/dist/services/typesafe-config.js.map +1 -0
  234. package/mcp-server/dist/services/typesafe.d.ts +213 -0
  235. package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
  236. package/mcp-server/dist/services/typesafe.js +330 -0
  237. package/mcp-server/dist/services/typesafe.js.map +1 -0
  238. package/mcp-server/dist/services/vector-cache.d.ts +68 -0
  239. package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
  240. package/mcp-server/dist/services/vector-cache.js +271 -0
  241. package/mcp-server/dist/services/vector-cache.js.map +1 -0
  242. package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
  243. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
  244. package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
  245. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
  246. package/mcp-server/dist/tools/agents.d.ts +212 -0
  247. package/mcp-server/dist/tools/agents.d.ts.map +1 -0
  248. package/mcp-server/dist/tools/agents.js +407 -0
  249. package/mcp-server/dist/tools/agents.js.map +1 -0
  250. package/mcp-server/dist/tools/bluesky.d.ts +39 -0
  251. package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
  252. package/mcp-server/dist/tools/bluesky.js +1144 -0
  253. package/mcp-server/dist/tools/bluesky.js.map +1 -0
  254. package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
  255. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
  256. package/mcp-server/dist/tools/browser-advanced.js +106 -0
  257. package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
  258. package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
  259. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
  260. package/mcp-server/dist/tools/browser-batch.js +187 -0
  261. package/mcp-server/dist/tools/browser-batch.js.map +1 -0
  262. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
  263. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
  264. package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
  265. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
  266. package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
  267. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
  268. package/mcp-server/dist/tools/browser-interact.js +300 -0
  269. package/mcp-server/dist/tools/browser-interact.js.map +1 -0
  270. package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
  271. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
  272. package/mcp-server/dist/tools/browser-navigate.js +142 -0
  273. package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
  274. package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
  275. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
  276. package/mcp-server/dist/tools/browser-observe.js +2009 -0
  277. package/mcp-server/dist/tools/browser-observe.js.map +1 -0
  278. package/mcp-server/dist/tools/browser.d.ts +34 -0
  279. package/mcp-server/dist/tools/browser.d.ts.map +1 -0
  280. package/mcp-server/dist/tools/browser.js +112 -0
  281. package/mcp-server/dist/tools/browser.js.map +1 -0
  282. package/mcp-server/dist/tools/card-tools.d.ts +82 -0
  283. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
  284. package/mcp-server/dist/tools/card-tools.js +137 -0
  285. package/mcp-server/dist/tools/card-tools.js.map +1 -0
  286. package/mcp-server/dist/tools/card.d.ts +8 -0
  287. package/mcp-server/dist/tools/card.d.ts.map +1 -0
  288. package/mcp-server/dist/tools/card.js +16 -0
  289. package/mcp-server/dist/tools/card.js.map +1 -0
  290. package/mcp-server/dist/tools/category.d.ts +32 -0
  291. package/mcp-server/dist/tools/category.d.ts.map +1 -0
  292. package/mcp-server/dist/tools/category.js +290 -0
  293. package/mcp-server/dist/tools/category.js.map +1 -0
  294. package/mcp-server/dist/tools/choice.d.ts +160 -0
  295. package/mcp-server/dist/tools/choice.d.ts.map +1 -0
  296. package/mcp-server/dist/tools/choice.js +80 -0
  297. package/mcp-server/dist/tools/choice.js.map +1 -0
  298. package/mcp-server/dist/tools/computer.d.ts +71 -0
  299. package/mcp-server/dist/tools/computer.d.ts.map +1 -0
  300. package/mcp-server/dist/tools/computer.js +174 -0
  301. package/mcp-server/dist/tools/computer.js.map +1 -0
  302. package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
  303. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
  304. package/mcp-server/dist/tools/discord-channel.js +236 -0
  305. package/mcp-server/dist/tools/discord-channel.js.map +1 -0
  306. package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
  307. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
  308. package/mcp-server/dist/tools/discord-guild.js +138 -0
  309. package/mcp-server/dist/tools/discord-guild.js.map +1 -0
  310. package/mcp-server/dist/tools/discord-member.d.ts +26 -0
  311. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
  312. package/mcp-server/dist/tools/discord-member.js +122 -0
  313. package/mcp-server/dist/tools/discord-member.js.map +1 -0
  314. package/mcp-server/dist/tools/discord-message.d.ts +36 -0
  315. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
  316. package/mcp-server/dist/tools/discord-message.js +184 -0
  317. package/mcp-server/dist/tools/discord-message.js.map +1 -0
  318. package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
  319. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
  320. package/mcp-server/dist/tools/discord-onboarding.js +179 -0
  321. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
  322. package/mcp-server/dist/tools/discord-role.d.ts +34 -0
  323. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
  324. package/mcp-server/dist/tools/discord-role.js +178 -0
  325. package/mcp-server/dist/tools/discord-role.js.map +1 -0
  326. package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
  327. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
  328. package/mcp-server/dist/tools/discord-thread.js +88 -0
  329. package/mcp-server/dist/tools/discord-thread.js.map +1 -0
  330. package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
  331. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
  332. package/mcp-server/dist/tools/discord-webhook.js +154 -0
  333. package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
  334. package/mcp-server/dist/tools/discord.d.ts +7 -0
  335. package/mcp-server/dist/tools/discord.d.ts.map +1 -0
  336. package/mcp-server/dist/tools/discord.js +25 -0
  337. package/mcp-server/dist/tools/discord.js.map +1 -0
  338. package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
  339. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
  340. package/mcp-server/dist/tools/fb-groups.js +369 -0
  341. package/mcp-server/dist/tools/fb-groups.js.map +1 -0
  342. package/mcp-server/dist/tools/forget.d.ts +14 -0
  343. package/mcp-server/dist/tools/forget.d.ts.map +1 -0
  344. package/mcp-server/dist/tools/forget.js +26 -0
  345. package/mcp-server/dist/tools/forget.js.map +1 -0
  346. package/mcp-server/dist/tools/git-tools.d.ts +24 -0
  347. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
  348. package/mcp-server/dist/tools/git-tools.js +107 -0
  349. package/mcp-server/dist/tools/git-tools.js.map +1 -0
  350. package/mcp-server/dist/tools/git.d.ts +7 -0
  351. package/mcp-server/dist/tools/git.d.ts.map +1 -0
  352. package/mcp-server/dist/tools/git.js +11 -0
  353. package/mcp-server/dist/tools/git.js.map +1 -0
  354. package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
  355. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
  356. package/mcp-server/dist/tools/gsc/experience.js +351 -0
  357. package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
  358. package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
  359. package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
  360. package/mcp-server/dist/tools/gsc/helpers.js +258 -0
  361. package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
  362. package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
  363. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
  364. package/mcp-server/dist/tools/gsc/indexing.js +357 -0
  365. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
  366. package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
  367. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
  368. package/mcp-server/dist/tools/gsc/inspect.js +321 -0
  369. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
  370. package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
  371. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
  372. package/mcp-server/dist/tools/gsc/navigate.js +197 -0
  373. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
  374. package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
  375. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
  376. package/mcp-server/dist/tools/gsc/performance.js +274 -0
  377. package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
  378. package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
  379. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
  380. package/mcp-server/dist/tools/gsc/settings.js +425 -0
  381. package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
  382. package/mcp-server/dist/tools/gsc.d.ts +7 -0
  383. package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
  384. package/mcp-server/dist/tools/gsc.js +51 -0
  385. package/mcp-server/dist/tools/gsc.js.map +1 -0
  386. package/mcp-server/dist/tools/image-tools.d.ts +24 -0
  387. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
  388. package/mcp-server/dist/tools/image-tools.js +80 -0
  389. package/mcp-server/dist/tools/image-tools.js.map +1 -0
  390. package/mcp-server/dist/tools/image.d.ts +10 -0
  391. package/mcp-server/dist/tools/image.d.ts.map +1 -0
  392. package/mcp-server/dist/tools/image.js +14 -0
  393. package/mcp-server/dist/tools/image.js.map +1 -0
  394. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
  395. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
  396. package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
  397. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
  398. package/mcp-server/dist/tools/leonardo.d.ts +13 -0
  399. package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
  400. package/mcp-server/dist/tools/leonardo.js +21 -0
  401. package/mcp-server/dist/tools/leonardo.js.map +1 -0
  402. package/mcp-server/dist/tools/loop.d.ts +30 -0
  403. package/mcp-server/dist/tools/loop.d.ts.map +1 -0
  404. package/mcp-server/dist/tools/loop.js +401 -0
  405. package/mcp-server/dist/tools/loop.js.map +1 -0
  406. package/mcp-server/dist/tools/memories.d.ts +56 -0
  407. package/mcp-server/dist/tools/memories.d.ts.map +1 -0
  408. package/mcp-server/dist/tools/memories.js +171 -0
  409. package/mcp-server/dist/tools/memories.js.map +1 -0
  410. package/mcp-server/dist/tools/morelogin.d.ts +24 -0
  411. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
  412. package/mcp-server/dist/tools/morelogin.js +79 -0
  413. package/mcp-server/dist/tools/morelogin.js.map +1 -0
  414. package/mcp-server/dist/tools/profile.d.ts +17 -0
  415. package/mcp-server/dist/tools/profile.d.ts.map +1 -0
  416. package/mcp-server/dist/tools/profile.js +114 -0
  417. package/mcp-server/dist/tools/profile.js.map +1 -0
  418. package/mcp-server/dist/tools/recall.d.ts +71 -0
  419. package/mcp-server/dist/tools/recall.d.ts.map +1 -0
  420. package/mcp-server/dist/tools/recall.js +147 -0
  421. package/mcp-server/dist/tools/recall.js.map +1 -0
  422. package/mcp-server/dist/tools/reflect.d.ts +56 -0
  423. package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
  424. package/mcp-server/dist/tools/reflect.js +157 -0
  425. package/mcp-server/dist/tools/reflect.js.map +1 -0
  426. package/mcp-server/dist/tools/remember.d.ts +62 -0
  427. package/mcp-server/dist/tools/remember.d.ts.map +1 -0
  428. package/mcp-server/dist/tools/remember.js +201 -0
  429. package/mcp-server/dist/tools/remember.js.map +1 -0
  430. package/mcp-server/dist/tools/response.d.ts +23 -0
  431. package/mcp-server/dist/tools/response.d.ts.map +1 -0
  432. package/mcp-server/dist/tools/response.js +24 -0
  433. package/mcp-server/dist/tools/response.js.map +1 -0
  434. package/mcp-server/dist/tools/restore.d.ts +14 -0
  435. package/mcp-server/dist/tools/restore.d.ts.map +1 -0
  436. package/mcp-server/dist/tools/restore.js +23 -0
  437. package/mcp-server/dist/tools/restore.js.map +1 -0
  438. package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
  439. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
  440. package/mcp-server/dist/tools/style-guide-tools.js +231 -0
  441. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
  442. package/mcp-server/dist/tools/style-guide.d.ts +8 -0
  443. package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
  444. package/mcp-server/dist/tools/style-guide.js +12 -0
  445. package/mcp-server/dist/tools/style-guide.js.map +1 -0
  446. package/mcp-server/dist/tools/sync.d.ts +20 -0
  447. package/mcp-server/dist/tools/sync.d.ts.map +1 -0
  448. package/mcp-server/dist/tools/sync.js +156 -0
  449. package/mcp-server/dist/tools/sync.js.map +1 -0
  450. package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
  451. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
  452. package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
  453. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
  454. package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
  455. package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
  456. package/mcp-server/dist/tools/tictactoe.js +11 -0
  457. package/mcp-server/dist/tools/tictactoe.js.map +1 -0
  458. package/mcp-server/dist/tools/utils.d.ts +19 -0
  459. package/mcp-server/dist/tools/utils.d.ts.map +1 -0
  460. package/mcp-server/dist/tools/utils.js +54 -0
  461. package/mcp-server/dist/tools/utils.js.map +1 -0
  462. package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
  463. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
  464. package/mcp-server/dist/tools/whiteboard-format.js +337 -0
  465. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
  466. package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
  467. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
  468. package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
  469. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
  470. package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
  471. package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
  472. package/mcp-server/dist/tools/whiteboard.js +16 -0
  473. package/mcp-server/dist/tools/whiteboard.js.map +1 -0
  474. package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
  475. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
  476. package/mcp-server/dist/tools/youtube/analytics.js +83 -0
  477. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
  478. package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
  479. package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
  480. package/mcp-server/dist/tools/youtube/dedup.js +56 -0
  481. package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
  482. package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
  483. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
  484. package/mcp-server/dist/tools/youtube/download.js +30 -0
  485. package/mcp-server/dist/tools/youtube/download.js.map +1 -0
  486. package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
  487. package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
  488. package/mcp-server/dist/tools/youtube/helpers.js +143 -0
  489. package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
  490. package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
  491. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
  492. package/mcp-server/dist/tools/youtube/media.js +97 -0
  493. package/mcp-server/dist/tools/youtube/media.js.map +1 -0
  494. package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
  495. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
  496. package/mcp-server/dist/tools/youtube/metadata.js +106 -0
  497. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
  498. package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
  499. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
  500. package/mcp-server/dist/tools/youtube/navigate.js +71 -0
  501. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
  502. package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
  503. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
  504. package/mcp-server/dist/tools/youtube/playlist.js +128 -0
  505. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
  506. package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
  507. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
  508. package/mcp-server/dist/tools/youtube/schedule.js +147 -0
  509. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
  510. package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
  511. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
  512. package/mcp-server/dist/tools/youtube/source.js +85 -0
  513. package/mcp-server/dist/tools/youtube/source.js.map +1 -0
  514. package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
  515. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
  516. package/mcp-server/dist/tools/youtube/upload.js +255 -0
  517. package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
  518. package/mcp-server/dist/tools/youtube.d.ts +12 -0
  519. package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
  520. package/mcp-server/dist/tools/youtube.js +38 -0
  521. package/mcp-server/dist/tools/youtube.js.map +1 -0
  522. package/mcp-server/dist/tui.d.ts +3 -0
  523. package/mcp-server/dist/tui.d.ts.map +1 -0
  524. package/mcp-server/dist/tui.js +806 -0
  525. package/mcp-server/dist/tui.js.map +1 -0
  526. package/mcp-server/dist/types.d.ts +77 -0
  527. package/mcp-server/dist/types.d.ts.map +1 -0
  528. package/mcp-server/dist/types.js +2 -0
  529. package/mcp-server/dist/types.js.map +1 -0
  530. package/mcp-server/package-lock.json +4798 -0
  531. package/mcp-server/package.json +55 -38
  532. package/mcp-server/run.mjs +12 -2
  533. package/mcp-server/src/config.ts +48 -3
  534. package/mcp-server/src/http.ts +296 -93
  535. package/mcp-server/src/index.ts +546 -203
  536. package/mcp-server/src/preload-http.ts +19 -18
  537. package/mcp-server/src/preload.ts +19 -18
  538. package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
  539. package/mcp-server/src/services/assistant-judgments.ts +157 -0
  540. package/mcp-server/src/services/browser-assist-gate.ts +383 -0
  541. package/mcp-server/src/services/browser-assist.ts +267 -0
  542. package/mcp-server/src/services/browser-bench.ts +477 -0
  543. package/mcp-server/src/services/browser-judgments.ts +414 -0
  544. package/mcp-server/src/services/browser-risk.ts +484 -0
  545. package/mcp-server/src/services/categories.ts +36 -19
  546. package/mcp-server/src/services/codex-config-heal.ts +403 -0
  547. package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
  548. package/mcp-server/src/services/desktop-assist.ts +308 -0
  549. package/mcp-server/src/services/desktop-bench.ts +372 -0
  550. package/mcp-server/src/services/desktop-client.ts +76 -0
  551. package/mcp-server/src/services/desktop-judgments.ts +218 -0
  552. package/mcp-server/src/services/desktop-risk.ts +743 -0
  553. package/mcp-server/src/services/edit-stale.ts +299 -0
  554. package/mcp-server/src/services/embedding-service.ts +108 -0
  555. package/mcp-server/src/services/embedding-worker.ts +46 -0
  556. package/mcp-server/src/services/fb-regions.ts +221 -0
  557. package/mcp-server/src/services/file-checksums.ts +26 -9
  558. package/mcp-server/src/services/file-excerpt.ts +59 -0
  559. package/mcp-server/src/services/identity.ts +291 -0
  560. package/mcp-server/src/services/local-embeddings.ts +2 -77
  561. package/mcp-server/src/services/memory-client.ts +23 -0
  562. package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
  563. package/mcp-server/src/services/memory-judgments.ts +901 -0
  564. package/mcp-server/src/services/memory-maintenance.ts +360 -0
  565. package/mcp-server/src/services/memory-retrieval.ts +349 -0
  566. package/mcp-server/src/services/memory-schema.ts +280 -0
  567. package/mcp-server/src/services/memory-sessions.ts +87 -0
  568. package/mcp-server/src/services/memory-triage.ts +182 -0
  569. package/mcp-server/src/services/memory-verification.ts +68 -0
  570. package/mcp-server/src/services/memory-writer.ts +125 -0
  571. package/mcp-server/src/services/neural-interface.ts +1217 -73
  572. package/mcp-server/src/services/profiles.ts +428 -0
  573. package/mcp-server/src/services/secret-gate.ts +52 -0
  574. package/mcp-server/src/services/server-instructions.ts +122 -0
  575. package/mcp-server/src/services/sqlite.ts +1456 -823
  576. package/mcp-server/src/services/typesafe-bench.ts +247 -0
  577. package/mcp-server/src/services/typesafe-config.ts +790 -0
  578. package/mcp-server/src/services/typesafe.ts +390 -0
  579. package/mcp-server/src/services/vector-cache.ts +255 -0
  580. package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
  581. package/mcp-server/src/tools/agents.ts +539 -0
  582. package/mcp-server/src/tools/bluesky.ts +1186 -0
  583. package/mcp-server/src/tools/browser-advanced.ts +15 -6
  584. package/mcp-server/src/tools/browser-batch.ts +178 -0
  585. package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
  586. package/mcp-server/src/tools/browser-interact.ts +362 -255
  587. package/mcp-server/src/tools/browser-navigate.ts +164 -77
  588. package/mcp-server/src/tools/browser-observe.ts +2222 -1330
  589. package/mcp-server/src/tools/browser.ts +181 -107
  590. package/mcp-server/src/tools/card.ts +7 -5
  591. package/mcp-server/src/tools/choice.ts +103 -0
  592. package/mcp-server/src/tools/computer.ts +190 -0
  593. package/mcp-server/src/tools/discord.ts +10 -23
  594. package/mcp-server/src/tools/fb-groups.ts +387 -0
  595. package/mcp-server/src/tools/forget.ts +1 -1
  596. package/mcp-server/src/tools/git.ts +3 -1
  597. package/mcp-server/src/tools/gsc/experience.ts +440 -0
  598. package/mcp-server/src/tools/gsc/helpers.ts +288 -0
  599. package/mcp-server/src/tools/gsc/indexing.ts +495 -0
  600. package/mcp-server/src/tools/gsc/inspect.ts +391 -0
  601. package/mcp-server/src/tools/gsc/navigate.ts +236 -0
  602. package/mcp-server/src/tools/gsc/performance.ts +352 -0
  603. package/mcp-server/src/tools/gsc/settings.ts +564 -0
  604. package/mcp-server/src/tools/gsc.ts +95 -0
  605. package/mcp-server/src/tools/image.ts +3 -1
  606. package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
  607. package/mcp-server/src/tools/leonardo.ts +7 -5
  608. package/mcp-server/src/tools/loop.ts +118 -25
  609. package/mcp-server/src/tools/memories.ts +65 -5
  610. package/mcp-server/src/tools/morelogin.ts +81 -0
  611. package/mcp-server/src/tools/profile.ts +122 -0
  612. package/mcp-server/src/tools/recall.ts +105 -261
  613. package/mcp-server/src/tools/reflect.ts +171 -145
  614. package/mcp-server/src/tools/remember.ts +223 -115
  615. package/mcp-server/src/tools/style-guide-tools.ts +250 -0
  616. package/mcp-server/src/tools/style-guide.ts +16 -0
  617. package/mcp-server/src/tools/sync.ts +66 -9
  618. package/mcp-server/src/tools/tictactoe.ts +3 -1
  619. package/mcp-server/src/tools/utils.ts +22 -0
  620. package/mcp-server/src/tools/whiteboard-format.ts +394 -0
  621. package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
  622. package/mcp-server/src/tools/whiteboard.ts +7 -5
  623. package/mcp-server/src/tools/youtube/analytics.ts +106 -0
  624. package/mcp-server/src/tools/youtube/dedup.ts +57 -0
  625. package/mcp-server/src/tools/youtube/download.ts +41 -0
  626. package/mcp-server/src/tools/youtube/helpers.ts +183 -0
  627. package/mcp-server/src/tools/youtube/media.ts +109 -0
  628. package/mcp-server/src/tools/youtube/metadata.ts +112 -0
  629. package/mcp-server/src/tools/youtube/navigate.ts +81 -0
  630. package/mcp-server/src/tools/youtube/playlist.ts +134 -0
  631. package/mcp-server/src/tools/youtube/schedule.ts +158 -0
  632. package/mcp-server/src/tools/youtube/source.ts +101 -0
  633. package/mcp-server/src/tools/youtube/upload.ts +296 -0
  634. package/mcp-server/src/tools/youtube.ts +47 -0
  635. package/mcp-server/src/tui.ts +8 -9
  636. package/neural-interface/games/TicTacToe/1.svg +1 -0
  637. package/neural-interface/games/TicTacToe/2.svg +1 -0
  638. package/neural-interface/games/TicTacToe/3.svg +1 -0
  639. package/neural-interface/games/TicTacToe/4.svg +1 -0
  640. package/neural-interface/games/TicTacToe/5.svg +1 -0
  641. package/neural-interface/games/TicTacToe/6.svg +1 -0
  642. package/neural-interface/games/TicTacToe/7.svg +1 -0
  643. package/neural-interface/games/TicTacToe/8.svg +1 -0
  644. package/neural-interface/games/TicTacToe/9.svg +1 -0
  645. package/neural-interface/games/TicTacToe/Board.svg +1 -0
  646. package/neural-interface/games/TicTacToe/Circle.svg +1 -0
  647. package/neural-interface/games/TicTacToe/Cross.svg +1 -0
  648. package/neural-interface/i18n/en.json +7022 -0
  649. package/neural-interface/i18n/pt-BR.json +7026 -0
  650. package/neural-interface/lib/assistant-api.js +1005 -0
  651. package/neural-interface/lib/assistant-attachments.js +121 -0
  652. package/neural-interface/lib/assistant-brains/claude.js +207 -0
  653. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
  654. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
  655. package/neural-interface/lib/assistant-brains/codex.js +213 -0
  656. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
  657. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
  658. package/neural-interface/lib/assistant-brains/opencode.js +320 -0
  659. package/neural-interface/lib/assistant-budget.js +318 -0
  660. package/neural-interface/lib/assistant-catalog.js +731 -0
  661. package/neural-interface/lib/assistant-clarify.js +664 -0
  662. package/neural-interface/lib/assistant-config.js +232 -0
  663. package/neural-interface/lib/assistant-dispatch.js +2744 -0
  664. package/neural-interface/lib/assistant-envelope.js +582 -0
  665. package/neural-interface/lib/assistant-evidence.js +236 -0
  666. package/neural-interface/lib/assistant-hidden-models.js +272 -0
  667. package/neural-interface/lib/assistant-jev.js +117 -0
  668. package/neural-interface/lib/assistant-judgments.js +111 -0
  669. package/neural-interface/lib/assistant-media.js +335 -0
  670. package/neural-interface/lib/assistant-memory.js +398 -0
  671. package/neural-interface/lib/assistant-persona.js +444 -0
  672. package/neural-interface/lib/assistant-plan-permissions.js +448 -0
  673. package/neural-interface/lib/assistant-playbooks/design.md +87 -0
  674. package/neural-interface/lib/assistant-playbooks.js +72 -0
  675. package/neural-interface/lib/assistant-pricing.js +159 -0
  676. package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
  677. package/neural-interface/lib/assistant-route-gate.js +212 -0
  678. package/neural-interface/lib/assistant-router.js +1324 -0
  679. package/neural-interface/lib/assistant-runtime.js +3337 -0
  680. package/neural-interface/lib/assistant-task-prompt.js +472 -0
  681. package/neural-interface/lib/assistant-usage.js +1688 -0
  682. package/neural-interface/lib/augmented-path.js +70 -0
  683. package/neural-interface/lib/automation-storage-migration.js +54 -0
  684. package/neural-interface/lib/backup-service.js +851 -0
  685. package/neural-interface/lib/backup-zip-reader.js +262 -0
  686. package/neural-interface/lib/browser-assist-api.js +41 -0
  687. package/neural-interface/lib/browser-console.js +176 -0
  688. package/neural-interface/lib/browser-execution.js +241 -0
  689. package/neural-interface/lib/browser-extraction.js +191 -0
  690. package/neural-interface/lib/browser-language.js +83 -0
  691. package/neural-interface/lib/browser-screenshot.js +399 -0
  692. package/neural-interface/lib/browser-semantic-context.js +639 -0
  693. package/neural-interface/lib/browser-snapshots.js +192 -0
  694. package/neural-interface/lib/browser-tab-recovery.js +119 -0
  695. package/neural-interface/lib/browser-targets.js +70 -0
  696. package/neural-interface/lib/browser-tool-policy.js +926 -0
  697. package/neural-interface/lib/claude-accounts.js +230 -0
  698. package/neural-interface/lib/claude-agent-bridge.js +3203 -0
  699. package/neural-interface/lib/claude-bypass-policy.js +99 -0
  700. package/neural-interface/lib/claude-executable.js +241 -0
  701. package/neural-interface/lib/claude-history.js +429 -0
  702. package/neural-interface/lib/claude-model-catalog.js +444 -0
  703. package/neural-interface/lib/claude-panel-requests.js +165 -0
  704. package/neural-interface/lib/claude-panel-session.js +272 -0
  705. package/neural-interface/lib/claude-permission-rules.js +153 -0
  706. package/neural-interface/lib/claude-session-cleanup.js +71 -0
  707. package/neural-interface/lib/claude-session-list.js +82 -0
  708. package/neural-interface/lib/claude-session-ops.js +140 -0
  709. package/neural-interface/lib/claude-settings-view.js +51 -0
  710. package/neural-interface/lib/claude-skin-connect.js +79 -0
  711. package/neural-interface/lib/claude-stall-policy.js +7 -0
  712. package/neural-interface/lib/claude-temporary.js +435 -0
  713. package/neural-interface/lib/cli-update-planner.js +312 -0
  714. package/neural-interface/lib/codex-capabilities.d.ts +41 -0
  715. package/neural-interface/lib/codex-capabilities.js +148 -0
  716. package/neural-interface/lib/codex-collaboration-mode.js +17 -0
  717. package/neural-interface/lib/codex-compat-rpc.js +241 -0
  718. package/neural-interface/lib/codex-mcp-approvals.js +56 -0
  719. package/neural-interface/lib/codex-mcp-apps.js +47 -0
  720. package/neural-interface/lib/codex-model-context.js +202 -0
  721. package/neural-interface/lib/codex-plan-store.js +71 -0
  722. package/neural-interface/lib/codex-request-journal.js +242 -0
  723. package/neural-interface/lib/codex-runtime-path.js +213 -0
  724. package/neural-interface/lib/codex-session-catalog.js +231 -0
  725. package/neural-interface/lib/codex-session-settings.js +46 -0
  726. package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
  727. package/neural-interface/lib/codex-thread-history.js +165 -0
  728. package/neural-interface/lib/codex-user-verification.js +34 -0
  729. package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
  730. package/neural-interface/lib/confined-fs.js +256 -0
  731. package/neural-interface/lib/db.js +922 -0
  732. package/neural-interface/lib/design-md.js +9 -0
  733. package/neural-interface/lib/desktop/api.js +92 -0
  734. package/neural-interface/lib/desktop/audit.js +62 -0
  735. package/neural-interface/lib/desktop/build.js +423 -0
  736. package/neural-interface/lib/desktop/config.js +606 -0
  737. package/neural-interface/lib/desktop/coords.js +76 -0
  738. package/neural-interface/lib/desktop/fake-helper.js +1192 -0
  739. package/neural-interface/lib/desktop/frames.js +77 -0
  740. package/neural-interface/lib/desktop/grants.js +84 -0
  741. package/neural-interface/lib/desktop/guards.js +170 -0
  742. package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
  743. package/neural-interface/lib/desktop/lease.js +51 -0
  744. package/neural-interface/lib/desktop/manager.js +545 -0
  745. package/neural-interface/lib/desktop/press-contexts.js +103 -0
  746. package/neural-interface/lib/desktop/protocol.js +495 -0
  747. package/neural-interface/lib/desktop/service.js +1202 -0
  748. package/neural-interface/lib/effort-levels.js +148 -0
  749. package/neural-interface/lib/event-loop-monitor.js +67 -0
  750. package/neural-interface/lib/exec-loop-command.js +209 -0
  751. package/neural-interface/lib/folder-picker.ps1 +77 -0
  752. package/neural-interface/lib/http-guards.js +287 -0
  753. package/neural-interface/lib/jsonl-lines.js +94 -0
  754. package/neural-interface/lib/loop-logger.js +127 -0
  755. package/neural-interface/lib/mcp-client-config.js +133 -0
  756. package/neural-interface/lib/mcp-installer.js +487 -0
  757. package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
  758. package/neural-interface/lib/mcp-profile-servers.js +120 -0
  759. package/neural-interface/lib/memo-cache.js +84 -0
  760. package/neural-interface/lib/memory-api.js +758 -0
  761. package/neural-interface/lib/memory-map-api.js +1150 -0
  762. package/neural-interface/lib/memory-map-layout.js +872 -0
  763. package/neural-interface/lib/memory-map-worker.js +165 -0
  764. package/neural-interface/lib/memory-session-capture.js +81 -0
  765. package/neural-interface/lib/memory-timers.js +62 -0
  766. package/neural-interface/lib/morelogin.js +301 -0
  767. package/neural-interface/lib/native-binary-runtime.js +382 -0
  768. package/neural-interface/lib/native-loop-goal.js +33 -0
  769. package/neural-interface/lib/native-loop-providers.js +1002 -0
  770. package/neural-interface/lib/native-loop-runtime.js +747 -0
  771. package/neural-interface/lib/offline-page.js +54 -0
  772. package/neural-interface/lib/opencode-hidden-models.js +113 -0
  773. package/neural-interface/lib/opencode-history.js +253 -0
  774. package/neural-interface/lib/opencode-runtime-config.js +58 -0
  775. package/neural-interface/lib/opencode-v2-client.js +486 -0
  776. package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
  777. package/neural-interface/lib/path-confine.js +213 -0
  778. package/neural-interface/lib/plugin-installer.js +264 -0
  779. package/neural-interface/lib/project-storage.js +766 -0
  780. package/neural-interface/lib/pty-host/core.js +603 -0
  781. package/neural-interface/lib/pty-host/flow.js +43 -0
  782. package/neural-interface/lib/pty-host/host.js +47 -0
  783. package/neural-interface/lib/pty-host/manager.js +360 -0
  784. package/neural-interface/lib/pty-host/protocol.js +81 -0
  785. package/neural-interface/lib/pty-host/snapshot.js +133 -0
  786. package/neural-interface/lib/remote-policy.js +582 -0
  787. package/neural-interface/lib/rulesets/api.js +175 -0
  788. package/neural-interface/lib/rulesets/fs-safe.js +245 -0
  789. package/neural-interface/lib/rulesets/installer.js +1453 -0
  790. package/neural-interface/lib/rulesets/json-edit.js +186 -0
  791. package/neural-interface/lib/rulesets/lock.js +170 -0
  792. package/neural-interface/lib/rulesets/managed-text.js +314 -0
  793. package/neural-interface/lib/rulesets/normalise.js +24 -0
  794. package/neural-interface/lib/rulesets/render.js +84 -0
  795. package/neural-interface/lib/schedule-launch-intent.js +40 -0
  796. package/neural-interface/lib/session-cache-builder.js +524 -0
  797. package/neural-interface/lib/session-chunker.js +344 -0
  798. package/neural-interface/lib/session-indexer.js +327 -0
  799. package/neural-interface/lib/session-title-generator.js +232 -0
  800. package/neural-interface/lib/skill-prompt-bridge.js +11 -0
  801. package/neural-interface/lib/style-guide/api.js +375 -0
  802. package/neural-interface/lib/style-guide/color.js +352 -0
  803. package/neural-interface/lib/style-guide/export-css.js +99 -0
  804. package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
  805. package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
  806. package/neural-interface/lib/style-guide/fonts.json +227 -0
  807. package/neural-interface/lib/style-guide/import.js +1268 -0
  808. package/neural-interface/lib/style-guide/pointers.js +103 -0
  809. package/neural-interface/lib/style-guide/presets.js +232 -0
  810. package/neural-interface/lib/style-guide/render-design-md.js +319 -0
  811. package/neural-interface/lib/style-guide/schema.js +848 -0
  812. package/neural-interface/lib/style-guide/security.js +43 -0
  813. package/neural-interface/lib/style-guide/store.js +491 -0
  814. package/neural-interface/lib/style-guide/summary.js +106 -0
  815. package/neural-interface/lib/style-guide/tokens.js +170 -0
  816. package/neural-interface/lib/synabun-update-plan.js +324 -0
  817. package/neural-interface/lib/synabun-version.js +179 -0
  818. package/neural-interface/lib/system-restore.js +99 -0
  819. package/neural-interface/lib/terminal-env.js +86 -0
  820. package/neural-interface/lib/ui-blob-store.js +106 -0
  821. package/neural-interface/lib/vector-cache.js +2 -0
  822. package/neural-interface/lib/virtual-socket.js +60 -0
  823. package/neural-interface/lib/whatsapp/api.js +146 -0
  824. package/neural-interface/lib/whatsapp/auth-store.js +389 -0
  825. package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
  826. package/neural-interface/lib/whatsapp/brain.js +128 -0
  827. package/neural-interface/lib/whatsapp/bridge.js +1499 -0
  828. package/neural-interface/lib/whatsapp/cards.js +519 -0
  829. package/neural-interface/lib/whatsapp/commands.js +92 -0
  830. package/neural-interface/lib/whatsapp/config.js +291 -0
  831. package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
  832. package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
  833. package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
  834. package/neural-interface/lib/whatsapp/connector/package.json +8 -0
  835. package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
  836. package/neural-interface/lib/whatsapp/format.js +552 -0
  837. package/neural-interface/lib/whatsapp/host-core.js +1225 -0
  838. package/neural-interface/lib/whatsapp/host.js +119 -0
  839. package/neural-interface/lib/whatsapp/identity.js +288 -0
  840. package/neural-interface/lib/whatsapp/inbound.js +149 -0
  841. package/neural-interface/lib/whatsapp/installer.js +562 -0
  842. package/neural-interface/lib/whatsapp/manager.js +638 -0
  843. package/neural-interface/lib/whatsapp/paths.js +81 -0
  844. package/neural-interface/lib/whatsapp/protocol.js +327 -0
  845. package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
  846. package/neural-interface/lib/whatsapp/redact.js +130 -0
  847. package/neural-interface/lib/whatsapp/service.js +1305 -0
  848. package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
  849. package/neural-interface/package-lock.json +4431 -0
  850. package/neural-interface/package.json +58 -27
  851. package/neural-interface/public/apple-touch-icon.png +0 -0
  852. package/neural-interface/public/category-logos/synabun.png +0 -0
  853. package/neural-interface/public/claude.svg +1 -0
  854. package/neural-interface/public/favicon-16x16.png +0 -0
  855. package/neural-interface/public/favicon-32x32.png +0 -0
  856. package/neural-interface/public/icon-192.png +0 -0
  857. package/neural-interface/public/icon-512.png +0 -0
  858. package/neural-interface/public/index.html +32 -182
  859. package/neural-interface/public/index2d.html +2 -2
  860. package/neural-interface/public/install-app.html +31 -0
  861. package/neural-interface/public/logo-synabun.png +0 -0
  862. package/neural-interface/public/logoHD.png +0 -0
  863. package/neural-interface/public/manifest.json +2 -0
  864. package/neural-interface/public/mcp.svg +4 -0
  865. package/neural-interface/public/offline.html +670 -299
  866. package/neural-interface/public/onboarding.html +804 -161
  867. package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
  868. package/neural-interface/public/shared/agent-runtime-options.js +596 -0
  869. package/neural-interface/public/shared/api.js +339 -28
  870. package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
  871. package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
  872. package/neural-interface/public/shared/assistant/asst-api.js +270 -0
  873. package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
  874. package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
  875. package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
  876. package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
  877. package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
  878. package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
  879. package/neural-interface/public/shared/assistant/asst-mascot.js +313 -0
  880. package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
  881. package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
  882. package/neural-interface/public/shared/assistant/asst-rack-styles.js +586 -0
  883. package/neural-interface/public/shared/assistant/asst-render.js +3305 -0
  884. package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
  885. package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
  886. package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
  887. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
  888. package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
  889. package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
  890. package/neural-interface/public/shared/assistant/asst-state.js +727 -0
  891. package/neural-interface/public/shared/assistant/asst-styles.js +1686 -0
  892. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
  893. package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
  894. package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
  895. package/neural-interface/public/shared/automation-context.js +75 -0
  896. package/neural-interface/public/shared/blob-store.js +99 -0
  897. package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
  898. package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
  899. package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
  900. package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
  901. package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
  902. package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
  903. package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
  904. package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
  905. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
  906. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
  907. package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
  908. package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
  909. package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
  910. package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
  911. package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
  912. package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
  913. package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
  914. package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
  915. package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
  916. package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
  917. package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
  918. package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
  919. package/neural-interface/public/shared/cli-status.js +169 -0
  920. package/neural-interface/public/shared/command-runner-model.js +30 -0
  921. package/neural-interface/public/shared/constants.js +22 -5
  922. package/neural-interface/public/shared/cp/cp-agents.js +186 -0
  923. package/neural-interface/public/shared/cp/cp-bash.js +252 -0
  924. package/neural-interface/public/shared/cp/cp-compose.js +159 -0
  925. package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
  926. package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
  927. package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
  928. package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
  929. package/neural-interface/public/shared/cp/cp-diff.js +255 -0
  930. package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
  931. package/neural-interface/public/shared/cp/cp-events.js +846 -0
  932. package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
  933. package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
  934. package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
  935. package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
  936. package/neural-interface/public/shared/cp/cp-restore.js +224 -0
  937. package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
  938. package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
  939. package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
  940. package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
  941. package/neural-interface/public/shared/cp/cp-styles.js +349 -0
  942. package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
  943. package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
  944. package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
  945. package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
  946. package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
  947. package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
  948. package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
  949. package/neural-interface/public/shared/graph-activity.js +68 -0
  950. package/neural-interface/public/shared/html-shell.js +116 -17
  951. package/neural-interface/public/shared/i18n.js +132 -34
  952. package/neural-interface/public/shared/node-positions-store.js +44 -0
  953. package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
  954. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
  955. package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
  956. package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
  957. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
  958. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
  959. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
  960. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
  961. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
  962. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
  963. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
  964. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
  965. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
  966. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
  967. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
  968. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
  969. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
  970. package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
  971. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
  972. package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
  973. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
  974. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
  975. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
  976. package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
  977. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
  978. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
  979. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
  980. package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
  981. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
  982. package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
  983. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
  984. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
  985. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
  986. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
  987. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
  988. package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
  989. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
  990. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
  991. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
  992. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
  993. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
  994. package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
  995. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
  996. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
  997. package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
  998. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
  999. package/neural-interface/public/shared/provider-icons.js +30 -0
  1000. package/neural-interface/public/shared/pwa-install-page.js +34 -0
  1001. package/neural-interface/public/shared/pwa-install.css +12 -0
  1002. package/neural-interface/public/shared/pwa-install.js +28 -0
  1003. package/neural-interface/public/shared/session-title.js +100 -0
  1004. package/neural-interface/public/shared/settings/settings-ia.js +389 -0
  1005. package/neural-interface/public/shared/settings/settings-kit.js +234 -0
  1006. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  1007. package/neural-interface/public/shared/settings/settings-search.js +463 -0
  1008. package/neural-interface/public/shared/settings/settings-shell.js +570 -0
  1009. package/neural-interface/public/shared/start-bridge.js +583 -0
  1010. package/neural-interface/public/shared/state.js +5 -0
  1011. package/neural-interface/public/shared/storage.js +50 -7
  1012. package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
  1013. package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
  1014. package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
  1015. package/neural-interface/public/shared/styleguide/sg-colors.js +151 -0
  1016. package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
  1017. package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
  1018. package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
  1019. package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
  1020. package/neural-interface/public/shared/styleguide/sg-import-export.js +48 -0
  1021. package/neural-interface/public/shared/styleguide/sg-kit.js +284 -0
  1022. package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
  1023. package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
  1024. package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
  1025. package/neural-interface/public/shared/styleguide/sg-panel.js +237 -0
  1026. package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
  1027. package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
  1028. package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
  1029. package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
  1030. package/neural-interface/public/shared/styleguide/sg-typography.js +125 -0
  1031. package/neural-interface/public/shared/styles.css +8614 -1477
  1032. package/neural-interface/public/shared/synabun-mascot.js +1625 -0
  1033. package/neural-interface/public/shared/synabun-ticker.js +376 -0
  1034. package/neural-interface/public/shared/term-ready.js +307 -0
  1035. package/neural-interface/public/shared/term-renderer.js +343 -0
  1036. package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
  1037. package/neural-interface/public/shared/ui-assistant.js +77 -0
  1038. package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
  1039. package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
  1040. package/neural-interface/public/shared/ui-codex-panel.js +12 -0
  1041. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  1042. package/neural-interface/public/shared/ui-cost-widget.js +55 -34
  1043. package/neural-interface/public/shared/ui-detail.js +77 -14
  1044. package/neural-interface/public/shared/ui-explorer.js +360 -181
  1045. package/neural-interface/public/shared/ui-file-explorer.js +485 -107
  1046. package/neural-interface/public/shared/ui-help.js +2 -2
  1047. package/neural-interface/public/shared/ui-image-gallery.js +45 -21
  1048. package/neural-interface/public/shared/ui-judgments.js +1346 -0
  1049. package/neural-interface/public/shared/ui-keybinds.js +3 -2
  1050. package/neural-interface/public/shared/ui-link.js +105 -51
  1051. package/neural-interface/public/shared/ui-loading.js +222 -11
  1052. package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
  1053. package/neural-interface/public/shared/ui-menubar.js +100 -31
  1054. package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
  1055. package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
  1056. package/neural-interface/public/shared/ui-native-window-id.js +7 -0
  1057. package/neural-interface/public/shared/ui-navbar.js +357 -222
  1058. package/neural-interface/public/shared/ui-notifications.js +58 -12
  1059. package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
  1060. package/neural-interface/public/shared/ui-panels.js +15 -7
  1061. package/neural-interface/public/shared/ui-resume.js +823 -653
  1062. package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
  1063. package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
  1064. package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
  1065. package/neural-interface/public/shared/ui-sessions.js +789 -34
  1066. package/neural-interface/public/shared/ui-settings.js +5774 -2194
  1067. package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
  1068. package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
  1069. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
  1070. package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
  1071. package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
  1072. package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
  1073. package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
  1074. package/neural-interface/public/shared/ui-skills.js +18 -32
  1075. package/neural-interface/public/shared/ui-styleguide.js +35 -0
  1076. package/neural-interface/public/shared/ui-sync.js +87 -0
  1077. package/neural-interface/public/shared/ui-terminal.js +7863 -6385
  1078. package/neural-interface/public/shared/ui-tool-updates.js +292 -0
  1079. package/neural-interface/public/shared/ui-tooltip.js +156 -15
  1080. package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
  1081. package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
  1082. package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
  1083. package/neural-interface/public/shared/ui-tutorial.css +102 -0
  1084. package/neural-interface/public/shared/ui-tutorial.js +557 -1076
  1085. package/neural-interface/public/shared/ui-update.js +420 -68
  1086. package/neural-interface/public/shared/ui-whatsapp.js +941 -0
  1087. package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
  1088. package/neural-interface/public/shared/ui-workspaces.js +2 -1
  1089. package/neural-interface/public/shared/utils.js +58 -0
  1090. package/neural-interface/public/shared/vterm-buffer.js +15 -5
  1091. package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
  1092. package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
  1093. package/neural-interface/public/sidepanel-session.html +18 -0
  1094. package/neural-interface/public/sw.js +10 -2
  1095. package/neural-interface/public/synabun.png +0 -0
  1096. package/neural-interface/public/variant/2d/graph.js +1707 -1635
  1097. package/neural-interface/public/variant/2d/lasso.js +2 -1
  1098. package/neural-interface/public/variant/2d/layout.js +595 -594
  1099. package/neural-interface/public/variant/2d/main.js +557 -521
  1100. package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
  1101. package/neural-interface/public/variant/3d/main.js +517 -500
  1102. package/neural-interface/public/variant/3d/map-controls.js +144 -0
  1103. package/neural-interface/public/variant/3d/map-data.js +251 -0
  1104. package/neural-interface/public/variant/3d/map-labels.js +241 -0
  1105. package/neural-interface/public/variant/3d/map-pick.js +90 -0
  1106. package/neural-interface/public/variant/3d/map-renderer.js +521 -0
  1107. package/neural-interface/public/variant/3d/map-shaders.js +158 -0
  1108. package/neural-interface/public/variant/3d/map-view.js +618 -0
  1109. package/neural-interface/public/vendor/three/manifest.json +3 -0
  1110. package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
  1111. package/neural-interface/public/vendor/xterm/manifest.json +3 -0
  1112. package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
  1113. package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
  1114. package/neural-interface/scripts/build-vendor.mjs +64 -0
  1115. package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
  1116. package/neural-interface/scripts/rebuild-pty.js +39 -22
  1117. package/neural-interface/scripts/vendor-three-entry.js +24 -0
  1118. package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
  1119. package/neural-interface/server.js +33983 -15000
  1120. package/neural-interface/skins/default/skin.css +90 -0
  1121. package/neural-interface/skins/default/skin.json +8 -0
  1122. package/neural-interface/templates/rulesets/README.md +9 -0
  1123. package/neural-interface/templates/rulesets/claude.md +3 -0
  1124. package/neural-interface/templates/rulesets/codex.md +2 -0
  1125. package/neural-interface/templates/rulesets/coexistence.md +6 -0
  1126. package/neural-interface/templates/rulesets/core.md +26 -0
  1127. package/neural-interface/templates/rulesets/cursor.md +2 -0
  1128. package/neural-interface/templates/rulesets/gemini.md +2 -0
  1129. package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
  1130. package/neural-interface/templates/rulesets/manifest.json +12 -0
  1131. package/neural-interface/templates/rulesets/opencode.md +2 -0
  1132. package/package.json +131 -53
  1133. package/postinstall.js +55 -26
  1134. package/preuninstall.js +187 -0
  1135. package/scripts/restore-data-home-from-backup.mjs +239 -0
  1136. package/setup.js +872 -260
  1137. package/skills/leonardo/SKILL.md +19 -10
  1138. package/skills/leonardo/modules/image-prompter.md +18 -12
  1139. package/skills/leonardo/modules/video-prompter.md +19 -13
  1140. package/skills/synabun/SKILL.md +209 -185
  1141. package/skills/synabun/icon.png +0 -0
  1142. package/skills/synabun/modules/audit.md +380 -374
  1143. package/skills/synabun/modules/changelog.md +15 -6
  1144. package/skills/synabun/modules/idea.md +148 -144
  1145. package/skills/synabun/modules/memorize.md +13 -4
  1146. package/skills/synabun/modules/schedule.md +198 -0
  1147. package/skills/synabun/synabunicon.png +0 -0
  1148. package/updater.cmd +31 -0
  1149. package/updater.mjs +382 -0
  1150. package/updater.sh +29 -0
  1151. package/hooks/claude-code/test-user-learning.mjs +0 -531
  1152. package/neural-interface/public/claude-chat.css +0 -878
  1153. package/neural-interface/public/claude-chat.html +0 -92
  1154. package/neural-interface/public/claude-chat.js +0 -1168
  1155. package/neural-interface/public/index-legacy.html +0 -14677
  1156. package/neural-interface/public/index2d-legacy.html +0 -8193
  1157. package/neural-interface/public/shared/html-term-renderer.js +0 -1022
  1158. package/neural-interface/public/variant/3d/background.js +0 -884
  1159. package/neural-interface/public/variant/3d/camera.js +0 -661
  1160. package/neural-interface/public/variant/3d/gfx.js +0 -116
  1161. package/neural-interface/public/variant/3d/graph.js +0 -2399
  1162. package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
  1163. package/neural-interface/templates/CLAUDE-template.md +0 -211
@@ -0,0 +1,1324 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — Assistant router (DeepSeek-Harness-style request routing)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // The assistant's CURRENT brain model is the planner: it classifies each
6
+ // actionable request and proposes where it should run (here on its own model,
7
+ // or dispatched to a Claude / Codex / OpenCode worker model). This module
8
+ // never classifies anything itself. It enforces the per-session route mode:
9
+ //
10
+ // always-ask every actionable task becomes a route card the user answers
11
+ // ask-unsure a card only when the brain is unsure, names an unknown model,
12
+ // omits one, picks a model that cannot see an image task, or
13
+ // one that cannot make the image / video a creation task needs
14
+ // never the user's saved route for the task class runs at once (it
15
+ // binds over the brain's pick); without one, the brain's pick
16
+ // runs (invalid picks are corrected)
17
+ //
18
+ // Image creation and Video creation (`requires`, `dispatchOnly`) always run on a
19
+ // worker whose model generates that medium (catalog `outputs`): any other pick is
20
+ // corrected or disabled, and with no such model anywhere agent_route declines
21
+ // (no_capable_model) instead of asking. Design (`vision`, `dispatchOnly`) works
22
+ // the same way on a worker whose model can see: only a confirmed `vision: true`
23
+ // on a listed row counts, as only a confirmed output does, so a provider whose
24
+ // catalog is unavailable (OpenCode down) has models that are unknown, not
25
+ // disabled — they neither bind nor run design — a claude-* id the list lacks is
26
+ // unknown unless it resolves to a listed row, and with no catalog value nothing
27
+ // qualifies.
28
+ //
29
+ // It validates targets against the catalog, remembers per-class preferences,
30
+ // holds un-routed dispatches until the user picks (dispatcher `awaiting_route`)
31
+ // and suggests one-tier escalations when a run fails or reports blocked.
32
+ // All I/O is injected (catalog, config store, sinks into the runtime/dispatcher).
33
+
34
+ import { randomBytes } from 'node:crypto';
35
+ import { CATALOG_PROVIDERS, TIERS, defaultIsHidden, defaultModelRow, findModel, hiddenModelId, priceText, providerAllHidden, providerEmpty, rowIsHidden, rowMakes } from './assistant-catalog.js';
36
+ import { effectiveRouting, normalizeRouteMode } from './assistant-config.js';
37
+ import { normalizeEffort } from './effort-levels.js';
38
+
39
+ export const TASK_CLASS_META = Object.freeze({
40
+ chat: { label: 'Chat', description: 'Conversation and questions answered from knowledge or memory', defaultKind: 'direct' },
41
+ quick: { label: 'Quick task', description: 'Small edits, one-off shell commands, lookups', defaultKind: 'direct' },
42
+ code: { label: 'Coding', description: 'A feature or bug fix inside one repository', defaultKind: 'dispatch' },
43
+ complex: { label: 'Complex engineering', description: 'Architecture, cross-file refactors, hard debugging', defaultKind: 'dispatch' },
44
+ review: { label: 'Review', description: 'A second model reviews code, a plan or a result', defaultKind: 'dispatch' },
45
+ research: { label: 'Research', description: 'Web or codebase research and summaries', defaultKind: 'direct' },
46
+ browser: { label: 'Browser', description: 'Web tasks in the SynaBun browser', defaultKind: 'direct' },
47
+ computer: { label: 'Computer use', description: 'Operate desktop apps on this Mac (needs a model that can see)', defaultKind: 'direct' },
48
+ automation: { label: 'Automation', description: 'Loops, schedules and recurring work', defaultKind: 'direct' },
49
+ // Always one worker on a model that can see, with SynaBun's design rules (`playbook`, assistant-playbooks.js);
50
+ // its screenshots and images are collected like generated ones (`collects`), but it must make none.
51
+ design: { label: 'Design', description: 'UI/UX research, design systems, mockups and prototypes', defaultKind: 'dispatch', vision: true, collects: 'image', playbook: 'design', dispatchOnly: true },
52
+ // Made by the model itself (Codex's built-in image tool, an OpenCode model that outputs the medium), always on a worker.
53
+ image_gen: { label: 'Image creation', description: 'Create images with a model that generates them', defaultKind: 'dispatch', requires: 'image', dispatchOnly: true },
54
+ video_gen: { label: 'Video creation', description: 'Create videos with a model that generates them', defaultKind: 'dispatch', requires: 'video', dispatchOnly: true },
55
+ });
56
+ export const TASK_CLASSES = Object.freeze(Object.keys(TASK_CLASS_META));
57
+ export const PREFERENCE_KEYS = Object.freeze([...TASK_CLASSES, 'vision']);
58
+ export const ROUTE_REASONS = Object.freeze(['always-ask', 'low_confidence', 'no_confidence', 'model_missing', 'model_unknown', 'model_disabled', 'model_unavailable', 'needs_vision', 'needs_output', 'no_capable_model']);
59
+ export const MODEL_DISABLED_TEXT = "Disabled in the Assistant's Models list.";
60
+ const VISION_CLASSES = new Set(['computer']);
61
+ const MEDIUM_WORDS = { image: 'images', video: 'videos' };
62
+ /** The medium ('image' | 'video') a task class creates, else null. */
63
+ export function requiredOutput(taskClass) { return TASK_CLASS_META[taskClass]?.requires || null; }
64
+ /** "This model can't generate images." */
65
+ export function cannotMakeText(medium) { return `This model can't generate ${MEDIUM_WORDS[medium] || medium}.`; }
66
+ /**
67
+ * Does the class always run on a model that can see (Design)? Like image / video creation's
68
+ * outputs (rowMakes), only a confirmed `vision: true` counts; an unknown `null` never does.
69
+ * Computer use's softer check (VISION_CLASSES) only refuses a confirmed `false`.
70
+ */
71
+ export function requiresVision(taskClass) { return TASK_CLASS_META[taskClass]?.vision === true; }
72
+ export const CANNOT_SEE_TEXT = "This model can't see images.";
73
+ export const UNKNOWN_SIGHT_TEXT = "This model isn't known to see images.";
74
+ export const UNAVAILABLE_TEXT = "This model isn't available right now.";
75
+ /** The card / answer text for a model design cannot use (a designFit() verdict). */
76
+ function fitText(fit) { return fit === 'blind' ? CANNOT_SEE_TEXT : fit === 'unavailable' ? UNAVAILABLE_TEXT : UNKNOWN_SIGHT_TEXT; }
77
+ /** What a run of the class collects into its media: the medium it makes, else what it gathers (Design's screenshots). */
78
+ export function collectedMedia(taskClass) { return requiredOutput(taskClass) || TASK_CLASS_META[taskClass]?.collects || null; }
79
+ /** The playbook a run of the class gets (assistant-playbooks.js), else null. */
80
+ export function classPlaybook(taskClass) { return TASK_CLASS_META[taskClass]?.playbook || null; }
81
+ /**
82
+ * What the class changes about a worker run besides its model (the medium it must make, the
83
+ * playbook it gets), else null. A route approved for one cannot carry a dispatch of another
84
+ * (the dispatcher's ROUTE_CLASS_MISMATCH).
85
+ */
86
+ export function runContract(taskClass) { return requiredOutput(taskClass) || classPlaybook(taskClass); }
87
+ const TIER_RANK = { small: 0, medium: 1, large: 2 };
88
+ const MAX_ESCALATION_DEPTH = 2;
89
+ const DECIDED_TTL_MS = 60 * 60_000;
90
+
91
+ function clip(value, max = 200) {
92
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
93
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
94
+ }
95
+ function routeError(code, message, status = 400, extra = {}) {
96
+ const error = new Error(message);
97
+ error.code = code;
98
+ error.status = status;
99
+ Object.assign(error, extra);
100
+ return error;
101
+ }
102
+ function tierRank(tier) { return TIER_RANK[tier] ?? 1; }
103
+ function outPrice(row) { const n = Number(row?.price?.output); return Number.isFinite(n) ? n : Number.POSITIVE_INFINITY; }
104
+ function sameTarget(a, b) {
105
+ if (!a || !b) return false;
106
+ return a.kind === b.kind && a.provider === b.provider && String(a.model || '') === String(b.model || '') && String(a.effort || '') === String(b.effort || '');
107
+ }
108
+
109
+ /**
110
+ * A target on a model the user hid (Assistant → Models, or Settings → OpenCode
111
+ * for OpenCode), or a dispatch with no model onto a provider whose every
112
+ * model is hidden.
113
+ */
114
+ function isDisabled(catalog, target) {
115
+ if (!target?.provider) return false;
116
+ // A named model is unusable when hidden, or when its provider answered with nothing connected
117
+ // (an unavailable catalog is not an empty one: that stays lenient).
118
+ if (target.model) return !!hiddenModelId(catalog, target.provider, target.model) || providerEmpty(catalog, target.provider);
119
+ // No model named: nothing to run when every model is hidden, or the provider has none connected.
120
+ return target.kind === 'dispatch' && (providerAllHidden(catalog, target.provider) || providerEmpty(catalog, target.provider));
121
+ }
122
+ /** The provider's rows minus the hidden ones (a hand-built catalog may still carry them). */
123
+ function visibleRows(catalog, provider) {
124
+ return (catalog?.models?.[provider] || []).filter((row) => !rowIsHidden(catalog, { ...row, provider: row.provider || provider }));
125
+ }
126
+ /** A dispatch with no model whose provider default is hidden runs on this row instead. */
127
+ function pinnedDefault(catalog, target) {
128
+ if (target?.kind !== 'dispatch' || target.model || !defaultIsHidden(catalog, target.provider)) return null;
129
+ return defaultModelRow(catalog, target.provider);
130
+ }
131
+ /** Does the model `target` runs on (its provider default when it names none) generate `medium`? Unknown never counts. */
132
+ export function targetMakes(catalog, target, medium) {
133
+ if (!medium) return true;
134
+ if (!target?.provider) return false;
135
+ const row = target.model ? findModel(catalog, target.provider, target.model)?.row : defaultModelRow(catalog, target.provider);
136
+ return rowMakes(row, medium);
137
+ }
138
+ /** The provider's enabled, available rows that generate `medium`, cheapest first (price, then tier). */
139
+ function makerRows(catalog, provider, medium) {
140
+ return visibleRows(catalog, provider).filter((row) => row.status !== 'unavailable' && rowMakes(row, medium))
141
+ .sort((a, b) => outPrice(a) - outPrice(b) || tierRank(a.tier) - tierRank(b.tier));
142
+ }
143
+ /** Is any enabled model in the catalog able to generate `medium`? */
144
+ export function anyMaker(catalog, medium) {
145
+ return CATALOG_PROVIDERS.some((provider) => makerRows(catalog, provider, medium).length > 0);
146
+ }
147
+ /**
148
+ * Can a design run use `target`? 'ok', 'blind' (vision false), 'unknown' (vision not known) or
149
+ * 'unavailable' (its catalog row is marked unavailable, which seeingRows never picks either).
150
+ * A claude-* id the catalog does not list (findModel 'unlisted', which other classes take as
151
+ * seeing) is 'unknown': design needs a listed row, so such an id counts only when the catalog
152
+ * resolves it to one ("claude-opus-5-5" → the opus row), and then that row decides.
153
+ * Other classes do not look at availability.
154
+ */
155
+ export function designFit(catalog, target, { brain = {}, brainInfo = null } = {}) {
156
+ const hit = target?.model ? findModel(catalog, target.provider, target.model) : null;
157
+ if (hit?.match === 'unlisted') return 'unknown';
158
+ const vision = describeTarget(target, { catalog, brain, brainInfo })?.vision;
159
+ if (vision === false) return 'blind';
160
+ if (vision !== true) return 'unknown';
161
+ const row = target?.model ? hit?.row : defaultModelRow(catalog, target?.provider);
162
+ return row?.status === 'unavailable' ? 'unavailable' : 'ok';
163
+ }
164
+ /** The provider's enabled, available rows that can see, cheapest first (price, then tier). */
165
+ function seeingRows(catalog, provider) {
166
+ return visibleRows(catalog, provider).filter((row) => row.status !== 'unavailable' && row.vision === true)
167
+ .sort((a, b) => outPrice(a) - outPrice(b) || tierRank(a.tier) - tierRank(b.tier));
168
+ }
169
+ /** Is any enabled model in the catalog able to see? */
170
+ export function anySeer(catalog) {
171
+ return CATALOG_PROVIDERS.some((provider) => seeingRows(catalog, provider).length > 0);
172
+ }
173
+
174
+ export function normalizeConfidence(value) {
175
+ if (value === null || value === undefined || value === '') return null;
176
+ let n = Number(value);
177
+ if (!Number.isFinite(n)) return null;
178
+ if (n > 1 && n <= 100) n /= 100;
179
+ return Math.min(1, Math.max(0, n));
180
+ }
181
+
182
+ /**
183
+ * Normalize the brain's proposal (agent_route body, or a dispatch spec reshaped
184
+ * into one). A "direct" proposal on another provider is really a dispatch; for a
185
+ * dispatch-only class (image / video creation, design) every proposal is a dispatch,
186
+ * a "here" one on the brain's own model (`coercedDirect` records the first).
187
+ * Design (`visionOnly`) also always needs a model that can see.
188
+ */
189
+ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
190
+ const rawClass = String(body.task_class ?? body.taskClass ?? '').trim().toLowerCase();
191
+ const taskClass = TASK_CLASSES.includes(rawClass) ? rawClass : 'quick';
192
+ const meta = TASK_CLASS_META[taskClass];
193
+ const confidence = normalizeConfidence(body.confidence);
194
+ const visionOnly = meta.vision === true;
195
+ const needsVision = !!(body.needs_vision ?? body.needsVision) || VISION_CLASSES.has(taskClass) || visionOnly;
196
+ const needsOutput = meta.requires || null;
197
+ const dispatchOnly = meta.dispatchOnly === true;
198
+ const proposals = [];
199
+ let coercedDirect = false;
200
+ for (const raw of Array.isArray(body.proposals) ? body.proposals.slice(0, 3) : []) {
201
+ if (!raw || typeof raw !== 'object') continue;
202
+ let kind = raw.kind === 'direct' ? 'direct' : 'dispatch';
203
+ let provider = CATALOG_PROVIDERS.includes(raw.provider) ? raw.provider : null;
204
+ let model = raw.model ? String(raw.model).trim().slice(0, 200) : null;
205
+ if (kind === 'direct') {
206
+ if (provider && provider !== brain.provider) kind = 'dispatch';
207
+ else provider = brain.provider || provider;
208
+ }
209
+ if (!provider) continue;
210
+ if (dispatchOnly && kind === 'direct') {
211
+ kind = 'dispatch';
212
+ model = model || brain.model || null;
213
+ if (!proposals.length) coercedDirect = true;
214
+ }
215
+ proposals.push({
216
+ kind, provider, model,
217
+ effort: raw.effort ? String(raw.effort).trim().slice(0, 40) : null,
218
+ reason: raw.reason ? clip(raw.reason, 240) : null,
219
+ });
220
+ }
221
+ if (!proposals.length) {
222
+ const kind = meta.defaultKind;
223
+ proposals.push({ kind, provider: brain.provider || 'claude-code', model: kind === 'direct' ? (brain.model || null) : null, effort: null, reason: null });
224
+ }
225
+ const request = { taskClass, taskClassLabel: meta.label, summary: clip(body.summary || '', 400), confidence, needsVision, proposals };
226
+ if (needsOutput || dispatchOnly) Object.assign(request, { needsOutput, dispatchOnly, coercedDirect });
227
+ if (visionOnly) request.visionOnly = true;
228
+ return request;
229
+ }
230
+
231
+ /** Catalog-backed description of a target: label, tier, vision, outputs, price, context. */
232
+ export function describeTarget(target, { catalog = null, brain = {}, brainInfo = null } = {}) {
233
+ if (!target) return null;
234
+ const isBrainModel = target.kind === 'direct' && target.provider === brain.provider && String(target.model || '') === String(brain.model || '');
235
+ let row = null;
236
+ let match = null;
237
+ if (target.model) { const hit = findModel(catalog, target.provider, target.model); row = hit?.row || null; match = hit?.match || null; }
238
+ else row = defaultModelRow(catalog, target.provider);
239
+ const info = isBrainModel && brainInfo ? brainInfo : null;
240
+ const label = info?.label || row?.label || target.model || `${target.provider} default`;
241
+ const unlistedClaude = target.provider === 'claude-code' && match === 'unlisted';
242
+ return {
243
+ kind: target.kind, provider: target.provider, model: target.model || null, effort: target.effort || null,
244
+ label, tier: info?.tier || row?.tier || null,
245
+ vision: info ? info.vision ?? null : (row ? row.vision ?? null : (unlistedClaude ? true : null)),
246
+ // What it generates besides text (image / video creation needs a true one).
247
+ outputs: row ? row.outputs ?? null : (unlistedClaude ? { image: false, video: false } : null),
248
+ price: info?.price || row?.price || null, contextWindow: info?.contextWindow || row?.contextWindow || null,
249
+ validated: match || (row ? 'default' : (target.model ? (isDisabled(catalog, target) ? 'disabled' : 'unknown') : 'default')),
250
+ };
251
+ }
252
+
253
+ /**
254
+ * `target` with its effort checked against its model (effort-levels.js): an
255
+ * unsupported level becomes the nearest one the model runs, and the change is
256
+ * pushed onto `corrections` as { field: 'effort', from, to, reason: 'effort_unsupported' }.
257
+ */
258
+ export function effortChecked(target, catalog, corrections = null) {
259
+ if (!target?.effort) return target;
260
+ const { effort, corrected } = normalizeEffort({ provider: target.provider, model: target.model || null, effort: target.effort, catalog });
261
+ if (corrected) corrections?.push({ field: 'effort', from: corrected.from, to: corrected.to, reason: 'effort_unsupported' });
262
+ return effort === target.effort ? target : { ...target, effort };
263
+ }
264
+
265
+ /**
266
+ * A saved route as a target. `dispatchOnly` (image / video creation): a saved
267
+ * "here" becomes a dispatch onto the same model (the brain's, when it named none).
268
+ */
269
+ function preferenceTarget(pref, brain, catalog = null, { dispatchOnly = false } = {}) {
270
+ if (!pref?.provider) return null;
271
+ const here = pref.kind === 'direct' && pref.provider === brain.provider;
272
+ const kind = here && !dispatchOnly ? 'direct' : 'dispatch';
273
+ const model = pref.model || (here && dispatchOnly ? brain.model || null : null);
274
+ const target = { kind, provider: pref.provider, model, effort: pref.effort || null };
275
+ // A remembered route onto a model the user since disabled is not applied.
276
+ return catalog && isDisabled(catalog, target) ? null : target;
277
+ }
278
+
279
+ /** "here on <model>" / "<provider>/<model|default>", with " (<effort>)". */
280
+ export function routeText(target) {
281
+ if (!target) return 'none';
282
+ const where = target.kind === 'direct' ? `here${target.model ? ` on ${target.model}` : ''}` : `${target.provider}/${target.model || 'default'}`;
283
+ return `${where}${target.effort ? ` (${target.effort})` : ''}`;
284
+ }
285
+
286
+ /** Cheapest vision-capable model, same provider first. */
287
+ function visionFallback(catalog, provider, { brain = {}, routing = {} } = {}) {
288
+ const pref = preferenceTarget(routing.preferences?.vision, brain, catalog);
289
+ if (pref) return pref;
290
+ const rows = visibleRows(catalog, provider).filter((row) => row.vision === true && row.status !== 'unavailable');
291
+ const sorted = [...rows].sort((a, b) => outPrice(a) - outPrice(b));
292
+ if (sorted.length) return { kind: provider === brain.provider ? 'direct' : 'dispatch', provider, model: sorted[0].id, effort: null };
293
+ for (const other of CATALOG_PROVIDERS) {
294
+ if (other === provider) continue;
295
+ const alt = visibleRows(catalog, other).filter((row) => row.vision === true).sort((a, b) => outPrice(a) - outPrice(b))[0];
296
+ if (alt) return { kind: 'dispatch', provider: other, model: alt.id, effort: null };
297
+ }
298
+ return null;
299
+ }
300
+
301
+ /**
302
+ * A worker model that generates `medium` for an image / video creation task:
303
+ * the class's saved route when it can, else the cheapest maker on `provider`,
304
+ * else the cheapest maker anywhere (price, then tier). With `needsVision` (a
305
+ * reference image the worker must see) every requirement counts together: the
306
+ * first of those that can see, else one whose vision is unknown, else the
307
+ * first. null when no model makes the medium.
308
+ */
309
+ function generationFallback(catalog, provider, medium, { brain = {}, routing = {}, taskClass = null, needsVision = false } = {}) {
310
+ const candidates = [];
311
+ const pref = preferenceTarget(routing.preferences?.[taskClass], brain, catalog, { dispatchOnly: true });
312
+ if (pref && targetMakes(catalog, pref, medium)) candidates.push(pref);
313
+ if (provider) for (const row of makerRows(catalog, provider, medium)) candidates.push({ kind: 'dispatch', provider, model: row.id, effort: null });
314
+ const rank = (p) => CATALOG_PROVIDERS.indexOf(p);
315
+ CATALOG_PROVIDERS.filter((p) => p !== provider)
316
+ .flatMap((p) => makerRows(catalog, p, medium).map((row) => ({ provider: p, row })))
317
+ .sort((a, b) => outPrice(a.row) - outPrice(b.row) || tierRank(a.row.tier) - tierRank(b.row.tier) || rank(a.provider) - rank(b.provider))
318
+ .forEach(({ provider: p, row }) => candidates.push({ kind: 'dispatch', provider: p, model: row.id, effort: null }));
319
+ if (!needsVision) return candidates[0] || null;
320
+ const sight = (target) => describeTarget(target, { catalog, brain })?.vision;
321
+ return candidates.find((t) => sight(t) === true) || candidates.find((t) => sight(t) !== false) || candidates[0] || null;
322
+ }
323
+
324
+ /**
325
+ * A worker model that can see for a Design task: the class's saved route when it
326
+ * can, else the cheapest model on `provider` that can, else the cheapest anywhere
327
+ * (price, then tier). null when no model can see.
328
+ */
329
+ function sightFallback(catalog, provider, { brain = {}, routing = {}, taskClass = null } = {}) {
330
+ const pref = preferenceTarget(routing.preferences?.[taskClass], brain, catalog, { dispatchOnly: true });
331
+ if (pref && designFit(catalog, pref, { brain }) === 'ok') return pref;
332
+ const own = provider ? seeingRows(catalog, provider)[0] : null;
333
+ if (own) return { kind: 'dispatch', provider, model: own.id, effort: null };
334
+ const rank = (p) => CATALOG_PROVIDERS.indexOf(p);
335
+ const alt = CATALOG_PROVIDERS.filter((p) => p !== provider)
336
+ .flatMap((p) => seeingRows(catalog, p).map((row) => ({ provider: p, row })))
337
+ .sort((a, b) => outPrice(a.row) - outPrice(b.row) || tierRank(a.row.tier) - tierRank(b.row.tier) || rank(a.provider) - rank(b.provider))[0];
338
+ return alt ? { kind: 'dispatch', provider: alt.provider, model: alt.row.id, effort: null } : null;
339
+ }
340
+
341
+ /**
342
+ * Decide whether the user must be asked. Returns
343
+ * { ask, reasons[], auto: target|null, proposal: target, corrections[], source }.
344
+ * `auto` is the target to run when ask === false. For image / video creation
345
+ * with no model in the catalog that makes the medium, and for design with none
346
+ * that can see, it returns { ask: false, unavailable: 'no_capable_model', auto: null }
347
+ * in every mode.
348
+ */
349
+ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainInfo = null, catalog = null }) {
350
+ const reasons = [];
351
+ const corrections = [];
352
+ const first = request.proposals[0];
353
+ let proposal = { kind: first.kind, provider: first.provider, model: first.model, effort: first.effort };
354
+ let source = 'brain';
355
+ if (request.taskClass === 'chat') {
356
+ return { ask: false, reasons, auto: { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null }, proposal, corrections, source };
357
+ }
358
+ const label = TASK_CLASS_META[request.taskClass]?.label || request.taskClass;
359
+ // Image / video creation runs only on a worker whose model generates the medium; design on one that can see.
360
+ const medium = request.needsOutput || null;
361
+ const dispatchOnly = request.dispatchOnly === true;
362
+ const visionOnly = request.visionOnly === true;
363
+ if ((medium && !anyMaker(catalog, medium)) || (visionOnly && !anySeer(catalog))) {
364
+ return { ask: false, unavailable: 'no_capable_model', reasons: ['no_capable_model'], auto: null, proposal, corrections, source, bound: false };
365
+ }
366
+ if (request.coercedDirect) {
367
+ corrections.push({ field: 'kind', from: 'direct', to: 'dispatch', reason: 'dispatch_only', text: `${label} always runs on a worker, not here` });
368
+ }
369
+ const sees = (target) => describeTarget(target, { catalog, brain, brainInfo })?.vision;
370
+ const fit = (target) => designFit(catalog, target, { brain, brainInfo });
371
+ // Too blind for this task: design needs a confirmed true (null never counts) on an available row; other vision work refuses a confirmed false.
372
+ const tooBlind = (target) => (visionOnly ? fit(target) !== 'ok' : sees(target) === false);
373
+ const remembered = preferenceTarget(routing.preferences?.[request.taskClass], brain, null, { dispatchOnly });
374
+ const disabled = !!remembered && isDisabled(catalog, remembered);
375
+ // A saved route onto a model that cannot make the medium (or, for design, is not known to see) is not applied either.
376
+ const incapable = !!remembered && !disabled && (!targetMakes(catalog, remembered, medium) || (visionOnly && tooBlind(remembered)));
377
+ const pref = remembered && !disabled && !incapable ? remembered : null;
378
+ // Omitted model: a remembered preference for the class fills it.
379
+ if (!proposal.model && proposal.kind === 'dispatch') {
380
+ if (pref && (pref.provider === proposal.provider || !first.model)) { proposal = { ...pref, effort: proposal.effort || pref.effort }; source = 'remembered'; }
381
+ else if (remembered && disabled && (remembered.provider === proposal.provider || !first.model)) {
382
+ // The user hid the remembered route's model since.
383
+ reasons.push('model_disabled');
384
+ corrections.push({ field: 'model', from: remembered.model, to: null, reason: 'model_disabled' });
385
+ } else reasons.push('model_missing');
386
+ }
387
+ if (proposal.kind === 'direct' && !proposal.model) proposal.model = brain.model || null;
388
+ // The effort is checked against the model it will run on (a merged preference included).
389
+ proposal = effortChecked(proposal, catalog, corrections);
390
+ if (proposal.model && findModel(catalog, proposal.provider, proposal.model) === null) reasons.push(isDisabled(catalog, proposal) ? 'model_disabled' : 'model_unknown');
391
+ else if (!proposal.model && isDisabled(catalog, proposal) && !reasons.includes('model_disabled')) reasons.push('model_disabled');
392
+ if (request.needsVision && tooBlind(proposal)) reasons.push(visionOnly && fit(proposal) === 'unavailable' ? 'model_unavailable' : 'needs_vision');
393
+ if (medium && !targetMakes(catalog, proposal, medium)) reasons.push('needs_output');
394
+ if (request.confidence === null) reasons.push('no_confidence');
395
+ else if (request.confidence < (routing.askBelow ?? 0.75)) reasons.push('low_confidence');
396
+
397
+ const effective = normalizeRouteMode(mode) || 'ask-unsure';
398
+ if (effective === 'always-ask') return { ask: true, reasons: ['always-ask', ...reasons], auto: null, proposal, corrections, source };
399
+ if (effective === 'ask-unsure') {
400
+ const unsure = reasons.filter((r) => r !== 'no_confidence' || source !== 'remembered');
401
+ if (unsure.length) return { ask: true, reasons: unsure, auto: null, proposal, corrections, source };
402
+ return { ask: false, reasons, auto: proposal, proposal, corrections, source };
403
+ }
404
+ // never: the user's saved route for the class binds (the brain only classifies);
405
+ // without an enabled one, run the brain's pick, correcting what cannot run.
406
+ let auto = { ...proposal };
407
+ const bound = !!pref;
408
+ if (bound) {
409
+ // Provider and model from the saved route; its effort when set, else the brain's.
410
+ auto = { kind: pref.kind, provider: pref.provider, model: pref.model || (pref.kind === 'direct' ? brain.model || null : null), effort: pref.effort || proposal.effort || null };
411
+ const pinnedPref = pinnedDefault(catalog, auto);
412
+ if (pinnedPref) auto = { ...auto, model: pinnedPref.id };
413
+ if (!sameTarget({ ...auto, effort: null }, { ...proposal, effort: null }) || (pref.effort && pref.effort !== proposal.effort)) {
414
+ corrections.push({
415
+ field: 'route', from: routeText(proposal), to: routeText(auto), reason: 'remembered_route',
416
+ text: `saved ${label} route ${routeText(auto)} replaced ${routeText(proposal)}`,
417
+ });
418
+ }
419
+ source = 'remembered';
420
+ } else {
421
+ if (remembered && disabled && request.taskClass !== 'chat' && !corrections.some((c) => c.reason === 'model_disabled' && c.from === remembered.model)) {
422
+ // A saved route on a model the user hid since is not applied: the brain's pick stands.
423
+ corrections.push({
424
+ field: 'route', from: routeText(remembered), to: null, reason: 'remembered_disabled',
425
+ text: `saved ${label} route ${routeText(remembered)} is disabled in the Models list; kept the assistant's pick`,
426
+ });
427
+ } else if (incapable) {
428
+ // A saved image / video creation route onto a model that cannot make the medium, or a design route onto one not known to see.
429
+ const verdict = medium && !targetMakes(catalog, remembered, medium) ? null : fit(remembered);
430
+ const cannot = !verdict ? `cannot make ${MEDIUM_WORDS[medium] || medium}`
431
+ : verdict === 'blind' ? 'cannot see images' : verdict === 'unavailable' ? 'is unavailable' : 'is not known to see images';
432
+ corrections.push({
433
+ field: 'route', from: routeText(remembered), to: null, reason: 'remembered_incapable',
434
+ text: `saved ${label} route ${routeText(remembered)} ${cannot}; not applied`,
435
+ });
436
+ }
437
+ const invalid = reasons.find((r) => r === 'model_unknown' || r === 'model_disabled');
438
+ if (invalid && auto.model) {
439
+ corrections.push({ field: 'model', from: auto.model, to: null, reason: invalid });
440
+ auto = { ...auto, model: null };
441
+ }
442
+ // No model and the provider default is hidden: name the visible stand-in.
443
+ const pinned = pinnedDefault(catalog, auto);
444
+ if (pinned) auto = { ...auto, model: pinned.id };
445
+ // Every model of the provider is hidden: the strongest visible Claude model takes it
446
+ // (image / video creation: a model that makes the medium does, below).
447
+ if (isDisabled(catalog, auto) && !auto.model && !medium) {
448
+ const alt = strongestClaude(catalog);
449
+ if (alt && auto.provider !== 'claude-code') { corrections.push({ field: 'provider', from: auto.provider, to: 'claude-code', reason: 'model_disabled' }); auto = { kind: 'dispatch', provider: 'claude-code', model: alt.id, effort: null }; }
450
+ }
451
+ }
452
+ // Image / video creation weighs vision and output together below, so one fix cannot undo the other.
453
+ if (request.needsVision && !medium && tooBlind(auto)) {
454
+ if (visionOnly) {
455
+ // Design: the saved route when it can see, else the cheapest model that can on the same provider, else anywhere.
456
+ const seer = sightFallback(catalog, auto.provider, { brain, routing, taskClass: request.taskClass });
457
+ if (seer) {
458
+ corrections.push(fit(auto) === 'unavailable'
459
+ ? { field: 'model', from: routeText(auto), to: routeText(seer), reason: 'model_unavailable', text: `${label} needs an available model that can see: ${routeText(auto)} is unavailable → ${routeText(seer)}` }
460
+ : { field: 'model', from: routeText(auto), to: routeText(seer), reason: 'needs_vision', text: `${label} needs a model that can see: ${routeText(auto)} → ${routeText(seer)}` });
461
+ auto = seer;
462
+ }
463
+ } else {
464
+ const vision = visionFallback(catalog, auto.provider, { brain, routing });
465
+ if (vision) { corrections.push({ field: 'model', from: auto.model, to: vision.model, reason: 'needs_vision' }); auto = vision; }
466
+ }
467
+ }
468
+ if (medium) {
469
+ const makes = targetMakes(catalog, auto, medium);
470
+ const blind = request.needsVision && sees(auto) === false;
471
+ if (!makes || blind) {
472
+ // The saved route when it can, else the cheapest maker on the same provider, else anywhere (one that can see first).
473
+ const maker = generationFallback(catalog, auto.provider, medium, { brain, routing, taskClass: request.taskClass, needsVision: request.needsVision });
474
+ // A maker that cannot see is only swapped for one that can.
475
+ if (maker && (!makes || sees(maker) === true)) {
476
+ const words = MEDIUM_WORDS[medium] || medium;
477
+ corrections.push(makes
478
+ ? { field: 'model', from: routeText(auto), to: routeText(maker), reason: 'needs_vision', text: `${label} needs a model that can see the image it works from: ${routeText(auto)} → ${routeText(maker)}` }
479
+ : { field: 'model', from: routeText(auto), to: routeText(maker), reason: 'needs_output', text: `${label} needs a model that makes ${words}${request.needsVision && sees(maker) === true ? ' and can see' : ''}: ${routeText(auto)} → ${routeText(maker)}` });
480
+ auto = maker;
481
+ }
482
+ }
483
+ }
484
+ if (dispatchOnly && auto.kind !== 'dispatch') auto = { ...auto, kind: 'dispatch' };
485
+ // A corrected model or provider re-checks the effort.
486
+ auto = effortChecked(auto, catalog, corrections);
487
+ if (!bound && corrections.some((c) => c.reason !== 'remembered_disabled' && c.reason !== 'remembered_incapable')) source = 'corrected';
488
+ return { ask: false, reasons, auto, proposal, corrections, source, bound };
489
+ }
490
+
491
+ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVision, needsOutput = null, visionOnly = false, reason = null }) {
492
+ const target = effortChecked(rawTarget, catalog);
493
+ const d = describeTarget(target, { catalog, brain, brainInfo });
494
+ const here = target.kind === 'direct';
495
+ const label = here ? `Do it here with ${d.label}` : `${providerLabel(target.provider)} · ${d.label}${target.effort ? ` · ${target.effort}` : ''}`;
496
+ // Design: only a confirmed vision: true on an available row; other vision work: a confirmed false is disabled, unknown is a warning.
497
+ const fit = visionOnly ? designFit(catalog, target, { brain, brainInfo }) : 'ok';
498
+ const option = {
499
+ id, kind: target.kind, provider: target.provider, model: target.model || null, effort: target.effort || null,
500
+ // A design card shows sight as design judges it (an unlisted claude-* id is unknown there).
501
+ label, badge, tier: d.tier, vision: fit === 'unknown' ? null : d.vision, price: d.price, contextWindow: d.contextWindow,
502
+ reason: reason || null, warning: null, disabled: false, disabledReason: null,
503
+ };
504
+ if (needsOutput) option.outputs = d.outputs || null;
505
+ if (isDisabled(catalog, target)) { option.disabled = true; option.disabledReason = providerEmpty(catalog, target.provider) ? 'No model of this provider is connected.' : MODEL_DISABLED_TEXT; }
506
+ else if (needsOutput && !targetMakes(catalog, target, needsOutput)) { option.disabled = true; option.disabledReason = cannotMakeText(needsOutput); }
507
+ else if (fit !== 'ok') { option.disabled = true; option.disabledReason = fitText(fit); }
508
+ else if (needsVision && d.vision === false) { option.disabled = true; option.disabledReason = 'This model cannot see images, and this task needs to.'; }
509
+ else if (needsVision && d.vision === null) option.warning = 'Image support unknown for this model.';
510
+ if (here && brainInfo?.contextWindow && d.contextWindow && d.contextWindow < brainInfo.contextWindow) {
511
+ option.warning = `Smaller context (${Math.round(d.contextWindow / 1000)}k) than the current model; a long conversation may not fit.`;
512
+ }
513
+ return option;
514
+ }
515
+
516
+ export function providerLabel(provider) {
517
+ return provider === 'claude-code' ? 'Claude Code' : provider === 'codex' ? 'Codex' : provider === 'opencode' ? 'OpenCode' : String(provider || 'Assistant');
518
+ }
519
+
520
+ /** `seeingOnly` (design): only rows with a confirmed vision: true, where `needsVision` only drops a confirmed false. */
521
+ function neighbour(catalog, anchor, direction, { needsVision = false, seeingOnly = false, needsOutput = null } = {}) {
522
+ const all = visibleRows(catalog, anchor.provider).filter((row) => row.status !== 'unavailable' && row.toolcall !== false && (!needsVision || row.vision !== false) && (!seeingOnly || row.vision === true) && (!needsOutput || rowMakes(row, needsOutput)));
523
+ // A "default" alias duplicates a real model row: prefer the real id.
524
+ const rows = all.filter((row) => !(row.id === 'default' && all.some((other) => other.id !== 'default' && other.upstream === row.upstream)));
525
+ if (!rows.length) return null;
526
+ const current = anchor.model ? findModel(catalog, anchor.provider, anchor.model)?.row : defaultModelRow(catalog, anchor.provider);
527
+ const rank = tierRank(current?.tier);
528
+ const price = outPrice(current);
529
+ const others = rows.filter((row) => row.id !== current?.id && row.upstream !== current?.upstream);
530
+ if (direction === 'cheaper') {
531
+ const pool = others.filter((row) => tierRank(row.tier) < rank || (tierRank(row.tier) === rank && outPrice(row) < price));
532
+ pool.sort((a, b) => tierRank(b.tier) - tierRank(a.tier) || outPrice(a) - outPrice(b));
533
+ return pool[0] || null;
534
+ }
535
+ // Stronger = the cheapest model of the next tier up; only when there is no
536
+ // higher tier, a pricier model of the same tier.
537
+ const higher = others.filter((row) => tierRank(row.tier) > rank);
538
+ if (higher.length) {
539
+ higher.sort((a, b) => tierRank(a.tier) - tierRank(b.tier) || outPrice(a) - outPrice(b));
540
+ return higher[0];
541
+ }
542
+ const same = others.filter((row) => tierRank(row.tier) === rank && Number.isFinite(outPrice(row)) && outPrice(row) > price);
543
+ same.sort((a, b) => outPrice(a) - outPrice(b));
544
+ return same[0] || null;
545
+ }
546
+
547
+ /** First large-tier Claude row, preferring a real id over the "default" alias. */
548
+ function strongestClaude(catalog) {
549
+ const rows = visibleRows(catalog, 'claude-code').filter((row) => row.tier === 'large' && row.status !== 'unavailable');
550
+ return rows.find((row) => row.id !== 'default') || rows[0] || null;
551
+ }
552
+
553
+ /**
554
+ * Route-card options: here, the brain's proposals, remembered, cheaper, stronger.
555
+ * Image / video creation: no "here" (always a worker), models that cannot make
556
+ * the medium disabled, and — when the suggestion cannot — the model that would
557
+ * run it instead (badge "capable"). Design: the same with "can see".
558
+ */
559
+ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo = null, catalog = null, routing = {} }) {
560
+ const medium = request.needsOutput || null;
561
+ const dispatchOnly = request.dispatchOnly === true;
562
+ const visionOnly = request.visionOnly === true;
563
+ const ctx = { catalog, brain, brainInfo, needsVision: request.needsVision, needsOutput: medium, visionOnly };
564
+ const options = [];
565
+ const push = (option) => { if (!options.some((o) => sameTarget(o, option))) options.push(option); };
566
+ const hereTarget = { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null };
567
+ request.proposals.forEach((p, index) => {
568
+ const target = { kind: p.kind, provider: p.provider, model: p.model || (index === 0 ? evaluation.proposal?.model || null : null), effort: p.effort };
569
+ if (index === 0 && evaluation.proposal) Object.assign(target, { model: evaluation.proposal.model, effort: evaluation.proposal.effort, provider: evaluation.proposal.provider, kind: evaluation.proposal.kind });
570
+ push(optionFor(`s${index + 1}`, target, 'suggested', { ...ctx, reason: p.reason }));
571
+ });
572
+ if (!dispatchOnly) push(optionFor('here', hereTarget, 'current', ctx));
573
+ const pref = preferenceTarget(routing.preferences?.[request.taskClass], brain, catalog, { dispatchOnly });
574
+ if (pref) push(optionFor('r1', pref, 'remembered', ctx));
575
+ const anchor = evaluation.proposal?.kind === 'dispatch' || dispatchOnly ? evaluation.proposal || request.proposals[0] : hereTarget;
576
+ const cheaper = neighbour(catalog, anchor, 'cheaper', { needsVision: request.needsVision, seeingOnly: visionOnly, needsOutput: medium });
577
+ if (cheaper) push(optionFor('c1', { kind: anchor.provider === brain.provider && anchor.kind === 'direct' ? 'direct' : 'dispatch', provider: anchor.provider, model: cheaper.id, effort: null }, 'cheaper', ctx));
578
+ let stronger = neighbour(catalog, anchor, 'stronger', { needsVision: request.needsVision, seeingOnly: visionOnly, needsOutput: medium });
579
+ let strongerProvider = anchor.provider;
580
+ if (!stronger && anchor.provider !== 'claude-code' && !medium) {
581
+ stronger = strongestClaude(catalog);
582
+ strongerProvider = 'claude-code';
583
+ }
584
+ if (stronger) push(optionFor('x1', { kind: strongerProvider === brain.provider && anchor.kind === 'direct' ? 'direct' : 'dispatch', provider: strongerProvider, model: stronger.id, effort: null }, 'stronger', ctx));
585
+ const suggestion = evaluation.proposal || request.proposals[0];
586
+ if (medium && (!targetMakes(catalog, suggestion, medium) || (request.needsVision && describeTarget(suggestion, { catalog, brain, brainInfo })?.vision === false))) {
587
+ const maker = generationFallback(catalog, anchor.provider, medium, { brain, routing, taskClass: request.taskClass, needsVision: request.needsVision });
588
+ const option = maker ? optionFor('g1', maker, 'capable', ctx) : null;
589
+ if (option && !option.disabled) push(option);
590
+ }
591
+ if (visionOnly && !medium && designFit(catalog, suggestion, { brain, brainInfo }) !== 'ok') {
592
+ const seer = sightFallback(catalog, anchor.provider, { brain, routing, taskClass: request.taskClass });
593
+ const option = seer ? optionFor('g1', seer, 'capable', ctx) : null;
594
+ if (option && !option.disabled) push(option);
595
+ }
596
+ // Order: suggested first, then here, remembered, a capable stand-in, cheaper, stronger.
597
+ const order = { suggested: 0, current: 1, remembered: 2, capable: 3, cheaper: 4, stronger: 5 };
598
+ options.sort((a, b) => (order[a.badge] ?? 9) - (order[b.badge] ?? 9) || String(a.id).localeCompare(String(b.id)));
599
+ const defaultOption = options.find((o) => o.badge === 'suggested' && !o.disabled) || options.find((o) => !o.disabled) || options[0];
600
+ return { options, defaultOptionId: defaultOption?.id || 'here' };
601
+ }
602
+
603
+ /** Validate a route-card answer; returns { declined } or { target, optionId, remember }. */
604
+ export function targetFromAnswer({ card, response = {}, catalog = null, brain = {} }) {
605
+ if (response.decline === true || response.behavior === 'deny' || response.cancel === true) return { declined: true };
606
+ const optionId = String(response.optionId || response.option_id || '').trim();
607
+ const remember = response.remember === true || (typeof response.remember === 'string' && response.remember.length > 0);
608
+ const option = optionId && optionId !== 'other' ? (card.options || []).find((o) => o.id === optionId) : null;
609
+ // A card can remain open while the catalog or hidden-model list changes.
610
+ // Check the chosen target again instead of trusting the option's old state.
611
+ const ensureAvailable = (target) => {
612
+ if (target.model && findModel(catalog, target.provider, target.model) === null) {
613
+ if (isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `${target.model} is disabled in the Assistant's Models list. Pick an enabled model.`);
614
+ throw routeError('ROUTE_INVALID', `Unknown model ${target.model} for ${target.provider}.`);
615
+ }
616
+ if (!target.model && isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `Every ${target.provider} model is disabled in the Assistant's Models list. Pick another provider.`);
617
+ // Design: only a confirmed vision: true on an available row (unknown or unavailable is refused too);
618
+ // other vision work: a confirmed false.
619
+ if (card.visionOnly) {
620
+ const verdict = designFit(catalog, target, { brain });
621
+ if (verdict !== 'ok') throw routeError('ROUTE_INVALID', fitText(verdict));
622
+ } else if (card.needsVision && describeTarget(target, { catalog, brain })?.vision === false) {
623
+ throw routeError('ROUTE_INVALID', 'This model cannot see images, and this task needs to.');
624
+ }
625
+ if (card.needsOutput && !targetMakes(catalog, target, card.needsOutput)) throw routeError('ROUTE_INVALID', cannotMakeText(card.needsOutput));
626
+ };
627
+ // Image / video creation and design always run on a worker: a "here" pick is a dispatch onto that model.
628
+ const dispatchOnly = card.dispatchOnly === true;
629
+ if (option) {
630
+ if (option.disabled) throw routeError('ROUTE_INVALID', option.disabledReason || 'That option is not available for this task.');
631
+ const corrections = [];
632
+ const target = effortChecked({ kind: dispatchOnly ? 'dispatch' : option.kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort }, catalog, corrections);
633
+ ensureAvailable(target);
634
+ return { target, optionId, remember, corrections };
635
+ }
636
+ const t = response.target;
637
+ if (!t || typeof t !== 'object' || !CATALOG_PROVIDERS.includes(t.provider)) throw routeError('ROUTE_INVALID', 'Pick one of the options or a catalog model.');
638
+ const kind = t.kind === 'direct' && t.provider === brain.provider && !dispatchOnly ? 'direct' : 'dispatch';
639
+ const corrections = [];
640
+ const target = effortChecked({ kind, provider: t.provider, model: t.model ? String(t.model) : null, effort: t.effort ? String(t.effort).trim().slice(0, 40) : null }, catalog, corrections);
641
+ ensureAvailable(target);
642
+ return { target, optionId: 'other', remember, corrections };
643
+ }
644
+
645
+ /**
646
+ * One-tier escalation for a failed/blocked run (ladder → tier → remembered complex → strongest;
647
+ * an image / video creation run only onto a model that makes the medium, so never the last two;
648
+ * a design run only onto one that can see),
649
+ * shaped by what kept it from finishing when Jev judged that (worker-outcome):
650
+ * access / needs_user → kind 'none': no target (a stronger model hits the same wall), with
651
+ * what is needed; returned even at the maximum depth
652
+ * transient → kind 'retry': the same provider and model, one level deeper (a design
653
+ * run whose model can no longer run design escalates instead)
654
+ * capability / other / unjudged → kind 'escalate': the ladder, as before
655
+ * Depth 2 caps retries and escalations alike. Nothing here escalates on its own.
656
+ */
657
+ export function escalationFor({ run, catalog = null, routing = {}, depth = 0 }) {
658
+ if (!run) return null;
659
+ let reason = null;
660
+ if (run.state === 'failed' && run.completionReason !== 'budget_cap') reason = 'failed';
661
+ else if (run.lastResult?.status === 'blocked') reason = 'blocked';
662
+ // A status Jev read from a message without a ## Result block is a usable result.
663
+ else if (run.lastResult && run.lastResult.found === false && run.lastResult.source !== 'jev' && (run.state === 'idle' || run.state === 'completed')) reason = 'no_result';
664
+ if (!reason) return null;
665
+ const provider = run.provider;
666
+ const current = run.model ? findModel(catalog, provider, run.model)?.row : defaultModelRow(catalog, provider);
667
+ const from = { provider, model: run.model || null, tier: current?.tier || null };
668
+ const judged = reason === 'failed' ? run.failure : run.lastResult;
669
+ const cause = typeof judged?.cause === 'string' && judged.cause ? judged.cause : null;
670
+ if (cause === 'access' || cause === 'needs_user') {
671
+ return { reason, cause, kind: 'none', from, to: null, depth, needs: judged?.needs ? clip(judged.needs, 200) : null };
672
+ }
673
+ if (depth >= MAX_ESCALATION_DEPTH) return null;
674
+ // An image / video creation run escalates only onto a model that generates the medium, a design run onto one that can see.
675
+ const runClass = run.route?.taskClass || run.taskClass;
676
+ const medium = requiredOutput(runClass);
677
+ const sight = requiresVision(runClass);
678
+ const canRun = (target) => targetMakes(catalog, target, medium) && !(sight && designFit(catalog, target) !== 'ok');
679
+ // A design run is retried only on a model that can still run design (it sees and is available);
680
+ // otherwise the escalation below finds one that can. With no catalog value to tell (the cache was
681
+ // just dropped) the retry stands: the launch checks it against a fresh catalog.
682
+ const retryable = !sight || !catalog || designFit(catalog, { kind: 'dispatch', provider, model: run.model || null }) === 'ok';
683
+ if (cause === 'transient' && !isDisabled(catalog, { provider, model: run.model }) && retryable) {
684
+ return { reason, cause, kind: 'retry', from, to: describeTarget(effortChecked({ kind: 'dispatch', provider, model: run.model || null, effort: run.effort || null }, catalog), { catalog }), depth: depth + 1 };
685
+ }
686
+ let to = null;
687
+ const ladder = Array.isArray(routing.ladders?.[provider]) ? routing.ladders[provider] : null;
688
+ if (ladder && run.model) {
689
+ const index = ladder.indexOf(run.model);
690
+ // The next rung the user has not hidden.
691
+ const next = index === -1 ? null : ladder.slice(index + 1).find((model) => !isDisabled(catalog, { provider, model }) && canRun({ kind: 'dispatch', provider, model }));
692
+ if (next) to = { kind: 'dispatch', provider, model: next, effort: null };
693
+ }
694
+ if (!to) {
695
+ const stronger = neighbour(catalog, { kind: 'dispatch', provider, model: run.model || null }, 'stronger', { needsOutput: medium, seeingOnly: sight });
696
+ if (stronger) to = { kind: 'dispatch', provider, model: stronger.id, effort: null };
697
+ }
698
+ if (!to && !medium) {
699
+ const pref = routing.preferences?.complex;
700
+ if (pref?.provider && !(pref.provider === provider && pref.model === run.model) && !isDisabled(catalog, pref) && canRun({ kind: 'dispatch', provider: pref.provider, model: pref.model || null })) to = { kind: 'dispatch', provider: pref.provider, model: pref.model || null, effort: pref.effort || null };
701
+ }
702
+ if (!to && provider !== 'claude-code' && !medium) {
703
+ const opus = strongestClaude(catalog);
704
+ if (opus && canRun({ kind: 'dispatch', provider: 'claude-code', model: opus.id })) to = { kind: 'dispatch', provider: 'claude-code', model: opus.id, effort: 'high' };
705
+ }
706
+ if (!to) return null;
707
+ return { reason, cause, kind: 'escalate', from, to: describeTarget(effortChecked(to, catalog), { catalog }), depth: depth + 1 };
708
+ }
709
+
710
+ // ── Stateful service ─────────────────────────────────────────────────────────
711
+
712
+ /**
713
+ * @param {object} deps
714
+ * @param {object} deps.catalog createAssistantCatalog() — full(), peek(), brainInfo()
715
+ * @param {object} deps.configStore createAssistantConfigStore() — routing(), setPreference()
716
+ * @param {(id:string)=>({brain, routingMode}|null)} deps.getSession
717
+ * @param {object} deps.sinks { sendCard(sessionId, packet), cancelCard(sessionId, requestId, reason),
718
+ * routeEvent(sessionId, phase, route), mailbox(sessionId, item),
719
+ * continueDirect(sessionId, {target, route, text}) → false when the pick's turn is gone,
720
+ * startHeld(runId, target, meta),
721
+ * checkHeld(runId, target) → why the held run could not start on target ({ code, message, status }) or null,
722
+ * declineHeld(runId, reason), routed(sessionId, result), routeFailed(sessionId, error) }
723
+ * @param {object} [deps.clarifier] assistant-clarify.js — { blocking(sessionId, { independent }) }: while the
724
+ * user has not answered the brain's questions, agent_route answers "clarifying"
725
+ */
726
+ export function createAssistantRouter({
727
+ catalog,
728
+ configStore = null,
729
+ getSession = () => null,
730
+ sinks = {},
731
+ now = Date.now,
732
+ randomId = () => randomBytes(6).toString('hex'),
733
+ log = () => {},
734
+ clarifier = null,
735
+ } = {}) {
736
+ if (!catalog) throw new Error('createAssistantRouter requires a catalog');
737
+ const cards = new Map(); // routeId → card
738
+ const byRequest = new Map(); // requestId → routeId
739
+ // routeId → decided route (reusable by agent_dispatch route_id; a remote session's is
740
+ // used up by the first dispatch that starts a run: `consumed`).
741
+ const decided = new Map();
742
+ const stamps = new Map(); // sessionId → last stamp shown to the brain
743
+ const lastClosed = new Map(); // routeId → { routeId, settled } of recently closed cards
744
+ const call = (name, ...args) => { try { return sinks[name]?.(...args); } catch (error) { log('router:sink-error', `${name}: ${error?.message || error}`); return undefined; } };
745
+
746
+ function routing() { try { return configStore?.routing?.() || effectiveRouting({}); } catch { return effectiveRouting({}); } }
747
+ function modeFor(sessionId) {
748
+ const session = getSession(sessionId);
749
+ return normalizeRouteMode(session?.routingMode) || routing().defaultMode;
750
+ }
751
+ async function catalogValue() {
752
+ try { return await catalog.full(); } catch (error) { log('router:catalog-error', error?.message || String(error)); return catalog.peek?.() || { models: {} }; }
753
+ }
754
+ function brainInfoFor(brain) { try { return catalog.brainInfo(brain); } catch { return null; } }
755
+ function requireSession(sessionId) {
756
+ const session = sessionId ? getSession(sessionId) : null;
757
+ if (!session) throw routeError('SESSION_NOT_FOUND', `Unknown assistant session ${sessionId}`, 404);
758
+ return session;
759
+ }
760
+ function pruneDecided() {
761
+ const cutoff = now();
762
+ for (const [id, route] of decided) if (route.expiresAt < cutoff) decided.delete(id);
763
+ }
764
+
765
+ function routeView(source, { catalog: cat = null, brain = {}, brainInfo = null } = {}) {
766
+ const target = source.target ? describeTarget(source.target, { catalog: cat, brain, brainInfo }) : null;
767
+ return {
768
+ routeId: source.routeId, runIds: [...(source.runIds || [])], taskClass: source.taskClass, taskClassLabel: TASK_CLASS_META[source.taskClass]?.label || source.taskClass,
769
+ summary: source.summary || '', confidence: source.confidence ?? null, reason: source.reason || null,
770
+ target, alternatives: (source.alternatives || []).slice(0, 4), decidedBy: source.decidedBy || null, remembered: !!source.remembered,
771
+ optionId: source.optionId || null, corrections: source.corrections || [], mode: source.mode || null, status: source.status || null,
772
+ // Why a route was declined without a card (no_capable_model), else null.
773
+ reasonCode: source.reasonCode || null,
774
+ // Design's no_capable_model: no model can see.
775
+ ...(source.needs ? { needs: source.needs } : {}),
776
+ };
777
+ }
778
+
779
+ function nextText(status, target, { routeId, label } = {}) {
780
+ if (status === 'pending') return 'The user has not picked a model yet. End your turn now with one short line saying you are waiting for their choice. Their decision arrives as a [SynaBun Mailbox] route_decided event; do not start the task before it.';
781
+ if (status === 'declined') return 'The user declined this route. Do not start the task; ask what they would like instead.';
782
+ // The user wrote something else instead of choosing (cancel 'superseded'): their message is next, so nothing is asked here.
783
+ if (status === 'moved_on') return 'The user did not pick a model: they sent a new message instead, which you get next. Nothing was approved, so do not start this task. End your turn now, with no text unless you have a result to report.';
784
+ if (status === 'expired' || status === 'cancelled') return 'This route is no longer valid. Do not start the task; ask the user how to proceed.';
785
+ if (!target) return 'Proceed.';
786
+ if (target.kind === 'dispatch') {
787
+ return `Dispatch now with agent_dispatch route_id="${routeId}" (provider ${target.provider}${target.model ? `, model ${target.model}` : ', provider default model'}${target.effort ? `, effort ${target.effort}` : ''}). Do not ask the user again about the model.`;
788
+ }
789
+ if (target.continuation) return `The user chose to run this here on ${label || target.model}. End your turn now with one short line; SynaBun continues this task on that model immediately after.`;
790
+ return 'Proceed here yourself now.';
791
+ }
792
+
793
+ /** A "do it here" pick on another model or effort: the runtime continues the task on it. */
794
+ function continuesHere(target, brain) {
795
+ if (!target || target.kind !== 'direct') return false;
796
+ if (String(target.model || '') === String(brain.model || '') && !target.effort) return false;
797
+ if (String(target.model || '') === String(brain.model || '') && String(target.effort || '') === String(brain.effort || '')) return false;
798
+ return true;
799
+ }
800
+ /** → whether the runtime took the continuation (continueDirect): false for a pick whose turn is gone. */
801
+ function maybeContinuation(sessionId, target, brain, source) {
802
+ if (!continuesHere(target, brain)) return false;
803
+ return call('continueDirect', sessionId, { target, route: source, text: source.summary || '' }) !== false;
804
+ }
805
+
806
+ function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false } = {}) {
807
+ const described = target ? { ...describeTarget(target, { catalog: cat, brain, brainInfo }), continuation } : null;
808
+ // A saved route that replaced the brain's pick (never mode) says so first.
809
+ const binding = (corrections || []).find((c) => c?.reason === 'remembered_route');
810
+ const skipped = (corrections || []).find((c) => c?.reason === 'remembered_disabled');
811
+ // Image / video creation: a pick that cannot make the medium was moved onto one that can.
812
+ const maker = (corrections || []).find((c) => c?.reason === 'needs_output');
813
+ const lead = binding ? `The user's saved route for this task class applies (${binding.to}), not your pick (${binding.from}). `
814
+ : skipped ? `The saved route for this task class (${skipped.from}) is on a disabled model, so your pick stands. `
815
+ : maker ? `Your pick (${maker.from}) cannot make what this task creates, so it runs on ${maker.to}. ` : '';
816
+ return {
817
+ ok: true, status, routeId, mode, source, target: described, remembered, continuation,
818
+ corrections, runIds, expiresAt: card?.expiresAt ? new Date(card.expiresAt).toISOString() : null,
819
+ next: `${lead}${nextText(status, described, { routeId, label: described?.label })}`,
820
+ };
821
+ }
822
+
823
+ /** What the brain tells the user when no model makes the medium, or can see for design (agent_route's `next`, the NO_CAPABLE_MODEL refusal). */
824
+ function noMakerText(request) {
825
+ if (!request.needsOutput) {
826
+ return `No model in the user's catalog can see images, so this ${request.taskClassLabel} task cannot run. Do not start it or dispatch it. Tell the user in one line that the ${request.taskClassLabel} row in Model routes has no model that can see yet: they can connect or enable one (in the Models list), then pick it for that row.`;
827
+ }
828
+ const words = MEDIUM_WORDS[request.needsOutput] || request.needsOutput;
829
+ return `No model in the user's catalog can make ${words}, so this ${request.taskClassLabel} task cannot run. Do not start it or dispatch it. Tell the user in one line that the ${request.taskClassLabel} row in Model routes has no model that makes ${words} yet: they can connect or enable one (in the Models list), then pick it for that row.`;
830
+ }
831
+ /** Image / video creation with no model that makes the medium (design: none that can see): declined at once, in every mode (no card). */
832
+ function unavailableResult({ sessionId, request, mode, cat, brain, brainInfo }) {
833
+ const routeId = `route-${randomId()}`;
834
+ const words = MEDIUM_WORDS[request.needsOutput] || request.needsOutput;
835
+ const needs = request.needsOutput ? null : 'vision';
836
+ const route = {
837
+ routeId, status: 'declined', sessionId, taskClass: request.taskClass, summary: request.summary, confidence: request.confidence,
838
+ target: null, decidedBy: 'rule', remembered: false, corrections: [], mode, runIds: [], reasonCode: 'no_capable_model',
839
+ reason: needs ? 'No model in your catalog can see images yet.' : `No model in your catalog can make ${words} yet.`,
840
+ ...(needs ? { needs } : {}),
841
+ };
842
+ call('routeEvent', sessionId, 'declined', routeView(route, { catalog: cat, brain, brainInfo }));
843
+ return {
844
+ ok: true, status: 'declined', reason: 'no_capable_model', requires: request.needsOutput, ...(needs ? { needs } : {}), routeId, mode, source: 'rule', target: null,
845
+ remembered: false, continuation: false, corrections: [], runIds: [], expiresAt: null, next: noMakerText(request),
846
+ };
847
+ }
848
+
849
+ /**
850
+ * A WhatsApp session whose computer use is "after approval" (session.computer, the runtime's
851
+ * remoteComputerUse answer): when what a plain yes approves on this card is a computer task
852
+ * done here, on the brain's own model (no continuation onto another model, no worker), the
853
+ * card says the Mac will be controlled and that yes is also the computer approval of the turn
854
+ * that carries the route out. → { optionId } (the one option that carries it), else null.
855
+ * Decided once, when the card is made, and stored with it: never derived again from the class
856
+ * or the target an answer names.
857
+ */
858
+ function computerConsent({ session, request, options, defaultOptionId, brain, origin }) {
859
+ if (origin !== 'agent_route' || session?.remote !== true || session?.computer?.state !== 'ask') return null;
860
+ if (request.taskClass !== 'computer') return null;
861
+ const option = options.find((o) => o.id === defaultOptionId);
862
+ if (!option || option.disabled || option.kind !== 'direct' || option.provider !== brain.provider) return null;
863
+ if (continuesHere({ kind: 'direct', provider: option.provider, model: option.model, effort: option.effort }, brain)) return null;
864
+ return { optionId: option.id };
865
+ }
866
+
867
+ function createCard({ sessionId, request, evaluation, brain, brainInfo, cat, mode, origin, workflowId = null, runIds = [], session = null }) {
868
+ const routeId = `route-${randomId()}`;
869
+ const requestId = routeId;
870
+ const r = routing();
871
+ const { options, defaultOptionId } = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r });
872
+ const computer = computerConsent({ session, request, options, defaultOptionId, brain, origin });
873
+ const expiresAt = now() + r.cardTimeoutMinutes * 60_000;
874
+ const pref = r.preferences?.[request.taskClass];
875
+ const card = {
876
+ routeId, requestId, sessionId, origin, status: 'pending', mode, workflowId,
877
+ taskClass: request.taskClass, summary: request.summary, confidence: request.confidence, needsVision: request.needsVision,
878
+ needsOutput: request.needsOutput || null, dispatchOnly: request.dispatchOnly === true, visionOnly: request.visionOnly === true,
879
+ reasons: evaluation.reasons, request, options, defaultOptionId, runIds: new Set(runIds), waiters: new Set(),
880
+ createdAt: now(), expiresAt, timer: null, mailboxOnDecision: origin === 'dispatch', brain,
881
+ // The option whose approval is also the computer approval (computerConsent), as this card was sent.
882
+ computer,
883
+ alternatives: options.filter((o) => o.badge !== 'suggested').map((o) => ({ id: o.id, kind: o.kind, provider: o.provider, model: o.model, effort: o.effort, label: o.label, badge: o.badge })),
884
+ };
885
+ card.packet = {
886
+ type: 'control_request',
887
+ request_id: requestId,
888
+ request: {
889
+ subtype: 'route', kind: 'route', provider: 'synabun', routeId, origin, runIds: [...card.runIds], workflowId, sessionId, mode,
890
+ reasons: evaluation.reasons, taskClass: request.taskClass, taskClassLabel: request.taskClassLabel, summary: request.summary,
891
+ confidence: request.confidence, needsVision: request.needsVision,
892
+ // Image / video creation: the medium the model must generate, and no "here" (always a worker).
893
+ requires: request.needsOutput || null, dispatchOnly: request.dispatchOnly === true,
894
+ // Design: only models that can see.
895
+ visionOnly: request.visionOnly === true,
896
+ brain: { provider: brain.provider, model: brain.model || null, label: brainInfo?.label || brain.model || providerLabel(brain.provider), vision: brainInfo?.vision ?? null },
897
+ options, defaultOptionId,
898
+ other: { allowed: true, providers: CATALOG_PROVIDERS, catalogPath: '/api/assistant/catalog' },
899
+ remember: { available: request.taskClass !== 'chat', taskClass: request.taskClass, label: request.taskClassLabel, current: pref ? (pref.label || [pref.provider, pref.model].filter(Boolean).join('/')) : null },
900
+ decline: { allowed: true, label: 'Cancel' },
901
+ // Approving this option also lets the Assistant control the Mac for the task: every host says so in words.
902
+ ...(computer ? { computer: { optionId: computer.optionId } } : {}),
903
+ createdAt: new Date(card.createdAt).toISOString(), expiresAt: new Date(expiresAt).toISOString(),
904
+ },
905
+ };
906
+ card.timer = setTimeout(() => expire(routeId), Math.max(1000, expiresAt - now()));
907
+ card.timer.unref?.();
908
+ cards.set(routeId, card);
909
+ byRequest.set(requestId, routeId);
910
+ return card;
911
+ }
912
+ function refreshPacketRuns(card) { card.packet.request.runIds = [...card.runIds]; }
913
+
914
+ // The outcome stays on the card: a producer that has not started waiting yet (a host closed
915
+ // the card while it was being sent) reads it instead of waiting on a card that is gone.
916
+ function settle(card, outcome) {
917
+ card.settled = outcome;
918
+ for (const waiter of [...card.waiters]) { try { waiter(outcome); } catch {} }
919
+ card.waiters.clear();
920
+ }
921
+ /**
922
+ * What a dispatch is told when its route card closed before a run could wait on it (a host
923
+ * closed it while it was being sent, or between the hold and the attachment): nothing is held.
924
+ */
925
+ function routeClosedError(card) {
926
+ const text = card?.settled?.movedOn
927
+ ? 'The user sent a new message instead of choosing where this task runs, which you get next. Nothing was started: do not dispatch it again. End your turn now, with no text unless you have a result to report.'
928
+ : 'This route is no longer valid: its card closed before the run could wait on it. Nothing was started; ask the user how to proceed.';
929
+ return routeError('ROUTE_CANCELLED', text, 409, { routeId: card?.routeId || null });
930
+ }
931
+ function closeCard(card) {
932
+ if (card.timer) { clearTimeout(card.timer); card.timer = null; }
933
+ byRequest.delete(card.requestId);
934
+ cards.delete(card.routeId);
935
+ // Why it closed, for a producer that attaches a run a moment too late (bounded).
936
+ lastClosed.set(card.routeId, { routeId: card.routeId, settled: card.settled || null });
937
+ while (lastClosed.size > 50) lastClosed.delete(lastClosed.keys().next().value);
938
+ }
939
+
940
+ // `reason` 'superseded': a "do it here" pick whose turn is gone (answer), or the user
941
+ // wrote something else instead of choosing (cancel: `movedOn`). It reads as expired, and
942
+ // the brain gets no mailbox event: the user already moved on.
943
+ function expire(routeId, reason = 'expired', { movedOn = false } = {}) {
944
+ const card = cards.get(routeId);
945
+ if (!card || card.status !== 'pending') return;
946
+ card.status = reason === 'expired' || reason === 'superseded' ? 'expired' : 'cancelled';
947
+ const status = card.status;
948
+ for (const runId of card.runIds) call('declineHeld', runId, reason === 'expired' ? 'route_expired' : `route_${reason}`);
949
+ call('cancelCard', card.sessionId, card.requestId, reason);
950
+ const view = routeView({ ...card, status, decidedBy: reason === 'expired' ? 'timeout' : null }, { catalog: catalog.peek?.(), brain: card.brain });
951
+ call('routeEvent', card.sessionId, status === 'expired' ? 'expired' : 'cancelled', view);
952
+ if ((card.mailboxOnDecision || card.runIds.size) && reason !== 'session_closed' && reason !== 'aborted' && reason !== 'superseded') {
953
+ call('mailbox', card.sessionId, { kind: 'route_expired', route: view, runIds: [...card.runIds], text: `Route card for "${card.summary || card.taskClass}" ${status === 'expired' ? 'expired without an answer' : `was cancelled (${reason})`}.` });
954
+ }
955
+ settle(card, { status, routeId, ...(movedOn ? { movedOn: true } : {}) });
956
+ closeCard(card);
957
+ }
958
+
959
+ function waitFor(card, ms, signal) {
960
+ return new Promise((resolve) => {
961
+ let done = false;
962
+ const finish = (value) => { if (done) return; done = true; clearTimeout(timer); card.waiters.delete(finish); signal?.removeEventListener?.('abort', onAbort); resolve(value); };
963
+ const onAbort = () => finish(null);
964
+ const timer = setTimeout(() => finish(null), ms);
965
+ timer.unref?.();
966
+ card.waiters.add(finish);
967
+ signal?.addEventListener?.('abort', onAbort, { once: true });
968
+ });
969
+ }
970
+
971
+ /**
972
+ * agent_route: the brain's proposal → approved | pending | declined. The
973
+ * result (or a router failure) also reaches the runtime's per-turn route
974
+ * gate through the `routed` / `routeFailed` sinks.
975
+ */
976
+ async function propose(args = {}) {
977
+ const origin = args.origin || 'agent_route';
978
+ try {
979
+ const result = await proposeRoute(args);
980
+ // The class rides along: a "chat" route must not unlock the gate.
981
+ if (origin === 'agent_route') call('routed', args.sessionId, result, { taskClass: normalizeRouteRequest(args.body || {}).taskClass });
982
+ return result;
983
+ } catch (error) {
984
+ if (origin === 'agent_route') call('routeFailed', args.sessionId, error);
985
+ throw error;
986
+ }
987
+ }
988
+ /** The user has not answered the brain's questions: nothing that depends on them is routed (the gate holds). */
989
+ function clarifyingResult(sessionId, waiting) {
990
+ const what = waiting.summary ? `: "${clip(waiting.summary, 80)}"` : '';
991
+ return {
992
+ ok: true, status: 'clarifying', routeId: null, mode: modeFor(sessionId), source: null, target: null, remembered: false, continuation: false,
993
+ corrections: [], runIds: [], expiresAt: null, briefId: waiting.briefId,
994
+ next: `The user has not answered your questions yet (brief ${waiting.briefId}${what}). End your turn now with one short line and route this after the answers arrive (a [SynaBun Mailbox] clarify_answered event, or the user's next message). If this task does not depend on them, call agent_route again with independent:true.`,
995
+ };
996
+ }
997
+ async function proposeRoute({ sessionId, body = {}, origin = 'agent_route', waitMs = null, signal = null } = {}) {
998
+ pruneDecided();
999
+ const session = requireSession(sessionId);
1000
+ const waiting = origin === 'agent_route' ? clarifier?.blocking?.(sessionId, { independent: body.independent === true }) : null;
1001
+ if (waiting) return clarifyingResult(sessionId, waiting);
1002
+ const brain = session.brain || {};
1003
+ const cat = await catalogValue();
1004
+ const brainInfo = brainInfoFor(brain);
1005
+ const request = normalizeRouteRequest(body, { brain });
1006
+ const mode = modeFor(sessionId);
1007
+ const r = routing();
1008
+ const evaluation = evaluateRoute({ mode, routing: r, request, brain, brainInfo, catalog: cat });
1009
+ if (evaluation.unavailable) return unavailableResult({ sessionId, request, mode, cat, brain, brainInfo });
1010
+ if (!evaluation.ask) {
1011
+ const routeId = `route-${randomId()}`;
1012
+ const target = evaluation.auto;
1013
+ const decidedBy = evaluation.source === 'remembered' ? 'remembered' : 'brain';
1014
+ const route = {
1015
+ routeId, status: 'approved', sessionId, taskClass: request.taskClass, summary: request.summary, confidence: request.confidence,
1016
+ target, decidedBy, remembered: evaluation.source === 'remembered', corrections: evaluation.corrections, mode,
1017
+ reason: request.proposals[0]?.reason || null, runIds: [], expiresAt: now() + DECIDED_TTL_MS,
1018
+ };
1019
+ decided.set(routeId, route);
1020
+ if (request.taskClass !== 'chat') call('routeEvent', sessionId, 'auto', routeView(route, { catalog: cat, brain, brainInfo }));
1021
+ const continuation = origin === 'agent_route' ? maybeContinuation(sessionId, target, brain, route) : false;
1022
+ return resultFor('approved', { routeId, mode, source: evaluation.source, target, corrections: evaluation.corrections, cat, brain, brainInfo, remembered: route.remembered, continuation });
1023
+ }
1024
+ // The session as it is now (the catalog was awaited since): its computer-use answer decides what the card says.
1025
+ const card = createCard({ sessionId, request, evaluation, brain, brainInfo, cat, mode, origin, session: getSession(sessionId) || session });
1026
+ call('sendCard', sessionId, card.packet);
1027
+ // A host can close the card while it is being sent (the WhatsApp bridge, when the user already
1028
+ // wrote something else): no "card" event after its "expired", no wait, never "pending".
1029
+ if (card.status === 'pending') call('routeEvent', sessionId, 'card', routeView({ ...card, status: 'pending', target: evaluation.proposal }, { catalog: cat, brain, brainInfo }));
1030
+ // Wait the configured time, never past the caller's own deadline (MCP
1031
+ // clients give up on a tool call after ~60 s by default).
1032
+ const configured = r.waitSeconds * 1000;
1033
+ const budget = waitMs === null || waitMs === undefined ? configured : Math.min(configured, Number(waitMs));
1034
+ const wait = Math.max(0, Math.min(Number.isFinite(budget) ? budget : 0, 110_000));
1035
+ const outcome = card.settled || (wait > 0 ? await waitFor(card, wait, signal) : null);
1036
+ if (!outcome) {
1037
+ card.mailboxOnDecision = true;
1038
+ return resultFor('pending', { routeId: card.routeId, mode, card, cat, brain, brainInfo });
1039
+ }
1040
+ if (outcome.result) return outcome.result;
1041
+ const closed = resultFor(outcome.status || 'cancelled', { routeId: card.routeId, mode, card, cat, brain, brainInfo });
1042
+ // The user wrote something else instead of choosing: the turn that asked ends quietly (their message is next).
1043
+ return outcome.movedOn ? { ...closed, reason: 'superseded', next: nextText('moved_on') } : closed;
1044
+ }
1045
+
1046
+ /** The open card of a request or route id; throws for one that is unknown, closed or already decided. */
1047
+ function openCard(idOrRequestId, { sessionId = null } = {}) {
1048
+ const routeId = byRequest.get(String(idOrRequestId)) || String(idOrRequestId);
1049
+ const card = cards.get(routeId);
1050
+ const scoped = sessionId !== null && sessionId !== undefined;
1051
+ if (!card) {
1052
+ const done = decided.get(routeId);
1053
+ if (done && (!scoped || done.sessionId === String(sessionId))) throw routeError('ROUTE_ALREADY_DECIDED', 'This route was already decided.', 409);
1054
+ throw routeError('ROUTE_NOT_FOUND', `Unknown route ${idOrRequestId}`, 404);
1055
+ }
1056
+ // A card of another session is not this caller's to answer: it reads as unknown.
1057
+ if (scoped && card.sessionId !== String(sessionId)) throw routeError('ROUTE_NOT_FOUND', `Unknown route ${idOrRequestId}`, 404);
1058
+ if (card.status !== 'pending') throw routeError('ROUTE_ALREADY_DECIDED', 'This route was already decided.', 409);
1059
+ return card;
1060
+ }
1061
+ /**
1062
+ * Answer a pending card (from the UI socket or REST). `sessionId`: the session
1063
+ * the answer came in on; a card of another session is refused (ROUTE_NOT_FOUND).
1064
+ */
1065
+ async function answer(idOrRequestId, response = {}, { origin = 'ui', sessionId = null, authority = null } = {}) {
1066
+ const card = openCard(idOrRequestId, { sessionId });
1067
+ const routeId = card.routeId;
1068
+ const cat = await catalogValue();
1069
+ // The card can close while the catalog is read (the user wrote something else: cancel; a
1070
+ // timeout; another host's answer). Only the card that is still this one, still pending, is
1071
+ // decided: an answer in progress never undoes a cancellation or decides a card twice.
1072
+ if (cards.get(routeId) !== card || card.status !== 'pending') openCard(routeId, { sessionId });
1073
+ const session = getSession(card.sessionId) || { brain: card.brain };
1074
+ const brain = session.brain || card.brain || {};
1075
+ const brainInfo = brainInfoFor(brain);
1076
+ const parsed = targetFromAnswer({ card, response, catalog: cat, brain });
1077
+ if (parsed.declined) {
1078
+ card.status = 'declined';
1079
+ for (const runId of card.runIds) call('declineHeld', runId, 'route_declined');
1080
+ const view = routeView({ ...card, status: 'declined', decidedBy: 'user' }, { catalog: cat, brain, brainInfo });
1081
+ call('routeEvent', card.sessionId, 'declined', view);
1082
+ const result = resultFor('declined', { routeId, mode: card.mode, card, cat, brain, brainInfo, runIds: [...card.runIds] });
1083
+ if (card.mailboxOnDecision && !card.waiters.size) call('mailbox', card.sessionId, { kind: 'route_declined', route: view, runIds: [...card.runIds], text: `The user declined the route for "${card.summary || card.taskClass}".` });
1084
+ settle(card, { status: 'declined', result });
1085
+ closeCard(card);
1086
+ return result;
1087
+ }
1088
+ const { target, optionId, remember, corrections = [] } = parsed;
1089
+ // Every held run must be able to start on the pick (the dispatcher's checkHeld: a design run's
1090
+ // sight and browser settings on that provider). A pick one would refuse is not approved, saved
1091
+ // or reported started: the card stays open with the reason.
1092
+ for (const runId of card.runIds) {
1093
+ const refusal = call('checkHeld', runId, target);
1094
+ if (refusal) throw routeError(refusal.code || 'ROUTE_INVALID', refusal.message || 'The held run cannot start on that model.', Number(refusal.status) || 400);
1095
+ }
1096
+ // "Do it here" on another model continues in the turn the card was raised in,
1097
+ // handed over first. The runtime refuses a pick that turn no longer owns (a
1098
+ // newer prompt, a Stop or the session closing came after it): then nothing is
1099
+ // approved or remembered, and the card expires on every host.
1100
+ const continuation = !card.runIds.size && continuesHere(target, brain);
1101
+ // The decision is claimed here, before the first side effect (the runtime's continuation, the
1102
+ // held runs, the mailbox): nothing a sink does can close or decide this card again meanwhile.
1103
+ card.status = 'approved';
1104
+ if (continuation && !maybeContinuation(card.sessionId, target, brain, { routeId, summary: card.summary, taskClass: card.taskClass, target, decidedBy: 'user', optionId })) {
1105
+ card.status = 'pending';
1106
+ expire(routeId, 'superseded');
1107
+ log('router:superseded', `${routeId}: a "do it here" pick after its turn was replaced; nothing approved`);
1108
+ return { ...resultFor('expired', { routeId, mode: card.mode, card, cat, brain, brainInfo }), reason: 'superseded' };
1109
+ }
1110
+ let remembered = false;
1111
+ if (remember && card.taskClass !== 'chat' && configStore?.setPreference) {
1112
+ try {
1113
+ const described = describeTarget(target, { catalog: cat, brain, brainInfo });
1114
+ configStore.setPreference(card.taskClass, { ...target, label: described?.label || null }, { by: 'user' });
1115
+ remembered = true;
1116
+ } catch (error) { log('router:remember-error', error?.message || String(error)); }
1117
+ }
1118
+ const route = {
1119
+ routeId, status: 'approved', sessionId: card.sessionId, taskClass: card.taskClass, summary: card.summary, confidence: card.confidence,
1120
+ target, decidedBy: 'user', remembered, optionId, corrections, mode: card.mode, runIds: [...card.runIds],
1121
+ reason: card.options.find((o) => o.id === optionId)?.reason || null, alternatives: card.alternatives, expiresAt: now() + DECIDED_TTL_MS,
1122
+ };
1123
+ decided.set(routeId, route);
1124
+ const heldRuns = [...card.runIds];
1125
+ // A remote session: the held run this card was for is its one dispatch.
1126
+ if (heldRuns.length && session.remote === true) route.consumed = true;
1127
+ for (const runId of heldRuns) call('startHeld', runId, target, { routeId, decidedBy: 'user', remembered, optionId });
1128
+ const view = routeView(route, { catalog: cat, brain, brainInfo });
1129
+ call('routeEvent', card.sessionId, 'decided', view);
1130
+ const inTurnWaiter = card.waiters.size > 0;
1131
+ const viaMailbox = !inTurnWaiter && !continuation && (card.mailboxOnDecision || heldRuns.length > 0);
1132
+ // The card said the Mac will be controlled, and this is the option it said it for: the yes is
1133
+ // also the computer approval of the turn that carries the route out (this one, or the mailbox
1134
+ // turn that executes the decided route). From what the card recorded when it was sent; the
1135
+ // runtime checks who answered and that nothing changed since. Before the result goes back.
1136
+ if (card.computer && optionId === card.computer.optionId && target.kind === 'direct' && !continuation && !heldRuns.length) {
1137
+ // `authority` is passed on untouched and never kept: the runtime alone can tell whose it is.
1138
+ call('computerApproved', card.sessionId, { routeId, origin, viaMailbox, authority });
1139
+ }
1140
+ const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation });
1141
+ if (viaMailbox) {
1142
+ call('mailbox', card.sessionId, { kind: 'route_decided', route: view, runIds: heldRuns, text: heldRuns.length ? `The user picked ${view.target?.label || 'a model'}; the held run${heldRuns.length === 1 ? '' : 's'} started.` : `The user picked ${view.target?.label || 'a model'}. ${result.next}` });
1143
+ }
1144
+ settle(card, { status: 'approved', result });
1145
+ closeCard(card);
1146
+ log('router:decided', `${routeId} ${target.provider}/${target.model || 'default'} by ${origin}`);
1147
+ return result;
1148
+ }
1149
+
1150
+ /**
1151
+ * agent_dispatch enforcement. Returns { action:'start', spec, route, claimed? } or
1152
+ * { action:'hold', routeId, spec, route }. UI dispatches are never held.
1153
+ * A remote session (WhatsApp: session.remote) gets one dispatch per card: a
1154
+ * decided route is used up by the first dispatch that starts a run (one the
1155
+ * dispatcher refuses first gives it back: releaseRoute), a card holds one run,
1156
+ * and a workflow does not fan out under one card. The desktop keeps both.
1157
+ */
1158
+ async function resolveDispatch({ sessionId, spec = {}, origin = 'assistant' } = {}) {
1159
+ pruneDecided();
1160
+ if (origin === 'ui' || !sessionId) return { action: 'start', spec, route: null };
1161
+ const session = getSession(sessionId);
1162
+ if (!session) return { action: 'start', spec, route: null };
1163
+ const brain = session.brain || {};
1164
+ const cat = await catalogValue();
1165
+ const brainInfo = brainInfoFor(brain);
1166
+ const mode = modeFor(sessionId);
1167
+ const r = routing();
1168
+ const routeId = spec.routeId ? String(spec.routeId) : null;
1169
+ const remote = session.remote === true;
1170
+ if (routeId) {
1171
+ const approved = decided.get(routeId);
1172
+ if (approved && approved.sessionId === sessionId && approved.target && !approved.consumed) {
1173
+ // A remote session's route is claimed here, so a second dispatch cannot take it meanwhile.
1174
+ // `claimed` tells the dispatcher to give it back (releaseRoute) if it refuses the launch
1175
+ // before a run exists: only the run it starts uses the route up.
1176
+ if (remote) approved.consumed = true;
1177
+ const t = approved.target;
1178
+ const applied = { ...spec, provider: t.provider, model: t.model || null, effort: t.effort || spec.effort || null };
1179
+ return { action: 'start', spec: applied, route: { ...routeMeta(approved, cat, brain, brainInfo), requested: { provider: spec.provider || null, model: spec.model || null, effort: spec.effort || null } }, ...(remote ? { claimed: true } : {}) };
1180
+ }
1181
+ const pending = cards.get(routeId);
1182
+ if (pending && pending.sessionId === sessionId && pending.status === 'pending' && !(remote && pending.runIds.size)) {
1183
+ return { action: 'hold', routeId, spec, route: pendingMeta(pending) };
1184
+ }
1185
+ }
1186
+ const request = normalizeRouteRequest({
1187
+ task_class: spec.taskClass || 'code', confidence: spec.confidence ?? null, summary: spec.title || clip(spec.task, 200),
1188
+ needs_vision: !!spec.usesComputer,
1189
+ proposals: [{ kind: 'dispatch', provider: spec.provider || brain.provider, model: spec.model || null, effort: spec.effort || null }],
1190
+ }, { brain });
1191
+ if (!spec.provider) request.proposals[0].provider = brain.provider || 'claude-code';
1192
+ const evaluation = evaluateRoute({ mode, routing: r, request, brain, brainInfo, catalog: cat });
1193
+ if (evaluation.unavailable) throw routeError('NO_CAPABLE_MODEL', noMakerText(request), 409, { requires: request.needsOutput, ...(request.needsOutput ? {} : { needs: 'vision' }), taskClass: request.taskClass });
1194
+ if (!evaluation.ask) {
1195
+ const t = evaluation.auto;
1196
+ const applied = { ...spec, provider: t.provider, model: t.model || null, effort: t.effort || spec.effort || null };
1197
+ const route = {
1198
+ routeId: `route-${randomId()}`, status: 'auto', decidedBy: evaluation.source === 'remembered' ? 'remembered' : 'brain', mode,
1199
+ taskClass: request.taskClass, confidence: request.confidence, target: t, corrections: evaluation.corrections,
1200
+ remembered: evaluation.source === 'remembered',
1201
+ };
1202
+ return { action: 'start', spec: applied, route: { ...routeMeta(route, cat, brain, brainInfo), requested: { provider: spec.provider || null, model: spec.model || null, effort: spec.effort || null } } };
1203
+ }
1204
+ let card = spec.workflowId && !remote ? [...cards.values()].find((c) => c.sessionId === sessionId && c.status === 'pending' && c.workflowId && c.workflowId === spec.workflowId) : null;
1205
+ if (!card) {
1206
+ card = createCard({ sessionId, request, evaluation, brain, brainInfo, cat, mode, origin: 'dispatch', workflowId: spec.workflowId || null });
1207
+ call('sendCard', sessionId, card.packet);
1208
+ // Closed while it was being sent: there is no card to hold a run on.
1209
+ if (card.status !== 'pending') throw routeClosedError(card);
1210
+ call('routeEvent', sessionId, 'card', routeView({ ...card, status: 'pending', target: evaluation.proposal }, { catalog: cat, brain, brainInfo }));
1211
+ }
1212
+ return { action: 'hold', routeId: card.routeId, spec, route: pendingMeta(card) };
1213
+ }
1214
+
1215
+ function routeMeta(route, cat, brain, brainInfo) {
1216
+ return {
1217
+ routeId: route.routeId, status: route.status === 'approved' ? 'approved' : route.status, decidedBy: route.decidedBy || null, mode: route.mode || null,
1218
+ taskClass: route.taskClass || null, taskClassLabel: TASK_CLASS_META[route.taskClass]?.label || null, confidence: route.confidence ?? null,
1219
+ target: route.target ? describeTarget(route.target, { catalog: cat, brain, brainInfo }) : null,
1220
+ corrections: route.corrections || [], remembered: !!route.remembered, decidedAt: new Date(now()).toISOString(),
1221
+ };
1222
+ }
1223
+ function pendingMeta(card) {
1224
+ return {
1225
+ routeId: card.routeId, status: 'pending', decidedBy: null, mode: card.mode, taskClass: card.taskClass,
1226
+ taskClassLabel: TASK_CLASS_META[card.taskClass]?.label || null, confidence: card.confidence ?? null,
1227
+ target: null, corrections: [], remembered: false, decidedAt: null,
1228
+ };
1229
+ }
1230
+
1231
+ /**
1232
+ * Give back a remote route resolveDispatch claimed, when the dispatcher refused that dispatch
1233
+ * before a run existed (MODEL_CANNOT_SEE, BROWSER_SETTINGS_CONFLICT, a busy provider…): retrying
1234
+ * the route_id gets the route again, so it runs or is refused for the same reason, and never
1235
+ * falls through to a fresh route of another class.
1236
+ */
1237
+ function releaseRoute(routeId) {
1238
+ const route = decided.get(String(routeId || ''));
1239
+ if (!route?.consumed) return false;
1240
+ route.consumed = false;
1241
+ return true;
1242
+ }
1243
+
1244
+ /** Attach runs to an open card → false when the card is gone (the caller must not leave a run waiting on it). */
1245
+ function holdRuns(routeId, runIds = []) {
1246
+ const card = cards.get(routeId);
1247
+ if (!card || card.status !== 'pending') return false;
1248
+ for (const runId of runIds) if (runId) card.runIds.add(String(runId));
1249
+ card.mailboxOnDecision = true;
1250
+ refreshPacketRuns(card);
1251
+ return true;
1252
+ }
1253
+ function cancelForRun(runId, reason = 'run_stopped') {
1254
+ for (const card of [...cards.values()]) {
1255
+ if (!card.runIds.delete(String(runId))) continue;
1256
+ refreshPacketRuns(card);
1257
+ if (!card.runIds.size && card.origin === 'dispatch') expire(card.routeId, reason);
1258
+ }
1259
+ }
1260
+ /**
1261
+ * Close one pending card of a session without a decision (requestId or routeId) → true when
1262
+ * it was open. `sessionId` is required: a card of another session is never closed here.
1263
+ * 'superseded': the user wrote something else instead of answering (the WhatsApp bridge);
1264
+ * its held runs are declined, nothing is approved, and the brain hears no "declined" event.
1265
+ */
1266
+ function cancel(idOrRequestId, reason = 'cancelled', { sessionId = null } = {}) {
1267
+ const routeId = byRequest.get(String(idOrRequestId)) || String(idOrRequestId);
1268
+ const card = cards.get(routeId);
1269
+ if (!card || card.status !== 'pending') return false;
1270
+ if (!sessionId || card.sessionId !== String(sessionId)) return false;
1271
+ expire(routeId, reason, { movedOn: reason === 'superseded' });
1272
+ return true;
1273
+ }
1274
+ function cancelForSession(sessionId, reason = 'session_closed', { origins = null } = {}) {
1275
+ for (const card of [...cards.values()]) {
1276
+ if (card.sessionId !== sessionId) continue;
1277
+ if (origins && !origins.includes(card.origin)) continue;
1278
+ expire(card.routeId, reason);
1279
+ }
1280
+ }
1281
+ function pendingCards(sessionId) { return [...cards.values()].filter((c) => c.sessionId === sessionId && c.status === 'pending').map((c) => c.packet); }
1282
+ /** A request id of an open route card; with `sessionId`, only a card of that session. */
1283
+ function owns(requestId, sessionId = null) {
1284
+ const routeId = byRequest.get(String(requestId));
1285
+ if (!routeId) return false;
1286
+ return sessionId === null || sessionId === undefined || cards.get(routeId)?.sessionId === String(sessionId);
1287
+ }
1288
+ function status(routeId) {
1289
+ const card = cards.get(routeId);
1290
+ if (card) return { routeId, status: card.status, sessionId: card.sessionId, runIds: [...card.runIds], taskClass: card.taskClass, expiresAt: new Date(card.expiresAt).toISOString(), request: card.packet.request };
1291
+ const route = decided.get(routeId);
1292
+ if (route) return { routeId, status: route.status, sessionId: route.sessionId, runIds: route.runIds || [], taskClass: route.taskClass, target: route.target, decidedBy: route.decidedBy };
1293
+ return null;
1294
+ }
1295
+
1296
+ /** The `[SynaBun Router]` line the brain sees when mode/preferences change. */
1297
+ function stamp(sessionId) {
1298
+ const r = routing();
1299
+ const mode = modeFor(sessionId);
1300
+ const prefs = Object.entries(r.preferences || {}).filter(([, p]) => p?.provider)
1301
+ .map(([cls, p]) => `${cls} → ${p.kind === 'direct' ? 'here' : p.provider}${p.model ? `/${p.model}` : ''}${p.effort ? ` (${p.effort})` : ''}`);
1302
+ const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model)'
1303
+ : mode === 'never' ? 'never ask (agent_route approves at once; the saved routes below are binding and agent_route applies them over your pick, so classify the task correctly)'
1304
+ : `ask when unsure (agent_route asks the user below ${Math.round(r.askBelow * 100)}% confidence or for unknown/missing models)`;
1305
+ const text = `[SynaBun Router] Route mode: ${modeLine}.${prefs.length ? ` ${mode === 'never' ? 'Saved routes (binding)' : 'Remembered routes'}: ${prefs.join('; ')}.` : ''}`;
1306
+ const changed = stamps.get(sessionId) !== text;
1307
+ return { text, changed, commit: () => stamps.set(sessionId, text) };
1308
+ }
1309
+
1310
+ function shutdown() {
1311
+ for (const card of [...cards.values()]) { if (card.timer) clearTimeout(card.timer); settle(card, { status: 'cancelled' }); }
1312
+ cards.clear();
1313
+ byRequest.clear();
1314
+ }
1315
+
1316
+ return {
1317
+ propose, answer, resolveDispatch, releaseRoute, holdRuns, cancel, cancelForRun, cancelForSession, pendingCards, owns, status, modeFor, stamp, shutdown,
1318
+ routeClosedError: (routeId) => routeClosedError(lastClosed.get(String(routeId || '')) || { routeId: String(routeId || '') }),
1319
+ routing, escalationFor: ({ run, depth = 0 }) => escalationFor({ run, catalog: catalog.peek?.() || null, routing: routing(), depth }),
1320
+ _internals: { cards, decided, byRequest },
1321
+ };
1322
+ }
1323
+
1324
+ export { TIERS, priceText };