@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,116 @@
1
+ // /btw command - Fork the current session with full context and send a new prompt.
2
+ // Unlike /fork, this does not replay past messages in Discord. It just creates
3
+ // a new thread, forks the entire session (no messageID), and immediately
4
+ // dispatches the user's prompt so the forked session starts working right away.
5
+ import { ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
6
+ import { getThreadSession, setThreadSession } from '../database.js';
7
+ import { initializeOpencodeForDirectory } from '../opencode.js';
8
+ import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
9
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
10
+ import { createLogger, LogPrefix } from '../logger.js';
11
+ const logger = createLogger(LogPrefix.FORK);
12
+ export async function handleBtwCommand({ command, appId, }) {
13
+ const channel = command.channel;
14
+ if (!channel) {
15
+ await command.reply({
16
+ content: 'This command can only be used in a channel',
17
+ flags: MessageFlags.Ephemeral,
18
+ });
19
+ return;
20
+ }
21
+ const isThread = [
22
+ ChannelType.PublicThread,
23
+ ChannelType.PrivateThread,
24
+ ChannelType.AnnouncementThread,
25
+ ].includes(channel.type);
26
+ if (!isThread) {
27
+ await command.reply({
28
+ content: 'This command can only be used in a thread with an active session',
29
+ flags: MessageFlags.Ephemeral,
30
+ });
31
+ return;
32
+ }
33
+ const prompt = command.options.getString('prompt', true);
34
+ const resolved = await resolveWorkingDirectory({
35
+ channel: channel,
36
+ });
37
+ if (!resolved) {
38
+ await command.reply({
39
+ content: 'Could not determine project directory for this channel',
40
+ flags: MessageFlags.Ephemeral,
41
+ });
42
+ return;
43
+ }
44
+ const { projectDirectory } = resolved;
45
+ const sessionId = await getThreadSession(channel.id);
46
+ if (!sessionId) {
47
+ await command.reply({
48
+ content: 'No active session in this thread',
49
+ flags: MessageFlags.Ephemeral,
50
+ });
51
+ return;
52
+ }
53
+ await command.deferReply({ flags: MessageFlags.Ephemeral });
54
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
55
+ if (getClient instanceof Error) {
56
+ await command.editReply({
57
+ content: `Failed to fork session: ${getClient.message}`,
58
+ });
59
+ return;
60
+ }
61
+ try {
62
+ // Fork the entire session (no messageID = fork at the latest point)
63
+ const forkResponse = await getClient().session.fork({
64
+ sessionID: sessionId,
65
+ });
66
+ if (!forkResponse.data) {
67
+ await command.editReply('Failed to fork session');
68
+ return;
69
+ }
70
+ const forkedSession = forkResponse.data;
71
+ const textChannel = await resolveTextChannel(channel);
72
+ if (!textChannel) {
73
+ await command.editReply('Could not resolve parent text channel');
74
+ return;
75
+ }
76
+ const threadName = `btw: ${prompt}`.slice(0, 100);
77
+ const thread = await textChannel.threads.create({
78
+ name: threadName,
79
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
80
+ reason: `btw fork from session ${sessionId}`,
81
+ });
82
+ // Claim the forked session immediately so external polling does not race
83
+ await setThreadSession(thread.id, forkedSession.id);
84
+ await thread.members.add(command.user.id);
85
+ logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`);
86
+ // Short status message with prompt instead of replaying past messages
87
+ const sourceThreadLink = `<#${channel.id}>`;
88
+ await sendThreadMessage(thread, `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`);
89
+ const wrappedPrompt = [
90
+ `The user asked a side question while you were working on another task.`,
91
+ `This is a forked session whose ONLY goal is to answer this question.`,
92
+ `Do NOT continue, resume, or reference the previous task. Only answer the question below.\n`,
93
+ prompt,
94
+ ].join('\n');
95
+ const runtime = getOrCreateRuntime({
96
+ threadId: thread.id,
97
+ thread,
98
+ projectDirectory,
99
+ sdkDirectory: projectDirectory,
100
+ channelId: textChannel.id,
101
+ appId,
102
+ });
103
+ await runtime.enqueueIncoming({
104
+ prompt: wrappedPrompt,
105
+ userId: command.user.id,
106
+ username: command.user.displayName,
107
+ appId,
108
+ mode: 'opencode',
109
+ });
110
+ await command.editReply(`Session forked! Continue in ${thread.toString()}`);
111
+ }
112
+ catch (error) {
113
+ logger.error('Error in /btw:', error);
114
+ await command.editReply(`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`);
115
+ }
116
+ }
@@ -0,0 +1,120 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory, getOpencodeClient, } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.COMPACT);
8
+ export async function handleCompactCommand({ command, }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
14
+ });
15
+ return;
16
+ }
17
+ const isThread = [
18
+ ChannelType.PublicThread,
19
+ ChannelType.PrivateThread,
20
+ ChannelType.AnnouncementThread,
21
+ ].includes(channel.type);
22
+ if (!isThread) {
23
+ await command.reply({
24
+ content: 'This command can only be used in a thread with an active session',
25
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
26
+ });
27
+ return;
28
+ }
29
+ const resolved = await resolveWorkingDirectory({
30
+ channel: channel,
31
+ });
32
+ if (!resolved) {
33
+ await command.reply({
34
+ content: 'Could not determine project directory for this channel',
35
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
36
+ });
37
+ return;
38
+ }
39
+ const { projectDirectory, workingDirectory } = resolved;
40
+ const sessionId = await getThreadSession(channel.id);
41
+ if (!sessionId) {
42
+ await command.reply({
43
+ content: 'No active session in this thread',
44
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
45
+ });
46
+ return;
47
+ }
48
+ // Ensure server is running for the base project directory
49
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
50
+ if (getClient instanceof Error) {
51
+ await command.reply({
52
+ content: `Failed to compact: ${getClient.message}`,
53
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
54
+ });
55
+ return;
56
+ }
57
+ const client = getOpencodeClient(projectDirectory);
58
+ if (!client) {
59
+ await command.reply({
60
+ content: 'Failed to get OpenCode client',
61
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
62
+ });
63
+ return;
64
+ }
65
+ // Defer reply since compaction may take a moment
66
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
67
+ try {
68
+ // Get session messages to find the model from the last user message
69
+ const messagesResult = await client.session.messages({
70
+ sessionID: sessionId,
71
+ directory: workingDirectory,
72
+ });
73
+ if (messagesResult.error || !messagesResult.data) {
74
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error);
75
+ await command.editReply({
76
+ content: 'Failed to compact: Could not retrieve session messages',
77
+ });
78
+ return;
79
+ }
80
+ // Find the last user message to get the model
81
+ const lastUserMessage = [...messagesResult.data]
82
+ .reverse()
83
+ .find((msg) => msg.info.role === 'user');
84
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
85
+ await command.editReply({
86
+ content: 'Failed to compact: No user message found in session',
87
+ });
88
+ return;
89
+ }
90
+ const { providerID, modelID } = lastUserMessage.info.model;
91
+ const result = await client.session.summarize({
92
+ sessionID: sessionId,
93
+ directory: workingDirectory,
94
+ providerID,
95
+ modelID,
96
+ auto: false,
97
+ });
98
+ if (result.error) {
99
+ logger.error('[COMPACT] Error:', result.error);
100
+ const errorMessage = 'data' in result.error && result.error.data
101
+ ? result.error.data.message ||
102
+ 'Unknown error'
103
+ : 'Unknown error';
104
+ await command.editReply({
105
+ content: `Failed to compact: ${errorMessage}`,
106
+ });
107
+ return;
108
+ }
109
+ await command.editReply({
110
+ content: `šŸ“¦ Session **compacted** successfully`,
111
+ });
112
+ logger.log(`Session ${sessionId} compacted by user`);
113
+ }
114
+ catch (error) {
115
+ logger.error('[COMPACT] Error:', error);
116
+ await command.editReply({
117
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
118
+ });
119
+ }
120
+ }
@@ -0,0 +1,140 @@
1
+ // /context-usage command - Show token usage and context window percentage for the current session.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ import * as errore from 'errore';
8
+ const logger = createLogger(LogPrefix.SESSION);
9
+ function getTokenTotal({ input, output, reasoning, cache, }) {
10
+ return input + output + reasoning + cache.read + cache.write;
11
+ }
12
+ export async function handleContextUsageCommand({ command, }) {
13
+ const channel = command.channel;
14
+ if (!channel) {
15
+ await command.reply({
16
+ content: 'This command can only be used in a channel',
17
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
18
+ });
19
+ return;
20
+ }
21
+ const isThread = [
22
+ ChannelType.PublicThread,
23
+ ChannelType.PrivateThread,
24
+ ChannelType.AnnouncementThread,
25
+ ].includes(channel.type);
26
+ if (!isThread) {
27
+ await command.reply({
28
+ content: 'This command can only be used in a thread with an active session',
29
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
30
+ });
31
+ return;
32
+ }
33
+ const resolved = await resolveWorkingDirectory({
34
+ channel: channel,
35
+ });
36
+ if (!resolved) {
37
+ await command.reply({
38
+ content: 'Could not determine project directory for this channel',
39
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
40
+ });
41
+ return;
42
+ }
43
+ const { projectDirectory, workingDirectory } = resolved;
44
+ const sessionId = await getThreadSession(channel.id);
45
+ if (!sessionId) {
46
+ await command.reply({
47
+ content: 'No active session in this thread',
48
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
49
+ });
50
+ return;
51
+ }
52
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
53
+ if (getClient instanceof Error) {
54
+ await command.reply({
55
+ content: `Failed to get context usage: ${getClient.message}`,
56
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
57
+ });
58
+ return;
59
+ }
60
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
61
+ try {
62
+ const messagesResponse = await getClient().session.messages({
63
+ sessionID: sessionId,
64
+ directory: workingDirectory,
65
+ });
66
+ const messages = messagesResponse.data || [];
67
+ const assistantMessages = messages.filter((m) => m.info.role === 'assistant');
68
+ if (assistantMessages.length === 0) {
69
+ await command.editReply({
70
+ content: 'No assistant messages in this session yet',
71
+ });
72
+ return;
73
+ }
74
+ const lastAssistant = [...assistantMessages].reverse().find((m) => {
75
+ if (m.info.role !== 'assistant') {
76
+ return false;
77
+ }
78
+ if (!m.info.tokens) {
79
+ return false;
80
+ }
81
+ return getTokenTotal(m.info.tokens) > 0;
82
+ });
83
+ if (!lastAssistant || lastAssistant.info.role !== 'assistant') {
84
+ await command.editReply({
85
+ content: 'Token usage not available for this session yet',
86
+ });
87
+ return;
88
+ }
89
+ const { tokens, modelID, providerID } = lastAssistant.info;
90
+ const totalTokens = getTokenTotal(tokens);
91
+ // Sum cost across all assistant messages for accurate session total
92
+ // (AssistantMessage.cost is per-message, not cumulative)
93
+ const totalCost = assistantMessages.reduce((sum, m) => {
94
+ if (m.info.role === 'assistant' && 'cost' in m.info) {
95
+ return sum + (m.info.cost || 0);
96
+ }
97
+ return sum;
98
+ }, 0);
99
+ // Fetch model context limit from provider API
100
+ let contextLimit;
101
+ const providersResult = await errore.tryAsync(() => {
102
+ return getClient().provider.list({ directory: workingDirectory });
103
+ });
104
+ if (providersResult instanceof Error) {
105
+ logger.error('[CONTEXT-USAGE] Failed to fetch provider info:', providersResult);
106
+ }
107
+ else {
108
+ const provider = providersResult.data?.all?.find((p) => p.id === providerID);
109
+ const model = provider?.models?.[modelID];
110
+ if (model?.limit?.context) {
111
+ contextLimit = model.limit.context;
112
+ }
113
+ }
114
+ const formattedTokens = totalTokens.toLocaleString('en-US');
115
+ const formattedCost = totalCost > 0 ? `$${totalCost.toFixed(4)}` : '$0.00';
116
+ const lines = [];
117
+ if (contextLimit) {
118
+ const percentage = Math.round((totalTokens / contextLimit) * 100);
119
+ const formattedLimit = contextLimit.toLocaleString('en-US');
120
+ lines.push(`**Context usage:** ${percentage}%, ${formattedTokens} / ${formattedLimit} tokens`);
121
+ }
122
+ else {
123
+ lines.push(`**Context usage:** ${formattedTokens} tokens (context limit unavailable)`);
124
+ }
125
+ if (modelID) {
126
+ lines.push(`**Model:** ${modelID}`);
127
+ }
128
+ if (totalCost > 0) {
129
+ lines.push(`**Session cost:** ${formattedCost}`);
130
+ }
131
+ await command.editReply({ content: lines.join('\n') });
132
+ logger.log(`Context usage shown for session ${sessionId}: ${totalTokens} tokens`);
133
+ }
134
+ catch (error) {
135
+ logger.error('[CONTEXT-USAGE] Error:', error);
136
+ await command.editReply({
137
+ content: `Failed to get context usage: ${error instanceof Error ? error.message : 'Unknown error'}`,
138
+ });
139
+ }
140
+ }
@@ -0,0 +1,130 @@
1
+ // /create-new-project command - Create a new project folder, initialize git, and start a session.
2
+ // Also exports createNewProject() for reuse during onboarding (welcome channel creation).
3
+ import { ChannelType } from 'discord.js';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { execAsync } from '../worktrees.js';
7
+ import { getProjectsDir } from '../config.js';
8
+ import { createProjectChannels } from '../channel-management.js';
9
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
10
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
11
+ import { createLogger, LogPrefix } from '../logger.js';
12
+ const logger = createLogger(LogPrefix.CREATE_PROJECT);
13
+ /**
14
+ * Core project creation logic: creates directory, inits git, creates Discord channels.
15
+ * Reused by the slash command handler and by onboarding (welcome channel).
16
+ * Returns null if the project directory already exists.
17
+ */
18
+ export async function createNewProject({ guild, projectName, botName, }) {
19
+ const sanitizedName = projectName
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9-]/g, '-')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-|-$/g, '')
24
+ .slice(0, 100);
25
+ if (!sanitizedName) {
26
+ return null;
27
+ }
28
+ const projectsDir = getProjectsDir();
29
+ const projectDirectory = path.join(projectsDir, sanitizedName);
30
+ if (!fs.existsSync(projectsDir)) {
31
+ fs.mkdirSync(projectsDir, { recursive: true });
32
+ logger.log(`Created projects directory: ${projectsDir}`);
33
+ }
34
+ if (fs.existsSync(projectDirectory)) {
35
+ return null;
36
+ }
37
+ fs.mkdirSync(projectDirectory, { recursive: true });
38
+ logger.log(`Created project directory: ${projectDirectory}`);
39
+ // Git init — gracefully skip if git is not installed
40
+ try {
41
+ await execAsync('git init', { cwd: projectDirectory, timeout: 10_000 });
42
+ logger.log(`Initialized git in: ${projectDirectory}`);
43
+ }
44
+ catch (error) {
45
+ logger.warn(`Could not initialize git in ${projectDirectory}: ${error instanceof Error ? error.message : String(error)}`);
46
+ }
47
+ const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
48
+ guild,
49
+ projectDirectory,
50
+ botName,
51
+ });
52
+ return {
53
+ textChannelId,
54
+ voiceChannelId,
55
+ channelName,
56
+ projectDirectory,
57
+ sanitizedName,
58
+ };
59
+ }
60
+ export async function handleCreateNewProjectCommand({ command, appId, }) {
61
+ await command.deferReply();
62
+ const projectName = command.options.getString('name', true);
63
+ const guild = command.guild;
64
+ const channel = command.channel;
65
+ if (!guild) {
66
+ await command.editReply('This command can only be used in a guild');
67
+ return;
68
+ }
69
+ if (!channel || channel.type !== ChannelType.GuildText) {
70
+ await command.editReply('This command can only be used in a text channel');
71
+ return;
72
+ }
73
+ try {
74
+ const result = await createNewProject({
75
+ guild,
76
+ projectName,
77
+ botName: command.client.user?.username,
78
+ });
79
+ if (!result) {
80
+ const sanitizedName = projectName
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9-]/g, '-')
83
+ .replace(/-+/g, '-')
84
+ .replace(/^-|-$/g, '')
85
+ .slice(0, 100);
86
+ if (!sanitizedName) {
87
+ await command.editReply('Invalid project name');
88
+ return;
89
+ }
90
+ const projectDirectory = path.join(getProjectsDir(), sanitizedName);
91
+ await command.editReply(`Project directory already exists: ${projectDirectory}`);
92
+ return;
93
+ }
94
+ const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName, } = result;
95
+ const textChannel = (await guild.channels.fetch(textChannelId));
96
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : '';
97
+ await command.editReply(`āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\n_Starting session..._`);
98
+ const starterMessage = await textChannel.send({
99
+ content: `šŸš€ **New project initialized**\nšŸ“ \`${projectDirectory}\``,
100
+ flags: SILENT_MESSAGE_FLAGS,
101
+ });
102
+ const thread = await starterMessage.startThread({
103
+ name: `Init: ${sanitizedName}`,
104
+ autoArchiveDuration: 1440,
105
+ reason: 'New project session',
106
+ });
107
+ // Add user to thread so it appears in their sidebar
108
+ await thread.members.add(command.user.id);
109
+ const runtime = getOrCreateRuntime({
110
+ threadId: thread.id,
111
+ thread,
112
+ projectDirectory,
113
+ sdkDirectory: projectDirectory,
114
+ channelId: textChannel.id,
115
+ appId,
116
+ });
117
+ await runtime.enqueueIncoming({
118
+ prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
119
+ userId: command.user.id,
120
+ username: command.user.displayName,
121
+ appId,
122
+ mode: 'opencode',
123
+ });
124
+ logger.log(`Created new project ${channelName} at ${projectDirectory}`);
125
+ }
126
+ catch (error) {
127
+ logger.error('[CREATE-NEW-PROJECT] Error:', error);
128
+ await command.editReply(`Failed to create new project: ${error instanceof Error ? error.message : 'Unknown error'}`);
129
+ }
130
+ }
@@ -0,0 +1,63 @@
1
+ // /diff command - Show git diff as a shareable URL.
2
+ import { ChannelType, EmbedBuilder, MessageFlags, } from 'discord.js';
3
+ import path from 'node:path';
4
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ import { uploadGitDiffViaCritique } from '../critique-utils.js';
7
+ const logger = createLogger(LogPrefix.DIFF);
8
+ export async function handleDiffCommand({ command, }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
14
+ });
15
+ return;
16
+ }
17
+ const isThread = [
18
+ ChannelType.PublicThread,
19
+ ChannelType.PrivateThread,
20
+ ChannelType.AnnouncementThread,
21
+ ].includes(channel.type);
22
+ const isTextChannel = channel.type === ChannelType.GuildText;
23
+ if (!isThread && !isTextChannel) {
24
+ await command.reply({
25
+ content: 'This command can only be used in a text channel or thread',
26
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
27
+ });
28
+ return;
29
+ }
30
+ const resolved = await resolveWorkingDirectory({
31
+ channel: channel,
32
+ });
33
+ if (!resolved) {
34
+ await command.reply({
35
+ content: 'Could not determine project directory for this channel',
36
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
37
+ });
38
+ return;
39
+ }
40
+ const { workingDirectory } = resolved;
41
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
42
+ const projectName = path.basename(workingDirectory);
43
+ const title = `${projectName}: Discord /diff`;
44
+ const result = await uploadGitDiffViaCritique({
45
+ title,
46
+ cwd: workingDirectory,
47
+ });
48
+ if (!result) {
49
+ await command.editReply({ content: 'No changes to show' });
50
+ return;
51
+ }
52
+ if (result.error || !result.url) {
53
+ await command.editReply({ content: result.error || 'No changes to show' });
54
+ return;
55
+ }
56
+ const imageUrl = `https://critique.work/og/${result.id}.png`;
57
+ const embed = new EmbedBuilder()
58
+ .setTitle(title)
59
+ .setURL(result.url)
60
+ .setImage(imageUrl);
61
+ await command.editReply({ embeds: [embed] });
62
+ logger.log(`Diff shared: ${result.url}`);
63
+ }