pleumrouter 0.5.46 → 0.5.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
39
39
  | `pleum logout` | 서버 세션 revoke + 로컬 토큰 삭제(언어·기본 모델 등 나머지 설정은 유지) |
40
40
  | `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
41
41
  | `pleum wire [claude\|codex]` | Desktop용으로 `~/.claude/settings.json`·`~/.codex/config.toml`에 같은 `plm_` 키를 병합. 생략 시 둘 다 |
42
- | `pleum unwire [claude\|codex] [--revoke-key]` | wire가 넣은 설정만 제거. `--revoke-key`는 서버 키도 폐기 |
42
+ | `pleum unwire [tool...] [--revoke-key]` | wire가 바꾼 설정을 원래 값으로 복원(도구 생략 시 배선된 도구 전부). `--revoke-key`는 서버 키도 폐기 — 다른 배선 도구가 남아 있으면 거부 |
43
43
  | `pleum run [model] ["프롬프트"] [--attach image]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. 이미지 업로드는 명시적으로 `--attach ./shot.png`(1회 실행) 또는 REPL의 `@./shot.png`를 사용한다. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
44
44
  | `pleum models [query] [--json]` | 모델 목록(공개) — provider·컨텍스트·가격(₩/1M) 컬럼, `--json`은 스크립트용 |
45
45
  | `pleum models <id> --info` | 모델 상세 — 가격·컨텍스트·provider별 지연/처리량/업타임·주간 사용량·형제 모델 |
