@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
package/src/bin.ts ADDED
@@ -0,0 +1,182 @@
1
+ // Crash-recovery supervisor for `otto gateway start`.
2
+ //
3
+ // Parents run goke-backed `cli.js` directly for every invocation except top-level
4
+ // `otto gateway start` (without `--help`). Those long-running installs get an
5
+ // outer Node process that restarts `cli.js` on non-clean exits (crash, OOM, etc.).
6
+ // Exit code `0` / `EXIT_NO_RESTART=64` / SIGTERM+SIGINT grace paths suppress restarts.
7
+ //
8
+ // When __OTTO_CHILD is set, we're supervised — import `cli.js` directly.
9
+ //
10
+ // V8 heap snapshot flags:
11
+ // Injects --heapsnapshot-near-heap-limit=3 and --diagnostic-dir so V8 writes
12
+ // heap snapshots internally as it approaches the heap limit. This catches OOM
13
+ // situations where SIGKILL (exit 137) would kill the process before our
14
+ // heap-monitor.ts polling can react. The polling monitor is kept as an early
15
+ // warning system at 85% usage; the V8 flag is the last-resort safety net.
16
+
17
+ import { spawn } from 'node:child_process'
18
+ import fs from 'node:fs'
19
+ import os from 'node:os'
20
+ import path from 'node:path'
21
+
22
+ const HEAP_SNAPSHOT_DIR = path.join(os.homedir(), '.otto', 'heap-snapshots')
23
+ const STARTUP_LOG_WAIT_MS = 2_500
24
+ const STARTUP_LOG_POLL_MS = 200
25
+ const STARTUP_LOG_MAX_LINES = 12
26
+
27
+ const argv = process.argv.slice(2)
28
+ const isHelpFlag = process.argv.includes('--help')
29
+ const isGatewayDaemon = process.env.OTTO_GATEWAY_DAEMON === '1'
30
+
31
+ function isGatewayStartInvocation(): boolean {
32
+ return argv[0] === 'gateway' && argv[1] === 'start'
33
+ }
34
+
35
+ function resolveDataDirFromArgv(): string {
36
+ const inlineArg = argv.find((entry) => {
37
+ return entry.startsWith('--data-dir=')
38
+ })
39
+ if (inlineArg) {
40
+ return path.resolve(inlineArg.slice('--data-dir='.length))
41
+ }
42
+
43
+ const dataDirIndex = argv.findIndex((entry) => {
44
+ return entry === '--data-dir'
45
+ })
46
+ if (dataDirIndex !== -1) {
47
+ const value = argv[dataDirIndex + 1]
48
+ if (value && !value.startsWith('--')) {
49
+ return path.resolve(value)
50
+ }
51
+ }
52
+
53
+ return path.join(os.homedir(), '.otto')
54
+ }
55
+
56
+ async function printStartupLogs({
57
+ dataDir,
58
+ }: {
59
+ dataDir: string
60
+ }): Promise<void> {
61
+ const logPath = path.join(dataDir, 'otto.log')
62
+ const seenLines = new Set<string>()
63
+ const deadline = Date.now() + STARTUP_LOG_WAIT_MS
64
+
65
+ while (Date.now() < deadline && seenLines.size < STARTUP_LOG_MAX_LINES) {
66
+ if (fs.existsSync(logPath)) {
67
+ const content = fs.readFileSync(logPath, 'utf8')
68
+ const lines = content
69
+ .split(/\r?\n/)
70
+ .map((entry) => {
71
+ return entry.trim()
72
+ })
73
+ .filter((entry) => {
74
+ return entry.length > 0
75
+ })
76
+
77
+ for (const line of lines) {
78
+ if (seenLines.has(line)) {
79
+ continue
80
+ }
81
+ seenLines.add(line)
82
+ console.error(`[gateway] ${line}`)
83
+ if (seenLines.size >= STARTUP_LOG_MAX_LINES) {
84
+ return
85
+ }
86
+ }
87
+ }
88
+
89
+ await new Promise<void>((resolve) => {
90
+ setTimeout(resolve, STARTUP_LOG_POLL_MS)
91
+ })
92
+ }
93
+ }
94
+
95
+ if (process.env.__OTTO_CHILD || isHelpFlag || !isGatewayStartInvocation()) {
96
+ await import('./cli.js')
97
+ } else if (!isGatewayDaemon) {
98
+ const dataDir = resolveDataDirFromArgv()
99
+ const daemon = spawn(process.execPath, process.argv.slice(1), {
100
+ stdio: 'ignore',
101
+ detached: true,
102
+ env: { ...process.env, OTTO_GATEWAY_DAEMON: '1' },
103
+ windowsHide: process.platform === 'win32',
104
+ })
105
+ daemon.unref()
106
+ console.error('otto gateway start: launched in background')
107
+ await printStartupLogs({ dataDir })
108
+ process.exit(0)
109
+ } else {
110
+ console.error(
111
+ 'otto gateway start: supervised process will restart the child on crash',
112
+ )
113
+ console.error()
114
+ const EXIT_NO_RESTART = 64
115
+ const MAX_RAPID_RESTARTS = 5
116
+ const RAPID_RESTART_WINDOW_MS = 60_000
117
+ const RESTART_DELAY_MS = 2_000
118
+
119
+ const restartTimestamps: number[] = []
120
+ let child: ReturnType<typeof spawn> | null = null
121
+ let shutdownRequested = false
122
+
123
+ function start() {
124
+ if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
125
+ fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true })
126
+ }
127
+ const heapArgs = [
128
+ `--heapsnapshot-near-heap-limit=3`,
129
+ `--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
130
+ ]
131
+ const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)]
132
+ child = spawn(process.argv[0]!, args, {
133
+ stdio: 'ignore',
134
+ env: { ...process.env, __OTTO_CHILD: '1' },
135
+ windowsHide: process.platform === 'win32',
136
+ })
137
+
138
+ child.on('exit', (code, signal) => {
139
+ if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
140
+ process.exit(code ?? 0)
141
+ return
142
+ }
143
+
144
+ const now = Date.now()
145
+ restartTimestamps.push(now)
146
+ while (
147
+ restartTimestamps.length > 0 &&
148
+ restartTimestamps[0]! < now - RAPID_RESTART_WINDOW_MS
149
+ ) {
150
+ restartTimestamps.shift()
151
+ }
152
+
153
+ if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
154
+ console.error(
155
+ `[otto] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`,
156
+ )
157
+ process.exit(1)
158
+ return
159
+ }
160
+
161
+ const reason = signal ? `signal ${signal}` : `code ${code}`
162
+ console.error(
163
+ `[otto] Process exited with ${reason}, restarting in ${RESTART_DELAY_MS / 1000}s...`,
164
+ )
165
+ setTimeout(start, RESTART_DELAY_MS)
166
+ })
167
+ }
168
+
169
+ for (const sig of ['SIGTERM', 'SIGINT'] as const) {
170
+ process.on(sig, () => {
171
+ shutdownRequested = true
172
+ child?.kill(sig)
173
+ })
174
+ }
175
+ for (const sig of ['SIGUSR1', 'SIGUSR2'] as const) {
176
+ process.on(sig, () => {
177
+ child?.kill(sig)
178
+ })
179
+ }
180
+
181
+ start()
182
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { extractBtwPrefix } from './btw-prefix-detection.js'
3
+
4
+ describe('extractBtwPrefix', () => {
5
+ test('matches lowercase prefix', () => {
6
+ expect(extractBtwPrefix('btw fix this')).toMatchInlineSnapshot(`
7
+ {
8
+ "prompt": "fix this",
9
+ }
10
+ `)
11
+ })
12
+
13
+ test('matches uppercase prefix', () => {
14
+ expect(extractBtwPrefix('BTW check this')).toMatchInlineSnapshot(`
15
+ {
16
+ "prompt": "check this",
17
+ }
18
+ `)
19
+ })
20
+
21
+ test('keeps multiline content', () => {
22
+ expect(extractBtwPrefix(' btw first line\nsecond line ')).toMatchInlineSnapshot(`
23
+ {
24
+ "prompt": "first line
25
+ second line",
26
+ }
27
+ `)
28
+ })
29
+
30
+ test('matches dot separator', () => {
31
+ expect(extractBtwPrefix('btw. fix this')).toMatchInlineSnapshot(`
32
+ {
33
+ "prompt": "fix this",
34
+ }
35
+ `)
36
+ })
37
+
38
+ test('matches comma separator', () => {
39
+ expect(extractBtwPrefix('btw, fix this')).toMatchInlineSnapshot(`
40
+ {
41
+ "prompt": "fix this",
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('matches colon separator', () => {
47
+ expect(extractBtwPrefix('btw: fix this')).toMatchInlineSnapshot(`
48
+ {
49
+ "prompt": "fix this",
50
+ }
51
+ `)
52
+ })
53
+
54
+ test('matches punctuation without trailing space', () => {
55
+ expect(extractBtwPrefix('btw.fix this')).toMatchInlineSnapshot(`
56
+ {
57
+ "prompt": "fix this",
58
+ }
59
+ `)
60
+ })
61
+
62
+ test('does not match without separating whitespace', () => {
63
+ expect(extractBtwPrefix('btwfix this')).toMatchInlineSnapshot(`null`)
64
+ })
65
+
66
+ test('does not match mid-message', () => {
67
+ expect(extractBtwPrefix('hello btw fix this')).toMatchInlineSnapshot(`null`)
68
+ })
69
+
70
+ test('does not match empty payload', () => {
71
+ expect(extractBtwPrefix('btw ')).toMatchInlineSnapshot(`null`)
72
+ })
73
+ })
@@ -0,0 +1,23 @@
1
+ // Detects the raw `btw ` Discord message shortcut used to fork a side-question
2
+ // thread without invoking the /btw slash command UI.
3
+
4
+ export function extractBtwPrefix(
5
+ content: string,
6
+ ): { prompt: string } | null {
7
+ if (!content) {
8
+ return null
9
+ }
10
+
11
+ // Match "btw" followed by whitespace or punctuation (. , : ; ! ?) then the prompt
12
+ const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i)
13
+ if (!match) {
14
+ return null
15
+ }
16
+
17
+ const prompt = match[1]?.trim()
18
+ if (!prompt) {
19
+ return null
20
+ }
21
+
22
+ return { prompt }
23
+ }
@@ -0,0 +1,376 @@
1
+ // Discord channel and category management.
2
+ // Creates and manages Otto project channels (text + voice pairs),
3
+ // extracts channel metadata from topic tags, and ensures category structure.
4
+
5
+ import {
6
+ ChannelType,
7
+ type CategoryChannel,
8
+ type Guild,
9
+ type TextChannel,
10
+ } from 'discord.js'
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import {
14
+ getChannelDirectory,
15
+ setChannelDirectory,
16
+ findChannelsByDirectory,
17
+ } from './database.js'
18
+ import { getProjectsDir } from './config.js'
19
+ import { execAsync } from './worktrees.js'
20
+ import { createLogger, LogPrefix } from './logger.js'
21
+
22
+ const logger = createLogger(LogPrefix.CHANNEL)
23
+
24
+ // Legacy category names kept for backward-compat lookup on existing servers.
25
+ // New categories are created with the current "Otto" / "Otto Audio" names.
26
+ const CATEGORY_NAME = 'Otto'
27
+ const CATEGORY_NAME_AUDIO = 'Otto Audio'
28
+ const LEGACY_CATEGORY_NAME = 'Kimaki'
29
+ const LEGACY_CATEGORY_NAME_AUDIO = 'Kimaki Audio'
30
+
31
+ export async function ensureOttoCategory(
32
+ guild: Guild,
33
+ botName?: string,
34
+ ): Promise<CategoryChannel> {
35
+ // Skip appending bot name if it's already "otto" to avoid "Otto otto"
36
+ const isOttoBot = botName?.toLowerCase() === 'otto'
37
+ const categoryName = botName && !isOttoBot ? `${CATEGORY_NAME} ${botName}` : CATEGORY_NAME
38
+ // Legacy names to check when looking up existing categories on older servers
39
+ const isLegacyKimakiBot = botName?.toLowerCase() === 'otto'
40
+ const legacyCategoryName =
41
+ botName && !isLegacyKimakiBot ? `${LEGACY_CATEGORY_NAME} ${botName}` : LEGACY_CATEGORY_NAME
42
+
43
+ const existingCategory = guild.channels.cache.find(
44
+ (channel): channel is CategoryChannel => {
45
+ if (channel.type !== ChannelType.GuildCategory) {
46
+ return false
47
+ }
48
+ const name = channel.name.toLowerCase()
49
+ return (
50
+ name === categoryName.toLowerCase() ||
51
+ name === legacyCategoryName.toLowerCase()
52
+ )
53
+ },
54
+ )
55
+
56
+ if (existingCategory) {
57
+ return existingCategory
58
+ }
59
+
60
+ return guild.channels.create({
61
+ name: categoryName,
62
+ type: ChannelType.GuildCategory,
63
+ })
64
+ }
65
+
66
+ // Keep old export name as an alias for any callers that haven't been updated yet
67
+ export const ensureKimakiCategory = ensureOttoCategory
68
+
69
+ export async function ensureOttoAudioCategory(
70
+ guild: Guild,
71
+ botName?: string,
72
+ ): Promise<CategoryChannel> {
73
+ // Skip appending bot name if it's already "otto" to avoid "Otto Audio otto"
74
+ const isOttoBot = botName?.toLowerCase() === 'otto'
75
+ const categoryName =
76
+ botName && !isOttoBot ? `${CATEGORY_NAME_AUDIO} ${botName}` : CATEGORY_NAME_AUDIO
77
+ const isLegacyKimakiBot = botName?.toLowerCase() === 'otto'
78
+ const legacyCategoryName =
79
+ botName && !isLegacyKimakiBot
80
+ ? `${LEGACY_CATEGORY_NAME_AUDIO} ${botName}`
81
+ : LEGACY_CATEGORY_NAME_AUDIO
82
+
83
+ const existingCategory = guild.channels.cache.find(
84
+ (channel): channel is CategoryChannel => {
85
+ if (channel.type !== ChannelType.GuildCategory) {
86
+ return false
87
+ }
88
+ const name = channel.name.toLowerCase()
89
+ return (
90
+ name === categoryName.toLowerCase() ||
91
+ name === legacyCategoryName.toLowerCase()
92
+ )
93
+ },
94
+ )
95
+
96
+ if (existingCategory) {
97
+ return existingCategory
98
+ }
99
+
100
+ return guild.channels.create({
101
+ name: categoryName,
102
+ type: ChannelType.GuildCategory,
103
+ })
104
+ }
105
+
106
+ export const ensureKimakiAudioCategory = ensureOttoAudioCategory
107
+
108
+ export async function createProjectChannels({
109
+ guild,
110
+ projectDirectory,
111
+ botName,
112
+ enableVoiceChannels = false,
113
+ }: {
114
+ guild: Guild
115
+ projectDirectory: string
116
+ botName?: string
117
+ enableVoiceChannels?: boolean
118
+ }): Promise<{
119
+ textChannelId: string
120
+ voiceChannelId: string | null
121
+ channelName: string
122
+ }> {
123
+ const baseName = path.basename(projectDirectory)
124
+ const channelName = `${baseName}`
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9-]/g, '-')
127
+ .slice(0, 100)
128
+
129
+ const ottoCategory = await ensureOttoCategory(guild, botName)
130
+
131
+ const textChannel = await guild.channels.create({
132
+ name: channelName,
133
+ type: ChannelType.GuildText,
134
+ parent: ottoCategory,
135
+ // Channel configuration is stored in SQLite, not in the topic
136
+ })
137
+
138
+ await setChannelDirectory({
139
+ channelId: textChannel.id,
140
+ directory: projectDirectory,
141
+ channelType: 'text',
142
+ })
143
+
144
+ let voiceChannelId: string | null = null
145
+
146
+ if (enableVoiceChannels) {
147
+ const ottoAudioCategory = await ensureOttoAudioCategory(guild, botName)
148
+
149
+ const voiceChannel = await guild.channels.create({
150
+ name: channelName,
151
+ type: ChannelType.GuildVoice,
152
+ parent: ottoAudioCategory,
153
+ })
154
+
155
+ await setChannelDirectory({
156
+ channelId: voiceChannel.id,
157
+ directory: projectDirectory,
158
+ channelType: 'voice',
159
+ })
160
+
161
+ voiceChannelId = voiceChannel.id
162
+ }
163
+
164
+ return {
165
+ textChannelId: textChannel.id,
166
+ voiceChannelId,
167
+ channelName,
168
+ }
169
+ }
170
+
171
+ export type ChannelWithTags = {
172
+ id: string
173
+ name: string
174
+ description: string | null
175
+ ottoDirectory?: string
176
+ }
177
+
178
+ export async function getChannelsWithDescriptions(
179
+ guild: Guild,
180
+ ): Promise<ChannelWithTags[]> {
181
+ const channels: ChannelWithTags[] = []
182
+
183
+ const textChannels = guild.channels.cache.filter((channel) =>
184
+ channel.isTextBased(),
185
+ )
186
+
187
+ for (const channel of textChannels.values()) {
188
+ const textChannel = channel as TextChannel
189
+ const description = textChannel.topic || null
190
+
191
+ // Get channel config from database instead of parsing XML from topic
192
+ const channelConfig = await getChannelDirectory(textChannel.id)
193
+
194
+ channels.push({
195
+ id: textChannel.id,
196
+ name: textChannel.name,
197
+ description,
198
+ ottoDirectory: channelConfig?.directory,
199
+ })
200
+ }
201
+
202
+ return channels
203
+ }
204
+
205
+ const DEFAULT_GITIGNORE = `node_modules/
206
+ dist/
207
+ .env
208
+ .env.*
209
+ !.env.example
210
+ .DS_Store
211
+ tmp/
212
+ *.log
213
+ __pycache__/
214
+ *.pyc
215
+ .venv/
216
+ *.egg-info/
217
+ `
218
+
219
+ const DEFAULT_CHANNEL_TOPIC =
220
+ 'General channel for misc tasks with Otto. Not connected to a specific OpenCode project or repository.'
221
+
222
+ /**
223
+ * Create (or find) the default "otto" channel for general-purpose tasks.
224
+ * Channel name is "otto-{botName}" for self-hosted bots, "otto" for gateway.
225
+ * Directory is <dataDir>/projects/otto (or the legacy <dataDir>/projects/otto
226
+ * if that already exists on disk), git-initialized with a .gitignore.
227
+ *
228
+ * Idempotency: checks the database for an existing channel mapped to the
229
+ * otto (or legacy otto) projects directory. Also scans guild channels by
230
+ * name+category as a fallback for channels created before DB mapping existed.
231
+ */
232
+ export async function createDefaultOttoChannel({
233
+ guild,
234
+ botName,
235
+ appId,
236
+ isGatewayMode,
237
+ }: {
238
+ guild: Guild
239
+ botName?: string
240
+ appId: string
241
+ isGatewayMode: boolean
242
+ }): Promise<{
243
+ textChannel: TextChannel
244
+ textChannelId: string
245
+ channelName: string
246
+ projectDirectory: string
247
+ } | null> {
248
+ // Use the legacy "otto" sub-directory if it already exists on disk so
249
+ // existing users keep their project history. New installs use "otto".
250
+ const legacyProjectDirectory = path.join(getProjectsDir(), 'otto')
251
+ const projectDirectory = fs.existsSync(legacyProjectDirectory)
252
+ ? legacyProjectDirectory
253
+ : path.join(getProjectsDir(), 'otto')
254
+
255
+ // Ensure the project directory exists before any DB mapping restoration
256
+ // or git setup. Custom data dirs may not have <dataDir>/projects created
257
+ // yet, and later writes assume the full path is present.
258
+ if (!fs.existsSync(projectDirectory)) {
259
+ fs.mkdirSync(projectDirectory, { recursive: true })
260
+ logger.log(`Created default otto directory: ${projectDirectory}`)
261
+ }
262
+
263
+ // Hydrate guild channels from API so the cache scan is complete
264
+ try {
265
+ await guild.channels.fetch()
266
+ } catch (error) {
267
+ logger.warn(
268
+ `Could not fetch guild channels for ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`,
269
+ )
270
+ }
271
+
272
+ // 1. Check database for existing channel mapped to this directory.
273
+ // Check ALL mappings (not just the first) since the same directory could
274
+ // have stale rows from deleted channels or other guilds.
275
+ const existingMappings = await findChannelsByDirectory({
276
+ directory: projectDirectory,
277
+ channelType: 'text',
278
+ })
279
+ const mappedChannelInGuild = existingMappings
280
+ .map((row) => guild.channels.cache.get(row.channel_id))
281
+ .find((ch): ch is TextChannel => ch?.type === ChannelType.GuildText)
282
+ if (mappedChannelInGuild) {
283
+ logger.log(`Default otto channel already exists: ${mappedChannelInGuild.id}`)
284
+ return null
285
+ }
286
+
287
+ // 2. Fallback: detect existing channel by name+category (handles both the
288
+ // current "otto"/"otto-*" names and legacy "otto"/"otto-*" names).
289
+ const ottoCategory = await ensureOttoCategory(guild, botName)
290
+ const existingByName = guild.channels.cache.find((ch): ch is TextChannel => {
291
+ if (ch.type !== ChannelType.GuildText) {
292
+ return false
293
+ }
294
+ if (ch.parentId !== ottoCategory.id) {
295
+ return false
296
+ }
297
+ return (
298
+ ch.name === 'otto' ||
299
+ ch.name.startsWith('otto-') ||
300
+ ch.name === 'otto' ||
301
+ ch.name.startsWith('otto-')
302
+ )
303
+ })
304
+ if (existingByName) {
305
+ logger.log(
306
+ `Found existing default channel by name: ${existingByName.id}, restoring DB mapping`,
307
+ )
308
+ await setChannelDirectory({
309
+ channelId: existingByName.id,
310
+ directory: projectDirectory,
311
+ channelType: 'text',
312
+ skipIfExists: true,
313
+ })
314
+ return null
315
+ }
316
+
317
+ // Git init — gracefully skip if git is not installed
318
+ const gitDir = path.join(projectDirectory, '.git')
319
+ if (!fs.existsSync(gitDir)) {
320
+ try {
321
+ await execAsync('git init', { cwd: projectDirectory, timeout: 10_000 })
322
+ logger.log(`Initialized git in: ${projectDirectory}`)
323
+ } catch (error) {
324
+ logger.warn(
325
+ `Could not initialize git in ${projectDirectory}: ${error instanceof Error ? error.stack : String(error)}`,
326
+ )
327
+ }
328
+ }
329
+
330
+ // Write .gitignore if it doesn't exist
331
+ const gitignorePath = path.join(projectDirectory, '.gitignore')
332
+ if (!fs.existsSync(gitignorePath)) {
333
+ fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE)
334
+ }
335
+
336
+ // Channel name: "otto-{botName}" for self-hosted, "otto" for gateway
337
+ const channelName = (() => {
338
+ if (isGatewayMode || !botName) {
339
+ return 'otto'
340
+ }
341
+ const sanitized = botName
342
+ .toLowerCase()
343
+ .replace(/[^a-z0-9-]/g, '-')
344
+ .replace(/-+/g, '-')
345
+ .replace(/^-|-$/g, '')
346
+ if (!sanitized || sanitized === 'otto') {
347
+ return 'otto'
348
+ }
349
+ return `otto-${sanitized}`.slice(0, 100)
350
+ })()
351
+
352
+ const textChannel = await guild.channels.create({
353
+ name: channelName,
354
+ type: ChannelType.GuildText,
355
+ parent: ottoCategory,
356
+ topic: DEFAULT_CHANNEL_TOPIC,
357
+ })
358
+
359
+ await setChannelDirectory({
360
+ channelId: textChannel.id,
361
+ directory: projectDirectory,
362
+ channelType: 'text',
363
+ })
364
+
365
+ logger.log(`Created default otto channel: #${channelName} (${textChannel.id})`)
366
+
367
+ return {
368
+ textChannel,
369
+ textChannelId: textChannel.id,
370
+ channelName,
371
+ projectDirectory,
372
+ }
373
+ }
374
+
375
+ // Keep legacy export name so any unupdated callers still compile
376
+ export const createDefaultKimakiChannel = createDefaultOttoChannel