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
package/index.ts CHANGED
@@ -1,22 +1,54 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
2
  import { tool } from "@opencode-ai/plugin"
3
3
  import { $ } from "bun"
4
- import { parseRepoSpec, buildGitUrl, cloneRepo, updateRepo, getRepoInfo } from "./src/git"
4
+ import { parseRepoSpec, buildGitUrl, cloneRepo, updateRepo, switchBranch, getRepoInfo } from "./src/git"
5
5
  import { createRepoExplorerAgent } from "./src/agents/repo-explorer"
6
6
  import {
7
7
  loadManifest,
8
8
  saveManifest,
9
9
  withManifestLock,
10
+ setCacheDir,
10
11
  type RepoEntry,
11
12
  } from "./src/manifest"
12
13
  import { scanLocalRepos, matchRemoteToSpec, findLocalRepoByName } from "./src/scanner"
13
- import { homedir } from "node:os"
14
- import { join } from "node:path"
14
+ import { homedir, tmpdir } from "node:os"
15
+ import { dirname, isAbsolute, join } from "node:path"
15
16
  import { existsSync } from "node:fs"
16
- import { rm, readFile } from "node:fs/promises"
17
+ import { appendFile, mkdir, rm, readFile } from "node:fs/promises"
17
18
 
18
19
  interface Config {
19
- localSearchPaths: string[]
20
+ localSearchPaths?: string[]
21
+ cleanupMaxAgeDays?: number
22
+ cacheDir?: string
23
+ useHttps?: boolean
24
+ autoSyncOnExplore?: boolean
25
+ autoSyncIntervalHours?: number
26
+ defaultBranch?: string
27
+ includeProjectParent?: boolean
28
+ debug?: boolean
29
+ repoExplorerModel?: string
30
+ debugLogPath?: string
31
+ }
32
+
33
+ const DEFAULTS = {
34
+ cleanupMaxAgeDays: 30,
35
+ cacheDir: join(tmpdir(), "opencode-repos"),
36
+ useHttps: false,
37
+ autoSyncOnExplore: true,
38
+ autoSyncIntervalHours: 24,
39
+ defaultBranch: "main",
40
+ includeProjectParent: true,
41
+ debug: false,
42
+ repoExplorerModel: "opencode/grok-code",
43
+ debugLogPath: join(homedir(), ".cache", "opencode-repos", "debug.log"),
44
+ } as const
45
+
46
+ function parseModelString(modelString: string): { providerID: string; modelID: string } | undefined {
47
+ const parts = modelString.split("/")
48
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
49
+ return undefined
50
+ }
51
+ return { providerID: parts[0], modelID: parts[1] }
20
52
  }
21
53
 
22
54
  async function loadConfig(): Promise<Config | null> {
@@ -34,12 +66,863 @@ async function loadConfig(): Promise<Config | null> {
34
66
  }
35
67
  }
36
68
 
37
- const CACHE_DIR = join(homedir(), ".cache", "opencode-repos")
38
69
 
