pleumrouter 0.5.31 → 0.5.33
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/bin/pleum.mjs +10 -3
- package/package.json +1 -1
- package/src/completion.mjs +1 -1
- package/src/config.mjs +13 -0
- package/src/interactive.mjs +13 -6
- package/src/launch.mjs +24 -5
- package/src/locales/es.mjs +39 -0
- package/src/locales/ja.mjs +39 -0
- package/src/locales/ko.mjs +39 -0
- package/src/locales/zh.mjs +39 -0
- package/src/menu.mjs +159 -15
- package/src/recipes.mjs +45 -16
- package/src/settings.mjs +34 -8
- package/src/update.mjs +157 -0
- package/src/upgrade.mjs +8 -14
package/bin/pleum.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { login, loginDevice, logout } from "../src/login.mjs";
|
|
4
|
-
import { launch, launchHelp } from "../src/launch.mjs";
|
|
4
|
+
import { launch, launchHelp, recoverAgySettings } from "../src/launch.mjs";
|
|
5
5
|
import { wire, unwire, wireHelp, unwireHelp } from "../src/wire.mjs";
|
|
6
6
|
import { run } from "../src/run.mjs";
|
|
7
7
|
import { listModels } from "../src/models.mjs";
|
|
@@ -11,12 +11,17 @@ import { top } from "../src/top.mjs";
|
|
|
11
11
|
import { health } from "../src/health.mjs";
|
|
12
12
|
import { completion } from "../src/completion.mjs";
|
|
13
13
|
import { upgrade } from "../src/upgrade.mjs";
|
|
14
|
+
import { runUpdateCommand } from "../src/update.mjs";
|
|
14
15
|
import { mainMenu, runAgentMenu } from "../src/menu.mjs";
|
|
15
16
|
import { lang } from "../src/lang.mjs";
|
|
16
17
|
import { RECIPES } from "../src/recipes.mjs";
|
|
17
18
|
import { t } from "../src/i18n.mjs";
|
|
18
19
|
import { sanitizeTerminalText } from "../src/ansi.mjs";
|
|
19
20
|
|
|
21
|
+
// 이전 비정상 종료로 인한 잔존 백업 설정(agy 등)이 있으면 즉시 안전 복구
|
|
22
|
+
recoverAgySettings();
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
// 명령 구문(왼쪽)은 로케일과 무관 — 설명(오른쪽)만 번역, 폭은 가장 긴 구문 기준으로 계산.
|
|
21
26
|
function buildHelp() {
|
|
22
27
|
const rows = [
|
|
@@ -39,7 +44,8 @@ function buildHelp() {
|
|
|
39
44
|
["pleum health", t("Check gateway connectivity and login status")],
|
|
40
45
|
["pleum lang [code]", t("Show/change display language (en|ko|ja|zh|es)")],
|
|
41
46
|
["pleum completion <zsh|bash>", t("Print a shell completion script")],
|
|
42
|
-
["pleum
|
|
47
|
+
["pleum update [--force]", t("Update CLI to the latest version")],
|
|
48
|
+
["pleum upgrade", t("Alias for pleum update --force")],
|
|
43
49
|
["pleum version", t("Print the version")],
|
|
44
50
|
];
|
|
45
51
|
const width = Math.max(...rows.map(([cmd]) => cmd.length)) + 2;
|
|
@@ -141,8 +147,9 @@ try {
|
|
|
141
147
|
case "completion":
|
|
142
148
|
completion(rest[0]);
|
|
143
149
|
break;
|
|
150
|
+
case "update":
|
|
144
151
|
case "upgrade":
|
|
145
|
-
|
|
152
|
+
await runUpdateCommand(rest);
|
|
146
153
|
break;
|
|
147
154
|
case "version":
|
|
148
155
|
case "-v":
|
package/package.json
CHANGED
package/src/completion.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { t } from "./i18n.mjs";
|
|
4
4
|
import { launchToolNames } from "./recipes.mjs";
|
|
5
5
|
|
|
6
|
-
const COMMANDS = "login logout lang launch wire unwire run models top balance usage health version upgrade completion help";
|
|
6
|
+
const COMMANDS = "login logout lang launch wire unwire run models top balance usage health version update upgrade completion help";
|
|
7
7
|
const TOOLS = launchToolNames().join(" ");
|
|
8
8
|
|
|
9
9
|
function bash() {
|
package/src/config.mjs
CHANGED
|
@@ -241,3 +241,16 @@ export async function authFetch(path, init = {}) {
|
|
|
241
241
|
});
|
|
242
242
|
}
|
|
243
243
|
|
|
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
|
+
|
package/src/interactive.mjs
CHANGED
|
@@ -92,6 +92,13 @@ export function keyFeeder() {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// 문자열의 터미널 줄 수 계산 — 개행 문자(\n)가 있어도 줄 단위로 쪼개어 실제 출력 행 수를 정확히 합산
|
|
96
|
+
export function rowsFor(s, width = terminalWidth()) {
|
|
97
|
+
return String(s)
|
|
98
|
+
.split("\n")
|
|
99
|
+
.reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / Math.max(width, 1))), 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
// 화살표 이동 · 타이핑 필터 · Enter 선택 · Esc/Ctrl+C 취소(null).
|
|
96
103
|
// 비TTY면 즉시 null — 호출자가 기존 번호 피커 등으로 폴백한다.
|
|
97
104
|
// process.exit는 절대 호출하지 않는다(취소 처리는 호출자 몫).
|
|
@@ -112,7 +119,6 @@ export async function select(items, {
|
|
|
112
119
|
let windowStart = 0;
|
|
113
120
|
let lastRows = 0;
|
|
114
121
|
|
|
115
|
-
const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
|
|
116
122
|
const erase = () => {
|
|
117
123
|
if (!lastRows) return;
|
|
118
124
|
output.write("\r" + (lastRows > 1 ? `\x1b[${lastRows - 1}A` : "") + "\x1b[0J");
|
|
@@ -130,14 +136,14 @@ export async function select(items, {
|
|
|
130
136
|
windowStart = Math.max(0, Math.min(windowStart, Math.max(0, pool.length - limit)));
|
|
131
137
|
|
|
132
138
|
const shown = pool.slice(windowStart, windowStart + limit);
|
|
133
|
-
const
|
|
139
|
+
const rawLines = [
|
|
134
140
|
`${style.plum("?")} ${style.bold(message)} ${style.dim(t("type to filter · ↑↓ move · Enter select · Esc cancel"))}`,
|
|
135
141
|
`${style.gray("›")} ${query}`,
|
|
136
142
|
];
|
|
137
143
|
if (windowStart > 0) {
|
|
138
|
-
|
|
144
|
+
rawLines.push(style.dim(` ${t("↑ {n} more above", { n: windowStart })}`));
|
|
139
145
|
}
|
|
140
|
-
|
|
146
|
+
rawLines.push(
|
|
141
147
|
...shown.map((it, idx) => {
|
|
142
148
|
const itemIndex = windowStart + idx;
|
|
143
149
|
return itemIndex === cursor
|
|
@@ -147,10 +153,11 @@ export async function select(items, {
|
|
|
147
153
|
);
|
|
148
154
|
const remainingBelow = pool.length - (windowStart + shown.length);
|
|
149
155
|
if (remainingBelow > 0) {
|
|
150
|
-
|
|
156
|
+
rawLines.push(style.dim(` ${t("… {n} more — type to narrow down", { n: remainingBelow })}`));
|
|
151
157
|
}
|
|
152
|
-
if (!pool.length)
|
|
158
|
+
if (!pool.length) rawLines.push(style.dim(` ${t("No matches")}`));
|
|
153
159
|
erase();
|
|
160
|
+
const lines = rawLines.flatMap((l) => String(l).split("\n"));
|
|
154
161
|
const frame = lines.join("\n");
|
|
155
162
|
output.write(frame);
|
|
156
163
|
lastRows = lines.reduce((n, l) => n + rowsFor(l), 0);
|
package/src/launch.mjs
CHANGED
|
@@ -146,16 +146,28 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
146
146
|
if (exit) process.exit(130);
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
const onException = (err) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
if (err) console.error(err);
|
|
152
|
+
if (exit) process.exit(1);
|
|
153
|
+
};
|
|
154
|
+
|
|
149
155
|
process.once("SIGINT", onSignal);
|
|
150
156
|
process.once("SIGTERM", onSignal);
|
|
151
157
|
process.once("SIGHUP", onSignal);
|
|
158
|
+
process.once("SIGQUIT", onSignal);
|
|
152
159
|
process.once("exit", cleanup);
|
|
160
|
+
process.once("uncaughtException", onException);
|
|
161
|
+
process.once("unhandledRejection", onException);
|
|
153
162
|
|
|
154
163
|
const removeListeners = () => {
|
|
155
164
|
process.removeListener("SIGINT", onSignal);
|
|
156
165
|
process.removeListener("SIGTERM", onSignal);
|
|
157
166
|
process.removeListener("SIGHUP", onSignal);
|
|
167
|
+
process.removeListener("SIGQUIT", onSignal);
|
|
158
168
|
process.removeListener("exit", cleanup);
|
|
169
|
+
process.removeListener("uncaughtException", onException);
|
|
170
|
+
process.removeListener("unhandledRejection", onException);
|
|
159
171
|
};
|
|
160
172
|
|
|
161
173
|
const activeCtx = getActiveContext();
|
|
@@ -228,6 +240,12 @@ export function setupAgySettings(model, {
|
|
|
228
240
|
rm = rmSync,
|
|
229
241
|
} = {}) {
|
|
230
242
|
const { settingsPath, backupPath } = paths;
|
|
243
|
+
|
|
244
|
+
// 비정상 종료로 남은 이전 백업이 있으면 먼저 안전 복구
|
|
245
|
+
if (exists(backupPath)) {
|
|
246
|
+
recoverAgySettings({ paths, read, write, exists, rm });
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
const originalExists = exists(settingsPath);
|
|
232
250
|
let originalContent = null;
|
|
233
251
|
let parsed = null;
|
|
@@ -241,7 +259,8 @@ export function setupAgySettings(model, {
|
|
|
241
259
|
}
|
|
242
260
|
}
|
|
243
261
|
|
|
244
|
-
// 1. 비정상 종료 대비 백업 메타데이터 저장
|
|
262
|
+
// 1. 비정상 종료 대비 백업 메타데이터 저장 (gemini 값은 pleum의 잔재이므로 백업에서도 배제)
|
|
263
|
+
const hadProvider = Boolean(parsed && "modelProvider" in parsed && parsed.modelProvider !== "gemini");
|
|
245
264
|
try {
|
|
246
265
|
mkdir(dirname(backupPath), { recursive: true, mode: 0o700 });
|
|
247
266
|
write(
|
|
@@ -250,8 +269,8 @@ export function setupAgySettings(model, {
|
|
|
250
269
|
{
|
|
251
270
|
settingsPath,
|
|
252
271
|
originalExists,
|
|
253
|
-
originalHadModelProvider:
|
|
254
|
-
originalModelProvider: parsed?.modelProvider
|
|
272
|
+
originalHadModelProvider: hadProvider,
|
|
273
|
+
originalModelProvider: hadProvider ? parsed?.modelProvider : null,
|
|
255
274
|
originalHadModel: Boolean(parsed && "model" in parsed),
|
|
256
275
|
originalModel: parsed?.model ?? null,
|
|
257
276
|
originalContent,
|
|
@@ -280,7 +299,7 @@ export function setupAgySettings(model, {
|
|
|
280
299
|
if (currentContent) {
|
|
281
300
|
try {
|
|
282
301
|
const currentObj = JSON.parse(currentContent);
|
|
283
|
-
if (
|
|
302
|
+
if (hadProvider && parsed?.modelProvider) {
|
|
284
303
|
currentObj.modelProvider = parsed.modelProvider;
|
|
285
304
|
} else {
|
|
286
305
|
delete currentObj.modelProvider;
|
|
@@ -328,7 +347,7 @@ export function recoverAgySettings({
|
|
|
328
347
|
const curContent = read(settingsPath, "utf8");
|
|
329
348
|
try {
|
|
330
349
|
const curObj = JSON.parse(curContent);
|
|
331
|
-
if (backup.originalHadModelProvider) {
|
|
350
|
+
if (backup.originalHadModelProvider && backup.originalModelProvider && backup.originalModelProvider !== "gemini") {
|
|
332
351
|
curObj.modelProvider = backup.originalModelProvider;
|
|
333
352
|
} else {
|
|
334
353
|
delete curObj.modelProvider;
|
package/src/locales/es.mjs
CHANGED
|
@@ -288,5 +288,44 @@ export const ES = {
|
|
|
288
288
|
"by tag": "por etiqueta",
|
|
289
289
|
"by user": "por usuario",
|
|
290
290
|
"Failed to fetch balance ({status})": "Error al obtener el saldo ({status})",
|
|
291
|
+
|
|
292
|
+
// Update & Auto-update
|
|
293
|
+
"Update CLI to the latest version": "Actualizar CLI a la última versión",
|
|
294
|
+
"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
|
+
"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
|
+
"Updating pleumrouter CLI...": "Actualizando PleumRouter CLI...",
|
|
303
|
+
"Updating pleumrouter (force reinstall)...": "Actualizando PleumRouter (reinstalación forzada)...",
|
|
304
|
+
"Already using the latest version of pleumrouter (v{version}).": "Ya está utilizando la última versión de PleumRouter (v{version}).",
|
|
305
|
+
"Use --force (-f) to reinstall anyway.": "Utilice --force (-f) para reinstalar de todos modos.",
|
|
306
|
+
"Updating pleumrouter from v{from} to v{to}...": "Actualizando PleumRouter de v{from} a v{to}...",
|
|
307
|
+
"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
|
+
// Claude Code Tier Mapping
|
|
311
|
+
"Sonnet model": "Modelo Sonnet",
|
|
312
|
+
"Opus model": "Modelo Opus",
|
|
313
|
+
"Haiku model": "Modelo Haiku",
|
|
314
|
+
|
|
315
|
+
// Codex & agy Effort
|
|
316
|
+
"Select reasoning effort": "Seleccionar esfuerzo de razonamiento",
|
|
317
|
+
"Medium (Recommended)": "Medio (Recomendado)",
|
|
318
|
+
"Balanced reasoning speed and depth": "Velocidad y profundidad de razonamiento equilibradas",
|
|
319
|
+
"High (Deep reasoning)": "Alto (Razonamiento profundo)",
|
|
320
|
+
"Maximum reasoning depth for complex tasks": "Máxima profundidad de razonamiento para tareas complejas",
|
|
321
|
+
"Low (Fast response)": "Bajo (Respuesta rápida)",
|
|
322
|
+
"Lightweight reasoning for quick answers": "Razonamiento ligero para respuestas rápidas",
|
|
323
|
+
"High (Recommended)": "Alto (Recomendado)",
|
|
324
|
+
"High reasoning effort for standard tasks": "Alto esfuerzo de razonamiento para tareas estándar",
|
|
325
|
+
"Max (Maximum reasoning)": "Máximo (Razonamiento extremo)",
|
|
326
|
+
"Full reasoning effort for difficult problems": "Esfuerzo de razonamiento completo para problemas difíciles",
|
|
327
|
+
"Medium (Standard)": "Medio (Estándar)",
|
|
328
|
+
"Balanced reasoning effort": "Esfuerzo de razonamiento equilibrado",
|
|
329
|
+
"Low reasoning effort for rapid iteration": "Bajo esfuerzo de razonamiento para iteraciones rápidas",
|
|
291
330
|
};
|
|
292
331
|
|
package/src/locales/ja.mjs
CHANGED
|
@@ -288,5 +288,44 @@ export const JA = {
|
|
|
288
288
|
"by tag": "タグ別",
|
|
289
289
|
"by user": "ユーザー別",
|
|
290
290
|
"Failed to fetch balance ({status})": "残高の取得に失敗しました ({status})",
|
|
291
|
+
|
|
292
|
+
// Update & Auto-update
|
|
293
|
+
"Update CLI to the latest version": "CLIを最新バージョンにアップデート",
|
|
294
|
+
"Alias for pleum update --force": "pleum update --force のエイリアス",
|
|
295
|
+
"Auto update": "自動アップデート",
|
|
296
|
+
"Enabled (updates automatically on start)": "有効 (起動時に自動確認・更新)",
|
|
297
|
+
"Disabled": "無効",
|
|
298
|
+
"Update available": "更新可能",
|
|
299
|
+
"Update available: v{current} → v{latest}": "アップデートがあります: v{current} → v{latest}",
|
|
300
|
+
"Update available: v{current} → v{latest}. Run 'pleum update' to update.": "アップデートがあります: v{current} → v{latest}。「pleum update」を実行して更新してください。",
|
|
301
|
+
"Auto-updating PleumRouter CLI...": "PleumRouter CLI を自動アップデート中...",
|
|
302
|
+
"Updating pleumrouter CLI...": "PleumRouter CLI をアップデート中...",
|
|
303
|
+
"Updating pleumrouter (force reinstall)...": "PleumRouter をアップデート中 (強制再インストール)...",
|
|
304
|
+
"Already using the latest version of pleumrouter (v{version}).": "すでに最新バージョンの PleumRouter を使用しています (v{version})。",
|
|
305
|
+
"Use --force (-f) to reinstall anyway.": "強制再インストールするには --force (-f) を使用してください。",
|
|
306
|
+
"Updating pleumrouter from v{from} to v{to}...": "PleumRouter を v{from} から v{to} へアップデート中...",
|
|
307
|
+
"Update completed! Please run `pleum` again to use the new version.": "アップデート完了!新しいバージョンを使うには `pleum` を再起動してください。",
|
|
308
|
+
"Update completed! Please run `pleum` again.": "アップデート完了!`pleum` を再起動してください。",
|
|
309
|
+
|
|
310
|
+
// Claude Code Tier Mapping
|
|
311
|
+
"Sonnet model": "Sonnet モデル",
|
|
312
|
+
"Opus model": "Opus モデル",
|
|
313
|
+
"Haiku model": "Haiku モデル",
|
|
314
|
+
|
|
315
|
+
// Codex & agy Effort
|
|
316
|
+
"Select reasoning effort": "推論強度 (effort) の選択",
|
|
317
|
+
"Medium (Recommended)": "中 (推奨)",
|
|
318
|
+
"Balanced reasoning speed and depth": "推論速度と深さのバランス",
|
|
319
|
+
"High (Deep reasoning)": "高 (深層推論)",
|
|
320
|
+
"Maximum reasoning depth for complex tasks": "複雑なタスク向けの最大の推論深度",
|
|
321
|
+
"Low (Fast response)": "低 (高速応答)",
|
|
322
|
+
"Lightweight reasoning for quick answers": "素早い応答のための軽量推論",
|
|
323
|
+
"High (Recommended)": "高 (推奨)",
|
|
324
|
+
"High reasoning effort for standard tasks": "標準タスク向けの高い推論強度",
|
|
325
|
+
"Max (Maximum reasoning)": "最大 (極限推論)",
|
|
326
|
+
"Full reasoning effort for difficult problems": "難関タスク向けの最大の推論強度",
|
|
327
|
+
"Medium (Standard)": "中 (標準)",
|
|
328
|
+
"Balanced reasoning effort": "バランスの取れた標準的な推論強度",
|
|
329
|
+
"Low reasoning effort for rapid iteration": "迅速なイテレーション向けの低い推論強度",
|
|
291
330
|
};
|
|
292
331
|
|
package/src/locales/ko.mjs
CHANGED
|
@@ -351,5 +351,44 @@ export const KO = {
|
|
|
351
351
|
"by tag": "태그별",
|
|
352
352
|
"by user": "사용자별",
|
|
353
353
|
"Failed to fetch balance ({status})": "잔액 조회 실패 ({status})",
|
|
354
|
+
|
|
355
|
+
// Update & Auto-update
|
|
356
|
+
"Update CLI to the latest version": "CLI를 최신 버전으로 업데이트",
|
|
357
|
+
"Alias for pleum update --force": "pleum update --force의 별칭",
|
|
358
|
+
"Auto update": "자동 업데이트",
|
|
359
|
+
"Enabled (updates automatically on start)": "켜짐 (시작 시 자동 확인 및 업데이트)",
|
|
360
|
+
"Disabled": "꺼짐",
|
|
361
|
+
"Update available": "업데이트 가능",
|
|
362
|
+
"Update available: v{current} → v{latest}": "업데이트 가능: v{current} → v{latest}",
|
|
363
|
+
"Update available: v{current} → v{latest}. Run 'pleum update' to update.": "업데이트 가능: v{current} → v{latest}. 'pleum update'를 실행하여 업데이트하세요.",
|
|
364
|
+
"Auto-updating PleumRouter CLI...": "PleumRouter CLI 자동 업데이트 중...",
|
|
365
|
+
"Updating pleumrouter CLI...": "PleumRouter CLI 업데이트 중...",
|
|
366
|
+
"Updating pleumrouter (force reinstall)...": "PleumRouter 업데이트 중 (강제 재설치)...",
|
|
367
|
+
"Already using the latest version of pleumrouter (v{version}).": "이미 최신 버전의 PleumRouter를 사용 중입니다 (v{version}).",
|
|
368
|
+
"Use --force (-f) to reinstall anyway.": "재설치하려면 --force (-f) 옵션을 사용하세요.",
|
|
369
|
+
"Updating pleumrouter from v{from} to v{to}...": "PleumRouter를 v{from}에서 v{to}로 업데이트 중...",
|
|
370
|
+
"Update completed! Please run `pleum` again to use the new version.": "업데이트 완료! 새 버전을 사용하려면 `pleum`을 다시 실행하세요.",
|
|
371
|
+
"Update completed! Please run `pleum` again.": "업데이트 완료! `pleum`을 다시 실행하세요.",
|
|
372
|
+
|
|
373
|
+
// Claude Code Tier Mapping
|
|
374
|
+
"Sonnet model": "Sonnet 모델",
|
|
375
|
+
"Opus model": "Opus 모델",
|
|
376
|
+
"Haiku model": "Haiku 모델",
|
|
377
|
+
|
|
378
|
+
// Codex & agy Effort
|
|
379
|
+
"Select reasoning effort": "추론 강도(effort) 선택",
|
|
380
|
+
"Medium (Recommended)": "보통 (추천)",
|
|
381
|
+
"Balanced reasoning speed and depth": "추론 속도와 깊이의 균형",
|
|
382
|
+
"High (Deep reasoning)": "높음 (심층 추론)",
|
|
383
|
+
"Maximum reasoning depth for complex tasks": "복잡한 작업을 위한 최대 추론 깊이",
|
|
384
|
+
"Low (Fast response)": "낮음 (빠른 응답)",
|
|
385
|
+
"Lightweight reasoning for quick answers": "빠른 응답을 위한 가벼운 추론",
|
|
386
|
+
"High (Recommended)": "높음 (추천)",
|
|
387
|
+
"High reasoning effort for standard tasks": "일반 작업을 위한 높은 추론 강도",
|
|
388
|
+
"Max (Maximum reasoning)": "최대 (한계 추론)",
|
|
389
|
+
"Full reasoning effort for difficult problems": "고난도 문제를 위한 최대 추론 강도",
|
|
390
|
+
"Medium (Standard)": "보통 (표준)",
|
|
391
|
+
"Balanced reasoning effort": "균형 잡힌 표준 추론 강도",
|
|
392
|
+
"Low reasoning effort for rapid iteration": "빠른 반복 작업을 위한 낮은 추론 강도",
|
|
354
393
|
};
|
|
355
394
|
|
package/src/locales/zh.mjs
CHANGED
|
@@ -288,5 +288,44 @@ export const ZH = {
|
|
|
288
288
|
"by tag": "按标签",
|
|
289
289
|
"by user": "按用户",
|
|
290
290
|
"Failed to fetch balance ({status})": "获取余额失败 ({status})",
|
|
291
|
+
|
|
292
|
+
// Update & Auto-update
|
|
293
|
+
"Update CLI to the latest version": "将 CLI 更新到最新版本",
|
|
294
|
+
"Alias for pleum update --force": "pleum update --force 的别名",
|
|
295
|
+
"Auto update": "自动更新",
|
|
296
|
+
"Enabled (updates automatically on start)": "已启用 (启动时自动检查并更新)",
|
|
297
|
+
"Disabled": "已禁用",
|
|
298
|
+
"Update available": "有可用更新",
|
|
299
|
+
"Update available: v{current} → v{latest}": "有可用更新: v{current} → v{latest}",
|
|
300
|
+
"Update available: v{current} → v{latest}. Run 'pleum update' to update.": "有可用更新: v{current} → v{latest}。运行 'pleum update' 进行更新。",
|
|
301
|
+
"Auto-updating PleumRouter CLI...": "正在自动更新 PleumRouter CLI...",
|
|
302
|
+
"Updating pleumrouter CLI...": "正在更新 PleumRouter CLI...",
|
|
303
|
+
"Updating pleumrouter (force reinstall)...": "正在更新 PleumRouter (强制重新安装)...",
|
|
304
|
+
"Already using the latest version of pleumrouter (v{version}).": "已在使用最新版本的 PleumRouter (v{version})。",
|
|
305
|
+
"Use --force (-f) to reinstall anyway.": "如需强制重新安装,请使用 --force (-f)。",
|
|
306
|
+
"Updating pleumrouter from v{from} to v{to}...": "正在将 PleumRouter 从 v{from} 更新到 v{to}...",
|
|
307
|
+
"Update completed! Please run `pleum` again to use the new version.": "更新完成!请重新运行 `pleum` 以使用新版本。",
|
|
308
|
+
"Update completed! Please run `pleum` again.": "更新完成!请重新运行 `pleum`。",
|
|
309
|
+
|
|
310
|
+
// Claude Code Tier Mapping
|
|
311
|
+
"Sonnet model": "Sonnet 模型",
|
|
312
|
+
"Opus model": "Opus 模型",
|
|
313
|
+
"Haiku model": "Haiku 模型",
|
|
314
|
+
|
|
315
|
+
// Codex & agy Effort
|
|
316
|
+
"Select reasoning effort": "选择推理强度 (effort)",
|
|
317
|
+
"Medium (Recommended)": "中 (推荐)",
|
|
318
|
+
"Balanced reasoning speed and depth": "推理速度与深度的平衡",
|
|
319
|
+
"High (Deep reasoning)": "高 (深度推理)",
|
|
320
|
+
"Maximum reasoning depth for complex tasks": "适合复杂任务的最大推理深度",
|
|
321
|
+
"Low (Fast response)": "低 (快速响应)",
|
|
322
|
+
"Lightweight reasoning for quick answers": "快速解答的轻量推理",
|
|
323
|
+
"High (Recommended)": "高 (推荐)",
|
|
324
|
+
"High reasoning effort for standard tasks": "标准任务的高推理强度",
|
|
325
|
+
"Max (Maximum reasoning)": "最大 (极值推理)",
|
|
326
|
+
"Full reasoning effort for difficult problems": "困难问题的全力推理强度",
|
|
327
|
+
"Medium (Standard)": "中 (标准)",
|
|
328
|
+
"Balanced reasoning effort": "平衡的标准推理强度",
|
|
329
|
+
"Low reasoning effort for rapid iteration": "快速迭代的低推理强度",
|
|
291
330
|
};
|
|
292
331
|
|
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, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
15
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig, isLoggedIn, touchSession, getActiveContext } from "./config.mjs";
|
|
16
|
+
import { readConfig, writeConfig, isLoggedIn, touchSession, getActiveContext, isAutoUpdateEnabled } from "./config.mjs";
|
|
17
17
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
19
|
import { fetchModels, matchModels, listModels, formatModelLabel, fetchCoreCodingModels, CORE_MODEL_PROVIDERS } from "./models.mjs";
|
|
@@ -24,6 +24,7 @@ import { usage } from "./usage.mjs";
|
|
|
24
24
|
import { balance } from "./balance.mjs";
|
|
25
25
|
import { health } from "./health.mjs";
|
|
26
26
|
import { runSettings } from "./settings.mjs";
|
|
27
|
+
import { checkLatestVersion, performUpdate } from "./update.mjs";
|
|
27
28
|
import { t } from "./i18n.mjs";
|
|
28
29
|
|
|
29
30
|
// 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
|
|
@@ -200,6 +201,80 @@ export async function pickModelFor(item, defaultModel) {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
export async function pickClaudeCodeTiers(agentItem) {
|
|
205
|
+
const cfg = readConfig() ?? {};
|
|
206
|
+
const savedTiers = cfg.claude_tiers || {};
|
|
207
|
+
const defaultTiers = {
|
|
208
|
+
sonnet: "claude-sonnet-5",
|
|
209
|
+
opus: "claude-opus-5-5",
|
|
210
|
+
haiku: DEFAULT_MODEL,
|
|
211
|
+
};
|
|
212
|
+
const baseTiers = {
|
|
213
|
+
sonnet: savedTiers.sonnet || defaultTiers.sonnet,
|
|
214
|
+
opus: savedTiers.opus || defaultTiers.opus,
|
|
215
|
+
haiku: savedTiers.haiku || defaultTiers.haiku,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// 1/3단계: Sonnet 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
219
|
+
const sonnet = await pickModelFor(
|
|
220
|
+
{ title: `${agentItem.title} (1/3) — ${t("Sonnet model")}` },
|
|
221
|
+
baseTiers.sonnet,
|
|
222
|
+
);
|
|
223
|
+
if (!sonnet) return null;
|
|
224
|
+
|
|
225
|
+
// 2/3단계: Opus 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
226
|
+
const opus = await pickModelFor(
|
|
227
|
+
{ title: `${agentItem.title} (2/3) — ${t("Opus model")}` },
|
|
228
|
+
baseTiers.opus,
|
|
229
|
+
);
|
|
230
|
+
if (!opus) return null;
|
|
231
|
+
|
|
232
|
+
// 3/3단계: Haiku 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
233
|
+
const haiku = await pickModelFor(
|
|
234
|
+
{ title: `${agentItem.title} (3/3) — ${t("Haiku model")}` },
|
|
235
|
+
baseTiers.haiku,
|
|
236
|
+
);
|
|
237
|
+
if (!haiku) return null;
|
|
238
|
+
|
|
239
|
+
const resultTiers = { sonnet, opus, haiku };
|
|
240
|
+
cfg.claude_tiers = resultTiers;
|
|
241
|
+
writeConfig(cfg);
|
|
242
|
+
return resultTiers;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function pickCodexEffort(defaultEffort = "medium") {
|
|
246
|
+
const options = [
|
|
247
|
+
{ id: "medium", title: `⚡ ${t("Medium (Recommended)")}`, desc: t("Balanced reasoning speed and depth") },
|
|
248
|
+
{ id: "high", title: `🔥 ${t("High (Deep reasoning)")}`, desc: t("Maximum reasoning depth for complex tasks") },
|
|
249
|
+
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Lightweight reasoning for quick answers") },
|
|
250
|
+
];
|
|
251
|
+
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
252
|
+
const picked = await select(options, {
|
|
253
|
+
message: `Codex — ${t("Select reasoning effort")}`,
|
|
254
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
255
|
+
limit: 5,
|
|
256
|
+
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
257
|
+
});
|
|
258
|
+
return picked ? picked.id : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function pickAgyEffort(defaultEffort = "high") {
|
|
262
|
+
const options = [
|
|
263
|
+
{ id: "high", title: `⚡ ${t("High (Recommended)")}`, desc: t("High reasoning effort for standard tasks") },
|
|
264
|
+
{ id: "max", title: `🚀 ${t("Max (Maximum reasoning)")}`, desc: t("Full reasoning effort for difficult problems") },
|
|
265
|
+
{ id: "medium", title: `⚖️ ${t("Medium (Standard)")}`, desc: t("Balanced reasoning effort") },
|
|
266
|
+
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Low reasoning effort for rapid iteration") },
|
|
267
|
+
];
|
|
268
|
+
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
269
|
+
const picked = await select(options, {
|
|
270
|
+
message: `Antigravity CLI (agy) — ${t("Select reasoning effort")}`,
|
|
271
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
272
|
+
limit: 6,
|
|
273
|
+
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
274
|
+
});
|
|
275
|
+
return picked ? picked.id : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
203
278
|
// 미설치 도구: ollama처럼 확인 후 그 자리에서 설치(stdio 상속). 성공 시 true.
|
|
204
279
|
async function ensureInstalled(r) {
|
|
205
280
|
if (!autoInstallable(r)) {
|
|
@@ -244,7 +319,10 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
244
319
|
let windowStart = 0;
|
|
245
320
|
let lastRows = 0;
|
|
246
321
|
|
|
247
|
-
const rowsFor = (s) =>
|
|
322
|
+
const rowsFor = (s) =>
|
|
323
|
+
String(s)
|
|
324
|
+
.split("\n")
|
|
325
|
+
.reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / Math.max(terminalWidth(), 1))), 0);
|
|
248
326
|
const erase = () => {
|
|
249
327
|
if (!lastRows) return;
|
|
250
328
|
output.write("\r" + (lastRows > 1 ? `\x1b[${lastRows - 1}A` : "") + "\x1b[0J");
|
|
@@ -296,8 +374,9 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
296
374
|
lines.push(style.dim(footer));
|
|
297
375
|
|
|
298
376
|
erase();
|
|
299
|
-
|
|
300
|
-
|
|
377
|
+
const frameLines = lines.flatMap((l) => String(l).split("\n"));
|
|
378
|
+
output.write(frameLines.join("\n"));
|
|
379
|
+
lastRows = frameLines.reduce((n, l) => n + rowsFor(l), 0);
|
|
301
380
|
};
|
|
302
381
|
|
|
303
382
|
const move = (dir) => {
|
|
@@ -375,7 +454,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
375
454
|
});
|
|
376
455
|
}
|
|
377
456
|
|
|
378
|
-
async function menuSelect() {
|
|
457
|
+
async function menuSelect(updateInfo = null) {
|
|
379
458
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
380
459
|
const cfg = readConfig();
|
|
381
460
|
const items = buildItems({
|
|
@@ -384,8 +463,19 @@ async function menuSelect() {
|
|
|
384
463
|
});
|
|
385
464
|
const last = cfg?.last_selection;
|
|
386
465
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
466
|
+
const headerLines = [`${style.bold(style.plum("PleumRouter"))} ${style.dim(`pleum ${version}`)}`];
|
|
467
|
+
if (updateInfo?.isOutdated) {
|
|
468
|
+
headerLines.push(
|
|
469
|
+
style.yellow(
|
|
470
|
+
`💡 ${t("Update available: v{current} → v{latest}. Run 'pleum update' to update.", {
|
|
471
|
+
current: updateInfo.currentVersion,
|
|
472
|
+
latest: updateInfo.latestVersion,
|
|
473
|
+
})}`,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
387
477
|
return runList(items, {
|
|
388
|
-
header:
|
|
478
|
+
header: headerLines.join("\n"),
|
|
389
479
|
footer: t("↑/↓ move • enter run • → set model • esc quit"),
|
|
390
480
|
initialCursor,
|
|
391
481
|
});
|
|
@@ -403,7 +493,7 @@ async function agentSelect() {
|
|
|
403
493
|
}
|
|
404
494
|
|
|
405
495
|
// 에이전트 실행 메뉴 — bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유.
|
|
406
|
-
// 에이전트를 고르면 곧바로
|
|
496
|
+
// 에이전트를 고르면 곧바로 모델/티어/추론 피커를 띄워 기본 모델 또는
|
|
407
497
|
// 선택한 우리 모델로 코딩 에이전트를 실행한다.
|
|
408
498
|
export async function runAgentMenu({ loop = false } = {}) {
|
|
409
499
|
const io = process.stdin;
|
|
@@ -430,15 +520,53 @@ export async function runAgentMenu({ loop = false } = {}) {
|
|
|
430
520
|
}
|
|
431
521
|
|
|
432
522
|
const agentSaved = readConfig()?.launch_models ?? {};
|
|
433
|
-
let
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (!
|
|
523
|
+
let launchModelPayload = null;
|
|
524
|
+
|
|
525
|
+
if (agentItem.tool === "claude") {
|
|
526
|
+
const tiers = await pickClaudeCodeTiers(agentItem);
|
|
527
|
+
if (!tiers) continue;
|
|
528
|
+
launchModelPayload = tiers;
|
|
529
|
+
saveModel("claude", tiers.sonnet);
|
|
530
|
+
} else if (agentItem.tool === "codex") {
|
|
531
|
+
let chosenModel = agentPicked.model;
|
|
532
|
+
if (!chosenModel) {
|
|
533
|
+
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
534
|
+
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
535
|
+
if (!chosenModel) continue;
|
|
536
|
+
}
|
|
537
|
+
saveModel("codex", chosenModel);
|
|
538
|
+
const cfg = readConfig() ?? {};
|
|
539
|
+
const effort = await pickCodexEffort(cfg.codex_effort || "medium");
|
|
540
|
+
if (!effort) continue;
|
|
541
|
+
cfg.codex_effort = effort;
|
|
542
|
+
writeConfig(cfg);
|
|
543
|
+
launchModelPayload = { model: chosenModel, effort };
|
|
544
|
+
} else if (agentItem.tool === "agy") {
|
|
545
|
+
let chosenModel = agentPicked.model;
|
|
546
|
+
if (!chosenModel) {
|
|
547
|
+
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
548
|
+
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
549
|
+
if (!chosenModel) continue;
|
|
550
|
+
}
|
|
551
|
+
saveModel("agy", chosenModel);
|
|
552
|
+
const cfg = readConfig() ?? {};
|
|
553
|
+
const effort = await pickAgyEffort(cfg.agy_effort || "high");
|
|
554
|
+
if (!effort) continue;
|
|
555
|
+
cfg.agy_effort = effort;
|
|
556
|
+
writeConfig(cfg);
|
|
557
|
+
launchModelPayload = { model: chosenModel, effort };
|
|
558
|
+
} else {
|
|
559
|
+
let chosenModel = agentPicked.model;
|
|
560
|
+
if (!chosenModel) {
|
|
561
|
+
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
562
|
+
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
563
|
+
if (!chosenModel) continue;
|
|
564
|
+
}
|
|
565
|
+
saveModel(agentItem.tool, chosenModel);
|
|
566
|
+
launchModelPayload = chosenModel;
|
|
438
567
|
}
|
|
439
568
|
|
|
440
|
-
|
|
441
|
-
await launch(agentItem.tool, chosenModel, [], { exit: false });
|
|
569
|
+
await launch(agentItem.tool, launchModelPayload, [], { exit: false });
|
|
442
570
|
console.log();
|
|
443
571
|
if (!loop) return true;
|
|
444
572
|
}
|
|
@@ -457,8 +585,24 @@ export async function mainMenu() {
|
|
|
457
585
|
if (!isLoggedIn()) await login();
|
|
458
586
|
else await touchSession().catch(() => {});
|
|
459
587
|
|
|
588
|
+
let updateInfo = null;
|
|
589
|
+
try {
|
|
590
|
+
updateInfo = await checkLatestVersion();
|
|
591
|
+
if (updateInfo?.isOutdated) {
|
|
592
|
+
if (isAutoUpdateEnabled()) {
|
|
593
|
+
console.log(`💡 ${t("Update available: v{current} → v{latest}", { current: updateInfo.currentVersion, latest: updateInfo.latestVersion })}`);
|
|
594
|
+
console.log(`▶ ${t("Auto-updating PleumRouter CLI...")}\n`);
|
|
595
|
+
const code = await performUpdate();
|
|
596
|
+
if (code === 0) {
|
|
597
|
+
console.log(`\n✓ ${t("Update completed! Please run `pleum` again.")}`);
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch {}
|
|
603
|
+
|
|
460
604
|
for (;;) {
|
|
461
|
-
const picked = await menuSelect();
|
|
605
|
+
const picked = await menuSelect(updateInfo);
|
|
462
606
|
if (picked === undefined) return false; // 비TTY
|
|
463
607
|
if (picked === null || picked === "quit") return true; // esc/q/ctrl+c — ollama처럼 조용히 종료
|
|
464
608
|
|
package/src/recipes.mjs
CHANGED
|
@@ -42,16 +42,29 @@ export const RECIPES = {
|
|
|
42
42
|
},
|
|
43
43
|
// 함정: ANTHROPIC_BASE_URL은 반드시 루트(/v1 없이) — CLI가 /v1/messages를 스스로 덧붙인다.
|
|
44
44
|
// /v1을 넣으면 /v1/v1/messages가 되어 실패.
|
|
45
|
-
env: (key) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
env: (key, m) => {
|
|
46
|
+
const isObj = m && typeof m === "object";
|
|
47
|
+
const sonnet = (isObj ? m.sonnet : m) || "claude-sonnet-5";
|
|
48
|
+
const opus = (isObj ? m.opus : null) || "claude-opus-5-5";
|
|
49
|
+
const haiku = (isObj ? m.haiku : null) || DEFAULT_MODEL;
|
|
50
|
+
const toAnthropic = (x) => (x.startsWith("anthropic/") ? x : `anthropic/${x}`);
|
|
51
|
+
return {
|
|
52
|
+
ANTHROPIC_BASE_URL: ROOT,
|
|
53
|
+
ANTHROPIC_API_KEY: key,
|
|
54
|
+
ANTHROPIC_AUTH_TOKEN: key,
|
|
55
|
+
// /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
|
|
56
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
57
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic(sonnet),
|
|
58
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic(opus),
|
|
59
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: toAnthropic(haiku),
|
|
60
|
+
ANTHROPIC_SMALL_MODEL: toAnthropic(haiku),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
args: (m) => {
|
|
64
|
+
const isObj = m && typeof m === "object";
|
|
65
|
+
const main = (isObj ? m.sonnet : m) || DEFAULT_MODEL;
|
|
66
|
+
return ["--model", main.startsWith("anthropic/") ? main : `anthropic/${main}`];
|
|
67
|
+
},
|
|
55
68
|
},
|
|
56
69
|
codex: {
|
|
57
70
|
label: "Codex CLI",
|
|
@@ -63,7 +76,11 @@ export const RECIPES = {
|
|
|
63
76
|
referer: "https://github.com/openai/codex",
|
|
64
77
|
},
|
|
65
78
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
66
|
-
args: () =>
|
|
79
|
+
args: (m) => {
|
|
80
|
+
const isObj = m && typeof m === "object";
|
|
81
|
+
const effort = isObj ? m.effort : null;
|
|
82
|
+
return effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
83
|
+
},
|
|
67
84
|
},
|
|
68
85
|
agy: {
|
|
69
86
|
label: "Antigravity CLI",
|
|
@@ -80,7 +97,14 @@ export const RECIPES = {
|
|
|
80
97
|
GEMINI_API_KEY: key,
|
|
81
98
|
GOOGLE_API_KEY: key,
|
|
82
99
|
}),
|
|
83
|
-
args: (m) =>
|
|
100
|
+
args: (m) => {
|
|
101
|
+
const isObj = m && typeof m === "object";
|
|
102
|
+
const model = (isObj ? m.model : m) || DEFAULT_MODEL;
|
|
103
|
+
const effort = isObj ? m.effort : null;
|
|
104
|
+
const res = ["--model", model];
|
|
105
|
+
if (effort) res.push("--effort", effort);
|
|
106
|
+
return res;
|
|
107
|
+
},
|
|
84
108
|
},
|
|
85
109
|
opencode: {
|
|
86
110
|
label: "opencode",
|
|
@@ -378,12 +402,15 @@ export function tomlBasicString(value) {
|
|
|
378
402
|
return `"${escaped}"`;
|
|
379
403
|
}
|
|
380
404
|
|
|
381
|
-
export function codexToml(model) {
|
|
382
|
-
const
|
|
405
|
+
export function codexToml(model, effort) {
|
|
406
|
+
const isObj = model && typeof model === "object";
|
|
407
|
+
const m = (isObj ? model.model : model) || DEFAULT_MODEL;
|
|
408
|
+
const eff = (isObj ? model.effort : effort) || null;
|
|
409
|
+
const effortLine = eff ? `model_reasoning_effort = ${tomlBasicString(eff)}\n` : "";
|
|
383
410
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
384
411
|
model = ${tomlBasicString(m)}
|
|
385
412
|
model_provider = "pleum"
|
|
386
|
-
|
|
413
|
+
${effortLine}
|
|
387
414
|
[model_providers.pleum]
|
|
388
415
|
name = "PleumRouter"
|
|
389
416
|
base_url = ${tomlBasicString(V1)}
|
|
@@ -401,7 +428,9 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
401
428
|
throw new Error("agy settings.json must be a JSON object");
|
|
402
429
|
}
|
|
403
430
|
}
|
|
404
|
-
|
|
431
|
+
const isObj = model && typeof model === "object";
|
|
432
|
+
const m = (isObj ? model.model : model) || DEFAULT_MODEL;
|
|
433
|
+
return { ...obj, modelProvider: "gemini", model: m };
|
|
405
434
|
}
|
|
406
435
|
|
|
407
436
|
export function piModelsJson(model) {
|
package/src/settings.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { style, sanitizeTerminalText } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
|
-
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext, isAutoUpdateEnabled, setAutoUpdateEnabled } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
13
13
|
import { RECIPES } from "./recipes.mjs";
|
|
14
14
|
import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
|
|
@@ -18,12 +18,25 @@ import { completion } from "./completion.mjs";
|
|
|
18
18
|
import { lang } from "./lang.mjs";
|
|
19
19
|
import { switchCreditWorkspace } from "./balance.mjs";
|
|
20
20
|
import { runList, saveModel } from "./menu.mjs";
|
|
21
|
+
import { checkLatestVersion, isNewerVersion, runUpdateCommand } from "./update.mjs";
|
|
21
22
|
|
|
22
23
|
// Settings 서브메뉴 항목(순수 함수 — 테스트 대상).
|
|
23
|
-
export function buildSettingsItems({
|
|
24
|
+
export function buildSettingsItems({
|
|
25
|
+
lang,
|
|
26
|
+
loggedIn,
|
|
27
|
+
keyPrefix,
|
|
28
|
+
version,
|
|
29
|
+
activeContext = { type: "personal" },
|
|
30
|
+
autoUpdate = true,
|
|
31
|
+
latestVersion = null,
|
|
32
|
+
}) {
|
|
24
33
|
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
25
34
|
? `${t("Team")}: ${activeContext.org_name}`
|
|
26
35
|
: t("Personal Credit");
|
|
36
|
+
const updateDesc = latestVersion && isNewerVersion(version, latestVersion)
|
|
37
|
+
? `${t("Current version: {version}", { version })} → v${latestVersion} (${t("Update available")})`
|
|
38
|
+
: t("Current version: {version}", { version });
|
|
39
|
+
|
|
27
40
|
return [
|
|
28
41
|
{ id: "settings:lang", title: t("Language"), desc: `${t("current")}: ${LANGS[lang] ?? lang}` },
|
|
29
42
|
{ id: "settings:models", title: t("Default models"), desc: t("Set the default model used per agent") },
|
|
@@ -38,7 +51,12 @@ export function buildSettingsItems({ lang, loggedIn, keyPrefix, version, activeC
|
|
|
38
51
|
: `${t("Logged in")} (${t("key")} ${keyPrefix}…)`
|
|
39
52
|
: t("Not logged in"),
|
|
40
53
|
},
|
|
41
|
-
{
|
|
54
|
+
{
|
|
55
|
+
id: "settings:auto_update",
|
|
56
|
+
title: t("Auto update"),
|
|
57
|
+
desc: autoUpdate ? t("Enabled (updates automatically on start)") : t("Disabled"),
|
|
58
|
+
},
|
|
59
|
+
{ id: "settings:update", title: t("Update CLI"), desc: updateDesc },
|
|
42
60
|
{ id: "settings:completion", title: t("Shell completion"), desc: t("Print a zsh/bash completion script") },
|
|
43
61
|
];
|
|
44
62
|
}
|
|
@@ -73,12 +91,18 @@ function clearSavedModel(tool) {
|
|
|
73
91
|
|
|
74
92
|
async function settingsSelect() {
|
|
75
93
|
const cfg = readConfig();
|
|
94
|
+
let updateInfo = null;
|
|
95
|
+
try {
|
|
96
|
+
updateInfo = await checkLatestVersion();
|
|
97
|
+
} catch {}
|
|
76
98
|
const items = buildSettingsItems({
|
|
77
99
|
lang: getLocale(),
|
|
78
100
|
loggedIn: isLoggedIn(cfg),
|
|
79
101
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
80
102
|
version: readVersion(),
|
|
81
103
|
activeContext: getActiveContext(cfg),
|
|
104
|
+
autoUpdate: isAutoUpdateEnabled(cfg),
|
|
105
|
+
latestVersion: updateInfo?.latestVersion,
|
|
82
106
|
});
|
|
83
107
|
return runList(items, {
|
|
84
108
|
header: style.bold(style.plum(t("Settings"))),
|
|
@@ -164,12 +188,14 @@ export async function runSettings() {
|
|
|
164
188
|
case "settings:account":
|
|
165
189
|
await runAccount();
|
|
166
190
|
continue;
|
|
191
|
+
case "settings:auto_update": {
|
|
192
|
+
const next = !isAutoUpdateEnabled();
|
|
193
|
+
setAutoUpdateEnabled(next);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
167
196
|
case "settings:update":
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// mainMenu()의 chat 케이스처럼 즉시 루프를 벗어난다.
|
|
171
|
-
console.log(t("Reinstalling — pleum will exit when finished. Run `pleum` again afterward."));
|
|
172
|
-
upgrade();
|
|
197
|
+
console.log(t("Updating pleumrouter CLI..."));
|
|
198
|
+
await runUpdateCommand([], { exit: false });
|
|
173
199
|
return "quit";
|
|
174
200
|
case "settings:completion":
|
|
175
201
|
await runCompletion();
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { t } from "./i18n.mjs";
|
|
6
|
+
|
|
7
|
+
const REGISTRY_URL = "https://registry.npmjs.org/pleumrouter/latest";
|
|
8
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1시간 캐시
|
|
9
|
+
|
|
10
|
+
export function readCliVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
|
+
return pkg.version;
|
|
14
|
+
} catch {
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseSemver(v) {
|
|
20
|
+
if (!v) return [0, 0, 0];
|
|
21
|
+
const clean = String(v).trim().replace(/^v/, "");
|
|
22
|
+
const parts = clean.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
23
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isNewerVersion(current, latest) {
|
|
27
|
+
const [cMaj, cMin, cPat] = parseSemver(current);
|
|
28
|
+
const [lMaj, lMin, lPat] = parseSemver(latest);
|
|
29
|
+
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
30
|
+
if (lMin !== cMin) return lMin > cMin;
|
|
31
|
+
return lPat > cPat;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getUpdateCachePath() {
|
|
35
|
+
return join(homedir(), ".pleum", "update-cache.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readUpdateCache(cachePath = getUpdateCachePath()) {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(cachePath)) return null;
|
|
41
|
+
return JSON.parse(readFileSync(cachePath, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeUpdateCache(latestVersion, cachePath = getUpdateCachePath()) {
|
|
48
|
+
try {
|
|
49
|
+
const dir = join(homedir(), ".pleum");
|
|
50
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
51
|
+
writeFileSync(
|
|
52
|
+
cachePath,
|
|
53
|
+
JSON.stringify({ last_check: Date.now(), latest_version: latestVersion }, null, 2),
|
|
54
|
+
{ mode: 0o600 },
|
|
55
|
+
);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchLatestVersionFromRegistry({ timeoutMs = 2000, fetchFn = globalThis.fetch } = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
const res = await fetchFn(REGISTRY_URL, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
headers: { Accept: "application/json" },
|
|
66
|
+
});
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (!res.ok) return null;
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return data?.version ?? null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function checkLatestVersion({
|
|
77
|
+
force = false,
|
|
78
|
+
timeoutMs = 2000,
|
|
79
|
+
currentVersion = null,
|
|
80
|
+
cachePath = getUpdateCachePath(),
|
|
81
|
+
fetchFn = globalThis.fetch,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const cur = currentVersion || readCliVersion();
|
|
84
|
+
const cached = readUpdateCache(cachePath);
|
|
85
|
+
|
|
86
|
+
if (!force && cached?.latest_version && typeof cached.last_check === "number") {
|
|
87
|
+
if (Date.now() - cached.last_check < CHECK_INTERVAL_MS) {
|
|
88
|
+
return {
|
|
89
|
+
currentVersion: cur,
|
|
90
|
+
latestVersion: cached.latest_version,
|
|
91
|
+
isOutdated: isNewerVersion(cur, cached.latest_version),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const remote = await fetchLatestVersionFromRegistry({ timeoutMs, fetchFn });
|
|
97
|
+
if (remote) {
|
|
98
|
+
writeUpdateCache(remote, cachePath);
|
|
99
|
+
return {
|
|
100
|
+
currentVersion: cur,
|
|
101
|
+
latestVersion: remote,
|
|
102
|
+
isOutdated: isNewerVersion(cur, remote),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 네트워크 실패 시 기존 캐시가 있으면 재활용, 없으면 현재 버전으로 안전 폴백
|
|
107
|
+
const fallbackLatest = cached?.latest_version || cur;
|
|
108
|
+
return {
|
|
109
|
+
currentVersion: cur,
|
|
110
|
+
latestVersion: fallbackLatest,
|
|
111
|
+
isOutdated: isNewerVersion(cur, fallbackLatest),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function performUpdate({ bin = null } = {}) {
|
|
116
|
+
const npmCmd = bin || (process.platform === "win32" ? "npm.cmd" : "npm");
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const child = spawn(npmCmd, ["install", "-g", "pleumrouter@latest"], {
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
shell: false,
|
|
121
|
+
});
|
|
122
|
+
child.on("error", (e) => {
|
|
123
|
+
console.error(`✗ ${e.message}`);
|
|
124
|
+
resolve(1);
|
|
125
|
+
});
|
|
126
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function runUpdateCommand(args = [], { exit = true } = {}) {
|
|
131
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
132
|
+
const cur = readCliVersion();
|
|
133
|
+
|
|
134
|
+
if (force) {
|
|
135
|
+
console.log(`▶ ${t("Updating pleumrouter (force reinstall)...")}\n`);
|
|
136
|
+
const code = await performUpdate();
|
|
137
|
+
if (exit) process.exit(code);
|
|
138
|
+
return code;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { latestVersion, isOutdated } = await checkLatestVersion({ force: true });
|
|
142
|
+
|
|
143
|
+
if (!isOutdated) {
|
|
144
|
+
console.log(`✓ ${t("Already using the latest version of pleumrouter (v{version}).", { version: cur })}`);
|
|
145
|
+
console.log(t("Use --force (-f) to reinstall anyway."));
|
|
146
|
+
if (exit) process.exit(0);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`▶ ${t("Updating pleumrouter from v{from} to v{to}...", { from: cur, to: latestVersion })}\n`);
|
|
151
|
+
const code = await performUpdate();
|
|
152
|
+
if (code === 0) {
|
|
153
|
+
console.log(`\n✓ ${t("Update completed! Please run `pleum` again to use the new version.")}`);
|
|
154
|
+
}
|
|
155
|
+
if (exit) process.exit(code);
|
|
156
|
+
return code;
|
|
157
|
+
}
|
package/src/upgrade.mjs
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { performUpdate, runUpdateCommand } from "./update.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
shell: false,
|
|
10
|
-
});
|
|
11
|
-
child.on("error", (e) => {
|
|
12
|
-
console.error(`✗ ${e.message}`);
|
|
13
|
-
process.exit(1);
|
|
14
|
-
});
|
|
15
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3
|
+
export { performUpdate, runUpdateCommand };
|
|
4
|
+
|
|
5
|
+
// 최신 버전으로 전역 재설치. 하위 호환을 위해 유지하며 process.exit() 호출 규약을 보존한다.
|
|
6
|
+
export async function upgrade() {
|
|
7
|
+
const code = await runUpdateCommand(["--force"], { exit: false });
|
|
8
|
+
process.exit(code ?? 0);
|
|
16
9
|
}
|
|
10
|
+
|