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.
- package/AGENTS.md +180 -0
- package/README.md +103 -3
- package/TODO.md +3 -0
- package/index.ts +1590 -158
- package/oh-my-opencode/.github/FUNDING.yml +15 -0
- package/oh-my-opencode/.github/ISSUE_TEMPLATE/bug_report.yml +129 -0
- package/oh-my-opencode/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/oh-my-opencode/.github/ISSUE_TEMPLATE/feature_request.yml +100 -0
- package/oh-my-opencode/.github/ISSUE_TEMPLATE/general.yml +83 -0
- package/oh-my-opencode/.github/assets/google.jpg +0 -0
- package/oh-my-opencode/.github/assets/hero.jpg +0 -0
- package/oh-my-opencode/.github/assets/indent.jpg +0 -0
- package/oh-my-opencode/.github/assets/microsoft.jpg +0 -0
- package/oh-my-opencode/.github/assets/omo.png +0 -0
- package/oh-my-opencode/.github/assets/orchestrator-atlas.png +0 -0
- package/oh-my-opencode/.github/assets/sisyphus.png +0 -0
- package/oh-my-opencode/.github/assets/sisyphuslabs.png +0 -0
- package/oh-my-opencode/.github/pull_request_template.md +34 -0
- package/oh-my-opencode/.github/workflows/ci.yml +138 -0
- package/oh-my-opencode/.github/workflows/cla.yml +41 -0
- package/oh-my-opencode/.github/workflows/lint-workflows.yml +22 -0
- package/oh-my-opencode/.github/workflows/publish.yml +165 -0
- package/oh-my-opencode/.github/workflows/sisyphus-agent.yml +500 -0
- package/oh-my-opencode/.opencode/background-tasks.json +27 -0
- package/oh-my-opencode/.opencode/command/get-unpublished-changes.md +84 -0
- package/oh-my-opencode/.opencode/command/omomomo.md +37 -0
- package/oh-my-opencode/.opencode/command/publish.md +257 -0
- package/oh-my-opencode/AGENTS.md +179 -0
- package/oh-my-opencode/CLA.md +58 -0
- package/oh-my-opencode/CONTRIBUTING.md +268 -0
- package/oh-my-opencode/LICENSE.md +82 -0
- package/oh-my-opencode/README.ja.md +370 -0
- package/oh-my-opencode/README.md +376 -0
- package/oh-my-opencode/README.zh-cn.md +380 -0
- package/oh-my-opencode/assets/oh-my-opencode.schema.json +2171 -0
- package/oh-my-opencode/bin/oh-my-opencode.js +80 -0
- package/oh-my-opencode/bin/platform.js +38 -0
- package/oh-my-opencode/bin/platform.test.ts +148 -0
- package/oh-my-opencode/bun.lock +314 -0
- package/oh-my-opencode/bunfig.toml +2 -0
- package/oh-my-opencode/docs/category-skill-guide.md +200 -0
- package/oh-my-opencode/docs/cli-guide.md +272 -0
- package/oh-my-opencode/docs/configurations.md +654 -0
- package/oh-my-opencode/docs/features.md +550 -0
- package/oh-my-opencode/docs/guide/installation.md +288 -0
- package/oh-my-opencode/docs/guide/overview.md +97 -0
- package/oh-my-opencode/docs/guide/understanding-orchestration-system.md +445 -0
- package/oh-my-opencode/docs/orchestration-guide.md +152 -0
- package/oh-my-opencode/docs/ultrawork-manifesto.md +197 -0
- package/oh-my-opencode/package.json +89 -0
- package/oh-my-opencode/packages/darwin-arm64/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/darwin-arm64/package.json +22 -0
- package/oh-my-opencode/packages/darwin-x64/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/darwin-x64/package.json +22 -0
- package/oh-my-opencode/packages/linux-arm64/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/linux-arm64/package.json +25 -0
- package/oh-my-opencode/packages/linux-arm64-musl/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/linux-arm64-musl/package.json +25 -0
- package/oh-my-opencode/packages/linux-x64/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/linux-x64/package.json +25 -0
- package/oh-my-opencode/packages/linux-x64-musl/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/linux-x64-musl/package.json +25 -0
- package/oh-my-opencode/packages/windows-x64/bin/.gitkeep +0 -0
- package/oh-my-opencode/packages/windows-x64/package.json +22 -0
- package/oh-my-opencode/postinstall.mjs +43 -0
- package/oh-my-opencode/script/build-binaries.ts +103 -0
- package/oh-my-opencode/script/build-schema.ts +28 -0
- package/oh-my-opencode/script/generate-changelog.ts +92 -0
- package/oh-my-opencode/script/publish.ts +344 -0
- package/oh-my-opencode/signatures/cla.json +676 -0
- package/oh-my-opencode/src/agents/AGENTS.md +67 -0
- package/oh-my-opencode/src/agents/atlas.ts +1383 -0
- package/oh-my-opencode/src/agents/dynamic-agent-prompt-builder.ts +400 -0
- package/oh-my-opencode/src/agents/explore.ts +122 -0
- package/oh-my-opencode/src/agents/index.ts +13 -0
- package/oh-my-opencode/src/agents/librarian.ts +326 -0
- package/oh-my-opencode/src/agents/metis.ts +315 -0
- package/oh-my-opencode/src/agents/momus.test.ts +57 -0
- package/oh-my-opencode/src/agents/momus.ts +444 -0
- package/oh-my-opencode/src/agents/multimodal-looker.ts +56 -0
- package/oh-my-opencode/src/agents/oracle.ts +122 -0
- package/oh-my-opencode/src/agents/prometheus-prompt.test.ts +22 -0
- package/oh-my-opencode/src/agents/prometheus-prompt.ts +1196 -0
- package/oh-my-opencode/src/agents/sisyphus-junior.test.ts +232 -0
- package/oh-my-opencode/src/agents/sisyphus-junior.ts +134 -0
- package/oh-my-opencode/src/agents/sisyphus.ts +633 -0
- package/oh-my-opencode/src/agents/types.ts +80 -0
- package/oh-my-opencode/src/agents/utils.test.ts +311 -0
- package/oh-my-opencode/src/agents/utils.ts +240 -0
- package/oh-my-opencode/src/cli/AGENTS.md +91 -0
- package/oh-my-opencode/src/cli/config-manager.test.ts +364 -0
- package/oh-my-opencode/src/cli/config-manager.ts +641 -0
- package/oh-my-opencode/src/cli/doctor/checks/auth.test.ts +114 -0
- package/oh-my-opencode/src/cli/doctor/checks/auth.ts +115 -0
- package/oh-my-opencode/src/cli/doctor/checks/config.test.ts +103 -0
- package/oh-my-opencode/src/cli/doctor/checks/config.ts +123 -0
- package/oh-my-opencode/src/cli/doctor/checks/dependencies.test.ts +152 -0
- package/oh-my-opencode/src/cli/doctor/checks/dependencies.ts +163 -0
- package/oh-my-opencode/src/cli/doctor/checks/gh.test.ts +151 -0
- package/oh-my-opencode/src/cli/doctor/checks/gh.ts +171 -0
- package/oh-my-opencode/src/cli/doctor/checks/index.ts +34 -0
- package/oh-my-opencode/src/cli/doctor/checks/lsp.test.ts +134 -0
- package/oh-my-opencode/src/cli/doctor/checks/lsp.ts +77 -0
- package/oh-my-opencode/src/cli/doctor/checks/mcp.test.ts +115 -0
- package/oh-my-opencode/src/cli/doctor/checks/mcp.ts +128 -0
- package/oh-my-opencode/src/cli/doctor/checks/opencode.test.ts +227 -0
- package/oh-my-opencode/src/cli/doctor/checks/opencode.ts +178 -0
- package/oh-my-opencode/src/cli/doctor/checks/plugin.test.ts +109 -0
- package/oh-my-opencode/src/cli/doctor/checks/plugin.ts +124 -0
- package/oh-my-opencode/src/cli/doctor/checks/version.test.ts +148 -0
- package/oh-my-opencode/src/cli/doctor/checks/version.ts +135 -0
- package/oh-my-opencode/src/cli/doctor/constants.ts +72 -0
- package/oh-my-opencode/src/cli/doctor/formatter.test.ts +218 -0
- package/oh-my-opencode/src/cli/doctor/formatter.ts +140 -0
- package/oh-my-opencode/src/cli/doctor/index.ts +11 -0
- package/oh-my-opencode/src/cli/doctor/runner.test.ts +153 -0
- package/oh-my-opencode/src/cli/doctor/runner.ts +132 -0
- package/oh-my-opencode/src/cli/doctor/types.ts +113 -0
- package/oh-my-opencode/src/cli/get-local-version/formatter.ts +66 -0
- package/oh-my-opencode/src/cli/get-local-version/index.ts +106 -0
- package/oh-my-opencode/src/cli/get-local-version/types.ts +14 -0
- package/oh-my-opencode/src/cli/index.ts +153 -0
- package/oh-my-opencode/src/cli/install.ts +523 -0
- package/oh-my-opencode/src/cli/model-fallback.ts +246 -0
- package/oh-my-opencode/src/cli/run/completion.test.ts +170 -0
- package/oh-my-opencode/src/cli/run/completion.ts +79 -0
- package/oh-my-opencode/src/cli/run/events.test.ts +155 -0
- package/oh-my-opencode/src/cli/run/events.ts +325 -0
- package/oh-my-opencode/src/cli/run/index.ts +2 -0
- package/oh-my-opencode/src/cli/run/runner.ts +159 -0
- package/oh-my-opencode/src/cli/run/types.ts +76 -0
- package/oh-my-opencode/src/cli/types.ts +40 -0
- package/oh-my-opencode/src/config/index.ts +26 -0
- package/oh-my-opencode/src/config/schema.test.ts +444 -0
- package/oh-my-opencode/src/config/schema.ts +339 -0
- package/oh-my-opencode/src/features/AGENTS.md +77 -0
- package/oh-my-opencode/src/features/background-agent/concurrency.test.ts +418 -0
- package/oh-my-opencode/src/features/background-agent/concurrency.ts +137 -0
- package/oh-my-opencode/src/features/background-agent/index.ts +3 -0
- package/oh-my-opencode/src/features/background-agent/manager.test.ts +1928 -0
- package/oh-my-opencode/src/features/background-agent/manager.ts +1335 -0
- package/oh-my-opencode/src/features/background-agent/types.ts +66 -0
- package/oh-my-opencode/src/features/boulder-state/constants.ts +13 -0
- package/oh-my-opencode/src/features/boulder-state/index.ts +3 -0
- package/oh-my-opencode/src/features/boulder-state/storage.test.ts +250 -0
- package/oh-my-opencode/src/features/boulder-state/storage.ts +150 -0
- package/oh-my-opencode/src/features/boulder-state/types.ts +26 -0
- package/oh-my-opencode/src/features/builtin-commands/commands.ts +89 -0
- package/oh-my-opencode/src/features/builtin-commands/index.ts +2 -0
- package/oh-my-opencode/src/features/builtin-commands/templates/init-deep.ts +300 -0
- package/oh-my-opencode/src/features/builtin-commands/templates/ralph-loop.ts +38 -0
- package/oh-my-opencode/src/features/builtin-commands/templates/refactor.ts +619 -0
- package/oh-my-opencode/src/features/builtin-commands/templates/start-work.ts +72 -0
- package/oh-my-opencode/src/features/builtin-commands/types.ts +9 -0
- package/oh-my-opencode/src/features/builtin-skills/frontend-ui-ux/SKILL.md +78 -0
- package/oh-my-opencode/src/features/builtin-skills/git-master/SKILL.md +1105 -0
- package/oh-my-opencode/src/features/builtin-skills/index.ts +2 -0
- package/oh-my-opencode/src/features/builtin-skills/skills.ts +1203 -0
- package/oh-my-opencode/src/features/builtin-skills/types.ts +16 -0
- package/oh-my-opencode/src/features/claude-code-agent-loader/index.ts +2 -0
- package/oh-my-opencode/src/features/claude-code-agent-loader/loader.ts +90 -0
- package/oh-my-opencode/src/features/claude-code-agent-loader/types.ts +17 -0
- package/oh-my-opencode/src/features/claude-code-command-loader/index.ts +2 -0
- package/oh-my-opencode/src/features/claude-code-command-loader/loader.ts +144 -0
- package/oh-my-opencode/src/features/claude-code-command-loader/types.ts +46 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/env-expander.ts +27 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/index.ts +11 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.test.ts +162 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.ts +113 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/transformer.ts +53 -0
- package/oh-my-opencode/src/features/claude-code-mcp-loader/types.ts +42 -0
- package/oh-my-opencode/src/features/claude-code-plugin-loader/index.ts +3 -0
- package/oh-my-opencode/src/features/claude-code-plugin-loader/loader.ts +486 -0
- package/oh-my-opencode/src/features/claude-code-plugin-loader/types.ts +210 -0
- package/oh-my-opencode/src/features/claude-code-session-state/index.ts +1 -0
- package/oh-my-opencode/src/features/claude-code-session-state/state.test.ts +126 -0
- package/oh-my-opencode/src/features/claude-code-session-state/state.ts +37 -0
- package/oh-my-opencode/src/features/context-injector/collector.test.ts +330 -0
- package/oh-my-opencode/src/features/context-injector/collector.ts +85 -0
- package/oh-my-opencode/src/features/context-injector/index.ts +14 -0
- package/oh-my-opencode/src/features/context-injector/injector.test.ts +122 -0
- package/oh-my-opencode/src/features/context-injector/injector.ts +167 -0
- package/oh-my-opencode/src/features/context-injector/types.ts +91 -0
- package/oh-my-opencode/src/features/hook-message-injector/constants.ts +6 -0
- package/oh-my-opencode/src/features/hook-message-injector/index.ts +4 -0
- package/oh-my-opencode/src/features/hook-message-injector/injector.ts +195 -0
- package/oh-my-opencode/src/features/hook-message-injector/types.ts +47 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.test.ts +448 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.ts +180 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/blocking.test.ts +210 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/blocking.ts +62 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/discover-worker.ts +59 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/index.ts +4 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/loader.test.ts +273 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/loader.ts +259 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/merger.ts +267 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.test.ts +267 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.ts +206 -0
- package/oh-my-opencode/src/features/opencode-skill-loader/types.ts +38 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.test.ts +201 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.ts +27 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/index.ts +2 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/manager.test.ts +611 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/manager.ts +520 -0
- package/oh-my-opencode/src/features/skill-mcp-manager/types.ts +14 -0
- package/oh-my-opencode/src/features/task-toast-manager/index.ts +2 -0
- package/oh-my-opencode/src/features/task-toast-manager/manager.test.ts +249 -0
- package/oh-my-opencode/src/features/task-toast-manager/manager.ts +215 -0
- package/oh-my-opencode/src/features/task-toast-manager/types.ts +24 -0
- package/oh-my-opencode/src/hooks/AGENTS.md +73 -0
- package/oh-my-opencode/src/hooks/agent-usage-reminder/constants.ts +54 -0
- package/oh-my-opencode/src/hooks/agent-usage-reminder/index.ts +109 -0
- package/oh-my-opencode/src/hooks/agent-usage-reminder/storage.ts +42 -0
- package/oh-my-opencode/src/hooks/agent-usage-reminder/types.ts +6 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +307 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.ts +485 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/index.ts +151 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/parser.ts +201 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts +33 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +184 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-types.ts +44 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +77 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.ts +250 -0
- package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/types.ts +42 -0
- package/oh-my-opencode/src/hooks/atlas/index.test.ts +953 -0
- package/oh-my-opencode/src/hooks/atlas/index.ts +771 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/constants.ts +12 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/detector.test.ts +296 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/detector.ts +65 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/executor.ts +205 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/index.test.ts +254 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/index.ts +89 -0
- package/oh-my-opencode/src/hooks/auto-slash-command/types.ts +23 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/cache.ts +93 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/checker.test.ts +24 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/checker.ts +284 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/constants.ts +64 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/index.test.ts +254 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/index.ts +260 -0
- package/oh-my-opencode/src/hooks/auto-update-checker/types.ts +29 -0
- package/oh-my-opencode/src/hooks/background-compaction/index.ts +87 -0
- package/oh-my-opencode/src/hooks/background-notification/index.ts +28 -0
- package/oh-my-opencode/src/hooks/background-notification/types.ts +5 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/AGENTS.md +70 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/config-loader.ts +107 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/config.ts +103 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/index.ts +401 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/plugin-config.ts +12 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/post-tool-use.ts +199 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/pre-compact.ts +109 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/pre-tool-use.ts +172 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/stop.ts +118 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/todo.ts +76 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/tool-input-cache.ts +47 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/transcript.ts +252 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/types.ts +204 -0
- package/oh-my-opencode/src/hooks/claude-code-hooks/user-prompt-submit.ts +117 -0
- package/oh-my-opencode/src/hooks/comment-checker/cli.test.ts +68 -0
- package/oh-my-opencode/src/hooks/comment-checker/cli.ts +221 -0
- package/oh-my-opencode/src/hooks/comment-checker/downloader.ts +196 -0
- package/oh-my-opencode/src/hooks/comment-checker/index.ts +171 -0
- package/oh-my-opencode/src/hooks/comment-checker/types.ts +33 -0
- package/oh-my-opencode/src/hooks/compaction-context-injector/index.ts +61 -0
- package/oh-my-opencode/src/hooks/context-window-monitor.ts +99 -0
- package/oh-my-opencode/src/hooks/delegate-task-retry/index.test.ts +119 -0
- package/oh-my-opencode/src/hooks/delegate-task-retry/index.ts +136 -0
- package/oh-my-opencode/src/hooks/directory-agents-injector/constants.ts +9 -0
- package/oh-my-opencode/src/hooks/directory-agents-injector/index.ts +182 -0
- package/oh-my-opencode/src/hooks/directory-agents-injector/storage.ts +48 -0
- package/oh-my-opencode/src/hooks/directory-agents-injector/types.ts +5 -0
- package/oh-my-opencode/src/hooks/directory-readme-injector/constants.ts +9 -0
- package/oh-my-opencode/src/hooks/directory-readme-injector/index.ts +177 -0
- package/oh-my-opencode/src/hooks/directory-readme-injector/storage.ts +48 -0
- package/oh-my-opencode/src/hooks/directory-readme-injector/types.ts +5 -0
- package/oh-my-opencode/src/hooks/edit-error-recovery/index.test.ts +126 -0
- package/oh-my-opencode/src/hooks/edit-error-recovery/index.ts +57 -0
- package/oh-my-opencode/src/hooks/empty-task-response-detector.ts +27 -0
- package/oh-my-opencode/src/hooks/index.ts +32 -0
- package/oh-my-opencode/src/hooks/interactive-bash-session/constants.ts +15 -0
- package/oh-my-opencode/src/hooks/interactive-bash-session/index.ts +262 -0
- package/oh-my-opencode/src/hooks/interactive-bash-session/storage.ts +59 -0
- package/oh-my-opencode/src/hooks/interactive-bash-session/types.ts +11 -0
- package/oh-my-opencode/src/hooks/keyword-detector/constants.ts +300 -0
- package/oh-my-opencode/src/hooks/keyword-detector/detector.ts +52 -0
- package/oh-my-opencode/src/hooks/keyword-detector/index.test.ts +529 -0
- package/oh-my-opencode/src/hooks/keyword-detector/index.ts +100 -0
- package/oh-my-opencode/src/hooks/keyword-detector/types.ts +4 -0
- package/oh-my-opencode/src/hooks/non-interactive-env/constants.ts +70 -0
- package/oh-my-opencode/src/hooks/non-interactive-env/detector.ts +19 -0
- package/oh-my-opencode/src/hooks/non-interactive-env/index.test.ts +323 -0
- package/oh-my-opencode/src/hooks/non-interactive-env/index.ts +63 -0
- package/oh-my-opencode/src/hooks/non-interactive-env/types.ts +3 -0
- package/oh-my-opencode/src/hooks/prometheus-md-only/constants.ts +32 -0
- package/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts +488 -0
- package/oh-my-opencode/src/hooks/prometheus-md-only/index.ts +136 -0
- package/oh-my-opencode/src/hooks/ralph-loop/constants.ts +5 -0
- package/oh-my-opencode/src/hooks/ralph-loop/index.test.ts +835 -0
- package/oh-my-opencode/src/hooks/ralph-loop/index.ts +417 -0
- package/oh-my-opencode/src/hooks/ralph-loop/storage.ts +115 -0
- package/oh-my-opencode/src/hooks/ralph-loop/types.ts +19 -0
- package/oh-my-opencode/src/hooks/rules-injector/constants.ts +30 -0
- package/oh-my-opencode/src/hooks/rules-injector/finder.test.ts +381 -0
- package/oh-my-opencode/src/hooks/rules-injector/finder.ts +263 -0
- package/oh-my-opencode/src/hooks/rules-injector/index.ts +223 -0
- package/oh-my-opencode/src/hooks/rules-injector/matcher.ts +63 -0
- package/oh-my-opencode/src/hooks/rules-injector/parser.test.ts +226 -0
- package/oh-my-opencode/src/hooks/rules-injector/parser.ts +211 -0
- package/oh-my-opencode/src/hooks/rules-injector/storage.ts +59 -0
- package/oh-my-opencode/src/hooks/rules-injector/types.ts +57 -0
- package/oh-my-opencode/src/hooks/session-notification-utils.ts +140 -0
- package/oh-my-opencode/src/hooks/session-notification.test.ts +361 -0
- package/oh-my-opencode/src/hooks/session-notification.ts +330 -0
- package/oh-my-opencode/src/hooks/session-recovery/constants.ts +10 -0
- package/oh-my-opencode/src/hooks/session-recovery/index.test.ts +223 -0
- package/oh-my-opencode/src/hooks/session-recovery/index.ts +435 -0
- package/oh-my-opencode/src/hooks/session-recovery/storage.ts +390 -0
- package/oh-my-opencode/src/hooks/session-recovery/types.ts +98 -0
- package/oh-my-opencode/src/hooks/start-work/index.test.ts +402 -0
- package/oh-my-opencode/src/hooks/start-work/index.ts +242 -0
- package/oh-my-opencode/src/hooks/task-resume-info/index.ts +36 -0
- package/oh-my-opencode/src/hooks/think-mode/detector.ts +57 -0
- package/oh-my-opencode/src/hooks/think-mode/index.test.ts +353 -0
- package/oh-my-opencode/src/hooks/think-mode/index.ts +89 -0
- package/oh-my-opencode/src/hooks/think-mode/switcher.test.ts +461 -0
- package/oh-my-opencode/src/hooks/think-mode/switcher.ts +222 -0
- package/oh-my-opencode/src/hooks/think-mode/types.ts +21 -0
- package/oh-my-opencode/src/hooks/thinking-block-validator/index.ts +171 -0
- package/oh-my-opencode/src/hooks/todo-continuation-enforcer.test.ts +876 -0
- package/oh-my-opencode/src/hooks/todo-continuation-enforcer.ts +480 -0
- package/oh-my-opencode/src/hooks/tool-output-truncator.test.ts +168 -0
- package/oh-my-opencode/src/hooks/tool-output-truncator.ts +61 -0
- package/oh-my-opencode/src/index.ts +589 -0
- package/oh-my-opencode/src/mcp/AGENTS.md +70 -0
- package/oh-my-opencode/src/mcp/context7.ts +6 -0
- package/oh-my-opencode/src/mcp/grep-app.ts +6 -0
- package/oh-my-opencode/src/mcp/index.test.ts +86 -0
- package/oh-my-opencode/src/mcp/index.ts +32 -0
- package/oh-my-opencode/src/mcp/types.ts +9 -0
- package/oh-my-opencode/src/mcp/websearch.ts +10 -0
- package/oh-my-opencode/src/plugin-config.test.ts +119 -0
- package/oh-my-opencode/src/plugin-config.ts +135 -0
- package/oh-my-opencode/src/plugin-handlers/config-handler.test.ts +103 -0
- package/oh-my-opencode/src/plugin-handlers/config-handler.ts +399 -0
- package/oh-my-opencode/src/plugin-handlers/index.ts +1 -0
- package/oh-my-opencode/src/plugin-state.ts +30 -0
- package/oh-my-opencode/src/shared/AGENTS.md +63 -0
- package/oh-my-opencode/src/shared/agent-tool-restrictions.ts +44 -0
- package/oh-my-opencode/src/shared/agent-variant.test.ts +83 -0
- package/oh-my-opencode/src/shared/agent-variant.ts +40 -0
- package/oh-my-opencode/src/shared/claude-config-dir.test.ts +60 -0
- package/oh-my-opencode/src/shared/claude-config-dir.ts +11 -0
- package/oh-my-opencode/src/shared/command-executor.ts +225 -0
- package/oh-my-opencode/src/shared/config-errors.ts +18 -0
- package/oh-my-opencode/src/shared/config-path.ts +47 -0
- package/oh-my-opencode/src/shared/data-path.ts +22 -0
- package/oh-my-opencode/src/shared/deep-merge.test.ts +336 -0
- package/oh-my-opencode/src/shared/deep-merge.ts +53 -0
- package/oh-my-opencode/src/shared/dynamic-truncator.ts +193 -0
- package/oh-my-opencode/src/shared/external-plugin-detector.test.ts +133 -0
- package/oh-my-opencode/src/shared/external-plugin-detector.ts +132 -0
- package/oh-my-opencode/src/shared/file-reference-resolver.ts +85 -0
- package/oh-my-opencode/src/shared/file-utils.ts +40 -0
- package/oh-my-opencode/src/shared/first-message-variant.test.ts +32 -0
- package/oh-my-opencode/src/shared/first-message-variant.ts +28 -0
- package/oh-my-opencode/src/shared/frontmatter.test.ts +262 -0
- package/oh-my-opencode/src/shared/frontmatter.ts +31 -0
- package/oh-my-opencode/src/shared/hook-disabled.ts +22 -0
- package/oh-my-opencode/src/shared/index.ts +29 -0
- package/oh-my-opencode/src/shared/jsonc-parser.test.ts +266 -0
- package/oh-my-opencode/src/shared/jsonc-parser.ts +66 -0
- package/oh-my-opencode/src/shared/logger.ts +20 -0
- package/oh-my-opencode/src/shared/migration.test.ts +602 -0
- package/oh-my-opencode/src/shared/migration.ts +191 -0
- package/oh-my-opencode/src/shared/model-resolver.test.ts +101 -0
- package/oh-my-opencode/src/shared/model-resolver.ts +35 -0
- package/oh-my-opencode/src/shared/model-sanitizer.ts +12 -0
- package/oh-my-opencode/src/shared/opencode-config-dir.test.ts +318 -0
- package/oh-my-opencode/src/shared/opencode-config-dir.ts +142 -0
- package/oh-my-opencode/src/shared/opencode-version.test.ts +223 -0
- package/oh-my-opencode/src/shared/opencode-version.ts +72 -0
- package/oh-my-opencode/src/shared/pattern-matcher.ts +29 -0
- package/oh-my-opencode/src/shared/permission-compat.test.ts +134 -0
- package/oh-my-opencode/src/shared/permission-compat.ts +77 -0
- package/oh-my-opencode/src/shared/session-cursor.test.ts +66 -0
- package/oh-my-opencode/src/shared/session-cursor.ts +85 -0
- package/oh-my-opencode/src/shared/shell-env.test.ts +278 -0
- package/oh-my-opencode/src/shared/shell-env.ts +111 -0
- package/oh-my-opencode/src/shared/snake-case.ts +49 -0
- package/oh-my-opencode/src/shared/system-directive.ts +40 -0
- package/oh-my-opencode/src/shared/tool-name.ts +26 -0
- package/oh-my-opencode/src/shared/zip-extractor.ts +83 -0
- package/oh-my-opencode/src/tools/AGENTS.md +74 -0
- package/oh-my-opencode/src/tools/ast-grep/cli.ts +230 -0
- package/oh-my-opencode/src/tools/ast-grep/constants.ts +261 -0
- package/oh-my-opencode/src/tools/ast-grep/downloader.ts +128 -0
- package/oh-my-opencode/src/tools/ast-grep/index.ts +13 -0
- package/oh-my-opencode/src/tools/ast-grep/tools.ts +112 -0
- package/oh-my-opencode/src/tools/ast-grep/types.ts +61 -0
- package/oh-my-opencode/src/tools/ast-grep/utils.ts +102 -0
- package/oh-my-opencode/src/tools/background-task/constants.ts +7 -0
- package/oh-my-opencode/src/tools/background-task/index.ts +7 -0
- package/oh-my-opencode/src/tools/background-task/tools.ts +479 -0
- package/oh-my-opencode/src/tools/background-task/types.ts +16 -0
- package/oh-my-opencode/src/tools/call-omo-agent/constants.ts +7 -0
- package/oh-my-opencode/src/tools/call-omo-agent/index.ts +3 -0
- package/oh-my-opencode/src/tools/call-omo-agent/tools.ts +338 -0
- package/oh-my-opencode/src/tools/call-omo-agent/types.ts +27 -0
- package/oh-my-opencode/src/tools/delegate-task/constants.ts +205 -0
- package/oh-my-opencode/src/tools/delegate-task/index.ts +3 -0
- package/oh-my-opencode/src/tools/delegate-task/tools.test.ts +1575 -0
- package/oh-my-opencode/src/tools/delegate-task/tools.ts +885 -0
- package/oh-my-opencode/src/tools/delegate-task/types.ts +9 -0
- package/oh-my-opencode/src/tools/glob/cli.test.ts +158 -0
- package/oh-my-opencode/src/tools/glob/cli.ts +191 -0
- package/oh-my-opencode/src/tools/glob/constants.ts +12 -0
- package/oh-my-opencode/src/tools/glob/index.ts +3 -0
- package/oh-my-opencode/src/tools/glob/tools.ts +41 -0
- package/oh-my-opencode/src/tools/glob/types.ts +22 -0
- package/oh-my-opencode/src/tools/glob/utils.ts +26 -0
- package/oh-my-opencode/src/tools/grep/cli.ts +229 -0
- package/oh-my-opencode/src/tools/grep/constants.ts +127 -0
- package/oh-my-opencode/src/tools/grep/downloader.test.ts +103 -0
- package/oh-my-opencode/src/tools/grep/downloader.ts +145 -0
- package/oh-my-opencode/src/tools/grep/index.ts +3 -0
- package/oh-my-opencode/src/tools/grep/tools.ts +40 -0
- package/oh-my-opencode/src/tools/grep/types.ts +39 -0
- package/oh-my-opencode/src/tools/grep/utils.ts +53 -0
- package/oh-my-opencode/src/tools/index.ts +72 -0
- package/oh-my-opencode/src/tools/interactive-bash/constants.ts +18 -0
- package/oh-my-opencode/src/tools/interactive-bash/index.ts +4 -0
- package/oh-my-opencode/src/tools/interactive-bash/tools.ts +126 -0
- package/oh-my-opencode/src/tools/interactive-bash/utils.ts +71 -0
- package/oh-my-opencode/src/tools/look-at/constants.ts +3 -0
- package/oh-my-opencode/src/tools/look-at/index.ts +3 -0
- package/oh-my-opencode/src/tools/look-at/tools.test.ts +73 -0
- package/oh-my-opencode/src/tools/look-at/tools.ts +173 -0
- package/oh-my-opencode/src/tools/look-at/types.ts +4 -0
- package/oh-my-opencode/src/tools/lsp/client.ts +596 -0
- package/oh-my-opencode/src/tools/lsp/config.test.ts +130 -0
- package/oh-my-opencode/src/tools/lsp/config.ts +285 -0
- package/oh-my-opencode/src/tools/lsp/constants.ts +390 -0
- package/oh-my-opencode/src/tools/lsp/index.ts +7 -0
- package/oh-my-opencode/src/tools/lsp/tools.ts +261 -0
- package/oh-my-opencode/src/tools/lsp/types.ts +124 -0
- package/oh-my-opencode/src/tools/lsp/utils.ts +406 -0
- package/oh-my-opencode/src/tools/session-manager/constants.ts +97 -0
- package/oh-my-opencode/src/tools/session-manager/index.ts +3 -0
- package/oh-my-opencode/src/tools/session-manager/storage.test.ts +315 -0
- package/oh-my-opencode/src/tools/session-manager/storage.ts +238 -0
- package/oh-my-opencode/src/tools/session-manager/tools.test.ts +124 -0
- package/oh-my-opencode/src/tools/session-manager/tools.ts +146 -0
- package/oh-my-opencode/src/tools/session-manager/types.ts +99 -0
- package/oh-my-opencode/src/tools/session-manager/utils.test.ts +160 -0
- package/oh-my-opencode/src/tools/session-manager/utils.ts +199 -0
- package/oh-my-opencode/src/tools/skill/constants.ts +8 -0
- package/oh-my-opencode/src/tools/skill/index.ts +3 -0
- package/oh-my-opencode/src/tools/skill/tools.test.ts +239 -0
- package/oh-my-opencode/src/tools/skill/tools.ts +200 -0
- package/oh-my-opencode/src/tools/skill/types.ts +31 -0
- package/oh-my-opencode/src/tools/skill-mcp/constants.ts +3 -0
- package/oh-my-opencode/src/tools/skill-mcp/index.ts +3 -0
- package/oh-my-opencode/src/tools/skill-mcp/tools.test.ts +215 -0
- package/oh-my-opencode/src/tools/skill-mcp/tools.ts +172 -0
- package/oh-my-opencode/src/tools/skill-mcp/types.ts +8 -0
- package/oh-my-opencode/src/tools/slashcommand/index.ts +2 -0
- package/oh-my-opencode/src/tools/slashcommand/tools.ts +252 -0
- package/oh-my-opencode/src/tools/slashcommand/types.ts +28 -0
- package/oh-my-opencode/test-setup.ts +6 -0
- package/oh-my-opencode/tsconfig.json +20 -0
- package/package.json +1 -1
- package/src/__tests__/git.test.ts +7 -2
- package/src/__tests__/manifest.test.ts +5 -5
- package/src/agents/repo-explorer.ts +2 -1
- package/src/git.ts +18 -3
- 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
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
76
|
-
const repoKey = `${spec.owner}/${spec.repo}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1025
|
+
let output = `## ${statusText}
|
|
140
1026
|
|
|
141
|
-
**Repository**: ${
|
|
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 ||
|
|
170
|
-
const repoKey = `${spec.owner}/${spec.repo}
|
|
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
|
-
|
|
1079
|
+
let output = `## Repository not found
|
|
177
1080
|
|
|
178
|
-
Repository \`${
|
|
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
|
-
|
|
1108
|
+
let filePaths: string[] = []
|
|
187
1109
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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 ${
|
|
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,
|
|
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
|
|
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
|
|
263
|
-
output += "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
344
|
-
const repoKey =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
391
|
-
const repoKey = `${spec.owner}/${spec.repo}
|
|
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
|
-
|
|
1372
|
+
let output = `## Repository not found
|
|
398
1373
|
|
|
399
|
-
Repository \`${
|
|
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**: ${
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
Failed to get git status for ${
|
|
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
|
-
|
|
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
|
-
|
|
1436
|
+
let output = `## Repository Updated
|
|
440
1437
|
|
|
441
|
-
**Repository**: ${
|
|
1438
|
+
**Repository**: ${repoKey}
|
|
442
1439
|
**Path**: ${entry.path}
|
|
443
|
-
**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
|
-
|
|
1453
|
+
let output = `## Update Failed
|
|
450
1454
|
|
|
451
|
-
Failed to update ${
|
|
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'
|
|
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
|
|
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
|
-
|
|
1492
|
+
let output = `## Repository not found
|
|
483
1493
|
|
|
484
|
-
Repository \`${
|
|
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
|
-
|
|
1521
|
+
let output = `## Local Repository Unregistered
|
|
505
1522
|
|
|
506
|
-
**Repository**: ${
|
|
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
|
-
|
|
1539
|
+
let output = `## Confirmation Required
|
|
516
1540
|
|
|
517
|
-
**Repository**: ${
|
|
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: "${
|
|
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
|
-
|
|
1568
|
+
let output = `## Cached Repository Deleted
|
|
538
1569
|
|
|
539
|
-
**Repository**: ${
|
|
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: "${
|
|
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
|
-
|
|
1594
|
+
let output = `## Deletion Failed
|
|
557
1595
|
|
|
558
|
-
Failed to delete ${
|
|
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
|
-
|
|
601
|
-
if (config?.localSearchPaths?.length) {
|
|
1645
|
+
if (localSearchPaths.length > 0) {
|
|
602
1646
|
try {
|
|
603
1647
|
const localResults = await findLocalRepoByName(
|
|
604
|
-
|
|
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
|
-
|
|
1739
|
+
repo_pick_dir: tool({
|
|
690
1740
|
description:
|
|
691
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
|
704
|
-
const branch = spec.branch || "main"
|
|
705
|
-
const repoKey = `${spec.owner}/${spec.repo}@${branch}`
|
|
1830
|
+
const explicitRepos = args.repos?.filter(Boolean) ?? []
|
|
706
1831
|
|
|
707
|
-
|
|
708
|
-
|
|
1832
|
+
if (explicitRepos.length === 0 && !args.query) {
|
|
1833
|
+
return `## Missing repository query
|
|
709
1834
|
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1898
|
+
debugInfo.candidates = [candidate]
|
|
1899
|
+
debugInfo.selectedTargets.push({
|
|
1900
|
+
repoKey: candidate.key,
|
|
1901
|
+
branch: candidate.branch ?? defaultBranch,
|
|
1902
|
+
})
|
|
1903
|
+
}
|
|
735
1904
|
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1984
|
+
const manifest = await loadManifest()
|
|
1985
|
+
let allowGithub = false
|
|
745
1986
|
|
|
746
|
-
|
|
1987
|
+
if (args.query?.includes("/")) {
|
|
1988
|
+
try {
|
|
1989
|
+
parseRepoSpec(args.query)
|
|
1990
|
+
allowGithub = true
|
|
1991
|
+
} catch {
|
|
1992
|
+
allowGithub = false
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
747
1995
|
|
|
748
|
-
|
|
1996
|
+
debugInfo.allowGithub = allowGithub
|
|
749
1997
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1998
|
+
const { candidates, exactRepoKey, branchOverride } = await resolveCandidates(
|
|
1999
|
+
args.query!,
|
|
2000
|
+
localSearchPaths,
|
|
2001
|
+
manifest,
|
|
2002
|
+
defaultBranch,
|
|
2003
|
+
allowGithub
|
|
2004
|
+
)
|
|
755
2005
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
782
|
-
return `## Exploration failed
|
|
2116
|
+
await touchRepoAccess(target.repoKey)
|
|
783
2117
|
|
|
784
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return
|
|
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
|
-
|
|
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
|
}),
|