pleumrouter 0.5.40 → 0.5.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.40",
3
+ "version": "0.5.42",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/install.mjs CHANGED
@@ -2,22 +2,25 @@ import { spawn } from "node:child_process";
2
2
  import { resolveLaunchCommand } from "./launch.mjs";
3
3
  import { t } from "./i18n.mjs";
4
4
 
5
- // 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이 안전하게 실행한다.
5
+ // 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이 실행한다.
6
6
  //
7
- // 지원 형태(정의된 단계만 실행):
8
- // - 공백 구분 argv 명령 1개 이상("npm i -g x")
9
- // - "&&" 로 이어진 순차 단계("python -m pip install aider-install && aider-install")
10
- // 미지원(셸 문법 — shell:true 없이는 실행 불가, 주입 표면을 만들지 않는다):
11
- // - 파이프("|")·리다이렉트(<>)·치환($`;) 등 → {manual: true} 로 수동 안내.
7
+ // 지원 형태:
8
+ // - 공백 구분 argv("npm i -g x")와 "&&" 순차 단계
9
+ // - 고정된 curl|sh 한 줄. curl과 sh/bash를 각각 spawn하고 stdout만 잇는다.
10
+ // - https .sh 주소 하나(antigravity)는 curl -fsSL | bash 로 같은 경로.
11
+ // 그 외 파이프·리다이렉트·치환은 {manual: true}. 문서 HTML은 실행하지 않는다.
12
12
  // Windows .cmd/.bat spawn(EINVAL)은 resolveLaunchCommand 가 node 진입점으로
13
13
  // 우회해 예방한다(#10).
14
14
 
