@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,483 @@
1
+ // Gateway-proxy integration test.
2
+ // Starts a discord-digital-twin (fake Discord), a gateway-proxy Rust binary
3
+ // in front of it, and the kimaki bot connecting through the proxy.
4
+ // Validates that messages create threads, bot replies, and multi-tenant
5
+ // guild filtering routes events to the right clients.
6
+ //
7
+ // Requires the gateway-proxy binary at gateway-proxy/target/release/gateway-proxy.
8
+ // If not found, all tests are skipped.
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import url from 'node:url';
12
+ import net from 'node:net';
13
+ import { spawn } from 'node:child_process';
14
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest';
15
+ import { ChannelType, Client, GatewayIntentBits, Partials, Routes, } from 'discord.js';
16
+ import { DigitalDiscord } from 'discord-digital-twin/src';
17
+ import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
18
+ import { startHranaServer, stopHranaServer } from './hrana-server.js';
19
+ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, } from './database.js';
20
+ import { setDataDir } from './config.js';
21
+ import { startDiscordBot } from './discord-bot.js';
22
+ import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, } from './test-utils.js';
23
+ import { stopOpencodeServer } from './opencode.js';
24
+ import { createDiscordRest } from './discord-urls.js';
25
+ import { store } from './store.js';
26
+ // --- Constants ---
27
+ const BINARY_PATH = path.resolve(process.cwd(), '..', 'gateway-proxy', 'target', 'release', 'gateway-proxy');
28
+ const TEST_USER_ID = '900000000000000001';
29
+ const CHANNEL_1_ID = '900000000000000010';
30
+ const CHANNEL_2_ID = '900000000000000020';
31
+ const GUILD_1_ID = '900000000000000100';
32
+ const GUILD_2_ID = '900000000000000200';
33
+ const binaryExists = fs.existsSync(BINARY_PATH);
34
+ // --- Helpers ---
35
+ function getAvailablePort() {
36
+ return new Promise((resolve, reject) => {
37
+ const srv = net.createServer();
38
+ srv.listen(0, () => {
39
+ const addr = srv.address();
40
+ if (!addr || typeof addr === 'string') {
41
+ srv.close();
42
+ reject(new Error('Failed to get port'));
43
+ return;
44
+ }
45
+ const port = addr.port;
46
+ srv.close(() => {
47
+ resolve(port);
48
+ });
49
+ });
50
+ });
51
+ }
52
+ function createRunDirectories() {
53
+ const root = path.resolve(process.cwd(), 'tmp', 'gateway-proxy-e2e');
54
+ fs.mkdirSync(root, { recursive: true });
55
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
56
+ const projectDirectory = path.join(root, 'project');
57
+ fs.mkdirSync(projectDirectory, { recursive: true });
58
+ initTestGitRepo(projectDirectory);
59
+ return { root, dataDir, projectDirectory };
60
+ }
61
+ function createDiscordJsClient({ restUrl }) {
62
+ return new Client({
63
+ intents: [
64
+ GatewayIntentBits.Guilds,
65
+ GatewayIntentBits.GuildMessages,
66
+ GatewayIntentBits.MessageContent,
67
+ ],
68
+ partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
69
+ rest: { api: restUrl, version: '10' },
70
+ });
71
+ }
72
+ function hasStringId(value) {
73
+ if (typeof value !== 'object' || value === null) {
74
+ return false;
75
+ }
76
+ if (!('id' in value)) {
77
+ return false;
78
+ }
79
+ return typeof value.id === 'string';
80
+ }
81
+ function createMatchers() {
82
+ const defaultReply = {
83
+ id: 'default-reply',
84
+ priority: 10,
85
+ when: { lastMessageRole: 'user' },
86
+ then: {
87
+ parts: [
88
+ { type: 'stream-start', warnings: [] },
89
+ { type: 'text-start', id: 'reply' },
90
+ { type: 'text-delta', id: 'reply', delta: 'gateway-proxy-reply' },
91
+ { type: 'text-end', id: 'reply' },
92
+ {
93
+ type: 'finish',
94
+ finishReason: 'stop',
95
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
96
+ },
97
+ ],
98
+ },
99
+ };
100
+ return [defaultReply];
101
+ }
102
+ async function waitForProxyReady({ port, timeoutMs = 30_000, }) {
103
+ const start = Date.now();
104
+ while (Date.now() - start < timeoutMs) {
105
+ try {
106
+ const res = await fetch(`http://127.0.0.1:${port}/shard-count`);
107
+ if (res.ok) {
108
+ return;
109
+ }
110
+ }
111
+ catch {
112
+ // not ready yet
113
+ }
114
+ await new Promise((r) => {
115
+ setTimeout(r, 500);
116
+ });
117
+ }
118
+ throw new Error(`gateway-proxy not ready after ${timeoutMs}ms`);
119
+ }
120
+ function startGatewayProxy({ configDir, port, twinPort, botToken, gatewayUrl, }) {
121
+ const config = {
122
+ log_level: 'info',
123
+ token: botToken,
124
+ intents: 32511,
125
+ shards: 1,
126
+ port,
127
+ validate_token: true,
128
+ gateway_url: gatewayUrl,
129
+ twilight_http_proxy: `127.0.0.1:${twinPort}`,
130
+ externally_accessible_url: `ws://127.0.0.1:${port}`,
131
+ cache: {
132
+ channels: true,
133
+ presences: false,
134
+ emojis: false,
135
+ current_member: true,
136
+ members: false,
137
+ roles: true,
138
+ scheduled_events: false,
139
+ stage_instances: false,
140
+ stickers: false,
141
+ users: false,
142
+ voice_states: false,
143
+ },
144
+ clients: {
145
+ 'client-a': {
146
+ secret: 'secret-a',
147
+ guilds: [GUILD_1_ID],
148
+ },
149
+ 'client-b': {
150
+ secret: 'secret-b',
151
+ guilds: [GUILD_2_ID],
152
+ },
153
+ },
154
+ };
155
+ const configPath = path.join(configDir, 'config.json');
156
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
157
+ const child = spawn(BINARY_PATH, [], {
158
+ cwd: configDir,
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ env: { ...process.env, RUST_LOG: 'debug' },
161
+ });
162
+ const showLogs = !!process.env['KIMAKI_TEST_LOGS'];
163
+ child.stdout?.on('data', (data) => {
164
+ const line = data.toString().trim();
165
+ if (line && showLogs) {
166
+ console.log(`[gateway-proxy] ${line}`);
167
+ }
168
+ });
169
+ child.stderr?.on('data', (data) => {
170
+ const line = data.toString().trim();
171
+ if (line && showLogs) {
172
+ console.log(`[gateway-proxy] ${line}`);
173
+ }
174
+ });
175
+ return { process: child, configPath };
176
+ }
177
+ // --- Test suite ---
178
+ const describeIf = binaryExists ? describe : describe.skip;
179
+ describeIf('gateway-proxy e2e', () => {
180
+ let discord;
181
+ let proxyProcess;
182
+ let botClient;
183
+ let directories;
184
+ let proxyPort;
185
+ let previousDefaultVerbosity;
186
+ let firstThreadId;
187
+ let testStartTime = Date.now();
188
+ beforeAll(async () => {
189
+ testStartTime = Date.now();
190
+ const lockPort = chooseLockPort({ key: CHANNEL_1_ID });
191
+ directories = createRunDirectories();
192
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
193
+ process.env['KIMAKI_VITEST'] = '1';
194
+ setDataDir(directories.dataDir);
195
+ previousDefaultVerbosity = store.getState().defaultVerbosity;
196
+ store.setState({ defaultVerbosity: 'text_only' });
197
+ const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
198
+ proxyPort = await getAvailablePort();
199
+ // Start digital-twin with 2 guilds, each with a text channel.
200
+ // gatewayUrlOverride makes GET /gateway/bot return the proxy's URL
201
+ // so discord.js clients connect through the proxy, not directly to twin.
202
+ discord = new DigitalDiscord({
203
+ guilds: [
204
+ {
205
+ id: GUILD_1_ID,
206
+ name: 'Guild One',
207
+ ownerId: TEST_USER_ID,
208
+ channels: [
209
+ { id: CHANNEL_1_ID, name: 'general-1', type: ChannelType.GuildText },
210
+ ],
211
+ },
212
+ {
213
+ id: GUILD_2_ID,
214
+ name: 'Guild Two',
215
+ ownerId: TEST_USER_ID,
216
+ channels: [
217
+ { id: CHANNEL_2_ID, name: 'general-2', type: ChannelType.GuildText },
218
+ ],
219
+ },
220
+ ],
221
+ users: [{ id: TEST_USER_ID, username: 'proxy-tester' }],
222
+ gatewayUrlOverride: `ws://127.0.0.1:${proxyPort}`,
223
+ dbUrl: `file:${digitalDiscordDbPath}`,
224
+ });
225
+ await discord.start();
226
+ // Write opencode.json with deterministic provider
227
+ const providerNpm = url
228
+ .pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
229
+ .toString();
230
+ const opencodeConfig = buildDeterministicOpencodeConfig({
231
+ providerName: 'deterministic-provider',
232
+ providerNpm,
233
+ model: 'deterministic-v2',
234
+ smallModel: 'deterministic-v2',
235
+ settings: { strict: false, matchers: createMatchers() },
236
+ });
237
+ fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
238
+ // Start gateway-proxy binary pointing at twin
239
+ const proxyConfigDir = path.join(directories.dataDir, 'proxy');
240
+ fs.mkdirSync(proxyConfigDir, { recursive: true });
241
+ const proxy = startGatewayProxy({
242
+ configDir: proxyConfigDir,
243
+ port: proxyPort,
244
+ twinPort: discord.port,
245
+ botToken: discord.botToken,
246
+ gatewayUrl: discord.gatewayUrl,
247
+ });
248
+ proxyProcess = proxy.process;
249
+ // Wait for proxy to be ready (HTTP server up)
250
+ await waitForProxyReady({ port: proxyPort, timeoutMs: 30_000 });
251
+ // Initialize kimaki database
252
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
253
+ const hranaResult = await startHranaServer({ dbPath });
254
+ if (hranaResult instanceof Error) {
255
+ throw hranaResult;
256
+ }
257
+ process.env['KIMAKI_DB_URL'] = hranaResult;
258
+ await initDatabase();
259
+ await setBotToken(discord.botUserId, discord.botToken);
260
+ // Register channel 1 with kimaki (bot will create sessions for messages here)
261
+ await setChannelDirectory({
262
+ channelId: CHANNEL_1_ID,
263
+ directory: directories.projectDirectory,
264
+ channelType: 'text',
265
+ });
266
+ // Start the kimaki bot connected through the proxy
267
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl });
268
+ await startDiscordBot({
269
+ token: discord.botToken,
270
+ appId: discord.botUserId,
271
+ discordClient: botClient,
272
+ });
273
+ }, 120_000);
274
+ afterAll(async () => {
275
+ if (directories) {
276
+ await cleanupTestSessions({
277
+ projectDirectory: directories.projectDirectory,
278
+ testStartTime,
279
+ });
280
+ }
281
+ if (botClient) {
282
+ botClient.destroy();
283
+ }
284
+ if (proxyProcess && !proxyProcess.killed) {
285
+ proxyProcess.kill('SIGTERM');
286
+ }
287
+ await stopOpencodeServer();
288
+ await Promise.all([
289
+ closeDatabase().catch(() => { }),
290
+ stopHranaServer().catch(() => { }),
291
+ discord?.stop().catch(() => { }),
292
+ ]);
293
+ delete process.env['KIMAKI_LOCK_PORT'];
294
+ delete process.env['KIMAKI_DB_URL'];
295
+ delete process.env['KIMAKI_VITEST'];
296
+ if (previousDefaultVerbosity) {
297
+ store.setState({ defaultVerbosity: previousDefaultVerbosity });
298
+ }
299
+ if (directories) {
300
+ fs.rmSync(directories.dataDir, { recursive: true, force: true });
301
+ }
302
+ }, 30_000);
303
+ test('message creates thread and bot replies through proxy', async () => {
304
+ await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
305
+ content: 'hello from gateway proxy test',
306
+ });
307
+ const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
308
+ timeout: 15_000,
309
+ predicate: (t) => {
310
+ return t.name?.includes('hello from gateway proxy test') ?? false;
311
+ },
312
+ });
313
+ expect(thread).toBeDefined();
314
+ expect(thread.id).toBeTruthy();
315
+ firstThreadId = thread.id;
316
+ const reply = await discord.thread(thread.id).waitForBotReply({ timeout: 15_000 });
317
+ await waitForFooterMessage({
318
+ discord,
319
+ threadId: thread.id,
320
+ timeout: 15_000,
321
+ });
322
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
323
+ "--- from: user (proxy-tester)
324
+ hello from gateway proxy test
325
+ --- from: assistant (TestBot)
326
+ ⬥ gateway-proxy-reply
327
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
328
+ `);
329
+ expect(reply).toBeDefined();
330
+ expect(reply.content.trim().length).toBeGreaterThan(0);
331
+ }, 15_000);
332
+ test('follow-up message in thread gets bot reply', async () => {
333
+ const existingMessages = await discord.thread(firstThreadId).getMessages();
334
+ const existingIds = new Set(existingMessages.map((m) => m.id));
335
+ await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
336
+ content: 'follow up through proxy',
337
+ });
338
+ const reply = await discord.thread(firstThreadId).waitForMessage({
339
+ predicate: (m) => !existingIds.has(m.id) && m.author.id === discord.botUserId,
340
+ });
341
+ await waitForFooterMessage({
342
+ discord,
343
+ threadId: firstThreadId,
344
+ timeout: 4_000,
345
+ afterMessageIncludes: 'follow up through proxy',
346
+ afterAuthorId: TEST_USER_ID,
347
+ });
348
+ expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
349
+ "--- from: user (proxy-tester)
350
+ hello from gateway proxy test
351
+ --- from: assistant (TestBot)
352
+ ⬥ gateway-proxy-reply
353
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
354
+ --- from: user (proxy-tester)
355
+ follow up through proxy
356
+ --- from: assistant (TestBot)
357
+ ⬥ gateway-proxy-reply
358
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
359
+ `);
360
+ expect(reply).toBeDefined();
361
+ expect(reply.content.trim().length).toBeGreaterThan(0);
362
+ }, 15_000);
363
+ // Reconnect test lives in gateway-proxy-reconnect.e2e.test.ts.
364
+ // It was here before but kills the proxy mid-suite, breaking shared
365
+ // state (bot/proxy connection) for all subsequent tests.
366
+ test('shell command via ! prefix in thread', async () => {
367
+ const existingMessages = await discord.thread(firstThreadId).getMessages();
368
+ const existingIds = new Set(existingMessages.map((m) => m.id));
369
+ await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
370
+ content: '!echo proxy-shell-test',
371
+ });
372
+ // The bot replies with a loading message then edits it with the result.
373
+ // The predicate waits for the edited version containing "exited with".
374
+ const reply = await discord.thread(firstThreadId).waitForMessage({
375
+ predicate: (m) => !existingIds.has(m.id) &&
376
+ m.author.id === discord.botUserId &&
377
+ m.content.includes('exited with'),
378
+ });
379
+ expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
380
+ "--- from: user (proxy-tester)
381
+ hello from gateway proxy test
382
+ --- from: assistant (TestBot)
383
+ ⬥ gateway-proxy-reply
384
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
385
+ --- from: user (proxy-tester)
386
+ follow up through proxy
387
+ --- from: assistant (TestBot)
388
+ ⬥ gateway-proxy-reply
389
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
390
+ --- from: user (proxy-tester)
391
+ !echo proxy-shell-test
392
+ --- from: assistant (TestBot)
393
+ \`echo proxy-shell-test\` exited with 0
394
+ \`\`\`
395
+ proxy-shell-test
396
+ \`\`\`"
397
+ `);
398
+ expect(reply.content).toContain('proxy-shell-test');
399
+ }, 15_000);
400
+ test('second message creates separate thread', async () => {
401
+ await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
402
+ content: 'second message through proxy',
403
+ });
404
+ const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
405
+ predicate: (t) => (t.name?.includes('second message through proxy') ?? false) &&
406
+ t.id !== firstThreadId,
407
+ });
408
+ expect(thread).toBeDefined();
409
+ expect(thread.id).not.toBe(firstThreadId);
410
+ const reply = await discord.thread(thread.id).waitForBotReply();
411
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
412
+ "--- from: user (proxy-tester)
413
+ second message through proxy
414
+ --- from: assistant (TestBot)
415
+ ⬥ gateway-proxy-reply"
416
+ `);
417
+ expect(reply).toBeDefined();
418
+ expect(reply.content.trim().length).toBeGreaterThan(0);
419
+ }, 15_000);
420
+ test('guild-2 message does not create thread (guild isolation)', async () => {
421
+ await discord.channel(CHANNEL_2_ID).user(TEST_USER_ID).sendMessage({
422
+ content: 'should not create thread in guild 2',
423
+ });
424
+ // Brief wait for events to propagate through the local system.
425
+ // The proxy filters guild-2 events away from client-a, so no thread
426
+ // should be created. 100ms is more than enough for local event routing.
427
+ await new Promise((r) => {
428
+ setTimeout(r, 100);
429
+ });
430
+ const threads = await discord.channel(CHANNEL_2_ID).getThreads();
431
+ expect(threads).toHaveLength(0);
432
+ }, 5_000);
433
+ test('slash command routes INTERACTION_CREATE through proxy', async () => {
434
+ const { id: interactionId } = await discord
435
+ .channel(CHANNEL_1_ID)
436
+ .user(TEST_USER_ID)
437
+ .runSlashCommand({
438
+ name: 'run-shell-command',
439
+ options: [{ name: 'command', type: 3, value: 'echo proxy-slash-test' }],
440
+ });
441
+ const ack = await discord.channel(CHANNEL_1_ID).waitForInteractionAck({
442
+ interactionId,
443
+ });
444
+ expect(ack.acknowledged).toBe(true);
445
+ }, 15_000);
446
+ test('REST client operations work through proxy and enforce guild scope', async () => {
447
+ const previousBaseUrl = store.getState().discordBaseUrl;
448
+ store.setState({ discordBaseUrl: `http://127.0.0.1:${proxyPort}` });
449
+ try {
450
+ const botRest = createDiscordRest(discord.botToken);
451
+ const clientRest = createDiscordRest('client-a:secret-a');
452
+ const posted = await botRest.post(Routes.channelMessages(CHANNEL_1_ID), {
453
+ body: { content: 'rest-proxy-test-message' },
454
+ });
455
+ expect(hasStringId(posted)).toBe(true);
456
+ if (!hasStringId(posted)) {
457
+ throw new Error('Expected REST message create response to include id');
458
+ }
459
+ const thread = await botRest.post(Routes.threads(CHANNEL_1_ID, posted.id), {
460
+ body: { name: 'rest-proxy-thread' },
461
+ });
462
+ expect(hasStringId(thread)).toBe(true);
463
+ const channel = await botRest.get(Routes.channel(CHANNEL_1_ID));
464
+ expect(hasStringId(channel)).toBe(true);
465
+ const guildChannels = await clientRest.get(Routes.guildChannels(GUILD_1_ID));
466
+ expect(Array.isArray(guildChannels)).toBe(true);
467
+ const forbiddenGuildResponse = await fetch(`http://127.0.0.1:${proxyPort}/api/v10${Routes.guildChannels(GUILD_2_ID)}`, {
468
+ method: 'GET',
469
+ headers: {
470
+ Authorization: 'Bot client-a:secret-a',
471
+ },
472
+ });
473
+ expect(forbiddenGuildResponse.status).toBe(403);
474
+ const gatewayInfo = await clientRest.get(Routes.gatewayBot());
475
+ expect(typeof gatewayInfo).toBe('object');
476
+ const me = await clientRest.get(Routes.user('@me'));
477
+ expect(hasStringId(me)).toBe(true);
478
+ }
479
+ finally {
480
+ store.setState({ discordBaseUrl: previousBaseUrl });
481
+ }
482
+ }, 15_000);
483
+ });
@@ -0,0 +1,111 @@
1
+ // Main thread interface for the GenAI worker.
2
+ // Spawns and manages the worker thread, handling message passing for
3
+ // audio input/output, tool call completions, and graceful shutdown.
4
+ import { Worker } from 'node:worker_threads';
5
+ import { createLogger, LogPrefix } from './logger.js';
6
+ import { notifyError } from './sentry.js';
7
+ const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER);
8
+ const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER);
9
+ export function createGenAIWorker(options) {
10
+ return new Promise((resolve, reject) => {
11
+ const worker = new Worker(new URL('../dist/genai-worker.js', import.meta.url));
12
+ // Handle messages from worker
13
+ worker.on('message', (message) => {
14
+ switch (message.type) {
15
+ case 'assistantOpusPacket':
16
+ options.onAssistantOpusPacket(message.packet);
17
+ break;
18
+ case 'assistantStartSpeaking':
19
+ options.onAssistantStartSpeaking?.();
20
+ break;
21
+ case 'assistantStopSpeaking':
22
+ options.onAssistantStopSpeaking?.();
23
+ break;
24
+ case 'assistantInterruptSpeaking':
25
+ options.onAssistantInterruptSpeaking?.();
26
+ break;
27
+ case 'toolCallCompleted':
28
+ options.onToolCallCompleted?.(message);
29
+ break;
30
+ case 'error':
31
+ genaiWorkerLogger.error('Error:', message.error);
32
+ options.onError?.(message.error);
33
+ break;
34
+ case 'ready':
35
+ genaiWorkerLogger.log('Ready');
36
+ // Resolve with the worker interface
37
+ resolve({
38
+ sendRealtimeInput({ audio, audioStreamEnd }) {
39
+ worker.postMessage({
40
+ type: 'sendRealtimeInput',
41
+ audio,
42
+ audioStreamEnd,
43
+ });
44
+ },
45
+ sendTextInput(text) {
46
+ worker.postMessage({
47
+ type: 'sendTextInput',
48
+ text,
49
+ });
50
+ },
51
+ interrupt() {
52
+ worker.postMessage({
53
+ type: 'interrupt',
54
+ });
55
+ },
56
+ async stop() {
57
+ genaiWrapperLogger.log('Stopping worker...');
58
+ // Send stop message to trigger graceful shutdown
59
+ worker.postMessage({ type: 'stop' });
60
+ // Wait for worker to exit gracefully (with timeout)
61
+ await new Promise((resolve) => {
62
+ let resolved = false;
63
+ // Listen for worker exit
64
+ worker.once('exit', (code) => {
65
+ if (!resolved) {
66
+ resolved = true;
67
+ genaiWrapperLogger.log(`[GENAI WORKER WRAPPER] Worker exited with code ${code}`);
68
+ resolve();
69
+ }
70
+ });
71
+ // Timeout after 5 seconds and force terminate
72
+ setTimeout(() => {
73
+ if (!resolved) {
74
+ resolved = true;
75
+ genaiWrapperLogger.log('[GENAI WORKER WRAPPER] Worker did not exit gracefully, terminating...');
76
+ worker.terminate().then(() => {
77
+ genaiWrapperLogger.log('Worker terminated');
78
+ resolve();
79
+ });
80
+ }
81
+ }, 5000);
82
+ });
83
+ },
84
+ });
85
+ break;
86
+ }
87
+ });
88
+ // Handle worker errors
89
+ worker.on('error', (error) => {
90
+ genaiWorkerLogger.error('Worker error:', error);
91
+ reject(error);
92
+ });
93
+ worker.on('exit', (code) => {
94
+ if (code !== 0) {
95
+ genaiWorkerLogger.error(`Worker stopped with exit code ${code}`);
96
+ void notifyError(new Error(`GenAI worker exited with code ${code}`), 'GenAI worker non-zero exit after init');
97
+ }
98
+ });
99
+ // Send initialization message
100
+ const initMessage = {
101
+ type: 'init',
102
+ directory: options.directory,
103
+ systemMessage: options.systemMessage,
104
+ guildId: options.guildId,
105
+ channelId: options.channelId,
106
+ appId: options.appId,
107
+ geminiApiKey: options.geminiApiKey,
108
+ };
109
+ worker.postMessage(initMessage);
110
+ });
111
+ }