ethos-router 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/index.js +95 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ ethos codex # Codex 를 에토스 라우터로 실행
18
18
  | `ethos login` | 브라우저에서 승인하고 키를 받습니다 |
19
19
  | `ethos logout` | 키를 정지하고 설정을 지웁니다 |
20
20
  | `ethos whoami` | 어느 워크스페이스로 붙어 있는지 봅니다 |
21
+ | `ethos use [코드]` | 어느 모델로 붙을지 정합니다 |
21
22
  | `ethos claude [인자…]` | Claude Code 를 에토스 라우터로 실행합니다 |
22
23
  | `ethos codex [인자…]` | Codex 를 에토스 라우터로 실행합니다 |
23
24
  | `ethos env` | 다른 도구에 넣을 환경변수를 찍습니다 |
@@ -27,6 +28,20 @@ ethos codex # Codex 를 에토스 라우터로 실행
27
28
  | `ethos usage` | 기간별 사용 내역을 봅니다 |
28
29
  | `ethos doctor` | 어디가 막혔는지 점검합니다 |
29
30
 
31
+ ## 모델
32
+
33
+ `ethos claude` 는 붙을 모델을 정해서 넘깁니다. **정해 주지 않으면 도구가 제
34
+ 기본 이름을 보내는데, 저희 카탈로그 코드에는 공급사 접두사가 붙어 있어서
35
+ 그 이름으로는 못 찾습니다.**
36
+
37
+ ```bash
38
+ ethos use # 지금 무엇으로 붙는지
39
+ ethos models claude # 쓸 수 있는 코드
40
+ ethos use anthropic/claude-opus-5 # 바꾸기
41
+ ```
42
+
43
+ 고른 값은 설정에 남습니다. `ETHOS_MODEL` 로 한 번만 덮어쓸 수도 있습니다.
44
+
30
45
  ## 키
31
46
 
32
47
  `ethos login` 은 키를 **두 개** 받아 `~/.ethos/config.json` 에 0600 으로
package/index.js CHANGED
@@ -16,7 +16,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
16
16
  import { homedir, hostname } from "node:os";
17
17
  import { dirname, join } from "node:path";
18
18
 
19
- const VERSION = "0.1.0";
19
+ const VERSION = "0.1.2";
20
20
  const DEFAULT_BASE = "https://router.ethos.ai.kr";
21
21
  const CONFIG_PATH = join(homedir(), ".ethos", "config.json");
22
22
 
