@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,366 @@
1
+ // Worktree management command: /new-worktree
2
+ // Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
3
+ // Creates thread immediately, then worktree in background so user can type
4
+ import { ChannelType, REST, } from 'discord.js';
5
+ import fs from 'node:fs';
6
+ import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadWorktree, } from '../database.js';
7
+ import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ import { notifyError } from '../sentry.js';
10
+ import { createWorktreeWithSubmodules, execAsync, listBranchesByLastCommit, validateBranchRef, } from '../worktrees.js';
11
+ import { WORKTREE_PREFIX } from './merge-worktree.js';
12
+ import * as errore from 'errore';
13
+ const logger = createLogger(LogPrefix.WORKTREE);
14
+ /** Status message shown while a worktree is being created. */
15
+ export function worktreeCreatingMessage(worktreeName) {
16
+ return `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`;
17
+ }
18
+ class WorktreeError extends Error {
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.name = 'WorktreeError';
22
+ }
23
+ }
24
+ /**
25
+ * Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
26
+ * "My Feature" → "opencode/kimaki-my-feature"
27
+ * Returns empty string if no valid name can be extracted.
28
+ */
29
+ export function formatWorktreeName(name) {
30
+ const formatted = name
31
+ .toLowerCase()
32
+ .trim()
33
+ .replace(/\s+/g, '-')
34
+ .replace(/[^a-z0-9-]/g, '');
35
+ if (!formatted) {
36
+ return '';
37
+ }
38
+ return `opencode/kimaki-${formatted}`;
39
+ }
40
+ /**
41
+ * Derive worktree name from thread name.
42
+ * Handles existing "⬦ worktree: opencode/kimaki-name" format or uses thread name directly.
43
+ */
44
+ function deriveWorktreeNameFromThread(threadName) {
45
+ // Handle existing "⬦ worktree: opencode/kimaki-name" format
46
+ const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i);
47
+ const extractedName = worktreeMatch?.[1]?.trim();
48
+ if (extractedName) {
49
+ // If already has opencode/kimaki- prefix, return as is
50
+ if (extractedName.startsWith('opencode/kimaki-')) {
51
+ return extractedName;
52
+ }
53
+ return formatWorktreeName(extractedName);
54
+ }
55
+ // Use thread name directly
56
+ return formatWorktreeName(threadName);
57
+ }
58
+ /**
59
+ * Get project directory from database.
60
+ */
61
+ async function getProjectDirectoryFromChannel(channel) {
62
+ const channelConfig = await getChannelDirectory(channel.id);
63
+ if (!channelConfig) {
64
+ return new WorktreeError('This channel is not configured with a project directory');
65
+ }
66
+ if (!fs.existsSync(channelConfig.directory)) {
67
+ return new WorktreeError(`Directory does not exist: ${channelConfig.directory}`);
68
+ }
69
+ return channelConfig.directory;
70
+ }
71
+ /**
72
+ * Create worktree and update the status message when done.
73
+ * Handles the full lifecycle: pending DB entry, git creation, DB ready/error,
74
+ * tree emoji reaction, and editing the status message.
75
+ *
76
+ * starterMessage is optional — if omitted, status edits are skipped (creation
77
+ * still proceeds). This keeps worktree creation independent of Discord message
78
+ * delivery, so a transient send failure never silently skips the worktree.
79
+ *
80
+ * Returns the worktree directory on success, or an Error on failure.
81
+ * Never throws — all internal errors are caught and returned as Error values.
82
+ */
83
+ export async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseBranch, rest, }) {
84
+ return errore.tryAsync({
85
+ try: async () => {
86
+ logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
87
+ await createPendingWorktree({
88
+ threadId: thread.id,
89
+ worktreeName,
90
+ projectDirectory,
91
+ });
92
+ // Serialize status message edits so onProgress can't overwrite the
93
+ // final success/error edit even if Discord's API is slow.
94
+ let editChain = Promise.resolve();
95
+ const editStatus = (content) => {
96
+ editChain = editChain
97
+ .then(async () => {
98
+ await starterMessage?.edit(content);
99
+ })
100
+ .catch(() => { });
101
+ };
102
+ const worktreeResult = await createWorktreeWithSubmodules({
103
+ directory: projectDirectory,
104
+ name: worktreeName,
105
+ baseBranch,
106
+ onProgress: (phase) => {
107
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`);
108
+ },
109
+ });
110
+ if (worktreeResult instanceof Error) {
111
+ const errorMsg = worktreeResult.message;
112
+ logger.error('[WORKTREE] Creation failed:', worktreeResult);
113
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
114
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`);
115
+ await editChain;
116
+ return worktreeResult;
117
+ }
118
+ // Success - update database and edit starter message
119
+ await setWorktreeReady({
120
+ threadId: thread.id,
121
+ worktreeDirectory: worktreeResult.directory,
122
+ });
123
+ // React with tree emoji to mark as worktree thread
124
+ await reactToThread({
125
+ rest,
126
+ threadId: thread.id,
127
+ channelId: thread.parentId || undefined,
128
+ emoji: '🌳',
129
+ });
130
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n` +
131
+ `📁 \`${worktreeResult.directory}\`\n` +
132
+ `🌿 Branch: \`${worktreeResult.branch}\``);
133
+ await editChain;
134
+ return worktreeResult.directory;
135
+ },
136
+ catch: (e) => {
137
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e);
138
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
139
+ },
140
+ });
141
+ }
142
+ async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
143
+ const listResult = await errore.tryAsync({
144
+ try: () => execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
145
+ catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
146
+ });
147
+ if (errore.isError(listResult)) {
148
+ return listResult;
149
+ }
150
+ const lines = listResult.stdout.split('\n');
151
+ let currentPath = '';
152
+ const branchRef = `refs/heads/${worktreeName}`;
153
+ for (const line of lines) {
154
+ if (line.startsWith('worktree ')) {
155
+ currentPath = line.slice('worktree '.length);
156
+ continue;
157
+ }
158
+ if (line.startsWith('branch ') &&
159
+ line.slice('branch '.length) === branchRef) {
160
+ return currentPath || undefined;
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ export async function handleNewWorktreeCommand({ command, }) {
166
+ await command.deferReply();
167
+ const channel = command.channel;
168
+ if (!channel) {
169
+ await command.editReply('Cannot determine channel');
170
+ return;
171
+ }
172
+ const isThread = channel.type === ChannelType.PublicThread ||
173
+ channel.type === ChannelType.PrivateThread;
174
+ // Handle command in existing thread - attach worktree to this thread
175
+ if (isThread) {
176
+ await handleWorktreeInThread({
177
+ command,
178
+ thread: channel,
179
+ });
180
+ return;
181
+ }
182
+ // Handle command in text channel - create new thread with worktree (existing behavior)
183
+ if (channel.type !== ChannelType.GuildText) {
184
+ await command.editReply('This command can only be used in text channels or threads');
185
+ return;
186
+ }
187
+ const rawName = command.options.getString('name');
188
+ const rawBaseBranch = command.options.getString('base-branch') || undefined;
189
+ if (!rawName) {
190
+ await command.editReply('Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`');
191
+ return;
192
+ }
193
+ const worktreeName = formatWorktreeName(rawName);
194
+ if (!worktreeName) {
195
+ await command.editReply('Invalid worktree name. Please use letters, numbers, and spaces.');
196
+ return;
197
+ }
198
+ const textChannel = channel;
199
+ const projectDirectory = await getProjectDirectoryFromChannel(textChannel);
200
+ if (errore.isError(projectDirectory)) {
201
+ await command.editReply(projectDirectory.message);
202
+ return;
203
+ }
204
+ let baseBranch = rawBaseBranch;
205
+ if (baseBranch) {
206
+ const validated = await validateBranchRef({
207
+ directory: projectDirectory,
208
+ ref: baseBranch,
209
+ });
210
+ if (validated instanceof Error) {
211
+ await command.editReply(`Invalid base branch: \`${baseBranch}\``);
212
+ return;
213
+ }
214
+ baseBranch = validated;
215
+ }
216
+ const existingWorktree = await findExistingWorktreePath({
217
+ projectDirectory,
218
+ worktreeName,
219
+ });
220
+ if (errore.isError(existingWorktree)) {
221
+ await command.editReply(existingWorktree.message);
222
+ return;
223
+ }
224
+ if (existingWorktree) {
225
+ await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``);
226
+ return;
227
+ }
228
+ // Create thread immediately so user can start typing
229
+ const result = await errore.tryAsync({
230
+ try: async () => {
231
+ const starterMessage = await textChannel.send({
232
+ content: worktreeCreatingMessage(worktreeName),
233
+ flags: SILENT_MESSAGE_FLAGS,
234
+ });
235
+ const thread = await starterMessage.startThread({
236
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
237
+ autoArchiveDuration: 1440,
238
+ reason: 'Worktree session',
239
+ });
240
+ // Add user to thread so it appears in their sidebar
241
+ await thread.members.add(command.user.id);
242
+ return { thread, starterMessage };
243
+ },
244
+ catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
245
+ });
246
+ if (errore.isError(result)) {
247
+ logger.error('[NEW-WORKTREE] Error:', result.cause);
248
+ await command.editReply(result.message);
249
+ return;
250
+ }
251
+ const { thread, starterMessage } = result;
252
+ await command.editReply(`Creating worktree in ${thread.toString()}`);
253
+ // Create worktree in background (don't await)
254
+ createWorktreeInBackground({
255
+ thread,
256
+ starterMessage,
257
+ worktreeName,
258
+ projectDirectory,
259
+ baseBranch,
260
+ rest: command.client.rest,
261
+ }).catch((e) => {
262
+ logger.error('[NEW-WORKTREE] Background error:', e);
263
+ void notifyError(e, 'Background worktree creation failed');
264
+ });
265
+ }
266
+ /**
267
+ * Handle /new-worktree when called inside an existing thread.
268
+ * Attaches a worktree to the current thread, using thread name if no name provided.
269
+ */
270
+ async function handleWorktreeInThread({ command, thread, }) {
271
+ // Error if thread already has a worktree
272
+ if (await getThreadWorktree(thread.id)) {
273
+ await command.editReply('This thread already has a worktree attached.');
274
+ return;
275
+ }
276
+ // Get worktree name from parameter or derive from thread name
277
+ const rawName = command.options.getString('name');
278
+ const rawBaseBranch = command.options.getString('base-branch') || undefined;
279
+ const worktreeName = rawName
280
+ ? formatWorktreeName(rawName)
281
+ : deriveWorktreeNameFromThread(thread.name);
282
+ if (!worktreeName) {
283
+ await command.editReply('Invalid worktree name. Please provide a name or rename the thread.');
284
+ return;
285
+ }
286
+ // Get parent channel for project directory
287
+ const parent = thread.parent;
288
+ if (!parent || parent.type !== ChannelType.GuildText) {
289
+ await command.editReply('Cannot determine parent channel');
290
+ return;
291
+ }
292
+ const projectDirectory = await getProjectDirectoryFromChannel(parent);
293
+ if (errore.isError(projectDirectory)) {
294
+ await command.editReply(projectDirectory.message);
295
+ return;
296
+ }
297
+ let baseBranch = rawBaseBranch;
298
+ if (baseBranch) {
299
+ const validated = await validateBranchRef({
300
+ directory: projectDirectory,
301
+ ref: baseBranch,
302
+ });
303
+ if (validated instanceof Error) {
304
+ await command.editReply(`Invalid base branch: \`${baseBranch}\``);
305
+ return;
306
+ }
307
+ baseBranch = validated;
308
+ }
309
+ const existingWorktreePath = await findExistingWorktreePath({
310
+ projectDirectory,
311
+ worktreeName,
312
+ });
313
+ if (errore.isError(existingWorktreePath)) {
314
+ await command.editReply(existingWorktreePath.message);
315
+ return;
316
+ }
317
+ if (existingWorktreePath) {
318
+ await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
319
+ return;
320
+ }
321
+ // Send status message in thread
322
+ const statusMessage = await thread.send({
323
+ content: worktreeCreatingMessage(worktreeName),
324
+ flags: SILENT_MESSAGE_FLAGS,
325
+ });
326
+ await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`);
327
+ createWorktreeInBackground({
328
+ thread,
329
+ starterMessage: statusMessage,
330
+ worktreeName,
331
+ projectDirectory,
332
+ baseBranch,
333
+ rest: command.client.rest,
334
+ }).catch((e) => {
335
+ logger.error('[NEW-WORKTREE] Background error:', e);
336
+ void notifyError(e, 'Background worktree creation failed (in-thread)');
337
+ });
338
+ }
339
+ /**
340
+ * Autocomplete handler for /new-worktree base-branch option.
341
+ * Lists local + remote branches sorted by most recent commit date.
342
+ */
343
+ export async function handleNewWorktreeAutocomplete({ interaction, }) {
344
+ try {
345
+ const focusedValue = interaction.options.getFocused();
346
+ // interaction.channel can be null when the channel isn't cached
347
+ // (common with gateway-proxy). Use channelId which is always available
348
+ // from the raw interaction payload.
349
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
350
+ if (!projectDirectory) {
351
+ await interaction.respond([]);
352
+ return;
353
+ }
354
+ const branches = await listBranchesByLastCommit({
355
+ directory: projectDirectory,
356
+ query: focusedValue,
357
+ });
358
+ await interaction.respond(branches.map((name) => {
359
+ return { name, value: name };
360
+ }));
361
+ }
362
+ catch (e) {
363
+ logger.error('[NEW-WORKTREE] Autocomplete error:', e);
364
+ await interaction.respond([]).catch(() => { });
365
+ }
366
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Reusable paginated select menu helpers for Discord StringSelectMenuBuilder.
3
+ * Discord caps select menus at 25 options. This module slices a full options
4
+ * list into pages of PAGE_SIZE real items and appends "← Previous page" /
5
+ * "Next page →" sentinel options so the user can navigate. Handlers detect
6
+ * sentinel values via parsePaginationValue() and re-render the same select
7
+ * with the new page — reusing the same customId, no new interaction handlers.
8
+ */
9
+ const NAV_PREFIX = '__page_nav:';
10
+ /** 23 real items per page, leaving room for up to 2 nav sentinels (prev + next). */
11
+ const PAGE_SIZE = 23;
12
+ /**
13
+ * Build the options array for a single page, with prev/next nav sentinels.
14
+ * If allOptions fits in 25 items, returns them all with no nav items.
15
+ */
16
+ export function buildPaginatedOptions({ allOptions, page, }) {
17
+ // No pagination needed — everything fits in one Discord select
18
+ if (allOptions.length <= 25) {
19
+ return { options: allOptions, totalPages: 1 };
20
+ }
21
+ const totalPages = Math.ceil(allOptions.length / PAGE_SIZE);
22
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
23
+ const start = safePage * PAGE_SIZE;
24
+ const slice = allOptions.slice(start, start + PAGE_SIZE);
25
+ const result = [];
26
+ if (safePage > 0) {
27
+ result.push({
28
+ label: `← Previous page (${safePage}/${totalPages})`,
29
+ value: `${NAV_PREFIX}${safePage - 1}`,
30
+ description: 'Go to previous page',
31
+ });
32
+ }
33
+ result.push(...slice);
34
+ if (safePage < totalPages - 1) {
35
+ result.push({
36
+ label: `Next page → (${safePage + 2}/${totalPages})`,
37
+ value: `${NAV_PREFIX}${safePage + 1}`,
38
+ description: 'Go to next page',
39
+ });
40
+ }
41
+ return { options: result, totalPages };
42
+ }
43
+ /**
44
+ * Check if a selected value is a pagination nav sentinel.
45
+ * Returns the target page number if so, undefined otherwise.
46
+ */
47
+ export function parsePaginationValue(value) {
48
+ if (!value.startsWith(NAV_PREFIX)) {
49
+ return undefined;
50
+ }
51
+ const pageStr = value.slice(NAV_PREFIX.length);
52
+ const page = Number(pageStr);
53
+ if (Number.isNaN(page)) {
54
+ return undefined;
55
+ }
56
+ return page;
57
+ }
@@ -0,0 +1,274 @@
1
+ // Permission button handler - Shows buttons for permission requests.
2
+ // When OpenCode asks for permission, this module renders 3 buttons:
3
+ // Accept, Accept Always, and Deny.
4
+ import { ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, } from 'discord.js';
5
+ import crypto from 'node:crypto';
6
+ import { getOpencodeClient } from '../opencode.js';
7
+ import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const logger = createLogger(LogPrefix.PERMISSIONS);
10
+ function wildcardMatch({ value, pattern, }) {
11
+ let escapedPattern = pattern
12
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
13
+ .replace(/\*/g, '.*')
14
+ .replace(/\?/g, '.');
15
+ if (escapedPattern.endsWith(' .*')) {
16
+ escapedPattern = escapedPattern.slice(0, -3) + '( .*)?';
17
+ }
18
+ return new RegExp(`^${escapedPattern}$`, 's').test(value);
19
+ }
20
+ export function arePatternsCoveredBy({ patterns, coveringPatterns, }) {
21
+ return patterns.every((pattern) => {
22
+ return coveringPatterns.some((coveringPattern) => {
23
+ return wildcardMatch({ value: pattern, pattern: coveringPattern });
24
+ });
25
+ });
26
+ }
27
+ export function compactPermissionPatterns(patterns) {
28
+ const uniquePatterns = Array.from(new Set(patterns));
29
+ return uniquePatterns.filter((pattern, index) => {
30
+ return !uniquePatterns.some((candidate, candidateIndex) => {
31
+ if (candidateIndex === index) {
32
+ return false;
33
+ }
34
+ return wildcardMatch({ value: pattern, pattern: candidate });
35
+ });
36
+ });
37
+ }
38
+ // Store pending permission contexts by hash.
39
+ // TTL prevents unbounded growth if user never clicks a permission button.
40
+ const PERMISSION_CONTEXT_TTL_MS = 10 * 60 * 1000;
41
+ export const pendingPermissionContexts = new Map();
42
+ // Atomic take: removes context from Map and returns it. Only the first caller
43
+ // (TTL expiry or button click) wins, preventing duplicate permission replies.
44
+ function takePendingPermissionContext(contextHash) {
45
+ const ctx = pendingPermissionContexts.get(contextHash);
46
+ if (!ctx) {
47
+ return undefined;
48
+ }
49
+ pendingPermissionContexts.delete(contextHash);
50
+ return ctx;
51
+ }
52
+ /**
53
+ * Show permission buttons for a permission request.
54
+ * Displays 3 buttons in a row: Accept, Accept Always, Deny.
55
+ * Returns the message ID and context hash for tracking.
56
+ */
57
+ export async function showPermissionButtons({ thread, permission, directory, permissionDirectory, subtaskLabel, }) {
58
+ const contextHash = crypto.randomBytes(8).toString('hex');
59
+ const context = {
60
+ permission,
61
+ requestIds: [permission.id],
62
+ directory,
63
+ permissionDirectory,
64
+ thread,
65
+ contextHash,
66
+ };
67
+ pendingPermissionContexts.set(contextHash, context);
68
+ // Auto-reject on TTL expiry so the OpenCode session doesn't hang forever
69
+ // waiting for a permission reply that will never come. Uses atomic take
70
+ // so only one of TTL-expiry or button-click can win.
71
+ setTimeout(async () => {
72
+ const ctx = takePendingPermissionContext(contextHash);
73
+ if (!ctx) {
74
+ return;
75
+ }
76
+ const client = getOpencodeClient(ctx.directory);
77
+ if (client) {
78
+ const requestIds = ctx.requestIds.length > 0
79
+ ? ctx.requestIds
80
+ : [ctx.permission.id];
81
+ await Promise.all(requestIds.map((requestId) => {
82
+ return client.permission.reply({
83
+ requestID: requestId,
84
+ directory: ctx.permissionDirectory,
85
+ reply: 'reject',
86
+ });
87
+ })).catch((error) => {
88
+ logger.error('Failed to auto-reject expired permission:', error);
89
+ });
90
+ }
91
+ }, PERMISSION_CONTEXT_TTL_MS).unref();
92
+ const patternStr = compactPermissionPatterns(permission.patterns).join(', ');
93
+ // Build 3 buttons for permission actions
94
+ const acceptButton = new ButtonBuilder()
95
+ .setCustomId(`permission_once:${contextHash}`)
96
+ .setLabel('Accept')
97
+ .setStyle(ButtonStyle.Success);
98
+ const acceptAlwaysButton = new ButtonBuilder()
99
+ .setCustomId(`permission_always:${contextHash}`)
100
+ .setLabel('Accept Always')
101
+ .setStyle(ButtonStyle.Success);
102
+ const denyButton = new ButtonBuilder()
103
+ .setCustomId(`permission_reject:${contextHash}`)
104
+ .setLabel('Deny')
105
+ .setStyle(ButtonStyle.Secondary);
106
+ const actionRow = new ActionRowBuilder().addComponents(acceptButton, acceptAlwaysButton, denyButton);
107
+ const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : '';
108
+ const externalDirLine = permission.permission === 'external_directory'
109
+ ? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
110
+ : '';
111
+ const fullContent = `⚠️ **Permission Required**\n` +
112
+ subtaskLine +
113
+ `**Type:** \`${permission.permission}\`\n` +
114
+ externalDirLine +
115
+ (patternStr ? `**Pattern:** \`${patternStr}\`` : '');
116
+ const permissionMessage = await thread.send({
117
+ content: fullContent.slice(0, 1900),
118
+ components: [actionRow],
119
+ flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
120
+ });
121
+ context.messageId = permissionMessage.id;
122
+ logger.log(`Showed permission buttons for ${permission.id}`);
123
+ return { messageId: permissionMessage.id, contextHash };
124
+ }
125
+ function updatePermissionMessage({ context, status, }) {
126
+ if (!context.messageId) {
127
+ return;
128
+ }
129
+ context.thread.messages
130
+ .fetch(context.messageId)
131
+ .then((message) => {
132
+ const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ');
133
+ const externalDirLine = context.permission.permission === 'external_directory'
134
+ ? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
135
+ : '';
136
+ return message.edit({
137
+ content: `⚠️ **Permission Required**\n` +
138
+ `**Type:** \`${context.permission.permission}\`\n` +
139
+ externalDirLine +
140
+ (patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
141
+ status,
142
+ components: [],
143
+ });
144
+ })
145
+ .catch((error) => {
146
+ logger.error('Failed to update permission message:', error);
147
+ });
148
+ }
149
+ export async function cancelPendingPermission(threadId) {
150
+ const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
151
+ return context.thread.id === threadId;
152
+ });
153
+ if (contexts.length === 0) {
154
+ return false;
155
+ }
156
+ let cancelledCount = 0;
157
+ for (const context of contexts) {
158
+ const pendingContext = takePendingPermissionContext(context.contextHash);
159
+ if (!pendingContext) {
160
+ continue;
161
+ }
162
+ const client = getOpencodeClient(pendingContext.directory);
163
+ if (!client) {
164
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
165
+ logger.error('Failed to dismiss pending permission: OpenCode server not found');
166
+ continue;
167
+ }
168
+ const requestIds = pendingContext.requestIds.length > 0
169
+ ? pendingContext.requestIds
170
+ : [pendingContext.permission.id];
171
+ const result = await Promise.all(requestIds.map((requestId) => {
172
+ return client.permission.reply({
173
+ requestID: requestId,
174
+ directory: pendingContext.permissionDirectory,
175
+ reply: 'reject',
176
+ });
177
+ })).then(() => {
178
+ return 'ok';
179
+ }).catch((error) => {
180
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
181
+ logger.error('Failed to dismiss pending permission:', error);
182
+ return 'error';
183
+ });
184
+ if (result === 'error') {
185
+ continue;
186
+ }
187
+ updatePermissionMessage({
188
+ context: pendingContext,
189
+ status: '_Permission dismissed - user sent a new message._',
190
+ });
191
+ cancelledCount++;
192
+ }
193
+ if (cancelledCount > 0) {
194
+ logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`);
195
+ }
196
+ return cancelledCount > 0;
197
+ }
198
+ /**
199
+ * Handle button click for permission.
200
+ */
201
+ export async function handlePermissionButton(interaction) {
202
+ const customId = interaction.customId;
203
+ // Extract action and hash from customId (e.g., "permission_once:abc123")
204
+ const [actionPart, contextHash] = customId.split(':');
205
+ if (!actionPart || !contextHash) {
206
+ return;
207
+ }
208
+ const response = actionPart.replace('permission_', '');
209
+ // Atomic take: if TTL already expired and auto-rejected, context is gone.
210
+ const context = takePendingPermissionContext(contextHash);
211
+ if (!context) {
212
+ await interaction.update({ components: [] });
213
+ return;
214
+ }
215
+ await interaction.deferUpdate();
216
+ try {
217
+ const permClient = getOpencodeClient(context.directory);
218
+ if (!permClient) {
219
+ throw new Error('OpenCode server not found for directory');
220
+ }
221
+ const requestIds = context.requestIds.length > 0
222
+ ? context.requestIds
223
+ : [context.permission.id];
224
+ await Promise.all(requestIds.map((requestId) => {
225
+ return permClient.permission.reply({
226
+ requestID: requestId,
227
+ directory: context.permissionDirectory,
228
+ reply: response,
229
+ });
230
+ }));
231
+ // Context already removed by takePendingPermissionContext above.
232
+ // Update message: show result and remove dropdown
233
+ const resultText = (() => {
234
+ switch (response) {
235
+ case 'once':
236
+ return '✅ Permission **accepted**';
237
+ case 'always':
238
+ return '✅ Permission **accepted** (auto-approve similar requests)';
239
+ case 'reject':
240
+ return '❌ Permission **rejected**';
241
+ }
242
+ })();
243
+ updatePermissionMessage({
244
+ context,
245
+ status: resultText,
246
+ });
247
+ logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
248
+ }
249
+ catch (error) {
250
+ logger.error('Error handling permission:', error);
251
+ await interaction.editReply({
252
+ content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
253
+ components: [],
254
+ });
255
+ }
256
+ }
257
+ export function addPermissionRequestToContext({ contextHash, requestId, }) {
258
+ const context = pendingPermissionContexts.get(contextHash);
259
+ if (!context) {
260
+ return false;
261
+ }
262
+ if (context.requestIds.includes(requestId)) {
263
+ return false;
264
+ }
265
+ context.requestIds = [...context.requestIds, requestId];
266
+ pendingPermissionContexts.set(contextHash, context);
267
+ return true;
268
+ }
269
+ /**
270
+ * Clean up a pending permission context (e.g., on auto-reject).
271
+ */
272
+ export function cleanupPermissionContext(contextHash) {
273
+ pendingPermissionContexts.delete(contextHash);
274
+ }