pleumrouter 0.5.23 → 0.5.25
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 +1 -1
- package/package.json +1 -1
- package/src/config.mjs +69 -22
- package/src/health.mjs +2 -2
- package/src/locales/es.mjs +5 -0
- package/src/locales/ja.mjs +5 -0
- package/src/locales/ko.mjs +5 -0
- package/src/locales/zh.mjs +5 -0
- package/src/menu.mjs +7 -1
- package/src/recipes.mjs +86 -2
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ npm link # 로컬에서 pleum 명령 노출
|
|
|
97
97
|
|
|
98
98
|
1. `npm version patch` (또는 minor/major)로 `package.json` bump 후 커밋.
|
|
99
99
|
2. `git tag cli-v$(node -p "require('./package.json').version")` → `git push origin HEAD --tags`.
|
|
100
|
-
3. 워크플로가 `secrets.NPM_TOKEN`으로 `npm publish
|
|
100
|
+
3. 워크플로가 `secrets.NPM_TOKEN`으로 `npm publish` 실행. (private GitHub repo라 `--provenance` 불가)
|
|
101
101
|
|
|
102
102
|
태그 규칙: `cli-v{semver}` (예: `cli-v0.5.15`).
|
|
103
103
|
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -62,15 +62,9 @@ export function clearCredentials(existing) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
* (아래 requireAuth 참조) 미로그인으로 취급한다 — 그래야 런처가 자동으로 재로그인을 태운다.
|
|
65
|
+
* JWT payload 파싱(서명 검증 없는 디코드) — iat, exp 등의 클레임 추출용.
|
|
67
66
|
*/
|
|
68
|
-
export function
|
|
69
|
-
return Boolean(cfg?.access_token);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** JWT payload `exp`(epoch sec) — 서명 검증 없이 refresh 트리거 힌트. */
|
|
73
|
-
export function readJwtExp(token) {
|
|
67
|
+
export function readJwtPayload(token) {
|
|
74
68
|
if (typeof token !== "string") return null;
|
|
75
69
|
const parts = token.split(".");
|
|
76
70
|
const segment = parts[1];
|
|
@@ -80,14 +74,32 @@ export function readJwtExp(token) {
|
|
|
80
74
|
const pad = b64.length % 4;
|
|
81
75
|
if (pad) b64 += "=".repeat(4 - pad);
|
|
82
76
|
const json = Buffer.from(b64, "base64").toString("utf8");
|
|
83
|
-
|
|
84
|
-
return typeof payload.exp === "number" ? payload.exp : null;
|
|
77
|
+
return JSON.parse(json);
|
|
85
78
|
} catch {
|
|
86
79
|
return null;
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
82
|
|
|
90
|
-
/**
|
|
83
|
+
/** JWT payload `exp`(epoch sec) */
|
|
84
|
+
export function readJwtExp(token) {
|
|
85
|
+
const payload = readJwtPayload(token);
|
|
86
|
+
return typeof payload?.exp === "number" ? payload.exp : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** JWT payload `iat`(epoch sec) */
|
|
90
|
+
export function readJwtIat(token) {
|
|
91
|
+
const payload = readJwtPayload(token);
|
|
92
|
+
return typeof payload?.iat === "number" ? payload.iat : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 토큰 만료 여부 판정 (skewSeconds 여유 포함, unparseable은 false 반환) */
|
|
96
|
+
export function isTokenExpired(token, skewSeconds = 0) {
|
|
97
|
+
const exp = readJwtExp(token);
|
|
98
|
+
if (exp == null) return false;
|
|
99
|
+
return exp * 1000 <= Date.now() + skewSeconds * 1000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** access가 곧 만료(기본 60s 여유)면 true. unparseable은 만료로 간주. */
|
|
91
103
|
export function accessTokenIsStale(accessToken, skewSeconds = 60) {
|
|
92
104
|
const exp = readJwtExp(accessToken);
|
|
93
105
|
if (exp == null) return true;
|
|
@@ -95,29 +107,59 @@ export function accessTokenIsStale(accessToken, skewSeconds = 60) {
|
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
/**
|
|
98
|
-
*
|
|
110
|
+
* 사용 가능한 세션 보유 여부.
|
|
111
|
+
* 1) access_token 또는 refresh_token이 없으면 미로그인.
|
|
112
|
+
* 2) refresh_token이 이미 7일 만료되었으면 미로그인.
|
|
113
|
+
* 3) 레거시 `key`(plm_)만 있는 상태는 requireAuth()가 거부하므로 미로그인.
|
|
114
|
+
*/
|
|
115
|
+
export function isLoggedIn(cfg = readConfig()) {
|
|
116
|
+
if (!cfg?.access_token || !cfg?.refresh_token) return false;
|
|
117
|
+
if (isTokenExpired(cfg.refresh_token)) return false;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 7일 슬라이딩 세션 갱신 (1시간 스로틀링).
|
|
123
|
+
* CLI 접속 시 마지막 갱신(iat)으로부터 minIntervalSeconds(기본 3600초=1시간) 이상 지났거나
|
|
124
|
+
* access_token이 곧 만료(stale)되면 /v1/auth/refresh를 호출해 만료일을 현재+7일로 연장한다.
|
|
125
|
+
*/
|
|
126
|
+
export async function touchSession(cfg = readConfig(), { minIntervalSeconds = 3600 } = {}) {
|
|
127
|
+
if (!isLoggedIn(cfg)) return null;
|
|
128
|
+
const iat = readJwtIat(cfg.refresh_token) ?? readJwtIat(cfg.access_token);
|
|
129
|
+
const ageSeconds = iat ? Math.max(0, Math.floor(Date.now() / 1000 - iat)) : Infinity;
|
|
130
|
+
const isStale = accessTokenIsStale(cfg.access_token, 300);
|
|
131
|
+
if (!isStale && ageSeconds < minIntervalSeconds) {
|
|
132
|
+
return cfg.access_token;
|
|
133
|
+
}
|
|
134
|
+
return await refreshAccessToken(cfg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 유효한 access_token을 반환. stale이거나 1시간 경과 시 refresh하여 슬라이딩 윈도우 유지.
|
|
139
|
+
* 레거시 `key`(plm_)만 있으면 재로그인 안내.
|
|
99
140
|
*/
|
|
100
141
|
export async function requireAuth() {
|
|
101
|
-
|
|
142
|
+
let cfg = readConfig();
|
|
102
143
|
if (cfg?.key && !cfg?.access_token) {
|
|
103
144
|
console.error(
|
|
104
145
|
t("Legacy API key login is no longer supported. Run:\n pleum login"),
|
|
105
146
|
);
|
|
106
147
|
process.exit(1);
|
|
107
148
|
}
|
|
108
|
-
if (!cfg
|
|
149
|
+
if (!isLoggedIn(cfg)) {
|
|
109
150
|
console.error(t("Login required. Run this first:\n pleum login"));
|
|
110
151
|
process.exit(1);
|
|
111
152
|
}
|
|
112
|
-
|
|
153
|
+
const token = await touchSession(cfg);
|
|
154
|
+
if (token) return token;
|
|
155
|
+
|
|
156
|
+
cfg = readConfig();
|
|
157
|
+
if (cfg?.access_token && !accessTokenIsStale(cfg.access_token, 0)) {
|
|
113
158
|
return cfg.access_token;
|
|
114
159
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
return refreshed;
|
|
160
|
+
|
|
161
|
+
console.error(t("Session expired. Run:\n pleum login"));
|
|
162
|
+
process.exit(1);
|
|
121
163
|
}
|
|
122
164
|
|
|
123
165
|
export async function refreshAccessToken(cfg = readConfig()) {
|
|
@@ -129,7 +171,12 @@ export async function refreshAccessToken(cfg = readConfig()) {
|
|
|
129
171
|
headers: { "Content-Type": "application/json" },
|
|
130
172
|
body: JSON.stringify({ refresh_token: cfg.refresh_token }),
|
|
131
173
|
});
|
|
132
|
-
if (!res.ok)
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
if (res.status === 401 || res.status === 403) {
|
|
176
|
+
writeConfig(clearCredentials(cfg));
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
133
180
|
const data = await res.json();
|
|
134
181
|
if (!data.access_token || !data.refresh_token) return null;
|
|
135
182
|
const exp = readJwtExp(data.access_token);
|
package/src/health.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `pleum health` — 게이트웨이 연결성 + 로그인 상태 한눈에.
|
|
2
2
|
// 셀프호스트/스테이징(PLEUM_BASE_URL) 점검·고객지원 트리아지용. 공개 /v1/health 재사용.
|
|
3
3
|
|
|
4
|
-
import { ROOT, V1, readConfig } from "./config.mjs";
|
|
4
|
+
import { ROOT, V1, readConfig, isLoggedIn } from "./config.mjs";
|
|
5
5
|
import { style, divider } from "./ansi.mjs";
|
|
6
6
|
import { t } from "./i18n.mjs";
|
|
7
7
|
|
|
@@ -39,7 +39,7 @@ 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 = isLoggedIn(cfg)
|
|
43
43
|
? "oauth"
|
|
44
44
|
: cfg?.key
|
|
45
45
|
? cfg.key.slice(0, 8)
|
package/src/locales/es.mjs
CHANGED
|
@@ -24,6 +24,11 @@ export const ES = {
|
|
|
24
24
|
"Google Antigravity CLI (agy)": "CLI de Google Antigravity (agy)",
|
|
25
25
|
"Pi coding agent (picode is a Pi package)": "Agente Pi (picode es un paquete de Pi)",
|
|
26
26
|
"Charmbracelet terminal coding agent": "Agente de terminal Charmbracelet",
|
|
27
|
+
"MiniMax's autonomous coding agent": "Agente de codificación autónomo de MiniMax",
|
|
28
|
+
"Xiaomi's terminal coding agent with persistent memory": "Agente de codificación de terminal de Xiaomi con memoria persistente",
|
|
29
|
+
"Autonomous personal AI coding agent": "Agente personal autónomo de codificación por IA",
|
|
30
|
+
"Terminal AI coding engine for complex tasks": "Motor de codificación de IA en terminal para tareas complejas",
|
|
31
|
+
"Local code execution & pair programmer": "Ejecución de código local y programador en pareja",
|
|
27
32
|
"Available: {list}": "Disponibles: {list}",
|
|
28
33
|
"Balance": "Saldo",
|
|
29
34
|
"Balance is below the auto-charge threshold ({threshold} ₩).": "El saldo está por debajo del umbral de recarga automática ({threshold} ₩).",
|
package/src/locales/ja.mjs
CHANGED
|
@@ -24,6 +24,11 @@ export const JA = {
|
|
|
24
24
|
"Google Antigravity CLI (agy)": "Google Antigravity CLI (agy)",
|
|
25
25
|
"Pi coding agent (picode is a Pi package)": "Pi コーディングエージェント (picode は Pi パッケージ)",
|
|
26
26
|
"Charmbracelet terminal coding agent": "Charmbracelet ターミナルコーディングエージェント",
|
|
27
|
+
"MiniMax's autonomous coding agent": "MiniMax の自律型コーディングエージェント",
|
|
28
|
+
"Xiaomi's terminal coding agent with persistent memory": "永続メモリを搭載した Xiaomi のターミナルコーディングエージェント",
|
|
29
|
+
"Autonomous personal AI coding agent": "個人向け自律型 AI コーディングエージェント",
|
|
30
|
+
"Terminal AI coding engine for complex tasks": "複雑なタスク向けターミナル AI コーディングエンジン",
|
|
31
|
+
"Local code execution & pair programmer": "ローカルコード実行&ペアプログラマー",
|
|
27
32
|
"Available: {list}": "利用可能: {list}",
|
|
28
33
|
"Balance": "残高",
|
|
29
34
|
"Balance is below the auto-charge threshold ({threshold} ₩).": "残高が自動チャージのしきい値 ({threshold} ₩) を下回っています。",
|
package/src/locales/ko.mjs
CHANGED
|
@@ -157,6 +157,11 @@ export const KO = {
|
|
|
157
157
|
"Google Antigravity CLI (agy)": "Google Antigravity CLI (agy)",
|
|
158
158
|
"Pi coding agent (picode is a Pi package)": "Pi 코딩 에이전트 (picode는 Pi 패키지)",
|
|
159
159
|
"Charmbracelet terminal coding agent": "Charmbracelet 터미널 코딩 에이전트",
|
|
160
|
+
"MiniMax's autonomous coding agent": "MiniMax의 자율 코딩 에이전트",
|
|
161
|
+
"Xiaomi's terminal coding agent with persistent memory": "영구 메모리를 탑재한 Xiaomi의 터미널 코딩 에이전트",
|
|
162
|
+
"Autonomous personal AI coding agent": "개인용 자율 AI 코딩 에이전트",
|
|
163
|
+
"Terminal AI coding engine for complex tasks": "복잡한 작업을 위한 터미널 AI 코딩 엔진",
|
|
164
|
+
"Local code execution & pair programmer": "로컬 코드 실행 및 페어 프로그래머",
|
|
160
165
|
"Log in": "로그인",
|
|
161
166
|
"Log in via browser and save the API key": "브라우저로 로그인하고 API 키를 저장합니다",
|
|
162
167
|
"Chat with a model": "모델과 채팅",
|
package/src/locales/zh.mjs
CHANGED
|
@@ -24,6 +24,11 @@ export const ZH = {
|
|
|
24
24
|
"Google Antigravity CLI (agy)": "Google Antigravity CLI (agy)",
|
|
25
25
|
"Pi coding agent (picode is a Pi package)": "Pi 编程代理(picode 是 Pi 包)",
|
|
26
26
|
"Charmbracelet terminal coding agent": "Charmbracelet 终端编程代理",
|
|
27
|
+
"MiniMax's autonomous coding agent": "MiniMax 自主编程代理",
|
|
28
|
+
"Xiaomi's terminal coding agent with persistent memory": "带持久记忆的 Xiaomi 终端编程代理",
|
|
29
|
+
"Autonomous personal AI coding agent": "个人自主 AI 编程代理",
|
|
30
|
+
"Terminal AI coding engine for complex tasks": "用于复杂任务的终端 AI 编程引擎",
|
|
31
|
+
"Local code execution & pair programmer": "本地代码执行与结对编程器",
|
|
27
32
|
"Available: {list}": "可用:{list}",
|
|
28
33
|
"Balance": "余额",
|
|
29
34
|
"Balance is below the auto-charge threshold ({threshold} ₩).": "余额低于自动充值阈值({threshold} ₩)。",
|
package/src/menu.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
14
|
import { style, displayWidth, terminalWidth, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
15
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig, isLoggedIn } from "./config.mjs";
|
|
16
|
+
import { readConfig, writeConfig, isLoggedIn, touchSession } from "./config.mjs";
|
|
17
17
|
import { RECIPES, MANUAL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
19
|
import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
|
|
@@ -44,6 +44,11 @@ const AGENT_DESC = {
|
|
|
44
44
|
agy: "Google Antigravity CLI (agy)",
|
|
45
45
|
pi: "Pi coding agent (picode is a Pi package)",
|
|
46
46
|
crush: "Charmbracelet terminal coding agent",
|
|
47
|
+
minimax: "MiniMax's autonomous coding agent",
|
|
48
|
+
mimo: "Xiaomi's terminal coding agent with persistent memory",
|
|
49
|
+
openclaw: "Autonomous personal AI coding agent",
|
|
50
|
+
plandex: "Terminal AI coding engine for complex tasks",
|
|
51
|
+
interpreter: "Local code execution & pair programmer",
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
|
|
@@ -298,6 +303,7 @@ export async function mainMenu() {
|
|
|
298
303
|
// login()의 실패(취소·2분 타임아웃·state 불일치)는 그대로 던져 bin/pleum.mjs의
|
|
299
304
|
// 최상위 catch가 `✗ <메시지>` + exit 1로 처리하게 둔다(메뉴를 띄우지 않는다).
|
|
300
305
|
if (!isLoggedIn()) await login();
|
|
306
|
+
else await touchSession().catch(() => {});
|
|
301
307
|
|
|
302
308
|
for (;;) {
|
|
303
309
|
const picked = await menuSelect();
|
package/src/recipes.mjs
CHANGED
|
@@ -13,8 +13,7 @@ import { ROOT, V1 } from "./config.mjs";
|
|
|
13
13
|
//
|
|
14
14
|
// 커스텀 엔드포인트가 없어 PleumRouter로 라우팅할 수 없으면 등록하지 않는다:
|
|
15
15
|
// Cursor CLI · Amp · Factory Droid · Kiro(공식 BYOK 없음, aws/kiro#9367) ·
|
|
16
|
-
// Cline CLI(cline auth가 사용자 cred를 디스크에 고정)
|
|
17
|
-
// OpenClaw(세션형 코딩 CLI가 아님).
|
|
16
|
+
// Cline CLI(cline auth가 사용자 cred를 디스크에 고정).
|
|
18
17
|
export const RECIPES = {
|
|
19
18
|
claude: {
|
|
20
19
|
label: "Claude Code",
|
|
@@ -235,6 +234,84 @@ export const RECIPES = {
|
|
|
235
234
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
236
235
|
args: () => [],
|
|
237
236
|
},
|
|
237
|
+
minimax: {
|
|
238
|
+
label: "MiniMax Code",
|
|
239
|
+
bin: "mcode",
|
|
240
|
+
type: "env",
|
|
241
|
+
install: "npm install -g @minimax-ai/code@latest",
|
|
242
|
+
attribution: {
|
|
243
|
+
title: "MiniMax Code",
|
|
244
|
+
referer: "https://github.com/MiniMax-AI/minimax-code",
|
|
245
|
+
},
|
|
246
|
+
env: (key, model) => ({
|
|
247
|
+
OPENAI_BASE_URL: V1,
|
|
248
|
+
OPENAI_API_KEY: key,
|
|
249
|
+
...(model ? { OPENAI_MODEL: model } : {}),
|
|
250
|
+
}),
|
|
251
|
+
args: (m) => (m ? ["--model", m] : []),
|
|
252
|
+
},
|
|
253
|
+
mimo: {
|
|
254
|
+
label: "MiMo Code",
|
|
255
|
+
bin: "mimo",
|
|
256
|
+
type: "env",
|
|
257
|
+
install: "npm install -g @mimo-ai/cli",
|
|
258
|
+
attribution: {
|
|
259
|
+
title: "MiMo Code",
|
|
260
|
+
referer: "https://github.com/XiaomiMiMo/MiMo-Code",
|
|
261
|
+
},
|
|
262
|
+
env: (key, model) => ({
|
|
263
|
+
OPENAI_BASE_URL: V1,
|
|
264
|
+
OPENAI_API_KEY: key,
|
|
265
|
+
...(model ? { OPENAI_MODEL: model } : {}),
|
|
266
|
+
}),
|
|
267
|
+
args: (m) => (m ? ["--model", m] : []),
|
|
268
|
+
},
|
|
269
|
+
openclaw: {
|
|
270
|
+
label: "OpenClaw",
|
|
271
|
+
bin: "openclaw",
|
|
272
|
+
type: "env",
|
|
273
|
+
install: "npm install -g openclaw@latest",
|
|
274
|
+
attribution: {
|
|
275
|
+
title: "OpenClaw",
|
|
276
|
+
referer: "https://github.com/openclaw/openclaw",
|
|
277
|
+
},
|
|
278
|
+
env: (key, model) => ({
|
|
279
|
+
OPENAI_BASE_URL: V1,
|
|
280
|
+
OPENAI_API_KEY: key,
|
|
281
|
+
...(model ? { OPENAI_MODEL: model } : {}),
|
|
282
|
+
}),
|
|
283
|
+
args: (m) => (m ? ["--model", m] : []),
|
|
284
|
+
},
|
|
285
|
+
plandex: {
|
|
286
|
+
label: "Plandex",
|
|
287
|
+
bin: "plandex",
|
|
288
|
+
type: "env",
|
|
289
|
+
install: "curl -sL https://plandex.ai/install.sh | bash",
|
|
290
|
+
attribution: {
|
|
291
|
+
title: "Plandex",
|
|
292
|
+
referer: "https://github.com/plandex-ai/plandex",
|
|
293
|
+
},
|
|
294
|
+
env: (key) => ({
|
|
295
|
+
OPENAI_API_BASE: V1,
|
|
296
|
+
OPENAI_API_KEY: key,
|
|
297
|
+
}),
|
|
298
|
+
args: () => [],
|
|
299
|
+
},
|
|
300
|
+
interpreter: {
|
|
301
|
+
label: "Open Interpreter",
|
|
302
|
+
bin: "interpreter",
|
|
303
|
+
type: "env",
|
|
304
|
+
install: "pip install open-interpreter",
|
|
305
|
+
attribution: {
|
|
306
|
+
title: "Open Interpreter",
|
|
307
|
+
referer: "https://github.com/OpenInterpreter/open-interpreter",
|
|
308
|
+
},
|
|
309
|
+
env: (key) => ({
|
|
310
|
+
OPENAI_API_BASE: V1,
|
|
311
|
+
OPENAI_API_KEY: key,
|
|
312
|
+
}),
|
|
313
|
+
args: (m) => (m ? ["--model", `openai/${m}`] : []),
|
|
314
|
+
},
|
|
238
315
|
};
|
|
239
316
|
|
|
240
317
|
export const LAUNCH_ALIASES = {
|
|
@@ -242,6 +319,13 @@ export const LAUNCH_ALIASES = {
|
|
|
242
319
|
picode: "pi",
|
|
243
320
|
ohmypi: "omp",
|
|
244
321
|
"oh-my-pi": "omp",
|
|
322
|
+
qwencode: "qwen",
|
|
323
|
+
"qwen-code": "qwen",
|
|
324
|
+
"minimax-code": "minimax",
|
|
325
|
+
mcode: "minimax",
|
|
326
|
+
mimocode: "mimo",
|
|
327
|
+
"mimo-code": "mimo",
|
|
328
|
+
"open-interpreter": "interpreter",
|
|
245
329
|
};
|
|
246
330
|
|
|
247
331
|
export function resolveLaunchTool(name) {
|