@otto-assistant/otto 0.1.1 → 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 (637) 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/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,305 @@
1
+ // Undo/Redo commands - /undo, /redo
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.UNDO_REDO);
8
+ async function waitForSessionIdle({ client, sessionId, directory, timeoutMs = 2_000, }) {
9
+ const deadline = Date.now() + timeoutMs;
10
+ while (Date.now() < deadline) {
11
+ const statusResponse = await client.session.status({ directory });
12
+ const sessionStatus = statusResponse.data?.[sessionId];
13
+ if (!sessionStatus || sessionStatus.type === 'idle') {
14
+ return;
15
+ }
16
+ await new Promise((resolve) => {
17
+ setTimeout(resolve, 50);
18
+ });
19
+ }
20
+ }
21
+ export async function handleUndoCommand({ command, }) {
22
+ const channel = command.channel;
23
+ if (!channel) {
24
+ await command.reply({
25
+ content: 'This command can only be used in a channel',
26
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
27
+ });
28
+ return;
29
+ }
30
+ const isThread = [
31
+ ChannelType.PublicThread,
32
+ ChannelType.PrivateThread,
33
+ ChannelType.AnnouncementThread,
34
+ ].includes(channel.type);
35
+ if (!isThread) {
36
+ await command.reply({
37
+ content: 'This command can only be used in a thread with an active session',
38
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
39
+ });
40
+ return;
41
+ }
42
+ const resolved = await resolveWorkingDirectory({
43
+ channel: channel,
44
+ });
45
+ if (!resolved) {
46
+ await command.reply({
47
+ content: 'Could not determine project directory for this channel',
48
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
49
+ });
50
+ return;
51
+ }
52
+ const { projectDirectory, workingDirectory } = resolved;
53
+ const sessionId = await getThreadSession(channel.id);
54
+ if (!sessionId) {
55
+ await command.reply({
56
+ content: 'No active session in this thread',
57
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
58
+ });
59
+ return;
60
+ }
61
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
62
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
63
+ if (getClient instanceof Error) {
64
+ await command.editReply(`Failed to undo: ${getClient.message}`);
65
+ return;
66
+ }
67
+ try {
68
+ const client = getClient();
69
+ // Fetch session to check existing revert state
70
+ const sessionResponse = await client.session.get({
71
+ sessionID: sessionId,
72
+ directory: workingDirectory,
73
+ });
74
+ if (sessionResponse.error) {
75
+ await command.editReply(`Failed to undo: ${JSON.stringify(sessionResponse.error)}`);
76
+ return;
77
+ }
78
+ // Abort if session is busy before reverting, matching TUI behavior
79
+ // (use-session-commands.tsx always aborts non-idle sessions before revert).
80
+ // session.status() returns a sparse map — only non-idle sessions have entries,
81
+ // so a missing key means idle.
82
+ const statusResponse = await client.session.status({
83
+ directory: workingDirectory,
84
+ });
85
+ const sessionStatus = statusResponse.data?.[sessionId];
86
+ if (sessionStatus && sessionStatus.type !== 'idle') {
87
+ await client.session.abort({
88
+ sessionID: sessionId,
89
+ directory: workingDirectory,
90
+ }).catch((error) => {
91
+ logger.warn(`[UNDO] abort failed for ${sessionId}`, error);
92
+ });
93
+ await waitForSessionIdle({
94
+ client,
95
+ sessionId,
96
+ directory: workingDirectory,
97
+ });
98
+ }
99
+ const messagesResponse = await client.session.messages({
100
+ sessionID: sessionId,
101
+ directory: workingDirectory,
102
+ });
103
+ if (messagesResponse.error) {
104
+ await command.editReply(`Failed to undo: ${JSON.stringify(messagesResponse.error)}`);
105
+ return;
106
+ }
107
+ if (!messagesResponse.data || messagesResponse.data.length === 0) {
108
+ await command.editReply('No messages to undo');
109
+ return;
110
+ }
111
+ // Follow the same approach as the OpenCode TUI (use-session-commands.tsx):
112
+ // find the last user message that is before the current revert point
113
+ // (or the last user message if no revert is active). This matches the
114
+ // TUI's `findLast(userMessages(), (x) => !revert || x.id < revert)`.
115
+ const currentRevert = sessionResponse.data?.revert?.messageID;
116
+ const userMessages = messagesResponse.data.filter((m) => {
117
+ return m.info.role === 'user';
118
+ });
119
+ const targetUserMessage = [...userMessages].reverse().find((m) => {
120
+ return !currentRevert || m.info.id < currentRevert;
121
+ });
122
+ if (!targetUserMessage) {
123
+ await command.editReply('No messages to undo');
124
+ return;
125
+ }
126
+ const targetAssistantMessage = [...messagesResponse.data].reverse().find((m) => {
127
+ return m.info.role === 'assistant' && m.info.parentID === targetUserMessage.info.id;
128
+ });
129
+ const revertMessageId = targetAssistantMessage?.info.id || targetUserMessage.info.id;
130
+ // session.revert() reverts filesystem patches (file edits, writes) and
131
+ // marks the session with revert.messageID. Messages are NOT deleted — they
132
+ // get cleaned up automatically on the next promptAsync() call via
133
+ // SessionRevert.cleanup(). The model only sees messages before the revert
134
+ // point when processing the next prompt.
135
+ logger.log(`[UNDO] session.revert start messageId=${revertMessageId}`);
136
+ let response = await client.session.revert({
137
+ sessionID: sessionId,
138
+ directory: workingDirectory,
139
+ messageID: revertMessageId,
140
+ });
141
+ logger.log(`[UNDO] session.revert done error=${Boolean(response.error)}`);
142
+ if (response.error) {
143
+ logger.log('[UNDO] retry wait idle before revert retry');
144
+ await waitForSessionIdle({
145
+ client,
146
+ sessionId,
147
+ directory: workingDirectory,
148
+ });
149
+ logger.log('[UNDO] retry revert start');
150
+ response = await client.session.revert({
151
+ sessionID: sessionId,
152
+ directory: workingDirectory,
153
+ messageID: revertMessageId,
154
+ });
155
+ logger.log(`[UNDO] retry revert done error=${Boolean(response.error)}`);
156
+ if (response.error) {
157
+ await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
158
+ return;
159
+ }
160
+ }
161
+ const diffInfo = response.data?.revert?.diff
162
+ ? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
163
+ : '';
164
+ await command.editReply(`Undone - reverted last assistant message${diffInfo}`);
165
+ logger.log(`Session ${sessionId} reverted at message ${revertMessageId}`);
166
+ }
167
+ catch (error) {
168
+ logger.error('[UNDO] Error:', error);
169
+ await command.editReply(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`);
170
+ }
171
+ }
172
+ export async function handleRedoCommand({ command, }) {
173
+ const channel = command.channel;
174
+ if (!channel) {
175
+ await command.reply({
176
+ content: 'This command can only be used in a channel',
177
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
178
+ });
179
+ return;
180
+ }
181
+ const isThread = [
182
+ ChannelType.PublicThread,
183
+ ChannelType.PrivateThread,
184
+ ChannelType.AnnouncementThread,
185
+ ].includes(channel.type);
186
+ if (!isThread) {
187
+ await command.reply({
188
+ content: 'This command can only be used in a thread with an active session',
189
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
190
+ });
191
+ return;
192
+ }
193
+ const resolved = await resolveWorkingDirectory({
194
+ channel: channel,
195
+ });
196
+ if (!resolved) {
197
+ await command.reply({
198
+ content: 'Could not determine project directory for this channel',
199
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
200
+ });
201
+ return;
202
+ }
203
+ const { projectDirectory, workingDirectory } = resolved;
204
+ const sessionId = await getThreadSession(channel.id);
205
+ if (!sessionId) {
206
+ await command.reply({
207
+ content: 'No active session in this thread',
208
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
209
+ });
210
+ return;
211
+ }
212
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
213
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
214
+ if (getClient instanceof Error) {
215
+ await command.editReply(`Failed to redo: ${getClient.message}`);
216
+ return;
217
+ }
218
+ try {
219
+ const client = getClient();
220
+ // Fetch session to check existing revert state
221
+ const sessionResponse = await client.session.get({
222
+ sessionID: sessionId,
223
+ directory: workingDirectory,
224
+ });
225
+ if (sessionResponse.error) {
226
+ await command.editReply(`Failed to redo: ${JSON.stringify(sessionResponse.error)}`);
227
+ return;
228
+ }
229
+ const revertMessageID = sessionResponse.data?.revert?.messageID;
230
+ if (!revertMessageID) {
231
+ await command.editReply('Nothing to redo - no previous undo found');
232
+ return;
233
+ }
234
+ // Abort if session is busy before reverting/unreverting — both enforce
235
+ // assertNotBusy in OpenCode and would fail with "Session is busy"
236
+ const redoStatusResponse = await client.session.status({
237
+ directory: workingDirectory,
238
+ });
239
+ const redoSessionStatus = redoStatusResponse.data?.[sessionId];
240
+ if (redoSessionStatus && redoSessionStatus.type !== 'idle') {
241
+ await client.session.abort({
242
+ sessionID: sessionId,
243
+ directory: workingDirectory,
244
+ }).catch((error) => {
245
+ logger.warn(`[REDO] abort failed for ${sessionId}`, error);
246
+ });
247
+ await waitForSessionIdle({
248
+ client,
249
+ sessionId,
250
+ directory: workingDirectory,
251
+ });
252
+ }
253
+ await new Promise((resolve) => {
254
+ setTimeout(resolve, 500);
255
+ });
256
+ // Follow the same approach as the OpenCode TUI (use-session-commands.tsx):
257
+ // find the next user message after the current revert point. If one exists,
258
+ // move the revert cursor forward to it (one step redo). If none exists,
259
+ // fully unrevert — we're at the end of the message history.
260
+ const messagesResponse = await client.session.messages({
261
+ sessionID: sessionId,
262
+ directory: workingDirectory,
263
+ });
264
+ if (messagesResponse.error) {
265
+ await command.editReply(`Failed to redo: ${JSON.stringify(messagesResponse.error)}`);
266
+ return;
267
+ }
268
+ const userMessages = (messagesResponse.data ?? []).filter((m) => {
269
+ return m.info.role === 'user';
270
+ });
271
+ const nextMessage = userMessages.find((m) => {
272
+ return m.info.id > revertMessageID;
273
+ });
274
+ if (!nextMessage) {
275
+ // No more messages after revert point — fully unrevert
276
+ const response = await client.session.unrevert({
277
+ sessionID: sessionId,
278
+ directory: workingDirectory,
279
+ });
280
+ if (response.error) {
281
+ await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
282
+ return;
283
+ }
284
+ await command.editReply('Restored - session fully back to previous state');
285
+ logger.log(`Session ${sessionId} unrevert completed`);
286
+ return;
287
+ }
288
+ // Move revert cursor forward one step to the next user message
289
+ const response = await client.session.revert({
290
+ sessionID: sessionId,
291
+ directory: workingDirectory,
292
+ messageID: nextMessage.info.id,
293
+ });
294
+ if (response.error) {
295
+ await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
296
+ return;
297
+ }
298
+ await command.editReply('Restored one step forward');
299
+ logger.log(`Session ${sessionId} redo: moved revert to ${nextMessage.info.id}`);
300
+ }
301
+ catch (error) {
302
+ logger.error('[REDO] Error:', error);
303
+ await command.editReply(`Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`);
304
+ }
305
+ }
@@ -0,0 +1,139 @@
1
+ // /unset-model-override command - Remove model overrides and use default instead.
2
+ import { ChatInputCommandInteraction, ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getChannelModel, getSessionModel, getThreadSession, clearSessionModel, } from '../database.js';
4
+ import { getPrisma } from '../db.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js';
7
+ import { getRuntime } from '../session-handler/thread-session-runtime.js';
8
+ import { getCurrentModelInfo } from './model.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ const unsetModelLogger = createLogger(LogPrefix.MODEL);
11
+ function formatModelSource(type, agentName) {
12
+ switch (type) {
13
+ case 'session':
14
+ return 'session override';
15
+ case 'agent':
16
+ return `agent "${agentName}"`;
17
+ case 'channel':
18
+ return 'channel override';
19
+ case 'global':
20
+ return 'global default';
21
+ case 'opencode-config':
22
+ case 'opencode-recent':
23
+ case 'opencode-provider-default':
24
+ return 'opencode default';
25
+ default:
26
+ return 'none';
27
+ }
28
+ }
29
+ /**
30
+ * Handle the /unset-model-override slash command.
31
+ * In thread: clears session override if exists, otherwise channel override.
32
+ * In channel: clears channel override.
33
+ */
34
+ export async function handleUnsetModelCommand({ interaction, appId, }) {
35
+ unsetModelLogger.log('[UNSET-MODEL] handleUnsetModelCommand called');
36
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
37
+ const channel = interaction.channel;
38
+ if (!channel) {
39
+ await interaction.editReply({
40
+ content: 'This command can only be used in a channel',
41
+ });
42
+ return;
43
+ }
44
+ const isThread = [
45
+ ChannelType.PublicThread,
46
+ ChannelType.PrivateThread,
47
+ ChannelType.AnnouncementThread,
48
+ ].includes(channel.type);
49
+ let projectDirectory;
50
+ let targetChannelId;
51
+ let sessionId;
52
+ if (isThread) {
53
+ const thread = channel;
54
+ const textChannel = await resolveTextChannel(thread);
55
+ const metadata = await getOttoMetadata(textChannel);
56
+ projectDirectory = metadata.projectDirectory;
57
+ targetChannelId = textChannel?.id || channel.id;
58
+ sessionId = await getThreadSession(thread.id);
59
+ }
60
+ else if (channel.type === ChannelType.GuildText) {
61
+ const textChannel = channel;
62
+ const metadata = await getOttoMetadata(textChannel);
63
+ projectDirectory = metadata.projectDirectory;
64
+ targetChannelId = channel.id;
65
+ }
66
+ else {
67
+ await interaction.editReply({
68
+ content: 'This command can only be used in text channels or threads',
69
+ });
70
+ return;
71
+ }
72
+ if (!projectDirectory) {
73
+ await interaction.editReply({
74
+ content: 'This channel is not configured with a project directory',
75
+ });
76
+ return;
77
+ }
78
+ // Check what overrides exist
79
+ const [sessionPref, channelPref] = await Promise.all([
80
+ sessionId ? getSessionModel(sessionId) : Promise.resolve(undefined),
81
+ getChannelModel(targetChannelId),
82
+ ]);
83
+ let clearedType = null;
84
+ let clearedModel;
85
+ if (isThread && sessionId && sessionPref) {
86
+ // In thread with session override: clear session
87
+ await clearSessionModel(sessionId);
88
+ clearedType = 'session';
89
+ clearedModel = sessionPref.modelId;
90
+ unsetModelLogger.log(`[UNSET-MODEL] Cleared session model for ${sessionId}`);
91
+ }
92
+ else if (channelPref) {
93
+ // Clear channel override
94
+ const prisma = await getPrisma();
95
+ await prisma.channel_models.deleteMany({
96
+ where: { channel_id: targetChannelId },
97
+ });
98
+ clearedType = 'channel';
99
+ clearedModel = channelPref.modelId;
100
+ unsetModelLogger.log(`[UNSET-MODEL] Cleared channel model for ${targetChannelId}`);
101
+ }
102
+ else {
103
+ await interaction.editReply({
104
+ content: 'No model override to clear.',
105
+ });
106
+ return;
107
+ }
108
+ // Get the new model that will be used
109
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
110
+ let newModelText = 'unknown';
111
+ if (!(getClient instanceof Error)) {
112
+ const newModelInfo = await getCurrentModelInfo({
113
+ sessionId,
114
+ channelId: targetChannelId,
115
+ appId,
116
+ getClient,
117
+ directory: projectDirectory,
118
+ });
119
+ newModelText =
120
+ newModelInfo.type === 'none'
121
+ ? 'none'
122
+ : `\`${newModelInfo.model}\` (${formatModelSource(newModelInfo.type, 'agentName' in newModelInfo ? newModelInfo.agentName : undefined)})`;
123
+ }
124
+ // Check if there's a running request and abort+retry with new model (only for session changes in threads)
125
+ let retried = false;
126
+ if (isThread && clearedType === 'session' && sessionId) {
127
+ const runtime = getRuntime(channel.id);
128
+ if (runtime) {
129
+ retried = await runtime.retryLastUserPrompt();
130
+ }
131
+ }
132
+ const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel';
133
+ const retriedText = retried
134
+ ? '\n_Restarting current request with new model..._'
135
+ : '';
136
+ await interaction.editReply({
137
+ content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}${retriedText}`,
138
+ });
139
+ }
@@ -0,0 +1,48 @@
1
+ // /upgrade-and-restart command - Upgrade otto to the latest version and restart the bot.
2
+ // Checks npm for a newer version, installs it globally, then spawns a new otto process.
3
+ // Spawn `otto gateway start`; the new process evicts the old one (single-instance lock).
4
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ import { getCurrentVersion, upgrade } from '../upgrade.js';
7
+ import { spawn } from 'node:child_process';
8
+ const logger = createLogger(LogPrefix.CLI);
9
+ export async function handleUpgradeAndRestartCommand({ command, }) {
10
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
11
+ logger.log('[UPGRADE] /upgrade-and-restart triggered');
12
+ try {
13
+ const currentVersion = getCurrentVersion();
14
+ const newVersion = await upgrade();
15
+ if (!newVersion) {
16
+ await command.editReply({
17
+ content: `Already on latest version: **v${currentVersion}**`,
18
+ });
19
+ return;
20
+ }
21
+ await command.editReply({
22
+ content: `Upgraded otto **v${currentVersion}** -> **v${newVersion}**. Restarting bot...`,
23
+ });
24
+ // Spawning `otto gateway start` works even if the user originally ran via npx/bunx:
25
+ // `npm i -g @otto-assistant/otto@latest` creates a global bin link, and
26
+ // npx resolves local -> global -> cache -> registry, so it prefers the
27
+ // global install. bunx shares the same global cache too.
28
+ const scriptPath = process.argv[1];
29
+ if (!scriptPath) {
30
+ throw new Error('Unable to resolve current CLI script path');
31
+ }
32
+ const child = spawn(process.execPath, [scriptPath, 'gateway', 'start'], {
33
+ stdio: 'ignore',
34
+ detached: true,
35
+ env: process.env,
36
+ shell: false,
37
+ windowsHide: process.platform === 'win32',
38
+ });
39
+ child.unref();
40
+ logger.debug('Started new background otto process');
41
+ }
42
+ catch (error) {
43
+ logger.error('[UPGRADE] Failed:', error);
44
+ await command.editReply({
45
+ content: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`,
46
+ });
47
+ }
48
+ }
@@ -0,0 +1,155 @@
1
+ // User-defined OpenCode command handler.
2
+ // Handles slash commands that map to user-configured commands in opencode.json.
3
+ import { ChannelType, MessageFlags, } from 'discord.js';
4
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
5
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ import { getChannelDirectory, getThreadSession } from '../database.js';
8
+ import { store } from '../store.js';
9
+ import fs from 'node:fs';
10
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD);
11
+ const DISCORD_MESSAGE_LIMIT = 2000;
12
+ const DISCORD_THREAD_NAME_LIMIT = 100;
13
+ export const handleUserCommand = async ({ command, appId, }) => {
14
+ const discordCommandName = command.commandName;
15
+ // Look up the original OpenCode command name from the mapping populated at registration.
16
+ // The sanitized Discord name is lossy (e.g. foo:bar → foo-bar), so resolving from
17
+ // the exact registered slash command name avoids collisions.
18
+ const registered = store.getState().registeredUserCommands.find((c) => c.discordCommandName === discordCommandName);
19
+ const fallbackBase = discordCommandName.replace(/-(cmd|skill|mcp-prompt)$/, '');
20
+ const commandName = registered?.name || fallbackBase;
21
+ const args = command.options.getString('arguments') || '';
22
+ const commandInvocation = args ? `/${commandName} ${args}` : `/${commandName}`;
23
+ const threadOpeningMessage = commandInvocation.length <= DISCORD_MESSAGE_LIMIT
24
+ ? commandInvocation
25
+ : `${commandInvocation.slice(0, DISCORD_MESSAGE_LIMIT - 14)}... truncated`;
26
+ userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`);
27
+ const channel = command.channel;
28
+ userCommandLogger.log(`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`);
29
+ const isThread = channel &&
30
+ [
31
+ ChannelType.PublicThread,
32
+ ChannelType.PrivateThread,
33
+ ChannelType.AnnouncementThread,
34
+ ].includes(channel.type);
35
+ const isTextChannel = channel?.type === ChannelType.GuildText;
36
+ if (!channel || (!isTextChannel && !isThread)) {
37
+ await command.reply({
38
+ content: 'This command can only be used in text channels or threads',
39
+ flags: MessageFlags.Ephemeral,
40
+ });
41
+ return;
42
+ }
43
+ let projectDirectory;
44
+ let textChannel = null;
45
+ let thread = null;
46
+ if (isThread) {
47
+ // Running in an existing thread - get project directory from parent channel
48
+ thread = channel;
49
+ textChannel = thread.parent;
50
+ // Verify this thread has an existing session
51
+ const sessionId = await getThreadSession(thread.id);
52
+ if (!sessionId) {
53
+ await command.reply({
54
+ content: 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
55
+ flags: MessageFlags.Ephemeral,
56
+ });
57
+ return;
58
+ }
59
+ if (textChannel) {
60
+ const channelConfig = await getChannelDirectory(textChannel.id);
61
+ projectDirectory = channelConfig?.directory;
62
+ }
63
+ }
64
+ else {
65
+ // Running in a text channel - will create a new thread
66
+ textChannel = channel;
67
+ const channelConfig = await getChannelDirectory(textChannel.id);
68
+ projectDirectory = channelConfig?.directory;
69
+ }
70
+ if (!projectDirectory) {
71
+ await command.reply({
72
+ content: 'This channel is not configured with a project directory',
73
+ flags: MessageFlags.Ephemeral,
74
+ });
75
+ return;
76
+ }
77
+ if (!fs.existsSync(projectDirectory)) {
78
+ await command.reply({
79
+ content: `Directory does not exist: ${projectDirectory}`,
80
+ flags: MessageFlags.Ephemeral,
81
+ });
82
+ return;
83
+ }
84
+ await command.deferReply();
85
+ try {
86
+ // Use the dedicated session.command API instead of formatting as text prompt
87
+ const commandPayload = { name: commandName, arguments: args };
88
+ if (isThread && thread) {
89
+ // Running in existing thread - just send the command
90
+ await command.editReply(`Running ${commandInvocation}...`);
91
+ const runtime = getOrCreateRuntime({
92
+ threadId: thread.id,
93
+ thread,
94
+ projectDirectory,
95
+ sdkDirectory: projectDirectory,
96
+ channelId: textChannel?.id,
97
+ appId,
98
+ });
99
+ await runtime.enqueueIncoming({
100
+ prompt: '',
101
+ userId: command.user.id,
102
+ username: command.user.displayName,
103
+ command: commandPayload,
104
+ appId,
105
+ mode: 'local-queue',
106
+ });
107
+ }
108
+ else if (textChannel) {
109
+ // Running in text channel - create a new thread
110
+ const starterMessage = await textChannel.send({
111
+ content: threadOpeningMessage,
112
+ flags: SILENT_MESSAGE_FLAGS,
113
+ });
114
+ const newThread = await starterMessage.startThread({
115
+ name: commandInvocation.slice(0, DISCORD_THREAD_NAME_LIMIT),
116
+ autoArchiveDuration: 1440,
117
+ reason: `OpenCode command: ${commandName}`,
118
+ });
119
+ // Add user to thread so it appears in their sidebar
120
+ await newThread.members.add(command.user.id);
121
+ await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
122
+ const runtime = getOrCreateRuntime({
123
+ threadId: newThread.id,
124
+ thread: newThread,
125
+ projectDirectory,
126
+ sdkDirectory: projectDirectory,
127
+ channelId: textChannel.id,
128
+ appId,
129
+ });
130
+ await runtime.enqueueIncoming({
131
+ prompt: '',
132
+ userId: command.user.id,
133
+ username: command.user.displayName,
134
+ command: commandPayload,
135
+ appId,
136
+ mode: 'local-queue',
137
+ });
138
+ }
139
+ }
140
+ catch (error) {
141
+ userCommandLogger.error(`Error executing /${commandName}:`, error);
142
+ const errorMessage = error instanceof Error ? error.message : String(error);
143
+ if (command.deferred) {
144
+ await command.editReply({
145
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
146
+ });
147
+ }
148
+ else {
149
+ await command.reply({
150
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
151
+ flags: MessageFlags.Ephemeral,
152
+ });
153
+ }
154
+ }
155
+ };