39
- export const OpencodeRepos: Plugin = async ({ client }) => {
70
+
71
+ async function runCleanup(maxAgeDays: number): Promise<void> {
72
+ const cutoffMs = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000)
73
+
74
+ try {
75
+ const manifest = await loadManifest()
76
+ const staleKeys: string[] = []
77
+
78
+ for (const [repoKey, entry] of Object.entries(manifest.repos)) {
79
+ if (entry.type !== "cached") continue
80
+
81
+ const lastAccessedMs = new Date(entry.lastAccessed).getTime()
82
+ if (lastAccessedMs < cutoffMs) {
83
+ staleKeys.push(repoKey)
84
+ }
85
+ }
86
+
87
+ if (staleKeys.length === 0) return
88
+
89
+ await withManifestLock(async () => {
90
+ const updatedManifest = await loadManifest()
91
+
92
+ for (const key of staleKeys) {
93
+ const entry = updatedManifest.repos[key]
94
+ if (!entry || entry.type !== "cached") continue
95
+
96
+ try {
97
+ await rm(entry.path, { recursive: true, force: true })
98
+ delete updatedManifest.repos[key]
99
+ } catch {}
100
+ }
101
+
102
+ await saveManifest(updatedManifest)
103
+ })
104
+ } catch {}
105
+ }
106
+
107
+ function resolveSearchPaths(
108
+ configuredPaths: string[],
109
+ includeProjectParent: boolean,
110
+ projectDirectory?: string
111
+ ): string[] {
112
+ const resolved = [...configuredPaths]
113
+
114
+ if (includeProjectParent && projectDirectory) {
115
+ const parentDir = dirname(projectDirectory)
116
+ if (!resolved.includes(parentDir)) {
117
+ resolved.push(parentDir)
118
+ }
119
+ }
120
+
121
+ return resolved
122
+ }
123
+
124
+ function shouldSyncRepo(lastUpdated: string | undefined, intervalHours: number): boolean {
125
+ if (!lastUpdated) return true
126
+
127
+ const lastUpdatedMs = new Date(lastUpdated).getTime()
128
+ if (Number.isNaN(lastUpdatedMs)) return true
129
+
130
+ const intervalMs = intervalHours * 60 * 60 * 1000
131
+ return Date.now() - lastUpdatedMs >= intervalMs
132
+ }
133
+
134
+ function expandHomePath(input: string): string {
135
+ if (input.startsWith("~/")) {
136
+ return join(homedir(), input.slice(2))
137
+ }
138
+
139
+ return input
140
+ }
141
+
142
+ async function resolveLocalPathQuery(
143
+ query: string,
144
+ defaultBranch: string
145
+ ): Promise<{ candidate: RepoCandidate | null; error?: string }> {
146
+ const expanded = expandHomePath(query.trim())
147
+
148
+ if (!isAbsolute(expanded)) {
149
+ return { candidate: null }
150
+ }
151
+
152
+ if (!existsSync(expanded)) {
153
+ return { candidate: null, error: `Path does not exist: ${expanded}` }
154
+ }
155
+
156
+ if (!existsSync(join(expanded, ".git"))) {
157
+ return { candidate: null, error: `No .git directory found at: ${expanded}` }
158
+ }
159
+
160
+ try {
161
+ const [remote, branch] = await Promise.all([
162
+ $`git -C ${expanded} remote get-url origin`.text(),
163
+ $`git -C ${expanded} branch --show-current`.text(),
164
+ ])
165
+
166
+ if (!remote.trim()) {
167
+ return {
168
+ candidate: null,
169
+ error: "Local repository has no origin remote. Add one to use repo_query.",
170
+ }
171
+ }
172
+
173
+ const spec = matchRemoteToSpec(remote.trim())
174
+ if (!spec) {
175
+ return {
176
+ candidate: null,
177
+ error: "Origin remote is not a supported GitHub URL.",
178
+ }
179
+ }
180
+
181
+ return {
182
+ candidate: {
183
+ key: spec,
184
+ source: "local",
185
+ path: expanded,
186
+ branch: branch.trim() || defaultBranch,
187
+ remote: remote.trim(),
188
+ },
189
+ }
190
+ } catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error)
192
+ return {
193
+ candidate: null,
194
+ error: `Failed to read local repository metadata: ${message}`,
195
+ }
196
+ }
197
+ }
198
+
199
+ async function registerLocalRepo(
200
+ repoKey: string,
201
+ path: string,
202
+ branch: string,
203
+ remote: string
204
+ ): Promise<void> {
205
+ await withManifestLock(async () => {
206
+ const manifest = await loadManifest()
207
+ if (manifest.repos[repoKey]) return
208
+
209
+ const now = new Date().toISOString()
210
+ manifest.repos[repoKey] = {
211
+ type: "local",
212
+ path,
213
+ lastAccessed: now,
214
+ currentBranch: branch,
215
+ shallow: false,
216
+ }
217
+
218
+ manifest.localIndex[remote] = path
219
+ await saveManifest(manifest)
220
+ })
221
+ }
222
+
223
+ async function resolveCandidates(
224
+ query: string,
225
+ searchPaths: string[],
226
+ manifest: Awaited<ReturnType<typeof loadManifest>>,
227
+ defaultBranch: string,
228
+ allowGithub: boolean
229
+ ): Promise<{
230
+ candidates: RepoCandidate[]
231
+ exactRepoKey: string | null
232
+ branchOverride: string | null
233
+ }> {
234
+ const trimmed = query.trim()
235
+ let exactRepoKey: string | null = null
236
+ let branchOverride: string | null = null
237
+
238
+ if (trimmed.includes("/")) {
239
+ try {
240
+ const spec = parseRepoSpec(trimmed)
241
+ exactRepoKey = `${spec.owner}/${spec.repo}`
242
+ branchOverride = spec.branch ?? null
243
+ } catch {
244
+ exactRepoKey = null
245
+ branchOverride = null
246
+ }
247
+ }
248
+
249
+ const queryLower = trimmed.toLowerCase()
250
+ const candidates: RepoCandidate[] = []
251
+
252
+ for (const [repoKey, entry] of Object.entries(manifest.repos)) {
253
+ if (exactRepoKey) {
254
+ if (repoKey.toLowerCase() !== exactRepoKey.toLowerCase()) continue
255
+ candidates.push({
256
+ key: repoKey,
257
+ source: "registered",
258
+ path: entry.path,
259
+ branch: entry.currentBranch,
260
+ })
261
+ continue
262
+ }
263
+
264
+ if (repoKey.toLowerCase().includes(queryLower)) {
265
+ candidates.push({
266
+ key: repoKey,
267
+ source: "registered",
268
+ path: entry.path,
269
+ branch: entry.currentBranch,
270
+ })
271
+ }
272
+ }
273
+
274
+ if (searchPaths.length > 0) {
275
+ try {
276
+ const localResults = await findLocalRepoByName(searchPaths, trimmed)
277
+ for (const local of localResults) {
278
+ if (exactRepoKey && local.spec.toLowerCase() !== exactRepoKey.toLowerCase()) {
279
+ continue
280
+ }
281
+
282
+ candidates.push({
283
+ key: local.spec,
284
+ source: "local",
285
+ path: local.path,
286
+ branch: local.branch || defaultBranch,
287
+ remote: local.remote,
288
+ })
289
+ }
290
+ } catch {}
291
+ }
292
+
293
+ if (allowGithub) {
294
+ try {
295
+ if (exactRepoKey) {
296
+ const repoCheck =
297
+ await $`gh repo view ${exactRepoKey} --json nameWithOwner,description,url 2>/dev/null`.text()
298
+ const repo = JSON.parse(repoCheck)
299
+ candidates.push({
300
+ key: repo.nameWithOwner,
301
+ source: "github",
302
+ description: repo.description || "",
303
+ url: repo.url,
304
+ branch: branchOverride ?? defaultBranch,
305
+ })
306
+ } else {
307
+ const searchResult =
308
+ await $`gh search repos ${trimmed} --limit 5 --json fullName,description,url 2>/dev/null`.text()
309
+ const repos = JSON.parse(searchResult)
310
+ for (const repo of repos) {
311
+ candidates.push({
312
+ key: repo.fullName,
313
+ source: "github",
314
+ description: repo.description || "",
315
+ url: repo.url,
316
+ branch: defaultBranch,
317
+ })
318
+ }
319
+ }
320
+ } catch {}
321
+ }
322
+
323
+ return {
324
+ candidates: uniqueCandidates(candidates),
325
+ exactRepoKey,
326
+ branchOverride,
327
+ }
328
+ }
329
+
330
+ async function ensureRepoAvailable(
331
+ repoKey: string,
332
+ branch: string,
333
+ cacheDir: string,
334
+ useHttps: boolean,
335
+ autoSyncOnExplore: boolean,
336
+ autoSyncIntervalHours: number
337
+ ): Promise<{
338
+ repoPath: string
339
+ branch: string
340
+ type: "cached" | "local"
341
+ }> {
342
+ let manifest = await loadManifest()
343
+ const entry = manifest.repos[repoKey]
344
+
345
+ if (!entry) {
346
+ const [owner, repo] = repoKey.split("/")
347
+ const repoPath = join(cacheDir, owner, repo)
348
+ const url = buildGitUrl(owner, repo, useHttps)
349
+
350
+ try {
351
+ await withManifestLock(async () => {
352
+ await cloneRepo(url, repoPath, { branch })
353
+
354
+ const now = new Date().toISOString()
355
+ const updatedManifest = await loadManifest()
356
+ updatedManifest.repos[repoKey] = {
357
+ type: "cached",
358
+ path: repoPath,
359
+ clonedAt: now,
360
+ lastAccessed: now,
361
+ lastUpdated: now,
362
+ currentBranch: branch,
363
+ shallow: true,
364
+ }
365
+ await saveManifest(updatedManifest)
366
+ })
367
+ } catch (error) {
368
+ const message = error instanceof Error ? error.message : String(error)
369
+ throw new Error(`Clone failed for ${repoKey}@${branch}: ${message}`)
370
+ }
371
+
372
+ return {
373
+ repoPath,
374
+ branch,
375
+ type: "cached",
376
+ }
377
+ }
378
+
379
+ const repoPath = entry.path
380
+
381
+ if (entry.type === "cached") {
382
+ try {
383
+ if (entry.currentBranch !== branch) {
384
+ await switchBranch(repoPath, branch)
385
+ await withManifestLock(async () => {
386
+ const updatedManifest = await loadManifest()
387
+ if (updatedManifest.repos[repoKey]) {
388
+ updatedManifest.repos[repoKey].currentBranch = branch
389
+ updatedManifest.repos[repoKey].lastUpdated = new Date().toISOString()
390
+ await saveManifest(updatedManifest)
391
+ }
392
+ })
393
+ } else if (autoSyncOnExplore && shouldSyncRepo(entry.lastUpdated, autoSyncIntervalHours)) {
394
+ await updateRepo(repoPath)
395
+ await withManifestLock(async () => {
396
+ const updatedManifest = await loadManifest()
397
+ if (updatedManifest.repos[repoKey]) {
398
+ updatedManifest.repos[repoKey].lastUpdated = new Date().toISOString()
399
+ await saveManifest(updatedManifest)
400
+ }
401
+ })
402
+ }
403
+ } catch (error) {
404
+ const message = error instanceof Error ? error.message : String(error)
405
+ throw new Error(`Update failed for ${repoKey}@${branch}: ${message}`)
406
+ }
407
+ }
408
+
409
+ return {
410
+ repoPath,
411
+ branch,
412
+ type: entry.type,
413
+ }
414
+ }
415
+
416
+ async function runRepoExplorer(
417
+ client: RepoClient,
418
+ ctx: RepoToolContext,
419
+ repoKey: string,
420
+ repoPath: string,
421
+ question: string,
422
+ model?: string,
423
+ parentDirectory?: string
424
+ ): Promise<string> {
425
+ let sessionID: string | undefined
426
+
427
+ // Validate that repo-explorer agent is available
428
+ try {
429
+ const agentsResult = await client.app.agents()
430
+ const agents = agentsResult.data ?? []
431
+ const agentNames = agents.map((a) => a.name)
432
+
433
+ if (debugLogger) {
434
+ debugLogger("repo_explore", [
435
+ `repoKey: ${repoKey}`,
436
+ `availableAgents: ${agentNames.join(", ") || "none"}`,
437
+ `repoExplorerExists: ${agentNames.includes("repo-explorer")}`,
438
+ ])
439
+ }
440
+
441
+ if (!agentNames.includes("repo-explorer")) {
442
+ return `## Exploration failed
443
+
444
+ Repository: ${repoKey}
445
+ Path: ${repoPath}
446
+
447
+ The repo-explorer agent is not available. Available agents: ${agentNames.join(", ") || "none"}
448
+
449
+ This may indicate the opencode-repos plugin is not properly loaded.`
450
+ }
451
+ } catch (error) {
452
+ if (debugLogger) {
453
+ const message = error instanceof Error ? error.message : String(error)
454
+ debugLogger("repo_explore", [
455
+ `repoKey: ${repoKey}`,
456
+ `agentValidationError: ${message}`,
457
+ ])
458
+ }
459
+ // Continue anyway - the session.prompt will fail with a clearer error if agent doesn't exist
460
+ }
461
+
462
+ // Use parent directory for session creation (like oh-my-opencode does)
463
+ // The repoPath will be passed in the prompt context instead
464
+ const sessionDirectory = parentDirectory ?? repoPath
465
+
466
+ try {
467
+ const createResult = await client.session.create({
468
+ body: {
469
+ parentID: ctx.sessionID,
470
+ title: `Repo explorer: ${repoKey}`,
471
+ },
472
+ query: {
473
+ directory: sessionDirectory,
474
+ },
475
+ })
476
+
477
+ sessionID = createResult.data?.id
478
+ } catch (error) {
479
+ const message = error instanceof Error ? error.message : String(error)
480
+ return `## Exploration failed
481
+
482
+ Repository: ${repoKey}
483
+ Path: ${repoPath}
484
+
485
+ Failed to create a subagent session: ${message}`
486
+ }
487
+
488
+ if (!sessionID) {
489
+ return `## Exploration failed
490
+
491
+ Repository: ${repoKey}
492
+ Path: ${repoPath}
493
+
494
+ Failed to create a subagent session (no session ID returned).`
495
+ }
496
+
497
+ const explorationPrompt = `Index the codebase and answer the following question:
498
+
499
+ ${question}
500
+
501
+ Working directory: ${repoPath}
502
+
503
+ You have access to all standard code exploration tools:
504
+ - read: Read files
505
+ - glob: Find files by pattern
506
+ - grep: Search for patterns
507
+ - bash: Run git commands if needed
508
+
509
+ Remember to:
510
+ - Start with high-level structure (README, package.json, main files)
511
+ - Cite specific files and line numbers
512
+ - Include relevant code snippets
513
+ - Explain how components interact
514
+ `
515
+
516
+ await Bun.sleep(150)
517
+
518
+ let promptAttempt = 0
519
+ const promptMaxAttempts = 3
520
+
521
+ const parsedModel = model ? parseModelString(model) : undefined
522
+
523
+ while (promptAttempt < promptMaxAttempts) {
524
+ try {
525
+ if (debugLogger) {
526
+ debugLogger("repo_explore", [
527
+ `repoKey: ${repoKey}`,
528
+ `sessionID: ${sessionID}`,
529
+ `promptSending: attempt ${promptAttempt + 1}`,
530
+ `model: ${parsedModel ? `${parsedModel.providerID}/${parsedModel.modelID}` : "default"}`,
531
+ ])
532
+ }
533
+ await client.session.prompt({
534
+ path: { id: sessionID },
535
+ body: {
536
+ agent: "repo-explorer",
537
+ tools: {
538
+ task: false,
539
+ delegate_task: false,
540
+ },
541
+ parts: [{ type: "text", text: explorationPrompt }],
542
+ ...(parsedModel ? { model: parsedModel } : {}),
543
+ },
544
+ })
545
+ if (debugLogger) {
546
+ debugLogger("repo_explore", [
547
+ `repoKey: ${repoKey}`,
548
+ `sessionID: ${sessionID}`,
549
+ `promptSent: success`,
550
+ ])
551
+ }
552
+ break
553
+ } catch (error) {
554
+ promptAttempt += 1
555
+ const message = error instanceof Error ? error.message : String(error)
556
+ if (debugLogger) {
557
+ debugLogger("repo_explore", [
558
+ `repoKey: ${repoKey}`,
559
+ `sessionID: ${sessionID}`,
560
+ `promptError: ${message}`,
561
+ `promptAttempt: ${promptAttempt}`,
562
+ ])
563
+ }
564
+
565
+ if (promptAttempt >= promptMaxAttempts) {
566
+ return `## Exploration failed
567
+
568
+ Repository: ${repoKey}
569
+ Session: ${sessionID}
570
+ Path: ${repoPath}
571
+
572
+ Failed to run exploration agent: ${message}`
573
+ }
574
+
575
+ await Bun.sleep(300 * promptAttempt)
576
+ }
577
+ }
578
+
579
+ const pollStart = Date.now()
580
+ const maxPollMs = 5 * 60 * 1000
581
+ const pollIntervalMs = 500
582
+ const minStabilityTimeMs = 10000
583
+ const stabilityPollsRequired = 3
584
+ let lastMsgCount = 0
585
+ let stablePolls = 0
586
+ let pollCount = 0
587
+
588
+ if (debugLogger) {
589
+ debugLogger("repo_explore", [
590
+ `repoKey: ${repoKey}`,
591
+ `sessionID: ${sessionID}`,
592
+ `pollStart: starting poll loop`,
593
+ ])
594
+ }
595
+
596
+ while (Date.now() - pollStart < maxPollMs) {
597
+ await Bun.sleep(pollIntervalMs)
598
+ pollCount++
599
+
600
+ try {
601
+ const statusResult = await client.session.status()
602
+ const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
603
+ const sessionStatus = allStatuses[sessionID]
604
+
605
+ if (pollCount % 10 === 0 && debugLogger) {
606
+ debugLogger("repo_explore", [
607
+ `repoKey: ${repoKey}`,
608
+ `sessionID: ${sessionID}`,
609
+ `pollCount: ${pollCount}`,
610
+ `elapsed: ${Math.floor((Date.now() - pollStart) / 1000)}s`,
611
+ `sessionStatus: ${sessionStatus?.type ?? "not_in_status"}`,
612
+ `stablePolls: ${stablePolls}`,
613
+ `lastMsgCount: ${lastMsgCount}`,
614
+ ])
615
+ }
616
+
617
+ if (sessionStatus && sessionStatus.type !== "idle") {
618
+ stablePolls = 0
619
+ lastMsgCount = 0
620
+ continue
621
+ }
622
+
623
+ const elapsed = Date.now() - pollStart
624
+ if (elapsed < minStabilityTimeMs) {
625
+ continue
626
+ }
627
+
628
+ const messagesResult = await client.session.messages({ path: { id: sessionID } })
629
+ const messages = messagesResult.data ?? []
630
+ const currentMsgCount = messages.length
631
+
632
+ if (currentMsgCount === lastMsgCount) {
633
+ stablePolls += 1
634
+ if (stablePolls >= stabilityPollsRequired) {
635
+ if (debugLogger) {
636
+ debugLogger("repo_explore", [
637
+ `repoKey: ${repoKey}`,
638
+ `sessionID: ${sessionID}`,
639
+ `pollComplete: messages stable`,
640
+ `currentMsgCount: ${currentMsgCount}`,
641
+ ])
642
+ }
643
+ break
644
+ }
645
+ } else {
646
+ stablePolls = 0
647
+ lastMsgCount = currentMsgCount
648
+ }
649
+ } catch (error) {
650
+ const message = error instanceof Error ? error.message : String(error)
651
+ if (debugLogger) {
652
+ debugLogger("repo_explore", [
653
+ `repoKey: ${repoKey}`,
654
+ `sessionID: ${sessionID}`,
655
+ `pollError: ${message}`,
656
+ ])
657
+ }
658
+ }
659
+ }
660
+
661
+ if (Date.now() - pollStart >= maxPollMs) {
662
+ return `## Exploration failed
663
+
664
+ Repository: ${repoKey}
665
+ Session: ${sessionID}
666
+ Path: ${repoPath}
667
+
668
+ Timed out waiting for subagent output.`
669
+ }
670
+
671
+ const messagesResult = await client.session.messages({ path: { id: sessionID } })
672
+ const messages = messagesResult.data ?? []
673
+ const extracted: string[] = []
674
+
675
+ for (const message of messages) {
676
+ const parts = message.parts ?? []
677
+ for (const part of parts) {
678
+ if (
679
+ typeof part === "object" &&
680
+ part &&
681
+ "type" in part &&
682
+ (part as { type: string }).type === "text" &&
683
+ "text" in part
684
+ ) {
685
+ const textValue = (part as { text?: string }).text
686
+ if (textValue) extracted.push(textValue)
687
+ } else if (
688
+ typeof part === "object" &&
689
+ part &&
690
+ "type" in part &&
691
+ (part as { type: string }).type === "tool_result"
692
+ ) {
693
+ const toolResult = part as { content?: unknown }
694
+ if (typeof toolResult.content === "string") {
695
+ extracted.push(toolResult.content)
696
+ } else if (Array.isArray(toolResult.content)) {
697
+ for (const block of toolResult.content) {
698
+ if (block && typeof block === "object" && "text" in block) {
699
+ const blockText = (block as { text?: string }).text
700
+ if (blockText) extracted.push(blockText)
701
+ }
702
+ }
703
+ }
704
+ }
705
+ }
706
+ }
707
+
708
+ const responseText = extracted.filter(Boolean).join("\n\n")
709
+ return responseText || "No response from exploration agent."
710
+ }
711
+
712
+ interface RepoToolContext {
713
+ sessionID: string
714
+ }
715
+
716
+ interface PermissionContext {
717
+ ask: (input: {
718
+ permission: "external_directory"
719
+ patterns: string[]
720
+ always: string[]
721
+ metadata: Record<string, string>
722
+ }) => Promise<void>
723
+ }
724
+
725
+ interface RepoClient {
726
+ session: {
727
+ create: (input: {
728
+ body: { parentID?: string; title?: string }
729
+ query?: { directory?: string }
730
+ }) => Promise<{ data?: { id?: string } }>
731
+ get: (input: { path: { id: string } }) => Promise<{ data?: { id?: string; directory?: string } }>
732
+ status: () => Promise<{ data?: Record<string, { type: string }> }>
733
+ messages: (input: { path: { id: string } }) => Promise<{ data?: Array<{ parts?: unknown[] }> }>
734
+ prompt: (input: {
735
+ path: { id: string }
736
+ body: {
737
+ agent: string
738
+ parts: Array<{ type: "text"; text: string }>
739
+ tools?: Record<string, boolean>
740
+ model?: { providerID: string; modelID: string }
741
+ }
742
+ }) => Promise<{
743
+ error?: unknown
744
+ data?: { parts?: Array<{ type: string; text?: string }> }
745
+ }>
746
+ }
747
+ app: {
748
+ agents: () => Promise<{ data?: Array<{ name: string; mode?: string }> }>
749
+ }
750
+ }
751
+
752
+ type RepoSource = "registered" | "local" | "github"
753
+
754
+ interface RepoCandidate {
755
+ key: string
756
+ source: RepoSource
757
+ path?: string
758
+ branch?: string
759
+ description?: string
760
+ url?: string
761
+ remote?: string
762
+ }
763
+
764
+ interface RepoQueryDebugInfo {
765
+ query: string
766
+ allowGithub: boolean
767
+ localSearchPaths: string[]
768
+ candidates: RepoCandidate[]
769
+ selectedTargets: Array<{ repoKey: string; branch: string }>
770
+ }
771
+
772
+ function uniqueCandidates(candidates: RepoCandidate[]): RepoCandidate[] {
773
+ const map = new Map<string, RepoCandidate>()
774
+ for (const candidate of candidates) {
775
+ if (!map.has(candidate.key)) {
776
+ map.set(candidate.key, candidate)
777
+ }
778
+ }
779
+ return Array.from(map.values())
780
+ }
781
+
782
+ async function touchRepoAccess(repoKey: string): Promise<void> {
783
+ await withManifestLock(async () => {
784
+ const updatedManifest = await loadManifest()
785
+ if (updatedManifest.repos[repoKey]) {
786
+ updatedManifest.repos[repoKey].lastAccessed = new Date().toISOString()
787
+ await saveManifest(updatedManifest)
788
+ }
789
+ })
790
+ }
791
+
792
+ function formatRepoQueryDebug(info: RepoQueryDebugInfo): string {
793
+ let output = "## Debug\n\n"
794
+ output += `Query: ${info.query}\n`
795
+ output += `GitHub search enabled: ${info.allowGithub}\n`
796
+ output += `Local search paths: ${info.localSearchPaths.length}\n`
797
+ for (const path of info.localSearchPaths) {
798
+ output += `- ${path}\n`
799
+ }
800
+ output += "\nCandidates:\n"
801
+ if (info.candidates.length === 0) {
802
+ output += "- none\n"
803
+ } else {
804
+ for (const candidate of info.candidates) {
805
+ output += `- ${candidate.key} (${candidate.source})\n`
806
+ }
807
+ }
808
+ output += "\nSelected targets:\n"
809
+ if (info.selectedTargets.length === 0) {
810
+ output += "- none\n"
811
+ } else {
812
+ for (const target of info.selectedTargets) {
813
+ output += `- ${target.repoKey} @ ${target.branch}\n`
814
+ }
815
+ }
816
+ return output
817
+ }
818
+
819
+ type DebugLogger = (toolName: string, lines: string[]) => void
820
+
821
+ let debugLogger: DebugLogger | null = null
822
+
823
+ function appendDebug(
824
+ output: string,
825
+ toolName: string,
826
+ lines: string[],
827
+ enabled: boolean
828
+ ): string {
829
+ if (!enabled) return output
830
+
831
+ if (debugLogger) {
832
+ debugLogger(toolName, lines)
833
+ }
834
+
835
+ let section = `\n\n## Debug\n\nTool: ${toolName}\n`
836
+ for (const line of lines) {
837
+ section += `- ${line}\n`
838
+ }
839
+
840
+ return `${output}${section}`
841
+ }
842
+
843
+ function createDebugLogger(path: string, enabled: boolean): DebugLogger | null {
844
+ if (!enabled) return null
845
+
846
+ return (toolName, lines) => {
847
+ const timestamp = new Date().toISOString()
848
+ const payload = [
849
+ `[${timestamp}] ${toolName}`,
850
+ ...lines.map((line) => `- ${line}`),
851
+ "",
852
+ ].join("\n")
853
+
854
+ void appendFile(path, payload)
855
+ }
856
+ }
857
+
858
+ function logRepoQueryDebug(info: RepoQueryDebugInfo): void {
859
+ if (!debugLogger) return
860
+
861
+ const lines: string[] = [
862
+ `query: ${info.query}`,
863
+ `allowGithub: ${info.allowGithub}`,
864
+ `localSearchPaths: ${info.localSearchPaths.length}`,
865
+ `candidates: ${info.candidates.map((c) => c.key).join(", ") || "none"}`,
866
+ `selected: ${info.selectedTargets.map((t) => t.repoKey).join(", ") || "none"}`,
867
+ ]
868
+
869
+ debugLogger("repo_query", lines)
870
+ }
871
+
872
+ async function requestExternalDirectoryAccess(
873
+ ctx: PermissionContext,
874
+ targetPath: string
875
+ ): Promise<void> {
876
+ const parentDir = dirname(targetPath)
877
+ const glob = join(parentDir, "*")
878
+ await ctx.ask({
879
+ permission: "external_directory",
880
+ patterns: [glob],
881
+ always: [glob],
882
+ metadata: {
883
+ filepath: targetPath,
884
+ parentDir,
885
+ },
886
+ })
887
+ }
888
+
889
+ export const OpencodeRepos: Plugin = async ({ client, directory }) => {
890
+ const userConfig = await loadConfig()
891
+
892
+ const cacheDir = userConfig?.cacheDir ?? DEFAULTS.cacheDir
893
+ const useHttps = userConfig?.useHttps ?? DEFAULTS.useHttps
894
+ const autoSyncOnExplore = userConfig?.autoSyncOnExplore ?? DEFAULTS.autoSyncOnExplore
895
+ const autoSyncIntervalHours =
896
+ userConfig?.autoSyncIntervalHours ?? DEFAULTS.autoSyncIntervalHours
897
+ const defaultBranch = userConfig?.defaultBranch ?? DEFAULTS.defaultBranch
898
+ const cleanupMaxAgeDays = userConfig?.cleanupMaxAgeDays ?? DEFAULTS.cleanupMaxAgeDays
899
+ const includeProjectParent =
900
+ userConfig?.includeProjectParent ?? DEFAULTS.includeProjectParent
901
+ const debugEnabled = userConfig?.debug ?? DEFAULTS.debug
902
+ const repoExplorerModel =
903
+ userConfig?.repoExplorerModel ?? DEFAULTS.repoExplorerModel
904
+ const debugLogPath = expandHomePath(
905
+ userConfig?.debugLogPath ?? DEFAULTS.debugLogPath
906
+ )
907
+ const localSearchPaths = resolveSearchPaths(
908
+ userConfig?.localSearchPaths ?? [],
909
+ includeProjectParent,
910
+ directory
911
+ )
912
+
913
+ setCacheDir(cacheDir)
914
+
915
+ if (debugEnabled) {
916
+ await mkdir(dirname(debugLogPath), { recursive: true })
917
+ }
918
+
919
+ debugLogger = createDebugLogger(debugLogPath, debugEnabled)
920
+
921
+ runCleanup(cleanupMaxAgeDays)
922
+
40
923
  return {
41
924
  config: async (config) => {
42
- const explorerAgent = createRepoExplorerAgent()
925
+ const explorerAgent = createRepoExplorerAgent(repoExplorerModel)
43
926
  config.agent = {
44
927
  ...config.agent,
45
928
  "repo-explorer": explorerAgent,
@@ -57,7 +940,7 @@ When user mentions another project or asks about external code:
57
940
  tool: {
58
941
  repo_clone: tool({
59
942
  description:
60
- "Clone a repository to local cache or return path if already cached. Supports public and private (SSH) repos. Example: repo_clone({ repo: 'vercel/next.js' }) or repo_clone({ repo: 'vercel/next.js@canary', force: true })",
943
+ "Clone a repository to local cache or return path if already cached. Supports public and private repos. Example: repo_clone({ repo: 'vercel/next.js' }) or repo_clone({ repo: 'vercel/next.js@canary', force: true })",
61
944
  args: {
62
945
  repo: tool.schema
63
946
  .string()
@@ -72,30 +955,32 @@ When user mentions another project or asks about external code:
72
955
  },
73
956
  async execute(args) {
74
957
  const spec = parseRepoSpec(args.repo)
75
- const branch = spec.branch || "main"
76
- const repoKey = `${spec.owner}/${spec.repo}@${branch}`
958
+ const branch = spec.branch || defaultBranch
959
+ const repoKey = `${spec.owner}/${spec.repo}`
77
960
 
78
961
  const result = await withManifestLock(async () => {
79
962
  const manifest = await loadManifest()
80
-
81
963
  const existingEntry = manifest.repos[repoKey]
964
+ const destPath = join(cacheDir, spec.owner, spec.repo)
965
+
82
966
  if (existingEntry && !args.force) {
967
+ if (existingEntry.currentBranch !== branch) {
968
+ await switchBranch(existingEntry.path, branch)
969
+ existingEntry.currentBranch = branch
970
+ existingEntry.lastUpdated = new Date().toISOString()
971
+ }
83
972
  existingEntry.lastAccessed = new Date().toISOString()
84
973
  await saveManifest(manifest)
85
974
 
86
975
  return {
87
976
  path: existingEntry.path,
977
+ branch,
88
978
  status: "cached" as const,
89
979
  alreadyExists: true,
90
980
  }
91
981
  }
92
982
 
93
- const destPath = join(
94
- CACHE_DIR,
95
- spec.owner,
96
- `${spec.repo}@${branch}`
97
- )
98
- const url = buildGitUrl(spec.owner, spec.repo)
983
+ const url = buildGitUrl(spec.owner, spec.repo, useHttps)
99
984
 
100
985
  if (args.force && existingEntry) {
101
986
  try {
@@ -118,7 +1003,7 @@ When user mentions another project or asks about external code:
118
1003
  clonedAt: now,
119
1004
  lastAccessed: now,
120
1005
  lastUpdated: now,
121
- defaultBranch: branch,
1006
+ currentBranch: branch,
122
1007
  shallow: true,
123
1008
  }
124
1009
  manifest.repos[repoKey] = entry
@@ -127,6 +1012,7 @@ When user mentions another project or asks about external code:
127
1012
 
128
1013
  return {
129
1014
  path: destPath,
1015
+ branch,
130
1016
  status: "cloned" as const,
131
1017
  alreadyExists: false,
132
1018
  }
@@ -136,13 +1022,30 @@ When user mentions another project or asks about external code:
136
1022
  ? "Repository already cached"
137
1023
  : "Successfully cloned repository"
138
1024
 
139
- return `## ${statusText}
1025
+ let output = `## ${statusText}
140
1026
 
141
- **Repository**: ${args.repo}
1027
+ **Repository**: ${repoKey}
1028
+ **Branch**: ${result.branch}
142
1029
  **Path**: ${result.path}
143
1030
  **Status**: ${result.status}
144
1031
 
145
1032
  You can now use \`repo_read\` to access files from this repository.`
1033
+
1034
+ output = appendDebug(
1035
+ output,
1036
+ "repo_clone",
1037
+ [
1038
+ `repoKey: ${repoKey}`,
1039
+ `branch: ${result.branch}`,
1040
+ `path: ${result.path}`,
1041
+ `cacheDir: ${cacheDir}`,
1042
+ `useHttps: ${useHttps}`,
1043
+ `force: ${args.force}`,
1044
+ ],
1045
+ debugEnabled
1046
+ )
1047
+
1048
+ return output
146
1049
  },
147
1050
  }),
148
1051
 
@@ -166,37 +1069,62 @@ You can now use \`repo_read\` to access files from this repository.`
166
1069
  },
167
1070
  async execute(args) {
168
1071
  const spec = parseRepoSpec(args.repo)
169
- const branch = spec.branch || "main"
170
- const repoKey = `${spec.owner}/${spec.repo}@${branch}`
1072
+ const branch = spec.branch || defaultBranch
1073
+ const repoKey = `${spec.owner}/${spec.repo}`
171
1074
 
172
1075
  const manifest = await loadManifest()
173
1076
  const entry = manifest.repos[repoKey]
174
1077
 
175
1078
  if (!entry) {
176
- return `## Repository not found
1079
+ let output = `## Repository not found
177
1080
 
178
- Repository \`${args.repo}\` is not registered.
1081
+ Repository \`${spec.owner}/${spec.repo}\` is not registered.
179
1082
 
180
1083
  Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
1084
+ output = appendDebug(
1085
+ output,
1086
+ "repo_read",
1087
+ [`repoKey: ${repoKey}`, `branch: ${branch}`],
1088
+ debugEnabled
1089
+ )
1090
+ return output
1091
+ }
1092
+
1093
+ if (entry.type === "cached" && entry.currentBranch !== branch) {
1094
+ await switchBranch(entry.path, branch)
1095
+ await withManifestLock(async () => {
1096
+ const updatedManifest = await loadManifest()
1097
+ if (updatedManifest.repos[repoKey]) {
1098
+ updatedManifest.repos[repoKey].currentBranch = branch
1099
+ updatedManifest.repos[repoKey].lastUpdated = new Date().toISOString()
1100
+ await saveManifest(updatedManifest)
1101
+ }
1102
+ })
181
1103
  }
182
1104
 
183
1105
  const repoPath = entry.path
184
1106
  const fullPath = join(repoPath, args.path)
185
1107
 
186
- let filePaths: string[] = []
1108
+ let filePaths: string[] = []
187
1109
 
188
- if (args.path.includes("*") || args.path.includes("?")) {
189
- const fdResult = await $`fd -t f -g ${args.path} ${repoPath}`.text()
190
- filePaths = fdResult.split("\n").filter(Boolean)
191
- } else {
192
- filePaths = [fullPath]
193
- }
1110
+ if (args.path.includes("*") || args.path.includes("?")) {
1111
+ const fdResult = await $`fd -t f -g ${args.path} ${repoPath}`.text()
1112
+ filePaths = fdResult.split("\n").filter(Boolean)
1113
+ } else {
1114
+ filePaths = [fullPath]
1115
+ }
194
1116
 
195
1117
  if (filePaths.length === 0) {
196
- return `No files found matching path: ${args.path}`
1118
+ const output = appendDebug(
1119
+ `No files found matching path: ${args.path}`,
1120
+ "repo_read",
1121
+ [`repoKey: ${repoKey}`, `branch: ${branch}`],
1122
+ debugEnabled
1123
+ )
1124
+ return output
197
1125
  }
198
1126
 
199
- let output = `## Files from ${args.repo}\n\n`
1127
+ let output = `## Files from ${repoKey} @ ${branch}\n\n`
200
1128
  const maxLines = args.maxLines ?? 500
201
1129
 
202
1130
  for (const filePath of filePaths) {
@@ -231,13 +1159,24 @@ Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
231
1159
  }
232
1160
  })
233
1161
 
1162
+ output = appendDebug(
1163
+ output,
1164
+ "repo_read",
1165
+ [
1166
+ `repoKey: ${repoKey}`,
1167
+ `branch: ${branch}`,
1168
+ `files: ${filePaths.length}`,
1169
+ ],
1170
+ debugEnabled
1171
+ )
1172
+
234
1173
  return output
235
1174
  },
236
1175
  }),
237
1176
 
238
1177
  repo_list: tool({
239
1178
  description:
240
- "List all registered repositories (cached and local). Shows metadata like type, branch, last accessed, and size.",
1179
+ "List all registered repositories (cached and local). Shows metadata like type, current branch, freshness (for cached), and size.",
241
1180
  args: {
242
1181
  type: tool.schema
243
1182
  .enum(["all", "cached", "local"])
@@ -255,21 +1194,32 @@ Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
255
1194
  })
256
1195
 
257
1196
  if (filteredRepos.length === 0) {
258
- return "No repositories registered."
1197
+ return appendDebug(
1198
+ "No repositories registered.",
1199
+ "repo_list",
1200
+ [`type: ${args.type}`],
1201
+ debugEnabled
1202
+ )
259
1203
  }
260
1204
 
261
1205
  let output = "## Registered Repositories\n\n"
262
- output += "| Repo | Type | Branch | Last Accessed | Size |\n"
263
- output += "|------|------|--------|---------------|------|\n"
1206
+ output += "| Repo | Type | Branch | Last Updated | Size |\n"
1207
+ output += "|------|------|--------|--------------|------|\n"
264
1208
 
265
1209
  for (const [repoKey, entry] of filteredRepos) {
266
- const repoName = repoKey.substring(0, repoKey.lastIndexOf("@"))
267
- const lastAccessed = new Date(entry.lastAccessed).toLocaleDateString()
268
1210
  const size = entry.sizeBytes
269
1211
  ? `${Math.round(entry.sizeBytes / 1024 / 1024)}MB`
270
1212
  : "-"
271
1213
 
272
- output += `| ${repoName} | ${entry.type} | ${entry.defaultBranch} | ${lastAccessed} | ${size} |\n`
1214
+ let freshness = "-"
1215
+ if (entry.type === "cached" && entry.lastUpdated) {
1216
+ const daysSinceUpdate = Math.floor(
1217
+ (Date.now() - new Date(entry.lastUpdated).getTime()) / (1000 * 60 * 60 * 24)
1218
+ )
1219
+ freshness = daysSinceUpdate === 0 ? "today" : `${daysSinceUpdate}d ago`
1220
+ }
1221
+
1222
+ output += `| ${repoKey} | ${entry.type} | ${entry.currentBranch} | ${freshness} | ${size} |\n`
273
1223
  }
274
1224
 
275
1225
  const cachedCount = filteredRepos.filter(
@@ -280,7 +1230,12 @@ Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
280
1230
  ).length
281
1231
  output += `\nTotal: ${filteredRepos.length} repos (${cachedCount} cached, ${localCount} local)`
282
1232
 
283
- return output
1233
+ return appendDebug(
1234
+ output,
1235
+ "repo_list",
1236
+ [`type: ${args.type}`, `total: ${filteredRepos.length}`],
1237
+ debugEnabled
1238
+ )
284
1239
  },
285
1240
  }),
286
1241
 
@@ -294,15 +1249,10 @@ Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
294
1249
  .describe("Override search paths (default: from config)"),
295
1250
  },
296
1251
  async execute(args) {
297
- let searchPaths: string[] | null = args.paths || null
298
-
299
- if (!searchPaths) {
300
- const config = await loadConfig()
301
- searchPaths = config?.localSearchPaths || null
302
- }
1252
+ const searchPaths = args.paths ?? (localSearchPaths.length > 0 ? localSearchPaths : null)
303
1253
 
304
1254
  if (!searchPaths || searchPaths.length === 0) {
305
- return `## No search paths configured
1255
+ let output = `## No search paths configured
306
1256
 
307
1257
  Create a config file at \`~/.config/opencode/opencode-repos.json\`:
308
1258
 
@@ -317,17 +1267,31 @@ Create a config file at \`~/.config/opencode/opencode-repos.json\`:
317
1267
  \`\`\`
318
1268
 
319
1269
  Or provide paths directly: \`repo_scan({ paths: ["~/projects"] })\``
1270
+ output = appendDebug(
1271
+ output,
1272
+ "repo_scan",
1273
+ ["searchPaths: none"],
1274
+ debugEnabled
1275
+ )
1276
+ return output
320
1277
  }
321
1278
 
322
1279
  const foundRepos = await scanLocalRepos(searchPaths)
323
1280
 
324
1281
  if (foundRepos.length === 0) {
325
- return `## No repositories found
1282
+ let output = `## No repositories found
326
1283
 
327
1284
  Searched ${searchPaths.length} path(s):
328
1285
  ${searchPaths.map((p) => `- ${p}`).join("\n")}
329
1286
 
330
1287
  No git repositories with remotes were found.`
1288
+ output = appendDebug(
1289
+ output,
1290
+ "repo_scan",
1291
+ [`searchPaths: ${searchPaths.length}`],
1292
+ debugEnabled
1293
+ )
1294
+ return output
331
1295
  }
332
1296
 
333
1297
  let newCount = 0
@@ -340,8 +1304,8 @@ No git repositories with remotes were found.`
340
1304
  const spec = matchRemoteToSpec(repo.remote)
341
1305
  if (!spec) continue
342
1306
 
343
- const branch = repo.branch || "main"
344
- const repoKey = `${spec}@${branch}`
1307
+ const branch = repo.branch || defaultBranch
1308
+ const repoKey = spec
345
1309
 
346
1310
  if (manifest.repos[repoKey]) {
347
1311
  existingCount++
@@ -353,7 +1317,7 @@ No git repositories with remotes were found.`
353
1317
  type: "local",
354
1318
  path: repo.path,
355
1319
  lastAccessed: now,
356
- defaultBranch: branch,
1320
+ currentBranch: branch,
357
1321
  shallow: false,
358
1322
  }
359
1323
 
@@ -365,19 +1329,30 @@ No git repositories with remotes were found.`
365
1329
  await saveManifest(manifest)
366
1330
  })
367
1331
 
368
- return `## Local Repository Scan Complete
1332
+ let output = `## Local Repository Scan Complete
369
1333
 
370
1334
  **Found**: ${foundRepos.length} repositories in ${searchPaths.length} path(s)
371
1335
  **New**: ${newCount} repos registered
372
1336
  **Existing**: ${existingCount} repos already registered
373
1337
 
374
1338
  ${newCount > 0 ? "Use `repo_list()` to see all registered repositories." : ""}`
1339
+ output = appendDebug(
1340
+ output,
1341
+ "repo_scan",
1342
+ [
1343
+ `searchPaths: ${searchPaths.length}`,
1344
+ `found: ${foundRepos.length}`,
1345
+ `new: ${newCount}`,
1346
+ ],
1347
+ debugEnabled
1348
+ )
1349
+ return output
375
1350
  },
376
1351
  }),
377
1352
 
378
1353
  repo_update: tool({
379
1354
  description:
380
- "Update a cached repository to latest. For local repos, shows git status without modifying. Only cached repos (cloned via repo_clone) are updated.",
1355
+ "Update a cached repository to latest. Optionally switch to a different branch first. For local repos, shows git status without modifying.",
381
1356
  args: {
382
1357
  repo: tool.schema
383
1358
  .string()
@@ -387,18 +1362,25 @@ ${newCount > 0 ? "Use `repo_list()` to see all registered repositories." : ""}`
387
1362
  },
388
1363
  async execute(args) {
389
1364
  const spec = parseRepoSpec(args.repo)
390
- const branch = spec.branch || "main"
391
- const repoKey = `${spec.owner}/${spec.repo}@${branch}`
1365
+ const requestedBranch = spec.branch
1366
+ const repoKey = `${spec.owner}/${spec.repo}`
392
1367
 
393
1368
  const manifest = await loadManifest()
394
1369
  const entry = manifest.repos[repoKey]
395
1370
 
396
1371
  if (!entry) {
397
- return `## Repository not found
1372
+ let output = `## Repository not found
398
1373
 
399
- Repository \`${args.repo}\` is not registered.
1374
+ Repository \`${repoKey}\` is not registered.
400
1375
 
401
1376
  Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
1377
+ output = appendDebug(
1378
+ output,
1379
+ "repo_update",
1380
+ [`repoKey: ${repoKey}`],
1381
+ debugEnabled
1382
+ )
1383
+ return output
402
1384
  }
403
1385
 
404
1386
  if (entry.type === "local") {
@@ -407,8 +1389,9 @@ Use \`repo_clone({ repo: "${args.repo}" })\` to clone it first.`
407
1389
 
408
1390
  return `## Local Repository Status
409
1391
 
410
- **Repository**: ${args.repo}
1392
+ **Repository**: ${repoKey}
411
1393
  **Path**: ${entry.path}
1394
+ **Branch**: ${entry.currentBranch}
412
1395
  **Type**: Local (not modified by plugin)
413
1396
 
414
1397
  \`\`\`
@@ -416,41 +1399,69 @@ ${status || "Working tree clean"}
416
1399
  \`\`\``
417
1400
  } catch (error) {
418
1401
  const message = error instanceof Error ? error.message : String(error)
419
- return `## Error getting status
420
-
421
- Failed to get git status for ${args.repo}: ${message}`
1402
+ let output = `## Error getting status
1403
+
1404
+ Failed to get git status for ${repoKey}: ${message}`
1405
+ output = appendDebug(
1406
+ output,
1407
+ "repo_update",
1408
+ [`repoKey: ${repoKey}`, `path: ${entry.path}`],
1409
+ debugEnabled
1410
+ )
1411
+ return output
422
1412
  }
423
1413
  }
424
1414
 
425
1415
  try {
426
- await updateRepo(entry.path, branch)
1416
+ const targetBranch = requestedBranch || entry.currentBranch
1417
+
1418
+ if (targetBranch !== entry.currentBranch) {
1419
+ await switchBranch(entry.path, targetBranch)
1420
+ } else {
1421
+ await updateRepo(entry.path)
1422
+ }
427
1423
 
428
1424
  const info = await getRepoInfo(entry.path)
429
1425
 
430
1426
  await withManifestLock(async () => {
431
1427
  const updatedManifest = await loadManifest()
432
1428
  if (updatedManifest.repos[repoKey]) {
1429
+ updatedManifest.repos[repoKey].currentBranch = targetBranch
433
1430
  updatedManifest.repos[repoKey].lastUpdated = new Date().toISOString()
434
1431
  updatedManifest.repos[repoKey].lastAccessed = new Date().toISOString()
435
1432
  await saveManifest(updatedManifest)
436
1433
  }
437
1434
  })
438
1435
 
439
- return `## Repository Updated
1436
+ let output = `## Repository Updated
440
1437
 
441
- **Repository**: ${args.repo}
1438
+ **Repository**: ${repoKey}
442
1439
  **Path**: ${entry.path}
443
- **Branch**: ${branch}
1440
+ **Branch**: ${targetBranch}
444
1441
  **Latest Commit**: ${info.commit.substring(0, 7)}
445
1442
 
446
1443
  Repository has been updated to the latest commit.`
1444
+ output = appendDebug(
1445
+ output,
1446
+ "repo_update",
1447
+ [`repoKey: ${repoKey}`, `branch: ${targetBranch}`],
1448
+ debugEnabled
1449
+ )
1450
+ return output
447
1451
  } catch (error) {
448
1452
  const message = error instanceof Error ? error.message : String(error)
449
- return `## Update Failed
1453
+ let output = `## Update Failed
450
1454
 
451
- Failed to update ${args.repo}: ${message}
1455
+ Failed to update ${repoKey}: ${message}
452
1456
 
453
1457
  The repository may be corrupted. Try \`repo_clone({ repo: "${args.repo}", force: true })\` to re-clone.`
1458
+ output = appendDebug(
1459
+ output,
1460
+ "repo_update",
1461
+ [`repoKey: ${repoKey}`],
1462
+ debugEnabled
1463
+ )
1464
+ return output
454
1465
  }
455
1466
  },
