@otto-assistant/otto 0.1.2 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (638) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,645 @@
1
+ // Worktree management command: /new-worktree
2
+ // Uses OpenCode SDK v2 to create worktrees with otto- prefix
3
+ // Creates thread immediately, then worktree in background so user can type
4
+
5
+ import {
6
+ ChannelType,
7
+ REST,
8
+ type TextChannel,
9
+ type ThreadChannel,
10
+ type Message,
11
+ } from 'discord.js'
12
+ import fs from 'node:fs'
13
+ import type { CommandContext } from './types.js'
14
+ import {
15
+ createPendingWorktree,
16
+ setWorktreeReady,
17
+ setWorktreeError,
18
+ getChannelDirectory,
19
+ getThreadWorktree,
20
+ getThreadSession,
21
+ } from '../database.js'
22
+ import {
23
+ SILENT_MESSAGE_FLAGS,
24
+ reactToThread,
25
+ resolveProjectDirectoryFromAutocomplete,
26
+ } from '../discord-utils.js'
27
+ import { createLogger, LogPrefix } from '../logger.js'
28
+ import { notifyError } from '../sentry.js'
29
+ import {
30
+ createWorktreeWithSubmodules,
31
+ execAsync,
32
+ listBranchesByLastCommit,
33
+ validateBranchRef,
34
+ } from '../worktrees.js'
35
+ import {
36
+ buildExternalDirectoryPermissionRules,
37
+ getOpencodeClient,
38
+ initializeOpencodeForDirectory,
39
+ } from '../opencode.js'
40
+ import { WORKTREE_PREFIX } from './merge-worktree.js'
41
+ import type { AutocompleteContext } from './types.js'
42
+ import * as errore from 'errore'
43
+
44
+ const logger = createLogger(LogPrefix.WORKTREE)
45
+ const DEFAULT_WORKTREE_BASE_REF = 'HEAD'
46
+
47
+ async function resolveRequestedWorktreeBaseRef({
48
+ projectDirectory,
49
+ rawBaseBranch,
50
+ }: {
51
+ projectDirectory: string
52
+ rawBaseBranch?: string
53
+ }): Promise<string | Error> {
54
+ if (!rawBaseBranch) {
55
+ // Default to the current local HEAD so worktrees can branch from
56
+ // unpublished commits in the main checkout.
57
+ return DEFAULT_WORKTREE_BASE_REF
58
+ }
59
+
60
+ return validateBranchRef({
61
+ directory: projectDirectory,
62
+ ref: rawBaseBranch,
63
+ })
64
+ }
65
+
66
+ /** Status message shown while a worktree is being created. */
67
+ export function worktreeCreatingMessage(worktreeName: string): string {
68
+ return `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`
69
+ }
70
+
71
+ class WorktreeError extends Error {
72
+ constructor(message: string, options?: ErrorOptions) {
73
+ super(message, options)
74
+ this.name = 'WorktreeError'
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Lowercase, collapse whitespace to dashes, drop non-[a-z0-9-] chars.
80
+ * Does NOT add the `opencode/otto-` prefix — callers do that so they can
81
+ * optionally compress the slug first for auto-derived names.
82
+ */
83
+ export function slugifyWorktreeName(name: string): string {
84
+ return name
85
+ .toLowerCase()
86
+ .trim()
87
+ .replace(/\s+/g, '-')
88
+ .replace(/[^a-z0-9-]/g, '')
89
+ }
90
+
91
+ /**
92
+ * Compress a slug by stripping vowels from each dash-separated word, but
93
+ * keeping the first character so the word stays recognizable.
94
+ * Only applied to slugs longer than 20 chars — short names are left alone.
95
+ *
96
+ * "configurable-sidebar-width-by-component" → "cnfgrbl-sdbr-wdth-by-cmpnnt"
97
+ *
98
+ * Used ONLY for auto-derived worktree names (thread name, prompt slug)
99
+ * so long Discord titles don't produce 80-char folder paths that make
100
+ * the agent lazy and reuse the previous worktree. User-provided names
101
+ * via `--worktree <name>` or `/new-worktree name:` are never compressed.
102
+ */
103
+ export function shortenWorktreeSlug(slug: string): string {
104
+ if (slug.length <= 20) {
105
+ return slug
106
+ }
107
+ const shortened = slug
108
+ .split('-')
109
+ .map((word) => {
110
+ if (!word) {
111
+ return word
112
+ }
113
+ const first = word[0]
114
+ const rest = word.slice(1).replace(/[aeiou]/g, '')
115
+ return first + rest
116
+ })
117
+ .join('-')
118
+ return shortened || slug
119
+ }
120
+
121
+ /**
122
+ * Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/otto- prefix.
123
+ * "My Feature" → "opencode/otto-my-feature"
124
+ * Returns empty string if no valid name can be extracted.
125
+ *
126
+ * This is the "explicit" path used when the user provides a specific name.
127
+ * The slug is NOT compressed — if you ask for `my-long-explicit-branch-name`
128
+ * you get `opencode/otto-my-long-explicit-branch-name` verbatim.
129
+ */
130
+ export function formatWorktreeName(name: string): string {
131
+ const slug = slugifyWorktreeName(name)
132
+ if (!slug) {
133
+ return ''
134
+ }
135
+ return `opencode/otto-${slug}`
136
+ }
137
+
138
+ /**
139
+ * Format an auto-derived worktree name (from a Discord thread title or a
140
+ * prompt). Same as formatWorktreeName but compresses slugs longer than 20
141
+ * chars by stripping vowels so the on-disk folder name stays short.
142
+ */
143
+ export function formatAutoWorktreeName(name: string): string {
144
+ const slug = slugifyWorktreeName(name)
145
+ if (!slug) {
146
+ return ''
147
+ }
148
+ return `opencode/otto-${shortenWorktreeSlug(slug)}`
149
+ }
150
+
151
+ /**
152
+ * Derive worktree name from thread name.
153
+ * Handles existing "⬦ worktree: opencode/otto-name" (or legacy opencode/otto-name) format,
154
+ * or uses thread name directly. Uses formatAutoWorktreeName so long thread titles get vowel-compressed.
155
+ */
156
+ function deriveWorktreeNameFromThread(threadName: string): string {
157
+ // Handle existing "⬦ worktree: opencode/otto-name" (or legacy otto-) format
158
+ const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
159
+ const extractedName = worktreeMatch?.[1]?.trim()
160
+ if (extractedName) {
161
+ // If already has opencode/otto- or legacy opencode/otto- prefix, return as is
162
+ if (extractedName.startsWith('opencode/otto-') || extractedName.startsWith('opencode/otto-')) {
163
+ return extractedName
164
+ }
165
+ return formatAutoWorktreeName(extractedName)
166
+ }
167
+ // Use thread name directly (compressed if > 20 chars)
168
+ return formatAutoWorktreeName(threadName)
169
+ }
170
+
171
+ /**
172
+ * Get project directory from database.
173
+ */
174
+ async function getProjectDirectoryFromChannel(
175
+ channel: TextChannel,
176
+ ): Promise<string | WorktreeError> {
177
+ const channelConfig = await getChannelDirectory(channel.id)
178
+
179
+ if (!channelConfig) {
180
+ return new WorktreeError(
181
+ 'This channel is not configured with a project directory',
182
+ )
183
+ }
184
+
185
+ if (!fs.existsSync(channelConfig.directory)) {
186
+ return new WorktreeError(
187
+ `Directory does not exist: ${channelConfig.directory}`,
188
+ )
189
+ }
190
+
191
+ return channelConfig.directory
192
+ }
193
+
194
+ /**
195
+ * Create worktree and update the status message when done.
196
+ * Handles the full lifecycle: pending DB entry, git creation, DB ready/error,
197
+ * tree emoji reaction, and editing the status message.
198
+ *
199
+ * starterMessage is optional — if omitted, status edits are skipped (creation
200
+ * still proceeds). This keeps worktree creation independent of Discord message
201
+ * delivery, so a transient send failure never silently skips the worktree.
202
+ *
203
+ * Returns the worktree directory on success, or an Error on failure.
204
+ * Never throws — all internal errors are caught and returned as Error values.
205
+ */
206
+ export async function createWorktreeInBackground({
207
+ thread,
208
+ starterMessage,
209
+ worktreeName,
210
+ projectDirectory,
211
+ baseBranch,
212
+ rest,
213
+ }: {
214
+ thread: ThreadChannel
215
+ starterMessage?: Message
216
+ worktreeName: string
217
+ projectDirectory: string
218
+ baseBranch?: string
219
+ rest: REST
220
+ }): Promise<string | Error> {
221
+ return errore.tryAsync({
222
+ try: async () => {
223
+ logger.log(
224
+ `Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`,
225
+ )
226
+
227
+ await createPendingWorktree({
228
+ threadId: thread.id,
229
+ worktreeName,
230
+ projectDirectory,
231
+ })
232
+
233
+ // Serialize status message edits so onProgress can't overwrite the
234
+ // final success/error edit even if Discord's API is slow.
235
+ let editChain: Promise<void> = Promise.resolve()
236
+ const editStatus = (content: string) => {
237
+ editChain = editChain
238
+ .then(async () => {
239
+ await starterMessage?.edit(content)
240
+ })
241
+ .catch(() => {})
242
+ }
243
+
244
+ const worktreeResult = await createWorktreeWithSubmodules({
245
+ directory: projectDirectory,
246
+ name: worktreeName,
247
+ baseBranch,
248
+ onProgress: (phase) => {
249
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`)
250
+ },
251
+ })
252
+
253
+ if (worktreeResult instanceof Error) {
254
+ const errorMsg = worktreeResult.message
255
+ logger.error('[WORKTREE] Creation failed:', worktreeResult)
256
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
257
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`)
258
+ await editChain
259
+ return worktreeResult
260
+ }
261
+
262
+ // Success - update database and edit starter message
263
+ await setWorktreeReady({
264
+ threadId: thread.id,
265
+ worktreeDirectory: worktreeResult.directory,
266
+ })
267
+
268
+ await denyPreviousCheckoutForExistingSession({
269
+ threadId: thread.id,
270
+ projectDirectory,
271
+ })
272
+
273
+ // React with tree emoji to mark as worktree thread
274
+ await reactToThread({
275
+ rest,
276
+ threadId: thread.id,
277
+ channelId: thread.parentId || undefined,
278
+ emoji: '🌳',
279
+ })
280
+
281
+ editStatus(
282
+ `🌳 **Worktree: ${worktreeName}**\n` +
283
+ `📁 \`${worktreeResult.directory}\`\n` +
284
+ `🌿 Branch: \`${worktreeResult.branch}\``,
285
+ )
286
+ await editChain
287
+
288
+ return worktreeResult.directory
289
+ },
290
+ catch: (e) => {
291
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e)
292
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
293
+ },
294
+ })
295
+ }
296
+
297
+ async function denyPreviousCheckoutForExistingSession({
298
+ threadId,
299
+ projectDirectory,
300
+ }: {
301
+ threadId: string
302
+ projectDirectory: string
303
+ }): Promise<void> {
304
+ const sessionId = await getThreadSession(threadId)
305
+ if (!sessionId) {
306
+ return
307
+ }
308
+
309
+ const initializeResult = await initializeOpencodeForDirectory(projectDirectory)
310
+ if (initializeResult instanceof Error) {
311
+ logger.warn(
312
+ `[WORKTREE] Failed to initialize OpenCode before denying previous checkout for thread ${threadId}: ${initializeResult.message}`,
313
+ )
314
+ return
315
+ }
316
+
317
+ const client = getOpencodeClient(projectDirectory)
318
+ if (!client) {
319
+ logger.warn(
320
+ `[WORKTREE] Missing OpenCode client for previous checkout deny update in thread ${threadId}`,
321
+ )
322
+ return
323
+ }
324
+
325
+ const updateResult = await errore.tryAsync({
326
+ try: async () => {
327
+ // SDK types don't include 'permission' yet — upstream added this API
328
+ // for session permission updates (worktree isolation). Cast to bypass.
329
+ await client.session.update({
330
+ sessionID: sessionId,
331
+ permission: buildExternalDirectoryPermissionRules({
332
+ resolvedPattern: projectDirectory.replaceAll('\\', '/'),
333
+ action: 'deny',
334
+ }),
335
+ } as Parameters<typeof client.session.update>[0])
336
+ },
337
+ catch: (e) =>
338
+ new Error('Failed to deny previous checkout for existing session', {
339
+ cause: e,
340
+ }),
341
+ })
342
+ if (updateResult instanceof Error) {
343
+ logger.warn(
344
+ `[WORKTREE] Failed to deny previous checkout for existing session in thread ${threadId}: ${updateResult.message}`,
345
+ )
346
+ return
347
+ }
348
+
349
+ logger.log(
350
+ `[WORKTREE] Denied previous checkout for existing session ${sessionId} in thread ${threadId}`,
351
+ )
352
+ }
353
+
354
+ async function findExistingWorktreePath({
355
+ projectDirectory,
356
+ worktreeName,
357
+ }: {
358
+ projectDirectory: string
359
+ worktreeName: string
360
+ }): Promise<string | undefined | Error> {
361
+ const listResult = await errore.tryAsync({
362
+ try: () =>
363
+ execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
364
+ catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
365
+ })
366
+ if (errore.isError(listResult)) {
367
+ return listResult
368
+ }
369
+
370
+ const lines = listResult.stdout.split('\n')
371
+ let currentPath = ''
372
+ const branchRef = `refs/heads/${worktreeName}`
373
+
374
+ for (const line of lines) {
375
+ if (line.startsWith('worktree ')) {
376
+ currentPath = line.slice('worktree '.length)
377
+ continue
378
+ }
379
+ if (
380
+ line.startsWith('branch ') &&
381
+ line.slice('branch '.length) === branchRef
382
+ ) {
383
+ return currentPath || undefined
384
+ }
385
+ }
386
+
387
+ return undefined
388
+ }
389
+
390
+ export async function handleNewWorktreeCommand({
391
+ command,
392
+ }: CommandContext): Promise<void> {
393
+ await command.deferReply()
394
+
395
+ const channel = command.channel
396
+ if (!channel) {
397
+ await command.editReply('Cannot determine channel')
398
+ return
399
+ }
400
+
401
+ // Handle command in existing thread - attach worktree to this thread
402
+ if (
403
+ channel.type === ChannelType.PublicThread ||
404
+ channel.type === ChannelType.PrivateThread
405
+ ) {
406
+ await handleWorktreeInThread({
407
+ command,
408
+ thread: channel,
409
+ })
410
+ return
411
+ }
412
+
413
+ // Handle command in text channel - create new thread with worktree (existing behavior)
414
+ if (channel.type !== ChannelType.GuildText) {
415
+ await command.editReply(
416
+ 'This command can only be used in text channels or threads',
417
+ )
418
+ return
419
+ }
420
+
421
+ const rawName = command.options.getString('name')
422
+ const rawBaseBranch = command.options.getString('base-branch') || undefined
423
+ if (!rawName) {
424
+ await command.editReply(
425
+ 'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
426
+ )
427
+ return
428
+ }
429
+
430
+ const worktreeName = formatWorktreeName(rawName)
431
+ if (!worktreeName) {
432
+ await command.editReply(
433
+ 'Invalid worktree name. Please use letters, numbers, and spaces.',
434
+ )
435
+ return
436
+ }
437
+
438
+ const textChannel = channel
439
+
440
+ const projectDirectory = await getProjectDirectoryFromChannel(
441
+ textChannel,
442
+ )
443
+ if (errore.isError(projectDirectory)) {
444
+ await command.editReply(projectDirectory.message)
445
+ return
446
+ }
447
+
448
+ const baseBranch = await resolveRequestedWorktreeBaseRef({
449
+ projectDirectory,
450
+ rawBaseBranch,
451
+ })
452
+ if (baseBranch instanceof Error) {
453
+ await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``)
454
+ return
455
+ }
456
+
457
+ const existingWorktree = await findExistingWorktreePath({
458
+ projectDirectory,
459
+ worktreeName,
460
+ })
461
+ if (errore.isError(existingWorktree)) {
462
+ await command.editReply(existingWorktree.message)
463
+ return
464
+ }
465
+ if (existingWorktree) {
466
+ await command.editReply(
467
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``,
468
+ )
469
+ return
470
+ }
471
+
472
+ // Create thread immediately so user can start typing
473
+ const result = await errore.tryAsync({
474
+ try: async () => {
475
+ const starterMessage = await textChannel.send({
476
+ content: worktreeCreatingMessage(worktreeName),
477
+ flags: SILENT_MESSAGE_FLAGS,
478
+ })
479
+
480
+ const thread = await starterMessage.startThread({
481
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
482
+ autoArchiveDuration: 1440,
483
+ reason: 'Worktree session',
484
+ })
485
+
486
+ // Add user to thread so it appears in their sidebar
487
+ await thread.members.add(command.user.id)
488
+
489
+ return { thread, starterMessage }
490
+ },
491
+ catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
492
+ })
493
+
494
+ if (errore.isError(result)) {
495
+ logger.error('[NEW-WORKTREE] Error:', result.cause)
496
+ await command.editReply(result.message)
497
+ return
498
+ }
499
+
500
+ const { thread, starterMessage } = result
501
+
502
+ await command.editReply(`Creating worktree in ${thread.toString()}`)
503
+
504
+ // Create worktree in background (don't await)
505
+ createWorktreeInBackground({
506
+ thread,
507
+ starterMessage,
508
+ worktreeName,
509
+ projectDirectory,
510
+ baseBranch,
511
+ rest: command.client.rest,
512
+ }).catch((e) => {
513
+ logger.error('[NEW-WORKTREE] Background error:', e)
514
+ void notifyError(e, 'Background worktree creation failed')
515
+ })
516
+ }
517
+
518
+ /**
519
+ * Handle /new-worktree when called inside an existing thread.
520
+ * Attaches a worktree to the current thread, using thread name if no name provided.
521
+ */
522
+ async function handleWorktreeInThread({
523
+ command,
524
+ thread,
525
+ }: {
526
+ command: CommandContext['command']
527
+ thread: ThreadChannel
528
+ }): Promise<void> {
529
+ // Error if thread already has a worktree
530
+ if (await getThreadWorktree(thread.id)) {
531
+ await command.editReply('This thread already has a worktree attached.')
532
+ return
533
+ }
534
+
535
+ // Get worktree name from parameter or derive from thread name
536
+ const rawName = command.options.getString('name')
537
+ const rawBaseBranch = command.options.getString('base-branch') || undefined
538
+ const worktreeName = rawName
539
+ ? formatWorktreeName(rawName)
540
+ : deriveWorktreeNameFromThread(thread.name)
541
+
542
+ if (!worktreeName) {
543
+ await command.editReply(
544
+ 'Invalid worktree name. Please provide a name or rename the thread.',
545
+ )
546
+ return
547
+ }
548
+
549
+ // Get parent channel for project directory
550
+ const parent = thread.parent
551
+ if (!parent || parent.type !== ChannelType.GuildText) {
552
+ await command.editReply('Cannot determine parent channel')
553
+ return
554
+ }
555
+
556
+ const projectDirectory = await getProjectDirectoryFromChannel(
557
+ parent,
558
+ )
559
+ if (errore.isError(projectDirectory)) {
560
+ await command.editReply(projectDirectory.message)
561
+ return
562
+ }
563
+
564
+ const baseBranch = await resolveRequestedWorktreeBaseRef({
565
+ projectDirectory,
566
+ rawBaseBranch,
567
+ })
568
+ if (baseBranch instanceof Error) {
569
+ await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``)
570
+ return
571
+ }
572
+
573
+ const existingWorktreePath = await findExistingWorktreePath({
574
+ projectDirectory,
575
+ worktreeName,
576
+ })
577
+ if (errore.isError(existingWorktreePath)) {
578
+ await command.editReply(existingWorktreePath.message)
579
+ return
580
+ }
581
+ if (existingWorktreePath) {
582
+ await command.editReply(
583
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
584
+ )
585
+ return
586
+ }
587
+
588
+ // Send status message in thread
589
+ const statusMessage = await thread.send({
590
+ content: worktreeCreatingMessage(worktreeName),
591
+ flags: SILENT_MESSAGE_FLAGS,
592
+ })
593
+
594
+ await command.editReply(
595
+ `Creating worktree \`${worktreeName}\` for this thread...`,
596
+ )
597
+
598
+ createWorktreeInBackground({
599
+ thread,
600
+ starterMessage: statusMessage,
601
+ worktreeName,
602
+ projectDirectory,
603
+ baseBranch,
604
+ rest: command.client.rest,
605
+ }).catch((e) => {
606
+ logger.error('[NEW-WORKTREE] Background error:', e)
607
+ void notifyError(e, 'Background worktree creation failed (in-thread)')
608
+ })
609
+ }
610
+
611
+ /**
612
+ * Autocomplete handler for /new-worktree base-branch option.
613
+ * Lists local + remote branches sorted by most recent commit date.
614
+ */
615
+ export async function handleNewWorktreeAutocomplete({
616
+ interaction,
617
+ }: AutocompleteContext): Promise<void> {
618
+ try {
619
+ const focusedValue = interaction.options.getFocused()
620
+
621
+ // interaction.channel can be null when the channel isn't cached
622
+ // (common with gateway-proxy). Use channelId which is always available
623
+ // from the raw interaction payload.
624
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
625
+
626
+ if (!projectDirectory) {
627
+ await interaction.respond([])
628
+ return
629
+ }
630
+
631
+ const branches = await listBranchesByLastCommit({
632
+ directory: projectDirectory,
633
+ query: focusedValue,
634
+ })
635
+
636
+ await interaction.respond(
637
+ branches.map((name) => {
638
+ return { name, value: name }
639
+ }),
640
+ )
641
+ } catch (e) {
642
+ logger.error('[NEW-WORKTREE] Autocomplete error:', e)
643
+ await interaction.respond([]).catch(() => {})
644
+ }
645
+ }