opencode-resolve 0.1.8 → 0.1.9
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/README.ko.md +27 -11
- package/README.md +27 -11
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +40 -3
- package/dist/config.js +8 -2
- package/dist/hooks/index.js +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/types.d.ts +2 -2
- package/opencode-resolve.example.json +3 -0
- package/opencode-resolve.reference.jsonc +38 -17
- package/package.json +5 -1
- package/scripts/install-git-hooks.mjs +28 -0
- package/scripts/install-local.mjs +64 -4
- package/scripts/postinstall.mjs +390 -34
package/README.ko.md
CHANGED
|
@@ -156,11 +156,11 @@ npm install -g opencode-resolve
|
|
|
156
156
|
|
|
157
157
|
1. `opencode-resolve`를 `~/.config/opencode/opencode.json`의 `plugin` 배열에 추가 (이미 없는 경우).
|
|
158
158
|
2. 파일이 존재하지 않는 경우, 현재 모델 프로바이더에 맞게 적응된 `~/.config/opencode/resolve.json`을 생성:
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
159
|
+
- **인터랙티브 터미널** → 항상 `mix` / `gpt` / `glm`을 묻고, `bronze` / `silver` / `gold` 3티어 모델을 고르게 합니다. `mix`에서는 전용 `codex`, `glm` primary 에이전트를 켤지도 묻습니다.
|
|
160
|
+
- **비대화형 설치** → 모델 pinning을 추측하지 않습니다. `profile: "mix"`, `models: {}`를 쓰고 세 primary 경로(`resolver`, `codex`, `glm`)를 활성화하므로 이후 명시적으로 모델을 고르면 됩니다.
|
|
161
|
+
- **GLM/ZAI 모델 감지** → 비밀값 없는 로컬 ZAI MCP 부트스트랩은 계속 추가합니다.
|
|
162
162
|
|
|
163
|
-
기존 `resolve.json` 파일은
|
|
163
|
+
기존 `resolve.json` 파일은 **동의 없이 덮어쓰지 않습니다**. 재설치 시 인터랙티브 환경이면 기존 파일을 업데이트할지, 백업 후 fresh setup을 다시 돌릴지 묻습니다. 비대화형 자동화에서는 `OPENCODE_RESOLVE_REINSTALL=fresh` 또는 `OPENCODE_RESOLVE_REINSTALL=update`를 설정하세요.
|
|
164
164
|
|
|
165
165
|
자동 등록을 건너뛰려면:
|
|
166
166
|
|
|
@@ -552,7 +552,7 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
|
|
|
552
552
|
| `autoApprove` | `boolean` | `true` | 하위 호환/가독성 플래그. 현재 동작은 내장 기본 권한과 `permission.ask` bash 분류기가 제어하며, 이 플래그가 권한을 재작성하지 않음. |
|
|
553
553
|
| `autoUpdate` | `boolean` | `true` | npm 버전 확인과 OpenCode 플러그인 캐시 갱신 알림을 best-effort로 수행. 비활성화하려면 false. |
|
|
554
554
|
| `maxParallelSubagents` | `positive integer` | _미설정_ | 동시 coder 수에 대한 선택적 프롬프트 수준 상한. 미설정 시 resolver는 soft fan-out 가이드를 사용하고 rate-limit 에러에 backoff. GLM profile도 사용자가 설정하지 않는 한 hard cap을 걸지 않음. |
|
|
555
|
-
| `models` | `object` | `{}` | 별칭 맵. 키는 에이전트 이름 또는 `fast
|
|
555
|
+
| `models` | `object` | `{}` | 별칭 맵. 키는 에이전트 이름 또는 `bronze`/`silver`/`gold`, `gpt-*`, `glm-*`, `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt` 같은 별칭. 값은 모델 id 또는 다른 별칭. |
|
|
556
556
|
| `agents` | `object` | `{}` | 에이전트별 재정의 (아래 참조). |
|
|
557
557
|
| `config` | `string` | _없음_ | 설정 파일의 사용자 정의 경로 (프로젝트 상대경로 또는 절대경로). |
|
|
558
558
|
|
|
@@ -647,6 +647,7 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
|
|
|
647
647
|
- 이미 설정한 키는 **절대** 수정하지 않음.
|
|
648
648
|
- `enabled` 목록, `models` 맵, 또는 `agents` 재정의를 **절대** 다시 쓰지 않음.
|
|
649
649
|
- `enabled`가 설정되어 있고 `"resolver"`를 포함하지 않으면, 추가를 제안하는 한 줄 팁을 출력. 파일은 그대로 유지.
|
|
650
|
+
- 인터랙티브 재설치에서는 기존 파일을 업데이트할지, 백업 후 fresh setup을 다시 돌릴지 물음. 비대화형 자동화에서는 fresh 재설치에 `OPENCODE_RESOLVE_REINSTALL=fresh`, 보존 마이그레이션에 `OPENCODE_RESOLVE_REINSTALL=update` 사용.
|
|
650
651
|
|
|
651
652
|
### 적응형 최초 설치 프리셋
|
|
652
653
|
|
|
@@ -654,12 +655,12 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
|
|
|
654
655
|
|
|
655
656
|
| 감지된 프로바이더 | 프리셋 |
|
|
656
657
|
|---|---|
|
|
657
|
-
|
|
|
658
|
-
|
|
|
659
|
-
|
|
|
660
|
-
|
|
|
658
|
+
| 인터랙티브 터미널 | 항상 `mix` / `gpt` / `glm`을 묻고 3티어 모델을 고르게 하며, `mix`에서는 `codex`와 `glm` primary 에이전트 활성화도 묻습니다 |
|
|
659
|
+
| 비대화형 설치 | 모델 pinning을 추측하지 않음; `profile: "mix"`, `models: {}`를 쓰고 `resolver`, `codex`, `glm` primary 경로를 활성화 |
|
|
660
|
+
| 레거시 opt-in | `OPENCODE_RESOLVE_AUTO_PRESET=1`을 설정하면 비대화형 provider-adapted 프리셋 허용 |
|
|
661
|
+
| GLM/ZAI 감지 | API 키를 복사하지 않고 ZAI MCP 부트스트랩 추가 |
|
|
661
662
|
|
|
662
|
-
언제든지 프리셋을 변경하려면 `resolve.json`의 `models`를 직접 편집하거나,
|
|
663
|
+
언제든지 프리셋을 변경하려면 `resolve.json`의 `models`를 직접 편집하거나, `OPENCODE_RESOLVE_REINSTALL=fresh`로 재설치하세요.
|
|
663
664
|
|
|
664
665
|
마이그레이션을 완전히 건너뛰려면:
|
|
665
666
|
|
|
@@ -748,6 +749,9 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
|
|
|
748
749
|
| `strong` | 강력한/비싼 모델의 프로바이더 중립적 별칭 |
|
|
749
750
|
| `mini` | 미니/효율적 모델의 프로바이더 중립적 별칭 |
|
|
750
751
|
| `codex` | 코덱스 스타일 코딩 모델의 프로바이더 중립적 별칭 |
|
|
752
|
+
| `bronze` / `silver` / `gold` | scout / coder / reasoner 3티어 별칭 |
|
|
753
|
+
| `gpt-bronze` / `gpt-silver` / `gpt-gold` | mixed 설정의 GPT/Codex 전용 3티어 별칭 |
|
|
754
|
+
| `glm-bronze` / `glm-silver` / `glm-gold` | mixed 설정의 GLM 전용 3티어 별칭 |
|
|
751
755
|
| `quick` | 레거시 별칭 (`fast`와 동등) |
|
|
752
756
|
| `deep` | 레거시 별칭 (`strong`과 동등) |
|
|
753
757
|
| `glm` | 레거시 별칭 (하위 호환성) |
|
|
@@ -762,6 +766,8 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
|
|
|
762
766
|
| 에이전트 | 기본 | 모드 | Edit | Bash | WebFetch | 용도 |
|
|
763
767
|
|---|:---:|---|---|---|---|---|
|
|
764
768
|
| `resolver` | 예 (핵심) | `all` | allow | ask (분류기 라우팅) | allow | 컨텍스트 효율적 오케스트레이터. 작업을 검증된 체크포인트로 분해, coder 디스패치, 각각 검증, 반복 회복 실패 시 차단 사항 보고. |
|
|
769
|
+
| `codex` | 아니오 | `all` | allow | ask (분류기 라우팅) | allow | `resolver`와 같은 검증 resolve-loop 스타일의 Codex 최적화 primary. 최초 설치 GPT/mix 프리셋 또는 명시 설정으로 활성화. |
|
|
770
|
+
| `glm` | 아니오 | `all` | allow | ask (분류기 라우팅) | allow | `resolver`와 같은 검증 resolve-loop 스타일의 GLM 최적화 primary. 최초 설치 GLM/mix 프리셋 또는 명시 설정으로 활성화. |
|
|
765
771
|
| `coder` | 예 (핵심) | `subagent` | allow | ask (분류기 라우팅) | allow | 집중된 구현자. 가장 작은 올바른 패치. 필요한 파일만 읽음. |
|
|
766
772
|
| `explorer` | 예 (서브에이전트) | `subagent` | **deny** | **deny** | allow | 내부 빠른 코드베이스 스카우트. Resolver가 범위가 불명확할 때만 디스패치; 좁은 범위는 로컬 read/grep/glob 선호. |
|
|
767
773
|
| `reviewer` | 예 (서브에이전트) | `subagent` | **deny** | **deny** | allow | 내부 검증 갭 감사자. 비사소한 변경에 검증 갭이 있을 때 디스패치. |
|
|
@@ -784,7 +790,7 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
|
|
|
784
790
|
|
|
785
791
|
지원되는 권한 값: `ask`, `allow`, `deny`.
|
|
786
792
|
|
|
787
|
-
지원되는 모델 별칭 키: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, 그리고 지원되는 모든 에이전트 이름. 별칭은 `models`에 정의된 경우에만 해석됩니다.
|
|
793
|
+
지원되는 모델 별칭 키: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, `bronze`, `silver`, `gold`, `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`, `glm-gold`, 그리고 지원되는 모든 에이전트 이름. 별칭은 `models`에 정의된 경우에만 해석됩니다.
|
|
788
794
|
|
|
789
795
|
`preserveNative`은 가독성을 위해 허용되지만, 네이티브 `plan`과 `build`는 항상 보존됩니다. 플러그인은 빌트인 OpenCode 에이전트를 절대 다시 쓰지 않습니다.
|
|
790
796
|
|
|
@@ -875,12 +881,20 @@ opencode plugin opencode-resolve --global --force
|
|
|
875
881
|
|
|
876
882
|
```sh
|
|
877
883
|
npm install
|
|
884
|
+
npm run hooks:install
|
|
878
885
|
npm test
|
|
879
886
|
npm run install:local
|
|
880
887
|
```
|
|
881
888
|
|
|
882
889
|
`install:local`은 플러그인을 빌드하고, OpenCode 전역 플러그인 디렉토리에 링크하며, `~/.config/opencode/resolve.json`이 없으면 생성합니다.
|
|
883
890
|
|
|
891
|
+
`hooks:install`은 이 checkout의 `core.hooksPath`를 `.githooks`로 설정합니다. 추적되는 훅은 의도적으로 빡세게 동작합니다:
|
|
892
|
+
|
|
893
|
+
| 훅 | 게이트 |
|
|
894
|
+
|---|---|
|
|
895
|
+
| `pre-commit` | `npm run typecheck`, `npm test`, `npm run coverage` |
|
|
896
|
+
| `pre-push` | 전체 `pre-commit` 게이트와 `npm pack --dry-run`을 통과해야 원격에 반영 |
|
|
897
|
+
|
|
884
898
|
수동 로컬 설치:
|
|
885
899
|
|
|
886
900
|
```sh
|
|
@@ -900,6 +914,7 @@ ln -sf "$PWD/dist/index.js" ~/.config/opencode/plugins/opencode-resolve.js
|
|
|
900
914
|
```sh
|
|
901
915
|
npm run typecheck
|
|
902
916
|
npm test
|
|
917
|
+
npm run coverage
|
|
903
918
|
npm run build
|
|
904
919
|
```
|
|
905
920
|
|
|
@@ -910,6 +925,7 @@ npm run build
|
|
|
910
925
|
```sh
|
|
911
926
|
npm run typecheck
|
|
912
927
|
npm test
|
|
928
|
+
npm run coverage
|
|
913
929
|
npm audit --audit-level=moderate
|
|
914
930
|
npm publish --dry-run
|
|
915
931
|
```
|
package/README.md
CHANGED
|
@@ -156,11 +156,11 @@ The `postinstall` script automatically:
|
|
|
156
156
|
|
|
157
157
|
1. Adds `opencode-resolve` to `~/.config/opencode/opencode.json` `plugin` array (if not already present).
|
|
158
158
|
2. Creates `~/.config/opencode/resolve.json` adapted to your current model provider when the file does not exist:
|
|
159
|
-
- **
|
|
160
|
-
- **
|
|
161
|
-
- **
|
|
159
|
+
- **Interactive terminal** → always asks for `mix` / `gpt` / `glm`, then lets you pick three-tier `bronze` / `silver` / `gold` models. In `mix`, it also asks whether to enable dedicated `codex` and `glm` primary agents.
|
|
160
|
+
- **Non-interactive install** → does not guess model pinning. It writes `profile: "mix"`, leaves `models: {}`, and enables the three primary routes (`resolver`, `codex`, `glm`) so you can pin models explicitly afterward.
|
|
161
|
+
- **GLM/ZAI model detected** → still adds the non-secret local ZAI MCP bootstrap.
|
|
162
162
|
|
|
163
|
-
Existing `resolve.json` files are **never overwritten
|
|
163
|
+
Existing `resolve.json` files are **never overwritten without consent**. On reinstall, interactive setup asks whether to update the existing file or back it up and run fresh setup. For non-interactive automation, set `OPENCODE_RESOLVE_REINSTALL=fresh` or `OPENCODE_RESOLVE_REINSTALL=update`.
|
|
164
164
|
|
|
165
165
|
To skip automatic registration:
|
|
166
166
|
|
|
@@ -552,7 +552,7 @@ Every accepted top-level option:
|
|
|
552
552
|
| `autoApprove` | `boolean` | `true` | Compatibility/readability flag. Current behavior is controlled by built-in base permissions and the `permission.ask` bash classifier; the flag does not rewrite permissions. |
|
|
553
553
|
| `autoUpdate` | `boolean` | `true` | Best-effort npm version check and OpenCode plugin cache refresh notice. Set false to disable. |
|
|
554
554
|
| `maxParallelSubagents` | `positive integer` | _unset_ | Optional prompt-level cap on simultaneous coders. When unset, the resolver uses soft fan-out guidance and backs off on rate-limit errors. GLM profile does not impose a hard cap unless you set one. |
|
|
555
|
-
| `models` | `object` | `{}` | Alias map. Keys are agent names or `fast
|
|
555
|
+
| `models` | `object` | `{}` | Alias map. Keys are agent names or provider-neutral aliases including `bronze`/`silver`/`gold`, `gpt-*`, `glm-*`, `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`. Values are model ids or other aliases. |
|
|
556
556
|
| `agents` | `object` | `{}` | Per-agent overrides (see below). |
|
|
557
557
|
| `config` | `string` | _none_ | Custom path to a config file (relative to the project or absolute). |
|
|
558
558
|
|
|
@@ -647,6 +647,7 @@ When you upgrade to a newer version of `opencode-resolve`, the `postinstall` scr
|
|
|
647
647
|
- **Never** modifies keys you have already set.
|
|
648
648
|
- **Never** rewrites your `enabled` list, `models` map, or `agents` overrides.
|
|
649
649
|
- If `enabled` is set and does not include `"resolver"`, prints a one-line tip suggesting you add it. Your file is left untouched.
|
|
650
|
+
- In an interactive reinstall, asks whether to update the existing file or back it up and run fresh setup. In non-interactive automation, use `OPENCODE_RESOLVE_REINSTALL=fresh` for a fresh reinstall or `OPENCODE_RESOLVE_REINSTALL=update` to preserve and migrate.
|
|
650
651
|
|
|
651
652
|
### Adaptive first-install preset
|
|
652
653
|
|
|
@@ -654,12 +655,12 @@ When `resolve.json` does **not** exist, postinstall inspects your OpenCode model
|
|
|
654
655
|
|
|
655
656
|
| Detected provider | Preset |
|
|
656
657
|
|---|---|
|
|
657
|
-
|
|
|
658
|
-
|
|
|
659
|
-
|
|
|
660
|
-
|
|
|
658
|
+
| Interactive terminal | Always prompts for `mix` / `gpt` / `glm`, asks for three-tier model picks, and in `mix` asks whether to enable `codex` and `glm` primary agents |
|
|
659
|
+
| Non-interactive install | Does not guess model pinning; writes `profile: "mix"`, `models: {}`, and enables `resolver`, `codex`, and `glm` as primary routes |
|
|
660
|
+
| Legacy opt-in | Set `OPENCODE_RESOLVE_AUTO_PRESET=1` to allow non-interactive provider-adapted presets |
|
|
661
|
+
| GLM/ZAI detected | Adds the ZAI MCP bootstrap without copying API keys |
|
|
661
662
|
|
|
662
|
-
To change presets at any time, edit `models` in `resolve.json` directly or
|
|
663
|
+
To change presets at any time, edit `models` in `resolve.json` directly or run reinstall with `OPENCODE_RESOLVE_REINSTALL=fresh`.
|
|
663
664
|
|
|
664
665
|
Skip the migration entirely with:
|
|
665
666
|
|
|
@@ -748,6 +749,9 @@ In this setup, `plan` uses `openai/gpt-5.3-codex`; native `build`, resolve `code
|
|
|
748
749
|
| `strong` | Provider-neutral alias for a strong/expensive model |
|
|
749
750
|
| `mini` | Provider-neutral alias for a mini/efficient model |
|
|
750
751
|
| `codex` | Provider-neutral alias for a codex-style coding model |
|
|
752
|
+
| `bronze` / `silver` / `gold` | Three-tier scout / coder / reasoner aliases |
|
|
753
|
+
| `gpt-bronze` / `gpt-silver` / `gpt-gold` | GPT/Codex-specific three-tier aliases for mixed setups |
|
|
754
|
+
| `glm-bronze` / `glm-silver` / `glm-gold` | GLM-specific three-tier aliases for mixed setups |
|
|
751
755
|
| `quick` | Legacy alias (equivalent to `fast`) |
|
|
752
756
|
| `deep` | Legacy alias (equivalent to `strong`) |
|
|
753
757
|
| `glm` | Legacy alias (backward compatibility) |
|
|
@@ -762,6 +766,8 @@ Aliases only resolve when defined in `models`. Agent names (`coder`, `resolver`,
|
|
|
762
766
|
| Agent | Default | Mode | Edit | Bash | WebFetch | Purpose |
|
|
763
767
|
|---|:---:|---|---|---|---|---|
|
|
764
768
|
| `resolver` | Yes (core) | `all` | allow | ask (classifier-routed) | allow | Context-efficient orchestrator. Decomposes work into verified checkpoints, dispatches coder, verifies each, and reports blockers when repeated recovery fails. |
|
|
769
|
+
| `codex` | No | `all` | allow | ask (classifier-routed) | allow | Codex-optimized primary resolver with the same verified resolve-loop style as `resolver`. Enabled by first-install GPT/mix presets or explicitly. |
|
|
770
|
+
| `glm` | No | `all` | allow | ask (classifier-routed) | allow | GLM-optimized primary resolver with the same verified resolve-loop style as `resolver`. Enabled by first-install GLM/mix presets or explicitly. |
|
|
765
771
|
| `coder` | Yes (core) | `subagent` | allow | ask (classifier-routed) | allow | Focused implementer. Smallest correct patch. Reads only needed files. |
|
|
766
772
|
| `explorer` | Yes (subagent) | `subagent` | **deny** | **deny** | allow | Internal fast codebase scout. Resolver dispatches when scope is genuinely unknown; prefers local read/grep/glob for narrow scope. |
|
|
767
773
|
| `reviewer` | Yes (subagent) | `subagent` | **deny** | **deny** | allow | Internal verification-gap auditor. Resolver dispatches for post-change verification gaps on non-trivial changes. |
|
|
@@ -784,7 +790,7 @@ Supported modes:
|
|
|
784
790
|
|
|
785
791
|
Supported permission values: `ask`, `allow`, `deny`.
|
|
786
792
|
|
|
787
|
-
Supported model alias keys: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, and every supported agent name. Aliases only resolve when defined in `models`.
|
|
793
|
+
Supported model alias keys: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, `bronze`, `silver`, `gold`, `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`, `glm-gold`, and every supported agent name. Aliases only resolve when defined in `models`.
|
|
788
794
|
|
|
789
795
|
`preserveNative` is accepted for readability, but native `plan` and `build` are always preserved. The plugin never rewrites built-in OpenCode agents.
|
|
790
796
|
|
|
@@ -875,12 +881,20 @@ From this repository:
|
|
|
875
881
|
|
|
876
882
|
```sh
|
|
877
883
|
npm install
|
|
884
|
+
npm run hooks:install
|
|
878
885
|
npm test
|
|
879
886
|
npm run install:local
|
|
880
887
|
```
|
|
881
888
|
|
|
882
889
|
`install:local` builds the plugin, links it into the OpenCode global plugin directory, and creates `~/.config/opencode/resolve.json` if it does not exist.
|
|
883
890
|
|
|
891
|
+
`hooks:install` sets this checkout's `core.hooksPath` to `.githooks`. The tracked hooks are intentionally strict:
|
|
892
|
+
|
|
893
|
+
| Hook | Gate |
|
|
894
|
+
|---|---|
|
|
895
|
+
| `pre-commit` | `npm run typecheck`, `npm test`, `npm run coverage` |
|
|
896
|
+
| `pre-push` | full `pre-commit` gate plus `npm pack --dry-run` before anything reaches the remote |
|
|
897
|
+
|
|
884
898
|
Manual local install:
|
|
885
899
|
|
|
886
900
|
```sh
|
|
@@ -900,6 +914,7 @@ Run the normal checks:
|
|
|
900
914
|
```sh
|
|
901
915
|
npm run typecheck
|
|
902
916
|
npm test
|
|
917
|
+
npm run coverage
|
|
903
918
|
npm run build
|
|
904
919
|
```
|
|
905
920
|
|
|
@@ -910,6 +925,7 @@ Before publishing:
|
|
|
910
925
|
```sh
|
|
911
926
|
npm run typecheck
|
|
912
927
|
npm test
|
|
928
|
+
npm run coverage
|
|
913
929
|
npm audit --audit-level=moderate
|
|
914
930
|
npm publish --dry-run
|
|
915
931
|
```
|
package/dist/agents.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { ResolveAgentName, ResolveAgentConfig, ModelAlias, TierName } from "./types.js";
|
|
2
2
|
export declare const DEFAULT_MODELS: Partial<Record<ModelAlias, string>>;
|
|
3
3
|
export declare const DEFAULT_ENABLED: ResolveAgentName[];
|
|
4
|
-
export declare const VALID_AGENT_NAMES: readonly ["coder", "reviewer", "resolver", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner"];
|
|
4
|
+
export declare const VALID_AGENT_NAMES: readonly ["coder", "reviewer", "resolver", "codex", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner"];
|
|
5
5
|
export declare const VALID_AGENT_NAME_SET: Set<string>;
|
|
6
6
|
export declare const DEFAULT_AGENT_CONFIG: Record<ResolveAgentName, Required<Pick<ResolveAgentConfig, "mode" | "description" | "prompt" | "color">> & ResolveAgentConfig>;
|
|
7
7
|
export declare const GLM_CODER_PROMPT: string;
|
|
8
8
|
export declare const GPT_CODER_PROMPT: string;
|
|
9
9
|
export declare function buildGLMResolverPrompt(maxParallelSubagents: number | undefined): string;
|
|
10
10
|
export declare function buildGPTResolverPrompt(): string;
|
|
11
|
+
export declare function buildCodexResolverPrompt(): string;
|
|
11
12
|
export declare function buildResolverPrompt(maxParallelSubagents: number | undefined): string;
|
|
12
|
-
export declare const VALID_MODEL_ALIASES: readonly ["coder", "reviewer", "resolver", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner", "glm", "gpt", "quick", "deep", "fast", "strong", "mini", "codex", "bronze", "silver", "gold"];
|
|
13
|
+
export declare const VALID_MODEL_ALIASES: readonly ["coder", "reviewer", "resolver", "codex", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner", "glm", "gpt", "quick", "deep", "fast", "strong", "mini", "codex", "bronze", "silver", "gold", "gpt-bronze", "gpt-silver", "gpt-gold", "glm-bronze", "glm-silver", "glm-gold"];
|
|
13
14
|
export declare const VALID_MODEL_ALIAS_SET: Set<string>;
|
|
14
15
|
export declare const VALID_PROFILES: Set<string>;
|
|
15
16
|
export declare const VALID_TIERS: Set<string>;
|
package/dist/agents.js
CHANGED
|
@@ -4,6 +4,7 @@ export const VALID_AGENT_NAMES = [
|
|
|
4
4
|
"coder",
|
|
5
5
|
"reviewer",
|
|
6
6
|
"resolver",
|
|
7
|
+
"codex",
|
|
7
8
|
"glm",
|
|
8
9
|
"architect",
|
|
9
10
|
"gpt-coder",
|
|
@@ -72,6 +73,18 @@ export const DEFAULT_AGENT_CONFIG = {
|
|
|
72
73
|
webfetch: "allow",
|
|
73
74
|
},
|
|
74
75
|
},
|
|
76
|
+
codex: {
|
|
77
|
+
mode: "all",
|
|
78
|
+
color: "#FFB347",
|
|
79
|
+
maxSteps: 35,
|
|
80
|
+
description: "Codex-optimized primary resolver for agentic coding work. Use when a Codex-style OpenAI coding model is pinned and you want a dedicated user-facing route separate from the neutral resolver.",
|
|
81
|
+
prompt: buildCodexResolverPrompt(),
|
|
82
|
+
permission: {
|
|
83
|
+
edit: "allow",
|
|
84
|
+
bash: "ask",
|
|
85
|
+
webfetch: "allow",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
75
88
|
architect: {
|
|
76
89
|
mode: "subagent",
|
|
77
90
|
color: "#00BFFF",
|
|
@@ -282,6 +295,23 @@ export function buildGPTResolverPrompt() {
|
|
|
282
295
|
"Specialists: explorer (scope unknown), reviewer (verification gap), deep-reviewer (risky/security/architectural), debugger (verify failure diagnosis), planner (user asks for plan).",
|
|
283
296
|
].join("\n");
|
|
284
297
|
}
|
|
298
|
+
export function buildCodexResolverPrompt() {
|
|
299
|
+
return [
|
|
300
|
+
"You are Codex Resolver, the OpenAI Codex-optimized primary agent for OpenCode Resolve.",
|
|
301
|
+
"Use Codex-style software engineering judgment: inspect narrowly, patch directly when trivial, dispatch focused coders when parallel work is useful, and verify before reporting.",
|
|
302
|
+
"",
|
|
303
|
+
"Prefer coder for implementation slices, explorer for quick scope discovery, reviewer for verification gaps, deep-reviewer for risky/security/architecture changes, debugger for verify failure diagnosis, planner only when the user asks for a plan.",
|
|
304
|
+
"Dispatch coder with: TASK (atomic goal), OUTCOME (success criteria), MUST DO, MUST NOT DO, CONTEXT (files/patterns).",
|
|
305
|
+
"After EVERY coder return: verify it works + follows codebase patterns. If not → re-dispatch with precise fix.",
|
|
306
|
+
"INTELLIGENT RECOVERY: On verify failure, dispatch debugger FIRST to diagnose root cause, THEN re-dispatch coder with precise fix. Do NOT blindly retry.",
|
|
307
|
+
"Trivial fixes → apply yourself. No subagent needed.",
|
|
308
|
+
"3 consecutive failures → STOP, REVERT, REPORT, ASK user.",
|
|
309
|
+
"",
|
|
310
|
+
"Verify: type check or lint MUST pass on changed files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
|
|
311
|
+
"",
|
|
312
|
+
"NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
|
|
313
|
+
].join("\n");
|
|
314
|
+
}
|
|
285
315
|
export function buildResolverPrompt(maxParallelSubagents) {
|
|
286
316
|
const explicitLimit = typeof maxParallelSubagents === "number" && Number.isFinite(maxParallelSubagents)
|
|
287
317
|
? Math.max(1, Math.trunc(maxParallelSubagents))
|
|
@@ -327,16 +357,22 @@ export const VALID_MODEL_ALIASES = [
|
|
|
327
357
|
"bronze",
|
|
328
358
|
"silver",
|
|
329
359
|
"gold",
|
|
360
|
+
"gpt-bronze",
|
|
361
|
+
"gpt-silver",
|
|
362
|
+
"gpt-gold",
|
|
363
|
+
"glm-bronze",
|
|
364
|
+
"glm-silver",
|
|
365
|
+
"glm-gold",
|
|
330
366
|
];
|
|
331
367
|
export const VALID_MODEL_ALIAS_SET = new Set(VALID_MODEL_ALIASES);
|
|
332
368
|
export const VALID_PROFILES = new Set(["mix", "glm", "gpt"]);
|
|
333
369
|
export const VALID_TIERS = new Set(["bronze", "silver", "gold"]);
|
|
334
|
-
export const GLM_ENABLED = ["coder", "resolver", "explorer", "reviewer", "planner"];
|
|
335
|
-
export const GPT_ENABLED = ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"];
|
|
370
|
+
export const GLM_ENABLED = ["coder", "resolver", "glm", "explorer", "reviewer", "planner"];
|
|
371
|
+
export const GPT_ENABLED = ["coder", "resolver", "codex", "explorer", "reviewer", "deep-reviewer", "planner"];
|
|
336
372
|
export const TIER_ENABLED = {
|
|
337
373
|
bronze: ["coder", "resolver"],
|
|
338
374
|
silver: ["coder", "resolver", "explorer", "reviewer", "planner"],
|
|
339
|
-
gold: ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner", "debugger", "researcher"],
|
|
375
|
+
gold: ["coder", "resolver", "codex", "glm", "explorer", "reviewer", "deep-reviewer", "planner", "debugger", "researcher"],
|
|
340
376
|
};
|
|
341
377
|
export const GLM_AGENT_OVERRIDES = {
|
|
342
378
|
coder: { maxSteps: 15 },
|
|
@@ -348,6 +384,7 @@ export const GLM_AGENT_OVERRIDES = {
|
|
|
348
384
|
export const GPT_AGENT_OVERRIDES = {
|
|
349
385
|
coder: { maxSteps: 25 },
|
|
350
386
|
resolver: { maxSteps: 40 },
|
|
387
|
+
codex: { maxSteps: 40 },
|
|
351
388
|
explorer: { maxSteps: 8 },
|
|
352
389
|
reviewer: { maxSteps: 10 },
|
|
353
390
|
"deep-reviewer": { maxSteps: 15 },
|
package/dist/config.js
CHANGED
|
@@ -47,7 +47,7 @@ export function applyResolveConfig(config, resolveConfig, projectContext) {
|
|
|
47
47
|
agentConfig.prompt = buildResolverPrompt(maxParallelSubagents);
|
|
48
48
|
}
|
|
49
49
|
// Inject project context into all resolver-type agents
|
|
50
|
-
if ((name === "resolver" || name === "glm") && contextInjection) {
|
|
50
|
+
if ((name === "resolver" || name === "codex" || name === "glm") && contextInjection) {
|
|
51
51
|
agentConfig.prompt = agentConfig.prompt + "\n\n" + contextInjection;
|
|
52
52
|
}
|
|
53
53
|
// Inject verify commands into coder prompts
|
|
@@ -153,7 +153,13 @@ export function mergeAgents(left, right) {
|
|
|
153
153
|
export function resolveModel(model, models) {
|
|
154
154
|
if (!model)
|
|
155
155
|
return undefined;
|
|
156
|
-
|
|
156
|
+
let current = model;
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
while (models[current] !== undefined && !seen.has(current)) {
|
|
159
|
+
seen.add(current);
|
|
160
|
+
current = models[current] ?? current;
|
|
161
|
+
}
|
|
162
|
+
return current;
|
|
157
163
|
}
|
|
158
164
|
export function buildPermission(basePermission, userPermission) {
|
|
159
165
|
const merged = {
|
package/dist/hooks/index.js
CHANGED
|
@@ -192,7 +192,7 @@ export function getHooks(directory, options, sessionState) {
|
|
|
192
192
|
output.temperature = capTemperature(output.temperature, 0.3);
|
|
193
193
|
}
|
|
194
194
|
// Write agents: slightly higher temperature for creative problem-solving
|
|
195
|
-
const writeAgents = new Set(["coder", "resolver", "glm", "gpt-coder"]);
|
|
195
|
+
const writeAgents = new Set(["coder", "resolver", "codex", "glm", "gpt-coder"]);
|
|
196
196
|
if (writeAgents.has(input.agent) && output.temperature === undefined) {
|
|
197
197
|
output.temperature = 0.5;
|
|
198
198
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
|
|
5
5
|
import { classifyBashCommand, runCommand, sanitizeShellArg, truncateOutput } from "../utils.js";
|
|
6
6
|
import { DIAGNOSTICS_TTL_MS } from "../state.js";
|
|
7
7
|
import { VALID_PROFILES, VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
|
|
8
|
-
const WRITE_CAPABLE_AGENTS = new Set(["resolver", "coder", "glm", "gpt-coder", "debugger"]);
|
|
8
|
+
const WRITE_CAPABLE_AGENTS = new Set(["resolver", "codex", "coder", "glm", "gpt-coder", "debugger"]);
|
|
9
9
|
function canWriteFromTool(ctx) {
|
|
10
10
|
return typeof ctx.agent !== "string" || WRITE_CAPABLE_AGENTS.has(ctx.agent);
|
|
11
11
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type PermissionValue = "ask" | "allow" | "deny";
|
|
2
|
-
export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
|
|
3
|
-
export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold";
|
|
2
|
+
export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "codex" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
|
|
3
|
+
export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold" | "gpt-bronze" | "gpt-silver" | "gpt-gold" | "glm-bronze" | "glm-silver" | "glm-gold";
|
|
4
4
|
export type AgentMode = "subagent" | "primary" | "all";
|
|
5
5
|
export type ResolveAgentConfig = {
|
|
6
6
|
enabled?: boolean;
|
|
@@ -119,9 +119,12 @@
|
|
|
119
119
|
// -----------------------------------------------------------------------
|
|
120
120
|
// models (object)
|
|
121
121
|
// Alias map. Keys are either an agent name (coder, reviewer, resolver,
|
|
122
|
-
// architect, gpt-coder, debugger, researcher, explorer,
|
|
122
|
+
// codex, glm, architect, gpt-coder, debugger, researcher, explorer,
|
|
123
|
+
// deep-reviewer, planner)
|
|
123
124
|
// OR one of the well-known aliases (`glm`, `gpt`, `quick`, `deep`,
|
|
124
|
-
// `fast`, `strong`, `mini`, `codex`
|
|
125
|
+
// `fast`, `strong`, `mini`, `codex`, `bronze`, `silver`, `gold`,
|
|
126
|
+
// `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`,
|
|
127
|
+
// `glm-gold`).
|
|
125
128
|
// Values are model identifiers OR another alias.
|
|
126
129
|
//
|
|
127
130
|
// Resolution order for a given agent:
|
|
@@ -137,11 +140,17 @@
|
|
|
137
140
|
// When resolve.json is created for the first time by postinstall, the
|
|
138
141
|
// plugin inspects your OpenCode model config and writes an adaptive
|
|
139
142
|
// preset:
|
|
140
|
-
// -
|
|
141
|
-
//
|
|
142
|
-
// -
|
|
143
|
-
//
|
|
144
|
-
//
|
|
143
|
+
// - interactive terminal → always prompts for mix/gpt/glm, three-tier models,
|
|
144
|
+
// and optional dedicated codex/glm primary agents
|
|
145
|
+
// - non-interactive install → no guessed model pinning; profile mix,
|
|
146
|
+
// models {}, and resolver/codex/glm primary routes enabled
|
|
147
|
+
// - OPENCODE_RESOLVE_AUTO_PRESET=1 → legacy non-interactive
|
|
148
|
+
// provider-adapted presets
|
|
149
|
+
// Existing resolve.json files are never overwritten without consent.
|
|
150
|
+
// Interactive reinstall asks whether to update or back up and run fresh
|
|
151
|
+
// setup. Non-interactive automation can set:
|
|
152
|
+
// OPENCODE_RESOLVE_REINSTALL=fresh
|
|
153
|
+
// OPENCODE_RESOLVE_REINSTALL=update
|
|
145
154
|
//
|
|
146
155
|
// Example — one efficient model for everything (recommended default):
|
|
147
156
|
// "models": {}
|
|
@@ -154,19 +163,22 @@
|
|
|
154
163
|
// "resolver": "strong"
|
|
155
164
|
// }
|
|
156
165
|
//
|
|
157
|
-
// Example — GLM-only preset (auto-generated for GLM/ZAI users):
|
|
158
|
-
//
|
|
166
|
+
// Example — GLM-only three-tier preset (auto-generated for GLM/ZAI users):
|
|
167
|
+
// GLM fills all tiers — no GPT dependency, so token exhaustion on
|
|
159
168
|
// third-party providers never blocks agent execution.
|
|
160
169
|
// "models": {
|
|
161
170
|
// "glm": "zai-coding-plan/glm-5.1",
|
|
162
|
-
// "
|
|
163
|
-
// "
|
|
164
|
-
// "
|
|
165
|
-
// "
|
|
166
|
-
// "
|
|
167
|
-
// "
|
|
168
|
-
// "
|
|
169
|
-
// "
|
|
171
|
+
// "bronze": "zai-coding-plan/glm-5.1",
|
|
172
|
+
// "silver": "zai-coding-plan/glm-5.1",
|
|
173
|
+
// "gold": "zai-coding-plan/glm-5.1",
|
|
174
|
+
// "fast": "bronze",
|
|
175
|
+
// "strong": "gold",
|
|
176
|
+
// "coder": "silver",
|
|
177
|
+
// "resolver": "gold",
|
|
178
|
+
// "reviewer": "gold",
|
|
179
|
+
// "deep-reviewer": "gold",
|
|
180
|
+
// "explorer": "bronze",
|
|
181
|
+
// "planner": "gold"
|
|
170
182
|
// }
|
|
171
183
|
//
|
|
172
184
|
// Legacy aliases (glm, gpt, quick, deep) remain supported for backward
|
|
@@ -209,6 +221,15 @@
|
|
|
209
221
|
}
|
|
210
222
|
},
|
|
211
223
|
|
|
224
|
+
// codex — Codex-optimized primary resolver (selectable in agent picker)
|
|
225
|
+
// Same verified resolve-loop style as resolver, with a Codex-specific
|
|
226
|
+
// system prompt. Enabled by GPT/mix first-install presets when selected.
|
|
227
|
+
"codex": {
|
|
228
|
+
"enabled": false,
|
|
229
|
+
"mode": "all",
|
|
230
|
+
"maxSteps": 35
|
|
231
|
+
},
|
|
232
|
+
|
|
212
233
|
// Internal specialist subagents — enabled by default as subagents only.
|
|
213
234
|
// Core path is resolver→coder. These are dispatched only when justified.
|
|
214
235
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-resolve",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "OpenCode plugin that adds a lightweight resolver/coder harness for continuous agentic coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/jshsakura/opencode-resolve#readme",
|
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"coverage": "npm run build && node --experimental-test-coverage --test-coverage-lines=79 --test-coverage-branches=69 --test-coverage-functions=85 --test test/*.mjs",
|
|
34
|
+
"hooks:install": "node scripts/install-git-hooks.mjs",
|
|
33
35
|
"install:local": "npm run build && node scripts/install-local.mjs",
|
|
34
36
|
"postinstall": "node scripts/postinstall.mjs",
|
|
35
37
|
"prepack": "npm test",
|
|
38
|
+
"precommit": "npm run typecheck && npm test && npm run coverage",
|
|
39
|
+
"prepush": "npm run precommit && npm pack --dry-run",
|
|
36
40
|
"test": "npm run build && node --test test/*.mjs",
|
|
37
41
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
38
42
|
},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { chmodSync, existsSync } from "node:fs"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
8
|
+
const hooksDir = join(root, ".githooks")
|
|
9
|
+
|
|
10
|
+
if (!existsSync(join(root, ".git"))) {
|
|
11
|
+
console.error("[opencode-resolve] not a git checkout; cannot install hooks")
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const hook of ["pre-commit", "pre-push"]) {
|
|
16
|
+
chmodSync(join(hooksDir, hook), 0o755)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = spawnSync("git", ["config", "core.hooksPath", ".githooks"], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
process.exit(result.status ?? 1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log("[opencode-resolve] git hooks installed: core.hooksPath=.githooks")
|
|
@@ -2,6 +2,7 @@ import { constants } from "node:fs"
|
|
|
2
2
|
import { access, mkdir, readFile, symlink, unlink, writeFile } from "node:fs/promises"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
import { createInterface } from "node:readline/promises"
|
|
5
6
|
import { fileURLToPath } from "node:url"
|
|
6
7
|
|
|
7
8
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
@@ -25,11 +26,58 @@ await symlink(pluginTarget, pluginLink)
|
|
|
25
26
|
|
|
26
27
|
if (!(await exists(resolveConfigPath))) {
|
|
27
28
|
await createAdaptiveResolveConfig()
|
|
29
|
+
} else {
|
|
30
|
+
await handleExistingResolveConfig()
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
console.log(`Linked plugin: ${pluginLink} -> ${pluginTarget}`)
|
|
31
34
|
console.log(`Resolve config: ${resolveConfigPath}`)
|
|
32
35
|
|
|
36
|
+
async function handleExistingResolveConfig() {
|
|
37
|
+
const action = await chooseExistingResolveConfigAction()
|
|
38
|
+
if (action !== "fresh") {
|
|
39
|
+
console.log(`Existing resolve config preserved: ${resolveConfigPath}`)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await backupResolveConfig()
|
|
44
|
+
await createAdaptiveResolveConfig()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function chooseExistingResolveConfigAction() {
|
|
48
|
+
const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
|
|
49
|
+
if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
|
|
50
|
+
if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
|
|
51
|
+
if (requested) {
|
|
52
|
+
console.warn(`Ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
56
|
+
console.log(`Existing resolve config found; preserving it. Set OPENCODE_RESOLVE_REINSTALL=fresh for a fresh reinstall.`)
|
|
57
|
+
return "update"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
61
|
+
try {
|
|
62
|
+
console.log("")
|
|
63
|
+
console.log(`Existing resolve config found: ${resolveConfigPath}`)
|
|
64
|
+
console.log(" 1. update existing config — preserve your settings")
|
|
65
|
+
console.log(" 2. fresh reinstall — back up resolve.json and create a new config")
|
|
66
|
+
const raw = await rl.question("Existing config [1=update, 2=fresh reinstall, default 1]: ")
|
|
67
|
+
return raw.trim() === "2" ? "fresh" : "update"
|
|
68
|
+
} finally {
|
|
69
|
+
rl.close()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function backupResolveConfig() {
|
|
74
|
+
const raw = await readFile(resolveConfigPath, "utf8")
|
|
75
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
76
|
+
const backupPath = `${resolveConfigPath}.bak.${stamp}`
|
|
77
|
+
await writeFile(backupPath, raw)
|
|
78
|
+
console.log(`Backed up existing resolve config to ${backupPath}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
33
81
|
async function createAdaptiveResolveConfig() {
|
|
34
82
|
const raw = await readFile(exampleConfig, "utf8")
|
|
35
83
|
const example = JSON.parse(raw)
|
|
@@ -42,10 +90,20 @@ async function createAdaptiveResolveConfig() {
|
|
|
42
90
|
// opencode.json not found or unreadable — use empty config
|
|
43
91
|
}
|
|
44
92
|
|
|
45
|
-
const currentModel = detectOpenCodeModel(opencodeConfig)
|
|
46
|
-
const preset = buildModelPreset(currentModel)
|
|
47
|
-
|
|
48
93
|
const resolveConfig = { ...example }
|
|
94
|
+
let preset = {}
|
|
95
|
+
if (process.env.OPENCODE_RESOLVE_AUTO_PRESET === "1") {
|
|
96
|
+
const currentModel = detectOpenCodeModel(opencodeConfig)
|
|
97
|
+
preset = buildModelPreset(currentModel)
|
|
98
|
+
} else {
|
|
99
|
+
resolveConfig.profile = "mix"
|
|
100
|
+
resolveConfig.models = {}
|
|
101
|
+
resolveConfig.agents = {
|
|
102
|
+
...resolveConfig.agents,
|
|
103
|
+
codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
|
|
104
|
+
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
105
|
+
}
|
|
106
|
+
}
|
|
49
107
|
if (preset && Object.keys(preset).length > 0) {
|
|
50
108
|
resolveConfig.models = preset
|
|
51
109
|
}
|
|
@@ -53,7 +111,9 @@ async function createAdaptiveResolveConfig() {
|
|
|
53
111
|
await mkdir(configDir, { recursive: true })
|
|
54
112
|
await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
|
|
55
113
|
|
|
56
|
-
const label =
|
|
114
|
+
const label = process.env.OPENCODE_RESOLVE_AUTO_PRESET === "1"
|
|
115
|
+
? getPresetLabel(detectOpenCodeModel(opencodeConfig))
|
|
116
|
+
: "prompt-required"
|
|
57
117
|
console.log(`Created ${resolveConfigPath} (preset: ${label})`)
|
|
58
118
|
}
|
|
59
119
|
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -41,6 +41,26 @@ const COMPANION_PLUGINS = [
|
|
|
41
41
|
},
|
|
42
42
|
]
|
|
43
43
|
|
|
44
|
+
const OPENAI_MODEL_HINTS = [
|
|
45
|
+
"openai/gpt-5.5",
|
|
46
|
+
"openai/gpt-5.4",
|
|
47
|
+
"openai/gpt-5.3-codex",
|
|
48
|
+
"openai/gpt-5.3-codex-spark",
|
|
49
|
+
"openai/gpt-5.2",
|
|
50
|
+
"openai/gpt-5-mini",
|
|
51
|
+
"openai/gpt-4o-mini",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const GLM_MODEL_HINTS = [
|
|
55
|
+
"zai-coding-plan/glm-5.1",
|
|
56
|
+
"zai/glm-5.1",
|
|
57
|
+
"zai/glm-5",
|
|
58
|
+
"zai/glm-4.7-flash",
|
|
59
|
+
"zai/glm-4.5-flash",
|
|
60
|
+
"zai/glm-4.5",
|
|
61
|
+
"zai/glm-4.5-air",
|
|
62
|
+
]
|
|
63
|
+
|
|
44
64
|
if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
|
|
45
65
|
process.exit(0)
|
|
46
66
|
}
|
|
@@ -69,6 +89,7 @@ async function readOwnVersion() {
|
|
|
69
89
|
|
|
70
90
|
async function registerPlugin() {
|
|
71
91
|
await mkdir(configDir, { recursive: true })
|
|
92
|
+
const scriptedAnswers = await readScriptedAnswersIfNeeded()
|
|
72
93
|
|
|
73
94
|
const config = await readOpenCodeConfig()
|
|
74
95
|
let configChanged = addPlugin(config)
|
|
@@ -89,14 +110,62 @@ async function registerPlugin() {
|
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
if (!(await exists(resolveConfigPath))) {
|
|
92
|
-
await createAdaptiveResolveConfig(config)
|
|
113
|
+
await createAdaptiveResolveConfig(config, scriptedAnswers)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await handleExistingResolveConfig(config, scriptedAnswers)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
|
|
121
|
+
const action = await chooseExistingResolveConfigAction(scriptedAnswers)
|
|
122
|
+
if (action === "fresh") {
|
|
123
|
+
await backupResolveConfig()
|
|
124
|
+
await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers)
|
|
93
125
|
return
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
await migrateResolveConfig()
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
async function
|
|
131
|
+
async function chooseExistingResolveConfigAction(scriptedAnswers) {
|
|
132
|
+
const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
|
|
133
|
+
if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
|
|
134
|
+
if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
|
|
135
|
+
if (requested) {
|
|
136
|
+
console.warn(`[${packageName}] ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
|
|
140
|
+
const canPrompt = Boolean((process.stdin.isTTY && process.stdout.isTTY) || forcePrompt)
|
|
141
|
+
if (!canPrompt) {
|
|
142
|
+
console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
|
|
143
|
+
console.log(`[${packageName}] for a fresh reinstall, rerun in a TTY or set OPENCODE_RESOLVE_REINSTALL=fresh.`)
|
|
144
|
+
return "update"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rl = createPromptInterface(scriptedAnswers)
|
|
148
|
+
try {
|
|
149
|
+
console.log("")
|
|
150
|
+
console.log(`[${packageName}] Existing resolve config found: ${resolveConfigPath}`)
|
|
151
|
+
console.log(" 1. update existing config — preserve your settings and add missing defaults")
|
|
152
|
+
console.log(" 2. fresh reinstall — back up resolve.json and run setup again")
|
|
153
|
+
const answer = await askChoice(rl, "Existing config [1=update, 2=fresh reinstall, default 1]: ", ["1", "2"], "1")
|
|
154
|
+
return answer === "2" ? "fresh" : "update"
|
|
155
|
+
} finally {
|
|
156
|
+
rl.close()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function backupResolveConfig() {
|
|
161
|
+
const raw = await readFile(resolveConfigPath, "utf8")
|
|
162
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
163
|
+
const backupPath = `${resolveConfigPath}.bak.${stamp}`
|
|
164
|
+
await writeFile(backupPath, raw)
|
|
165
|
+
console.log(`[${packageName}] backed up existing resolve config to ${backupPath}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
|
|
100
169
|
await assertReadable(exampleConfigPath)
|
|
101
170
|
const raw = await readFile(exampleConfigPath, "utf8")
|
|
102
171
|
const example = JSON.parse(raw)
|
|
@@ -104,6 +173,44 @@ async function createAdaptiveResolveConfig(opencodeConfig) {
|
|
|
104
173
|
const currentModel = detectOpenCodeModel(opencodeConfig)
|
|
105
174
|
const allModels = detectAllModels(opencodeConfig)
|
|
106
175
|
const resolveConfig = { ...example }
|
|
176
|
+
const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
|
|
177
|
+
const canPrompt = Boolean(
|
|
178
|
+
(process.stdin.isTTY && process.stdout.isTTY) || forcePrompt,
|
|
179
|
+
)
|
|
180
|
+
const interactivePreset = canPrompt
|
|
181
|
+
? await buildInteractivePreset(currentModel, allModels, scriptedAnswers)
|
|
182
|
+
: undefined
|
|
183
|
+
|
|
184
|
+
if (interactivePreset) {
|
|
185
|
+
resolveConfig.profile = interactivePreset.profile
|
|
186
|
+
if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
|
|
187
|
+
else delete resolveConfig.tier
|
|
188
|
+
if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
|
|
189
|
+
resolveConfig.models = interactivePreset.models
|
|
190
|
+
resolveConfig.agents = {
|
|
191
|
+
...resolveConfig.agents,
|
|
192
|
+
...(interactivePreset.agents ?? {}),
|
|
193
|
+
}
|
|
194
|
+
await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
|
|
195
|
+
console.log(`[${packageName}] created ${resolveConfigPath} (preset: ${interactivePreset.label})`)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (process.env.OPENCODE_RESOLVE_AUTO_PRESET !== "1") {
|
|
200
|
+
resolveConfig.profile = "mix"
|
|
201
|
+
delete resolveConfig.tier
|
|
202
|
+
resolveConfig.models = {}
|
|
203
|
+
resolveConfig.agents = {
|
|
204
|
+
...resolveConfig.agents,
|
|
205
|
+
codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
|
|
206
|
+
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
207
|
+
}
|
|
208
|
+
await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
|
|
209
|
+
console.log(`[${packageName}] created ${resolveConfigPath} (preset: prompt-required)`)
|
|
210
|
+
console.log(`[${packageName}] model pinning was not guessed because no interactive prompt was available.`)
|
|
211
|
+
console.log(`[${packageName}] rerun setup in a TTY or edit ${resolveConfigPath} to choose GPT/GLM three-tier models.`)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
107
214
|
|
|
108
215
|
// Profile selection based on detected providers
|
|
109
216
|
const hasGLM = allModels.some((m) => isGLMModel(m))
|
|
@@ -114,13 +221,28 @@ async function createAdaptiveResolveConfig(opencodeConfig) {
|
|
|
114
221
|
// GLM only → GLM profile, silver tier (token-efficient, no deep-reviewer)
|
|
115
222
|
resolveConfig.profile = "glm"
|
|
116
223
|
resolveConfig.tier = "silver"
|
|
224
|
+
resolveConfig.agents = {
|
|
225
|
+
...resolveConfig.agents,
|
|
226
|
+
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
227
|
+
}
|
|
117
228
|
} else if (hasGPT && !hasGLM) {
|
|
118
229
|
// GPT only → GPT profile, gold tier (full power)
|
|
119
230
|
resolveConfig.profile = "gpt"
|
|
120
231
|
resolveConfig.tier = "gold"
|
|
232
|
+
resolveConfig.agents = {
|
|
233
|
+
...resolveConfig.agents,
|
|
234
|
+
codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
|
|
235
|
+
}
|
|
121
236
|
} else {
|
|
122
237
|
// Mixed or unknown provider → explicit mix profile. No tier: use DEFAULT_ENABLED.
|
|
123
238
|
resolveConfig.profile = "mix"
|
|
239
|
+
if (hasGLM && hasGPT) {
|
|
240
|
+
resolveConfig.agents = {
|
|
241
|
+
...resolveConfig.agents,
|
|
242
|
+
codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
|
|
243
|
+
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
244
|
+
}
|
|
245
|
+
}
|
|
124
246
|
}
|
|
125
247
|
|
|
126
248
|
if (preset && Object.keys(preset).length > 0) {
|
|
@@ -161,21 +283,33 @@ function detectOpenCodeModel(config) {
|
|
|
161
283
|
}
|
|
162
284
|
|
|
163
285
|
function buildModelPreset(currentModel, allModels = []) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
286
|
+
const detectedGLMModels = allModels.filter(isGLMModel)
|
|
287
|
+
const detectedGPTModels = allModels.filter(isGPTModel)
|
|
288
|
+
const glmModels = detectedGLMModels.length > 0 ? collectModelChoices(detectedGLMModels, isGLMModel, GLM_MODEL_HINTS, false) : []
|
|
289
|
+
const gptModels = detectedGPTModels.length > 0 ? collectModelChoices(detectedGPTModels, isGPTModel, OPENAI_MODEL_HINTS, false) : []
|
|
290
|
+
const glmModel = glmModels[0]
|
|
291
|
+
const gptModel = gptModels[0]
|
|
166
292
|
|
|
167
293
|
if (glmModel && gptModel) {
|
|
294
|
+
const glmTiers = chooseThreeTier(glmModels, "glm", false)
|
|
295
|
+
const gptTiers = chooseThreeTier(gptModels, "gpt", false)
|
|
168
296
|
return {
|
|
169
297
|
mix: "gpt",
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
298
|
+
gpt: gptTiers.gold,
|
|
299
|
+
bronze: glmTiers.bronze,
|
|
300
|
+
silver: glmTiers.silver,
|
|
301
|
+
gold: gptTiers.gold,
|
|
302
|
+
"glm-bronze": glmTiers.bronze,
|
|
303
|
+
"glm-silver": glmTiers.silver,
|
|
304
|
+
"glm-gold": glmTiers.gold,
|
|
305
|
+
"gpt-bronze": gptTiers.bronze,
|
|
306
|
+
"gpt-silver": gptTiers.silver,
|
|
307
|
+
"gpt-gold": gptTiers.gold,
|
|
308
|
+
fast: "bronze",
|
|
309
|
+
strong: "gold",
|
|
310
|
+
mini: "bronze",
|
|
311
|
+
codex: "gpt-gold",
|
|
312
|
+
glm: glmTiers.gold,
|
|
179
313
|
explorer: "bronze",
|
|
180
314
|
coder: "silver",
|
|
181
315
|
resolver: "gold",
|
|
@@ -186,8 +320,6 @@ function buildModelPreset(currentModel, allModels = []) {
|
|
|
186
320
|
}
|
|
187
321
|
|
|
188
322
|
if (!currentModel) {
|
|
189
|
-
if (glmModel) return buildGLMOnlyPreset(glmModel)
|
|
190
|
-
if (gptModel) return buildGPTOnlyPreset(gptModel)
|
|
191
323
|
return {}
|
|
192
324
|
}
|
|
193
325
|
|
|
@@ -208,32 +340,256 @@ function buildModelPreset(currentModel, allModels = []) {
|
|
|
208
340
|
}
|
|
209
341
|
|
|
210
342
|
function buildGLMOnlyPreset(model) {
|
|
343
|
+
const tiers = chooseThreeTier(collectModelChoices([model], isGLMModel, GLM_MODEL_HINTS, false), "glm", false)
|
|
211
344
|
return {
|
|
212
|
-
glm:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
345
|
+
glm: tiers.gold,
|
|
346
|
+
bronze: tiers.bronze,
|
|
347
|
+
silver: tiers.silver,
|
|
348
|
+
gold: tiers.gold,
|
|
349
|
+
fast: "bronze",
|
|
350
|
+
strong: "gold",
|
|
351
|
+
mini: "bronze",
|
|
352
|
+
coder: "silver",
|
|
353
|
+
resolver: "gold",
|
|
354
|
+
reviewer: "gold",
|
|
355
|
+
"deep-reviewer": "gold",
|
|
356
|
+
explorer: "bronze",
|
|
357
|
+
planner: "gold",
|
|
221
358
|
}
|
|
222
359
|
}
|
|
223
360
|
|
|
224
361
|
function buildGPTOnlyPreset(model) {
|
|
362
|
+
const tiers = chooseThreeTier(collectModelChoices([model], isGPTModel, OPENAI_MODEL_HINTS, false), "gpt", false)
|
|
363
|
+
return {
|
|
364
|
+
gpt: tiers.gold,
|
|
365
|
+
bronze: tiers.bronze,
|
|
366
|
+
silver: tiers.silver,
|
|
367
|
+
gold: tiers.gold,
|
|
368
|
+
fast: "bronze",
|
|
369
|
+
strong: "gold",
|
|
370
|
+
mini: "bronze",
|
|
371
|
+
codex: "gold",
|
|
372
|
+
coder: "silver",
|
|
373
|
+
resolver: "gold",
|
|
374
|
+
explorer: "bronze",
|
|
375
|
+
reviewer: "gold",
|
|
376
|
+
"deep-reviewer": "gold",
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function buildInteractivePreset(currentModel, allModels, scriptedAnswers) {
|
|
381
|
+
const choices = {
|
|
382
|
+
gpt: collectModelChoices(allModels, isGPTModel, OPENAI_MODEL_HINTS),
|
|
383
|
+
glm: collectModelChoices(allModels, isGLMModel, GLM_MODEL_HINTS),
|
|
384
|
+
}
|
|
385
|
+
if (currentModel) {
|
|
386
|
+
if (isGPTModel(currentModel)) choices.gpt = unique([currentModel, ...choices.gpt])
|
|
387
|
+
if (isGLMModel(currentModel)) choices.glm = unique([currentModel, ...choices.glm])
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const rl = createPromptInterface(scriptedAnswers)
|
|
391
|
+
try {
|
|
392
|
+
console.log("")
|
|
393
|
+
console.log(`[${packageName}] Choose resolve profile:`)
|
|
394
|
+
console.log(" 1. mix — neutral resolver plus optional Codex and GLM primary agents")
|
|
395
|
+
console.log(" 2. gpt — GPT/Codex-only, three-tier")
|
|
396
|
+
console.log(" 3. glm — GLM-only, three-tier")
|
|
397
|
+
const profileAnswer = await askChoice(rl, "Profile [1=mix, 2=gpt, 3=glm, default 1]: ", ["1", "2", "3"], "1")
|
|
398
|
+
const profile = profileAnswer === "2" ? "gpt" : profileAnswer === "3" ? "glm" : "mix"
|
|
399
|
+
|
|
400
|
+
if (profile === "gpt") {
|
|
401
|
+
const tiers = await askThreeTier(rl, "GPT/Codex", choices.gpt)
|
|
402
|
+
return {
|
|
403
|
+
label: "gpt-three-tier",
|
|
404
|
+
profile: "gpt",
|
|
405
|
+
tier: "gold",
|
|
406
|
+
models: buildGPTThreeTierModels(tiers),
|
|
407
|
+
agents: { codex: { enabled: true } },
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (profile === "glm") {
|
|
412
|
+
const tiers = await askThreeTier(rl, "GLM", choices.glm)
|
|
413
|
+
return {
|
|
414
|
+
label: "glm-three-tier",
|
|
415
|
+
profile: "glm",
|
|
416
|
+
tier: "gold",
|
|
417
|
+
models: buildGLMThreeTierModels(tiers),
|
|
418
|
+
agents: { glm: { enabled: true } },
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const useCodex = await askYesNo(rl, "Enable dedicated Codex primary agent too? [Y/n]: ", true)
|
|
423
|
+
const useGLM = await askYesNo(rl, "Enable dedicated GLM primary agent too? [Y/n]: ", true)
|
|
424
|
+
const gptTiers = await askThreeTier(rl, "Codex/GPT", choices.gpt)
|
|
425
|
+
const glmTiers = await askThreeTier(rl, "GLM", choices.glm)
|
|
426
|
+
return {
|
|
427
|
+
label: "mix-three-tier",
|
|
428
|
+
profile: "mix",
|
|
429
|
+
models: buildMixedThreeTierModels(gptTiers, glmTiers),
|
|
430
|
+
agents: {
|
|
431
|
+
codex: { enabled: useCodex },
|
|
432
|
+
glm: { enabled: useGLM },
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
} finally {
|
|
436
|
+
rl.close()
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function createPromptInterface(scriptedAnswers) {
|
|
441
|
+
return scriptedAnswers
|
|
442
|
+
? {
|
|
443
|
+
async question(prompt) {
|
|
444
|
+
const answer = scriptedAnswers.length > 0 ? scriptedAnswers.shift() ?? "" : ""
|
|
445
|
+
process.stdout.write(prompt)
|
|
446
|
+
process.stdout.write(`${answer}\n`)
|
|
447
|
+
return answer
|
|
448
|
+
},
|
|
449
|
+
close() {},
|
|
450
|
+
}
|
|
451
|
+
: createInterface({ input: process.stdin, output: process.stdout })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function askThreeTier(rl, label, models) {
|
|
455
|
+
const choices = models.length > 0 ? models : (label.toLowerCase().includes("glm") ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS)
|
|
456
|
+
console.log("")
|
|
457
|
+
console.log(`[${packageName}] ${label} model choices:`)
|
|
458
|
+
choices.forEach((model, index) => console.log(` ${index + 1}. ${model}`))
|
|
459
|
+
const defaults = chooseThreeTier(choices, label.toLowerCase().includes("glm") ? "glm" : "gpt")
|
|
460
|
+
const bronze = await askModel(rl, choices, `Pick ${label} bronze/scout [default ${defaults.bronze}]: `, defaults.bronze)
|
|
461
|
+
const silver = await askModel(rl, choices, `Pick ${label} silver/coder [default ${defaults.silver}]: `, defaults.silver)
|
|
462
|
+
const gold = await askModel(rl, choices, `Pick ${label} gold/reasoner [default ${defaults.gold}]: `, defaults.gold)
|
|
463
|
+
return { bronze, silver, gold }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function askModel(rl, choices, question, defaultValue) {
|
|
467
|
+
const answer = (await rl.question(question)).trim()
|
|
468
|
+
if (!answer) return defaultValue
|
|
469
|
+
const index = Number.parseInt(answer, 10)
|
|
470
|
+
if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1]
|
|
471
|
+
return answer
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function askChoice(rl, question, valid, defaultValue) {
|
|
475
|
+
const answer = (await rl.question(question)).trim().toLowerCase()
|
|
476
|
+
if (!answer) return defaultValue
|
|
477
|
+
return valid.includes(answer) ? answer : defaultValue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function askYesNo(rl, question, defaultValue) {
|
|
481
|
+
const answer = (await rl.question(question)).trim().toLowerCase()
|
|
482
|
+
if (!answer) return defaultValue
|
|
483
|
+
return answer === "y" || answer === "yes"
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildMixedThreeTierModels(gptTiers, glmTiers) {
|
|
487
|
+
return {
|
|
488
|
+
mix: "gpt-gold",
|
|
489
|
+
gpt: "gpt-gold",
|
|
490
|
+
glm: "glm-gold",
|
|
491
|
+
bronze: "glm-bronze",
|
|
492
|
+
silver: "glm-silver",
|
|
493
|
+
gold: "gpt-gold",
|
|
494
|
+
"gpt-bronze": gptTiers.bronze,
|
|
495
|
+
"gpt-silver": gptTiers.silver,
|
|
496
|
+
"gpt-gold": gptTiers.gold,
|
|
497
|
+
"glm-bronze": glmTiers.bronze,
|
|
498
|
+
"glm-silver": glmTiers.silver,
|
|
499
|
+
"glm-gold": glmTiers.gold,
|
|
500
|
+
fast: "bronze",
|
|
501
|
+
strong: "gold",
|
|
502
|
+
mini: "bronze",
|
|
503
|
+
codex: "gpt-gold",
|
|
504
|
+
explorer: "bronze",
|
|
505
|
+
coder: "silver",
|
|
506
|
+
resolver: "gold",
|
|
507
|
+
reviewer: "gold",
|
|
508
|
+
"deep-reviewer": "gold",
|
|
509
|
+
planner: "gold",
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function buildGPTThreeTierModels(tiers) {
|
|
514
|
+
return {
|
|
515
|
+
gpt: "gold",
|
|
516
|
+
bronze: tiers.bronze,
|
|
517
|
+
silver: tiers.silver,
|
|
518
|
+
gold: tiers.gold,
|
|
519
|
+
fast: "bronze",
|
|
520
|
+
strong: "gold",
|
|
521
|
+
mini: "bronze",
|
|
522
|
+
codex: "gold",
|
|
523
|
+
explorer: "bronze",
|
|
524
|
+
coder: "silver",
|
|
525
|
+
resolver: "gold",
|
|
526
|
+
reviewer: "gold",
|
|
527
|
+
"deep-reviewer": "gold",
|
|
528
|
+
planner: "gold",
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildGLMThreeTierModels(tiers) {
|
|
225
533
|
return {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
explorer: "
|
|
234
|
-
|
|
235
|
-
|
|
534
|
+
glm: "gold",
|
|
535
|
+
bronze: tiers.bronze,
|
|
536
|
+
silver: tiers.silver,
|
|
537
|
+
gold: tiers.gold,
|
|
538
|
+
fast: "bronze",
|
|
539
|
+
strong: "gold",
|
|
540
|
+
mini: "bronze",
|
|
541
|
+
explorer: "bronze",
|
|
542
|
+
coder: "silver",
|
|
543
|
+
resolver: "gold",
|
|
544
|
+
reviewer: "gold",
|
|
545
|
+
"deep-reviewer": "gold",
|
|
546
|
+
planner: "gold",
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function collectModelChoices(allModels, predicate, hints, includeFallbackHints = true) {
|
|
551
|
+
const detected = allModels.filter(predicate)
|
|
552
|
+
const providerIds = new Set(detected.map((model) => model.split("/")[0]).filter(Boolean))
|
|
553
|
+
const matchingHints = includeFallbackHints
|
|
554
|
+
? hints.filter((model) => providerIds.size === 0 || providerIds.has(model.split("/")[0]) || detected.length < 3)
|
|
555
|
+
: []
|
|
556
|
+
return unique([...detected, ...matchingHints])
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function chooseThreeTier(models, family, includeFallbackHints = true) {
|
|
560
|
+
const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
|
|
561
|
+
const choices = unique(includeFallbackHints ? [...models, ...fallback] : models)
|
|
562
|
+
return {
|
|
563
|
+
bronze: preferModel(choices, family === "glm" ? ["flash", "air", "mini"] : ["spark", "mini", "4o-mini"], choices[0]),
|
|
564
|
+
silver: preferModel(choices, family === "glm" ? ["5.1", "4.5"] : ["codex", "5.3", "5.2"], choices[1] ?? choices[0]),
|
|
565
|
+
gold: preferModel(choices, family === "glm" ? ["5.1", "5", "4.5"] : ["5.5", "5.4", "gpt-5.3-codex"], choices[2] ?? choices[1] ?? choices[0]),
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function preferModel(models, needles, fallback) {
|
|
570
|
+
return models.find((model) => {
|
|
571
|
+
const lower = model.toLowerCase()
|
|
572
|
+
return needles.some((needle) => lower.includes(needle))
|
|
573
|
+
}) ?? fallback
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function unique(values) {
|
|
577
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function readAllStdin() {
|
|
581
|
+
let raw = ""
|
|
582
|
+
for await (const chunk of process.stdin) {
|
|
583
|
+
raw += chunk
|
|
584
|
+
}
|
|
585
|
+
return raw
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function readScriptedAnswersIfNeeded() {
|
|
589
|
+
if (process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1" && !process.stdin.isTTY) {
|
|
590
|
+
return (await readAllStdin()).split(/\r?\n/)
|
|
236
591
|
}
|
|
592
|
+
return undefined
|
|
237
593
|
}
|
|
238
594
|
|
|
239
595
|
function getPresetLabel(currentModel) {
|