456
1467
  }),
@@ -462,7 +1473,7 @@ The repository may be corrupted. Try \`repo_clone({ repo: "${args.repo}", force:
462
1473
  repo: tool.schema
463
1474
  .string()
464
1475
  .describe(
465
- "Repository in format 'owner/repo' or 'owner/repo@branch'"
1476
+ "Repository in format 'owner/repo'"
466
1477
  ),
467
1478
  confirm: tool.schema
468
1479
  .boolean()
@@ -472,18 +1483,24 @@ The repository may be corrupted. Try \`repo_clone({ repo: "${args.repo}", force:
472
1483
  },
473
1484
  async execute(args) {
474
1485
  const spec = parseRepoSpec(args.repo)
475
- const branch = spec.branch || "main"
476
- const repoKey = `${spec.owner}/${spec.repo}@${branch}`
1486
+ const repoKey = `${spec.owner}/${spec.repo}`
477
1487
 
478
1488
  const manifest = await loadManifest()
479
1489
  const entry = manifest.repos[repoKey]
480
1490
 
481
1491
  if (!entry) {
482
- return `## Repository not found
1492
+ let output = `## Repository not found
483
1493
 
484
- Repository \`${args.repo}\` is not registered.
1494
+ Repository \`${repoKey}\` is not registered.
485
1495
 
486
1496
  Use \`repo_list()\` to see all registered repositories.`
