Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
1f9f907ccf release: v2.5.1 2025-12-23 17:10:11 +00:00
YeonGyu-Kim
6ee761d978 feat(config): add user reviews to docs and improve Antigravity provider config
- Add user reviews section to READMEs (EN, KO, JA, ZH-CN)
- Update Antigravity request.ts: change default model from gemini-3-pro-preview to gemini-3-pro-high
- Enhance config-manager.ts: add full model specs (name, limit, modalities) to provider config
- Add comprehensive test suite for config-manager (config-manager.test.ts)

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-24 02:08:01 +09:00
YeonGyu-Kim
fd8e62fba3 fix(publish): include CLI build step to ensure dist/cli/index.js is packaged in npm release
The CLI module was missing from the npm package because the publish workflow
did not include the 'bun build src/cli/index.ts' step. This caused 'bunx oh-my-opencode
install' to fail with missing dist/cli/index.js at runtime. Added explicit CLI build
and verification step to prevent this regression.

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-24 02:06:57 +09:00
github-actions[bot]
f5c7f430c2 release: v2.5.0 2025-12-23 14:43:57 +00:00
YeonGyu-Kim
b8e70f9529 docs: update LLM agent install guide to use CLI installer
Related to #153

Co-authored-by: Taegeon Alan Go <32065632+gtg7784@users.noreply.github.com>

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-23 22:42:43 +09:00
YeonGyu-Kim
5dbd5ac6b1 feat(cli): add interactive install command
Related to #153

Co-authored-by: Taegeon Alan Go <32065632+gtg7784@users.noreply.github.com>

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-23 22:42:31 +09:00
YeonGyu-Kim
908521746f fix(publish): include schema.json in release commit
🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-23 22:25:30 +09:00
15 changed files with 1344 additions and 371 deletions

View File

@@ -103,9 +103,10 @@ jobs:
- name: Build
run: |
echo "=== Running bun build ==="
bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi
echo "=== bun build exit code: $? ==="
echo "=== Running bun build (main) ==="
bun build src/index.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi
echo "=== Running bun build (CLI) ==="
bun build src/cli/index.ts --outdir dist/cli --target bun --format esm
echo "=== Running tsc ==="
tsc --emitDeclarationOnly
echo "=== Running build:schema ==="
@@ -113,8 +114,12 @@ jobs:
- name: Verify build output
run: |
echo "=== dist/ contents ==="
ls -la dist/
echo "=== dist/cli/ contents ==="
ls -la dist/cli/
test -f dist/index.js || (echo "ERROR: dist/index.js not found!" && exit 1)
test -f dist/cli/index.js || (echo "ERROR: dist/cli/index.js not found!" && exit 1)
- name: Publish
run: bun run script/publish.ts

View File

