@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
+ // /screenshare command - Start screen sharing via VNC + WebSocket bridge + kimaki tunnel.
2
+ // On macOS: uses built-in Screen Sharing (port 5900).
3
+ // On Linux: spawns x11vnc against the current $DISPLAY.
4
+ // Exposes the VNC stream via an in-process websockify bridge and a traforo tunnel,
5
+ // then sends the user a noVNC URL they can open in a browser.
6
+ //
7
+ // /screenshare-stop command - Stops the active screen share for this guild.
8
+
9
+ import { MessageFlags } from 'discord.js'
10
+ import crypto from 'node:crypto'
11
+ import { spawn, type ChildProcess } from 'node:child_process'
12
+ import net from 'node:net'
13
+ import { TunnelClient } from 'traforo/client'
14
+ import type { CommandContext } from './types.js'
15
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
16
+ import { startWebsockify } from '../websockify.js'
17
+ import { createLogger } from '../logger.js'
18
+ import { execAsync } from '../worktrees.js'
19
+ import type { WebSocketServer } from 'ws'
20
+
21
+ const logger = createLogger('SCREEN')
22
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS
23
+
24
+ export type ScreenshareSession = {
25
+ tunnelClient: TunnelClient
26
+ wss: WebSocketServer
27
+ /** x11vnc child process, only on Linux */
28
+ vncProcess: ChildProcess | undefined
29
+ url: string
30
+ noVncUrl: string
31
+ startedBy: string
32
+ startedAt: number
33
+ /** Auto-kill timer */
34
+ timeoutTimer: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ /** One active screenshare per guild (Discord) or per machine (CLI) */
38
+ const activeSessions = new Map<string, ScreenshareSession>()
39
+
40
+ const VNC_PORT = 5900
41
+ const MAX_SESSION_MINUTES = 30
42
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000
43
+ const TUNNEL_BASE_DOMAIN = 'kimaki.xyz'
44
+ const SCREENSHARE_TUNNEL_ID_BYTES = 16
45
+
46
+ // Public noVNC client — we point it at our tunnel URL
47
+ export function buildNoVncUrl({ tunnelHost }: { tunnelHost: string }): string {
48
+ const params = new URLSearchParams({
49
+ autoconnect: 'true',
50
+ host: tunnelHost,
51
+ port: '443',
52
+ encrypt: '1',
53
+ resize: 'scale',
54
+ view_only: 'false',
55
+ })
56
+ return `https://novnc.com/noVNC/vnc.html?${params.toString()}`
57
+ }
58
+
59
+ export function createScreenshareTunnelId(): string {
60
+ return crypto.randomBytes(SCREENSHARE_TUNNEL_ID_BYTES).toString('hex')
61
+ }
62
+
63
+ // macOS has two separate services:
64
+ // - "Screen Sharing" = view-only VNC (com.apple.screensharing)
65
+ // - "Remote Management" = full control VNC with mouse/keyboard (ARDAgent)
66
+ // We need Remote Management for interactive control, not just Screen Sharing.
67
+ export async function ensureMacRemoteManagement(): Promise<void> {
68
+ // Check if port 5900 is listening via netstat (no sudo needed).
69
+ // lsof and launchctl list both require sudo for system daemons.
70
+ try {
71
+ const { stdout } = await execAsync(
72
+ 'netstat -an | grep "\\.5900 " | grep LISTEN',
73
+ { timeout: 5000 },
74
+ )
75
+ if (stdout.trim()) {
76
+ return
77
+ }
78
+ } catch {
79
+ // not listening
80
+ }
81
+
82
+ throw new Error(
83
+ 'macOS Remote Management is not enabled.\n' +
84
+ 'Enable it: **System Settings > General > Sharing > Remote Management**\n' +
85
+ 'Make sure "VNC viewers may control screen with password" is enabled.\n' +
86
+ 'Or via terminal:\n' +
87
+ '```\nsudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \\\n' +
88
+ ' -activate -configure -allowAccessFor -allUsers -privs -all \\\n' +
89
+ ' -clientopts -setvnclegacy -vnclegacy yes \\\n' +
90
+ ' -restart -agent -console\n```',
91
+ )
92
+ }
93
+
94
+ export function spawnX11Vnc(): ChildProcess {
95
+ const display = process.env['DISPLAY'] || ':0'
96
+ const child = spawn('x11vnc', [
97
+ '-display', display,
98
+ '-nopw',
99
+ '-localhost',
100
+ '-rfbport', String(VNC_PORT),
101
+ '-shared',
102
+ '-forever',
103
+ ], {
104
+ stdio: ['ignore', 'pipe', 'pipe'],
105
+ })
106
+
107
+ child.stdout?.on('data', (data: Buffer) => {
108
+ logger.log(`x11vnc: ${data.toString().trim()}`)
109
+ })
110
+ child.stderr?.on('data', (data: Buffer) => {
111
+ logger.error(`x11vnc: ${data.toString().trim()}`)
112
+ })
113
+
114
+ return child
115
+ }
116
+
117
+ function waitForPort({
118
+ port,
119
+ process: proc,
120
+ timeoutMs,
121
+ }: {
122
+ port: number
123
+ process: ChildProcess
124
+ timeoutMs: number
125
+ }): Promise<void> {
126
+ return new Promise((resolve, reject) => {
127
+ const maxAttempts = Math.ceil(timeoutMs / 100)
128
+ let attempts = 0
129
+ const check = () => {
130
+ if (proc.exitCode !== null) {
131
+ reject(new Error(`x11vnc exited with code ${proc.exitCode} before becoming ready`))
132
+ return
133
+ }
134
+ const sock = net.createConnection(port, 'localhost')
135
+ sock.on('connect', () => {
136
+ sock.destroy()
137
+ resolve()
138
+ })
139
+ sock.on('error', () => {
140
+ sock.destroy()
141
+ if (++attempts >= maxAttempts) {
142
+ reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`))
143
+ } else {
144
+ setTimeout(check, 100)
145
+ }
146
+ })
147
+ }
148
+ check()
149
+ })
150
+ }
151
+
152
+ export function cleanupSession(session: ScreenshareSession): void {
153
+ clearTimeout(session.timeoutTimer)
154
+ try {
155
+ session.tunnelClient.close()
156
+ } catch {}
157
+ try {
158
+ session.wss.close()
159
+ } catch {}
160
+ if (session.vncProcess) {
161
+ try {
162
+ session.vncProcess.kill()
163
+ } catch {}
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Core screenshare start logic, reused by both Discord command and CLI.
169
+ * Returns the session or throws on failure.
170
+ */
171
+ export async function startScreenshare({
172
+ sessionKey,
173
+ startedBy,
174
+ }: {
175
+ sessionKey: string
176
+ startedBy: string
177
+ }): Promise<ScreenshareSession> {
178
+ const existing = activeSessions.get(sessionKey)
179
+ if (existing) {
180
+ throw new Error(`Screen sharing is already active: ${existing.noVncUrl}`)
181
+ }
182
+
183
+ const platform = process.platform
184
+ let vncProcess: ChildProcess | undefined
185
+
186
+ // Step 1: ensure VNC server is running
187
+ if (platform === 'darwin') {
188
+ await ensureMacRemoteManagement()
189
+ } else if (platform === 'linux') {
190
+ if (!process.env['DISPLAY']) {
191
+ throw new Error('No $DISPLAY found. Screen sharing requires a running X11 display.')
192
+ }
193
+ try {
194
+ await execAsync('which x11vnc', { timeout: 3000 })
195
+ } catch {
196
+ throw new Error('x11vnc is not installed. Install it with: sudo apt install x11vnc')
197
+ }
198
+ vncProcess = spawnX11Vnc()
199
+ // Wait for x11vnc to actually be ready (port 5900 accepting connections)
200
+ // instead of a blind 1s sleep. Polls every 100ms, fails if process exits first.
201
+ await waitForPort({ port: VNC_PORT, process: vncProcess, timeoutMs: 3000 })
202
+ } else {
203
+ throw new Error(`Screen sharing is not supported on ${platform}. Only macOS and Linux are supported.`)
204
+ }
205
+
206
+ // Step 2: start in-process websockify bridge
207
+ let wsInstance: Awaited<ReturnType<typeof startWebsockify>>
208
+ try {
209
+ wsInstance = await startWebsockify({
210
+ wsPort: 0,
211
+ tcpHost: 'localhost',
212
+ tcpPort: VNC_PORT,
213
+ })
214
+ } catch (err) {
215
+ if (vncProcess) {
216
+ vncProcess.kill()
217
+ }
218
+ throw err
219
+ }
220
+
221
+ // Step 3: create tunnel
222
+ const tunnelId = createScreenshareTunnelId()
223
+ const tunnelClient = new TunnelClient({
224
+ localPort: wsInstance.port,
225
+ tunnelId,
226
+ baseDomain: TUNNEL_BASE_DOMAIN,
227
+ })
228
+
229
+ try {
230
+ await Promise.race([
231
+ tunnelClient.connect(),
232
+ new Promise<never>((_, reject) => {
233
+ setTimeout(() => {
234
+ reject(new Error('Tunnel connection timed out after 15s'))
235
+ }, 15000)
236
+ }),
237
+ ])
238
+ } catch (err) {
239
+ tunnelClient.close()
240
+ wsInstance.close()
241
+ if (vncProcess) {
242
+ vncProcess.kill()
243
+ }
244
+ throw err
245
+ }
246
+
247
+ const tunnelHost = `${tunnelId}-tunnel.${TUNNEL_BASE_DOMAIN}`
248
+ const tunnelUrl = `https://${tunnelHost}`
249
+ const noVncUrl = buildNoVncUrl({ tunnelHost })
250
+
251
+ // Auto-kill after a short session so a leaked URL does not stay usable all day.
252
+ const timeoutTimer = setTimeout(() => {
253
+ logger.log(
254
+ `Screen share auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`,
255
+ )
256
+ stopScreenshare({ sessionKey })
257
+ }, MAX_SESSION_MS)
258
+ // Don't keep the process alive just for this timer
259
+ timeoutTimer.unref()
260
+
261
+ const session: ScreenshareSession = {
262
+ tunnelClient,
263
+ wss: wsInstance.wss,
264
+ vncProcess,
265
+ url: tunnelUrl,
266
+ noVncUrl,
267
+ startedBy,
268
+ startedAt: Date.now(),
269
+ timeoutTimer,
270
+ }
271
+
272
+ activeSessions.set(sessionKey, session)
273
+ logger.log(`Screen share started by ${startedBy}: ${tunnelUrl}`)
274
+
275
+ return session
276
+ }
277
+
278
+ /**
279
+ * Core screenshare stop logic, reused by both Discord command and CLI.
280
+ */
281
+ export function stopScreenshare({ sessionKey }: { sessionKey: string }): boolean {
282
+ const session = activeSessions.get(sessionKey)
283
+ if (!session) {
284
+ return false
285
+ }
286
+ cleanupSession(session)
287
+ activeSessions.delete(sessionKey)
288
+ logger.log(`Screen share stopped (key: ${sessionKey})`)
289
+ return true
290
+ }
291
+
292
+ export async function handleScreenshareCommand({
293
+ command,
294
+ }: CommandContext): Promise<void> {
295
+ const guildId = command.guildId
296
+ if (!guildId) {
297
+ await command.reply({
298
+ content: 'This command can only be used in a server',
299
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
300
+ })
301
+ return
302
+ }
303
+
304
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS })
305
+
306
+ try {
307
+ const session = await startScreenshare({
308
+ sessionKey: guildId,
309
+ startedBy: command.user.tag,
310
+ })
311
+ await command.editReply({
312
+ content:
313
+ `Screen sharing started. This reply is private and the URL uses a high-entropy tunnel id. ` +
314
+ `It will auto-stop after ${MAX_SESSION_MINUTES} minutes. Use /screenshare-stop to stop sooner.\n` +
315
+ `${session.noVncUrl}`,
316
+ })
317
+ } catch (err) {
318
+ logger.error('Failed to start screen share:', err)
319
+ await command.editReply({
320
+ content: `Failed to start screen share: ${err instanceof Error ? err.message : String(err)}`,
321
+ })
322
+ }
323
+ }
324
+
325
+ export async function handleScreenshareStopCommand({
326
+ command,
327
+ }: CommandContext): Promise<void> {
328
+ const guildId = command.guildId
329
+ if (!guildId) {
330
+ await command.reply({
331
+ content: 'This command can only be used in a server',
332
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
333
+ })
334
+ return
335
+ }
336
+
337
+ const stopped = stopScreenshare({ sessionKey: guildId })
338
+ if (!stopped) {
339
+ await command.reply({
340
+ content: 'No active screen share to stop',
341
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
342
+ })
343
+ return
344
+ }
345
+
346
+ await command.reply({
347
+ content: 'Screen sharing stopped',
348
+ flags: SILENT_MESSAGE_FLAGS,
349
+ })
350
+ }
351
+
352
+ /** Cleanup all sessions on bot shutdown */
353
+ export function cleanupAllScreenshares(): void {
354
+ for (const [guildId, session] of activeSessions) {
355
+ cleanupSession(session)
356
+ activeSessions.delete(guildId)
357
+ }
358
+ }
359
+
360
+ // Kill all screenshares when the process exits (Ctrl+C, SIGTERM, etc.)
361
+ function onProcessExit(): void {
362
+ cleanupAllScreenshares()
363
+ }
364
+ process.on('SIGINT', onProcessExit)
365
+ process.on('SIGTERM', onProcessExit)
366
+ process.on('exit', onProcessExit)
@@ -0,0 +1,109 @@
1
+ // /session-id command - Show current session ID and an opencode attach command.
2
+
3
+ import {
4
+ ChannelType,
5
+ MessageFlags,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import type { CommandContext } from './types.js'
10
+ import { getThreadSession } from '../database.js'
11
+ import {
12
+ resolveWorkingDirectory,
13
+ SILENT_MESSAGE_FLAGS,
14
+ } from '../discord-utils.js'
15
+ import {
16
+ getOpencodeServerPort,
17
+ initializeOpencodeForDirectory,
18
+ } from '../opencode.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
+
21
+ const logger = createLogger(LogPrefix.SESSION)
22
+
23
+ function shellQuote(value: string): string {
24
+ if (!value) {
25
+ return "''"
26
+ }
27
+ return `'${value.replaceAll("'", `'"'"'`)}'`
28
+ }
29
+
30
+ export async function handleSessionIdCommand({
31
+ command,
32
+ }: CommandContext): Promise<void> {
33
+ const channel = command.channel
34
+
35
+ if (!channel) {
36
+ await command.reply({
37
+ content: 'This command can only be used in a channel',
38
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
39
+ })
40
+ return
41
+ }
42
+
43
+ const isThread = [
44
+ ChannelType.PublicThread,
45
+ ChannelType.PrivateThread,
46
+ ChannelType.AnnouncementThread,
47
+ ].includes(channel.type)
48
+
49
+ if (!isThread) {
50
+ await command.reply({
51
+ content:
52
+ 'This command can only be used in a thread with an active session',
53
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
54
+ })
55
+ return
56
+ }
57
+
58
+ const resolved = await resolveWorkingDirectory({
59
+ channel: channel as TextChannel | ThreadChannel,
60
+ })
61
+
62
+ if (!resolved) {
63
+ await command.reply({
64
+ content: 'Could not determine project directory for this channel',
65
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
66
+ })
67
+ return
68
+ }
69
+
70
+ const { projectDirectory, workingDirectory } = resolved
71
+ const sessionId = await getThreadSession(channel.id)
72
+
73
+ if (!sessionId) {
74
+ await command.reply({
75
+ content: 'No active session in this thread',
76
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
77
+ })
78
+ return
79
+ }
80
+
81
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
82
+
83
+ let port = getOpencodeServerPort(projectDirectory)
84
+ if (!port) {
85
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
86
+ if (getClient instanceof Error) {
87
+ await command.editReply({
88
+ content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
89
+ })
90
+ return
91
+ }
92
+ port = getOpencodeServerPort(projectDirectory)
93
+ }
94
+
95
+ if (!port) {
96
+ await command.editReply({
97
+ content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
98
+ })
99
+ return
100
+ }
101
+
102
+ const attachUrl = `http://127.0.0.1:${port}`
103
+ const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`
104
+
105
+ await command.editReply({
106
+ content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
107
+ })
108
+ logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`)
109
+ }
@@ -0,0 +1,227 @@
1
+ // /new-session command - Start a new OpenCode session.
2
+
3
+ import { ChannelType, type TextChannel } from 'discord.js'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import type { CommandContext, AutocompleteContext } from './types.js'
7
+ import { getChannelDirectory } from '../database.js'
8
+ import { initializeOpencodeForDirectory } from '../opencode.js'
9
+ import { SILENT_MESSAGE_FLAGS, resolveProjectDirectoryFromAutocomplete } from '../discord-utils.js'
10
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
11
+ import { createLogger, LogPrefix } from '../logger.js'
12
+ import * as errore from 'errore'
13
+
14
+ const logger = createLogger(LogPrefix.SESSION)
15
+
16
+ export async function handleSessionCommand({
17
+ command,
18
+ appId,
19
+ }: CommandContext): Promise<void> {
20
+ await command.deferReply()
21
+
22
+ const prompt = command.options.getString('prompt', true)
23
+ const filesString = command.options.getString('files') || ''
24
+ const agent = command.options.getString('agent') || undefined
25
+ const channel = command.channel
26
+
27
+ if (!channel || channel.type !== ChannelType.GuildText) {
28
+ await command.editReply('This command can only be used in text channels')
29
+ return
30
+ }
31
+
32
+ const textChannel = channel as TextChannel
33
+
34
+ const channelConfig = await getChannelDirectory(textChannel.id)
35
+ const projectDirectory = channelConfig?.directory
36
+
37
+ if (!projectDirectory) {
38
+ await command.editReply(
39
+ 'This channel is not configured with a project directory',
40
+ )
41
+ return
42
+ }
43
+
44
+ if (!fs.existsSync(projectDirectory)) {
45
+ await command.editReply(`Directory does not exist: ${projectDirectory}`)
46
+ return
47
+ }
48
+
49
+ try {
50
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
51
+ if (getClient instanceof Error) {
52
+ await command.editReply(getClient.message)
53
+ return
54
+ }
55
+
56
+ const files = filesString
57
+ .split(',')
58
+ .map((f) => f.trim())
59
+ .filter((f) => f)
60
+
61
+ let fullPrompt = prompt
62
+ if (files.length > 0) {
63
+ fullPrompt = `${prompt}\n\n@${files.join(' @')}`
64
+ }
65
+
66
+ const starterMessage = await textChannel.send({
67
+ content: `🚀 **Starting OpenCode session**\n📝 ${prompt}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
68
+ flags: SILENT_MESSAGE_FLAGS,
69
+ })
70
+
71
+ const thread = await starterMessage.startThread({
72
+ name: prompt.slice(0, 100),
73
+ autoArchiveDuration: 1440,
74
+ reason: 'OpenCode session',
75
+ })
76
+
77
+ // Add user to thread so it appears in their sidebar
78
+ await thread.members.add(command.user.id)
79
+
80
+ await command.editReply(`Created new session in ${thread.toString()}`)
81
+
82
+ const runtime = getOrCreateRuntime({
83
+ threadId: thread.id,
84
+ thread,
85
+ projectDirectory,
86
+ sdkDirectory: projectDirectory,
87
+ channelId: textChannel.id,
88
+ appId,
89
+ })
90
+ await runtime.enqueueIncoming({
91
+ prompt: fullPrompt,
92
+ userId: command.user.id,
93
+ username: command.user.displayName,
94
+ agent,
95
+ appId,
96
+ mode: 'opencode',
97
+ })
98
+ } catch (error) {
99
+ logger.error('[SESSION] Error:', error)
100
+ await command.editReply(
101
+ `Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`,
102
+ )
103
+ }
104
+ }
105
+
106
+ async function handleAgentAutocomplete({
107
+ interaction,
108
+ }: {
109
+ interaction: AutocompleteContext['interaction']
110
+ }): Promise<void> {
111
+ const focusedValue = interaction.options.getFocused()
112
+
113
+ // interaction.channel can be null when the channel isn't cached
114
+ // (common with gateway-proxy). Use channelId which is always available
115
+ // from the raw interaction payload.
116
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
117
+
118
+ if (!projectDirectory) {
119
+ await interaction.respond([])
120
+ return
121
+ }
122
+
123
+ try {
124
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
125
+ if (getClient instanceof Error) {
126
+ await interaction.respond([])
127
+ return
128
+ }
129
+
130
+ const agentsResponse = await getClient().app.agents({
131
+ directory: projectDirectory,
132
+ })
133
+
134
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
135
+ await interaction.respond([])
136
+ return
137
+ }
138
+
139
+ const agents = agentsResponse.data
140
+ .filter((a) => {
141
+ const hidden = (a as { hidden?: boolean }).hidden
142
+ return (a.mode === 'primary' || a.mode === 'all') && !hidden
143
+ })
144
+ .filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
145
+ .slice(0, 25)
146
+
147
+ const choices = agents.map((agent) => ({
148
+ name: agent.name.slice(0, 100),
149
+ value: agent.name,
150
+ }))
151
+
152
+ await interaction.respond(choices)
153
+ } catch (error) {
154
+ logger.error('[AUTOCOMPLETE] Error fetching agents:', error)
155
+ await interaction.respond([])
156
+ }
157
+ }
158
+
159
+ export async function handleSessionAutocomplete({
160
+ interaction,
161
+ }: AutocompleteContext): Promise<void> {
162
+ const focusedOption = interaction.options.getFocused(true)
163
+
164
+ if (focusedOption.name === 'agent') {
165
+ await handleAgentAutocomplete({ interaction })
166
+ return
167
+ }
168
+
169
+ if (focusedOption.name !== 'files') {
170
+ return
171
+ }
172
+
173
+ const focusedValue = focusedOption.value
174
+
175
+ const parts = focusedValue.split(',')
176
+ const previousFiles = parts
177
+ .slice(0, -1)
178
+ .map((f) => f.trim())
179
+ .filter((f) => f)
180
+ const currentQuery = (parts[parts.length - 1] || '').trim()
181
+
182
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
183
+
184
+ if (!projectDirectory) {
185
+ await interaction.respond([])
186
+ return
187
+ }
188
+
189
+ try {
190
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
191
+ if (getClient instanceof Error) {
192
+ await interaction.respond([])
193
+ return
194
+ }
195
+
196
+ const response = await getClient().find.files({
197
+ query: currentQuery || '',
198
+ })
199
+
200
+ const files = response.data || []
201
+
202
+ const prefix =
203
+ previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
204
+
205
+ const choices = files
206
+ .map((file: string) => {
207
+ const fullValue = prefix + file
208
+ const allFiles = [...previousFiles, file]
209
+ const allBasenames = allFiles.map((f) => f.split('/').pop() || f)
210
+ let displayName = allBasenames.join(', ')
211
+ if (displayName.length > 100) {
212
+ displayName = '…' + displayName.slice(-97)
213
+ }
214
+ return {
215
+ name: displayName,
216
+ value: fullValue,
217
+ }
218
+ })
219
+ .filter((choice) => choice.value.length <= 100)
220
+ .slice(0, 25)
221
+
222
+ await interaction.respond(choices)
223
+ } catch (error) {
224
+ logger.error('[AUTOCOMPLETE] Error fetching files:', error)
225
+ await interaction.respond([])
226
+ }
227
+ }