@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,275 @@
1
+ // File upload tool handler - Shows Discord modal with FileUploadBuilder.
2
+ // When the AI uses the otto_file_upload tool, the plugin inserts a row into
3
+ // the ipc_requests DB table. The bot polls this table, picks up the request,
4
+ // and shows a button in the thread. User clicks it to open a modal with a
5
+ // native file picker. Uploaded files are downloaded to the project directory.
6
+ // The bot writes file paths back to ipc_requests.response, and the plugin
7
+ // polls until the response appears.
8
+ import { ButtonBuilder, ButtonStyle, ActionRowBuilder, ModalBuilder, FileUploadBuilder, LabelBuilder, ComponentType, MessageFlags, } from 'discord.js';
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { createLogger, LogPrefix } from '../logger.js';
13
+ import { notifyError } from '../sentry.js';
14
+ import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
15
+ const logger = createLogger(LogPrefix.FILE_UPLOAD);
16
+ // 5 minute TTL for pending contexts - if user doesn't click within this time,
17
+ // clean up the context and resolve with empty array to unblock the plugin tool
18
+ const PENDING_TTL_MS = 5 * 60 * 1000;
19
+ export const pendingFileUploadContexts = new Map();
20
+ /**
21
+ * Sanitize an attachment filename to prevent path traversal.
22
+ * Strips directory separators, .., and null bytes from the name.
23
+ * Prepends a short random prefix to avoid collisions between uploads.
24
+ */
25
+ function sanitizeFilename(name) {
26
+ // Extract just the base name (strips any directory components)
27
+ let sanitized = path.basename(name);
28
+ // Remove null bytes and other dangerous characters
29
+ sanitized = sanitized.replace(/[\x00]/g, '');
30
+ // If somehow still empty or just dots, give it a safe name
31
+ if (!sanitized || sanitized === '.' || sanitized === '..') {
32
+ sanitized = 'upload';
33
+ }
34
+ // Prefix with short random id to avoid collisions
35
+ const prefix = crypto.randomBytes(4).toString('hex');
36
+ return `${prefix}-${sanitized}`;
37
+ }
38
+ /**
39
+ * Safely resolve a pending context exactly once. Prevents double-resolve from
40
+ * cancel/submit races by checking the `resolved` flag.
41
+ */
42
+ function resolveContext(context, filePaths) {
43
+ if (context.resolved) {
44
+ return false;
45
+ }
46
+ context.resolved = true;
47
+ clearTimeout(context.timer);
48
+ pendingFileUploadContexts.delete(context.contextHash);
49
+ context.resolve(filePaths);
50
+ return true;
51
+ }
52
+ /**
53
+ * Show a button in the thread that opens a file upload modal when clicked.
54
+ * Returns a promise that resolves with the downloaded file paths.
55
+ */
56
+ export function showFileUploadButton({ thread, sessionId, directory, prompt, maxFiles, }) {
57
+ return new Promise((resolve, reject) => {
58
+ const contextHash = crypto.randomBytes(8).toString('hex');
59
+ // TTL timer: auto-cleanup if user never clicks the button
60
+ const timer = setTimeout(() => {
61
+ const ctx = pendingFileUploadContexts.get(contextHash);
62
+ if (ctx && !ctx.resolved) {
63
+ logger.log(`File upload timed out for session ${sessionId}, hash=${contextHash}`);
64
+ resolveContext(ctx, []);
65
+ // Remove button from message
66
+ if (ctx.messageId) {
67
+ ctx.thread.messages
68
+ .fetch(ctx.messageId)
69
+ .then((msg) => {
70
+ return msg.edit({
71
+ content: `**File Upload Requested**\n${prompt.slice(0, 1900)}\n_Timed out_`,
72
+ components: [],
73
+ });
74
+ })
75
+ .catch(() => { });
76
+ }
77
+ }
78
+ }, PENDING_TTL_MS);
79
+ const context = {
80
+ sessionId,
81
+ directory,
82
+ thread,
83
+ prompt,
84
+ maxFiles,
85
+ contextHash,
86
+ resolve,
87
+ reject,
88
+ resolved: false,
89
+ timer,
90
+ };
91
+ pendingFileUploadContexts.set(contextHash, context);
92
+ const uploadButton = new ButtonBuilder()
93
+ .setCustomId(`file_upload_btn:${contextHash}`)
94
+ .setLabel('Upload Files')
95
+ .setStyle(ButtonStyle.Primary);
96
+ const actionRow = new ActionRowBuilder().addComponents(uploadButton);
97
+ thread
98
+ .send({
99
+ content: `**File Upload Requested**\n${prompt.slice(0, 1900)}`,
100
+ components: [actionRow],
101
+ flags: NOTIFY_MESSAGE_FLAGS,
102
+ })
103
+ .then((msg) => {
104
+ context.messageId = msg.id;
105
+ logger.log(`Showed file upload button for session ${sessionId}, hash=${contextHash}`);
106
+ })
107
+ .catch((err) => {
108
+ clearTimeout(timer);
109
+ pendingFileUploadContexts.delete(contextHash);
110
+ reject(new Error('Failed to send file upload button', { cause: err }));
111
+ });
112
+ });
113
+ }
114
+ /**
115
+ * Handle the file upload button click - opens a modal with FileUploadBuilder.
116
+ */
117
+ export async function handleFileUploadButton(interaction) {
118
+ const customId = interaction.customId;
119
+ if (!customId.startsWith('file_upload_btn:')) {
120
+ return;
121
+ }
122
+ const contextHash = customId.replace('file_upload_btn:', '');
123
+ const context = pendingFileUploadContexts.get(contextHash);
124
+ if (!context || context.resolved) {
125
+ await interaction.reply({
126
+ content: 'This file upload request has expired.',
127
+ flags: MessageFlags.Ephemeral,
128
+ });
129
+ return;
130
+ }
131
+ const fileUpload = new FileUploadBuilder()
132
+ .setCustomId('uploaded_files')
133
+ .setMinValues(1)
134
+ .setMaxValues(context.maxFiles);
135
+ const label = new LabelBuilder()
136
+ .setLabel('Files')
137
+ .setDescription(context.prompt.slice(0, 100))
138
+ .setFileUploadComponent(fileUpload);
139
+ const modal = new ModalBuilder()
140
+ .setCustomId(`file_upload_modal:${contextHash}`)
141
+ .setTitle('Upload Files')
142
+ .addLabelComponents(label);
143
+ await interaction.showModal(modal);
144
+ }
145
+ /**
146
+ * Handle the modal submission - download files and resolve the pending promise.
147
+ */
148
+ export async function handleFileUploadModalSubmit(interaction) {
149
+ const customId = interaction.customId;
150
+ if (!customId.startsWith('file_upload_modal:')) {
151
+ return;
152
+ }
153
+ const contextHash = customId.replace('file_upload_modal:', '');
154
+ const context = pendingFileUploadContexts.get(contextHash);
155
+ if (!context || context.resolved) {
156
+ await interaction.reply({
157
+ content: 'This file upload request has expired.',
158
+ flags: MessageFlags.Ephemeral,
159
+ });
160
+ return;
161
+ }
162
+ try {
163
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
164
+ // File upload data is nested in the LabelModalData -> FileUploadModalData
165
+ const fileField = interaction.fields.getField('uploaded_files', ComponentType.FileUpload);
166
+ const attachments = fileField.attachments;
167
+ if (!attachments || attachments.size === 0) {
168
+ await interaction.editReply({ content: 'No files were uploaded.' });
169
+ updateButtonMessage(context, '_No files uploaded_');
170
+ resolveContext(context, []);
171
+ return;
172
+ }
173
+ const uploadsDir = path.join(context.directory, 'uploads');
174
+ fs.mkdirSync(uploadsDir, { recursive: true });
175
+ const downloadedPaths = [];
176
+ const errors = [];
177
+ for (const [, attachment] of attachments) {
178
+ // Check if context was cancelled (e.g. user sent new message) while
179
+ // we were downloading previous files - stop downloading more
180
+ if (context.resolved) {
181
+ break;
182
+ }
183
+ try {
184
+ const response = await fetch(attachment.url);
185
+ if (!response.ok) {
186
+ errors.push(`Failed to download ${attachment.name}: HTTP ${response.status}`);
187
+ continue;
188
+ }
189
+ const buffer = Buffer.from(await response.arrayBuffer());
190
+ const safeName = sanitizeFilename(attachment.name);
191
+ const filePath = path.join(uploadsDir, safeName);
192
+ fs.writeFileSync(filePath, buffer);
193
+ downloadedPaths.push(filePath);
194
+ }
195
+ catch (err) {
196
+ const msg = err instanceof Error ? err.message : String(err);
197
+ errors.push(`Failed to download ${attachment.name}: ${msg}`);
198
+ }
199
+ }
200
+ // If context was resolved by cancel/timeout during download, don't try to
201
+ // resolve again - just update the ephemeral reply
202
+ if (context.resolved) {
203
+ await interaction.editReply({ content: 'Upload was cancelled.' });
204
+ return;
205
+ }
206
+ const fileNames = downloadedPaths.map((p) => {
207
+ return path.basename(p);
208
+ });
209
+ updateButtonMessage(context, downloadedPaths.length > 0
210
+ ? `Uploaded: ${fileNames.join(', ')}`
211
+ : '_Upload failed_');
212
+ const summary = (() => {
213
+ if (downloadedPaths.length > 0 && errors.length === 0) {
214
+ return `Uploaded ${downloadedPaths.length} file(s) successfully.`;
215
+ }
216
+ if (downloadedPaths.length > 0 && errors.length > 0) {
217
+ return `Uploaded ${downloadedPaths.length} file(s). Errors: ${errors.join('; ')}`;
218
+ }
219
+ return `Upload failed: ${errors.join('; ')}`;
220
+ })();
221
+ await interaction.editReply({ content: summary });
222
+ resolveContext(context, downloadedPaths);
223
+ logger.log(`File upload completed for session ${context.sessionId}: ${downloadedPaths.length} files`);
224
+ }
225
+ catch (err) {
226
+ // Ensure context is always resolved even on unexpected errors
227
+ // so the plugin tool doesn't hang indefinitely
228
+ logger.error('Error in file upload modal submit:', err);
229
+ void notifyError(err, 'File upload modal submit error');
230
+ resolveContext(context, []);
231
+ }
232
+ }
233
+ /**
234
+ * Best-effort update of the original button message (remove button, append status).
235
+ */
236
+ function updateButtonMessage(context, status) {
237
+ if (!context.messageId) {
238
+ return;
239
+ }
240
+ context.thread.messages
241
+ .fetch(context.messageId)
242
+ .then((msg) => {
243
+ return msg.edit({
244
+ content: `**File Upload Requested**\n${context.prompt.slice(0, 1900)}\n${status}`,
245
+ components: [],
246
+ });
247
+ })
248
+ .catch(() => { });
249
+ }
250
+ /**
251
+ * Cancel ALL pending file uploads for a thread (e.g. when user sends a new message).
252
+ */
253
+ export async function cancelPendingFileUpload(threadId) {
254
+ const toCancel = [];
255
+ for (const [, ctx] of pendingFileUploadContexts) {
256
+ if (ctx.thread.id === threadId) {
257
+ toCancel.push(ctx);
258
+ }
259
+ }
260
+ if (toCancel.length === 0) {
261
+ return false;
262
+ }
263
+ let cancelled = 0;
264
+ for (const context of toCancel) {
265
+ const didResolve = resolveContext(context, []);
266
+ if (didResolve) {
267
+ updateButtonMessage(context, '_Cancelled - user sent a new message_');
268
+ cancelled++;
269
+ }
270
+ }
271
+ if (cancelled > 0) {
272
+ logger.log(`Cancelled ${cancelled} file upload(s) for thread ${threadId}`);
273
+ }
274
+ return cancelled > 0;
275
+ }
@@ -0,0 +1,177 @@
1
+ // /fork-subagent command - Fork a subagent task session into a new thread.
2
+ import { ActionRowBuilder, MessageFlags, StringSelectMenuBuilder, ThreadAutoArchiveDuration, } from 'discord.js';
3
+ import { getSessionEventSnapshot, getThreadSession, setThreadSession, } from '../database.js';
4
+ import { resolveTextChannel, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
5
+ import { collectSessionChunks, batchChunksForDiscord, } from '../message-formatting.js';
6
+ import { initializeOpencodeForDirectory } from '../opencode.js';
7
+ import { getDerivedSubagentSessions, } from '../session-handler/event-stream-state.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ import { getThreadChannel, parsePersistedEventRows, } from './fork.js';
10
+ const forkLogger = createLogger(LogPrefix.FORK);
11
+ function truncateLabelPart(text, maxLength) {
12
+ if (text.length <= maxLength) {
13
+ return text;
14
+ }
15
+ if (maxLength <= 1) {
16
+ return text.slice(0, maxLength);
17
+ }
18
+ return `${text.slice(0, maxLength - 1)}…`;
19
+ }
20
+ function getSubagentOptionLabel({ subagentType, description, }) {
21
+ const agent = truncateLabelPart(subagentType || 'task', 24);
22
+ const cleanedDescription = description?.trim() || 'No description';
23
+ const descriptionBudget = Math.max(1, 100 - agent.length - 3);
24
+ const truncatedDescription = truncateLabelPart(cleanedDescription, descriptionBudget);
25
+ return `${agent} · ${truncatedDescription}`;
26
+ }
27
+ export async function handleForkSubagentCommand(interaction) {
28
+ const threadChannel = getThreadChannel(interaction.channel);
29
+ if (threadChannel instanceof Error) {
30
+ await interaction.reply({
31
+ content: threadChannel.message,
32
+ flags: MessageFlags.Ephemeral,
33
+ });
34
+ return;
35
+ }
36
+ const resolved = await resolveWorkingDirectory({
37
+ channel: threadChannel,
38
+ });
39
+ if (!resolved) {
40
+ await interaction.reply({
41
+ content: 'Could not determine project directory for this channel',
42
+ flags: MessageFlags.Ephemeral,
43
+ });
44
+ return;
45
+ }
46
+ const sessionId = await getThreadSession(threadChannel.id);
47
+ if (!sessionId) {
48
+ await interaction.reply({
49
+ content: 'No active session in this thread',
50
+ flags: MessageFlags.Ephemeral,
51
+ });
52
+ return;
53
+ }
54
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
55
+ const rows = await getSessionEventSnapshot({ sessionId });
56
+ const events = parsePersistedEventRows({ rows });
57
+ const subagentSessions = getDerivedSubagentSessions({
58
+ events,
59
+ mainSessionId: sessionId,
60
+ }).slice(0, 25);
61
+ if (subagentSessions.length === 0) {
62
+ await interaction.editReply({
63
+ content: 'No subagent task sessions found in this thread',
64
+ });
65
+ return;
66
+ }
67
+ const options = subagentSessions.map((subagentSession) => ({
68
+ label: getSubagentOptionLabel({
69
+ subagentType: subagentSession.subagentType,
70
+ description: subagentSession.description,
71
+ }),
72
+ value: subagentSession.childSessionId,
73
+ description: new Date(subagentSession.timestamp).toLocaleString().slice(0, 100),
74
+ }));
75
+ const selectMenu = new StringSelectMenuBuilder()
76
+ .setCustomId(`fork_subagent_select:${sessionId}`)
77
+ .setPlaceholder('Select a subagent session to fork')
78
+ .addOptions(options);
79
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
80
+ await interaction.editReply({
81
+ content: '**Fork Subagent Session**\nSelect a subagent task session to fork into a new thread:',
82
+ components: [actionRow],
83
+ });
84
+ }
85
+ export async function handleForkSubagentSelectMenu(interaction) {
86
+ const customId = interaction.customId;
87
+ if (!customId.startsWith('fork_subagent_select:')) {
88
+ return;
89
+ }
90
+ const [, parentSessionId] = customId.split(':');
91
+ if (!parentSessionId) {
92
+ await interaction.reply({
93
+ content: 'Invalid selection data',
94
+ flags: MessageFlags.Ephemeral,
95
+ });
96
+ return;
97
+ }
98
+ const selectedSessionId = interaction.values[0];
99
+ if (!selectedSessionId) {
100
+ await interaction.reply({
101
+ content: 'No subagent session selected',
102
+ flags: MessageFlags.Ephemeral,
103
+ });
104
+ return;
105
+ }
106
+ await interaction.deferReply();
107
+ const threadChannel = getThreadChannel(interaction.channel);
108
+ if (threadChannel instanceof Error) {
109
+ await interaction.editReply(threadChannel.message);
110
+ return;
111
+ }
112
+ const resolved = await resolveWorkingDirectory({
113
+ channel: threadChannel,
114
+ });
115
+ if (!resolved) {
116
+ await interaction.editReply('Could not determine project directory for this channel');
117
+ return;
118
+ }
119
+ const rows = await getSessionEventSnapshot({ sessionId: parentSessionId });
120
+ const events = parsePersistedEventRows({ rows });
121
+ const selectedSubagent = getDerivedSubagentSessions({
122
+ events,
123
+ mainSessionId: parentSessionId,
124
+ }).find((candidate) => {
125
+ return candidate.childSessionId === selectedSessionId;
126
+ });
127
+ const getClient = await initializeOpencodeForDirectory(resolved.projectDirectory);
128
+ if (getClient instanceof Error) {
129
+ await interaction.editReply(`Failed to fork session: ${getClient.message}`);
130
+ return;
131
+ }
132
+ const forkResponse = await getClient().session.fork({
133
+ sessionID: selectedSessionId,
134
+ });
135
+ if (!forkResponse.data) {
136
+ await interaction.editReply('Failed to fork session');
137
+ return;
138
+ }
139
+ const textChannel = await resolveTextChannel(threadChannel);
140
+ if (!textChannel) {
141
+ await interaction.editReply('Could not resolve parent text channel');
142
+ return;
143
+ }
144
+ const forkedSession = forkResponse.data;
145
+ const forkedThread = await textChannel.threads.create({
146
+ name: `Fork: ${selectedSubagent?.description || selectedSubagent?.subagentType || 'subagent session'}`.slice(0, 100),
147
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
148
+ reason: `Forked subagent session ${selectedSessionId}`,
149
+ });
150
+ await setThreadSession(forkedThread.id, forkedSession.id);
151
+ await forkedThread.members.add(interaction.user.id);
152
+ forkLogger.log(`Created forked subagent session ${forkedSession.id} in thread ${forkedThread.id} from ${selectedSessionId}`);
153
+ const agentLabel = selectedSubagent?.subagentType || 'task';
154
+ const descriptionLabel = selectedSubagent?.description || 'No description';
155
+ await sendThreadMessage(forkedThread, `**Forked subagent session created!**\nAgent: \`${agentLabel}\`\nTask: ${descriptionLabel}\nFrom: \`${selectedSessionId}\`\nNew session: \`${forkedSession.id}\``);
156
+ try {
157
+ const messagesResponse = await getClient().session.messages({
158
+ sessionID: forkedSession.id,
159
+ });
160
+ if (messagesResponse.data) {
161
+ const { chunks } = collectSessionChunks({
162
+ messages: messagesResponse.data,
163
+ limit: 30,
164
+ });
165
+ const batched = batchChunksForDiscord(chunks);
166
+ for (const batch of batched) {
167
+ await sendThreadMessage(forkedThread, batch.content);
168
+ }
169
+ }
170
+ }
171
+ catch (error) {
172
+ forkLogger.error('Error replaying forked subagent history:', error);
173
+ await sendThreadMessage(forkedThread, 'Failed to load session messages, but the session is connected and ready to continue.');
174
+ }
175
+ await sendThreadMessage(forkedThread, 'You can now continue the conversation from this point.');
176
+ await interaction.editReply(`Subagent session forked! Continue in ${forkedThread.toString()}`);
177
+ }