@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,70 @@
1
+ // /toggle-worktrees command.
2
+ // Allows per-channel opt-in for automatic worktree creation,
3
+ // as an alternative to the global --use-worktrees CLI flag.
4
+
5
+ import {
6
+ ChatInputCommandInteraction,
7
+ MessageFlags,
8
+ ChannelType,
9
+ type TextChannel,
10
+ } from 'discord.js'
11
+ import {
12
+ getChannelWorktreesEnabled,
13
+ setChannelWorktreesEnabled,
14
+ } from '../database.js'
15
+ import { getOttoMetadata } from '../discord-utils.js'
16
+ import { createLogger, LogPrefix } from '../logger.js'
17
+
18
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
19
+
20
+ /**
21
+ * Handle the /toggle-worktrees slash command.
22
+ * Toggles automatic worktree creation for new sessions in this channel.
23
+ */
24
+ export async function handleToggleWorktreesCommand({
25
+ command,
26
+ }: {
27
+ command: ChatInputCommandInteraction
28
+ appId: string
29
+ }): Promise<void> {
30
+ worktreeSettingsLogger.log('[TOGGLE_WORKTREES] Command called')
31
+
32
+ const channel = command.channel
33
+
34
+ if (!channel || channel.type !== ChannelType.GuildText) {
35
+ await command.reply({
36
+ content: 'This command can only be used in text channels (not threads).',
37
+ flags: MessageFlags.Ephemeral,
38
+ })
39
+ return
40
+ }
41
+
42
+ const textChannel = channel as TextChannel
43
+ const metadata = await getOttoMetadata(textChannel)
44
+
45
+ if (!metadata.projectDirectory) {
46
+ await command.reply({
47
+ content:
48
+ 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
49
+ flags: MessageFlags.Ephemeral,
50
+ })
51
+ return
52
+ }
53
+
54
+ const wasEnabled = await getChannelWorktreesEnabled(textChannel.id)
55
+ const nextEnabled = !wasEnabled
56
+ await setChannelWorktreesEnabled(textChannel.id, nextEnabled)
57
+
58
+ const nextLabel = nextEnabled ? 'enabled' : 'disabled'
59
+
60
+ worktreeSettingsLogger.log(
61
+ `[TOGGLE_WORKTREES] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`,
62
+ )
63
+
64
+ await command.reply({
65
+ content: nextEnabled
66
+ ? `Worktrees **enabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will now automatically create git worktrees.\n\nNew setting for **#${textChannel.name}**: **enabled**.`
67
+ : `Worktrees **disabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will use the main project directory.\n\nNew setting for **#${textChannel.name}**: **disabled**.`,
68
+ flags: MessageFlags.Ephemeral,
69
+ })
70
+ }
@@ -0,0 +1,645 @@
1
+ // /worktrees command — list all git worktrees for the current channel's project.
2
+ // Uses `git worktree list --porcelain` as source of truth, enriched with
3
+ // DB metadata (thread link, created_at) when available. Shows otto-created,
4
+ // opencode-created, and manually created worktrees in a single table.
5
+ // Renders a markdown table that the CV2 pipeline auto-formats for Discord,
6
+ // including HTML-backed action buttons for deletable worktrees.
7
+
8
+ import {
9
+ ButtonInteraction,
10
+ ChatInputCommandInteraction,
11
+ ChannelType,
12
+ ComponentType,
13
+ MessageFlags,
14
+ type TextChannel,
15
+ type ThreadChannel,
16
+ type APIMessageTopLevelComponent,
17
+ type APITextDisplayComponent,
18
+ type InteractionEditReplyOptions,
19
+ } from 'discord.js'
20
+ import {
21
+ deleteThreadWorktree,
22
+ type ThreadWorktree,
23
+ } from '../database.js'
24
+ import { getPrisma } from '../db.js'
25
+ import { splitTablesFromMarkdown } from '../format-tables.js'
26
+ import {
27
+ buildHtmlActionCustomId,
28
+ cancelHtmlActionsForOwner,
29
+ registerHtmlAction,
30
+ } from '../html-actions.js'
31
+ import * as errore from 'errore'
32
+ import crypto from 'node:crypto'
33
+ import { GitCommandError } from '../errors.js'
34
+ import { resolveWorkingDirectory } from '../discord-utils.js'
35
+ import {
36
+ deleteWorktree,
37
+ git,
38
+ getDefaultBranch,
39
+ listGitWorktrees,
40
+ type GitWorktree,
41
+ } from '../worktrees.js'
42
+ import path from 'node:path'
43
+
44
+ // Extracts the git stderr from a deleteWorktree error via errore.findCause.
45
+ // Chain: Error { cause: GitCommandError { cause: CommandError { stderr } } }.
46
+ export function extractGitStderr(error: Error): string | undefined {
47
+ const gitErr = errore.findCause(error, GitCommandError)
48
+ const stderr = (gitErr?.cause as { stderr?: string } | undefined)?.stderr?.trim()
49
+ if (stderr && stderr.length > 0) {
50
+ return stderr
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ export function formatTimeAgo(date: Date): string {
56
+ const diffMs = Date.now() - date.getTime()
57
+ if (diffMs < 0) {
58
+ return 'just now'
59
+ }
60
+ const totalSeconds = Math.floor(diffMs / 1000)
61
+ if (totalSeconds < 60) {
62
+ return `${totalSeconds}s ago`
63
+ }
64
+ const totalMinutes = Math.floor(totalSeconds / 60)
65
+ if (totalMinutes < 60) {
66
+ return `${totalMinutes}m ago`
67
+ }
68
+ const hours = Math.floor(totalMinutes / 60)
69
+ const minutes = totalMinutes % 60
70
+ if (hours < 24) {
71
+ return minutes > 0 ? `${hours}h ${minutes}m ago` : `${hours}h ago`
72
+ }
73
+ const days = Math.floor(hours / 24)
74
+ const remainingHours = hours % 24
75
+ return remainingHours > 0 ? `${days}d ${remainingHours}h ago` : `${days}d ago`
76
+ }
77
+
78
+ // Stable button ID derived from directory path via sha1 hash.
79
+ // Avoids collisions that truncated path suffixes can cause.
80
+ function worktreeButtonKey(directory: string): string {
81
+ return crypto.createHash('sha1').update(directory).digest('hex').slice(0, 12)
82
+ }
83
+
84
+ // Unified worktree row that merges git data with optional DB metadata.
85
+ type WorktreeRow = {
86
+ directory: string
87
+ branch: string | null
88
+ name: string
89
+ threadId: string | null
90
+ guildId: string | null
91
+ createdAt: Date | null
92
+ source: 'otto' | 'opencode' | 'manual'
93
+ // DB-only worktrees (pending/error) won't appear in git list
94
+ dbStatus: 'ready' | 'pending' | 'error'
95
+ // Git-level flags that block deletion
96
+ locked: boolean
97
+ prunable: boolean
98
+ }
99
+
100
+ type WorktreeGitStatus = {
101
+ dirty: boolean
102
+ aheadCount: number
103
+ }
104
+
105
+ type WorktreesReplyTarget = {
106
+ guildId: string
107
+ userId: string
108
+ channelId: string
109
+ projectDirectory: string
110
+ notice?: string
111
+ editReply: (
112
+ options: string | InteractionEditReplyOptions,
113
+ ) => Promise<unknown>
114
+ }
115
+
116
+ // 5s timeout per git call — prevents hangs from deleted dirs, git locks, slow disks.
117
+ // Returns null on timeout/error so the table shows "unknown" for that worktree.
118
+ const GIT_CMD_TIMEOUT = 5_000
119
+ const GLOBAL_TIMEOUT = 10_000
120
+
121
+ // Detect worktree source from branch name and directory path.
122
+ // opencode/otto-* and legacy opencode/otto-* branches → otto,
123
+ // opencode worktree paths → opencode, else manual.
124
+ function detectWorktreeSource({
125
+ branch,
126
+ directory,
127
+ }: {
128
+ branch: string | null
129
+ directory: string
130
+ }): 'otto' | 'opencode' | 'manual' {
131
+ if (branch?.startsWith('opencode/otto-') || branch?.startsWith('opencode/otto-')) {
132
+ return 'otto'
133
+ }
134
+ // opencode stores worktrees under ~/.local/share/opencode/worktree/
135
+ if (directory.includes('/opencode/worktree/')) {
136
+ return 'opencode'
137
+ }
138
+ return 'manual'
139
+ }
140
+
141
+ // Checks dirty state and commits ahead of default branch in parallel.
142
+ // Returns null when the directory is missing / git commands fail / timeout.
143
+ async function getWorktreeGitStatus({
144
+ directory,
145
+ defaultBranch,
146
+ }: {
147
+ directory: string
148
+ defaultBranch: string
149
+ }): Promise<WorktreeGitStatus | null> {
150
+ try {
151
+ // Use raw git calls so errors/timeouts are visible — isDirty() swallows
152
+ // errors and returns false, which would render "merged" instead of "unknown".
153
+ const [statusResult, aheadResult] = await Promise.all([
154
+ git(directory, 'status --porcelain', { timeout: GIT_CMD_TIMEOUT }),
155
+ git(directory, `rev-list --count "${defaultBranch}..HEAD"`, {
156
+ timeout: GIT_CMD_TIMEOUT,
157
+ }),
158
+ ])
159
+ if (statusResult instanceof Error || aheadResult instanceof Error) {
160
+ return null
161
+ }
162
+ const aheadCount = parseInt(aheadResult, 10)
163
+ if (!Number.isFinite(aheadCount)) {
164
+ return null
165
+ }
166
+ return { dirty: statusResult.length > 0, aheadCount }
167
+ } catch {
168
+ return null
169
+ }
170
+ }
171
+
172
+ function buildWorktreeTable({
173
+ rows,
174
+ gitStatuses,
175
+ guildId,
176
+ }: {
177
+ rows: WorktreeRow[]
178
+ gitStatuses: (WorktreeGitStatus | null)[]
179
+ guildId: string
180
+ }): string {
181
+ const header = '| Source | Name | Status | Created | Folder | Action |'
182
+ const separator = '|---|---|---|---|---|---|'
183
+ const tableRows = rows.map((row, i) => {
184
+ const sourceCell = (() => {
185
+ if (row.threadId && row.guildId) {
186
+ const threadLink = `[${row.source}](https://discord.com/channels/${row.guildId}/${row.threadId})`
187
+ return threadLink
188
+ }
189
+ return row.source
190
+ })()
191
+ const name = row.name
192
+ const gs = gitStatuses[i] ?? null
193
+ const status = (() => {
194
+ if (row.dbStatus !== 'ready') {
195
+ return row.dbStatus
196
+ }
197
+ if (row.locked) {
198
+ return 'locked'
199
+ }
200
+ if (row.prunable) {
201
+ return 'prunable'
202
+ }
203
+ if (!gs) {
204
+ return 'unknown'
205
+ }
206
+ const parts: string[] = []
207
+ if (gs.dirty) {
208
+ parts.push('dirty')
209
+ }
210
+ if (gs.aheadCount > 0) {
211
+ parts.push(`${gs.aheadCount} ahead`)
212
+ } else {
213
+ parts.push('merged')
214
+ }
215
+ return parts.join(', ')
216
+ })()
217
+ const created = row.createdAt ? formatTimeAgo(row.createdAt) : '-'
218
+ const folder = row.directory
219
+ const action = buildActionCell({ row, gitStatus: gs })
220
+ return `| ${sourceCell} | ${name} | ${status} | ${created} | ${folder} | ${action} |`
221
+ })
222
+ return [header, separator, ...tableRows].join('\n')
223
+ }
224
+
225
+ function buildActionCell({
226
+ row,
227
+ gitStatus,
228
+ }: {
229
+ row: WorktreeRow
230
+ gitStatus: WorktreeGitStatus | null
231
+ }): string {
232
+ if (!canDeleteWorktree({ row, gitStatus })) {
233
+ return '-'
234
+ }
235
+ return buildDeleteButtonHtml({
236
+ buttonId: `del-wt-${worktreeButtonKey(row.directory)}`,
237
+ })
238
+ }
239
+
240
+ function buildDeleteButtonHtml({
241
+ buttonId,
242
+ }: {
243
+ buttonId: string
244
+ }): string {
245
+ return `<button id="${buttonId}" variant="secondary">Delete</button>`
246
+ }
247
+
248
+ function canDeleteWorktree({
249
+ row,
250
+ gitStatus,
251
+ }: {
252
+ row: WorktreeRow
253
+ gitStatus: WorktreeGitStatus | null
254
+ }): boolean {
255
+ if (row.dbStatus !== 'ready') {
256
+ return false
257
+ }
258
+ if (row.locked) {
259
+ return false
260
+ }
261
+ if (!gitStatus) {
262
+ return false
263
+ }
264
+ if (gitStatus.dirty) {
265
+ return false
266
+ }
267
+ return gitStatus.aheadCount === 0
268
+ }
269
+
270
+ // Resolves git statuses for all worktrees within a single global deadline.
271
+ async function resolveGitStatuses({
272
+ rows,
273
+ projectDirectory,
274
+ timeout,
275
+ }: {
276
+ rows: WorktreeRow[]
277
+ projectDirectory: string
278
+ timeout: number
279
+ }): Promise<(WorktreeGitStatus | null)[]> {
280
+ const nullFallback = rows.map(() => null)
281
+
282
+ let timer: ReturnType<typeof setTimeout> | undefined
283
+ const deadline = new Promise<(WorktreeGitStatus | null)[]>((resolve) => {
284
+ timer = setTimeout(() => {
285
+ resolve(nullFallback)
286
+ }, timeout)
287
+ })
288
+
289
+ const work = (async () => {
290
+ const defaultBranch = await getDefaultBranch(projectDirectory, {
291
+ timeout: GIT_CMD_TIMEOUT,
292
+ })
293
+
294
+ return Promise.all(
295
+ rows.map((row) => {
296
+ if (row.dbStatus !== 'ready' || row.locked || row.prunable) {
297
+ return null
298
+ }
299
+ return getWorktreeGitStatus({ directory: row.directory, defaultBranch })
300
+ }),
301
+ )
302
+ })()
303
+
304
+ try {
305
+ return await Promise.race([work, deadline])
306
+ } finally {
307
+ clearTimeout(timer)
308
+ }
309
+ }
310
+
311
+ // Merge git worktrees with DB metadata into unified WorktreeRows.
312
+ // Git is the source of truth for what exists on disk. DB rows that aren't
313
+ // in the git list (pending/error) are appended at the end.
314
+ async function buildWorktreeRows({
315
+ projectDirectory,
316
+ gitWorktrees,
317
+ }: {
318
+ projectDirectory: string
319
+ gitWorktrees: GitWorktree[]
320
+ }): Promise<WorktreeRow[]> {
321
+ const prisma = await getPrisma()
322
+ const dbWorktrees = await prisma.thread_worktrees.findMany({
323
+ where: { project_directory: projectDirectory },
324
+ })
325
+
326
+ // Index DB worktrees by directory for fast lookup
327
+ const dbByDirectory = new Map<string, ThreadWorktree>()
328
+ for (const dbWt of dbWorktrees) {
329
+ if (dbWt.worktree_directory) {
330
+ dbByDirectory.set(dbWt.worktree_directory, dbWt)
331
+ }
332
+ }
333
+
334
+ // Track which DB rows got matched so we can append unmatched ones
335
+ const matchedDbThreadIds = new Set<string>()
336
+
337
+ // Build rows from git worktrees (the source of truth for on-disk state).
338
+ // Use real DB status when available — a git-visible worktree whose DB row
339
+ // is still 'pending' means setup hasn't finished (race window).
340
+ const gitRows: WorktreeRow[] = gitWorktrees.map((gw) => {
341
+ const dbMatch = dbByDirectory.get(gw.directory)
342
+ if (dbMatch) {
343
+ matchedDbThreadIds.add(dbMatch.thread_id)
344
+ }
345
+ const source = detectWorktreeSource({
346
+ branch: gw.branch,
347
+ directory: gw.directory,
348
+ })
349
+ const name = gw.branch ?? path.basename(gw.directory)
350
+ const dbStatus: 'ready' | 'pending' | 'error' = (() => {
351
+ if (!dbMatch) {
352
+ return 'ready'
353
+ }
354
+ if (dbMatch.status === 'error') {
355
+ return 'error'
356
+ }
357
+ if (dbMatch.status === 'pending') {
358
+ return 'pending'
359
+ }
360
+ return 'ready'
361
+ })()
362
+ return {
363
+ directory: gw.directory,
364
+ branch: gw.branch,
365
+ name,
366
+ threadId: dbMatch?.thread_id ?? null,
367
+ guildId: null, // filled in by caller
368
+ createdAt: dbMatch?.created_at ?? null,
369
+ source,
370
+ dbStatus,
371
+ locked: gw.locked,
372
+ prunable: gw.prunable,
373
+ }
374
+ })
375
+
376
+ // Append DB-only worktrees (pending/error/stale — not visible to git).
377
+ // Preserve actual DB status so stale 'ready' rows show as 'ready' (missing).
378
+ const dbOnlyRows: WorktreeRow[] = dbWorktrees
379
+ .filter((dbWt) => {
380
+ return !matchedDbThreadIds.has(dbWt.thread_id)
381
+ })
382
+ .map((dbWt) => {
383
+ const dbStatus: 'ready' | 'pending' | 'error' = (() => {
384
+ if (dbWt.status === 'error') {
385
+ return 'error'
386
+ }
387
+ if (dbWt.status === 'pending') {
388
+ return 'pending'
389
+ }
390
+ return 'ready'
391
+ })()
392
+ return {
393
+ directory: dbWt.worktree_directory ?? dbWt.project_directory,
394
+ branch: null,
395
+ name: dbWt.worktree_name,
396
+ threadId: dbWt.thread_id,
397
+ guildId: null,
398
+ createdAt: dbWt.created_at,
399
+ source: 'otto' as const,
400
+ dbStatus,
401
+ locked: false,
402
+ prunable: false,
403
+ }
404
+ })
405
+
406
+ return [...gitRows, ...dbOnlyRows]
407
+ }
408
+
409
+ function getWorktreesActionOwnerKey({
410
+ userId,
411
+ channelId,
412
+ }: {
413
+ userId: string
414
+ channelId: string
415
+ }): string {
416
+ return `worktrees:${userId}:${channelId}`
417
+ }
418
+
419
+ function isProjectChannel(
420
+ channel: ChatInputCommandInteraction['channel'] | ButtonInteraction['channel'],
421
+ ): boolean {
422
+ if (!channel) {
423
+ return false
424
+ }
425
+
426
+ return [
427
+ ChannelType.GuildText,
428
+ ChannelType.PublicThread,
429
+ ChannelType.PrivateThread,
430
+ ChannelType.AnnouncementThread,
431
+ ].includes(channel.type)
432
+ }
433
+
434
+ async function renderWorktreesReply({
435
+ guildId,
436
+ userId,
437
+ channelId,
438
+ projectDirectory,
439
+ notice,
440
+ editReply,
441
+ }: WorktreesReplyTarget): Promise<void> {
442
+ const ownerKey = getWorktreesActionOwnerKey({ userId, channelId })
443
+ cancelHtmlActionsForOwner(ownerKey)
444
+
445
+ const gitWorktrees = await listGitWorktrees({
446
+ projectDirectory,
447
+ timeout: GIT_CMD_TIMEOUT,
448
+ })
449
+ // On git failure, fall back to empty list (DB-only rows still shown)
450
+ const gitList = gitWorktrees instanceof Error ? [] : gitWorktrees
451
+
452
+ const rows = await buildWorktreeRows({ projectDirectory, gitWorktrees: gitList })
453
+ // Inject guildId into all rows for thread link rendering
454
+ for (const row of rows) {
455
+ row.guildId = guildId
456
+ }
457
+
458
+ if (rows.length === 0) {
459
+ const message = notice
460
+ ? `${notice}\n\nNo worktrees found.`
461
+ : 'No worktrees found.'
462
+ const textDisplay: APITextDisplayComponent = {
463
+ type: ComponentType.TextDisplay,
464
+ content: message,
465
+ }
466
+ await editReply({
467
+ components: [textDisplay],
468
+ flags: MessageFlags.IsComponentsV2,
469
+ })
470
+ return
471
+ }
472
+
473
+ const gitStatuses = await resolveGitStatuses({
474
+ rows,
475
+ projectDirectory,
476
+ timeout: GLOBAL_TIMEOUT,
477
+ })
478
+
479
+ // Map deletable worktrees by button ID for the HTML action resolver.
480
+ // Uses the same worktreeButtonKey() as buildActionCell.
481
+ const deletableRowsByButtonId = new Map<string, WorktreeRow>()
482
+ rows.forEach((row, index) => {
483
+ const gitStatus = gitStatuses[index] ?? null
484
+ if (!canDeleteWorktree({ row, gitStatus })) {
485
+ return
486
+ }
487
+ deletableRowsByButtonId.set(`del-wt-${worktreeButtonKey(row.directory)}`, row)
488
+ })
489
+
490
+ const tableMarkdown = buildWorktreeTable({
491
+ rows,
492
+ gitStatuses,
493
+ guildId,
494
+ })
495
+ const markdown = notice ? `${notice}\n\n${tableMarkdown}` : tableMarkdown
496
+ const segments = splitTablesFromMarkdown(markdown, {
497
+ resolveButtonCustomId: ({ button }) => {
498
+ const row = deletableRowsByButtonId.get(button.id)
499
+ if (!row) {
500
+ return new Error(`No worktree registered for button ${button.id}`)
501
+ }
502
+
503
+ const actionId = registerHtmlAction({
504
+ ownerKey,
505
+ threadId: row.threadId ?? row.directory,
506
+ run: async ({ interaction }) => {
507
+ await handleDeleteWorktreeAction({
508
+ interaction,
509
+ row,
510
+ projectDirectory,
511
+ })
512
+ },
513
+ })
514
+ return buildHtmlActionCustomId(actionId)
515
+ },
516
+ })
517
+
518
+ const components: APIMessageTopLevelComponent[] = segments.flatMap((segment) => {
519
+ if (segment.type === 'components') {
520
+ return segment.components
521
+ }
522
+
523
+ const textDisplay: APITextDisplayComponent = {
524
+ type: ComponentType.TextDisplay,
525
+ content: segment.text,
526
+ }
527
+ return [textDisplay]
528
+ })
529
+
530
+ await editReply({
531
+ components,
532
+ flags: MessageFlags.IsComponentsV2,
533
+ })
534
+ }
535
+
536
+ async function handleDeleteWorktreeAction({
537
+ interaction,
538
+ row,
539
+ projectDirectory,
540
+ }: {
541
+ interaction: ButtonInteraction
542
+ row: WorktreeRow
543
+ projectDirectory: string
544
+ }): Promise<void> {
545
+ const guildId = interaction.guildId
546
+ if (!guildId) {
547
+ await interaction.editReply({
548
+ components: [
549
+ {
550
+ type: ComponentType.TextDisplay,
551
+ content: 'This action can only be used in a server.',
552
+ },
553
+ ],
554
+ flags: MessageFlags.IsComponentsV2,
555
+ })
556
+ return
557
+ }
558
+
559
+ // Pass branch name for branch cleanup. Empty string for detached HEAD
560
+ // worktrees so deleteWorktree skips the `git branch -d` step.
561
+ const displayName = row.branch ?? row.name
562
+ const deleteResult = await deleteWorktree({
563
+ projectDirectory,
564
+ worktreeDirectory: row.directory,
565
+ worktreeName: row.branch ?? '',
566
+ })
567
+ if (deleteResult instanceof Error) {
568
+ const gitStderr = extractGitStderr(deleteResult)
569
+ const detail = gitStderr
570
+ ? `\`\`\`\n${gitStderr}\n\`\`\``
571
+ : deleteResult.message
572
+ await interaction
573
+ .followUp({
574
+ content: `Failed to delete \`${displayName}\`\n${detail}`,
575
+ flags: MessageFlags.Ephemeral,
576
+ })
577
+ .catch(() => {
578
+ return undefined
579
+ })
580
+ return
581
+ }
582
+
583
+ // Clean up DB row if this was an otto-tracked worktree
584
+ if (row.threadId) {
585
+ await deleteThreadWorktree(row.threadId)
586
+ }
587
+
588
+ await renderWorktreesReply({
589
+ guildId,
590
+ userId: interaction.user.id,
591
+ channelId: interaction.channelId,
592
+ projectDirectory,
593
+ notice: `Deleted \`${displayName}\`.`,
594
+ editReply: (options) => {
595
+ return interaction.editReply(options)
596
+ },
597
+ })
598
+ }
599
+
600
+ export async function handleWorktreesCommand({
601
+ command,
602
+ }: {
603
+ command: ChatInputCommandInteraction
604
+ appId: string
605
+ }): Promise<void> {
606
+ const channel = command.channel
607
+ const guildId = command.guildId
608
+ if (!guildId || !channel) {
609
+ await command.reply({
610
+ content: 'This command can only be used in a server channel.',
611
+ flags: MessageFlags.Ephemeral,
612
+ })
613
+ return
614
+ }
615
+
616
+ if (!isProjectChannel(channel)) {
617
+ await command.reply({
618
+ content: 'This command can only be used in a project channel or thread.',
619
+ flags: MessageFlags.Ephemeral,
620
+ })
621
+ return
622
+ }
623
+
624
+ const resolved = await resolveWorkingDirectory({
625
+ channel: channel as TextChannel | ThreadChannel,
626
+ })
627
+ if (!resolved) {
628
+ await command.reply({
629
+ content: 'Could not determine the project folder for this channel.',
630
+ flags: MessageFlags.Ephemeral,
631
+ })
632
+ return
633
+ }
634
+
635
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
636
+ await renderWorktreesReply({
637
+ guildId,
638
+ userId: command.user.id,
639
+ channelId: command.channelId,
640
+ projectDirectory: resolved.projectDirectory,
641
+ editReply: (options) => {
642
+ return command.editReply(options)
643
+ },
644
+ })
645
+ }