@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,155 @@
1
+ // /remove-project command - Remove Discord channels for a project.
2
+
3
+ import path from 'node:path'
4
+ import * as errore from 'errore'
5
+ import type { CommandContext, AutocompleteContext } from './types.js'
6
+ import {
7
+ findChannelsByDirectory,
8
+ deleteChannelDirectoriesByDirectory,
9
+ getAllTextChannelDirectories,
10
+ } from '../database.js'
11
+ import { createLogger, LogPrefix } from '../logger.js'
12
+ import { abbreviatePath } from '../utils.js'
13
+
14
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT)
15
+
16
+ export async function handleRemoveProjectCommand({
17
+ command,
18
+ appId,
19
+ }: CommandContext): Promise<void> {
20
+ await command.deferReply()
21
+
22
+ const directory = command.options.getString('project', true)
23
+ const guild = command.guild
24
+
25
+ if (!guild) {
26
+ await command.editReply('This command can only be used in a guild')
27
+ return
28
+ }
29
+
30
+ try {
31
+ // Get channel IDs for this directory
32
+ const channels = await findChannelsByDirectory({ directory })
33
+
34
+ if (channels.length === 0) {
35
+ await command.editReply(
36
+ `No channels found for directory: \`${directory}\``,
37
+ )
38
+ return
39
+ }
40
+
41
+ const deletedChannels: string[] = []
42
+ const failedChannels: string[] = []
43
+
44
+ for (const { channel_id, channel_type } of channels as Array<{
45
+ channel_id: string
46
+ channel_type: string
47
+ }>) {
48
+ const channel = await errore.tryAsync({
49
+ try: () => guild.channels.fetch(channel_id),
50
+ catch: (e) => e as Error,
51
+ })
52
+
53
+ if (channel instanceof Error) {
54
+ logger.error(`Failed to fetch channel ${channel_id}:`, channel)
55
+ failedChannels.push(`${channel_type}: ${channel_id}`)
56
+ continue
57
+ }
58
+
59
+ if (channel) {
60
+ try {
61
+ await channel.delete(`Removed by /remove-project command`)
62
+ deletedChannels.push(`${channel_type}: ${channel_id}`)
63
+ } catch (error) {
64
+ logger.error(`Failed to delete channel ${channel_id}:`, error)
65
+ failedChannels.push(`${channel_type}: ${channel_id}`)
66
+ }
67
+ } else {
68
+ deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
69
+ }
70
+ }
71
+
72
+ // Remove from database
73
+ await deleteChannelDirectoriesByDirectory(directory)
74
+
75
+ const projectName = path.basename(directory)
76
+ let message = `Removed project **${projectName}**\n`
77
+ message += `Directory: \`${directory}\`\n\n`
78
+
79
+ if (deletedChannels.length > 0) {
80
+ message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`
81
+ }
82
+
83
+ if (failedChannels.length > 0) {
84
+ message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`
85
+ }
86
+
87
+ await command.editReply(message)
88
+ logger.log(`Removed project ${projectName} at ${directory}`)
89
+ } catch (error) {
90
+ logger.error('[REMOVE-PROJECT] Error:', error)
91
+ await command.editReply(
92
+ `Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`,
93
+ )
94
+ }
95
+ }
96
+
97
+ export async function handleRemoveProjectAutocomplete({
98
+ interaction,
99
+ appId,
100
+ }: AutocompleteContext): Promise<void> {
101
+ const focusedValue = interaction.options.getFocused()
102
+ const guild = interaction.guild
103
+
104
+ if (!guild) {
105
+ await interaction.respond([])
106
+ return
107
+ }
108
+
109
+ try {
110
+ // Get all directories with channels
111
+ const allChannels = (await findChannelsByDirectory({
112
+ channelType: 'text',
113
+ })) as Array<{
114
+ directory: string
115
+ channel_id: string
116
+ }>
117
+
118
+ // Filter to only channels that exist in this guild
119
+ const projectsInGuild: { directory: string; channelId: string }[] = []
120
+
121
+ for (const { directory, channel_id } of allChannels) {
122
+ const channel = await errore.tryAsync({
123
+ try: () => guild.channels.fetch(channel_id),
124
+ catch: (e) => e as Error,
125
+ })
126
+ if (channel instanceof Error) {
127
+ // Channel not in this guild, skip
128
+ continue
129
+ }
130
+ if (channel) {
131
+ projectsInGuild.push({ directory, channelId: channel_id })
132
+ }
133
+ }
134
+
135
+ const projects = projectsInGuild
136
+ .filter(({ directory }) => {
137
+ const baseName = path.basename(directory)
138
+ const searchText = `${baseName} ${directory}`.toLowerCase()
139
+ return searchText.includes(focusedValue.toLowerCase())
140
+ })
141
+ .slice(0, 25)
142
+ .map(({ directory }) => {
143
+ const name = `${path.basename(directory)} (${abbreviatePath(directory)})`
144
+ return {
145
+ name: name.length > 100 ? name.slice(0, 99) + '...' : name,
146
+ value: directory,
147
+ }
148
+ })
149
+
150
+ await interaction.respond(projects)
151
+ } catch (error) {
152
+ logger.error('[AUTOCOMPLETE] Error fetching projects:', error)
153
+ await interaction.respond([])
154
+ }
155
+ }
@@ -0,0 +1,162 @@
1
+ // /restart-opencode-server command - Restart the single shared opencode server
2
+ // and re-register Discord slash commands.
3
+ // Used for resolving opencode state issues, internal bugs, refreshing auth state,
4
+ // plugins, and picking up new/changed slash commands or agents. Aborts in-progress
5
+ // sessions in this channel before restarting. Note: since there is one shared server,
6
+ // this restart affects all projects. Other runtimes reconnect through their listener
7
+ // backoff loop once the shared server comes back.
8
+
9
+ import {
10
+ ChannelType,
11
+ MessageFlags,
12
+ type ThreadChannel,
13
+ type TextChannel,
14
+ } from 'discord.js'
15
+ import type { Command as OpencodeCommand } from '@opencode-ai/sdk/v2'
16
+ import type { CommandContext } from './types.js'
17
+ import { initializeOpencodeForDirectory, restartOpencodeServer } from '../opencode.js'
18
+ import {
19
+ resolveWorkingDirectory,
20
+ SILENT_MESSAGE_FLAGS,
21
+ } from '../discord-utils.js'
22
+ import { createLogger, LogPrefix } from '../logger.js'
23
+ import { disposeRuntimesForDirectory } from '../session-handler/thread-session-runtime.js'
24
+ import { registerCommands, type AgentInfo } from '../discord-command-registration.js'
25
+
26
+ const logger = createLogger(LogPrefix.OPENCODE)
27
+
28
+ export async function handleRestartOpencodeServerCommand({
29
+ command,
30
+ appId,
31
+ }: CommandContext): Promise<void> {
32
+ const channel = command.channel
33
+
34
+ if (!channel) {
35
+ await command.reply({
36
+ content: 'This command can only be used in a channel',
37
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
38
+ })
39
+ return
40
+ }
41
+
42
+ const isThread = [
43
+ ChannelType.PublicThread,
44
+ ChannelType.PrivateThread,
45
+ ChannelType.AnnouncementThread,
46
+ ].includes(channel.type)
47
+
48
+ const isTextChannel = channel.type === ChannelType.GuildText
49
+
50
+ if (!isThread && !isTextChannel) {
51
+ await command.reply({
52
+ content: 'This command can only be used in text channels or threads',
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 } = resolved
71
+
72
+ // Defer reply since restart may take a moment
73
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
74
+
75
+ // Dispose all runtimes for this directory/channel scope.
76
+ // disposeRuntimesForDirectory aborts active runs, kills listeners, and
77
+ // removes runtimes from the registry. Scoped by channelId so runtimes
78
+ // in other channels sharing the same project directory are not affected.
79
+ const parentChannelId = isThread
80
+ ? (channel as ThreadChannel).parentId
81
+ : channel.id
82
+ const abortedCount = disposeRuntimesForDirectory({
83
+ directory: projectDirectory,
84
+ channelId: parentChannelId || undefined,
85
+ })
86
+
87
+ logger.log(`[RESTART] Restarting shared opencode server`)
88
+
89
+ const result = await restartOpencodeServer()
90
+
91
+ if (result instanceof Error) {
92
+ logger.error('[RESTART] Failed:', result)
93
+ await command.editReply({
94
+ content: `Failed to restart opencode server: ${result.message}`,
95
+ })
96
+ return
97
+ }
98
+
99
+ const abortMsg =
100
+ abortedCount > 0
101
+ ? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
102
+ : ''
103
+ await command.editReply({
104
+ content: `Opencode server **restarted** successfully${abortMsg}. Re-registering slash commands...`,
105
+ })
106
+ logger.log('[RESTART] Shared opencode server restarted')
107
+
108
+ // Re-register Discord slash commands after restart so new/changed
109
+ // commands, agents, and plugins are picked up immediately.
110
+ const token = command.client.token
111
+ if (!token) {
112
+ logger.error('[RESTART] No bot token available, skipping command registration')
113
+ await command.editReply({
114
+ content: `Opencode server **restarted**${abortMsg}, but slash command re-registration skipped (no bot token)`,
115
+ })
116
+ return
117
+ }
118
+ const guildIds = [...command.client.guilds.cache.keys()]
119
+
120
+ const opencodeResult = await initializeOpencodeForDirectory(projectDirectory)
121
+ const [userCommands, agents]: [OpencodeCommand[], AgentInfo[]] =
122
+ await (async (): Promise<[OpencodeCommand[], AgentInfo[]]> => {
123
+ if (opencodeResult instanceof Error) {
124
+ logger.warn('[RESTART] OpenCode init failed, registering without user commands:', opencodeResult.message)
125
+ return [[], []]
126
+ }
127
+ const getClient = opencodeResult
128
+ const [cmds, ags] = await Promise.all([
129
+ getClient()
130
+ .command.list({ directory: projectDirectory })
131
+ .then((r) => r.data || [])
132
+ .catch((e) => {
133
+ logger.warn('[RESTART] Failed to load user commands:', e instanceof Error ? e.stack : String(e))
134
+ return [] as OpencodeCommand[]
135
+ }),
136
+ getClient()
137
+ .app.agents({ directory: projectDirectory })
138
+ .then((r) => r.data || [])
139
+ .catch((e) => {
140
+ logger.warn('[RESTART] Failed to load agents:', e instanceof Error ? e.stack : String(e))
141
+ return [] as AgentInfo[]
142
+ }),
143
+ ])
144
+ return [cmds, ags]
145
+ })()
146
+
147
+ const registerResult = await registerCommands({ token, appId, guildIds, userCommands, agents })
148
+ .then(() => null)
149
+ .catch((e: unknown) => (e instanceof Error ? e : new Error(String(e))))
150
+ if (registerResult instanceof Error) {
151
+ logger.error('[RESTART] Failed to re-register commands:', registerResult.message)
152
+ await command.editReply({
153
+ content: `Opencode server **restarted**${abortMsg}, but slash command re-registration failed: ${registerResult.message}`,
154
+ })
155
+ return
156
+ }
157
+
158
+ logger.log('[RESTART] Slash commands re-registered')
159
+ await command.editReply({
160
+ content: `Opencode server **restarted** and slash commands **re-registered**${abortMsg}`,
161
+ })
162
+ }
@@ -0,0 +1,230 @@
1
+ // /resume command - Resume an existing OpenCode session.
2
+
3
+ import {
4
+ ChannelType,
5
+ ThreadAutoArchiveDuration,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import fs from 'node:fs'
10
+ import type { CommandContext, AutocompleteContext } from './types.js'
11
+ import {
12
+ getChannelDirectory,
13
+ setThreadSession,
14
+ setPartMessagesBatch,
15
+ getAllThreadSessionIds,
16
+ } from '../database.js'
17
+ import { initializeOpencodeForDirectory } from '../opencode.js'
18
+ import {
19
+ sendThreadMessage,
20
+ resolveProjectDirectoryFromAutocomplete,
21
+ NOTIFY_MESSAGE_FLAGS,
22
+ } from '../discord-utils.js'
23
+ import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js'
24
+ import { createLogger, LogPrefix } from '../logger.js'
25
+ import * as errore from 'errore'
26
+
27
+ const logger = createLogger(LogPrefix.RESUME)
28
+
29
+ export async function handleResumeCommand({
30
+ command,
31
+ }: CommandContext): Promise<void> {
32
+ await command.deferReply()
33
+
34
+ const sessionId = command.options.getString('session', true)
35
+ const channel = command.channel
36
+
37
+ const isThread =
38
+ channel &&
39
+ [
40
+ ChannelType.PublicThread,
41
+ ChannelType.PrivateThread,
42
+ ChannelType.AnnouncementThread,
43
+ ].includes(channel.type)
44
+
45
+ if (isThread) {
46
+ await command.editReply(
47
+ 'This command can only be used in project channels, not threads',
48
+ )
49
+ return
50
+ }
51
+
52
+ if (!channel || channel.type !== ChannelType.GuildText) {
53
+ await command.editReply('This command can only be used in text channels')
54
+ return
55
+ }
56
+
57
+ const textChannel = channel as TextChannel
58
+
59
+ const channelConfig = await getChannelDirectory(textChannel.id)
60
+ const projectDirectory = channelConfig?.directory
61
+
62
+ if (!projectDirectory) {
63
+ await command.editReply(
64
+ 'This channel is not configured with a project directory',
65
+ )
66
+ return
67
+ }
68
+
69
+ if (!fs.existsSync(projectDirectory)) {
70
+ await command.editReply(`Directory does not exist: ${projectDirectory}`)
71
+ return
72
+ }
73
+
74
+ try {
75
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
76
+ if (getClient instanceof Error) {
77
+ await command.editReply(getClient.message)
78
+ return
79
+ }
80
+
81
+ const sessionResponse = await getClient().session.get({
82
+ sessionID: sessionId,
83
+ })
84
+
85
+ if (!sessionResponse.data) {
86
+ await command.editReply('Session not found')
87
+ return
88
+ }
89
+
90
+ const sessionTitle = sessionResponse.data.title
91
+
92
+ const thread = await textChannel.threads.create({
93
+ name: `Resume: ${sessionTitle}`.slice(0, 100),
94
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
95
+ reason: `Resuming session ${sessionId}`,
96
+ })
97
+
98
+ // Claim the resumed session immediately so external polling does not race
99
+ // and create a duplicate Sync thread before the rest of this setup runs.
100
+ await setThreadSession(thread.id, sessionId)
101
+
102
+ // Add user to thread so it appears in their sidebar
103
+ await thread.members.add(command.user.id)
104
+
105
+ logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
106
+
107
+ const messagesResponse = await getClient().session.messages({
108
+ sessionID: sessionId,
109
+ })
110
+
111
+ if (!messagesResponse.data) {
112
+ throw new Error('Failed to fetch session messages')
113
+ }
114
+
115
+ const messages = messagesResponse.data
116
+
117
+ await command.editReply(
118
+ `Resumed session "${sessionTitle}" in ${thread.toString()}`,
119
+ )
120
+
121
+ await sendThreadMessage(
122
+ thread,
123
+ `**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
124
+ )
125
+
126
+ try {
127
+ const { chunks, skippedCount } = collectSessionChunks({
128
+ messages,
129
+ limit: 30,
130
+ })
131
+
132
+ if (skippedCount > 0) {
133
+ await sendThreadMessage(
134
+ thread,
135
+ `*Skipped ${skippedCount} older assistant parts...*`,
136
+ )
137
+ }
138
+
139
+ const batched = batchChunksForDiscord(chunks)
140
+ for (const batch of batched) {
141
+ const discordMessage = await sendThreadMessage(thread, batch.content)
142
+ await setPartMessagesBatch(
143
+ batch.partIds.map((partId) => ({
144
+ partId,
145
+ messageId: discordMessage.id,
146
+ threadId: thread.id,
147
+ })),
148
+ )
149
+ }
150
+
151
+ const messageCount = messages.length
152
+
153
+ await sendThreadMessage(
154
+ thread,
155
+ `**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
156
+ )
157
+ } catch (sendError) {
158
+ logger.error('[RESUME] Error sending messages to thread:', sendError)
159
+ await sendThreadMessage(
160
+ thread,
161
+ `Failed to load message history, but session is connected. You can still send new messages.`,
162
+ { flags: NOTIFY_MESSAGE_FLAGS },
163
+ )
164
+ }
165
+ } catch (error) {
166
+ logger.error('[RESUME] Error:', error)
167
+ await command.editReply(
168
+ `Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`,
169
+ )
170
+ }
171
+ }
172
+
173
+ export async function handleResumeAutocomplete({
174
+ interaction,
175
+ }: AutocompleteContext): Promise<void> {
176
+ const focusedValue = interaction.options.getFocused()
177
+
178
+ // interaction.channel can be null when the channel isn't cached
179
+ // (common with gateway-proxy). Use channelId which is always available
180
+ // from the raw interaction payload.
181
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
182
+
183
+ if (!projectDirectory) {
184
+ await interaction.respond([])
185
+ return
186
+ }
187
+
188
+ try {
189
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
190
+ if (getClient instanceof Error) {
191
+ await interaction.respond([])
192
+ return
193
+ }
194
+
195
+ const sessionsResponse = await getClient().session.list()
196
+ if (!sessionsResponse.data) {
197
+ await interaction.respond([])
198
+ return
199
+ }
200
+
201
+ const existingSessionIds = new Set(await getAllThreadSessionIds())
202
+
203
+ const sessions = sessionsResponse.data
204
+ .filter((session) => !existingSessionIds.has(session.id))
205
+ .filter((session) =>
206
+ session.title.toLowerCase().includes(focusedValue.toLowerCase()),
207
+ )
208
+ .slice(0, 25)
209
+ .map((session) => {
210
+ const dateStr = new Date(session.time.updated).toLocaleString()
211
+ const suffix = ` (${dateStr})`
212
+ const maxTitleLength = 100 - suffix.length
213
+
214
+ let title = session.title
215
+ if (title.length > maxTitleLength) {
216
+ title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…'
217
+ }
218
+
219
+ return {
220
+ name: `${title}${suffix}`,
221
+ value: session.id,
222
+ }
223
+ })
224
+
225
+ await interaction.respond(sessions)
226
+ } catch (error) {
227
+ logger.error('[AUTOCOMPLETE] Error fetching sessions:', error)
228
+ await interaction.respond([])
229
+ }
230
+ }
@@ -0,0 +1,123 @@
1
+ // /run-shell-command command - Run an arbitrary shell command in the project directory.
2
+ // Resolves the project directory from the channel and executes the command with it as cwd.
3
+ // Also used by the ! prefix shortcut in discord messages (e.g. "!ls -la").
4
+ // Messages starting with ! are intercepted before session handling and routed here.
5
+
6
+ import {
7
+ ChannelType,
8
+ MessageFlags,
9
+ type TextChannel,
10
+ type ThreadChannel,
11
+ } from 'discord.js'
12
+ import type { CommandContext } from './types.js'
13
+ import {
14
+ resolveWorkingDirectory,
15
+ SILENT_MESSAGE_FLAGS,
16
+ } from '../discord-utils.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
+ import { execAsync } from '../worktrees.js'
19
+ import { stripAnsi } from '../utils.js'
20
+
21
+ const logger = createLogger(LogPrefix.INTERACTION)
22
+
23
+ const MAX_OUTPUT_CHARS = 1900
24
+
25
+ export async function runShellCommand({
26
+ command,
27
+ directory,
28
+ }: {
29
+ command: string
30
+ directory: string
31
+ }): Promise<string> {
32
+ try {
33
+ const { stdout, stderr } = await execAsync(command, { cwd: directory })
34
+ const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim())
35
+
36
+ const header = `\`${command}\` exited with 0`
37
+ if (!output) {
38
+ return header
39
+ }
40
+ return formatOutput(output, header)
41
+ } catch (error) {
42
+ const execError = error as {
43
+ stdout?: string
44
+ stderr?: string
45
+ message?: string
46
+ code?: number | string
47
+ }
48
+ const output = stripAnsi(
49
+ [execError.stdout, execError.stderr].filter(Boolean).join('\n').trim(),
50
+ )
51
+ const exitCode = execError.code ?? 1
52
+ logger.error(
53
+ `[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`,
54
+ error,
55
+ )
56
+
57
+ const header = `\`${command}\` exited with ${exitCode}`
58
+ return formatOutput(output || execError.message || 'Unknown error', header)
59
+ }
60
+ }
61
+
62
+ export async function handleRunCommand({
63
+ command,
64
+ }: CommandContext): Promise<void> {
65
+ const channel = command.channel
66
+
67
+ if (!channel) {
68
+ await command.reply({
69
+ content: 'This command can only be used in a channel.',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ })
72
+ return
73
+ }
74
+
75
+ const isThread = [
76
+ ChannelType.PublicThread,
77
+ ChannelType.PrivateThread,
78
+ ChannelType.AnnouncementThread,
79
+ ].includes(channel.type)
80
+
81
+ const isTextChannel = channel.type === ChannelType.GuildText
82
+
83
+ if (!isThread && !isTextChannel) {
84
+ await command.reply({
85
+ content: 'This command can only be used in a text channel or thread.',
86
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
87
+ })
88
+ return
89
+ }
90
+
91
+ const resolved = await resolveWorkingDirectory({
92
+ channel: channel as TextChannel | ThreadChannel,
93
+ })
94
+
95
+ if (!resolved) {
96
+ await command.reply({
97
+ content: 'Could not determine project directory for this channel.',
98
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
99
+ })
100
+ return
101
+ }
102
+
103
+ const input = command.options.getString('command', true)
104
+
105
+ await command.deferReply()
106
+
107
+ const result = await runShellCommand({
108
+ command: input,
109
+ directory: resolved.workingDirectory,
110
+ })
111
+ await command.editReply({ content: result })
112
+ }
113
+
114
+ function formatOutput(output: string, header: string): string {
115
+ // Reserve space for header + newline + code block delimiters (```\n...\n```)
116
+ const overhead = header.length + 1 + 3 + 1 + 1 + 3 // header\n```\n...\n```
117
+ const maxContent = MAX_OUTPUT_CHARS - overhead
118
+ const truncated =
119
+ output.length > maxContent
120
+ ? output.slice(0, maxContent - 14) + '\n... truncated'
121
+ : output
122
+ return `${header}\n\`\`\`\n${truncated}\n\`\`\``
123
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js'
3
+
4
+ describe('screenshare security defaults', () => {
5
+ test('generates a 128-bit tunnel id', () => {
6
+ const ids = new Set(
7
+ Array.from({ length: 32 }, () => {
8
+ return createScreenshareTunnelId()
9
+ }),
10
+ )
11
+
12
+ expect(ids.size).toBe(32)
13
+ for (const id of ids) {
14
+ expect(id).toMatch(/^[0-9a-f]{32}$/)
15
+ }
16
+ })
17
+
18
+ test('builds a secure noVNC URL', () => {
19
+ const url = new URL(
20
+ buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }),
21
+ )
22
+
23
+ expect(url.origin).toBe('https://novnc.com')
24
+ expect(url.searchParams.get('host')).toBe(
25
+ '0123456789abcdef-tunnel.kimaki.xyz',
26
+ )
27
+ expect(url.searchParams.get('port')).toBe('443')
28
+ expect(url.searchParams.get('encrypt')).toBe('1')
29
+ })
30
+ })