pleumrouter 0.5.44 → 0.5.45

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 CHANGED
@@ -16,7 +16,7 @@ import { top } from "../src/top.mjs";
16
16
  import { health } from "../src/health.mjs";
17
17
  import { completion } from "../src/completion.mjs";
18
18
  import { runUpdateCommand } from "../src/update.mjs";
19
- import { mainMenu, runAgentMenu } from "../src/menu.mjs";
19
+ import { mainMenu, runAgentMenu, runWireMenu } from "../src/menu.mjs";
20
20
  import { lang } from "../src/lang.mjs";
21
21
  import { RECIPES } from "../src/recipes.mjs";
22
22
  import { t } from "../src/i18n.mjs";
@@ -135,8 +135,13 @@ try {
135
135
  break;
136
136
  }
137
137
  case "wire":
138
- if (rest[0] === "-h" || rest[0] === "--help") wireHelp();
139
- else await wire(rest);
138
+ if (rest[0] === "-h" || rest[0] === "--help") {
139
+ wireHelp();
140
+ } else if (rest.length === 0 && process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function") {
141
+ await runWireMenu({ loop: false });
142
+ } else {
143
+ await wire(rest);
144
+ }
140
145
  break;
141
146
  case "unwire":
142
147
  if (rest[0] === "-h" || rest[0] === "--help") unwireHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.44",
3
+ "version": "0.5.45",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -211,3 +211,133 @@ export async function select(items, {
211
211
  render();
212
212
  });
213
213
  }
