@otto-assistant/otto 0.1.2 → 0.7.16

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 (638) 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-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +655 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +893 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +369 -0
  47. package/dist/commands/model.js +798 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +179 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1124 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +789 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1181 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +488 -0
  324. package/src/commands/model.ts +1082 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1507 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +232 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1462 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,198 @@
1
+ // User-defined OpenCode command handler.
2
+ // Handles slash commands that map to user-configured commands in opencode.json.
3
+
4
+ import type { CommandContext, CommandHandler } from './types.js'
5
+ import {
6
+ ChannelType,
7
+ MessageFlags,
8
+ type TextChannel,
9
+ type ThreadChannel,
10
+ } from 'discord.js'
11
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
12
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
13
+ import { createLogger, LogPrefix } from '../logger.js'
14
+ import { getChannelDirectory, getThreadSession } from '../database.js'
15
+ import { store } from '../store.js'
16
+ import fs from 'node:fs'
17
+
18
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD)
19
+ const DISCORD_MESSAGE_LIMIT = 2000
20
+ const DISCORD_THREAD_NAME_LIMIT = 100
21
+
22
+ export const handleUserCommand: CommandHandler = async ({
23
+ command,
24
+ appId,
25
+ }: CommandContext) => {
26
+ const discordCommandName = command.commandName
27
+ // Look up the original OpenCode command name from the mapping populated at registration.
28
+ // The sanitized Discord name is lossy (e.g. foo:bar → foo-bar), so resolving from
29
+ // the exact registered slash command name avoids collisions.
30
+ const registered = store.getState().registeredUserCommands.find(
31
+ (c) => c.discordCommandName === discordCommandName,
32
+ )
33
+ const fallbackBase = discordCommandName.replace(/-(cmd|skill|mcp-prompt)$/, '')
34
+ const commandName = registered?.name || fallbackBase
35
+ const args = command.options.getString('arguments') || ''
36
+ const commandInvocation = args ? `/${commandName} ${args}` : `/${commandName}`
37
+ const threadOpeningMessage =
38
+ commandInvocation.length <= DISCORD_MESSAGE_LIMIT
39
+ ? commandInvocation
40
+ : `${commandInvocation.slice(0, DISCORD_MESSAGE_LIMIT - 14)}... truncated`
41
+
42
+ userCommandLogger.log(
43
+ `Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`,
44
+ )
45
+
46
+ const channel = command.channel
47
+
48
+ userCommandLogger.log(
49
+ `Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
50
+ )
51
+
52
+ const isThread =
53
+ channel &&
54
+ [
55
+ ChannelType.PublicThread,
56
+ ChannelType.PrivateThread,
57
+ ChannelType.AnnouncementThread,
58
+ ].includes(channel.type)
59
+
60
+ const isTextChannel = channel?.type === ChannelType.GuildText
61
+
62
+ if (!channel || (!isTextChannel && !isThread)) {
63
+ await command.reply({
64
+ content: 'This command can only be used in text channels or threads',
65
+ flags: MessageFlags.Ephemeral,
66
+ })
67
+ return
68
+ }
69
+
70
+ let projectDirectory: string | undefined
71
+ let textChannel: TextChannel | null = null
72
+ let thread: ThreadChannel | null = null
73
+
74
+ if (isThread) {
75
+ // Running in an existing thread - get project directory from parent channel
76
+ thread = channel as ThreadChannel
77
+ textChannel = thread.parent as TextChannel | null
78
+
79
+ // Verify this thread has an existing session
80
+ const sessionId = await getThreadSession(thread.id)
81
+
82
+ if (!sessionId) {
83
+ await command.reply({
84
+ content:
85
+ 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
86
+ flags: MessageFlags.Ephemeral,
87
+ })
88
+ return
89
+ }
90
+
91
+ if (textChannel) {
92
+ const channelConfig = await getChannelDirectory(textChannel.id)
93
+ projectDirectory = channelConfig?.directory
94
+ }
95
+ } else {
96
+ // Running in a text channel - will create a new thread
97
+ textChannel = channel as TextChannel
98
+
99
+ const channelConfig = await getChannelDirectory(textChannel.id)
100
+ projectDirectory = channelConfig?.directory
101
+ }
102
+
103
+ if (!projectDirectory) {
104
+ await command.reply({
105
+ content: 'This channel is not configured with a project directory',
106
+ flags: MessageFlags.Ephemeral,
107
+ })
108
+ return
109
+ }
110
+
111
+ if (!fs.existsSync(projectDirectory)) {
112
+ await command.reply({
113
+ content: `Directory does not exist: ${projectDirectory}`,
114
+ flags: MessageFlags.Ephemeral,
115
+ })
116
+ return
117
+ }
118
+
119
+ await command.deferReply()
120
+
121
+ try {
122
+ // Use the dedicated session.command API instead of formatting as text prompt
123
+ const commandPayload = { name: commandName, arguments: args }
124
+
125
+ if (isThread && thread) {
126
+ // Running in existing thread - just send the command
127
+ await command.editReply(`Running ${commandInvocation}...`)
128
+
129
+ const runtime = getOrCreateRuntime({
130
+ threadId: thread.id,
131
+ thread,
132
+ projectDirectory,
133
+ sdkDirectory: projectDirectory,
134
+ channelId: textChannel?.id,
135
+ appId,
136
+ })
137
+ await runtime.enqueueIncoming({
138
+ prompt: '',
139
+ userId: command.user.id,
140
+ username: command.user.displayName,
141
+ command: commandPayload,
142
+ appId,
143
+ mode: 'local-queue',
144
+ })
145
+ } else if (textChannel) {
146
+ // Running in text channel - create a new thread
147
+ const starterMessage = await textChannel.send({
148
+ content: threadOpeningMessage,
149
+ flags: SILENT_MESSAGE_FLAGS,
150
+ })
151
+
152
+ const newThread = await starterMessage.startThread({
153
+ name: commandInvocation.slice(0, DISCORD_THREAD_NAME_LIMIT),
154
+ autoArchiveDuration: 1440,
155
+ reason: `OpenCode command: ${commandName}`,
156
+ })
157
+
158
+ // Add user to thread so it appears in their sidebar
159
+ await newThread.members.add(command.user.id)
160
+
161
+ await command.editReply(
162
+ `Started /${commandName} in ${newThread.toString()}`,
163
+ )
164
+
165
+ const runtime = getOrCreateRuntime({
166
+ threadId: newThread.id,
167
+ thread: newThread,
168
+ projectDirectory,
169
+ sdkDirectory: projectDirectory,
170
+ channelId: textChannel.id,
171
+ appId,
172
+ })
173
+ await runtime.enqueueIncoming({
174
+ prompt: '',
175
+ userId: command.user.id,
176
+ username: command.user.displayName,
177
+ command: commandPayload,
178
+ appId,
179
+ mode: 'local-queue',
180
+ })
181
+ }
182
+ } catch (error) {
183
+ userCommandLogger.error(`Error executing /${commandName}:`, error)
184
+
185
+ const errorMessage = error instanceof Error ? error.message : String(error)
186
+
187
+ if (command.deferred) {
188
+ await command.editReply({
189
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
190
+ })
191
+ } else {
192
+ await command.reply({
193
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
194
+ flags: MessageFlags.Ephemeral,
195
+ })
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,173 @@
1
+ // /verbosity command.
2
+ // Shows a dropdown to set output verbosity level for sessions in a channel.
3
+ // 'text_and_essential_tools' (default): shows text and essential tools (edits, custom MCP tools)
4
+ // 'tools_and_text': shows all output including tool executions
5
+ // 'text_only': only shows text responses
6
+
7
+ import {
8
+ ChatInputCommandInteraction,
9
+ StringSelectMenuInteraction,
10
+ StringSelectMenuBuilder,
11
+ ActionRowBuilder,
12
+ MessageFlags,
13
+ ChannelType,
14
+ type ThreadChannel,
15
+ } from 'discord.js'
16
+ import {
17
+ getChannelVerbosity,
18
+ setChannelVerbosity,
19
+ type VerbosityLevel,
20
+ } from '../database.js'
21
+ import { getPrisma } from '../db.js'
22
+ import { store } from '../store.js'
23
+ import { createLogger, LogPrefix } from '../logger.js'
24
+
25
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
26
+
27
+ const VERBOSITY_OPTIONS: Array<{
28
+ value: VerbosityLevel
29
+ label: string
30
+ description: string
31
+ }> = [
32
+ {
33
+ value: 'tools_and_text',
34
+ label: 'Tools and text',
35
+ description: 'All output including tool executions and status messages',
36
+ },
37
+ {
38
+ value: 'text_and_essential_tools',
39
+ label: 'Text and essential tools',
40
+ description: 'Text + essential tools (edits, custom MCP). Hides read/search.',
41
+ },
42
+ {
43
+ value: 'text_only',
44
+ label: 'Text only',
45
+ description: 'Only text responses. Hides all tools and status messages.',
46
+ },
47
+ ]
48
+
49
+ function resolveChannelId(channel: ChatInputCommandInteraction['channel']): string | null {
50
+ if (!channel) {
51
+ return null
52
+ }
53
+ if (channel.type === ChannelType.GuildText) {
54
+ return channel.id
55
+ }
56
+ if (
57
+ channel.type === ChannelType.PublicThread ||
58
+ channel.type === ChannelType.PrivateThread ||
59
+ channel.type === ChannelType.AnnouncementThread
60
+ ) {
61
+ return (channel as ThreadChannel).parentId || channel.id
62
+ }
63
+ return channel.id
64
+ }
65
+
66
+ /**
67
+ * Check if there is a per-channel verbosity override in the DB.
68
+ * Returns the override value if it exists, null otherwise.
69
+ */
70
+ async function getChannelVerbosityOverride(
71
+ channelId: string,
72
+ ): Promise<VerbosityLevel | null> {
73
+ const prisma = await getPrisma()
74
+ const row = await prisma.channel_verbosity.findUnique({
75
+ where: { channel_id: channelId },
76
+ })
77
+ if (row?.verbosity) {
78
+ return row.verbosity as VerbosityLevel
79
+ }
80
+ return null
81
+ }
82
+
83
+ /**
84
+ * Handle the /verbosity slash command.
85
+ * Shows a dropdown with the current verbosity level and available options.
86
+ */
87
+ export async function handleVerbosityCommand({
88
+ command,
89
+ }: {
90
+ command: ChatInputCommandInteraction
91
+ appId: string
92
+ }): Promise<void> {
93
+ verbosityLogger.log('[VERBOSITY] Command called')
94
+
95
+ const channelId = resolveChannelId(command.channel)
96
+ if (!channelId) {
97
+ await command.reply({
98
+ content: 'Could not determine channel.',
99
+ flags: MessageFlags.Ephemeral,
100
+ })
101
+ return
102
+ }
103
+
104
+ const override = await getChannelVerbosityOverride(channelId)
105
+ const currentLevel = override || store.getState().defaultVerbosity
106
+ const source = override ? 'channel override' : 'global default'
107
+
108
+ const options = VERBOSITY_OPTIONS.map((opt) => ({
109
+ label: opt.label,
110
+ value: opt.value,
111
+ description: opt.description,
112
+ default: opt.value === currentLevel,
113
+ }))
114
+
115
+ const selectMenu = new StringSelectMenuBuilder()
116
+ .setCustomId(`verbosity_select:${channelId}`)
117
+ .setPlaceholder('Select verbosity level')
118
+ .addOptions(options)
119
+
120
+ const actionRow =
121
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
122
+
123
+ await command.reply({
124
+ content: `**Verbosity**\nCurrent: \`${currentLevel}\` (${source})`,
125
+ components: [actionRow],
126
+ flags: MessageFlags.Ephemeral,
127
+ })
128
+ }
129
+
130
+ /**
131
+ * Handle the verbosity select menu interaction.
132
+ * Sets the selected verbosity level for the channel.
133
+ */
134
+ export async function handleVerbositySelectMenu(
135
+ interaction: StringSelectMenuInteraction,
136
+ ): Promise<void> {
137
+ const customId = interaction.customId
138
+ if (!customId.startsWith('verbosity_select:')) {
139
+ return
140
+ }
141
+
142
+ await interaction.deferUpdate()
143
+
144
+ const channelId = customId.replace('verbosity_select:', '')
145
+ const level = interaction.values[0] as VerbosityLevel | undefined
146
+
147
+ if (!level) {
148
+ await interaction.editReply({
149
+ content: 'No level selected.',
150
+ components: [],
151
+ })
152
+ return
153
+ }
154
+
155
+ const currentLevel = await getChannelVerbosity(channelId)
156
+ if (currentLevel === level) {
157
+ await interaction.editReply({
158
+ content: `Verbosity is already \`${level}\` for this channel.`,
159
+ components: [],
160
+ })
161
+ return
162
+ }
163
+
164
+ await setChannelVerbosity(channelId, level)
165
+ verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`)
166
+
167
+ const description = VERBOSITY_OPTIONS.find((o) => o.value === level)?.description || ''
168
+
169
+ await interaction.editReply({
170
+ content: `Verbosity set to \`${level}\` for this channel.\n${description}\nApplies immediately, including active sessions.`,
171
+ components: [],
172
+ })
173
+ }
@@ -0,0 +1,342 @@
1
+ import crypto from 'node:crypto'
2
+ import { spawn, type ChildProcess } from 'node:child_process'
3
+ import net from 'node:net'
4
+ import {
5
+ ChannelType,
6
+ MessageFlags,
7
+ type TextChannel,
8
+ type ThreadChannel,
9
+ } from 'discord.js'
10
+ import { TunnelClient } from 'traforo/client'
11
+ import type { CommandContext } from './types.js'
12
+ import {
13
+ resolveWorkingDirectory,
14
+ SILENT_MESSAGE_FLAGS,
15
+ } from '../discord-utils.js'
16
+ import { createLogger } from '../logger.js'
17
+
18
+ const logger = createLogger('VSCODE')
19
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS
20
+ const MAX_SESSION_MINUTES = 30
21
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000
22
+ const TUNNEL_BASE_DOMAIN = 'otto.dev'
23
+ const TUNNEL_ID_BYTES = 16
24
+ const READY_TIMEOUT_MS = 60_000
25
+ const LOCAL_HOST = '127.0.0.1'
26
+
27
+ export type VscodeSession = {
28
+ coderaftProcess: ChildProcess
29
+ tunnelClient: TunnelClient
30
+ url: string
31
+ workingDirectory: string
32
+ startedBy: string
33
+ startedAt: number
34
+ timeoutTimer: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ const activeSessions = new Map<string, VscodeSession>()
38
+
39
+ export function createVscodeTunnelId(): string {
40
+ return crypto.randomBytes(TUNNEL_ID_BYTES).toString('hex')
41
+ }
42
+
43
+ export function buildCoderaftArgs({
44
+ port,
45
+ workingDirectory,
46
+ }: {
47
+ port: number
48
+ workingDirectory: string
49
+ }): string[] {
50
+ return [
51
+ 'coderaft',
52
+ '--port',
53
+ String(port),
54
+ '--host',
55
+ LOCAL_HOST,
56
+ '--without-connection-token',
57
+ '--disable-workspace-trust',
58
+ '--default-folder',
59
+ workingDirectory,
60
+ ]
61
+ }
62
+
63
+ function createPortWaiter({
64
+ port,
65
+ process: proc,
66
+ timeoutMs,
67
+ }: {
68
+ port: number
69
+ process: ChildProcess
70
+ timeoutMs: number
71
+ }): Promise<void> {
72
+ return new Promise((resolve, reject) => {
73
+ const maxAttempts = Math.ceil(timeoutMs / 100)
74
+ let attempts = 0
75
+
76
+ const check = (): void => {
77
+ if (proc.exitCode !== null) {
78
+ reject(new Error(`coderaft exited with code ${proc.exitCode} before becoming ready`))
79
+ return
80
+ }
81
+
82
+ const socket = net.createConnection(port, LOCAL_HOST)
83
+ socket.on('connect', () => {
84
+ socket.destroy()
85
+ resolve()
86
+ })
87
+ socket.on('error', () => {
88
+ socket.destroy()
89
+ attempts += 1
90
+ if (attempts >= maxAttempts) {
91
+ reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`))
92
+ return
93
+ }
94
+ setTimeout(check, 100)
95
+ })
96
+ }
97
+
98
+ check()
99
+ })
100
+ }
101
+
102
+ function getAvailablePort(): Promise<number> {
103
+ return new Promise((resolve, reject) => {
104
+ const server = net.createServer()
105
+ server.on('error', reject)
106
+ server.listen(0, LOCAL_HOST, () => {
107
+ const address = server.address()
108
+ if (!address || typeof address === 'string') {
109
+ server.close(() => {
110
+ reject(new Error('Failed to resolve an available port'))
111
+ })
112
+ return
113
+ }
114
+ const port = address.port
115
+ server.close((error) => {
116
+ if (error) {
117
+ reject(error)
118
+ return
119
+ }
120
+ resolve(port)
121
+ })
122
+ })
123
+ })
124
+ }
125
+
126
+ function cleanupSession(session: VscodeSession): void {
127
+ clearTimeout(session.timeoutTimer)
128
+ try {
129
+ session.tunnelClient.close()
130
+ } catch {}
131
+ if (session.coderaftProcess.exitCode === null) {
132
+ try {
133
+ session.coderaftProcess.kill('SIGTERM')
134
+ } catch {}
135
+ }
136
+ }
137
+
138
+ export function getActiveVscodeSession({ sessionKey }: { sessionKey: string }): VscodeSession | undefined {
139
+ return activeSessions.get(sessionKey)
140
+ }
141
+
142
+ export function stopVscode({ sessionKey }: { sessionKey: string }): boolean {
143
+ const session = activeSessions.get(sessionKey)
144
+ if (!session) {
145
+ return false
146
+ }
147
+
148
+ activeSessions.delete(sessionKey)
149
+ cleanupSession(session)
150
+ logger.log(`VS Code stopped (key: ${sessionKey})`)
151
+ return true
152
+ }
153
+
154
+ export async function startVscode({
155
+ sessionKey,
156
+ startedBy,
157
+ workingDirectory,
158
+ }: {
159
+ sessionKey: string
160
+ startedBy: string
161
+ workingDirectory: string
162
+ }): Promise<VscodeSession> {
163
+ const existing = activeSessions.get(sessionKey)
164
+ if (existing) {
165
+ return existing
166
+ }
167
+
168
+ const port = await getAvailablePort()
169
+ const tunnelId = createVscodeTunnelId()
170
+ const args = buildCoderaftArgs({
171
+ port,
172
+ workingDirectory,
173
+ })
174
+ const coderaftProcess = spawn('bunx', args, {
175
+ cwd: workingDirectory,
176
+ stdio: ['ignore', 'pipe', 'pipe'],
177
+ env: {
178
+ ...process.env,
179
+ PORT: String(port),
180
+ },
181
+ })
182
+
183
+ coderaftProcess.stdout?.on('data', (data: Buffer) => {
184
+ logger.log(data.toString().trim())
185
+ })
186
+ coderaftProcess.stderr?.on('data', (data: Buffer) => {
187
+ logger.error(data.toString().trim())
188
+ })
189
+
190
+ try {
191
+ await createPortWaiter({
192
+ port,
193
+ process: coderaftProcess,
194
+ timeoutMs: READY_TIMEOUT_MS,
195
+ })
196
+ } catch (error) {
197
+ if (coderaftProcess.exitCode === null) {
198
+ coderaftProcess.kill('SIGTERM')
199
+ }
200
+ throw error
201
+ }
202
+
203
+ const tunnelClient = new TunnelClient({
204
+ localPort: port,
205
+ localHost: LOCAL_HOST,
206
+ tunnelId,
207
+ baseDomain: TUNNEL_BASE_DOMAIN,
208
+ })
209
+
210
+ try {
211
+ await Promise.race([
212
+ tunnelClient.connect(),
213
+ new Promise<never>((_, reject) => {
214
+ setTimeout(() => {
215
+ reject(new Error('Tunnel connection timed out after 15s'))
216
+ }, 15_000)
217
+ }),
218
+ ])
219
+ } catch (error) {
220
+ tunnelClient.close()
221
+ if (coderaftProcess.exitCode === null) {
222
+ coderaftProcess.kill('SIGTERM')
223
+ }
224
+ throw error
225
+ }
226
+
227
+ const url = tunnelClient.url
228
+
229
+ const timeoutTimer = setTimeout(() => {
230
+ logger.log(`VS Code auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`)
231
+ stopVscode({ sessionKey })
232
+ }, MAX_SESSION_MS)
233
+ timeoutTimer.unref()
234
+
235
+ const session: VscodeSession = {
236
+ coderaftProcess,
237
+ tunnelClient,
238
+ url,
239
+ workingDirectory,
240
+ startedBy,
241
+ startedAt: Date.now(),
242
+ timeoutTimer,
243
+ }
244
+
245
+ coderaftProcess.once('exit', (code, signal) => {
246
+ const current = activeSessions.get(sessionKey)
247
+ if (current !== session) {
248
+ return
249
+ }
250
+ logger.log(`VS Code process exited (key: ${sessionKey}, code: ${code}, signal: ${signal ?? 'none'})`)
251
+ stopVscode({ sessionKey })
252
+ })
253
+
254
+ activeSessions.set(sessionKey, session)
255
+ logger.log(`VS Code started by ${startedBy}: ${url}`)
256
+ return session
257
+ }
258
+
259
+ export async function handleVscodeCommand({
260
+ command,
261
+ }: CommandContext): Promise<void> {
262
+ const channel = command.channel
263
+ if (!channel) {
264
+ await command.reply({
265
+ content: 'This command can only be used in a channel.',
266
+ flags: SECURE_REPLY_FLAGS,
267
+ })
268
+ return
269
+ }
270
+
271
+ const isThread = [
272
+ ChannelType.PublicThread,
273
+ ChannelType.PrivateThread,
274
+ ChannelType.AnnouncementThread,
275
+ ].includes(channel.type)
276
+ const isTextChannel = channel.type === ChannelType.GuildText
277
+ if (!isThread && !isTextChannel) {
278
+ await command.reply({
279
+ content: 'This command can only be used in a text channel or thread.',
280
+ flags: SECURE_REPLY_FLAGS,
281
+ })
282
+ return
283
+ }
284
+
285
+ const resolved = await resolveWorkingDirectory({
286
+ channel: channel as TextChannel | ThreadChannel,
287
+ })
288
+ if (!resolved) {
289
+ await command.reply({
290
+ content: 'Could not determine project directory for this channel.',
291
+ flags: SECURE_REPLY_FLAGS,
292
+ })
293
+ return
294
+ }
295
+
296
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS })
297
+
298
+ const sessionKey = channel.id
299
+ const existing = getActiveVscodeSession({ sessionKey })
300
+ if (existing) {
301
+ await command.editReply({
302
+ content:
303
+ `VS Code is already running for this thread. ` +
304
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes from startup.\n` +
305
+ `${existing.url}`,
306
+ })
307
+ return
308
+ }
309
+
310
+ try {
311
+ const session = await startVscode({
312
+ sessionKey,
313
+ startedBy: command.user.tag,
314
+ workingDirectory: resolved.workingDirectory,
315
+ })
316
+ await command.editReply({
317
+ content:
318
+ `VS Code started for \`${session.workingDirectory}\`. ` +
319
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes, so open it before it expires.\n` +
320
+ `${session.url}`,
321
+ })
322
+ } catch (error) {
323
+ logger.error('Failed to start VS Code:', error)
324
+ await command.editReply({
325
+ content: `Failed to start VS Code: ${error instanceof Error ? error.message : String(error)}`,
326
+ })
327
+ }
328
+ }
329
+
330
+ export function cleanupAllVscodeSessions(): void {
331
+ for (const sessionKey of activeSessions.keys()) {
332
+ stopVscode({ sessionKey })
333
+ }
334
+ }
335
+
336
+ function onProcessExit(): void {
337
+ cleanupAllVscodeSessions()
338
+ }
339
+
340
+ process.on('SIGINT', onProcessExit)
341
+ process.on('SIGTERM', onProcessExit)
342
+ process.on('exit', onProcessExit)