pleumrouter 0.5.18 → 0.5.21

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
@@ -35,6 +35,8 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
35
35
  | `pleum login [--device]` | 브라우저 OAuth 로그인 → access/refresh 토큰을 `~/.pleum/config.json`에 저장(API 키 미발급). `--device`는 SSH·CI |
36
36
  | `pleum logout` | 서버 세션 revoke + 로컬 토큰 삭제(언어·기본 모델 등 나머지 설정은 유지) |
37
37
  | `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
38
+ | `pleum wire [claude\|codex]` | Desktop용으로 `~/.claude/settings.json`·`~/.codex/config.toml`에 전용 `plm_` 키를 병합. 생략 시 둘 다. `pleum launch`의 격리 홈과는 별개 |
39
+ | `pleum unwire [claude\|codex] [--revoke-key]` | wire가 넣은 설정만 제거. `--revoke-key`는 서버 키도 폐기 |
38
40
  | `pleum run [model] ["프롬프트"] [--attach image]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. 이미지 업로드는 명시적으로 `--attach ./shot.png`(1회 실행) 또는 REPL의 `@./shot.png`를 사용한다. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
39
41
  | `pleum models [query] [--json]` | 모델 목록(공개) — provider·컨텍스트·가격(₩/1M) 컬럼, `--json`은 스크립트용 |
40
42
  | `pleum models <id> --info` | 모델 상세 — 가격·컨텍스트·provider별 지연/처리량/업타임·주간 사용량·형제 모델 |
package/bin/pleum.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { login, loginDevice, logout } from "../src/login.mjs";
4
4
  import { launch, launchHelp } from "../src/launch.mjs";
5
+ import { wire, unwire, wireHelp, unwireHelp } from "../src/wire.mjs";
5
6
  import { run } from "../src/run.mjs";
6
7
  import { listModels } from "../src/models.mjs";
7
8
  import { balance } from "../src/balance.mjs";
@@ -26,6 +27,8 @@ function buildHelp() {
26
27
  ["pleum launch [tool] [--model id]", t("Run a coding agent against PleumRouter")],
27
28
  ["", `(${Object.keys(RECIPES).join(" · ")})`],
28
29
  ["", t("omit the tool to see the supported list and install status")],
30
+ ["pleum wire [claude|codex]", t("persist config in ~/.claude or ~/.codex for Desktop apps")],
31
+ ["pleum unwire [claude|codex]", t("Removes the PleumRouter provider from Desktop config files")],
29
32
  ['pleum run [model] ["prompt"] [--attach image]', t("Terminal chat (REPL) / one-shot / pipe")],
30
33
  ["", t("omit the model for an arrow-key picker; /help lists commands")],
31
34
  ["pleum models [query] [--json]", t("Model list (price, context included)")],
@@ -43,12 +46,13 @@ function buildHelp() {
43
46
  const body = rows.map(([cmd, desc]) => ` ${cmd.padEnd(width)}${desc}`).join("\n");
44
47
 
45
48
  const examples = [
49
+ ["pleum wire", null],
46
50
  ["pleum launch claude --model claude-sonnet-5", null],
47
51
  ["pleum launch codex --model gpt-5.3-codex -- --full-auto", null],
48
52
  ["pleum run gpt-5.4-mini", t("interactive")],
49
53
  [`pleum run gpt-5.4-mini "${t("Summarize in one line: ...")}"`, t("one-shot")],
50
54
  [`cat error.log | pleum run gpt-5.4-mini "${t("what caused this?")}"`, t("pipe")],
51
- ["pleum models deepseek/deepseek-v4-flash --info", t("model detail")],
55
+ ["pleum models deepseek/deepseek-v4.1-flash --info", t("model detail")],
52
56
  ["pleum usage --by day --days 7", t("last 7 days, by day")],
53
57
  ["pleum login --device", t("SSH/CI environment")],
54
58
  ];
@@ -101,6 +105,14 @@ try {
101
105
  await launch(tool, model, passthrough);
102
106
  break;
103
107
  }
108
+ case "wire":
109
+ if (rest[0] === "-h" || rest[0] === "--help") wireHelp();
110
+ else await wire(rest);
111
+ break;
112
+ case "unwire":
113
+ if (rest[0] === "-h" || rest[0] === "--help") unwireHelp();
114
+ else await unwire(rest);
115
+ break;
104
116
  case "run":
105
117
  await run(rest);
106
118
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.18",
3
+ "version": "0.5.21",
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
@@ -10,5 +10,7 @@ export async function balance() {
10
10
  const data = await res.json();
11
11
  const plm = data.balance_krw;
12
12
  const usd = data.balance_usd != null ? ` / $${data.balance_usd.toFixed(4)}` : "";
13
- console.log(`잔액: ${plm.toLocaleString("ko-KR")} 크레딧${usd}`);
13
+ // 원장 눈금이 0.1이라 잔액은 소수 1자리 고정 표기(딱 떨어져도 ".0"을 남긴다).
14
+ const shown = plm.toLocaleString("ko-KR", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
15
+ console.log(`잔액: ${shown} 크레딧${usd}`);
14
16
  }
@@ -3,7 +3,7 @@
3
3
  import { t } from "./i18n.mjs";
4
4
  import { RECIPES, MANUAL } from "./recipes.mjs";
5
5
 
6
- const COMMANDS = "login logout lang launch run models top balance usage health version upgrade completion help";
6
+ const COMMANDS = "login logout lang launch wire unwire run models top balance usage health version upgrade completion help";
7
7
  const TOOLS = [...Object.keys(RECIPES), ...Object.keys(MANUAL)].join(" ");
8
8
 
9
9
  function bash() {
@@ -15,6 +15,7 @@ _pleum() {
15
15
  fi
16
16
  case "$prev" in
17
17
  launch) COMPREPLY=( $(compgen -W "${TOOLS}" -- "$cur") );;
18
+ wire|unwire) COMPREPLY=( $(compgen -W "claude codex" -- "$cur") );;
18
19
  run) COMPREPLY=( $(compgen -W "$(pleum models 2>/dev/null | awk 'NR>1{print $1}')" -- "$cur") );;
19
20
  completion) COMPREPLY=( $(compgen -W "zsh bash" -- "$cur") );;
20
21
  esac
@@ -31,6 +32,7 @@ _pleum() {
31
32
  fi
32
33
  case \${words[2]} in
33
34
  launch) compadd ${TOOLS};;
35
+ wire|unwire) compadd claude codex;;
34
36
  run) compadd \${(f)"$(pleum models 2>/dev/null | awk 'NR>1{print \$1}')"};;
