@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,410 @@
1
+ // Session-to-markdown renderer for sharing.
2
+ // Generates shareable markdown from OpenCode sessions, formatting
3
+ // user messages, assistant responses, tool calls, and reasoning blocks.
4
+ // Uses errore for type-safe error handling.
5
+
6
+ import type { OpencodeClient } from '@opencode-ai/sdk/v2'
7
+ import * as errore from 'errore'
8
+ import { createTaggedError } from 'errore'
9
+ import YAML from 'yaml'
10
+ import { formatDateTime } from './utils.js'
11
+ import { extractNonXmlContent } from './xml.js'
12
+ import { createLogger, LogPrefix } from './logger.js'
13
+ import { SessionNotFoundError, MessagesNotFoundError } from './errors.js'
14
+
15
+ // Generic error for unexpected exceptions in async operations
16
+ class UnexpectedError extends createTaggedError({
17
+ name: 'UnexpectedError',
18
+ }) {}
19
+
20
+ const markdownLogger = createLogger(LogPrefix.MARKDOWN)
21
+
22
+ const TOOL_OUTPUT_MAX_CHARS = 30_000
23
+
24
+ export class ShareMarkdown {
25
+ constructor(private client: OpencodeClient) {}
26
+
27
+ /**
28
+ * Generate a markdown representation of a session
29
+ * @param options Configuration options
30
+ * @returns Error or markdown string
31
+ */
32
+ async generate(options: {
33
+ sessionID: string
34
+ includeSystemInfo?: boolean
35
+ lastAssistantOnly?: boolean
36
+ }): Promise<SessionNotFoundError | MessagesNotFoundError | string> {
37
+ const { sessionID, includeSystemInfo, lastAssistantOnly } = options
38
+
39
+ // Get session info
40
+ const sessionResponse = await this.client.session.get({
41
+ sessionID,
42
+ })
43
+ if (!sessionResponse.data) {
44
+ return new SessionNotFoundError({ sessionId: sessionID })
45
+ }
46
+ const session = sessionResponse.data
47
+
48
+ // Get all messages
49
+ const messagesResponse = await this.client.session.messages({
50
+ sessionID,
51
+ })
52
+ if (!messagesResponse.data) {
53
+ return new MessagesNotFoundError({ sessionId: sessionID })
54
+ }
55
+ const messages = messagesResponse.data
56
+
57
+ // If lastAssistantOnly, filter to only the last assistant message
58
+ const messagesToRender = lastAssistantOnly
59
+ ? (() => {
60
+ const assistantMessages = messages.filter(
61
+ (m) => m.info.role === 'assistant',
62
+ )
63
+ return assistantMessages.length > 0
64
+ ? [assistantMessages[assistantMessages.length - 1]]
65
+ : []
66
+ })()
67
+ : messages
68
+
69
+ // Build markdown
70
+ const lines: string[] = []
71
+
72
+ // Only include header and session info if not lastAssistantOnly
73
+ if (!lastAssistantOnly) {
74
+ // Header
75
+ lines.push(`# ${session.title || 'Untitled Session'}`)
76
+ lines.push('')
77
+
78
+ // Session metadata
79
+ if (includeSystemInfo === true) {
80
+ lines.push('## Session Information')
81
+ lines.push('')
82
+ lines.push(
83
+ `- **Created**: ${formatDateTime(new Date(session.time.created))}`,
84
+ )
85
+ lines.push(
86
+ `- **Updated**: ${formatDateTime(new Date(session.time.updated))}`,
87
+ )
88
+ if (session.version) {
89
+ lines.push(`- **OpenCode Version**: v${session.version}`)
90
+ }
91
+ lines.push('')
92
+ }
93
+
94
+ // Process messages
95
+ lines.push('## Conversation')
96
+ lines.push('')
97
+ }
98
+
99
+ for (const message of messagesToRender) {
100
+ const messageLines = this.renderMessage(message!.info, message!.parts)
101
+ lines.push(...messageLines)
102
+ lines.push('')
103
+ }
104
+
105
+ return lines.join('\n')
106
+ }
107
+
108
+ private renderMessage(message: any, parts: any[]): string[] {
109
+ const lines: string[] = []
110
+
111
+ if (message.role === 'user') {
112
+ lines.push('### 👤 User')
113
+ lines.push('')
114
+
115
+ for (const part of parts) {
116
+ if (part.type === 'text' && part.text) {
117
+ const cleanedText = extractNonXmlContent(part.text)
118
+ if (cleanedText.trim()) {
119
+ lines.push(cleanedText)
120
+ lines.push('')
121
+ }
122
+ } else if (part.type === 'file') {
123
+ lines.push(`📎 **Attachment**: ${part.filename || 'unnamed file'}`)
124
+ if (part.url) {
125
+ lines.push(` - URL: ${part.url}`)
126
+ }
127
+ lines.push('')
128
+ }
129
+ }
130
+ } else if (message.role === 'assistant') {
131
+ lines.push(`### 🤖 Assistant (${message.modelID || 'unknown model'})`)
132
+ lines.push('')
133
+
134
+ // Filter and process parts
135
+ const filteredParts = parts.filter((part) => {
136
+ if (part.type === 'step-start' && parts.indexOf(part) > 0) return false
137
+ if (part.type === 'snapshot') return false
138
+ if (part.type === 'patch') return false
139
+ if (part.type === 'step-finish') return false
140
+ if (part.type === 'text' && part.synthetic === true) return false
141
+ if (part.type === 'tool' && part.tool === 'todoread') return false
142
+ if (part.type === 'text' && !part.text) return false
143
+ if (
144
+ part.type === 'tool' &&
145
+ (part.state.status === 'pending' || part.state.status === 'running')
146
+ )
147
+ return false
148
+ return true
149
+ })
150
+
151
+ for (const part of filteredParts) {
152
+ const partLines = this.renderPart(part, message)
153
+ lines.push(...partLines)
154
+ }
155
+
156
+ // Add completion time if available
157
+ if (message.time?.completed) {
158
+ const duration = message.time.completed - message.time.created
159
+ lines.push('')
160
+ lines.push(`*Completed in ${this.formatDuration(duration)}*`)
161
+ }
162
+ }
163
+
164
+ return lines
165
+ }
166
+
167
+ private renderPart(part: any, message: any): string[] {
168
+ const lines: string[] = []
169
+
170
+ switch (part.type) {
171
+ case 'text':
172
+ if (part.text) {
173
+ lines.push(part.text)
174
+ lines.push('')
175
+ }
176
+ break
177
+
178
+ case 'reasoning':
179
+ if (part.text) {
180
+ lines.push('<details>')
181
+ lines.push('<summary>💭 Thinking</summary>')
182
+ lines.push('')
183
+ lines.push(part.text)
184
+ lines.push('')
185
+ lines.push('</details>')
186
+ lines.push('')
187
+ }
188
+ break
189
+
190
+ case 'tool':
191
+ if (part.state.status === 'completed') {
192
+ const output: string = part.state.output || ''
193
+ const isOversized = output.length > TOOL_OUTPUT_MAX_CHARS
194
+
195
+ if (isOversized) {
196
+ lines.push(
197
+ `> ⚠️ **Large tool output** (${output.length.toLocaleString()} chars, truncated to ${TOOL_OUTPUT_MAX_CHARS.toLocaleString()})`,
198
+ )
199
+ lines.push('')
200
+ }
201
+
202
+ lines.push(`#### 🛠️ Tool: ${part.tool}`)
203
+ lines.push('')
204
+
205
+ // Render input parameters in YAML
206
+ if (part.state.input && Object.keys(part.state.input).length > 0) {
207
+ lines.push('**Input:**')
208
+ lines.push('```yaml')
209
+ lines.push(YAML.stringify(part.state.input, null, { lineWidth: 0 }))
210
+ lines.push('```')
211
+ lines.push('')
212
+ }
213
+
214
+ // Render output, truncated if too large
215
+ if (output) {
216
+ lines.push('**Output:**')
217
+ lines.push('```')
218
+ lines.push(
219
+ isOversized
220
+ ? output.slice(0, TOOL_OUTPUT_MAX_CHARS) +
221
+ '\n...(truncated)'
222
+ : output,
223
+ )
224
+ lines.push('```')
225
+ lines.push('')
226
+ }
227
+
228
+ // Add timing info if significant
229
+ if (part.state.time?.start && part.state.time?.end) {
230
+ const duration = part.state.time.end - part.state.time.start
231
+ if (duration > 2000) {
232
+ lines.push(`*Duration: ${this.formatDuration(duration)}*`)
233
+ lines.push('')
234
+ }
235
+ }
236
+ } else if (part.state.status === 'error') {
237
+ lines.push(`#### ❌ Tool Error: ${part.tool}`)
238
+ lines.push('')
239
+ lines.push('```')
240
+ lines.push(part.state.error || 'Unknown error')
241
+ lines.push('```')
242
+ lines.push('')
243
+ }
244
+ break
245
+
246
+ case 'step-start':
247
+ lines.push(`**Started using ${message.providerID}/${message.modelID}**`)
248
+ lines.push('')
249
+ break
250
+ }
251
+
252
+ return lines
253
+ }
254
+
255
+ private formatDuration(ms: number): string {
256
+ if (ms < 1000) return `${ms}ms`
257
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
258
+ const minutes = Math.floor(ms / 60000)
259
+ const seconds = Math.floor((ms % 60000) / 1000)
260
+ return `${minutes}m ${seconds}s`
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Generate compact session context for voice transcription.
266
+ * Includes system prompt (optional), user messages, assistant text,
267
+ * and tool calls in compact form (name + params only, no output).
268
+ */
269
+ export function getCompactSessionContext({
270
+ client,
271
+ sessionId,
272
+ includeSystemPrompt = false,
273
+ maxMessages = 20,
274
+ }: {
275
+ client: OpencodeClient
276
+ sessionId: string
277
+ includeSystemPrompt?: boolean
278
+ maxMessages?: number
279
+ }): Promise<UnexpectedError | string> {
280
+ return errore.tryAsync({
281
+ try: async () => {
282
+ const messagesResponse = await client.session.messages({
283
+ sessionID: sessionId,
284
+ })
285
+ const messages = messagesResponse.data || []
286
+
287
+ const lines: string[] = []
288
+
289
+ // Get system prompt if requested
290
+ // Note: OpenCode SDK doesn't expose system prompt directly. We try multiple approaches:
291
+ // 1. session.system field (if available in future SDK versions)
292
+ // 2. synthetic text part in first assistant message (current approach)
293
+ if (includeSystemPrompt && messages.length > 0) {
294
+ const firstAssistant = messages.find((m) => m.info.role === 'assistant')
295
+ if (firstAssistant) {
296
+ // look for text part marked as synthetic (system prompt)
297
+ const systemPart = (firstAssistant.parts || []).find(
298
+ (p) => p.type === 'text' && (p as any).synthetic === true,
299
+ )
300
+ if (systemPart && 'text' in systemPart && systemPart.text) {
301
+ lines.push('[System Prompt]')
302
+ const truncated = systemPart.text.slice(0, 3000)
303
+ lines.push(truncated)
304
+ if (systemPart.text.length > 3000) {
305
+ lines.push('...(truncated)')
306
+ }
307
+ lines.push('')
308
+ }
309
+ }
310
+ }
311
+
312
+ // Process recent messages
313
+ const recentMessages = messages.slice(-maxMessages)
314
+
315
+ for (const msg of recentMessages) {
316
+ if (msg.info.role === 'user') {
317
+ const textParts = (msg.parts || [])
318
+ .filter((p) => p.type === 'text')
319
+ .map((p) => (p.type === 'text' ? extractNonXmlContent(p.text || '') : ''))
320
+ .filter(Boolean)
321
+ if (textParts.length > 0) {
322
+ lines.push(`[User]: ${textParts.join(' ').slice(0, 1000)}`)
323
+ lines.push('')
324
+ }
325
+ } else if (msg.info.role === 'assistant') {
326
+ // Get assistant text parts (non-synthetic, non-empty)
327
+ const textParts = (msg.parts || [])
328
+ .filter(
329
+ (p) => p.type === 'text' && !p.synthetic && p.text,
330
+ )
331
+ .map((p) => (p.type === 'text' ? p.text : ''))
332
+ .filter(Boolean)
333
+ if (textParts.length > 0) {
334
+ lines.push(`[Assistant]: ${textParts.join(' ').slice(0, 1000)}`)
335
+ lines.push('')
336
+ }
337
+
338
+ // Get tool calls in compact form (name + params only)
339
+ const toolParts = (msg.parts || []).filter(
340
+ (p) =>
341
+ p.type === 'tool' &&
342
+ 'state' in p &&
343
+ p.state?.status === 'completed',
344
+ )
345
+ for (const part of toolParts) {
346
+ if (part.type === 'tool' && 'tool' in part && 'state' in part) {
347
+ const toolName = part.tool
348
+ // skip noisy tools
349
+ if (toolName === 'todoread' || toolName === 'todowrite') {
350
+ continue
351
+ }
352
+ const input = part.state?.input || {}
353
+ const normalize = (value: string) =>
354
+ value.replace(/\s+/g, ' ').trim()
355
+ // compact params: just key=value on one line
356
+ const params = Object.entries(input)
357
+ .map(([k, v]) => {
358
+ const val =
359
+ typeof v === 'string'
360
+ ? v.slice(0, 100)
361
+ : JSON.stringify(v).slice(0, 100)
362
+ return `${k}=${normalize(val)}`
363
+ })
364
+ .join(', ')
365
+ lines.push(`[Tool ${toolName}]: ${params}`)
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ return lines.join('\n').slice(0, 8000)
372
+ },
373
+ catch: (e) => {
374
+ markdownLogger.error('Failed to get compact session context:', e)
375
+ return new UnexpectedError({
376
+ message: 'Failed to get compact session context',
377
+ cause: e,
378
+ })
379
+ },
380
+ })
381
+ }
382
+
383
+ /**
384
+ * Get the last session for a directory (excluding the current one).
385
+ */
386
+ export function getLastSessionId({
387
+ client,
388
+ excludeSessionId,
389
+ }: {
390
+ client: OpencodeClient
391
+ excludeSessionId?: string
392
+ }): Promise<UnexpectedError | (string | null)> {
393
+ return errore.tryAsync({
394
+ try: async () => {
395
+ const sessionsResponse = await client.session.list()
396
+ const sessions = sessionsResponse.data || []
397
+
398
+ // Sessions are sorted by time, get the most recent one that isn't the current
399
+ const lastSession = sessions.find((s) => s.id !== excludeSessionId)
400
+ return lastSession?.id || null
401
+ },
402
+ catch: (e) => {
403
+ markdownLogger.error('Failed to get last session:', e)
404
+ return new UnexpectedError({
405
+ message: 'Failed to get last session',
406
+ cause: e,
407
+ })
408
+ },
409
+ })
410
+ }
@@ -0,0 +1,163 @@
1
+ // OpenCode plugin that snapshots the MEMORY.md heading overview once per
2
+ // session and injects that frozen snapshot on the first real user message.
3
+ // The snapshot is cached by session ID so later MEMORY.md edits do not change
4
+ // the prompt for the same session and do not invalidate OpenCode's cache.
5
+
6
+ import crypto from 'node:crypto'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import type { Plugin } from '@opencode-ai/plugin'
10
+ import * as errore from 'errore'
11
+ import {
12
+ createPluginLogger,
13
+ formatPluginErrorWithStack,
14
+ setPluginLogFilePath,
15
+ } from './plugin-logger.js'
16
+ import { condenseMemoryMd } from './condense-memory.js'
17
+ import { initSentry, notifyError } from './sentry.js'
18
+
19
+ const logger = createPluginLogger('OPENCODE')
20
+
21
+ type SessionState = {
22
+ hasFrozenOverview: boolean
23
+ frozenOverviewText: string | null
24
+ injected: boolean
25
+ }
26
+
27
+ function createSessionState(): SessionState {
28
+ return {
29
+ hasFrozenOverview: false,
30
+ frozenOverviewText: null,
31
+ injected: false,
32
+ }
33
+ }
34
+
35
+ function buildMemoryOverviewReminder({ condensed }: { condensed: string }): string {
36
+ // Trailing newline so this synthetic part does not fuse with the next text
37
+ // part when the model concatenates message parts.
38
+ return `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>\n`
39
+ }
40
+
41
+ async function freezeMemoryOverview({
42
+ directory,
43
+ state,
44
+ }: {
45
+ directory: string
46
+ state: SessionState
47
+ }): Promise<string | null> {
48
+ if (state.hasFrozenOverview) {
49
+ return state.frozenOverviewText
50
+ }
51
+
52
+ const memoryPath = path.join(directory, 'MEMORY.md')
53
+ const memoryContentResult = await fs.promises.readFile(memoryPath, 'utf-8').catch(() => {
54
+ return null
55
+ })
56
+ if (!memoryContentResult) {
57
+ state.hasFrozenOverview = true
58
+ state.frozenOverviewText = null
59
+ return null
60
+ }
61
+
62
+ const condensed = condenseMemoryMd(memoryContentResult)
63
+ state.hasFrozenOverview = true
64
+ state.frozenOverviewText = buildMemoryOverviewReminder({ condensed })
65
+ return state.frozenOverviewText
66
+ }
67
+
68
+ const memoryOverviewPlugin: Plugin = async ({ directory }) => {
69
+ initSentry()
70
+
71
+ const dataDir = process.env.OTTO_DATA_DIR
72
+ if (dataDir) {
73
+ setPluginLogFilePath(dataDir)
74
+ }
75
+
76
+ const sessions = new Map<string, SessionState>()
77
+
78
+ function getOrCreateSessionState({ sessionID }: { sessionID: string }): SessionState {
79
+ const existing = sessions.get(sessionID)
80
+ if (existing) {
81
+ return existing
82
+ }
83
+ const state = createSessionState()
84
+ sessions.set(sessionID, state)
85
+ return state
86
+ }
87
+
88
+ return {
89
+ 'chat.message': async (input, output) => {
90
+ const result = await errore.tryAsync({
91
+ try: async () => {
92
+ const state = getOrCreateSessionState({ sessionID: input.sessionID })
93
+ if (state.injected) {
94
+ return
95
+ }
96
+
97
+ const firstPart = output.parts.find((part) => {
98
+ if (part.type !== 'text') {
99
+ return true
100
+ }
101
+ return part.synthetic !== true
102
+ })
103
+ if (!firstPart || firstPart.type !== 'text' || firstPart.text.trim().length === 0) {
104
+ return
105
+ }
106
+
107
+ const overviewText = await freezeMemoryOverview({ directory, state })
108
+ state.injected = true
109
+ if (!overviewText) {
110
+ return
111
+ }
112
+
113
+ output.parts.push({
114
+ id: `prt_${crypto.randomUUID()}`,
115
+ sessionID: input.sessionID,
116
+ messageID: firstPart.messageID,
117
+ type: 'text' as const,
118
+ text: overviewText,
119
+ synthetic: true,
120
+ })
121
+ },
122
+ catch: (error) => {
123
+ return new Error('memory overview chat.message hook failed', {
124
+ cause: error,
125
+ })
126
+ },
127
+ })
128
+ if (!(result instanceof Error)) {
129
+ return
130
+ }
131
+ logger.warn(
132
+ `[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`,
133
+ )
134
+ void notifyError(result, 'memory overview plugin chat.message hook failed')
135
+ },
136
+ event: async ({ event }) => {
137
+ const result = await errore.tryAsync({
138
+ try: async () => {
139
+ if (event.type !== 'session.deleted') {
140
+ return
141
+ }
142
+ const id = event.properties?.info?.id
143
+ if (!id) {
144
+ return
145
+ }
146
+ sessions.delete(id)
147
+ },
148
+ catch: (error) => {
149
+ return new Error('memory overview event hook failed', {
150
+ cause: error,
151
+ })
152
+ },
153
+ })
154
+ if (!(result instanceof Error)) {
155
+ return
156
+ }
157
+ logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`)
158
+ void notifyError(result, 'memory overview plugin event hook failed')
159
+ },
160
+ }
161
+ }
162
+
163
+ export { memoryOverviewPlugin }