opencode-repos 0.2.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 (475) hide show
  1. package/AGENTS.md +180 -0
  2. package/README.md +103 -3
  3. package/TODO.md +3 -0
  4. package/index.ts +1590 -158
  5. package/oh-my-opencode/.github/FUNDING.yml +15 -0
  6. package/oh-my-opencode/.github/ISSUE_TEMPLATE/bug_report.yml +129 -0
  7. package/oh-my-opencode/.github/ISSUE_TEMPLATE/config.yml +8 -0
  8. package/oh-my-opencode/.github/ISSUE_TEMPLATE/feature_request.yml +100 -0
  9. package/oh-my-opencode/.github/ISSUE_TEMPLATE/general.yml +83 -0
  10. package/oh-my-opencode/.github/assets/google.jpg +0 -0
  11. package/oh-my-opencode/.github/assets/hero.jpg +0 -0
  12. package/oh-my-opencode/.github/assets/indent.jpg +0 -0
  13. package/oh-my-opencode/.github/assets/microsoft.jpg +0 -0
  14. package/oh-my-opencode/.github/assets/omo.png +0 -0
  15. package/oh-my-opencode/.github/assets/orchestrator-atlas.png +0 -0
  16. package/oh-my-opencode/.github/assets/sisyphus.png +0 -0
  17. package/oh-my-opencode/.github/assets/sisyphuslabs.png +0 -0
  18. package/oh-my-opencode/.github/pull_request_template.md +34 -0
  19. package/oh-my-opencode/.github/workflows/ci.yml +138 -0
  20. package/oh-my-opencode/.github/workflows/cla.yml +41 -0
  21. package/oh-my-opencode/.github/workflows/lint-workflows.yml +22 -0
  22. package/oh-my-opencode/.github/workflows/publish.yml +165 -0
  23. package/oh-my-opencode/.github/workflows/sisyphus-agent.yml +500 -0
  24. package/oh-my-opencode/.opencode/background-tasks.json +27 -0
  25. package/oh-my-opencode/.opencode/command/get-unpublished-changes.md +84 -0
  26. package/oh-my-opencode/.opencode/command/omomomo.md +37 -0
  27. package/oh-my-opencode/.opencode/command/publish.md +257 -0
  28. package/oh-my-opencode/AGENTS.md +179 -0
  29. package/oh-my-opencode/CLA.md +58 -0
  30. package/oh-my-opencode/CONTRIBUTING.md +268 -0
  31. package/oh-my-opencode/LICENSE.md +82 -0
  32. package/oh-my-opencode/README.ja.md +370 -0
  33. package/oh-my-opencode/README.md +376 -0
  34. package/oh-my-opencode/README.zh-cn.md +380 -0
  35. package/oh-my-opencode/assets/oh-my-opencode.schema.json +2171 -0
  36. package/oh-my-opencode/bin/oh-my-opencode.js +80 -0
  37. package/oh-my-opencode/bin/platform.js +38 -0
  38. package/oh-my-opencode/bin/platform.test.ts +148 -0
  39. package/oh-my-opencode/bun.lock +314 -0
  40. package/oh-my-opencode/bunfig.toml +2 -0
  41. package/oh-my-opencode/docs/category-skill-guide.md +200 -0
  42. package/oh-my-opencode/docs/cli-guide.md +272 -0
  43. package/oh-my-opencode/docs/configurations.md +654 -0
  44. package/oh-my-opencode/docs/features.md +550 -0
  45. package/oh-my-opencode/docs/guide/installation.md +288 -0
  46. package/oh-my-opencode/docs/guide/overview.md +97 -0
  47. package/oh-my-opencode/docs/guide/understanding-orchestration-system.md +445 -0
  48. package/oh-my-opencode/docs/orchestration-guide.md +152 -0
  49. package/oh-my-opencode/docs/ultrawork-manifesto.md +197 -0
  50. package/oh-my-opencode/package.json +89 -0
  51. package/oh-my-opencode/packages/darwin-arm64/bin/.gitkeep +0 -0
  52. package/oh-my-opencode/packages/darwin-arm64/package.json +22 -0
  53. package/oh-my-opencode/packages/darwin-x64/bin/.gitkeep +0 -0
  54. package/oh-my-opencode/packages/darwin-x64/package.json +22 -0
  55. package/oh-my-opencode/packages/linux-arm64/bin/.gitkeep +0 -0
  56. package/oh-my-opencode/packages/linux-arm64/package.json +25 -0
  57. package/oh-my-opencode/packages/linux-arm64-musl/bin/.gitkeep +0 -0
  58. package/oh-my-opencode/packages/linux-arm64-musl/package.json +25 -0
  59. package/oh-my-opencode/packages/linux-x64/bin/.gitkeep +0 -0
  60. package/oh-my-opencode/packages/linux-x64/package.json +25 -0
  61. package/oh-my-opencode/packages/linux-x64-musl/bin/.gitkeep +0 -0
  62. package/oh-my-opencode/packages/linux-x64-musl/package.json +25 -0
  63. package/oh-my-opencode/packages/windows-x64/bin/.gitkeep +0 -0
  64. package/oh-my-opencode/packages/windows-x64/package.json +22 -0
  65. package/oh-my-opencode/postinstall.mjs +43 -0
  66. package/oh-my-opencode/script/build-binaries.ts +103 -0
  67. package/oh-my-opencode/script/build-schema.ts +28 -0
  68. package/oh-my-opencode/script/generate-changelog.ts +92 -0
  69. package/oh-my-opencode/script/publish.ts +344 -0
  70. package/oh-my-opencode/signatures/cla.json +676 -0
  71. package/oh-my-opencode/src/agents/AGENTS.md +67 -0
  72. package/oh-my-opencode/src/agents/atlas.ts +1383 -0
  73. package/oh-my-opencode/src/agents/dynamic-agent-prompt-builder.ts +400 -0
  74. package/oh-my-opencode/src/agents/explore.ts +122 -0
  75. package/oh-my-opencode/src/agents/index.ts +13 -0
  76. package/oh-my-opencode/src/agents/librarian.ts +326 -0
  77. package/oh-my-opencode/src/agents/metis.ts +315 -0
  78. package/oh-my-opencode/src/agents/momus.test.ts +57 -0
  79. package/oh-my-opencode/src/agents/momus.ts +444 -0
  80. package/oh-my-opencode/src/agents/multimodal-looker.ts +56 -0
  81. package/oh-my-opencode/src/agents/oracle.ts +122 -0
  82. package/oh-my-opencode/src/agents/prometheus-prompt.test.ts +22 -0
  83. package/oh-my-opencode/src/agents/prometheus-prompt.ts +1196 -0
  84. package/oh-my-opencode/src/agents/sisyphus-junior.test.ts +232 -0
  85. package/oh-my-opencode/src/agents/sisyphus-junior.ts +134 -0
  86. package/oh-my-opencode/src/agents/sisyphus.ts +633 -0
  87. package/oh-my-opencode/src/agents/types.ts +80 -0
  88. package/oh-my-opencode/src/agents/utils.test.ts +311 -0
  89. package/oh-my-opencode/src/agents/utils.ts +240 -0
  90. package/oh-my-opencode/src/cli/AGENTS.md +91 -0
  91. package/oh-my-opencode/src/cli/config-manager.test.ts +364 -0
  92. package/oh-my-opencode/src/cli/config-manager.ts +641 -0
  93. package/oh-my-opencode/src/cli/doctor/checks/auth.test.ts +114 -0
  94. package/oh-my-opencode/src/cli/doctor/checks/auth.ts +115 -0
  95. package/oh-my-opencode/src/cli/doctor/checks/config.test.ts +103 -0
  96. package/oh-my-opencode/src/cli/doctor/checks/config.ts +123 -0
  97. package/oh-my-opencode/src/cli/doctor/checks/dependencies.test.ts +152 -0
  98. package/oh-my-opencode/src/cli/doctor/checks/dependencies.ts +163 -0
  99. package/oh-my-opencode/src/cli/doctor/checks/gh.test.ts +151 -0
  100. package/oh-my-opencode/src/cli/doctor/checks/gh.ts +171 -0
  101. package/oh-my-opencode/src/cli/doctor/checks/index.ts +34 -0
  102. package/oh-my-opencode/src/cli/doctor/checks/lsp.test.ts +134 -0
  103. package/oh-my-opencode/src/cli/doctor/checks/lsp.ts +77 -0
  104. package/oh-my-opencode/src/cli/doctor/checks/mcp.test.ts +115 -0
  105. package/oh-my-opencode/src/cli/doctor/checks/mcp.ts +128 -0
  106. package/oh-my-opencode/src/cli/doctor/checks/opencode.test.ts +227 -0
  107. package/oh-my-opencode/src/cli/doctor/checks/opencode.ts +178 -0
  108. package/oh-my-opencode/src/cli/doctor/checks/plugin.test.ts +109 -0
  109. package/oh-my-opencode/src/cli/doctor/checks/plugin.ts +124 -0
  110. package/oh-my-opencode/src/cli/doctor/checks/version.test.ts +148 -0
  111. package/oh-my-opencode/src/cli/doctor/checks/version.ts +135 -0
  112. package/oh-my-opencode/src/cli/doctor/constants.ts +72 -0
  113. package/oh-my-opencode/src/cli/doctor/formatter.test.ts +218 -0
  114. package/oh-my-opencode/src/cli/doctor/formatter.ts +140 -0
  115. package/oh-my-opencode/src/cli/doctor/index.ts +11 -0
  116. package/oh-my-opencode/src/cli/doctor/runner.test.ts +153 -0
  117. package/oh-my-opencode/src/cli/doctor/runner.ts +132 -0
  118. package/oh-my-opencode/src/cli/doctor/types.ts +113 -0
  119. package/oh-my-opencode/src/cli/get-local-version/formatter.ts +66 -0
  120. package/oh-my-opencode/src/cli/get-local-version/index.ts +106 -0
  121. package/oh-my-opencode/src/cli/get-local-version/types.ts +14 -0
  122. package/oh-my-opencode/src/cli/index.ts +153 -0
  123. package/oh-my-opencode/src/cli/install.ts +523 -0
  124. package/oh-my-opencode/src/cli/model-fallback.ts +246 -0
  125. package/oh-my-opencode/src/cli/run/completion.test.ts +170 -0
  126. package/oh-my-opencode/src/cli/run/completion.ts +79 -0
  127. package/oh-my-opencode/src/cli/run/events.test.ts +155 -0
  128. package/oh-my-opencode/src/cli/run/events.ts +325 -0
  129. package/oh-my-opencode/src/cli/run/index.ts +2 -0
  130. package/oh-my-opencode/src/cli/run/runner.ts +159 -0
  131. package/oh-my-opencode/src/cli/run/types.ts +76 -0
  132. package/oh-my-opencode/src/cli/types.ts +40 -0
  133. package/oh-my-opencode/src/config/index.ts +26 -0
  134. package/oh-my-opencode/src/config/schema.test.ts +444 -0
  135. package/oh-my-opencode/src/config/schema.ts +339 -0
  136. package/oh-my-opencode/src/features/AGENTS.md +77 -0
  137. package/oh-my-opencode/src/features/background-agent/concurrency.test.ts +418 -0
  138. package/oh-my-opencode/src/features/background-agent/concurrency.ts +137 -0
  139. package/oh-my-opencode/src/features/background-agent/index.ts +3 -0
  140. package/oh-my-opencode/src/features/background-agent/manager.test.ts +1928 -0
  141. package/oh-my-opencode/src/features/background-agent/manager.ts +1335 -0
  142. package/oh-my-opencode/src/features/background-agent/types.ts +66 -0
  143. package/oh-my-opencode/src/features/boulder-state/constants.ts +13 -0
  144. package/oh-my-opencode/src/features/boulder-state/index.ts +3 -0
  145. package/oh-my-opencode/src/features/boulder-state/storage.test.ts +250 -0
  146. package/oh-my-opencode/src/features/boulder-state/storage.ts +150 -0
  147. package/oh-my-opencode/src/features/boulder-state/types.ts +26 -0
  148. package/oh-my-opencode/src/features/builtin-commands/commands.ts +89 -0
  149. package/oh-my-opencode/src/features/builtin-commands/index.ts +2 -0
  150. package/oh-my-opencode/src/features/builtin-commands/templates/init-deep.ts +300 -0
  151. package/oh-my-opencode/src/features/builtin-commands/templates/ralph-loop.ts +38 -0
  152. package/oh-my-opencode/src/features/builtin-commands/templates/refactor.ts +619 -0
  153. package/oh-my-opencode/src/features/builtin-commands/templates/start-work.ts +72 -0
  154. package/oh-my-opencode/src/features/builtin-commands/types.ts +9 -0
  155. package/oh-my-opencode/src/features/builtin-skills/frontend-ui-ux/SKILL.md +78 -0
  156. package/oh-my-opencode/src/features/builtin-skills/git-master/SKILL.md +1105 -0
  157. package/oh-my-opencode/src/features/builtin-skills/index.ts +2 -0
  158. package/oh-my-opencode/src/features/builtin-skills/skills.ts +1203 -0
  159. package/oh-my-opencode/src/features/builtin-skills/types.ts +16 -0
  160. package/oh-my-opencode/src/features/claude-code-agent-loader/index.ts +2 -0
  161. package/oh-my-opencode/src/features/claude-code-agent-loader/loader.ts +90 -0
  162. package/oh-my-opencode/src/features/claude-code-agent-loader/types.ts +17 -0
  163. package/oh-my-opencode/src/features/claude-code-command-loader/index.ts +2 -0
  164. package/oh-my-opencode/src/features/claude-code-command-loader/loader.ts +144 -0
  165. package/oh-my-opencode/src/features/claude-code-command-loader/types.ts +46 -0
  166. package/oh-my-opencode/src/features/claude-code-mcp-loader/env-expander.ts +27 -0
  167. package/oh-my-opencode/src/features/claude-code-mcp-loader/index.ts +11 -0
  168. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.test.ts +162 -0
  169. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.ts +113 -0
  170. package/oh-my-opencode/src/features/claude-code-mcp-loader/transformer.ts +53 -0
  171. package/oh-my-opencode/src/features/claude-code-mcp-loader/types.ts +42 -0
  172. package/oh-my-opencode/src/features/claude-code-plugin-loader/index.ts +3 -0
  173. package/oh-my-opencode/src/features/claude-code-plugin-loader/loader.ts +486 -0
  174. package/oh-my-opencode/src/features/claude-code-plugin-loader/types.ts +210 -0
  175. package/oh-my-opencode/src/features/claude-code-session-state/index.ts +1 -0
  176. package/oh-my-opencode/src/features/claude-code-session-state/state.test.ts +126 -0
  177. package/oh-my-opencode/src/features/claude-code-session-state/state.ts +37 -0
  178. package/oh-my-opencode/src/features/context-injector/collector.test.ts +330 -0
  179. package/oh-my-opencode/src/features/context-injector/collector.ts +85 -0
  180. package/oh-my-opencode/src/features/context-injector/index.ts +14 -0
  181. package/oh-my-opencode/src/features/context-injector/injector.test.ts +122 -0
  182. package/oh-my-opencode/src/features/context-injector/injector.ts +167 -0
  183. package/oh-my-opencode/src/features/context-injector/types.ts +91 -0
  184. package/oh-my-opencode/src/features/hook-message-injector/constants.ts +6 -0
  185. package/oh-my-opencode/src/features/hook-message-injector/index.ts +4 -0
  186. package/oh-my-opencode/src/features/hook-message-injector/injector.ts +195 -0
  187. package/oh-my-opencode/src/features/hook-message-injector/types.ts +47 -0
  188. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.test.ts +448 -0
  189. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.ts +180 -0
  190. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.test.ts +210 -0
  191. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.ts +62 -0
  192. package/oh-my-opencode/src/features/opencode-skill-loader/discover-worker.ts +59 -0
  193. package/oh-my-opencode/src/features/opencode-skill-loader/index.ts +4 -0
  194. package/oh-my-opencode/src/features/opencode-skill-loader/loader.test.ts +273 -0
  195. package/oh-my-opencode/src/features/opencode-skill-loader/loader.ts +259 -0
  196. package/oh-my-opencode/src/features/opencode-skill-loader/merger.ts +267 -0
  197. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.test.ts +267 -0
  198. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.ts +206 -0
  199. package/oh-my-opencode/src/features/opencode-skill-loader/types.ts +38 -0
  200. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.test.ts +201 -0
  201. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.ts +27 -0
  202. package/oh-my-opencode/src/features/skill-mcp-manager/index.ts +2 -0
  203. package/oh-my-opencode/src/features/skill-mcp-manager/manager.test.ts +611 -0
  204. package/oh-my-opencode/src/features/skill-mcp-manager/manager.ts +520 -0
  205. package/oh-my-opencode/src/features/skill-mcp-manager/types.ts +14 -0
  206. package/oh-my-opencode/src/features/task-toast-manager/index.ts +2 -0
  207. package/oh-my-opencode/src/features/task-toast-manager/manager.test.ts +249 -0
  208. package/oh-my-opencode/src/features/task-toast-manager/manager.ts +215 -0
  209. package/oh-my-opencode/src/features/task-toast-manager/types.ts +24 -0
  210. package/oh-my-opencode/src/hooks/AGENTS.md +73 -0
  211. package/oh-my-opencode/src/hooks/agent-usage-reminder/constants.ts +54 -0
  212. package/oh-my-opencode/src/hooks/agent-usage-reminder/index.ts +109 -0
  213. package/oh-my-opencode/src/hooks/agent-usage-reminder/storage.ts +42 -0
  214. package/oh-my-opencode/src/hooks/agent-usage-reminder/types.ts +6 -0
  215. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +307 -0
  216. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.ts +485 -0
  217. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/index.ts +151 -0
  218. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/parser.ts +201 -0
  219. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts +33 -0
  220. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +184 -0
  221. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-types.ts +44 -0
  222. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +77 -0
  223. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.ts +250 -0
  224. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/types.ts +42 -0
  225. package/oh-my-opencode/src/hooks/atlas/index.test.ts +953 -0
  226. package/oh-my-opencode/src/hooks/atlas/index.ts +771 -0
  227. package/oh-my-opencode/src/hooks/auto-slash-command/constants.ts +12 -0
  228. package/oh-my-opencode/src/hooks/auto-slash-command/detector.test.ts +296 -0
  229. package/oh-my-opencode/src/hooks/auto-slash-command/detector.ts +65 -0
  230. package/oh-my-opencode/src/hooks/auto-slash-command/executor.ts +205 -0
  231. package/oh-my-opencode/src/hooks/auto-slash-command/index.test.ts +254 -0
  232. package/oh-my-opencode/src/hooks/auto-slash-command/index.ts +89 -0
  233. package/oh-my-opencode/src/hooks/auto-slash-command/types.ts +23 -0
  234. package/oh-my-opencode/src/hooks/auto-update-checker/cache.ts +93 -0
  235. package/oh-my-opencode/src/hooks/auto-update-checker/checker.test.ts +24 -0
  236. package/oh-my-opencode/src/hooks/auto-update-checker/checker.ts +284 -0
  237. package/oh-my-opencode/src/hooks/auto-update-checker/constants.ts +64 -0
  238. package/oh-my-opencode/src/hooks/auto-update-checker/index.test.ts +254 -0
  239. package/oh-my-opencode/src/hooks/auto-update-checker/index.ts +260 -0
  240. package/oh-my-opencode/src/hooks/auto-update-checker/types.ts +29 -0
  241. package/oh-my-opencode/src/hooks/background-compaction/index.ts +87 -0
  242. package/oh-my-opencode/src/hooks/background-notification/index.ts +28 -0
  243. package/oh-my-opencode/src/hooks/background-notification/types.ts +5 -0
  244. package/oh-my-opencode/src/hooks/claude-code-hooks/AGENTS.md +70 -0
  245. package/oh-my-opencode/src/hooks/claude-code-hooks/config-loader.ts +107 -0
  246. package/oh-my-opencode/src/hooks/claude-code-hooks/config.ts +103 -0
  247. package/oh-my-opencode/src/hooks/claude-code-hooks/index.ts +401 -0
  248. package/oh-my-opencode/src/hooks/claude-code-hooks/plugin-config.ts +12 -0
  249. package/oh-my-opencode/src/hooks/claude-code-hooks/post-tool-use.ts +199 -0
  250. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-compact.ts +109 -0
  251. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-tool-use.ts +172 -0
  252. package/oh-my-opencode/src/hooks/claude-code-hooks/stop.ts +118 -0
  253. package/oh-my-opencode/src/hooks/claude-code-hooks/todo.ts +76 -0
  254. package/oh-my-opencode/src/hooks/claude-code-hooks/tool-input-cache.ts +47 -0
  255. package/oh-my-opencode/src/hooks/claude-code-hooks/transcript.ts +252 -0
  256. package/oh-my-opencode/src/hooks/claude-code-hooks/types.ts +204 -0
  257. package/oh-my-opencode/src/hooks/claude-code-hooks/user-prompt-submit.ts +117 -0
  258. package/oh-my-opencode/src/hooks/comment-checker/cli.test.ts +68 -0
  259. package/oh-my-opencode/src/hooks/comment-checker/cli.ts +221 -0
  260. package/oh-my-opencode/src/hooks/comment-checker/downloader.ts +196 -0
  261. package/oh-my-opencode/src/hooks/comment-checker/index.ts +171 -0
  262. package/oh-my-opencode/src/hooks/comment-checker/types.ts +33 -0
  263. package/oh-my-opencode/src/hooks/compaction-context-injector/index.ts +61 -0
  264. package/oh-my-opencode/src/hooks/context-window-monitor.ts +99 -0
  265. package/oh-my-opencode/src/hooks/delegate-task-retry/index.test.ts +119 -0
  266. package/oh-my-opencode/src/hooks/delegate-task-retry/index.ts +136 -0
  267. package/oh-my-opencode/src/hooks/directory-agents-injector/constants.ts +9 -0
  268. package/oh-my-opencode/src/hooks/directory-agents-injector/index.ts +182 -0
  269. package/oh-my-opencode/src/hooks/directory-agents-injector/storage.ts +48 -0
  270. package/oh-my-opencode/src/hooks/directory-agents-injector/types.ts +5 -0
  271. package/oh-my-opencode/src/hooks/directory-readme-injector/constants.ts +9 -0
  272. package/oh-my-opencode/src/hooks/directory-readme-injector/index.ts +177 -0
  273. package/oh-my-opencode/src/hooks/directory-readme-injector/storage.ts +48 -0
  274. package/oh-my-opencode/src/hooks/directory-readme-injector/types.ts +5 -0
  275. package/oh-my-opencode/src/hooks/edit-error-recovery/index.test.ts +126 -0
  276. package/oh-my-opencode/src/hooks/edit-error-recovery/index.ts +57 -0
  277. package/oh-my-opencode/src/hooks/empty-task-response-detector.ts +27 -0
  278. package/oh-my-opencode/src/hooks/index.ts +32 -0
  279. package/oh-my-opencode/src/hooks/interactive-bash-session/constants.ts +15 -0
  280. package/oh-my-opencode/src/hooks/interactive-bash-session/index.ts +262 -0
  281. package/oh-my-opencode/src/hooks/interactive-bash-session/storage.ts +59 -0
  282. package/oh-my-opencode/src/hooks/interactive-bash-session/types.ts +11 -0
  283. package/oh-my-opencode/src/hooks/keyword-detector/constants.ts +300 -0
  284. package/oh-my-opencode/src/hooks/keyword-detector/detector.ts +52 -0
  285. package/oh-my-opencode/src/hooks/keyword-detector/index.test.ts +529 -0
  286. package/oh-my-opencode/src/hooks/keyword-detector/index.ts +100 -0
  287. package/oh-my-opencode/src/hooks/keyword-detector/types.ts +4 -0
  288. package/oh-my-opencode/src/hooks/non-interactive-env/constants.ts +70 -0
  289. package/oh-my-opencode/src/hooks/non-interactive-env/detector.ts +19 -0
  290. package/oh-my-opencode/src/hooks/non-interactive-env/index.test.ts +323 -0
  291. package/oh-my-opencode/src/hooks/non-interactive-env/index.ts +63 -0
  292. package/oh-my-opencode/src/hooks/non-interactive-env/types.ts +3 -0
  293. package/oh-my-opencode/src/hooks/prometheus-md-only/constants.ts +32 -0
  294. package/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts +488 -0
  295. package/oh-my-opencode/src/hooks/prometheus-md-only/index.ts +136 -0
  296. package/oh-my-opencode/src/hooks/ralph-loop/constants.ts +5 -0
  297. package/oh-my-opencode/src/hooks/ralph-loop/index.test.ts +835 -0
  298. package/oh-my-opencode/src/hooks/ralph-loop/index.ts +417 -0
  299. package/oh-my-opencode/src/hooks/ralph-loop/storage.ts +115 -0
  300. package/oh-my-opencode/src/hooks/ralph-loop/types.ts +19 -0
  301. package/oh-my-opencode/src/hooks/rules-injector/constants.ts +30 -0
  302. package/oh-my-opencode/src/hooks/rules-injector/finder.test.ts +381 -0
  303. package/oh-my-opencode/src/hooks/rules-injector/finder.ts +263 -0
  304. package/oh-my-opencode/src/hooks/rules-injector/index.ts +223 -0
  305. package/oh-my-opencode/src/hooks/rules-injector/matcher.ts +63 -0
  306. package/oh-my-opencode/src/hooks/rules-injector/parser.test.ts +226 -0
  307. package/oh-my-opencode/src/hooks/rules-injector/parser.ts +211 -0
  308. package/oh-my-opencode/src/hooks/rules-injector/storage.ts +59 -0
  309. package/oh-my-opencode/src/hooks/rules-injector/types.ts +57 -0
  310. package/oh-my-opencode/src/hooks/session-notification-utils.ts +140 -0
  311. package/oh-my-opencode/src/hooks/session-notification.test.ts +361 -0
  312. package/oh-my-opencode/src/hooks/session-notification.ts +330 -0
  313. package/oh-my-opencode/src/hooks/session-recovery/constants.ts +10 -0
  314. package/oh-my-opencode/src/hooks/session-recovery/index.test.ts +223 -0
  315. package/oh-my-opencode/src/hooks/session-recovery/index.ts +435 -0
  316. package/oh-my-opencode/src/hooks/session-recovery/storage.ts +390 -0
  317. package/oh-my-opencode/src/hooks/session-recovery/types.ts +98 -0
  318. package/oh-my-opencode/src/hooks/start-work/index.test.ts +402 -0
  319. package/oh-my-opencode/src/hooks/start-work/index.ts +242 -0
  320. package/oh-my-opencode/src/hooks/task-resume-info/index.ts +36 -0
  321. package/oh-my-opencode/src/hooks/think-mode/detector.ts +57 -0
  322. package/oh-my-opencode/src/hooks/think-mode/index.test.ts +353 -0
  323. package/oh-my-opencode/src/hooks/think-mode/index.ts +89 -0
  324. package/oh-my-opencode/src/hooks/think-mode/switcher.test.ts +461 -0
  325. package/oh-my-opencode/src/hooks/think-mode/switcher.ts +222 -0
  326. package/oh-my-opencode/src/hooks/think-mode/types.ts +21 -0
  327. package/oh-my-opencode/src/hooks/thinking-block-validator/index.ts +171 -0
  328. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.test.ts +876 -0
  329. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.ts +480 -0
  330. package/oh-my-opencode/src/hooks/tool-output-truncator.test.ts +168 -0
  331. package/oh-my-opencode/src/hooks/tool-output-truncator.ts +61 -0
  332. package/oh-my-opencode/src/index.ts +589 -0
  333. package/oh-my-opencode/src/mcp/AGENTS.md +70 -0
  334. package/oh-my-opencode/src/mcp/context7.ts +6 -0
  335. package/oh-my-opencode/src/mcp/grep-app.ts +6 -0
  336. package/oh-my-opencode/src/mcp/index.test.ts +86 -0
  337. package/oh-my-opencode/src/mcp/index.ts +32 -0
  338. package/oh-my-opencode/src/mcp/types.ts +9 -0
  339. package/oh-my-opencode/src/mcp/websearch.ts +10 -0
  340. package/oh-my-opencode/src/plugin-config.test.ts +119 -0
  341. package/oh-my-opencode/src/plugin-config.ts +135 -0
  342. package/oh-my-opencode/src/plugin-handlers/config-handler.test.ts +103 -0
  343. package/oh-my-opencode/src/plugin-handlers/config-handler.ts +399 -0
  344. package/oh-my-opencode/src/plugin-handlers/index.ts +1 -0
  345. package/oh-my-opencode/src/plugin-state.ts +30 -0
  346. package/oh-my-opencode/src/shared/AGENTS.md +63 -0
  347. package/oh-my-opencode/src/shared/agent-tool-restrictions.ts +44 -0
  348. package/oh-my-opencode/src/shared/agent-variant.test.ts +83 -0
  349. package/oh-my-opencode/src/shared/agent-variant.ts +40 -0
  350. package/oh-my-opencode/src/shared/claude-config-dir.test.ts +60 -0
  351. package/oh-my-opencode/src/shared/claude-config-dir.ts +11 -0
  352. package/oh-my-opencode/src/shared/command-executor.ts +225 -0
  353. package/oh-my-opencode/src/shared/config-errors.ts +18 -0
  354. package/oh-my-opencode/src/shared/config-path.ts +47 -0
  355. package/oh-my-opencode/src/shared/data-path.ts +22 -0
  356. package/oh-my-opencode/src/shared/deep-merge.test.ts +336 -0
  357. package/oh-my-opencode/src/shared/deep-merge.ts +53 -0
  358. package/oh-my-opencode/src/shared/dynamic-truncator.ts +193 -0
  359. package/oh-my-opencode/src/shared/external-plugin-detector.test.ts +133 -0
  360. package/oh-my-opencode/src/shared/external-plugin-detector.ts +132 -0
  361. package/oh-my-opencode/src/shared/file-reference-resolver.ts +85 -0
  362. package/oh-my-opencode/src/shared/file-utils.ts +40 -0
  363. package/oh-my-opencode/src/shared/first-message-variant.test.ts +32 -0
  364. package/oh-my-opencode/src/shared/first-message-variant.ts +28 -0
  365. package/oh-my-opencode/src/shared/frontmatter.test.ts +262 -0
  366. package/oh-my-opencode/src/shared/frontmatter.ts +31 -0
  367. package/oh-my-opencode/src/shared/hook-disabled.ts +22 -0
  368. package/oh-my-opencode/src/shared/index.ts +29 -0
  369. package/oh-my-opencode/src/shared/jsonc-parser.test.ts +266 -0
  370. package/oh-my-opencode/src/shared/jsonc-parser.ts +66 -0
  371. package/oh-my-opencode/src/shared/logger.ts +20 -0
  372. package/oh-my-opencode/src/shared/migration.test.ts +602 -0
  373. package/oh-my-opencode/src/shared/migration.ts +191 -0
  374. package/oh-my-opencode/src/shared/model-resolver.test.ts +101 -0
  375. package/oh-my-opencode/src/shared/model-resolver.ts +35 -0
  376. package/oh-my-opencode/src/shared/model-sanitizer.ts +12 -0
  377. package/oh-my-opencode/src/shared/opencode-config-dir.test.ts +318 -0
  378. package/oh-my-opencode/src/shared/opencode-config-dir.ts +142 -0
  379. package/oh-my-opencode/src/shared/opencode-version.test.ts +223 -0
  380. package/oh-my-opencode/src/shared/opencode-version.ts +72 -0
  381. package/oh-my-opencode/src/shared/pattern-matcher.ts +29 -0
  382. package/oh-my-opencode/src/shared/permission-compat.test.ts +134 -0
  383. package/oh-my-opencode/src/shared/permission-compat.ts +77 -0
  384. package/oh-my-opencode/src/shared/session-cursor.test.ts +66 -0
  385. package/oh-my-opencode/src/shared/session-cursor.ts +85 -0
  386. package/oh-my-opencode/src/shared/shell-env.test.ts +278 -0
  387. package/oh-my-opencode/src/shared/shell-env.ts +111 -0
  388. package/oh-my-opencode/src/shared/snake-case.ts +49 -0
  389. package/oh-my-opencode/src/shared/system-directive.ts +40 -0
  390. package/oh-my-opencode/src/shared/tool-name.ts +26 -0
  391. package/oh-my-opencode/src/shared/zip-extractor.ts +83 -0
  392. package/oh-my-opencode/src/tools/AGENTS.md +74 -0
  393. package/oh-my-opencode/src/tools/ast-grep/cli.ts +230 -0
  394. package/oh-my-opencode/src/tools/ast-grep/constants.ts +261 -0
  395. package/oh-my-opencode/src/tools/ast-grep/downloader.ts +128 -0
  396. package/oh-my-opencode/src/tools/ast-grep/index.ts +13 -0
  397. package/oh-my-opencode/src/tools/ast-grep/tools.ts +112 -0
  398. package/oh-my-opencode/src/tools/ast-grep/types.ts +61 -0
  399. package/oh-my-opencode/src/tools/ast-grep/utils.ts +102 -0
  400. package/oh-my-opencode/src/tools/background-task/constants.ts +7 -0
  401. package/oh-my-opencode/src/tools/background-task/index.ts +7 -0
  402. package/oh-my-opencode/src/tools/background-task/tools.ts +479 -0
  403. package/oh-my-opencode/src/tools/background-task/types.ts +16 -0
  404. package/oh-my-opencode/src/tools/call-omo-agent/constants.ts +7 -0
  405. package/oh-my-opencode/src/tools/call-omo-agent/index.ts +3 -0
  406. package/oh-my-opencode/src/tools/call-omo-agent/tools.ts +338 -0
  407. package/oh-my-opencode/src/tools/call-omo-agent/types.ts +27 -0
  408. package/oh-my-opencode/src/tools/delegate-task/constants.ts +205 -0
  409. package/oh-my-opencode/src/tools/delegate-task/index.ts +3 -0
  410. package/oh-my-opencode/src/tools/delegate-task/tools.test.ts +1575 -0
  411. package/oh-my-opencode/src/tools/delegate-task/tools.ts +885 -0
  412. package/oh-my-opencode/src/tools/delegate-task/types.ts +9 -0
  413. package/oh-my-opencode/src/tools/glob/cli.test.ts +158 -0
  414. package/oh-my-opencode/src/tools/glob/cli.ts +191 -0
  415. package/oh-my-opencode/src/tools/glob/constants.ts +12 -0
  416. package/oh-my-opencode/src/tools/glob/index.ts +3 -0
  417. package/oh-my-opencode/src/tools/glob/tools.ts +41 -0
  418. package/oh-my-opencode/src/tools/glob/types.ts +22 -0
  419. package/oh-my-opencode/src/tools/glob/utils.ts +26 -0
  420. package/oh-my-opencode/src/tools/grep/cli.ts +229 -0
  421. package/oh-my-opencode/src/tools/grep/constants.ts +127 -0
  422. package/oh-my-opencode/src/tools/grep/downloader.test.ts +103 -0
  423. package/oh-my-opencode/src/tools/grep/downloader.ts +145 -0
  424. package/oh-my-opencode/src/tools/grep/index.ts +3 -0
  425. package/oh-my-opencode/src/tools/grep/tools.ts +40 -0
  426. package/oh-my-opencode/src/tools/grep/types.ts +39 -0
  427. package/oh-my-opencode/src/tools/grep/utils.ts +53 -0
  428. package/oh-my-opencode/src/tools/index.ts +72 -0
  429. package/oh-my-opencode/src/tools/interactive-bash/constants.ts +18 -0
  430. package/oh-my-opencode/src/tools/interactive-bash/index.ts +4 -0
  431. package/oh-my-opencode/src/tools/interactive-bash/tools.ts +126 -0
  432. package/oh-my-opencode/src/tools/interactive-bash/utils.ts +71 -0
  433. package/oh-my-opencode/src/tools/look-at/constants.ts +3 -0
  434. package/oh-my-opencode/src/tools/look-at/index.ts +3 -0
  435. package/oh-my-opencode/src/tools/look-at/tools.test.ts +73 -0
  436. package/oh-my-opencode/src/tools/look-at/tools.ts +173 -0
  437. package/oh-my-opencode/src/tools/look-at/types.ts +4 -0
  438. package/oh-my-opencode/src/tools/lsp/client.ts +596 -0
  439. package/oh-my-opencode/src/tools/lsp/config.test.ts +130 -0
  440. package/oh-my-opencode/src/tools/lsp/config.ts +285 -0
  441. package/oh-my-opencode/src/tools/lsp/constants.ts +390 -0
  442. package/oh-my-opencode/src/tools/lsp/index.ts +7 -0
  443. package/oh-my-opencode/src/tools/lsp/tools.ts +261 -0
  444. package/oh-my-opencode/src/tools/lsp/types.ts +124 -0
  445. package/oh-my-opencode/src/tools/lsp/utils.ts +406 -0
  446. package/oh-my-opencode/src/tools/session-manager/constants.ts +97 -0
  447. package/oh-my-opencode/src/tools/session-manager/index.ts +3 -0
  448. package/oh-my-opencode/src/tools/session-manager/storage.test.ts +315 -0
  449. package/oh-my-opencode/src/tools/session-manager/storage.ts +238 -0
  450. package/oh-my-opencode/src/tools/session-manager/tools.test.ts +124 -0
  451. package/oh-my-opencode/src/tools/session-manager/tools.ts +146 -0
  452. package/oh-my-opencode/src/tools/session-manager/types.ts +99 -0
  453. package/oh-my-opencode/src/tools/session-manager/utils.test.ts +160 -0
  454. package/oh-my-opencode/src/tools/session-manager/utils.ts +199 -0
  455. package/oh-my-opencode/src/tools/skill/constants.ts +8 -0
  456. package/oh-my-opencode/src/tools/skill/index.ts +3 -0
  457. package/oh-my-opencode/src/tools/skill/tools.test.ts +239 -0
  458. package/oh-my-opencode/src/tools/skill/tools.ts +200 -0
  459. package/oh-my-opencode/src/tools/skill/types.ts +31 -0
  460. package/oh-my-opencode/src/tools/skill-mcp/constants.ts +3 -0
  461. package/oh-my-opencode/src/tools/skill-mcp/index.ts +3 -0
  462. package/oh-my-opencode/src/tools/skill-mcp/tools.test.ts +215 -0
  463. package/oh-my-opencode/src/tools/skill-mcp/tools.ts +172 -0
  464. package/oh-my-opencode/src/tools/skill-mcp/types.ts +8 -0
  465. package/oh-my-opencode/src/tools/slashcommand/index.ts +2 -0
  466. package/oh-my-opencode/src/tools/slashcommand/tools.ts +252 -0
  467. package/oh-my-opencode/src/tools/slashcommand/types.ts +28 -0
  468. package/oh-my-opencode/test-setup.ts +6 -0
  469. package/oh-my-opencode/tsconfig.json +20 -0
  470. package/package.json +1 -1
  471. package/src/__tests__/git.test.ts +7 -2
  472. package/src/__tests__/manifest.test.ts +5 -5
  473. package/src/agents/repo-explorer.ts +2 -1
  474. package/src/git.ts +18 -3
  475. package/src/manifest.ts +22 -15