1497
+ output = appendDebug(
1498
+ output,
1499
+ "repo_remove",
1500
+ [`repoKey: ${repoKey}`],
1501
+ debugEnabled
1502
+ )
1503
+ return output
487
1504
  }
488
1505
 
489
1506
  if (entry.type === "local") {
@@ -501,28 +1518,42 @@ Use \`repo_list()\` to see all registered repositories.`
501
1518
  await saveManifest(updatedManifest)
502
1519
  })
503
1520
 
504
- return `## Local Repository Unregistered
1521
+ let output = `## Local Repository Unregistered
505
1522
 
506
- **Repository**: ${args.repo}
1523
+ **Repository**: ${repoKey}
507
1524
  **Path**: ${entry.path}
508
1525
 
509
1526
  The repository has been unregistered. Files are preserved at the path above.
510
1527
 
511
1528
  To re-register, run \`repo_scan()\`.`
1529
+ output = appendDebug(
1530
+ output,
1531
+ "repo_remove",
1532
+ [`repoKey: ${repoKey}`, `path: ${entry.path}`],
1533
+ debugEnabled
1534
+ )
1535
+ return output
512
1536
  }
513
1537
 
514
1538
  if (!args.confirm) {
515
- return `## Confirmation Required
1539
+ let output = `## Confirmation Required
516
1540
 
517
- **Repository**: ${args.repo}
1541
+ **Repository**: ${repoKey}
518
1542
  **Path**: ${entry.path}
519
1543
  **Type**: Cached (cloned by plugin)
520
1544
 
521
1545
  This will **permanently delete** the cached repository from disk.
522
1546
 
523
- To proceed: \`repo_remove({ repo: "${args.repo}", confirm: true })\`
1547
+ To proceed: \`repo_remove({ repo: "${repoKey}", confirm: true })\`
524
1548
 
525
1549
  To keep the repo but unregister it, manually delete it from \`~/.cache/opencode-repos/manifest.json\`.`
1550
+ output = appendDebug(
1551
+ output,
1552
+ "repo_remove",
1553
+ [`repoKey: ${repoKey}`, `path: ${entry.path}`],
1554
+ debugEnabled
1555
+ )
1556
+ return output
526
1557
  }
