pleumrouter 0.5.5 → 0.5.7
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 +2 -2
- package/bin/pleum.mjs +14 -6
- package/package.json +1 -1
- package/src/ansi.mjs +12 -0
- package/src/balance.mjs +2 -6
- package/src/config.mjs +121 -7
- package/src/health.mjs +6 -2
- package/src/launch.mjs +68 -9
- package/src/locales/es.mjs +8 -2
- package/src/locales/ja.mjs +8 -2
- package/src/locales/ko.mjs +9 -3
- package/src/locales/zh.mjs +8 -2
- package/src/login.mjs +60 -19
- package/src/markdown.mjs +5 -2
- package/src/menu.mjs +4 -4
- package/src/models.mjs +27 -13
- package/src/recipes.mjs +20 -2
- package/src/run.mjs +136 -50
- package/src/settings.mjs +11 -7
- package/src/usage.mjs +3 -7
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
|
|
|
33
33
|
| `pleum login [--device]` | 브라우저로 로그인 → CLI가 API 키를 자동 발급·저장(`~/.pleum/config.json`). `--device`는 SSH·CI |
|
|
34
34
|
| `pleum logout` | 저장된 API 키만 삭제(언어·기본 모델 등 나머지 설정은 유지) |
|
|
35
35
|
| `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
|
|
36
|
-
| `pleum run [model] ["프롬프트"]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
|
|
36
|
+
| `pleum run [model] ["프롬프트"] [--attach image]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. 이미지 업로드는 명시적으로 `--attach ./shot.png`(1회 실행) 또는 REPL의 `@./shot.png`를 사용한다. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
|
|
37
37
|
| `pleum models [query] [--json]` | 모델 목록(공개) — provider·컨텍스트·가격(₩/1M) 컬럼, `--json`은 스크립트용 |
|
|
38
38
|
| `pleum models <id> --info` | 모델 상세 — 가격·컨텍스트·provider별 지연/처리량/업타임·주간 사용량·형제 모델 |
|
|
39
39
|
| `pleum top [--period daily\|weekly\|monthly]` | 라우터 전체 모델 리더보드 + 제공자 점유율(공개 통계) |
|
|
@@ -64,7 +64,7 @@ cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
|
|
|
64
64
|
- 응답 후 비용이 현지 통화(한국 ₩)로 표시(스트리밍 마지막 청크 기준)
|
|
65
65
|
- **Ctrl+C**: 생성 중엔 1회로 중단(대화 유지), 대기 중엔 2초 내 2회로 종료
|
|
66
66
|
- 여러 줄 입력: `"""` 한 줄 → 본문 → `"""` 한 줄
|
|
67
|
-
- 이미지:
|
|
67
|
+
- 이미지: 명시적으로 첨부 — REPL에서는 `이 그림 설명해줘 @./shot.png`, 1회 실행에서는 `--attach ./shot.png`. 일반 프롬프트의 `./shot.png` 텍스트는 업로드되지 않는다. 첨부는 일반 파일·이미지 magic bytes·5 MiB/장(총 10 MiB)을 확인하고 심볼릭 링크는 거부한다.
|
|
68
68
|
- `↑` 로 지난 입력 재호출(히스토리는 `~/.pleum/history`에 영속)
|
|
69
69
|
- 색상 끄기: `NO_COLOR=1` (비TTY·파이프에선 자동 무색)
|
|
70
70
|
|
package/bin/pleum.mjs
CHANGED
|
@@ -10,23 +10,24 @@ import { top } from "../src/top.mjs";
|
|
|
10
10
|
import { health } from "../src/health.mjs";
|
|
11
11
|
import { completion } from "../src/completion.mjs";
|
|
12
12
|
import { upgrade } from "../src/upgrade.mjs";
|
|
13
|
-
import { readConfig, writeConfig, clearCredentials } from "../src/config.mjs";
|
|
13
|
+
import { readConfig, writeConfig, clearCredentials, V1 } from "../src/config.mjs";
|
|
14
14
|
import { mainMenu } from "../src/menu.mjs";
|
|
15
15
|
import { lang } from "../src/lang.mjs";
|
|
16
16
|
import { RECIPES } from "../src/recipes.mjs";
|
|
17
17
|
import { t } from "../src/i18n.mjs";
|
|
18
|
+
import { sanitizeTerminalText } from "../src/ansi.mjs";
|
|
18
19
|
|
|
19
20
|
// 명령 구문(왼쪽)은 로케일과 무관 — 설명(오른쪽)만 번역, 폭은 가장 긴 구문 기준으로 계산.
|
|
20
21
|
function buildHelp() {
|
|
21
22
|
const rows = [
|
|
22
23
|
["pleum", t("(no args) interactive launcher menu — keyboard only (TTY only)")],
|
|
23
|
-
["pleum login [--device]", t("Log in via browser
|
|
24
|
+
["pleum login [--device]", t("Log in via browser (OAuth session, no API key)")],
|
|
24
25
|
["", t("--device: browser-less environments (SSH/CI)")],
|
|
25
|
-
["pleum logout", t("
|
|
26
|
+
["pleum logout", t("Revoke session and clear local tokens")],
|
|
26
27
|
["pleum launch [tool] [--model id]", t("Run a coding agent against PleumRouter")],
|
|
27
28
|
["", `(${Object.keys(RECIPES).join(" · ")})`],
|
|
28
29
|
["", t("omit the tool to see the supported list and install status")],
|
|
29
|
-
['pleum run [model] ["prompt"]', t("Terminal chat (REPL) / one-shot / pipe")],
|
|
30
|
+
['pleum run [model] ["prompt"] [--attach image]', t("Terminal chat (REPL) / one-shot / pipe")],
|
|
30
31
|
["", t("omit the model for an arrow-key picker; /help lists commands")],
|
|
31
32
|
["pleum models [query] [--json]", t("Model list (price, context included)")],
|
|
32
33
|
["pleum models <id> --info", t("Model detail (price, perf, per-provider uptime)")],
|
|
@@ -86,7 +87,14 @@ try {
|
|
|
86
87
|
break;
|
|
87
88
|
case "logout": {
|
|
88
89
|
const cfg = readConfig();
|
|
89
|
-
if (cfg)
|
|
90
|
+
if (cfg?.access_token) {
|
|
91
|
+
// best-effort server revoke — 실패해도 로컬 자격증명은 지운다.
|
|
92
|
+
await fetch(`${V1}/auth/logout`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { Authorization: `Bearer ${cfg.access_token}` },
|
|
95
|
+
}).catch(() => {});
|
|
96
|
+
}
|
|
97
|
+
if (cfg) writeConfig(clearCredentials(cfg));
|
|
90
98
|
console.log(`✓ ${t("Logged out")}`);
|
|
91
99
|
break;
|
|
92
100
|
}
|
|
@@ -146,6 +154,6 @@ try {
|
|
|
146
154
|
process.exit(1);
|
|
147
155
|
}
|
|
148
156
|
} catch (e) {
|
|
149
|
-
console.error(`✗ ${e.message}`);
|
|
157
|
+
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
150
158
|
process.exit(1);
|
|
151
159
|
}
|
package/package.json
CHANGED
package/src/ansi.mjs
CHANGED
|
@@ -55,6 +55,18 @@ export function stripAnsi(s) {
|
|
|
55
55
|
.replace(/\x1b\[[0-9;<]*[A-Za-z]/g, "");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// 원격 응답/메타데이터는 신뢰하지 않는다. 터미널 상태를 바꾸는 ESC·C0/C1 제어문자는
|
|
59
|
+
// 제거하되, 텍스트 스트리밍에 필요한 LF와 TAB은 그대로 둔다. CSI/OSC 등의 본문까지
|
|
60
|
+
// 먼저 제거해 ESC만 떼었을 때 남는 `[2J` 같은 노이즈도 남기지 않는다.
|
|
61
|
+
export function sanitizeTerminalText(value) {
|
|
62
|
+
return String(value ?? "")
|
|
63
|
+
.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "") // OSC (title, hyperlink, clipboard…)
|
|
64
|
+
.replace(/\x1b[PX^_][\s\S]*?\x1b\\/g, "") // DCS, SOS, PM, APC
|
|
65
|
+
.replace(/(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g, "") // CSI + C1 CSI
|
|
66
|
+
.replace(/\x1b(?:[()][0-2A-Z0-9]|[=>][0-?]*|.)/g, "") // remaining ESC sequences
|
|
67
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
// OSC 8 클릭 가능한 하이퍼링크. 무색(파이프·NO_COLOR·dumb)이면 텍스트만.
|
|
59
71
|
export function hyperlink(url, text = url, level = style.level) {
|
|
60
72
|
return level ? `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\` : text;
|
package/src/balance.mjs
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { requireKey } from "./config.mjs";
|
|
1
|
+
import { authFetch } from "./config.mjs";
|
|
3
2
|
|
|
4
3
|
export async function balance() {
|
|
5
|
-
const
|
|
6
|
-
const res = await fetch(`${V1}/credits`, {
|
|
7
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
8
|
-
});
|
|
4
|
+
const res = await authFetch("/credits");
|
|
9
5
|
if (!res.ok) {
|
|
10
6
|
const body = await res.text().catch(() => "");
|
|
11
7
|
console.error(`잔액 조회 실패 (HTTP ${res.status})${body ? `: ${body}` : ""}`);
|
package/src/config.mjs
CHANGED
|
@@ -30,23 +30,137 @@ export function writeConfig(cfg) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// 로그인 저장 — 기존 설정(lang·launch_models·last_selection)을 보존한 채 자격증명만 병합.
|
|
33
|
-
// login/loginDevice가 매번 writeConfig({key, base_url})로 전체를 덮어쓰던 버그의 수정.
|
|
34
33
|
export function mergeLoginConfig(existing, patch) {
|
|
35
|
-
|
|
34
|
+
const merged = { ...(existing ?? {}), ...patch };
|
|
35
|
+
// undefined patch values strip the key (e.g. remove legacy `key` after OAuth login).
|
|
36
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
37
|
+
if (v === undefined) delete merged[k];
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
// 로그아웃 — key·base_url
|
|
42
|
+
// 로그아웃 — OAuth 토큰·레거시 key·base_url 제거. lang·launch_models 등 나머지 설정은 보존.
|
|
39
43
|
export function clearCredentials(existing) {
|
|
40
|
-
const {
|
|
44
|
+
const {
|
|
45
|
+
key,
|
|
46
|
+
base_url,
|
|
47
|
+
access_token,
|
|
48
|
+
refresh_token,
|
|
49
|
+
token_expires_at,
|
|
50
|
+
...rest
|
|
51
|
+
} = existing ?? {};
|
|
41
52
|
return rest;
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
/** JWT payload `exp`(epoch sec) — 서명 검증 없이 refresh 트리거 힌트. */
|
|
56
|
+
export function readJwtExp(token) {
|
|
57
|
+
if (typeof token !== "string") return null;
|
|
58
|
+
const parts = token.split(".");
|
|
59
|
+
const segment = parts[1];
|
|
60
|
+
if (!segment) return null;
|
|
61
|
+
try {
|
|
62
|
+
let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
+
const pad = b64.length % 4;
|
|
64
|
+
if (pad) b64 += "=".repeat(4 - pad);
|
|
65
|
+
const json = Buffer.from(b64, "base64").toString("utf8");
|
|
66
|
+
const payload = JSON.parse(json);
|
|
67
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** access가 곧 만료(기본 60s 여유)면 true. */
|
|
74
|
+
export function accessTokenIsStale(accessToken, skewSeconds = 60) {
|
|
75
|
+
const exp = readJwtExp(accessToken);
|
|
76
|
+
if (exp == null) return true;
|
|
77
|
+
return exp * 1000 <= Date.now() + skewSeconds * 1000;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 유효한 access_token을 반환. stale이면 refresh. 레거시 `key`(plm_)만 있으면 재로그인 안내.
|
|
82
|
+
*/
|
|
83
|
+
export async function requireAuth() {
|
|
84
|
+
const cfg = readConfig();
|
|
85
|
+
if (cfg?.key && !cfg?.access_token) {
|
|
86
|
+
console.error(
|
|
87
|
+
t("Legacy API key login is no longer supported. Run:\n pleum login"),
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
if (!cfg?.access_token || !cfg?.refresh_token) {
|
|
92
|
+
console.error(t("Login required. Run this first:\n pleum login"));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (!accessTokenIsStale(cfg.access_token)) {
|
|
96
|
+
return cfg.access_token;
|
|
97
|
+
}
|
|
98
|
+
const refreshed = await refreshAccessToken(cfg);
|
|
99
|
+
if (!refreshed) {
|
|
100
|
+
console.error(t("Session expired. Run:\n pleum login"));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
return refreshed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @deprecated use requireAuth — kept as alias for gradual call-site migration */
|
|
45
107
|
export function requireKey() {
|
|
108
|
+
// sync callers still exist; they must switch to await requireAuth().
|
|
109
|
+
// For sync contexts we only check presence and return access_token (no refresh).
|
|
46
110
|
const cfg = readConfig();
|
|
47
|
-
if (!cfg?.
|
|
111
|
+
if (cfg?.key && !cfg?.access_token) {
|
|
112
|
+
console.error(
|
|
113
|
+
t("Legacy API key login is no longer supported. Run:\n pleum login"),
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (!cfg?.access_token) {
|
|
48
118
|
console.error(t("Login required. Run this first:\n pleum login"));
|
|
49
119
|
process.exit(1);
|
|
50
120
|
}
|
|
51
|
-
return cfg.
|
|
121
|
+
return cfg.access_token;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function refreshAccessToken(cfg = readConfig()) {
|
|
125
|
+
if (!cfg?.refresh_token) return null;
|
|
126
|
+
const base = (cfg.base_url || ROOT).replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`${base}/v1/auth/refresh`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ refresh_token: cfg.refresh_token }),
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) return null;
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
if (!data.access_token || !data.refresh_token) return null;
|
|
136
|
+
const exp = readJwtExp(data.access_token);
|
|
137
|
+
writeConfig(
|
|
138
|
+
mergeLoginConfig(cfg, {
|
|
139
|
+
access_token: data.access_token,
|
|
140
|
+
refresh_token: data.refresh_token,
|
|
141
|
+
token_expires_at: exp ? exp * 1000 : undefined,
|
|
142
|
+
key: undefined,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
return data.access_token;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Bearer 요청 — 401이면 1회 refresh 후 재시도. */
|
|
152
|
+
export async function authFetch(path, init = {}) {
|
|
153
|
+
const token = await requireAuth();
|
|
154
|
+
const url = path.startsWith("http") ? path : `${V1}${path.startsWith("/") ? path : `/${path}`}`;
|
|
155
|
+
const headers = { ...(init.headers || {}), Authorization: `Bearer ${token}` };
|
|
156
|
+
let res = await fetch(url, { ...init, headers });
|
|
157
|
+
if (res.status !== 401) return res;
|
|
158
|
+
|
|
159
|
+
const cfg = readConfig();
|
|
160
|
+
const refreshed = await refreshAccessToken(cfg);
|
|
161
|
+
if (!refreshed) return res;
|
|
162
|
+
return fetch(url, {
|
|
163
|
+
...init,
|
|
164
|
+
headers: { ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
|
|
165
|
+
});
|
|
52
166
|
}
|
package/src/health.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export function renderHealth({ root, health, error, ms, keyPrefix }, styler = st
|
|
|
31
31
|
}
|
|
32
32
|
lines.push(
|
|
33
33
|
keyPrefix
|
|
34
|
-
? ok(`${t("Logged in")} ${styler.dim(`(${t("key")} ${keyPrefix}
|
|
34
|
+
? ok(`${t("Logged in")} ${styler.dim(`(${keyPrefix === "oauth" ? t("OAuth session") : `${t("key")} ${keyPrefix}…`})`)}`)
|
|
35
35
|
: warn(`${t("Not logged in")} ${styler.dim("— pleum login")}`),
|
|
36
36
|
);
|
|
37
37
|
return lines.join("\n");
|
|
@@ -39,7 +39,11 @@ export function renderHealth({ root, health, error, ms, keyPrefix }, styler = st
|
|
|
39
39
|
|
|
40
40
|
export async function health() {
|
|
41
41
|
const cfg = readConfig();
|
|
42
|
-
const keyPrefix = cfg?.
|
|
42
|
+
const keyPrefix = cfg?.access_token
|
|
43
|
+
? "oauth"
|
|
44
|
+
: cfg?.key
|
|
45
|
+
? cfg.key.slice(0, 8)
|
|
46
|
+
: null;
|
|
43
47
|
const t0 = Date.now();
|
|
44
48
|
let data = null;
|
|
45
49
|
let error = null;
|
package/src/launch.mjs
CHANGED
|
@@ -1,19 +1,69 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, delimiter } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { join, delimiter, dirname, resolve } from "node:path";
|
|
5
|
+
import { requireAuth } from "./config.mjs";
|
|
6
6
|
import { RECIPES, MANUAL, codexToml } from "./recipes.mjs";
|
|
7
7
|
import { t } from "./i18n.mjs";
|
|
8
|
+
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
8
9
|
|
|
9
10
|
// PATH에서 실행 파일을 찾는다(설치 여부 표시용). menu.mjs도 사용.
|
|
10
11
|
export function onPath(bin) {
|
|
11
|
-
const exts = process.platform === "win32" ? [".
|
|
12
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
12
13
|
return (process.env.PATH || "")
|
|
13
14
|
.split(delimiter)
|
|
14
15
|
.some((d) => d && exts.some((e) => existsSync(join(d, bin + e))));
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// Windows는 .cmd/.bat 파일을 셸 없이 직접 실행할 수 없다. npm이 만드는 표준 .cmd
|
|
19
|
+
// shim은 실제 Node 엔트리포인트를 가리키므로 그 JS 파일을 현재 Node로 직접 실행한다.
|
|
20
|
+
// 임의의 batch script는 cmd.exe로 넘기지 않고 명시적으로 거부한다. 그래야 --model과
|
|
21
|
+
// `--` 패스스루 인자가 shell 문법으로 재해석될 수 없다.
|
|
22
|
+
function windowsCommandCandidates(bin, path, pathDelimiter) {
|
|
23
|
+
const hasPath = /[\\/]/.test(bin);
|
|
24
|
+
const hasExtension = /\.[^\\/]+$/.test(bin);
|
|
25
|
+
const names = hasExtension ? [bin] : [`${bin}.exe`, `${bin}.cmd`, `${bin}.bat`, bin];
|
|
26
|
+
const dirs = hasPath ? [""] : path.split(pathDelimiter).filter(Boolean);
|
|
27
|
+
return dirs.flatMap((dir) => names.map((name) => (dir ? join(dir, name) : name)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nodeEntryFromWindowsShim(script, { read = readFileSync, exists = existsSync } = {}) {
|
|
31
|
+
let source;
|
|
32
|
+
try {
|
|
33
|
+
source = String(read(script, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// npm cmd-shim uses either %dp0% or %~dp0 followed by the package's JS entry.
|
|
38
|
+
const match = /"(?:%~dp0|%dp0%)[\\/]+([^"\r\n]+?\.(?:[cm]?js))"/i.exec(source);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
const entry = resolve(dirname(script), match[1].replace(/[\\/]+/g, "/"));
|
|
41
|
+
return exists(entry) ? entry : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 플랫폼·filesystem 주입으로 Windows 경로 정책을 Linux/macOS에서도 회귀 테스트한다.
|
|
45
|
+
export function resolveLaunchCommand(bin, args, {
|
|
46
|
+
platform = process.platform,
|
|
47
|
+
path = process.env.PATH || "",
|
|
48
|
+
delimiter: pathDelimiter = delimiter,
|
|
49
|
+
exists = existsSync,
|
|
50
|
+
read = readFileSync,
|
|
51
|
+
node = process.execPath,
|
|
52
|
+
} = {}) {
|
|
53
|
+
if (platform !== "win32") return { command: bin, args };
|
|
54
|
+
const found = windowsCommandCandidates(bin, path, pathDelimiter).find((candidate) => exists(candidate));
|
|
55
|
+
if (!found) return { command: bin, args };
|
|
56
|
+
if (!/\.(?:cmd|bat)$/i.test(found)) return { command: found, args };
|
|
57
|
+
|
|
58
|
+
const entry = nodeEntryFromWindowsShim(found, { read, exists });
|
|
59
|
+
if (!entry) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Windows command script '${bin}' cannot be launched safely. Install an .exe or a standard npm Node shim.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return { command: node, args: [entry, ...args] };
|
|
65
|
+
}
|
|
66
|
+
|
|
17
67
|
// `pleum launch`(도구 미지정) — 지원 도구 + 설치 여부를 안내.
|
|
18
68
|
export function launchHelp() {
|
|
19
69
|
console.log(t("Usage: pleum launch <tool> [--model id] [-- tool args]\n"));
|
|
@@ -32,7 +82,7 @@ export function launchHelp() {
|
|
|
32
82
|
|
|
33
83
|
// exit:false면 process.exit 대신 자식 종료 코드를 반환 — 런처 메뉴의 실행 후 복귀용.
|
|
34
84
|
export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
35
|
-
const key =
|
|
85
|
+
const key = await requireAuth();
|
|
36
86
|
const r = RECIPES[tool];
|
|
37
87
|
|
|
38
88
|
if (!r) {
|
|
@@ -60,13 +110,22 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
60
110
|
|
|
61
111
|
const args = [...r.args(model), ...passthrough];
|
|
62
112
|
if (r.note) console.log(`ℹ ${t(r.note)}`);
|
|
63
|
-
console.log(`▶ ${t("Running {label}: {cmd}", { label: r.label, cmd: `${r.bin} ${args.join(" ")}` })}\n`);
|
|
113
|
+
console.log(`▶ ${t("Running {label}: {cmd}", { label: r.label, cmd: sanitizeTerminalText(`${r.bin} ${args.join(" ")}`) })}\n`);
|
|
114
|
+
|
|
115
|
+
let command;
|
|
116
|
+
try {
|
|
117
|
+
command = resolveLaunchCommand(r.bin, args);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
120
|
+
if (exit) process.exit(1);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
64
123
|
|
|
65
124
|
return await new Promise((resolve) => {
|
|
66
|
-
const child = spawn(
|
|
125
|
+
const child = spawn(command.command, command.args, {
|
|
67
126
|
env,
|
|
68
127
|
stdio: "inherit",
|
|
69
|
-
shell:
|
|
128
|
+
shell: false,
|
|
70
129
|
});
|
|
71
130
|
child.on("error", (e) => {
|
|
72
131
|
if (e.code === "ENOENT") {
|
|
@@ -74,7 +133,7 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
74
133
|
if (exit) process.exit(127);
|
|
75
134
|
return resolve(127);
|
|
76
135
|
}
|
|
77
|
-
console.error(`✗ ${e.message}`);
|
|
136
|
+
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
78
137
|
if (exit) process.exit(1);
|
|
79
138
|
resolve(1);
|
|
80
139
|
});
|
package/src/locales/es.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const ES = {
|
|
|
12
12
|
"Account": "Cuenta",
|
|
13
13
|
"All models — price and context included": "Todos los modelos — precio y contexto incluidos",
|
|
14
14
|
"Anthropic's coding agent — supports subagents": "Agente de codificación de Anthropic — admite subagentes",
|
|
15
|
-
"Attaching and uploading {n} local image(s)
|
|
15
|
+
"Attaching and uploading {n} explicit local image(s).": "Adjuntando y subiendo {n} imagen(es) local(es) seleccionada(s) explícitamente.",
|
|
16
16
|
"Auto-run:": "Ejecución automática:",
|
|
17
17
|
"Autonomous dev agent": "Agente de desarrollo autónomo",
|
|
18
18
|
"Multi-provider terminal coding agent": "Agente de codificación de terminal multi-proveedor",
|
|
@@ -32,7 +32,7 @@ export const ES = {
|
|
|
32
32
|
"Choose a number, or search (Enter to cancel): ": "Elige un número, o busca (Enter para cancelar): ",
|
|
33
33
|
"Choose a shell": "Elegir una shell",
|
|
34
34
|
"Code": "Código",
|
|
35
|
-
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images:
|
|
35
|
+
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images: attach explicitly with @./shot.png (or --attach ./shot.png for one-shot commands)": "Comandos:\n /help, /? esta ayuda\n /clear reinicia el contexto de la conversación (conserva el system prompt)\n /system [text] define el system prompt (sin argumento muestra el valor actual)\n /model [id] cambia de modelo durante la sesión (sin argumento = selector con flechas)\n /models [query] explora la lista de modelos\n /save [name] guarda la sesión actual (por defecto: last)\n /load [name] carga una sesión guardada\n /verbose activa/desactiva estadísticas de tokens/velocidad\n /exit, /quit salir\n Entrada multilínea: una línea con \"\"\" → cuerpo → una línea con \"\"\"\n Imágenes: adjunta explícitamente con @./shot.png (o --attach ./shot.png para comandos de una sola ejecución)",
|
|
36
36
|
"Context cleared": "Contexto borrado",
|
|
37
37
|
"Ctrl+C again to quit (or /exit)": "Ctrl+C de nuevo para salir (o /exit)",
|
|
38
38
|
"Current language: {name}": "Idioma actual: {name}",
|
|
@@ -80,6 +80,12 @@ export const ES = {
|
|
|
80
80
|
"Log out? [Y/n] ": "¿Cerrar sesión? [Y/n] ",
|
|
81
81
|
"Logged in": "Sesión iniciada",
|
|
82
82
|
"Logged in. Saved the key to ~/.pleum/config.json.": "Sesión iniciada. Clave guardada en ~/.pleum/config.json.",
|
|
83
|
+
"Logged in. Saved the session to ~/.pleum/config.json.": "Sesión iniciada. Sesión guardada en ~/.pleum/config.json.",
|
|
84
|
+
"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",
|
|
85
|
+
"Session expired. Run:\n pleum login": "La sesión ha caducado. Ejecuta:\n pleum login",
|
|
86
|
+
"OAuth session": "Sesión OAuth",
|
|
87
|
+
"Log in via browser (OAuth session, no API key)": "Iniciar sesión en el navegador (sesión OAuth, sin clave API)",
|
|
88
|
+
"Revoke session and clear local tokens": "Revocar sesión y borrar tokens locales",
|
|
83
89
|
"Logged out": "Sesión cerrada",
|
|
84
90
|
"Login required. Run this first:\n pleum login": "Se requiere iniciar sesión. Ejecuta esto primero:\n pleum login",
|
|
85
91
|
"Login timed out (2 minutes)": "Tiempo de espera agotado para iniciar sesión (2 minutos)",
|
package/src/locales/ja.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const JA = {
|
|
|
12
12
|
"Account": "アカウント",
|
|
13
13
|
"All models — price and context included": "全モデル — 価格・コンテキスト込み",
|
|
14
14
|
"Anthropic's coding agent — supports subagents": "Anthropic のコーディングエージェント — サブエージェント対応",
|
|
15
|
-
"Attaching and uploading {n} local image(s)
|
|
15
|
+
"Attaching and uploading {n} explicit local image(s).": "明示的に選択したローカル画像 {n} 件を添付・アップロード中。",
|
|
16
16
|
"Auto-run:": "自動実行:",
|
|
17
17
|
"Autonomous dev agent": "自律開発エージェント",
|
|
18
18
|
"Multi-provider terminal coding agent": "マルチプロバイダー対応のターミナルコーディングエージェント",
|
|
@@ -32,7 +32,7 @@ export const JA = {
|
|
|
32
32
|
"Choose a number, or search (Enter to cancel): ": "番号を選択、または検索してください (Enter でキャンセル): ",
|
|
33
33
|
"Choose a shell": "シェルを選択",
|
|
34
34
|
"Code": "コード",
|
|
35
|
-
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images:
|
|
35
|
+
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images: attach explicitly with @./shot.png (or --attach ./shot.png for one-shot commands)": "コマンド:\n /help, /? このヘルプ\n /clear 会話コンテキストをリセット (システムプロンプトは保持)\n /system [text] システムプロンプトを設定 (引数なしで現在値を表示)\n /model [id] セッション中にモデルを切り替え (引数なし = 矢印キーで選択)\n /models [query] モデル一覧を閲覧\n /save [name] 現在のセッションを保存 (デフォルト: last)\n /load [name] 保存したセッションを読み込み\n /verbose トークン/速度統計の表示切替\n /exit, /quit 終了\n 複数行入力: \"\"\" の行 → 本文 → \"\"\" の行\n 画像: @./shot.png で明示的に添付 (1回実行では --attach ./shot.png)",
|
|
36
36
|
"Context cleared": "コンテキストをクリアしました",
|
|
37
37
|
"Ctrl+C again to quit (or /exit)": "もう一度 Ctrl+C で終了 (または /exit)",
|
|
38
38
|
"Current language: {name}": "現在の言語: {name}",
|
|
@@ -80,6 +80,12 @@ export const JA = {
|
|
|
80
80
|
"Log out? [Y/n] ": "ログアウトしますか? [Y/n] ",
|
|
81
81
|
"Logged in": "ログイン済み",
|
|
82
82
|
"Logged in. Saved the key to ~/.pleum/config.json.": "ログインしました。キーを ~/.pleum/config.json に保存しました。",
|
|
83
|
+
"Logged in. Saved the session to ~/.pleum/config.json.": "ログインしました。セッションを ~/.pleum/config.json に保存しました。",
|
|
84
|
+
"Legacy API key login is no longer supported. Run:\n pleum login": "レガシー API キーログインはサポートされなくなりました。実行してください:\n pleum login",
|
|
85
|
+
"Session expired. Run:\n pleum login": "セッションの有効期限が切れました。実行してください:\n pleum login",
|
|
86
|
+
"OAuth session": "OAuth セッション",
|
|
87
|
+
"Log in via browser (OAuth session, no API key)": "ブラウザでログイン(OAuth セッション、API キーなし)",
|
|
88
|
+
"Revoke session and clear local tokens": "セッションを取り消し、ローカル トークンを削除",
|
|
83
89
|
"Logged out": "ログアウトしました",
|
|
84
90
|
"Login required. Run this first:\n pleum login": "ログインが必要です。まず以下を実行してください:\n pleum login",
|
|
85
91
|
"Login timed out (2 minutes)": "ログインがタイムアウトしました (2分)",
|
package/src/locales/ko.mjs
CHANGED
|
@@ -26,6 +26,12 @@ export const KO = {
|
|
|
26
26
|
"브라우저에서 로그인하세요. 자동으로 안 열리면 아래 URL을 여세요:",
|
|
27
27
|
"Login timed out (2 minutes)": "로그인 시간 초과(2분)",
|
|
28
28
|
"Logged in. Saved the key to ~/.pleum/config.json.": "로그인 완료. 키를 ~/.pleum/config.json 에 저장했습니다.",
|
|
29
|
+
"Logged in. Saved the session to ~/.pleum/config.json.": "로그인 완료. 세션을 ~/.pleum/config.json 에 저장했습니다.",
|
|
30
|
+
"Legacy API key login is no longer supported. Run:\n pleum login": "레거시 API 키 로그인은 더 이상 지원되지 않습니다. 실행하세요:\n pleum login",
|
|
31
|
+
"Session expired. Run:\n pleum login": "세션이 만료되었습니다. 실행하세요:\n pleum login",
|
|
32
|
+
"OAuth session": "OAuth 세션",
|
|
33
|
+
"Log in via browser (OAuth session, no API key)": "브라우저로 로그인 (OAuth 세션, API 키 없음)",
|
|
34
|
+
"Revoke session and clear local tokens": "세션 폐기 및 로컬 토큰 삭제",
|
|
29
35
|
"Failed to issue a device code (server responded {status})": "device code 발급 실패 (서버 응답 {status})",
|
|
30
36
|
"Code": "코드",
|
|
31
37
|
"Log in at the URL above, then enter the code (within {min} min).": "위 URL에서 로그인 후 코드를 입력하세요 ({min}분 내).",
|
|
@@ -176,7 +182,7 @@ export const KO = {
|
|
|
176
182
|
[`Specify a model: pleum run <model> ["prompt"] (model selection isn't available on non-interactive stdin)`]:
|
|
177
183
|
`모델을 지정하세요: pleum run <model> ["프롬프트"] (비대화형 stdin에선 모델 선택 불가)`,
|
|
178
184
|
'Usage: pleum run <model> ["prompt"]\nModel list: pleum models': '사용법: pleum run <model> ["프롬프트"]\n모델 목록: pleum models',
|
|
179
|
-
"Attaching and uploading {n} local image(s)
|
|
185
|
+
"Attaching and uploading {n} explicit local image(s).": "명시적으로 선택한 로컬 이미지 {n}개를 첨부해 업로드합니다.",
|
|
180
186
|
"Goodbye.": "안녕히 가세요.",
|
|
181
187
|
"Ctrl+C again to quit (or /exit)": "한 번 더 Ctrl+C 하면 종료 (/exit 도 가능)",
|
|
182
188
|
'/help for commands · /exit to quit · wrap multi-line input in """ · Ctrl+C to stop generation':
|
|
@@ -192,7 +198,7 @@ export const KO = {
|
|
|
192
198
|
/verbose toggle token/speed stats
|
|
193
199
|
/exit, /quit quit
|
|
194
200
|
Multi-line input: a line with """ → body → a line with """
|
|
195
|
-
Images:
|
|
201
|
+
Images: attach explicitly with @./shot.png (or --attach ./shot.png for one-shot commands)`]: `명령:
|
|
196
202
|
/help, /? 이 도움말
|
|
197
203
|
/clear 대화 컨텍스트 초기화(시스템 프롬프트는 유지)
|
|
198
204
|
/system [텍스트] 시스템 프롬프트 설정(인자 없으면 현재값 표시)
|
|
@@ -203,7 +209,7 @@ export const KO = {
|
|
|
203
209
|
/verbose 토큰·속도 통계 표시 토글
|
|
204
210
|
/exit, /quit 종료
|
|
205
211
|
여러 줄 입력: """ 한 줄 → 본문 → """ 한 줄
|
|
206
|
-
이미지:
|
|
212
|
+
이미지: @./shot.png로 명시 첨부(1회 실행은 --attach ./shot.png)`,
|
|
207
213
|
"Context cleared": "컨텍스트 초기화",
|
|
208
214
|
"System prompt set": "시스템 프롬프트 설정",
|
|
209
215
|
"Current system prompt:": "현재 시스템 프롬프트:",
|
package/src/locales/zh.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const ZH = {
|
|
|
12
12
|
"Account": "账户",
|
|
13
13
|
"All models — price and context included": "所有模型 — 含价格与上下文",
|
|
14
14
|
"Anthropic's coding agent — supports subagents": "Anthropic 的编程代理 — 支持子代理",
|
|
15
|
-
"Attaching and uploading {n} local image(s)
|
|
15
|
+
"Attaching and uploading {n} explicit local image(s).": "正在附加并上传明确选择的 {n} 张本地图片。",
|
|
16
16
|
"Auto-run:": "自动运行:",
|
|
17
17
|
"Autonomous dev agent": "自主开发代理",
|
|
18
18
|
"Multi-provider terminal coding agent": "多提供商终端编程代理",
|
|
@@ -32,7 +32,7 @@ export const ZH = {
|
|
|
32
32
|
"Choose a number, or search (Enter to cancel): ": "选择编号,或搜索(Enter 取消): ",
|
|
33
33
|
"Choose a shell": "选择 shell",
|
|
34
34
|
"Code": "代码",
|
|
35
|
-
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images:
|
|
35
|
+
"Commands:\n /help, /? this help\n /clear reset the conversation context (keeps the system prompt)\n /system [text] set the system prompt (no arg shows the current value)\n /model [id] switch models mid-session (no arg = arrow-key picker)\n /models [query] browse the model list\n /save [name] save the current session (default: last)\n /load [name] load a saved session\n /verbose toggle token/speed stats\n /exit, /quit quit\n Multi-line input: a line with \"\"\" → body → a line with \"\"\"\n Images: attach explicitly with @./shot.png (or --attach ./shot.png for one-shot commands)": "命令:\n /help, /? 本帮助\n /clear 重置对话上下文(保留系统提示词)\n /system [text] 设置系统提示词(不带参数则显示当前值)\n /model [id] 会话中途切换模型(不带参数 = 方向键选择器)\n /models [query] 浏览模型列表\n /save [name] 保存当前会话(默认:last)\n /load [name] 加载已保存的会话\n /verbose 切换 token/速度统计\n /exit, /quit 退出\n 多行输入:一行 \"\"\" → 正文 → 一行 \"\"\"\n 图片:使用 @./shot.png 显式附加(单次命令使用 --attach ./shot.png)",
|
|
36
36
|
"Context cleared": "上下文已清除",
|
|
37
37
|
"Ctrl+C again to quit (or /exit)": "再次按 Ctrl+C 退出(或 /exit)",
|
|
38
38
|
"Current language: {name}": "当前语言:{name}",
|
|
@@ -80,6 +80,12 @@ export const ZH = {
|
|
|
80
80
|
"Log out? [Y/n] ": "要登出吗? [Y/n] ",
|
|
81
81
|
"Logged in": "已登录",
|
|
82
82
|
"Logged in. Saved the key to ~/.pleum/config.json.": "已登录。密钥已保存至 ~/.pleum/config.json。",
|
|
83
|
+
"Logged in. Saved the session to ~/.pleum/config.json.": "已登录。会话已保存至 ~/.pleum/config.json。",
|
|
84
|
+
"Legacy API key login is no longer supported. Run:\n pleum login": "不再支持旧版 API 密钥登录。请运行:\n pleum login",
|
|
85
|
+
"Session expired. Run:\n pleum login": "会话已过期。请运行:\n pleum login",
|
|
86
|
+
"OAuth session": "OAuth 会话",
|
|
87
|
+
"Log in via browser (OAuth session, no API key)": "通过浏览器登录(OAuth 会话,无 API 密钥)",
|
|
88
|
+
"Revoke session and clear local tokens": "撤销会话并清除本地令牌",
|
|
83
89
|
"Logged out": "已登出",
|
|
84
90
|
"Login required. Run this first:\n pleum login": "需要登录。请先运行:\n pleum login",
|
|
85
91
|
"Login timed out (2 minutes)": "登录超时(2 分钟)",
|