@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,469 @@
1
+ // OpenCode plugin that injects synthetic message parts for context awareness:
2
+ // - Git branch / detached HEAD changes
3
+ // - Working directory (pwd) changes (e.g. after /new-worktree mid-session)
4
+ // - MEMORY.md reminder after a large assistant reply
5
+ // - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
6
+ //
7
+ // Synthetic parts are hidden from the TUI but sent to the model, keeping it
8
+ // aware of context changes without cluttering the UI.
9
+ //
10
+ // State design: all per-session mutable state is encapsulated in a single
11
+ // SessionState object per session ID. One Map, one delete() on cleanup.
12
+ // Decision logic is extracted into pure functions that take state + input
13
+ // and return whether to inject — making them testable without mocking.
14
+ //
15
+ // Exported from otto-opencode-plugin.ts — each export is treated as a separate
16
+ // plugin by OpenCode's plugin loader.
17
+
18
+ import type { Plugin } from '@opencode-ai/plugin'
19
+ import crypto from 'node:crypto'
20
+ import * as errore from 'errore'
21
+ import {
22
+ createPluginLogger,
23
+ formatPluginErrorWithStack,
24
+ setPluginLogFilePath,
25
+ } from './plugin-logger.js'
26
+ import { setDataDir } from './config.js'
27
+ import { initSentry, notifyError } from './sentry.js'
28
+ import { execAsync } from './exec-async.js'
29
+ import {
30
+ ONBOARDING_TUTORIAL_INSTRUCTIONS,
31
+ TUTORIAL_WELCOME_TEXT,
32
+ } from './onboarding-tutorial.js'
33
+
34
+ const logger = createPluginLogger('OPENCODE')
35
+
36
+ // ── Types ────────────────────────────────────────────────────────
37
+
38
+ type GitState = {
39
+ key: string
40
+ kind: 'branch' | 'detached-head' | 'detached-submodule'
41
+ label: string
42
+ warning: string | null
43
+ }
44
+
45
+ // All per-session mutable state in one place. One Map entry, one delete.
46
+ type SessionState = {
47
+ gitState: GitState | undefined
48
+ gitStateDirectory: string | undefined
49
+ gitStateCheckedAtMs: number
50
+ lastMemoryReminderAssistantMessageId: string | undefined
51
+ latestAssistantMessage: AssistantMessageInfo | undefined
52
+ tutorialInjected: boolean
53
+
54
+ // Last directory observed from plugin input.directory.
55
+ // Updated on each real user message to detect cwd switches.
56
+ resolvedDirectory: string | undefined
57
+ // Last directory we announced via pwd injection.
58
+ announcedDirectory: string | undefined
59
+ }
60
+
61
+ // ── Pure derivation functions ────────────────────────────────────
62
+ // These take state + fresh input and return whether to inject.
63
+ // No side effects, no mutations — easy to test with fixtures.
64
+
65
+ export function shouldInjectBranch({
66
+ previousGitState,
67
+ currentGitState,
68
+ }: {
69
+ previousGitState: GitState | undefined
70
+ currentGitState: GitState | null
71
+ }): { inject: false } | { inject: true; text: string } {
72
+ if (!currentGitState) {
73
+ return { inject: false }
74
+ }
75
+ if (previousGitState && previousGitState.key === currentGitState.key) {
76
+ return { inject: false }
77
+ }
78
+ // Trailing newline so this synthetic part does not fuse with the next text
79
+ // part when the model concatenates message parts.
80
+ const base = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`
81
+ return { inject: true, text: `${base}\n` }
82
+ }
83
+
84
+ export function shouldInjectPwd({
85
+ currentDir,
86
+ previousDir,
87
+ announcedDir,
88
+ }: {
89
+ currentDir: string
90
+ previousDir: string | undefined
91
+ announcedDir: string | undefined
92
+ }): { inject: false } | { inject: true; text: string } {
93
+ if (announcedDir === currentDir) {
94
+ return { inject: false }
95
+ }
96
+
97
+ const priorDirectory = announcedDir || previousDir
98
+ if (!priorDirectory || priorDirectory === currentDir) {
99
+ return { inject: false }
100
+ }
101
+
102
+ return {
103
+ inject: true,
104
+ // Trailing newline so this synthetic part does not fuse with the next text
105
+ // part when the model concatenates message parts.
106
+ text:
107
+ `\n[working directory changed (cwd / pwd has changed). ` +
108
+ `The user expects you to edit files in the new cwd. ` +
109
+ `Previous folder (DO NOT TOUCH): ${priorDirectory}. ` +
110
+ `New folder (new cwd / pwd, edit files here): ${currentDir}. ` +
111
+ `You MUST read, write, and edit files only under the new folder ${currentDir}. ` +
112
+ `You MUST NOT read, write, or edit any files under the previous folder ${priorDirectory} — ` +
113
+ `that folder is a separate checkout and the user or another agent may be actively working there, ` +
114
+ `so writing to it would override their unrelated changes.]\n`,
115
+ }
116
+ }
117
+
118
+ const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000
119
+ const GIT_STATE_CACHE_TTL_MS = 2_000
120
+
121
+ type AssistantTokenUsage = {
122
+ input: number
123
+ output: number
124
+ reasoning: number
125
+ cache: { read: number; write: number }
126
+ }
127
+
128
+ type AssistantMessageInfo = {
129
+ id: string
130
+ role: string
131
+ time?: { completed?: number; created?: number }
132
+ tokens?: AssistantTokenUsage
133
+ }
134
+
135
+ export function shouldInjectMemoryReminderFromLatestAssistant({
136
+ lastMemoryReminderAssistantMessageId,
137
+ latestAssistantMessage,
138
+ threshold = MEMORY_REMINDER_OUTPUT_TOKENS,
139
+ }: {
140
+ lastMemoryReminderAssistantMessageId?: string
141
+ latestAssistantMessage: AssistantMessageInfo | undefined
142
+ threshold?: number
143
+ }): { inject: false } | { inject: true; assistantMessageId: string } {
144
+ if (!latestAssistantMessage) {
145
+ return { inject: false }
146
+ }
147
+ if (latestAssistantMessage.role !== 'assistant') {
148
+ return { inject: false }
149
+ }
150
+ if (typeof latestAssistantMessage.time?.completed !== 'number') {
151
+ return { inject: false }
152
+ }
153
+ if (!latestAssistantMessage.tokens) {
154
+ return { inject: false }
155
+ }
156
+ if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
157
+ return { inject: false }
158
+ }
159
+ const outputTokens = Math.max(
160
+ 0,
161
+ latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning,
162
+ )
163
+ if (outputTokens < threshold) {
164
+ return { inject: false }
165
+ }
166
+ return { inject: true, assistantMessageId: latestAssistantMessage.id }
167
+ }
168
+
169
+ export function shouldInjectTutorial({
170
+ alreadyInjected,
171
+ parts,
172
+ }: {
173
+ alreadyInjected: boolean
174
+ parts: Array<{ type: string; text?: string }>
175
+ }): boolean {
176
+ if (alreadyInjected) {
177
+ return false
178
+ }
179
+ return parts.some((part) => {
180
+ return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT)
181
+ })
182
+ }
183
+
184
+ // ── Impure helpers (I/O) ─────────────────────────────────────────
185
+
186
+ async function resolveGitState({
187
+ directory,
188
+ }: {
189
+ directory: string
190
+ }): Promise<GitState | null> {
191
+ const branchResult = await errore.tryAsync(() => {
192
+ return execAsync('git symbolic-ref --short HEAD', { cwd: directory })
193
+ })
194
+ if (!(branchResult instanceof Error)) {
195
+ const branch = branchResult.stdout.trim()
196
+ if (branch) {
197
+ return {
198
+ key: `branch:${branch}`,
199
+ kind: 'branch',
200
+ label: branch,
201
+ warning: null,
202
+ }
203
+ }
204
+ }
205
+
206
+ const shaResult = await errore.tryAsync(() => {
207
+ return execAsync('git rev-parse --short HEAD', { cwd: directory })
208
+ })
209
+ if (shaResult instanceof Error) {
210
+ return null
211
+ }
212
+
213
+ const shortSha = shaResult.stdout.trim()
214
+ if (!shortSha) {
215
+ return null
216
+ }
217
+
218
+ const superprojectResult = await errore.tryAsync(() => {
219
+ return execAsync('git rev-parse --show-superproject-working-tree', {
220
+ cwd: directory,
221
+ })
222
+ })
223
+ const superproject =
224
+ superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim()
225
+ if (superproject) {
226
+ return {
227
+ key: `detached-submodule:${shortSha}`,
228
+ kind: 'detached-submodule',
229
+ label: `detached submodule @ ${shortSha}`,
230
+ warning:
231
+ `\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
232
+ 'create or switch to a branch before committing.]',
233
+ }
234
+ }
235
+
236
+ return {
237
+ key: `detached-head:${shortSha}`,
238
+ kind: 'detached-head',
239
+ label: `detached HEAD @ ${shortSha}`,
240
+ warning:
241
+ `\n[warning: repository is in detached HEAD at ${shortSha}. ` +
242
+ 'create or switch to a branch before committing.]',
243
+ }
244
+ }
245
+
246
+ // ── Plugin ───────────────────────────────────────────────────────
247
+
248
+ const contextAwarenessPlugin: Plugin = async ({ directory }) => {
249
+ initSentry()
250
+
251
+ const dataDir = process.env.OTTO_DATA_DIR
252
+ if (dataDir) {
253
+ setDataDir(dataDir)
254
+ setPluginLogFilePath(dataDir)
255
+ }
256
+
257
+ // Single Map for all per-session state. One entry per session, one
258
+ // delete on cleanup — no parallel Maps that can drift out of sync.
259
+ const sessions = new Map<string, SessionState>()
260
+
261
+ function getOrCreateSession(sessionID: string): SessionState {
262
+ const existing = sessions.get(sessionID)
263
+ if (existing) {
264
+ return existing
265
+ }
266
+ const state: SessionState = {
267
+ gitState: undefined,
268
+ gitStateDirectory: undefined,
269
+ gitStateCheckedAtMs: 0,
270
+ lastMemoryReminderAssistantMessageId: undefined,
271
+ latestAssistantMessage: undefined,
272
+ tutorialInjected: false,
273
+
274
+ resolvedDirectory: undefined,
275
+ announcedDirectory: undefined,
276
+ }
277
+ sessions.set(sessionID, state)
278
+ return state
279
+ }
280
+
281
+ return {
282
+ 'chat.message': async (input, output) => {
283
+ const hookResult = await errore.tryAsync({
284
+ try: async () => {
285
+ const { sessionID } = input
286
+ const state = getOrCreateSession(sessionID)
287
+
288
+ // -- Onboarding tutorial injection --
289
+ // Runs before the non-synthetic text guard because the tutorial
290
+ // marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
291
+ // parts prepended by message-preprocessing.ts. The old separate
292
+ // plugin had no such guard, so this preserves that behavior.
293
+ const firstTextPart = output.parts.find((part) => {
294
+ return part.type === 'text'
295
+ })
296
+ if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
297
+ state.tutorialInjected = true
298
+ output.parts.push({
299
+ id: `prt_${crypto.randomUUID()}`,
300
+ sessionID,
301
+ messageID: firstTextPart.messageID,
302
+ type: 'text' as const,
303
+ text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>\n`,
304
+ synthetic: true,
305
+ })
306
+ }
307
+
308
+ // -- Find first non-synthetic user text part --
309
+ // All remaining injections (branch, pwd, memory, time gap) only
310
+ // apply to real user messages, not empty or synthetic-only messages.
311
+ const first = output.parts.find((part) => {
312
+ if (part.type !== 'text') {
313
+ return true
314
+ }
315
+ return part.synthetic !== true
316
+ })
317
+ if (!first || first.type !== 'text' || first.text.trim().length === 0) {
318
+ return
319
+ }
320
+
321
+ const messageID = first.messageID
322
+
323
+ const latestAssistantMessage = state.latestAssistantMessage
324
+
325
+ // The plugin request directory is the current directory Otto asked
326
+ // OpenCode to operate on for this message. Keep the previous observed
327
+ // directory in session state to detect cwd changes without SDK calls.
328
+ const effectiveDirectory = directory
329
+ const previousDirectory = state.resolvedDirectory
330
+ state.resolvedDirectory = effectiveDirectory
331
+
332
+ // -- Branch / detached HEAD detection --
333
+ // Avoid spawning git subprocesses on every turn in the same directory.
334
+ // Refresh only on directory change or after a short TTL.
335
+ const previousGitState = state.gitState
336
+ const now = Date.now()
337
+ const shouldRefreshGitState =
338
+ !state.gitState ||
339
+ state.gitStateDirectory !== effectiveDirectory ||
340
+ now - state.gitStateCheckedAtMs >= GIT_STATE_CACHE_TTL_MS
341
+ const gitState = shouldRefreshGitState
342
+ ? await resolveGitState({ directory: effectiveDirectory })
343
+ : state.gitState || null
344
+ if (shouldRefreshGitState) {
345
+ state.gitState = gitState || undefined
346
+ state.gitStateDirectory = effectiveDirectory
347
+ state.gitStateCheckedAtMs = now
348
+ }
349
+
350
+ // -- Working directory change detection --
351
+ const pwdResult = shouldInjectPwd({
352
+ currentDir: effectiveDirectory,
353
+ previousDir:
354
+ previousDirectory && previousDirectory !== effectiveDirectory
355
+ ? previousDirectory
356
+ : undefined,
357
+ announcedDir: state.announcedDirectory,
358
+ })
359
+ if (pwdResult.inject) {
360
+ state.announcedDirectory = effectiveDirectory
361
+ output.parts.push({
362
+ id: `prt_${crypto.randomUUID()}`,
363
+ sessionID,
364
+ messageID,
365
+ type: 'text' as const,
366
+ text: pwdResult.text,
367
+ synthetic: true,
368
+ })
369
+ }
370
+
371
+ const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
372
+ lastMemoryReminderAssistantMessageId:
373
+ state.lastMemoryReminderAssistantMessageId,
374
+ latestAssistantMessage,
375
+ })
376
+ if (memoryReminder.inject) {
377
+ output.parts.push({
378
+ id: `prt_${crypto.randomUUID()}`,
379
+ sessionID,
380
+ messageID,
381
+ type: 'text' as const,
382
+ text: '<system-reminder>The previous assistant message was large. If the conversation had non-obvious learnings that prevent future mistakes and are not already in code comments or AGENTS.md, add them to MEMORY.md with concise titles and brief content (2-3 sentences max).</system-reminder>\n',
383
+ synthetic: true,
384
+ })
385
+ state.lastMemoryReminderAssistantMessageId =
386
+ memoryReminder.assistantMessageId
387
+ }
388
+
389
+ // -- Branch injection (last synthetic part) --
390
+ const branchResult = shouldInjectBranch({
391
+ previousGitState,
392
+ currentGitState: gitState,
393
+ })
394
+ if (branchResult.inject) {
395
+ state.gitState = gitState!
396
+ output.parts.push({
397
+ id: `prt_${crypto.randomUUID()}`,
398
+ sessionID,
399
+ messageID,
400
+ type: 'text' as const,
401
+ text: branchResult.text,
402
+ synthetic: true,
403
+ })
404
+ }
405
+ },
406
+ catch: (error) => {
407
+ return new Error('context-awareness chat.message hook failed', { cause: error })
408
+ },
409
+ })
410
+ if (hookResult instanceof Error) {
411
+ logger.warn(
412
+ `[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`,
413
+ )
414
+ void notifyError(hookResult, 'context-awareness plugin chat.message hook failed')
415
+ }
416
+ },
417
+
418
+ // Keep session-local assistant metadata updated from event stream and
419
+ // clean up state when sessions are deleted.
420
+ event: async ({ event }) => {
421
+ const eventResult = await errore.tryAsync({
422
+ try: async () => {
423
+ if (event.type === 'message.updated') {
424
+ const info = event.properties?.info
425
+ if (!info || info.role !== 'assistant' || typeof info.id !== 'string') {
426
+ return
427
+ }
428
+ const infoWithSession = info as { sessionID?: unknown }
429
+ const sessionID =
430
+ typeof infoWithSession.sessionID === 'string'
431
+ ? infoWithSession.sessionID
432
+ : undefined
433
+ if (!sessionID) {
434
+ return
435
+ }
436
+ const state = getOrCreateSession(sessionID)
437
+ state.latestAssistantMessage = {
438
+ id: info.id,
439
+ role: info.role,
440
+ time: info.time,
441
+ tokens: info.tokens,
442
+ }
443
+ return
444
+ }
445
+
446
+ if (event.type !== 'session.deleted') {
447
+ return
448
+ }
449
+ const id = event.properties?.info?.id
450
+ if (!id) {
451
+ return
452
+ }
453
+ sessions.delete(id)
454
+ },
455
+ catch: (error) => {
456
+ return new Error('context-awareness event hook failed', { cause: error })
457
+ },
458
+ })
459
+ if (eventResult instanceof Error) {
460
+ logger.warn(
461
+ `[context-awareness-plugin] ${formatPluginErrorWithStack(eventResult)}`,
462
+ )
463
+ void notifyError(eventResult, 'context-awareness plugin event hook failed')
464
+ }
465
+ },
466
+ }
467
+ }
468
+
469
+ export { contextAwarenessPlugin }
@@ -0,0 +1,139 @@
1
+ // Shared utilities for invoking the critique CLI and parsing its JSON output.
2
+ // Used by /diff command and footer diff link uploads.
3
+
4
+ import { execAsync } from './worktrees.js'
5
+ import { createLogger, LogPrefix } from './logger.js'
6
+
7
+ const logger = createLogger(LogPrefix.DIFF)
8
+
9
+ const CRITIQUE_TIMEOUT_MS = 30_000
10
+
11
+ /**
12
+ * Shell-quote a string by wrapping in single quotes and escaping embedded
13
+ * single quotes. Prevents injection when interpolating into shell commands.
14
+ */
15
+ function shellQuote(s: string): string {
16
+ return `'${s.replace(/'/g, "'\\''")}'`
17
+ }
18
+
19
+ export type CritiqueResult = {
20
+ url: string
21
+ id: string
22
+ error?: undefined
23
+ } | {
24
+ url?: undefined
25
+ id?: undefined
26
+ error: string
27
+ }
28
+
29
+ /**
30
+ * Parse critique --json output. Critique prints progress to stderr and JSON
31
+ * to stdout. The JSON line contains { url, id } on success or { error } on
32
+ * failure. We scan all lines for the first valid JSON object with a url or
33
+ * error field, falling back to searching for a critique.work URL in the raw
34
+ * output.
35
+ */
36
+ export function parseCritiqueOutput(output: string): CritiqueResult | undefined {
37
+ const lines = output.trim().split('\n')
38
+ for (const line of lines) {
39
+ if (!line.startsWith('{')) {
40
+ continue
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(line) as {
44
+ url?: string
45
+ id?: string
46
+ error?: string
47
+ }
48
+ if (parsed.error) {
49
+ return { error: parsed.error }
50
+ }
51
+ if (parsed.url && parsed.id) {
52
+ return { url: parsed.url, id: parsed.id }
53
+ }
54
+ } catch {
55
+ // not valid JSON, try next line
56
+ }
57
+ }
58
+ // Fallback: try to find a URL in the raw output
59
+ const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/)
60
+ if (urlMatch) {
61
+ const url = urlMatch[0]
62
+ // Extract ID from URL path: /v/{id}
63
+ const idMatch = url.match(/\/v\/([a-f0-9]+)/)
64
+ const id = idMatch?.[1]
65
+ if (id) {
66
+ return { url, id }
67
+ }
68
+ // URL without parseable id — return as error so callers don't build
69
+ // broken OG image URLs from an empty id
70
+ return { error: url }
71
+ }
72
+ return undefined
73
+ }
74
+
75
+ /**
76
+ * Run critique on the current git working tree diff and return the result.
77
+ * Used by the /diff slash command.
78
+ */
79
+ export async function uploadGitDiffViaCritique({
80
+ title,
81
+ cwd,
82
+ }: {
83
+ title: string
84
+ cwd: string
85
+ }): Promise<CritiqueResult | undefined> {
86
+ try {
87
+ const { stdout, stderr } = await execAsync(
88
+ `critique --web ${shellQuote(title)} --json`,
89
+ { cwd, timeout: CRITIQUE_TIMEOUT_MS },
90
+ )
91
+ return parseCritiqueOutput(stdout || stderr)
92
+ } catch (error) {
93
+ // exec error includes stdout/stderr — try to parse JSON from it
94
+ const execError = error as {
95
+ stdout?: string
96
+ stderr?: string
97
+ message?: string
98
+ }
99
+ const output = execError.stdout || execError.stderr || ''
100
+ const parsed = parseCritiqueOutput(output)
101
+ if (parsed) {
102
+ return parsed
103
+ }
104
+ const message = execError.message || 'Unknown error'
105
+ if (message.includes('command not found') || message.includes('ENOENT')) {
106
+ return { error: 'critique not available' }
107
+ }
108
+ return { error: `Failed to generate diff: ${message.slice(0, 200)}` }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Upload a .patch file to critique.work via critique --stdin.
114
+ * Returns the critique URL on success, undefined on failure.
115
+ * Default timeout is 10s since this runs in the background (footer edit).
116
+ */
117
+ export async function uploadPatchViaCritique({
118
+ patchPath,
119
+ title,
120
+ cwd,
121
+ timeoutMs = 10_000,
122
+ }: {
123
+ patchPath: string
124
+ title: string
125
+ cwd: string
126
+ timeoutMs?: number
127
+ }): Promise<string | undefined> {
128
+ try {
129
+ const { stdout } = await execAsync(
130
+ `critique --stdin --web ${shellQuote(title)} --json < ${shellQuote(patchPath)}`,
131
+ { cwd, timeout: timeoutMs },
132
+ )
133
+ const result = parseCritiqueOutput(stdout)
134
+ return result?.url
135
+ } catch (error) {
136
+ logger.error('critique upload failed:', error)
137
+ return undefined
138
+ }
139
+ }