@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,692 @@
1
+ // E2e capture tests for generating real OpenCode session-event JSONL fixtures.
2
+ // Uses opencode-cached-provider + Gemini to record real tool/lifecycle streams
3
+ // (task, interruption, permission, action buttons, and question flows).
4
+
5
+ import fs from 'node:fs'
6
+
7
+ import path from 'node:path'
8
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
9
+ import { ChannelType, Client, GatewayIntentBits, Partials, type APIMessage } from 'discord.js'
10
+ import { DigitalDiscord } from 'discord-digital-twin/src'
11
+ import { CachedOpencodeProviderProxy } from 'opencode-cached-provider'
12
+ import { setDataDir } from './config.js'
13
+ import { store } from './store.js'
14
+ import { startDiscordBot } from './discord-bot.js'
15
+ import {
16
+ closeDatabase,
17
+ getChannelVerbosity,
18
+ initDatabase,
19
+ setBotToken,
20
+ setChannelDirectory,
21
+ setChannelVerbosity,
22
+ type VerbosityLevel,
23
+ } from './database.js'
24
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
25
+ import { chooseLockPort, cleanupTestSessions, initTestGitRepo } from './test-utils.js'
26
+ import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage } from './test-utils.js'
27
+ import { stopOpencodeServer } from './opencode.js'
28
+ import { disposeRuntime, pendingPermissions } from './session-handler/thread-session-runtime.js'
29
+ import { pendingActionButtonContexts } from './commands/action-buttons.js'
30
+ import { pendingQuestionContexts } from './commands/ask-question.js'
31
+ import type { OpencodeEventLogEntry } from './session-handler/opencode-session-event-log.js'
32
+
33
+ const geminiApiKey =
34
+ process.env['GEMINI_API_KEY'] ||
35
+ process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
36
+ ''
37
+ const geminiModel = process.env['GEMINI_FLASH_MODEL'] || 'gemini-2.5-flash'
38
+ const shouldRunRealCapture =
39
+ geminiApiKey.length > 0 && process.env['KIMAKI_RUN_REAL_EVENT_CAPTURE'] === '1'
40
+ const realCaptureTest = shouldRunRealCapture ? test : test.skip
41
+
42
+ const TEST_USER_ID = '200000000000003001'
43
+ const TEXT_CHANNEL_ID = '200000000000003002'
44
+
45
+ function createRunDirectories() {
46
+ const root = path.resolve(process.cwd(), 'tmp', 'event-stream-real-capture-e2e')
47
+ fs.mkdirSync(root, { recursive: true })
48
+
49
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
50
+ const projectDirectory = path.join(root, 'project')
51
+ const providerCacheDbPath = path.join(root, 'provider-cache.db')
52
+ const sessionEventsDir = path.join(root, 'opencode-session-events')
53
+ const fixtureOutputDir = path.resolve(
54
+ process.cwd(),
55
+ 'src',
56
+ 'session-handler',
57
+ 'event-stream-fixtures',
58
+ )
59
+ fs.mkdirSync(projectDirectory, { recursive: true })
60
+ initTestGitRepo(projectDirectory)
61
+ fs.mkdirSync(sessionEventsDir, { recursive: true })
62
+
63
+ return {
64
+ root,
65
+ dataDir,
66
+ projectDirectory,
67
+ providerCacheDbPath,
68
+ sessionEventsDir,
69
+ fixtureOutputDir,
70
+ }
71
+ }
72
+
73
+
74
+
75
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
76
+ return new Client({
77
+ intents: [
78
+ GatewayIntentBits.Guilds,
79
+ GatewayIntentBits.GuildMessages,
80
+ GatewayIntentBits.MessageContent,
81
+ GatewayIntentBits.GuildVoiceStates,
82
+ ],
83
+ partials: [
84
+ Partials.Channel,
85
+ Partials.Message,
86
+ Partials.User,
87
+ Partials.ThreadMember,
88
+ ],
89
+ rest: {
90
+ api: restUrl,
91
+ version: '10',
92
+ },
93
+ })
94
+ }
95
+
96
+ function readJsonlEvents(filePath: string): OpencodeEventLogEntry[] {
97
+ const content = fs.readFileSync(filePath, 'utf8')
98
+ const lines = content.split('\n').filter((line) => {
99
+ return line.trim().length > 0
100
+ })
101
+ return lines.map((line) => {
102
+ return JSON.parse(line) as OpencodeEventLogEntry
103
+ })
104
+ }
105
+
106
+ function hasToolEvent({ events, tool }: { events: OpencodeEventLogEntry[]; tool: string }): boolean {
107
+ return events.some((line) => {
108
+ if (line.event.type !== 'message.part.updated') {
109
+ return false
110
+ }
111
+ const part = line.event.properties.part
112
+ if (part.type !== 'tool') {
113
+ return false
114
+ }
115
+ return part.tool === tool
116
+ })
117
+ }
118
+
119
+ function listJsonlFiles(directory: string): string[] {
120
+ return fs.readdirSync(directory).filter((name) => {
121
+ return name.endsWith('.jsonl')
122
+ })
123
+ }
124
+
125
+ async function waitForNewOrUpdatedSessionLog({
126
+ directory,
127
+ before,
128
+ timeoutMs,
129
+ }: {
130
+ directory: string
131
+ before: Map<string, { size: number; mtimeMs: number }>
132
+ timeoutMs: number
133
+ }): Promise<string> {
134
+ const start = Date.now()
135
+ while (Date.now() - start < timeoutMs) {
136
+ const files = listJsonlFiles(directory)
137
+ const changedFiles = files.filter((fileName) => {
138
+ const filePath = path.join(directory, fileName)
139
+ const stat = fs.statSync(filePath)
140
+ const previous = before.get(fileName)
141
+ if (!previous) {
142
+ return true
143
+ }
144
+ return stat.size > previous.size || stat.mtimeMs > previous.mtimeMs
145
+ })
146
+ if (changedFiles.length > 0) {
147
+ const newest = [...changedFiles].sort((a, b) => {
148
+ const aMtime = fs.statSync(path.join(directory, a)).mtimeMs
149
+ const bMtime = fs.statSync(path.join(directory, b)).mtimeMs
150
+ return bMtime - aMtime
151
+ })[0]
152
+ if (newest) {
153
+ return path.join(directory, newest)
154
+ }
155
+ }
156
+ await new Promise((resolve) => {
157
+ setTimeout(resolve, 200)
158
+ })
159
+ }
160
+ throw new Error('Timed out waiting for changed session event log file')
161
+ }
162
+
163
+ async function waitForPendingPermission({
164
+ threadId,
165
+ timeoutMs,
166
+ }: {
167
+ threadId: string
168
+ timeoutMs: number
169
+ }): Promise<{ contextHash: string; messageId: string }> {
170
+ const start = Date.now()
171
+ while (Date.now() - start < timeoutMs) {
172
+ const perms = pendingPermissions.get(threadId)
173
+ const first = perms ? [...perms.values()][0] : undefined
174
+ if (first?.contextHash && first.messageId) {
175
+ return { contextHash: first.contextHash, messageId: first.messageId }
176
+ }
177
+ await new Promise((resolve) => {
178
+ setTimeout(resolve, 100)
179
+ })
180
+ }
181
+ throw new Error('Timed out waiting for pending permission context')
182
+ }
183
+
184
+ async function waitForPendingActionButtons({
185
+ threadId,
186
+ timeoutMs,
187
+ }: {
188
+ threadId: string
189
+ timeoutMs: number
190
+ }): Promise<{ contextHash: string; messageId: string }> {
191
+ const start = Date.now()
192
+ while (Date.now() - start < timeoutMs) {
193
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
194
+ return context.thread.id === threadId && !context.resolved && Boolean(context.messageId)
195
+ })
196
+ if (entry && entry[1].messageId) {
197
+ return { contextHash: entry[0], messageId: entry[1].messageId }
198
+ }
199
+ await new Promise((resolve) => {
200
+ setTimeout(resolve, 100)
201
+ })
202
+ }
203
+ throw new Error('Timed out waiting for pending action buttons context')
204
+ }
205
+
206
+ async function waitForPendingQuestion({
207
+ discord,
208
+ threadId,
209
+ timeoutMs,
210
+ }: {
211
+ discord: DigitalDiscord
212
+ threadId: string
213
+ timeoutMs: number
214
+ }): Promise<{ contextHash: string; questionMessage: APIMessage }> {
215
+ const start = Date.now()
216
+ while (Date.now() - start < timeoutMs) {
217
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
218
+ return context.thread.id === threadId
219
+ })
220
+ if (entry) {
221
+ const [contextHash, context] = entry
222
+ const questionMessage = await discord.thread(threadId).waitForMessage({
223
+ timeout: 10_000,
224
+ predicate: (message) => {
225
+ return message.author.id === discord.botUserId
226
+ && message.content.includes('Choose one option')
227
+ },
228
+ })
229
+ if (questionMessage) {
230
+ return {
231
+ contextHash,
232
+ questionMessage,
233
+ }
234
+ }
235
+ }
236
+ await new Promise((resolve) => {
237
+ setTimeout(resolve, 100)
238
+ })
239
+ }
240
+ throw new Error('Timed out waiting for pending question context')
241
+ }
242
+
243
+ describe('real event stream capture fixtures (cached provider)', () => {
244
+ const directories = createRunDirectories()
245
+ let lockPort = 0
246
+ let previousDefaultVerbosity: VerbosityLevel | null = null
247
+ let testStartTime = Date.now()
248
+ let botClient: Client | null = null
249
+
250
+ const proxy = new CachedOpencodeProviderProxy({
251
+ cacheDbPath: directories.providerCacheDbPath,
252
+ targetBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
253
+ apiKey: geminiApiKey,
254
+ cacheMethods: ['POST'],
255
+ })
256
+
257
+ const digitalDiscordDbPath = path.join(
258
+ directories.dataDir,
259
+ 'digital-discord.db',
260
+ )
261
+
262
+ const discord = new DigitalDiscord({
263
+ guild: {
264
+ name: 'Real Event Capture Guild',
265
+ ownerId: TEST_USER_ID,
266
+ },
267
+ channels: [
268
+ {
269
+ id: TEXT_CHANNEL_ID,
270
+ name: 'real-event-capture',
271
+ type: ChannelType.GuildText,
272
+ },
273
+ ],
274
+ users: [
275
+ {
276
+ id: TEST_USER_ID,
277
+ username: 'real-capture-user',
278
+ },
279
+ ],
280
+ dbUrl: `file:${digitalDiscordDbPath}`,
281
+ })
282
+
283
+ async function captureFixture({
284
+ fixtureName,
285
+ beforeFiles,
286
+ assertEvents,
287
+ }: {
288
+ fixtureName: string
289
+ beforeFiles: Map<string, { size: number; mtimeMs: number }>
290
+ assertEvents: (events: OpencodeEventLogEntry[]) => void
291
+ }): Promise<void> {
292
+ const newLogPath = await waitForNewOrUpdatedSessionLog({
293
+ directory: directories.sessionEventsDir,
294
+ before: beforeFiles,
295
+ timeoutMs: 120_000,
296
+ })
297
+ const fixturePath = path.join(directories.fixtureOutputDir, fixtureName)
298
+ fs.copyFileSync(newLogPath, fixturePath)
299
+ const events = readJsonlEvents(fixturePath)
300
+ assertEvents(events)
301
+ }
302
+
303
+ function getSessionLogState(): Map<string, { size: number; mtimeMs: number }> {
304
+ const files = listJsonlFiles(directories.sessionEventsDir)
305
+ return new Map(
306
+ files.map((fileName) => {
307
+ const stat = fs.statSync(path.join(directories.sessionEventsDir, fileName))
308
+ return [fileName, { size: stat.size, mtimeMs: stat.mtimeMs }]
309
+ }),
310
+ )
311
+ }
312
+
313
+ beforeAll(async () => {
314
+ testStartTime = Date.now()
315
+ lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
316
+
317
+ listJsonlFiles(directories.sessionEventsDir).forEach((fileName) => {
318
+ fs.rmSync(path.join(directories.sessionEventsDir, fileName), {
319
+ force: true,
320
+ })
321
+ })
322
+
323
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
324
+ process.env['KIMAKI_LOG_OPENCODE_SESSION_EVENTS'] = '1'
325
+ process.env['KIMAKI_OPENCODE_SESSION_EVENTS_DIR'] = directories.sessionEventsDir
326
+ setDataDir(directories.dataDir)
327
+
328
+ previousDefaultVerbosity = store.getState().defaultVerbosity
329
+ store.setState({ defaultVerbosity: 'tools_and_text' })
330
+
331
+ await Promise.all([proxy.start(), discord.start()])
332
+
333
+ const opencodeConfig = proxy.buildOpencodeConfig({
334
+ providerName: 'cached-google-real-events',
335
+ providerNpm: '@ai-sdk/google',
336
+ model: geminiModel,
337
+ smallModel: geminiModel,
338
+ })
339
+ fs.writeFileSync(
340
+ path.join(directories.projectDirectory, 'opencode.json'),
341
+ JSON.stringify(opencodeConfig, null, 2),
342
+ )
343
+
344
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
345
+ const hranaResult = await startHranaServer({ dbPath })
346
+ if (hranaResult instanceof Error) {
347
+ throw hranaResult
348
+ }
349
+ process.env['KIMAKI_DB_URL'] = hranaResult
350
+ await initDatabase()
351
+ await setBotToken(discord.botUserId, discord.botToken)
352
+ await setChannelDirectory({
353
+ channelId: TEXT_CHANNEL_ID,
354
+ directory: directories.projectDirectory,
355
+ channelType: 'text',
356
+ })
357
+ await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
358
+ expect(await getChannelVerbosity(TEXT_CHANNEL_ID)).toBe('tools_and_text')
359
+
360
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
361
+ await startDiscordBot({
362
+ token: discord.botToken,
363
+ appId: discord.botUserId,
364
+ discordClient: botClient,
365
+ })
366
+ }, 180_000)
367
+
368
+ afterEach(async () => {
369
+ [...pendingActionButtonContexts.values()].forEach((context) => {
370
+ clearTimeout(context.timer)
371
+ })
372
+ pendingActionButtonContexts.clear()
373
+ pendingQuestionContexts.clear()
374
+ pendingPermissions.clear()
375
+
376
+ const threadIds = [...store.getState().threads.keys()]
377
+ threadIds.forEach((threadId) => {
378
+ disposeRuntime(threadId)
379
+ })
380
+ await cleanupTestSessions({
381
+ projectDirectory: directories.projectDirectory,
382
+ testStartTime,
383
+ })
384
+ }, 180_000)
385
+
386
+ afterAll(async () => {
387
+ await cleanupTestSessions({
388
+ projectDirectory: directories.projectDirectory,
389
+ testStartTime,
390
+ })
391
+
392
+ if (botClient) {
393
+ botClient.destroy()
394
+ }
395
+
396
+ await stopOpencodeServer()
397
+ await Promise.all([
398
+ closeDatabase().catch(() => {
399
+ return
400
+ }),
401
+ stopHranaServer().catch(() => {
402
+ return
403
+ }),
404
+ proxy.stop().catch(() => {
405
+ return
406
+ }),
407
+ discord.stop().catch(() => {
408
+ return
409
+ }),
410
+ ])
411
+
412
+ delete process.env['KIMAKI_LOCK_PORT']
413
+ delete process.env['KIMAKI_DB_URL']
414
+ delete process.env['KIMAKI_LOG_OPENCODE_SESSION_EVENTS']
415
+ delete process.env['KIMAKI_OPENCODE_SESSION_EVENTS_DIR']
416
+
417
+ if (previousDefaultVerbosity) {
418
+ store.setState({ defaultVerbosity: previousDefaultVerbosity })
419
+ }
420
+
421
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
422
+ }, 180_000)
423
+
424
+ realCaptureTest(
425
+ 'capture real task flow fixture',
426
+ async () => {
427
+ const beforeFiles = getSessionLogState()
428
+ const prompt =
429
+ 'REAL_FIXTURE_TASK_NORMAL. First response MUST be exactly one tool call: tool `task` with {"description":"inspect repository","subagent_type":"general","prompt":"Read this repository and return exactly: task-subagent-done"}. Do not answer with plain text before the tool call. After the task result returns, respond with exactly: task-normal-done.'
430
+
431
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
432
+ content: prompt,
433
+ })
434
+
435
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
436
+ timeout: 120_000,
437
+ predicate: (t) => {
438
+ return t.name?.includes('REAL_FIXTURE_TASK_NORMAL') ?? false
439
+ },
440
+ })
441
+
442
+ await waitForBotMessageContaining({
443
+ discord,
444
+ threadId: thread.id,
445
+ userId: TEST_USER_ID,
446
+ text: '┣ task',
447
+ timeout: 300_000,
448
+ })
449
+
450
+ await waitForBotMessageContaining({
451
+ discord,
452
+ threadId: thread.id,
453
+ userId: TEST_USER_ID,
454
+ text: 'task-normal-done',
455
+ timeout: 300_000,
456
+ })
457
+
458
+ await captureFixture({
459
+ fixtureName: 'real-session-task-normal.jsonl',
460
+ beforeFiles,
461
+ assertEvents: (events) => {
462
+ expect(events.length).toBeGreaterThan(0)
463
+ expect(hasToolEvent({ events, tool: 'task' })).toBe(true)
464
+ },
465
+ })
466
+ },
467
+ 900_000,
468
+ )
469
+
470
+ realCaptureTest(
471
+ 'capture real task interruption fixture',
472
+ async () => {
473
+ const beforeFiles = getSessionLogState()
474
+ const setupPrompt =
475
+ 'REAL_FIXTURE_TASK_INTERRUPT_START. First response MUST call tool `task` with {"description":"long analysis","subagent_type":"general","prompt":"Perform a long analysis over many files and produce extensive notes"}. Do not send plain text before the tool call.'
476
+
477
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
478
+ content: setupPrompt,
479
+ })
480
+
481
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
482
+ timeout: 120_000,
483
+ predicate: (t) => {
484
+ return t.name?.includes('REAL_FIXTURE_TASK_INTERRUPT_START') ?? false
485
+ },
486
+ })
487
+
488
+ await waitForBotMessageContaining({
489
+ discord,
490
+ threadId: thread.id,
491
+ userId: TEST_USER_ID,
492
+ text: '┣ task',
493
+ timeout: 300_000,
494
+ })
495
+
496
+ await discord.thread(thread.id).user(TEST_USER_ID).sendMessage({
497
+ content: 'REAL_FIXTURE_TASK_INTERRUPT_FOLLOWUP. Stop and reply with exactly: task-interrupt-done.',
498
+ })
499
+
500
+ await waitForBotReplyAfterUserMessage({
501
+ discord,
502
+ threadId: thread.id,
503
+ userId: TEST_USER_ID,
504
+ userMessageIncludes: 'REAL_FIXTURE_TASK_INTERRUPT_FOLLOWUP',
505
+ timeout: 300_000,
506
+ })
507
+
508
+ await captureFixture({
509
+ fixtureName: 'real-session-task-user-interruption.jsonl',
510
+ beforeFiles,
511
+ assertEvents: (events) => {
512
+ expect(events.length).toBeGreaterThan(0)
513
+ expect(hasToolEvent({ events, tool: 'task' })).toBe(true)
514
+ },
515
+ })
516
+ },
517
+ 900_000,
518
+ )
519
+
520
+ realCaptureTest(
521
+ 'capture real permission fixture for external path access',
522
+ async () => {
523
+ const beforeFiles = getSessionLogState()
524
+ const prompt =
525
+ 'REAL_FIXTURE_PERMISSION_EXTERNAL. Use bash (hasSideEffect false) to read this file outside the workspace: /Users/morse/.zprofile. Then summarize the first line.'
526
+
527
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
528
+ content: prompt,
529
+ })
530
+
531
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
532
+ timeout: 120_000,
533
+ predicate: (t) => {
534
+ return t.name?.includes('REAL_FIXTURE_PERMISSION_EXTERNAL') ?? false
535
+ },
536
+ })
537
+
538
+ const pending = await waitForPendingPermission({
539
+ threadId: thread.id,
540
+ timeoutMs: 300_000,
541
+ })
542
+
543
+ const interaction = await discord.thread(thread.id).user(TEST_USER_ID).clickButton({
544
+ messageId: pending.messageId,
545
+ customId: `permission_once:${pending.contextHash}`,
546
+ })
547
+
548
+ await discord.thread(thread.id).waitForInteractionAck({
549
+ interactionId: interaction.id,
550
+ timeout: 30_000,
551
+ })
552
+ await discord.thread(thread.id).waitForBotReply({ timeout: 300_000 })
553
+
554
+ await captureFixture({
555
+ fixtureName: 'real-session-permission-external-file.jsonl',
556
+ beforeFiles,
557
+ assertEvents: (events) => {
558
+ const hasPermissionAsked = events.some((line) => {
559
+ return line.event.type === 'permission.asked'
560
+ })
561
+ const hasPermissionReplied = events.some((line) => {
562
+ return line.event.type === 'permission.replied'
563
+ })
564
+ expect(hasPermissionAsked).toBe(true)
565
+ expect(hasPermissionReplied).toBe(true)
566
+ },
567
+ })
568
+ },
569
+ 900_000,
570
+ )
571
+
572
+ realCaptureTest(
573
+ 'capture real action buttons fixture',
574
+ async () => {
575
+ const beforeFiles = getSessionLogState()
576
+ const prompt =
577
+ 'REAL_FIXTURE_ACTION_BUTTONS. First response MUST call tool `kimaki_action_buttons` with {"buttons":[{"label":"Approve capture","color":"green"}]}. Do not send text before the tool call. After user clicks, reply exactly: action-buttons-done.'
578
+
579
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
580
+ content: prompt,
581
+ })
582
+
583
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
584
+ timeout: 120_000,
585
+ predicate: (t) => {
586
+ return t.name?.includes('REAL_FIXTURE_ACTION_BUTTONS') ?? false
587
+ },
588
+ })
589
+
590
+ const action = await waitForPendingActionButtons({
591
+ threadId: thread.id,
592
+ timeoutMs: 300_000,
593
+ })
594
+
595
+ await waitForBotMessageContaining({
596
+ discord,
597
+ threadId: thread.id,
598
+ userId: TEST_USER_ID,
599
+ text: 'Action Required',
600
+ timeout: 300_000,
601
+ })
602
+
603
+ const interaction = await discord.thread(thread.id).user(TEST_USER_ID).clickButton({
604
+ messageId: action.messageId,
605
+ customId: `action_button:${action.contextHash}:0`,
606
+ })
607
+
608
+ await discord.thread(thread.id).waitForInteractionAck({
609
+ interactionId: interaction.id,
610
+ timeout: 30_000,
611
+ })
612
+ await waitForBotMessageContaining({
613
+ discord,
614
+ threadId: thread.id,
615
+ userId: TEST_USER_ID,
616
+ text: 'action-buttons-done',
617
+ timeout: 300_000,
618
+ })
619
+
620
+ await captureFixture({
621
+ fixtureName: 'real-session-action-buttons.jsonl',
622
+ beforeFiles,
623
+ assertEvents: (events) => {
624
+ expect(events.length).toBeGreaterThan(0)
625
+ const hasActionTool = hasToolEvent({ events, tool: 'kimaki_action_buttons' })
626
+ expect(hasActionTool).toBe(true)
627
+ },
628
+ })
629
+ },
630
+ 900_000,
631
+ )
632
+
633
+ realCaptureTest(
634
+ 'capture real question tool fixture',
635
+ async () => {
636
+ const beforeFiles = getSessionLogState()
637
+ const prompt =
638
+ 'REAL_FIXTURE_QUESTION_TOOL. First response MUST call tool `question` with {"questions":[{"question":"Choose one option","header":"Pick one","options":[{"label":"Alpha","description":"Alpha option"},{"label":"Beta","description":"Beta option"}]}]}. Do not send text before the tool call. After user selects, reply exactly: question-tool-done.'
639
+
640
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
641
+ content: prompt,
642
+ })
643
+
644
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
645
+ timeout: 120_000,
646
+ predicate: (t) => {
647
+ return t.name?.includes('REAL_FIXTURE_QUESTION_TOOL') ?? false
648
+ },
649
+ })
650
+
651
+ const pending = await waitForPendingQuestion({
652
+ discord,
653
+ threadId: thread.id,
654
+ timeoutMs: 300_000,
655
+ })
656
+
657
+ const interaction = await discord.thread(thread.id).user(TEST_USER_ID).selectMenu({
658
+ messageId: pending.questionMessage.id,
659
+ customId: `ask_question:${pending.contextHash}:0`,
660
+ values: ['0'],
661
+ })
662
+
663
+ await discord.thread(thread.id).waitForInteractionAck({
664
+ interactionId: interaction.id,
665
+ timeout: 30_000,
666
+ })
667
+ await waitForBotMessageContaining({
668
+ discord,
669
+ threadId: thread.id,
670
+ userId: TEST_USER_ID,
671
+ text: 'question-tool-done',
672
+ timeout: 300_000,
673
+ })
674
+
675
+ await captureFixture({
676
+ fixtureName: 'real-session-question-tool.jsonl',
677
+ beforeFiles,
678
+ assertEvents: (events) => {
679
+ const hasQuestionAsked = events.some((line) => {
680
+ return line.event.type === 'question.asked'
681
+ })
682
+ const hasQuestionReplied = events.some((line) => {
683
+ return line.event.type === 'question.replied'
684
+ })
685
+ expect(hasQuestionAsked).toBe(true)
686
+ expect(hasQuestionReplied).toBe(true)
687
+ },
688
+ })
689
+ },
690
+ 900_000,
691
+ )
692
+ })