pleumrouter 0.5.36 → 0.5.39
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 +6 -4
- package/bin/pleum.mjs +5 -2
- package/package.json +1 -1
- package/src/config.mjs +11 -14
- package/src/epipe.mjs +10 -0
- package/src/install.mjs +65 -0
- package/src/launch.mjs +125 -9
- package/src/locales/es.mjs +17 -55
- package/src/locales/ja.mjs +17 -55
- package/src/locales/ko.mjs +17 -59
- package/src/locales/zh.mjs +17 -55
- package/src/login.mjs +24 -12
- package/src/menu.mjs +80 -167
- package/src/models.mjs +10 -134
- package/src/recipes.mjs +40 -42
- package/src/settings.mjs +0 -1
- package/src/update.mjs +8 -3
- package/src/wire.mjs +13 -3
- package/src/upgrade.mjs +0 -10
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ bun install -g pleumrouter # bun
|
|
|
17
17
|
## 빠른 시작
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
pleum # Claude Code · Codex · Antigravity ·
|
|
20
|
+
pleum # 에이전트 실행 → Claude Code · Codex · Antigravity · OpenCode
|
|
21
21
|
pleum claude # Claude Code 바로 실행
|
|
22
22
|
pleum codex # Codex 바로 실행
|
|
23
23
|
pleum agy # Antigravity CLI 바로 실행
|
|
@@ -33,8 +33,8 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
|
33
33
|
|
|
34
34
|
| 명령 | 설명 |
|
|
35
35
|
|------|------|
|
|
36
|
-
| `pleum` | (인자 없음, TTY)
|
|
37
|
-
| `pleum claude\|codex\|agy\|opencode [--model id]` | 네 우선 지원 도구를 한 명령으로 실행. 최초 실행은 브라우저 로그인 후 에이전트 키 발급 |
|
|
36
|
+
| `pleum` | (인자 없음, TTY) 에이전트 실행 → Claude Code·Codex·Antigravity·OpenCode. 앞의 세 도구는 공식/기본 모델 또는 custom(beta)을 선택. 파이프에선 도움말 출력 |
|
|
37
|
+
| `pleum claude\|codex\|agy\|opencode [--model id]` | 네 우선 지원 도구를 한 명령으로 실행. Claude·Codex·agy는 모델 미지정 시 공식/기본 모드, `--model` 지정 시 custom 모드. 최초 실행은 브라우저 로그인 후 에이전트 키 발급 |
|
|
38
38
|
| `pleum login [--device]` | 브라우저 OAuth 로그인 → access/refresh 토큰을 `~/.pleum/config.json`에 저장(API 키 미발급). `--device`는 SSH·CI |
|
|
39
39
|
| `pleum logout` | 서버 세션 revoke + 로컬 토큰 삭제(언어·기본 모델 등 나머지 설정은 유지) |
|
|
40
40
|
| `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
|
|
@@ -81,7 +81,9 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
|
81
81
|
env·실행 인자 또는 일부 도구의 격리 홈(`PI_CODING_AGENT_DIR` / `CRUSH_GLOBAL_CONFIG`)으로 연결한다. Codex는 `-c` 임시 설정을 써서 기존 `~/.codex`의 세션과 설정을 유지한다.
|
|
82
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`.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
공식/기본 모드에서도 요청은 PleumRouter를 거쳐 Pleum API 크레딧으로 정산된다. Claude custom은 주 대화 모델만 바꾸며 일반 서브에이전트는 Pleum의 Sonnet으로 실행한다(Claude의 fork·`model: inherit` skill은 주 대화 모델을 상속할 수 있다). OpenCode 실행 세션의 `/models`에는 활성 Pleum 텍스트 대화 모델만 표시하며 추천 10개를 즐겨찾기 상단에 둔다. 기존 즐겨찾기와 최근 기록은 보존한다.
|
|
85
|
+
|
|
86
|
+
`agy`만 사용자 `~/.gemini/antigravity-cli/settings.json`에 `modelProvider=gemini`를 임시 병합하고 종료 시 복구한다(제품이 이 파일 없이 키를 무시함). agy custom은 설치 버전의 모델 목록과 실제 요청 ID가 확인된 Gemini 모델만 실행한다. Kiro는 공식 BYOK가 없어 지원하지 않는다.
|
|
85
87
|
|
|
86
88
|
## 환경변수
|
|
87
89
|
|
package/bin/pleum.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// #9: `pleum ... | head` 처럼 소비자가 먼저 닫히면 Node는 stdout 'error'(EPIPE)
|
|
3
|
+
// 를 unhandled로 크래시시킨다 — EPIPE만 정상 종료로 흡수한다(src/epipe.mjs).
|
|
4
|
+
import { installEpipeGuard } from "../src/epipe.mjs";
|
|
5
|
+
installEpipeGuard();
|
|
2
6
|
import { readFileSync } from "node:fs";
|
|
3
7
|
import { login, loginDevice, logout } from "../src/login.mjs";
|
|
4
8
|
import { launch, launchHelp, recoverAgySettings } from "../src/launch.mjs";
|
|
@@ -10,7 +14,6 @@ import { usage } from "../src/usage.mjs";
|
|
|
10
14
|
import { top } from "../src/top.mjs";
|
|
11
15
|
import { health } from "../src/health.mjs";
|
|
12
16
|
import { completion } from "../src/completion.mjs";
|
|
13
|
-
import { upgrade } from "../src/upgrade.mjs";
|
|
14
17
|
import { runUpdateCommand } from "../src/update.mjs";
|
|
15
18
|
import { mainMenu, runAgentMenu } from "../src/menu.mjs";
|
|
16
19
|
import { lang } from "../src/lang.mjs";
|
|
@@ -55,7 +58,7 @@ function buildHelp() {
|
|
|
55
58
|
const examples = [
|
|
56
59
|
["pleum wire", null],
|
|
57
60
|
["pleum launch claude --model claude-sonnet-5", null],
|
|
58
|
-
["pleum launch codex --model gpt-
|
|
61
|
+
["pleum launch codex --model gpt-6-sol -- --full-auto", null],
|
|
59
62
|
["pleum claude", null],
|
|
60
63
|
["pleum codex", null],
|
|
61
64
|
["pleum agy", null],
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync, renameSync } from "node:fs";
|
|
4
4
|
import { t } from "./i18n.mjs";
|
|
5
5
|
|
|
6
6
|
// 공개 게이트웨이 루트(canonical API). /v1/* 를 Next.js Route Handler 가 프록시하는
|
|
@@ -35,7 +35,16 @@ export function readConfig() {
|
|
|
35
35
|
|
|
36
36
|
export function writeConfig(cfg) {
|
|
37
37
|
mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
|
38
|
-
|
|
38
|
+
// 기존 파일 퍼미션과 무관하게 항상 0600이 되게 원자적으로 쓴다(#41과 동일 계약).
|
|
39
|
+
const tmp = `${FILE}.pleum-tmp-${process.pid}`;
|
|
40
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
41
|
+
try {
|
|
42
|
+
chmodSync(tmp, 0o600);
|
|
43
|
+
renameSync(tmp, FILE);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try { unlinkSync(tmp); } catch {}
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
// 로그인 저장 — 기존 설정(lang·launch_models·last_selection)을 보존한 채 자격증명만 병합.
|
|
@@ -241,16 +250,4 @@ export async function authFetch(path, init = {}) {
|
|
|
241
250
|
});
|
|
242
251
|
}
|
|
243
252
|
|
|
244
|
-
/** 자동 업데이트 활성화 여부 (기본값: true). */
|
|
245
|
-
export function isAutoUpdateEnabled(cfg = readConfig()) {
|
|
246
|
-
return cfg?.auto_update !== false;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** 자동 업데이트 설정 저장. */
|
|
250
|
-
export function setAutoUpdateEnabled(enabled) {
|
|
251
|
-
const cfg = readConfig() ?? {};
|
|
252
|
-
cfg.auto_update = Boolean(enabled);
|
|
253
|
-
writeConfig(cfg);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
253
|
|
package/src/epipe.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// #9: `pleum ... | head` 등 조기 종료 파이프에서 stdout/stdstderr 'error'(EPIPE)
|
|
2
|
+
// 가 unhandled 크래시를 일으키던 것을 흡수한다. EPIPE만 정상 exit(0)으로 끝내고
|
|
3
|
+
// 그 외 오류는 그대로 전파된다(실패로 취급).
|
|
4
|
+
export function installEpipeGuard(streams = [process.stdout, process.stderr], exit = (n) => process.exit(n)) {
|
|
5
|
+
for (const stream of streams) {
|
|
6
|
+
stream?.on?.("error", (e) => {
|
|
7
|
+
if (e?.code === "EPIPE") exit(0);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolveLaunchCommand } from "./launch.mjs";
|
|
3
|
+
import { t } from "./i18n.mjs";
|
|
4
|
+
|
|
5
|
+
// 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이 안전하게 실행한다.
|
|
6
|
+
//
|
|
7
|
+
// 지원 형태(정의된 단계만 실행):
|
|
8
|
+
// - 공백 구분 argv 명령 1개 이상("npm i -g x")
|
|
9
|
+
// - "&&" 로 이어진 순차 단계("python -m pip install aider-install && aider-install")
|
|
10
|
+
// 미지원(셸 문법 — shell:true 없이는 실행 불가, 주입 표면을 만들지 않는다):
|
|
11
|
+
// - 파이프("|")·리다이렉트(<>)·치환($`;) 등 → {manual: true} 로 수동 안내.
|
|
12
|
+
// Windows .cmd/.bat spawn(EINVAL)은 resolveLaunchCommand 가 node 진입점으로
|
|
13
|
+
// 우회해 예방한다(#10).
|
|
14
|
+
|
|
15
|
+
const SHELL_METACHARS = /[|<>;$`' "\\]/;
|
|
16
|
+
|
|
17
|
+
export function parseInstallCommand(install) {
|
|
18
|
+
const raw = String(install ?? "").trim();
|
|
19
|
+
if (!raw) return { manual: true, reason: "empty" };
|
|
20
|
+
// URL 단독(antigravity/goose) — 실행할 명령이 아니라 문서 링크.
|
|
21
|
+
if (/^https?:\/\//.test(raw) && !/\s/.test(raw)) return { manual: true, reason: "url" };
|
|
22
|
+
const steps = raw.split("&&").map((part) => part.trim().split(/\s+/).filter(Boolean));
|
|
23
|
+
if (steps.some((args) => args.length === 0)) return { manual: true, reason: "empty-step" };
|
|
24
|
+
if (steps.some((args) => args.some((tok) => SHELL_METACHARS.test(tok)))) {
|
|
25
|
+
return { manual: true, reason: "shell-syntax" };
|
|
26
|
+
}
|
|
27
|
+
return { steps };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 설치 명령 실행. {code, manual} 반환 — manual이면 호출자가 수동 안내를 출력한다.
|
|
32
|
+
* spawnImpl/resolve는 테스트 주입용.
|
|
33
|
+
*/
|
|
34
|
+
export async function runInstallCommand(install, {
|
|
35
|
+
spawnImpl = spawn,
|
|
36
|
+
resolve = resolveLaunchCommand,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const parsed = parseInstallCommand(install);
|
|
39
|
+
if (parsed.manual) return { manual: true, reason: parsed.reason, code: null };
|
|
40
|
+
|
|
41
|
+
for (const args of parsed.steps) {
|
|
42
|
+
const [bin, ...rest] = args;
|
|
43
|
+
const code = await new Promise((res) => {
|
|
44
|
+
let command;
|
|
45
|
+
try {
|
|
46
|
+
command = resolve(bin, rest);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(`✗ ${e.message}`);
|
|
49
|
+
res(1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const child = spawnImpl(command.command, command.args, { stdio: "inherit", shell: false });
|
|
53
|
+
child.on("exit", (n) => res(n ?? 1));
|
|
54
|
+
child.on("error", () => res(1));
|
|
55
|
+
});
|
|
56
|
+
if (code !== 0) return { manual: false, code };
|
|
57
|
+
}
|
|
58
|
+
return { manual: false, code: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 수동 설치 안내 출력 — 미지원 셸 문법을 절대 임의 실행하지 않는다. */
|
|
62
|
+
export function printManualInstall(install) {
|
|
63
|
+
console.log(t("This install command needs a shell pipe; run it manually:"));
|
|
64
|
+
console.log(` $ ${install}`);
|
|
65
|
+
}
|
package/src/launch.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, mkdtempSync, renameSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, delimiter, dirname, resolve } from "node:path";
|
|
5
|
-
import { readConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
5
|
+
import { readConfig, isLoggedIn, getActiveContext, ROOT } from "./config.mjs";
|
|
6
6
|
import { login } from "./login.mjs";
|
|
7
7
|
import { ensureWireKey } from "./wire.mjs";
|
|
8
8
|
import {
|
|
9
9
|
RECIPES,
|
|
10
|
+
CORE_MODELS,
|
|
10
11
|
MANUAL,
|
|
11
12
|
LAUNCH_ALIASES,
|
|
12
13
|
resolveLaunchTool,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from "./recipes.mjs";
|
|
18
19
|
import { t } from "./i18n.mjs";
|
|
19
20
|
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
21
|
+
import { fetchModels, codingModels } from "./models.mjs";
|
|
20
22
|
|
|
21
23
|
// PATH에서 실행 파일을 찾는다(설치 여부 표시용). menu.mjs도 사용.
|
|
22
24
|
export function onPath(bin) {
|
|
@@ -52,6 +54,14 @@ function nodeEntryFromWindowsShim(script, { read = readFileSync, exists = exists
|
|
|
52
54
|
return exists(entry) ? entry : null;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
// POSIX 관례: 시그널 종료 exit code = 128 + 신호 번호(SIGINT=130, SIGTERM=143).
|
|
58
|
+
const SIGNAL_NUMBERS = { HUP: 1, INT: 2, QUIT: 3, ABRT: 6, KILL: 9, TERM: 15 };
|
|
59
|
+
export function signalExitCode(signal) {
|
|
60
|
+
if (!signal) return 0;
|
|
61
|
+
const num = SIGNAL_NUMBERS[signal.replace(/^SIG/, "").toUpperCase()];
|
|
62
|
+
return 128 + (num ?? 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// 플랫폼·filesystem 주입으로 Windows 경로 정책을 Linux/macOS에서도 회귀 테스트한다.
|
|
56
66
|
export function resolveLaunchCommand(bin, args, {
|
|
57
67
|
platform = process.platform,
|
|
@@ -102,6 +112,81 @@ export function resolveLaunchModel(tool, explicit, cfg = readConfig()) {
|
|
|
102
112
|
return explicit || cfg?.launch_models?.[tool] || recipe?.defaultModel;
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
// agy의 Google API-key 모드는 계정 모드보다 모델이 적다. 임시 HOME으로 실제
|
|
116
|
+
// 설치 버전의 목록을 읽어, 선택 모델이 다른 Gemini 모델로 조용히 바뀌는 일을 막는다.
|
|
117
|
+
export function listAgyModelIds({ run = execFileSync } = {}) {
|
|
118
|
+
const home = mkdtempSync(join(tmpdir(), "pleum-agy-models-"));
|
|
119
|
+
try {
|
|
120
|
+
const settingsDir = join(home, ".gemini", "antigravity-cli");
|
|
121
|
+
mkdirSync(settingsDir, { recursive: true, mode: 0o700 });
|
|
122
|
+
writeFileSync(join(settingsDir, "settings.json"), '{"modelProvider":"gemini"}', { mode: 0o600 });
|
|
123
|
+
const agyCmd = resolveLaunchCommand("agy", ["models"]);
|
|
124
|
+
const output = run(agyCmd.command, agyCmd.args, {
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
timeout: 15000,
|
|
127
|
+
env: {
|
|
128
|
+
...process.env,
|
|
129
|
+
HOME: home,
|
|
130
|
+
USERPROFILE: home,
|
|
131
|
+
GEMINI_API_KEY: "pleum-model-list-probe",
|
|
132
|
+
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return new Set(String(output).split(/\r?\n/).map((line) => line.split(/\s/)[0]).filter((id) => /^gemini-/.test(id)));
|
|
136
|
+
} finally {
|
|
137
|
+
rmSync(home, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 설치된 agy 1.2.12를 모의 Gemini 엔드포인트에 연결해 실제 요청 ID가
|
|
142
|
+
// 선택 ID와 일치함을 확인한 모델만 열어 둔다. 새 모델은 같은 검증 후 추가한다.
|
|
143
|
+
const AGY_VERIFIED_MODELS = new Map([
|
|
144
|
+
["gemini-3.8-flash", "gemini-3.8-flash"],
|
|
145
|
+
["gemini-3.7-flash", "gemini-3.7-flash"],
|
|
146
|
+
["gemini-3.6-flash", "gemini-3.6-flash"],
|
|
147
|
+
["gemini-3.1-pro-preview", "gemini-3.1-pro"],
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
export function agyCliModel(model, ids) {
|
|
151
|
+
const cliBase = AGY_VERIFIED_MODELS.get(model);
|
|
152
|
+
if (!cliBase) return null;
|
|
153
|
+
return [cliBase, `${cliBase}-medium`, `${cliBase}-high`, `${cliBase}-low`, `${cliBase}-max`].find((id) => ids.has(id)) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function supportsAgyModel(model, ids) {
|
|
157
|
+
return Boolean(agyCliModel(model, ids));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function opencodeStateDir({ run = execFileSync } = {}) {
|
|
161
|
+
try {
|
|
162
|
+
const ocCmd = resolveLaunchCommand("opencode", ["debug", "paths"]);
|
|
163
|
+
const paths = run(ocCmd.command, ocCmd.args, { encoding: "utf8", timeout: 5000 });
|
|
164
|
+
const state = /^state\s+(.+)$/m.exec(String(paths))?.[1]?.trim();
|
|
165
|
+
if (state) return state;
|
|
166
|
+
} catch {}
|
|
167
|
+
return join(process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "opencode");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function seedOpenCodeFavorites(models, { stateDir = opencodeStateDir() } = {}) {
|
|
171
|
+
const path = join(stateDir, "model.json");
|
|
172
|
+
const state = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
173
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("Invalid OpenCode model state");
|
|
174
|
+
const current = Array.isArray(state.favorite) ? state.favorite : [];
|
|
175
|
+
const active = new Set(codingModels(models).map((m) => m.id));
|
|
176
|
+
const pins = CORE_MODELS.filter((id) => active.has(id)).map((modelID) => ({ providerID: "openai", modelID }));
|
|
177
|
+
const pinKeys = new Set(pins.map((m) => `${m.providerID}/${m.modelID}`));
|
|
178
|
+
const favorite = [...pins, ...current.filter((m) => !pinKeys.has(`${m.providerID}/${m.modelID}`))];
|
|
179
|
+
if (JSON.stringify(favorite) === JSON.stringify(current)) return;
|
|
180
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
181
|
+
const temp = join(stateDir, `model.json.pleum-${process.pid}`);
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(temp, JSON.stringify({ ...state, favorite }), { mode: 0o600 });
|
|
184
|
+
renameSync(temp, path);
|
|
185
|
+
} finally {
|
|
186
|
+
rmSync(temp, { force: true });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
105
190
|
// 공통 실행 파이프라인의 설정 어댑터. 신규 에이전트는 env/args만 쓰거나
|
|
106
191
|
// 별도 홈이 필요할 때 타입 핸들러 하나를 등록하면 된다.
|
|
107
192
|
const SETUP = {
|
|
@@ -130,6 +215,9 @@ export async function launch(tool, model, passthrough, {
|
|
|
130
215
|
getKey = agentKey,
|
|
131
216
|
pathCheck = onPath,
|
|
132
217
|
spawnChild = spawn,
|
|
218
|
+
getModels = fetchModels,
|
|
219
|
+
getAgyModels = listAgyModelIds,
|
|
220
|
+
seedFavorites = seedOpenCodeFavorites,
|
|
133
221
|
} = {}) {
|
|
134
222
|
recoverAgySettings();
|
|
135
223
|
const resolved = resolveLaunchTool(tool);
|
|
@@ -156,15 +244,41 @@ export async function launch(tool, model, passthrough, {
|
|
|
156
244
|
return 127;
|
|
157
245
|
}
|
|
158
246
|
|
|
159
|
-
const key = await getKey();
|
|
160
247
|
model = resolveLaunchModel(resolved, model);
|
|
248
|
+
let catalog;
|
|
249
|
+
if (model?.mode === "custom" || resolved === "opencode") {
|
|
250
|
+
catalog = codingModels(await getModels());
|
|
251
|
+
const chosen = model?.mode === "custom" ? model.model : model;
|
|
252
|
+
const [id, provider] = String(chosen || "").split("@");
|
|
253
|
+
const entry = catalog.find((m) => m.id === id);
|
|
254
|
+
if (!entry || (provider && !entry.providers?.some((p) => p.provider === provider))) {
|
|
255
|
+
throw new Error(t("Model is not an active coding model: {model}", { model: sanitizeTerminalText(chosen) }));
|
|
256
|
+
}
|
|
257
|
+
if (resolved === "agy") {
|
|
258
|
+
const cliModel = agyCliModel(chosen, getAgyModels());
|
|
259
|
+
if (!cliModel) {
|
|
260
|
+
throw new Error(t("Antigravity does not support this model through its Gemini API mode: {model}", { model: sanitizeTerminalText(chosen) }));
|
|
261
|
+
}
|
|
262
|
+
model = { ...model, cliModel };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const key = await getKey();
|
|
161
266
|
|
|
162
|
-
const env = { ...process.env
|
|
267
|
+
const env = { ...process.env };
|
|
163
268
|
for (const name of r.unsetEnv || []) delete env[name];
|
|
269
|
+
Object.assign(env, r.env(key, model, catalog));
|
|
164
270
|
const setup = SETUP[r.type || "env"];
|
|
165
271
|
if (!setup) throw new Error(`Unknown agent setup type: ${r.type}`);
|
|
166
272
|
let restore = setup(env, model);
|
|
167
273
|
|
|
274
|
+
if (resolved === "opencode") {
|
|
275
|
+
try {
|
|
276
|
+
seedFavorites(catalog);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.log(`ℹ ${t("Could not add Pleum models to OpenCode favorites: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
168
282
|
const cleanup = () => {
|
|
169
283
|
if (restore) {
|
|
170
284
|
restore();
|
|
@@ -240,11 +354,13 @@ export async function launch(tool, model, passthrough, {
|
|
|
240
354
|
if (exit) process.exit(1);
|
|
241
355
|
resolve(1);
|
|
242
356
|
});
|
|
243
|
-
child.on("exit", (code) => {
|
|
357
|
+
child.on("exit", (code, signal) => {
|
|
244
358
|
cleanup();
|
|
245
359
|
removeListeners();
|
|
246
|
-
|
|
247
|
-
|
|
360
|
+
// 시그널 종료는 code가 null — exit 0은 "정상 완료"로 거짓 보고한다(#116).
|
|
361
|
+
const effective = code ?? signalExitCode(signal);
|
|
362
|
+
if (exit) process.exit(effective);
|
|
363
|
+
resolve(effective);
|
|
248
364
|
});
|
|
249
365
|
});
|
|
250
366
|
}
|
package/src/locales/es.mjs
CHANGED
|
@@ -9,7 +9,6 @@ export const ES = {
|
|
|
9
9
|
"/help for commands · /exit to quit · wrap multi-line input in \"\"\" · Ctrl+C to stop generation": "/help para comandos · /exit para salir · envuelve la entrada multilínea en \"\"\" · Ctrl+C para detener la generación",
|
|
10
10
|
"1) Add to {file}:": "1) Añade a {file}:",
|
|
11
11
|
"2) Export the key and run:": "2) Exporta la clave y ejecuta:",
|
|
12
|
-
"Account": "Cuenta",
|
|
13
12
|
"All models — price and context included": "Todos los modelos — precio y contexto incluidos",
|
|
14
13
|
"Anthropic's coding agent — supports subagents": "Agente de codificación de Anthropic — admite subagentes",
|
|
15
14
|
"Attaching and uploading {n} explicit local image(s).": "Adjuntando y subiendo {n} imagen(es) local(es) seleccionada(s) explícitamente.",
|
|
@@ -31,8 +30,6 @@ export const ES = {
|
|
|
31
30
|
"Local code execution & pair programmer": "Ejecución de código local y programador en pareja",
|
|
32
31
|
"Available: {list}": "Disponibles: {list}",
|
|
33
32
|
"Balance": "Saldo",
|
|
34
|
-
"Balance is below the auto-charge threshold ({threshold} ₩).": "El saldo está por debajo del umbral de recarga automática ({threshold} ₩).",
|
|
35
|
-
"Balance:": "Saldo:",
|
|
36
33
|
"Block's local-first agent": "Agente local-first de Block",
|
|
37
34
|
"Chat with a model": "Chatear con un modelo",
|
|
38
35
|
"Check gateway connectivity and login status": "Verificar la conectividad del gateway y el estado de la sesión",
|
|
@@ -49,12 +46,9 @@ export const ES = {
|
|
|
49
46
|
"Current system prompt:": "System prompt actual:",
|
|
50
47
|
"Current version: {version}": "Versión actual: {version}",
|
|
51
48
|
"Default models": "Modelos predeterminados",
|
|
52
|
-
"Delete the saved key": "Eliminar la clave guardada",
|
|
53
49
|
"Error:": "Error:",
|
|
54
|
-
"Example: pleum launch claude --model claude-sonnet-4": "Ejemplo: pleum launch claude --model claude-sonnet-4",
|
|
55
50
|
"Examples:": "Ejemplos:",
|
|
56
51
|
"FX rate USD/KRW {rate}": "Tipo de cambio USD/KRW {rate}",
|
|
57
|
-
"Failed to check balance (HTTP {status})": "No se pudo verificar el saldo (HTTP {status})",
|
|
58
52
|
"Failed to fetch leaderboard ({status})": "No se pudo obtener el ranking ({status})",
|
|
59
53
|
"Failed to fetch model detail ({status})": "No se pudo obtener el detalle del modelo ({status})",
|
|
60
54
|
"Failed to fetch model list ({status})": "No se pudo obtener la lista de modelos ({status})",
|
|
@@ -71,8 +65,6 @@ export const ES = {
|
|
|
71
65
|
"Necesita GitHub Copilot CLI 2026-04 o posterior para el enrutamiento de proveedores BYOK.",
|
|
72
66
|
"Gemini CLI speaks Google's native generateContent format — set the model after launch with `/model google/<id>`.":
|
|
73
67
|
"Gemini CLI usa el formato nativo generateContent de Google — establece el modelo tras el inicio con `/model google/<id>`.",
|
|
74
|
-
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.":
|
|
75
|
-
"Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json. Enruta cualquier modelo de codificación a través de PleumRouter.",
|
|
76
68
|
"↑ {n} more above": "↑ {n} más arriba",
|
|
77
69
|
"↓ {n} more below": "↓ {n} más abajo",
|
|
78
70
|
"Auto (best route)": "Auto (mejor ruta)",
|
|
@@ -89,24 +81,18 @@ export const ES = {
|
|
|
89
81
|
"Gateway": "Gateway",
|
|
90
82
|
"Gateway connection failed: {error}": "Error de conexión al gateway: {error}",
|
|
91
83
|
"No response within {s}s": "Sin respuesta en {s} s",
|
|
92
|
-
"Gateway connectivity · login status": "Conectividad del gateway · estado de la sesión",
|
|
93
84
|
"Goodbye.": "Adiós.",
|
|
94
85
|
"Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose define su modelo en su propia configuración — usa el formato openai/<id> en `goose configure`.",
|
|
95
|
-
"Health check": "Verificación de estado",
|
|
96
86
|
"Install canceled — manual install: {install}": "Instalación cancelada — instalación manual: {install}",
|
|
97
87
|
"Install failed (exit {code})": "Error de instalación (código de salida {code})",
|
|
98
88
|
"Language": "Idioma",
|
|
99
89
|
"Language set to {name}": "Idioma configurado a {name}",
|
|
100
90
|
"Language, default models, account, updates": "Idioma, modelos predeterminados, cuenta, actualizaciones",
|
|
101
|
-
"Leaderboard": "Ranking",
|
|
102
91
|
"Log in": "Iniciar sesión",
|
|
103
92
|
"Log in at the URL above, then enter the code (within {min} min).": "Inicia sesión en la URL de arriba y luego introduce el código (dentro de {min} min).",
|
|
104
93
|
"Log in from your browser. If it doesn't open automatically, open this URL:": "Inicia sesión desde tu navegador. Si no se abre automáticamente, abre esta URL:",
|
|
105
|
-
"Log in via browser + auto-save the key": "Iniciar sesión por navegador y guardar la clave automáticamente",
|
|
106
|
-
"Log in via browser and save the API key": "Iniciar sesión por navegador y guardar la clave de API",
|
|
107
94
|
"Log out? [Y/n] ": "¿Cerrar sesión? [Y/n] ",
|
|
108
95
|
"Logged in": "Sesión iniciada",
|
|
109
|
-
"Logged in. Saved the key to ~/.pleum/config.json.": "Sesión iniciada. Clave guardada en ~/.pleum/config.json.",
|
|
110
96
|
"Logged in. Saved the session to ~/.pleum/config.json.": "Sesión iniciada. Sesión guardada en ~/.pleum/config.json.",
|
|
111
97
|
"Legacy API key login is no longer supported. Run:\n pleum login": "El inicio de sesión con clave API heredada ya no es compatible. Ejecuta:\n pleum login",
|
|
112
98
|
"Session expired. Run:\n pleum login": "La sesión ha caducado. Ejecuta:\n pleum login",
|
|
@@ -125,7 +111,6 @@ export const ES = {
|
|
|
125
111
|
"Model list": "Lista de modelos",
|
|
126
112
|
"Model list (price, context included)": "Lista de modelos (precio, contexto incluido)",
|
|
127
113
|
"Model not found: {id} (list: pleum models)": "Modelo no encontrado: {id} (lista: pleum models)",
|
|
128
|
-
"Model rankings · provider share": "Ranking de modelos · cuota por proveedor",
|
|
129
114
|
"No data": "Sin datos",
|
|
130
115
|
"No matches": "Sin coincidencias",
|
|
131
116
|
"No matches — showing the full list.": "Sin coincidencias — mostrando la lista completa.",
|
|
@@ -147,8 +132,6 @@ export const ES = {
|
|
|
147
132
|
"Print a zsh/bash completion script": "Imprimir un script de autocompletado de zsh/bash",
|
|
148
133
|
"Print the version": "Imprimir la versión",
|
|
149
134
|
"Provider share": "Cuota por proveedor",
|
|
150
|
-
"Reinstall the latest version": "Reinstalar la última versión",
|
|
151
|
-
"Reinstalling — pleum will exit when finished. Run `pleum` again afterward.": "Reinstalando — pleum se cerrará al terminar. Ejecuta pleum de nuevo después.",
|
|
152
135
|
"Run a coding agent against PleumRouter": "Ejecutar un agente de codificación contra PleumRouter",
|
|
153
136
|
"Run pleum login again to get a new key (recent keys include it by default).": "Ejecuta pleum login de nuevo para obtener una nueva clave (las claves recientes la incluyen por defecto).",
|
|
154
137
|
"Running {label}: {cmd}": "Ejecutando {label}: {cmd}",
|
|
@@ -164,9 +147,7 @@ export const ES = {
|
|
|
164
147
|
"Shell completion": "Autocompletado de shell",
|
|
165
148
|
"Show/change display language (en|ko|ja|zh|es)": "Mostrar/cambiar el idioma de visualización (en|ko|ja|zh|es)",
|
|
166
149
|
"Specify a model: pleum run <model> [\"prompt\"] (model selection isn't available on non-interactive stdin)": "Especifica un modelo: pleum run <model> [\"prompt\"] (la selección de modelo no está disponible en stdin no interactivo)",
|
|
167
|
-
"Spend over the last 30 days": "Gasto de los últimos 30 días",
|
|
168
150
|
"Start an interactive chat": "Iniciar un chat interactivo",
|
|
169
|
-
"State mismatch or missing key": "Discrepancia de estado o clave faltante",
|
|
170
151
|
"Stats {state}": "Estadísticas {state}",
|
|
171
152
|
"Summarize in one line: ...": "Resume en una línea: ...",
|
|
172
153
|
"Switched model →": "Modelo cambiado →",
|
|
@@ -174,7 +155,6 @@ export const ES = {
|
|
|
174
155
|
"Terminal chat (REPL) / one-shot / pipe": "Chat de terminal (REPL) / un solo disparo / pipe",
|
|
175
156
|
"Terminal pair programming": "Programación en pareja por terminal",
|
|
176
157
|
"The code has expired.": "El código ha expirado.",
|
|
177
|
-
"The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.": "Es posible que el servidor aún no admita la verificación de saldo por clave de API, o que la clave haya expirado — intenta pleum login de nuevo.",
|
|
178
158
|
"This API key doesn't have usage:read permission.": "Esta clave de API no tiene el permiso usage:read.",
|
|
179
159
|
"Timed out: the code has expired.": "Tiempo de espera agotado: el código ha expirado.",
|
|
180
160
|
"Total": "Total",
|
|
@@ -206,7 +186,6 @@ export const ES = {
|
|
|
206
186
|
"Unsupported tool: {tool}": "Herramienta no compatible: {tool}",
|
|
207
187
|
"Update CLI": "Actualizar la CLI",
|
|
208
188
|
"Usage": "Uso",
|
|
209
|
-
"Usage report": "Informe de uso",
|
|
210
189
|
"Usage: pleum completion <zsh|bash>": "Uso: pleum completion <zsh|bash>",
|
|
211
190
|
"Usage: pleum launch <tool> [--model id] [-- tool args]\n": "Uso: pleum launch <tool> [--model id] [-- tool args]\n",
|
|
212
191
|
"Usage: pleum models <model-id> --info": "Uso: pleum models <model-id> --info",
|
|
@@ -246,7 +225,6 @@ export const ES = {
|
|
|
246
225
|
"price = per 1M tokens (display rate; actual billing may vary slightly with the intraday rate)": "precio = por 1M tokens (tarifa de referencia; la facturación real puede variar ligeramente según la tarifa intradía)",
|
|
247
226
|
"price/1M": "precio/1M",
|
|
248
227
|
"select a model": "selecciona un modelo",
|
|
249
|
-
"supported": "compatible",
|
|
250
228
|
"switch: /model <id> · list: /models": "cambiar: /model <id> · lista: /models",
|
|
251
229
|
"tokens": "tokens",
|
|
252
230
|
"total": "total",
|
|
@@ -262,8 +240,6 @@ export const ES = {
|
|
|
262
240
|
"{n} providers configured": "{n} proveedores configurados",
|
|
263
241
|
"… {n} more — narrow with a search term": "… {n} más — acota con un término de búsqueda",
|
|
264
242
|
"… {n} more — type to narrow down": "… {n} más — escribe para acotar",
|
|
265
|
-
"↑/↓ move • enter run • → set model • esc quit": "↑/↓ mover • enter ejecutar • → definir modelo • esc salir",
|
|
266
|
-
"↑/↓ move • enter run • → set model • esc back": "↑/↓ mover • enter ejecutar • → definir modelo • esc volver",
|
|
267
243
|
"↑/↓ move • enter select • esc back": "↑/↓ mover • enter seleccionar • esc volver",
|
|
268
244
|
"↑/↓ move • enter set model • esc back": "↑/↓ mover • enter definir modelo • esc volver",
|
|
269
245
|
"⏹ aborted": "⏹ abortado",
|
|
@@ -289,47 +265,33 @@ export const ES = {
|
|
|
289
265
|
"by user": "por usuario",
|
|
290
266
|
"Failed to fetch balance ({status})": "Error al obtener el saldo ({status})",
|
|
291
267
|
|
|
268
|
+
"This install command needs a shell pipe; run it manually:": "Este comando de instalación requiere una tubería de shell; ejecútalo manualmente:",
|
|
269
|
+
|
|
292
270
|
// Update & Auto-update
|
|
293
271
|
"Update CLI to the latest version": "Actualizar CLI a la última versión",
|
|
294
272
|
"Alias for pleum update --force": "Alias de pleum update --force",
|
|
295
|
-
"Auto update": "Actualización automática",
|
|
296
|
-
"Enabled (updates automatically on start)": "Activada (comprueba y actualiza al inicio)",
|
|
297
|
-
"Disabled": "Desactivada",
|
|
298
273
|
"Update available": "Actualización disponible",
|
|
299
|
-
"Update available: v{current} → v{latest}": "Actualización disponible: v{current} → v{latest}",
|
|
300
|
-
"Update available: v{current} → v{latest}. Run 'pleum update' to update.": "Actualización disponible: v{current} → v{latest}. Ejecute 'pleum update' para actualizar.",
|
|
301
|
-
"Auto-updating PleumRouter CLI...": "Actualizando automáticamente PleumRouter CLI...",
|
|
302
274
|
"Updating pleumrouter CLI...": "Actualizando PleumRouter CLI...",
|
|
303
275
|
"Updating pleumrouter (force reinstall)...": "Actualizando PleumRouter (reinstalación forzada)...",
|
|
304
276
|
"Already using the latest version of pleumrouter (v{version}).": "Ya está utilizando la última versión de PleumRouter (v{version}).",
|
|
305
277
|
"Use --force (-f) to reinstall anyway.": "Utilice --force (-f) para reinstalar de todos modos.",
|
|
306
278
|
"Updating pleumrouter from v{from} to v{to}...": "Actualizando PleumRouter de v{from} a v{to}...",
|
|
307
279
|
"Update completed! Please run `pleum` again to use the new version.": "¡Actualización completada! Ejecute `pleum` de nuevo para usar la nueva versión.",
|
|
308
|
-
"Update completed! Please run `pleum` again.": "¡Actualización completada! Ejecute `pleum` de nuevo.",
|
|
309
|
-
|
|
310
280
|
// Claude Code Tier Mapping
|
|
311
|
-
"Sonnet model": "Modelo Sonnet",
|
|
312
|
-
"Opus model": "Modelo Opus",
|
|
313
|
-
"Haiku model": "Modelo Haiku",
|
|
314
|
-
"Use existing model configuration?": "¿Continuar con la configuración de modelos existente?",
|
|
315
|
-
"Keep existing configuration": "Continuar con la configuración existente",
|
|
316
|
-
"Reconfigure tiers": "Reconfigurar modelos",
|
|
317
|
-
"Choose Sonnet, Opus, and Haiku models again": "Volver a elegir los modelos Sonnet, Opus y Haiku",
|
|
318
|
-
|
|
319
281
|
// Codex & agy Effort
|
|
320
|
-
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
282
|
+
// Agent launch modes
|
|
283
|
+
"Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom solo admite modelos Gemini verificados en modo de clave API.",
|
|
284
|
+
"Anthropic official models": "Continuar con modelos oficiales de Anthropic",
|
|
285
|
+
"OpenAI official models": "Continuar con modelos oficiales de OpenAI",
|
|
286
|
+
"Antigravity default models": "Continuar con modelos predeterminados de Antigravity",
|
|
287
|
+
"Choose models inside the agent": "Elegir modelo dentro del agente",
|
|
288
|
+
"Choose my model (beta)": "Elegir mi modelo (beta)",
|
|
289
|
+
"Select a PleumRouter coding model": "Elegir un modelo de código de PleumRouter",
|
|
290
|
+
"No compatible coding models available": "No hay modelos de código compatibles",
|
|
291
|
+
"Antigravity model discovery failed: {error}": "No se pudo consultar los modelos de Antigravity: {error}",
|
|
292
|
+
"Model is not an active coding model: {model}": "No es un modelo de código activo: {model}",
|
|
293
|
+
"Antigravity does not support this model through its Gemini API mode: {model}": "Antigravity no admite este modelo en modo Gemini API: {model}",
|
|
294
|
+
"Could not add Pleum models to OpenCode favorites: {error}": "No se pudieron añadir modelos Pleum a favoritos de OpenCode: {error}",
|
|
295
|
+
"↑/↓ move • enter select • esc quit": "↑/↓ mover • enter seleccionar • esc salir",
|
|
296
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json. Los modelos Gemini compatibles usan PleumRouter.",
|
|
334
297
|
};
|
|
335
|
-
|