@@ -211,15 +211,29 @@ const TOOLS = {
211
211
  bin: "claude",
212
212
  install: "npm i -g @anthropic-ai/claude-code",
213
213
  // 앤트로픽 형식 창구. 도구가 뒤에 /v1/messages 를 붙인다.
214
- env: (key, base) => ({
214
+ //
215
+ // **ANTHROPIC_API_KEY 는 넣지 않는다.** 둘 다 있으면 API_KEY 가 이기는데,
216
+ // 그 값이 있으면 Claude Code 가 "이 키를 쓸까요" 승인 절차를 따로 탄다.
217
+ // 게이트웨이 연결의 조합은 BASE_URL + AUTH_TOKEN 둘뿐이다.
218
+ env: (key, base, model) => ({
215
219
  ANTHROPIC_BASE_URL: `${base}/claude`,
216
220
  ANTHROPIC_AUTH_TOKEN: key,
217
- ANTHROPIC_API_KEY: key,
221
+ // **모델을 정해 주지 않으면 도구가 제 기본 이름을 보낸다.** 우리 카탈로그
222
+ // 코드에는 공급사 접두사가 붙어 있어서 그 이름으로는 못 찾고, 도구는
223
+ // 전체화면 화면이라 오류가 화면과 함께 지워진다 — "아무 일도 안 일어남" 이
224
+ // 된다. 실제로 그랬다(2026-09-09).
225
+ ANTHROPIC_MODEL: model.main,
226
+ // 요약·제목 같은 잔일에 쓰는 작은 모델. 이걸 안 정해 주면 주 모델은
227
+ // 붙는데 잔일에서만 조용히 실패한다 — 원인을 찾기 훨씬 어렵다.
228
+ ANTHROPIC_SMALL_FAST_MODEL: model.small,
218
229
  }),
219
230
  },
220
231
  codex: {
221
232
  bin: "codex",
222
233
  install: "npm i -g @openai/codex",
234
+ // 모델은 정해 주지 않는다. 이 도구가 어떤 변수로 모델을 읽는지 확인한 적이
235
+ // 없고, 확인 안 한 값을 넣으면 붙는 것처럼 보이다가 호출에서만 틀린다.
236
+ // 모델은 도구의 제 방식(--model 등)으로 고르고, 코드는 ethos models 로 본다.
223
237
  env: (key, base) => ({
224
238
  OPENAI_BASE_URL: `${base}/v1`,
225
239
  OPENAI_API_KEY: key,
@@ -227,16 +241,41 @@ const TOOLS = {
227
241
  },
228
242
  };
229
243
 
230
- function toolEnv(name, key, base) {
231
- return TOOLS[name].env(key, base);
244
+ /**
245
+ * 어느 모델로 붙일지.
246
+ *
247
+ * 고른 값은 설정에 남는다 — 매번 정하게 하면 대부분 기본값으로 돌고, 기본값이
248
+ * 무엇인지 모른 채 쓴다. `ethos use <코드>` 로 바꾸고 `ethos whoami` 에서 확인한다.
249
+ * 코드는 `ethos models` 가 보여주는 그대로다(공급사 접두사 포함).
250
+ */
251
+ const DEFAULT_MODEL = { main: "anthropic/claude-sonnet-5", small: "anthropic/claude-haiku-4.5" };
252
+
253
+ function modelFor(cfg) {
254
+ return {
255
+ main: process.env.ETHOS_MODEL || cfg.model || DEFAULT_MODEL.main,
256
+ small: process.env.ETHOS_SMALL_MODEL || cfg.smallModel || DEFAULT_MODEL.small,
257
+ };
258
+ }
259
+
260
+ function toolEnv(name, key, base, model) {
261
+ return TOOLS[name].env(key, base, model);
232
262
  }
233
263
 
234
264
  async function cmdRunTool(name, rest) {
235
265
  const cfg = loadConfig();
236
266
  const key = needKey(cfg);
237
267
  const t = TOOLS[name];
238
- const env = { ...process.env, ...toolEnv(name, key, baseUrl(cfg)) };
268
+ const base = baseUrl(cfg);
269
+ const model = modelFor(cfg);
270
+ const env = { ...process.env, ...toolEnv(name, key, base, model) };
271
+
272
+ // 무엇을 어디로 띄우는지 한 줄 남긴다. 이 도구들은 전체화면 화면을 쓰기
273
+ // 때문에, 뜨자마자 죽으면 **화면이 복원되면서 오류 메시지까지 같이
274
+ // 사라진다.** 그러면 사용자에게는 아무 일도 안 일어난 것처럼 보인다.
275
+ // 이 줄이 있으면 적어도 "우리는 띄웠고 저쪽이 죽었다"는 것은 남는다.
276
+ err(dim(` ${t.bin} · ${base} · ${model.main}`));
239
277
 
278
+ const started = Date.now();
240
279
  const child = spawn(t.bin, rest, { stdio: "inherit", env });
241
280
  child.on("error", (e) => {
242
281
  if (e.code === "ENOENT") {
@@ -245,6 +284,15 @@ async function cmdRunTool(name, rest) {
245
284
  die(e.message);
246
285
  });
247
286
  child.on("exit", (code, signal) => {
287
+ // 곧바로 죽었으면 오류를 봤을 리가 없다 — 보는 방법을 알려준다.
288
+ // 인자를 준 실행(--version 등)은 빨리 끝나는 게 정상이므로 빼고,
289
+ // 대화형으로 띄웠는데 곧바로 끝났거나 실패로 끝난 경우만 말한다.
290
+ const tooFast = Date.now() - started < 3000;
291
+ if (!signal && tooFast && (rest.length === 0 || code !== 0)) {
292
+ err(dim(` ${t.bin} 이 바로 종료됐습니다(코드 ${code ?? 0}).`));
293
+ err(dim(` 화면이 지워져 메시지가 안 보일 수 있습니다. 아래로 직접 확인하세요:`));
294
+ err(dim(` eval "$(ethos env)" && ${t.bin}`));
295
+ }
248
296
  // 신호로 죽은 것을 0 으로 보고하면 스크립트가 성공으로 읽는다
249
297
  process.exit(signal ? 1 : (code ?? 0));
250
298
  });
@@ -258,7 +306,8 @@ function cmdEnv(args) {
258
306
  const cfg = loadConfig();
259
307
  const key = needKey(cfg);
260
308
  const base = baseUrl(cfg);
261
- const vars = { ...toolEnv("claude", key, base), ...toolEnv("codex", key, base) };
309
+ const model = modelFor(cfg);
310
+ const vars = { ...toolEnv("claude", key, base, model), ...toolEnv("codex", key, base, model) };
262
311
  const shell = args.shell || (process.env.SHELL?.includes("fish") ? "fish" : "posix");
263
312
  for (const [k, v] of Object.entries(vars)) {
264
313
  out(shell === "fish" ? `set -gx ${k} ${v}`
@@ -431,6 +480,42 @@ function which(bin) {
431
480
  });
432
481
  }
433
482
 
483
+ // ── use ──────────────────────────────────────────────────
484
+ //
485
+ // 어느 모델로 붙을지 정한다. 코드가 카탈로그에 있는지 **정하는 자리에서**
486
+ // 확인한다 — 안 하면 오타가 설정에 그대로 저장되고, 그 다음 실행에서
487
+ // 도구가 뜨자마자 죽는다(화면이 걷히면서 오류까지 사라진다).
488
+ async function cmdUse(args) {
489
+ const cfg = loadConfig();
490
+ const code = args._[0];
491
+ if (!code) {
492
+ const m = modelFor(cfg);
493
+ out("");
494
+ out(` 지금 모델 ${bold(m.main)}`);
495
+ out(` 잔일 모델 ${m.small}`);
496
+ out("");
497
+ out(dim(' 바꾸려면 ethos use <코드> (코드는 ethos models 로 봅니다)'));
498
+ out(dim(' 잔일까지 ethos use <코드> --small <코드>'));
499
+ out("");
500
+ return;
501
+ }
502
+
503
+ const { json: d } = await call("/v1/prices");
504
+ const ids = new Set(d.models.map((m) => m.id));
505
+ for (const c of [code, args.small].filter(Boolean)) {
506
+ if (!ids.has(c)) {
507
+ // 비슷한 것을 같이 보여준다. "없습니다" 만 남기면 다음 행동이 없다.
508
+ const near = [...ids].filter((x) => x.includes(c.split("/").pop().slice(0, 6))).slice(0, 5);
509
+ die(`카탈로그에 없는 모델입니다: ${c}`,
510
+ near.length ? `비슷한 코드: ${near.join(", ")}` : "ethos models 로 코드를 확인하세요.");
511
+ }
512
+ }
513
+ cfg.model = code;
514
+ if (args.small) cfg.smallModel = args.small;
515
+ saveConfig(cfg);
516
+ out(green("✓ ") + `${code} 로 붙습니다.` + (args.small ? ` (잔일 ${args.small})` : ""));
517
+ }
518
+
434
519
  // ── whoami ────────────────────────────────────────────────
435
520
  function cmdWhoami() {
436
521
  const cfg = loadConfig();
@@ -438,6 +523,7 @@ function cmdWhoami() {
438
523
  out("");
439
524
  out(` 워크스페이스 ${bold(cfg.workspace?.name ?? dim("알 수 없음"))}`);
440
525
  out(` 창구 주소 ${baseUrl(cfg)}`);
526
+ out(` 모델 ${modelFor(cfg).main}`);
441
527
  // 키 전체를 찍지 않는다. 스크린샷과 화면 공유로 새어 나가는 자리다.
442
528
  out(` 키 ${apiKey(cfg).slice(0, 12)}…`);
443
529
  out("");
@@ -451,6 +537,7 @@ function help() {
451
537
  ${bold("ethos login")} 브라우저에서 승인하고 키를 받습니다
452
538
  ${bold("ethos logout")} 키를 정지하고 설정을 지웁니다
453
539
  ${bold("ethos whoami")} 어느 워크스페이스로 붙어 있는지 봅니다
540
+ ${bold("ethos use")} ${dim("[코드]")} 어느 모델로 붙을지 정합니다 ${dim("(빈 값이면 지금 값)")}
454
541
 
455
542
  ${bold("ethos claude")} ${dim("[인자…]")} Claude Code 를 에토스 라우터로 실행합니다
456
543
  ${bold("ethos codex")} ${dim("[인자…]")} Codex 를 에토스 라우터로 실행합니다
@@ -501,6 +588,7 @@ async function main() {
501
588
  case "login": return await cmdLogin(args);
502
589
  case "logout": return await cmdLogout();
503
590
  case "whoami": return cmdWhoami();
591
+ case "use": return await cmdUse(args);
504
592
  case "env": return cmdEnv(args);
505
593
  case "models": return await cmdModels(args);
506
594
  case "chat": return await cmdChat(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethos-router",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "에토스 라우터 명령줄 도구. 한 번 로그인하면 코딩 도구가 에토스 라우터를 보게 됩니다.",
5
5
  "type": "module",
6
6
  "bin": {