pleumrouter 0.5.25 → 0.5.26

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
@@ -11,7 +11,7 @@ import { top } from "../src/top.mjs";
11
11
  import { health } from "../src/health.mjs";
12
12
  import { completion } from "../src/completion.mjs";
13
13
  import { upgrade } from "../src/upgrade.mjs";
14
- import { mainMenu } from "../src/menu.mjs";
14
+ import { mainMenu, runAgentMenu } from "../src/menu.mjs";
15
15
  import { lang } from "../src/lang.mjs";
16
16
  import { RECIPES } from "../src/recipes.mjs";
17
17
  import { t } from "../src/i18n.mjs";
@@ -98,6 +98,11 @@ try {
98
98
  case "launch": {
99
99
  const tool = rest[0];
100
100
  if (!tool) {
101
+ const io = process.stdin;
102
+ if (io.isTTY && process.stdout.isTTY && typeof io.setRawMode === "function") {
103
+ await runAgentMenu({ loop: false });
104
+ break;
105
+ }
101
106
  launchHelp();
102
107
  break;
103
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,7 @@ export async function select(items, {
100
100
  formatItem = String,
101
101
  matches = (all, q) => (q ? all.filter((it) => String(formatItem(it)).toLowerCase().includes(q.toLowerCase())) : all),
102
102
  limit = 10,
103
+ initialCursor = 0,
103
104
  input = process.stdin,
104
105
  output = process.stdout,
105
106
  } = {}) {
@@ -107,7 +108,7 @@ export async function select(items, {
107
108
 
108
109
  return new Promise((resolve) => {
109
110
  let query = "";
110
- let cursor = 0;
111
+ let cursor = Math.max(0, Math.min(initialCursor, Math.max(0, (items?.length ?? 1) - 1)));
111
112
  let lastRows = 0;
112
113
 
113
114
  const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
package/src/launch.mjs CHANGED
@@ -126,7 +126,7 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
126
126
  if (r.type === "crush-home") {
127
127
  env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model));
128
128
  }
129
- if (r.type === "agy-settings") ensureAgySettings();
129
+ if (r.type === "agy-settings") ensureAgySettings(model);
130
130
 
131
131
  const args = [...r.args(model), ...passthrough];
132
132
  if (r.note) console.log(`ℹ ${t(r.note)}`);
@@ -171,12 +171,12 @@ function writeIsolatedHome(name, filename, contents) {
171
171
  return home;
172
172
  }
173
173
 
174
- function ensureAgySettings() {
174
+ function ensureAgySettings(model) {
175
175
  const dir = join(homedir(), ".gemini", "antigravity-cli");
176
176
  const path = join(dir, "settings.json");
177
177
  mkdirSync(dir, { recursive: true, mode: 0o700 });
178
178
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
179
- writeFileSync(path, `${JSON.stringify(mergeAgySettings(existing), null, 2)}\n`, { mode: 0o600 });
179
+ writeFileSync(path, `${JSON.stringify(mergeAgySettings(existing, model), null, 2)}\n`, { mode: 0o600 });
180
180
  }
181
181
 
182
182
  function printManual(tool, key) {
package/src/menu.mjs CHANGED
@@ -14,7 +14,7 @@ import { createInterface } from "node:readline";
14
14
  import { style, displayWidth, terminalWidth, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
15
15
  import { keyFeeder, select } from "./interactive.mjs";
16
16
  import { readConfig, writeConfig, isLoggedIn, touchSession } from "./config.mjs";
17
- import { RECIPES, MANUAL } from "./recipes.mjs";
17
+ import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
18
18
  import { onPath, launch } from "./launch.mjs";
19
19
  import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
20
20
  import { run } from "./run.mjs";
@@ -120,7 +120,7 @@ function saveLastSelection(id) {
120
120
  writeConfig(cfg);
121
121
  }
122
122
 
123
- async function pickModelFor(item) {
123
+ export async function pickModelFor(item, defaultModel) {
124
124
  let models;
125
125
  try {
126
126
  models = await fetchModels();
@@ -128,11 +128,14 @@ async function pickModelFor(item) {
128
128
  console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
129
129
  return undefined;
130
130
  }
131
+ const targetId = defaultModel || DEFAULT_MODEL;
132
+ const initialIndex = models.findIndex((x) => x.id === targetId);
131
133
  const m = await select(models, {
132
134
  message: `${item.title} — ${t("select a model")}`,
133
135
  formatItem: (x) => formatModelLabel(x),
134
136
  matches: matchModels,
135
- limit: 12,
137
+ limit: 10,
138
+ initialCursor: initialIndex >= 0 ? initialIndex : 0,
136
139
  });
137
140
  return m?.id;
138
141
  }
@@ -229,7 +232,8 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
229
232
  // 선택되면 ollama처럼 즉시 실행으로 이어가고, 취소면 메뉴 복귀.
230
233
  const configure = async (it) => {
231
234
  suspend();
232
- const picked = await pickModelFor(it);
235
+ const currentModel = it.tool ? (readConfig()?.launch_models?.[it.tool] || DEFAULT_MODEL) : undefined;
236
+ const picked = await pickModelFor(it, currentModel);
233
237
  if (picked) {
234
238
  input.pause();
235
239
  resolve({ item: it, model: picked });
@@ -292,6 +296,48 @@ async function agentSelect() {
292
296
  });
293
297
  }
294
298
 
299
+ // 에이전트 실행 메뉴 — bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유.
300
+ // 에이전트를 고르면 곧바로 핵심 10개 모델 피커를 띄워 기본 모델(deepseek-v4.1-flash) 또는
301
+ // 선택한 우리 모델로 코딩 에이전트를 실행한다.
302
+ export async function runAgentMenu({ loop = false } = {}) {
303
+ const io = process.stdin;
304
+ if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
305
+ if (!isLoggedIn()) await login();
306
+ else await touchSession().catch(() => {});
307
+
308
+ for (;;) {
309
+ const agentPicked = await agentSelect();
310
+ if (agentPicked === "quit") return "quit";
311
+ if (!agentPicked) return null;
312
+
313
+ const { item: agentItem } = agentPicked;
314
+ if (agentItem.manual) {
315
+ await launch(agentItem.tool, undefined, [], { exit: false });
316
+ if (!loop) return true;
317
+ continue;
318
+ }
319
+
320
+ const r = RECIPES[agentItem.tool];
321
+ if (!onPath(r.bin) && !(await ensureInstalled(r))) {
322
+ if (!loop) return false;
323
+ continue;
324
+ }
325
+
326
+ const agentSaved = readConfig()?.launch_models ?? {};
327
+ let chosenModel = agentPicked.model;
328
+ if (!chosenModel) {
329
+ const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
330
+ chosenModel = await pickModelFor(agentItem, defaultForTool);
331
+ if (!chosenModel) continue; // Esc 취소 시 에이전트 목록으로 복귀
332
+ }
333
+
334
+ saveModel(agentItem.tool, chosenModel);
335
+ await launch(agentItem.tool, chosenModel, [], { exit: false });
336
+ console.log();
337
+ if (!loop) return true;
338
+ }
339
+ }
340
+
295
341
  // bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
296
342
  // ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
297
343
  export async function mainMenu() {
@@ -342,19 +388,9 @@ export async function mainMenu() {
342
388
  continue; // esc/q — 메인 메뉴로 복귀
343
389
  }
344
390
  case "agents": {
345
- const agentPicked = await agentSelect();
346
- if (agentPicked === "quit") return true; // 서브메뉴에서 ctrl+c → 전체 종료
347
- if (!agentPicked) continue; // esc/q — 메인 메뉴로 복귀
348
- const { item: agentItem, model: agentModel } = agentPicked;
349
- if (agentModel && !agentItem.manual) saveModel(agentItem.tool, agentModel);
350
- if (!agentItem.manual) {
351
- const r = RECIPES[agentItem.tool];
352
- if (!onPath(r.bin) && !(await ensureInstalled(r))) continue;
353
- }
354
- const agentSaved = readConfig()?.launch_models ?? {};
355
- await launch(agentItem.tool, agentModel ?? agentSaved[agentItem.tool], [], { exit: false });
356
- console.log(); // 에이전트 출력과 다음 메뉴 사이 여백
357
- continue; // ollama처럼 에이전트 종료 후 메뉴 복귀
391
+ const r = await runAgentMenu({ loop: true });
392
+ if (r === "quit") return true;
393
+ continue;
358
394
  }
359
395
  }
360
396
  }
package/src/recipes.mjs CHANGED
@@ -1,5 +1,20 @@
1
1
  import { ROOT, V1 } from "./config.mjs";
2
2
 
3
+ export const DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash";
4
+
5
+ export const CORE_MODELS = [
6
+ "deepseek-ai/DeepSeek-V4.1-Flash",
7
+ "claude-sonnet-5",
8
+ "claude-opus-5-5",
9
+ "gpt-6-astra",
10
+ "claude-fable-5-1",
11
+ "gemini-3.8-flash",
12
+ "glm-5.3-flash",
13
+ "kimi-k3",
14
+ "gpt-6-sol",
15
+ "gpt-6-luna",
16
+ ];
17
+
3
18
  // 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
4
19
  // type "env" → 환경변수만 세팅하고 바로 실행.
5
20
  // type "codex-home" → 격리 CODEX_HOME에 config.toml (사용자 ~/.codex 보존).
@@ -34,7 +49,7 @@ export const RECIPES = {
34
49
  // /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
35
50
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
36
51
  }),
37
- args: (m) => (m ? ["--model", `anthropic/${m}`] : []),
52
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
38
53
  },
39
54
  codex: {
40
55
  label: "Codex CLI",
@@ -58,7 +73,7 @@ export const RECIPES = {
58
73
  referer: "https://aider.chat",
59
74
  },
60
75
  env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
61
- args: (m) => (m ? ["--model", `openai/${m}`] : []),
76
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
62
77
  },
63
78
  goose: {
64
79
  label: "Goose",
@@ -98,20 +113,25 @@ export const RECIPES = {
98
113
  title: "opencode",
99
114
  referer: "https://opencode.ai",
100
115
  },
101
- env: (key, model) => ({
102
- PLEUM_API_KEY: key,
103
- OPENCODE_CONFIG_CONTENT: JSON.stringify({
104
- provider: {
105
- pleum: {
106
- npm: "@ai-sdk/openai-compatible",
107
- name: "PleumRouter",
108
- options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
109
- models: { [model || "gpt-5.4-mini"]: {} },
116
+ env: (key, model) => {
117
+ const active = model || DEFAULT_MODEL;
118
+ const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
119
+ models[active] = {};
120
+ return {
121
+ PLEUM_API_KEY: key,
122
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
123
+ provider: {
124
+ pleum: {
125
+ npm: "@ai-sdk/openai-compatible",
126
+ name: "PleumRouter",
127
+ options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
128
+ models,
129
+ },
110
130
  },
111
- },
112
- }),
113
- }),
114
- args: (m) => ["--model", `pleum/${m || "gpt-5.4-mini"}`],
131
+ }),
132
+ };
133
+ },
134
+ args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
115
135
  },
116
136
  omp: {
117
137
  label: "omp (Oh My Pi)",
@@ -126,7 +146,7 @@ export const RECIPES = {
126
146
  referer: "https://omp.sh",
127
147
  },
128
148
  env: (key) => ({ ANTHROPIC_BASE_URL: ROOT, ANTHROPIC_API_KEY: key }),
129
- args: (m) => (m ? ["--model", `anthropic/${m}`] : []),
149
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
130
150
  },
131
151
  hermes: {
132
152
  label: "Hermes Agent",
@@ -150,7 +170,7 @@ export const RECIPES = {
150
170
  title: "Qwen Code",
151
171
  referer: "https://github.com/QwenLM/qwen-code",
152
172
  },
153
- env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-5.4-mini" }),
173
+ env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || DEFAULT_MODEL }),
154
174
  args: () => [],
155
175
  },
156
176
  copilot: {
@@ -167,7 +187,7 @@ export const RECIPES = {
167
187
  COPILOT_PROVIDER_BASE_URL: V1,
168
188
  COPILOT_PROVIDER_TYPE: "openai",
169
189
  COPILOT_PROVIDER_API_KEY: key,
170
- COPILOT_MODEL: model || "gpt-5.4-mini",
190
+ COPILOT_MODEL: model || DEFAULT_MODEL,
171
191
  }),
172
192
  args: () => [],
173
193
  },
@@ -189,7 +209,7 @@ export const RECIPES = {
189
209
  GEMINI_API_KEY: key,
190
210
  GOOGLE_API_KEY: key,
191
211
  }),
192
- args: (m) => (m ? ["--model", `google/${m}`] : []),
212
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
193
213
  },
194
214
  agy: {
195
215
  label: "Antigravity CLI",
@@ -206,7 +226,7 @@ export const RECIPES = {
206
226
  GEMINI_API_KEY: key,
207
227
  GOOGLE_API_KEY: key,
208
228
  }),
209
- args: (m) => (m ? ["--model", `google/${m}`] : []),
229
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
210
230
  },
211
231
  pi: {
212
232
  label: "Pi",
@@ -219,7 +239,7 @@ export const RECIPES = {
219
239
  referer: "https://pi.dev",
220
240
  },
221
241
  env: (key) => ({ PLEUM_API_KEY: key }),
222
- args: (m) => ["--model", m || "gpt-5.4-mini"],
242
+ args: (m) => ["--model", m || DEFAULT_MODEL],
223
243
  },
224
244
  crush: {
225
245
  label: "Crush",
@@ -246,9 +266,9 @@ export const RECIPES = {
246
266
  env: (key, model) => ({
247
267
  OPENAI_BASE_URL: V1,
248
268
  OPENAI_API_KEY: key,
249
- ...(model ? { OPENAI_MODEL: model } : {}),
269
+ OPENAI_MODEL: model || DEFAULT_MODEL,
250
270
  }),
251
- args: (m) => (m ? ["--model", m] : []),
271
+ args: (m) => ["--model", m || DEFAULT_MODEL],
252
272
  },
253
273
  mimo: {
254
274
  label: "MiMo Code",
@@ -262,9 +282,9 @@ export const RECIPES = {
262
282
  env: (key, model) => ({
263
283
  OPENAI_BASE_URL: V1,
264
284
  OPENAI_API_KEY: key,
265
- ...(model ? { OPENAI_MODEL: model } : {}),
285
+ OPENAI_MODEL: model || DEFAULT_MODEL,
266
286
  }),
267
- args: (m) => (m ? ["--model", m] : []),
287
+ args: (m) => ["--model", m || DEFAULT_MODEL],
268
288
  },
269
289
  openclaw: {
270
290
  label: "OpenClaw",
@@ -278,9 +298,9 @@ export const RECIPES = {
278
298
  env: (key, model) => ({
279
299
  OPENAI_BASE_URL: V1,
280
300
  OPENAI_API_KEY: key,
281
- ...(model ? { OPENAI_MODEL: model } : {}),
301
+ OPENAI_MODEL: model || DEFAULT_MODEL,
282
302
  }),
283
- args: (m) => (m ? ["--model", m] : []),
303
+ args: (m) => ["--model", m || DEFAULT_MODEL],
284
304
  },
285
305
  plandex: {
286
306
  label: "Plandex",
@@ -310,7 +330,7 @@ export const RECIPES = {
310
330
  OPENAI_API_BASE: V1,
311
331
  OPENAI_API_KEY: key,
312
332
  }),
313
- args: (m) => (m ? ["--model", `openai/${m}`] : []),
333
+ args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
314
334
  },
315
335
  };
316
336
 
@@ -357,7 +377,7 @@ export function tomlBasicString(value) {
357
377
  }
358
378
 
359
379
  export function codexToml(model) {
360
- const m = model || "gpt-5.4-mini";
380
+ const m = model || DEFAULT_MODEL;
361
381
  return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
362
382
  model = ${tomlBasicString(m)}
363
383
  model_provider = "pleum"
@@ -371,7 +391,7 @@ supports_websockets = false
371
391
  `;
372
392
  }
373
393
 
374
- export function mergeAgySettings(existingJson) {
394
+ export function mergeAgySettings(existingJson, model) {
375
395
  let obj = {};
376
396
  if (existingJson && String(existingJson).trim()) {
377
397
  obj = JSON.parse(existingJson);
@@ -379,11 +399,11 @@ export function mergeAgySettings(existingJson) {
379
399
  throw new Error("agy settings.json must be a JSON object");
380
400
  }
381
401
  }
382
- return { ...obj, modelProvider: "gemini" };
402
+ return { ...obj, modelProvider: "gemini", model: model || DEFAULT_MODEL };
383
403
  }
384
404
 
385
405
  export function piModelsJson(model) {
386
- const m = model || "gpt-5.4-mini";
406
+ const m = model || DEFAULT_MODEL;
387
407
  return `${JSON.stringify({
388
408
  providers: {
389
409
  pleum: {
@@ -397,7 +417,7 @@ export function piModelsJson(model) {
397
417
  }
398
418
 
399
419
  export function crushJson(model) {
400
- const m = model || "gpt-5.4-mini";
420
+ const m = model || DEFAULT_MODEL;
401
421
  return `${JSON.stringify({
402
422
  providers: {
403
423
  pleum: {