pleumrouter 0.5.35 → 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 +54 -27
- package/src/menu.mjs +30 -122
- package/src/recipes.mjs +57 -20
- package/src/settings.mjs +1 -13
- package/src/wire.mjs +37 -21
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,31 +150,25 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
116
150
|
process.exit(1);
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
env.CODEX_HOME = writeIsolatedHome("codex-home", "config.toml", codexToml(model));
|
|
127
|
-
}
|
|
128
|
-
if (r.type === "pi-home") {
|
|
129
|
-
env.PI_CODING_AGENT_DIR = writeIsolatedHome("pi-home", "models.json", piModelsJson(model));
|
|
130
|
-
}
|
|
131
|
-
if (r.type === "crush-home") {
|
|
132
|
-
env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model));
|
|
133
|
-
}
|
|
159
|
+
const key = await getKey();
|
|
160
|
+
model = resolveLaunchModel(resolved, model);
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
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);
|
|
139
167
|
|
|
140
168
|
const cleanup = () => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
if (restore) {
|
|
170
|
+
restore();
|
|
171
|
+
restore = null;
|
|
144
172
|
}
|
|
145
173
|
};
|
|
146
174
|
|
|
@@ -195,7 +223,7 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
195
223
|
}
|
|
196
224
|
|
|
197
225
|
return await new Promise((resolve) => {
|
|
198
|
-
const child =
|
|
226
|
+
const child = spawnChild(command.command, command.args, {
|
|
199
227
|
env,
|
|
200
228
|
stdio: "inherit",
|
|
201
229
|
shell: false,
|
|
@@ -380,4 +408,3 @@ function printManual(tool, key) {
|
|
|
380
408
|
console.log(m.snippet());
|
|
381
409
|
console.log(`\n${t("2) Export the key and run:")}\n export PLEUM_API_KEY=${key}\n`);
|
|
382
410
|
}
|
|
383
|
-
|
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,8 +55,10 @@ 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
64
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
@@ -73,23 +84,44 @@ export const RECIPES = {
|
|
|
73
84
|
codex: {
|
|
74
85
|
label: "Codex CLI",
|
|
75
86
|
bin: "codex",
|
|
76
|
-
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" },
|
|
77
92
|
install: "npm i -g @openai/codex",
|
|
78
93
|
attribution: {
|
|
79
94
|
title: "Codex",
|
|
80
95
|
referer: "https://github.com/openai/codex",
|
|
81
96
|
},
|
|
82
97
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
83
|
-
args: (
|
|
84
|
-
const
|
|
85
|
-
const effort =
|
|
86
|
-
|
|
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
|
+
];
|
|
87
115
|
},
|
|
88
116
|
},
|
|
89
117
|
agy: {
|
|
90
118
|
label: "Antigravity CLI",
|
|
91
119
|
bin: "agy",
|
|
92
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" },
|
|
93
125
|
install: "https://antigravity.google/cli/install.sh",
|
|
94
126
|
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.",
|
|
95
127
|
attribution: {
|
|
@@ -103,35 +135,40 @@ export const RECIPES = {
|
|
|
103
135
|
}),
|
|
104
136
|
args: (m) => {
|
|
105
137
|
const isObj = m && typeof m === "object";
|
|
106
|
-
const model = (isObj ? m.model : m) ||
|
|
138
|
+
const model = (isObj ? m.model : m) || "gemini-3.8-flash";
|
|
107
139
|
const effort = isObj ? m.effort : null;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
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
|
+
: [];
|
|
111
145
|
},
|
|
112
146
|
},
|
|
113
147
|
opencode: {
|
|
114
148
|
label: "opencode",
|
|
115
149
|
bin: "opencode",
|
|
116
150
|
type: "env",
|
|
151
|
+
defaultModel: "gpt-6-sol",
|
|
117
152
|
install: "npm i -g opencode-ai@latest",
|
|
118
|
-
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만
|
|
119
|
-
//
|
|
153
|
+
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다.
|
|
154
|
+
// OpenCode는 openai 공급자 ID에만 Responses용 getModel을 등록한다.
|
|
155
|
+
// 사용자 설정 파일을 수정하지 않고 이번 프로세스의 openai 공급자를 Pleum으로 덮는다.
|
|
156
|
+
// OPENCODE_CONFIG_CONTENT로 이번 실행에만 공급자를 주입한다.
|
|
120
157
|
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
121
158
|
attribution: {
|
|
122
159
|
title: "opencode",
|
|
123
160
|
referer: "https://opencode.ai",
|
|
124
161
|
},
|
|
125
162
|
env: (key, model) => {
|
|
126
|
-
const active = model ||
|
|
163
|
+
const active = model || "gpt-6-sol";
|
|
127
164
|
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
128
165
|
models[active] = {};
|
|
129
166
|
return {
|
|
130
167
|
PLEUM_API_KEY: key,
|
|
131
168
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
132
169
|
provider: {
|
|
133
|
-
|
|
134
|
-
npm: "@ai-sdk/openai
|
|
170
|
+
openai: {
|
|
171
|
+
npm: "@ai-sdk/openai",
|
|
135
172
|
name: "PleumRouter",
|
|
136
173
|
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
137
174
|
models,
|
|
@@ -140,7 +177,7 @@ export const RECIPES = {
|
|
|
140
177
|
}),
|
|
141
178
|
};
|
|
142
179
|
},
|
|
143
|
-
args: (m) => ["--model", `
|
|
180
|
+
args: (m) => ["--model", `openai/${m || "gpt-6-sol"}`],
|
|
144
181
|
},
|
|
145
182
|
aider: {
|
|
146
183
|
label: "Aider",
|
|
@@ -408,7 +445,7 @@ export function tomlBasicString(value) {
|
|
|
408
445
|
|
|
409
446
|
export function codexToml(model, effort) {
|
|
410
447
|
const isObj = model && typeof model === "object";
|
|
411
|
-
const m = (isObj ? model.model : model) ||
|
|
448
|
+
const m = (isObj ? model.model : model) || "gpt-6-sol";
|
|
412
449
|
const eff = (isObj ? model.effort : effort) || null;
|
|
413
450
|
const effortLine = eff ? `model_reasoning_effort = ${tomlBasicString(eff)}\n` : "";
|
|
414
451
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
@@ -433,7 +470,7 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
433
470
|
}
|
|
434
471
|
}
|
|
435
472
|
const isObj = model && typeof model === "object";
|
|
436
|
-
const m = (isObj ? model.model : model) ||
|
|
473
|
+
const m = (isObj ? model.model : model) || "gemini-3.8-flash";
|
|
437
474
|
return { ...obj, modelProvider: "gemini", model: m };
|
|
438
475
|
}
|
|
439
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,7 +27,6 @@ export function buildSettingsItems({
|
|
|
27
27
|
keyPrefix,
|
|
28
28
|
version,
|
|
29
29
|
activeContext = { type: "personal" },
|
|
30
|
-
autoUpdate = true,
|
|
31
30
|
latestVersion = null,
|
|
32
31
|
}) {
|
|
33
32
|
const contextDesc = activeContext?.type === "team"
|
|
@@ -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 }) {
|
|
@@ -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) {
|