@@ -157,76 +157,23 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
あなたがユーザーの oh-my-opencode 設定を支援する LLM エージェントなら、以下の手順に従ってください:
### ステップ 0: セットアップについてユーザーに尋ねる
### ステップ 0: サブスクリプション情報を確認
OpenCode は様々なモデルをサポートしており、当プラグインは様々な最先端 LLM の使用を提案しています。そのため、ユーザーに以下の質問をする必要があります。
以下の 3 つの質問をして CLI オプションを決定してください:
**重要**: ユーザーがどのプロバイダーを持っているか追跡してください。ユーザーが Claude を持っていると確認した場合のみ、`anthropic/claude-opus-4-5` をフォールバックとして使用できます。Claude がない場合は、代わりに `opencode/big-pickle` をフォールバックとして使用してください。
1. **Claude Pro/Max のサブスクリプションをお持ちですか?**
- **はい**: **max20 (20x モード) を使用していますか?**
- **max20** → `--claude=max20`
- **max20 ではない** → `--claude=yes`
- **いいえ** → `--claude=no`
1. **Claude Pro/Max のサブスクリプションをお持ちですか?** お持ちでない場合、メインオーケストレーターモデルとして 'opencode/big-pickle' (GLM 4.6 として知られる) を使用することになります (非推奨)。
- ユーザーが**はい**と答えた場合、追加質問: **max20 (20x モード) を使用していますか?**
- **「はい」 (max20)**: ToDo 追加: "ユーザーの Claude Pro/Max 認証設定" — librarian に Claude Sonnet 4.5 をフル使用。**記憶: Claude がフォールバックとして使用可能。**
- **「いいえ」 (max20 ではない)**: ToDo 追加: "ユーザーの Claude Pro/Max 認証設定"、そして `~/.config/opencode/oh-my-opencode.json` に以下をマージ。**記憶: Claude がフォールバックとして使用可能。**
```json
{
"agents": {
"librarian": { "model": "opencode/big-pickle" }
}
}
```
- ユーザーが**「いいえ」** (Claude サブスクリプションなし) と答えた場合、`~/.config/opencode/oh-my-opencode.json` に以下を保存。**記憶: Claude は使用不可 — すべてのフォールバックは `opencode/big-pickle` を使用する必要があります。**
```json
{
"agents": {
"Sisyphus": { "model": "opencode/big-pickle" },
"librarian": { "model": "opencode/big-pickle" }
}
}
```
2. **ChatGPT のサブスクリプションをお持ちですか?**
- **はい**`--chatgpt=yes`
- **いいえ** → `--chatgpt=no`
2. **ChatGPT のサブスクリプションをお持ちですか?** お持ちでない場合、oracle エージェントにフォールバックモデルを使用することになります。
- ユーザーが**はい**と答えた場合、ToDo 追加: "ユーザーの ChatGPT 認証設定"
- ユーザーが**いいえ**と答えた場合:
- **Claude が使用可能な場合**: `oh-my-opencode.json` に以下をマージ:
```json
{
"agents": {
"oracle": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **Claude が使用不可の場合**: `oh-my-opencode.json` に以下をマージ:
```json
{
"agents": {
"oracle": { "model": "opencode/big-pickle" }
}
}
```
3. **Gemini モデルを統合しますか?** 統合しない場合、美しいデザインを享受できません。
- ユーザーが**「はい」**と答えた場合、ToDo 追加: "Google OAuth 設定"
- ユーザーが**「いいえ」**と答えた場合:
- **Claude が使用可能な場合**: `oh-my-opencode.json` に以下をマージ:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
"document-writer": { "model": "anthropic/claude-opus-4-5" },
"multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **Claude が使用不可の場合**: `oh-my-opencode.json` に以下をマージ:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
"document-writer": { "model": "opencode/big-pickle" },
"multimodal-looker": { "model": "opencode/big-pickle" }
}
}
```
3. **Gemini モデルを使用しますか?**
- **はい**`--gemini=yes`
- **いいえ**`--gemini=no`
### ステップ 1: OpenCode インストール確認
@@ -241,36 +188,28 @@ fi
OpenCode がインストールされていない場合は、[OpenCode インストールガイド](https://opencode.ai/docs)を確認し、インストールを進めてください。コンテキストを節約するため、サブエージェントを呼び出してインストールを行わせ、その結果を報告させるように案内してください。
### ステップ 2: oh-my-opencode プラグイン設定
### ステップ 2: インストーラーを実行
#### JSONC
- `~/.config/opencode/opencode.jsonc` が存在する場合は、そこで `oh-my-opencode` プラグインを追加してください。
#### JSON
- opencode.jsonc が存在しない場合は、以下のコマンドを実行して作成・登録してください。
ユーザーの回答に基づいて、適切なフラグで CLI インストーラーを実行してください:
```bash
mkdir -p ~/.config/opencode
if [ -f ~/.config/opencode/opencode.json ]; then
# 既存の設定ファイルがある場合: jq または python で plugin を追加
if command -v jq &> /dev/null; then
jq '.plugin = ((.plugin // []) + ["oh-my-opencode"] | unique)' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
else
echo "~/.config/opencode/opencode.json の plugin 配列に \"oh-my-opencode\" を追加してください"
fi
else
# 新しい設定ファイル作成
echo '{"plugin":["oh-my-opencode"]}' > ~/.config/opencode/opencode.json
fi
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no>
```
**例:**
- すべてのサブスクリプション + max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes`
- Claude のみmax20 なし): `bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no`
- サブスクリプションなし: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no`
CLI が行うこと:
- `opencode.json` にプラグインを登録
- サブスクリプションフラグに基づいてエージェントモデルを設定
- 必要な認証ステップを表示
### ステップ 3: 設定確認
```bash
opencode --version # 1.0.132 以上であること
opencode --version # 1.0.150 以上であること
cat ~/.config/opencode/opencode.json # plugin 配列に "oh-my-opencode" が含まれていること
```
@@ -904,6 +843,29 @@ OpenCode が Debian / ArchLinux だとしたら、Oh My OpenCode は Ubuntu / [O
*素晴らしいヒーロー画像を作成してくれた [@junhoyeo](https://github.com/junhoyeo) に感謝します*
## ユーザーレビュー
> "人間が3ヶ月かかる仕事をClaude Codeが7日でやるなら、Sisyphusは1時間でやります"
> -- B, Quant Researcher
> "Oh My Opencodeを使って、たった1日で8000個のeslint警告を解消しました"
> -- Jacob Ferrari, from [X](https://x.com/jacobferrari_/status/2003258761952289061)
> "@yeon_gyu_kimを説得できるなら雇うべきです。彼はopencodeに革命を起こしました"
> -- [Sam Altmanの投稿へ](https://x.com/mysticaltech/status/2001858758608376079)
> "これをコアに取り入れて彼を採用すべきです。マジで。本当に、本当に、本当に良いです"
> -- Henning Kilset, from X
> "やばい、これマジで本物だ @androolloyd oh my opencode 最高すぎる"
> -- z80.eth, from [X](https://x.com/0xz80/status/2001815226505924791)
> "oh-my-opencodeを使ってください、もう戻れませんよ"
> -- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "Oh My Opencodeは頂点に立っています、敵はいません"
> -- [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
## こちらの企業の専門家にご愛用いただいています
- [Indent](https://indentcorp.com)

View File

@@ -154,76 +154,23 @@ OpenCode 가 낭만이 사라진것같은 오늘날의 시대에, 당신에게
당신이 사용자의 oh-my-opencode 설정을 돕는 LLM Agent라면, 다음 단계를 따르세요. 업데이트 과정이라고 할지라도 아래의 내용을 그대로 따르세요. 단, 설치 과정에서 이미 답했을 질문은 또 하지 않습니다.
### 0단계: 사용자에게 설정에 대해 물어보세요.
### 0단계: 구독 정보 확인
OpenCode는 다양한 모델을 지원하며, 이 플러그인은 다양한 프론티어 LLM 사용을 제안합니다. 따라서 사용자에게 다음 질문을 해야 합니다.
사용자에게 다음 3가지 질문을 하여 CLI 옵션을 결정하세요:
**중요**: 사용자가 어떤 프로바이더를 가지고 있는지 추적하세요. 사용자가 Claude가 있다고 확인한 경우에만 `anthropic/claude-opus-4-5`를 폴백으로 사용할 수 있습니다. Claude가 없다면 `opencode/big-pickle`을 폴백으로 사용해야 합니다.
1. **Claude Pro/Max 구독이 있으신가요?**
- **예**: **max20 (20x 모드)를 사용하고 계신가요?**
- **max20** → `--claude=max20`
- **max20 아님** → `--claude=yes`
- **아니오** → `--claude=no`
1. **Claude Pro/Max 구독이 있으신가요?** 그렇지 않다면 메인 오케스트레이터 모델로 'opencode/big-pickle'(GLM 4.6으로 알려짐)을 사용하게 됩니다 (권장하지 않음).
- 사용자가 **'예'**라고 답하면, 추가 질문: **max20 (20x 모드)를 사용하고 계신가요?**
- **'예' (max20)**: 할 일 추가: "사용자 Claude Pro/Max 인증 설정" — librarian에 Claude Sonnet 4.5 전체 사용. **기억: Claude를 폴백으로 사용 가능.**
- **'아니오' (max20 아님)**: 할 일 추가: "사용자 Claude Pro/Max 인증 설정", 그리고 `~/.config/opencode/oh-my-opencode.json`에 다음을 병합. **기억: Claude를 폴백으로 사용 가능.**
```json
{
"agents": {
"librarian": { "model": "opencode/big-pickle" }
}
}
```
- 사용자가 **'아니오'** (Claude 구독 없음)라고 답하면, `~/.config/opencode/oh-my-opencode.json`에 다음을 저장. **기억: Claude 사용 불가 — 모든 폴백은 `opencode/big-pickle`을 사용해야 함.**
```json
{
"agents": {
"Sisyphus": { "model": "opencode/big-pickle" },
"librarian": { "model": "opencode/big-pickle" }
}
}
```
2. **ChatGPT 구독이 있으신가요?**
- **예** → `--chatgpt=yes`
- **아니오** → `--chatgpt=no`
2. **ChatGPT 구독이 있으신가요?** 그렇지 않다면 oracle 에이전트에 폴백 모델을 사용하게 됩니다.
- 사용자가 **'예'**라고 답하면, 할 일 추가: "사용자 ChatGPT 인증 설정"
- 사용자가 **'아니오'**라고 답하면:
- **Claude가 사용 가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
```json
{
"agents": {
"oracle": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **Claude가 사용 불가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
```json
{
"agents": {
"oracle": { "model": "opencode/big-pickle" }
}
}
```
3. **Gemini 모델을 통합하시겠습니까?** 그렇지 않다면 아름다운 디자인을 즐길 수 없습니다.
- 사용자가 **'예'**라고 답하면, 할 일 추가: "Google OAuth 설정"
- 사용자가 **'아니오'**라고 답하면:
- **Claude가 사용 가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
"document-writer": { "model": "anthropic/claude-opus-4-5" },
"multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **Claude가 사용 불가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
"document-writer": { "model": "opencode/big-pickle" },
"multimodal-looker": { "model": "opencode/big-pickle" }
}
}
```
3. **Gemini 모델을 사용하시겠습니까?**
- **예** → `--gemini=yes`
- **아니오**`--gemini=no`
### 1단계: OpenCode 설치 확인
@@ -238,36 +185,28 @@ fi
OpenCode가 설치되어 있지 않다면, [OpenCode 설치 가이드](https://opencode.ai/docs)를 읽고, 설치를 진행하세요. 컨텍스트를 줄이기 위해 Subagent 로 호출하여 설치를 하라고 안내하고, 그 결과를 보고 받는다고 말하세요.
### 2단계: oh-my-opencode 플러그인 설정
### 2단계: 설치 프로그램 실행
#### JSONC
- `~/.config/opencode/opencode.jsonc`가 존재한다면, 그곳에 `oh-my-opencode` 플러그인을 추가하세요.
#### JSON
- opencode.jsonc가 없다면, 다음 명령어를 실행하여 생성하고 등록하세요.
사용자의 답변을 바탕으로 적절한 플래그와 함께 CLI 설치 프로그램을 실행하세요:
```bash
mkdir -p ~/.config/opencode
if [ -f ~/.config/opencode/opencode.json ]; then
# 기존 설정 파일이 있는 경우: jq 또는 python으로 plugin 추가
if command -v jq &> /dev/null; then
jq '.plugin = ((.plugin // []) + ["oh-my-opencode"] | unique)' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
else
echo "~/.config/opencode/opencode.json의 plugin 배열에 \"oh-my-opencode\"를 추가하세요"
fi
else
# 새 설정 파일 생성
echo '{"plugin":["oh-my-opencode"]}' > ~/.config/opencode/opencode.json
fi
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no>
```
**예시:**
- 모든 구독 + max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes`
- Claude만 (max20 없음): `bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no`
- 구독 없음: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no`
CLI가 수행하는 작업:
- `opencode.json`에 플러그인 등록
- 구독 플래그에 따라 에이전트 모델 설정
- 필요한 인증 단계 안내
### 3단계: 설정 확인
```bash
opencode --version # 1.0.132 이상이어야 함
opencode --version # 1.0.150 이상이어야 함
cat ~/.config/opencode/opencode.json # plugin 배열에 "oh-my-opencode"가 있어야 함
```
@@ -898,6 +837,29 @@ OpenCode 를 사용하여 이 프로젝트의 99% 를 작성했습니다. 기능
*멋진 히어로 이미지를 만들어주신 히어로 [@junhoyeo](https://github.com/junhoyeo) 께 감사드립니다*
## 사용자 후기
> "인간이 3달 동안 할 일을 claude code 가 7일만에 해준다면, 시지푸스는 1시간만에 해준다"
> -- B, Quant Researcher
> "Oh My Opencode를 사용해서, 단 하루만에 8000개의 eslint 경고를 해결했습니다"
> -- Jacob Ferrari, from [X](https://x.com/jacobferrari_/status/2003258761952289061)
> "@yeon_gyu_kim 을 설득할 수 있다면 고용하세요, 이 사람은 opencode를 혁신했습니다."
> -- [to Sam Altman's post](https://x.com/mysticaltech/status/2001858758608376079)
> "이걸 코어에 넣고 그를 채용해야 합니다. 진심으로요. 이건 정말, 정말, 정말 좋습니다."
> -- Henning Kilset, from X
> "와 미쳤다 @androolloyd 이건 진짜다 oh my opencode 개쩐다"
> -- z80.eth, from [X](https://x.com/0xz80/status/2001815226505924791)
> "oh-my-opencode를 쓰세요, 절대 돌아갈 수 없을 겁니다"
> -- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "Oh My Opencode는 독보적입니다, 경쟁자가 없습니다"
> -- [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
## 다음 기업의 능력있는 개인들이 사용하고 있습니다
- [Indent](https://indentcorp.com)

160
README.md
View File

@@ -168,7 +168,17 @@ If you don't want all this, as mentioned, you can just pick and choose specific
### For Humans
Let the LLM Agent handle the grunt work. Paste this into a fresh opencode session:
Run the interactive installer:
```bash
bunx oh-my-opencode install
```
Follow the prompts to configure your Claude, ChatGPT, and Gemini subscriptions. After installation, authenticate your providers as instructed.
**Alternative: Let an LLM Agent do it**
Paste this into a fresh opencode session:
```
Install and configure by following the instructions here https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/README.md
```
@@ -182,80 +192,25 @@ If you're an LLM Agent helping set up oh-my-opencode, Welcome! You'll now help y
follow these steps:
### Step 0: Ask user about the setup.
### Step 0: Ask user about subscriptions
Since OpenCode supports various models, and our plugin suggests various frontier LLMs, you must ask user the following questions.
Ask the user these 3 questions to determine CLI options:
**IMPORTANT**: Track which providers the user has. Only use `anthropic/claude-opus-4-5` as fallback when the user confirmed they have Claude. If they don't have Claude, use `opencode/big-pickle` as fallback instead.
1. **Do you have a Claude Pro/Max Subscription?**
- If **yes**: **Are you on max20 (20x mode)?**
- **max20** → `--claude=max20`
- **not max20** → `--claude=yes`
- If **no**`--claude=no`
1. **Do you have a Claude Pro/Max Subscription?** Otherwise, we'll use 'opencode/big-pickle' (known for GLM 4.6) as a main orchestrator model (not recommended)
- If user said **yes**, ask follow-up: **Are you on max20 (20x mode)?**
- If **yes (max20)**: Add ToDo: "Setup user Claude Pro/Max Auth" — full Claude Sonnet 4.5 for librarian. **Remember: Claude is available for fallbacks.**
- If **no (not max20)**: Add ToDo: "Setup user Claude Pro/Max Auth", then merge the following to `~/.config/opencode/oh-my-opencode.json`. **Remember: Claude is available for fallbacks.**
```json
{
"agents": {
"librarian": { "model": "opencode/big-pickle" }
}
}
```
- If user said **no** (no Claude subscription), merge the following to `~/.config/opencode/oh-my-opencode.json`. **Remember: Claude is NOT available — all fallbacks must use `opencode/big-pickle`.**
```json
{
"agents": {
"Sisyphus": { "model": "opencode/big-pickle" },
"librarian": { "model": "opencode/big-pickle" }
}
}
```
2. **Do you have a ChatGPT Subscription?**
- If **yes**`--chatgpt=yes`
- If **no**`--chatgpt=no`
2. **Do you have a ChatGPT Subscription?** Otherwise, we'll use a fallback model for oracle agent.
- If user said **yes**, add ToDo: "Setup user ChatGPT Auth"
- If user said **no**:
- **If Claude is available**: merge the following to `oh-my-opencode.json`:
```json
{
"agents": {
"oracle": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **If Claude is NOT available**: merge the following to `oh-my-opencode.json`:
```json
{
"agents": {
"oracle": { "model": "opencode/big-pickle" }
}
}
```
3. **Will you integrate Gemini models?**
- If **yes**`--gemini=yes`
- If **no**`--gemini=no`
3. **Will you integrate Gemini models?** Otherwise, you cannot enjoy the beautiful design.
- If user said **yes**, add ToDo: "Setup Google OAuth"
- If user said **no**:
- **If Claude is available**: merge the following to `oh-my-opencode.json`:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
"document-writer": { "model": "anthropic/claude-opus-4-5" },
"multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **If Claude is NOT available**: merge the following to `oh-my-opencode.json`:
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
"document-writer": { "model": "opencode/big-pickle" },
"multimodal-looker": { "model": "opencode/big-pickle" }
}
}
```
### Step 1: Install OpenCode, if not
Run following command to verify OpenCode Installation:
### Step 1: Install OpenCode (if not installed)
```bash
if command -v opencode &> /dev/null; then
@@ -269,37 +224,29 @@ fi
If OpenCode isn't installed, check the [OpenCode Installation Guide](https://opencode.ai/docs).
Spawn a subagent to handle installation and report back - to save context.
### Step 2: Configure oh-my-opencode Plugin
### Step 2: Run the installer
#### JSONC
- If `~/.config/opencode/opencode.jsonc` exists, work in there to add `oh-my-opencode` plugin
#### JSON
- If no opencode.jsonc exists, run following command to create and register it.
Based on user's answers, run the CLI installer with appropriate flags:
```bash
mkdir -p ~/.config/opencode
if [ -f ~/.config/opencode/opencode.json ]; then
# Existing config: add plugin using jq or python
if command -v jq &> /dev/null; then
jq '.plugin = ((.plugin // []) + ["oh-my-opencode"] | unique)' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
else
echo "Please add \"oh-my-opencode\" to the plugin array in ~/.config/opencode/opencode.json"
fi
else
# Create new config
echo '{"plugin":["oh-my-opencode"]}' > ~/.config/opencode/opencode.json
fi
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no>
```
**Examples:**
- User has all subscriptions with max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes`
- User has only Claude (no max20): `bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no`
- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no`
The CLI will:
- Register the plugin in `opencode.json`
- Configure agent models based on subscription flags
- Show which auth steps are needed
### Step 3: Verify Setup
```bash
opencode --version # Should be 1.0.132 or higher
cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugin array, or maybe check jsonc
opencode --version # Should be 1.0.150 or higher
cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugin array
```
### Step 4: Configure Authentication
@@ -491,8 +438,8 @@ To remove oh-my-opencode:
- **oracle** (`openai/gpt-5.2`): Architecture, code review, strategy. Uses GPT-5.2 for its stellar logical reasoning and deep analysis. Inspired by AmpCode.
- **librarian** (`anthropic/claude-sonnet-4-5`): Multi-repo analysis, doc lookup, implementation examples. Uses Claude Sonnet 4.5 for deep codebase understanding and GitHub research with evidence-based answers. Inspired by AmpCode.
- **explore** (`opencode/grok-code`): Fast codebase exploration and pattern matching. Claude Code uses Haiku; we use Grok—it's free, blazing fast, and plenty smart for file traversal. Inspired by Claude Code.
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
- **document-writer** (`google/gemini-3-pro-preview`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-high`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
- **document-writer** (`google/gemini-3-flash`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
- **multimodal-looker** (`google/gemini-3-flash`): Visual content specialist. Analyzes PDFs, images, diagrams to extract information.
The main agent invokes these automatically, but you can call them explicitly:
@@ -962,6 +909,29 @@ I have no affiliation with any project or model mentioned here. This is purely p
*Special thanks to [@junhoyeo](https://github.com/junhoyeo) for this amazing hero image.*
## Reviews
> "If Claude Code does in 7 days what human does in 3 months, Sisyphus does in 1 hour"
> -- B, Quant Researcher
> "Knocked out 8000 eslint warnings with Oh My Opencode, just in a day"
> -- Jacob Ferrari, from [X](https://x.com/jacobferrari_/status/2003258761952289061)
> "Hire @yeon_gyu_kim if you can convince him, this dude has revolutionized opencode."
> -- [to Sam Altman's post](https://x.com/mysticaltech/status/2001858758608376079)
> "You guys should pull this into core and recruit him. Seriously. It's really, really, really good."
> -- Henning Kilset, from X
> "ok yeah holy shit @androolloyd this thing is legit oh my opencode is sick"
> -- z80.eth, from [X](https://x.com/0xz80/status/2001815226505924791)
> "use oh-my-opencode, you will never go back"
> -- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "Oh My Opencode is king of the hill and has no contenders"
> -- [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
## Loved by professionals at
- [Indent](https://indentcorp.com)

View File

@@ -163,76 +163,23 @@ Install and configure by following the instructions here https://raw.githubuserc
按步骤来:
### 步骤 0摸清用户底细
### 步骤 0确认订阅情况
OpenCode 支持各种模型,我们这个插件也建议用各种前沿 LLM。所以先问清楚。
问用户这 3 个问题来确定 CLI 参数:
**重要**:追踪用户有哪些 provider。只有用户确认有 Claude 的情况下,才能用 `anthropic/claude-opus-4-5` 做 fallback。没有 Claude 就用 `opencode/big-pickle` 做 fallback。
1. **有 Claude Pro/Max 订阅吗?**
- **有****用的是 max20 (20x 模式) 吗?**
- **max20** → `--claude=max20`
- **不是 max20** → `--claude=yes`
- **没有** → `--claude=no`
1. **有 Claude Pro/Max 订阅吗?** 没有的话,主编排模型只能用 'opencode/big-pickle'(也就是 GLM 4.6),这体验可不太好(不推荐)。
- 用户说**有**,继续问:**你用的是 max20 (20x 模式) 吗?**
- **是 (max20)**:加个待办:"配置用户 Claude Pro/Max 认证" — librarian 用完整的 Claude Sonnet 4.5。**记住Claude 可以做 fallback。**
- **不是 (没有 max20)**:加个待办:"配置用户 Claude Pro/Max 认证",然后把这个合并到 `~/.config/opencode/oh-my-opencode.json`。**记住Claude 可以做 fallback。**
```json
{
"agents": {
"librarian": { "model": "opencode/big-pickle" }
}
}
```
- 用户说**没有** (没有 Claude 订阅),把这坨配置写到 `~/.config/opencode/oh-my-opencode.json`。**记住:没有 Claude — 所有 fallback 都得用 `opencode/big-pickle`。**
```json
{
"agents": {
"Sisyphus": { "model": "opencode/big-pickle" },
"librarian": { "model": "opencode/big-pickle" }
}
}
```
2. **有 ChatGPT 订阅吗?**
- **有**`--chatgpt=yes`
- **没有** → `--chatgpt=no`
2. **有 ChatGPT 订阅吗?** 没有的话oracle Agent 就用 fallback 模型了。
- 用户说****,加个待办:"配置用户 ChatGPT 认证"
- 用户说**没有**
- **有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`
```json
{
"agents": {
"oracle": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **没有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`
```json
{
"agents": {
"oracle": { "model": "opencode/big-pickle" }
}
}
```
3. **要接 Gemini 模型吗?** 不接的话,那么漂亮的 UI 设计你就无福消受了。
- 用户说**要**,加个待办:"配置 Google OAuth"
- 用户说**不要**
- **有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
"document-writer": { "model": "anthropic/claude-opus-4-5" },
"multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
}
}
```
- **没有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`
```json
{
"agents": {
"frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
"document-writer": { "model": "opencode/big-pickle" },
"multimodal-looker": { "model": "opencode/big-pickle" }
}
}
```
3. **要用 Gemini 模型吗?**
- ****`--gemini=yes`
- **不要** → `--gemini=no`
### 步骤 1确认 OpenCode 装没装
@@ -249,36 +196,28 @@ fi
如果没装,看 [OpenCode 安装指南](https://opencode.ai/docs)。可以生成个子 Agent 去搞定安装,完事汇报——省点上下文。
### 步骤 2配置 oh-my-opencode 插件
### 步骤 2跑安装程序
#### JSONC
- 如果 `~/.config/opencode/opencode.jsonc` 存在,就在里面加 `oh-my-opencode` 插件。
#### JSON
- 如果没 opencode.jsonc跑这个命令创建并注册
根据用户的回答,用对应的参数运行 CLI 安装程序:
```bash
mkdir -p ~/.config/opencode
if [ -f ~/.config/opencode/opencode.json ]; then
# 已有配置:用 jq 或 python 加插件
if command -v jq &> /dev/null; then
jq '.plugin = ((.plugin // []) + ["oh-my-opencode"] | unique)' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
else
echo "请手动在 ~/.config/opencode/opencode.json 的 plugin 数组里加上 \"oh-my-opencode\""
fi
else
# 新建配置
echo '{"plugin":["oh-my-opencode"]}' > ~/.config/opencode/opencode.json
fi
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no>
```
**例子:**
- 全套订阅 + max20`bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes`
- 只有 Claude没 max20`bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no`
- 啥订阅都没有:`bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no`
CLI 会干这些事:
-`opencode.json` 注册插件
- 根据订阅参数配置 Agent 模型
- 告诉你接下来要做哪些认证
### 步骤 3验货
```bash
opencode --version # 得是 1.0.132 以上
opencode --version # 得是 1.0.150 以上
cat ~/.config/opencode/opencode.json # plugin 数组里得有 "oh-my-opencode"
```
@@ -858,7 +797,6 @@ Oh My OpenCode 送你重构工具(重命名、代码操作)。
**警告**:这些功能是实验性的,可能会导致意外行为。只有在理解其影响的情况下才启用。
## 作者的话
装个 Oh My OpenCode 试试。
@@ -904,6 +842,29 @@ Oh My OpenCode 送你重构工具(重命名、代码操作)。
*感谢 [@junhoyeo](https://github.com/junhoyeo) 制作了这张超帅的 hero 图。*
## 用户评价
> "如果 Claude Code 能在 7 天内完成人类 3 个月的工作,那么 Sisyphus 只需要 1 小时"
> -- B, Quant Researcher
> "只用了一天,就用 Oh My Opencode 干掉了 8000 个 eslint 警告"
> -- Jacob Ferrari, from [X](https://x.com/jacobferrari_/status/2003258761952289061)
> "如果你能说服 @yeon_gyu_kim就雇佣他吧这家伙彻底彻底改变了 opencode"
> -- [回复 Sam Altman 的帖子](https://x.com/mysticaltech/status/2001858758608376079)
> "你们应该把它合并到核心代码里并聘用他。认真的。这真的、真的、真的很好"
> -- Henning Kilset, from X
> "哇靠 @androolloyd 这玩意儿是真的oh my opencode 太强了"
> -- z80.eth, from [X](https://x.com/0xz80/status/2001815226505924791)
> "用了 oh-my-opencode你就回不去了"
> -- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "Oh My Opencode 独孤求败,没有对手"
> -- [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
## 以下企业的专业人士都在用
- [Indent](https://indentcorp.com)

View File

@@ -1225,6 +1225,9 @@
"type": "number",
"minimum": 0.5,
"maximum": 0.95
},
"truncate_all_tool_outputs": {
"type": "boolean"
}
}
},

View File

@@ -7,10 +7,13 @@
"dependencies": {
"@ast-grep/cli": "^0.40.0",
"@ast-grep/napi": "^0.40.0",
"@clack/prompts": "^0.11.0",
"@code-yeongyu/comment-checker": "^0.6.0",
"@openauthjs/openauth": "^0.4.3",
"@opencode-ai/plugin": "^1.0.162",
"commander": "^14.0.2",
"hono": "^4.10.4",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"xdg-basedir": "^5.1.0",
"zod": "^4.1.8",
@@ -64,6 +67,10 @@
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
"@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.6.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-VtDPrhbUJcb5BIS18VMcY/N/xSLbMr6dpU9MO1NYQyEDhI4pSIx07K4gOlCutG/nHVCjO+HEarn8rttODP+5UA=="],
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
@@ -94,14 +101,20 @@
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
"jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],

View File

@@ -1,10 +1,13 @@
{
"name": "oh-my-opencode",
"version": "2.4.7",
"version": "2.5.1",
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"oh-my-opencode": "./dist/cli/index.js"
},
"files": [
"dist"
],
@@ -20,7 +23,7 @@
"./schema.json": "./dist/oh-my-opencode.schema.json"
},
"scripts": {
"build": "bun build src/index.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun run build:schema",
"build": "bun build src/index.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && bun run build:schema",
"build:schema": "bun run script/build-schema.ts",
"clean": "rm -rf dist",
"prepublishOnly": "bun run clean && bun run build",
@@ -49,10 +52,13 @@
"dependencies": {
"@ast-grep/cli": "^0.40.0",
"@ast-grep/napi": "^0.40.0",
"@clack/prompts": "^0.11.0",
"@code-yeongyu/comment-checker": "^0.6.0",
"@openauthjs/openauth": "^0.4.3",
"@opencode-ai/plugin": "^1.0.162",
"commander": "^14.0.2",
"hono": "^4.10.4",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"xdg-basedir": "^5.1.0",
"zod": "^4.1.8"

View File

@@ -122,7 +122,7 @@ async function gitTagAndRelease(newVersion: string, notes: string[]): Promise<vo
console.log("\nCommitting and tagging...")
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
await $`git config user.name "github-actions[bot]"`
await $`git add package.json`
await $`git add package.json assets/oh-my-opencode.schema.json`
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow()
if (hasStagedChanges.exitCode !== 0) {

View File

@@ -5,10 +5,10 @@
*/
import {
ANTIGRAVITY_HEADERS,
ANTIGRAVITY_ENDPOINT_FALLBACKS,
ANTIGRAVITY_API_VERSION,
SKIP_THOUGHT_SIGNATURE_VALIDATOR,
ANTIGRAVITY_API_VERSION,
ANTIGRAVITY_ENDPOINT_FALLBACKS,
ANTIGRAVITY_HEADERS,
SKIP_THOUGHT_SIGNATURE_VALIDATOR,
} from "./constants"
import type { AntigravityRequestBody } from "./types"
@@ -262,7 +262,7 @@ export function transformRequest(options: TransformRequestOptions): TransformedR
} = options
const effectiveModel =
modelName || extractModelFromBody(body) || extractModelFromUrl(url) || "gemini-3-pro-preview"
modelName || extractModelFromBody(body) || extractModelFromUrl(url) || "gemini-3-pro-high"
const streaming = isStreamingRequest(url, body)
const action = streaming ? "streamGenerateContent" : "generateContent"

View File

@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test"
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager"
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
test("Gemini models include full spec (limit + modalities)", () => {
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
expect(google).toBeTruthy()
const models = google.models as Record<string, any>
expect(models).toBeTruthy()
const required = [
"gemini-3-pro-high",
"gemini-3-pro-medium",
"gemini-3-pro-low",
"gemini-3-flash",
"gemini-3-flash-lite",
]
for (const key of required) {
const model = models[key]
expect(model).toBeTruthy()
expect(typeof model.name).toBe("string")
expect(model.name.includes("(Antigravity)")).toBe(true)
expect(model.limit).toBeTruthy()
expect(typeof model.limit.context).toBe("number")
expect(typeof model.limit.output).toBe("number")
expect(model.modalities).toBeTruthy()
expect(Array.isArray(model.modalities.input)).toBe(true)
expect(Array.isArray(model.modalities.output)).toBe(true)
}
})
})

514
src/cli/config-manager.ts Normal file
View File

@@ -0,0 +1,514 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
const OPENCODE_PACKAGE_JSON = join(OPENCODE_CONFIG_DIR, "package.json")
const OMO_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json")
const CHATGPT_HOTFIX_REPO = "code-yeongyu/opencode-openai-codex-auth#fix/orphaned-function-call-output-with-tools"
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
try {
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`)
if (!res.ok) return null
const data = await res.json() as { version: string }
return data.version
} catch {
return null
}
}
type ConfigFormat = "json" | "jsonc" | "none"
interface OpenCodeConfig {
plugin?: string[]
[key: string]: unknown
}
export function detectConfigFormat(): { format: ConfigFormat; path: string } {
if (existsSync(OPENCODE_JSONC)) {
return { format: "jsonc", path: OPENCODE_JSONC }
}
if (existsSync(OPENCODE_JSON)) {
return { format: "json", path: OPENCODE_JSON }
}
return { format: "none", path: OPENCODE_JSON }
}
function stripJsoncComments(content: string): string {
let result = ""
let i = 0
let inString = false
let escape = false
while (i < content.length) {
const char = content[i]
if (escape) {
result += char
escape = false
i++
continue
}
if (char === "\\") {
result += char
escape = true
i++
continue
}
if (char === '"' && !inString) {
inString = true
result += char
i++
continue
}
if (char === '"' && inString) {
inString = false
result += char
i++
continue
}
if (inString) {
result += char
i++
continue
}
// Outside string - check for comments
if (char === "/" && content[i + 1] === "/") {
// Line comment - skip to end of line
while (i < content.length && content[i] !== "\n") {
i++
}
continue
}
if (char === "/" && content[i + 1] === "*") {
// Block comment - skip to */
i += 2
while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) {
i++
}
i += 2
continue
}
result += char
i++
}
return result.replace(/,(\s*[}\]])/g, "$1")
}
function parseConfig(path: string, isJsonc: boolean): OpenCodeConfig | null {
try {
const content = readFileSync(path, "utf-8")
const cleaned = isJsonc ? stripJsoncComments(content) : content
return JSON.parse(cleaned) as OpenCodeConfig
} catch {
return null
}
}
function ensureConfigDir(): void {
if (!existsSync(OPENCODE_CONFIG_DIR)) {
mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true })
}
}
export function addPluginToOpenCodeConfig(): ConfigMergeResult {
ensureConfigDir()
const { format, path } = detectConfigFormat()
const pluginName = "oh-my-opencode"
try {
if (format === "none") {
const config: OpenCodeConfig = { plugin: [pluginName] }
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
return { success: true, configPath: path }
}
const config = parseConfig(path, format === "jsonc")
if (!config) {
return { success: false, configPath: path, error: "Failed to parse config" }
}
const plugins = config.plugin ?? []
if (plugins.some((p) => p.startsWith(pluginName))) {
return { success: true, configPath: path }
}
config.plugin = [...plugins, pluginName]
if (format === "jsonc") {
const content = readFileSync(path, "utf-8")
const pluginArrayRegex = /"plugin"\s*:\s*\[([\s\S]*?)\]/
const match = content.match(pluginArrayRegex)
if (match) {
const arrayContent = match[1].trim()
const newArrayContent = arrayContent
? `${arrayContent},\n "${pluginName}"`
: `"${pluginName}"`
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${newArrayContent}\n ]`)
writeFileSync(path, newContent)
} else {
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginName}"],`)
writeFileSync(path, newContent)
}
} else {
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
}
return { success: true, configPath: path }
} catch (err) {
return { success: false, configPath: path, error: String(err) }
}
}
function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
const result = { ...target }
for (const key of Object.keys(source) as Array<keyof T>) {
const sourceValue = source[key]
const targetValue = result[key]
if (
sourceValue !== null &&
typeof sourceValue === "object" &&
!Array.isArray(sourceValue) &&
targetValue !== null &&
typeof targetValue === "object" &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(
targetValue as Record<string, unknown>,
sourceValue as Record<string, unknown>
) as T[keyof T]
} else if (sourceValue !== undefined) {
result[key] = sourceValue as T[keyof T]
}
}
return result
}
export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
const config: Record<string, unknown> = {
$schema: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
}
if (installConfig.hasGemini) {
config.google_auth = false
}
const agents: Record<string, Record<string, unknown>> = {}
if (!installConfig.hasClaude) {
agents["Sisyphus"] = { model: "opencode/big-pickle" }
agents["librarian"] = { model: "opencode/big-pickle" }
} else if (!installConfig.isMax20) {
agents["librarian"] = { model: "opencode/big-pickle" }
}
if (!installConfig.hasChatGPT) {
agents["oracle"] = {
model: installConfig.hasClaude ? "anthropic/claude-opus-4-5" : "opencode/big-pickle",
}
}
if (installConfig.hasGemini) {
agents["frontend-ui-ux-engineer"] = { model: "google/gemini-3-pro-high" }
agents["document-writer"] = { model: "google/gemini-3-flash" }
agents["multimodal-looker"] = { model: "google/gemini-3-flash" }
} else {
const fallbackModel = installConfig.hasClaude ? "anthropic/claude-opus-4-5" : "opencode/big-pickle"
agents["frontend-ui-ux-engineer"] = { model: fallbackModel }
agents["document-writer"] = { model: fallbackModel }
agents["multimodal-looker"] = { model: fallbackModel }
}
if (Object.keys(agents).length > 0) {
config.agents = agents
}
return config
}
export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult {
ensureConfigDir()
try {
const newConfig = generateOmoConfig(installConfig)
if (existsSync(OMO_CONFIG)) {
const content = readFileSync(OMO_CONFIG, "utf-8")
const cleaned = stripJsoncComments(content)
const existing = JSON.parse(cleaned) as Record<string, unknown>
delete existing.agents
const merged = deepMerge(existing, newConfig)
writeFileSync(OMO_CONFIG, JSON.stringify(merged, null, 2) + "\n")
} else {
writeFileSync(OMO_CONFIG, JSON.stringify(newConfig, null, 2) + "\n")
}
return { success: true, configPath: OMO_CONFIG }
} catch (err) {
return { success: false, configPath: OMO_CONFIG, error: String(err) }
}
}
export async function isOpenCodeInstalled(): Promise<boolean> {
try {
const proc = Bun.spawn(["opencode", "--version"], {
stdout: "pipe",
stderr: "pipe",
})
await proc.exited
return proc.exitCode === 0
} catch {
return false
}
}
export async function getOpenCodeVersion(): Promise<string | null> {
try {
const proc = Bun.spawn(["opencode", "--version"], {
stdout: "pipe",
stderr: "pipe",
})
const output = await new Response(proc.stdout).text()
await proc.exited
return proc.exitCode === 0 ? output.trim() : null
} catch {
return null
}
}
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
ensureConfigDir()
const { format, path } = detectConfigFormat()
try {
const existingConfig = format !== "none" ? parseConfig(path, format === "jsonc") : null
const plugins: string[] = existingConfig?.plugin ?? []
if (config.hasGemini) {
const version = await fetchLatestVersion("opencode-antigravity-auth")
const pluginEntry = version ? `opencode-antigravity-auth@${version}` : "opencode-antigravity-auth"
if (!plugins.some((p) => p.startsWith("opencode-antigravity-auth"))) {
plugins.push(pluginEntry)
}
}
if (config.hasChatGPT) {
if (!plugins.some((p) => p.startsWith("opencode-openai-codex-auth"))) {
plugins.push("opencode-openai-codex-auth")
}
}
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
return { success: true, configPath: path }
} catch (err) {
return { success: false, configPath: path, error: String(err) }
}
}
export function setupChatGPTHotfix(): ConfigMergeResult {
ensureConfigDir()
try {
let packageJson: Record<string, unknown> = {}
if (existsSync(OPENCODE_PACKAGE_JSON)) {
const content = readFileSync(OPENCODE_PACKAGE_JSON, "utf-8")
packageJson = JSON.parse(content)
}
const deps = (packageJson.dependencies ?? {}) as Record<string, string>
deps["opencode-openai-codex-auth"] = CHATGPT_HOTFIX_REPO
packageJson.dependencies = deps
writeFileSync(OPENCODE_PACKAGE_JSON, JSON.stringify(packageJson, null, 2) + "\n")
return { success: true, configPath: OPENCODE_PACKAGE_JSON }
} catch (err) {
return { success: false, configPath: OPENCODE_PACKAGE_JSON, error: String(err) }
}
}
export async function runBunInstall(): Promise<boolean> {
try {
const proc = Bun.spawn(["bun", "install"], {
cwd: OPENCODE_CONFIG_DIR,
stdout: "pipe",
stderr: "pipe",
})
await proc.exited
return proc.exitCode === 0
} catch {
return false
}
}
export const ANTIGRAVITY_PROVIDER_CONFIG = {
google: {
name: "Google",
api: "antigravity",
// NOTE: opencode-antigravity-auth expects full model specs (name/limit/modalities).
// If these are incomplete, models may appear but fail at runtime (e.g. 404).
models: {
"gemini-3-pro-high": {
name: "Gemini 3 Pro High (Antigravity)",
thinking: true,
attachment: true,
limit: { context: 1048576, output: 65535 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"gemini-3-pro-medium": {
name: "Gemini 3 Pro Medium (Antigravity)",
thinking: true,
attachment: true,
limit: { context: 1048576, output: 65535 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"gemini-3-pro-low": {
name: "Gemini 3 Pro Low (Antigravity)",
thinking: true,
attachment: true,
limit: { context: 1048576, output: 65535 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"gemini-3-flash": {
name: "Gemini 3 Flash (Antigravity)",
attachment: true,
limit: { context: 1048576, output: 65536 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"gemini-3-flash-lite": {
name: "Gemini 3 Flash Lite (Antigravity)",
attachment: true,
limit: { context: 1048576, output: 65536 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
},
},
}
const CODEX_PROVIDER_CONFIG = {
openai: {
name: "OpenAI",
api: "codex",
models: {
"gpt-5.2": { name: "GPT-5.2" },
"o3": { name: "o3", thinking: true },
"o4-mini": { name: "o4-mini", thinking: true },
"codex-1": { name: "Codex-1" },
},
},
}
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
ensureConfigDir()
const { format, path } = detectConfigFormat()
try {
const existingConfig = format !== "none" ? parseConfig(path, format === "jsonc") : null
const newConfig = { ...(existingConfig ?? {}) }
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
if (config.hasGemini) {
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
}
if (config.hasChatGPT) {
providers.openai = CODEX_PROVIDER_CONFIG.openai
}
if (Object.keys(providers).length > 0) {
newConfig.provider = providers
}
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
return { success: true, configPath: path }
} catch (err) {
return { success: false, configPath: path, error: String(err) }
}
}
interface OmoConfigData {
google_auth?: boolean
agents?: Record<string, { model?: string }>
}
export function detectCurrentConfig(): DetectedConfig {
const result: DetectedConfig = {
isInstalled: false,
hasClaude: true,
isMax20: true,
hasChatGPT: true,
hasGemini: false,
}
const { format, path } = detectConfigFormat()
if (format === "none") {
return result
}
const openCodeConfig = parseConfig(path, format === "jsonc")
if (!openCodeConfig) {
return result
}
const plugins = openCodeConfig.plugin ?? []
result.isInstalled = plugins.some((p) => p.startsWith("oh-my-opencode"))
if (!result.isInstalled) {
return result
}
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
result.hasChatGPT = plugins.some((p) => p.startsWith("opencode-openai-codex-auth"))
if (!existsSync(OMO_CONFIG)) {
return result
}
try {
const content = readFileSync(OMO_CONFIG, "utf-8")
const omoConfig = JSON.parse(stripJsoncComments(content)) as OmoConfigData
const agents = omoConfig.agents ?? {}
if (agents["Sisyphus"]?.model === "opencode/big-pickle") {
result.hasClaude = false
result.isMax20 = false
} else if (agents["librarian"]?.model === "opencode/big-pickle") {
result.hasClaude = true
result.isMax20 = false
}
if (agents["oracle"]?.model?.startsWith("anthropic/")) {
result.hasChatGPT = false
} else if (agents["oracle"]?.model === "opencode/big-pickle") {
result.hasChatGPT = false
}
if (omoConfig.google_auth === false) {
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
}
} catch {
/* intentionally empty - malformed config returns defaults */
}
return result
}

54
src/cli/index.ts Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bun
import { Command } from "commander"
import { install } from "./install"
import type { InstallArgs } from "./types"
const packageJson = await import("../../package.json")
const VERSION = packageJson.version
const program = new Command()
program
.name("oh-my-opencode")
.description("The ultimate OpenCode plugin - multi-model orchestration, LSP tools, and more")
.version(VERSION, "-v, --version", "Show version number")
program
.command("install")
.description("Install and configure oh-my-opencode with interactive setup")
.option("--no-tui", "Run in non-interactive mode (requires all options)")
.option("--claude <value>", "Claude subscription: no, yes, max20")
.option("--chatgpt <value>", "ChatGPT subscription: no, yes")
.option("--gemini <value>", "Gemini integration: no, yes")
.option("--skip-auth", "Skip authentication setup hints")
.addHelpText("after", `
Examples:
$ bunx oh-my-opencode install
$ bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes
$ bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no
Model Providers:
Claude Required for Sisyphus (main orchestrator) and Librarian agents
ChatGPT Powers the Oracle agent for debugging and architecture
Gemini Powers frontend, documentation, and multimodal agents
`)
.action(async (options) => {
const args: InstallArgs = {
tui: options.tui !== false,
claude: options.claude,
chatgpt: options.chatgpt,
gemini: options.gemini,
skipAuth: options.skipAuth ?? false,
}
const exitCode = await install(args)
process.exit(exitCode)
})
program
.command("version")
.description("Show version information")
.action(() => {
console.log(`oh-my-opencode v${VERSION}`)
})
program.parse()

456
src/cli/install.ts Normal file
View File

@@ -0,0 +1,456 @@
import * as p from "@clack/prompts"
import color from "picocolors"
import type { InstallArgs, InstallConfig, ClaudeSubscription, BooleanArg, DetectedConfig } from "./types"
import {
addPluginToOpenCodeConfig,
writeOmoConfig,
isOpenCodeInstalled,
getOpenCodeVersion,
addAuthPlugins,
setupChatGPTHotfix,
runBunInstall,
addProviderConfig,
detectCurrentConfig,
} from "./config-manager"
const SYMBOLS = {
check: color.green("✓"),
cross: color.red("✗"),
arrow: color.cyan("→"),
bullet: color.dim("•"),
info: color.blue(""),
warn: color.yellow("⚠"),
star: color.yellow("★"),
}
function formatProvider(name: string, enabled: boolean, detail?: string): string {
const status = enabled ? SYMBOLS.check : color.dim("○")
const label = enabled ? color.white(name) : color.dim(name)
const suffix = detail ? color.dim(` (${detail})`) : ""
return ` ${status} ${label}${suffix}`
}
function formatConfigSummary(config: InstallConfig): string {
const lines: string[] = []
lines.push(color.bold(color.white("Configuration Summary")))
lines.push("")
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
lines.push(formatProvider("ChatGPT", config.hasChatGPT))
lines.push(formatProvider("Gemini", config.hasGemini))
lines.push("")
lines.push(color.dim("─".repeat(40)))
lines.push("")
lines.push(color.bold(color.white("Agent Configuration")))
lines.push("")
const sisyphusModel = config.hasClaude ? "claude-opus-4-5" : "big-pickle"
const oracleModel = config.hasChatGPT ? "gpt-5.2" : (config.hasClaude ? "claude-opus-4-5" : "big-pickle")
const librarianModel = config.hasClaude && config.isMax20 ? "claude-sonnet-4-5" : "big-pickle"
const frontendModel = config.hasGemini ? "gemini-3-pro-high" : (config.hasClaude ? "claude-opus-4-5" : "big-pickle")
lines.push(` ${SYMBOLS.bullet} Sisyphus ${SYMBOLS.arrow} ${color.cyan(sisyphusModel)}`)
lines.push(` ${SYMBOLS.bullet} Oracle ${SYMBOLS.arrow} ${color.cyan(oracleModel)}`)
lines.push(` ${SYMBOLS.bullet} Librarian ${SYMBOLS.arrow} ${color.cyan(librarianModel)}`)
lines.push(` ${SYMBOLS.bullet} Frontend ${SYMBOLS.arrow} ${color.cyan(frontendModel)}`)
return lines.join("\n")
}
function printHeader(isUpdate: boolean): void {
const mode = isUpdate ? "Update" : "Install"
console.log()
console.log(color.bgMagenta(color.white(` oMoMoMoMo... ${mode} `)))
console.log()
}
function printStep(step: number, total: number, message: string): void {
const progress = color.dim(`[${step}/${total}]`)
console.log(`${progress} ${message}`)
}
function printSuccess(message: string): void {
console.log(`${SYMBOLS.check} ${message}`)
}
function printError(message: string): void {
console.log(`${SYMBOLS.cross} ${color.red(message)}`)
}
function printInfo(message: string): void {
console.log(`${SYMBOLS.info} ${message}`)
}
function printWarning(message: string): void {
console.log(`${SYMBOLS.warn} ${color.yellow(message)}`)
}
function printBox(content: string, title?: string): void {
const lines = content.split("\n")
const maxWidth = Math.max(...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, "").length), title?.length ?? 0) + 4
const border = color.dim("─".repeat(maxWidth))
console.log()
if (title) {
console.log(color.dim("┌─") + color.bold(` ${title} `) + color.dim("─".repeat(maxWidth - title.length - 4)) + color.dim("┐"))
} else {
console.log(color.dim("┌") + border + color.dim("┐"))
}
for (const line of lines) {
const stripped = line.replace(/\x1b\[[0-9;]*m/g, "")
const padding = maxWidth - stripped.length
console.log(color.dim("│") + ` ${line}${" ".repeat(padding - 1)}` + color.dim("│"))
}
console.log(color.dim("└") + border + color.dim("┘"))
console.log()
}
function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (args.claude === undefined) {
errors.push("--claude is required (values: no, yes, max20)")
} else if (!["no", "yes", "max20"].includes(args.claude)) {
errors.push(`Invalid --claude value: ${args.claude} (expected: no, yes, max20)`)
}
if (args.chatgpt === undefined) {
errors.push("--chatgpt is required (values: no, yes)")
} else if (!["no", "yes"].includes(args.chatgpt)) {
errors.push(`Invalid --chatgpt value: ${args.chatgpt} (expected: no, yes)`)
}
if (args.gemini === undefined) {
errors.push("--gemini is required (values: no, yes)")
} else if (!["no", "yes"].includes(args.gemini)) {
errors.push(`Invalid --gemini value: ${args.gemini} (expected: no, yes)`)
}
return { valid: errors.length === 0, errors }
}
function argsToConfig(args: InstallArgs): InstallConfig {
return {
hasClaude: args.claude !== "no",
isMax20: args.claude === "max20",
hasChatGPT: args.chatgpt === "yes",
hasGemini: args.gemini === "yes",
}
}
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; chatgpt: BooleanArg; gemini: BooleanArg } {
let claude: ClaudeSubscription = "no"
if (detected.hasClaude) {
claude = detected.isMax20 ? "max20" : "yes"
}
return {
claude,
chatgpt: detected.hasChatGPT ? "yes" : "no",
gemini: detected.hasGemini ? "yes" : "no",
}
}
async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | null> {
const initial = detectedToInitialValues(detected)
const claude = await p.select({
message: "Do you have a Claude Pro/Max subscription?",
options: [
{ value: "no" as const, label: "No", hint: "Will use opencode/big-pickle as fallback" },
{ value: "yes" as const, label: "Yes (standard)", hint: "Claude Opus 4.5 for orchestration" },
{ value: "max20" as const, label: "Yes (max20 mode)", hint: "Full power with Claude Sonnet 4.5 for Librarian" },
],
initialValue: initial.claude,
})
if (p.isCancel(claude)) {
p.cancel("Installation cancelled.")
return null
}
const chatgpt = await p.select({
message: "Do you have a ChatGPT Plus/Pro subscription?",
options: [
{ value: "no" as const, label: "No", hint: "Oracle will use fallback model" },
{ value: "yes" as const, label: "Yes", hint: "GPT-5.2 for debugging and architecture" },
],
initialValue: initial.chatgpt,
})
if (p.isCancel(chatgpt)) {
p.cancel("Installation cancelled.")
return null
}
const gemini = await p.select({
message: "Will you integrate Google Gemini?",
options: [
{ value: "no" as const, label: "No", hint: "Frontend/docs agents will use fallback" },
{ value: "yes" as const, label: "Yes", hint: "Beautiful UI generation with Gemini 3 Pro" },
],
initialValue: initial.gemini,
})
if (p.isCancel(gemini)) {
p.cancel("Installation cancelled.")
return null
}
return {
hasClaude: claude !== "no",
isMax20: claude === "max20",
hasChatGPT: chatgpt === "yes",
hasGemini: gemini === "yes",
}
}
async function runNonTuiInstall(args: InstallArgs): Promise<number> {
const validation = validateNonTuiArgs(args)
if (!validation.valid) {
printHeader(false)
printError("Validation failed:")
for (const err of validation.errors) {
console.log(` ${SYMBOLS.bullet} ${err}`)
}
console.log()
printInfo("Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --chatgpt=<no|yes> --gemini=<no|yes>")
console.log()
return 1
}
const detected = detectCurrentConfig()
const isUpdate = detected.isInstalled
printHeader(isUpdate)
const totalSteps = 6
let step = 1
printStep(step++, totalSteps, "Checking OpenCode installation...")
const installed = await isOpenCodeInstalled()
if (!installed) {
printError("OpenCode is not installed on this system.")
printInfo("Visit https://opencode.ai/docs for installation instructions")
return 1
}
const version = await getOpenCodeVersion()
printSuccess(`OpenCode ${version ?? ""} detected`)
if (isUpdate) {
const initial = detectedToInitialValues(detected)
printInfo(`Current config: Claude=${initial.claude}, ChatGPT=${initial.chatgpt}, Gemini=${initial.gemini}`)
}
const config = argsToConfig(args)
printStep(step++, totalSteps, "Adding oh-my-opencode plugin...")
const pluginResult = addPluginToOpenCodeConfig()
if (!pluginResult.success) {
printError(`Failed: ${pluginResult.error}`)
return 1
}
printSuccess(`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`)
if (config.hasGemini || config.hasChatGPT) {
printStep(step++, totalSteps, "Adding auth plugins...")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
printError(`Failed: ${authResult.error}`)
return 1
}
printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`)
printStep(step++, totalSteps, "Adding provider configurations...")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
printError(`Failed: ${providerResult.error}`)
return 1
}
printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`)
} else {
step += 2
}
if (config.hasChatGPT) {
printStep(step++, totalSteps, "Setting up ChatGPT hotfix...")
const hotfixResult = setupChatGPTHotfix()
if (!hotfixResult.success) {
printError(`Failed: ${hotfixResult.error}`)
return 1
}
printSuccess(`Hotfix configured ${SYMBOLS.arrow} ${color.dim(hotfixResult.configPath)}`)
printInfo("Installing dependencies with bun...")
const bunSuccess = await runBunInstall()
if (bunSuccess) {
printSuccess("Dependencies installed")
} else {
printWarning("bun install failed - run manually: cd ~/.config/opencode && bun i")
}
} else {
step++
}
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
printError(`Failed: ${omoResult.error}`)
return 1
}
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini) {
printWarning("No model providers configured. Using opencode/big-pickle as fallback.")
}
if ((config.hasClaude || config.hasChatGPT || config.hasGemini) && !args.skipAuth) {
console.log(color.bold("Next Steps - Authenticate your providers:"))
console.log()
if (config.hasClaude) {
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select Anthropic → Claude Pro/Max)")}`)
}
if (config.hasChatGPT) {
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select OpenAI → ChatGPT Plus/Pro)")}`)
}
if (config.hasGemini) {
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select Google → OAuth with Antigravity)")}`)
}
console.log()
}
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
console.log(` Run ${color.cyan("opencode")} to start!`)
console.log()
console.log(color.dim("oMoMoMoMo... Enjoy!"))
console.log()
return 0
}
export async function install(args: InstallArgs): Promise<number> {
if (!args.tui) {
return runNonTuiInstall(args)
}
const detected = detectCurrentConfig()
const isUpdate = detected.isInstalled
p.intro(color.bgMagenta(color.white(isUpdate ? " oMoMoMoMo... Update " : " oMoMoMoMo... ")))
if (isUpdate) {
const initial = detectedToInitialValues(detected)
p.log.info(`Existing configuration detected: Claude=${initial.claude}, ChatGPT=${initial.chatgpt}, Gemini=${initial.gemini}`)
}
const s = p.spinner()
s.start("Checking OpenCode installation")
const installed = await isOpenCodeInstalled()
if (!installed) {
s.stop("OpenCode is not installed")
p.log.error("OpenCode is not installed on this system.")
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
p.outro(color.red("Please install OpenCode first."))
return 1
}
const version = await getOpenCodeVersion()
s.stop(`OpenCode ${version ?? "installed"} ${color.green("✓")}`)
const config = await runTuiMode(detected)
if (!config) return 1
s.start("Adding oh-my-opencode to OpenCode config")
const pluginResult = addPluginToOpenCodeConfig()
if (!pluginResult.success) {
s.stop(`Failed to add plugin: ${pluginResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
s.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
if (config.hasGemini || config.hasChatGPT) {
s.start("Adding auth plugins (fetching latest versions)")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
s.stop(`Failed to add auth plugins: ${authResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
s.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`)
s.start("Adding provider configurations")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
s.stop(`Failed to add provider config: ${providerResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
s.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`)
}
if (config.hasChatGPT) {
s.start("Setting up ChatGPT hotfix")
const hotfixResult = setupChatGPTHotfix()
if (!hotfixResult.success) {
s.stop(`Failed to setup hotfix: ${hotfixResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
s.stop(`Hotfix configured in ${color.cyan(hotfixResult.configPath)}`)
s.start("Installing dependencies with bun")
const bunSuccess = await runBunInstall()
if (bunSuccess) {
s.stop("Dependencies installed")
} else {
s.stop(color.yellow("bun install failed - run manually: cd ~/.config/opencode && bun i"))
}
}
s.start("Writing oh-my-opencode configuration")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
s.stop(`Failed to write config: ${omoResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
s.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini) {
p.log.warn("No model providers configured. Using opencode/big-pickle as fallback.")
}
p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
if ((config.hasClaude || config.hasChatGPT || config.hasGemini) && !args.skipAuth) {
const steps: string[] = []
if (config.hasClaude) {
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select Anthropic → Claude Pro/Max)")}`)
}
if (config.hasChatGPT) {
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select OpenAI → ChatGPT Plus/Pro)")}`)
}
if (config.hasGemini) {
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select Google → OAuth with Antigravity)")}`)
}
p.note(steps.join("\n"), "Next Steps - Authenticate your providers")
}
p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!"))
p.log.message(`Run ${color.cyan("opencode")} to start!`)
p.outro(color.green("oMoMoMoMo... Enjoy!"))
return 0
}

31
src/cli/types.ts Normal file
View File

@@ -0,0 +1,31 @@
export type ClaudeSubscription = "no" | "yes" | "max20"
export type BooleanArg = "no" | "yes"
export interface InstallArgs {
tui: boolean
claude?: ClaudeSubscription
chatgpt?: BooleanArg
gemini?: BooleanArg
skipAuth?: boolean
}
export interface InstallConfig {
hasClaude: boolean
isMax20: boolean
hasChatGPT: boolean
hasGemini: boolean
}
export interface ConfigMergeResult {
success: boolean
configPath: string
error?: string
}
export interface DetectedConfig {
isInstalled: boolean
hasClaude: boolean
isMax20: boolean
hasChatGPT: boolean
hasGemini: boolean
}