@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,616 @@
1
+ // Discord-specific utility functions.
2
+ // Handles markdown splitting for Discord's 2000-char limit, code block escaping,
3
+ // thread message sending, and channel metadata extraction from topic tags.
4
+ // Use namespace import for CJS interop — discord.js is CJS and its named
5
+ // exports aren't detectable by all ESM loaders (e.g. tsx/esbuild) because
6
+ // discord.js uses tslib's __exportStar which is opaque to static analysis.
7
+ import * as discord from 'discord.js';
8
+ const { ChannelType, GuildMember, MessageFlags, PermissionsBitField, REST, Routes } = discord;
9
+ import { discordApiUrl } from './discord-urls.js';
10
+ import { Lexer } from 'marked';
11
+ import { splitTablesFromMarkdown } from './format-tables.js';
12
+ import { getChannelDirectory, getThreadWorktree } from './database.js';
13
+ import { limitHeadingDepth } from './limit-heading-depth.js';
14
+ import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
15
+ import { createLogger, LogPrefix } from './logger.js';
16
+ import * as errore from 'errore';
17
+ import mime from 'mime';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ const discordLogger = createLogger(LogPrefix.DISCORD);
21
+ /**
22
+ * Centralized permission check for Otto bot access.
23
+ * Returns true if the member has permission to use the bot:
24
+ * - Server owner, Administrator, Manage Server, or "Otto" role (case-insensitive).
25
+ * Also accepts the legacy "Kimaki" role for backward compat on existing servers.
26
+ * Returns false if member is null or has the "no-otto" / "no-otto" role (overrides all).
27
+ */
28
+ export function hasOttoBotPermission(member, guild) {
29
+ if (!member) {
30
+ return false;
31
+ }
32
+ // Accept both "no-otto" and legacy "no-otto" as deny roles
33
+ const isDenied = hasRoleByName(member, 'no-otto', guild) ||
34
+ hasRoleByName(member, 'no-otto', guild);
35
+ if (isDenied) {
36
+ return false;
37
+ }
38
+ const memberPermissions = member instanceof GuildMember
39
+ ? member.permissions
40
+ : new PermissionsBitField(BigInt(member.permissions));
41
+ const ownerId = member instanceof GuildMember ? member.guild.ownerId : guild?.ownerId;
42
+ const memberId = member instanceof GuildMember ? member.id : member.user.id;
43
+ const isOwner = ownerId ? memberId === ownerId : false;
44
+ const isAdmin = memberPermissions.has(PermissionsBitField.Flags.Administrator);
45
+ const canManageServer = memberPermissions.has(PermissionsBitField.Flags.ManageGuild);
46
+ // Accept both "otto" and legacy "otto" as allow roles
47
+ const hasOttoRole = hasRoleByName(member, 'otto', guild) || hasRoleByName(member, 'otto', guild);
48
+ return isOwner || isAdmin || canManageServer || hasOttoRole;
49
+ }
50
+ // Keep legacy name as alias for callers not yet updated
51
+ export const hasKimakiBotPermission = hasOttoBotPermission;
52
+ function hasRoleByName(member, roleName, guild) {
53
+ const target = roleName.toLowerCase();
54
+ if (member instanceof GuildMember) {
55
+ return member.roles.cache.some((role) => role.name.toLowerCase() === target);
56
+ }
57
+ if (!guild) {
58
+ return false;
59
+ }
60
+ const roleIds = Array.isArray(member.roles) ? member.roles : [];
61
+ for (const roleId of roleIds) {
62
+ const role = guild.roles.cache.get(roleId);
63
+ if (role?.name.toLowerCase() === target) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+ /**
70
+ * Check if the member has the "no-otto" (or legacy "no-otto") role that blocks bot access.
71
+ * Separate from hasOttoBotPermission so callers can show a specific error message.
72
+ */
73
+ export function hasNoOttoRole(member) {
74
+ if (!member?.roles?.cache) {
75
+ return false;
76
+ }
77
+ return member.roles.cache.some((role) => role.name.toLowerCase() === 'no-otto' ||
78
+ role.name.toLowerCase() === 'no-otto');
79
+ }
80
+ // Keep legacy name as alias
81
+ export const hasNoKimakiRole = hasNoOttoRole;
82
+ /**
83
+ * React to a thread's starter message with an emoji.
84
+ * Thread ID equals the starter message ID in Discord.
85
+ */
86
+ export async function reactToThread({ rest, threadId, channelId, emoji, }) {
87
+ const parentChannelId = await (async () => {
88
+ if (channelId) {
89
+ return channelId;
90
+ }
91
+ // Fetch the thread to get its parent channel ID
92
+ const threadResult = await errore.tryAsync(() => {
93
+ return rest.get(Routes.channel(threadId));
94
+ });
95
+ if (threadResult instanceof Error) {
96
+ discordLogger.warn(`Failed to fetch thread ${threadId}:`, threadResult.message);
97
+ return null;
98
+ }
99
+ return threadResult.parent_id || null;
100
+ })();
101
+ if (!parentChannelId) {
102
+ discordLogger.warn(`Could not resolve parent channel for thread ${threadId}`);
103
+ return;
104
+ }
105
+ // React to the thread starter message in the parent channel.
106
+ // Thread ID equals the starter message ID for threads created from messages.
107
+ const result = await errore.tryAsync(() => {
108
+ return rest.put(Routes.channelMessageOwnReaction(parentChannelId, threadId, encodeURIComponent(emoji)));
109
+ });
110
+ if (result instanceof Error) {
111
+ discordLogger.warn(`Failed to react to thread ${threadId} with ${emoji}:`, result.message);
112
+ }
113
+ }
114
+ export async function archiveThread({ rest, threadId, parentChannelId, sessionId, client, archiveDelay = 0, }) {
115
+ await reactToThread({
116
+ rest,
117
+ threadId,
118
+ channelId: parentChannelId,
119
+ emoji: '📁',
120
+ });
121
+ if (client && sessionId) {
122
+ await softArchiveSession({ client, sessionId });
123
+ }
124
+ if (archiveDelay > 0) {
125
+ await new Promise((resolve) => {
126
+ setTimeout(() => {
127
+ resolve();
128
+ }, archiveDelay);
129
+ });
130
+ }
131
+ await rest.patch(Routes.channel(threadId), {
132
+ body: { archived: true },
133
+ });
134
+ }
135
+ export async function softArchiveSession({ client, sessionId, }) {
136
+ const updateResult = await errore.tryAsync({
137
+ try: async () => {
138
+ const sessionResponse = await client.session.get({
139
+ sessionID: sessionId,
140
+ });
141
+ if (!sessionResponse.data) {
142
+ return;
143
+ }
144
+ const currentTitle = sessionResponse.data.title || '';
145
+ const newTitle = currentTitle.startsWith('📁')
146
+ ? currentTitle
147
+ : `📁 ${currentTitle}`.trim();
148
+ await client.session.update({
149
+ sessionID: sessionId,
150
+ title: newTitle,
151
+ });
152
+ },
153
+ catch: (e) => new Error('Failed to update session title', { cause: e }),
154
+ });
155
+ if (updateResult instanceof Error) {
156
+ discordLogger.warn(`[archive-thread] ${updateResult.message}`);
157
+ }
158
+ const abortResult = await errore.tryAsync({
159
+ try: async () => {
160
+ await client.session.abort({ sessionID: sessionId });
161
+ },
162
+ catch: (e) => new Error('Failed to abort session', { cause: e }),
163
+ });
164
+ if (abortResult instanceof Error) {
165
+ discordLogger.warn(`[archive-thread] ${abortResult.message}`);
166
+ }
167
+ }
168
+ export async function applyThreadDeletionSyncPolicy({ client, sessionId, mode, }) {
169
+ if (mode === 'hard') {
170
+ await client.session.delete({ sessionID: sessionId });
171
+ return 'hard';
172
+ }
173
+ await softArchiveSession({ client, sessionId });
174
+ return 'soft';
175
+ }
176
+ /** Remove Discord mentions from text so they don't appear in thread titles */
177
+ export function stripMentions(text) {
178
+ return text
179
+ .replace(/<@!?\d+>/g, '') // user mentions
180
+ .replace(/<@&\d+>/g, '') // role mentions
181
+ .replace(/<#\d+>/g, '') // channel mentions
182
+ .replace(/\s+/g, ' ')
183
+ .trim();
184
+ }
185
+ export const SILENT_MESSAGE_FLAGS = 4 | 4096;
186
+ // Same as SILENT but without SuppressNotifications - triggers badge/notification
187
+ export const NOTIFY_MESSAGE_FLAGS = 4;
188
+ export function escapeBackticksInCodeBlocks(markdown) {
189
+ const lexer = new Lexer();
190
+ const tokens = lexer.lex(markdown);
191
+ let result = '';
192
+ for (const token of tokens) {
193
+ if (token.type === 'code') {
194
+ const escapedCode = token.text.replace(/`/g, '\\`');
195
+ result += '```' + (token.lang || '') + '\n' + escapedCode + '\n```\n';
196
+ }
197
+ else {
198
+ result += token.raw;
199
+ }
200
+ }
201
+ return result;
202
+ }
203
+ export function splitMarkdownForDiscord({ content, maxLength, }) {
204
+ if (content.length <= maxLength) {
205
+ return [content];
206
+ }
207
+ const lexer = new Lexer();
208
+ const tokens = lexer.lex(content);
209
+ const lines = [];
210
+ const ensureNewlineBeforeCode = () => {
211
+ const last = lines[lines.length - 1];
212
+ if (!last) {
213
+ return;
214
+ }
215
+ if (last.text.endsWith('\n')) {
216
+ return;
217
+ }
218
+ lines.push({
219
+ text: '\n',
220
+ inCodeBlock: false,
221
+ lang: '',
222
+ isOpeningFence: false,
223
+ isClosingFence: false,
224
+ });
225
+ };
226
+ for (const token of tokens) {
227
+ if (token.type === 'code') {
228
+ ensureNewlineBeforeCode();
229
+ const lang = token.lang || '';
230
+ lines.push({
231
+ text: '```' + lang + '\n',
232
+ inCodeBlock: false,
233
+ lang,
234
+ isOpeningFence: true,
235
+ isClosingFence: false,
236
+ });
237
+ const codeLines = token.text.split('\n');
238
+ for (const codeLine of codeLines) {
239
+ lines.push({
240
+ text: codeLine + '\n',
241
+ inCodeBlock: true,
242
+ lang,
243
+ isOpeningFence: false,
244
+ isClosingFence: false,
245
+ });
246
+ }
247
+ lines.push({
248
+ text: '```\n',
249
+ inCodeBlock: false,
250
+ lang: '',
251
+ isOpeningFence: false,
252
+ isClosingFence: true,
253
+ });
254
+ }
255
+ else {
256
+ const rawLines = token.raw.split('\n');
257
+ for (let i = 0; i < rawLines.length; i++) {
258
+ const isLast = i === rawLines.length - 1;
259
+ const text = isLast ? rawLines[i] : rawLines[i] + '\n';
260
+ if (text) {
261
+ lines.push({
262
+ text,
263
+ inCodeBlock: false,
264
+ lang: '',
265
+ isOpeningFence: false,
266
+ isClosingFence: false,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ }
272
+ const chunks = [];
273
+ let currentChunk = '';
274
+ let currentLang = null;
275
+ // helper to split a long line into smaller pieces at word boundaries or hard breaks
276
+ const splitLongLine = (text, available, inCode) => {
277
+ const pieces = [];
278
+ let remaining = text;
279
+ while (remaining.length > available) {
280
+ let splitAt = available;
281
+ // for non-code, try to split at word boundary
282
+ if (!inCode) {
283
+ const lastSpace = remaining.lastIndexOf(' ', available);
284
+ if (lastSpace > available * 0.5) {
285
+ splitAt = lastSpace + 1;
286
+ }
287
+ }
288
+ pieces.push(remaining.slice(0, splitAt));
289
+ remaining = remaining.slice(splitAt);
290
+ }
291
+ if (remaining) {
292
+ pieces.push(remaining);
293
+ }
294
+ return pieces;
295
+ };
296
+ const closingFence = '```\n';
297
+ for (const line of lines) {
298
+ // openingFenceSize accounts for the fence text when starting a fresh chunk
299
+ const openingFenceSize = currentChunk.length === 0 && (line.inCodeBlock || line.isOpeningFence)
300
+ ? ('```' + line.lang + '\n').length
301
+ : 0;
302
+ // When opening fence starts a fresh chunk, its size is in openingFenceSize.
303
+ // Otherwise count it normally so the overflow check doesn't miss the fence text.
304
+ const lineLength = line.isOpeningFence && currentChunk.length === 0 ? 0 : line.text.length;
305
+ const activeFenceOverhead = currentLang !== null || openingFenceSize > 0 ? closingFence.length : 0;
306
+ const wouldExceed = currentChunk.length +
307
+ openingFenceSize +
308
+ lineLength +
309
+ activeFenceOverhead >
310
+ maxLength;
311
+ if (wouldExceed) {
312
+ // handle case where single line is longer than maxLength
313
+ if (line.text.length > maxLength) {
314
+ // first, flush current chunk if any
315
+ if (currentChunk) {
316
+ if (currentLang !== null) {
317
+ currentChunk += '```\n';
318
+ }
319
+ chunks.push(currentChunk);
320
+ currentChunk = '';
321
+ }
322
+ // calculate overhead for code block markers
323
+ const codeBlockOverhead = line.inCodeBlock
324
+ ? ('```' + line.lang + '\n').length + '```\n'.length
325
+ : 0;
326
+ // ensure at least 10 chars available, even if maxLength is very small
327
+ const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50);
328
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
329
+ for (let i = 0; i < pieces.length; i++) {
330
+ const piece = pieces[i];
331
+ if (line.inCodeBlock) {
332
+ chunks.push('```' + line.lang + '\n' + piece + '```\n');
333
+ }
334
+ else {
335
+ chunks.push(piece);
336
+ }
337
+ }
338
+ currentLang = null;
339
+ continue;
340
+ }
341
+ // normal case: line fits in a chunk but current chunk would overflow
342
+ if (currentChunk) {
343
+ if (currentLang !== null) {
344
+ currentChunk += '```\n';
345
+ }
346
+ chunks.push(currentChunk);
347
+ if (line.isClosingFence && currentLang !== null) {
348
+ currentChunk = '';
349
+ currentLang = null;
350
+ continue;
351
+ }
352
+ if (line.inCodeBlock || line.isOpeningFence) {
353
+ const lang = line.lang;
354
+ currentChunk = '```' + lang + '\n';
355
+ if (!line.isOpeningFence) {
356
+ currentChunk += line.text;
357
+ }
358
+ currentLang = lang;
359
+ }
360
+ else {
361
+ currentChunk = line.text;
362
+ currentLang = null;
363
+ }
364
+ }
365
+ else {
366
+ // currentChunk is empty but line still exceeds - shouldn't happen after above check
367
+ const openingFence = line.inCodeBlock || line.isOpeningFence;
368
+ const openingFenceSize = openingFence
369
+ ? ('```' + line.lang + '\n').length
370
+ : 0;
371
+ if (line.text.length + openingFenceSize + activeFenceOverhead >
372
+ maxLength) {
373
+ const fencedOverhead = openingFence
374
+ ? ('```' + line.lang + '\n').length + closingFence.length
375
+ : 0;
376
+ const availablePerChunk = Math.max(10, maxLength - fencedOverhead - 50);
377
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
378
+ for (const piece of pieces) {
379
+ if (openingFence) {
380
+ chunks.push('```' + line.lang + '\n' + piece + closingFence);
381
+ }
382
+ else {
383
+ chunks.push(piece);
384
+ }
385
+ }
386
+ currentChunk = '';
387
+ currentLang = null;
388
+ }
389
+ else {
390
+ if (openingFence) {
391
+ currentChunk = '```' + line.lang + '\n';
392
+ if (!line.isOpeningFence) {
393
+ currentChunk += line.text;
394
+ }
395
+ currentLang = line.lang;
396
+ }
397
+ else {
398
+ currentChunk = line.text;
399
+ currentLang = null;
400
+ }
401
+ }
402
+ }
403
+ }
404
+ else {
405
+ currentChunk += line.text;
406
+ if (line.inCodeBlock || line.isOpeningFence) {
407
+ currentLang = line.lang;
408
+ }
409
+ else if (line.isClosingFence) {
410
+ currentLang = null;
411
+ }
412
+ }
413
+ }
414
+ if (currentChunk) {
415
+ if (currentLang !== null) {
416
+ currentChunk += closingFence;
417
+ }
418
+ chunks.push(currentChunk);
419
+ }
420
+ return chunks;
421
+ }
422
+ export async function sendThreadMessage(thread, content, options) {
423
+ const MAX_LENGTH = 2000;
424
+ // Split content into text and CV2 component segments (tables → Container components)
425
+ const segments = splitTablesFromMarkdown(content);
426
+ const baseFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
427
+ let firstMessage;
428
+ for (const segment of segments) {
429
+ if (segment.type === 'components') {
430
+ const message = await thread.send({
431
+ components: segment.components,
432
+ flags: MessageFlags.IsComponentsV2 | baseFlags,
433
+ });
434
+ if (!firstMessage) {
435
+ firstMessage = message;
436
+ }
437
+ continue;
438
+ }
439
+ // Apply text transformations to text segments
440
+ let text = segment.text;
441
+ text = unnestCodeBlocksFromLists(text);
442
+ text = limitHeadingDepth(text);
443
+ text = escapeBackticksInCodeBlocks(text);
444
+ if (!text.trim()) {
445
+ continue;
446
+ }
447
+ const sendFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
448
+ const chunks = splitMarkdownForDiscord({
449
+ content: text,
450
+ maxLength: MAX_LENGTH,
451
+ });
452
+ if (chunks.length > 1) {
453
+ discordLogger.log(`MESSAGE: Splitting ${text.length} chars into ${chunks.length} messages`);
454
+ }
455
+ for (let chunk of chunks) {
456
+ if (!chunk) {
457
+ continue;
458
+ }
459
+ // Safety net: hard-truncate if splitting still produced an oversized chunk
460
+ if (chunk.length > MAX_LENGTH) {
461
+ chunk = chunk.slice(0, MAX_LENGTH - 4) + '...';
462
+ }
463
+ const message = await thread.send({ content: chunk, flags: sendFlags });
464
+ if (!firstMessage) {
465
+ firstMessage = message;
466
+ }
467
+ }
468
+ }
469
+ return firstMessage;
470
+ }
471
+ export async function resolveTextChannel(channel) {
472
+ if (!channel) {
473
+ return null;
474
+ }
475
+ if (channel.type === ChannelType.GuildText) {
476
+ return channel;
477
+ }
478
+ if (channel.type === ChannelType.PublicThread ||
479
+ channel.type === ChannelType.PrivateThread ||
480
+ channel.type === ChannelType.AnnouncementThread) {
481
+ const parentId = channel.parentId;
482
+ if (parentId) {
483
+ const parent = await channel.guild.channels.fetch(parentId);
484
+ if (parent?.type === ChannelType.GuildText) {
485
+ return parent;
486
+ }
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+ export function escapeDiscordFormatting(text) {
492
+ return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`');
493
+ }
494
+ export async function getOttoMetadata(textChannel) {
495
+ if (!textChannel) {
496
+ return {};
497
+ }
498
+ const channelConfig = await getChannelDirectory(textChannel.id);
499
+ if (!channelConfig) {
500
+ return {};
501
+ }
502
+ return {
503
+ projectDirectory: channelConfig.directory,
504
+ };
505
+ }
506
+ // Legacy alias kept for backward compat
507
+ export const getKimakiMetadata = getOttoMetadata;
508
+ /**
509
+ * Resolve project directory from an autocomplete interaction.
510
+ * Uses interaction.channelId (always available from raw payload) instead of
511
+ * interaction.channel (cache-based getter, often null with gateway-proxy).
512
+ * Checks the channel ID directly in DB, then tries thread worktree lookup,
513
+ * then falls back to fetching the channel to resolve thread parent.
514
+ */
515
+ export async function resolveProjectDirectoryFromAutocomplete(interaction) {
516
+ const channelId = interaction.channelId;
517
+ // Direct channel lookup — works when the command is run from a project text channel
518
+ const channelConfig = await getChannelDirectory(channelId);
519
+ if (channelConfig) {
520
+ return channelConfig.directory;
521
+ }
522
+ // If we're in a thread, try worktree info first (has project_directory)
523
+ const worktreeInfo = await getThreadWorktree(channelId);
524
+ if (worktreeInfo?.project_directory) {
525
+ return worktreeInfo.project_directory;
526
+ }
527
+ // Thread fallback: resolve parent channel ID and look up its directory.
528
+ // Try cached channel first, then fetch if cache misses (gateway-proxy scenario).
529
+ const cachedParentId = interaction.channel?.isThread() ? interaction.channel.parentId : null;
530
+ if (cachedParentId) {
531
+ const parentConfig = await getChannelDirectory(cachedParentId);
532
+ if (parentConfig) {
533
+ return parentConfig.directory;
534
+ }
535
+ }
536
+ // Last resort: fetch the channel from Discord API to get parentId for threads
537
+ // when the channel isn't cached at all (common with gateway-proxy).
538
+ if (!cachedParentId) {
539
+ const fetched = await errore.tryAsync({
540
+ try: () => { return interaction.client.channels.fetch(channelId); },
541
+ catch: (e) => { return e; },
542
+ });
543
+ if (!(fetched instanceof Error) && fetched?.isThread() && fetched.parentId) {
544
+ const parentConfig = await getChannelDirectory(fetched.parentId);
545
+ if (parentConfig) {
546
+ return parentConfig.directory;
547
+ }
548
+ }
549
+ }
550
+ return undefined;
551
+ }
552
+ /**
553
+ * Resolve the working directory for a channel or thread.
554
+ * Returns both the base project directory (for server init) and the working directory
555
+ * (worktree directory if in a worktree thread, otherwise same as projectDirectory).
556
+ * This prevents commands from accidentally running in the base project dir when a
557
+ * worktree is active — the bug that caused /diff, /compact, etc. to use wrong cwd.
558
+ */
559
+ export async function resolveWorkingDirectory({ channel, }) {
560
+ const isThread = [
561
+ ChannelType.PublicThread,
562
+ ChannelType.PrivateThread,
563
+ ChannelType.AnnouncementThread,
564
+ ].includes(channel.type);
565
+ const textChannel = isThread
566
+ ? await resolveTextChannel(channel)
567
+ : channel;
568
+ const metadata = await getOttoMetadata(textChannel);
569
+ if (!metadata.projectDirectory) {
570
+ return undefined;
571
+ }
572
+ let workingDirectory = metadata.projectDirectory;
573
+ if (isThread) {
574
+ const worktreeInfo = await getThreadWorktree(channel.id);
575
+ if (worktreeInfo?.status === 'ready' && worktreeInfo.worktree_directory) {
576
+ workingDirectory = worktreeInfo.worktree_directory;
577
+ }
578
+ }
579
+ return {
580
+ projectDirectory: metadata.projectDirectory,
581
+ workingDirectory,
582
+ };
583
+ }
584
+ /**
585
+ * Upload files to a Discord thread/channel in a single message.
586
+ * Sending all files in one message causes Discord to display images in a grid layout.
587
+ */
588
+ export async function uploadFilesToDiscord({ threadId, botToken, files, }) {
589
+ if (files.length === 0) {
590
+ return;
591
+ }
592
+ // Build attachments array for all files
593
+ const attachments = files.map((file, index) => ({
594
+ id: index,
595
+ filename: path.basename(file),
596
+ }));
597
+ const formData = new FormData();
598
+ formData.append('payload_json', JSON.stringify({ attachments }));
599
+ // Append each file with its array index, with correct MIME type for grid display
600
+ files.forEach((file, index) => {
601
+ const buffer = fs.readFileSync(file);
602
+ const mimeType = mime.getType(file) || 'application/octet-stream';
603
+ formData.append(`files[${index}]`, new Blob([buffer], { type: mimeType }), path.basename(file));
604
+ });
605
+ const response = await fetch(discordApiUrl(`/channels/${threadId}/messages`), {
606
+ method: 'POST',
607
+ headers: {
608
+ Authorization: `Bot ${botToken}`,
609
+ },
610
+ body: formData,
611
+ });
612
+ if (!response.ok) {
613
+ const error = await response.text();
614
+ throw new Error(`Discord API error: ${response.status} - ${error}`);
615
+ }
616
+ }