@otto-assistant/otto 0.1.2 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (638) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,65 @@
1
+ // /abort command - Abort the current OpenCode request in this thread.
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 { getRuntime } from '../session-handler/thread-session-runtime.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ const logger = createLogger(LogPrefix.ABORT);
9
+ export async function handleAbortCommand({ command, }) {
10
+ const channel = command.channel;
11
+ if (!channel) {
12
+ await command.reply({
13
+ content: 'This command can only be used in a channel',
14
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
15
+ });
16
+ return;
17
+ }
18
+ const isThread = [
19
+ ChannelType.PublicThread,
20
+ ChannelType.PrivateThread,
21
+ ChannelType.AnnouncementThread,
22
+ ].includes(channel.type);
23
+ if (!isThread) {
24
+ await command.reply({
25
+ content: 'This command can only be used in a thread with an active session',
26
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
27
+ });
28
+ return;
29
+ }
30
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
31
+ const resolved = await resolveWorkingDirectory({
32
+ channel: channel,
33
+ });
34
+ if (!resolved) {
35
+ await command.editReply('Could not determine project directory for this channel');
36
+ return;
37
+ }
38
+ const { projectDirectory } = resolved;
39
+ const sessionId = await getThreadSession(channel.id);
40
+ if (!sessionId) {
41
+ await command.editReply('No active session in this thread');
42
+ return;
43
+ }
44
+ // abortActiveRun delegates to session.abort(), run settlement stays event-driven.
45
+ const runtime = getRuntime(channel.id);
46
+ if (runtime) {
47
+ runtime.abortActiveRun('user-requested');
48
+ }
49
+ else {
50
+ // No runtime but session exists — fall back to direct API abort
51
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
52
+ if (getClient instanceof Error) {
53
+ await command.editReply(`Failed to abort: ${getClient.message}`);
54
+ return;
55
+ }
56
+ try {
57
+ await getClient().session.abort({ sessionID: sessionId });
58
+ }
59
+ catch (error) {
60
+ logger.error('[ABORT] API abort failed:', error);
61
+ }
62
+ }
63
+ await command.editReply('Request **aborted**');
64
+ logger.log(`Session ${sessionId} aborted by user`);
65
+ }
@@ -0,0 +1,245 @@
1
+ // Action button tool handler - Shows Discord buttons for quick model actions.
2
+ // Used by the otto_action_buttons tool to render up to 3 buttons and route
3
+ // button clicks back into the session as a new user message.
4
+ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, } from 'discord.js';
5
+ import crypto from 'node:crypto';
6
+ import { getThreadSession } from '../database.js';
7
+ import { NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
8
+ import { createLogger } from '../logger.js';
9
+ import { notifyError } from '../sentry.js';
10
+ import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
11
+ const logger = createLogger('ACT_BTN');
12
+ const PENDING_TTL_MS = 24 * 60 * 60 * 1000;
13
+ export const pendingActionButtonContexts = new Map();
14
+ const pendingActionButtonRequests = new Map();
15
+ const pendingActionButtonRequestWaiters = new Map();
16
+ export function queueActionButtonsRequest(request) {
17
+ pendingActionButtonRequests.set(request.sessionId, request);
18
+ const waiter = pendingActionButtonRequestWaiters.get(request.sessionId);
19
+ if (!waiter) {
20
+ return;
21
+ }
22
+ pendingActionButtonRequestWaiters.delete(request.sessionId);
23
+ waiter(request);
24
+ }
25
+ export async function waitForQueuedActionButtonsRequest({ sessionId, timeoutMs, }) {
26
+ const queued = pendingActionButtonRequests.get(sessionId);
27
+ if (queued) {
28
+ pendingActionButtonRequests.delete(sessionId);
29
+ return queued;
30
+ }
31
+ return await new Promise((resolve) => {
32
+ const timeout = setTimeout(() => {
33
+ const currentWaiter = pendingActionButtonRequestWaiters.get(sessionId);
34
+ if (!currentWaiter || currentWaiter !== onRequest) {
35
+ return;
36
+ }
37
+ pendingActionButtonRequestWaiters.delete(sessionId);
38
+ resolve(undefined);
39
+ }, timeoutMs);
40
+ const onRequest = (request) => {
41
+ clearTimeout(timeout);
42
+ pendingActionButtonRequests.delete(sessionId);
43
+ resolve(request);
44
+ };
45
+ pendingActionButtonRequestWaiters.set(sessionId, onRequest);
46
+ });
47
+ }
48
+ function toButtonStyle(color) {
49
+ if (color === 'blue') {
50
+ return ButtonStyle.Primary;
51
+ }
52
+ if (color === 'green') {
53
+ return ButtonStyle.Success;
54
+ }
55
+ if (color === 'red') {
56
+ return ButtonStyle.Danger;
57
+ }
58
+ return ButtonStyle.Secondary;
59
+ }
60
+ function resolveContext(context) {
61
+ if (context.resolved) {
62
+ return false;
63
+ }
64
+ context.resolved = true;
65
+ clearTimeout(context.timer);
66
+ pendingActionButtonContexts.delete(context.contextHash);
67
+ return true;
68
+ }
69
+ function updateButtonMessage({ context, status, }) {
70
+ if (!context.messageId) {
71
+ return;
72
+ }
73
+ context.thread.messages
74
+ .fetch(context.messageId)
75
+ .then((message) => {
76
+ return message.edit({
77
+ content: `**Action Required**\n${status}`,
78
+ components: [],
79
+ });
80
+ })
81
+ .catch(() => { });
82
+ }
83
+ async function sendClickedActionToModel({ interaction, thread, prompt, }) {
84
+ const resolved = await resolveWorkingDirectory({ channel: thread });
85
+ if (!resolved) {
86
+ throw new Error('Could not resolve project directory for thread');
87
+ }
88
+ const username = interaction.user.globalName || interaction.user.username;
89
+ // Action button clicks use opencode queue mode.
90
+ const runtime = getOrCreateRuntime({
91
+ threadId: thread.id,
92
+ thread,
93
+ projectDirectory: resolved.projectDirectory,
94
+ sdkDirectory: resolved.workingDirectory,
95
+ channelId: thread.parentId || thread.id,
96
+ });
97
+ await runtime.enqueueIncoming({
98
+ prompt,
99
+ userId: interaction.user.id,
100
+ username,
101
+ mode: 'opencode',
102
+ });
103
+ }
104
+ export async function showActionButtons({ thread, sessionId, directory, buttons, silent, }) {
105
+ const safeButtons = buttons
106
+ .slice(0, 3)
107
+ .map((button) => {
108
+ return {
109
+ label: button.label.trim().slice(0, 80),
110
+ color: button.color,
111
+ };
112
+ })
113
+ .filter((button) => {
114
+ return button.label.length > 0;
115
+ });
116
+ if (safeButtons.length === 0) {
117
+ throw new Error('No valid buttons to display');
118
+ }
119
+ const contextHash = crypto.randomBytes(8).toString('hex');
120
+ const timer = setTimeout(() => {
121
+ const current = pendingActionButtonContexts.get(contextHash);
122
+ if (!current || current.resolved) {
123
+ return;
124
+ }
125
+ resolveContext(current);
126
+ updateButtonMessage({ context: current, status: '_Expired_' });
127
+ }, PENDING_TTL_MS);
128
+ const context = {
129
+ sessionId,
130
+ directory,
131
+ thread,
132
+ buttons: safeButtons,
133
+ contextHash,
134
+ resolved: false,
135
+ timer,
136
+ };
137
+ pendingActionButtonContexts.set(contextHash, context);
138
+ const row = new ActionRowBuilder().addComponents(...safeButtons.map((button, index) => {
139
+ return new ButtonBuilder()
140
+ .setCustomId(`action_button:${contextHash}:${index}`)
141
+ .setLabel(button.label)
142
+ .setStyle(toButtonStyle(button.color));
143
+ }));
144
+ try {
145
+ const message = await thread.send({
146
+ content: '**Action Required**',
147
+ components: [row],
148
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
149
+ });
150
+ context.messageId = message.id;
151
+ logger.log(`Showed ${safeButtons.length} action button(s) for session ${sessionId}`);
152
+ }
153
+ catch (error) {
154
+ clearTimeout(timer);
155
+ pendingActionButtonContexts.delete(contextHash);
156
+ throw new Error('Failed to send action buttons', { cause: error });
157
+ }
158
+ }
159
+ export async function handleActionButton(interaction) {
160
+ const customId = interaction.customId;
161
+ if (!customId.startsWith('action_button:')) {
162
+ return;
163
+ }
164
+ const [, contextHash, indexPart] = customId.split(':');
165
+ if (!contextHash || !indexPart) {
166
+ await interaction.reply({
167
+ content: 'Invalid action button.',
168
+ flags: MessageFlags.Ephemeral,
169
+ });
170
+ return;
171
+ }
172
+ const context = pendingActionButtonContexts.get(contextHash);
173
+ if (!context || context.resolved) {
174
+ await interaction.reply({
175
+ content: 'This action is no longer available.',
176
+ flags: MessageFlags.Ephemeral,
177
+ });
178
+ return;
179
+ }
180
+ const buttonIndex = Number.parseInt(indexPart, 10);
181
+ const button = context.buttons[buttonIndex];
182
+ if (!button) {
183
+ await interaction.reply({
184
+ content: 'This action is no longer available.',
185
+ flags: MessageFlags.Ephemeral,
186
+ });
187
+ return;
188
+ }
189
+ await interaction.deferUpdate();
190
+ const claimed = resolveContext(context);
191
+ if (!claimed) {
192
+ return;
193
+ }
194
+ const thread = interaction.channel;
195
+ if (!thread?.isThread()) {
196
+ logger.warn('[ACTION] Button clicked outside thread channel');
197
+ await interaction.editReply({
198
+ content: '**Action Required**\n_This action is no longer available._',
199
+ components: [],
200
+ });
201
+ return;
202
+ }
203
+ const currentSessionId = await getThreadSession(thread.id);
204
+ if (!currentSessionId || currentSessionId !== context.sessionId) {
205
+ await interaction.editReply({
206
+ content: '**Action Required**\n_Expired due to session change._',
207
+ components: [],
208
+ });
209
+ return;
210
+ }
211
+ await interaction.editReply({
212
+ content: `**Action Required**\n_Selected: ${button.label}_`,
213
+ components: [],
214
+ });
215
+ const prompt = `User clicked: ${button.label}`;
216
+ try {
217
+ await sendClickedActionToModel({
218
+ interaction,
219
+ thread,
220
+ prompt,
221
+ });
222
+ }
223
+ catch (error) {
224
+ logger.error('[ACTION] Failed to send click to model:', error);
225
+ void notifyError(error, 'Action button click send to model failed');
226
+ await sendThreadMessage(thread, `Failed to send action click: ${error instanceof Error ? error.message : String(error)}`, { flags: NOTIFY_MESSAGE_FLAGS });
227
+ }
228
+ }
229
+ /**
230
+ * Dismiss pending action buttons for a thread (e.g. user sent a new message).
231
+ * Removes buttons from the message and cleans up context.
232
+ */
233
+ export function cancelPendingActionButtons(threadId) {
234
+ for (const [, ctx] of pendingActionButtonContexts) {
235
+ if (ctx.thread.id !== threadId) {
236
+ continue;
237
+ }
238
+ if (!resolveContext(ctx)) {
239
+ continue;
240
+ }
241
+ updateButtonMessage({ context: ctx, status: '_Buttons dismissed._' });
242
+ return true;
243
+ }
244
+ return false;
245
+ }
@@ -0,0 +1,124 @@
1
+ // /add-dir command - Expand the current session's external_directory permissions.
2
+ // Resolves the requested directory against the active working directory, then
3
+ // updates the current session permission rules via OpenCode.
4
+ import { ChannelType, MessageFlags, } from 'discord.js';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { getThreadSession } from '../database.js';
8
+ import { buildExternalDirectoryPermissionRules, getOpencodeClient, initializeOpencodeForDirectory, } from '../opencode.js';
9
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
10
+ import { createLogger, LogPrefix } from '../logger.js';
11
+ const logger = createLogger(LogPrefix.PERMISSIONS);
12
+ const ALL_DIRECTORIES_PATTERN = '*';
13
+ export function resolveDirectoryPermissionPattern({ input, workingDirectory, }) {
14
+ const trimmedInput = input.trim();
15
+ if (!trimmedInput) {
16
+ return new Error('Directory is required');
17
+ }
18
+ if (trimmedInput === ALL_DIRECTORIES_PATTERN) {
19
+ return ALL_DIRECTORIES_PATTERN;
20
+ }
21
+ const absolutePath = path.resolve(workingDirectory, trimmedInput);
22
+ if (!fs.existsSync(absolutePath)) {
23
+ return new Error(`Directory does not exist: ${absolutePath}`);
24
+ }
25
+ let stats;
26
+ try {
27
+ stats = fs.statSync(absolutePath);
28
+ }
29
+ catch (error) {
30
+ return new Error(`Failed to inspect directory: ${absolutePath}`, { cause: error });
31
+ }
32
+ if (!stats.isDirectory()) {
33
+ return new Error(`Not a directory: ${absolutePath}`);
34
+ }
35
+ return absolutePath.replaceAll('\\', '/');
36
+ }
37
+ export function buildAddDirPermissionRules({ resolvedPattern, }) {
38
+ return buildExternalDirectoryPermissionRules({
39
+ resolvedPattern,
40
+ action: 'allow',
41
+ });
42
+ }
43
+ export async function handleAddDirCommand({ command, }) {
44
+ const channel = command.channel;
45
+ if (!channel) {
46
+ await command.reply({
47
+ content: 'This command can only be used in a channel',
48
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
49
+ });
50
+ return;
51
+ }
52
+ const isThread = [
53
+ ChannelType.PublicThread,
54
+ ChannelType.PrivateThread,
55
+ ChannelType.AnnouncementThread,
56
+ ].includes(channel.type);
57
+ if (!isThread) {
58
+ await command.reply({
59
+ content: 'This command can only be used in a thread with an active session',
60
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
61
+ });
62
+ return;
63
+ }
64
+ const resolvedDirectories = await resolveWorkingDirectory({
65
+ channel: channel,
66
+ });
67
+ if (!resolvedDirectories) {
68
+ await command.reply({
69
+ content: 'Could not determine project directory for this channel',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ });
72
+ return;
73
+ }
74
+ const requestedDirectory = command.options.getString('directory') ?? ALL_DIRECTORIES_PATTERN;
75
+ const resolvedPattern = resolveDirectoryPermissionPattern({
76
+ input: requestedDirectory,
77
+ workingDirectory: resolvedDirectories.workingDirectory,
78
+ });
79
+ if (resolvedPattern instanceof Error) {
80
+ await command.reply({
81
+ content: resolvedPattern.message,
82
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
83
+ });
84
+ return;
85
+ }
86
+ const sessionId = await getThreadSession(channel.id);
87
+ if (!sessionId) {
88
+ await command.reply({
89
+ content: 'No active session in this thread',
90
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
91
+ });
92
+ return;
93
+ }
94
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
95
+ const getClient = await initializeOpencodeForDirectory(resolvedDirectories.projectDirectory);
96
+ if (getClient instanceof Error) {
97
+ await command.editReply(`Failed to update session permissions: ${getClient.message}`);
98
+ return;
99
+ }
100
+ const client = getOpencodeClient(resolvedDirectories.projectDirectory);
101
+ if (!client) {
102
+ await command.editReply('Failed to get OpenCode client');
103
+ return;
104
+ }
105
+ try {
106
+ // SDK types don't include 'permission' yet — upstream added this API
107
+ // for session permission updates (add-dir). Cast to bypass until SDK catches up.
108
+ const updateResponse = await client.session.update({
109
+ sessionID: sessionId,
110
+ permission: buildAddDirPermissionRules({ resolvedPattern }),
111
+ });
112
+ if (updateResponse.error) {
113
+ await command.editReply('Failed to update session permissions');
114
+ return;
115
+ }
116
+ await command.editReply(resolvedPattern === ALL_DIRECTORIES_PATTERN
117
+ ? 'Updated session permissions: all external directories are now allowed'
118
+ : `Updated session permissions: allowed \`${resolvedPattern}\``);
119
+ }
120
+ catch (error) {
121
+ logger.error('[ADD-DIR] Failed to update session permissions:', error);
122
+ await command.editReply(`Failed to update session permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
123
+ }
124
+ }
@@ -0,0 +1,126 @@
1
+ // Tests for /add-dir permission helpers.
2
+ import { describe, expect, test } from 'vitest';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { buildAddDirPermissionRules, resolveDirectoryPermissionPattern, } from './add-dir.js';
7
+ import { buildExternalDirectoryPermissionRules, buildSessionPermissions, } from '../opencode.js';
8
+ describe('resolveDirectoryPermissionPattern', () => {
9
+ test('resolves relative directories against the working directory', () => {
10
+ const root = path.resolve(process.cwd(), 'tmp', 'add-dir-test');
11
+ const nested = path.join(root, 'nested');
12
+ fs.mkdirSync(nested, { recursive: true });
13
+ const result = resolveDirectoryPermissionPattern({
14
+ input: './nested',
15
+ workingDirectory: root,
16
+ });
17
+ expect(result).toBe(nested.replaceAll('\\', '/'));
18
+ });
19
+ test('supports allowing every directory with *', () => {
20
+ expect(resolveDirectoryPermissionPattern({
21
+ input: ' * ',
22
+ workingDirectory: '/repo',
23
+ })).toBe('*');
24
+ expect(buildAddDirPermissionRules({
25
+ resolvedPattern: '*',
26
+ })).toMatchInlineSnapshot(`
27
+ [
28
+ {
29
+ "action": "allow",
30
+ "pattern": "*",
31
+ "permission": "external_directory",
32
+ },
33
+ ]
34
+ `);
35
+ });
36
+ test('builds allow rules for a specific directory', () => {
37
+ expect(buildAddDirPermissionRules({
38
+ resolvedPattern: '/repo/extra',
39
+ })).toMatchInlineSnapshot(`
40
+ [
41
+ {
42
+ "action": "allow",
43
+ "pattern": "/repo/extra",
44
+ "permission": "external_directory",
45
+ },
46
+ {
47
+ "action": "allow",
48
+ "pattern": "/repo/extra/*",
49
+ "permission": "external_directory",
50
+ },
51
+ ]
52
+ `);
53
+ });
54
+ test('builds deny rules for a specific directory', () => {
55
+ expect(buildExternalDirectoryPermissionRules({
56
+ resolvedPattern: '/repo',
57
+ action: 'deny',
58
+ })).toMatchInlineSnapshot(`
59
+ [
60
+ {
61
+ "action": "deny",
62
+ "pattern": "/repo",
63
+ "permission": "external_directory",
64
+ },
65
+ {
66
+ "action": "deny",
67
+ "pattern": "/repo/*",
68
+ "permission": "external_directory",
69
+ },
70
+ ]
71
+ `);
72
+ });
73
+ test('worktree sessions deny the original checkout last', () => {
74
+ expect(buildSessionPermissions({
75
+ directory: '/Users/me/.otto/worktrees/hash/feature',
76
+ originalRepoDirectory: '/Users/me/project',
77
+ }).slice(-2)).toMatchInlineSnapshot(`
78
+ [
79
+ {
80
+ "action": "deny",
81
+ "pattern": "/Users/me/project",
82
+ "permission": "external_directory",
83
+ },
84
+ {
85
+ "action": "deny",
86
+ "pattern": "/Users/me/project/*",
87
+ "permission": "external_directory",
88
+ },
89
+ ]
90
+ `);
91
+ });
92
+ test('pre-allows common toolchain caches under home with ~ patterns', () => {
93
+ const home = os.homedir().replaceAll('\\', '/');
94
+ expect(buildSessionPermissions({
95
+ directory: '/Users/me/project',
96
+ }).filter((rule) => {
97
+ return [
98
+ `${home}/.cache/zig`,
99
+ `${home}/.cargo`,
100
+ `${home}/.cache/go-build`,
101
+ `${home}/go/pkg`,
102
+ ].includes(rule.pattern);
103
+ })).toEqual([
104
+ {
105
+ permission: 'external_directory',
106
+ pattern: `${home}/.cache/zig`,
107
+ action: 'allow',
108
+ },
109
+ {
110
+ permission: 'external_directory',
111
+ pattern: `${home}/.cargo`,
112
+ action: 'allow',
113
+ },
114
+ {
115
+ permission: 'external_directory',
116
+ pattern: `${home}/.cache/go-build`,
117
+ action: 'allow',
118
+ },
119
+ {
120
+ permission: 'external_directory',
121
+ pattern: `${home}/go/pkg`,
122
+ action: 'allow',
123
+ },
124
+ ]);
125
+ });
126
+ });
@@ -0,0 +1,113 @@
1
+ // /add-project command - Create Discord channels for an existing OpenCode project.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { findChannelsByDirectory, getAllTextChannelDirectories, } from '../database.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { createProjectChannels } from '../channel-management.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ import { abbreviatePath } from '../utils.js';
9
+ import * as errore from 'errore';
10
+ const logger = createLogger(LogPrefix.ADD_PROJECT);
11
+ export async function handleAddProjectCommand({ command, }) {
12
+ await command.deferReply();
13
+ const projectId = command.options.getString('project', true);
14
+ const guild = command.guild;
15
+ if (!guild) {
16
+ await command.editReply('This command can only be used in a guild');
17
+ return;
18
+ }
19
+ try {
20
+ const currentDir = process.cwd();
21
+ const getClient = await initializeOpencodeForDirectory(currentDir);
22
+ if (getClient instanceof Error) {
23
+ await command.editReply(getClient.message);
24
+ return;
25
+ }
26
+ const projectsResponse = await getClient().project.list({});
27
+ if (!projectsResponse.data) {
28
+ await command.editReply('Failed to fetch projects');
29
+ return;
30
+ }
31
+ const project = projectsResponse.data.find((p) => p.id === projectId);
32
+ if (!project) {
33
+ await command.editReply('Project not found');
34
+ return;
35
+ }
36
+ const directory = project.worktree;
37
+ if (!fs.existsSync(directory)) {
38
+ await command.editReply(`Directory does not exist: ${directory}`);
39
+ return;
40
+ }
41
+ const existingChannels = await findChannelsByDirectory({
42
+ directory,
43
+ channelType: 'text',
44
+ });
45
+ if (existingChannels.length > 0) {
46
+ await command.editReply(`A channel already exists for this directory: <#${existingChannels[0].channel_id}>`);
47
+ return;
48
+ }
49
+ const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
50
+ guild,
51
+ projectDirectory: directory,
52
+ botName: command.client.user?.username,
53
+ });
54
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : '';
55
+ await command.editReply(`āœ… Created channels for project:\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\nšŸ“ Directory: \`${directory}\``);
56
+ logger.log(`Created channels for project ${channelName} at ${directory}`);
57
+ }
58
+ catch (error) {
59
+ logger.error('[ADD-PROJECT] Error:', error);
60
+ await command.editReply(`Failed to create channels: ${error instanceof Error ? error.message : 'Unknown error'}`);
61
+ }
62
+ }
63
+ export async function handleAddProjectAutocomplete({ interaction, }) {
64
+ const focusedValue = interaction.options.getFocused();
65
+ try {
66
+ const currentDir = process.cwd();
67
+ const getClient = await initializeOpencodeForDirectory(currentDir);
68
+ if (getClient instanceof Error) {
69
+ await interaction.respond([]);
70
+ return;
71
+ }
72
+ const projectsResponse = await getClient().project.list({});
73
+ if (!projectsResponse.data) {
74
+ await interaction.respond([]);
75
+ return;
76
+ }
77
+ const existingDirs = await getAllTextChannelDirectories();
78
+ const existingDirSet = new Set(existingDirs);
79
+ const availableProjects = projectsResponse.data.filter((project) => {
80
+ if (existingDirSet.has(project.worktree)) {
81
+ return false;
82
+ }
83
+ if (path.basename(project.worktree).startsWith('opencode-test-')) {
84
+ return false;
85
+ }
86
+ return true;
87
+ });
88
+ const projects = availableProjects
89
+ .filter((project) => {
90
+ const baseName = path.basename(project.worktree);
91
+ const searchText = `${baseName} ${project.worktree}`.toLowerCase();
92
+ return searchText.includes(focusedValue.toLowerCase());
93
+ })
94
+ .sort((a, b) => {
95
+ const aTime = a.time.initialized || a.time.created;
96
+ const bTime = b.time.initialized || b.time.created;
97
+ return bTime - aTime;
98
+ })
99
+ .slice(0, 25)
100
+ .map((project) => {
101
+ const name = `${path.basename(project.worktree)} (${abbreviatePath(project.worktree)})`;
102
+ return {
103
+ name: name.length > 100 ? name.slice(0, 99) + '…' : name,
104
+ value: project.id,
105
+ };
106
+ });
107
+ await interaction.respond(projects);
108
+ }
109
+ catch (error) {
110
+ logger.error('[AUTOCOMPLETE] Error fetching projects:', error);
111
+ await interaction.respond([]);
112
+ }
113
+ }