pleumrouter 0.5.44 → 0.5.46
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 +8 -3
- package/package.json +1 -1
- package/src/balance.mjs +38 -137
- package/src/config.mjs +5 -18
- package/src/interactive.mjs +130 -0
- package/src/launch.mjs +2 -6
- package/src/locales/es.mjs +41 -0
- package/src/locales/ja.mjs +41 -0
- package/src/locales/ko.mjs +41 -0
- package/src/locales/zh.mjs +41 -0
- package/src/menu.mjs +218 -5
- package/src/run.mjs +19 -2
- package/src/settings.mjs +3 -6
- package/src/wire.mjs +766 -70
- package/src/workspace.mjs +58 -1
package/src/locales/ko.mjs
CHANGED
|
@@ -233,6 +233,7 @@ export const KO = {
|
|
|
233
233
|
"{model} thinking…": "{model} 생각 중…",
|
|
234
234
|
"⏹ aborted": "⏹ 중단됨",
|
|
235
235
|
"⏹ generation aborted — the partial reply stays in context": "⏹ 생성 중단됨 — 부분 응답은 컨텍스트에 유지",
|
|
236
|
+
"partial reply kept in context": "중간까지 받은 응답을 컨텍스트에 남겼습니다",
|
|
236
237
|
"Select a model": "모델 선택",
|
|
237
238
|
"… {n} more — narrow with a search term": "… {n}개 더 — 검색어로 좁히기",
|
|
238
239
|
"Choose a number, or search (Enter to cancel): ": "번호 선택 또는 검색어 (Enter=취소): ",
|
|
@@ -275,6 +276,8 @@ export const KO = {
|
|
|
275
276
|
"Unsupported tool: {tool}. Use claude or codex.": "지원하지 않는 도구: {tool}. claude 또는 codex를 쓰세요.",
|
|
276
277
|
"Wrote {path}": "{path}에 썼습니다",
|
|
277
278
|
"Wired {tool}.": "{tool}을 배선했습니다.",
|
|
279
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "배선 키는 배선된 모든 도구와 launch가 공유하므로, 과금 공간을 바꾸면 전부에 적용됩니다.",
|
|
280
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: JSON 형식이 아닙니다 — 고치거나 삭제하세요. 아무것도 쓰지 않았습니다",
|
|
278
281
|
"Desktop key: {prefix}…": "데스크톱 키: {prefix}…",
|
|
279
282
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
280
283
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (/v1 없음), Bearer, plm_ 키를 붙여넣으세요.",
|
|
@@ -365,4 +368,42 @@ export const KO = {
|
|
|
365
368
|
"Could not add Pleum models to OpenCode favorites: {error}": "OpenCode 즐겨찾기에 Pleum 모델을 추가하지 못했습니다: {error}",
|
|
366
369
|
"↑/↓ move • enter select • esc quit": "↑/↓ 이동 • enter 선택 • esc 종료",
|
|
367
370
|
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "~/.gemini/antigravity-cli/settings.json에 modelProvider=gemini를 씁니다. 지원되는 Gemini 모델을 PleumRouter로 라우팅합니다.",
|
|
371
|
+
|
|
372
|
+
// Agent setup (wire)
|
|
373
|
+
"Select models": "모델 선택",
|
|
374
|
+
"Space toggle · Enter confirm · Esc cancel": "Space=선택/해제 · Enter=확정 · Esc=취소",
|
|
375
|
+
"Wire coding agents to use PleumRouter permanently": "코딩 에이전트를 PleumRouter에 영구 배선(wire)합니다",
|
|
376
|
+
"{n} wired": "{n}개 배선됨",
|
|
377
|
+
"Agent setup": "에이전트 세팅",
|
|
378
|
+
"Wired": "배선됨",
|
|
379
|
+
"Not wired": "미배선",
|
|
380
|
+
"API billing only": "API 과금만 연결",
|
|
381
|
+
"Reconfigure (change model or workspace)": "설정 변경 (모델·워크스페이스 다시 선택)",
|
|
382
|
+
"Update settings, model, or billing workspace": "설정, 모델 또는 과금 워크스페이스를 변경합니다",
|
|
383
|
+
"Unwire (restore original settings)": "배선 해제 (원래 설정 및 구독으로 복원)",
|
|
384
|
+
"Remove PleumRouter configuration and restore original state": "PleumRouter 설정을 제거하고 원래 상태로 되돌립니다",
|
|
385
|
+
"Choose action": "동작 선택",
|
|
386
|
+
"Unwired {tool}.": "{tool} 배선을 해제했습니다.",
|
|
387
|
+
"Notice before wiring": "배선 전 주의사항",
|
|
388
|
+
"To restore original settings anytime, run: pleum unwire {tool}": "언제든 원래 설정으로 되돌리려면 실행하세요: pleum unwire {tool}",
|
|
389
|
+
"Proceed with wiring {label}? [Y/n] ": "{label} 배선을 진행할까요? [Y/n] ",
|
|
390
|
+
"Canceled.": "취소되었습니다.",
|
|
391
|
+
"Register all active coding models": "전체 활성 코딩 모델 자동 등록",
|
|
392
|
+
"Auto-registers full PleumRouter coding catalog": "전체 활성 PleumRouter 코딩 모델 카탈로그를 자동 등록합니다",
|
|
393
|
+
"Register recommended Top 10 models": "추천 Top 10 코딩 모델만 등록",
|
|
394
|
+
"Registers Top 10 coding models": "추천 Top 10 코딩 모델을 등록합니다",
|
|
395
|
+
"Pick models manually (multi-select)": "모델 리스트 직접 다중 선택",
|
|
396
|
+
"Select specific models to include in config": "설정에 포함할 특정 모델들을 직접 고릅니다",
|
|
397
|
+
"Model configuration mode": "모델 설정 모드",
|
|
398
|
+
"Select default startup model": "시작 기본 모델 선택",
|
|
399
|
+
"API billing only (keep agent's default models)": "API 빌링만 연결 (에이전트 기본 모델 유지)",
|
|
400
|
+
"Route API billing to PleumRouter without locking a custom model": "커스텀 모델을 고정하지 않고 API 호출·과금만 PleumRouter로 연결합니다",
|
|
401
|
+
"Select a specific model": "사용할 모델 직접 선택",
|
|
402
|
+
"Pick a PleumRouter model to set as default": "PleumRouter 카탈로그에서 기본으로 사용할 모델을 고릅니다",
|
|
403
|
+
"Select billing workspace for this agent": "이 에이전트에 연결할 과금 워크스페이스 선택",
|
|
404
|
+
"Once wired, running `claude` (CLI or Desktop) will route all requests through PleumRouter API credits instead of your Claude Pro/Max subscription until you run `pleum unwire claude`.": "배선(Wire)하면 `pleum unwire claude`로 해제하기 전까지 터미널·Desktop의 `claude` 실행이 Claude Pro/Max 구독이 아닌 PleumRouter API 크레딧으로 호출·과금됩니다.",
|
|
405
|
+
"Once wired, running `codex` (CLI or Desktop) will route all requests through PleumRouter API credits instead of your ChatGPT Plus/Pro plan or existing OpenAI key until you run `pleum unwire codex`.": "배선(Wire)하면 `pleum unwire codex`로 해제하기 전까지 `codex` 실행이 ChatGPT Plus/Pro 플랜이나 기존 OpenAI 키 대신 PleumRouter API 크레딧으로 호출·과금됩니다.",
|
|
406
|
+
"Once wired, Antigravity CLI (`agy`) will use PleumRouter Gemini API mode and deduct PleumRouter credits instead of Google account login until you run `pleum unwire agy`.": "배선(Wire)하면 `pleum unwire agy`로 해제하기 전까지 Antigravity CLI(`agy`)가 Google 계정 로그인 대신 PleumRouter 크레딧으로 호출·과금됩니다.",
|
|
407
|
+
"Once wired, OpenCode (CLI, Desktop, IDE) will register PleumRouter in ~/.config/opencode/opencode.json and charge PleumRouter credits when Pleum models are used.": "배선(Wire)하면 ~/.config/opencode/opencode.json에 PleumRouter가 등록되며, Pleum 모델 사용 시 PleumRouter 크레딧이 차감됩니다.",
|
|
408
|
+
"Once wired, running `{tool}` will route API calls through PleumRouter and deduct PleumRouter credits until you run `pleum unwire {tool}`.": "배선(Wire)하면 `pleum unwire {tool}`로 해제하기 전까지 `{tool}` 실행 시 API 호출이 PleumRouter를 거치며 크레딧이 차감됩니다.",
|
|
368
409
|
};
|
package/src/locales/zh.mjs
CHANGED
|
@@ -169,6 +169,8 @@ export const ZH = {
|
|
|
169
169
|
"Unsupported tool: {tool}. Use claude or codex.": "不支持的工具:{tool}。请使用 claude 或 codex。",
|
|
170
170
|
"Wrote {path}": "已写入 {path}",
|
|
171
171
|
"Wired {tool}.": "已接线 {tool}。",
|
|
172
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "接入密钥由所有已接入的工具和 launch 共享,因此更改计费工作区会对全部生效。",
|
|
173
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: 不是有效的 JSON — 请修复或删除,未写入任何内容",
|
|
172
174
|
"Desktop key: {prefix}…": "桌面密钥:{prefix}…",
|
|
173
175
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
174
176
|
"Claude Code Desktop:Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference。Gateway base URL {root}(不要 /v1),Bearer,粘贴 plm_ 密钥。",
|
|
@@ -244,6 +246,7 @@ export const ZH = {
|
|
|
244
246
|
"↑/↓ move • enter set model • esc back": "↑/↓ 移动 • enter 设置模型 • esc 返回",
|
|
245
247
|
"⏹ aborted": "⏹ 已中止",
|
|
246
248
|
"⏹ generation aborted — the partial reply stays in context": "⏹ 生成已中止 — 部分回复保留在上下文中",
|
|
249
|
+
"partial reply kept in context": "已接收的部分回复已保留在上下文中",
|
|
247
250
|
"✓ installed": "✓ 已安装",
|
|
248
251
|
"✗ not installed —": "✗ 未安装 —",
|
|
249
252
|
"Log out": "登出",
|
|
@@ -306,4 +309,42 @@ export const ZH = {
|
|
|
306
309
|
"Could not add Pleum models to OpenCode favorites: {error}": "无法将 Pleum 模型加入 OpenCode 收藏:{error}",
|
|
307
310
|
"↑/↓ move • enter select • esc quit": "↑/↓ 移动 • enter 选择 • esc 退出",
|
|
308
311
|
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "将 modelProvider=gemini 写入 ~/.gemini/antigravity-cli/settings.json。支持的 Gemini 模型经由 PleumRouter 路由。",
|
|
312
|
+
|
|
313
|
+
// Agent setup (wire)
|
|
314
|
+
"Select models": "多选模型",
|
|
315
|
+
"Space toggle · Enter confirm · Esc cancel": "Space=勾选/取消 · Enter=确认 · Esc=取消",
|
|
316
|
+
"Wire coding agents to use PleumRouter permanently": "将编程代理永久连接到 PleumRouter (wire)",
|
|
317
|
+
"{n} wired": "已连接 {n} 个",
|
|
318
|
+
"Agent setup": "代理设置",
|
|
319
|
+
"Wired": "已连接",
|
|
320
|
+
"Not wired": "未连接",
|
|
321
|
+
"API billing only": "仅连接 API 计费",
|
|
322
|
+
"Reconfigure (change model or workspace)": "重新配置 (更改模型或工作区)",
|
|
323
|
+
"Update settings, model, or billing workspace": "更改配置、模型或计费工作区",
|
|
324
|
+
"Unwire (restore original settings)": "解除连接 (恢复原始设置与订阅)",
|
|
325
|
+
"Remove PleumRouter configuration and restore original state": "移除 PleumRouter 配置并恢复原始状态",
|
|
326
|
+
"Choose action": "选择操作",
|
|
327
|
+
"Unwired {tool}.": "已解除 {tool} 的连接。",
|
|
328
|
+
"Notice before wiring": "连接前注意事项",
|
|
329
|
+
"To restore original settings anytime, run: pleum unwire {tool}": "随时恢复原始设置请运行: pleum unwire {tool}",
|
|
330
|
+
"Proceed with wiring {label}? [Y/n] ": "是否继续连接 {label}?[Y/n] ",
|
|
331
|
+
"Canceled.": "已取消。",
|
|
332
|
+
"Register all active coding models": "自动注册所有可用编程模型",
|
|
333
|
+
"Auto-registers full PleumRouter coding catalog": "自动注册完整的 PleumRouter 编程模型目录",
|
|
334
|
+
"Register recommended Top 10 models": "仅注册推荐的 Top 10 编程模型",
|
|
335
|
+
"Registers Top 10 coding models": "注册推荐的 Top 10 编程模型",
|
|
336
|
+
"Pick models manually (multi-select)": "手动多选模型列表",
|
|
337
|
+
"Select specific models to include in config": "手动选择要包含在配置中的模型",
|
|
338
|
+
"Model configuration mode": "模型配置模式",
|
|
339
|
+
"Select default startup model": "选择默认启动模型",
|
|
340
|
+
"API billing only (keep agent's default models)": "仅连接 API 计费 (保留代理默认模型)",
|
|
341
|
+
"Route API billing to PleumRouter without locking a custom model": "不锁定自定义模型,仅将 API 调用与计费路由至 PleumRouter",
|
|
342
|
+
"Select a specific model": "直接选择要使用的模型",
|
|
343
|
+
"Pick a PleumRouter model to set as default": "从 PleumRouter 目录中选择默认模型",
|
|
344
|
+
"Select billing workspace for this agent": "选择此代理绑定的计费工作区",
|
|
345
|
+
"Once wired, running `claude` (CLI or Desktop) will route all requests through PleumRouter API credits instead of your Claude Pro/Max subscription until you run `pleum unwire claude`.": "连接(wire)后,在执行 `pleum unwire claude` 解除之前,运行 `claude` 将使用 PleumRouter API 积分计费,而不是您的 Claude Pro/Max 订阅。",
|
|
346
|
+
"Once wired, running `codex` (CLI or Desktop) will route all requests through PleumRouter API credits instead of your ChatGPT Plus/Pro plan or existing OpenAI key until you run `pleum unwire codex`.": "连接(wire)后,在执行 `pleum unwire codex` 解除之前,运行 `codex` 将使用 PleumRouter API 积分计费,而不是 ChatGPT Plus/Pro 或现有 OpenAI 密钥。",
|
|
347
|
+
"Once wired, Antigravity CLI (`agy`) will use PleumRouter Gemini API mode and deduct PleumRouter credits instead of Google account login until you run `pleum unwire agy`.": "连接(wire)后,在执行 `pleum unwire agy` 解除之前,Antigravity CLI(`agy`) 将使用 PleumRouter 积分计费,而不是 Google 账号登录。",
|
|
348
|
+
"Once wired, OpenCode (CLI, Desktop, IDE) will register PleumRouter in ~/.config/opencode/opencode.json and charge PleumRouter credits when Pleum models are used.": "连接(wire)后将在 ~/.config/opencode/opencode.json 中注册 PleumRouter,使用 Pleum 模型时将扣除积分。",
|
|
349
|
+
"Once wired, running `{tool}` will route API calls through PleumRouter and deduct PleumRouter credits until you run `pleum unwire {tool}`.": "连接(wire)后,在执行 `pleum unwire {tool}` 解除之前,运行 `{tool}` 的 API 调用将通过 PleumRouter 计费。",
|
|
309
350
|
};
|
package/src/menu.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
7
|
-
import { keyFeeder, select } from "./interactive.mjs";
|
|
7
|
+
import { keyFeeder, select, multiSelect, confirm } from "./interactive.mjs";
|
|
8
8
|
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
9
9
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
10
10
|
import { onPath, launch, listAgyModelIds, supportsAgyModel } from "./launch.mjs";
|
|
@@ -15,6 +15,8 @@ import { balance } from "./balance.mjs";
|
|
|
15
15
|
import { runSettings } from "./settings.mjs";
|
|
16
16
|
import { t } from "./i18n.mjs";
|
|
17
17
|
import { parseInstallCommand, runInstallCommand, printManualInstall } from "./install.mjs";
|
|
18
|
+
import { getWireStatus, wire, unwire, WIRE_WARNINGS } from "./wire.mjs";
|
|
19
|
+
import { selectAndBindWorkspace, workspaceLabel } from "./workspace.mjs";
|
|
18
20
|
|
|
19
21
|
// 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
|
|
20
22
|
// 번역이 확정돼, 같은 프로세스 안에서 언어를 바꿔도(Settings) 이 설명들만 예전 언어로 남는다.
|
|
@@ -56,17 +58,23 @@ const AGENT_DESC = {
|
|
|
56
58
|
const autoInstallable = (r) => !parseInstallCommand(r.install).manual;
|
|
57
59
|
|
|
58
60
|
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
|
|
59
|
-
export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
|
|
61
|
+
export function buildItems({ savedModels = {}, activeContext = { type: "personal" }, wiredCount = 0 } = {}) {
|
|
60
62
|
const items = [{ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") }];
|
|
63
|
+
const wireDesc = wiredCount > 0
|
|
64
|
+
? `${t("Wire coding agents to use PleumRouter permanently")} · ${t("{n} wired", { n: wiredCount })}`
|
|
65
|
+
: t("Wire coding agents to use PleumRouter permanently");
|
|
66
|
+
items.push({
|
|
67
|
+
id: "wire",
|
|
68
|
+
title: t("Agent setup"),
|
|
69
|
+
desc: wireDesc,
|
|
70
|
+
});
|
|
61
71
|
items.push({
|
|
62
72
|
id: "chat",
|
|
63
73
|
tool: "chat",
|
|
64
74
|
title: t("Chat with a model"),
|
|
65
75
|
desc: `${t("Start an interactive chat")}${savedModels.chat ? ` · ${t("model")}: ${savedModels.chat}` : ""}`,
|
|
66
76
|
});
|
|
67
|
-
const contextDesc = activeContext
|
|
68
|
-
? `${t("Team")}: ${activeContext.org_name}`
|
|
69
|
-
: t("Personal");
|
|
77
|
+
const contextDesc = workspaceLabel(activeContext);
|
|
70
78
|
items.push(
|
|
71
79
|
{ id: "models", title: t("Model list"), desc: t("All models — price and context included") },
|
|
72
80
|
{ id: "balance", title: t("Balance"), desc: `${t("Check your credit balance")} · ${contextDesc}` },
|
|
@@ -75,6 +83,32 @@ export function buildItems({ savedModels = {}, activeContext = { type: "personal
|
|
|
75
83
|
return items;
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
// 에이전트 세팅 서브메뉴 항목(순수 함수 — 테스트 대상).
|
|
87
|
+
export function buildWireAgentItems({ statusMap = {}, installed = {} } = {}) {
|
|
88
|
+
const items = [];
|
|
89
|
+
for (const [tool, r] of Object.entries(RECIPES)) {
|
|
90
|
+
const st = statusMap[tool] || {};
|
|
91
|
+
const isWired = Boolean(st.wired);
|
|
92
|
+
const badge = isWired ? style.green(`● ${t("Wired")}`) : style.dim(`○ ${t("Not wired")}`);
|
|
93
|
+
const modeDesc = st.mode === "billing"
|
|
94
|
+
? t("API billing only")
|
|
95
|
+
: (st.model ? `${t("model")}: ${st.model}` : (st.mode === "list" ? t("Model list") : ""));
|
|
96
|
+
const pathDesc = st.targetPath ? style.dim(`(${st.targetPath})`) : "";
|
|
97
|
+
|
|
98
|
+
items.push({
|
|
99
|
+
id: `wire:${tool}`,
|
|
100
|
+
tool,
|
|
101
|
+
label: r.label,
|
|
102
|
+
wired: isWired,
|
|
103
|
+
title: `${r.label} ${badge}`,
|
|
104
|
+
desc: isWired
|
|
105
|
+
? `${modeDesc ? `${modeDesc} · ` : ""}${pathDesc}`
|
|
106
|
+
: `${t(AGENT_DESC[tool] ?? "")} · ${pathDesc}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
// 에이전트 서브메뉴 항목(순수 함수 — 테스트 대상). RECIPES(자동 실행) + MANUAL(설정 안내만) 전부 포함.
|
|
79
113
|
export function buildAgentItems({ installed, savedModels = {} }) {
|
|
80
114
|
const items = [];
|
|
@@ -390,9 +424,12 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
390
424
|
async function menuSelect() {
|
|
391
425
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
392
426
|
const cfg = readConfig();
|
|
427
|
+
const wireStatus = getWireStatus();
|
|
428
|
+
const wiredCount = Object.values(wireStatus).filter((s) => s.wired).length;
|
|
393
429
|
const items = buildItems({
|
|
394
430
|
savedModels: cfg?.launch_models ?? {},
|
|
395
431
|
activeContext: getActiveContext(cfg),
|
|
432
|
+
wiredCount,
|
|
396
433
|
});
|
|
397
434
|
const last = cfg?.last_selection;
|
|
398
435
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
@@ -471,6 +508,177 @@ export async function runAgentMenu({ loop = false } = {}) {
|
|
|
471
508
|
}
|
|
472
509
|
}
|
|
473
510
|
|
|
511
|
+
// bare pleum의 "에이전트 세팅"과 pleum wire (인자 없음)가 공유한다.
|
|
512
|
+
export async function runWireMenu({ loop = true } = {}) {
|
|
513
|
+
const io = process.stdin;
|
|
514
|
+
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
515
|
+
|
|
516
|
+
for (;;) {
|
|
517
|
+
const statusMap = getWireStatus();
|
|
518
|
+
const installed = Object.fromEntries(
|
|
519
|
+
Object.entries(RECIPES).map(([tool, r]) => [tool, onPath(r.bin)]),
|
|
520
|
+
);
|
|
521
|
+
const items = buildWireAgentItems({ statusMap, installed });
|
|
522
|
+
|
|
523
|
+
const picked = await runList(items, {
|
|
524
|
+
header: `${style.bold(style.plum(t("Agent setup")))} ${style.dim(`· ${t("Wire coding agents to use PleumRouter permanently")}`)}`,
|
|
525
|
+
footer: t("↑/↓ move • enter select • esc back"),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (picked === "quit") return "quit";
|
|
529
|
+
if (!picked) return null;
|
|
530
|
+
|
|
531
|
+
const { item } = picked;
|
|
532
|
+
const tool = item.tool;
|
|
533
|
+
const r = RECIPES[tool];
|
|
534
|
+
if (!r) continue;
|
|
535
|
+
const currentStatus = statusMap[tool];
|
|
536
|
+
|
|
537
|
+
// 이미 배선되어 있는 경우 설정 변경 vs 배선 해제 선택
|
|
538
|
+
if (currentStatus?.wired) {
|
|
539
|
+
const action = await select([
|
|
540
|
+
{ id: "reconfigure", title: t("Reconfigure (change model or workspace)"), desc: t("Update settings, model, or billing workspace") },
|
|
541
|
+
{ id: "unwire", title: t("Unwire (restore original settings)"), desc: t("Remove PleumRouter configuration and restore original state") },
|
|
542
|
+
], {
|
|
543
|
+
message: `${r.label} — ${t("Choose action")}`,
|
|
544
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (!action) continue;
|
|
548
|
+
if (action.id === "unwire") {
|
|
549
|
+
await unwire([tool]);
|
|
550
|
+
console.log(`\n✓ ${t("Unwired {tool}.", { tool: r.label })}\n`);
|
|
551
|
+
if (!loop) return true;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Step 1: 주의사항 고지 및 확인
|
|
557
|
+
const warningMsg = WIRE_WARNINGS[tool]
|
|
558
|
+
? t(WIRE_WARNINGS[tool])
|
|
559
|
+
: t(WIRE_WARNINGS.default, { tool: r.label });
|
|
560
|
+
|
|
561
|
+
console.log();
|
|
562
|
+
console.log(style.bold(style.yellow(`⚠️ ${t("Notice before wiring")} (${r.label})`)));
|
|
563
|
+
console.log(` ${warningMsg}`);
|
|
564
|
+
console.log(style.dim(` ${t("To restore original settings anytime, run: pleum unwire {tool}", { tool })}`));
|
|
565
|
+
console.log();
|
|
566
|
+
|
|
567
|
+
const ok = await confirm(t("Proceed with wiring {label}? [Y/n] ", { label: r.label }), { defaultYes: true });
|
|
568
|
+
if (!ok) {
|
|
569
|
+
console.log(t("Canceled."));
|
|
570
|
+
if (!loop) return false;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Step 2: 로그인 확인 및 과금 워크스페이스 선택
|
|
575
|
+
if (!isLoggedIn()) {
|
|
576
|
+
await login();
|
|
577
|
+
}
|
|
578
|
+
const chosenContext = await selectAndBindWorkspace();
|
|
579
|
+
|
|
580
|
+
// Step 3: 모델 세팅 모드 선택
|
|
581
|
+
const isModelListTool = ["opencode", "crush", "pi"].includes(tool);
|
|
582
|
+
let selectedMode = "billing";
|
|
583
|
+
let selectedModel = undefined;
|
|
584
|
+
let selectedModels = [];
|
|
585
|
+
|
|
586
|
+
if (isModelListTool) {
|
|
587
|
+
const modeChoice = await select([
|
|
588
|
+
{ id: "all", title: t("Register all active coding models"), desc: t("Auto-registers full PleumRouter coding catalog") },
|
|
589
|
+
{ id: "top10", title: t("Register recommended Top 10 models"), desc: t("Registers Top 10 coding models") },
|
|
590
|
+
{ id: "custom", title: t("Pick models manually (multi-select)"), desc: t("Select specific models to include in config") },
|
|
591
|
+
], {
|
|
592
|
+
message: `${r.label} — ${t("Model configuration mode")}`,
|
|
593
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
594
|
+
});
|
|
595
|
+
if (!modeChoice) continue;
|
|
596
|
+
|
|
597
|
+
let allCoding = [];
|
|
598
|
+
try {
|
|
599
|
+
allCoding = codingModels(await fetchModels());
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (modeChoice.id === "all") {
|
|
606
|
+
selectedModels = allCoding.map((m) => m.id);
|
|
607
|
+
selectedMode = "list";
|
|
608
|
+
} else if (modeChoice.id === "top10") {
|
|
609
|
+
selectedModels = coreCodingModels(allCoding).map((m) => m.id);
|
|
610
|
+
selectedMode = "list";
|
|
611
|
+
} else {
|
|
612
|
+
const pickedModels = await multiSelect(allCoding, {
|
|
613
|
+
message: `${r.label} — ${t("Select models")}`,
|
|
614
|
+
formatItem: (m) => formatModelLabel(m),
|
|
615
|
+
getId: (m) => m.id,
|
|
616
|
+
});
|
|
617
|
+
if (!pickedModels || pickedModels.length === 0) continue;
|
|
618
|
+
selectedModels = pickedModels.map((m) => m.id);
|
|
619
|
+
selectedMode = "list";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (selectedModels.length > 1) {
|
|
623
|
+
const defaultChoice = await select(selectedModels.map((id) => ({ id })), {
|
|
624
|
+
message: `${r.label} — ${t("Select default startup model")}`,
|
|
625
|
+
formatItem: (x) => sanitizeTerminalText(x.id),
|
|
626
|
+
});
|
|
627
|
+
if (defaultChoice) selectedModel = defaultChoice.id;
|
|
628
|
+
} else if (selectedModels.length === 1) {
|
|
629
|
+
selectedModel = selectedModels[0];
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
const modeChoice = await select([
|
|
633
|
+
{
|
|
634
|
+
id: "billing",
|
|
635
|
+
title: t("API billing only (keep agent's default models)"),
|
|
636
|
+
desc: t("Route API billing to PleumRouter without locking a custom model"),
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
id: "custom",
|
|
640
|
+
title: t("Select a specific model"),
|
|
641
|
+
desc: t("Pick a PleumRouter model to set as default"),
|
|
642
|
+
},
|
|
643
|
+
], {
|
|
644
|
+
message: `${r.label} — ${t("Model configuration mode")}`,
|
|
645
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (!modeChoice) continue;
|
|
649
|
+
|
|
650
|
+
if (modeChoice.id === "custom") {
|
|
651
|
+
let supported = () => true;
|
|
652
|
+
if (tool === "agy") {
|
|
653
|
+
try {
|
|
654
|
+
const ids = listAgyModelIds();
|
|
655
|
+
supported = (id) => supportsAgyModel(id, ids);
|
|
656
|
+
} catch {}
|
|
657
|
+
}
|
|
658
|
+
const pickedModel = await pickModelFor({ title: r.label }, undefined, { supported, allowProvider: tool !== "agy" });
|
|
659
|
+
if (!pickedModel) continue;
|
|
660
|
+
selectedModel = pickedModel;
|
|
661
|
+
selectedMode = "custom";
|
|
662
|
+
} else {
|
|
663
|
+
selectedMode = "billing";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Step 4: 배선 적용
|
|
668
|
+
await wire([tool], {
|
|
669
|
+
model: selectedModel,
|
|
670
|
+
models: selectedModels,
|
|
671
|
+
billingOnly: selectedMode === "billing",
|
|
672
|
+
activeContext: chosenContext,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
console.log(`\n✓ ${t("Wired {tool}.", { tool: r.label })}`);
|
|
676
|
+
console.log(style.dim(`${t("To restore original settings anytime, run: pleum unwire {tool}", { tool })}\n`));
|
|
677
|
+
|
|
678
|
+
if (!loop) return true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
474
682
|
// bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
|
|
475
683
|
// ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
|
|
476
684
|
export async function mainMenu() {
|
|
@@ -519,6 +727,11 @@ export async function mainMenu() {
|
|
|
519
727
|
if (r === "quit") return true;
|
|
520
728
|
continue;
|
|
521
729
|
}
|
|
730
|
+
case "wire": {
|
|
731
|
+
const r = await runWireMenu({ loop: true });
|
|
732
|
+
if (r === "quit") return true;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
522
735
|
}
|
|
523
736
|
}
|
|
524
737
|
}
|
package/src/run.mjs
CHANGED
|
@@ -254,6 +254,7 @@ async function repl(initialModel, initialVerbose, {
|
|
|
254
254
|
let lastSigint = 0;
|
|
255
255
|
let pickerActive = false; // /model 셀렉터가 rl을 잠시 닫는 동안 close 핸들러 무시
|
|
256
256
|
let rl;
|
|
257
|
+
let queue = Promise.resolve(); // 입력 직렬화 큐 — /model 피커가 rl을 다시 만들어도 유지된다
|
|
257
258
|
|
|
258
259
|
const send = async (text) => {
|
|
259
260
|
let added = false;
|
|
@@ -277,7 +278,11 @@ async function repl(initialModel, initialVerbose, {
|
|
|
277
278
|
else messages.pop(); // 응답 없음(즉시 중단 등) — user 턴 되돌림
|
|
278
279
|
} catch (e) {
|
|
279
280
|
console.error(`\n${style.red("✗")} ${sanitizeTerminalText(e.message)}`);
|
|
280
|
-
if (
|
|
281
|
+
if (e.partialText) {
|
|
282
|
+
// 중간에 끊긴 답도 맥락에 남겨 다음 질문이 이어받게 한다.
|
|
283
|
+
messages.push({ role: "assistant", content: e.partialText });
|
|
284
|
+
console.error(style.dim(t("partial reply kept in context")));
|
|
285
|
+
} else if (added) messages.pop(); // 실패한 user 턴 제거
|
|
281
286
|
} finally {
|
|
282
287
|
currentAbort = null;
|
|
283
288
|
}
|
|
@@ -388,7 +393,10 @@ async function repl(initialModel, initialVerbose, {
|
|
|
388
393
|
history,
|
|
389
394
|
historySize: 1000,
|
|
390
395
|
});
|
|
391
|
-
|
|
396
|
+
// 생성 중 입력은 순서대로 대기한다. 여러 줄을 한 질문으로 보내려면 """ 로 감싼다.
|
|
397
|
+
rl.on("line", (l) => {
|
|
398
|
+
queue = queue.then(() => onLine(l)).catch((e) => console.error(`✗ ${sanitizeTerminalText(e.message)}`));
|
|
399
|
+
});
|
|
392
400
|
rl.on("close", onClose);
|
|
393
401
|
rl.on("SIGINT", onSigint);
|
|
394
402
|
return rl;
|
|
@@ -542,6 +550,7 @@ export async function streamChat(model, messages, {
|
|
|
542
550
|
let cost = null;
|
|
543
551
|
let firstAt = 0;
|
|
544
552
|
let aborted = false;
|
|
553
|
+
let streamError = null; // 서버가 스트림 중간에 보낸 에러 프레임
|
|
545
554
|
const decoder = new TextDecoder();
|
|
546
555
|
try {
|
|
547
556
|
for await (const chunk of res.body) {
|
|
@@ -555,6 +564,8 @@ export async function streamChat(model, messages, {
|
|
|
555
564
|
if (data === "[DONE]") continue;
|
|
556
565
|
try {
|
|
557
566
|
const j = JSON.parse(data);
|
|
567
|
+
// 여기서 throw하면 아래 catch가 삼키므로 기록만 해 두고 루프 뒤에 던진다.
|
|
568
|
+
if (j.error) streamError = { message: j.error.message || "stream error", partial: j.error.partial === true };
|
|
558
569
|
if (j.usage) usage = j.usage;
|
|
559
570
|
if (j.cost) cost = j.cost; // 마지막 청크 — {usd, krw, fx_rate, markup_rate}
|
|
560
571
|
const delta = j.choices?.[0]?.delta?.content;
|
|
@@ -583,6 +594,12 @@ export async function streamChat(model, messages, {
|
|
|
583
594
|
sp.stop();
|
|
584
595
|
md.finish();
|
|
585
596
|
if (aborted) console.error(style.dim(t("⏹ generation aborted — the partial reply stays in context")));
|
|
597
|
+
if (streamError) {
|
|
598
|
+
// 받은 부분 응답을 함께 실어, REPL이 끊긴 답도 맥락에 남길 수 있게 한다.
|
|
599
|
+
const err = new Error(sanitizeTerminalText(streamError.message));
|
|
600
|
+
err.partialText = full;
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
586
603
|
|
|
587
604
|
const ms = Date.now() - t0;
|
|
588
605
|
const genSec = (Date.now() - (firstAt || t0)) / 1000;
|
package/src/settings.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 즉시 참조하면 순환의 평가 순서에 따라 깨질 수 있다).
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
|
-
import { style
|
|
9
|
+
import { style } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
11
|
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
@@ -16,6 +16,7 @@ import { login, logout } from "./login.mjs";
|
|
|
16
16
|
import { completion } from "./completion.mjs";
|
|
17
17
|
import { lang } from "./lang.mjs";
|
|
18
18
|
import { switchCreditWorkspace } from "./balance.mjs";
|
|
19
|
+
import { workspaceLabel } from "./workspace.mjs";
|
|
19
20
|
import { runList, saveModel } from "./menu.mjs";
|
|
20
21
|
import { checkLatestVersion, isNewerVersion, runUpdateCommand } from "./update.mjs";
|
|
21
22
|
|
|
@@ -28,11 +29,7 @@ export function buildSettingsItems({
|
|
|
28
29
|
activeContext = { type: "personal" },
|
|
29
30
|
latestVersion = null,
|
|
30
31
|
}) {
|
|
31
|
-
const contextDesc = activeContext?.type === "workspace"
|
|
32
|
-
? `${t("Workspace")}: ${sanitizeTerminalText(activeContext.workspace_name || activeContext.workspace_id)}`
|
|
33
|
-
: activeContext?.type === "team"
|
|
34
|
-
? `${t("Team")}: ${activeContext.org_name || activeContext.org_id}`
|
|
35
|
-
: t("Personal Credit");
|
|
32
|
+
const contextDesc = activeContext?.type === "workspace" ? workspaceLabel(activeContext) : t("Personal Credit");
|
|
36
33
|
const updateDesc = latestVersion && isNewerVersion(version, latestVersion)
|
|
37
34
|
? `${t("Current version: {version}", { version })} → v${latestVersion} (${t("Update available")})`
|
|
38
35
|
: t("Current version: {version}", { version });
|