@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,340 @@
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
+ import crypto from 'node:crypto';
18
+ import * as errore from 'errore';
19
+ import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
20
+ import { setDataDir } from './config.js';
21
+ import { initSentry, notifyError } from './sentry.js';
22
+ import { execAsync } from './exec-async.js';
23
+ import { ONBOARDING_TUTORIAL_INSTRUCTIONS, TUTORIAL_WELCOME_TEXT, } from './onboarding-tutorial.js';
24
+ const logger = createPluginLogger('OPENCODE');
25
+ // ── Pure derivation functions ────────────────────────────────────
26
+ // These take state + fresh input and return whether to inject.
27
+ // No side effects, no mutations — easy to test with fixtures.
28
+ export function shouldInjectBranch({ previousGitState, currentGitState, }) {
29
+ if (!currentGitState) {
30
+ return { inject: false };
31
+ }
32
+ if (previousGitState && previousGitState.key === currentGitState.key) {
33
+ return { inject: false };
34
+ }
35
+ // Trailing newline so this synthetic part does not fuse with the next text
36
+ // part when the model concatenates message parts.
37
+ const base = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`;
38
+ return { inject: true, text: `${base}\n` };
39
+ }
40
+ export function shouldInjectPwd({ currentDir, previousDir, announcedDir, }) {
41
+ if (announcedDir === currentDir) {
42
+ return { inject: false };
43
+ }
44
+ const priorDirectory = announcedDir || previousDir;
45
+ if (!priorDirectory || priorDirectory === currentDir) {
46
+ return { inject: false };
47
+ }
48
+ return {
49
+ inject: true,
50
+ // Trailing newline so this synthetic part does not fuse with the next text
51
+ // part when the model concatenates message parts.
52
+ text: `\n[working directory changed (cwd / pwd has changed). ` +
53
+ `The user expects you to edit files in the new cwd. ` +
54
+ `Previous folder (DO NOT TOUCH): ${priorDirectory}. ` +
55
+ `New folder (new cwd / pwd, edit files here): ${currentDir}. ` +
56
+ `You MUST read, write, and edit files only under the new folder ${currentDir}. ` +
57
+ `You MUST NOT read, write, or edit any files under the previous folder ${priorDirectory} — ` +
58
+ `that folder is a separate checkout and the user or another agent may be actively working there, ` +
59
+ `so writing to it would override their unrelated changes.]\n`,
60
+ };
61
+ }
62
+ const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000;
63
+ const GIT_STATE_CACHE_TTL_MS = 2_000;
64
+ export function shouldInjectMemoryReminderFromLatestAssistant({ lastMemoryReminderAssistantMessageId, latestAssistantMessage, threshold = MEMORY_REMINDER_OUTPUT_TOKENS, }) {
65
+ if (!latestAssistantMessage) {
66
+ return { inject: false };
67
+ }
68
+ if (latestAssistantMessage.role !== 'assistant') {
69
+ return { inject: false };
70
+ }
71
+ if (typeof latestAssistantMessage.time?.completed !== 'number') {
72
+ return { inject: false };
73
+ }
74
+ if (!latestAssistantMessage.tokens) {
75
+ return { inject: false };
76
+ }
77
+ if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
78
+ return { inject: false };
79
+ }
80
+ const outputTokens = Math.max(0, latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning);
81
+ if (outputTokens < threshold) {
82
+ return { inject: false };
83
+ }
84
+ return { inject: true, assistantMessageId: latestAssistantMessage.id };
85
+ }
86
+ export function shouldInjectTutorial({ alreadyInjected, parts, }) {
87
+ if (alreadyInjected) {
88
+ return false;
89
+ }
90
+ return parts.some((part) => {
91
+ return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT);
92
+ });
93
+ }
94
+ // ── Impure helpers (I/O) ─────────────────────────────────────────
95
+ async function resolveGitState({ directory, }) {
96
+ const branchResult = await errore.tryAsync(() => {
97
+ return execAsync('git symbolic-ref --short HEAD', { cwd: directory });
98
+ });
99
+ if (!(branchResult instanceof Error)) {
100
+ const branch = branchResult.stdout.trim();
101
+ if (branch) {
102
+ return {
103
+ key: `branch:${branch}`,
104
+ kind: 'branch',
105
+ label: branch,
106
+ warning: null,
107
+ };
108
+ }
109
+ }
110
+ const shaResult = await errore.tryAsync(() => {
111
+ return execAsync('git rev-parse --short HEAD', { cwd: directory });
112
+ });
113
+ if (shaResult instanceof Error) {
114
+ return null;
115
+ }
116
+ const shortSha = shaResult.stdout.trim();
117
+ if (!shortSha) {
118
+ return null;
119
+ }
120
+ const superprojectResult = await errore.tryAsync(() => {
121
+ return execAsync('git rev-parse --show-superproject-working-tree', {
122
+ cwd: directory,
123
+ });
124
+ });
125
+ const superproject = superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim();
126
+ if (superproject) {
127
+ return {
128
+ key: `detached-submodule:${shortSha}`,
129
+ kind: 'detached-submodule',
130
+ label: `detached submodule @ ${shortSha}`,
131
+ warning: `\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
132
+ 'create or switch to a branch before committing.]',
133
+ };
134
+ }
135
+ return {
136
+ key: `detached-head:${shortSha}`,
137
+ kind: 'detached-head',
138
+ label: `detached HEAD @ ${shortSha}`,
139
+ warning: `\n[warning: repository is in detached HEAD at ${shortSha}. ` +
140
+ 'create or switch to a branch before committing.]',
141
+ };
142
+ }
143
+ // ── Plugin ───────────────────────────────────────────────────────
144
+ const contextAwarenessPlugin = async ({ directory }) => {
145
+ initSentry();
146
+ const dataDir = process.env.OTTO_DATA_DIR;
147
+ if (dataDir) {
148
+ setDataDir(dataDir);
149
+ setPluginLogFilePath(dataDir);
150
+ }
151
+ // Single Map for all per-session state. One entry per session, one
152
+ // delete on cleanup — no parallel Maps that can drift out of sync.
153
+ const sessions = new Map();
154
+ function getOrCreateSession(sessionID) {
155
+ const existing = sessions.get(sessionID);
156
+ if (existing) {
157
+ return existing;
158
+ }
159
+ const state = {
160
+ gitState: undefined,
161
+ gitStateDirectory: undefined,
162
+ gitStateCheckedAtMs: 0,
163
+ lastMemoryReminderAssistantMessageId: undefined,
164
+ latestAssistantMessage: undefined,
165
+ tutorialInjected: false,
166
+ resolvedDirectory: undefined,
167
+ announcedDirectory: undefined,
168
+ };
169
+ sessions.set(sessionID, state);
170
+ return state;
171
+ }
172
+ return {
173
+ 'chat.message': async (input, output) => {
174
+ const hookResult = await errore.tryAsync({
175
+ try: async () => {
176
+ const { sessionID } = input;
177
+ const state = getOrCreateSession(sessionID);
178
+ // -- Onboarding tutorial injection --
179
+ // Runs before the non-synthetic text guard because the tutorial
180
+ // marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
181
+ // parts prepended by message-preprocessing.ts. The old separate
182
+ // plugin had no such guard, so this preserves that behavior.
183
+ const firstTextPart = output.parts.find((part) => {
184
+ return part.type === 'text';
185
+ });
186
+ if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
187
+ state.tutorialInjected = true;
188
+ output.parts.push({
189
+ id: `prt_${crypto.randomUUID()}`,
190
+ sessionID,
191
+ messageID: firstTextPart.messageID,
192
+ type: 'text',
193
+ text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>\n`,
194
+ synthetic: true,
195
+ });
196
+ }
197
+ // -- Find first non-synthetic user text part --
198
+ // All remaining injections (branch, pwd, memory, time gap) only
199
+ // apply to real user messages, not empty or synthetic-only messages.
200
+ const first = output.parts.find((part) => {
201
+ if (part.type !== 'text') {
202
+ return true;
203
+ }
204
+ return part.synthetic !== true;
205
+ });
206
+ if (!first || first.type !== 'text' || first.text.trim().length === 0) {
207
+ return;
208
+ }
209
+ const messageID = first.messageID;
210
+ const latestAssistantMessage = state.latestAssistantMessage;
211
+ // The plugin request directory is the current directory Otto asked
212
+ // OpenCode to operate on for this message. Keep the previous observed
213
+ // directory in session state to detect cwd changes without SDK calls.
214
+ const effectiveDirectory = directory;
215
+ const previousDirectory = state.resolvedDirectory;
216
+ state.resolvedDirectory = effectiveDirectory;
217
+ // -- Branch / detached HEAD detection --
218
+ // Avoid spawning git subprocesses on every turn in the same directory.
219
+ // Refresh only on directory change or after a short TTL.
220
+ const previousGitState = state.gitState;
221
+ const now = Date.now();
222
+ const shouldRefreshGitState = !state.gitState ||
223
+ state.gitStateDirectory !== effectiveDirectory ||
224
+ now - state.gitStateCheckedAtMs >= GIT_STATE_CACHE_TTL_MS;
225
+ const gitState = shouldRefreshGitState
226
+ ? await resolveGitState({ directory: effectiveDirectory })
227
+ : state.gitState || null;
228
+ if (shouldRefreshGitState) {
229
+ state.gitState = gitState || undefined;
230
+ state.gitStateDirectory = effectiveDirectory;
231
+ state.gitStateCheckedAtMs = now;
232
+ }
233
+ // -- Working directory change detection --
234
+ const pwdResult = shouldInjectPwd({
235
+ currentDir: effectiveDirectory,
236
+ previousDir: previousDirectory && previousDirectory !== effectiveDirectory
237
+ ? previousDirectory
238
+ : undefined,
239
+ announcedDir: state.announcedDirectory,
240
+ });
241
+ if (pwdResult.inject) {
242
+ state.announcedDirectory = effectiveDirectory;
243
+ output.parts.push({
244
+ id: `prt_${crypto.randomUUID()}`,
245
+ sessionID,
246
+ messageID,
247
+ type: 'text',
248
+ text: pwdResult.text,
249
+ synthetic: true,
250
+ });
251
+ }
252
+ const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
253
+ lastMemoryReminderAssistantMessageId: state.lastMemoryReminderAssistantMessageId,
254
+ latestAssistantMessage,
255
+ });
256
+ if (memoryReminder.inject) {
257
+ output.parts.push({
258
+ id: `prt_${crypto.randomUUID()}`,
259
+ sessionID,
260
+ messageID,
261
+ type: 'text',
262
+ 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',
263
+ synthetic: true,
264
+ });
265
+ state.lastMemoryReminderAssistantMessageId =
266
+ memoryReminder.assistantMessageId;
267
+ }
268
+ // -- Branch injection (last synthetic part) --
269
+ const branchResult = shouldInjectBranch({
270
+ previousGitState,
271
+ currentGitState: gitState,
272
+ });
273
+ if (branchResult.inject) {
274
+ state.gitState = gitState;
275
+ output.parts.push({
276
+ id: `prt_${crypto.randomUUID()}`,
277
+ sessionID,
278
+ messageID,
279
+ type: 'text',
280
+ text: branchResult.text,
281
+ synthetic: true,
282
+ });
283
+ }
284
+ },
285
+ catch: (error) => {
286
+ return new Error('context-awareness chat.message hook failed', { cause: error });
287
+ },
288
+ });
289
+ if (hookResult instanceof Error) {
290
+ logger.warn(`[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`);
291
+ void notifyError(hookResult, 'context-awareness plugin chat.message hook failed');
292
+ }
293
+ },
294
+ // Keep session-local assistant metadata updated from event stream and
295
+ // clean up state when sessions are deleted.
296
+ event: async ({ event }) => {
297
+ const eventResult = await errore.tryAsync({
298
+ try: async () => {
299
+ if (event.type === 'message.updated') {
300
+ const info = event.properties?.info;
301
+ if (!info || info.role !== 'assistant' || typeof info.id !== 'string') {
302
+ return;
303
+ }
304
+ const infoWithSession = info;
305
+ const sessionID = typeof infoWithSession.sessionID === 'string'
306
+ ? infoWithSession.sessionID
307
+ : undefined;
308
+ if (!sessionID) {
309
+ return;
310
+ }
311
+ const state = getOrCreateSession(sessionID);
312
+ state.latestAssistantMessage = {
313
+ id: info.id,
314
+ role: info.role,
315
+ time: info.time,
316
+ tokens: info.tokens,
317
+ };
318
+ return;
319
+ }
320
+ if (event.type !== 'session.deleted') {
321
+ return;
322
+ }
323
+ const id = event.properties?.info?.id;
324
+ if (!id) {
325
+ return;
326
+ }
327
+ sessions.delete(id);
328
+ },
329
+ catch: (error) => {
330
+ return new Error('context-awareness event hook failed', { cause: error });
331
+ },
332
+ });
333
+ if (eventResult instanceof Error) {
334
+ logger.warn(`[context-awareness-plugin] ${formatPluginErrorWithStack(eventResult)}`);
335
+ void notifyError(eventResult, 'context-awareness plugin event hook failed');
336
+ }
337
+ },
338
+ };
339
+ };
340
+ export { contextAwarenessPlugin };
@@ -0,0 +1,126 @@
1
+ // Tests for context-awareness directory switch reminders.
2
+ import { describe, expect, test } from 'vitest';
3
+ import { shouldInjectPwd, shouldInjectMemoryReminderFromLatestAssistant, } from './context-awareness-plugin.js';
4
+ describe('shouldInjectPwd', () => {
5
+ test('does not inject when current directory matches announced directory', () => {
6
+ const result = shouldInjectPwd({
7
+ currentDir: '/repo/worktree',
8
+ previousDir: '/repo/main',
9
+ announcedDir: '/repo/worktree',
10
+ });
11
+ expect(result).toMatchInlineSnapshot(`
12
+ {
13
+ "inject": false,
14
+ }
15
+ `);
16
+ });
17
+ test('does not inject without a previous directory to warn about', () => {
18
+ const result = shouldInjectPwd({
19
+ currentDir: '/repo/worktree',
20
+ previousDir: undefined,
21
+ announcedDir: undefined,
22
+ });
23
+ expect(result).toMatchInlineSnapshot(`
24
+ {
25
+ "inject": false,
26
+ }
27
+ `);
28
+ });
29
+ test('names previous and current directories in the correct order', () => {
30
+ const result = shouldInjectPwd({
31
+ currentDir: '/repo/worktree',
32
+ previousDir: '/repo/main',
33
+ announcedDir: undefined,
34
+ });
35
+ expect(result).toMatchInlineSnapshot(`
36
+ {
37
+ "inject": true,
38
+ "text": "
39
+ [working directory changed (cwd / pwd has changed). The user expects you to edit files in the new cwd. Previous folder (DO NOT TOUCH): /repo/main. New folder (new cwd / pwd, edit files here): /repo/worktree. You MUST read, write, and edit files only under the new folder /repo/worktree. You MUST NOT read, write, or edit any files under the previous folder /repo/main — that folder is a separate checkout and the user or another agent may be actively working there, so writing to it would override their unrelated changes.]
40
+ ",
41
+ }
42
+ `);
43
+ });
44
+ test('prefers the last announced directory as the previous directory', () => {
45
+ const result = shouldInjectPwd({
46
+ currentDir: '/repo/worktree-b',
47
+ previousDir: '/repo/main',
48
+ announcedDir: '/repo/worktree-a',
49
+ });
50
+ expect(result).toMatchInlineSnapshot(`
51
+ {
52
+ "inject": true,
53
+ "text": "
54
+ [working directory changed (cwd / pwd has changed). The user expects you to edit files in the new cwd. Previous folder (DO NOT TOUCH): /repo/worktree-a. New folder (new cwd / pwd, edit files here): /repo/worktree-b. You MUST read, write, and edit files only under the new folder /repo/worktree-b. You MUST NOT read, write, or edit any files under the previous folder /repo/worktree-a — that folder is a separate checkout and the user or another agent may be actively working there, so writing to it would override their unrelated changes.]
55
+ ",
56
+ }
57
+ `);
58
+ });
59
+ });
60
+ describe('shouldInjectMemoryReminderFromLatestAssistant', () => {
61
+ test('does not trigger before threshold', () => {
62
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
63
+ latestAssistantMessage: {
64
+ id: 'msg_asst_1',
65
+ role: 'assistant',
66
+ time: { completed: 1 },
67
+ tokens: {
68
+ input: 1_000,
69
+ output: 3_000,
70
+ reasoning: 500,
71
+ cache: { read: 0, write: 0 },
72
+ },
73
+ },
74
+ threshold: 10_000,
75
+ });
76
+ expect(result).toMatchInlineSnapshot(`
77
+ {
78
+ "inject": false,
79
+ }
80
+ `);
81
+ });
82
+ test('triggers when latest assistant message exceeds threshold', () => {
83
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
84
+ latestAssistantMessage: {
85
+ id: 'msg_asst_2',
86
+ role: 'assistant',
87
+ time: { completed: 2 },
88
+ tokens: {
89
+ input: 2_000,
90
+ output: 2_200,
91
+ reasoning: 400,
92
+ cache: { read: 0, write: 0 },
93
+ },
94
+ },
95
+ threshold: 2_000,
96
+ });
97
+ expect(result).toMatchInlineSnapshot(`
98
+ {
99
+ "assistantMessageId": "msg_asst_2",
100
+ "inject": true,
101
+ }
102
+ `);
103
+ });
104
+ test('does not trigger again for the same reminded assistant message', () => {
105
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
106
+ lastMemoryReminderAssistantMessageId: 'msg_asst_3',
107
+ latestAssistantMessage: {
108
+ id: 'msg_asst_3',
109
+ role: 'assistant',
110
+ time: { completed: 3 },
111
+ tokens: {
112
+ input: 2_000,
113
+ output: 2_200,
114
+ reasoning: 400,
115
+ cache: { read: 0, write: 0 },
116
+ },
117
+ },
118
+ threshold: 10_000,
119
+ });
120
+ expect(result).toMatchInlineSnapshot(`
121
+ {
122
+ "inject": false,
123
+ }
124
+ `);
125
+ });
126
+ });
@@ -0,0 +1,95 @@
1
+ // Shared utilities for invoking the critique CLI and parsing its JSON output.
2
+ // Used by /diff command and footer diff link uploads.
3
+ import { execAsync } from './worktrees.js';
4
+ import { createLogger, LogPrefix } from './logger.js';
5
+ const logger = createLogger(LogPrefix.DIFF);
6
+ const CRITIQUE_TIMEOUT_MS = 30_000;
7
+ /**
8
+ * Shell-quote a string by wrapping in single quotes and escaping embedded
9
+ * single quotes. Prevents injection when interpolating into shell commands.
10
+ */
11
+ function shellQuote(s) {
12
+ return `'${s.replace(/'/g, "'\\''")}'`;
13
+ }
14
+ /**
15
+ * Parse critique --json output. Critique prints progress to stderr and JSON
16
+ * to stdout. The JSON line contains { url, id } on success or { error } on
17
+ * failure. We scan all lines for the first valid JSON object with a url or
18
+ * error field, falling back to searching for a critique.work URL in the raw
19
+ * output.
20
+ */
21
+ export function parseCritiqueOutput(output) {
22
+ const lines = output.trim().split('\n');
23
+ for (const line of lines) {
24
+ if (!line.startsWith('{')) {
25
+ continue;
26
+ }
27
+ try {
28
+ const parsed = JSON.parse(line);
29
+ if (parsed.error) {
30
+ return { error: parsed.error };
31
+ }
32
+ if (parsed.url && parsed.id) {
33
+ return { url: parsed.url, id: parsed.id };
34
+ }
35
+ }
36
+ catch {
37
+ // not valid JSON, try next line
38
+ }
39
+ }
40
+ // Fallback: try to find a URL in the raw output
41
+ const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/);
42
+ if (urlMatch) {
43
+ const url = urlMatch[0];
44
+ // Extract ID from URL path: /v/{id}
45
+ const idMatch = url.match(/\/v\/([a-f0-9]+)/);
46
+ const id = idMatch?.[1];
47
+ if (id) {
48
+ return { url, id };
49
+ }
50
+ // URL without parseable id — return as error so callers don't build
51
+ // broken OG image URLs from an empty id
52
+ return { error: url };
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Run critique on the current git working tree diff and return the result.
58
+ * Used by the /diff slash command.
59
+ */
60
+ export async function uploadGitDiffViaCritique({ title, cwd, }) {
61
+ try {
62
+ const { stdout, stderr } = await execAsync(`critique --web ${shellQuote(title)} --json`, { cwd, timeout: CRITIQUE_TIMEOUT_MS });
63
+ return parseCritiqueOutput(stdout || stderr);
64
+ }
65
+ catch (error) {
66
+ // exec error includes stdout/stderr — try to parse JSON from it
67
+ const execError = error;
68
+ const output = execError.stdout || execError.stderr || '';
69
+ const parsed = parseCritiqueOutput(output);
70
+ if (parsed) {
71
+ return parsed;
72
+ }
73
+ const message = execError.message || 'Unknown error';
74
+ if (message.includes('command not found') || message.includes('ENOENT')) {
75
+ return { error: 'critique not available' };
76
+ }
77
+ return { error: `Failed to generate diff: ${message.slice(0, 200)}` };
78
+ }
79
+ }
80
+ /**
81
+ * Upload a .patch file to critique.work via critique --stdin.
82
+ * Returns the critique URL on success, undefined on failure.
83
+ * Default timeout is 10s since this runs in the background (footer edit).
84
+ */
85
+ export async function uploadPatchViaCritique({ patchPath, title, cwd, timeoutMs = 10_000, }) {
86
+ try {
87
+ const { stdout } = await execAsync(`critique --stdin --web ${shellQuote(title)} --json < ${shellQuote(patchPath)}`, { cwd, timeout: timeoutMs });
88
+ const result = parseCritiqueOutput(stdout);
89
+ return result?.url;
90
+ }
91
+ catch (error) {
92
+ logger.error('critique upload failed:', error);
93
+ return undefined;
94
+ }
95
+ }