package/bin/pleum.mjs CHANGED
@@ -38,7 +38,7 @@ function buildHelp() {
38
38
  ["", `(${Object.keys(RECIPES).join(" · ")})`],
39
39
  ["", t("omit the tool to see the supported list and install status")],
40
40
  ["pleum wire [claude|codex]", t("persist config in ~/.claude or ~/.codex for Desktop apps")],
41
- ["pleum unwire [claude|codex]", t("Removes the PleumRouter provider from Desktop config files")],
41
+ ["pleum unwire [tool]", t("Restore settings changed by wire (omit the tool = all wired tools)")],
42
42
  ['pleum run [model] ["prompt"] [--attach image]', t("Terminal chat (REPL) / one-shot / pipe")],
43
43
  ["", t("omit the model for an arrow-key picker; /help lists commands")],
44
44
  ["pleum models [query] [--json]", t("Model list (price, context included)")],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.46",
3
+ "version": "0.5.47",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,11 +161,12 @@ export const ES = {
161
161
  "Unknown command: /{cmd} (/help)": "Comando desconocido: /{cmd} (/help)",
162
162
  "Unknown command: {cmd}": "Comando desconocido: {cmd}",
163
163
  "Usage: pleum wire [claude|codex]\n": "Uso: pleum wire [claude|codex]\n",
164
- "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "Uso: pleum unwire [claude|codex] [--revoke-key]\n",
164
+ "Usage: pleum unwire [tool...] [--revoke-key]\n": "Uso: pleum unwire [tool...] [--revoke-key]\n",
165
165
  "Example: pleum wire": "Ejemplo: pleum wire",
166
166
  "Omit the tool name to wire both claude and codex.": "Si omites el nombre, cablea claude y codex.",
167
+ "Omit the tool name to unwire every wired tool.": "Si omites el nombre, desconecta todas las herramientas conectadas.",
167
168
  "persist config in ~/.claude or ~/.codex for Desktop apps": "guardar la config en ~/.claude o ~/.codex para apps Desktop",
168
- "Removes the PleumRouter provider from Desktop config files": "Quita el proveedor PleumRouter de los archivos Desktop",
169
+ "Restore settings changed by wire (omit the tool = all wired tools)": "Restaura la config que cambió wire (sin herramienta = todas las conectadas)",
169
170
  "Unsupported tool: {tool}. Use claude or codex.": "Herramienta no compatible: {tool}. Usa claude o codex.",
170
171
  "Wrote {path}": "Escrito en {path}",
171
172
  "Wired {tool}.": "Cableado {tool}.",
@@ -179,6 +180,9 @@ export const ES = {
179
180
  "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
180
181
  "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, la misma clave plm_. Cursor CLI queda fuera de alcance.",
181
182
  "Removed PleumRouter settings from {path}.": "Se quitó la config de PleumRouter de {path}.",
183
+ "{tool} is not wired by PleumRouter; left unchanged.": "{tool} no está conectado por PleumRouter; se dejó sin cambios.",
184
+ "No tools are wired by PleumRouter.": "PleumRouter no tiene herramientas conectadas.",
185
+ "Other tools still use this key: {tools}. Run `pleum unwire --revoke-key` to unwire all of them first.": "Otras herramientas aún usan esta clave: {tools}. Antes ejecuta `pleum unwire --revoke-key` para desconectarlas todas.",
182
186
  "No stored desktop API key.": "No hay clave API de escritorio guardada.",
183
187
  "Revoked desktop API key {id}.": "Se revocó la clave API de escritorio {id}.",
184
188
  "Failed to revoke the desktop API key (HTTP {status})": "No se pudo revocar la clave API de escritorio (HTTP {status})",
@@ -99,6 +99,7 @@ export const JA = {
99
99
  "Session expired. Run:\n pleum login": "セッションの有効期限が切れました。実行してください:\n pleum login",
100
100
  "OAuth session": "OAuth セッション",
101
101
  "Omit the tool name to wire both claude and codex.": "ツール名を省略すると claude と codex の両方を配線します。",
102
+ "Omit the tool name to unwire every wired tool.": "ツール名を省略すると、配線済みのツールをすべて解除します。",
102
103
  "Log in via browser (OAuth session, no API key)": "ブラウザでログイン(OAuth セッション、API キーなし)",
103
104
  "Revoke session and clear local tokens": "セッションを取り消し、ローカル トークンを削除",
104
105
  "Logged out": "ログアウトしました",
@@ -136,7 +137,7 @@ export const JA = {
136
137
  "Print a zsh/bash completion script": "zsh/bash補完スクリプトを出力",
137
138
  "Print the version": "バージョンを表示",
138
139
  "Provider share": "プロバイダーシェア",
139
- "Removes the PleumRouter provider from Desktop config files": "Desktop 設定ファイルから PleumRouter プロバイダーを削除",
140
+ "Restore settings changed by wire (omit the tool = all wired tools)": "wire が変更した設定を元に戻す(ツール省略で配線済みすべて)",
140
141
  "Revoked desktop API key {id}.": "デスクトップ API キー {id} を失効しました。",
141
142
  "Run a coding agent against PleumRouter": "PleumRouter に対してコーディングエージェントを実行",
142
143
  "Run pleum login again to get a new key (recent keys include it by default).": "新しいキーを取得するには pleum login を再実行してください (最近のキーはデフォルトで含まれます)。",
@@ -179,6 +180,9 @@ export const JA = {
179
180
  "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
180
181
  "Cursor IDE: Settings → Models → OpenAI Compatible。Base URL {v1}、同じ plm_ キー。Cursor CLI は対象外です。",
181
182
  "Removed PleumRouter settings from {path}.": "{path} から PleumRouter 設定を削除しました。",
183
+ "{tool} is not wired by PleumRouter; left unchanged.": "{tool} は PleumRouter で配線されていないため、そのままにしました。",
184
+ "No tools are wired by PleumRouter.": "PleumRouter で配線されたツールはありません。",
185
+ "Other tools still use this key: {tools}. Run `pleum unwire --revoke-key` to unwire all of them first.": "他のツールがまだこのキーを使っています: {tools}。先に `pleum unwire --revoke-key` ですべて解除してください。",
182
186
  "Failed to create a desktop API key (HTTP {status})": "デスクトップ API キーの発行に失敗しました (HTTP {status})",
183
187
  "kept the desktop key on this machine (logout does not delete it)": "このマシンのデスクトップキーは残します(logout では消しません)",
184
188
  "Unsupported language: {code}\n Available: {list}": "サポートされていない言語です: {code}\n 利用可能: {list}",
@@ -190,7 +194,7 @@ export const JA = {
190
194
  "Usage: pleum launch <tool> [--model id] [-- tool args]\n": "使い方: pleum launch <tool> [--model id] [-- tool args]\n",
191
195
  "Usage: pleum models <model-id> --info": "使い方: pleum models <model-id> --info",
192
196
  "Usage: pleum run <model> [\"prompt\"]\nModel list: pleum models": "使い方: pleum run <model> [\"prompt\"]\nモデル一覧: pleum models",
193
- "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "使い方: pleum unwire [claude|codex] [--revoke-key]\n",
197
+ "Usage: pleum unwire [tool...] [--revoke-key]\n": "使い方: pleum unwire [tool...] [--revoke-key]\n",
194
198
  "Usage: pleum wire [claude|codex]\n": "使い方: pleum wire [claude|codex]\n",
195
199
  "Use default (clear)": "デフォルトを使用(解除)",
196
200
  "Your spend report (model|provider|api_key|day|tag|user)": "支出レポート (model|provider|api_key|day|tag|user)",
@@ -268,11 +268,12 @@ export const KO = {
268
268
  "Logged out": "로그아웃됨",
269
269
  "Unknown command: {cmd}": "알 수 없는 명령: {cmd}",
270
270
  "Usage: pleum wire [claude|codex]\n": "사용법: pleum wire [claude|codex]\n",
271
- "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "사용법: pleum unwire [claude|codex] [--revoke-key]\n",
271
+ "Usage: pleum unwire [tool...] [--revoke-key]\n": "사용법: pleum unwire [tool...] [--revoke-key]\n",
272
272
  "Example: pleum wire": "예: pleum wire",
273
273
  "Omit the tool name to wire both claude and codex.": "도구 이름을 생략하면 claude와 codex를 모두 배선합니다.",
274
+ "Omit the tool name to unwire every wired tool.": "도구 이름을 생략하면 배선된 도구를 모두 해제합니다.",
274
275
  "persist config in ~/.claude or ~/.codex for Desktop apps": "Desktop 앱용으로 ~/.claude 또는 ~/.codex에 설정을 저장",
275
- "Removes the PleumRouter provider from Desktop config files": "Desktop 설정 파일에서 PleumRouter 프로바이더를 제거",
276
+ "Restore settings changed by wire (omit the tool = all wired tools)": "wire가 바꾼 설정을 복원 (도구 생략 = 배선된 도구 전부)",
276
277
  "Unsupported tool: {tool}. Use claude or codex.": "지원하지 않는 도구: {tool}. claude 또는 codex를 쓰세요.",
277
278
  "Wrote {path}": "{path}에 썼습니다",
278
279
  "Wired {tool}.": "{tool}을 배선했습니다.",
@@ -286,6 +287,9 @@ export const KO = {
286
287
  "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
287
288
  "Cursor IDE: 설정 → Models → OpenAI Compatible. Base URL {v1}, 같은 plm_ 키. Cursor CLI는 범위 밖입니다.",
288
289
  "Removed PleumRouter settings from {path}.": "{path}에서 PleumRouter 설정을 제거했습니다.",
290
+ "{tool} is not wired by PleumRouter; left unchanged.": "{tool}은(는) PleumRouter가 배선하지 않아 그대로 둡니다.",
291
+ "No tools are wired by PleumRouter.": "PleumRouter가 배선한 도구가 없습니다.",
292
+ "Other tools still use this key: {tools}. Run `pleum unwire --revoke-key` to unwire all of them first.": "다른 도구가 이 키를 아직 쓰고 있습니다: {tools}. 먼저 `pleum unwire --revoke-key`로 전부 해제하세요.",
289
293
  "No stored desktop API key.": "저장된 데스크톱 API 키가 없습니다.",
290
294
  "Revoked desktop API key {id}.": "데스크톱 API 키 {id}를 폐기했습니다.",
291
295
  "Failed to revoke the desktop API key (HTTP {status})": "데스크톱 API 키 폐기에 실패했습니다 (HTTP {status})",
@@ -161,11 +161,12 @@ export const ZH = {
161
161
  "Unknown command: /{cmd} (/help)": "未知命令:/{cmd} (/help)",
162
162
  "Unknown command: {cmd}": "未知命令:{cmd}",
163
163
  "Usage: pleum wire [claude|codex]\n": "用法:pleum wire [claude|codex]\n",
164
- "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "用法:pleum unwire [claude|codex] [--revoke-key]\n",
164
+ "Usage: pleum unwire [tool...] [--revoke-key]\n": "用法:pleum unwire [tool...] [--revoke-key]\n",
165
165
  "Example: pleum wire": "示例:pleum wire",
166
166
  "Omit the tool name to wire both claude and codex.": "省略工具名会同时接线 claude 和 codex。",
167
+ "Omit the tool name to unwire every wired tool.": "省略工具名会解除所有已接线的工具。",
167
168
  "persist config in ~/.claude or ~/.codex for Desktop apps": "为 Desktop 应用把配置写入 ~/.claude 或 ~/.codex",
168
- "Removes the PleumRouter provider from Desktop config files": "从 Desktop 配置文件中移除 PleumRouter 提供方",
169
+ "Restore settings changed by wire (omit the tool = all wired tools)": "还原 wire 修改的设置(省略工具名 = 所有已接线的工具)",
169
170
  "Unsupported tool: {tool}. Use claude or codex.": "不支持的工具:{tool}。请使用 claude 或 codex。",
170
171
  "Wrote {path}": "已写入 {path}",
171
172
  "Wired {tool}.": "已接线 {tool}。",
@@ -179,6 +180,9 @@ export const ZH = {
179
180
  "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
180
181
  "Cursor IDE:设置 → Models → OpenAI Compatible。Base URL {v1},同一把 plm_ 密钥。Cursor CLI 不在范围内。",
181
182
  "Removed PleumRouter settings from {path}.": "已从 {path} 移除 PleumRouter 设置。",
183
+ "{tool} is not wired by PleumRouter; left unchanged.": "{tool} 不是由 PleumRouter 接线的,保持不变。",
184
+ "No tools are wired by PleumRouter.": "没有由 PleumRouter 接线的工具。",
185
+ "Other tools still use this key: {tools}. Run `pleum unwire --revoke-key` to unwire all of them first.": "其他工具仍在使用此密钥:{tools}。请先运行 `pleum unwire --revoke-key` 全部解除。",
182
186
  "No stored desktop API key.": "没有保存的桌面 API 密钥。",
183
187
  "Revoked desktop API key {id}.": "已撤销桌面 API 密钥 {id}。",
184
188
  "Failed to revoke the desktop API key (HTTP {status})": "撤销桌面 API 密钥失败(HTTP {status})",
package/src/wire.mjs CHANGED
@@ -196,6 +196,49 @@ function removeRootValue(toml, name) {
196
196
  return root.replace(new RegExp(`^${name}\\s*=\\s*.+\\n?`, "m"), "") + rest;
197
197
  }
198
198
 
199
+ const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
200
+
201
+ // 블록을 지운 뒤 남는 구분 빈 줄(wire가 붙인 것)을 정리한다: 끝의 연속 줄바꿈은 하나로, 남은 게 없으면 "".
202
+ function tidyUnwired(text) {
203
+ const out = text.trimStart().replace(/\n{2,}$/, "\n");
204
+ return out.trim() ? out : "";
205
+ }
206
+
207
+ // 키 순서를 order에 맞춰 되살린다. 사라진 키는 건너뛰고, 그 뒤에 생긴 키는 현재 순서로 뒤에 붙는다.
208
+ function reorder(obj, order = []) {
209
+ const out = {};
210
+ for (const key of order) if (Object.hasOwn(obj, key)) out[key] = obj[key];
211
+ return Object.assign(out, obj);
212
+ }
213
+
214
+ // pleum이 배선한 Claude env인가: 게이트웨이가 우리 ROOT이거나 토큰이 plm_ 키.
215
+ function claudeEnvWired(env) {
216
+ const base = String(env?.ANTHROPIC_BASE_URL ?? "").replace(/\/+$/, "");
217
+ return base === ROOT || String(env?.ANTHROPIC_AUTH_TOKEN ?? "").startsWith("plm_");
218
+ }
219
+
220
+ // wire가 덮어쓰기 전의 사용자 값(claude·agy). 이미 pleum이 배선한 텍스트는 pleum 값을 원본으로 오인하므로 null.
221
+ // agy의 "gemini" provider는 launch.mjs와 같이 pleum 잔재로 본다.
222
+ function pickPrevious(tool, text) {
223
+ let obj;
224
+ try {
225
+ obj = parseJsonObject(text);
226
+ } catch {
227
+ return null;
228
+ }
229
+ if (tool === "agy") {
230
+ return obj.modelProvider === "gemini" ? null : { model: obj.model, modelProvider: obj.modelProvider };
231
+ }
232
+ if (claudeEnvWired(obj.env)) return null;
233
+ const env = isPlainObject(obj.env) ? obj.env : undefined;
234
+ return {
235
+ env: Object.fromEntries(CLAUDE_ENV_KEYS.filter((k) => env?.[k] !== undefined).map((k) => [k, env[k]])),
236
+ model: obj.model,
237
+ order: Object.keys(obj),
238
+ envOrder: env && Object.keys(env),
239
+ };
240
+ }
241
+
199
242
  // ── Claude settings.json ──────────────────────────────────────────
200
243
  export function wireClaudeSettings(existingJson, { root, key, model } = {}) {
201
244
  const obj = parseJsonObject(existingJson);
@@ -228,16 +271,28 @@ export function wireClaudeSettings(existingJson, { root, key, model } = {}) {
228
271
  return `${JSON.stringify(obj, null, 2)}\n`;
229
272
  }
230
273
 
231
- export function unwireClaudeSettings(existingJson) {
274
+ // previous: wire 전 사용자 값(pickPrevious). 없으면 pleum 키만 지운다.
275
+ export function unwireClaudeSettings(existingJson, previous = {}) {
232
276
  const obj = parseJsonObject(existingJson);
233
- if (obj.env && typeof obj.env === "object" && !Array.isArray(obj.env)) {
277
+ // wire는 obj.model과 같은 값을 ANTHROPIC_CUSTOM_MODEL_OPTION에도 쓴다 — claude- 접두사가 없는 모델도 이걸로 알아본다.
278
+ const customModel = isPlainObject(obj.env) ? obj.env.ANTHROPIC_CUSTOM_MODEL_OPTION : undefined;
279
+ if (isPlainObject(obj.env)) {
234
280
  for (const envKey of CLAUDE_ENV_KEYS) delete obj.env[envKey];
235
- if (Object.keys(obj.env).length === 0) delete obj.env;
236
281
  }
237
- if (obj.model && typeof obj.model === "string" && (obj.model.startsWith("anthropic/") || obj.model.startsWith("pleum/"))) {
238
- delete obj.model;
282
+ if (previous.env && Object.keys(previous.env).length > 0) {
283
+ obj.env = { ...(isPlainObject(obj.env) ? obj.env : {}), ...previous.env };
239
284
  }
240
- return `${JSON.stringify(obj, null, 2)}\n`;
285
+ if (isPlainObject(obj.env)) {
286
+ obj.env = reorder(obj.env, previous.envOrder);
287
+ if (Object.keys(obj.env).length === 0 && !previous.envOrder) delete obj.env;
288
+ }
289
+ // model은 pleum이 넣은 값이거나 billing wire가 지워 비어 있을 때만 되돌린다 — 그 뒤 사용자가 바꾼 값은 둔다.
290
+ const pleumModel = typeof obj.model === "string" && (obj.model.startsWith("anthropic/") || obj.model.startsWith("pleum/") || obj.model === customModel);
291
+ if (pleumModel || obj.model === undefined) {
292
+ if (previous.model === undefined) delete obj.model;
293
+ else obj.model = previous.model;
294
+ }
295
+ return `${JSON.stringify(reorder(obj, previous.order), null, 2)}\n`;
241
296
  }
242
297
 
243
298
  // ── Codex config.toml ─────────────────────────────────────────────
@@ -287,7 +342,10 @@ export function unwireCodexToml(existingToml) {
287
342
  previousModel ? `model = ${previousModel}` : "",
288
343
  previousProvider ? `model_provider = ${previousProvider}` : "",
289
344
  ].filter(Boolean).join("\n");
290
- return `${restored ? `${restored}\n\n` : ""}${body.trim()}`.replace(/\n{3,}/g, "\n\n").trimStart() + "\n";
345
+ // 복원한 root 줄 바로 뒤가 root 키면 붙이고(원래 모양), 테이블이면 빈 줄 하나를 둔다.
346
+ const rest = body.trim();
347
+ const gap = !restored || !rest ? "" : rest.startsWith("[") ? "\n\n" : "\n";
348
+ return `${restored}${gap}${rest}`.replace(/\n{3,}/g, "\n\n").trimStart() + "\n";
291
349
  }
292
350
 
293
351
  // ── OpenCode (opencode.json & auth.json) ──────────────────────────
@@ -369,9 +427,7 @@ export function wireAiderYaml(existingYaml = "", { v1, key, model } = {}) {
369
427
  }
370
428
 
371
429
  export function unwireAiderYaml(existingYaml = "") {
372
- return (existingYaml || "")
373
- .replace(new RegExp(`${WIRE_MARKER_START}[\\s\\S]*?${WIRE_MARKER_END}\\n?`, "g"), "")
374
- .trimStart();
430
+ return tidyUnwired((existingYaml || "").replace(new RegExp(`${WIRE_MARKER_START}[\\s\\S]*?${WIRE_MARKER_END}\\n?`, "g"), ""));
375
431
  }
376
432
 
377
433
  // ── Crush ~/.config/crush/crush.json ──────────────────────────────
@@ -426,10 +482,18 @@ export function wireAgySettings(existingJson, { model } = {}) {
426
482
  return `${JSON.stringify(obj, null, 2)}\n`;
427
483
  }
428
484
 
429
- export function unwireAgySettings(existingJson) {
485
+ // previous: wire 전 사용자 값. wiredModel: undefined=모름(옛 호출, model을 지운다), null=pleum이 model을 쓰지 않음, 문자열=pleum이 쓴 값.
486
+ // model은 pleum이 쓴 값일 때만 되돌린다.
487
+ export function unwireAgySettings(existingJson, previous = {}, wiredModel) {
430
488
  const obj = parseJsonObject(existingJson);
431
- if (obj.modelProvider === "gemini") delete obj.modelProvider;
432
- delete obj.model;
489
+ if (obj.modelProvider === "gemini") {
490
+ if (previous.modelProvider === undefined) delete obj.modelProvider;
491
+ else obj.modelProvider = previous.modelProvider;
492
+ }
493
+ if (wiredModel === undefined || obj.model === wiredModel) {
494
+ if (previous.model === undefined) delete obj.model;
495
+ else obj.model = previous.model;
496
+ }
433
497
  return `${JSON.stringify(obj, null, 2)}\n`;
434
498
  }
435
499
 
@@ -529,9 +593,7 @@ ${endMarker}`;
529
593
  export function unwireShellWrapper(existingRc = "", tool) {
530
594
  const startMarker = `# pleum-wire:${tool}-start`;
531
595
  const endMarker = `# pleum-wire:${tool}-end`;
532
- return (existingRc || "")
533
- .replace(new RegExp(`${startMarker}[\\s\\S]*?${endMarker}\\n?`, "g"), "")
534
- .trimStart();
596
+ return tidyUnwired((existingRc || "").replace(new RegExp(`${startMarker}[\\s\\S]*?${endMarker}\\n?`, "g"), ""));
535
597
  }
536
598
 
537
599
  // ── Wired State Metadata (~/.pleum/wired-agents.json) ─────────────
@@ -550,13 +612,24 @@ export function writeWiredState(paths, state) {
550
612
 
551
613
  // ── Wire Status Check ─────────────────────────────────────────────
552
614
  export function getWireStatus(home = homedir()) {
553
- const paths = wirePaths(home);
615
+ return wireStatusOf(wirePaths(home));
616
+ }
617
+
618
+ // paths를 그대로 받아, revert가 쓰는 rc 경로와 같은 기준으로 판정한다.
619
+ function wireStatusOf(paths) {
554
620
  const state = readWiredState(paths);
555
621
  const status = {};
556
622
 
557
- // 1. Claude
623
+ // 1. Claude — 게이트웨이/토큰이 pleum 것일 때만(사용자 본인 게이트웨이는 배선 아님). 파싱 불가(JSONC 등)는 예전 텍스트 검사.
558
624
  const claudeText = readText(paths.claude);
559
- const claudeWired = Boolean(claudeText && claudeText.includes("ANTHROPIC_BASE_URL"));
625
+ const claudeWired = (() => {
626
+ if (!claudeText) return false;
627
+ try {
628
+ return claudeEnvWired(parseJsonObject(claudeText).env);
629
+ } catch {
630
+ return claudeText.includes("ANTHROPIC_BASE_URL");
631
+ }
632
+ })();
560
633
  status.claude = {
561
634
  tool: "claude",
562
635
  label: "Claude Code",
@@ -627,9 +700,10 @@ export function getWireStatus(home = homedir()) {
627
700
  };
628
701
 
629
702
  // 7. Antigravity (agy)
703
+ // settings의 gemini provider만으로는 사용자 본인 설정·launch 임시값과 구분이 안 되므로, rc 블록이나 배선 기록이 함께 있어야 한다.
630
704
  const agyText = readText(paths.agy);
631
705
  const rcText = readText(paths.shellRc) || "";
632
- const agyWired = Boolean((agyText && agyText.includes('"modelProvider": "gemini"')) || rcText.includes("# pleum-wire:agy-start"));
706
+ const agyWired = Boolean(rcText.includes("# pleum-wire:agy-start") || (state.agy && agyText?.includes('"modelProvider": "gemini"')));
633
707
  status.agy = {
634
708
  tool: "agy",
635
709
  label: "Antigravity CLI",
@@ -785,7 +859,8 @@ export function wireHelp() {
785
859
  }
786
860
 
787
861
  export function unwireHelp() {
788
- console.log(t("Usage: pleum unwire [claude|codex] [--revoke-key]\n"));
862
+ console.log(t("Usage: pleum unwire [tool...] [--revoke-key]\n"));
863
+ console.log(` ${t("Omit the tool name to unwire every wired tool.")}`);
789
864
  }
790
865
 
791
866
  function printDesktopHints(record) {
@@ -807,14 +882,31 @@ function printDesktopHints(record) {
807
882
  console.log(t("kept the desktop key on this machine (logout does not delete it)"));
808
883
  }
809
884
 
885
+ // 이 기록의 wire가 agy settings에 쓴 model: 문자열, 안 썼으면 null(billing·모델 없음), 기록이 없으면 undefined.
886
+ function agyWiredModel(record) {
887
+ return record ? (record.mode === "billing" ? null : (record.model ?? null)) : undefined;
888
+ }
889
+
890
+ // 이미 배선 기록이 있으면 그 원본을 그대로 쓴다(재배선이 pleum 값을 원본으로 덮지 않게). 0.5.45~46 기록(previous 없음)은
891
+ // 현재 파일이 이미 배선된 상태라 .pleum.bak에서, 기록이 없으면 현재 파일에서 뽑는다(배선된 파일이면 .pleum.bak).
892
+ function originalValues(tool, paths, state, text) {
893
+ if (state[tool]?.previous) return state[tool].previous;
894
+ const fromText = state[tool] ? null : pickPrevious(tool, text);
895
+ return fromText ?? pickPrevious(tool, readText(paths[`${tool}Bak`])) ?? {};
896
+ }
897
+
810
898
  export function applyToolWiring(tool, paths, { key, model, models = [], billingOnly = false, activeContext } = {}) {
811
899
  const state = readWiredState(paths);
812
900
  const mode = billingOnly ? "billing" : (models.length > 0 ? "list" : (model ? "custom" : "billing"));
901
+ let previous; // claude·agy가 덮어쓰는 사용자 원래 값 — unwire가 복원한다
813
902
 
814
903
  switch (tool) {
815
904
  case "claude": {
816
905
  // 파싱이 끝나야 백업·쓰기로 간다 — 깨진 파일이면 .pleum.bak도 남기지 않는다.
817
- const next = transformConfig(paths.claude, (text) => wireClaudeSettings(text, { root: ROOT, key, model: billingOnly ? undefined : model }));
906
+ const next = transformConfig(paths.claude, (text) => {
907
+ previous = originalValues(tool, paths, state, text);
908
+ return wireClaudeSettings(text, { root: ROOT, key, model: billingOnly ? undefined : model });
909
+ });
818
910
  backupOnce(paths.claude, paths.claudeBak);
819
911
  writeSecretFile(paths.claude, next);
820
912
  console.log(t("Wrote {path}", { path: paths.claude }));
@@ -858,7 +950,13 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
858
950
  break;
859
951
  }
860
952
  case "agy": {
861
- const nextAgy = transformConfig(paths.agy, (text) => wireAgySettings(text, { model: billingOnly ? undefined : model }));
953
+ const writes = billingOnly ? undefined : model;
954
+ const nextAgy = transformConfig(paths.agy, (text) => {
955
+ previous = originalValues(tool, paths, state, text);
956
+ // model 없이 재배선하면 이전 wire가 쓴 pleum model을 걷어낸다(파일이 마지막 wire 기록과 어긋나지 않게).
957
+ const stale = agyWiredModel(state.agy);
958
+ return wireAgySettings(!writes && stale ? unwireAgySettings(text, previous, stale) : text, { model: writes });
959
+ });
862
960
  backupOnce(paths.agy, paths.agyBak);
863
961
  writeSecretFile(paths.agy, nextAgy);
864
962
  wireShellTool(paths, "agy", {
@@ -889,17 +987,25 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
889
987
  models,
890
988
  workspace_id: activeContext?.workspace_id,
891
989
  workspace_name: activeContext?.workspace_name,
990
+ ...(previous && { previous }),
892
991
  };
893
992
  writeWiredState(paths, state);
894
993
  }
895
994
 
896
995
  export function revertToolWiring(tool, paths) {
897
996
  const state = readWiredState(paths);
997
+ // 기록도 없고 pleum이 배선한 흔적도 없으면 사용자 본인 설정이다 — 한 글자도 쓰지 않는다.
998
+ if (!state[tool] && !wireStatusOf(paths)[tool]?.wired) {
999
+ console.log(t("{tool} is not wired by PleumRouter; left unchanged.", { tool }));
1000
+ return;
1001
+ }
898
1002
 
899
1003
  switch (tool) {
900
1004
  case "claude": {
901
1005
  if (existsSync(paths.claude)) {
902
- writeSecretFile(paths.claude, transformConfig(paths.claude, unwireClaudeSettings));
1006
+ // 기록이 없으면(0.5.45~46) .pleum.bak에서 원래 값을 뽑는다.
1007
+ const previous = state.claude?.previous ?? pickPrevious("claude", readText(paths.claudeBak)) ?? {};
1008
+ writeSecretFile(paths.claude, transformConfig(paths.claude, (text) => unwireClaudeSettings(text, previous)));
903
1009
  console.log(t("Removed PleumRouter settings from {path}.", { path: paths.claude }));
904
1010
  }
905
1011
  break;
@@ -943,7 +1049,8 @@ export function revertToolWiring(tool, paths) {
943
1049
  }
944
1050
  case "agy": {
945
1051
  if (existsSync(paths.agy)) {
946
- writeSecretFile(paths.agy, transformConfig(paths.agy, unwireAgySettings));
1052
+ const previous = state.agy?.previous ?? pickPrevious("agy", readText(paths.agyBak)) ?? {};
1053
+ writeSecretFile(paths.agy, transformConfig(paths.agy, (text) => unwireAgySettings(text, previous, agyWiredModel(state.agy))));
947
1054
  }
948
1055
  unwireShellTool(paths, "agy");
949
1056
  console.log(t("Removed PleumRouter settings from {path}.", { path: paths.agy }));
@@ -1009,9 +1116,19 @@ export async function wire(args = [], {
1009
1116
  if (failures.length) throw new Error(failures.join("\n"));
1010
1117
  }
1011
1118
 
1119
+ // 배선된 도구: 감지된 것 ∪ 배선 기록(state)에 있는 것(WIRE_TOOLS 순).
1120
+ function wiredTools(paths) {
1121
+ const state = readWiredState(paths);
1122
+ const status = wireStatusOf(paths);
1123
+ return WIRE_TOOLS.filter((tool) => state[tool] || status[tool]?.wired);
1124
+ }
1125
+
1012
1126
  export async function unwire(args = [], { auth = authFetch, home = homedir() } = {}) {
1013
- const { tools, revoke } = parseWireArgs(args);
1127
+ const { tools: named, revoke } = parseWireArgs(args);
1014
1128
  const paths = wirePaths(home);
1129
+ // 도구를 안 적으면 wire 기본값(claude·codex)이 아니라 배선된 도구 전부 — 키를 같이 쓰기 때문이다.
1130
+ const tools = args.some((a) => WIRE_TOOLS.includes(a)) ? named : wiredTools(paths);
1131
+ if (tools.length === 0) console.log(t("No tools are wired by PleumRouter."));
1015
1132
 
1016
1133
  const failures = [];
1017
1134
  for (const tool of tools) {
@@ -1026,6 +1143,12 @@ export async function unwire(args = [], { auth = authFetch, home = homedir() } =
1026
1143
 
1027
1144
  if (!revoke) return;
1028
1145
 
1146
+ // 키는 모든 배선 도구가 공유한다 — 다른 도구가 남아 있으면 폐기하지 않는다(그 도구들이 죽은 키로 실패한다).
1147
+ const others = wiredTools(paths).filter((tool) => !tools.includes(tool));
1148
+ if (others.length) {
1149
+ throw new Error(t("Other tools still use this key: {tools}. Run `pleum unwire --revoke-key` to unwire all of them first.", { tools: others.join(", ") }));
1150
+ }
1151
+
1029
1152
  const stored = (() => {
1030
1153
  try {
1031
1154
  return JSON.parse(readText(paths.wire) ?? "");