@otto-assistant/otto 0.1.2 → 0.7.16

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 +655 -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 +893 -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 +369 -0
  47. package/dist/commands/model.js +798 -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 +179 -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 +1124 -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 +789 -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 +1181 -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 +488 -0
  324. package/src/commands/model.ts +1082 -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 +1507 -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 +232 -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 +1462 -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,78 @@
1
+ // /session-id command - Show current session ID and an opencode attach command.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { getOpencodeServerPort, initializeOpencodeForDirectory, } from '../opencode.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.SESSION);
8
+ function shellQuote(value) {
9
+ if (!value) {
10
+ return "''";
11
+ }
12
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
13
+ }
14
+ export async function handleSessionIdCommand({ command, }) {
15
+ const channel = command.channel;
16
+ if (!channel) {
17
+ await command.reply({
18
+ content: 'This command can only be used in a channel',
19
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
20
+ });
21
+ return;
22
+ }
23
+ const isThread = [
24
+ ChannelType.PublicThread,
25
+ ChannelType.PrivateThread,
26
+ ChannelType.AnnouncementThread,
27
+ ].includes(channel.type);
28
+ if (!isThread) {
29
+ await command.reply({
30
+ content: 'This command can only be used in a thread with an active session',
31
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
+ });
33
+ return;
34
+ }
35
+ const resolved = await resolveWorkingDirectory({
36
+ channel: channel,
37
+ });
38
+ if (!resolved) {
39
+ await command.reply({
40
+ content: 'Could not determine project directory for this channel',
41
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
42
+ });
43
+ return;
44
+ }
45
+ const { projectDirectory, workingDirectory } = resolved;
46
+ const sessionId = await getThreadSession(channel.id);
47
+ if (!sessionId) {
48
+ await command.reply({
49
+ content: 'No active session in this thread',
50
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
51
+ });
52
+ return;
53
+ }
54
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
55
+ let port = getOpencodeServerPort(projectDirectory);
56
+ if (!port) {
57
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
58
+ if (getClient instanceof Error) {
59
+ await command.editReply({
60
+ content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
61
+ });
62
+ return;
63
+ }
64
+ port = getOpencodeServerPort(projectDirectory);
65
+ }
66
+ if (!port) {
67
+ await command.editReply({
68
+ content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
69
+ });
70
+ return;
71
+ }
72
+ const attachUrl = `http://127.0.0.1:${port}`;
73
+ const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`;
74
+ await command.editReply({
75
+ content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
76
+ });
77
+ logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`);
78
+ }
@@ -0,0 +1,176 @@
1
+ // /new-session command - Start a new OpenCode session.
2
+ import { ChannelType } from 'discord.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { getChannelDirectory } from '../database.js';
6
+ import { initializeOpencodeForDirectory } from '../opencode.js';
7
+ import { SILENT_MESSAGE_FLAGS, resolveProjectDirectoryFromAutocomplete } from '../discord-utils.js';
8
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ import * as errore from 'errore';
11
+ const logger = createLogger(LogPrefix.SESSION);
12
+ export async function handleSessionCommand({ command, appId, }) {
13
+ await command.deferReply();
14
+ const prompt = command.options.getString('prompt', true);
15
+ const filesString = command.options.getString('files') || '';
16
+ const agent = command.options.getString('agent') || undefined;
17
+ const channel = command.channel;
18
+ if (!channel || channel.type !== ChannelType.GuildText) {
19
+ await command.editReply('This command can only be used in text channels');
20
+ return;
21
+ }
22
+ const textChannel = channel;
23
+ const channelConfig = await getChannelDirectory(textChannel.id);
24
+ const projectDirectory = channelConfig?.directory;
25
+ if (!projectDirectory) {
26
+ await command.editReply('This channel is not configured with a project directory');
27
+ return;
28
+ }
29
+ if (!fs.existsSync(projectDirectory)) {
30
+ await command.editReply(`Directory does not exist: ${projectDirectory}`);
31
+ return;
32
+ }
33
+ try {
34
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
35
+ if (getClient instanceof Error) {
36
+ await command.editReply(getClient.message);
37
+ return;
38
+ }
39
+ const files = filesString
40
+ .split(',')
41
+ .map((f) => f.trim())
42
+ .filter((f) => f);
43
+ let fullPrompt = prompt;
44
+ if (files.length > 0) {
45
+ fullPrompt = `${prompt}\n\n@${files.join(' @')}`;
46
+ }
47
+ const starterMessage = await textChannel.send({
48
+ content: `🚀 **Starting OpenCode session**\n📝 ${prompt}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
49
+ flags: SILENT_MESSAGE_FLAGS,
50
+ });
51
+ const thread = await starterMessage.startThread({
52
+ name: prompt.slice(0, 100),
53
+ autoArchiveDuration: 1440,
54
+ reason: 'OpenCode session',
55
+ });
56
+ // Add user to thread so it appears in their sidebar
57
+ await thread.members.add(command.user.id);
58
+ await command.editReply(`Created new session in ${thread.toString()}`);
59
+ const runtime = getOrCreateRuntime({
60
+ threadId: thread.id,
61
+ thread,
62
+ projectDirectory,
63
+ sdkDirectory: projectDirectory,
64
+ channelId: textChannel.id,
65
+ appId,
66
+ });
67
+ await runtime.enqueueIncoming({
68
+ prompt: fullPrompt,
69
+ userId: command.user.id,
70
+ username: command.user.displayName,
71
+ agent,
72
+ appId,
73
+ mode: 'opencode',
74
+ });
75
+ }
76
+ catch (error) {
77
+ logger.error('[SESSION] Error:', error);
78
+ await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
79
+ }
80
+ }
81
+ async function handleAgentAutocomplete({ interaction, }) {
82
+ const focusedValue = interaction.options.getFocused();
83
+ // interaction.channel can be null when the channel isn't cached
84
+ // (common with gateway-proxy). Use channelId which is always available
85
+ // from the raw interaction payload.
86
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
87
+ if (!projectDirectory) {
88
+ await interaction.respond([]);
89
+ return;
90
+ }
91
+ try {
92
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
93
+ if (getClient instanceof Error) {
94
+ await interaction.respond([]);
95
+ return;
96
+ }
97
+ const agentsResponse = await getClient().app.agents({
98
+ directory: projectDirectory,
99
+ });
100
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
101
+ await interaction.respond([]);
102
+ return;
103
+ }
104
+ const agents = agentsResponse.data
105
+ .filter((a) => {
106
+ const hidden = a.hidden;
107
+ return (a.mode === 'primary' || a.mode === 'all') && !hidden;
108
+ })
109
+ .filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
110
+ .slice(0, 25);
111
+ const choices = agents.map((agent) => ({
112
+ name: agent.name.slice(0, 100),
113
+ value: agent.name,
114
+ }));
115
+ await interaction.respond(choices);
116
+ }
117
+ catch (error) {
118
+ logger.error('[AUTOCOMPLETE] Error fetching agents:', error);
119
+ await interaction.respond([]);
120
+ }
121
+ }
122
+ export async function handleSessionAutocomplete({ interaction, }) {
123
+ const focusedOption = interaction.options.getFocused(true);
124
+ if (focusedOption.name === 'agent') {
125
+ await handleAgentAutocomplete({ interaction });
126
+ return;
127
+ }
128
+ if (focusedOption.name !== 'files') {
129
+ return;
130
+ }
131
+ const focusedValue = focusedOption.value;
132
+ const parts = focusedValue.split(',');
133
+ const previousFiles = parts
134
+ .slice(0, -1)
135
+ .map((f) => f.trim())
136
+ .filter((f) => f);
137
+ const currentQuery = (parts[parts.length - 1] || '').trim();
138
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
139
+ if (!projectDirectory) {
140
+ await interaction.respond([]);
141
+ return;
142
+ }
143
+ try {
144
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
145
+ if (getClient instanceof Error) {
146
+ await interaction.respond([]);
147
+ return;
148
+ }
149
+ const response = await getClient().find.files({
150
+ query: currentQuery || '',
151
+ });
152
+ const files = response.data || [];
153
+ const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : '';
154
+ const choices = files
155
+ .map((file) => {
156
+ const fullValue = prefix + file;
157
+ const allFiles = [...previousFiles, file];
158
+ const allBasenames = allFiles.map((f) => f.split('/').pop() || f);
159
+ let displayName = allBasenames.join(', ');
160
+ if (displayName.length > 100) {
161
+ displayName = '…' + displayName.slice(-97);
162
+ }
163
+ return {
164
+ name: displayName,
165
+ value: fullValue,
166
+ };
167
+ })
168
+ .filter((choice) => choice.value.length <= 100)
169
+ .slice(0, 25);
170
+ await interaction.respond(choices);
171
+ }
172
+ catch (error) {
173
+ logger.error('[AUTOCOMPLETE] Error fetching files:', error);
174
+ await interaction.respond([]);
175
+ }
176
+ }
@@ -0,0 +1,80 @@
1
+ // /share command - Share the current session as a public URL.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.SHARE);
8
+ export async function handleShareCommand({ command, }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
14
+ });
15
+ return;
16
+ }
17
+ const isThread = [
18
+ ChannelType.PublicThread,
19
+ ChannelType.PrivateThread,
20
+ ChannelType.AnnouncementThread,
21
+ ].includes(channel.type);
22
+ if (!isThread) {
23
+ await command.reply({
24
+ content: 'This command can only be used in a thread with an active session',
25
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
26
+ });
27
+ return;
28
+ }
29
+ const resolved = await resolveWorkingDirectory({
30
+ channel: channel,
31
+ });
32
+ if (!resolved) {
33
+ await command.reply({
34
+ content: 'Could not determine project directory for this channel',
35
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
36
+ });
37
+ return;
38
+ }
39
+ const { projectDirectory } = resolved;
40
+ const sessionId = await getThreadSession(channel.id);
41
+ if (!sessionId) {
42
+ await command.reply({
43
+ content: 'No active session in this thread',
44
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
45
+ });
46
+ return;
47
+ }
48
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
49
+ if (getClient instanceof Error) {
50
+ await command.reply({
51
+ content: `Failed to share session: ${getClient.message}`,
52
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
53
+ });
54
+ return;
55
+ }
56
+ try {
57
+ const response = await getClient().session.share({
58
+ sessionID: sessionId,
59
+ });
60
+ if (!response.data?.share?.url) {
61
+ await command.reply({
62
+ content: 'Failed to generate share URL',
63
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
64
+ });
65
+ return;
66
+ }
67
+ await command.reply({
68
+ content: `🔗 **Session shared:** ${response.data.share.url}`,
69
+ flags: SILENT_MESSAGE_FLAGS,
70
+ });
71
+ logger.log(`Session ${sessionId} shared: ${response.data.share.url}`);
72
+ }
73
+ catch (error) {
74
+ logger.error('[SHARE] Error:', error);
75
+ await command.reply({
76
+ content: `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`,
77
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,205 @@
1
+ // /tasks command — list all scheduled tasks sorted by next run time.
2
+ // Renders a markdown table that the CV2 pipeline auto-formats for Discord,
3
+ // including HTML-backed action buttons for cancellable tasks.
4
+ import { ButtonInteraction, ChatInputCommandInteraction, ComponentType, MessageFlags, } from 'discord.js';
5
+ import { cancelScheduledTask, listScheduledTasks, } from '../database.js';
6
+ import { splitTablesFromMarkdown } from '../format-tables.js';
7
+ import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
8
+ import { formatTimeAgo } from './worktrees.js';
9
+ function formatTimeUntil(date) {
10
+ const diffMs = date.getTime() - Date.now();
11
+ if (diffMs <= 0) {
12
+ return 'due now';
13
+ }
14
+ const totalSeconds = Math.floor(diffMs / 1000);
15
+ if (totalSeconds < 60) {
16
+ return `in ${totalSeconds}s`;
17
+ }
18
+ const totalMinutes = Math.floor(totalSeconds / 60);
19
+ if (totalMinutes < 60) {
20
+ return `in ${totalMinutes}m`;
21
+ }
22
+ const hours = Math.floor(totalMinutes / 60);
23
+ const minutes = totalMinutes % 60;
24
+ if (hours < 24) {
25
+ return minutes > 0 ? `in ${hours}h ${minutes}m` : `in ${hours}h`;
26
+ }
27
+ const days = Math.floor(hours / 24);
28
+ const remainingHours = hours % 24;
29
+ return remainingHours > 0 ? `in ${days}d ${remainingHours}h` : `in ${days}d`;
30
+ }
31
+ function scheduleLabel(task) {
32
+ if (task.schedule_kind === 'cron') {
33
+ return task.cron_expr || 'cron';
34
+ }
35
+ return 'one-time';
36
+ }
37
+ function canCancelTask(task) {
38
+ return task.status === 'planned' || task.status === 'running';
39
+ }
40
+ // Escape pipe chars and collapse whitespace so free-text fields don't break
41
+ // GFM table column alignment.
42
+ function sanitizeTableCell(value) {
43
+ return value.replaceAll('|', '\\|').replace(/\s+/g, ' ').trim();
44
+ }
45
+ function buildCancelButtonHtml({ buttonId }) {
46
+ return `<button id="${buttonId}" variant="secondary">Delete</button>`;
47
+ }
48
+ function buildActionCell(task) {
49
+ if (!canCancelTask(task)) {
50
+ return '-';
51
+ }
52
+ return buildCancelButtonHtml({ buttonId: `cancel-task-${task.id}` });
53
+ }
54
+ // Cap rows to avoid exceeding Discord's 40-component CV2 limit.
55
+ // Each cancellable row renders as text + action row + button (~4 components),
56
+ // so 10 rows is a safe ceiling.
57
+ const MAX_TASK_ROWS = 10;
58
+ function buildTaskTable({ tasks, }) {
59
+ const header = '| ID | Status | Prompt | Schedule | Next Run | Action |';
60
+ const separator = '|---|---|---|---|---|---|';
61
+ const rows = tasks.map((task) => {
62
+ const id = String(task.id);
63
+ const status = task.status;
64
+ const prompt = sanitizeTableCell(task.prompt_preview.length > 240
65
+ ? task.prompt_preview.slice(0, 237) + '...'
66
+ : task.prompt_preview);
67
+ const schedule = sanitizeTableCell(scheduleLabel(task));
68
+ const nextRun = (() => {
69
+ if (task.status === 'completed' ||
70
+ task.status === 'cancelled' ||
71
+ task.status === 'failed') {
72
+ return task.last_run_at ? formatTimeAgo(task.last_run_at) : '-';
73
+ }
74
+ return formatTimeUntil(task.next_run_at);
75
+ })();
76
+ const action = buildActionCell(task);
77
+ return `| ${id} | ${status} | ${prompt} | ${schedule} | ${nextRun} | ${action} |`;
78
+ });
79
+ return [header, separator, ...rows].join('\n');
80
+ }
81
+ function getTasksActionOwnerKey({ userId, channelId, }) {
82
+ return `tasks:${userId}:${channelId}`;
83
+ }
84
+ async function renderTasksReply({ guildId, userId, channelId, showAll, notice, editReply, }) {
85
+ const ownerKey = getTasksActionOwnerKey({ userId, channelId });
86
+ cancelHtmlActionsForOwner(ownerKey);
87
+ const statuses = showAll
88
+ ? undefined
89
+ : ['planned', 'running'];
90
+ const allTasks = await listScheduledTasks({ statuses });
91
+ if (allTasks.length === 0) {
92
+ const message = notice
93
+ ? `${notice}\n\nNo scheduled tasks found.`
94
+ : 'No scheduled tasks found.';
95
+ const textDisplay = {
96
+ type: ComponentType.TextDisplay,
97
+ content: message,
98
+ };
99
+ await editReply({
100
+ components: [textDisplay],
101
+ flags: MessageFlags.IsComponentsV2,
102
+ });
103
+ return;
104
+ }
105
+ const tasks = allTasks.slice(0, MAX_TASK_ROWS);
106
+ const truncatedNotice = allTasks.length > MAX_TASK_ROWS
107
+ ? `Showing ${MAX_TASK_ROWS}/${allTasks.length} tasks. Use \`otto task list\` for full list.`
108
+ : undefined;
109
+ const combinedNotice = [notice, truncatedNotice].filter(Boolean).join('\n');
110
+ const cancellableTasksByButtonId = new Map();
111
+ tasks.forEach((task) => {
112
+ if (!canCancelTask(task)) {
113
+ return;
114
+ }
115
+ cancellableTasksByButtonId.set(`cancel-task-${task.id}`, task);
116
+ });
117
+ const tableMarkdown = buildTaskTable({ tasks });
118
+ const markdown = combinedNotice
119
+ ? `${combinedNotice}\n\n${tableMarkdown}`
120
+ : tableMarkdown;
121
+ const segments = splitTablesFromMarkdown(markdown, {
122
+ resolveButtonCustomId: ({ button }) => {
123
+ const task = cancellableTasksByButtonId.get(button.id);
124
+ if (!task) {
125
+ return new Error(`No task registered for button ${button.id}`);
126
+ }
127
+ const actionId = registerHtmlAction({
128
+ ownerKey,
129
+ threadId: String(task.id),
130
+ run: async ({ interaction }) => {
131
+ await handleCancelTaskAction({
132
+ interaction,
133
+ taskId: task.id,
134
+ showAll,
135
+ });
136
+ },
137
+ });
138
+ return buildHtmlActionCustomId(actionId);
139
+ },
140
+ });
141
+ const components = segments.flatMap((segment) => {
142
+ if (segment.type === 'components') {
143
+ return segment.components;
144
+ }
145
+ const textDisplay = {
146
+ type: ComponentType.TextDisplay,
147
+ content: segment.text,
148
+ };
149
+ return [textDisplay];
150
+ });
151
+ await editReply({
152
+ components,
153
+ flags: MessageFlags.IsComponentsV2,
154
+ });
155
+ }
156
+ async function handleCancelTaskAction({ interaction, taskId, showAll, }) {
157
+ const guildId = interaction.guildId;
158
+ if (!guildId) {
159
+ await interaction.editReply({
160
+ components: [
161
+ {
162
+ type: ComponentType.TextDisplay,
163
+ content: 'This action can only be used in a server.',
164
+ },
165
+ ],
166
+ flags: MessageFlags.IsComponentsV2,
167
+ });
168
+ return;
169
+ }
170
+ const cancelled = await cancelScheduledTask(taskId);
171
+ const notice = cancelled
172
+ ? `Cancelled task #${taskId}.`
173
+ : `Task #${taskId} not found or already finalized.`;
174
+ await renderTasksReply({
175
+ guildId,
176
+ userId: interaction.user.id,
177
+ channelId: interaction.channelId,
178
+ showAll,
179
+ notice,
180
+ editReply: (options) => {
181
+ return interaction.editReply(options);
182
+ },
183
+ });
184
+ }
185
+ export async function handleTasksCommand({ command, }) {
186
+ const guildId = command.guildId;
187
+ if (!guildId) {
188
+ await command.reply({
189
+ content: 'This command can only be used in a server.',
190
+ flags: MessageFlags.Ephemeral,
191
+ });
192
+ return;
193
+ }
194
+ const showAll = command.options.getBoolean('all') ?? false;
195
+ await command.deferReply({ flags: MessageFlags.Ephemeral });
196
+ await renderTasksReply({
197
+ guildId,
198
+ userId: command.user.id,
199
+ channelId: command.channelId,
200
+ showAll,
201
+ editReply: (options) => {
202
+ return command.editReply(options);
203
+ },
204
+ });
205
+ }
@@ -0,0 +1,50 @@
1
+ import { ChatInputCommandInteraction, MessageFlags, } from 'discord.js';
2
+ import { getThreadDeletionSyncMode, resetThreadDeletionSyncMode, setThreadDeletionSyncMode, } from '../database.js';
3
+ import { createLogger, LogPrefix } from '../logger.js';
4
+ const threadDeletionSyncLogger = createLogger(LogPrefix.CLI);
5
+ export const THREAD_DELETION_SYNC_CHOICES = [
6
+ { name: 'soft', value: 'soft' },
7
+ { name: 'hard', value: 'hard' },
8
+ { name: 'reset', value: 'reset' },
9
+ ];
10
+ function getModeLabel(mode) {
11
+ if (mode === 'hard') {
12
+ return 'hard';
13
+ }
14
+ return 'soft';
15
+ }
16
+ export async function handleThreadDeletionSyncCommand({ command, appId, }) {
17
+ const selectedMode = command.options.getString('mode');
18
+ if (!selectedMode) {
19
+ const currentMode = await getThreadDeletionSyncMode({ appId });
20
+ await command.reply({
21
+ content: `Thread deletion sync mode is **${getModeLabel(currentMode)}**.`,
22
+ flags: MessageFlags.Ephemeral,
23
+ });
24
+ return;
25
+ }
26
+ if (selectedMode === 'reset') {
27
+ await resetThreadDeletionSyncMode({ appId });
28
+ threadDeletionSyncLogger.log(`[THREAD_DELETION_SYNC] Reset mode to default for app ${appId}`);
29
+ await command.reply({
30
+ content: 'Thread deletion sync mode reset to **soft** (default).\nSoft keeps the OpenCode session and only archives/aborts it.',
31
+ flags: MessageFlags.Ephemeral,
32
+ });
33
+ return;
34
+ }
35
+ if (selectedMode !== 'soft' && selectedMode !== 'hard') {
36
+ await command.reply({
37
+ content: `Invalid mode: ${selectedMode}`,
38
+ flags: MessageFlags.Ephemeral,
39
+ });
40
+ return;
41
+ }
42
+ await setThreadDeletionSyncMode({ appId, mode: selectedMode });
43
+ threadDeletionSyncLogger.log(`[THREAD_DELETION_SYNC] Set mode ${selectedMode} for app ${appId}`);
44
+ await command.reply({
45
+ content: selectedMode === 'hard'
46
+ ? 'Thread deletion sync mode set to **hard**.\nDeleting a Discord thread will delete the mapped OpenCode session.'
47
+ : 'Thread deletion sync mode set to **soft**.\nDeleting a Discord thread will archive/abort the mapped OpenCode session.',
48
+ flags: MessageFlags.Ephemeral,
49
+ });
50
+ }
@@ -0,0 +1,2 @@
1
+ // Shared types for command handlers.
2
+ export {};