15
15
  const SHELL_METACHARS = /[|<>;$`' "\\]/;
16
+ const CURL_PIPE = /^curl\s+(-[fsSL]+)\s+(https:\/\/\S+)\s+\|\s+(sh|bash)$/;
16
17
 
17
18
  export function parseInstallCommand(install) {
18
19
  const raw = String(install ?? "").trim();
19
20
  if (!raw) return { manual: true, reason: "empty" };
20
- // URL 단독(antigravity/goose) — 실행할 명령이 아니라 문서 링크.
21
+ const piped = raw.match(CURL_PIPE);
22
+ if (piped) return { pipe: { curl: ["curl", piped[1], piped[2]], shell: piped[3] } };
23
+ if (/^https:\/\/\S+\.sh$/.test(raw)) return { pipe: { curl: ["curl", "-fsSL", raw], shell: "bash" } };
21
24
  if (/^https?:\/\//.test(raw) && !/\s/.test(raw)) return { manual: true, reason: "url" };
22
25
  const steps = raw.split("&&").map((part) => part.trim().split(/\s+/).filter(Boolean));
23
26
  if (steps.some((args) => args.length === 0)) return { manual: true, reason: "empty-step" };
@@ -31,12 +34,60 @@ export function parseInstallCommand(install) {
31
34
  * 설치 명령 실행. {code, manual} 반환 — manual이면 호출자가 수동 안내를 출력한다.
32
35
  * spawnImpl/resolve는 테스트 주입용.
33
36
  */
37
+ function runCurlPipe(pipe, spawnImpl, resolve) {
38
+ let curlCmd;
39
+ let shellCmd;
40
+ try {
41
+ curlCmd = resolve(pipe.curl[0], pipe.curl.slice(1));
42
+ shellCmd = resolve(pipe.shell, []);
43
+ } catch (e) {
44
+ console.error(`✗ ${e.message}`);
45
+ return Promise.resolve(1);
46
+ }
47
+ return new Promise((res) => {
48
+ const curlChild = spawnImpl(curlCmd.command, curlCmd.args, { stdio: ["ignore", "pipe", "inherit"] });
49
+ const shellChild = spawnImpl(shellCmd.command, shellCmd.args, { stdio: ["pipe", "inherit", "inherit"] });
50
+ if (!curlChild.stdout?.pipe || !shellChild.stdin) {
51
+ res(1);
52
+ return;
53
+ }
54
+ curlChild.stdout.pipe(shellChild.stdin);
55
+ let curlCode = null;
56
+ let shellCode = null;
57
+ const finish = () => {
58
+ if (curlCode == null || shellCode == null) return;
59
+ res(curlCode === 0 ? shellCode : curlCode);
60
+ };
61
+ curlChild.on("error", () => {
62
+ curlCode = 1;
63
+ try { shellChild.stdin?.end?.(); } catch { /* pipe가 이미 닫았을 수 있다 */ }
64
+ shellChild.kill?.();
65
+ finish();
66
+ });
67
+ shellChild.on("error", () => {
68
+ shellCode = 1;
69
+ finish();
70
+ });
71
+ curlChild.on("exit", (n) => {
72
+ curlCode = n ?? 1;
73
+ finish();
74
+ });
75
+ shellChild.on("exit", (n) => {
76
+ shellCode = n ?? 1;
77
+ finish();
78
+ });
79
+ });
80
+ }
81
+
34
82
  export async function runInstallCommand(install, {
35
83
  spawnImpl = spawn,
36
84
  resolve = resolveLaunchCommand,
37
85
  } = {}) {
38
86
  const parsed = parseInstallCommand(install);
39
87
  if (parsed.manual) return { manual: true, reason: parsed.reason, code: null };
88
+ if (parsed.pipe) {
89
+ return { manual: false, code: await runCurlPipe(parsed.pipe, spawnImpl, resolve) };
90
+ }
40
91
 
41
92
  for (const args of parsed.steps) {
42
93
  const [bin, ...rest] = args;
@@ -284,6 +284,9 @@ export const ES = {
284
284
  "Anthropic official models": "Continuar con modelos oficiales de Anthropic",
285
285
  "OpenAI official models": "Continuar con modelos oficiales de OpenAI",
286
286
  "Antigravity default models": "Continuar con modelos predeterminados de Antigravity",
287
+ "Gemini official models": "Continuar con modelos oficiales de Gemini",
288
+ "Qwen official models": "Continuar con modelos oficiales de Qwen",
289
+ "Copilot official models": "Continuar con modelos oficiales de Copilot",
287
290
  "Choose models inside the agent": "Elegir modelo dentro del agente",
288
291
  "Choose my model (beta)": "Elegir mi modelo (beta)",
289
292
  "Select a PleumRouter coding model": "Elegir un modelo de código de PleumRouter",
@@ -284,6 +284,9 @@ export const JA = {
284
284
  "Anthropic official models": "Anthropic 公式モデルで続行",
285
285
  "OpenAI official models": "OpenAI 公式モデルで続行",
286
286
  "Antigravity default models": "Antigravity 標準モデルで続行",
287
+ "Gemini official models": "Gemini 公式モデルで続行",
288
+ "Qwen official models": "Qwen 公式モデルで続行",
289
+ "Copilot official models": "Copilot 公式モデルで続行",
287
290
  "Choose models inside the agent": "エージェント内でモデルを選択",
288
291
  "Choose my model (beta)": "モデルを自分で選ぶ (beta)",
289
292
  "Select a PleumRouter coding model": "PleumRouter のコーディングモデルを選択",
@@ -343,6 +343,9 @@ export const KO = {
343
343
  "Anthropic official models": "Anthropic 공식 모델로 진행",
344
344
  "OpenAI official models": "OpenAI 공식 모델로 진행",
345
345
  "Antigravity default models": "Antigravity 기본 모델로 진행",
346
+ "Gemini official models": "Gemini 공식 모델로 진행",
347
+ "Qwen official models": "Qwen 공식 모델로 진행",
348
+ "Copilot official models": "Copilot 공식 모델로 진행",
346
349
  "Choose models inside the agent": "에이전트 안에서 모델 선택",
347
350
  "Choose my model (beta)": "모델 내가 custom하기(beta)",
348
351
  "Select a PleumRouter coding model": "PleumRouter 코딩 모델 선택",
@@ -284,6 +284,9 @@ export const ZH = {
284
284
  "Anthropic official models": "使用 Anthropic 官方模型",
285
285
  "OpenAI official models": "使用 OpenAI 官方模型",
286
286
  "Antigravity default models": "使用 Antigravity 默认模型",
287
+ "Gemini official models": "使用 Gemini 官方模型",
288
+ "Qwen official models": "使用 Qwen 官方模型",
289
+ "Copilot official models": "使用 Copilot 官方模型",
287
290
  "Choose models inside the agent": "在代理中选择模型",
288
291
  "Choose my model (beta)": "自选模型 (beta)",
289
292
  "Select a PleumRouter coding model": "选择 PleumRouter 编程模型",
package/src/menu.mjs CHANGED
@@ -19,6 +19,16 @@ import { parseInstallCommand, runInstallCommand, printManualInstall } from "./in
19
19
  // 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
20
20
  // 번역이 확정돼, 같은 프로세스 안에서 언어를 바꿔도(Settings) 이 설명들만 예전 언어로 남는다.
21
21
  // 실제 번역은 소비 지점(buildAgentItems)에서 매번 t()를 호출해 얻는다.
22
+ const AGENT_MODE_TOOLS = new Set(["claude", "codex", "agy", "gemini", "qwen", "copilot"]);
23
+ const NATIVE_MODE_LABEL = {
24
+ claude: "Anthropic official models",
25
+ codex: "OpenAI official models",
26
+ agy: "Antigravity default models",
27
+ gemini: "Gemini official models",
28
+ qwen: "Qwen official models",
29
+ copilot: "Copilot official models",
30
+ };
31
+
22
32
  const AGENT_DESC = {
23
33
  claude: "Anthropic's coding agent — supports subagents",
24
34
  codex: "OpenAI's coding agent",
@@ -41,8 +51,8 @@ const AGENT_DESC = {
41
51
  interpreter: "Local code execution & pair programmer",
42
52
  };
43
53
 
44
- // 실행 가능한(셸 문법 없는 argv 단계) 설치만 자동 설치 — URL·파이프·리다이렉트는
45
- // 수동 안내한다(#40: naive split이 "|"·"&&" 를 인자로 넘겨 전부 실패하던 결함).
54
+ // 설치 가능한 명령은 엔터로 실행한다. npm -g, curl|sh, https .sh 가 여기 들어온다.
55
+ // 문서 주소와 그 외 셸 문법은 수동 안내로 남긴다.
46
56
  const autoInstallable = (r) => !parseInstallCommand(r.install).manual;
47
57
 
48
58
  // 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
@@ -193,10 +203,8 @@ export async function pickModelFor(item, defaultModel, { supported = () => true,
193
203
  }
194
204
 
195
205
  export function agentModeItems(tool) {
196
- const native = tool === "claude" ? "Anthropic official models"
197
- : tool === "codex" ? "OpenAI official models" : "Antigravity default models";
198
206
  return [
199
- { id: "native", title: t(native), desc: t("Choose models inside the agent") },
207
+ { id: "native", title: t(NATIVE_MODE_LABEL[tool]), desc: t("Choose models inside the agent") },
200
208
  { id: "custom", title: t("Choose my model (beta)"), desc: t("Select a PleumRouter coding model") },
201
209
  ];
202
210
  }
@@ -215,8 +223,7 @@ async function ensureInstalled(r) {
215
223
  return false;
216
224
  }
217
225
  console.log(style.dim(`$ ${r.install}`));
218
- // 공용 실행기: "&&" 순차 단계 지원, Windows .cmd는 resolveLaunchCommand 우회,
219
- // 셸 문법(파이프 등)은 절대 임의 실행하지 않고 수동 안내로 명시한다.
226
+ // 공용 실행기: "&&" 순차 단계와 고정된 curl|sh. Windows .cmd는 resolveLaunchCommand 우회.
220
227
  const result = await runInstallCommand(r.install);
221
228
  if (result.manual) {
222
229
  printManualInstall(r.install);
@@ -363,7 +370,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
363
370
  else if (key.type === "down" || (key.type === "char" && key.value === "j")) move(1);
364
371
  else if (key.type === "right" || (key.type === "char" && key.value === "l")) {
365
372
  const it = items[cursor];
366
- if (!it.tool || it.manual || ["claude", "codex", "agy", "opencode"].includes(it.tool)) continue;
373
+ if (!it.tool || it.manual || it.tool === "opencode" || AGENT_MODE_TOOLS.has(it.tool)) continue;
367
374
  return void (await configure(it));
368
375
  } else continue;
369
376
  dirty = true;
@@ -432,7 +439,7 @@ export async function runAgentMenu({ loop = false } = {}) {
432
439
  }
433
440
 
434
441
  let model = agentPicked.model;
435
- if (["claude", "codex", "agy"].includes(agentItem.tool)) {
442
+ if (AGENT_MODE_TOOLS.has(agentItem.tool)) {
436
443
  const mode = await select(agentModeItems(agentItem.tool), {
437
444
  message: agentItem.title,
438
445
  formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
package/src/recipes.mjs CHANGED
@@ -2,6 +2,17 @@ import { ROOT, V1 } from "./config.mjs";
2
2
 
3
3
  export const DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash";
4
4
 
5
+ // 미지정은 공식/기본 모드. 문자열·custom 객체만 모델 하나를 고정한다.
6
+ function chosenModel(selection) {
7
+ if (selection == null || (typeof selection === "object" && selection.mode === "native")) return undefined;
8
+ const raw = typeof selection === "object" ? selection.model : selection;
9
+ return raw ? String(raw) : undefined;
10
+ }
11
+
12
+ const nativeOrCustom = (explicit) => explicit && typeof explicit === "object"
13
+ ? explicit
14
+ : explicit ? { mode: "custom", model: explicit } : { mode: "native" };
15
+
5
16
  export const CORE_MODELS = [
6
17
  "deepseek-ai/DeepSeek-V4.1-Flash",
7
18
  "claude-sonnet-5",
@@ -35,9 +46,7 @@ export const RECIPES = {
35
46
  bin: "claude",
36
47
  type: "env",
37
48
  unsetEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_CUSTOM_MODEL_OPTION", "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION"],
38
- resolveModel: (explicit) => explicit && typeof explicit === "object"
39
- ? explicit
40
- : explicit ? { mode: "custom", model: explicit } : { mode: "native" },
49
+ resolveModel: nativeOrCustom,
41
50
  install: "npm i -g @anthropic-ai/claude-code",
42
51
  // 앱 랭킹 귀속 메타 — 백엔드 featured_agents / UA fallback과 동기화.
43
52
  attribution: {
@@ -117,9 +126,7 @@ export const RECIPES = {
117
126
  label: "Antigravity CLI",
118
127
  bin: "agy",
119
128
  type: "agy-settings",
120
- resolveModel: (explicit) => explicit && typeof explicit === "object"
121
- ? explicit
122
- : explicit ? { mode: "custom", model: explicit } : { mode: "native" },
129
+ resolveModel: nativeOrCustom,
123
130
  install: "https://antigravity.google/cli/install.sh",
124
131
  note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.",
125
132
  attribution: {
@@ -141,7 +148,7 @@ export const RECIPES = {
141
148
  label: "opencode",
142
149
  bin: "opencode",
143
150
  type: "env",
144
- defaultModel: "gpt-6-sol",
151
+ defaultModel: DEFAULT_MODEL,
145
152
  install: "npm i -g opencode-ai@latest",
146
153
  // opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다.
147
154
  // OpenCode는 openai 공급자 ID에만 Responses용 getModel을 등록한다.
@@ -153,12 +160,14 @@ export const RECIPES = {
153
160
  referer: "https://opencode.ai",
154
161
  },
155
162
  env: (key, model, catalog = []) => {
156
- const active = model || "gpt-6-sol";
163
+ // OpenCode 2는 루트 `--model`을 거절한다. 시작 모델은 이번 실행 설정에만 둔다.
164
+ const active = (chosenModel(model) || DEFAULT_MODEL).split("@")[0];
157
165
  const models = Object.fromEntries(catalog.map(({ id }) => [id, {}]));
158
166
  models[active] = {};
159
167
  return {
160
168
  PLEUM_API_KEY: key,
161
169
  OPENCODE_CONFIG_CONTENT: JSON.stringify({
170
+ model: `openai/${active}`,
162
171
  enabled_providers: ["openai"],
163
172
  disabled_providers: [],
164
173
  provider: {
@@ -173,7 +182,7 @@ export const RECIPES = {
173
182
  }),
174
183
  };
175
184
  },
176
- args: (m) => ["--model", `openai/${m || "gpt-6-sol"}`],
185
+ args: () => [],
177
186
  },
178
187
  aider: {
179
188
  label: "Aider",
@@ -191,7 +200,7 @@ export const RECIPES = {
191
200
  label: "Goose",
192
201
  bin: "goose",
193
202
  type: "env",
194
- install: "https://block.github.io/goose/docs/getting-started/installation",
203
+ install: "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash",
195
204
  note: "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.",
196
205
  attribution: {
197
206
  title: "Goose",
@@ -250,7 +259,13 @@ export const RECIPES = {
250
259
  title: "Qwen Code",
251
260
  referer: "https://github.com/QwenLM/qwen-code",
252
261
  },
253
- env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || DEFAULT_MODEL }),
262
+ resolveModel: nativeOrCustom,
263
+ // 모델이 없으면 비대화형 실행이 OpenAI 호환 인증을 고르지 못한다. 정규 모드는 Qwen 모델로 시작한다.
264
+ env: (key, selection) => ({
265
+ OPENAI_BASE_URL: V1,
266
+ OPENAI_API_KEY: key,
267
+ OPENAI_MODEL: chosenModel(selection) || "qwen3.8-max",
268
+ }),
254
269
  args: () => [],
255
270
  },
256
271
  copilot: {
@@ -263,12 +278,16 @@ export const RECIPES = {
263
278
  title: "GitHub Copilot CLI",
264
279
  referer: "https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli",
265
280
  },
266
- env: (key, model) => ({
267
- COPILOT_PROVIDER_BASE_URL: V1,
268
- COPILOT_PROVIDER_TYPE: "openai",
269
- COPILOT_PROVIDER_API_KEY: key,
270
- COPILOT_MODEL: model || DEFAULT_MODEL,
271
- }),
281
+ resolveModel: nativeOrCustom,
282
+ env: (key, selection) => {
283
+ const model = chosenModel(selection);
284
+ return {
285
+ COPILOT_PROVIDER_BASE_URL: V1,
286
+ COPILOT_PROVIDER_TYPE: "openai",
287
+ COPILOT_PROVIDER_API_KEY: key,
288
+ COPILOT_MODEL: model || DEFAULT_MODEL,
289
+ };
290
+ },
272
291
  args: () => [],
273
292
  },
274
293
  gemini: {
@@ -284,12 +303,17 @@ export const RECIPES = {
284
303
  title: "Gemini CLI",
285
304
  referer: "https://github.com/google-gemini/gemini-cli",
286
305
  },
306
+ resolveModel: nativeOrCustom,
287
307
  env: (key) => ({
288
308
  GOOGLE_GEMINI_BASE_URL: ROOT,
289
309
  GEMINI_API_KEY: key,
290
310
  GOOGLE_API_KEY: key,
291
311
  }),
292
- args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
312
+ args: (selection) => {
313
+ const model = chosenModel(selection);
314
+ if (!model) return [];
315
+ return ["--model", model.startsWith("google/") ? model : `google/${model}`];
316
+ },
293
317
  },
294
318
  pi: {
295
319
  label: "Pi",