pleumrouter 0.5.35 → 0.5.37
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 +14 -9
- package/bin/pleum.mjs +17 -4
- package/package.json +1 -1
- package/src/completion.mjs +1 -1
- package/src/launch.mjs +159 -28
- package/src/locales/es.mjs +15 -3
- package/src/locales/ja.mjs +15 -3
- package/src/locales/ko.mjs +15 -3
- package/src/locales/zh.mjs +15 -3
- package/src/menu.mjs +70 -252
- package/src/models.mjs +9 -133
- package/src/recipes.mjs +73 -38
- package/src/settings.mjs +1 -13
- package/src/wire.mjs +37 -21
package/src/menu.mjs
CHANGED
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
// 동작 패리티(ollama cmd/tui/tui.go·cmd/launch 소스 분석 기반):
|
|
3
|
-
// · 인라인 렌더(대체 화면 아님), ▸ 커서 + dim 설명 2줄 항목, 하단 키 안내
|
|
4
|
-
// · →(configure) = 모델 피커를 강제로 띄우고 선택 즉시 실행(+ launch_models에 저장)
|
|
5
|
-
// · Enter = 저장된 모델(launch_models)로 바로 실행, 미설치면 Y/n 확인 후 자동 설치
|
|
6
|
-
// · 실행 종료 후 메뉴로 복귀(ollama runInteractiveTUI 루프), last_selection 커서 복원
|
|
7
|
-
// · 키보드 전용: ↑/k ↓/j 이동 · enter/space 실행 · →/l 모델 설정 · esc/q 뒤로(최상위에선 종료) · ctrl+c 즉시 종료
|
|
8
|
-
// 에이전트는 최상위에 나열하지 않고 "에이전트 실행" 한 항목으로 묶어 서브메뉴(agentSelect)로 진입한다 —
|
|
9
|
-
// 지원 에이전트가 많아질수록 최상위 목록이 늘어지는 것을 막기 위함(runList는 두 메뉴가 공유하는 렌더 루프).
|
|
1
|
+
// bare `pleum` 인라인 런처. 첫 화면의 에이전트 실행 → 도구 → 실행 모드를 공유한다.
|
|
10
2
|
|
|
11
3
|
import { readFileSync } from "node:fs";
|
|
12
4
|
import { spawn } from "node:child_process";
|
|
13
5
|
import { createInterface } from "node:readline";
|
|
14
6
|
import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
7
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig, isLoggedIn,
|
|
8
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
17
9
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
|
-
import { onPath, launch } from "./launch.mjs";
|
|
19
|
-
import { fetchModels, matchModels, listModels, formatModelLabel,
|
|
10
|
+
import { onPath, launch, listAgyModelIds, supportsAgyModel } from "./launch.mjs";
|
|
11
|
+
import { fetchModels, matchModels, listModels, formatModelLabel, codingModels, coreCodingModels } from "./models.mjs";
|
|
20
12
|
import { run } from "./run.mjs";
|
|
21
13
|
import { login } from "./login.mjs";
|
|
22
|
-
import { top } from "./top.mjs";
|
|
23
|
-
import { usage } from "./usage.mjs";
|
|
24
14
|
import { balance } from "./balance.mjs";
|
|
25
|
-
import { health } from "./health.mjs";
|
|
26
15
|
import { runSettings } from "./settings.mjs";
|
|
27
|
-
import { checkLatestVersion, performUpdate } from "./update.mjs";
|
|
28
16
|
import { t } from "./i18n.mjs";
|
|
29
17
|
|
|
30
18
|
// 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
|
|
@@ -54,27 +42,21 @@ const AGENT_DESC = {
|
|
|
54
42
|
|
|
55
43
|
const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
|
|
56
44
|
|
|
57
|
-
// 최상위 메뉴 항목(순수 함수 — 테스트 대상).
|
|
58
|
-
// 로그인 항목도 없다 — CLI는 로그인 사용자용이라 mainMenu()가 진입 시점에 게이트로 처리하고,
|
|
59
|
-
// 로그인/로그아웃 전환은 설정>계정이 소유한다.
|
|
45
|
+
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
|
|
60
46
|
export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
|
|
61
|
-
const items = [];
|
|
47
|
+
const items = [{ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") }];
|
|
62
48
|
items.push({
|
|
63
49
|
id: "chat",
|
|
64
50
|
tool: "chat",
|
|
65
51
|
title: t("Chat with a model"),
|
|
66
52
|
desc: `${t("Start an interactive chat")}${savedModels.chat ? ` · ${t("model")}: ${savedModels.chat}` : ""}`,
|
|
67
53
|
});
|
|
68
|
-
items.push({ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") });
|
|
69
54
|
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
70
55
|
? `${t("Team")}: ${activeContext.org_name}`
|
|
71
56
|
: t("Personal");
|
|
72
57
|
items.push(
|
|
73
58
|
{ id: "models", title: t("Model list"), desc: t("All models — price and context included") },
|
|
74
|
-
{ id: "top", title: t("Leaderboard"), desc: t("Model rankings · provider share") },
|
|
75
|
-
{ id: "usage", title: t("Usage report"), desc: t("Spend over the last 30 days") },
|
|
76
59
|
{ id: "balance", title: t("Balance"), desc: `${t("Check your credit balance")} · ${contextDesc}` },
|
|
77
|
-
{ id: "health", title: t("Health check"), desc: t("Gateway connectivity · login status") },
|
|
78
60
|
{ id: "settings", title: t("Settings"), desc: t("Language, default models, account, updates") },
|
|
79
61
|
);
|
|
80
62
|
return items;
|
|
@@ -89,7 +71,7 @@ export function buildAgentItems({ installed, savedModels = {} }) {
|
|
|
89
71
|
tool,
|
|
90
72
|
title: `${r.label}${installed[tool] ? "" : autoInstallable(r) ? ` (${t("install")})` : ` (${t("not installed")})`}`,
|
|
91
73
|
desc: installed[tool]
|
|
92
|
-
? `${t(AGENT_DESC[tool] ?? "")}${savedModels[tool] ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`
|
|
74
|
+
? `${t(AGENT_DESC[tool] ?? "")}${savedModels[tool] && !["claude", "codex", "agy"].includes(tool) ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`
|
|
93
75
|
: autoInstallable(r)
|
|
94
76
|
? `${t("Press Enter to install now")} — ${r.install}`
|
|
95
77
|
: `${t("Manual install:")} ${hyperlink(r.install)}`,
|
|
@@ -124,9 +106,10 @@ function saveLastSelection(id) {
|
|
|
124
106
|
writeConfig(cfg);
|
|
125
107
|
}
|
|
126
108
|
|
|
127
|
-
export async function pickProviderForModel(
|
|
109
|
+
export async function pickProviderForModel(model, { input = process.stdin, output = process.stdout } = {}) {
|
|
110
|
+
const modelId = typeof model === "string" ? model : model?.id;
|
|
128
111
|
if (!modelId || modelId === "__view_all__") return modelId;
|
|
129
|
-
const providers =
|
|
112
|
+
const providers = [...new Set((typeof model === "string" ? [] : model.providers || []).map((p) => p.provider).filter(Boolean))];
|
|
130
113
|
if (!providers.length) return modelId;
|
|
131
114
|
|
|
132
115
|
const options = [
|
|
@@ -135,10 +118,10 @@ export async function pickProviderForModel(modelId, { input = process.stdin, out
|
|
|
135
118
|
title: `⚡ ${t("Auto (best route)")}`,
|
|
136
119
|
desc: t("PleumRouter auto-routing — lowest price, lowest latency, automatic failover"),
|
|
137
120
|
},
|
|
138
|
-
...providers.map((
|
|
139
|
-
id:
|
|
140
|
-
title:
|
|
141
|
-
desc: t("Pin to {provider} (@{provider})", { provider
|
|
121
|
+
...providers.map((provider) => ({
|
|
122
|
+
id: provider,
|
|
123
|
+
title: provider,
|
|
124
|
+
desc: t("Pin to {provider} (@{provider})", { provider }),
|
|
142
125
|
})),
|
|
143
126
|
];
|
|
144
127
|
|
|
@@ -155,9 +138,20 @@ export async function pickProviderForModel(modelId, { input = process.stdin, out
|
|
|
155
138
|
return `${modelId}@${chosen.id}`;
|
|
156
139
|
}
|
|
157
140
|
|
|
158
|
-
export async function pickModelFor(item, defaultModel) {
|
|
141
|
+
export async function pickModelFor(item, defaultModel, { supported = () => true, allowProvider = true } = {}) {
|
|
142
|
+
let allModels;
|
|
143
|
+
try {
|
|
144
|
+
allModels = codingModels(await fetchModels()).filter((m) => supported(m.id));
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (!allModels.length) {
|
|
150
|
+
console.log(`${style.red("✗")} ${t("No compatible coding models available")}`);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
159
153
|
for (;;) {
|
|
160
|
-
const core =
|
|
154
|
+
const core = coreCodingModels(allModels);
|
|
161
155
|
const items = [
|
|
162
156
|
...core,
|
|
163
157
|
{
|
|
@@ -178,13 +172,6 @@ export async function pickModelFor(item, defaultModel) {
|
|
|
178
172
|
|
|
179
173
|
let pickedModelId = m.id;
|
|
180
174
|
if (pickedModelId === "__view_all__") {
|
|
181
|
-
let allModels;
|
|
182
|
-
try {
|
|
183
|
-
allModels = await fetchModels();
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
175
|
const allPicked = await select(allModels, {
|
|
189
176
|
message: `${item.title} — ${t("select a model")}`,
|
|
190
177
|
formatItem: (x) => formatModelLabel(x),
|
|
@@ -195,113 +182,20 @@ export async function pickModelFor(item, defaultModel) {
|
|
|
195
182
|
pickedModelId = allPicked.id;
|
|
196
183
|
}
|
|
197
184
|
|
|
198
|
-
const
|
|
185
|
+
const selected = allModels.find((x) => x.id === pickedModelId);
|
|
186
|
+
const withProvider = allowProvider ? await pickProviderForModel(selected) : pickedModelId;
|
|
199
187
|
if (!withProvider) continue;
|
|
200
188
|
return withProvider;
|
|
201
189
|
}
|
|
202
190
|
}
|
|
203
191
|
|
|
204
|
-
export
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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-클릭 확인 메뉴: Enter 1회로 기존 설정대로 즉시 실행
|
|
219
|
-
if (savedTiers && savedTiers.sonnet && savedTiers.opus && savedTiers.haiku) {
|
|
220
|
-
const keepChoice = await select(
|
|
221
|
-
[
|
|
222
|
-
{
|
|
223
|
-
id: "keep",
|
|
224
|
-
title: t("Keep existing configuration"),
|
|
225
|
-
desc: `Sonnet: ${savedTiers.sonnet} · Opus: ${savedTiers.opus} · Haiku: ${savedTiers.haiku}`,
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
id: "change",
|
|
229
|
-
title: t("Reconfigure tiers"),
|
|
230
|
-
desc: t("Choose Sonnet, Opus, and Haiku models again"),
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
{
|
|
234
|
-
message: `${agentItem.title} — ${t("Use existing model configuration?")}`,
|
|
235
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
236
|
-
limit: 5,
|
|
237
|
-
input,
|
|
238
|
-
output,
|
|
239
|
-
},
|
|
240
|
-
);
|
|
241
|
-
if (!keepChoice) return null;
|
|
242
|
-
if (keepChoice.id === "keep") {
|
|
243
|
-
return savedTiers;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 1/3단계: Sonnet 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
248
|
-
const sonnet = await pickModelFor(
|
|
249
|
-
{ title: `${agentItem.title} (1/3) — ${t("Sonnet model")}` },
|
|
250
|
-
baseTiers.sonnet,
|
|
251
|
-
);
|
|
252
|
-
if (!sonnet) return null;
|
|
253
|
-
|
|
254
|
-
// 2/3단계: Opus 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
255
|
-
const opus = await pickModelFor(
|
|
256
|
-
{ title: `${agentItem.title} (2/3) — ${t("Opus model")}` },
|
|
257
|
-
baseTiers.opus,
|
|
258
|
-
);
|
|
259
|
-
if (!opus) return null;
|
|
260
|
-
|
|
261
|
-
// 3/3단계: Haiku 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
262
|
-
const haiku = await pickModelFor(
|
|
263
|
-
{ title: `${agentItem.title} (3/3) — ${t("Haiku model")}` },
|
|
264
|
-
baseTiers.haiku,
|
|
265
|
-
);
|
|
266
|
-
if (!haiku) return null;
|
|
267
|
-
|
|
268
|
-
const resultTiers = { sonnet, opus, haiku };
|
|
269
|
-
cfg.claude_tiers = resultTiers;
|
|
270
|
-
writeConfig(cfg);
|
|
271
|
-
return resultTiers;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export async function pickCodexEffort(defaultEffort = "medium") {
|
|
275
|
-
const options = [
|
|
276
|
-
{ id: "medium", title: `⚡ ${t("Medium (Recommended)")}`, desc: t("Balanced reasoning speed and depth") },
|
|
277
|
-
{ id: "high", title: `🔥 ${t("High (Deep reasoning)")}`, desc: t("Maximum reasoning depth for complex tasks") },
|
|
278
|
-
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Lightweight reasoning for quick answers") },
|
|
192
|
+
export function agentModeItems(tool) {
|
|
193
|
+
const native = tool === "claude" ? "Anthropic official models"
|
|
194
|
+
: tool === "codex" ? "OpenAI official models" : "Antigravity default models";
|
|
195
|
+
return [
|
|
196
|
+
{ id: "native", title: t(native), desc: t("Choose models inside the agent") },
|
|
197
|
+
{ id: "custom", title: t("Choose my model (beta)"), desc: t("Select a PleumRouter coding model") },
|
|
279
198
|
];
|
|
280
|
-
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
281
|
-
const picked = await select(options, {
|
|
282
|
-
message: `Codex — ${t("Select reasoning effort")}`,
|
|
283
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
284
|
-
limit: 5,
|
|
285
|
-
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
286
|
-
});
|
|
287
|
-
return picked ? picked.id : null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export async function pickAgyEffort(defaultEffort = "high") {
|
|
291
|
-
const options = [
|
|
292
|
-
{ id: "high", title: `⚡ ${t("High (Recommended)")}`, desc: t("High reasoning effort for standard tasks") },
|
|
293
|
-
{ id: "max", title: `🚀 ${t("Max (Maximum reasoning)")}`, desc: t("Full reasoning effort for difficult problems") },
|
|
294
|
-
{ id: "medium", title: `⚖️ ${t("Medium (Standard)")}`, desc: t("Balanced reasoning effort") },
|
|
295
|
-
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Low reasoning effort for rapid iteration") },
|
|
296
|
-
];
|
|
297
|
-
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
298
|
-
const picked = await select(options, {
|
|
299
|
-
message: `Antigravity CLI (agy) — ${t("Select reasoning effort")}`,
|
|
300
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
301
|
-
limit: 6,
|
|
302
|
-
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
303
|
-
});
|
|
304
|
-
return picked ? picked.id : null;
|
|
305
199
|
}
|
|
306
200
|
|
|
307
201
|
// 미설치 도구: ollama처럼 확인 후 그 자리에서 설치(stdio 상속). 성공 시 true.
|
|
@@ -466,7 +360,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
466
360
|
else if (key.type === "down" || (key.type === "char" && key.value === "j")) move(1);
|
|
467
361
|
else if (key.type === "right" || (key.type === "char" && key.value === "l")) {
|
|
468
362
|
const it = items[cursor];
|
|
469
|
-
if (!it.tool || it.manual
|
|
363
|
+
if (!it.tool || it.manual || ["claude", "codex", "agy", "opencode"].includes(it.tool)) continue;
|
|
470
364
|
return void (await configure(it));
|
|
471
365
|
} else continue;
|
|
472
366
|
dirty = true;
|
|
@@ -483,7 +377,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
483
377
|
});
|
|
484
378
|
}
|
|
485
379
|
|
|
486
|
-
async function menuSelect(
|
|
380
|
+
async function menuSelect() {
|
|
487
381
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
488
382
|
const cfg = readConfig();
|
|
489
383
|
const items = buildItems({
|
|
@@ -493,19 +387,9 @@ async function menuSelect(updateInfo = null) {
|
|
|
493
387
|
const last = cfg?.last_selection;
|
|
494
388
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
495
389
|
const headerLines = [`${style.bold(style.plum("PleumRouter"))} ${style.dim(`pleum ${version}`)}`];
|
|
496
|
-
if (updateInfo?.isOutdated) {
|
|
497
|
-
headerLines.push(
|
|
498
|
-
style.yellow(
|
|
499
|
-
`💡 ${t("Update available: v{current} → v{latest}. Run 'pleum update' to update.", {
|
|
500
|
-
current: updateInfo.currentVersion,
|
|
501
|
-
latest: updateInfo.latestVersion,
|
|
502
|
-
})}`,
|
|
503
|
-
),
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
390
|
return runList(items, {
|
|
507
391
|
header: headerLines.join("\n"),
|
|
508
|
-
footer: t("↑/↓ move • enter
|
|
392
|
+
footer: t("↑/↓ move • enter select • esc quit"),
|
|
509
393
|
initialCursor,
|
|
510
394
|
});
|
|
511
395
|
}
|
|
@@ -517,18 +401,14 @@ async function agentSelect() {
|
|
|
517
401
|
const items = buildAgentItems({ installed, savedModels: readConfig()?.launch_models ?? {} });
|
|
518
402
|
return runList(items, {
|
|
519
403
|
header: style.bold(style.plum(t("Run an agent via PleumRouter"))),
|
|
520
|
-
footer: t("↑/↓ move • enter
|
|
404
|
+
footer: t("↑/↓ move • enter select • esc back"),
|
|
521
405
|
});
|
|
522
406
|
}
|
|
523
407
|
|
|
524
|
-
//
|
|
525
|
-
// 에이전트를 고르면 곧바로 모델/티어/추론 피커를 띄워 기본 모델 또는
|
|
526
|
-
// 선택한 우리 모델로 코딩 에이전트를 실행한다.
|
|
408
|
+
// bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유한다.
|
|
527
409
|
export async function runAgentMenu({ loop = false } = {}) {
|
|
528
410
|
const io = process.stdin;
|
|
529
411
|
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
530
|
-
if (!isLoggedIn()) await login();
|
|
531
|
-
else await touchSession().catch(() => {});
|
|
532
412
|
|
|
533
413
|
for (;;) {
|
|
534
414
|
const agentPicked = await agentSelect();
|
|
@@ -548,54 +428,34 @@ export async function runAgentMenu({ loop = false } = {}) {
|
|
|
548
428
|
continue;
|
|
549
429
|
}
|
|
550
430
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
saveModel("agy", chosenModel);
|
|
581
|
-
const cfg = readConfig() ?? {};
|
|
582
|
-
const effort = await pickAgyEffort(cfg.agy_effort || "high");
|
|
583
|
-
if (!effort) continue;
|
|
584
|
-
cfg.agy_effort = effort;
|
|
585
|
-
writeConfig(cfg);
|
|
586
|
-
launchModelPayload = { model: chosenModel, effort };
|
|
587
|
-
} else {
|
|
588
|
-
let chosenModel = agentPicked.model;
|
|
589
|
-
if (!chosenModel) {
|
|
590
|
-
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
591
|
-
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
592
|
-
if (!chosenModel) continue;
|
|
593
|
-
}
|
|
594
|
-
saveModel(agentItem.tool, chosenModel);
|
|
595
|
-
launchModelPayload = chosenModel;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
await launch(agentItem.tool, launchModelPayload, [], { exit: false });
|
|
431
|
+
let model = agentPicked.model;
|
|
432
|
+
if (["claude", "codex", "agy"].includes(agentItem.tool)) {
|
|
433
|
+
const mode = await select(agentModeItems(agentItem.tool), {
|
|
434
|
+
message: agentItem.title,
|
|
435
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
436
|
+
limit: 4,
|
|
437
|
+
});
|
|
438
|
+
if (!mode) continue;
|
|
439
|
+
if (mode.id === "custom") {
|
|
440
|
+
let supported = () => true;
|
|
441
|
+
if (agentItem.tool === "agy") {
|
|
442
|
+
try {
|
|
443
|
+
const ids = listAgyModelIds();
|
|
444
|
+
supported = (id) => supportsAgyModel(id, ids);
|
|
445
|
+
console.log(style.dim(t("Antigravity custom supports only verified Gemini models in API-key mode.")));
|
|
446
|
+
} catch (e) {
|
|
447
|
+
console.log(`${style.red("✗")} ${t("Antigravity model discovery failed: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const saved = readConfig()?.launch_models?.[agentItem.tool];
|
|
452
|
+
const selected = await pickModelFor(agentItem, saved, { supported, allowProvider: agentItem.tool !== "agy" });
|
|
453
|
+
if (!selected) continue;
|
|
454
|
+
saveModel(agentItem.tool, selected);
|
|
455
|
+
model = { mode: "custom", model: selected };
|
|
456
|
+
} else model = { mode: "native" };
|
|
457
|
+
} else if (agentPicked.model) saveModel(agentItem.tool, agentPicked.model);
|
|
458
|
+
await launch(agentItem.tool, model, [], { exit: false });
|
|
599
459
|
console.log();
|
|
600
460
|
if (!loop) return true;
|
|
601
461
|
}
|
|
@@ -608,30 +468,8 @@ export async function mainMenu() {
|
|
|
608
468
|
// runList()와 같은 3조건을 쓴다. 이 가드가 없으면 `pleum | cat`이 브라우저를 열고 2분 매달린다.
|
|
609
469
|
const io = process.stdin;
|
|
610
470
|
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
611
|
-
// CLI는 로그인 사용자용이다 — 미로그인 진입이면 여기서 auth 로그인을 태운다.
|
|
612
|
-
// login()의 실패(취소·2분 타임아웃·state 불일치)는 그대로 던져 bin/pleum.mjs의
|
|
613
|
-
// 최상위 catch가 `✗ <메시지>` + exit 1로 처리하게 둔다(메뉴를 띄우지 않는다).
|
|
614
|
-
if (!isLoggedIn()) await login();
|
|
615
|
-
else await touchSession().catch(() => {});
|
|
616
|
-
|
|
617
|
-
let updateInfo = null;
|
|
618
|
-
try {
|
|
619
|
-
updateInfo = await checkLatestVersion();
|
|
620
|
-
if (updateInfo?.isOutdated) {
|
|
621
|
-
if (isAutoUpdateEnabled()) {
|
|
622
|
-
console.log(`💡 ${t("Update available: v{current} → v{latest}", { current: updateInfo.currentVersion, latest: updateInfo.latestVersion })}`);
|
|
623
|
-
console.log(`▶ ${t("Auto-updating PleumRouter CLI...")}\n`);
|
|
624
|
-
const code = await performUpdate();
|
|
625
|
-
if (code === 0) {
|
|
626
|
-
console.log(`\n✓ ${t("Update completed! Please run `pleum` again.")}`);
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
} catch {}
|
|
632
|
-
|
|
633
471
|
for (;;) {
|
|
634
|
-
const picked = await menuSelect(
|
|
472
|
+
const picked = await menuSelect();
|
|
635
473
|
if (picked === undefined) return false; // 비TTY
|
|
636
474
|
if (picked === null || picked === "quit") return true; // esc/q/ctrl+c — ollama처럼 조용히 종료
|
|
637
475
|
|
|
@@ -642,6 +480,7 @@ export async function mainMenu() {
|
|
|
642
480
|
|
|
643
481
|
switch (item.id) {
|
|
644
482
|
case "chat": {
|
|
483
|
+
if (!isLoggedIn()) await login();
|
|
645
484
|
const m = model ?? saved.chat;
|
|
646
485
|
await run(m ? [m] : []); // REPL 종료는 run 내부 process.exit — 메뉴 복귀 없음
|
|
647
486
|
return true;
|
|
@@ -653,20 +492,6 @@ export async function mainMenu() {
|
|
|
653
492
|
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
654
493
|
}
|
|
655
494
|
continue;
|
|
656
|
-
case "top":
|
|
657
|
-
try {
|
|
658
|
-
await top([]);
|
|
659
|
-
} catch (e) {
|
|
660
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
661
|
-
}
|
|
662
|
-
continue;
|
|
663
|
-
case "usage":
|
|
664
|
-
try {
|
|
665
|
-
await usage([]);
|
|
666
|
-
} catch (e) {
|
|
667
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
668
|
-
}
|
|
669
|
-
continue;
|
|
670
495
|
case "balance":
|
|
671
496
|
try {
|
|
672
497
|
await balance({ interactive: true, exitOnError: false });
|
|
@@ -674,13 +499,6 @@ export async function mainMenu() {
|
|
|
674
499
|
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
675
500
|
}
|
|
676
501
|
continue;
|
|
677
|
-
case "health":
|
|
678
|
-
try {
|
|
679
|
-
await health({ exitOnError: false });
|
|
680
|
-
} catch (e) {
|
|
681
|
-
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
682
|
-
}
|
|
683
|
-
continue;
|
|
684
502
|
case "settings": {
|
|
685
503
|
const r = await runSettings();
|
|
686
504
|
if (r === "quit") return true; // ctrl+c 또는 재설치(upgrade) 시작 — 전체 종료
|
package/src/models.mjs
CHANGED
|
@@ -2,129 +2,10 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
4
4
|
import { V1 } from "./config.mjs";
|
|
5
|
+
import { CORE_MODELS } from "./recipes.mjs";
|
|
5
6
|
import { style, padDisplay, divider, sanitizeTerminalText } from "./ansi.mjs";
|
|
6
7
|
import { t } from "./i18n.mjs";
|
|
7
8
|
|
|
8
|
-
export const CORE_MODEL_PROVIDERS = {
|
|
9
|
-
"deepseek-ai/DeepSeek-V4.1-Flash": [
|
|
10
|
-
{ id: "deepseek", name: "DeepSeek (Official)" },
|
|
11
|
-
{ id: "together", name: "Together AI" },
|
|
12
|
-
{ id: "fireworks", name: "Fireworks AI" },
|
|
13
|
-
{ id: "siliconflow", name: "SiliconFlow" },
|
|
14
|
-
],
|
|
15
|
-
"claude-sonnet-5": [
|
|
16
|
-
{ id: "anthropic", name: "Anthropic (Official)" },
|
|
17
|
-
{ id: "aws_bedrock", name: "AWS Bedrock" },
|
|
18
|
-
{ id: "gcp_vertex", name: "Google Cloud Vertex AI" },
|
|
19
|
-
],
|
|
20
|
-
"claude-opus-5-5": [
|
|
21
|
-
{ id: "anthropic", name: "Anthropic (Official)" },
|
|
22
|
-
{ id: "aws_bedrock", name: "AWS Bedrock" },
|
|
23
|
-
{ id: "gcp_vertex", name: "Google Cloud Vertex AI" },
|
|
24
|
-
],
|
|
25
|
-
"gpt-6-astra": [
|
|
26
|
-
{ id: "openai", name: "OpenAI (Official)" },
|
|
27
|
-
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
28
|
-
],
|
|
29
|
-
"gpt-6-sol": [
|
|
30
|
-
{ id: "openai", name: "OpenAI (Official)" },
|
|
31
|
-
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
32
|
-
],
|
|
33
|
-
"gpt-6-luna": [
|
|
34
|
-
{ id: "openai", name: "OpenAI (Official)" },
|
|
35
|
-
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
36
|
-
],
|
|
37
|
-
"gemini-3.8-flash": [
|
|
38
|
-
{ id: "google", name: "Google AI Studio (Official)" },
|
|
39
|
-
{ id: "vertex", name: "Google Cloud Vertex AI" },
|
|
40
|
-
],
|
|
41
|
-
"qwen3.8-max": [
|
|
42
|
-
{ id: "alibaba", name: "Alibaba DashScope (Official)" },
|
|
43
|
-
{ id: "together", name: "Together AI" },
|
|
44
|
-
{ id: "siliconflow", name: "SiliconFlow" },
|
|
45
|
-
],
|
|
46
|
-
"glm-5.3-flash": [
|
|
47
|
-
{ id: "zhipu", name: "Zhipu AI BigModel (Official)" },
|
|
48
|
-
{ id: "modelark", name: "ByteDance ModelArk" },
|
|
49
|
-
],
|
|
50
|
-
"kimi-k3": [
|
|
51
|
-
{ id: "moonshot", name: "Moonshot AI (Official)" },
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const CORE_MODEL_DEFAULTS = [
|
|
56
|
-
{
|
|
57
|
-
id: "deepseek-ai/DeepSeek-V4.1-Flash",
|
|
58
|
-
display_name: "DeepSeek V4.1 Flash (Ultra-fast coding)",
|
|
59
|
-
maker: "DeepSeek",
|
|
60
|
-
context_length: 131072,
|
|
61
|
-
pricing: { prompt: "0.00000014", completion: "0.00000028" },
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "claude-sonnet-5",
|
|
65
|
-
display_name: "Claude Sonnet 5 (Anthropic flagship coding)",
|
|
66
|
-
maker: "Anthropic",
|
|
67
|
-
context_length: 200000,
|
|
68
|
-
pricing: { prompt: "0.000003", completion: "0.000015" },
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: "claude-opus-5-5",
|
|
72
|
-
display_name: "Claude Opus 5.5 (Deep reasoning & architecture)",
|
|
73
|
-
maker: "Anthropic",
|
|
74
|
-
context_length: 200000,
|
|
75
|
-
pricing: { prompt: "0.000005", completion: "0.000025" },
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: "gpt-6-astra",
|
|
79
|
-
display_name: "GPT-6 Astra (OpenAI flagship reasoning)",
|
|
80
|
-
maker: "OpenAI",
|
|
81
|
-
context_length: 128000,
|
|
82
|
-
pricing: { prompt: "0.0000025", completion: "0.00001" },
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "gpt-6-sol",
|
|
86
|
-
display_name: "GPT-6 Sol (Fast coding & logic)",
|
|
87
|
-
maker: "OpenAI",
|
|
88
|
-
context_length: 128000,
|
|
89
|
-
pricing: { prompt: "0.000001", completion: "0.000004" },
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: "gpt-6-luna",
|
|
93
|
-
display_name: "GPT-6 Luna (Lightweight coding)",
|
|
94
|
-
maker: "OpenAI",
|
|
95
|
-
context_length: 128000,
|
|
96
|
-
pricing: { prompt: "0.0000005", completion: "0.000002" },
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: "gemini-3.8-flash",
|
|
100
|
-
display_name: "Gemini 3.8 Flash (1M context fast multimodal)",
|
|
101
|
-
maker: "Google",
|
|
102
|
-
context_length: 1048576,
|
|
103
|
-
pricing: { prompt: "0.00000035", completion: "0.0000014" },
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: "qwen3.8-max",
|
|
107
|
-
display_name: "Qwen 3.8 Max (Alibaba flagship coding)",
|
|
108
|
-
maker: "Alibaba",
|
|
109
|
-
context_length: 131072,
|
|
110
|
-
pricing: { prompt: "0.0000016", completion: "0.0000064" },
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: "glm-5.3-flash",
|
|
114
|
-
display_name: "GLM 5.3 Flash (Zhipu AI high-speed coding)",
|
|
115
|
-
maker: "Zhipu AI",
|
|
116
|
-
context_length: 128000,
|
|
117
|
-
pricing: { prompt: "0.0000001", completion: "0.0000002" },
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "kimi-k3",
|
|
121
|
-
display_name: "Kimi K3 (Moonshot 256K long-context coding)",
|
|
122
|
-
maker: "Moonshot",
|
|
123
|
-
context_length: 262144,
|
|
124
|
-
pricing: { prompt: "0.0000006", completion: "0.0000024" },
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
-
|
|
128
9
|
let memoryModelsCache = null;
|
|
129
10
|
let memoryCacheExpiry = 0;
|
|
130
11
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -133,19 +14,14 @@ function getCacheFilePath() {
|
|
|
133
14
|
return join(homedir(), ".pleum", "models-cache.json");
|
|
134
15
|
}
|
|
135
16
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const byId = new Map((liveList || []).map((m) => [m.id, m]));
|
|
145
|
-
return CORE_MODEL_DEFAULTS.map((def) => {
|
|
146
|
-
const live = byId.get(def.id);
|
|
147
|
-
return live ? { ...def, ...live } : { ...def };
|
|
148
|
-
});
|
|
17
|
+
// 코딩 에이전트는 텍스트 대화 API를 호출한다. 공개 카탈로그의 다른 생성 방식은 피커에서 제외한다.
|
|
18
|
+
export function codingModels(models) {
|
|
19
|
+
return models.filter((m) => m.is_active !== false && m.modality === "text" && m.operations?.includes("chat.generate"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function coreCodingModels(models) {
|
|
23
|
+
const byId = new Map(codingModels(models).map((m) => [m.id, m]));
|
|
24
|
+
return CORE_MODELS.map((id) => byId.get(id)).filter(Boolean);
|
|
149
25
|
}
|
|
150
26
|
|
|
151
27
|
// GET /v1/models 는 공개(인증 불필요). 슬러그는 provider/model 형식.
|