214
+
215
+ // 스페이스바 토글 · 타이핑 필터 · Enter 확정 · Esc/Ctrl+C 취소(null) 다중 선택기.
216
+ export async function multiSelect(items, {
217
+ message = t("Select models"),
218
+ formatItem = (x) => x?.title || x?.id || String(x),
219
+ getId = (x) => x?.id ?? String(x),
220
+ matches = (all, q) => (q ? all.filter((it) => String(formatItem(it)).toLowerCase().includes(q.toLowerCase())) : all),
221
+ initialSelected = [],
222
+ limit = 10,
223
+ input = process.stdin,
224
+ output = process.stdout,
225
+ } = {}) {
226
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return null;
227
+
228
+ return new Promise((resolve) => {
229
+ let query = "";
230
+ let cursor = 0;
231
+ let windowStart = 0;
232
+ let lastRows = 0;
233
+ const selectedIds = new Set(initialSelected);
234
+
235
+ const erase = () => {
236
+ if (!lastRows) return;
237
+ output.write("\r" + (lastRows > 1 ? `\x1b[${lastRows - 1}A` : "") + "\x1b[0J");
238
+ lastRows = 0;
239
+ };
240
+
241
+ const render = () => {
242
+ const pool = matches(items, query);
243
+ if (cursor >= pool.length) cursor = Math.max(0, pool.length - 1);
244
+ if (cursor < windowStart) {
245
+ windowStart = cursor;
246
+ } else if (cursor >= windowStart + limit) {
247
+ windowStart = cursor - limit + 1;
248
+ }
249
+ windowStart = Math.max(0, Math.min(windowStart, Math.max(0, pool.length - limit)));
250
+
251
+ const shown = pool.slice(windowStart, windowStart + limit);
252
+ const rawLines = [
253
+ `${style.plum("?")} ${style.bold(message)} ${style.dim(t("Space toggle · Enter confirm · Esc cancel"))}`,
254
+ `${style.gray("›")} ${query}`,
255
+ ];
256
+ if (windowStart > 0) {
257
+ rawLines.push(style.dim(` ${t("↑ {n} more above", { n: windowStart })}`));
258
+ }
259
+ rawLines.push(
260
+ ...shown.map((it, idx) => {
261
+ const itemIndex = windowStart + idx;
262
+ const id = getId(it);
263
+ const isSelected = selectedIds.has(id);
264
+ const checkMark = isSelected ? style.green("◉ ") : style.dim("◯ ");
265
+ return itemIndex === cursor
266
+ ? `${style.plum("❯")} ${checkMark}${style.bold(formatItem(it))}`
267
+ : ` ${checkMark}${style.dim(formatItem(it))}`;
268
+ }),
269
+ );
270
+ const remainingBelow = pool.length - (windowStart + shown.length);
271
+ if (remainingBelow > 0) {
272
+ rawLines.push(style.dim(` ${t("… {n} more — type to narrow down", { n: remainingBelow })}`));
273
+ }
274
+ if (!pool.length) rawLines.push(style.dim(` ${t("No matches")}`));
275
+ erase();
276
+ const lines = rawLines.flatMap((l) => String(l).split("\n"));
277
+ const frame = lines.join("\n");
278
+ output.write(frame);
279
+ lastRows = lines.reduce((n, l) => n + rowsFor(l), 0);
280
+ return pool;
281
+ };
282
+
283
+ const finish = (resultItems) => {
284
+ input.off("data", onData);
285
+ input.setRawMode(false);
286
+ input.pause();
287
+ output.write("\x1b[?25h");
288
+ erase();
289
+ if (resultItems != null && resultItems.length > 0) {
290
+ output.write(`${style.green("✓")} ${resultItems.map((x) => formatItem(x)).join(", ")}\n`);
291
+ }
292
+ resolve(resultItems);
293
+ };
294
+
295
+ const feed = keyFeeder();
296
+ const onData = (chunk) => {
297
+ let dirty = false;
298
+ for (const key of feed(chunk)) {
299
+ if (key.type === "ctrlc" || key.type === "escape") return finish(null);
300
+ if (key.type === "enter") {
301
+ const pool = matches(items, query);
302
+ if (selectedIds.size > 0) {
303
+ const picked = items.filter((it) => selectedIds.has(getId(it)));
304
+ return finish(picked.length > 0 ? picked : null);
305
+ }
306
+ const safeCursor = Math.max(0, Math.min(cursor, pool.length - 1));
307
+ return finish(pool[safeCursor] ? [pool[safeCursor]] : null);
308
+ }
309
+ if (key.type === "char" && key.value === " ") {
310
+ const pool = matches(items, query);
311
+ const current = pool[cursor];
312
+ if (current) {
313
+ const id = getId(current);
314
+ if (selectedIds.has(id)) selectedIds.delete(id);
315
+ else selectedIds.add(id);
316
+ }
317
+ } else if (key.type === "up") {
318
+ cursor = Math.max(0, cursor - 1);
319
+ } else if (key.type === "down") {
320
+ const pool = matches(items, query);
321
+ cursor = Math.min(Math.max(0, pool.length - 1), cursor + 1);
322
+ } else if (key.type === "backspace") {
323
+ query = query.slice(0, -1);
324
+ cursor = 0;
325
+ windowStart = 0;
326
+ } else if (key.type === "char") {
327
+ query += key.value;
328
+ cursor = 0;
329
+ windowStart = 0;
330
+ } else continue;
331
+ dirty = true;
332
+ }
333
+ if (dirty) render();
334
+ };
335
+
336
+ input.setEncoding("utf8");
337
+ input.setRawMode(true);
338
+ input.resume();
339
+ output.write("\x1b[?25l");
340
+ input.on("data", onData);
341
+ render();
342
+ });
343
+ }
@@ -306,4 +306,42 @@ export const ES = {
306
306
  "Could not add Pleum models to OpenCode favorites: {error}": "No se pudieron añadir modelos Pleum a favoritos de OpenCode: {error}",
307
307
  "↑/↓ move • enter select • esc quit": "↑/↓ mover • enter seleccionar • esc salir",
308
308
  "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json. Los modelos Gemini compatibles usan PleumRouter.",
309
+
310
+ // Agent setup (wire)
311
+ "Select models": "Seleccionar modelos",
312
+ "Space toggle · Enter confirm · Esc cancel": "Espacio=marcar · Enter=confirmar · Esc=cancelar",
313
+ "Wire coding agents to use PleumRouter permanently": "Conecta agentes de código a PleumRouter de forma permanente",
314
+ "{n} wired": "{n} conectados",
315
+ "Agent setup": "Configuración de agentes",
316
+ "Wired": "Conectado",
317
+ "Not wired": "No conectado",
318
+ "API billing only": "Solo facturación API",
319
+ "Reconfigure (change model or workspace)": "Reconfigurar (cambiar modelo o espacio)",
320
+ "Update settings, model, or billing workspace": "Modificar configuración, modelo o espacio de facturación",
321
+ "Unwire (restore original settings)": "Desconectar (restaurar configuración original)",
322
+ "Remove PleumRouter configuration and restore original state": "Eliminar la configuración de PleumRouter y restaurar el estado original",
323
+ "Choose action": "Elegir acción",
324
+ "Unwired {tool}.": "Se desconectó {tool}.",
325
+ "Notice before wiring": "Aviso antes de conectar",
326
+ "To restore original settings anytime, run: pleum unwire {tool}": "Para restaurar la configuración original en cualquier momento: pleum unwire {tool}",
327
+ "Proceed with wiring {label}? [Y/n] ": "¿Continuar con la conexión de {label}? [Y/n] ",
328
+ "Canceled.": "Cancelado.",
329
+ "Register all active coding models": "Registrar todos los modelos de código activos",
330
+ "Auto-registers full PleumRouter coding catalog": "Registra automáticamente todo el catálogo de modelos de PleumRouter",
331
+ "Register recommended Top 10 models": "Registrar los 10 modelos recomendados",
332
+ "Registers Top 10 coding models": "Registra los 10 mejores modelos de código",
333
+ "Pick models manually (multi-select)": "Seleccionar lista de modelos manualmente",
334
+ "Select specific models to include in config": "Elige manualmente los modelos que se incluirán en la configuración",
335
+ "Model configuration mode": "Modo de configuración de modelo",
336
+ "Select default startup model": "Seleccionar modelo predeterminado inicial",
337
+ "API billing only (keep agent's default models)": "Solo facturación API (mantener modelos predeterminados)",
338
+ "Route API billing to PleumRouter without locking a custom model": "Enruta la facturación API a PleumRouter sin fijar un modelo personalizado",
339
+ "Select a specific model": "Seleccionar un modelo específico",
340
+ "Pick a PleumRouter model to set as default": "Elige un modelo del catálogo de PleumRouter como predeterminado",
341
+ "Select billing workspace for this agent": "Selecciona el espacio de facturación para este agente",
342
+ "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`.": "Una vez conectado, ejecutar `claude` usará créditos API de PleumRouter en lugar de tu suscripción Claude Pro/Max hasta que ejecutes `pleum unwire claude`.",
343
+ "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`.": "Una vez conectado, ejecutar `codex` usará créditos API de PleumRouter en lugar de tu plan ChatGPT Plus/Pro o clave OpenAI hasta que ejecutes `pleum unwire codex`.",
344
+ "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`.": "Una vez conectado, Antigravity CLI (`agy`) usará créditos de PleumRouter en lugar del inicio de sesión de Google hasta que ejecutes `pleum unwire agy`.",
345
+ "Once wired, OpenCode (CLI, Desktop, IDE) will register PleumRouter in ~/.config/opencode/opencode.json and charge PleumRouter credits when Pleum models are used.": "Una vez conectado, OpenCode registrará PleumRouter en ~/.config/opencode/opencode.json y cobrará créditos al usar modelos de Pleum.",
346
+ "Once wired, running `{tool}` will route API calls through PleumRouter and deduct PleumRouter credits until you run `pleum unwire {tool}`.": "Una vez conectado, ejecutar `{tool}` enrutará las llamadas API por PleumRouter y descontará créditos hasta que ejecutes `pleum unwire {tool}`.",
309
347
  };
