Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5c7f430c2 | ||
|
|
b8e70f9529 | ||
|
|
5dbd5ac6b1 | ||
|
|
908521746f | ||
|
|
1e3cf4ea1b | ||
|
|
6c0b59dbd6 | ||
|
|
83c1b8d5a4 | ||
|
|
56deaa3a3e |
115
README.ja.md
115
README.ja.md
@@ -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" が含まれていること
|
||||
```
|
||||
|
||||
|
||||
115
README.ko.md
115
README.ko.md
@@ -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"가 있어야 함
|
||||
```
|
||||
|
||||
|
||||
133
README.md
133
README.md
@@ -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
|
||||
|
||||
115
README.zh-cn.md
115
README.zh-cn.md
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -1225,6 +1225,9 @@
|
||||
"type": "number",
|
||||
"minimum": 0.5,
|
||||
"maximum": 0.95
|
||||
},
|
||||
"truncate_all_tool_outputs": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
13
bun.lock
13
bun.lock
@@ -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=="],
|
||||
|
||||
10
package.json
10
package.json
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "2.4.6",
|
||||
"version": "2.5.0",
|
||||
"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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
484
src/cli/config-manager.ts
Normal file
484
src/cli/config-manager.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { homedir } from "node:os"
|
||||
import type { ConfigMergeResult, InstallConfig, DetectedConfig } 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
|
||||
}
|
||||
}
|
||||
|
||||
const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
api: "antigravity",
|
||||
models: {
|
||||
"gemini-3-pro-high": { name: "Gemini 3 Pro (High)", thinking: true, attachment: true },
|
||||
"gemini-3-pro-medium": { name: "Gemini 3 Pro (Medium)", thinking: true, attachment: true },
|
||||
"gemini-3-pro-low": { name: "Gemini 3 Pro (Low)", thinking: true, attachment: true },
|
||||
"gemini-3-flash": { name: "Gemini 3 Flash", attachment: true },
|
||||
"gemini-3-flash-lite": { name: "Gemini 3 Flash Lite", attachment: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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
54
src/cli/index.ts
Normal 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
456
src/cli/install.ts
Normal 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
31
src/cli/types.ts
Normal 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
|
||||
}
|
||||
@@ -184,7 +184,13 @@ export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig
|
||||
|
||||
const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {}
|
||||
|
||||
recordToolResult(input.sessionID, input.tool, cachedInput, (output.metadata as Record<string, unknown>) || {})
|
||||
// Use metadata if available and non-empty, otherwise wrap output.output in a structured object
|
||||
// This ensures plugin tools (call_omo_agent, background_task, task) that return strings
|
||||
// get their results properly recorded in transcripts instead of empty {}
|
||||
const metadata = output.metadata as Record<string, unknown> | undefined
|
||||
const hasMetadata = metadata && typeof metadata === "object" && Object.keys(metadata).length > 0
|
||||
const toolOutput = hasMetadata ? metadata : { output: output.output }
|
||||
recordToolResult(input.sessionID, input.tool, cachedInput, toolOutput)
|
||||
|
||||
if (!isHookDisabled(config, "PostToolUse")) {
|
||||
const postClient: PostToolUseClient = {
|
||||
|
||||
@@ -25,11 +25,6 @@ export function createKeywordDetectorHook() {
|
||||
const isFirstMessage = !sessionFirstMessageProcessed.has(input.sessionID)
|
||||
sessionFirstMessageProcessed.add(input.sessionID)
|
||||
|
||||
if (isFirstMessage) {
|
||||
log("Skipping keyword detection on first message for title generation", { sessionID: input.sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
const promptText = extractPromptText(output.parts)
|
||||
const messages = detectKeywords(promptText)
|
||||
|
||||
@@ -37,6 +32,19 @@ export function createKeywordDetectorHook() {
|
||||
return
|
||||
}
|
||||
|
||||
const context = messages.join("\n")
|
||||
|
||||
// First message: transform parts directly (for title generation compatibility)
|
||||
if (isFirstMessage) {
|
||||
log(`Keywords detected on first message, transforming parts directly`, { sessionID: input.sessionID, keywordCount: messages.length })
|
||||
const idx = output.parts.findIndex((p) => p.type === "text" && p.text)
|
||||
if (idx >= 0) {
|
||||
output.parts[idx].text = `${context}\n\n---\n\n${output.parts[idx].text ?? ""}`
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Subsequent messages: inject as separate message
|
||||
log(`Keywords detected: ${messages.length}`, { sessionID: input.sessionID })
|
||||
|
||||
const message = output.message as {
|
||||
@@ -46,7 +54,6 @@ export function createKeywordDetectorHook() {
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
|
||||
const context = messages.join("\n")
|
||||
log(`[keyword-detector] Injecting context for ${messages.length} keywords`, { sessionID: input.sessionID, contextLength: context.length })
|
||||
const success = injectHookMessage(input.sessionID, context, {
|
||||
agent: message.agent,
|
||||
|
||||
@@ -184,9 +184,15 @@ export function createPreemptiveCompactionHook(
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const messageDir = getMessageDir(sessionID)
|
||||
const storedMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
||||
|
||||
await ctx.client.session.promptAsync({
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text", text: "Continue" }] },
|
||||
body: {
|
||||
agent: storedMessage?.agent,
|
||||
parts: [{ type: "text", text: "Continue" }],
|
||||
},
|
||||
query: { directory: ctx.directory },
|
||||
})
|
||||
} catch {}
|
||||
|
||||
Reference in New Issue
Block a user