527
1558
 
528
1559
  try {
@@ -534,14 +1565,21 @@ To keep the repo but unregister it, manually delete it from \`~/.cache/opencode-
534
1565
  await saveManifest(updatedManifest)
535
1566
  })
536
1567
 
537
- return `## Cached Repository Deleted
1568
+ let output = `## Cached Repository Deleted
538
1569
 
539
- **Repository**: ${args.repo}
1570
+ **Repository**: ${repoKey}
540
1571
  **Path**: ${entry.path}
541
1572
 
542
1573
  The repository has been permanently deleted from disk and unregistered from the cache.
543
1574
 
544
- To re-clone: \`repo_clone({ repo: "${args.repo}" })\``
1575
+ To re-clone: \`repo_clone({ repo: "${repoKey}" })\``
1576
+ output = appendDebug(
1577
+ output,
1578
+ "repo_remove",
1579
+ [`repoKey: ${repoKey}`, `path: ${entry.path}`],
1580
+ debugEnabled
1581
+ )
1582
+ return output
545
1583
  } catch (error) {
546
1584
  const message = error instanceof Error ? error.message : String(error)
547
1585
 
@@ -553,11 +1591,18 @@ To re-clone: \`repo_clone({ repo: "${args.repo}" })\``
553
1591
  })