@@ -306,4 +306,42 @@ export const JA = {
306
306
  "Could not add Pleum models to OpenCode favorites: {error}": "OpenCode のお気に入りに Pleum モデルを追加できません: {error}",
307
307
  "↑/↓ move • enter select • esc quit": "↑/↓ 移動 • enter 選択 • esc 終了",
308
308
  "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "~/.gemini/antigravity-cli/settings.json に modelProvider=gemini を書き込みます。対応する Gemini モデルを PleumRouter 経由で使用します。",
309
+
310
+ // Agent setup (wire)
311
+ "Select models": "モデル複数選択",
312
+ "Space toggle · Enter confirm · Esc cancel": "Space=選択/解除 · Enter=確定 · Esc=キャンセル",
313
+ "Wire coding agents to use PleumRouter permanently": "コーディングエージェントをPleumRouterに常時接続(wire)します",
314
+ "{n} wired": "{n}個接続済み",
315
+ "Agent setup": "エージェント設定",
316
+ "Wired": "接続済み",
317
+ "Not wired": "未接続",
318
+ "API billing only": "API課金のみ接続",
319
+ "Reconfigure (change model or workspace)": "設定変更 (モデル・ワークスペースの再選択)",
320
+ "Update settings, model, or billing workspace": "設定、モデル、または課金ワークスペースを変更します",
321
+ "Unwire (restore original settings)": "接続解除 (元の設定・サブスクリプションに戻す)",
322
+ "Remove PleumRouter configuration and restore original state": "PleumRouterの設定を削除し、元の状態に戻します",
323
+ "Choose action": "動作を選択",
324
+ "Unwired {tool}.": "{tool}の接続を解除しました。",
325
+ "Notice before wiring": "接続前の注意事項",
326
+ "To restore original settings anytime, run: pleum unwire {tool}": "元の設定に戻すにはいつでも実行してください: pleum unwire {tool}",
327
+ "Proceed with wiring {label}? [Y/n] ": "{label} の接続(wire)を続行しますか? [Y/n] ",
328
+ "Canceled.": "キャンセルされました。",
329
+ "Register all active coding models": "有効な全コーディングモデルを自動登録",
330
+ "Auto-registers full PleumRouter coding catalog": "有効な全PleumRouterコーディングモデルカタログを自動登録します",
331
+ "Register recommended Top 10 models": "おすすめ Top 10 モデルのみ登録",
332
+ "Registers Top 10 coding models": "おすすめ Top 10 コーディングモデルを登録します",
333
+ "Pick models manually (multi-select)": "モデルリストを手動で複数選択",
334
+ "Select specific models to include in config": "設定に含めるモデルを手動で選択します",
335
+ "Model configuration mode": "モデル設定モード",
336
+ "Select default startup model": "起動時の既定モデルを選択",
337
+ "API billing only (keep agent's default models)": "API課金のみ接続 (エージェントの標準モデルを維持)",
338
+ "Route API billing to PleumRouter without locking a custom model": "カスタムモデルを固定せず、API呼び出しと課金のみをPleumRouterへ接続します",
339
+ "Select a specific model": "使用するモデルを直接選択",
340
+ "Pick a PleumRouter model to set as default": "PleumRouter カタログから既定のモデルを選択します",
341
+ "Select billing workspace for this agent": "このエージェントに紐付ける課金ワークスペースを選択",
342
+ "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` の実行は Claude Pro/Max サブスクリプションではなく PleumRouter API クレジットで課金されます。",
343
+ "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 クレジットで課金されます。",
344
+ "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 クレジットで課金されます。",
345
+ "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 モデル使用時にクレジットが消費されます。",
346
+ "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
347
  };
