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 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.46",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/balance.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import { authFetch, getActiveContext, setActiveContext } from "./config.mjs";
1
+ import { authFetch, getActiveContext } from "./config.mjs";
2
2
  import { style } from "./ansi.mjs";
3
3
  import { select } from "./interactive.mjs";
4
4
  import { t } from "./i18n.mjs";
5
+ import { fetchWorkspaceContext, selectAndBindWorkspace, workspaceLabel } from "./workspace.mjs";
5
6
 
6
7
  export function formatKrwCredit(amount) {
7
8
  return Number(amount || 0).toLocaleString("ko-KR", {
@@ -10,146 +11,59 @@ export function formatKrwCredit(amount) {
10
11
  });
11
12
  }
12
13
 
13
- export async function fetchCreditSummary() {
14
- const res = await authFetch("/credits");
14
+ // 개인 지갑 잔액만 조회한다. 공유 공간 목록·선택은 selectAndBindWorkspace가 맡는다.
15
+ export async function fetchCreditSummary({ auth = authFetch } = {}) {
16
+ const res = await auth("/credits");
15
17
  if (!res.ok) {
16
18
  const body = await res.text().catch(() => "");
17
19
  throw new Error(t("Failed to fetch balance ({status})", { status: res.status }) + (body ? `: ${body}` : ""));
18
20
  }
19
21
  const personal = await res.json();
20
-
21
- let orgs = [];
22
- try {
23
- const orgRes = await authFetch("/orgs");
24
- if (orgRes.ok) {
25
- orgs = await orgRes.json();
26
- }
27
- } catch {}
28
-
29
- let teamWallets = [];
30
- if (Array.isArray(orgs) && orgs.length > 0) {
31
- const results = await Promise.all(
32
- orgs.map(async (org) => {
33
- try {
34
- const wRes = await authFetch(`/orgs/${org.id}/wallet`);
35
- if (wRes.ok) {
36
- const wallet = await wRes.json();
37
- return {
38
- id: String(org.id),
39
- name: org.name,
40
- role: org.role,
41
- balance_krw: wallet.available_krw ?? wallet.balance_krw ?? 0,
42
- };
43
- }
44
- } catch {}
45
- return null;
46
- })
47
- );
48
- teamWallets = results.filter(Boolean);
49
- }
50
-
51
22
  return {
52
23
  personal: {
53
24
  balance_krw: personal.balance_krw ?? 0,
54
25
  balance_usd: personal.balance_usd,
55
26
  },
56
- teams: teamWallets,
57
27
  };
58
28
  }
59
29
 
60
- export function buildCreditOptions({ personal, teams = [], activeContext = { type: "personal" } }) {
61
- const options = [];
30
+ export function buildCreditOptions({ personal, activeContext = { type: "personal" } }) {
62
31
  const isPersonalActive = activeContext.type === "personal";
63
32
  const personalBalanceStr = `${formatKrwCredit(personal.balance_krw)} ${t("credits")}${
64
33
  personal.balance_usd != null ? ` / $${personal.balance_usd.toFixed(4)}` : ""
65
34
  }`;
66
35
 
67
- options.push({
68
- id: "personal",
69
- type: "personal",
70
- name: t("Personal"),
71
- title: t("Personal Credit"),
72
- balance: personalBalanceStr,
73
- isActive: isPersonalActive,
74
- label: `${isPersonalActive ? "✓ " : " "}${t("Personal Credit")}: ${personalBalanceStr}`,
75
- });
76
-
77
- for (const team of teams) {
78
- const isTeamActive = activeContext.type === "team" && String(activeContext.org_id) === String(team.id);
79
- const teamBalanceStr = `${formatKrwCredit(team.balance_krw)} ${t("credits")}`;
80
- const roleStr = team.role ? ` (${team.role})` : "";
81
- const teamTitle = `${t("Team")}: ${team.name}${roleStr}`;
82
- options.push({
83
- id: String(team.id),
84
- type: "team",
85
- name: team.name,
86
- title: teamTitle,
87
- balance: teamBalanceStr,
88
- isActive: isTeamActive,
89
- label: `${isTeamActive ? "✓ " : " "}${teamTitle}: ${teamBalanceStr}`,
90
- });
91
- }
92
-
93
- return options;
36
+ return [
37
+ {
38
+ id: "personal",
39
+ type: "personal",
40
+ name: t("Personal"),
41
+ title: t("Personal Credit"),
42
+ balance: personalBalanceStr,
43
+ isActive: isPersonalActive,
44
+ label: `${isPersonalActive ? "✓ " : " "}${t("Personal Credit")}: ${personalBalanceStr}`,
45
+ },
46
+ ];
94
47
  }
95
48
 
96
- export async function switchCreditWorkspace({ summary, selectFn = select } = {}) {
97
- let data;
98
- try {
99
- data = summary || (await fetchCreditSummary());
100
- } catch (e) {
101
- console.error(`✗ ${e.message}`);
102
- return getActiveContext();
103
- }
104
- const active = getActiveContext();
105
- const options = buildCreditOptions({
106
- personal: data.personal,
107
- teams: data.teams,
108
- activeContext: active,
109
- });
110
-
111
- if (options.length <= 1) {
112
- console.log(t("You are not a member of any team. Using personal credit."));
113
- return active;
114
- }
115
-
116
- const picked = await selectFn(options, {
117
- message: t("Select credit workspace"),
118
- formatItem: (x) => x.label,
119
- limit: 10,
120
- });
121
-
122
- if (!picked) return active;
123
-
124
- const targetOrgId = picked.type === "team" ? picked.id : null;
49
+ // 과금 공간 전환. 후보 선택·키 재바인딩·저장은 selectAndBindWorkspace가 한다.
50
+ export async function switchCreditWorkspace({ selectFn = select } = {}) {
51
+ return selectAndBindWorkspace({ selectFn });
52
+ }
125
53
 
54
+ // 전환 안내는 공유 공간 후보가 있을 때만 띄운다. 조회가 실패하면 조용히 생략한다.
55
+ async function hasWorkspaceChoices() {
126
56
  try {
127
- const res = await authFetch("/auth/cli-context", {
128
- method: "POST",
129
- headers: { "Content-Type": "application/json" },
130
- body: JSON.stringify({ org_id: targetOrgId }),
131
- });
132
- if (!res.ok) {
133
- const err = await res.text().catch(() => "");
134
- console.error(`✗ ${t("Failed to update credit context")}: ${err}`);
135
- return active;
136
- }
137
- } catch (e) {
138
- console.error(`✗ ${t("Failed to update credit context")}: ${e.message}`);
139
- return active;
57
+ const data = await fetchWorkspaceContext();
58
+ return (data?.workspace_candidates ?? []).length > 0;
59
+ } catch {
60
+ return false;
140
61
  }
141
-
142
- const newContext = setActiveContext({
143
- type: picked.type,
144
- org_id: targetOrgId,
145
- org_name: picked.type === "team" ? picked.name : null,
146
- });
147
-
148
- console.log(`✓ ${t("Active credit set to: {name}", { name: picked.title })}`);
149
- return newContext;
150
62
  }
151
63
 
152
64
  export async function balance({ interactive = false, switchOnly = false, exitOnError = true } = {}) {
65
+ if (switchOnly) return await switchCreditWorkspace();
66
+
153
67
  let summary;
154
68
  try {
155
69
  summary = await fetchCreditSummary();
@@ -159,28 +73,15 @@ export async function balance({ interactive = false, switchOnly = false, exitOnE
159
73
  return;
160
74
  }
161
75
 
162
- if (switchOnly) {
163
- return await switchCreditWorkspace({ summary });
164
- }
165
-
166
76
  const active = getActiveContext();
167
- const options = buildCreditOptions({
168
- personal: summary.personal,
169
- teams: summary.teams,
170
- activeContext: active,
171
- });
172
-
173
- if (!interactive || options.length <= 1) {
174
- console.log(t("Credit Balance:"));
175
- for (const opt of options) {
176
- const mark = opt.isActive ? style.bold(style.plum("▸ [Active] ")) : " [ ] ";
177
- console.log(`${mark}${opt.title}: ${opt.balance}`);
178
- }
179
- if (options.length > 1 && !interactive) {
180
- console.log(`\n${style.dim(t("Run `pleum balance --switch` to switch credit workspace."))}`);
181
- }
182
- return;
77
+ const [personal] = buildCreditOptions({ personal: summary.personal, activeContext: active });
78
+ const mark = personal.isActive ? style.bold(style.plum("▸ [Active] ")) : " [ ] ";
79
+ console.log(t("Credit Balance:"));
80
+ console.log(`${mark}${personal.title}: ${personal.balance}`);
81
+ console.log(` ${t("Credit workspace")}: ${workspaceLabel(active)}`);
82
+
83
+ if (interactive) return await switchCreditWorkspace();
84
+ if (await hasWorkspaceChoices()) {
85
+ console.log(`\n${style.dim(t("Run `pleum balance --switch` to switch credit workspace."))}`);
183
86
  }
184
-
185
- return await switchCreditWorkspace({ summary });
186
87
  }
package/src/config.mjs CHANGED
@@ -215,9 +215,7 @@ export function getActiveContext(cfg = readConfig()) {
215
215
  workspace_name: ctx.workspace_name || ctx.workspace_id,
216
216
  };
217
217
  }
218
- if (ctx && ctx.type === "team" && ctx.org_id) {
219
- return { type: "team", org_id: ctx.org_id, org_name: ctx.org_name || ctx.org_id };
220
- }
218
+ // 팀 컨텍스트는 제거됐다. 예전에 저장된 team 값도 개인으로 본다.
221
219
  return { type: "personal", org_id: null, org_name: null };
222
220
  }
223
221
 
@@ -230,29 +228,18 @@ export function setActiveContext(context, cfg = readConfig()) {
230
228
  workspace_id: context.workspace_id,
231
229
  workspace_name: context.workspace_name || null,
232
230
  };
233
- } else if (!context || context.type === "personal" || !context.org_id) {
234
- updated.active_context = { type: "personal" };
235
231
  } else {
236
- updated.active_context = {
237
- type: "team",
238
- org_id: context.org_id,
239
- org_name: context.org_name || null,
240
- };
232
+ updated.active_context = { type: "personal" };
241
233
  }
242
234
  writeConfig(updated);
243
235
  return updated.active_context;
244
236
  }
245
237
 
246
- /** Bearer 요청 — 401이면 1회 refresh 후 재시도. 활성 팀 컨텍스트가 있으면 X-Pleum-Org 첨부. */
238
+ /** Bearer 요청 — 401이면 1회 refresh 후 재시도. 과금 귀속은 키의 workspace_id가 정하므로 헤더를 붙이지 않는다. */
247
239
  export async function authFetch(path, init = {}) {
248
240
  const token = await requireAuth();
249
241
  const url = path.startsWith("http") ? path : `${V1}${path.startsWith("/") ? path : `/${path}`}`;
250
- const ctx = getActiveContext();
251
- const customHeaders = {};
252
- if (ctx.type === "team" && ctx.org_id) {
253
- customHeaders["X-Pleum-Org"] = ctx.org_id;
254
- }
255
- const headers = { ...customHeaders, ...(init.headers || {}), Authorization: `Bearer ${token}` };
242
+ const headers = { ...(init.headers || {}), Authorization: `Bearer ${token}` };
256
243
  let res = await fetch(url, { ...init, headers });
257
244
  if (res.status !== 401) return res;
258
245
 
@@ -261,7 +248,7 @@ export async function authFetch(path, init = {}) {
261
248
  if (!refreshed) return res;
262
249
  return fetch(url, {
263
250
  ...init,
264
- headers: { ...customHeaders, ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
251
+ headers: { ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
265
252
  });
266
253
  }
267
254
 
@@ -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
+ }
package/src/launch.mjs CHANGED
@@ -5,6 +5,7 @@ import { join, delimiter, dirname, resolve } from "node:path";
5
5
  import { readConfig, isLoggedIn, getActiveContext, ROOT } from "./config.mjs";
6
6
  import { login } from "./login.mjs";
7
7
  import { ensureWireKey } from "./wire.mjs";
8
+ import { workspaceLabel } from "./workspace.mjs";
8
9
  import {
9
10
  RECIPES,
10
11
  CORE_MODELS,
@@ -316,12 +317,7 @@ export async function launch(tool, model, passthrough, {
316
317
  process.removeListener("unhandledRejection", onException);
317
318
  };
318
319
 
319
- const activeCtx = getActiveContext();
320
- const creditMsg = activeCtx.type === "workspace"
321
- ? `${t("Workspace")}: ${sanitizeTerminalText(activeCtx.workspace_name)}`
322
- : activeCtx.type === "team"
323
- ? `${t("Team")}: ${activeCtx.org_name}`
324
- : t("Personal");
320
+ const creditMsg = workspaceLabel(getActiveContext());
325
321
  console.log(`ℹ ${t("Credit:")} ${creditMsg}`);
326
322
 
327
323
  const args = [...r.args(model), ...passthrough];
@@ -169,6 +169,8 @@ export const ES = {
169
169
  "Unsupported tool: {tool}. Use claude or codex.": "Herramienta no compatible: {tool}. Usa claude o codex.",
170
170
  "Wrote {path}": "Escrito en {path}",
171
171
  "Wired {tool}.": "Cableado {tool}.",
172
+ "The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "La clave de conexión la comparten todas las herramientas conectadas y launch, así que cambiar el espacio de facturación se aplica a todas.",
173
+ "{path}: not valid JSON — fix or remove it, nothing was written": "{path}: no es JSON válido — corríjalo o elimínelo; no se escribió nada",
172
174
  "Desktop key: {prefix}…": "Clave de escritorio: {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} (sin /v1), Bearer, pega la clave plm_.",
@@ -244,6 +246,7 @@ export const ES = {
244
246
  "↑/↓ move • enter set model • esc back": "↑/↓ mover • enter definir modelo • esc volver",
245
247
  "⏹ aborted": "⏹ abortado",
246
248
  "⏹ generation aborted — the partial reply stays in context": "⏹ generación abortada — la respuesta parcial permanece en el contexto",
249
+ "partial reply kept in context": "la respuesta parcial se mantuvo en el contexto",
247
250
  "✓ installed": "✓ instalado",
248
251
  "✗ not installed —": "✗ no instalado —",
249
252
  "Log out": "Cerrar sesión",
@@ -306,4 +309,42 @@ export const ES = {
306
309
  "Could not add Pleum models to OpenCode favorites: {error}": "No se pudieron añadir modelos Pleum a favoritos de OpenCode: {error}",
307
310
  "↑/↓ move • enter select • esc quit": "↑/↓ mover • enter seleccionar • esc salir",
308
311
  "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.",
312
+
313
+ // Agent setup (wire)
314
+ "Select models": "Seleccionar modelos",
315
+ "Space toggle · Enter confirm · Esc cancel": "Espacio=marcar · Enter=confirmar · Esc=cancelar",
316
+ "Wire coding agents to use PleumRouter permanently": "Conecta agentes de código a PleumRouter de forma permanente",
317
+ "{n} wired": "{n} conectados",
318
+ "Agent setup": "Configuración de agentes",
319
+ "Wired": "Conectado",
320
+ "Not wired": "No conectado",
321
+ "API billing only": "Solo facturación API",
322
+ "Reconfigure (change model or workspace)": "Reconfigurar (cambiar modelo o espacio)",
323
+ "Update settings, model, or billing workspace": "Modificar configuración, modelo o espacio de facturación",
324
+ "Unwire (restore original settings)": "Desconectar (restaurar configuración original)",
325
+ "Remove PleumRouter configuration and restore original state": "Eliminar la configuración de PleumRouter y restaurar el estado original",
326
+ "Choose action": "Elegir acción",
327
+ "Unwired {tool}.": "Se desconectó {tool}.",
328
+ "Notice before wiring": "Aviso antes de conectar",
329
+ "To restore original settings anytime, run: pleum unwire {tool}": "Para restaurar la configuración original en cualquier momento: pleum unwire {tool}",
330
+ "Proceed with wiring {label}? [Y/n] ": "¿Continuar con la conexión de {label}? [Y/n] ",
331
+ "Canceled.": "Cancelado.",
332
+ "Register all active coding models": "Registrar todos los modelos de código activos",
333
+ "Auto-registers full PleumRouter coding catalog": "Registra automáticamente todo el catálogo de modelos de PleumRouter",
334
+ "Register recommended Top 10 models": "Registrar los 10 modelos recomendados",
335
+ "Registers Top 10 coding models": "Registra los 10 mejores modelos de código",
336
+ "Pick models manually (multi-select)": "Seleccionar lista de modelos manualmente",
337
+ "Select specific models to include in config": "Elige manualmente los modelos que se incluirán en la configuración",
338
+ "Model configuration mode": "Modo de configuración de modelo",
339
+ "Select default startup model": "Seleccionar modelo predeterminado inicial",
340
+ "API billing only (keep agent's default models)": "Solo facturación API (mantener modelos predeterminados)",
341
+ "Route API billing to PleumRouter without locking a custom model": "Enruta la facturación API a PleumRouter sin fijar un modelo personalizado",
342
+ "Select a specific model": "Seleccionar un modelo específico",
343
+ "Pick a PleumRouter model to set as default": "Elige un modelo del catálogo de PleumRouter como predeterminado",
344
+ "Select billing workspace for this agent": "Selecciona el espacio de facturación para este agente",
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`.": "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`.",
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`.": "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`.",
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`.": "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`.",
348
+ "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.",
349
+ "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
350
  };
@@ -169,6 +169,8 @@ export const JA = {
169
169
  "Example: pleum wire": "例: pleum wire",
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 JA = {
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 JA = {
306
309
  "Could not add Pleum models to OpenCode favorites: {error}": "OpenCode のお気に入りに Pleum モデルを追加できません: {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.": "~/.gemini/antigravity-cli/settings.json に modelProvider=gemini を書き込みます。対応する 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} の接続(wire)を続行しますか? [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` の実行は Claude Pro/Max サブスクリプションではなく PleumRouter API クレジットで課金されます。",
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` の実行は ChatGPT Plus/Pro や既存の OpenAI キーではなく PleumRouter API クレジットで課金されます。",
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`) は Google アカウントではなく PleumRouter クレジットで課金されます。",
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
  };