opencode-repos 0.1.0 → 0.3.0

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 (477) hide show
  1. package/AGENTS.md +180 -0
  2. package/README.md +103 -3
  3. package/TODO.md +3 -0
  4. package/bun.lock +0 -85
  5. package/index.ts +1585 -153
  6. package/oh-my-opencode/.github/FUNDING.yml +15 -0
  7. package/oh-my-opencode/.github/ISSUE_TEMPLATE/bug_report.yml +129 -0
  8. package/oh-my-opencode/.github/ISSUE_TEMPLATE/config.yml +8 -0
  9. package/oh-my-opencode/.github/ISSUE_TEMPLATE/feature_request.yml +100 -0
  10. package/oh-my-opencode/.github/ISSUE_TEMPLATE/general.yml +83 -0
  11. package/oh-my-opencode/.github/assets/google.jpg +0 -0
  12. package/oh-my-opencode/.github/assets/hero.jpg +0 -0
  13. package/oh-my-opencode/.github/assets/indent.jpg +0 -0
  14. package/oh-my-opencode/.github/assets/microsoft.jpg +0 -0
  15. package/oh-my-opencode/.github/assets/omo.png +0 -0
  16. package/oh-my-opencode/.github/assets/orchestrator-atlas.png +0 -0
  17. package/oh-my-opencode/.github/assets/sisyphus.png +0 -0
  18. package/oh-my-opencode/.github/assets/sisyphuslabs.png +0 -0
  19. package/oh-my-opencode/.github/pull_request_template.md +34 -0
  20. package/oh-my-opencode/.github/workflows/ci.yml +138 -0
  21. package/oh-my-opencode/.github/workflows/cla.yml +41 -0
  22. package/oh-my-opencode/.github/workflows/lint-workflows.yml +22 -0
  23. package/oh-my-opencode/.github/workflows/publish.yml +165 -0
  24. package/oh-my-opencode/.github/workflows/sisyphus-agent.yml +500 -0
  25. package/oh-my-opencode/.opencode/background-tasks.json +27 -0
  26. package/oh-my-opencode/.opencode/command/get-unpublished-changes.md +84 -0
  27. package/oh-my-opencode/.opencode/command/omomomo.md +37 -0
  28. package/oh-my-opencode/.opencode/command/publish.md +257 -0
  29. package/oh-my-opencode/AGENTS.md +179 -0
  30. package/oh-my-opencode/CLA.md +58 -0
  31. package/oh-my-opencode/CONTRIBUTING.md +268 -0
  32. package/oh-my-opencode/LICENSE.md +82 -0
  33. package/oh-my-opencode/README.ja.md +370 -0
  34. package/oh-my-opencode/README.md +376 -0
  35. package/oh-my-opencode/README.zh-cn.md +380 -0
  36. package/oh-my-opencode/assets/oh-my-opencode.schema.json +2171 -0
  37. package/oh-my-opencode/bin/oh-my-opencode.js +80 -0
  38. package/oh-my-opencode/bin/platform.js +38 -0
  39. package/oh-my-opencode/bin/platform.test.ts +148 -0
  40. package/oh-my-opencode/bun.lock +314 -0
  41. package/oh-my-opencode/bunfig.toml +2 -0
  42. package/oh-my-opencode/docs/category-skill-guide.md +200 -0
  43. package/oh-my-opencode/docs/cli-guide.md +272 -0
  44. package/oh-my-opencode/docs/configurations.md +654 -0
  45. package/oh-my-opencode/docs/features.md +550 -0
  46. package/oh-my-opencode/docs/guide/installation.md +288 -0
  47. package/oh-my-opencode/docs/guide/overview.md +97 -0
  48. package/oh-my-opencode/docs/guide/understanding-orchestration-system.md +445 -0
  49. package/oh-my-opencode/docs/orchestration-guide.md +152 -0
  50. package/oh-my-opencode/docs/ultrawork-manifesto.md +197 -0
  51. package/oh-my-opencode/package.json +89 -0
  52. package/oh-my-opencode/packages/darwin-arm64/bin/.gitkeep +0 -0
  53. package/oh-my-opencode/packages/darwin-arm64/package.json +22 -0
  54. package/oh-my-opencode/packages/darwin-x64/bin/.gitkeep +0 -0
  55. package/oh-my-opencode/packages/darwin-x64/package.json +22 -0
  56. package/oh-my-opencode/packages/linux-arm64/bin/.gitkeep +0 -0
  57. package/oh-my-opencode/packages/linux-arm64/package.json +25 -0
  58. package/oh-my-opencode/packages/linux-arm64-musl/bin/.gitkeep +0 -0
  59. package/oh-my-opencode/packages/linux-arm64-musl/package.json +25 -0
  60. package/oh-my-opencode/packages/linux-x64/bin/.gitkeep +0 -0
  61. package/oh-my-opencode/packages/linux-x64/package.json +25 -0
  62. package/oh-my-opencode/packages/linux-x64-musl/bin/.gitkeep +0 -0
  63. package/oh-my-opencode/packages/linux-x64-musl/package.json +25 -0
  64. package/oh-my-opencode/packages/windows-x64/bin/.gitkeep +0 -0
  65. package/oh-my-opencode/packages/windows-x64/package.json +22 -0
  66. package/oh-my-opencode/postinstall.mjs +43 -0
  67. package/oh-my-opencode/script/build-binaries.ts +103 -0
  68. package/oh-my-opencode/script/build-schema.ts +28 -0
  69. package/oh-my-opencode/script/generate-changelog.ts +92 -0
  70. package/oh-my-opencode/script/publish.ts +344 -0
  71. package/oh-my-opencode/signatures/cla.json +676 -0
  72. package/oh-my-opencode/src/agents/AGENTS.md +67 -0
  73. package/oh-my-opencode/src/agents/atlas.ts +1383 -0
  74. package/oh-my-opencode/src/agents/dynamic-agent-prompt-builder.ts +400 -0
  75. package/oh-my-opencode/src/agents/explore.ts +122 -0
  76. package/oh-my-opencode/src/agents/index.ts +13 -0
  77. package/oh-my-opencode/src/agents/librarian.ts +326 -0
  78. package/oh-my-opencode/src/agents/metis.ts +315 -0
  79. package/oh-my-opencode/src/agents/momus.test.ts +57 -0
  80. package/oh-my-opencode/src/agents/momus.ts +444 -0
  81. package/oh-my-opencode/src/agents/multimodal-looker.ts +56 -0
  82. package/oh-my-opencode/src/agents/oracle.ts +122 -0
  83. package/oh-my-opencode/src/agents/prometheus-prompt.test.ts +22 -0
  84. package/oh-my-opencode/src/agents/prometheus-prompt.ts +1196 -0
  85. package/oh-my-opencode/src/agents/sisyphus-junior.test.ts +232 -0
  86. package/oh-my-opencode/src/agents/sisyphus-junior.ts +134 -0
  87. package/oh-my-opencode/src/agents/sisyphus.ts +633 -0
  88. package/oh-my-opencode/src/agents/types.ts +80 -0
  89. package/oh-my-opencode/src/agents/utils.test.ts +311 -0
  90. package/oh-my-opencode/src/agents/utils.ts +240 -0
  91. package/oh-my-opencode/src/cli/AGENTS.md +91 -0
  92. package/oh-my-opencode/src/cli/config-manager.test.ts +364 -0
  93. package/oh-my-opencode/src/cli/config-manager.ts +641 -0
  94. package/oh-my-opencode/src/cli/doctor/checks/auth.test.ts +114 -0
  95. package/oh-my-opencode/src/cli/doctor/checks/auth.ts +115 -0
  96. package/oh-my-opencode/src/cli/doctor/checks/config.test.ts +103 -0
  97. package/oh-my-opencode/src/cli/doctor/checks/config.ts +123 -0
  98. package/oh-my-opencode/src/cli/doctor/checks/dependencies.test.ts +152 -0
  99. package/oh-my-opencode/src/cli/doctor/checks/dependencies.ts +163 -0
  100. package/oh-my-opencode/src/cli/doctor/checks/gh.test.ts +151 -0
  101. package/oh-my-opencode/src/cli/doctor/checks/gh.ts +171 -0
  102. package/oh-my-opencode/src/cli/doctor/checks/index.ts +34 -0
  103. package/oh-my-opencode/src/cli/doctor/checks/lsp.test.ts +134 -0
  104. package/oh-my-opencode/src/cli/doctor/checks/lsp.ts +77 -0
  105. package/oh-my-opencode/src/cli/doctor/checks/mcp.test.ts +115 -0
  106. package/oh-my-opencode/src/cli/doctor/checks/mcp.ts +128 -0
  107. package/oh-my-opencode/src/cli/doctor/checks/opencode.test.ts +227 -0
  108. package/oh-my-opencode/src/cli/doctor/checks/opencode.ts +178 -0
  109. package/oh-my-opencode/src/cli/doctor/checks/plugin.test.ts +109 -0
  110. package/oh-my-opencode/src/cli/doctor/checks/plugin.ts +124 -0
  111. package/oh-my-opencode/src/cli/doctor/checks/version.test.ts +148 -0
  112. package/oh-my-opencode/src/cli/doctor/checks/version.ts +135 -0
  113. package/oh-my-opencode/src/cli/doctor/constants.ts +72 -0
  114. package/oh-my-opencode/src/cli/doctor/formatter.test.ts +218 -0
  115. package/oh-my-opencode/src/cli/doctor/formatter.ts +140 -0
  116. package/oh-my-opencode/src/cli/doctor/index.ts +11 -0
  117. package/oh-my-opencode/src/cli/doctor/runner.test.ts +153 -0
  118. package/oh-my-opencode/src/cli/doctor/runner.ts +132 -0
  119. package/oh-my-opencode/src/cli/doctor/types.ts +113 -0
  120. package/oh-my-opencode/src/cli/get-local-version/formatter.ts +66 -0
  121. package/oh-my-opencode/src/cli/get-local-version/index.ts +106 -0
  122. package/oh-my-opencode/src/cli/get-local-version/types.ts +14 -0
  123. package/oh-my-opencode/src/cli/index.ts +153 -0
  124. package/oh-my-opencode/src/cli/install.ts +523 -0
  125. package/oh-my-opencode/src/cli/model-fallback.ts +246 -0
  126. package/oh-my-opencode/src/cli/run/completion.test.ts +170 -0
  127. package/oh-my-opencode/src/cli/run/completion.ts +79 -0
  128. package/oh-my-opencode/src/cli/run/events.test.ts +155 -0
  129. package/oh-my-opencode/src/cli/run/events.ts +325 -0
  130. package/oh-my-opencode/src/cli/run/index.ts +2 -0
  131. package/oh-my-opencode/src/cli/run/runner.ts +159 -0
  132. package/oh-my-opencode/src/cli/run/types.ts +76 -0
  133. package/oh-my-opencode/src/cli/types.ts +40 -0
  134. package/oh-my-opencode/src/config/index.ts +26 -0
  135. package/oh-my-opencode/src/config/schema.test.ts +444 -0
  136. package/oh-my-opencode/src/config/schema.ts +339 -0
  137. package/oh-my-opencode/src/features/AGENTS.md +77 -0
  138. package/oh-my-opencode/src/features/background-agent/concurrency.test.ts +418 -0
  139. package/oh-my-opencode/src/features/background-agent/concurrency.ts +137 -0
  140. package/oh-my-opencode/src/features/background-agent/index.ts +3 -0
  141. package/oh-my-opencode/src/features/background-agent/manager.test.ts +1928 -0
  142. package/oh-my-opencode/src/features/background-agent/manager.ts +1335 -0
  143. package/oh-my-opencode/src/features/background-agent/types.ts +66 -0
  144. package/oh-my-opencode/src/features/boulder-state/constants.ts +13 -0
  145. package/oh-my-opencode/src/features/boulder-state/index.ts +3 -0
  146. package/oh-my-opencode/src/features/boulder-state/storage.test.ts +250 -0
  147. package/oh-my-opencode/src/features/boulder-state/storage.ts +150 -0
  148. package/oh-my-opencode/src/features/boulder-state/types.ts +26 -0
  149. package/oh-my-opencode/src/features/builtin-commands/commands.ts +89 -0
  150. package/oh-my-opencode/src/features/builtin-commands/index.ts +2 -0
  151. package/oh-my-opencode/src/features/builtin-commands/templates/init-deep.ts +300 -0
  152. package/oh-my-opencode/src/features/builtin-commands/templates/ralph-loop.ts +38 -0
  153. package/oh-my-opencode/src/features/builtin-commands/templates/refactor.ts +619 -0
  154. package/oh-my-opencode/src/features/builtin-commands/templates/start-work.ts +72 -0
  155. package/oh-my-opencode/src/features/builtin-commands/types.ts +9 -0
  156. package/oh-my-opencode/src/features/builtin-skills/frontend-ui-ux/SKILL.md +78 -0
  157. package/oh-my-opencode/src/features/builtin-skills/git-master/SKILL.md +1105 -0
  158. package/oh-my-opencode/src/features/builtin-skills/index.ts +2 -0
  159. package/oh-my-opencode/src/features/builtin-skills/skills.ts +1203 -0
  160. package/oh-my-opencode/src/features/builtin-skills/types.ts +16 -0
  161. package/oh-my-opencode/src/features/claude-code-agent-loader/index.ts +2 -0
  162. package/oh-my-opencode/src/features/claude-code-agent-loader/loader.ts +90 -0
  163. package/oh-my-opencode/src/features/claude-code-agent-loader/types.ts +17 -0
  164. package/oh-my-opencode/src/features/claude-code-command-loader/index.ts +2 -0
  165. package/oh-my-opencode/src/features/claude-code-command-loader/loader.ts +144 -0
  166. package/oh-my-opencode/src/features/claude-code-command-loader/types.ts +46 -0
  167. package/oh-my-opencode/src/features/claude-code-mcp-loader/env-expander.ts +27 -0
  168. package/oh-my-opencode/src/features/claude-code-mcp-loader/index.ts +11 -0
  169. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.test.ts +162 -0
  170. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.ts +113 -0
  171. package/oh-my-opencode/src/features/claude-code-mcp-loader/transformer.ts +53 -0
  172. package/oh-my-opencode/src/features/claude-code-mcp-loader/types.ts +42 -0
  173. package/oh-my-opencode/src/features/claude-code-plugin-loader/index.ts +3 -0
  174. package/oh-my-opencode/src/features/claude-code-plugin-loader/loader.ts +486 -0
  175. package/oh-my-opencode/src/features/claude-code-plugin-loader/types.ts +210 -0
  176. package/oh-my-opencode/src/features/claude-code-session-state/index.ts +1 -0
  177. package/oh-my-opencode/src/features/claude-code-session-state/state.test.ts +126 -0
  178. package/oh-my-opencode/src/features/claude-code-session-state/state.ts +37 -0
  179. package/oh-my-opencode/src/features/context-injector/collector.test.ts +330 -0
  180. package/oh-my-opencode/src/features/context-injector/collector.ts +85 -0
  181. package/oh-my-opencode/src/features/context-injector/index.ts +14 -0
  182. package/oh-my-opencode/src/features/context-injector/injector.test.ts +122 -0
  183. package/oh-my-opencode/src/features/context-injector/injector.ts +167 -0
  184. package/oh-my-opencode/src/features/context-injector/types.ts +91 -0
  185. package/oh-my-opencode/src/features/hook-message-injector/constants.ts +6 -0
  186. package/oh-my-opencode/src/features/hook-message-injector/index.ts +4 -0
  187. package/oh-my-opencode/src/features/hook-message-injector/injector.ts +195 -0
  188. package/oh-my-opencode/src/features/hook-message-injector/types.ts +47 -0
  189. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.test.ts +448 -0
  190. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.ts +180 -0
  191. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.test.ts +210 -0
  192. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.ts +62 -0
  193. package/oh-my-opencode/src/features/opencode-skill-loader/discover-worker.ts +59 -0
  194. package/oh-my-opencode/src/features/opencode-skill-loader/index.ts +4 -0
  195. package/oh-my-opencode/src/features/opencode-skill-loader/loader.test.ts +273 -0
  196. package/oh-my-opencode/src/features/opencode-skill-loader/loader.ts +259 -0
  197. package/oh-my-opencode/src/features/opencode-skill-loader/merger.ts +267 -0
  198. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.test.ts +267 -0
  199. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.ts +206 -0
  200. package/oh-my-opencode/src/features/opencode-skill-loader/types.ts +38 -0
  201. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.test.ts +201 -0
  202. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.ts +27 -0
  203. package/oh-my-opencode/src/features/skill-mcp-manager/index.ts +2 -0
  204. package/oh-my-opencode/src/features/skill-mcp-manager/manager.test.ts +611 -0
  205. package/oh-my-opencode/src/features/skill-mcp-manager/manager.ts +520 -0
  206. package/oh-my-opencode/src/features/skill-mcp-manager/types.ts +14 -0
  207. package/oh-my-opencode/src/features/task-toast-manager/index.ts +2 -0
  208. package/oh-my-opencode/src/features/task-toast-manager/manager.test.ts +249 -0
  209. package/oh-my-opencode/src/features/task-toast-manager/manager.ts +215 -0
  210. package/oh-my-opencode/src/features/task-toast-manager/types.ts +24 -0
  211. package/oh-my-opencode/src/hooks/AGENTS.md +73 -0
  212. package/oh-my-opencode/src/hooks/agent-usage-reminder/constants.ts +54 -0
  213. package/oh-my-opencode/src/hooks/agent-usage-reminder/index.ts +109 -0
  214. package/oh-my-opencode/src/hooks/agent-usage-reminder/storage.ts +42 -0
  215. package/oh-my-opencode/src/hooks/agent-usage-reminder/types.ts +6 -0
  216. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +307 -0
  217. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.ts +485 -0
  218. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/index.ts +151 -0
  219. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/parser.ts +201 -0
  220. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts +33 -0
  221. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +184 -0
  222. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-types.ts +44 -0
  223. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +77 -0
  224. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.ts +250 -0
  225. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/types.ts +42 -0
  226. package/oh-my-opencode/src/hooks/atlas/index.test.ts +953 -0
  227. package/oh-my-opencode/src/hooks/atlas/index.ts +771 -0
  228. package/oh-my-opencode/src/hooks/auto-slash-command/constants.ts +12 -0
  229. package/oh-my-opencode/src/hooks/auto-slash-command/detector.test.ts +296 -0
  230. package/oh-my-opencode/src/hooks/auto-slash-command/detector.ts +65 -0
  231. package/oh-my-opencode/src/hooks/auto-slash-command/executor.ts +205 -0
  232. package/oh-my-opencode/src/hooks/auto-slash-command/index.test.ts +254 -0
  233. package/oh-my-opencode/src/hooks/auto-slash-command/index.ts +89 -0
  234. package/oh-my-opencode/src/hooks/auto-slash-command/types.ts +23 -0
  235. package/oh-my-opencode/src/hooks/auto-update-checker/cache.ts +93 -0
  236. package/oh-my-opencode/src/hooks/auto-update-checker/checker.test.ts +24 -0
  237. package/oh-my-opencode/src/hooks/auto-update-checker/checker.ts +284 -0
  238. package/oh-my-opencode/src/hooks/auto-update-checker/constants.ts +64 -0
  239. package/oh-my-opencode/src/hooks/auto-update-checker/index.test.ts +254 -0
  240. package/oh-my-opencode/src/hooks/auto-update-checker/index.ts +260 -0
  241. package/oh-my-opencode/src/hooks/auto-update-checker/types.ts +29 -0
  242. package/oh-my-opencode/src/hooks/background-compaction/index.ts +87 -0
  243. package/oh-my-opencode/src/hooks/background-notification/index.ts +28 -0
  244. package/oh-my-opencode/src/hooks/background-notification/types.ts +5 -0
  245. package/oh-my-opencode/src/hooks/claude-code-hooks/AGENTS.md +70 -0
  246. package/oh-my-opencode/src/hooks/claude-code-hooks/config-loader.ts +107 -0
  247. package/oh-my-opencode/src/hooks/claude-code-hooks/config.ts +103 -0
  248. package/oh-my-opencode/src/hooks/claude-code-hooks/index.ts +401 -0
  249. package/oh-my-opencode/src/hooks/claude-code-hooks/plugin-config.ts +12 -0
  250. package/oh-my-opencode/src/hooks/claude-code-hooks/post-tool-use.ts +199 -0
  251. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-compact.ts +109 -0
  252. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-tool-use.ts +172 -0
  253. package/oh-my-opencode/src/hooks/claude-code-hooks/stop.ts +118 -0
  254. package/oh-my-opencode/src/hooks/claude-code-hooks/todo.ts +76 -0
  255. package/oh-my-opencode/src/hooks/claude-code-hooks/tool-input-cache.ts +47 -0
  256. package/oh-my-opencode/src/hooks/claude-code-hooks/transcript.ts +252 -0
  257. package/oh-my-opencode/src/hooks/claude-code-hooks/types.ts +204 -0
  258. package/oh-my-opencode/src/hooks/claude-code-hooks/user-prompt-submit.ts +117 -0
  259. package/oh-my-opencode/src/hooks/comment-checker/cli.test.ts +68 -0
  260. package/oh-my-opencode/src/hooks/comment-checker/cli.ts +221 -0
  261. package/oh-my-opencode/src/hooks/comment-checker/downloader.ts +196 -0
  262. package/oh-my-opencode/src/hooks/comment-checker/index.ts +171 -0
  263. package/oh-my-opencode/src/hooks/comment-checker/types.ts +33 -0
  264. package/oh-my-opencode/src/hooks/compaction-context-injector/index.ts +61 -0
  265. package/oh-my-opencode/src/hooks/context-window-monitor.ts +99 -0
  266. package/oh-my-opencode/src/hooks/delegate-task-retry/index.test.ts +119 -0
  267. package/oh-my-opencode/src/hooks/delegate-task-retry/index.ts +136 -0
  268. package/oh-my-opencode/src/hooks/directory-agents-injector/constants.ts +9 -0
  269. package/oh-my-opencode/src/hooks/directory-agents-injector/index.ts +182 -0
  270. package/oh-my-opencode/src/hooks/directory-agents-injector/storage.ts +48 -0
  271. package/oh-my-opencode/src/hooks/directory-agents-injector/types.ts +5 -0
  272. package/oh-my-opencode/src/hooks/directory-readme-injector/constants.ts +9 -0
  273. package/oh-my-opencode/src/hooks/directory-readme-injector/index.ts +177 -0
  274. package/oh-my-opencode/src/hooks/directory-readme-injector/storage.ts +48 -0
  275. package/oh-my-opencode/src/hooks/directory-readme-injector/types.ts +5 -0
  276. package/oh-my-opencode/src/hooks/edit-error-recovery/index.test.ts +126 -0
  277. package/oh-my-opencode/src/hooks/edit-error-recovery/index.ts +57 -0
  278. package/oh-my-opencode/src/hooks/empty-task-response-detector.ts +27 -0
  279. package/oh-my-opencode/src/hooks/index.ts +32 -0
  280. package/oh-my-opencode/src/hooks/interactive-bash-session/constants.ts +15 -0
  281. package/oh-my-opencode/src/hooks/interactive-bash-session/index.ts +262 -0
  282. package/oh-my-opencode/src/hooks/interactive-bash-session/storage.ts +59 -0
  283. package/oh-my-opencode/src/hooks/interactive-bash-session/types.ts +11 -0
  284. package/oh-my-opencode/src/hooks/keyword-detector/constants.ts +300 -0
  285. package/oh-my-opencode/src/hooks/keyword-detector/detector.ts +52 -0
  286. package/oh-my-opencode/src/hooks/keyword-detector/index.test.ts +529 -0
  287. package/oh-my-opencode/src/hooks/keyword-detector/index.ts +100 -0
  288. package/oh-my-opencode/src/hooks/keyword-detector/types.ts +4 -0
  289. package/oh-my-opencode/src/hooks/non-interactive-env/constants.ts +70 -0
  290. package/oh-my-opencode/src/hooks/non-interactive-env/detector.ts +19 -0
  291. package/oh-my-opencode/src/hooks/non-interactive-env/index.test.ts +323 -0
  292. package/oh-my-opencode/src/hooks/non-interactive-env/index.ts +63 -0
  293. package/oh-my-opencode/src/hooks/non-interactive-env/types.ts +3 -0
  294. package/oh-my-opencode/src/hooks/prometheus-md-only/constants.ts +32 -0
  295. package/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts +488 -0
  296. package/oh-my-opencode/src/hooks/prometheus-md-only/index.ts +136 -0
  297. package/oh-my-opencode/src/hooks/ralph-loop/constants.ts +5 -0
  298. package/oh-my-opencode/src/hooks/ralph-loop/index.test.ts +835 -0
  299. package/oh-my-opencode/src/hooks/ralph-loop/index.ts +417 -0
  300. package/oh-my-opencode/src/hooks/ralph-loop/storage.ts +115 -0
  301. package/oh-my-opencode/src/hooks/ralph-loop/types.ts +19 -0
  302. package/oh-my-opencode/src/hooks/rules-injector/constants.ts +30 -0
  303. package/oh-my-opencode/src/hooks/rules-injector/finder.test.ts +381 -0
  304. package/oh-my-opencode/src/hooks/rules-injector/finder.ts +263 -0
  305. package/oh-my-opencode/src/hooks/rules-injector/index.ts +223 -0
  306. package/oh-my-opencode/src/hooks/rules-injector/matcher.ts +63 -0
  307. package/oh-my-opencode/src/hooks/rules-injector/parser.test.ts +226 -0
  308. package/oh-my-opencode/src/hooks/rules-injector/parser.ts +211 -0
  309. package/oh-my-opencode/src/hooks/rules-injector/storage.ts +59 -0
  310. package/oh-my-opencode/src/hooks/rules-injector/types.ts +57 -0
  311. package/oh-my-opencode/src/hooks/session-notification-utils.ts +140 -0
  312. package/oh-my-opencode/src/hooks/session-notification.test.ts +361 -0
  313. package/oh-my-opencode/src/hooks/session-notification.ts +330 -0
  314. package/oh-my-opencode/src/hooks/session-recovery/constants.ts +10 -0
  315. package/oh-my-opencode/src/hooks/session-recovery/index.test.ts +223 -0
  316. package/oh-my-opencode/src/hooks/session-recovery/index.ts +435 -0
  317. package/oh-my-opencode/src/hooks/session-recovery/storage.ts +390 -0
  318. package/oh-my-opencode/src/hooks/session-recovery/types.ts +98 -0
  319. package/oh-my-opencode/src/hooks/start-work/index.test.ts +402 -0
  320. package/oh-my-opencode/src/hooks/start-work/index.ts +242 -0
  321. package/oh-my-opencode/src/hooks/task-resume-info/index.ts +36 -0
  322. package/oh-my-opencode/src/hooks/think-mode/detector.ts +57 -0
  323. package/oh-my-opencode/src/hooks/think-mode/index.test.ts +353 -0
  324. package/oh-my-opencode/src/hooks/think-mode/index.ts +89 -0
  325. package/oh-my-opencode/src/hooks/think-mode/switcher.test.ts +461 -0
  326. package/oh-my-opencode/src/hooks/think-mode/switcher.ts +222 -0
  327. package/oh-my-opencode/src/hooks/think-mode/types.ts +21 -0
  328. package/oh-my-opencode/src/hooks/thinking-block-validator/index.ts +171 -0
  329. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.test.ts +876 -0
  330. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.ts +480 -0
  331. package/oh-my-opencode/src/hooks/tool-output-truncator.test.ts +168 -0
  332. package/oh-my-opencode/src/hooks/tool-output-truncator.ts +61 -0
  333. package/oh-my-opencode/src/index.ts +589 -0
  334. package/oh-my-opencode/src/mcp/AGENTS.md +70 -0
  335. package/oh-my-opencode/src/mcp/context7.ts +6 -0
  336. package/oh-my-opencode/src/mcp/grep-app.ts +6 -0
  337. package/oh-my-opencode/src/mcp/index.test.ts +86 -0
  338. package/oh-my-opencode/src/mcp/index.ts +32 -0
  339. package/oh-my-opencode/src/mcp/types.ts +9 -0
  340. package/oh-my-opencode/src/mcp/websearch.ts +10 -0
  341. package/oh-my-opencode/src/plugin-config.test.ts +119 -0
  342. package/oh-my-opencode/src/plugin-config.ts +135 -0
  343. package/oh-my-opencode/src/plugin-handlers/config-handler.test.ts +103 -0
  344. package/oh-my-opencode/src/plugin-handlers/config-handler.ts +399 -0
  345. package/oh-my-opencode/src/plugin-handlers/index.ts +1 -0
  346. package/oh-my-opencode/src/plugin-state.ts +30 -0
  347. package/oh-my-opencode/src/shared/AGENTS.md +63 -0
  348. package/oh-my-opencode/src/shared/agent-tool-restrictions.ts +44 -0
  349. package/oh-my-opencode/src/shared/agent-variant.test.ts +83 -0
  350. package/oh-my-opencode/src/shared/agent-variant.ts +40 -0
  351. package/oh-my-opencode/src/shared/claude-config-dir.test.ts +60 -0
  352. package/oh-my-opencode/src/shared/claude-config-dir.ts +11 -0
  353. package/oh-my-opencode/src/shared/command-executor.ts +225 -0
  354. package/oh-my-opencode/src/shared/config-errors.ts +18 -0
  355. package/oh-my-opencode/src/shared/config-path.ts +47 -0
  356. package/oh-my-opencode/src/shared/data-path.ts +22 -0
  357. package/oh-my-opencode/src/shared/deep-merge.test.ts +336 -0
  358. package/oh-my-opencode/src/shared/deep-merge.ts +53 -0
  359. package/oh-my-opencode/src/shared/dynamic-truncator.ts +193 -0
  360. package/oh-my-opencode/src/shared/external-plugin-detector.test.ts +133 -0
  361. package/oh-my-opencode/src/shared/external-plugin-detector.ts +132 -0
  362. package/oh-my-opencode/src/shared/file-reference-resolver.ts +85 -0
  363. package/oh-my-opencode/src/shared/file-utils.ts +40 -0
  364. package/oh-my-opencode/src/shared/first-message-variant.test.ts +32 -0
  365. package/oh-my-opencode/src/shared/first-message-variant.ts +28 -0
  366. package/oh-my-opencode/src/shared/frontmatter.test.ts +262 -0
  367. package/oh-my-opencode/src/shared/frontmatter.ts +31 -0
  368. package/oh-my-opencode/src/shared/hook-disabled.ts +22 -0
  369. package/oh-my-opencode/src/shared/index.ts +29 -0
  370. package/oh-my-opencode/src/shared/jsonc-parser.test.ts +266 -0
  371. package/oh-my-opencode/src/shared/jsonc-parser.ts +66 -0
  372. package/oh-my-opencode/src/shared/logger.ts +20 -0
  373. package/oh-my-opencode/src/shared/migration.test.ts +602 -0
  374. package/oh-my-opencode/src/shared/migration.ts +191 -0
  375. package/oh-my-opencode/src/shared/model-resolver.test.ts +101 -0
  376. package/oh-my-opencode/src/shared/model-resolver.ts +35 -0
  377. package/oh-my-opencode/src/shared/model-sanitizer.ts +12 -0
  378. package/oh-my-opencode/src/shared/opencode-config-dir.test.ts +318 -0
  379. package/oh-my-opencode/src/shared/opencode-config-dir.ts +142 -0
  380. package/oh-my-opencode/src/shared/opencode-version.test.ts +223 -0
  381. package/oh-my-opencode/src/shared/opencode-version.ts +72 -0
  382. package/oh-my-opencode/src/shared/pattern-matcher.ts +29 -0
  383. package/oh-my-opencode/src/shared/permission-compat.test.ts +134 -0
  384. package/oh-my-opencode/src/shared/permission-compat.ts +77 -0
  385. package/oh-my-opencode/src/shared/session-cursor.test.ts +66 -0
  386. package/oh-my-opencode/src/shared/session-cursor.ts +85 -0
  387. package/oh-my-opencode/src/shared/shell-env.test.ts +278 -0
  388. package/oh-my-opencode/src/shared/shell-env.ts +111 -0
  389. package/oh-my-opencode/src/shared/snake-case.ts +49 -0
  390. package/oh-my-opencode/src/shared/system-directive.ts +40 -0
  391. package/oh-my-opencode/src/shared/tool-name.ts +26 -0
  392. package/oh-my-opencode/src/shared/zip-extractor.ts +83 -0
  393. package/oh-my-opencode/src/tools/AGENTS.md +74 -0
  394. package/oh-my-opencode/src/tools/ast-grep/cli.ts +230 -0
  395. package/oh-my-opencode/src/tools/ast-grep/constants.ts +261 -0
  396. package/oh-my-opencode/src/tools/ast-grep/downloader.ts +128 -0
  397. package/oh-my-opencode/src/tools/ast-grep/index.ts +13 -0
  398. package/oh-my-opencode/src/tools/ast-grep/tools.ts +112 -0
  399. package/oh-my-opencode/src/tools/ast-grep/types.ts +61 -0
  400. package/oh-my-opencode/src/tools/ast-grep/utils.ts +102 -0
  401. package/oh-my-opencode/src/tools/background-task/constants.ts +7 -0
  402. package/oh-my-opencode/src/tools/background-task/index.ts +7 -0
  403. package/oh-my-opencode/src/tools/background-task/tools.ts +479 -0
  404. package/oh-my-opencode/src/tools/background-task/types.ts +16 -0
  405. package/oh-my-opencode/src/tools/call-omo-agent/constants.ts +7 -0
  406. package/oh-my-opencode/src/tools/call-omo-agent/index.ts +3 -0
  407. package/oh-my-opencode/src/tools/call-omo-agent/tools.ts +338 -0
  408. package/oh-my-opencode/src/tools/call-omo-agent/types.ts +27 -0
  409. package/oh-my-opencode/src/tools/delegate-task/constants.ts +205 -0
  410. package/oh-my-opencode/src/tools/delegate-task/index.ts +3 -0
  411. package/oh-my-opencode/src/tools/delegate-task/tools.test.ts +1575 -0
  412. package/oh-my-opencode/src/tools/delegate-task/tools.ts +885 -0
  413. package/oh-my-opencode/src/tools/delegate-task/types.ts +9 -0
  414. package/oh-my-opencode/src/tools/glob/cli.test.ts +158 -0
  415. package/oh-my-opencode/src/tools/glob/cli.ts +191 -0
  416. package/oh-my-opencode/src/tools/glob/constants.ts +12 -0
  417. package/oh-my-opencode/src/tools/glob/index.ts +3 -0
  418. package/oh-my-opencode/src/tools/glob/tools.ts +41 -0
  419. package/oh-my-opencode/src/tools/glob/types.ts +22 -0
  420. package/oh-my-opencode/src/tools/glob/utils.ts +26 -0
  421. package/oh-my-opencode/src/tools/grep/cli.ts +229 -0
  422. package/oh-my-opencode/src/tools/grep/constants.ts +127 -0
  423. package/oh-my-opencode/src/tools/grep/downloader.test.ts +103 -0
  424. package/oh-my-opencode/src/tools/grep/downloader.ts +145 -0
  425. package/oh-my-opencode/src/tools/grep/index.ts +3 -0
  426. package/oh-my-opencode/src/tools/grep/tools.ts +40 -0
  427. package/oh-my-opencode/src/tools/grep/types.ts +39 -0
  428. package/oh-my-opencode/src/tools/grep/utils.ts +53 -0
  429. package/oh-my-opencode/src/tools/index.ts +72 -0
  430. package/oh-my-opencode/src/tools/interactive-bash/constants.ts +18 -0
  431. package/oh-my-opencode/src/tools/interactive-bash/index.ts +4 -0
  432. package/oh-my-opencode/src/tools/interactive-bash/tools.ts +126 -0
  433. package/oh-my-opencode/src/tools/interactive-bash/utils.ts +71 -0
  434. package/oh-my-opencode/src/tools/look-at/constants.ts +3 -0
  435. package/oh-my-opencode/src/tools/look-at/index.ts +3 -0
  436. package/oh-my-opencode/src/tools/look-at/tools.test.ts +73 -0
  437. package/oh-my-opencode/src/tools/look-at/tools.ts +173 -0
  438. package/oh-my-opencode/src/tools/look-at/types.ts +4 -0
  439. package/oh-my-opencode/src/tools/lsp/client.ts +596 -0
  440. package/oh-my-opencode/src/tools/lsp/config.test.ts +130 -0
  441. package/oh-my-opencode/src/tools/lsp/config.ts +285 -0
  442. package/oh-my-opencode/src/tools/lsp/constants.ts +390 -0
  443. package/oh-my-opencode/src/tools/lsp/index.ts +7 -0
  444. package/oh-my-opencode/src/tools/lsp/tools.ts +261 -0
  445. package/oh-my-opencode/src/tools/lsp/types.ts +124 -0
  446. package/oh-my-opencode/src/tools/lsp/utils.ts +406 -0
  447. package/oh-my-opencode/src/tools/session-manager/constants.ts +97 -0
  448. package/oh-my-opencode/src/tools/session-manager/index.ts +3 -0
  449. package/oh-my-opencode/src/tools/session-manager/storage.test.ts +315 -0
  450. package/oh-my-opencode/src/tools/session-manager/storage.ts +238 -0
  451. package/oh-my-opencode/src/tools/session-manager/tools.test.ts +124 -0
  452. package/oh-my-opencode/src/tools/session-manager/tools.ts +146 -0
  453. package/oh-my-opencode/src/tools/session-manager/types.ts +99 -0
  454. package/oh-my-opencode/src/tools/session-manager/utils.test.ts +160 -0
  455. package/oh-my-opencode/src/tools/session-manager/utils.ts +199 -0
  456. package/oh-my-opencode/src/tools/skill/constants.ts +8 -0
  457. package/oh-my-opencode/src/tools/skill/index.ts +3 -0
  458. package/oh-my-opencode/src/tools/skill/tools.test.ts +239 -0
  459. package/oh-my-opencode/src/tools/skill/tools.ts +200 -0
  460. package/oh-my-opencode/src/tools/skill/types.ts +31 -0
  461. package/oh-my-opencode/src/tools/skill-mcp/constants.ts +3 -0
  462. package/oh-my-opencode/src/tools/skill-mcp/index.ts +3 -0
  463. package/oh-my-opencode/src/tools/skill-mcp/tools.test.ts +215 -0
  464. package/oh-my-opencode/src/tools/skill-mcp/tools.ts +172 -0
  465. package/oh-my-opencode/src/tools/skill-mcp/types.ts +8 -0
  466. package/oh-my-opencode/src/tools/slashcommand/index.ts +2 -0
  467. package/oh-my-opencode/src/tools/slashcommand/tools.ts +252 -0
  468. package/oh-my-opencode/src/tools/slashcommand/types.ts +28 -0
  469. package/oh-my-opencode/test-setup.ts +6 -0
  470. package/oh-my-opencode/tsconfig.json +20 -0
  471. package/package.json +1 -4
  472. package/src/__tests__/git.test.ts +7 -2
  473. package/src/__tests__/manifest.test.ts +5 -5
  474. package/src/agents/repo-explorer.ts +2 -1
  475. package/src/git.ts +18 -3
  476. package/src/manifest.ts +22 -15
  477. package/src/scanner.ts +41 -32