@@ -365,4 +365,42 @@ export const KO = {
365
365
  "Could not add Pleum models to OpenCode favorites: {error}": "OpenCode 즐겨찾기에 Pleum 모델을 추가하지 못했습니다: {error}",
366
366
  "↑/↓ move • enter select • esc quit": "↑/↓ 이동 • enter 선택 • esc 종료",
367
367
  "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "~/.gemini/antigravity-cli/settings.json에 modelProvider=gemini를 씁니다. 지원되는 Gemini 모델을 PleumRouter로 라우팅합니다.",
368
+
369
+ // Agent setup (wire)
370
+ "Select models": "모델 선택",
371
+ "Space toggle · Enter confirm · Esc cancel": "Space=선택/해제 · Enter=확정 · Esc=취소",
372
+ "Wire coding agents to use PleumRouter permanently": "코딩 에이전트를 PleumRouter에 영구 배선(wire)합니다",
373
+ "{n} wired": "{n}개 배선됨",
374
+ "Agent setup": "에이전트 세팅",
375
+ "Wired": "배선됨",
376
+ "Not wired": "미배선",
377
+ "API billing only": "API 과금만 연결",
378
+ "Reconfigure (change model or workspace)": "설정 변경 (모델·워크스페이스 다시 선택)",
379
+ "Update settings, model, or billing workspace": "설정, 모델 또는 과금 워크스페이스를 변경합니다",
380
+ "Unwire (restore original settings)": "배선 해제 (원래 설정 및 구독으로 복원)",
381
+ "Remove PleumRouter configuration and restore original state": "PleumRouter 설정을 제거하고 원래 상태로 되돌립니다",
382
+ "Choose action": "동작 선택",
383
+ "Unwired {tool}.": "{tool} 배선을 해제했습니다.",
384
+ "Notice before wiring": "배선 전 주의사항",
385
+ "To restore original settings anytime, run: pleum unwire {tool}": "언제든 원래 설정으로 되돌리려면 실행하세요: pleum unwire {tool}",
386
+ "Proceed with wiring {label}? [Y/n] ": "{label} 배선을 진행할까요? [Y/n] ",
387
+ "Canceled.": "취소되었습니다.",
388
+ "Register all active coding models": "전체 활성 코딩 모델 자동 등록",
389
+ "Auto-registers full PleumRouter coding catalog": "전체 활성 PleumRouter 코딩 모델 카탈로그를 자동 등록합니다",
390
+ "Register recommended Top 10 models": "추천 Top 10 코딩 모델만 등록",
391
+ "Registers Top 10 coding models": "추천 Top 10 코딩 모델을 등록합니다",
392
+ "Pick models manually (multi-select)": "모델 리스트 직접 다중 선택",
393
+ "Select specific models to include in config": "설정에 포함할 특정 모델들을 직접 고릅니다",
394
+ "Model configuration mode": "모델 설정 모드",
395
+ "Select default startup model": "시작 기본 모델 선택",
396
+ "API billing only (keep agent's default models)": "API 빌링만 연결 (에이전트 기본 모델 유지)",
397
+ "Route API billing to PleumRouter without locking a custom model": "커스텀 모델을 고정하지 않고 API 호출·과금만 PleumRouter로 연결합니다",
398
+ "Select a specific model": "사용할 모델 직접 선택",
399
+ "Pick a PleumRouter model to set as default": "PleumRouter 카탈로그에서 기본으로 사용할 모델을 고릅니다",
400
+ "Select billing workspace for this agent": "이 에이전트에 연결할 과금 워크스페이스 선택",
401
+ "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 크레딧으로 호출·과금됩니다.",
402
+ "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 크레딧으로 호출·과금됩니다.",
403
+ "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 크레딧으로 호출·과금됩니다.",
404
+ "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 크레딧이 차감됩니다.",
405
+ "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
406
  };
