@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,116 @@
1
+ import { expect, test } from 'vitest'
2
+ import { limitHeadingDepth } from './limit-heading-depth.js'
3
+
4
+ test('converts h4 to h3', () => {
5
+ const input = '#### Fourth level heading'
6
+ const result = limitHeadingDepth(input)
7
+ expect(result).toMatchInlineSnapshot(`
8
+ "### Fourth level heading
9
+ "
10
+ `)
11
+ })
12
+
13
+ test('converts h5 to h3', () => {
14
+ const input = '##### Fifth level heading'
15
+ const result = limitHeadingDepth(input)
16
+ expect(result).toMatchInlineSnapshot(`
17
+ "### Fifth level heading
18
+ "
19
+ `)
20
+ })
21
+
22
+ test('converts h6 to h3', () => {
23
+ const input = '###### Sixth level heading'
24
+ const result = limitHeadingDepth(input)
25
+ expect(result).toMatchInlineSnapshot(`
26
+ "### Sixth level heading
27
+ "
28
+ `)
29
+ })
30
+
31
+ test('preserves h3 unchanged', () => {
32
+ const input = '### Third level heading'
33
+ const result = limitHeadingDepth(input)
34
+ expect(result).toMatchInlineSnapshot(`"### Third level heading"`)
35
+ })
36
+
37
+ test('preserves h2 unchanged', () => {
38
+ const input = '## Second level heading'
39
+ const result = limitHeadingDepth(input)
40
+ expect(result).toMatchInlineSnapshot(`"## Second level heading"`)
41
+ })
42
+
43
+ test('preserves h1 unchanged', () => {
44
+ const input = '# First level heading'
45
+ const result = limitHeadingDepth(input)
46
+ expect(result).toMatchInlineSnapshot(`"# First level heading"`)
47
+ })
48
+
49
+ test('handles multiple headings in document', () => {
50
+ const input = `# Title
51
+
52
+ Some text
53
+
54
+ ## Section
55
+
56
+ ### Subsection
57
+
58
+ #### Too deep
59
+
60
+ ##### Even deeper
61
+
62
+ Regular paragraph
63
+
64
+ ### Back to normal
65
+ `
66
+ const result = limitHeadingDepth(input)
67
+ expect(result).toMatchInlineSnapshot(`
68
+ "# Title
69
+
70
+ Some text
71
+
72
+ ## Section
73
+
74
+ ### Subsection
75
+
76
+ ### Too deep
77
+ ### Even deeper
78
+ Regular paragraph
79
+
80
+ ### Back to normal
81
+ "
82
+ `)
83
+ })
84
+
85
+ test('preserves heading with inline formatting', () => {
86
+ const input = '#### Heading with **bold** and `code`'
87
+ const result = limitHeadingDepth(input)
88
+ expect(result).toMatchInlineSnapshot(`
89
+ "### Heading with **bold** and \`code\`
90
+ "
91
+ `)
92
+ })
93
+
94
+ test('handles empty markdown', () => {
95
+ const result = limitHeadingDepth('')
96
+ expect(result).toMatchInlineSnapshot(`""`)
97
+ })
98
+
99
+ test('handles markdown with no headings', () => {
100
+ const input = 'Just some text\n\nAnd more text'
101
+ const result = limitHeadingDepth(input)
102
+ expect(result).toMatchInlineSnapshot(`
103
+ "Just some text
104
+
105
+ And more text"
106
+ `)
107
+ })
108
+
109
+ test('allows custom maxDepth', () => {
110
+ const input = '### Third level'
111
+ const result = limitHeadingDepth(input, 2)
112
+ expect(result).toMatchInlineSnapshot(`
113
+ "## Third level
114
+ "
115
+ `)
116
+ })
@@ -0,0 +1,26 @@
1
+ // Limit heading depth for Discord.
2
+ // Discord only supports headings up to ### (h3), so this converts
3
+ // ####, #####, etc. to ### to maintain consistent rendering.
4
+
5
+ import { Lexer, type Tokens } from 'marked'
6
+
7
+ export function limitHeadingDepth(markdown: string, maxDepth = 3): string {
8
+ const lexer = new Lexer()
9
+ const tokens = lexer.lex(markdown)
10
+
11
+ let result = ''
12
+ for (const token of tokens) {
13
+ if (token.type === 'heading') {
14
+ const heading = token as Tokens.Heading
15
+ if (heading.depth > maxDepth) {
16
+ const hashes = '#'.repeat(maxDepth)
17
+ result += hashes + ' ' + heading.text + '\n'
18
+ } else {
19
+ result += token.raw
20
+ }
21
+ } else {
22
+ result += token.raw
23
+ }
24
+ }
25
+ return result
26
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,215 @@
1
+ // Prefixed logging utility using @clack/prompts for consistent visual style.
2
+ // All log methods use clack's log.message() with appropriate symbols to prevent
3
+ // output interleaving from concurrent async operations.
4
+
5
+ import { log as clackLog } from '@clack/prompts'
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import util from 'node:util'
9
+ import pc from 'picocolors'
10
+ import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js'
11
+
12
+ // All known log prefixes - add new ones here to keep alignment consistent
13
+ export const LogPrefix = {
14
+ ABORT: 'ABORT',
15
+ ADD_PROJECT: 'ADD_PROJ',
16
+ AGENT: 'AGENT',
17
+ ASK_QUESTION: 'QUESTION',
18
+ CHANNEL: 'CHANNEL',
19
+ CLI: 'CLI',
20
+ COMPACT: 'COMPACT',
21
+ CREATE_PROJECT: 'NEW_PROJ',
22
+ DB: 'DB',
23
+ DIFF: 'DIFF',
24
+ FILE_UPLOAD: 'FILEUP',
25
+ DISCORD: 'DISCORD',
26
+ FORK: 'FORK',
27
+ FORMATTING: 'FORMAT',
28
+ GENAI: 'GENAI',
29
+ HEAP: 'HEAP',
30
+ GENAI_WORKER: 'GENAI_W',
31
+ INTERACTION: 'INTERACT',
32
+ IPC: 'IPC',
33
+ LOGIN: 'LOGIN',
34
+ MARKDOWN: 'MARKDOWN',
35
+ MCP: 'MCP',
36
+ MODEL: 'MODEL',
37
+ OPENAI: 'OPENAI',
38
+ OPENCODE: 'OPENCODE',
39
+ PERMISSIONS: 'PERMS',
40
+ QUEUE: 'QUEUE',
41
+ REMOVE_PROJECT: 'RM_PROJ',
42
+ RESUME: 'RESUME',
43
+ SESSION: 'SESSION',
44
+ SHARE: 'SHARE',
45
+ TASK: 'TASK',
46
+ TOOLS: 'TOOLS',
47
+ UNDO_REDO: 'UNDO',
48
+ USER_CMD: 'USER_CMD',
49
+ VERBOSITY: 'VERBOSE',
50
+ VOICE: 'VOICE',
51
+ WORKER: 'WORKER',
52
+ THINKING: 'THINK',
53
+ WORKTREE: 'WORKTREE',
54
+ XML: 'XML',
55
+ } as const
56
+
57
+ export type LogPrefixType = (typeof LogPrefix)[keyof typeof LogPrefix]
58
+
59
+ // compute max length from all known prefixes for alignment
60
+ const MAX_PREFIX_LENGTH = Math.max(
61
+ ...Object.values(LogPrefix).map((p) => p.length),
62
+ )
63
+
64
+ // Log file path is set by initLogFile() after the data directory is known.
65
+ // Before initLogFile() is called, file logging is skipped.
66
+ let logFilePath: string | null = null
67
+
68
+ /**
69
+ * Initialize file logging. Call this after setDataDir() so the log file
70
+ * is written to `<dataDir>/otto.log`. The log file is truncated on
71
+ * every bot startup so it contains only the current run's logs.
72
+ */
73
+ export function initLogFile(dataDir: string): void {
74
+ logFilePath = path.join(dataDir, 'otto.log')
75
+ const logDir = path.dirname(logFilePath)
76
+ if (!fs.existsSync(logDir)) {
77
+ fs.mkdirSync(logDir, { recursive: true })
78
+ }
79
+ fs.writeFileSync(
80
+ logFilePath,
81
+ `--- otto log started at ${new Date().toISOString()} ---\n`,
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Set the log file path without truncating. Use this in child processes
87
+ * (like the opencode plugin) that should append to the same log file
88
+ * the bot process already created with initLogFile().
89
+ */
90
+ export function setLogFilePath(dataDir: string): void {
91
+ logFilePath = path.join(dataDir, 'otto.log')
92
+ }
93
+
94
+ export function getLogFilePath(): string | null {
95
+ return logFilePath
96
+ }
97
+
98
+ const MAX_LOG_ARG_LENGTH = 1000
99
+
100
+ function truncate(str: string, max: number): string {
101
+ if (str.length <= max) return str
102
+ return str.slice(0, max) + `… [truncated ${str.length - max} chars]`
103
+ }
104
+
105
+ function formatArg(arg: unknown): string {
106
+ if (typeof arg === 'string') {
107
+ return truncate(sanitizeSensitiveText(arg, { redactPaths: false }), MAX_LOG_ARG_LENGTH)
108
+ }
109
+ const safeArg = sanitizeUnknownValue(arg, { redactPaths: false })
110
+ return truncate(util.inspect(safeArg, { colors: true, depth: 4 }), MAX_LOG_ARG_LENGTH)
111
+ }
112
+
113
+ export function formatErrorWithStack(error: unknown): string {
114
+ if (error instanceof Error) {
115
+ return sanitizeSensitiveText(
116
+ error.stack ?? `${error.name}: ${error.message}`,
117
+ { redactPaths: false },
118
+ )
119
+ }
120
+ if (typeof error === 'string') {
121
+ return sanitizeSensitiveText(error, { redactPaths: false })
122
+ }
123
+
124
+ // Keep this stable and safe for unknown values (handles circular structures).
125
+ const safeError = sanitizeUnknownValue(error, { redactPaths: false })
126
+ return sanitizeSensitiveText(util.inspect(safeError, { colors: false, depth: 4 }), {
127
+ redactPaths: false,
128
+ })
129
+ }
130
+
131
+ function writeToFile(level: string, prefix: string, args: unknown[]) {
132
+ const timestamp = new Date().toISOString()
133
+ const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`
134
+ if (!logFilePath) {
135
+ return
136
+ }
137
+ fs.appendFileSync(logFilePath, message)
138
+ }
139
+
140
+ function getTimestamp(): string {
141
+ const now = new Date()
142
+ return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
143
+ }
144
+
145
+ function padPrefix(prefix: string): string {
146
+ return prefix.padEnd(MAX_PREFIX_LENGTH)
147
+ }
148
+
149
+ function formatMessage(
150
+ timestamp: string,
151
+ prefix: string,
152
+ args: unknown[],
153
+ ): string {
154
+ return [pc.dim(timestamp), prefix, ...args.map(formatArg)].join(' ')
155
+ }
156
+
157
+ const noSpacing = { spacing: 0 }
158
+
159
+ // Suppress clack terminal output during vitest runs to avoid flooding
160
+ // test output with hundreds of log lines. File logging still works.
161
+ // Set OTTO_TEST_LOGS=1 (or OTTO_TEST_LOGS=1) when rerunning a failing
162
+ // test to see all otto logger output in the terminal for debugging.
163
+ const isVitest = !!(process.env['OTTO_VITEST'] || process.env['OTTO_VITEST'])
164
+ const showTestLogs = isVitest && !!(process.env['OTTO_TEST_LOGS'] || process.env['OTTO_TEST_LOGS'])
165
+
166
+ export function createLogger(prefix: LogPrefixType | string) {
167
+ const paddedPrefix = padPrefix(prefix)
168
+ const suppressConsole = isVitest && !showTestLogs
169
+ const log = (...args: unknown[]) => {
170
+ writeToFile('LOG', prefix, args)
171
+ if (suppressConsole) {
172
+ return
173
+ }
174
+ clackLog.message(
175
+ formatMessage(getTimestamp(), pc.cyan(paddedPrefix), args),
176
+ {
177
+ ...noSpacing,
178
+ },
179
+ )
180
+ }
181
+ return {
182
+ log,
183
+ error: (...args: unknown[]) => {
184
+ writeToFile('ERROR', prefix, args)
185
+ if (suppressConsole) {
186
+ return
187
+ }
188
+ clackLog.error(
189
+ formatMessage(getTimestamp(), pc.red(paddedPrefix), args),
190
+ noSpacing,
191
+ )
192
+ },
193
+ warn: (...args: unknown[]) => {
194
+ writeToFile('WARN', prefix, args)
195
+ if (suppressConsole) {
196
+ return
197
+ }
198
+ clackLog.warn(
199
+ formatMessage(getTimestamp(), pc.yellow(paddedPrefix), args),
200
+ noSpacing,
201
+ )
202
+ },
203
+ info: (...args: unknown[]) => {
204
+ writeToFile('INFO', prefix, args)
205
+ if (suppressConsole) {
206
+ return
207
+ }
208
+ clackLog.info(
209
+ formatMessage(getTimestamp(), pc.blue(paddedPrefix), args),
210
+ noSpacing,
211
+ )
212
+ },
213
+ debug: log,
214
+ }
215
+ }
@@ -0,0 +1,315 @@
1
+ // Deterministic markdown export tests.
2
+ // Uses the shared opencode server manager with the deterministic provider,
3
+ // creates sessions with known content, and validates markdown output.
4
+ // No dependency on machine-local session state.
5
+
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import url from 'node:url'
9
+ import { test, expect, beforeAll, afterAll } from 'vitest'
10
+ import type { OpencodeClient } from '@opencode-ai/sdk/v2'
11
+ import * as errore from 'errore'
12
+ import {
13
+ buildDeterministicOpencodeConfig,
14
+ type DeterministicMatcher,
15
+ } from 'opencode-deterministic-provider'
16
+ import { ShareMarkdown, getCompactSessionContext } from './markdown.js'
17
+ import { setDataDir } from './config.js'
18
+ import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js'
19
+ import { cleanupTestSessions, initTestGitRepo } from './test-utils.js'
20
+
21
+ const ROOT = path.resolve(process.cwd(), 'tmp', 'markdown-test')
22
+
23
+ function createRunDirectories() {
24
+ fs.mkdirSync(ROOT, { recursive: true })
25
+ const dataDir = fs.mkdtempSync(path.join(ROOT, 'data-'))
26
+ const projectDirectory = path.join(ROOT, 'project')
27
+ fs.mkdirSync(projectDirectory, { recursive: true })
28
+ initTestGitRepo(projectDirectory)
29
+ return { dataDir, projectDirectory }
30
+ }
31
+
32
+ function createMatchers(): DeterministicMatcher[] {
33
+ const helloMatcher: DeterministicMatcher = {
34
+ id: 'hello-reply',
35
+ priority: 100,
36
+ when: { latestUserTextIncludes: 'hello markdown test' },
37
+ then: {
38
+ parts: [
39
+ { type: 'stream-start', warnings: [] },
40
+ { type: 'text-start', id: 'hello-text' },
41
+ { type: 'text-delta', id: 'hello-text', delta: 'Hello! This is a deterministic markdown test response.' },
42
+ { type: 'text-end', id: 'hello-text' },
43
+ { type: 'finish', finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 8, totalTokens: 18 } },
44
+ ],
45
+ },
46
+ }
47
+
48
+ const defaultMatcher: DeterministicMatcher = {
49
+ id: 'default-reply',
50
+ priority: 1,
51
+ then: {
52
+ parts: [
53
+ { type: 'stream-start', warnings: [] },
54
+ { type: 'text-start', id: 'default-text' },
55
+ { type: 'text-delta', id: 'default-text', delta: 'ok' },
56
+ { type: 'text-end', id: 'default-text' },
57
+ { type: 'finish', finishReason: 'stop', usage: { inputTokens: 5, outputTokens: 1, totalTokens: 6 } },
58
+ ],
59
+ },
60
+ }
61
+
62
+ return [helloMatcher, defaultMatcher]
63
+ }
64
+
65
+ let client: OpencodeClient
66
+ let directories: ReturnType<typeof createRunDirectories>
67
+ let testStartTime: number
68
+ let sessionID: string
69
+
70
+ beforeAll(async () => {
71
+ testStartTime = Date.now()
72
+ directories = createRunDirectories()
73
+ setDataDir(directories.dataDir)
74
+
75
+ const providerNpm = url
76
+ .pathToFileURL(
77
+ path.resolve(
78
+ process.cwd(),
79
+ '..',
80
+ 'opencode-deterministic-provider',
81
+ 'src',
82
+ 'index.ts',
83
+ ),
84
+ )
85
+ .toString()
86
+
87
+ const opencodeConfig = buildDeterministicOpencodeConfig({
88
+ providerName: 'deterministic-provider',
89
+ providerNpm,
90
+ model: 'deterministic-v2',
91
+ smallModel: 'deterministic-v2',
92
+ settings: {
93
+ strict: false,
94
+ matchers: createMatchers(),
95
+ },
96
+ })
97
+ fs.writeFileSync(
98
+ path.join(directories.projectDirectory, 'opencode.json'),
99
+ JSON.stringify(opencodeConfig, null, 2),
100
+ )
101
+
102
+ // Start the shared opencode server via otto's server manager
103
+ const getClient = await initializeOpencodeForDirectory(
104
+ directories.projectDirectory,
105
+ )
106
+ if (getClient instanceof Error) {
107
+ throw getClient
108
+ }
109
+ client = getClient()
110
+
111
+ // Create a session and send a known prompt
112
+ const createResult = await client.session.create({
113
+ directory: directories.projectDirectory,
114
+ title: 'Markdown Test Session',
115
+ })
116
+ sessionID = createResult.data!.id
117
+
118
+ // Send prompt and wait for completion (promptAsync returns immediately)
119
+ await client.session.promptAsync({
120
+ sessionID,
121
+ directory: directories.projectDirectory,
122
+ parts: [{ type: 'text', text: 'hello markdown test' }],
123
+ })
124
+
125
+ // Wait for assistant text parts to be fully written (not just message existence).
126
+ // The deterministic provider responds instantly but opencode writes parts
127
+ // asynchronously, so we must poll until non-empty text content appears.
128
+ // Under parallel test load the server is slower, so use generous timeouts.
129
+ const maxWait = 15_000
130
+ const pollStart = Date.now()
131
+ while (Date.now() - pollStart < maxWait) {
132
+ const msgs = await client.session.messages({
133
+ sessionID,
134
+ directory: directories.projectDirectory,
135
+ })
136
+ const assistantMsg = msgs.data?.find((m) => m.info.role === 'assistant')
137
+ const hasTextParts = assistantMsg?.parts?.some((p) => {
138
+ return p.type === 'text' && p.text && !p.synthetic
139
+ })
140
+ if (hasTextParts) {
141
+ // Extra wait for step-start and other parts to be flushed
142
+ await new Promise((resolve) => {
143
+ setTimeout(resolve, 500)
144
+ })
145
+ break
146
+ }
147
+ await new Promise((resolve) => {
148
+ setTimeout(resolve, 200)
149
+ })
150
+ }
151
+ }, 20_000)
152
+
153
+ afterAll(async () => {
154
+ if (directories) {
155
+ await cleanupTestSessions({
156
+ projectDirectory: directories.projectDirectory,
157
+ testStartTime,
158
+ })
159
+ }
160
+ await stopOpencodeServer()
161
+ if (directories) {
162
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
163
+ }
164
+ }, 5_000)
165
+
166
+ // Strip dynamic parts (timestamps, durations, branch names) for stable assertions
167
+ function normalizeMarkdown(md: string): string {
168
+ return md
169
+ // Normalize "Completed in Xs" to a fixed string
170
+ .replace(/\*Completed in [\d.]+[ms]+\*/g, '*Completed in Xs*')
171
+ // Normalize "Duration: Xs" tool timing
172
+ .replace(/\*Duration: [\d.]+[ms]+\*/g, '*Duration: Xs*')
173
+ // Normalize ISO dates in session info
174
+ .replace(/\*\*Created\*\*: .+/g, '**Created**: <date>')
175
+ .replace(/\*\*Updated\*\*: .+/g, '**Updated**: <date>')
176
+ // Normalize opencode version
177
+ .replace(/\*\*OpenCode Version\*\*: v[\d.]+.*/g, '**OpenCode Version**: v<version>')
178
+ // Strip git branch context injected by opencode into user messages
179
+ .replace(/\[Current branch: [^\]]+\]\n?\n?/g, '')
180
+ .replace(/\[current git branch is [^\]]+\]\n?\n?/g, '')
181
+ .replace(/\[warning: repository is in detached HEAD[^\]]*\]\n?\n?/g, '')
182
+ }
183
+
184
+ test('generate markdown with system info', async () => {
185
+ const exporter = new ShareMarkdown(client)
186
+
187
+ const markdownResult = await exporter.generate({
188
+ sessionID,
189
+ includeSystemInfo: true,
190
+ })
191
+
192
+ expect(errore.isOk(markdownResult)).toBe(true)
193
+ const markdown = errore.unwrap(markdownResult)
194
+
195
+ expect(markdown).toContain('# Markdown Test Session')
196
+ expect(markdown).toContain('## Session Information')
197
+ expect(markdown).toContain('## Conversation')
198
+ expect(markdown).toContain('### 👤 User')
199
+ expect(markdown).toContain('hello markdown test')
200
+ expect(markdown).toContain('### 🤖 Assistant')
201
+ expect(markdown).toContain('Hello! This is a deterministic markdown test response.')
202
+ expect(markdown).toContain('**Started using deterministic-provider/deterministic-v2**')
203
+
204
+ const normalized = normalizeMarkdown(markdown)
205
+ expect(normalized).toMatchInlineSnapshot(`
206
+ "# Markdown Test Session
207
+
208
+ ## Session Information
209
+
210
+ - **Created**: <date>
211
+ - **Updated**: <date>
212
+ - **OpenCode Version**: v<version>
213
+
214
+ ## Conversation
215
+
216
+ ### 👤 User
217
+
218
+ hello markdown test
219
+
220
+
221
+ ### 🤖 Assistant (deterministic-v2)
222
+
223
+ **Started using deterministic-provider/deterministic-v2**
224
+
225
+ Hello! This is a deterministic markdown test response.
226
+
227
+
228
+ *Completed in Xs*
229
+ "
230
+ `)
231
+ })
232
+
233
+ test('generate markdown without system info', async () => {
234
+ const exporter = new ShareMarkdown(client)
235
+
236
+ const markdown = await exporter.generate({
237
+ sessionID,
238
+ includeSystemInfo: false,
239
+ })
240
+
241
+ expect(errore.isOk(markdown)).toBe(true)
242
+ const md = errore.unwrap(markdown as string)
243
+ expect(md).toContain('# Markdown Test Session')
244
+ expect(md).not.toContain('## Session Information')
245
+ expect(md).toContain('## Conversation')
246
+
247
+ const normalized = normalizeMarkdown(md)
248
+ expect(normalized).toMatchInlineSnapshot(`
249
+ "# Markdown Test Session
250
+
251
+ ## Conversation
252
+
253
+ ### 👤 User
254
+
255
+ hello markdown test
256
+
257
+
258
+ ### 🤖 Assistant (deterministic-v2)
259
+
260
+ **Started using deterministic-provider/deterministic-v2**
261
+
262
+ Hello! This is a deterministic markdown test response.
263
+
264
+
265
+ *Completed in Xs*
266
+ "
267
+ `)
268
+ })
269
+
270
+ test('error handling for non-existent session', async () => {
271
+ const exporter = new ShareMarkdown(client)
272
+ const badSessionID = 'ses_nonexistent_' + Date.now()
273
+
274
+ const result = await exporter.generate({ sessionID: badSessionID })
275
+ expect(result).toBeInstanceOf(Error)
276
+ expect((result as Error).message).toContain(`Session ${badSessionID} not found`)
277
+ })
278
+
279
+ test('getCompactSessionContext generates compact format', async () => {
280
+ const contextResult = await getCompactSessionContext({
281
+ client,
282
+ sessionId: sessionID,
283
+ includeSystemPrompt: false,
284
+ maxMessages: 10,
285
+ })
286
+
287
+ expect(errore.isOk(contextResult)).toBe(true)
288
+ const context = errore.unwrap(contextResult)
289
+
290
+ expect(context).toBeTruthy()
291
+ // User text may be prefixed with branch context injected by opencode
292
+ expect(context).toContain('hello markdown test')
293
+ expect(context).toContain('[User]:')
294
+ expect(context).toContain('[Assistant]:')
295
+ expect(context).toContain('Hello! This is a deterministic markdown test response.')
296
+ expect(context).not.toContain('[System Prompt]')
297
+ })
298
+
299
+ test('generate markdown with lastAssistantOnly', async () => {
300
+ const exporter = new ShareMarkdown(client)
301
+
302
+ const markdownResult = await exporter.generate({
303
+ sessionID,
304
+ lastAssistantOnly: true,
305
+ })
306
+
307
+ expect(errore.isOk(markdownResult)).toBe(true)
308
+ const markdown = errore.unwrap(markdownResult)
309
+
310
+ // lastAssistantOnly should NOT include title header or conversation section header
311
+ expect(markdown).not.toContain('# Markdown Test Session')
312
+ expect(markdown).not.toContain('## Conversation')
313
+ // Should contain the assistant response
314
+ expect(markdown).toContain('Hello! This is a deterministic markdown test response.')
315
+ })