554
1592
  } catch {}
555
1593
 
556
- return `## Deletion Failed
1594
+ let output = `## Deletion Failed
557
1595
 
558
- Failed to delete ${args.repo}: ${message}
1596
+ Failed to delete ${repoKey}: ${message}
559
1597
 
560
1598
  The repository has been unregistered from the manifest. You may need to manually delete the directory at: ${entry.path}`
1599
+ output = appendDebug(
1600
+ output,
1601
+ "repo_remove",
1602
+ [`repoKey: ${repoKey}`, `path: ${entry.path}`],
1603
+ debugEnabled
1604
+ )
1605
+ return output
561
1606
  }
562
1607
  },
563
1608
  }),
@@ -597,11 +1642,10 @@ The repository has been unregistered from the manifest. You may need to manually
597
1642
  }
598
1643
  }
599
1644
 
600
- const config = await loadConfig()
601
- if (config?.localSearchPaths?.length) {
1645
+ if (localSearchPaths.length > 0) {
602
1646
  try {
603
1647
  const localResults = await findLocalRepoByName(
604
- config.localSearchPaths,
1648
+ localSearchPaths,
605
1649
  query
606
1650
  )
607
1651
  for (const local of localResults) {
@@ -682,120 +1726,508 @@ The repository has been unregistered from the manifest. You may need to manually
682
1726
  output += `- Check if gh CLI is authenticated\n`
683
1727
  }
684
1728
 
1729
+ output = appendDebug(
1730
+ output,
1731
+ "repo_find",
1732
+ [`query: ${query}`, `localSearchPaths: ${localSearchPaths.length}`],
1733
+ debugEnabled
1734
+ )
685
1735
  return output
686
1736
  },
687
1737
  }),
688
1738
 
689
- repo_explore: tool({
1739
+ repo_pick_dir: tool({
690
1740
  description:
691
- "Explore a repository to understand its codebase. Spawns a specialized exploration agent that analyzes the repo and answers your question. The agent will read source files, trace code paths, and explain architecture.",
1741
+ "Open a native folder picker and return the selected path. Call this immediately after the user asks for a local repo (avoid delayed popups if the user is away).",
692
1742
  args: {
693
- repo: tool.schema
1743
+ prompt: tool.schema
694
1744
  .string()
1745
+ .optional()
1746
+ .describe("Prompt text shown in the picker dialog"),
1747
+ },
1748
+ async execute(args) {
1749
+ const promptText = args.prompt ?? "Select a repository folder"
1750
+ const platform = process.platform
1751
+
1752
+ if (platform === "darwin") {
1753
+ const safePrompt = promptText.replace(/"/g, "\\\"")
1754
+ const script = `POSIX path of (choose folder with prompt "${safePrompt}")`
1755
+ const result = await $`osascript -e ${script}`.text()
1756
+ const selected = result.trim()
1757
+
1758
+ if (!selected) {
1759
+ return appendDebug(
1760
+ "No folder selected.",
1761
+ "repo_pick_dir",
1762
+ [`platform: ${platform}`],
1763
+ debugEnabled
1764
+ )
1765
+ }
1766
+
1767
+ return appendDebug(
1768
+ `## Folder selected\n\n${selected}`,
1769
+ "repo_pick_dir",
1770
+ [`platform: ${platform}`],
1771
+ debugEnabled
1772
+ )
1773
+ }
1774
+
1775
+ if (platform === "win32") {
1776
+ const safePrompt = promptText.replace(/'/g, "''")
1777
+ const command = `[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');` +
1778
+ `$dialog = New-Object System.Windows.Forms.FolderBrowserDialog;` +
1779
+ `$dialog.Description='${safePrompt}';` +
1780
+ `if ($dialog.ShowDialog() -eq 'OK') { $dialog.SelectedPath }`
1781
+
1782
+ const result = await $`powershell -NoProfile -Command ${command}`.text()
1783
+ const selected = result.trim()
1784
+
1785
+ if (!selected) {
1786
+ return appendDebug(
1787
+ "No folder selected.",
1788
+ "repo_pick_dir",
1789
+ [`platform: ${platform}`],
1790
+ debugEnabled
1791
+ )
1792
+ }
1793
+
1794
+ return appendDebug(
1795
+ `## Folder selected\n\n${selected}`,
1796
+ "repo_pick_dir",
1797
+ [`platform: ${platform}`],
1798
+ debugEnabled
1799
+ )
1800
+ }
1801
+
1802
+ return appendDebug(
1803
+ "Folder picker is not supported on this platform.",
1804
+ "repo_pick_dir",
1805
+ [`platform: ${platform}`],
1806
+ debugEnabled
1807
+ )
1808
+ },
1809
+ }),
1810
+
1811
+ repo_query: tool({
1812
+ description:
1813
+ "Resolve a repository automatically and explore it with a subagent. Accepts local paths or owner/repo. Picks an exact match when possible, otherwise asks you to disambiguate. Can run multiple repos when specified.",
1814
+ args: {
1815
+ query: tool.schema
1816
+ .string()
1817
+ .optional()
695
1818
  .describe(
696
- "Repository in format 'owner/repo' or 'owner/repo@branch'"
1819
+ "Repository name, owner/repo, or absolute local path. Examples: 'next.js', 'vercel/next.js', '/Users/me/projects/app'"
697
1820
  ),
1821
+ repos: tool.schema
1822
+ .array(tool.schema.string())
1823
+ .optional()
1824
+ .describe("Explicit repositories to explore (owner/repo or owner/repo@branch)"),
698
1825
  question: tool.schema
699
1826
  .string()
700
1827
  .describe("What you want to understand about the codebase"),
701
1828
  },
702
1829
  async execute(args, ctx) {
703
- const spec = parseRepoSpec(args.repo)
704
- const branch = spec.branch || "main"
705
- const repoKey = `${spec.owner}/${spec.repo}@${branch}`
1830
+ const explicitRepos = args.repos?.filter(Boolean) ?? []
706
1831
 
707
- let manifest = await loadManifest()
708
- let repoPath: string
1832
+ if (explicitRepos.length === 0 && !args.query) {
1833
+ return `## Missing repository query
709
1834
 
710
- if (!manifest.repos[repoKey]) {
711
- try {
712
- repoPath = join(CACHE_DIR, spec.owner, `${spec.repo}@${branch}`)
713
- const url = buildGitUrl(spec.owner, spec.repo)
1835
+ Provide either \`query\` or a non-empty \`repos\` list.`
1836
+ }
714
1837
 
715
- await withManifestLock(async () => {
716
- await cloneRepo(url, repoPath, { branch })
1838
+ const targets: Array<{
1839
+ repoKey: string
1840
+ branch: string
1841
+ source?: RepoSource
1842
+ remote?: string
1843
+ path?: string
1844
+ }> = []
1845
+
1846
+ const debugInfo: RepoQueryDebugInfo = {
1847
+ query: args.query ?? explicitRepos.join(", "),
1848
+ allowGithub: false,
1849
+ localSearchPaths,
1850
+ candidates: [],
1851
+ selectedTargets: [],
1852
+ }
717
1853
 
718
- const now = new Date().toISOString()
719
- const updatedManifest = await loadManifest()
720
- updatedManifest.repos[repoKey] = {
721
- type: "cached",
722
- path: repoPath,
723
- clonedAt: now,
724
- lastAccessed: now,
725
- lastUpdated: now,
726
- defaultBranch: branch,
727
- shallow: true,
728
- }
729
- await saveManifest(updatedManifest)
1854
+ if (explicitRepos.length > 0) {
1855
+ for (const repo of explicitRepos) {
1856
+ try {
1857
+ const spec = parseRepoSpec(repo)
1858
+ targets.push({
1859
+ repoKey: `${spec.owner}/${spec.repo}`,
1860
+ branch: spec.branch || defaultBranch,
1861
+ })
1862
+ debugInfo.selectedTargets.push({
1863
+ repoKey: `${spec.owner}/${spec.repo}`,
1864
+ branch: spec.branch || defaultBranch,
1865
+ })
1866
+ } catch (error) {
1867
+ const message =
1868
+ error instanceof Error ? error.message : String(error)
1869
+ return `## Invalid repository
1870
+
1871
+ Failed to parse \`${repo}\`: ${message}`
1872
+ }
1873
+ }
1874
+ } else {
1875
+ const localPathResult = await resolveLocalPathQuery(
1876
+ args.query!,
1877
+ defaultBranch
1878
+ )
1879
+
1880
+ if (localPathResult.error) {
1881
+ let output = `## Local path error\n\n${localPathResult.error}`
1882
+ if (debugEnabled) {
1883
+ logRepoQueryDebug(debugInfo)
1884
+ output += `\n\n${formatRepoQueryDebug(debugInfo)}`
1885
+ }
1886
+ return output
1887
+ }
1888
+
1889
+ if (localPathResult.candidate) {
1890
+ const candidate = localPathResult.candidate
1891
+ targets.push({
1892
+ repoKey: candidate.key,
1893
+ branch: candidate.branch ?? defaultBranch,
1894
+ source: candidate.source,
1895
+ remote: candidate.remote,
1896
+ path: candidate.path,
730
1897
  })
731
- } catch (error) {
732
- const message =
733
- error instanceof Error ? error.message : String(error)
734
- return `## Failed to clone repository
1898
+ debugInfo.candidates = [candidate]
1899
+ debugInfo.selectedTargets.push({
1900
+ repoKey: candidate.key,
1901
+ branch: candidate.branch ?? defaultBranch,
1902
+ })
1903
+ }
735
1904
 
736
- Failed to clone ${args.repo}: ${message}
1905
+ if (targets.length > 0) {
1906
+ for (const target of targets) {
1907
+ if (target.source === "local" && target.remote && target.path) {
1908
+ await registerLocalRepo(
1909
+ target.repoKey,
1910
+ target.path,
1911
+ target.branch,
1912
+ target.remote
1913
+ )
1914
+ }
1915
+ }
1916
+ }
737
1917
 
738
- Please check that the repository exists and you have access to it.`
1918
+ if (targets.length > 0) {
1919
+ const results: Array<{ repoKey: string; response: string }> = []
1920
+
1921
+ for (const target of targets) {
1922
+ try {
1923
+ const resolved = await ensureRepoAvailable(
1924
+ target.repoKey,
1925
+ target.branch,
1926
+ cacheDir,
1927
+ useHttps,
1928
+ autoSyncOnExplore,
1929
+ autoSyncIntervalHours
1930
+ )
1931
+
1932
+ await requestExternalDirectoryAccess(
1933
+ ctx as PermissionContext,
1934
+ resolved.repoPath
1935
+ )
1936
+
1937
+ const response = await runRepoExplorer(
1938
+ client as RepoClient,
1939
+ ctx,
1940
+ target.repoKey,
1941
+ resolved.repoPath,
1942
+ args.question,
1943
+ repoExplorerModel,
1944
+ directory
1945
+ )
1946
+
1947
+ await touchRepoAccess(target.repoKey)
1948
+
1949
+ results.push({
1950
+ repoKey: target.repoKey,
1951
+ response,
1952
+ })
1953
+ } catch (error) {
1954
+ const message =
1955
+ error instanceof Error ? error.message : String(error)
1956
+ results.push({
1957
+ repoKey: target.repoKey,
1958
+ response: `## Exploration failed\n\n${message}`,
1959
+ })
1960
+ }
1961
+ }
1962
+
1963
+ if (results.length === 1) {
1964
+ if (debugEnabled) {
1965
+ logRepoQueryDebug(debugInfo)
1966
+ return `${results[0].response}\n\n${formatRepoQueryDebug(debugInfo)}`
1967
+ }
1968
+ return results[0].response
1969
+ }
1970
+
1971
+ let output = "## Repository Exploration Results\n\n"
1972
+ for (const result of results) {
1973
+ output += `### ${result.repoKey}\n\n${result.response}\n\n`
1974
+ }
1975
+
1976
+ if (debugEnabled) {
1977
+ logRepoQueryDebug(debugInfo)
1978
+ output += `${formatRepoQueryDebug(debugInfo)}\n`
1979
+ }
1980
+
1981
+ return output
739
1982
  }
740
- } else {
741
- repoPath = manifest.repos[repoKey].path
742
- }
743
1983
 
744
- const explorationPrompt = `Explore the codebase at ${repoPath} and answer the following question:
1984
+ const manifest = await loadManifest()
1985
+ let allowGithub = false
745
1986
 
746
- ${args.question}
1987
+ if (args.query?.includes("/")) {
1988
+ try {
1989
+ parseRepoSpec(args.query)
1990
+ allowGithub = true
1991
+ } catch {
1992
+ allowGithub = false
1993
+ }
1994
+ }
747
1995
 
748
- Working directory: ${repoPath}
1996
+ debugInfo.allowGithub = allowGithub
749
1997
 
750
- You have access to all standard code exploration tools:
751
- - read: Read files
752
- - glob: Find files by pattern
753
- - grep: Search for patterns
754
- - bash: Run git commands if needed
1998
+ const { candidates, exactRepoKey, branchOverride } = await resolveCandidates(
1999
+ args.query!,
2000
+ localSearchPaths,
2001
+ manifest,
2002
+ defaultBranch,
2003
+ allowGithub
2004
+ )
755
2005
 
756
- Remember to:
757
- - Start with high-level structure (README, package.json, main files)
758
- - Cite specific files and line numbers
759
- - Include relevant code snippets
760
- - Explain how components interact
2006
+ debugInfo.candidates = candidates
2007
+
2008
+ if (candidates.length === 0) {
2009
+ if (!allowGithub) {
2010
+ let output = `## No local repositories matched
2011
+
2012
+ No repositories matched \`${args.query}\` in the local registry.
2013
+
2014
+ Provide a local path or configure search paths:
2015
+
2016
+ - Use \`repo_pick_dir()\` to select a folder (GUI required)
2017
+ - Or set \`localSearchPaths\` in ~/.config/opencode/opencode-repos.json
2018
+ - Or pass an explicit repo in \`repos\` (owner/repo)
761
2019
  `
2020
+ if (debugEnabled) {
2021
+ logRepoQueryDebug(debugInfo)
2022
+ output += `\n${formatRepoQueryDebug(debugInfo)}`
2023
+ }
2024
+ return output
2025
+ }
2026
+ let output = `## No repositories found
762
2027
 
763
- try {
764
- const response = await client.session.prompt({
765
- path: { id: ctx.sessionID },
766
- body: {
767
- agent: "repo-explorer",
768
- parts: [{ type: "text", text: explorationPrompt }],
769
- },
770
- })
2028
+ No repositories matched \`${args.query}\`.
771
2029
 
772
- await withManifestLock(async () => {
773
- const updatedManifest = await loadManifest()
774
- if (updatedManifest.repos[repoKey]) {
775
- updatedManifest.repos[repoKey].lastAccessed =
776
- new Date().toISOString()
777
- await saveManifest(updatedManifest)
2030
+ Try:
2031
+ - Using owner/repo format for exact matches
2032
+ - Running \`repo_find({ query: "${args.query}" })\` to see available options`
2033
+ if (debugEnabled) {
2034
+ logRepoQueryDebug(debugInfo)
2035
+ output += `\n${formatRepoQueryDebug(debugInfo)}`
778
2036
  }
2037
+ return output
2038
+ }
2039
+
2040
+ if (candidates.length > 1 && !exactRepoKey) {
2041
+ let output = `## Multiple repositories matched "${args.query}"
2042
+
2043
+ Be more specific or pass an explicit list with \`repos\`.
2044
+
2045
+ `
2046
+ for (const candidate of candidates) {
2047
+ const sourceLabel = candidate.source === "registered" ? "registered" : candidate.source
2048
+ const description = candidate.description ? ` - ${candidate.description.slice(0, 80)}` : ""
2049
+ output += `- ${candidate.key} (${sourceLabel})${description}\n`
2050
+ }
2051
+
2052
+ if (debugEnabled) {
2053
+ logRepoQueryDebug(debugInfo)
2054
+ output += `\n${formatRepoQueryDebug(debugInfo)}`
2055
+ }
2056
+
2057
+ return output
2058
+ }
2059
+
2060
+ const selected = candidates[0]
2061
+ const branch = branchOverride ?? selected.branch ?? defaultBranch
2062
+
2063
+ targets.push({
2064
+ repoKey: selected.key,
2065
+ branch,
2066
+ source: selected.source,
2067
+ remote: selected.remote,
2068
+ path: selected.path,
2069
+ })
2070
+
2071
+ debugInfo.selectedTargets.push({
2072
+ repoKey: selected.key,
2073
+ branch,
779
2074
  })
2075
+ }
2076
+
2077
+ for (const target of targets) {
2078
+ if (target.source === "local" && target.remote && target.path) {
2079
+ await registerLocalRepo(
2080
+ target.repoKey,
2081
+ target.path,
2082
+ target.branch,
2083
+ target.remote
2084
+ )
2085
+ }
2086
+ }
2087
+
2088
+ const results: Array<{ repoKey: string; response: string }> = []
2089
+
2090
+ for (const target of targets) {
2091
+ try {
2092
+ const resolved = await ensureRepoAvailable(
2093
+ target.repoKey,
2094
+ target.branch,
2095
+ cacheDir,
2096
+ useHttps,
2097
+ autoSyncOnExplore,
2098
+ autoSyncIntervalHours
2099
+ )
2100
+
2101
+ await requestExternalDirectoryAccess(
2102
+ ctx as PermissionContext,
2103
+ resolved.repoPath
2104
+ )
2105
+
2106
+ const response = await runRepoExplorer(
2107
+ client as RepoClient,
2108
+ ctx,
2109
+ target.repoKey,
2110
+ resolved.repoPath,
2111
+ args.question,
2112
+ repoExplorerModel,
2113
+ directory
2114
+ )
780
2115
 
781
- if (response.error) {
782
- return `## Exploration failed
2116
+ await touchRepoAccess(target.repoKey)
783
2117
 
784
- Error from API: ${JSON.stringify(response.error)}`
2118
+ results.push({
2119
+ repoKey: target.repoKey,
2120
+ response,
2121
+ })
2122
+ } catch (error) {
2123
+ const message =
2124
+ error instanceof Error ? error.message : String(error)
2125
+ results.push({
2126
+ repoKey: target.repoKey,
2127
+ response: `## Exploration failed\n\n${message}`,
2128
+ })
2129
+ }
2130
+ }
2131
+
2132
+ if (results.length === 1) {
2133
+ if (debugEnabled) {
2134
+ logRepoQueryDebug(debugInfo)
2135
+ return `${results[0].response}\n\n${formatRepoQueryDebug(debugInfo)}`
785
2136
  }
2137
+ return results[0].response
2138
+ }
2139
+
2140
+ let output = "## Repository Exploration Results\n\n"
2141
+ for (const result of results) {
2142
+ output += `### ${result.repoKey}\n\n${result.response}\n\n`
2143
+ }
2144
+
2145
+ if (debugEnabled) {
2146
+ logRepoQueryDebug(debugInfo)
2147
+ output += `${formatRepoQueryDebug(debugInfo)}\n`
2148
+ }
2149
+
2150
+ return output
2151
+ },
2152
+ }),
2153
+
2154
+ repo_explore: tool({
2155
+ description:
2156
+ "Explore a repository to understand its codebase. Spawns a specialized exploration agent that analyzes the repo and answers your question. The agent will read source files, trace code paths, and explain architecture.",
2157
+ args: {
2158
+ repo: tool.schema
2159
+ .string()
2160
+ .describe(
2161
+ "Repository in format 'owner/repo' or 'owner/repo@branch'"
2162
+ ),
2163
+ question: tool.schema
2164
+ .string()
2165
+ .describe("What you want to understand about the codebase"),
2166
+ },
2167
+ async execute(args, ctx) {
2168
+ const spec = parseRepoSpec(args.repo)
2169
+ const branch = spec.branch || defaultBranch
2170
+ const repoKey = `${spec.owner}/${spec.repo}`
2171
+ let repoPath: string
2172
+
2173
+ try {
2174
+ const resolved = await ensureRepoAvailable(
2175
+ repoKey,
2176
+ branch,
2177
+ cacheDir,
2178
+ useHttps,
2179
+ autoSyncOnExplore,
2180
+ autoSyncIntervalHours
2181
+ )
2182
+ repoPath = resolved.repoPath
2183
+ await requestExternalDirectoryAccess(ctx as PermissionContext, repoPath)
2184
+ } catch (error) {
2185
+ const message =
2186
+ error instanceof Error ? error.message : String(error)
2187
+ const output = `## Failed to prepare repository
786
2188
 
787
- const parts = response.data?.parts || []
788
- const textParts = parts.filter(p => p.type === "text")
789
- const texts = textParts.map(p => "text" in p ? p.text : "").filter(Boolean)
790
- return texts.join("\n\n") || "No response from exploration agent."
2189
+ Failed to prepare ${args.repo}: ${message}
2190
+
2191
+ Please check that the repository exists and you have access to it.`
2192
+ return appendDebug(
2193
+ output,
2194
+ "repo_explore",
2195
+ [`repoKey: ${repoKey}`, `branch: ${branch}`],
2196
+ debugEnabled
2197
+ )
2198
+ }
2199
+
2200
+ try {
2201
+ const response = await runRepoExplorer(
2202
+ client as RepoClient,
2203
+ ctx,
2204
+ repoKey,
2205
+ repoPath,
2206
+ args.question,
2207
+ repoExplorerModel,
2208
+ directory
2209
+ )
2210
+ await touchRepoAccess(repoKey)
2211
+ return appendDebug(
2212
+ response,
2213
+ "repo_explore",
2214
+ [`repoKey: ${repoKey}`, `branch: ${branch}`, `path: ${repoPath}`],
2215
+ debugEnabled
2216
+ )
791
2217
  } catch (error) {
792
2218
  const message =
793
2219
  error instanceof Error ? error.message : String(error)
794
- return `## Exploration failed
2220
+ const output = `## Exploration failed
795
2221
 
796
2222
  Failed to spawn exploration agent: ${message}
797
2223
 
798
2224
  This may indicate an issue with the OpenCode session or agent registration.`
2225
+ return appendDebug(
2226
+ output,
2227
+ "repo_explore",
2228
+ [`repoKey: ${repoKey}`, `branch: ${branch}`],
2229
+ debugEnabled
2230
+ )
799
2231
  }
800
2232
  },
801
2233
  }),