@otto-assistant/bridge 0.4.92

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 (483) 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-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,142 @@
1
+ // Tests for context-awareness directory switch reminders.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import {
5
+ shouldInjectPwd,
6
+ shouldInjectMemoryReminderFromLatestAssistant,
7
+ } from './context-awareness-plugin.js'
8
+
9
+ describe('shouldInjectPwd', () => {
10
+ test('does not inject when current directory matches announced directory', () => {
11
+ const result = shouldInjectPwd({
12
+ currentDir: '/repo/worktree',
13
+ previousDir: '/repo/main',
14
+ announcedDir: '/repo/worktree',
15
+ })
16
+
17
+ expect(result).toMatchInlineSnapshot(`
18
+ {
19
+ "inject": false,
20
+ }
21
+ `)
22
+ })
23
+
24
+ test('does not inject without a previous directory to warn about', () => {
25
+ const result = shouldInjectPwd({
26
+ currentDir: '/repo/worktree',
27
+ previousDir: undefined,
28
+ announcedDir: undefined,
29
+ })
30
+
31
+ expect(result).toMatchInlineSnapshot(`
32
+ {
33
+ "inject": false,
34
+ }
35
+ `)
36
+ })
37
+
38
+ test('names previous and current directories in the correct order', () => {
39
+ const result = shouldInjectPwd({
40
+ currentDir: '/repo/worktree',
41
+ previousDir: '/repo/main',
42
+ announcedDir: undefined,
43
+ })
44
+
45
+ expect(result).toMatchInlineSnapshot(`
46
+ {
47
+ "inject": true,
48
+ "text": "
49
+ [working directory changed. Previous working directory: /repo/main. Current working directory: /repo/worktree. You MUST read, write, and edit files only under /repo/worktree. Do NOT read, write, or edit files under /repo/main.]",
50
+ }
51
+ `)
52
+ })
53
+
54
+ test('prefers the last announced directory as the previous directory', () => {
55
+ const result = shouldInjectPwd({
56
+ currentDir: '/repo/worktree-b',
57
+ previousDir: '/repo/main',
58
+ announcedDir: '/repo/worktree-a',
59
+ })
60
+
61
+ expect(result).toMatchInlineSnapshot(`
62
+ {
63
+ "inject": true,
64
+ "text": "
65
+ [working directory changed. Previous working directory: /repo/worktree-a. Current working directory: /repo/worktree-b. You MUST read, write, and edit files only under /repo/worktree-b. Do NOT read, write, or edit files under /repo/worktree-a.]",
66
+ }
67
+ `)
68
+ })
69
+ })
70
+
71
+ describe('shouldInjectMemoryReminderFromLatestAssistant', () => {
72
+ test('does not trigger before threshold', () => {
73
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
74
+ latestAssistantMessage: {
75
+ id: 'msg_asst_1',
76
+ role: 'assistant',
77
+ time: { completed: 1 },
78
+ tokens: {
79
+ input: 1_000,
80
+ output: 3_000,
81
+ reasoning: 500,
82
+ cache: { read: 0, write: 0 },
83
+ },
84
+ },
85
+ threshold: 10_000,
86
+ })
87
+
88
+ expect(result).toMatchInlineSnapshot(`
89
+ {
90
+ "inject": false,
91
+ }
92
+ `)
93
+ })
94
+
95
+ test('triggers when latest assistant message exceeds threshold', () => {
96
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
97
+ latestAssistantMessage: {
98
+ id: 'msg_asst_2',
99
+ role: 'assistant',
100
+ time: { completed: 2 },
101
+ tokens: {
102
+ input: 2_000,
103
+ output: 2_200,
104
+ reasoning: 400,
105
+ cache: { read: 0, write: 0 },
106
+ },
107
+ },
108
+ threshold: 2_000,
109
+ })
110
+
111
+ expect(result).toMatchInlineSnapshot(`
112
+ {
113
+ "assistantMessageId": "msg_asst_2",
114
+ "inject": true,
115
+ }
116
+ `)
117
+ })
118
+
119
+ test('does not trigger again for the same reminded assistant message', () => {
120
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
121
+ lastMemoryReminderAssistantMessageId: 'msg_asst_3',
122
+ latestAssistantMessage: {
123
+ id: 'msg_asst_3',
124
+ role: 'assistant',
125
+ time: { completed: 3 },
126
+ tokens: {
127
+ input: 2_000,
128
+ output: 2_200,
129
+ reasoning: 400,
130
+ cache: { read: 0, write: 0 },
131
+ },
132
+ },
133
+ threshold: 10_000,
134
+ })
135
+
136
+ expect(result).toMatchInlineSnapshot(`
137
+ {
138
+ "inject": false,
139
+ }
140
+ `)
141
+ })
142
+ })
@@ -0,0 +1,510 @@
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 table of contents on first message
5
+ // - MEMORY.md reminder after a large assistant reply
6
+ // - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
7
+ //
8
+ // Synthetic parts are hidden from the TUI but sent to the model, keeping it
9
+ // aware of context changes without cluttering the UI.
10
+ //
11
+ // State design: all per-session mutable state is encapsulated in a single
12
+ // SessionState object per session ID. One Map, one delete() on cleanup.
13
+ // Decision logic is extracted into pure functions that take state + input
14
+ // and return whether to inject — making them testable without mocking.
15
+ //
16
+ // Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
17
+ // plugin by OpenCode's plugin loader.
18
+
19
+ import type { Plugin } from '@opencode-ai/plugin'
20
+ import crypto from 'node:crypto'
21
+ import fs from 'node:fs'
22
+ import path from 'node:path'
23
+ import * as errore from 'errore'
24
+ import {
25
+ createPluginLogger,
26
+ formatPluginErrorWithStack,
27
+ setPluginLogFilePath,
28
+ } from './plugin-logger.js'
29
+ import { setDataDir } from './config.js'
30
+ import { initSentry, notifyError } from './sentry.js'
31
+ import { execAsync } from './exec-async.js'
32
+ import { condenseMemoryMd } from './condense-memory.js'
33
+ import {
34
+ ONBOARDING_TUTORIAL_INSTRUCTIONS,
35
+ TUTORIAL_WELCOME_TEXT,
36
+ } from './onboarding-tutorial.js'
37
+
38
+ const logger = createPluginLogger('OPENCODE')
39
+
40
+ // ── Types ────────────────────────────────────────────────────────
41
+
42
+ type GitState = {
43
+ key: string
44
+ kind: 'branch' | 'detached-head' | 'detached-submodule'
45
+ label: string
46
+ warning: string | null
47
+ }
48
+
49
+ // All per-session mutable state in one place. One Map entry, one delete.
50
+ type SessionState = {
51
+ gitState: GitState | undefined
52
+ memoryInjected: boolean
53
+ lastMemoryReminderAssistantMessageId: string | undefined
54
+ tutorialInjected: boolean
55
+ // Last directory observed via session.get(). Refreshed on each real user
56
+ // message so directory-change reminders compare the latest observed session
57
+ // directory against the current request directory.
58
+ resolvedDirectory: string | undefined
59
+ // Last directory we announced via pwd injection.
60
+ announcedDirectory: string | undefined
61
+ }
62
+
63
+ function createSessionState(): SessionState {
64
+ return {
65
+ gitState: undefined,
66
+ memoryInjected: false,
67
+ lastMemoryReminderAssistantMessageId: undefined,
68
+ tutorialInjected: false,
69
+ resolvedDirectory: undefined,
70
+ announcedDirectory: undefined,
71
+ }
72
+ }
73
+
74
+ // Minimal type for the opencode plugin client (v1 SDK style with path objects).
75
+ type PluginClient = {
76
+ session: {
77
+ get: (params: { path: { id: string } }) => Promise<{ data?: { directory?: string } }>
78
+ messages: (params: {
79
+ path: { id: string }
80
+ query?: { directory?: string; limit?: number }
81
+ }) => Promise<{ data?: Array<{ info: AssistantMessageInfo }> }>
82
+ }
83
+ }
84
+
85
+ // ── Pure derivation functions ────────────────────────────────────
86
+ // These take state + fresh input and return whether to inject.
87
+ // No side effects, no mutations — easy to test with fixtures.
88
+
89
+ export function shouldInjectBranch({
90
+ previousGitState,
91
+ currentGitState,
92
+ }: {
93
+ previousGitState: GitState | undefined
94
+ currentGitState: GitState | null
95
+ }): { inject: false } | { inject: true; text: string } {
96
+ if (!currentGitState) {
97
+ return { inject: false }
98
+ }
99
+ if (previousGitState && previousGitState.key === currentGitState.key) {
100
+ return { inject: false }
101
+ }
102
+ const text = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`
103
+ return { inject: true, text }
104
+ }
105
+
106
+ export function shouldInjectPwd({
107
+ currentDir,
108
+ previousDir,
109
+ announcedDir,
110
+ }: {
111
+ currentDir: string
112
+ previousDir: string | undefined
113
+ announcedDir: string | undefined
114
+ }): { inject: false } | { inject: true; text: string } {
115
+ if (announcedDir === currentDir) {
116
+ return { inject: false }
117
+ }
118
+
119
+ const priorDirectory = announcedDir || previousDir
120
+ if (!priorDirectory || priorDirectory === currentDir) {
121
+ return { inject: false }
122
+ }
123
+
124
+ return {
125
+ inject: true,
126
+ text:
127
+ `\n[working directory changed. Previous working directory: ${priorDirectory}. ` +
128
+ `Current working directory: ${currentDir}. ` +
129
+ `You MUST read, write, and edit files only under ${currentDir}. ` +
130
+ `Do NOT read, write, or edit files under ${priorDirectory}.]`,
131
+ }
132
+ }
133
+
134
+ const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000
135
+
136
+ type AssistantTokenUsage = {
137
+ input: number
138
+ output: number
139
+ reasoning: number
140
+ cache: { read: number; write: number }
141
+ }
142
+
143
+ type AssistantMessageInfo = {
144
+ id: string
145
+ role: string
146
+ time?: { completed?: number; created?: number }
147
+ tokens?: AssistantTokenUsage
148
+ }
149
+
150
+ function getOutputTokenTotal(tokens: AssistantTokenUsage): number {
151
+ return Math.max(0, tokens.output + tokens.reasoning)
152
+ }
153
+
154
+ export function shouldInjectMemoryReminderFromLatestAssistant({
155
+ lastMemoryReminderAssistantMessageId,
156
+ latestAssistantMessage,
157
+ threshold = MEMORY_REMINDER_OUTPUT_TOKENS,
158
+ }: {
159
+ lastMemoryReminderAssistantMessageId?: string
160
+ latestAssistantMessage: AssistantMessageInfo | undefined
161
+ threshold?: number
162
+ }): { inject: false } | { inject: true; assistantMessageId: string } {
163
+ if (!latestAssistantMessage) {
164
+ return { inject: false }
165
+ }
166
+ if (latestAssistantMessage.role !== 'assistant') {
167
+ return { inject: false }
168
+ }
169
+ if (typeof latestAssistantMessage.time?.completed !== 'number') {
170
+ return { inject: false }
171
+ }
172
+ if (!latestAssistantMessage.tokens) {
173
+ return { inject: false }
174
+ }
175
+ if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
176
+ return { inject: false }
177
+ }
178
+ const outputTokens = getOutputTokenTotal(latestAssistantMessage.tokens)
179
+ if (outputTokens < threshold) {
180
+ return { inject: false }
181
+ }
182
+ return { inject: true, assistantMessageId: latestAssistantMessage.id }
183
+ }
184
+
185
+ export function shouldInjectTutorial({
186
+ alreadyInjected,
187
+ parts,
188
+ }: {
189
+ alreadyInjected: boolean
190
+ parts: Array<{ type: string; text?: string }>
191
+ }): boolean {
192
+ if (alreadyInjected) {
193
+ return false
194
+ }
195
+ return parts.some((part) => {
196
+ return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT)
197
+ })
198
+ }
199
+
200
+ // ── Impure helpers (I/O) ─────────────────────────────────────────
201
+
202
+ async function resolveGitState({
203
+ directory,
204
+ }: {
205
+ directory: string
206
+ }): Promise<GitState | null> {
207
+ const branchResult = await errore.tryAsync(() => {
208
+ return execAsync('git symbolic-ref --short HEAD', { cwd: directory })
209
+ })
210
+ if (!(branchResult instanceof Error)) {
211
+ const branch = branchResult.stdout.trim()
212
+ if (branch) {
213
+ return {
214
+ key: `branch:${branch}`,
215
+ kind: 'branch',
216
+ label: branch,
217
+ warning: null,
218
+ }
219
+ }
220
+ }
221
+
222
+ const shaResult = await errore.tryAsync(() => {
223
+ return execAsync('git rev-parse --short HEAD', { cwd: directory })
224
+ })
225
+ if (shaResult instanceof Error) {
226
+ return null
227
+ }
228
+
229
+ const shortSha = shaResult.stdout.trim()
230
+ if (!shortSha) {
231
+ return null
232
+ }
233
+
234
+ const superprojectResult = await errore.tryAsync(() => {
235
+ return execAsync('git rev-parse --show-superproject-working-tree', {
236
+ cwd: directory,
237
+ })
238
+ })
239
+ const superproject =
240
+ superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim()
241
+ if (superproject) {
242
+ return {
243
+ key: `detached-submodule:${shortSha}`,
244
+ kind: 'detached-submodule',
245
+ label: `detached submodule @ ${shortSha}`,
246
+ warning:
247
+ `\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
248
+ 'create or switch to a branch before committing.]',
249
+ }
250
+ }
251
+
252
+ return {
253
+ key: `detached-head:${shortSha}`,
254
+ kind: 'detached-head',
255
+ label: `detached HEAD @ ${shortSha}`,
256
+ warning:
257
+ `\n[warning: repository is in detached HEAD at ${shortSha}. ` +
258
+ 'create or switch to a branch before committing.]',
259
+ }
260
+ }
261
+
262
+ // Resolve the last observed session directory via the SDK.
263
+ // Refreshed on every real user message because sessions can switch directories
264
+ // mid-thread and the pwd reminder must compare old vs new accurately.
265
+ async function resolveSessionDirectory({
266
+ client,
267
+ sessionID,
268
+ state,
269
+ }: {
270
+ client: PluginClient
271
+ sessionID: string
272
+ state: SessionState
273
+ }): Promise<{
274
+ currentDirectory: string | null
275
+ previousDirectory: string | undefined
276
+ }> {
277
+ const previousDirectory = state.resolvedDirectory
278
+ const result = await errore.tryAsync(() => {
279
+ return client.session.get({ path: { id: sessionID } })
280
+ })
281
+ if (result instanceof Error || !result.data?.directory) {
282
+ return {
283
+ currentDirectory: previousDirectory || null,
284
+ previousDirectory,
285
+ }
286
+ }
287
+ state.resolvedDirectory = result.data.directory
288
+ return {
289
+ currentDirectory: result.data.directory,
290
+ previousDirectory,
291
+ }
292
+ }
293
+
294
+ // ── Plugin ───────────────────────────────────────────────────────
295
+
296
+ const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
297
+ initSentry()
298
+
299
+ const dataDir = process.env.KIMAKI_DATA_DIR
300
+ if (dataDir) {
301
+ setDataDir(dataDir)
302
+ setPluginLogFilePath(dataDir)
303
+ }
304
+
305
+ // Single Map for all per-session state. One entry per session, one
306
+ // delete on cleanup — no parallel Maps that can drift out of sync.
307
+ const sessions = new Map<string, SessionState>()
308
+
309
+ function getOrCreateSession(sessionID: string): SessionState {
310
+ const existing = sessions.get(sessionID)
311
+ if (existing) {
312
+ return existing
313
+ }
314
+ const state = createSessionState()
315
+ sessions.set(sessionID, state)
316
+ return state
317
+ }
318
+
319
+ return {
320
+ 'chat.message': async (input, output) => {
321
+ const hookResult = await errore.tryAsync({
322
+ try: async () => {
323
+ const { sessionID } = input
324
+ const state = getOrCreateSession(sessionID)
325
+
326
+ // -- Onboarding tutorial injection --
327
+ // Runs before the non-synthetic text guard because the tutorial
328
+ // marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
329
+ // parts prepended by message-preprocessing.ts. The old separate
330
+ // plugin had no such guard, so this preserves that behavior.
331
+ const firstTextPart = output.parts.find((part) => {
332
+ return part.type === 'text'
333
+ })
334
+ if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
335
+ state.tutorialInjected = true
336
+ output.parts.push({
337
+ id: `prt_${crypto.randomUUID()}`,
338
+ sessionID,
339
+ messageID: firstTextPart.messageID,
340
+ type: 'text' as const,
341
+ text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>`,
342
+ synthetic: true,
343
+ })
344
+ }
345
+
346
+ // -- Find first non-synthetic user text part --
347
+ // All remaining injections (branch, pwd, memory, time gap) only
348
+ // apply to real user messages, not empty or synthetic-only messages.
349
+ const first = output.parts.find((part) => {
350
+ if (part.type !== 'text') {
351
+ return true
352
+ }
353
+ return part.synthetic !== true
354
+ })
355
+ if (!first || first.type !== 'text' || first.text.trim().length === 0) {
356
+ return
357
+ }
358
+
359
+ const messageID = first.messageID
360
+
361
+ const latestAssistantMessageResult = await errore.tryAsync(() => {
362
+ return client.session.messages({
363
+ path: { id: sessionID },
364
+ query: { directory, limit: 20 },
365
+ })
366
+ })
367
+ const latestAssistantMessage =
368
+ latestAssistantMessageResult instanceof Error
369
+ ? undefined
370
+ : [...(latestAssistantMessageResult.data || [])]
371
+ .reverse()
372
+ .find((entry) => {
373
+ return entry.info.role === 'assistant'
374
+ })
375
+ ?.info
376
+
377
+ // -- Resolve session working directory --
378
+ const sessionDirectory = await resolveSessionDirectory({
379
+ client,
380
+ sessionID,
381
+ state,
382
+ })
383
+ // The plugin request directory is the current directory Kimaki asked
384
+ // OpenCode to operate on for this message. Prefer it over session.get()
385
+ // when they disagree so reminders and MEMORY/branch context follow the
386
+ // new worktree immediately after a folder switch.
387
+ const effectiveDirectory = directory
388
+
389
+ // -- Branch / detached HEAD detection --
390
+ // Resolved early but injected last so it appears at the end of parts.
391
+ const gitState = await resolveGitState({ directory: effectiveDirectory })
392
+
393
+ // -- Working directory change detection --
394
+ const pwdResult = shouldInjectPwd({
395
+ currentDir: effectiveDirectory,
396
+ previousDir:
397
+ sessionDirectory.previousDirectory ||
398
+ (sessionDirectory.currentDirectory !== effectiveDirectory
399
+ ? sessionDirectory.currentDirectory || undefined
400
+ : undefined),
401
+ announcedDir: state.announcedDirectory,
402
+ })
403
+ if (pwdResult.inject) {
404
+ state.announcedDirectory = effectiveDirectory
405
+ output.parts.push({
406
+ id: `prt_${crypto.randomUUID()}`,
407
+ sessionID,
408
+ messageID,
409
+ type: 'text' as const,
410
+ text: pwdResult.text,
411
+ synthetic: true,
412
+ })
413
+ }
414
+
415
+ // -- MEMORY.md injection --
416
+ if (!state.memoryInjected) {
417
+ state.memoryInjected = true
418
+ const memoryPath = path.join(effectiveDirectory, 'MEMORY.md')
419
+ const memoryContent = await fs.promises
420
+ .readFile(memoryPath, 'utf-8')
421
+ .catch(() => null)
422
+ if (memoryContent) {
423
+ const condensed = condenseMemoryMd(memoryContent)
424
+ output.parts.push({
425
+ id: `prt_${crypto.randomUUID()}`,
426
+ sessionID,
427
+ messageID,
428
+ type: 'text' as const,
429
+ text: `<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>`,
430
+ synthetic: true,
431
+ })
432
+ }
433
+ }
434
+
435
+ const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
436
+ lastMemoryReminderAssistantMessageId:
437
+ state.lastMemoryReminderAssistantMessageId,
438
+ latestAssistantMessage,
439
+ })
440
+ if (memoryReminder.inject) {
441
+ output.parts.push({
442
+ id: `prt_${crypto.randomUUID()}`,
443
+ sessionID,
444
+ messageID,
445
+ type: 'text' as const,
446
+ 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>',
447
+ synthetic: true,
448
+ })
449
+ state.lastMemoryReminderAssistantMessageId =
450
+ memoryReminder.assistantMessageId
451
+ }
452
+
453
+ // -- Branch injection (last synthetic part) --
454
+ const branchResult = shouldInjectBranch({
455
+ previousGitState: state.gitState,
456
+ currentGitState: gitState,
457
+ })
458
+ if (branchResult.inject) {
459
+ state.gitState = gitState!
460
+ output.parts.push({
461
+ id: `prt_${crypto.randomUUID()}`,
462
+ sessionID,
463
+ messageID,
464
+ type: 'text' as const,
465
+ text: branchResult.text,
466
+ synthetic: true,
467
+ })
468
+ }
469
+ },
470
+ catch: (error) => {
471
+ return new Error('context-awareness chat.message hook failed', { cause: error })
472
+ },
473
+ })
474
+ if (hookResult instanceof Error) {
475
+ logger.warn(
476
+ `[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`,
477
+ )
478
+ void notifyError(hookResult, 'context-awareness plugin chat.message hook failed')
479
+ }
480
+ },
481
+
482
+ // Clean up per-session state when sessions are deleted.
483
+ // Single delete instead of parallel Map/Set deletes.
484
+ event: async ({ event }) => {
485
+ const cleanupResult = await errore.tryAsync({
486
+ try: async () => {
487
+ if (event.type !== 'session.deleted') {
488
+ return
489
+ }
490
+ const id = event.properties?.info?.id
491
+ if (!id) {
492
+ return
493
+ }
494
+ sessions.delete(id)
495
+ },
496
+ catch: (error) => {
497
+ return new Error('context-awareness event hook failed', { cause: error })
498
+ },
499
+ })
500
+ if (cleanupResult instanceof Error) {
501
+ logger.warn(
502
+ `[context-awareness-plugin] ${formatPluginErrorWithStack(cleanupResult)}`,
503
+ )
504
+ void notifyError(cleanupResult, 'context-awareness plugin event hook failed')
505
+ }
506
+ },
507
+ }
508
+ }
509
+
510
+ export { contextAwarenessPlugin }