pleumrouter 0.5.34 → 0.5.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/bin/pleum.mjs +17 -4
- package/package.json +1 -1
- package/src/completion.mjs +1 -1
- package/src/launch.mjs +55 -25
- package/src/login.mjs +18 -1
- package/src/menu.mjs +30 -122
- package/src/recipes.mjs +57 -21
- package/src/settings.mjs +3 -15
- package/src/wire.mjs +38 -22
package/README.md
CHANGED
|
@@ -17,25 +17,28 @@ bun install -g pleumrouter # bun
|
|
|
17
17
|
## 빠른 시작
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
pleum # Claude Code · Codex · Antigravity · opencode 바로 선택
|
|
21
|
+
pleum claude # Claude Code 바로 실행
|
|
22
|
+
pleum codex # Codex 바로 실행
|
|
23
|
+
pleum agy # Antigravity CLI 바로 실행
|
|
24
|
+
pleum opencode # opencode 바로 실행
|
|
25
|
+
pleum run gpt-5.4-mini # 터미널 채팅
|
|
24
26
|
npx pleumrouter run gpt-5.4-mini "한 줄 요약: ..." # 1회 실행(스크립트용)
|
|
25
27
|
cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
로그인
|
|
30
|
+
최초 실행 때 브라우저 로그인을 안내하고, 코딩 에이전트용 `plm_` 키를 한 번 발급해 재사용합니다. 로그인 세션은 `~/.pleum/config.json`, 에이전트 키는 `~/.pleum/wire.json`에 저장됩니다. 두 파일은 사용자만 읽을 수 있습니다. 에이전트 실행은 만료되는 로그인 토큰을 직접 전달하지 않습니다.
|
|
29
31
|
SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
30
32
|
## 명령
|
|
31
33
|
|
|
32
34
|
| 명령 | 설명 |
|
|
33
35
|
|------|------|
|
|
34
|
-
| `pleum` | (인자 없음, TTY)
|
|
36
|
+
| `pleum` | (인자 없음, TTY) 첫 화면에서 Claude Code·Codex·Antigravity·opencode를 바로 실행. ↑/↓ 이동 · enter 실행 · → 모델 설정 · esc 종료. 나머지 에이전트는 "에이전트 실행" 메뉴. 파이프에선 도움말 출력 |
|
|
37
|
+
| `pleum claude\|codex\|agy\|opencode [--model id]` | 네 우선 지원 도구를 한 명령으로 실행. 최초 실행은 브라우저 로그인 후 에이전트 키 발급 |
|
|
35
38
|
| `pleum login [--device]` | 브라우저 OAuth 로그인 → access/refresh 토큰을 `~/.pleum/config.json`에 저장(API 키 미발급). `--device`는 SSH·CI |
|
|
36
39
|
| `pleum logout` | 서버 세션 revoke + 로컬 토큰 삭제(언어·기본 모델 등 나머지 설정은 유지) |
|
|
37
40
|
| `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
|
|
38
|
-
| `pleum wire [claude\|codex]` | Desktop용으로 `~/.claude/settings.json`·`~/.codex/config.toml`에
|
|
41
|
+
| `pleum wire [claude\|codex]` | Desktop용으로 `~/.claude/settings.json`·`~/.codex/config.toml`에 같은 `plm_` 키를 병합. 생략 시 둘 다 |
|
|
39
42
|
| `pleum unwire [claude\|codex] [--revoke-key]` | wire가 넣은 설정만 제거. `--revoke-key`는 서버 키도 폐기 |
|
|
40
43
|
| `pleum run [model] ["프롬프트"] [--attach image]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. 이미지 업로드는 명시적으로 `--attach ./shot.png`(1회 실행) 또는 REPL의 `@./shot.png`를 사용한다. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
|
|
41
44
|
| `pleum models [query] [--json]` | 모델 목록(공개) — provider·컨텍스트·가격(₩/1M) 컬럼, `--json`은 스크립트용 |
|
|
@@ -75,7 +78,7 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
|
75
78
|
## 자동 실행 지원 도구
|
|
76
79
|
|
|
77
80
|
`claude` · `codex` · `aider` · `goose` · `openhands` · `opencode` · `omp` · `hermes` · `qwen` · `copilot` · `gemini` · `agy` · `pi` · `crush` · `minimax` · `mimo` · `openclaw` · `plandex` · `interpreter` —
|
|
78
|
-
env 또는 격리 홈(`
|
|
81
|
+
env·실행 인자 또는 일부 도구의 격리 홈(`PI_CODING_AGENT_DIR` / `CRUSH_GLOBAL_CONFIG`)으로 연결한다. Codex는 `-c` 임시 설정을 써서 기존 `~/.codex`의 세션과 설정을 유지한다.
|
|
79
82
|
별칭: `antigravity`→`agy`, `picode`→`pi`, `ohmypi`/`oh-my-pi`→`omp`, `qwencode`/`qwen-code`→`qwen`, `minimax-code`/`mcode`→`minimax`, `mimocode`/`mimo-code`→`mimo`, `open-interpreter`→`interpreter`.
|
|
80
83
|
|
|
81
84
|
`agy`만 사용자 `~/.gemini/antigravity-cli/settings.json`에 `modelProvider=gemini`를 병합한다(제품이 이 파일 없이 키를 무시함). Kiro는 공식 BYOK가 없어 지원하지 않는다.
|
package/bin/pleum.mjs
CHANGED
|
@@ -30,6 +30,7 @@ function buildHelp() {
|
|
|
30
30
|
["", t("--device: browser-less environments (SSH/CI)")],
|
|
31
31
|
["pleum logout", t("Revoke session and clear local tokens")],
|
|
32
32
|
["pleum launch [tool] [--model id]", t("Run a coding agent against PleumRouter")],
|
|
33
|
+
["pleum claude|codex|agy|opencode", t("Run a coding agent against PleumRouter")],
|
|
33
34
|
["", `(${Object.keys(RECIPES).join(" · ")})`],
|
|
34
35
|
["", t("omit the tool to see the supported list and install status")],
|
|
35
36
|
["pleum wire [claude|codex]", t("persist config in ~/.claude or ~/.codex for Desktop apps")],
|
|
@@ -55,6 +56,10 @@ function buildHelp() {
|
|
|
55
56
|
["pleum wire", null],
|
|
56
57
|
["pleum launch claude --model claude-sonnet-5", null],
|
|
57
58
|
["pleum launch codex --model gpt-5.3-codex -- --full-auto", null],
|
|
59
|
+
["pleum claude", null],
|
|
60
|
+
["pleum codex", null],
|
|
61
|
+
["pleum agy", null],
|
|
62
|
+
["pleum opencode", null],
|
|
58
63
|
["pleum run gpt-5.4-mini", t("interactive")],
|
|
59
64
|
[`pleum run gpt-5.4-mini "${t("Summarize in one line: ...")}"`, t("one-shot")],
|
|
60
65
|
[`cat error.log | pleum run gpt-5.4-mini "${t("what caused this?")}"`, t("pipe")],
|
|
@@ -72,7 +77,10 @@ function buildHelp() {
|
|
|
72
77
|
function parseLaunchArgs(args) {
|
|
73
78
|
const out = { model: undefined, passthrough: [] };
|
|
74
79
|
for (let i = 0; i < args.length; i++) {
|
|
75
|
-
if (args[i] === "--model" || args[i] === "-m")
|
|
80
|
+
if (args[i] === "--model" || args[i] === "-m") {
|
|
81
|
+
if (!args[i + 1] || args[i + 1] === "--") throw new Error("--model requires a model ID");
|
|
82
|
+
out.model = args[++i];
|
|
83
|
+
}
|
|
76
84
|
else if (args[i] === "--") {
|
|
77
85
|
out.passthrough.push(...args.slice(i + 1));
|
|
78
86
|
break;
|
|
@@ -101,8 +109,13 @@ try {
|
|
|
101
109
|
case "lang":
|
|
102
110
|
await lang(rest);
|
|
103
111
|
break;
|
|
104
|
-
case "launch":
|
|
105
|
-
|
|
112
|
+
case "launch":
|
|
113
|
+
case "claude":
|
|
114
|
+
case "codex":
|
|
115
|
+
case "agy":
|
|
116
|
+
case "antigravity":
|
|
117
|
+
case "opencode": {
|
|
118
|
+
const tool = cmd === "launch" ? rest[0] : cmd;
|
|
106
119
|
if (!tool) {
|
|
107
120
|
const io = process.stdin;
|
|
108
121
|
if (io.isTTY && process.stdout.isTTY && typeof io.setRawMode === "function") {
|
|
@@ -112,7 +125,7 @@ try {
|
|
|
112
125
|
launchHelp();
|
|
113
126
|
break;
|
|
114
127
|
}
|
|
115
|
-
const { model, passthrough } = parseLaunchArgs(rest.slice(1));
|
|
128
|
+
const { model, passthrough } = parseLaunchArgs(cmd === "launch" ? rest.slice(1) : rest);
|
|
116
129
|
await launch(tool, model, passthrough);
|
|
117
130
|
break;
|
|
118
131
|
}
|
package/package.json
CHANGED
package/src/completion.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { t } from "./i18n.mjs";
|
|
4
4
|
import { launchToolNames } from "./recipes.mjs";
|
|
5
5
|
|
|
6
|
-
const COMMANDS = "login logout lang launch wire unwire run models top balance usage health version update upgrade completion help";
|
|
6
|
+
const COMMANDS = "claude codex agy antigravity opencode login logout lang launch wire unwire run models top balance usage health version update upgrade completion help";
|
|
7
7
|
const TOOLS = launchToolNames().join(" ");
|
|
8
8
|
|
|
9
9
|
function bash() {
|
package/src/launch.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, delimiter, dirname, resolve } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { readConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
6
|
+
import { login } from "./login.mjs";
|
|
7
|
+
import { ensureWireKey } from "./wire.mjs";
|
|
6
8
|
import {
|
|
7
9
|
RECIPES,
|
|
8
10
|
MANUAL,
|
|
@@ -93,17 +95,49 @@ export function launchHelp() {
|
|
|
93
95
|
console.log(`\n${t("Example: pleum launch claude --model claude-sonnet-5")}`);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// 메뉴·단축 명령·`launch`가 모두 같은 저장 모델을 사용한다. 명시적 --model이 우선이다.
|
|
99
|
+
export function resolveLaunchModel(tool, explicit, cfg = readConfig()) {
|
|
100
|
+
const recipe = RECIPES[tool];
|
|
101
|
+
if (recipe?.resolveModel) return recipe.resolveModel(explicit, cfg);
|
|
102
|
+
return explicit || cfg?.launch_models?.[tool] || recipe?.defaultModel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 공통 실행 파이프라인의 설정 어댑터. 신규 에이전트는 env/args만 쓰거나
|
|
106
|
+
// 별도 홈이 필요할 때 타입 핸들러 하나를 등록하면 된다.
|
|
107
|
+
const SETUP = {
|
|
108
|
+
env: () => null,
|
|
109
|
+
"codex-home": (env, model) => { env.CODEX_HOME = writeIsolatedHome("codex-home", "config.toml", codexToml(model)); return null; },
|
|
110
|
+
"pi-home": (env, model) => { env.PI_CODING_AGENT_DIR = writeIsolatedHome("pi-home", "models.json", piModelsJson(model)); return null; },
|
|
111
|
+
"crush-home": (env, model) => { env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model)); return null; },
|
|
112
|
+
"agy-settings": (_env, model) => setupAgySettings(model),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
async function agentKey() {
|
|
116
|
+
if (!isLoggedIn()) {
|
|
117
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
118
|
+
throw new Error(t("Login required. Run this first:\n pleum login"));
|
|
119
|
+
}
|
|
120
|
+
await login();
|
|
121
|
+
}
|
|
122
|
+
// 로그인 JWT는 에이전트가 살아 있는 동안 갱신할 수 없다. 한 번 발급한 plm_ 키를
|
|
123
|
+
// launch와 wire가 공유하면 장시간 실행도 유지되고 Go 데이터플레인을 직접 사용한다.
|
|
124
|
+
return (await ensureWireKey()).key;
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
// exit:false면 process.exit 대신 자식 종료 코드를 반환 — 런처 메뉴의 실행 후 복귀용.
|
|
97
|
-
export async function launch(tool, model, passthrough, {
|
|
128
|
+
export async function launch(tool, model, passthrough, {
|
|
129
|
+
exit = true,
|
|
130
|
+
getKey = agentKey,
|
|
131
|
+
pathCheck = onPath,
|
|
132
|
+
spawnChild = spawn,
|
|
133
|
+
} = {}) {
|
|
98
134
|
recoverAgySettings();
|
|
99
|
-
|
|
100
|
-
const key = await requireAuth();
|
|
101
135
|
const resolved = resolveLaunchTool(tool);
|
|
102
136
|
const r = RECIPES[resolved];
|
|
103
137
|
|
|
104
138
|
if (!r) {
|
|
105
139
|
if (MANUAL[resolved]) {
|
|
106
|
-
printManual(resolved,
|
|
140
|
+
printManual(resolved, await getKey());
|
|
107
141
|
return;
|
|
108
142
|
}
|
|
109
143
|
const aliases = Object.entries(LAUNCH_ALIASES).map(([from, to]) => `${from} → ${to}`).join(", ");
|
|
@@ -116,28 +150,25 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
116
150
|
process.exit(1);
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
env.CODEX_HOME = writeIsolatedHome("codex-home", "config.toml", codexToml(model));
|
|
124
|
-
}
|
|
125
|
-
if (r.type === "pi-home") {
|
|
126
|
-
env.PI_CODING_AGENT_DIR = writeIsolatedHome("pi-home", "models.json", piModelsJson(model));
|
|
127
|
-
}
|
|
128
|
-
if (r.type === "crush-home") {
|
|
129
|
-
env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model));
|
|
153
|
+
if (!pathCheck(r.bin)) {
|
|
154
|
+
console.error(`✗ ${t("'{bin}' not found. Install:\n {install}", { bin: r.bin, install: r.install })}`);
|
|
155
|
+
if (exit) process.exit(127);
|
|
156
|
+
return 127;
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
159
|
+
const key = await getKey();
|
|
160
|
+
model = resolveLaunchModel(resolved, model);
|
|
161
|
+
|
|
162
|
+
const env = { ...process.env, ...r.env(key, model) };
|
|
163
|
+
for (const name of r.unsetEnv || []) delete env[name];
|
|
164
|
+
const setup = SETUP[r.type || "env"];
|
|
165
|
+
if (!setup) throw new Error(`Unknown agent setup type: ${r.type}`);
|
|
166
|
+
let restore = setup(env, model);
|
|
136
167
|
|
|
137
168
|
const cleanup = () => {
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
if (restore) {
|
|
170
|
+
restore();
|
|
171
|
+
restore = null;
|
|
141
172
|
}
|
|
142
173
|
};
|
|
143
174
|
|
|
@@ -192,7 +223,7 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
return await new Promise((resolve) => {
|
|
195
|
-
const child =
|
|
226
|
+
const child = spawnChild(command.command, command.args, {
|
|
196
227
|
env,
|
|
197
228
|
stdio: "inherit",
|
|
198
229
|
shell: false,
|
|
@@ -377,4 +408,3 @@ function printManual(tool, key) {
|
|
|
377
408
|
console.log(m.snippet());
|
|
378
409
|
console.log(`\n${t("2) Export the key and run:")}\n export PLEUM_API_KEY=${key}\n`);
|
|
379
410
|
}
|
|
380
|
-
|
package/src/login.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials } from "./config.mjs";
|
|
4
|
+
import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext } from "./config.mjs";
|
|
5
5
|
import { t } from "./i18n.mjs";
|
|
6
6
|
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
7
7
|
|
|
@@ -47,6 +47,21 @@ function saveTokens({ access_token, refresh_token, expires_in }) {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export async function syncCliContext(accessToken) {
|
|
51
|
+
if (!accessToken) return;
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`${V1}/auth/cli-context`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
55
|
+
});
|
|
56
|
+
if (res.ok) {
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
if (data?.active_context) {
|
|
59
|
+
setActiveContext(data.active_context);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
// 브라우저 핸드오프: 로컬 콜백 서버를 열고 대시보드 /cli-auth로 보낸다.
|
|
51
66
|
// 프론트가 로그인 확인 후 JWT를 발급해 127.0.0.1:PORT/cb 로 POST 한다.
|
|
52
67
|
export async function login() {
|
|
@@ -110,6 +125,7 @@ export async function login() {
|
|
|
110
125
|
});
|
|
111
126
|
|
|
112
127
|
saveTokens(tokens);
|
|
128
|
+
await syncCliContext(tokens.access_token);
|
|
113
129
|
console.log(`✓ ${t("Logged in. Saved the session to ~/.pleum/config.json.")}`);
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -157,6 +173,7 @@ export async function loginDevice() {
|
|
|
157
173
|
process.exit(1);
|
|
158
174
|
}
|
|
159
175
|
saveTokens(data);
|
|
176
|
+
await syncCliContext(data.access_token);
|
|
160
177
|
console.log(`✓ ${t("Logged in. Saved the session to ~/.pleum/config.json.")}`);
|
|
161
178
|
return;
|
|
162
179
|
}
|
package/src/menu.mjs
CHANGED
|
@@ -5,26 +5,21 @@
|
|
|
5
5
|
// · Enter = 저장된 모델(launch_models)로 바로 실행, 미설치면 Y/n 확인 후 자동 설치
|
|
6
6
|
// · 실행 종료 후 메뉴로 복귀(ollama runInteractiveTUI 루프), last_selection 커서 복원
|
|
7
7
|
// · 키보드 전용: ↑/k ↓/j 이동 · enter/space 실행 · →/l 모델 설정 · esc/q 뒤로(최상위에선 종료) · ctrl+c 즉시 종료
|
|
8
|
-
// 에이전트는
|
|
9
|
-
// 지원 에이전트가 많아질수록 최상위 목록이 늘어지는 것을 막기 위함(runList는 두 메뉴가 공유하는 렌더 루프).
|
|
8
|
+
// 4대 에이전트는 최상위에서 바로 실행하고, 나머지는 "에이전트 실행" 서브메뉴에서 고른다.
|
|
10
9
|
|
|
11
10
|
import { readFileSync } from "node:fs";
|
|
12
11
|
import { spawn } from "node:child_process";
|
|
13
12
|
import { createInterface } from "node:readline";
|
|
14
13
|
import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
14
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig, isLoggedIn,
|
|
15
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
17
16
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
17
|
import { onPath, launch } from "./launch.mjs";
|
|
19
18
|
import { fetchModels, matchModels, listModels, formatModelLabel, fetchCoreCodingModels, CORE_MODEL_PROVIDERS } from "./models.mjs";
|
|
20
19
|
import { run } from "./run.mjs";
|
|
21
20
|
import { login } from "./login.mjs";
|
|
22
|
-
import { top } from "./top.mjs";
|
|
23
|
-
import { usage } from "./usage.mjs";
|
|
24
21
|
import { balance } from "./balance.mjs";
|
|
25
|
-
import { health } from "./health.mjs";
|
|
26
22
|
import { runSettings } from "./settings.mjs";
|
|
27
|
-
import { checkLatestVersion, performUpdate } from "./update.mjs";
|
|
28
23
|
import { t } from "./i18n.mjs";
|
|
29
24
|
|
|
30
25
|
// 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
|
|
@@ -54,11 +49,18 @@ const AGENT_DESC = {
|
|
|
54
49
|
|
|
55
50
|
const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
|
|
56
51
|
|
|
57
|
-
// 최상위 메뉴 항목(순수 함수 — 테스트 대상).
|
|
58
|
-
|
|
59
|
-
// 로그인/로그아웃 전환은 설정>계정이 소유한다.
|
|
60
|
-
export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
|
|
52
|
+
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
|
|
53
|
+
export function buildItems({ savedModels = {}, activeContext = { type: "personal" }, installed = {} }) {
|
|
61
54
|
const items = [];
|
|
55
|
+
for (const tool of ["claude", "codex", "agy", "opencode"]) {
|
|
56
|
+
const r = RECIPES[tool];
|
|
57
|
+
items.push({
|
|
58
|
+
id: `launch:${tool}`,
|
|
59
|
+
tool,
|
|
60
|
+
title: `${r.label}${installed[tool] === false ? ` (${t("install")})` : ""}`,
|
|
61
|
+
desc: `${t(AGENT_DESC[tool])}${savedModels[tool] ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
62
64
|
items.push({
|
|
63
65
|
id: "chat",
|
|
64
66
|
tool: "chat",
|
|
@@ -71,10 +73,7 @@ export function buildItems({ savedModels = {}, activeContext = { type: "personal
|
|
|
71
73
|
: t("Personal");
|
|
72
74
|
items.push(
|
|
73
75
|
{ id: "models", title: t("Model list"), desc: t("All models — price and context included") },
|
|
74
|
-
{ id: "top", title: t("Leaderboard"), desc: t("Model rankings · provider share") },
|
|
75
|
-
{ id: "usage", title: t("Usage report"), desc: t("Spend over the last 30 days") },
|
|
76
76
|
{ id: "balance", title: t("Balance"), desc: `${t("Check your credit balance")} · ${contextDesc}` },
|
|
77
|
-
{ id: "health", title: t("Health check"), desc: t("Gateway connectivity · login status") },
|
|
78
77
|
{ id: "settings", title: t("Settings"), desc: t("Language, default models, account, updates") },
|
|
79
78
|
);
|
|
80
79
|
return items;
|
|
@@ -207,7 +206,7 @@ export async function pickClaudeCodeTiers(agentItem, { input = process.stdin, ou
|
|
|
207
206
|
const defaultTiers = {
|
|
208
207
|
sonnet: "claude-sonnet-5",
|
|
209
208
|
opus: "claude-opus-5-5",
|
|
210
|
-
haiku:
|
|
209
|
+
haiku: "claude-haiku-4-5",
|
|
211
210
|
};
|
|
212
211
|
const baseTiers = {
|
|
213
212
|
sonnet: savedTiers?.sonnet || defaultTiers.sonnet,
|
|
@@ -483,26 +482,17 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
483
482
|
});
|
|
484
483
|
}
|
|
485
484
|
|
|
486
|
-
async function menuSelect(
|
|
485
|
+
async function menuSelect() {
|
|
487
486
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
488
487
|
const cfg = readConfig();
|
|
489
488
|
const items = buildItems({
|
|
490
489
|
savedModels: cfg?.launch_models ?? {},
|
|
491
490
|
activeContext: getActiveContext(cfg),
|
|
491
|
+
installed: Object.fromEntries(["claude", "codex", "agy", "opencode"].map((tool) => [tool, onPath(RECIPES[tool].bin)])),
|
|
492
492
|
});
|
|
493
493
|
const last = cfg?.last_selection;
|
|
494
494
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
495
495
|
const headerLines = [`${style.bold(style.plum("PleumRouter"))} ${style.dim(`pleum ${version}`)}`];
|
|
496
|
-
if (updateInfo?.isOutdated) {
|
|
497
|
-
headerLines.push(
|
|
498
|
-
style.yellow(
|
|
499
|
-
`💡 ${t("Update available: v{current} → v{latest}. Run 'pleum update' to update.", {
|
|
500
|
-
current: updateInfo.currentVersion,
|
|
501
|
-
latest: updateInfo.latestVersion,
|
|
502
|
-
})}`,
|
|
503
|
-
),
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
496
|
return runList(items, {
|
|
507
497
|
header: headerLines.join("\n"),
|
|
508
498
|
footer: t("↑/↓ move • enter run • → set model • esc quit"),
|
|
@@ -522,13 +512,10 @@ async function agentSelect() {
|
|
|
522
512
|
}
|
|
523
513
|
|
|
524
514
|
// 에이전트 실행 메뉴 — bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유.
|
|
525
|
-
//
|
|
526
|
-
// 선택한 우리 모델로 코딩 에이전트를 실행한다.
|
|
515
|
+
// Enter는 저장 모델로 즉시 실행하고, →는 모델 선택 후 실행한다.
|
|
527
516
|
export async function runAgentMenu({ loop = false } = {}) {
|
|
528
517
|
const io = process.stdin;
|
|
529
518
|
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
530
|
-
if (!isLoggedIn()) await login();
|
|
531
|
-
else await touchSession().catch(() => {});
|
|
532
519
|
|
|
533
520
|
for (;;) {
|
|
534
521
|
const agentPicked = await agentSelect();
|
|
@@ -548,54 +535,8 @@ export async function runAgentMenu({ loop = false } = {}) {
|
|
|
548
535
|
continue;
|
|
549
536
|
}
|
|
550
537
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (agentItem.tool === "claude") {
|
|
555
|
-
const tiers = await pickClaudeCodeTiers(agentItem);
|
|
556
|
-
if (!tiers) continue;
|
|
557
|
-
launchModelPayload = tiers;
|
|
558
|
-
saveModel("claude", tiers.sonnet);
|
|
559
|
-
} else if (agentItem.tool === "codex") {
|
|
560
|
-
let chosenModel = agentPicked.model;
|
|
561
|
-
if (!chosenModel) {
|
|
562
|
-
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
563
|
-
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
564
|
-
if (!chosenModel) continue;
|
|
565
|
-
}
|
|
566
|
-
saveModel("codex", chosenModel);
|
|
567
|
-
const cfg = readConfig() ?? {};
|
|
568
|
-
const effort = await pickCodexEffort(cfg.codex_effort || "medium");
|
|
569
|
-
if (!effort) continue;
|
|
570
|
-
cfg.codex_effort = effort;
|
|
571
|
-
writeConfig(cfg);
|
|
572
|
-
launchModelPayload = { model: chosenModel, effort };
|
|
573
|
-
} else if (agentItem.tool === "agy") {
|
|
574
|
-
let chosenModel = agentPicked.model;
|
|
575
|
-
if (!chosenModel) {
|
|
576
|
-
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
577
|
-
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
578
|
-
if (!chosenModel) continue;
|
|
579
|
-
}
|
|
580
|
-
saveModel("agy", chosenModel);
|
|
581
|
-
const cfg = readConfig() ?? {};
|
|
582
|
-
const effort = await pickAgyEffort(cfg.agy_effort || "high");
|
|
583
|
-
if (!effort) continue;
|
|
584
|
-
cfg.agy_effort = effort;
|
|
585
|
-
writeConfig(cfg);
|
|
586
|
-
launchModelPayload = { model: chosenModel, effort };
|
|
587
|
-
} else {
|
|
588
|
-
let chosenModel = agentPicked.model;
|
|
589
|
-
if (!chosenModel) {
|
|
590
|
-
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
591
|
-
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
592
|
-
if (!chosenModel) continue;
|
|
593
|
-
}
|
|
594
|
-
saveModel(agentItem.tool, chosenModel);
|
|
595
|
-
launchModelPayload = chosenModel;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
await launch(agentItem.tool, launchModelPayload, [], { exit: false });
|
|
538
|
+
if (agentPicked.model) saveModel(agentItem.tool, agentPicked.model);
|
|
539
|
+
await launch(agentItem.tool, agentPicked.model, [], { exit: false });
|
|
599
540
|
console.log();
|
|
600
541
|
if (!loop) return true;
|
|
601
542
|
}
|
|
@@ -608,30 +549,8 @@ export async function mainMenu() {
|
|
|
608
549
|
// runList()와 같은 3조건을 쓴다. 이 가드가 없으면 `pleum | cat`이 브라우저를 열고 2분 매달린다.
|
|
609
550
|
const io = process.stdin;
|
|
610
551
|
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
611
|
-
// CLI는 로그인 사용자용이다 — 미로그인 진입이면 여기서 auth 로그인을 태운다.
|
|
612
|
-
// login()의 실패(취소·2분 타임아웃·state 불일치)는 그대로 던져 bin/pleum.mjs의
|
|
613
|
-
// 최상위 catch가 `✗ <메시지>` + exit 1로 처리하게 둔다(메뉴를 띄우지 않는다).
|
|
614
|
-
if (!isLoggedIn()) await login();
|
|
615
|
-
else await touchSession().catch(() => {});
|
|
616
|
-
|
|
617
|
-
let updateInfo = null;
|
|
618
|
-
try {
|
|
619
|
-
updateInfo = await checkLatestVersion();
|
|
620
|
-
if (updateInfo?.isOutdated) {
|
|
621
|
-
if (isAutoUpdateEnabled()) {
|
|
622
|
-
console.log(`💡 ${t("Update available: v{current} → v{latest}", { current: updateInfo.currentVersion, latest: updateInfo.latestVersion })}`);
|
|
623
|
-
console.log(`▶ ${t("Auto-updating PleumRouter CLI...")}\n`);
|
|
624
|
-
const code = await performUpdate();
|
|
625
|
-
if (code === 0) {
|
|
626
|
-
console.log(`\n✓ ${t("Update completed! Please run `pleum` again.")}`);
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
} catch {}
|
|
632
|
-
|
|
633
552
|
for (;;) {
|
|
634
|
-
const picked = await menuSelect(
|
|
553
|
+
const picked = await menuSelect();
|
|
635
554
|
if (picked === undefined) return false; // 비TTY
|
|
636
555
|
if (picked === null || picked === "quit") return true; // esc/q/ctrl+c — ollama처럼 조용히 종료
|
|
637
556
|
|
|
@@ -642,6 +561,7 @@ export async function mainMenu() {
|
|
|
642
561
|
|
|
643
562
|
switch (item.id) {
|
|
644
563
|
case "chat": {
|
|
564
|
+
if (!isLoggedIn()) await login();
|
|
645
565
|
const m = model ?? saved.chat;
|
|
646
566
|
await run(m ? [m] : []); // REPL 종료는 run 내부 process.exit — 메뉴 복귀 없음
|
|
647
567
|
return true;
|
|
@@ -653,20 +573,6 @@ export async function mainMenu() {
|
|
|
653
573
|
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
654
574
|
}
|
|
655
575
|
continue;
|
|
656
|
-
case "top":
|
|
657
|
-
try {
|
|
658
|
-
await top([]);
|
|
659
|
-
} catch (e) {
|
|
660
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
661
|
-
}
|
|
662
|
-
continue;
|
|
663
|
-
case "usage":
|
|
664
|
-
try {
|
|
665
|
-
await usage([]);
|
|
666
|
-
} catch (e) {
|
|
667
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
668
|
-
}
|
|
669
|
-
continue;
|
|
670
576
|
case "balance":
|
|
671
577
|
try {
|
|
672
578
|
await balance({ interactive: true, exitOnError: false });
|
|
@@ -674,13 +580,6 @@ export async function mainMenu() {
|
|
|
674
580
|
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
675
581
|
}
|
|
676
582
|
continue;
|
|
677
|
-
case "health":
|
|
678
|
-
try {
|
|
679
|
-
await health({ exitOnError: false });
|
|
680
|
-
} catch (e) {
|
|
681
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
682
|
-
}
|
|
683
|
-
continue;
|
|
684
583
|
case "settings": {
|
|
685
584
|
const r = await runSettings();
|
|
686
585
|
if (r === "quit") return true; // ctrl+c 또는 재설치(upgrade) 시작 — 전체 종료
|
|
@@ -691,6 +590,15 @@ export async function mainMenu() {
|
|
|
691
590
|
if (r === "quit") return true;
|
|
692
591
|
continue;
|
|
693
592
|
}
|
|
593
|
+
case "launch:claude":
|
|
594
|
+
case "launch:codex":
|
|
595
|
+
case "launch:agy":
|
|
596
|
+
case "launch:opencode": {
|
|
597
|
+
const tool = item.tool;
|
|
598
|
+
if (!onPath(RECIPES[tool].bin) && !(await ensureInstalled(RECIPES[tool]))) continue;
|
|
599
|
+
await launch(tool, model, [], { exit: false });
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
694
602
|
}
|
|
695
603
|
}
|
|
696
604
|
}
|
package/src/recipes.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const CORE_MODELS = [
|
|
|
17
17
|
|
|
18
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
19
19
|
// type "env" → 환경변수만 세팅하고 바로 실행.
|
|
20
|
-
//
|
|
20
|
+
// Codex는 -c config override로 사용자 CODEX_HOME을 유지하면서 provider만 이번 실행에 덮어쓴다.
|
|
21
21
|
// type "pi-home" → 격리 PI_CODING_AGENT_DIR에 models.json (사용자 ~/.pi 보존).
|
|
22
22
|
// type "crush-home" → 격리 CRUSH_GLOBAL_CONFIG 디렉터리에 crush.json (사용자 crush.json 보존).
|
|
23
23
|
// type "agy-settings" → ~/.gemini/antigravity-cli/settings.json 에 modelProvider=gemini 병합.
|
|
@@ -34,6 +34,15 @@ export const RECIPES = {
|
|
|
34
34
|
label: "Claude Code",
|
|
35
35
|
bin: "claude",
|
|
36
36
|
type: "env",
|
|
37
|
+
unsetEnv: ["ANTHROPIC_API_KEY"],
|
|
38
|
+
resolveModel: (explicit, cfg) => {
|
|
39
|
+
if (explicit && typeof explicit === "object") return explicit;
|
|
40
|
+
return {
|
|
41
|
+
sonnet: explicit || cfg?.launch_models?.claude || cfg?.claude_tiers?.sonnet || "claude-sonnet-5",
|
|
42
|
+
opus: cfg?.claude_tiers?.opus || "claude-opus-5-5",
|
|
43
|
+
haiku: cfg?.claude_tiers?.haiku || "claude-haiku-4-5",
|
|
44
|
+
};
|
|
45
|
+
},
|
|
37
46
|
install: "npm i -g @anthropic-ai/claude-code",
|
|
38
47
|
// 앱 랭킹 귀속 메타 — 백엔드 featured_agents / UA fallback과 동기화.
|
|
39
48
|
attribution: {
|
|
@@ -46,11 +55,12 @@ export const RECIPES = {
|
|
|
46
55
|
const isObj = m && typeof m === "object";
|
|
47
56
|
const sonnet = (isObj ? m.sonnet : m) || "claude-sonnet-5";
|
|
48
57
|
const opus = (isObj ? m.opus : null) || "claude-opus-5-5";
|
|
49
|
-
const haiku = (isObj ? m.haiku : null) ||
|
|
50
|
-
|
|
58
|
+
const haiku = (isObj ? m.haiku : null) || "claude-haiku-4-5";
|
|
59
|
+
// Claude 모델의 provider-qualified ID만 보정한다. 다른 회사 모델에
|
|
60
|
+
// anthropic/을 붙이면 카탈로그에 없는 ID가 되어 요청이 실패한다.
|
|
61
|
+
const toAnthropic = (x) => /^claude-/.test(x) ? `anthropic/${x}` : x;
|
|
51
62
|
return {
|
|
52
63
|
ANTHROPIC_BASE_URL: ROOT,
|
|
53
|
-
ANTHROPIC_API_KEY: key,
|
|
54
64
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
55
65
|
// CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY는 의도적으로 설정하지 않는다:
|
|
56
66
|
// 활성화 시 Claude Code가 GET /v1/models를 호출해 카탈로그 200+개 모델을
|
|
@@ -74,23 +84,44 @@ export const RECIPES = {
|
|
|
74
84
|
codex: {
|
|
75
85
|
label: "Codex CLI",
|
|
76
86
|
bin: "codex",
|
|
77
|
-
type: "
|
|
87
|
+
type: "env",
|
|
88
|
+
defaultModel: "gpt-6-sol",
|
|
89
|
+
resolveModel: (explicit, cfg) => explicit && typeof explicit === "object"
|
|
90
|
+
? explicit
|
|
91
|
+
: { model: explicit || cfg?.launch_models?.codex || "gpt-6-sol", effort: cfg?.codex_effort || "medium" },
|
|
78
92
|
install: "npm i -g @openai/codex",
|
|
79
93
|
attribution: {
|
|
80
94
|
title: "Codex",
|
|
81
95
|
referer: "https://github.com/openai/codex",
|
|
82
96
|
},
|
|
83
97
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
84
|
-
args: (
|
|
85
|
-
const
|
|
86
|
-
const effort =
|
|
87
|
-
|
|
98
|
+
args: (model) => {
|
|
99
|
+
const m = model && typeof model === "object" ? model.model : model;
|
|
100
|
+
const effort = model && typeof model === "object" ? model.effort : null;
|
|
101
|
+
const provider = {
|
|
102
|
+
name: "PleumRouter",
|
|
103
|
+
base_url: V1,
|
|
104
|
+
env_key: "PLEUM_API_KEY",
|
|
105
|
+
wire_api: "responses",
|
|
106
|
+
supports_websockets: false,
|
|
107
|
+
};
|
|
108
|
+
const providerToml = `{ ${Object.entries(provider).map(([k, v]) => `${k} = ${typeof v === "boolean" ? v : tomlBasicString(v)}`).join(", ")} }`;
|
|
109
|
+
return [
|
|
110
|
+
"-c", 'model_provider="pleum"',
|
|
111
|
+
"-c", `model_providers.pleum=${providerToml}`,
|
|
112
|
+
"--model", m || "gpt-6-sol",
|
|
113
|
+
...(effort ? ["-c", `model_reasoning_effort=${tomlBasicString(effort)}`] : []),
|
|
114
|
+
];
|
|
88
115
|
},
|
|
89
116
|
},
|
|
90
117
|
agy: {
|
|
91
118
|
label: "Antigravity CLI",
|
|
92
119
|
bin: "agy",
|
|
93
120
|
type: "agy-settings",
|
|
121
|
+
defaultModel: "gemini-3.8-flash",
|
|
122
|
+
resolveModel: (explicit, cfg) => explicit && typeof explicit === "object"
|
|
123
|
+
? explicit
|
|
124
|
+
: { model: explicit || cfg?.launch_models?.agy || "gemini-3.8-flash", effort: cfg?.agy_effort || "high" },
|
|
94
125
|
install: "https://antigravity.google/cli/install.sh",
|
|
95
126
|
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.",
|
|
96
127
|
attribution: {
|
|
@@ -104,35 +135,40 @@ export const RECIPES = {
|
|
|
104
135
|
}),
|
|
105
136
|
args: (m) => {
|
|
106
137
|
const isObj = m && typeof m === "object";
|
|
107
|
-
const model = (isObj ? m.model : m) ||
|
|
138
|
+
const model = (isObj ? m.model : m) || "gemini-3.8-flash";
|
|
108
139
|
const effort = isObj ? m.effort : null;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return
|
|
140
|
+
// agy의 --model은 내장 Gemini 모델만 받는다. 다른 카탈로그 ID는
|
|
141
|
+
// settings.json model로 전달하고 CLI 인자에는 싣지 않는다.
|
|
142
|
+
return /^(?:google\/)?gemini-/.test(model)
|
|
143
|
+
? ["--model", model.replace(/^google\//, ""), "--effort", effort || "high"]
|
|
144
|
+
: [];
|
|
112
145
|
},
|
|
113
146
|
},
|
|
114
147
|
opencode: {
|
|
115
148
|
label: "opencode",
|
|
116
149
|
bin: "opencode",
|
|
117
150
|
type: "env",
|
|
151
|
+
defaultModel: "gpt-6-sol",
|
|
118
152
|
install: "npm i -g opencode-ai@latest",
|
|
119
|
-
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만
|
|
120
|
-
//
|
|
153
|
+
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다.
|
|
154
|
+
// OpenCode는 openai 공급자 ID에만 Responses용 getModel을 등록한다.
|
|
155
|
+
// 사용자 설정 파일을 수정하지 않고 이번 프로세스의 openai 공급자를 Pleum으로 덮는다.
|
|
156
|
+
// OPENCODE_CONFIG_CONTENT로 이번 실행에만 공급자를 주입한다.
|
|
121
157
|
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
122
158
|
attribution: {
|
|
123
159
|
title: "opencode",
|
|
124
160
|
referer: "https://opencode.ai",
|
|
125
161
|
},
|
|
126
162
|
env: (key, model) => {
|
|
127
|
-
const active = model ||
|
|
163
|
+
const active = model || "gpt-6-sol";
|
|
128
164
|
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
129
165
|
models[active] = {};
|
|
130
166
|
return {
|
|
131
167
|
PLEUM_API_KEY: key,
|
|
132
168
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
133
169
|
provider: {
|
|
134
|
-
|
|
135
|
-
npm: "@ai-sdk/openai
|
|
170
|
+
openai: {
|
|
171
|
+
npm: "@ai-sdk/openai",
|
|
136
172
|
name: "PleumRouter",
|
|
137
173
|
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
138
174
|
models,
|
|
@@ -141,7 +177,7 @@ export const RECIPES = {
|
|
|
141
177
|
}),
|
|
142
178
|
};
|
|
143
179
|
},
|
|
144
|
-
args: (m) => ["--model", `
|
|
180
|
+
args: (m) => ["--model", `openai/${m || "gpt-6-sol"}`],
|
|
145
181
|
},
|
|
146
182
|
aider: {
|
|
147
183
|
label: "Aider",
|
|
@@ -409,7 +445,7 @@ export function tomlBasicString(value) {
|
|
|
409
445
|
|
|
410
446
|
export function codexToml(model, effort) {
|
|
411
447
|
const isObj = model && typeof model === "object";
|
|
412
|
-
const m = (isObj ? model.model : model) ||
|
|
448
|
+
const m = (isObj ? model.model : model) || "gpt-6-sol";
|
|
413
449
|
const eff = (isObj ? model.effort : effort) || null;
|
|
414
450
|
const effortLine = eff ? `model_reasoning_effort = ${tomlBasicString(eff)}\n` : "";
|
|
415
451
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
@@ -434,7 +470,7 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
434
470
|
}
|
|
435
471
|
}
|
|
436
472
|
const isObj = model && typeof model === "object";
|
|
437
|
-
const m = (isObj ? model.model : model) ||
|
|
473
|
+
const m = (isObj ? model.model : model) || "gemini-3.8-flash";
|
|
438
474
|
return { ...obj, modelProvider: "gemini", model: m };
|
|
439
475
|
}
|
|
440
476
|
|
package/src/settings.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { style, sanitizeTerminalText } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
|
-
import { readConfig, writeConfig, isLoggedIn, getActiveContext
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
13
13
|
import { RECIPES } from "./recipes.mjs";
|
|
14
14
|
import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
|
|
@@ -27,11 +27,10 @@ export function buildSettingsItems({
|
|
|
27
27
|
keyPrefix,
|
|
28
28
|
version,
|
|
29
29
|
activeContext = { type: "personal" },
|
|
30
|
-
autoUpdate = true,
|
|
31
30
|
latestVersion = null,
|
|
32
31
|
}) {
|
|
33
|
-
const contextDesc = activeContext?.type === "team"
|
|
34
|
-
? `${t("Team")}: ${activeContext.org_name}`
|
|
32
|
+
const contextDesc = activeContext?.type === "team"
|
|
33
|
+
? `${t("Team")}: ${activeContext.org_name || activeContext.org_id}`
|
|
35
34
|
: t("Personal Credit");
|
|
36
35
|
const updateDesc = latestVersion && isNewerVersion(version, latestVersion)
|
|
37
36
|
? `${t("Current version: {version}", { version })} → v${latestVersion} (${t("Update available")})`
|
|
@@ -51,11 +50,6 @@ export function buildSettingsItems({
|
|
|
51
50
|
: `${t("Logged in")} (${t("key")} ${keyPrefix}…)`
|
|
52
51
|
: t("Not logged in"),
|
|
53
52
|
},
|
|
54
|
-
{
|
|
55
|
-
id: "settings:auto_update",
|
|
56
|
-
title: t("Auto update"),
|
|
57
|
-
desc: autoUpdate ? t("Enabled (updates automatically on start)") : t("Disabled"),
|
|
58
|
-
},
|
|
59
53
|
{ id: "settings:update", title: t("Update CLI"), desc: updateDesc },
|
|
60
54
|
{ id: "settings:completion", title: t("Shell completion"), desc: t("Print a zsh/bash completion script") },
|
|
61
55
|
];
|
|
@@ -101,7 +95,6 @@ async function settingsSelect() {
|
|
|
101
95
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
102
96
|
version: readVersion(),
|
|
103
97
|
activeContext: getActiveContext(cfg),
|
|
104
|
-
autoUpdate: isAutoUpdateEnabled(cfg),
|
|
105
98
|
latestVersion: updateInfo?.latestVersion,
|
|
106
99
|
});
|
|
107
100
|
return runList(items, {
|
|
@@ -188,11 +181,6 @@ export async function runSettings() {
|
|
|
188
181
|
case "settings:account":
|
|
189
182
|
await runAccount();
|
|
190
183
|
continue;
|
|
191
|
-
case "settings:auto_update": {
|
|
192
|
-
const next = !isAutoUpdateEnabled();
|
|
193
|
-
setAutoUpdateEnabled(next);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
184
|
case "settings:update":
|
|
197
185
|
console.log(t("Updating pleumrouter CLI..."));
|
|
198
186
|
await runUpdateCommand([], { exit: false });
|
package/src/wire.mjs
CHANGED
|
@@ -69,12 +69,22 @@ function removeTomlTable(toml, name) {
|
|
|
69
69
|
return out.join("\n");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
function rootValue(toml, name) {
|
|
73
|
+
const root = toml.split(/^\s*\[/m, 1)[0];
|
|
74
|
+
const found = root.match(new RegExp(`^${name}\\s*=\\s*(.+)$`, "m"));
|
|
75
|
+
return found?.[1]?.trim() ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function previousValue(toml, name) {
|
|
79
|
+
const marked = toml.match(new RegExp(`^# previous_${name} = (.*)$`, "m"));
|
|
80
|
+
return marked ? marked[1].trim() : rootValue(stripWireBlock(toml), name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeRootValue(toml, name) {
|
|
84
|
+
const firstTable = toml.search(/^\s*\[/m);
|
|
85
|
+
const root = firstTable < 0 ? toml : toml.slice(0, firstTable);
|
|
86
|
+
const rest = firstTable < 0 ? "" : toml.slice(firstTable);
|
|
87
|
+
return root.replace(new RegExp(`^${name}\\s*=\\s*.+\\n?`, "m"), "") + rest;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
export function wireClaudeSettings(existingJson, { root, key }) {
|
|
@@ -87,10 +97,10 @@ export function wireClaudeSettings(existingJson, { root, key }) {
|
|
|
87
97
|
...(obj.env && typeof obj.env === "object" && !Array.isArray(obj.env) ? obj.env : {}),
|
|
88
98
|
ANTHROPIC_BASE_URL: root,
|
|
89
99
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
90
|
-
ANTHROPIC_API_KEY: key,
|
|
91
100
|
API_TIMEOUT_MS: "3000000",
|
|
92
101
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
93
102
|
};
|
|
103
|
+
delete obj.env.ANTHROPIC_API_KEY;
|
|
94
104
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
95
105
|
}
|
|
96
106
|
|
|
@@ -107,18 +117,22 @@ export function unwireClaudeSettings(existingJson) {
|
|
|
107
117
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
export function wireCodexToml(existingToml, { v1, key }) {
|
|
120
|
+
export function wireCodexToml(existingToml, { v1, key, model = "gpt-6-sol" }) {
|
|
111
121
|
const src = existingToml ?? "";
|
|
112
|
-
const
|
|
122
|
+
const previousProvider = previousValue(src, "model_provider");
|
|
123
|
+
const previousModel = previousValue(src, "model");
|
|
113
124
|
let body = stripWireBlock(src);
|
|
114
|
-
body = body.replace(/^model_provider\s*=\s*.+\n?/m, "");
|
|
115
125
|
body = removeTomlTable(body, "model_providers.pleum.http_headers");
|
|
116
126
|
body = removeTomlTable(body, "model_providers.pleum");
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
body = removeRootValue(removeRootValue(body, "model_provider"), "model");
|
|
128
|
+
const rootBlock = `${WIRE_MARKER_START}
|
|
129
|
+
# previous_model_provider = ${previousProvider}
|
|
130
|
+
# previous_model = ${previousModel}
|
|
131
|
+
model = ${tomlBasicString(model)}
|
|
119
132
|
model_provider = "pleum"
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
${WIRE_MARKER_END}
|
|
134
|
+
`;
|
|
135
|
+
const providerBlock = `[model_providers.pleum]
|
|
122
136
|
name = "PleumRouter"
|
|
123
137
|
base_url = ${tomlBasicString(v1)}
|
|
124
138
|
wire_api = "responses"
|
|
@@ -126,22 +140,24 @@ supports_websockets = false
|
|
|
126
140
|
|
|
127
141
|
[model_providers.pleum.http_headers]
|
|
128
142
|
Authorization = ${tomlBasicString(`Bearer ${key}`)}
|
|
129
|
-
${WIRE_MARKER_END}
|
|
130
143
|
`;
|
|
131
|
-
const trimmed = body.
|
|
132
|
-
return `${trimmed ?
|
|
144
|
+
const trimmed = body.trim();
|
|
145
|
+
return `${rootBlock}${trimmed ? `\n${trimmed}\n\n` : "\n"}${providerBlock}`;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
export function unwireCodexToml(existingToml) {
|
|
136
149
|
const src = existingToml ?? "";
|
|
137
|
-
const
|
|
150
|
+
const previousProvider = previousValue(src, "model_provider");
|
|
151
|
+
const previousModel = previousValue(src, "model");
|
|
138
152
|
let body = stripWireBlock(src);
|
|
139
153
|
body = removeTomlTable(body, "model_providers.pleum.http_headers");
|
|
140
154
|
body = removeTomlTable(body, "model_providers.pleum");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
body = removeRootValue(removeRootValue(body, "model_provider"), "model");
|
|
156
|
+
const restored = [
|
|
157
|
+
previousModel ? `model = ${previousModel}` : "",
|
|
158
|
+
previousProvider ? `model_provider = ${previousProvider}` : "",
|
|
159
|
+
].filter(Boolean).join("\n");
|
|
160
|
+
return `${restored ? `${restored}\n\n` : ""}${body.trim()}`.replace(/\n{3,}/g, "\n\n").trimStart() + "\n";
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
function parseJsonBody(res) {
|