@@ -306,4 +306,42 @@ export const ZH = {
306
306
  "Could not add Pleum models to OpenCode favorites: {error}": "无法将 Pleum 模型加入 OpenCode 收藏:{error}",
307
307
  "↑/↓ move • enter select • esc quit": "↑/↓ 移动 • enter 选择 • esc 退出",
308
308
  "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "将 modelProvider=gemini 写入 ~/.gemini/antigravity-cli/settings.json。支持的 Gemini 模型经由 PleumRouter 路由。",
309
+
310
+ // Agent setup (wire)
311
+ "Select models": "多选模型",
312
+ "Space toggle · Enter confirm · Esc cancel": "Space=勾选/取消 · Enter=确认 · Esc=取消",
313
+ "Wire coding agents to use PleumRouter permanently": "将编程代理永久连接到 PleumRouter (wire)",
314
+ "{n} wired": "已连接 {n} 个",
315
+ "Agent setup": "代理设置",
316
+ "Wired": "已连接",
317
+ "Not wired": "未连接",
318
+ "API billing only": "仅连接 API 计费",
319
+ "Reconfigure (change model or workspace)": "重新配置 (更改模型或工作区)",
320
+ "Update settings, model, or billing workspace": "更改配置、模型或计费工作区",
321
+ "Unwire (restore original settings)": "解除连接 (恢复原始设置与订阅)",
322
+ "Remove PleumRouter configuration and restore original state": "移除 PleumRouter 配置并恢复原始状态",
323
+ "Choose action": "选择操作",
324
+ "Unwired {tool}.": "已解除 {tool} 的连接。",
325
+ "Notice before wiring": "连接前注意事项",
326
+ "To restore original settings anytime, run: pleum unwire {tool}": "随时恢复原始设置请运行: pleum unwire {tool}",
327
+ "Proceed with wiring {label}? [Y/n] ": "是否继续连接 {label}?[Y/n] ",
328
+ "Canceled.": "已取消。",
329
+ "Register all active coding models": "自动注册所有可用编程模型",
330
+ "Auto-registers full PleumRouter coding catalog": "自动注册完整的 PleumRouter 编程模型目录",
331
+ "Register recommended Top 10 models": "仅注册推荐的 Top 10 编程模型",
332
+ "Registers Top 10 coding models": "注册推荐的 Top 10 编程模型",
333
+ "Pick models manually (multi-select)": "手动多选模型列表",
334
+ "Select specific models to include in config": "手动选择要包含在配置中的模型",
335
+ "Model configuration mode": "模型配置模式",
336
+ "Select default startup model": "选择默认启动模型",
337
+ "API billing only (keep agent's default models)": "仅连接 API 计费 (保留代理默认模型)",
338
+ "Route API billing to PleumRouter without locking a custom model": "不锁定自定义模型,仅将 API 调用与计费路由至 PleumRouter",
339
+ "Select a specific model": "直接选择要使用的模型",
340
+ "Pick a PleumRouter model to set as default": "从 PleumRouter 目录中选择默认模型",
341
+ "Select billing workspace for this agent": "选择此代理绑定的计费工作区",
342
+ "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 订阅。",
343
+ "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 密钥。",
344
+ "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 账号登录。",
345
+ "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 模型时将扣除积分。",
346
+ "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
347
  };
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 } from "./workspace.mjs";
18
20
 
