@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,794 @@
1
+ // /model command - Set the preferred model for this channel or session.
2
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
3
+ import crypto from 'node:crypto';
4
+ import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js';
7
+ import { getDefaultModel } from '../session-handler/model-utils.js';
8
+ import { getRuntime } from '../session-handler/thread-session-runtime.js';
9
+ import { getThinkingValuesForModel } from '../thinking-utils.js';
10
+ import { createLogger, LogPrefix } from '../logger.js';
11
+ import * as errore from 'errore';
12
+ import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
13
+ const modelLogger = createLogger(LogPrefix.MODEL);
14
+ // Store context by hash to avoid customId length limits (Discord max: 100 chars).
15
+ // Entries are TTL'd to prevent unbounded growth when users open /model and never
16
+ // interact with the select menu.
17
+ const MODEL_CONTEXT_TTL_MS = 10 * 60 * 1000;
18
+ const pendingModelContexts = new Map();
19
+ const pendingModelContextTimers = new Map();
20
+ function setModelContext(contextHash, context) {
21
+ const existingTimer = pendingModelContextTimers.get(contextHash);
22
+ if (existingTimer) {
23
+ clearTimeout(existingTimer);
24
+ }
25
+ pendingModelContexts.set(contextHash, context);
26
+ const timer = setTimeout(() => {
27
+ deleteModelContext(contextHash);
28
+ }, MODEL_CONTEXT_TTL_MS);
29
+ timer.unref();
30
+ pendingModelContextTimers.set(contextHash, timer);
31
+ }
32
+ function deleteModelContext(contextHash) {
33
+ const existingTimer = pendingModelContextTimers.get(contextHash);
34
+ if (existingTimer) {
35
+ clearTimeout(existingTimer);
36
+ pendingModelContextTimers.delete(contextHash);
37
+ }
38
+ pendingModelContexts.delete(contextHash);
39
+ }
40
+ function parseModelId(modelString) {
41
+ const [providerID, ...modelParts] = modelString.split('/');
42
+ const modelID = modelParts.join('/');
43
+ if (providerID && modelID) {
44
+ return { providerID, modelID };
45
+ }
46
+ return undefined;
47
+ }
48
+ export async function ensureSessionPreferencesSnapshot({ sessionId, channelId, appId, getClient, directory, agentOverride, modelOverride, force, }) {
49
+ const [sessionAgentPreference, sessionModelPreference] = await Promise.all([
50
+ getSessionAgent(sessionId),
51
+ getSessionModel(sessionId),
52
+ ]);
53
+ const shouldBootstrapSessionPreferences = force || (!sessionAgentPreference && !sessionModelPreference);
54
+ if (!shouldBootstrapSessionPreferences) {
55
+ return;
56
+ }
57
+ const bootstrappedAgent = agentOverride ||
58
+ sessionAgentPreference ||
59
+ (channelId ? await getChannelAgent(channelId) : undefined);
60
+ if (!sessionAgentPreference && bootstrappedAgent) {
61
+ await setSessionAgent(sessionId, bootstrappedAgent);
62
+ modelLogger.log(`[MODEL] Snapshotted session agent ${bootstrappedAgent} for session ${sessionId}`);
63
+ }
64
+ if (sessionModelPreference) {
65
+ return;
66
+ }
67
+ if (modelOverride) {
68
+ const parsedModelOverride = parseModelId(modelOverride);
69
+ if (parsedModelOverride) {
70
+ const bootstrappedVariant = await getVariantCascade({
71
+ sessionId,
72
+ channelId,
73
+ appId,
74
+ });
75
+ await setSessionModel({
76
+ sessionId,
77
+ modelId: modelOverride,
78
+ variant: bootstrappedVariant ?? null,
79
+ });
80
+ modelLogger.log(`[MODEL] Snapshotted explicit session model ${modelOverride} for session ${sessionId}`);
81
+ return;
82
+ }
83
+ modelLogger.warn(`[MODEL] Ignoring invalid explicit model override "${modelOverride}" for session ${sessionId}`);
84
+ }
85
+ const bootstrappedModel = await getCurrentModelInfo({
86
+ sessionId,
87
+ channelId,
88
+ appId,
89
+ agentPreference: bootstrappedAgent,
90
+ getClient,
91
+ directory,
92
+ });
93
+ if (bootstrappedModel.type === 'none') {
94
+ return;
95
+ }
96
+ const bootstrappedVariant = await getVariantCascade({
97
+ sessionId,
98
+ channelId,
99
+ appId,
100
+ });
101
+ await setSessionModel({
102
+ sessionId,
103
+ modelId: bootstrappedModel.model,
104
+ variant: bootstrappedVariant ?? null,
105
+ });
106
+ modelLogger.log(`[MODEL] Snapshotted session model ${bootstrappedModel.model} for session ${sessionId}`);
107
+ }
108
+ /**
109
+ * Get the current model info for a channel/session, including where it comes from.
110
+ * Priority: session > agent > channel > global > opencode default
111
+ */
112
+ export async function getCurrentModelInfo({ sessionId, channelId, appId, agentPreference, getClient, directory, }) {
113
+ if (getClient instanceof Error) {
114
+ return { type: 'none' };
115
+ }
116
+ // 1. Check session model preference
117
+ if (sessionId) {
118
+ const sessionPref = await getSessionModel(sessionId);
119
+ if (sessionPref) {
120
+ const parsed = parseModelId(sessionPref.modelId);
121
+ if (parsed) {
122
+ return { type: 'session', model: sessionPref.modelId, ...parsed };
123
+ }
124
+ }
125
+ }
126
+ // 2. Check agent's configured model
127
+ const effectiveAgent = agentPreference ??
128
+ (sessionId
129
+ ? (await getSessionAgent(sessionId)) ||
130
+ (channelId ? await getChannelAgent(channelId) : undefined)
131
+ : channelId
132
+ ? await getChannelAgent(channelId)
133
+ : undefined);
134
+ if (effectiveAgent) {
135
+ const agentsResponse = await getClient().app.agents({ directory });
136
+ if (agentsResponse.data) {
137
+ const agent = agentsResponse.data.find((a) => a.name === effectiveAgent);
138
+ if (agent?.model) {
139
+ const model = `${agent.model.providerID}/${agent.model.modelID}`;
140
+ return {
141
+ type: 'agent',
142
+ model,
143
+ providerID: agent.model.providerID,
144
+ modelID: agent.model.modelID,
145
+ agentName: effectiveAgent,
146
+ };
147
+ }
148
+ }
149
+ }
150
+ // 3. Check channel model preference
151
+ if (channelId) {
152
+ const channelPref = await getChannelModel(channelId);
153
+ if (channelPref) {
154
+ const parsed = parseModelId(channelPref.modelId);
155
+ if (parsed) {
156
+ return { type: 'channel', model: channelPref.modelId, ...parsed };
157
+ }
158
+ }
159
+ }
160
+ // 4. Check global model preference
161
+ if (appId) {
162
+ const globalPref = await getGlobalModel(appId);
163
+ if (globalPref) {
164
+ const parsed = parseModelId(globalPref.modelId);
165
+ if (parsed) {
166
+ return { type: 'global', model: globalPref.modelId, ...parsed };
167
+ }
168
+ }
169
+ }
170
+ // 5. Get opencode default (config > recent > provider default)
171
+ const defaultModel = await getDefaultModel({ getClient, directory });
172
+ if (defaultModel) {
173
+ const model = `${defaultModel.providerID}/${defaultModel.modelID}`;
174
+ return {
175
+ type: defaultModel.source,
176
+ model,
177
+ providerID: defaultModel.providerID,
178
+ modelID: defaultModel.modelID,
179
+ };
180
+ }
181
+ return { type: 'none' };
182
+ }
183
+ /**
184
+ * Handle the /model slash command.
185
+ * Shows a select menu with available providers.
186
+ */
187
+ export async function handleModelCommand({ interaction, appId, }) {
188
+ modelLogger.log('[MODEL] handleModelCommand called');
189
+ // Defer reply immediately to avoid 3-second timeout
190
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
191
+ modelLogger.log('[MODEL] Deferred reply');
192
+ const channel = interaction.channel;
193
+ if (!channel) {
194
+ await interaction.editReply({
195
+ content: 'This command can only be used in a channel',
196
+ });
197
+ return;
198
+ }
199
+ // Determine if we're in a thread or text channel
200
+ const isThread = [
201
+ ChannelType.PublicThread,
202
+ ChannelType.PrivateThread,
203
+ ChannelType.AnnouncementThread,
204
+ ].includes(channel.type);
205
+ let projectDirectory;
206
+ let targetChannelId;
207
+ let sessionId;
208
+ if (isThread) {
209
+ const thread = channel;
210
+ // Parallelize: resolve metadata and session ID at the same time
211
+ const [textChannel, threadSessionId] = await Promise.all([
212
+ resolveTextChannel(thread),
213
+ getThreadSession(thread.id),
214
+ ]);
215
+ const metadata = await getOttoMetadata(textChannel);
216
+ projectDirectory = metadata.projectDirectory;
217
+ targetChannelId = textChannel?.id || channel.id;
218
+ sessionId = threadSessionId;
219
+ }
220
+ else if (channel.type === ChannelType.GuildText) {
221
+ const textChannel = channel;
222
+ const metadata = await getOttoMetadata(textChannel);
223
+ projectDirectory = metadata.projectDirectory;
224
+ targetChannelId = channel.id;
225
+ }
226
+ else {
227
+ await interaction.editReply({
228
+ content: 'This command can only be used in text channels or threads',
229
+ });
230
+ return;
231
+ }
232
+ if (!projectDirectory) {
233
+ await interaction.editReply({
234
+ content: 'This channel is not configured with a project directory',
235
+ });
236
+ return;
237
+ }
238
+ try {
239
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
240
+ if (getClient instanceof Error) {
241
+ await interaction.editReply({ content: getClient.message });
242
+ return;
243
+ }
244
+ const effectiveAppId = appId;
245
+ if (isThread && sessionId) {
246
+ await ensureSessionPreferencesSnapshot({
247
+ sessionId,
248
+ channelId: targetChannelId,
249
+ appId: effectiveAppId,
250
+ getClient,
251
+ directory: projectDirectory,
252
+ });
253
+ }
254
+ // Parallelize: fetch providers, current model info, and variant cascade at the same time.
255
+ // getCurrentModelInfo does DB lookups first (fast) and only hits provider.list as fallback.
256
+ const [providersResponse, currentModelInfo, cascadeVariant] = await Promise.all([
257
+ getClient().provider.list({ directory: projectDirectory }),
258
+ getCurrentModelInfo({
259
+ sessionId,
260
+ channelId: targetChannelId,
261
+ appId: effectiveAppId,
262
+ getClient,
263
+ directory: projectDirectory,
264
+ }),
265
+ getVariantCascade({
266
+ sessionId,
267
+ channelId: targetChannelId,
268
+ appId: effectiveAppId,
269
+ }),
270
+ ]);
271
+ if (!providersResponse.data) {
272
+ await interaction.editReply({
273
+ content: 'Failed to fetch providers',
274
+ });
275
+ return;
276
+ }
277
+ const { all: allProviders, connected } = providersResponse.data;
278
+ // Filter to only connected providers (have credentials)
279
+ const availableProviders = allProviders.filter((p) => {
280
+ return connected.includes(p.id);
281
+ });
282
+ if (availableProviders.length === 0) {
283
+ await interaction.editReply({
284
+ content: 'No providers with credentials found. Use `/login` to connect a provider and add credentials.',
285
+ });
286
+ return;
287
+ }
288
+ const currentModelText = (() => {
289
+ switch (currentModelInfo.type) {
290
+ case 'session':
291
+ return `**Current (this thread):** \`${currentModelInfo.model}\``;
292
+ case 'agent':
293
+ return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
294
+ case 'channel':
295
+ return `**Current (channel override):** \`${currentModelInfo.model}\``;
296
+ case 'global':
297
+ return `**Current (global default):** \`${currentModelInfo.model}\``;
298
+ case 'opencode-config':
299
+ case 'opencode-recent':
300
+ case 'opencode-provider-default':
301
+ return `**Current (opencode default):** \`${currentModelInfo.model}\``;
302
+ case 'none':
303
+ return '**Current:** none';
304
+ }
305
+ })();
306
+ const variantText = (() => {
307
+ if (currentModelInfo.type === 'none' || !cascadeVariant) {
308
+ return '';
309
+ }
310
+ return `\n**Variant:** \`${cascadeVariant}\``;
311
+ })();
312
+ // Store context with a short hash key to avoid customId length limits.
313
+ const providerSelectHeader = `**Set Model Preference**\n${currentModelText}${variantText}\nSelect a provider:`;
314
+ const context = {
315
+ dir: projectDirectory,
316
+ channelId: targetChannelId,
317
+ sessionId: sessionId,
318
+ isThread: isThread,
319
+ thread: isThread ? channel : undefined,
320
+ appId,
321
+ providerSelectHeader,
322
+ };
323
+ const contextHash = crypto.randomBytes(8).toString('hex');
324
+ setModelContext(contextHash, context);
325
+ const allProviderOptions = [...availableProviders]
326
+ .sort((a, b) => a.name.localeCompare(b.name))
327
+ .map((provider) => {
328
+ const modelCount = Object.keys(provider.models || {}).length;
329
+ return {
330
+ label: provider.name.slice(0, 100),
331
+ value: provider.id,
332
+ description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
333
+ };
334
+ });
335
+ const { options } = buildPaginatedOptions({
336
+ allOptions: allProviderOptions,
337
+ page: 0,
338
+ });
339
+ const selectMenu = new StringSelectMenuBuilder()
340
+ .setCustomId(`model_provider:${contextHash}`)
341
+ .setPlaceholder('Select a provider')
342
+ .addOptions(options);
343
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
344
+ await interaction.editReply({
345
+ content: providerSelectHeader,
346
+ components: [actionRow],
347
+ });
348
+ }
349
+ catch (error) {
350
+ modelLogger.error('Error loading providers:', error);
351
+ await interaction.editReply({
352
+ content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
353
+ });
354
+ }
355
+ }
356
+ /**
357
+ * Handle the provider select menu interaction.
358
+ * Shows a second select menu with models for the chosen provider.
359
+ */
360
+ export async function handleProviderSelectMenu(interaction) {
361
+ const customId = interaction.customId;
362
+ if (!customId.startsWith('model_provider:')) {
363
+ return;
364
+ }
365
+ // Defer update immediately to avoid timeout
366
+ await interaction.deferUpdate();
367
+ const contextHash = customId.replace('model_provider:', '');
368
+ const context = pendingModelContexts.get(contextHash);
369
+ if (!context) {
370
+ await interaction.editReply({
371
+ content: 'Selection expired. Please run /model again.',
372
+ components: [],
373
+ });
374
+ return;
375
+ }
376
+ const selectedProviderId = interaction.values[0];
377
+ if (!selectedProviderId) {
378
+ await interaction.editReply({
379
+ content: 'No provider selected',
380
+ components: [],
381
+ });
382
+ return;
383
+ }
384
+ // Handle pagination nav — re-render the same provider select with new page
385
+ const providerNavPage = parsePaginationValue(selectedProviderId);
386
+ if (providerNavPage !== undefined) {
387
+ context.providerPage = providerNavPage;
388
+ setModelContext(contextHash, context);
389
+ const getClient = await initializeOpencodeForDirectory(context.dir);
390
+ if (getClient instanceof Error) {
391
+ await interaction.editReply({ content: getClient.message, components: [] });
392
+ return;
393
+ }
394
+ const providersResponse = await getClient().provider.list({ directory: context.dir });
395
+ if (!providersResponse.data) {
396
+ await interaction.editReply({ content: 'Failed to fetch providers', components: [] });
397
+ return;
398
+ }
399
+ const { all: allProviders, connected } = providersResponse.data;
400
+ const availableProviders = allProviders.filter((p) => connected.includes(p.id));
401
+ const allProviderOptions = [...availableProviders]
402
+ .sort((a, b) => a.name.localeCompare(b.name))
403
+ .map((p) => {
404
+ const modelCount = Object.keys(p.models || {}).length;
405
+ return {
406
+ label: p.name.slice(0, 100),
407
+ value: p.id,
408
+ description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
409
+ };
410
+ });
411
+ const { options } = buildPaginatedOptions({ allOptions: allProviderOptions, page: providerNavPage });
412
+ const selectMenu = new StringSelectMenuBuilder()
413
+ .setCustomId(`model_provider:${contextHash}`)
414
+ .setPlaceholder('Select a provider')
415
+ .addOptions(options);
416
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
417
+ await interaction.editReply({
418
+ content: context.providerSelectHeader || `**Set Model Preference**\nSelect a provider:`,
419
+ components: [actionRow],
420
+ });
421
+ return;
422
+ }
423
+ try {
424
+ const getClient = await initializeOpencodeForDirectory(context.dir);
425
+ if (getClient instanceof Error) {
426
+ await interaction.editReply({
427
+ content: getClient.message,
428
+ components: [],
429
+ });
430
+ return;
431
+ }
432
+ const providersResponse = await getClient().provider.list({
433
+ directory: context.dir,
434
+ });
435
+ if (!providersResponse.data) {
436
+ await interaction.editReply({
437
+ content: 'Failed to fetch providers',
438
+ components: [],
439
+ });
440
+ return;
441
+ }
442
+ const provider = providersResponse.data.all.find((p) => p.id === selectedProviderId);
443
+ if (!provider) {
444
+ await interaction.editReply({
445
+ content: 'Provider not found',
446
+ components: [],
447
+ });
448
+ return;
449
+ }
450
+ const models = Object.entries(provider.models || {})
451
+ .map(([modelId, model]) => ({
452
+ id: modelId,
453
+ name: model.name,
454
+ releaseDate: model.release_date,
455
+ }))
456
+ .sort((a, b) => a.name.localeCompare(b.name));
457
+ if (models.length === 0) {
458
+ await interaction.editReply({
459
+ content: `No models available for ${provider.name}`,
460
+ components: [],
461
+ });
462
+ return;
463
+ }
464
+ // Update context with provider info and reuse the same hash
465
+ context.providerId = selectedProviderId;
466
+ context.providerName = provider.name;
467
+ context.modelPage = 0;
468
+ setModelContext(contextHash, context);
469
+ const allModelOptions = models.map((model) => {
470
+ const dateStr = model.releaseDate
471
+ ? new Date(model.releaseDate).toLocaleDateString()
472
+ : 'Unknown date';
473
+ return {
474
+ label: model.name.slice(0, 100),
475
+ value: model.id,
476
+ description: dateStr.slice(0, 100),
477
+ };
478
+ });
479
+ const { options } = buildPaginatedOptions({
480
+ allOptions: allModelOptions,
481
+ page: 0,
482
+ });
483
+ const selectMenu = new StringSelectMenuBuilder()
484
+ .setCustomId(`model_select:${contextHash}`)
485
+ .setPlaceholder('Select a model')
486
+ .addOptions(options);
487
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
488
+ await interaction.editReply({
489
+ content: `**Set Model Preference**\nProvider: **${provider.name}**\nSelect a model:`,
490
+ components: [actionRow],
491
+ });
492
+ }
493
+ catch (error) {
494
+ modelLogger.error('Error loading models:', error);
495
+ await interaction.editReply({
496
+ content: `Failed to load models: ${error instanceof Error ? error.message : 'Unknown error'}`,
497
+ components: [],
498
+ });
499
+ }
500
+ }
501
+ /**
502
+ * Handle the model select menu interaction.
503
+ * Stores the model preference in the database.
504
+ */
505
+ export async function handleModelSelectMenu(interaction) {
506
+ const customId = interaction.customId;
507
+ if (!customId.startsWith('model_select:')) {
508
+ return;
509
+ }
510
+ // Defer update immediately
511
+ await interaction.deferUpdate();
512
+ const contextHash = customId.replace('model_select:', '');
513
+ const context = pendingModelContexts.get(contextHash);
514
+ if (!context || !context.providerId || !context.providerName) {
515
+ await interaction.editReply({
516
+ content: 'Selection expired. Please run /model again.',
517
+ components: [],
518
+ });
519
+ return;
520
+ }
521
+ const selectedModelId = interaction.values[0];
522
+ if (!selectedModelId) {
523
+ await interaction.editReply({
524
+ content: 'No model selected',
525
+ components: [],
526
+ });
527
+ return;
528
+ }
529
+ // Handle pagination nav — re-render the same model select with new page
530
+ const modelNavPage = parsePaginationValue(selectedModelId);
531
+ if (modelNavPage !== undefined) {
532
+ context.modelPage = modelNavPage;
533
+ setModelContext(contextHash, context);
534
+ const getClient = await initializeOpencodeForDirectory(context.dir);
535
+ if (getClient instanceof Error) {
536
+ await interaction.editReply({ content: getClient.message, components: [] });
537
+ return;
538
+ }
539
+ const providersResponse = await getClient().provider.list({ directory: context.dir });
540
+ const provider = providersResponse.data?.all.find((p) => p.id === context.providerId);
541
+ if (!provider) {
542
+ await interaction.editReply({ content: 'Provider not found', components: [] });
543
+ return;
544
+ }
545
+ const allModelOptions = Object.entries(provider.models || {})
546
+ .map(([modelId, model]) => ({
547
+ label: model.name.slice(0, 100),
548
+ value: modelId,
549
+ description: (model.release_date
550
+ ? new Date(model.release_date).toLocaleDateString()
551
+ : 'Unknown date').slice(0, 100),
552
+ }))
553
+ .sort((a, b) => a.label.localeCompare(b.label));
554
+ const { options } = buildPaginatedOptions({ allOptions: allModelOptions, page: modelNavPage });
555
+ const selectMenu = new StringSelectMenuBuilder()
556
+ .setCustomId(`model_select:${contextHash}`)
557
+ .setPlaceholder('Select a model')
558
+ .addOptions(options);
559
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
560
+ await interaction.editReply({
561
+ content: `**Set Model Preference**\nProvider: **${context.providerName}**\nSelect a model:`,
562
+ components: [actionRow],
563
+ });
564
+ return;
565
+ }
566
+ // Build full model ID: provider_id/model_id
567
+ const fullModelId = `${context.providerId}/${selectedModelId}`;
568
+ try {
569
+ context.selectedModelId = fullModelId;
570
+ setModelContext(contextHash, context);
571
+ // Check if model has variants (thinking levels) - if so, show variant picker first
572
+ const getClient = await initializeOpencodeForDirectory(context.dir);
573
+ if (!(getClient instanceof Error)) {
574
+ const providersResponse = await getClient().provider.list({
575
+ directory: context.dir,
576
+ });
577
+ if (providersResponse.data) {
578
+ const variants = getThinkingValuesForModel({
579
+ providers: providersResponse.data.all,
580
+ providerId: context.providerId,
581
+ modelId: selectedModelId,
582
+ });
583
+ if (variants.length > 0) {
584
+ context.availableVariants = variants;
585
+ setModelContext(contextHash, context);
586
+ const variantOptions = [
587
+ {
588
+ label: 'None (default)',
589
+ value: '__none__',
590
+ description: 'Use the model without a specific thinking level',
591
+ },
592
+ ...variants.slice(0, 24).map((v) => ({
593
+ label: v.slice(0, 100),
594
+ value: v,
595
+ description: `Use ${v} thinking`.slice(0, 100),
596
+ })),
597
+ ];
598
+ const selectMenu = new StringSelectMenuBuilder()
599
+ .setCustomId(`model_variant:${contextHash}`)
600
+ .setPlaceholder('Select a thinking level')
601
+ .addOptions(variantOptions);
602
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
603
+ await interaction.editReply({
604
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nSelect a thinking level:`,
605
+ components: [actionRow],
606
+ });
607
+ return;
608
+ }
609
+ }
610
+ }
611
+ // No variants available - skip to scope
612
+ context.selectedVariant = null;
613
+ setModelContext(contextHash, context);
614
+ await showScopeMenu({ interaction, contextHash, context });
615
+ }
616
+ catch (error) {
617
+ modelLogger.error('Error saving model preference:', error);
618
+ await interaction.editReply({
619
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
620
+ components: [],
621
+ });
622
+ }
623
+ }
624
+ /**
625
+ * Handle the variant select menu interaction.
626
+ * Stores the selected variant and shows the scope menu.
627
+ */
628
+ export async function handleModelVariantSelectMenu(interaction) {
629
+ const customId = interaction.customId;
630
+ if (!customId.startsWith('model_variant:')) {
631
+ return;
632
+ }
633
+ await interaction.deferUpdate();
634
+ const contextHash = customId.replace('model_variant:', '');
635
+ const context = pendingModelContexts.get(contextHash);
636
+ if (!context || !context.selectedModelId) {
637
+ await interaction.editReply({
638
+ content: 'Selection expired. Please run /model again.',
639
+ components: [],
640
+ });
641
+ return;
642
+ }
643
+ const selectedValue = interaction.values[0];
644
+ if (!selectedValue) {
645
+ await interaction.editReply({
646
+ content: 'No variant selected',
647
+ components: [],
648
+ });
649
+ return;
650
+ }
651
+ context.selectedVariant = selectedValue === '__none__' ? null : selectedValue;
652
+ setModelContext(contextHash, context);
653
+ await showScopeMenu({ interaction, contextHash, context });
654
+ }
655
+ async function showScopeMenu({ interaction, contextHash, context, }) {
656
+ const modelId = context.selectedModelId;
657
+ const modelDisplay = modelId.split('/')[1] || modelId;
658
+ const variantSuffix = context.selectedVariant
659
+ ? ` (${context.selectedVariant})`
660
+ : '';
661
+ const scopeOptions = [
662
+ ...(context.isThread && context.sessionId
663
+ ? [
664
+ {
665
+ label: 'This session only',
666
+ value: 'session',
667
+ description: 'Override for this session only',
668
+ },
669
+ ]
670
+ : []),
671
+ {
672
+ label: 'This channel only',
673
+ value: 'channel',
674
+ description: 'Override for this channel only',
675
+ },
676
+ {
677
+ label: 'Global default',
678
+ value: 'global',
679
+ description: 'Set for this channel and as default for all others',
680
+ },
681
+ ];
682
+ const selectMenu = new StringSelectMenuBuilder()
683
+ .setCustomId(`model_scope:${contextHash}`)
684
+ .setPlaceholder('Apply to...')
685
+ .addOptions(scopeOptions);
686
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
687
+ await interaction.editReply({
688
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nApply to:`,
689
+ components: [actionRow],
690
+ });
691
+ }
692
+ /**
693
+ * Handle the scope select menu interaction.
694
+ * Applies the model to either the channel or globally.
695
+ */
696
+ export async function handleModelScopeSelectMenu(interaction) {
697
+ const customId = interaction.customId;
698
+ if (!customId.startsWith('model_scope:')) {
699
+ return;
700
+ }
701
+ // Defer update immediately
702
+ await interaction.deferUpdate();
703
+ const contextHash = customId.replace('model_scope:', '');
704
+ const context = pendingModelContexts.get(contextHash);
705
+ if (!context ||
706
+ !context.providerId ||
707
+ !context.providerName ||
708
+ !context.selectedModelId) {
709
+ await interaction.editReply({
710
+ content: 'Selection expired. Please run /model again.',
711
+ components: [],
712
+ });
713
+ return;
714
+ }
715
+ const selectedScope = interaction.values[0];
716
+ if (!selectedScope) {
717
+ await interaction.editReply({
718
+ content: 'No scope selected',
719
+ components: [],
720
+ });
721
+ return;
722
+ }
723
+ const modelId = context.selectedModelId;
724
+ const modelDisplay = modelId.split('/')[1] || modelId;
725
+ const variant = context.selectedVariant ?? null;
726
+ const variantSuffix = variant ? ` (${variant})` : '';
727
+ const agentTip = '\n_Tip: create [agent .md files](https://github.com/otto-assistant/bridge/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_';
728
+ try {
729
+ if (selectedScope === 'session') {
730
+ if (!context.sessionId) {
731
+ deleteModelContext(contextHash);
732
+ await interaction.editReply({
733
+ content: 'No active session in this thread. Please run /model in a thread with a session.',
734
+ components: [],
735
+ });
736
+ return;
737
+ }
738
+ await setSessionModel({ sessionId: context.sessionId, modelId, variant });
739
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`);
740
+ let retried = false;
741
+ if (context.thread) {
742
+ const runtime = getRuntime(context.thread.id);
743
+ if (runtime) {
744
+ retried = await runtime.retryLastUserPrompt();
745
+ }
746
+ }
747
+ const retryNote = retried
748
+ ? '\n_Restarting current request with new model..._'
749
+ : '';
750
+ await interaction.editReply({
751
+ content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${retryNote}${agentTip}`,
752
+ flags: MessageFlags.SuppressEmbeds,
753
+ components: [],
754
+ });
755
+ }
756
+ else if (selectedScope === 'global') {
757
+ if (!context.appId) {
758
+ deleteModelContext(contextHash);
759
+ await interaction.editReply({
760
+ content: 'Cannot set global model: channel is not linked to a bot',
761
+ components: [],
762
+ });
763
+ return;
764
+ }
765
+ await setGlobalModel({ appId: context.appId, modelId, variant });
766
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
767
+ modelLogger.log(`Set global model ${modelId}${variantSuffix} for app ${context.appId} and channel ${context.channelId}`);
768
+ await interaction.editReply({
769
+ content: `Model set for this channel and as global default:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll channels will use this model (unless they have their own override).${agentTip}`,
770
+ flags: MessageFlags.SuppressEmbeds,
771
+ components: [],
772
+ });
773
+ }
774
+ else {
775
+ // channel scope
776
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
777
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for channel ${context.channelId}`);
778
+ await interaction.editReply({
779
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll new sessions in this channel will use this model.${agentTip}`,
780
+ flags: MessageFlags.SuppressEmbeds,
781
+ components: [],
782
+ });
783
+ }
784
+ // Clean up the context from memory
785
+ deleteModelContext(contextHash);
786
+ }
787
+ catch (error) {
788
+ modelLogger.error('Error saving model preference:', error);
789
+ await interaction.editReply({
790
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
791
+ components: [],
792
+ });
793
+ }
794
+ }