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 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 --provenance` 실행.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.mjs CHANGED
@@ -62,15 +62,9 @@ export function clearCredentials(existing) {
62
62
  }
63
63
 
64
64
  /**
65
- * 사용 가능한 세션 보유 여부. 레거시 `key`(plm_)만 있는 상태는 requireAuth()가 거부하므로
66
- * (아래 requireAuth 참조) 미로그인으로 취급한다 — 그래야 런처가 자동으로 재로그인을 태운다.
65
+ * JWT payload 파싱(서명 검증 없는 디코드) — iat, exp 등의 클레임 추출용.
67
66
  */
68
- export function isLoggedIn(cfg = readConfig()) {
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
- const payload = JSON.parse(json);
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
- /** access가 곧 만료(기본 60s 여유)면 true. */
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
- * 유효한 access_token을 반환. stale이면 refresh. 레거시 `key`(plm_)만 있으면 재로그인 안내.
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
- const cfg = readConfig();
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?.access_token || !cfg?.refresh_token) {
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
- if (!accessTokenIsStale(cfg.access_token)) {
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
- const refreshed = await refreshAccessToken(cfg);
116
- if (!refreshed) {
117
- console.error(t("Session expired. Run:\n pleum login"));
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) return null;
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?.access_token
42
+ const keyPrefix = isLoggedIn(cfg)
43
43
  ? "oauth"
44
44
  : cfg?.key
45
45
  ? cfg.key.slice(0, 8)
@@ -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} ₩).",
@@ -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} ₩) を下回っています。",
@@ -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": "모델과 채팅",
@@ -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) {