19
21
  // 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
20
22
  // 번역이 확정돼, 같은 프로세스 안에서 언어를 바꿔도(Settings) 이 설명들만 예전 언어로 남는다.
@@ -56,8 +58,16 @@ 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",
@@ -75,6 +85,32 @@ export function buildItems({ savedModels = {}, activeContext = { type: "personal
75
85
  return items;
76
86
  }
77
87
 
88
+ // 에이전트 세팅 서브메뉴 항목(순수 함수 — 테스트 대상).
89
+ export function buildWireAgentItems({ statusMap = {}, installed = {} } = {}) {
90
+ const items = [];
91
+ for (const [tool, r] of Object.entries(RECIPES)) {
92
+ const st = statusMap[tool] || {};
93
+ const isWired = Boolean(st.wired);
94
+ const badge = isWired ? style.green(`● ${t("Wired")}`) : style.dim(`○ ${t("Not wired")}`);
95
+ const modeDesc = st.mode === "billing"
96
+ ? t("API billing only")
97
+ : (st.model ? `${t("model")}: ${st.model}` : (st.mode === "list" ? t("Model list") : ""));
98
+ const pathDesc = st.targetPath ? style.dim(`(${st.targetPath})`) : "";
99
+
100
+ items.push({
101
+ id: `wire:${tool}`,
102
+ tool,
103
+ label: r.label,
104
+ wired: isWired,
105
+ title: `${r.label} ${badge}`,
106
+ desc: isWired
107
+ ? `${modeDesc ? `${modeDesc} · ` : ""}${pathDesc}`
108
+ : `${t(AGENT_DESC[tool] ?? "")} · ${pathDesc}`,
109
+ });
110
+ }
111
+ return items;
112
+ }
113
+
78
114
  // 에이전트 서브메뉴 항목(순수 함수 — 테스트 대상). RECIPES(자동 실행) + MANUAL(설정 안내만) 전부 포함.
79
115
  export function buildAgentItems({ installed, savedModels = {} }) {
80
116
  const items = [];
@@ -390,9 +426,12 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
390
426
  async function menuSelect() {
391
427
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
392
428
  const cfg = readConfig();
429
+ const wireStatus = getWireStatus();
430
+ const wiredCount = Object.values(wireStatus).filter((s) => s.wired).length;
393
431
  const items = buildItems({
394
432
  savedModels: cfg?.launch_models ?? {},
395
433
  activeContext: getActiveContext(cfg),
434
+ wiredCount,
396
435
  });
397
436
  const last = cfg?.last_selection;
398
437
  const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
@@ -471,6 +510,177 @@ export async function runAgentMenu({ loop = false } = {}) {
471
510
  }
472
511
  }
473
512
 
513
+ // bare pleum의 "에이전트 세팅"과 pleum wire (인자 없음)가 공유한다.
514
+ export async function runWireMenu({ loop = true } = {}) {
515
+ const io = process.stdin;
516
+ if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
517
+
518
+ for (;;) {
519
+ const statusMap = getWireStatus();
520
+ const installed = Object.fromEntries(
521
+ Object.entries(RECIPES).map(([tool, r]) => [tool, onPath(r.bin)]),
522
+ );
523
+ const items = buildWireAgentItems({ statusMap, installed });
524
+
525
+ const picked = await runList(items, {
526
+ header: `${style.bold(style.plum(t("Agent setup")))} ${style.dim(`· ${t("Wire coding agents to use PleumRouter permanently")}`)}`,
527
+ footer: t("↑/↓ move • enter select • esc back"),
528
+ });
529
+
530
+ if (picked === "quit") return "quit";
531
+ if (!picked) return null;
532
+
533
+ const { item } = picked;
534
+ const tool = item.tool;
535
+ const r = RECIPES[tool];
536
+ if (!r) continue;
537
+ const currentStatus = statusMap[tool];
538
+
539
+ // 이미 배선되어 있는 경우 설정 변경 vs 배선 해제 선택
540
+ if (currentStatus?.wired) {
541
+ const action = await select([
542
+ { id: "reconfigure", title: t("Reconfigure (change model or workspace)"), desc: t("Update settings, model, or billing workspace") },
543
+ { id: "unwire", title: t("Unwire (restore original settings)"), desc: t("Remove PleumRouter configuration and restore original state") },
544
+ ], {
545
+ message: `${r.label} — ${t("Choose action")}`,
546
+ formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
547
+ });
548
+
549
+ if (!action) continue;
550
+ if (action.id === "unwire") {
551
+ await unwire([tool]);
552
+ console.log(`\n✓ ${t("Unwired {tool}.", { tool: r.label })}\n`);
553
+ if (!loop) return true;
554
+ continue;
555
+ }
556
+ }
557
+
558
+ // Step 1: 주의사항 고지 및 확인
559
+ const warningMsg = WIRE_WARNINGS[tool]
560
+ ? t(WIRE_WARNINGS[tool])
561
+ : t(WIRE_WARNINGS.default, { tool: r.label });
562
+
563
+ console.log();
564
+ console.log(style.bold(style.yellow(`⚠️ ${t("Notice before wiring")} (${r.label})`)));
565
+ console.log(` ${warningMsg}`);
566
+ console.log(style.dim(` ${t("To restore original settings anytime, run: pleum unwire {tool}", { tool })}`));
567
+ console.log();
568
+
569
+ const ok = await confirm(t("Proceed with wiring {label}? [Y/n] ", { label: r.label }), { defaultYes: true });
570
+ if (!ok) {
571
+ console.log(t("Canceled."));
572
+ if (!loop) return false;
573
+ continue;
574
+ }
575
+
576
+ // Step 2: 로그인 확인 및 과금 워크스페이스 선택
577
+ if (!isLoggedIn()) {
578
+ await login();
579
+ }
580
+ const chosenContext = await selectAndBindWorkspace();
581
+
582
+ // Step 3: 모델 세팅 모드 선택
583
+ const isModelListTool = ["opencode", "crush", "pi"].includes(tool);
584
+ let selectedMode = "billing";
585
+ let selectedModel = undefined;
586
+ let selectedModels = [];
587
+
588
+ if (isModelListTool) {
589
+ const modeChoice = await select([
590
+ { id: "all", title: t("Register all active coding models"), desc: t("Auto-registers full PleumRouter coding catalog") },
591
+ { id: "top10", title: t("Register recommended Top 10 models"), desc: t("Registers Top 10 coding models") },
592
+ { id: "custom", title: t("Pick models manually (multi-select)"), desc: t("Select specific models to include in config") },
593
+ ], {
594
+ message: `${r.label} — ${t("Model configuration mode")}`,
595
+ formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
596
+ });
597
+ if (!modeChoice) continue;
598
+
599
+ let allCoding = [];
600
+ try {
601
+ allCoding = codingModels(await fetchModels());
602
+ } catch (e) {
603
+ console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
604
+ continue;
605
+ }
606
+
607
+ if (modeChoice.id === "all") {
608
+ selectedModels = allCoding.map((m) => m.id);
609
+ selectedMode = "list";
610
+ } else if (modeChoice.id === "top10") {
611
+ selectedModels = coreCodingModels(allCoding).map((m) => m.id);
612
+ selectedMode = "list";
613
+ } else {
614
+ const pickedModels = await multiSelect(allCoding, {
615
+ message: `${r.label} — ${t("Select models")}`,
616
+ formatItem: (m) => formatModelLabel(m),
617
+ getId: (m) => m.id,
618
+ });
619
+ if (!pickedModels || pickedModels.length === 0) continue;
620
+ selectedModels = pickedModels.map((m) => m.id);
621
+ selectedMode = "list";
622
+ }
623
+
624
+ if (selectedModels.length > 1) {
625
+ const defaultChoice = await select(selectedModels.map((id) => ({ id })), {
626
+ message: `${r.label} — ${t("Select default startup model")}`,
627
+ formatItem: (x) => x.id,
628
+ });
629
+ if (defaultChoice) selectedModel = defaultChoice.id;
630
+ } else if (selectedModels.length === 1) {
631
+ selectedModel = selectedModels[0];
632
+ }
633
+ } else {
634
+ const modeChoice = await select([
635
+ {
636
+ id: "billing",
637
+ title: t("API billing only (keep agent's default models)"),
638
+ desc: t("Route API billing to PleumRouter without locking a custom model"),
639
+ },
640
+ {
641
+ id: "custom",
642
+ title: t("Select a specific model"),
643
+ desc: t("Pick a PleumRouter model to set as default"),
644
+ },
645
+ ], {
646
+ message: `${r.label} — ${t("Model configuration mode")}`,
647
+ formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
648
+ });
649
+
650
+ if (!modeChoice) continue;
651
+
652
+ if (modeChoice.id === "custom") {
653
+ let supported = () => true;
654
+ if (tool === "agy") {
655
+ try {
656
+ const ids = listAgyModelIds();
657
+ supported = (id) => supportsAgyModel(id, ids);
658
+ } catch {}
659
+ }
660
+ const pickedModel = await pickModelFor({ title: r.label }, undefined, { supported, allowProvider: tool !== "agy" });
661
+ if (!pickedModel) continue;
662
+ selectedModel = pickedModel;
663
+ selectedMode = "custom";
664
+ } else {
665
+ selectedMode = "billing";
666
+ }
667
+ }
668
+
669
+ // Step 4: 배선 적용
670
+ await wire([tool], {
671
+ model: selectedModel,
672
+ models: selectedModels,
673
+ billingOnly: selectedMode === "billing",
674
+ activeContext: chosenContext,
675
+ });
676
+
677
+ console.log(`\n✓ ${t("Wired {tool}.", { tool: r.label })}`);
678
+ console.log(style.dim(`${t("To restore original settings anytime, run: pleum unwire {tool}", { tool })}\n`));
679
+
680
+ if (!loop) return true;
681
+ }
682
+ }
683
+
474
684
  // bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
475
685
  // ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
476
686
  export async function mainMenu() {
@@ -519,6 +729,11 @@ export async function mainMenu() {
519
729
  if (r === "quit") return true;
520
730
  continue;
521
731
  }
732
+ case "wire": {
733
+ const r = await runWireMenu({ loop: true });
734
+ if (r === "quit") return true;
735
+ continue;
736
+ }
522
737
  }
523
738
  }
524
739
  }