@thevinci/web 1.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 (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,566 @@
1
+ export const createNotificationTriggerRuntime = (deps) => {
2
+ const {
3
+ readSettingsFromDisk,
4
+ prepareNotificationLastMessage,
5
+ buildTemplateVariables,
6
+ extractLastMessageText,
7
+ fetchLastAssistantMessageText,
8
+ resolveNotificationTemplate,
9
+ shouldApplyResolvedTemplateMessage,
10
+ emitDesktopNotification,
11
+ broadcastUiNotification,
12
+ sendPushToAllUiSessions,
13
+ buildOpenCodeUrl,
14
+ getOpenCodeAuthHeaders,
15
+ } = deps;
16
+
17
+ let getIsWindowFocused = typeof deps.getIsWindowFocused === 'function'
18
+ ? deps.getIsWindowFocused
19
+ : null;
20
+
21
+ const setGetIsWindowFocused = (cb) => {
22
+ getIsWindowFocused = typeof cb === 'function' ? cb : null;
23
+ };
24
+
25
+ const PUSH_READY_COOLDOWN_MS = 5000;
26
+ const PUSH_QUESTION_DEBOUNCE_MS = 500;
27
+ const PUSH_PERMISSION_DEBOUNCE_MS = 500;
28
+ const pushQuestionDebounceTimers = new Map();
29
+ const pushPermissionDebounceTimers = new Map();
30
+ const notifiedPermissionRequests = new Set();
31
+ const lastReadyNotificationAt = new Map();
32
+
33
+ const sessionParentIdCache = new Map();
34
+ const SESSION_PARENT_CACHE_TTL_MS = 60 * 1000;
35
+
36
+ // Sessions where the client has enabled Permission Auto-Accept. Mirrored
37
+ // from the client-side permissionStore via POST /api/notifications/auto-accept
38
+ // so the server can suppress permission notifications BEFORE dispatch (the
39
+ // 500ms debounce race otherwise leaks notifications for auto-accepted
40
+ // permissions when the replied round-trip is slower than the debounce).
41
+ const autoAcceptingSessions = new Set();
42
+ const setAutoAcceptSession = (sessionId, enabled) => {
43
+ if (typeof sessionId !== 'string' || sessionId.length === 0) return;
44
+ if (enabled) {
45
+ autoAcceptingSessions.add(sessionId);
46
+ } else {
47
+ autoAcceptingSessions.delete(sessionId);
48
+ }
49
+ };
50
+
51
+ const buildSessionDeepLinkUrl = (sessionId) => {
52
+ if (!sessionId || typeof sessionId !== 'string') {
53
+ return '/';
54
+ }
55
+ return `/?session=${encodeURIComponent(sessionId)}`;
56
+ };
57
+
58
+ const getCachedSessionParentId = (sessionId) => {
59
+ const entry = sessionParentIdCache.get(sessionId);
60
+ if (!entry) return undefined;
61
+ if (Date.now() - entry.at > SESSION_PARENT_CACHE_TTL_MS) {
62
+ sessionParentIdCache.delete(sessionId);
63
+ return undefined;
64
+ }
65
+ return entry.parentID;
66
+ };
67
+
68
+ const setCachedSessionParentId = (sessionId, parentID) => {
69
+ if (!parentID) return;
70
+ sessionParentIdCache.set(sessionId, { parentID: parentID ?? null, at: Date.now() });
71
+ };
72
+
73
+ const getParentIdFromPayload = (payload) => {
74
+ if (!payload || typeof payload !== 'object') return null;
75
+ if (payload.type !== 'session.created' && payload.type !== 'session.updated') return null;
76
+ const parentID = payload.properties?.info?.parentID ?? null;
77
+ return typeof parentID === 'string' && parentID.length > 0 ? parentID : null;
78
+ };
79
+
80
+ const maybeCacheSessionParentFromPayload = (payload) => {
81
+ const sessionId = extractSessionIdFromPayload(payload);
82
+ if (typeof sessionId !== 'string' || sessionId.length === 0) return;
83
+ const parentID = getParentIdFromPayload(payload);
84
+ if (parentID) {
85
+ setCachedSessionParentId(sessionId, parentID);
86
+ }
87
+ };
88
+
89
+ const fetchSessionParentId = async (sessionId) => {
90
+ if (!sessionId) return undefined;
91
+
92
+ const cached = getCachedSessionParentId(sessionId);
93
+ if (cached !== undefined) return cached;
94
+
95
+ try {
96
+ const response = await fetch(buildOpenCodeUrl('/session', ''), {
97
+ method: 'GET',
98
+ headers: {
99
+ Accept: 'application/json',
100
+ ...getOpenCodeAuthHeaders(),
101
+ },
102
+ signal: AbortSignal.timeout(2000),
103
+ });
104
+ if (!response.ok) {
105
+ return undefined;
106
+ }
107
+ const data = await response.json().catch(() => null);
108
+ const sessions = Array.isArray(data)
109
+ ? data
110
+ : Array.isArray(data?.items)
111
+ ? data.items
112
+ : Array.isArray(data?.data)
113
+ ? data.data
114
+ : null;
115
+ if (!sessions) {
116
+ return undefined;
117
+ }
118
+
119
+ const match = sessions.find((session) => session && typeof session === 'object' && session.id === sessionId);
120
+ const parentID = match?.parentID ?? null;
121
+ setCachedSessionParentId(sessionId, parentID);
122
+ return parentID;
123
+ } catch {
124
+ return undefined;
125
+ }
126
+ };
127
+
128
+ // Mirrors client-side autoRespondsPermission: a session auto-accepts if it
129
+ // OR any ancestor is flagged. Walks the parent chain via fetchSessionParentId.
130
+ const isSessionAutoAccepting = async (sessionId) => {
131
+ if (!sessionId || autoAcceptingSessions.size === 0) return false;
132
+ let current = sessionId;
133
+ const seen = new Set();
134
+ while (current && !seen.has(current)) {
135
+ if (autoAcceptingSessions.has(current)) return true;
136
+ seen.add(current);
137
+ const parent = await fetchSessionParentId(current);
138
+ if (!parent) return false;
139
+ current = parent;
140
+ }
141
+ return false;
142
+ };
143
+
144
+ const extractSessionIdFromPayload = (payload) => {
145
+ if (!payload || typeof payload !== 'object') return null;
146
+ const props = payload.properties;
147
+ const info = props?.info;
148
+ const sessionId =
149
+ info?.sessionID ??
150
+ info?.sessionId ??
151
+ props?.sessionID ??
152
+ props?.sessionId ??
153
+ props?.session ??
154
+ null;
155
+ return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
156
+ };
157
+
158
+ const formatMode = (raw) => {
159
+ const value = typeof raw === 'string' ? raw.trim() : '';
160
+ const normalized = value.length > 0 ? value : 'agent';
161
+ return normalized
162
+ .split(/[-_\s]+/)
163
+ .filter(Boolean)
164
+ .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
165
+ .join(' ');
166
+ };
167
+
168
+ const formatModelId = (raw) => {
169
+ const value = typeof raw === 'string' ? raw.trim() : '';
170
+ if (!value) {
171
+ return 'Assistant';
172
+ }
173
+
174
+ const tokens = value.split(/[-_]+/).filter(Boolean);
175
+ const result = [];
176
+ for (let i = 0; i < tokens.length; i += 1) {
177
+ const current = tokens[i];
178
+ const next = tokens[i + 1];
179
+ if (/^\d+$/.test(current) && next && /^\d+$/.test(next)) {
180
+ result.push(`${current}.${next}`);
181
+ i += 1;
182
+ continue;
183
+ }
184
+ result.push(current);
185
+ }
186
+
187
+ return result
188
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
189
+ .join(' ');
190
+ };
191
+
192
+ const maybeSendPushForTrigger = async (payload) => {
193
+ if (!payload || typeof payload !== 'object') {
194
+ return;
195
+ }
196
+
197
+ maybeCacheSessionParentFromPayload(payload);
198
+
199
+ const sessionId = extractSessionIdFromPayload(payload);
200
+ if (payload.type === 'message.updated') {
201
+ const info = payload.properties?.info;
202
+ if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId) {
203
+ const settings = await readSettingsFromDisk();
204
+
205
+ if (settings.notifyOnSubtasks === false) {
206
+ const parentIDFromPayload = getParentIdFromPayload(payload);
207
+ const parentID = parentIDFromPayload
208
+ ? parentIDFromPayload
209
+ : await fetchSessionParentId(sessionId);
210
+
211
+ if (parentID) {
212
+ return;
213
+ }
214
+ }
215
+
216
+ if (settings.notifyOnCompletion === false) {
217
+ return;
218
+ }
219
+
220
+ if (settings.notificationMode !== 'always' && getIsWindowFocused?.()) {
221
+ return;
222
+ }
223
+
224
+ const now = Date.now();
225
+ const lastAt = lastReadyNotificationAt.get(sessionId) ?? 0;
226
+ if (now - lastAt < PUSH_READY_COOLDOWN_MS) {
227
+ return;
228
+ }
229
+ lastReadyNotificationAt.set(sessionId, now);
230
+
231
+ let title = `${formatMode(info?.mode)} agent is ready`;
232
+ let body = `${formatModelId(info?.modelID)} completed the task`;
233
+
234
+ try {
235
+ const templates = settings.notificationTemplates || {};
236
+ const isSubtask = await fetchSessionParentId(sessionId);
237
+ const completionTemplate = isSubtask && settings.notifyOnSubtasks !== false
238
+ ? (templates.subtask || templates.completion || { title: '{agent_name} is ready', message: '{model_name} completed the task' })
239
+ : (templates.completion || { title: '{agent_name} is ready', message: '{model_name} completed the task' });
240
+
241
+ const variables = await buildTemplateVariables(payload, sessionId);
242
+
243
+ const messageId = info?.id;
244
+ let lastMessage = extractLastMessageText(payload);
245
+ if (!lastMessage) {
246
+ lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
247
+ }
248
+
249
+ variables.last_message = await prepareNotificationLastMessage({
250
+ message: lastMessage,
251
+ settings,
252
+ });
253
+
254
+ const resolvedTitle = resolveNotificationTemplate(completionTemplate.title, variables);
255
+ const resolvedBody = resolveNotificationTemplate(completionTemplate.message, variables);
256
+ if (resolvedTitle) title = resolvedTitle;
257
+ if (shouldApplyResolvedTemplateMessage(completionTemplate.message, resolvedBody, variables)) body = resolvedBody;
258
+ } catch (error) {
259
+ console.warn('[Notification] Template resolution failed, using defaults:', error?.message || error);
260
+ }
261
+
262
+ if (settings.nativeNotificationsEnabled) {
263
+ const notificationPayload = {
264
+ title,
265
+ body,
266
+ tag: `ready-${sessionId}`,
267
+ kind: 'ready',
268
+ sessionId,
269
+ requireHidden: settings.notificationMode !== 'always',
270
+ };
271
+ emitDesktopNotification(notificationPayload);
272
+ broadcastUiNotification(notificationPayload);
273
+ }
274
+
275
+ await sendPushToAllUiSessions(
276
+ {
277
+ title,
278
+ body,
279
+ tag: `ready-${sessionId}`,
280
+ data: {
281
+ url: buildSessionDeepLinkUrl(sessionId),
282
+ sessionId,
283
+ type: 'ready',
284
+ },
285
+ },
286
+ { requireNoSse: true },
287
+ );
288
+ }
289
+
290
+ if (info?.role === 'assistant' && info?.finish === 'error' && sessionId) {
291
+ const settings = await readSettingsFromDisk();
292
+ if (settings.notifyOnError === false) return;
293
+
294
+ if (settings.notificationMode !== 'always' && getIsWindowFocused?.()) {
295
+ return;
296
+ }
297
+
298
+ let title = 'Tool error';
299
+ let body = 'An error occurred';
300
+
301
+ try {
302
+ const variables = await buildTemplateVariables(payload, sessionId);
303
+ const errorMessageId = info?.id;
304
+ let lastMessage = extractLastMessageText(payload);
305
+ if (!lastMessage) {
306
+ lastMessage = await fetchLastAssistantMessageText(sessionId, errorMessageId);
307
+ }
308
+
309
+ variables.last_message = await prepareNotificationLastMessage({
310
+ message: lastMessage,
311
+ settings,
312
+ });
313
+
314
+ const errorTemplate = (settings.notificationTemplates || {}).error || { title: 'Tool error', message: '{last_message}' };
315
+ const resolvedTitle = resolveNotificationTemplate(errorTemplate.title, variables);
316
+ const resolvedBody = resolveNotificationTemplate(errorTemplate.message, variables);
317
+ if (resolvedTitle) title = resolvedTitle;
318
+ if (shouldApplyResolvedTemplateMessage(errorTemplate.message, resolvedBody, variables)) body = resolvedBody;
319
+ } catch (error) {
320
+ console.warn('[Notification] Error template resolution failed, using defaults:', error?.message || error);
321
+ }
322
+
323
+ if (settings.nativeNotificationsEnabled) {
324
+ const notificationPayload = {
325
+ title,
326
+ body,
327
+ tag: `error-${sessionId}`,
328
+ kind: 'error',
329
+ sessionId,
330
+ requireHidden: settings.notificationMode !== 'always',
331
+ };
332
+ emitDesktopNotification(notificationPayload);
333
+ broadcastUiNotification(notificationPayload);
334
+ }
335
+
336
+ await sendPushToAllUiSessions(
337
+ {
338
+ title,
339
+ body,
340
+ tag: `error-${sessionId}`,
341
+ data: {
342
+ url: buildSessionDeepLinkUrl(sessionId),
343
+ sessionId,
344
+ type: 'error',
345
+ },
346
+ },
347
+ { requireNoSse: true },
348
+ );
349
+ }
350
+
351
+ return;
352
+ }
353
+
354
+ if (payload.type === 'question.asked' && sessionId) {
355
+ const existingTimer = pushQuestionDebounceTimers.get(sessionId);
356
+ if (existingTimer) {
357
+ clearTimeout(existingTimer);
358
+ }
359
+
360
+ const timer = setTimeout(async () => {
361
+ pushQuestionDebounceTimers.delete(sessionId);
362
+
363
+ const settings = await readSettingsFromDisk();
364
+ if (settings.notifyOnQuestion === false) {
365
+ return;
366
+ }
367
+
368
+ if (settings.notificationMode !== 'always' && getIsWindowFocused?.()) {
369
+ return;
370
+ }
371
+
372
+ const firstQuestion = payload.properties?.questions?.[0];
373
+ const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
374
+ const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
375
+
376
+ let title = /plan\s*mode/i.test(header)
377
+ ? 'Switch to plan mode'
378
+ : /build\s*agent/i.test(header)
379
+ ? 'Switch to build mode'
380
+ : header || 'Input needed';
381
+ let body = questionText || 'Agent is waiting for your response';
382
+
383
+ try {
384
+ const variables = await buildTemplateVariables(payload, sessionId);
385
+ variables.last_message = questionText || header || '';
386
+
387
+ const templates = settings.notificationTemplates || {};
388
+ const questionTemplate = templates.question || { title: 'Input needed', message: '{last_message}' };
389
+
390
+ const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
391
+ const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
392
+ if (resolvedTitle) title = resolvedTitle;
393
+ if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
394
+ } catch (error) {
395
+ console.warn('[Notification] Question template resolution failed, using defaults:', error?.message || error);
396
+ }
397
+
398
+ if (settings.nativeNotificationsEnabled) {
399
+ emitDesktopNotification({
400
+ kind: 'question',
401
+ title,
402
+ body,
403
+ tag: `question-${sessionId}`,
404
+ sessionId,
405
+ requireHidden: settings.notificationMode !== 'always',
406
+ });
407
+
408
+ broadcastUiNotification({
409
+ kind: 'question',
410
+ title,
411
+ body,
412
+ tag: `question-${sessionId}`,
413
+ sessionId,
414
+ requireHidden: settings.notificationMode !== 'always',
415
+ });
416
+ }
417
+
418
+ void sendPushToAllUiSessions(
419
+ {
420
+ title,
421
+ body,
422
+ tag: `question-${sessionId}`,
423
+ data: {
424
+ url: buildSessionDeepLinkUrl(sessionId),
425
+ sessionId,
426
+ type: 'question',
427
+ },
428
+ },
429
+ { requireNoSse: true },
430
+ );
431
+ }, PUSH_QUESTION_DEBOUNCE_MS);
432
+
433
+ pushQuestionDebounceTimers.set(sessionId, timer);
434
+ return;
435
+ }
436
+
437
+ if (payload.type === 'permission.replied' && sessionId) {
438
+ const requestId = payload.properties?.requestID ?? payload.properties?.requestId ?? payload.properties?.id;
439
+ const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
440
+ const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
441
+ if (!pendingNotification) {
442
+ return;
443
+ }
444
+
445
+ // Some runtimes may omit requestID on permission.replied.
446
+ // When request ID is missing, clear session debounce to avoid
447
+ // showing stale permission notifications for auto-approved prompts.
448
+ if (!requestKey || !pendingNotification.requestKey || pendingNotification.requestKey === requestKey) {
449
+ clearTimeout(pendingNotification.timer);
450
+ pushPermissionDebounceTimers.delete(sessionId);
451
+ }
452
+ return;
453
+ }
454
+
455
+ if (payload.type === 'permission.asked' && sessionId) {
456
+ const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
457
+ const permission = payload.properties?.permission;
458
+ const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
459
+ if (requestKey && notifiedPermissionRequests.has(requestKey)) {
460
+ return;
461
+ }
462
+
463
+ // Client may be in Permission Auto-Accept for this session (or any
464
+ // ancestor). Skip the whole notification path — the client responds
465
+ // directly and the user has opted out of approval prompts.
466
+ if (await isSessionAutoAccepting(sessionId)) {
467
+ if (requestKey) notifiedPermissionRequests.add(requestKey);
468
+ return;
469
+ }
470
+
471
+ const existingTimer = pushPermissionDebounceTimers.get(sessionId);
472
+ if (existingTimer) {
473
+ clearTimeout(existingTimer.timer);
474
+ }
475
+
476
+ const timer = setTimeout(async () => {
477
+ pushPermissionDebounceTimers.delete(sessionId);
478
+
479
+ if (await isSessionAutoAccepting(sessionId)) {
480
+ if (requestKey) notifiedPermissionRequests.add(requestKey);
481
+ return;
482
+ }
483
+
484
+ const settings = await readSettingsFromDisk();
485
+
486
+ if (settings.notifyOnQuestion === false) {
487
+ return;
488
+ }
489
+
490
+ if (settings.notificationMode !== 'always' && getIsWindowFocused?.()) {
491
+ return;
492
+ }
493
+
494
+ const sessionTitle = payload.properties?.sessionTitle;
495
+ const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
496
+ const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
497
+ ? sessionTitle.trim()
498
+ : permissionText || 'Agent is waiting for your approval';
499
+
500
+ let title = 'Permission required';
501
+ let body = fallbackMessage;
502
+
503
+ try {
504
+ const variables = await buildTemplateVariables(payload, sessionId);
505
+ variables.last_message = fallbackMessage;
506
+
507
+ const templates = settings.notificationTemplates || {};
508
+ const questionTemplate = templates.question || { title: 'Permission required', message: '{last_message}' };
509
+
510
+ const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
511
+ const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
512
+ if (resolvedTitle) title = resolvedTitle;
513
+ if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
514
+ } catch (error) {
515
+ console.warn('[Notification] Permission template resolution failed, using defaults:', error?.message || error);
516
+ }
517
+
518
+ if (settings.nativeNotificationsEnabled) {
519
+ emitDesktopNotification({
520
+ kind: 'permission',
521
+ title,
522
+ body,
523
+ tag: requestKey ? `permission-${requestKey}` : `permission-${sessionId}`,
524
+ sessionId,
525
+ requireHidden: settings.notificationMode !== 'always',
526
+ });
527
+
528
+ broadcastUiNotification({
529
+ kind: 'permission',
530
+ title,
531
+ body,
532
+ tag: requestKey ? `permission-${requestKey}` : `permission-${sessionId}`,
533
+ sessionId,
534
+ requireHidden: settings.notificationMode !== 'always',
535
+ });
536
+ }
537
+
538
+ if (requestKey) {
539
+ notifiedPermissionRequests.add(requestKey);
540
+ }
541
+
542
+ void sendPushToAllUiSessions(
543
+ {
544
+ title,
545
+ body,
546
+ tag: `permission-${sessionId}`,
547
+ data: {
548
+ url: buildSessionDeepLinkUrl(sessionId),
549
+ sessionId,
550
+ type: 'permission',
551
+ },
552
+ },
553
+ { requireNoSse: true },
554
+ );
555
+ }, PUSH_PERMISSION_DEBOUNCE_MS);
556
+
557
+ pushPermissionDebounceTimers.set(sessionId, { timer, requestKey });
558
+ }
559
+ };
560
+
561
+ return {
562
+ maybeSendPushForTrigger,
563
+ setAutoAcceptSession,
564
+ setGetIsWindowFocused,
565
+ };
566
+ };