@@ -0,0 +1,361 @@
1
+ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
2
+
3
+ import { createSessionNotification } from "./session-notification"
4
+ import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
5
+ import * as utils from "./session-notification-utils"
6
+
7
+ describe("session-notification", () => {
8
+ let notificationCalls: string[]
9
+
10
+ function createMockPluginInput() {
11
+ return {
12
+ $: async (cmd: TemplateStringsArray | string, ...values: any[]) => {
13
+ // #given - track notification commands (osascript, notify-send, powershell)
14
+ const cmdStr = typeof cmd === "string"
15
+ ? cmd
16
+ : cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
17
+
18
+ if (cmdStr.includes("osascript") || cmdStr.includes("notify-send") || cmdStr.includes("powershell")) {
19
+ notificationCalls.push(cmdStr)
20
+ }
21
+ return { stdout: "", stderr: "", exitCode: 0 }
22
+ },
23
+ client: {
24
+ session: {
25
+ todo: async () => ({ data: [] }),
26
+ },
27
+ },
28
+ directory: "/tmp/test",
29
+ } as any
30
+ }
31
+
32
+ beforeEach(() => {
33
+ _resetForTesting()
34
+ notificationCalls = []
35
+
36
+ spyOn(utils, "getOsascriptPath").mockResolvedValue("/usr/bin/osascript")
37
+ spyOn(utils, "getNotifySendPath").mockResolvedValue("/usr/bin/notify-send")
38
+ spyOn(utils, "getPowershellPath").mockResolvedValue("powershell")
39
+ spyOn(utils, "getAfplayPath").mockResolvedValue("/usr/bin/afplay")
40
+ spyOn(utils, "getPaplayPath").mockResolvedValue("/usr/bin/paplay")
41
+ spyOn(utils, "getAplayPath").mockResolvedValue("/usr/bin/aplay")
42
+ spyOn(utils, "startBackgroundCheck").mockImplementation(() => {})
43
+ })
44
+
45
+ afterEach(() => {
46
+ // #given - cleanup after each test
47
+ subagentSessions.clear()
48
+ setMainSession(undefined)
49
+ })
50
+
51
+ test("should not trigger notification for subagent session", async () => {
52
+ // #given - a subagent session exists
53
+ const subagentSessionID = "subagent-123"
54
+ subagentSessions.add(subagentSessionID)
55
+
56
+ const hook = createSessionNotification(createMockPluginInput(), {
57
+ idleConfirmationDelay: 0,
58
+ })
59
+
60
+ // #when - subagent session goes idle
61
+ await hook({
62
+ event: {
63
+ type: "session.idle",
64
+ properties: { sessionID: subagentSessionID },
65
+ },
66
+ })
67
+
68
+ // Wait for any pending timers
69
+ await new Promise((resolve) => setTimeout(resolve, 50))
70
+
71
+ // #then - notification should NOT be sent
72
+ expect(notificationCalls).toHaveLength(0)
73
+ })
74
+
75
+ test("should not trigger notification when mainSessionID is set and session is not main", async () => {
76
+ // #given - main session is set, but a different session goes idle
77
+ const mainSessionID = "main-123"
78
+ const otherSessionID = "other-456"
79
+ setMainSession(mainSessionID)
80
+
81
+ const hook = createSessionNotification(createMockPluginInput(), {
82
+ idleConfirmationDelay: 0,
83
+ })
84
+
85
+ // #when - non-main session goes idle
86
+ await hook({
87
+ event: {
88
+ type: "session.idle",
89
+ properties: { sessionID: otherSessionID },
90
+ },
91
+ })
92
+
93
+ // Wait for any pending timers
94
+ await new Promise((resolve) => setTimeout(resolve, 50))
95
+
96
+ // #then - notification should NOT be sent
97
+ expect(notificationCalls).toHaveLength(0)
98
+ })
99
+
100
+ test("should trigger notification for main session when idle", async () => {
101
+ // #given - main session is set
102
+ const mainSessionID = "main-789"
103
+ setMainSession(mainSessionID)
104
+
105
+ const hook = createSessionNotification(createMockPluginInput(), {
106
+ idleConfirmationDelay: 10,
107
+ skipIfIncompleteTodos: false,
108
+ })
109
+
110
+ // #when - main session goes idle
111
+ await hook({
112
+ event: {
113
+ type: "session.idle",
114
+ properties: { sessionID: mainSessionID },
115
+ },
116
+ })
117
+
118
+ // Wait for idle confirmation delay + buffer
119
+ await new Promise((resolve) => setTimeout(resolve, 100))
120
+
121
+ // #then - notification should be sent
122
+ expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
123
+ })
124
+
125
+ test("should skip notification for subagent even when mainSessionID is set", async () => {
126
+ // #given - both mainSessionID and subagent session exist
127
+ const mainSessionID = "main-999"
128
+ const subagentSessionID = "subagent-888"
129
+ setMainSession(mainSessionID)
130
+ subagentSessions.add(subagentSessionID)
131
+
132
+ const hook = createSessionNotification(createMockPluginInput(), {
133
+ idleConfirmationDelay: 0,
134
+ })
135
+
136
+ // #when - subagent session goes idle
137
+ await hook({
138
+ event: {
139
+ type: "session.idle",
140
+ properties: { sessionID: subagentSessionID },
141
+ },
142
+ })
143
+
144
+ // Wait for any pending timers
145
+ await new Promise((resolve) => setTimeout(resolve, 50))
146
+
147
+ // #then - notification should NOT be sent (subagent check takes priority)
148
+ expect(notificationCalls).toHaveLength(0)
149
+ })
150
+
151
+ test("should handle subagentSessions and mainSessionID checks in correct order", async () => {
152
+ // #given - main session and subagent session exist
153
+ const mainSessionID = "main-111"
154
+ const subagentSessionID = "subagent-222"
155
+ const unknownSessionID = "unknown-333"
156
+ setMainSession(mainSessionID)
157
+ subagentSessions.add(subagentSessionID)
158
+
159
+ const hook = createSessionNotification(createMockPluginInput(), {
160
+ idleConfirmationDelay: 0,
161
+ })
162
+
163
+ // #when - subagent session goes idle
164
+ await hook({
165
+ event: {
166
+ type: "session.idle",
167
+ properties: { sessionID: subagentSessionID },
168
+ },
169
+ })
170
+
171
+ // #when - unknown session goes idle (not main, not in subagentSessions)
172
+ await hook({
173
+ event: {
174
+ type: "session.idle",
175
+ properties: { sessionID: unknownSessionID },
176
+ },
177
+ })
178
+
179
+ // Wait for any pending timers
180
+ await new Promise((resolve) => setTimeout(resolve, 50))
181
+
182
+ // #then - no notifications (subagent blocked by subagentSessions, unknown blocked by mainSessionID check)
183
+ expect(notificationCalls).toHaveLength(0)
184
+ })
185
+
186
+ test("should cancel pending notification on session activity", async () => {
187
+ // #given - main session is set
188
+ const mainSessionID = "main-cancel"
189
+ setMainSession(mainSessionID)
190
+
191
+ const hook = createSessionNotification(createMockPluginInput(), {
192
+ idleConfirmationDelay: 100, // Long delay
193
+ skipIfIncompleteTodos: false,
194
+ })
195
+
196
+ // #when - session goes idle
197
+ await hook({
198
+ event: {
199
+ type: "session.idle",
200
+ properties: { sessionID: mainSessionID },
201
+ },
202
+ })
203
+
204
+ // #when - activity happens before delay completes
205
+ await hook({
206
+ event: {
207
+ type: "tool.execute.before",
208
+ properties: { sessionID: mainSessionID },
209
+ },
210
+ })
211
+
212
+ // Wait for original delay to pass
213
+ await new Promise((resolve) => setTimeout(resolve, 150))
214
+
215
+ // #then - notification should NOT be sent (cancelled by activity)
216
+ expect(notificationCalls).toHaveLength(0)
217
+ })
218
+
219
+ test("should handle session.created event without notification", async () => {
220
+ // #given - a new session is created
221
+ const hook = createSessionNotification(createMockPluginInput(), {})
222
+
223
+ // #when - session.created event fires
224
+ await hook({
225
+ event: {
226
+ type: "session.created",
227
+ properties: {
228
+ info: { id: "new-session", title: "Test Session" },
229
+ },
230
+ },
231
+ })
232
+
233
+ // Wait for any pending timers
234
+ await new Promise((resolve) => setTimeout(resolve, 50))
235
+
236
+ // #then - no notification should be triggered
237
+ expect(notificationCalls).toHaveLength(0)
238
+ })
239
+
240
+ test("should handle session.deleted event and cleanup state", async () => {
241
+ // #given - a session exists
242
+ const hook = createSessionNotification(createMockPluginInput(), {})
243
+
244
+ // #when - session.deleted event fires
245
+ await hook({
246
+ event: {
247
+ type: "session.deleted",
248
+ properties: {
249
+ info: { id: "deleted-session" },
250
+ },
251
+ },
252
+ })
253
+
254
+ // Wait for any pending timers
255
+ await new Promise((resolve) => setTimeout(resolve, 50))
256
+
257
+ // #then - no notification should be triggered
258
+ expect(notificationCalls).toHaveLength(0)
259
+ })
260
+
261
+ test("should mark session activity on message.updated event", async () => {
262
+ // #given - main session is set
263
+ const mainSessionID = "main-message"
264
+ setMainSession(mainSessionID)
265
+
266
+ const hook = createSessionNotification(createMockPluginInput(), {
267
+ idleConfirmationDelay: 50,
268
+ skipIfIncompleteTodos: false,
269
+ })
270
+
271
+ // #when - session goes idle, then message.updated fires
272
+ await hook({
273
+ event: {
274
+ type: "session.idle",
275
+ properties: { sessionID: mainSessionID },
276
+ },
277
+ })
278
+
279
+ await hook({
280
+ event: {
281
+ type: "message.updated",
282
+ properties: {
283
+ info: { sessionID: mainSessionID, role: "user", finish: false },
284
+ },
285
+ },
286
+ })
287
+
288
+ // Wait for idle delay to pass
289
+ await new Promise((resolve) => setTimeout(resolve, 100))
290
+
291
+ // #then - notification should NOT be sent (activity cancelled it)
292
+ expect(notificationCalls).toHaveLength(0)
293
+ })
294
+
295
+ test("should mark session activity on tool.execute.before event", async () => {
296
+ // #given - main session is set
297
+ const mainSessionID = "main-tool"
298
+ setMainSession(mainSessionID)
299
+
300
+ const hook = createSessionNotification(createMockPluginInput(), {
301
+ idleConfirmationDelay: 50,
302
+ skipIfIncompleteTodos: false,
303
+ })
304
+
305
+ // #when - session goes idle, then tool.execute.before fires
306
+ await hook({
307
+ event: {
308
+ type: "session.idle",
309
+ properties: { sessionID: mainSessionID },
310
+ },
311
+ })
312
+
313
+ await hook({
314
+ event: {
315
+ type: "tool.execute.before",
316
+ properties: { sessionID: mainSessionID },
317
+ },
318
+ })
319
+
320
+ // Wait for idle delay to pass
321
+ await new Promise((resolve) => setTimeout(resolve, 100))
322
+
323
+ // #then - notification should NOT be sent (activity cancelled it)
324
+ expect(notificationCalls).toHaveLength(0)
325
+ })
326
+
327
+ test("should not send duplicate notification for same session", async () => {
328
+ // #given - main session is set
329
+ const mainSessionID = "main-dup"
330
+ setMainSession(mainSessionID)
331
+
332
+ const hook = createSessionNotification(createMockPluginInput(), {
333
+ idleConfirmationDelay: 10,
334
+ skipIfIncompleteTodos: false,
335
+ })
336
+
337
+ // #when - session goes idle twice
338
+ await hook({
339
+ event: {
340
+ type: "session.idle",
341
+ properties: { sessionID: mainSessionID },
342
+ },
343
+ })
344
+
345
+ // Wait for first notification
346
+ await new Promise((resolve) => setTimeout(resolve, 50))
347
+
348
+ await hook({
349
+ event: {
350
+ type: "session.idle",
351
+ properties: { sessionID: mainSessionID },
352
+ },
353
+ })
354
+
355
+ // Wait for second potential notification
356
+ await new Promise((resolve) => setTimeout(resolve, 50))
357
+
358
+ // #then - only one notification should be sent
359
+ expect(notificationCalls).toHaveLength(1)
360
+ })
361
+ })
@@ -0,0 +1,330 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin"
2
+ import { platform } from "os"
3
+ import { subagentSessions, getMainSessionID } from "../features/claude-code-session-state"
4
+ import {
5
+ getOsascriptPath,
6
+ getNotifySendPath,
7
+ getPowershellPath,
8
+ getAfplayPath,
9
+ getPaplayPath,
10
+ getAplayPath,
11
+ startBackgroundCheck,
12
+ } from "./session-notification-utils"
13
+
14
+ interface Todo {
15
+ content: string
16
+ status: string
17
+ priority: string
18
+ id: string
19
+ }
20
+
21
+ interface SessionNotificationConfig {
22
+ title?: string
23
+ message?: string
24
+ playSound?: boolean
25
+ soundPath?: string
26
+ /** Delay in ms before sending notification to confirm session is still idle (default: 1500) */
27
+ idleConfirmationDelay?: number
28
+ /** Skip notification if there are incomplete todos (default: true) */
29
+ skipIfIncompleteTodos?: boolean
30
+ /** Maximum number of sessions to track before cleanup (default: 100) */
31
+ maxTrackedSessions?: number
32
+ }
33
+
34
+ type Platform = "darwin" | "linux" | "win32" | "unsupported"
35
+
36
+ function detectPlatform(): Platform {
37
+ const p = platform()
38
+ if (p === "darwin" || p === "linux" || p === "win32") return p
39
+ return "unsupported"
40
+ }
41
+
42
+ function getDefaultSoundPath(p: Platform): string {
43
+ switch (p) {
44
+ case "darwin":
45
+ return "/System/Library/Sounds/Glass.aiff"
46
+ case "linux":
47
+ return "/usr/share/sounds/freedesktop/stereo/complete.oga"
48
+ case "win32":
49
+ return "C:\\Windows\\Media\\notify.wav"
50
+ default:
51
+ return ""
52
+ }
53
+ }
54
+
55
+ async function sendNotification(
56
+ ctx: PluginInput,
57
+ p: Platform,
58
+ title: string,
59
+ message: string
60
+ ): Promise<void> {
61
+ switch (p) {
62
+ case "darwin": {
63
+ const osascriptPath = await getOsascriptPath()
64
+ if (!osascriptPath) return
65
+
66
+ const esTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
67
+ const esMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
68
+ await ctx.$`${osascriptPath} -e ${"display notification \"" + esMessage + "\" with title \"" + esTitle + "\""}`.catch(() => {})
69
+ break
70
+ }
71
+ case "linux": {
72
+ const notifySendPath = await getNotifySendPath()
73
+ if (!notifySendPath) return
74
+
75
+ await ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`.catch(() => {})
76
+ break
77
+ }
78
+ case "win32": {
79
+ const powershellPath = await getPowershellPath()
80
+ if (!powershellPath) return
81
+
82
+ const psTitle = title.replace(/'/g, "''")
83
+ const psMessage = message.replace(/'/g, "''")
84
+ const toastScript = `
85
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
86
+ $Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
87
+ $RawXml = [xml] $Template.GetXml()
88
+ ($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq '1'}).AppendChild($RawXml.CreateTextNode('${psTitle}')) | Out-Null
89
+ ($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq '2'}).AppendChild($RawXml.CreateTextNode('${psMessage}')) | Out-Null
90
+ $SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument
91
+ $SerializedXml.LoadXml($RawXml.OuterXml)
92
+ $Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
93
+ $Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode')
94
+ $Notifier.Show($Toast)
95
+ `.trim().replace(/\n/g, "; ")
96
+ await ctx.$`${powershellPath} -Command ${toastScript}`.catch(() => {})
97
+ break
98
+ }
99
+ }
100
+ }
101
+
102
+ async function playSound(ctx: PluginInput, p: Platform, soundPath: string): Promise<void> {
103
+ switch (p) {
104
+ case "darwin": {
105
+ const afplayPath = await getAfplayPath()
106
+ if (!afplayPath) return
107
+ ctx.$`${afplayPath} ${soundPath}`.catch(() => {})
108
+ break
109
+ }
110
+ case "linux": {
111
+ const paplayPath = await getPaplayPath()
112
+ if (paplayPath) {
113
+ ctx.$`${paplayPath} ${soundPath} 2>/dev/null`.catch(() => {})
114
+ } else {
115
+ const aplayPath = await getAplayPath()
116
+ if (aplayPath) {
117
+ ctx.$`${aplayPath} ${soundPath} 2>/dev/null`.catch(() => {})
118
+ }
119
+ }
120
+ break
121
+ }
122
+ case "win32": {
123
+ const powershellPath = await getPowershellPath()
124
+ if (!powershellPath) return
125
+ ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + soundPath.replace(/'/g, "''") + "').PlaySync()"}`.catch(() => {})
126
+ break
127
+ }
128
+ }
129
+ }
130
+
131
+ async function hasIncompleteTodos(ctx: PluginInput, sessionID: string): Promise<boolean> {
132
+ try {
133
+ const response = await ctx.client.session.todo({ path: { id: sessionID } })
134
+ const todos = (response.data ?? response) as Todo[]
135
+ if (!todos || todos.length === 0) return false
136
+ return todos.some((t) => t.status !== "completed" && t.status !== "cancelled")
137
+ } catch {
138
+ return false
139
+ }
140
+ }
141
+
142
+ export function createSessionNotification(
143
+ ctx: PluginInput,
144
+ config: SessionNotificationConfig = {}
145
+ ) {
146
+ const currentPlatform = detectPlatform()
147
+ const defaultSoundPath = getDefaultSoundPath(currentPlatform)
148
+
149
+ startBackgroundCheck(currentPlatform)
150
+
151
+ const mergedConfig = {
152
+ title: "OpenCode",
153
+ message: "Agent is ready for input",
154
+ playSound: false,
155
+ soundPath: defaultSoundPath,
156
+ idleConfirmationDelay: 1500,
157
+ skipIfIncompleteTodos: true,
158
+ maxTrackedSessions: 100,
159
+ ...config,
160
+ }
161
+
162
+ const notifiedSessions = new Set<string>()
163
+ const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
164
+ const sessionActivitySinceIdle = new Set<string>()
165
+ // Track notification execution version to handle race conditions
166
+ const notificationVersions = new Map<string, number>()
167
+ // Track sessions currently executing notification (prevents duplicate execution)
168
+ const executingNotifications = new Set<string>()
169
+
170
+ function cleanupOldSessions() {
171
+ const maxSessions = mergedConfig.maxTrackedSessions
172
+ if (notifiedSessions.size > maxSessions) {
173
+ const sessionsToRemove = Array.from(notifiedSessions).slice(0, notifiedSessions.size - maxSessions)
174
+ sessionsToRemove.forEach(id => notifiedSessions.delete(id))
175
+ }
176
+ if (sessionActivitySinceIdle.size > maxSessions) {
177
+ const sessionsToRemove = Array.from(sessionActivitySinceIdle).slice(0, sessionActivitySinceIdle.size - maxSessions)
178
+ sessionsToRemove.forEach(id => sessionActivitySinceIdle.delete(id))
179
+ }
180
+ if (notificationVersions.size > maxSessions) {
181
+ const sessionsToRemove = Array.from(notificationVersions.keys()).slice(0, notificationVersions.size - maxSessions)
182
+ sessionsToRemove.forEach(id => notificationVersions.delete(id))
183
+ }
184
+ if (executingNotifications.size > maxSessions) {
185
+ const sessionsToRemove = Array.from(executingNotifications).slice(0, executingNotifications.size - maxSessions)
186
+ sessionsToRemove.forEach(id => executingNotifications.delete(id))
187
+ }
188
+ }
189
+
190
+ function cancelPendingNotification(sessionID: string) {
191
+ const timer = pendingTimers.get(sessionID)
192
+ if (timer) {
193
+ clearTimeout(timer)
194
+ pendingTimers.delete(sessionID)
195
+ }
196
+ sessionActivitySinceIdle.add(sessionID)
197
+ // Increment version to invalidate any in-flight notifications
198
+ notificationVersions.set(sessionID, (notificationVersions.get(sessionID) ?? 0) + 1)
199
+ }
200
+
201
+ function markSessionActivity(sessionID: string) {
202
+ cancelPendingNotification(sessionID)
203
+ notifiedSessions.delete(sessionID)
204
+ }
205
+
206
+ async function executeNotification(sessionID: string, version: number) {
207
+ if (executingNotifications.has(sessionID)) {
208
+ pendingTimers.delete(sessionID)
209
+ return
210
+ }
211
+
212
+ if (notificationVersions.get(sessionID) !== version) {
213
+ pendingTimers.delete(sessionID)
214
+ return
215
+ }
216
+
217
+ if (sessionActivitySinceIdle.has(sessionID)) {
218
+ sessionActivitySinceIdle.delete(sessionID)
219
+ pendingTimers.delete(sessionID)
220
+ return
221
+ }
222
+
223
+ if (notifiedSessions.has(sessionID)) {
224
+ pendingTimers.delete(sessionID)
225
+ return
226
+ }
227
+
228
+ executingNotifications.add(sessionID)
229
+ try {
230
+ if (mergedConfig.skipIfIncompleteTodos) {
231
+ const hasPendingWork = await hasIncompleteTodos(ctx, sessionID)
232
+ if (notificationVersions.get(sessionID) !== version) {
233
+ return
234
+ }
235
+ if (hasPendingWork) return
236
+ }
237
+
238
+ if (notificationVersions.get(sessionID) !== version) {
239
+ return
240
+ }
241
+
242
+ if (sessionActivitySinceIdle.has(sessionID)) {
243
+ sessionActivitySinceIdle.delete(sessionID)
244
+ return
245
+ }
246
+
247
+ notifiedSessions.add(sessionID)
248
+
249
+ await sendNotification(ctx, currentPlatform, mergedConfig.title, mergedConfig.message)
250
+
251
+ if (mergedConfig.playSound && mergedConfig.soundPath) {
252
+ await playSound(ctx, currentPlatform, mergedConfig.soundPath)
253
+ }
254
+ } finally {
255
+ executingNotifications.delete(sessionID)
256
+ pendingTimers.delete(sessionID)
257
+ }
258
+ }
259
+
260
+ return async ({ event }: { event: { type: string; properties?: unknown } }) => {
261
+ if (currentPlatform === "unsupported") return
262
+
263
+ const props = event.properties as Record<string, unknown> | undefined
264
+
265
+ if (event.type === "session.updated" || event.type === "session.created") {
266
+ const info = props?.info as Record<string, unknown> | undefined
267
+ const sessionID = info?.id as string | undefined
268
+ if (sessionID) {
269
+ markSessionActivity(sessionID)
270
+ }
271
+ return
272
+ }
273
+
274
+ if (event.type === "session.idle") {
275
+ const sessionID = props?.sessionID as string | undefined
276
+ if (!sessionID) return
277
+
278
+ if (subagentSessions.has(sessionID)) return
279
+
280
+ // Only trigger notifications for the main session (not subagent sessions)
281
+ const mainSessionID = getMainSessionID()
282
+ if (mainSessionID && sessionID !== mainSessionID) return
283
+
284
+ if (notifiedSessions.has(sessionID)) return
285
+ if (pendingTimers.has(sessionID)) return
286
+ if (executingNotifications.has(sessionID)) return
287
+
288
+ sessionActivitySinceIdle.delete(sessionID)
289
+
290
+ const currentVersion = (notificationVersions.get(sessionID) ?? 0) + 1
291
+ notificationVersions.set(sessionID, currentVersion)
292
+
293
+ const timer = setTimeout(() => {
294
+ executeNotification(sessionID, currentVersion)
295
+ }, mergedConfig.idleConfirmationDelay)
296
+
297
+ pendingTimers.set(sessionID, timer)
298
+ cleanupOldSessions()
299
+ return
300
+ }
301
+
302
+ if (event.type === "message.updated" || event.type === "message.created") {
303
+ const info = props?.info as Record<string, unknown> | undefined
304
+ const sessionID = info?.sessionID as string | undefined
305
+ if (sessionID) {
306
+ markSessionActivity(sessionID)
307
+ }
308
+ return
309
+ }
310
+
311
+ if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
312
+ const sessionID = props?.sessionID as string | undefined
313
+ if (sessionID) {
314
+ markSessionActivity(sessionID)
315
+ }
316
+ return
317
+ }
318
+
319
+ if (event.type === "session.deleted") {
320
+ const sessionInfo = props?.info as { id?: string } | undefined
321
+ if (sessionInfo?.id) {
322
+ cancelPendingNotification(sessionInfo.id)
323
+ notifiedSessions.delete(sessionInfo.id)
324
+ sessionActivitySinceIdle.delete(sessionInfo.id)
325
+ notificationVersions.delete(sessionInfo.id)
326
+ executingNotifications.delete(sessionInfo.id)
327
+ }
328
+ }
329
+ }
330
+ }
@@ -0,0 +1,10 @@
1
+ import { join } from "node:path"
2
+ import { getOpenCodeStorageDir } from "../../shared/data-path"
3
+
4
+ export const OPENCODE_STORAGE = getOpenCodeStorageDir()
5
+ export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
6
+ export const PART_STORAGE = join(OPENCODE_STORAGE, "part")
7
+
8
+ export const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
9
+ export const META_TYPES = new Set(["step-start", "step-finish"])
10
+ export const CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"])