@@ -0,0 +1,381 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { findProjectRoot, findRuleFiles } from "./finder";
6
+
7
+ describe("findRuleFiles", () => {
8
+ const TEST_DIR = join(tmpdir(), `rules-injector-test-${Date.now()}`);
9
+ const homeDir = join(TEST_DIR, "home");
10
+
11
+ beforeEach(() => {
12
+ mkdirSync(TEST_DIR, { recursive: true });
13
+ mkdirSync(homeDir, { recursive: true });
14
+ mkdirSync(join(TEST_DIR, ".git"), { recursive: true });
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (existsSync(TEST_DIR)) {
19
+ rmSync(TEST_DIR, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ describe(".github/instructions/ discovery", () => {
24
+ it("should discover .github/instructions/*.instructions.md files", () => {
25
+ // #given .github/instructions/ with valid files
26
+ const instructionsDir = join(TEST_DIR, ".github", "instructions");
27
+ mkdirSync(instructionsDir, { recursive: true });
28
+ writeFileSync(
29
+ join(instructionsDir, "typescript.instructions.md"),
30
+ "TS rules"
31
+ );
32
+ writeFileSync(
33
+ join(instructionsDir, "python.instructions.md"),
34
+ "PY rules"
35
+ );
36
+
37
+ const srcDir = join(TEST_DIR, "src");
38
+ mkdirSync(srcDir, { recursive: true });
39
+ const currentFile = join(srcDir, "index.ts");
40
+ writeFileSync(currentFile, "code");
41
+
42
+ // #when finding rules for a file
43
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
44
+
45
+ // #then should find both instruction files
46
+ const paths = candidates.map((c) => c.path);
47
+ expect(
48
+ paths.some((p) => p.includes("typescript.instructions.md"))
49
+ ).toBe(true);
50
+ expect(paths.some((p) => p.includes("python.instructions.md"))).toBe(
51
+ true
52
+ );
53
+ });
54
+
55
+ it("should ignore non-.instructions.md files in .github/instructions/", () => {
56
+ // #given .github/instructions/ with invalid files
57
+ const instructionsDir = join(TEST_DIR, ".github", "instructions");
58
+ mkdirSync(instructionsDir, { recursive: true });
59
+ writeFileSync(
60
+ join(instructionsDir, "valid.instructions.md"),
61
+ "valid"
62
+ );
63
+ writeFileSync(join(instructionsDir, "invalid.md"), "invalid");
64
+ writeFileSync(join(instructionsDir, "readme.txt"), "readme");
65
+
66
+ const currentFile = join(TEST_DIR, "index.ts");
67
+ writeFileSync(currentFile, "code");
68
+
69
+ // #when finding rules
70
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
71
+
72
+ // #then should only find .instructions.md file
73
+ const paths = candidates.map((c) => c.path);
74
+ expect(paths.some((p) => p.includes("valid.instructions.md"))).toBe(
75
+ true
76
+ );
77
+ expect(paths.some((p) => p.endsWith("invalid.md"))).toBe(false);
78
+ expect(paths.some((p) => p.includes("readme.txt"))).toBe(false);
79
+ });
80
+
81
+ it("should discover nested .instructions.md files in subdirectories", () => {
82
+ // #given nested .github/instructions/ structure
83
+ const instructionsDir = join(TEST_DIR, ".github", "instructions");
84
+ const frontendDir = join(instructionsDir, "frontend");
85
+ mkdirSync(frontendDir, { recursive: true });
86
+ writeFileSync(
87
+ join(frontendDir, "react.instructions.md"),
88
+ "React rules"
89
+ );
90
+
91
+ const currentFile = join(TEST_DIR, "app.tsx");
92
+ writeFileSync(currentFile, "code");
93
+
94
+ // #when finding rules
95
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
96
+
97
+ // #then should find nested instruction file
98
+ const paths = candidates.map((c) => c.path);
99
+ expect(paths.some((p) => p.includes("react.instructions.md"))).toBe(
100
+ true
101
+ );
102
+ });
103
+ });
104
+
105
+ describe(".github/copilot-instructions.md (single file)", () => {
106
+ it("should discover copilot-instructions.md at project root", () => {
107
+ // #given .github/copilot-instructions.md at root
108
+ const githubDir = join(TEST_DIR, ".github");
109
+ mkdirSync(githubDir, { recursive: true });
110
+ writeFileSync(
111
+ join(githubDir, "copilot-instructions.md"),
112
+ "Global instructions"
113
+ );
114
+
115
+ const currentFile = join(TEST_DIR, "index.ts");
116
+ writeFileSync(currentFile, "code");
117
+
118
+ // #when finding rules
119
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
120
+
121
+ // #then should find the single file rule
122
+ const singleFile = candidates.find((c) =>
123
+ c.path.includes("copilot-instructions.md")
124
+ );
125
+ expect(singleFile).toBeDefined();
126
+ expect(singleFile?.isSingleFile).toBe(true);
127
+ });
128
+
129
+ it("should mark single file rules with isSingleFile: true", () => {
130
+ // #given copilot-instructions.md
131
+ const githubDir = join(TEST_DIR, ".github");
132
+ mkdirSync(githubDir, { recursive: true });
133
+ writeFileSync(
134
+ join(githubDir, "copilot-instructions.md"),
135
+ "Instructions"
136
+ );
137
+
138
+ const currentFile = join(TEST_DIR, "file.ts");
139
+ writeFileSync(currentFile, "code");
140
+
141
+ // #when finding rules
142
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
143
+
144
+ // #then isSingleFile should be true
145
+ const copilotFile = candidates.find((c) => c.isSingleFile);
146
+ expect(copilotFile).toBeDefined();
147
+ expect(copilotFile?.path).toContain("copilot-instructions.md");
148
+ });
149
+
150
+ it("should set distance to 0 for single file rules", () => {
151
+ // #given copilot-instructions.md at project root
152
+ const githubDir = join(TEST_DIR, ".github");
153
+ mkdirSync(githubDir, { recursive: true });
154
+ writeFileSync(
155
+ join(githubDir, "copilot-instructions.md"),
156
+ "Instructions"
157
+ );
158
+
159
+ const srcDir = join(TEST_DIR, "src", "deep", "nested");
160
+ mkdirSync(srcDir, { recursive: true });
161
+ const currentFile = join(srcDir, "file.ts");
162
+ writeFileSync(currentFile, "code");
163
+
164
+ // #when finding rules from deeply nested file
165
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
166
+
167
+ // #then single file should have distance 0
168
+ const copilotFile = candidates.find((c) => c.isSingleFile);
169
+ expect(copilotFile?.distance).toBe(0);
170
+ });
171
+ });
172
+
173
+ describe("backward compatibility", () => {
174
+ it("should still discover .claude/rules/ files", () => {
175
+ // #given .claude/rules/ directory
176
+ const rulesDir = join(TEST_DIR, ".claude", "rules");
177
+ mkdirSync(rulesDir, { recursive: true });
178
+ writeFileSync(join(rulesDir, "typescript.md"), "TS rules");
179
+
180
+ const currentFile = join(TEST_DIR, "index.ts");
181
+ writeFileSync(currentFile, "code");
182
+
183
+ // #when finding rules
184
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
185
+
186
+ // #then should find claude rules
187
+ const paths = candidates.map((c) => c.path);
188
+ expect(paths.some((p) => p.includes(".claude/rules/"))).toBe(true);
189
+ });
190
+
191
+ it("should still discover .cursor/rules/ files", () => {
192
+ // #given .cursor/rules/ directory
193
+ const rulesDir = join(TEST_DIR, ".cursor", "rules");
194
+ mkdirSync(rulesDir, { recursive: true });
195
+ writeFileSync(join(rulesDir, "python.md"), "PY rules");
196
+
197
+ const currentFile = join(TEST_DIR, "main.py");
198
+ writeFileSync(currentFile, "code");
199
+
200
+ // #when finding rules
201
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
202
+
203
+ // #then should find cursor rules
204
+ const paths = candidates.map((c) => c.path);
205
+ expect(paths.some((p) => p.includes(".cursor/rules/"))).toBe(true);
206
+ });
207
+
208
+ it("should discover .mdc files in rule directories", () => {
209
+ // #given .mdc file in .claude/rules/
210
+ const rulesDir = join(TEST_DIR, ".claude", "rules");
211
+ mkdirSync(rulesDir, { recursive: true });
212
+ writeFileSync(join(rulesDir, "advanced.mdc"), "MDC rules");
213
+
214
+ const currentFile = join(TEST_DIR, "app.ts");
215
+ writeFileSync(currentFile, "code");
216
+
217
+ // #when finding rules
218
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
219
+
220
+ // #then should find .mdc file
221
+ const paths = candidates.map((c) => c.path);
222
+ expect(paths.some((p) => p.endsWith("advanced.mdc"))).toBe(true);
223
+ });
224
+ });
225
+
226
+ describe("mixed sources", () => {
227
+ it("should discover rules from all sources", () => {
228
+ // #given rules in multiple directories
229
+ const claudeRules = join(TEST_DIR, ".claude", "rules");
230
+ const cursorRules = join(TEST_DIR, ".cursor", "rules");
231
+ const githubInstructions = join(TEST_DIR, ".github", "instructions");
232
+ const githubDir = join(TEST_DIR, ".github");
233
+
234
+ mkdirSync(claudeRules, { recursive: true });
235
+ mkdirSync(cursorRules, { recursive: true });
236
+ mkdirSync(githubInstructions, { recursive: true });
237
+
238
+ writeFileSync(join(claudeRules, "claude.md"), "claude");
239
+ writeFileSync(join(cursorRules, "cursor.md"), "cursor");
240
+ writeFileSync(
241
+ join(githubInstructions, "copilot.instructions.md"),
242
+ "copilot"
243
+ );
244
+ writeFileSync(join(githubDir, "copilot-instructions.md"), "global");
245
+
246
+ const currentFile = join(TEST_DIR, "index.ts");
247
+ writeFileSync(currentFile, "code");
248
+
249
+ // #when finding rules
250
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
251
+
252
+ // #then should find all rules
253
+ expect(candidates.length).toBeGreaterThanOrEqual(4);
254
+ const paths = candidates.map((c) => c.path);
255
+ expect(paths.some((p) => p.includes(".claude/rules/"))).toBe(true);
256
+ expect(paths.some((p) => p.includes(".cursor/rules/"))).toBe(true);
257
+ expect(paths.some((p) => p.includes(".github/instructions/"))).toBe(
258
+ true
259
+ );
260
+ expect(paths.some((p) => p.includes("copilot-instructions.md"))).toBe(
261
+ true
262
+ );
263
+ });
264
+
265
+ it("should not duplicate single file rules", () => {
266
+ // #given copilot-instructions.md
267
+ const githubDir = join(TEST_DIR, ".github");
268
+ mkdirSync(githubDir, { recursive: true });
269
+ writeFileSync(
270
+ join(githubDir, "copilot-instructions.md"),
271
+ "Instructions"
272
+ );
273
+
274
+ const currentFile = join(TEST_DIR, "file.ts");
275
+ writeFileSync(currentFile, "code");
276
+
277
+ // #when finding rules
278
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
279
+
280
+ // #then should only have one copilot-instructions.md entry
281
+ const copilotFiles = candidates.filter((c) =>
282
+ c.path.includes("copilot-instructions.md")
283
+ );
284
+ expect(copilotFiles.length).toBe(1);
285
+ });
286
+ });
287
+
288
+ describe("user-level rules", () => {
289
+ it("should discover user-level .claude/rules/ files", () => {
290
+ // #given user-level rules
291
+ const userRulesDir = join(homeDir, ".claude", "rules");
292
+ mkdirSync(userRulesDir, { recursive: true });
293
+ writeFileSync(join(userRulesDir, "global.md"), "Global user rules");
294
+
295
+ const currentFile = join(TEST_DIR, "app.ts");
296
+ writeFileSync(currentFile, "code");
297
+
298
+ // #when finding rules
299
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
300
+
301
+ // #then should find user-level rules
302
+ const userRule = candidates.find((c) => c.isGlobal);
303
+ expect(userRule).toBeDefined();
304
+ expect(userRule?.path).toContain("global.md");
305
+ });
306
+
307
+ it("should mark user-level rules as isGlobal: true", () => {
308
+ // #given user-level rules
309
+ const userRulesDir = join(homeDir, ".claude", "rules");
310
+ mkdirSync(userRulesDir, { recursive: true });
311
+ writeFileSync(join(userRulesDir, "user.md"), "User rules");
312
+
313
+ const currentFile = join(TEST_DIR, "app.ts");
314
+ writeFileSync(currentFile, "code");
315
+
316
+ // #when finding rules
317
+ const candidates = findRuleFiles(TEST_DIR, homeDir, currentFile);
318
+
319
+ // #then isGlobal should be true
320
+ const userRule = candidates.find((c) => c.path.includes("user.md"));
321
+ expect(userRule?.isGlobal).toBe(true);
322
+ expect(userRule?.distance).toBe(9999);
323
+ });
324
+ });
325
+ });
326
+
327
+ describe("findProjectRoot", () => {
328
+ const TEST_DIR = join(tmpdir(), `project-root-test-${Date.now()}`);
329
+
330
+ beforeEach(() => {
331
+ mkdirSync(TEST_DIR, { recursive: true });
332
+ });
333
+
334
+ afterEach(() => {
335
+ if (existsSync(TEST_DIR)) {
336
+ rmSync(TEST_DIR, { recursive: true, force: true });
337
+ }
338
+ });
339
+
340
+ it("should find project root with .git directory", () => {
341
+ // #given directory with .git
342
+ mkdirSync(join(TEST_DIR, ".git"), { recursive: true });
343
+ const nestedFile = join(TEST_DIR, "src", "components", "Button.tsx");
344
+ mkdirSync(join(TEST_DIR, "src", "components"), { recursive: true });
345
+ writeFileSync(nestedFile, "code");
346
+
347
+ // #when finding project root from nested file
348
+ const root = findProjectRoot(nestedFile);
349
+
350
+ // #then should return the directory with .git
351
+ expect(root).toBe(TEST_DIR);
352
+ });
353
+
354
+ it("should find project root with package.json", () => {
355
+ // #given directory with package.json
356
+ writeFileSync(join(TEST_DIR, "package.json"), "{}");
357
+ const nestedFile = join(TEST_DIR, "lib", "index.js");
358
+ mkdirSync(join(TEST_DIR, "lib"), { recursive: true });
359
+ writeFileSync(nestedFile, "code");
360
+
361
+ // #when finding project root
362
+ const root = findProjectRoot(nestedFile);
363
+
364
+ // #then should find the package.json directory
365
+ expect(root).toBe(TEST_DIR);
366
+ });
367
+
368
+ it("should return null when no project markers found", () => {
369
+ // #given directory without any project markers
370
+ const isolatedDir = join(TEST_DIR, "isolated");
371
+ mkdirSync(isolatedDir, { recursive: true });
372
+ const file = join(isolatedDir, "file.txt");
373
+ writeFileSync(file, "content");
374
+
375
+ // #when finding project root
376
+ const root = findProjectRoot(file);
377
+
378
+ // #then should return null
379
+ expect(root).toBeNull();
380
+ });
381
+ });
@@ -0,0 +1,263 @@
1
+ import {
2
+ existsSync,
3
+ readdirSync,
4
+ realpathSync,
5
+ statSync,
6
+ } from "node:fs";
7
+ import { dirname, join, relative } from "node:path";
8
+ import {
9
+ GITHUB_INSTRUCTIONS_PATTERN,
10
+ PROJECT_MARKERS,
11
+ PROJECT_RULE_FILES,
12
+ PROJECT_RULE_SUBDIRS,
13
+ RULE_EXTENSIONS,
14
+ USER_RULE_DIR,
15
+ } from "./constants";
16
+ import type { RuleFileCandidate } from "./types";
17
+
18
+ function isGitHubInstructionsDir(dir: string): boolean {
19
+ return dir.includes(".github/instructions") || dir.endsWith(".github/instructions");
20
+ }
21
+
22
+ function isValidRuleFile(fileName: string, dir: string): boolean {
23
+ if (isGitHubInstructionsDir(dir)) {
24
+ return GITHUB_INSTRUCTIONS_PATTERN.test(fileName);
25
+ }
26
+ return RULE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
27
+ }
28
+
29
+ /**
30
+ * Find project root by walking up from startPath.
31
+ * Checks for PROJECT_MARKERS (.git, pyproject.toml, package.json, etc.)
32
+ *
33
+ * @param startPath - Starting path to search from (file or directory)
34
+ * @returns Project root path or null if not found
35
+ */
36
+ export function findProjectRoot(startPath: string): string | null {
37
+ let current: string;
38
+
39
+ try {
40
+ const stat = statSync(startPath);
41
+ current = stat.isDirectory() ? startPath : dirname(startPath);
42
+ } catch {
43
+ current = dirname(startPath);
44
+ }
45
+
46
+ while (true) {
47
+ for (const marker of PROJECT_MARKERS) {
48
+ const markerPath = join(current, marker);
49
+ if (existsSync(markerPath)) {
50
+ return current;
51
+ }
52
+ }
53
+
54
+ const parent = dirname(current);
55
+ if (parent === current) {
56
+ return null;
57
+ }
58
+ current = parent;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Recursively find all rule files (*.md, *.mdc) in a directory
64
+ *
65
+ * @param dir - Directory to search
66
+ * @param results - Array to accumulate results
67
+ */
68
+ function findRuleFilesRecursive(dir: string, results: string[]): void {
69
+ if (!existsSync(dir)) return;
70
+
71
+ try {
72
+ const entries = readdirSync(dir, { withFileTypes: true });
73
+ for (const entry of entries) {
74
+ const fullPath = join(dir, entry.name);
75
+
76
+ if (entry.isDirectory()) {
77
+ findRuleFilesRecursive(fullPath, results);
78
+ } else if (entry.isFile()) {
79
+ if (isValidRuleFile(entry.name, dir)) {
80
+ results.push(fullPath);
81
+ }
82
+ }
83
+ }
84
+ } catch {
85
+ // Permission denied or other errors - silently skip
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Resolve symlinks safely with fallback to original path
91
+ *
92
+ * @param filePath - Path to resolve
93
+ * @returns Real path or original path if resolution fails
94
+ */
95
+ function safeRealpathSync(filePath: string): string {
96
+ try {
97
+ return realpathSync(filePath);
98
+ } catch {
99
+ return filePath;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Calculate directory distance between a rule file and current file.
105
+ * Distance is based on common ancestor within project root.
106
+ *
107
+ * @param rulePath - Path to the rule file
108
+ * @param currentFile - Path to the current file being edited
109
+ * @param projectRoot - Project root for relative path calculation
110
+ * @returns Distance (0 = same directory, higher = further)
111
+ */
112
+ export function calculateDistance(
113
+ rulePath: string,
114
+ currentFile: string,
115
+ projectRoot: string | null,
116
+ ): number {
117
+ if (!projectRoot) {
118
+ return 9999;
119
+ }
120
+
121
+ try {
122
+ const ruleDir = dirname(rulePath);
123
+ const currentDir = dirname(currentFile);
124
+
125
+ const ruleRel = relative(projectRoot, ruleDir);
126
+ const currentRel = relative(projectRoot, currentDir);
127
+
128
+ // Handle paths outside project root
129
+ if (ruleRel.startsWith("..") || currentRel.startsWith("..")) {
130
+ return 9999;
131
+ }
132
+
133
+ // Split by both forward and back slashes for cross-platform compatibility
134
+ // path.relative() returns OS-native separators (backslashes on Windows)
135
+ const ruleParts = ruleRel ? ruleRel.split(/[/\\]/) : [];
136
+ const currentParts = currentRel ? currentRel.split(/[/\\]/) : [];
137
+
138
+ // Find common prefix length
139
+ let common = 0;
140
+ for (let i = 0; i < Math.min(ruleParts.length, currentParts.length); i++) {
141
+ if (ruleParts[i] === currentParts[i]) {
142
+ common++;
143
+ } else {
144
+ break;
145
+ }
146
+ }
147
+
148
+ // Distance is how many directories up from current file to common ancestor
149
+ return currentParts.length - common;
150
+ } catch {
151
+ return 9999;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Find all rule files for a given context.
157
+ * Searches from currentFile upward to projectRoot for rule directories,
158
+ * then user-level directory (~/.claude/rules).
159
+ *
160
+ * IMPORTANT: This searches EVERY directory from file to project root.
161
+ * Not just the project root itself.
162
+ *
163
+ * @param projectRoot - Project root path (or null if outside any project)
164
+ * @param homeDir - User home directory
165
+ * @param currentFile - Current file being edited (for distance calculation)
166
+ * @returns Array of rule file candidates sorted by distance
167
+ */
168
+ export function findRuleFiles(
169
+ projectRoot: string | null,
170
+ homeDir: string,
171
+ currentFile: string,
172
+ ): RuleFileCandidate[] {
173
+ const candidates: RuleFileCandidate[] = [];
174
+ const seenRealPaths = new Set<string>();
175
+
176
+ // Search from current file's directory up to project root
177
+ let currentDir = dirname(currentFile);
178
+ let distance = 0;
179
+
180
+ while (true) {
181
+ // Search rule directories in current directory
182
+ for (const [parent, subdir] of PROJECT_RULE_SUBDIRS) {
183
+ const ruleDir = join(currentDir, parent, subdir);
184
+ const files: string[] = [];
185
+ findRuleFilesRecursive(ruleDir, files);
186
+
187
+ for (const filePath of files) {
188
+ const realPath = safeRealpathSync(filePath);
189
+ if (seenRealPaths.has(realPath)) continue;
190
+ seenRealPaths.add(realPath);
191
+
192
+ candidates.push({
193
+ path: filePath,
194
+ realPath,
195
+ isGlobal: false,
196
+ distance,
197
+ });
198
+ }
199
+ }
200
+
201
+ // Stop at project root or filesystem root
202
+ if (projectRoot && currentDir === projectRoot) break;
203
+ const parentDir = dirname(currentDir);
204
+ if (parentDir === currentDir) break;
205
+ currentDir = parentDir;
206
+ distance++;
207
+ }
208
+
209
+ // Check for single-file rules at project root (e.g., .github/copilot-instructions.md)
210
+ if (projectRoot) {
211
+ for (const ruleFile of PROJECT_RULE_FILES) {
212
+ const filePath = join(projectRoot, ruleFile);
213
+ if (existsSync(filePath)) {
214
+ try {
215
+ const stat = statSync(filePath);
216
+ if (stat.isFile()) {
217
+ const realPath = safeRealpathSync(filePath);
218
+ if (!seenRealPaths.has(realPath)) {
219
+ seenRealPaths.add(realPath);
220
+ candidates.push({
221
+ path: filePath,
222
+ realPath,
223
+ isGlobal: false,
224
+ distance: 0,
225
+ isSingleFile: true,
226
+ });
227
+ }
228
+ }
229
+ } catch {
230
+ // Skip if file can't be read
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ // Search user-level rule directory (~/.claude/rules)
237
+ const userRuleDir = join(homeDir, USER_RULE_DIR);
238
+ const userFiles: string[] = [];
239
+ findRuleFilesRecursive(userRuleDir, userFiles);
240
+
241
+ for (const filePath of userFiles) {
242
+ const realPath = safeRealpathSync(filePath);
243
+ if (seenRealPaths.has(realPath)) continue;
244
+ seenRealPaths.add(realPath);
245
+
246
+ candidates.push({
247
+ path: filePath,
248
+ realPath,
249
+ isGlobal: true,
250
+ distance: 9999, // Global rules always have max distance
251
+ });
252
+ }
253
+
254
+ // Sort by distance (closest first, then global rules last)
255
+ candidates.sort((a, b) => {
256
+ if (a.isGlobal !== b.isGlobal) {
257
+ return a.isGlobal ? 1 : -1;
258
+ }
259
+ return a.distance - b.distance;
260
+ });
261
+
262
+ return candidates;
263
+ }