35
37
  completion) compadd zsh bash;;
36
38
  esac
@@ -155,6 +155,28 @@ export const ES = {
155
155
  "Total": "Total",
156
156
  "Unknown command: /{cmd} (/help)": "Comando desconocido: /{cmd} (/help)",
157
157
  "Unknown command: {cmd}": "Comando desconocido: {cmd}",
158
+ "Usage: pleum wire [claude|codex]\n": "Uso: pleum wire [claude|codex]\n",
159
+ "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "Uso: pleum unwire [claude|codex] [--revoke-key]\n",
160
+ "Example: pleum wire": "Ejemplo: pleum wire",
161
+ "Omit the tool name to wire both claude and codex.": "Si omites el nombre, cablea claude y codex.",
162
+ "persist config in ~/.claude or ~/.codex for Desktop apps": "guardar la config en ~/.claude o ~/.codex para apps Desktop",
163
+ "Removes the PleumRouter provider from Desktop config files": "Quita el proveedor PleumRouter de los archivos Desktop",
164
+ "Unsupported tool: {tool}. Use claude or codex.": "Herramienta no compatible: {tool}. Usa claude o codex.",
165
+ "Wrote {path}": "Escrito en {path}",
166
+ "Wired {tool}.": "Cableado {tool}.",
167
+ "Desktop key: {prefix}…": "Clave de escritorio: {prefix}…",
168
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
169
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (sin /v1), Bearer, pega la clave plm_.",
170
+ "Codex Desktop reads ~/.codex/config.toml (user-level). Restart the app after wiring.":
171
+ "Codex Desktop lee ~/.codex/config.toml del usuario. Reinicia la app después de cablear.",
172
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
173
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, la misma clave plm_. Cursor CLI queda fuera de alcance.",
174
+ "Removed PleumRouter settings from {path}.": "Se quitó la config de PleumRouter de {path}.",
175
+ "No stored desktop API key.": "No hay clave API de escritorio guardada.",
176
+ "Revoked desktop API key {id}.": "Se revocó la clave API de escritorio {id}.",
177
+ "Failed to revoke the desktop API key (HTTP {status})": "No se pudo revocar la clave API de escritorio (HTTP {status})",
178
+ "Failed to create a desktop API key (HTTP {status})": "No se pudo crear la clave API de escritorio (HTTP {status})",
179
+ "kept the desktop key on this machine (logout does not delete it)": "la clave de escritorio se queda en esta máquina (logout no la borra)",
158
180
  "Unsupported language: {code}\n Available: {list}": "Idioma no compatible: {code}\n Disponibles: {list}",
159
181
  "Unsupported tool: {tool}": "Herramienta no compatible: {tool}",
160
182
  "Update CLI": "Actualizar la CLI",
@@ -52,6 +52,7 @@ export const JA = {
52
52
  "Failed to fetch model list: {error}": "モデル一覧の取得に失敗しました: {error}",
53
53
  "Failed to fetch usage ({status})": "使用状況の取得に失敗しました ({status})",
54
54
  "Failed to issue a device code (server responded {status})": "デバイスコードの発行に失敗しました (サーバー応答: {status})",
55
+ "Failed to revoke the desktop API key (HTTP {status})": "デスクトップ API キーの失効に失敗しました (HTTP {status})",
55
56
  "For OpenHands, prefix the model name with openai/ in its config.": "OpenHands の場合、設定でモデル名の先頭に openai/ を付けてください。",
56
57
  "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.":
57
58
  "この実行時のみ使う一時的な PleumRouter プロバイダー設定を生成します — 既存の opencode.json には影響しません。",
@@ -86,6 +87,7 @@ export const JA = {
86
87
  "Legacy API key login is no longer supported. Run:\n pleum login": "レガシー API キーログインはサポートされなくなりました。実行してください:\n pleum login",
87
88
  "Session expired. Run:\n pleum login": "セッションの有効期限が切れました。実行してください:\n pleum login",
88
89
  "OAuth session": "OAuth セッション",
90
+ "Omit the tool name to wire both claude and codex.": "ツール名を省略すると claude と codex の両方を配線します。",
89
91
  "Log in via browser (OAuth session, no API key)": "ブラウザでログイン(OAuth セッション、API キーなし)",
90
92
  "Revoke session and clear local tokens": "セッションを取り消し、ローカル トークンを削除",
91
93
  "Logged out": "ログアウトしました",
@@ -105,6 +107,7 @@ export const JA = {
105
107
  "No matches": "一致なし",
106
108
  "No matches — showing the full list.": "一致なし — 全リストを表示します。",
107
109
  "No matching models": "一致するモデルなし",
110
+ "No stored desktop API key.": "保存されたデスクトップ API キーはありません。",
108
111
  "No such session: {name}": "そのセッションは存在しません: {name}",
109
112
  "No system prompt": "システムプロンプトなし",
110
113
  "No usage history": "使用履歴なし",
@@ -116,6 +119,7 @@ export const JA = {
116
119
  "setup required": "設定が必要",
117
120
  "Installed — press Enter for the {file} setup snippet": "インストール済み — Enterで{file}の設定スニペットを表示",
118
121
  "Override base_url: PLEUM_BASE_URL environment variable": "base_url を上書き: PLEUM_BASE_URL 環境変数",
122
+ "persist config in ~/.claude or ~/.codex for Desktop apps": "Desktop アプリ向けに ~/.claude または ~/.codex に設定を保存",
119
123
  "PleumRouter terminal CLI": "PleumRouter ターミナルCLI",
120
124
  "Press Enter to install now": "Enter を押すと今すぐインストールします",
121
125
  "Print a shell completion script": "シェル補完スクリプトを出力",
@@ -123,6 +127,8 @@ export const JA = {
123
127
  "Print the version": "バージョンを表示",
124
128
  "Provider share": "プロバイダーシェア",
125
129
  "Reinstall the latest version": "最新バージョンを再インストール",
130
+ "Removes the PleumRouter provider from Desktop config files": "Desktop 設定ファイルから PleumRouter プロバイダーを削除",
131
+ "Revoked desktop API key {id}.": "デスクトップ API キー {id} を失効しました。",
126
132
  "Reinstalling — pleum will exit when finished. Run `pleum` again afterward.": "再インストール中です — 完了するとpleumは終了します。その後もう一度pleumを実行してください。",
127
133
  "Run a coding agent against PleumRouter": "PleumRouter に対してコーディングエージェントを実行",
128
134
  "Run pleum login again to get a new key (recent keys include it by default).": "新しいキーを取得するには pleum login を再実行してください (最近のキーはデフォルトで含まれます)。",
@@ -155,8 +161,22 @@ export const JA = {
155
161
  "Total": "合計",
156
162
  "Unknown command: /{cmd} (/help)": "不明なコマンド: /{cmd} (/help)",
157
163
  "Unknown command: {cmd}": "不明なコマンド: {cmd}",
164
+ "Example: pleum wire": "例: pleum wire",
165
+ "Wrote {path}": "{path} に書きました",
166
+ "Wired {tool}.": "{tool} を配線しました。",
167
+ "Desktop key: {prefix}…": "デスクトップキー: {prefix}…",
168
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
169
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root}(/v1 なし)、Bearer、plm_ キーを貼り付けます。",
170
+ "Codex Desktop reads ~/.codex/config.toml (user-level). Restart the app after wiring.":
171
+ "Codex Desktop はユーザーの ~/.codex/config.toml を読みます。配線後にアプリを再起動してください。",
172
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
173
+ "Cursor IDE: Settings → Models → OpenAI Compatible。Base URL {v1}、同じ plm_ キー。Cursor CLI は対象外です。",
174
+ "Removed PleumRouter settings from {path}.": "{path} から PleumRouter 設定を削除しました。",
175
+ "Failed to create a desktop API key (HTTP {status})": "デスクトップ API キーの発行に失敗しました (HTTP {status})",
176
+ "kept the desktop key on this machine (logout does not delete it)": "このマシンのデスクトップキーは残します(logout では消しません)",
158
177
  "Unsupported language: {code}\n Available: {list}": "サポートされていない言語です: {code}\n 利用可能: {list}",
159
178
  "Unsupported tool: {tool}": "サポートされていないツールです: {tool}",
179
+ "Unsupported tool: {tool}. Use claude or codex.": "サポートされていないツールです: {tool}。claude または codex を指定してください。",
160
180
  "Update CLI": "CLIを更新",
161
181
  "Usage": "使用状況",
162
182
  "Usage report": "使用状況レポート",
@@ -164,6 +184,8 @@ export const JA = {
164
184
  "Usage: pleum launch <tool> [--model id] [-- tool args]\n": "使い方: pleum launch <tool> [--model id] [-- tool args]\n",
165
185
  "Usage: pleum models <model-id> --info": "使い方: pleum models <model-id> --info",
166
186
  "Usage: pleum run <model> [\"prompt\"]\nModel list: pleum models": "使い方: pleum run <model> [\"prompt\"]\nモデル一覧: pleum models",
187
+ "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "使い方: pleum unwire [claude|codex] [--revoke-key]\n",
188
+ "Usage: pleum wire [claude|codex]\n": "使い方: pleum wire [claude|codex]\n",
167
189
  "Use default (clear)": "デフォルトを使用(解除)",
168
190
  "Your spend report (model|provider|api_key|day)": "支出レポート (model|provider|api_key|day)",
169
191
  "by day": "日別",
@@ -266,6 +266,28 @@ export const KO = {
266
266
  "Override base_url: PLEUM_BASE_URL environment variable": "base_url 덮어쓰기: PLEUM_BASE_URL 환경변수",
267
267
  "Logged out": "로그아웃됨",
268
268
  "Unknown command: {cmd}": "알 수 없는 명령: {cmd}",
269
+ "Usage: pleum wire [claude|codex]\n": "사용법: pleum wire [claude|codex]\n",
270
+ "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "사용법: pleum unwire [claude|codex] [--revoke-key]\n",
271
+ "Example: pleum wire": "예: pleum wire",
272
+ "Omit the tool name to wire both claude and codex.": "도구 이름을 생략하면 claude와 codex를 모두 배선합니다.",
273
+ "persist config in ~/.claude or ~/.codex for Desktop apps": "Desktop 앱용으로 ~/.claude 또는 ~/.codex에 설정을 저장",
274
+ "Removes the PleumRouter provider from Desktop config files": "Desktop 설정 파일에서 PleumRouter 프로바이더를 제거",
275
+ "Unsupported tool: {tool}. Use claude or codex.": "지원하지 않는 도구: {tool}. claude 또는 codex를 쓰세요.",
276
+ "Wrote {path}": "{path}에 썼습니다",
277
+ "Wired {tool}.": "{tool}을 배선했습니다.",
278
+ "Desktop key: {prefix}…": "데스크톱 키: {prefix}…",
279
+ "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
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (/v1 없음), Bearer, plm_ 키를 붙여넣으세요.",
281
+ "Codex Desktop reads ~/.codex/config.toml (user-level). Restart the app after wiring.":
282
+ "Codex Desktop은 사용자 ~/.codex/config.toml을 읽습니다. 배선 후 앱을 재시작하세요.",
283
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
284
+ "Cursor IDE: 설정 → Models → OpenAI Compatible. Base URL {v1}, 같은 plm_ 키. Cursor CLI는 범위 밖입니다.",
285
+ "Removed PleumRouter settings from {path}.": "{path}에서 PleumRouter 설정을 제거했습니다.",
286
+ "No stored desktop API key.": "저장된 데스크톱 API 키가 없습니다.",
287
+ "Revoked desktop API key {id}.": "데스크톱 API 키 {id}를 폐기했습니다.",
288
+ "Failed to revoke the desktop API key (HTTP {status})": "데스크톱 API 키 폐기에 실패했습니다 (HTTP {status})",
289
+ "Failed to create a desktop API key (HTTP {status})": "데스크톱 API 키 발급에 실패했습니다 (HTTP {status})",
290
+ "kept the desktop key on this machine (logout does not delete it)": "이 기기의 데스크톱 키는 남겨 둡니다 (logout이 지우지 않음)",
269
291
 
270
292
  // settings.mjs
271
293
  "Settings": "설정",
@@ -155,6 +155,28 @@ export const ZH = {
155
155
  "Total": "总计",
156
156
  "Unknown command: /{cmd} (/help)": "未知命令:/{cmd} (/help)",
157
157
  "Unknown command: {cmd}": "未知命令:{cmd}",
158
+ "Usage: pleum wire [claude|codex]\n": "用法:pleum wire [claude|codex]\n",
159
+ "Usage: pleum unwire [claude|codex] [--revoke-key]\n": "用法:pleum unwire [claude|codex] [--revoke-key]\n",
160
+ "Example: pleum wire": "示例:pleum wire",
161
+ "Omit the tool name to wire both claude and codex.": "省略工具名会同时接线 claude 和 codex。",
162
+ "persist config in ~/.claude or ~/.codex for Desktop apps": "为 Desktop 应用把配置写入 ~/.claude 或 ~/.codex",
163
+ "Removes the PleumRouter provider from Desktop config files": "从 Desktop 配置文件中移除 PleumRouter 提供方",
164
+ "Unsupported tool: {tool}. Use claude or codex.": "不支持的工具:{tool}。请使用 claude 或 codex。",
165
+ "Wrote {path}": "已写入 {path}",
166
+ "Wired {tool}.": "已接线 {tool}。",
167
+ "Desktop key: {prefix}…": "桌面密钥:{prefix}…",
168
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
169
+ "Claude Code Desktop:Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference。Gateway base URL {root}(不要 /v1),Bearer,粘贴 plm_ 密钥。",
170
+ "Codex Desktop reads ~/.codex/config.toml (user-level). Restart the app after wiring.":
171
+ "Codex Desktop 读取用户级 ~/.codex/config.toml。接线后请重启应用。",
172
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.":
173
+ "Cursor IDE:设置 → Models → OpenAI Compatible。Base URL {v1},同一把 plm_ 密钥。Cursor CLI 不在范围内。",
174
+ "Removed PleumRouter settings from {path}.": "已从 {path} 移除 PleumRouter 设置。",
175
+ "No stored desktop API key.": "没有保存的桌面 API 密钥。",
176
+ "Revoked desktop API key {id}.": "已撤销桌面 API 密钥 {id}。",
177
+ "Failed to revoke the desktop API key (HTTP {status})": "撤销桌面 API 密钥失败(HTTP {status})",
178
+ "Failed to create a desktop API key (HTTP {status})": "签发桌面 API 密钥失败(HTTP {status})",
179
+ "kept the desktop key on this machine (logout does not delete it)": "本机桌面密钥会保留(logout 不会删除)",
158
180
  "Unsupported language: {code}\n Available: {list}": "不支持的语言:{code}\n 可用:{list}",
159
181
  "Unsupported tool: {tool}": "不支持的工具:{tool}",
160
182
  "Update CLI": "更新 CLI",
package/src/recipes.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ROOT, V1 } from "./config.mjs";
2
2
 
3
- // 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/agent-integration + 에이전트별 공식 문서 조사(2026-07).
3
+ // 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서 조사(2026-07).
4
4
  // type "env" → 환경변수만 세팅하고 바로 실행.
5
5
  // type "codex-home" → 격리된 CODEX_HOME에 config.toml을 써서 실행(사용자 ~/.codex 보존).
6
6
  // note 필드는 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점에 번역이
@@ -218,6 +218,7 @@ name = "PleumRouter"
218
218
  base_url = ${tomlBasicString(V1)}
219
219
  env_key = "PLEUM_API_KEY"
220
220
  wire_api = "responses"
221
+ supports_websockets = false
221
222
  `;
222
223
  }
223
224
 
package/src/usage.mjs CHANGED
@@ -19,7 +19,9 @@ export function parseUsageArgs(rest = []) {
19
19
  return { by, days };
20
20
  }
21
21
 
22
- const fmtKrw = (n) => `${Math.round(n).toLocaleString("en-US")} 크레딧`;
22
+ // 원장 눈금 0.1 — Math.round로 자르면 소액 사용이 0으로 보인다.
23
+ const fmtKrw = (n) =>
24
+ `${Number(n).toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} 크레딧`;
23
25
 
24
26
  // report 응답 → 표시 문자열(순수 함수 — fixture 테스트 대상).
25
27
  export function renderUsage(data, styler = style) {
package/src/wire.mjs ADDED
@@ -0,0 +1,288 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname, join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { ROOT, V1, authFetch } from "./config.mjs";
5
+ import { tomlBasicString } from "./recipes.mjs";
6
+ import { t } from "./i18n.mjs";
7
+
8
+ export const WIRE_TOOLS = ["claude", "codex"];
9
+ export const WIRE_KEY_NAME = "pleum-desktop";
10
+ export const WIRE_MARKER_START = "# pleum-wire-start";
11
+ export const WIRE_MARKER_END = "# pleum-wire-end";
12
+
13
+ const CLAUDE_ENV_KEYS = ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"];
14
+
15
+ export function wirePaths(home = homedir()) {
16
+ const claude = join(home, ".claude", "settings.json");
17
+ const codex = join(home, ".codex", "config.toml");
18
+ return {
19
+ home,
20
+ wire: join(home, ".pleum", "wire.json"),
21
+ claude,
22
+ claudeBak: `${claude}.pleum.bak`,
23
+ codex,
24
+ codexBak: `${codex}.pleum.bak`,
25
+ };
26
+ }
27
+
28
+ function readText(path) {
29
+ try {
30
+ return readFileSync(path, "utf8");
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function writeSecretFile(path, contents, mode = 0o600) {
37
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
38
+ writeFileSync(path, contents, { mode });
39
+ }
40
+
41
+ function backupOnce(src, bak) {
42
+ if (existsSync(src) && !existsSync(bak)) writeSecretFile(bak, readFileSync(src));
43
+ }
44
+
45
+ function stripWireBlock(toml) {
46
+ const re = new RegExp(
47
+ `${WIRE_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${WIRE_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`,
48
+ );
49
+ return toml.replace(re, "");
50
+ }
51
+
52
+ function removeTomlTable(toml, name) {
53
+ const header = `[${name}]`;
54
+ const lines = toml.split("\n");
55
+ const out = [];
56
+ let skip = false;
57
+ for (const line of lines) {
58
+ const trimmed = line.trim();
59
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) skip = trimmed === header;
60
+ if (!skip) out.push(line);
61
+ }
62
+ return out.join("\n");
63
+ }
64
+
65
+ function readPreviousProvider(toml) {
66
+ const marked = toml.match(/# previous_model_provider = (.*)/);
67
+ if (marked) return marked[1].trim();
68
+ const stripped = stripWireBlock(toml);
69
+ const top = stripped.match(/^model_provider\s*=\s*(.+)$/m);
70
+ return top ? top[1].trim() : "";
71
+ }
72
+
73
+ export function wireClaudeSettings(existingJson, { root, key }) {
74
+ let obj = {};
75
+ if (existingJson && existingJson.trim()) {
76
+ obj = JSON.parse(existingJson);
77
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) obj = {};
78
+ }
79
+ obj.env = {
80
+ ...(obj.env && typeof obj.env === "object" && !Array.isArray(obj.env) ? obj.env : {}),
81
+ ANTHROPIC_BASE_URL: root,
82
+ ANTHROPIC_AUTH_TOKEN: key,
83
+ ANTHROPIC_API_KEY: key,
84
+ };
85
+ return `${JSON.stringify(obj, null, 2)}\n`;
86
+ }
87
+
88
+ export function unwireClaudeSettings(existingJson) {
89
+ if (!existingJson || !existingJson.trim()) return `${JSON.stringify({}, null, 2)}\n`;
90
+ const obj = JSON.parse(existingJson);
91
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
92
+ return `${JSON.stringify({}, null, 2)}\n`;
93
+ }
94
+ if (obj.env && typeof obj.env === "object" && !Array.isArray(obj.env)) {
95
+ for (const envKey of CLAUDE_ENV_KEYS) delete obj.env[envKey];
96
+ if (Object.keys(obj.env).length === 0) delete obj.env;
97
+ }
98
+ return `${JSON.stringify(obj, null, 2)}\n`;
99
+ }
100
+
101
+ export function wireCodexToml(existingToml, { v1, key }) {
102
+ const src = existingToml ?? "";
103
+ const previous = readPreviousProvider(src);
104
+ let body = stripWireBlock(src);
105
+ body = body.replace(/^model_provider\s*=\s*.+\n?/m, "");
106
+ body = removeTomlTable(body, "model_providers.pleum.http_headers");
107
+ body = removeTomlTable(body, "model_providers.pleum");
108
+ const block = `${WIRE_MARKER_START}
109
+ # previous_model_provider = ${previous}
110
+ model_provider = "pleum"
111
+
112
+ [model_providers.pleum]
113
+ name = "PleumRouter"
114
+ base_url = ${tomlBasicString(v1)}
115
+ wire_api = "responses"
116
+ supports_websockets = false
117
+
118
+ [model_providers.pleum.http_headers]
119
+ Authorization = ${tomlBasicString(`Bearer ${key}`)}
120
+ ${WIRE_MARKER_END}
121
+ `;
122
+ const trimmed = body.replace(/\s+$/, "");
123
+ return `${trimmed ? `${trimmed}\n\n` : ""}${block}`;
124
+ }
125
+
126
+ export function unwireCodexToml(existingToml) {
127
+ const src = existingToml ?? "";
128
+ const previous = readPreviousProvider(src);
129
+ let body = stripWireBlock(src);
130
+ body = removeTomlTable(body, "model_providers.pleum.http_headers");
131
+ body = removeTomlTable(body, "model_providers.pleum");
132
+ if (previous && previous !== '""' && !/^model_provider\s*=/m.test(body)) {
133
+ body = `model_provider = ${previous}\n${body.replace(/^\n+/, "")}`;
134
+ }
135
+ return body.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
136
+ }
137
+
138
+ function parseJsonBody(res) {
139
+ return res.json().catch(() => ({}));
140
+ }
141
+
142
+ export async function ensureWireKey({
143
+ auth = authFetch,
144
+ paths = wirePaths(),
145
+ } = {}) {
146
+ const stored = (() => {
147
+ try {
148
+ return JSON.parse(readText(paths.wire) ?? "");
149
+ } catch {
150
+ return null;
151
+ }
152
+ })();
153
+ if (stored?.key && stored?.id) {
154
+ const listed = await auth("/keys");
155
+ if (listed.ok) {
156
+ const data = await parseJsonBody(listed);
157
+ const items = Array.isArray(data.items) ? data.items : [];
158
+ const found = items.find((item) => String(item.id) === String(stored.id));
159
+ if (found && found.is_active !== false) return stored;
160
+ } else if (listed.status !== 404) {
161
+ return stored;
162
+ }
163
+ }
164
+
165
+ const created = await auth("/keys", {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ name: WIRE_KEY_NAME }),
169
+ });
170
+ if (!created.ok) {
171
+ throw new Error(t("Failed to create a desktop API key (HTTP {status})", { status: created.status }));
172
+ }
173
+ const data = await parseJsonBody(created);
174
+ if (!data.key || !data.id) {
175
+ throw new Error(t("Failed to create a desktop API key (HTTP {status})", { status: created.status }));
176
+ }
177
+ const record = { id: data.id, key: data.key, name: data.name ?? WIRE_KEY_NAME };
178
+ writeSecretFile(paths.wire, `${JSON.stringify(record, null, 2)}\n`);
179
+ return record;
180
+ }
181
+
182
+ export function parseWireArgs(args) {
183
+ const revoke = args.includes("--revoke-key");
184
+ const tools = args.filter((arg) => arg !== "--revoke-key");
185
+ if (tools.length === 0) return { tools: [...WIRE_TOOLS], revoke };
186
+ const unknown = tools.find((tool) => !WIRE_TOOLS.includes(tool));
187
+ if (unknown) {
188
+ throw new Error(t("Unsupported tool: {tool}. Use claude or codex.", { tool: unknown }));
189
+ }
190
+ return { tools: [...new Set(tools)], revoke };
191
+ }
192
+
193
+ export function wireHelp() {
194
+ console.log(t("Usage: pleum wire [claude|codex]\n"));
195
+ console.log(` ${t("Omit the tool name to wire both claude and codex.")}`);
196
+ console.log(` ${t("Example: pleum wire")}`);
197
+ }
198
+
199
+ export function unwireHelp() {
200
+ console.log(t("Usage: pleum unwire [claude|codex] [--revoke-key]\n"));
201
+ }
202
+
203
+ function printDesktopHints(record) {
204
+ const prefix = String(record.key).slice(0, 8);
205
+ console.log(t("Desktop key: {prefix}…", { prefix }));
206
+ console.log(
207
+ t(
208
+ "Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.",
209
+ { root: ROOT },
210
+ ),
211
+ );
212
+ console.log(t("Codex Desktop reads ~/.codex/config.toml (user-level). Restart the app after wiring."));
213
+ console.log(
214
+ t(
215
+ "Cursor IDE: Settings → Models → OpenAI Compatible. Base URL {v1}, same plm_ key. Cursor CLI is out of scope.",
216
+ { v1: V1 },
217
+ ),
218
+ );
219
+ console.log(t("kept the desktop key on this machine (logout does not delete it)"));
220
+ }
221
+
222
+ function applyClaude(paths, key) {
223
+ backupOnce(paths.claude, paths.claudeBak);
224
+ const next = wireClaudeSettings(readText(paths.claude), { root: ROOT, key });
225
+ writeSecretFile(paths.claude, next);
226
+ console.log(t("Wrote {path}", { path: paths.claude }));
227
+ }
228
+
229
+ function applyCodex(paths, key) {
230
+ backupOnce(paths.codex, paths.codexBak);
231
+ const next = wireCodexToml(readText(paths.codex) ?? "", { v1: V1, key });
232
+ writeSecretFile(paths.codex, next);
233
+ console.log(t("Wrote {path}", { path: paths.codex }));
234
+ }
235
+
236
+ function revertClaude(paths) {
237
+ if (!existsSync(paths.claude)) return;
238
+ writeSecretFile(paths.claude, unwireClaudeSettings(readText(paths.claude)));
239
+ console.log(t("Removed PleumRouter settings from {path}.", { path: paths.claude }));
240
+ }
241
+
242
+ function revertCodex(paths) {
243
+ if (!existsSync(paths.codex)) return;
244
+ writeSecretFile(paths.codex, unwireCodexToml(readText(paths.codex) ?? ""));
245
+ console.log(t("Removed PleumRouter settings from {path}.", { path: paths.codex }));
246
+ }
247
+
248
+ export async function wire(args, { auth = authFetch, home = homedir() } = {}) {
249
+ const { tools } = parseWireArgs(args);
250
+ const paths = wirePaths(home);
251
+ const record = await ensureWireKey({ auth, paths });
252
+ if (tools.includes("claude")) applyClaude(paths, record.key);
253
+ if (tools.includes("codex")) applyCodex(paths, record.key);
254
+ for (const tool of tools) console.log(t("Wired {tool}.", { tool }));
255
+ printDesktopHints(record);
256
+ }
257
+
258
+ export async function unwire(args, { auth = authFetch, home = homedir() } = {}) {
259
+ const { tools, revoke } = parseWireArgs(args);
260
+ const paths = wirePaths(home);
261
+ if (tools.includes("claude")) revertClaude(paths);
262
+ if (tools.includes("codex")) revertCodex(paths);
263
+ if (!revoke) return;
264
+
265
+ const stored = (() => {
266
+ try {
267
+ return JSON.parse(readText(paths.wire) ?? "");
268
+ } catch {
269
+ return null;
270
+ }
271
+ })();
272
+ if (!stored?.id) {
273
+ console.log(t("No stored desktop API key."));
274
+ return;
275
+ }
276
+ const res = await auth(`/keys/${stored.id}`, { method: "DELETE" });
277
+ if (!res.ok && res.status !== 404) {
278
+ throw new Error(t("Failed to revoke the desktop API key (HTTP {status})", { status: res.status }));
279
+ }
280
+ if (existsSync(paths.wire)) {
281
+ try {
282
+ unlinkSync(paths.wire);
283
+ } catch {
284
+ // already gone
285
+ }
286
+ }
287
+ console.log(t("Revoked desktop API key {id}.", { id: stored.id }));
288
+ }