bizrouter 0.2.0 → 0.2.1

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/AGENT_MANUAL.md CHANGED
@@ -34,6 +34,11 @@ If a management command fails with "콘솔 세션이 없습니다", ask the user
34
34
  `bizrouter login` (it opens a browser; the user approves an 8-character code).
35
35
  You cannot complete that approval yourself — it is deliberately a human step.
36
36
 
37
+ The session grants exactly what the approving user can do in the web console,
38
+ nothing more. The console's separate **Usage API key** (설정 → API 연동 → 사용량
39
+ 조회 API) is a third, read-only credential for the usage export endpoints; this
40
+ CLI neither uses nor manages it, and it is not an admin key.
41
+
37
42
  ## 3. Command cheat sheet
38
43
 
39
44
  ```
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # bizrouter CLI
2
2
 
3
- 쓰던 코딩 에이전트를 그대로 두고 명령 앞에 `bizrouter` 붙이면 BizRouter 로 연결됩니다. 로그인한 뒤에는 BizRouter 콘솔(API 키·정책·감사 로그·통계)도 명령으로, 또는 에이전트의 MCP 도구로 다룰 수 있습니다.
3
+ 쓰던 코딩 에이전트를 그대로 두고 명령 앞에 `bizrouter`만 붙이면 BizRouter로 연결됩니다. 로그인한 뒤에는 BizRouter 콘솔(API 키·정책·감사 로그·통계)도 명령으로, 또는 에이전트의 MCP 도구로 다룰 수 있습니다.
4
4
 
5
5
  ```bash
6
6
  npm install -g bizrouter # 또는: curl -fsSL https://bizrouter.ai/install.sh | bash
@@ -15,10 +15,10 @@ bizrouter hermes # Hermes
15
15
 
16
16
  ## 로그인이 해 주는 것
17
17
 
18
- `bizrouter login` 터미널에 8자리 코드를 보여주고 브라우저에서 BizRouter 콘솔을 엽니다. 콘솔에 로그인(비밀번호·Google·웍스AI·2차 인증 그대로)한 뒤 코드를 확인하고 「승인」하면
18
+ `bizrouter login`은 터미널에 8자리 코드를 보여주고 브라우저에서 BizRouter 콘솔을 엽니다. 콘솔에 로그인(비밀번호·Google·웍스AI·2차 인증 그대로)한 뒤 코드를 확인하고 「승인」하면
19
19
 
20
20
  - 이 기기용 **콘솔 세션**(30일)과
21
- - 이 기기 이름의 **API 키**(모델 호출용)가 함께 발급되어 `~/.bizrouter/credentials.json` 권한 600 으로 저장됩니다.
21
+ - 이 기기 이름의 **API 키**(모델 호출용)가 함께 발급되어 `~/.bizrouter/credentials.json`에 권한 600으로 저장됩니다.
22
22
 
23
23
  키만 붙여 넣던 방식도 그대로 됩니다: `bizrouter login --with-key`. CI·컨테이너에서는 `BIZROUTER_API_KEY`(모델 호출)·`BIZROUTER_SESSION_TOKEN`(콘솔) 환경 변수가 저장된 값보다 우선합니다. 세션은 콘솔 「계정 → 로그인된 기기」에서 언제든 종료할 수 있습니다.
24
24
 
@@ -26,12 +26,12 @@ bizrouter hermes # Hermes
26
26
 
27
27
  | 도구 | 실행 시 하는 일 | 설정 파일 변경 |
28
28
  | --- | --- | --- |
29
- | Claude Code | `ANTHROPIC_BASE_URL=https://api.bizrouter.ai/claude`, 토큰, 빈 `ANTHROPIC_API_KEY`, 모델 선택창 자동 채움을 넘기고, 사용자 settings.json 의 다른 게이트웨이 설정이 이기지 못하도록 명령행 settings 로 고정 | 없음 |
29
+ | Claude Code | `ANTHROPIC_BASE_URL=https://api.bizrouter.ai/claude`, 토큰, 빈 `ANTHROPIC_API_KEY`, 모델 선택창 자동 채움을 넘기고, 사용자 settings.json의 다른 게이트웨이 설정이 이기지 못하도록 명령행 settings로 고정 | 없음 |
30
30
  | Codex CLI | `-c` 오버라이드로 BizRouter provider(Responses API)와 모델·추론 강도 지정 | 없음 |
31
- | OpenCode | `OPENCODE_CONFIG_CONTENT` 이 키가 쓸 수 있는 모델 전체가 담긴 `bizrouter` provider 를 주입 | 없음 |
31
+ | OpenCode | `OPENCODE_CONFIG_CONTENT`로 이 키가 쓸 수 있는 모델 전체가 담긴 `bizrouter` provider를 주입 | 없음 |
32
32
  | Hermes | `custom` provider(`CUSTOM_BASE_URL`/`CUSTOM_API_KEY`)와 모델을 이번 실행에만 지정 | 없음 |
33
33
 
34
- 우리 옵션 뒤의 인자는 도구에 그대로 전달됩니다.
34
+ 도구는 평소처럼 실행됩니다. 바뀌는 것은 그 실행에서 모델을 호출하는 주소와 인증뿐이어서, 화면과 사용법은 같고 과금·모델 정책·감사 로그는 BizRouter 콘솔에 남습니다. `bizrouter` 없이 실행하면 평소대로 동작합니다. 우리 옵션 뒤의 인자는 도구에 그대로 전달됩니다.
35
35
 
36
36
  ```bash
37
37
  bizrouter claude --model anthropic/claude-opus-5 -p "실패하는 테스트 고쳐줘"
@@ -40,11 +40,11 @@ bizrouter opencode --model google/gemini-3.5-pro
40
40
  bizrouter claude --dry-run # 실행 대신 넘겨줄 환경 변수와 명령을 보여줍니다
41
41
  ```
42
42
 
43
- 콘솔 세션이 있으면 Claude Code·Codex·OpenCode 에 이 CLI 가 **MCP 서버 `bizrouter`** 로 함께 연결됩니다. 에이전트는 `bizrouter_api`(콘솔 API 호출)·`bizrouter_openapi`(API 탐색)·`bizrouter_docs`(문서)·`bizrouter_models`·`bizrouter_whoami` 도구로 API 키를 만들고, 정책을 바꾸고, 사용량을 읽습니다. 데이터를 바꾸는 호출은 사용자 확인 뒤 `confirm: true` 받아야만 실행됩니다. `--no-mcp` 끕니다.
43
+ 콘솔 세션이 있으면 Claude Code·Codex·OpenCode에 이 CLI가 **MCP 서버 `bizrouter`** 로 함께 연결됩니다. 에이전트는 `bizrouter_api`(콘솔 API 호출)·`bizrouter_openapi`(API 탐색)·`bizrouter_docs`(문서)·`bizrouter_models`·`bizrouter_whoami` 도구로 API 키를 만들고, 정책을 바꾸고, 사용량을 읽습니다. 데이터를 바꾸는 호출은 사용자 확인 뒤 `confirm: true`를 받아야만 실행됩니다. `--no-mcp`로 끕니다.
44
44
 
45
45
  ## 콘솔 관리 명령
46
46
 
47
- 모두 `--json` 지원하고, 데이터를 바꾸는 명령은 터미널에서 확인을 묻거나 `--yes` 요구합니다.
47
+ 모두 `--json`을 지원하고, 데이터를 바꾸는 명령은 터미널에서 확인을 묻거나 `--yes`를 요구합니다.
48
48
 
49
49
  ```bash
50
50
  bizrouter api --list [검색어] # 콘솔 API 전체 목록 (라이브 OpenAPI 기준)
@@ -63,21 +63,24 @@ bizrouter docs [주제] # 에이전트 매뉴얼(AG
63
63
  bizrouter mcp # MCP 서버(stdio)로 직접 실행
64
64
  ```
65
65
 
66
- 에이전트에게 맡길 때는 `bizrouter docs` 출력하는 매뉴얼을 먼저 읽히면 됩니다. 사람용 개발 문서(https://bizrouter.ai/docs)는 같은 소스에서 만든 https://bizrouter.ai/llms-full.txt 로도 제공됩니다.
66
+ 에이전트에게 맡길 때는 `bizrouter docs`가 출력하는 매뉴얼을 먼저 읽히면 됩니다. 사람용 개발 문서(https://bizrouter.ai/docs)는 같은 소스에서 만든 https://bizrouter.ai/llms-full.txt로도 제공됩니다.
67
67
 
68
68
  ## 그 밖의 명령
69
69
 
70
70
  ```bash
71
71
  bizrouter models [검색어] # 이 키로 쓸 수 있는 모델·컨텍스트·원화 단가
72
72
  bizrouter doctor # 키·세션·연결·설치·충돌 점검
73
- bizrouter setup claude|codex|opencode # 도구 설정 파일에 BizRouter 를 영구 등록 (백업 생성)
73
+ bizrouter setup claude|codex|opencode # 도구 설정 파일에 BizRouter를 영구 등록 (백업 생성)
74
74
  bizrouter auth / logout / env / update
75
75
  ```
76
76
 
77
77
  ## 알아둘 것
78
78
 
79
- - Claude Code 는 Anthropic 전용 필드(`thinking`, `cache_control`)를 항상 보내므로 Claude 모델만 실행할 수 있습니다. GPT·Gemini 는 Codex·OpenCode·Hermes 로 실행하십시오.
79
+ - Claude Code는 Anthropic 전용 필드(`thinking`, `cache_control`)를 항상 보내므로 Claude 모델만 실행할 수 있습니다. GPT·Gemini는 Codex·OpenCode·Hermes로 실행하십시오.
80
80
  - 콘솔 세션은 로그인한 사용자의 권한(owner/member)을 그대로 따릅니다. 구성원(member)은 자기 API 키와 사용량만 다룰 수 있습니다.
81
+ - API 키(`sk-br-v1-…`)의 권한은 예전과 같습니다. 모델 호출만 되고, 콘솔 관리는 브라우저에서 승인한 사람의 콘솔 세션으로만 됩니다. 범위는 그 사람이 웹 콘솔에서 할 수 있는 일과 같습니다.
82
+ - 콘솔의 「사용량 조회 API」 키는 별개의 읽기 전용 키입니다. CLI는 쓰지도 바꾸지도 않습니다.
83
+ - `credentials.json`의 세션 토큰은 30일짜리 콘솔 로그인입니다. 공유하거나 저장소에 넣지 말고, 의심스러우면 콘솔 「계정 → 로그인된 기기」에서 종료하십시오.
81
84
  - 요구 사항: Node.js 18 이상. 각 코딩 에이전트는 따로 설치돼 있어야 합니다.
82
85
 
83
86
  문서: https://bizrouter.ai/docs/cli
package/dist/api.js CHANGED
@@ -31,17 +31,17 @@ async function requestJson(path, apiKey) {
31
31
  catch (error) {
32
32
  const reason = error instanceof Error && error.name === 'AbortError' ? '응답이 8초 안에 오지 않았습니다' : String(error);
33
33
  throw new CliError(`BizRouter API(${apiBase()})에 연결할 수 없습니다: ${reason}`, {
34
- hint: '네트워크나 프록시 설정을 확인하세요. 사내망이면 api.bizrouter.ai 로 나가는 HTTPS 가 열려 있어야 합니다.',
34
+ hint: '네트워크나 프록시 설정을 확인하세요. 사내망이면 api.bizrouter.ai로 나가는 HTTPS가 열려 있어야 합니다.',
35
35
  });
36
36
  }
37
37
  finally {
38
38
  clearTimeout(timer);
39
39
  }
40
40
  if (response.status === 401 || response.status === 403) {
41
- throw new ApiError(response.status, 'API 키가 유효하지 않습니다.', '`bizrouter login` 으로 키를 다시 저장하세요.');
41
+ throw new ApiError(response.status, 'API 키가 유효하지 않습니다.', '`bizrouter login`으로 키를 다시 저장하세요.');
42
42
  }
43
43
  if (!response.ok) {
44
- throw new ApiError(response.status, `BizRouter API 가 ${response.status} 를 돌려주었습니다.`);
44
+ throw new ApiError(response.status, `BizRouter API가 ${response.status}를 돌려주었습니다.`);
45
45
  }
46
46
  return response.json();
47
47
  }
@@ -13,7 +13,7 @@ function parseData(raw) {
13
13
  return JSON.parse(text);
14
14
  }
15
15
  catch (error) {
16
- throw new CliError(`-d 값이 JSON 이 아닙니다: ${error instanceof Error ? error.message : String(error)}`, {
16
+ throw new CliError(`-d 값이 JSON이 아닙니다: ${error instanceof Error ? error.message : String(error)}`, {
17
17
  hint: `예: -d '{"name":"ci-key"}' 또는 -d @body.json (표준 입력은 -d @-)`,
18
18
  });
19
19
  }
@@ -78,7 +78,7 @@ export function parseApiArgs(argv) {
78
78
  }
79
79
  function printOperations(rows, keyword) {
80
80
  if (!rows.length) {
81
- print(keyword ? `「${keyword}」에 맞는 콘솔 API 가 없습니다.` : '콘솔 API 목록이 비어 있습니다.');
81
+ print(keyword ? `「${keyword}」에 맞는 콘솔 API가 없습니다.` : '콘솔 API 목록이 비어 있습니다.');
82
82
  return;
83
83
  }
84
84
  const pathWidth = Math.min(56, Math.max(...rows.map((r) => r.path.length), 10));
@@ -106,7 +106,7 @@ export async function apiCommand(argv) {
106
106
  const method = flags.method && argv.some((a) => a.toUpperCase() === flags.method) ? flags.method : undefined;
107
107
  const matches = findOperation(spec, method, flags.schema);
108
108
  if (!matches.length)
109
- throw new CliError(`콘솔 API 에 없는 경로입니다: ${normalizeConsolePath(flags.schema)}`, { hint: '`bizrouter api --list <검색어>` 찾아보십시오.' });
109
+ throw new CliError(`콘솔 API에 없는 경로입니다: ${normalizeConsolePath(flags.schema)}`, { hint: '`bizrouter api --list <검색어>`로 찾아보십시오.' });
110
110
  print(JSON.stringify(matches.length === 1 ? matches[0] : matches, null, 2));
111
111
  return 0;
112
112
  }
@@ -119,11 +119,11 @@ export async function apiCommand(argv) {
119
119
  const path = normalizeConsolePath(flags.path);
120
120
  if (MUTATING_METHODS.has(flags.method) && !flags.yes) {
121
121
  if (!process.stdin.isTTY) {
122
- throw new CliError(`${flags.method} ${path} 는 데이터를 바꾸는 요청입니다. 확인 없이 실행하려면 --yes 를 붙이십시오.`, {
123
- hint: '에이전트가 실행하는 경우, 사람에게 먼저 확인을 받은 뒤 --yes 를 붙여 다시 실행하십시오.',
122
+ throw new CliError(`${flags.method} ${path}는 데이터를 바꾸는 요청입니다. 확인 없이 실행하려면 --yes를 붙이십시오.`, {
123
+ hint: '에이전트가 실행하는 경우, 사람에게 먼저 확인을 받은 뒤 --yes를 붙여 다시 실행하십시오.',
124
124
  });
125
125
  }
126
- warn(`${flags.method} ${path} 는 데이터를 바꾸는 요청입니다.`);
126
+ warn(`${flags.method} ${path}는 데이터를 바꾸는 요청입니다.`);
127
127
  if (flags.data !== undefined)
128
128
  print(c.dim(JSON.stringify(flags.data, null, 2)));
129
129
  const answer = await readLine('실행할까요? (y/N) ');
@@ -77,8 +77,8 @@ async function confirmMutation(parsed, description, payload) {
77
77
  if (parsed.yes)
78
78
  return;
79
79
  if (!process.stdin.isTTY) {
80
- throw new CliError(`${description} — 데이터를 바꾸는 작업입니다. 확인 없이 실행하려면 --yes 를 붙이십시오.`, {
81
- hint: '에이전트가 실행하는 경우, 사람에게 먼저 확인을 받은 뒤 --yes 를 붙여 다시 실행하십시오.',
80
+ throw new CliError(`${description} — 데이터를 바꾸는 작업입니다. 확인 없이 실행하려면 --yes를 붙이십시오.`, {
81
+ hint: '에이전트가 실행하는 경우, 사람에게 먼저 확인을 받은 뒤 --yes를 붙여 다시 실행하십시오.',
82
82
  });
83
83
  }
84
84
  warn(description);
@@ -132,11 +132,11 @@ async function resolveKeyId(ref) {
132
132
  if (byName.length === 1)
133
133
  return byName[0];
134
134
  if (byName.length > 1)
135
- throw new CliError(`이름 「${ref}」 인 키가 ${byName.length}개입니다. id 로 지정하십시오.`, { hint: 'bizrouter keys list --all' });
135
+ throw new CliError(`이름 「${ref}」 인 키가 ${byName.length}개입니다. id로 지정하십시오.`, { hint: 'bizrouter keys list --all' });
136
136
  const byPreview = keys.filter((k) => k.key_preview && ref.endsWith(k.key_preview.slice(-4)) && ref.startsWith(k.key_preview.slice(0, 13)));
137
137
  if (byPreview.length === 1)
138
138
  return byPreview[0];
139
- throw new CliError(`API 키를 찾을 수 없습니다: ${ref}`, { hint: 'bizrouter keys list --all 로 id 나 이름을 확인하십시오.' });
139
+ throw new CliError(`API 키를 찾을 수 없습니다: ${ref}`, { hint: 'bizrouter keys list --all로 id 나 이름을 확인하십시오.' });
140
140
  }
141
141
  const KEYS_USAGE = [
142
142
  'bizrouter keys list [--all] [--search 검색어] [--json]',
@@ -191,7 +191,7 @@ export async function keysCommand(argv) {
191
191
  if (alertEmails)
192
192
  body.limit_alert_recipient_emails = alertEmails;
193
193
  if (body.credit_limit !== null && Number.isNaN(body.credit_limit))
194
- throw new CliError('--limit 은 원 단위 숫자여야 합니다.');
194
+ throw new CliError('--limit은 원 단위 숫자여야 합니다.');
195
195
  await confirmMutation(parsed, `API 키 「${name}」 을 만듭니다.`, body);
196
196
  const created = await send('POST', '/api-keys', body);
197
197
  return emit(parsed, created, () => {
@@ -238,7 +238,7 @@ export async function keysCommand(argv) {
238
238
  body.limit_alert_recipient_emails = alertEmails;
239
239
  if (!Object.keys(body).length)
240
240
  throw new CliError('바꿀 항목이 없습니다.', { hint: KEYS_USAGE });
241
- await confirmMutation(parsed, `API 키 「${key.name}」 (${key.key_preview}) 을 수정합니다.`, body);
241
+ await confirmMutation(parsed, `API 키 「${key.name}」 (${key.key_preview})을 수정합니다.`, body);
242
242
  const updated = await send('PATCH', `/api-keys/${key.id}`, body);
243
243
  return emit(parsed, updated, () => ok(`API 키 「${updated.name}」 을 수정했습니다.`));
244
244
  }
@@ -248,7 +248,7 @@ export async function keysCommand(argv) {
248
248
  if (!ref)
249
249
  throw new CliError('키 id 또는 이름이 필요합니다.', { hint: KEYS_USAGE });
250
250
  const key = await resolveKeyId(ref);
251
- await confirmMutation(parsed, `API 키 「${key.name}」 (${key.key_preview}) 을 삭제합니다. 이 키를 쓰는 서비스는 즉시 인증에 실패합니다.`);
251
+ await confirmMutation(parsed, `API 키 「${key.name}」 (${key.key_preview})을 삭제합니다. 이 키를 쓰는 서비스는 즉시 인증에 실패합니다.`);
252
252
  await send('DELETE', `/api-keys/${key.id}`);
253
253
  ok(`API 키 「${key.name}」 을 삭제했습니다.`);
254
254
  return 0;
@@ -425,7 +425,7 @@ export async function auditCommand(argv) {
425
425
  case 'show': {
426
426
  const id = rest[0];
427
427
  if (!id)
428
- throw new CliError('log_id 가 필요합니다.', { hint: AUDIT_USAGE });
428
+ throw new CliError('log_id가 필요합니다.', { hint: AUDIT_USAGE });
429
429
  const detail = await get(`/audit-logs/${id}`);
430
430
  print(JSON.stringify(detail, null, 2));
431
431
  return 0;
@@ -466,7 +466,7 @@ export async function routingCommand(argv) {
466
466
  const body = parseAssignments(rest);
467
467
  if (!Object.keys(body).length)
468
468
  throw new CliError('바꿀 항목이 없습니다.', { hint: ROUTING_USAGE });
469
- await confirmMutation(parsed, '기본 스마트 라우팅 정책을 변경합니다. bizrouter/route 로 오는 요청의 모델 선택이 바뀝니다.', body);
469
+ await confirmMutation(parsed, '기본 스마트 라우팅 정책을 변경합니다. bizrouter/route로 오는 요청의 모델 선택이 바뀝니다.', body);
470
470
  const updated = await send('PATCH', '/smart-routing/policy', body);
471
471
  return emit(parsed, updated, () => {
472
472
  ok('정책을 변경했습니다.');
@@ -523,7 +523,7 @@ export async function routingCommand(argv) {
523
523
  if (top.length)
524
524
  print(` 상위 모델: ${top.slice(0, 5).map((t) => `${t.model} ${t.count}`).join(', ')}`);
525
525
  print();
526
- print(c.dim(`개별 결정 ${data.items.length}건은 --json 으로 보십시오.`));
526
+ print(c.dim(`개별 결정 ${data.items.length}건은 --json으로 보십시오.`));
527
527
  });
528
528
  }
529
529
  default:
@@ -12,7 +12,7 @@ export function agentManualPath() {
12
12
  export function readAgentManual() {
13
13
  const path = agentManualPath();
14
14
  if (!existsSync(path))
15
- throw new CliError('에이전트 매뉴얼 파일(AGENT_MANUAL.md)이 패키지에 없습니다.', { hint: '`bizrouter update` 다시 설치하십시오.' });
15
+ throw new CliError('에이전트 매뉴얼 파일(AGENT_MANUAL.md)이 패키지에 없습니다.', { hint: '`bizrouter update`로 다시 설치하십시오.' });
16
16
  return readFileSync(path, 'utf8');
17
17
  }
18
18
  /** Split llms-full.txt (## title / URL: … / 분류: …) into pages. */
@@ -58,16 +58,16 @@ async function fetchText(url) {
58
58
  try {
59
59
  const response = await fetch(url, { headers: { Accept: 'text/plain', 'User-Agent': 'bizrouter-cli' }, signal: controller.signal });
60
60
  if (!response.ok)
61
- throw new CliError(`${url} 가 ${response.status} 를 돌려주었습니다.`);
61
+ throw new CliError(`${url}가 ${response.status}를 돌려주었습니다.`);
62
62
  const text = await response.text();
63
63
  if (/^\s*<!doctype html/i.test(text) || /^\s*<html/i.test(text))
64
- throw new CliError(`${url} 가 문서 대신 HTML 을 돌려주었습니다.`);
64
+ throw new CliError(`${url}가 문서 대신 HTML을 돌려주었습니다.`);
65
65
  return text;
66
66
  }
67
67
  catch (error) {
68
68
  if (error instanceof CliError)
69
69
  throw error;
70
- throw new CliError(`${url} 를 내려받을 수 없습니다: ${error instanceof Error ? error.message : String(error)}`);
70
+ throw new CliError(`${url}를 내려받을 수 없습니다: ${error instanceof Error ? error.message : String(error)}`);
71
71
  }
72
72
  finally {
73
73
  clearTimeout(timer);
@@ -124,7 +124,7 @@ export async function docsCommand(argv) {
124
124
  const { text, fromCache } = await loadLlmsFull({ force: flags.refresh });
125
125
  const pages = parseLlmsFull(text);
126
126
  if (fromCache)
127
- info(`캐시된 문서를 사용합니다 (최대 하루 · --refresh 로 다시 받기)`);
127
+ info(`캐시된 문서를 사용합니다 (최대 하루 · --refresh로 다시 받기)`);
128
128
  if (flags.full) {
129
129
  print(text.trimEnd());
130
130
  return 0;
@@ -144,7 +144,7 @@ export async function docsCommand(argv) {
144
144
  }
145
145
  const matches = findDocsPages(pages, flags.topic);
146
146
  if (!matches.length) {
147
- print(`「${flags.topic}」에 해당하는 문서가 없습니다. bizrouter docs --list 로 목록을 확인하십시오.`);
147
+ print(`「${flags.topic}」에 해당하는 문서가 없습니다. bizrouter docs --list로 목록을 확인하십시오.`);
148
148
  return 1;
149
149
  }
150
150
  for (const page of matches) {
@@ -28,15 +28,15 @@ export function conflictFindings(env, files) {
28
28
  if (env.ANTHROPIC_API_KEY) {
29
29
  findings.push({
30
30
  level: 'warn',
31
- text: `ANTHROPIC_API_KEY 가 셸에 설정돼 있습니다 (${maskKey(env.ANTHROPIC_API_KEY)}).`,
32
- hint: '`bizrouter claude` 실행 시 빈 값으로 덮어쓰므로 동작에는 문제가 없지만, `claude` 직접 실행하면 Anthropic 으로 바로 갑니다.',
31
+ text: `ANTHROPIC_API_KEY가 셸에 설정돼 있습니다 (${maskKey(env.ANTHROPIC_API_KEY)}).`,
32
+ hint: '`bizrouter claude`는 실행 시 빈 값으로 덮어쓰므로 동작에는 문제가 없지만, `claude`를 직접 실행하면 Anthropic으로 바로 갑니다.',
33
33
  });
34
34
  }
35
35
  if (env.ANTHROPIC_BASE_URL && !env.ANTHROPIC_BASE_URL.startsWith(base)) {
36
- findings.push({ level: 'warn', text: `ANTHROPIC_BASE_URL 이 다른 주소를 가리킵니다: ${env.ANTHROPIC_BASE_URL}`, hint: '`bizrouter claude` 이 값을 덮어씁니다.' });
36
+ findings.push({ level: 'warn', text: `ANTHROPIC_BASE_URL이 다른 주소를 가리킵니다: ${env.ANTHROPIC_BASE_URL}`, hint: '`bizrouter claude`는 이 값을 덮어씁니다.' });
37
37
  }
38
38
  if (env.OPENAI_BASE_URL) {
39
- findings.push({ level: 'warn', text: `OPENAI_BASE_URL 이 설정돼 있습니다: ${env.OPENAI_BASE_URL}`, hint: 'Codex 는 provider 설정이 우선이라 영향이 없지만, 다른 도구가 이 값을 읽을 수 있습니다.' });
39
+ findings.push({ level: 'warn', text: `OPENAI_BASE_URL이 설정돼 있습니다: ${env.OPENAI_BASE_URL}`, hint: 'Codex는 provider 설정이 우선이라 영향이 없지만, 다른 도구가 이 값을 읽을 수 있습니다.' });
40
40
  }
41
41
  if (files.claudeSettings) {
42
42
  try {
@@ -46,22 +46,22 @@ export function conflictFindings(env, files) {
46
46
  if (foreignBase) {
47
47
  findings.push({
48
48
  level: 'warn',
49
- text: `Claude Code settings.json 의 env 가 다른 게이트웨이를 가리킵니다: ${parsed.env?.ANTHROPIC_BASE_URL}`,
50
- hint: '`bizrouter claude` 명령행 settings 로 덮어쓰지만, `claude` 단독 실행은 그 주소로 갑니다. `bizrouter setup claude` 갈아탈 수 있습니다.',
49
+ text: `Claude Code settings.json의 env가 다른 게이트웨이를 가리킵니다: ${parsed.env?.ANTHROPIC_BASE_URL}`,
50
+ hint: '`bizrouter claude`는 명령행 settings로 덮어쓰지만, `claude` 단독 실행은 그 주소로 갑니다. `bizrouter setup claude`로 갈아탈 수 있습니다.',
51
51
  });
52
52
  }
53
53
  else if (keys.length) {
54
- findings.push({ level: 'ok', text: `Claude Code settings.json 에 env ${keys.length}개가 있고 BizRouter 와 충돌하지 않습니다.` });
54
+ findings.push({ level: 'ok', text: `Claude Code settings.json에 env ${keys.length}개가 있고 BizRouter와 충돌하지 않습니다.` });
55
55
  }
56
56
  }
57
57
  catch {
58
- findings.push({ level: 'warn', text: 'Claude Code settings.json 을 JSON 으로 읽을 수 없습니다.', hint: 'Claude Code 자체도 이 파일을 못 읽습니다. 문법을 확인하세요.' });
58
+ findings.push({ level: 'warn', text: 'Claude Code settings.json을 JSON으로 읽을 수 없습니다.', hint: 'Claude Code 자체도 이 파일을 못 읽습니다. 문법을 확인하세요.' });
59
59
  }
60
60
  }
61
61
  if (files.codexConfig) {
62
62
  const provider = /^\s*model_provider\s*=\s*"([^"]+)"/m.exec(files.codexConfig)?.[1];
63
63
  if (provider && provider !== 'bizrouter') {
64
- findings.push({ level: 'ok', text: `Codex config.toml 의 기본 provider 는 "${provider}" 입니다.`, hint: '`bizrouter codex` -c 로 덮어쓰므로 그대로 두어도 됩니다. `codex` 단독으로도 BizRouter 를 쓰려면 `bizrouter setup codex`.' });
64
+ findings.push({ level: 'ok', text: `Codex config.toml의 기본 provider는 "${provider}" 입니다.`, hint: '`bizrouter codex`는 -c로 덮어쓰므로 그대로 두어도 됩니다. `codex` 단독으로도 BizRouter를 쓰려면 `bizrouter setup codex`.' });
65
65
  }
66
66
  }
67
67
  return findings;
@@ -74,7 +74,7 @@ export async function doctorCommand() {
74
74
  : { level: 'fail', text: `Node.js ${process.versions.node} — 18 이상이 필요합니다.` });
75
75
  const session = resolveSession();
76
76
  if (session.source === 'none') {
77
- findings.push({ level: 'warn', text: '콘솔 세션이 없습니다 — 콘솔 관리 명령(api·keys·usage …)과 MCP 연결은 비활성입니다.', hint: '`bizrouter login` 으로 브라우저 승인을 받으십시오.' });
77
+ findings.push({ level: 'warn', text: '콘솔 세션이 없습니다 — 콘솔 관리 명령(api·keys·usage …)과 MCP 연결은 비활성입니다.', hint: '`bizrouter login`으로 브라우저 승인을 받으십시오.' });
78
78
  }
79
79
  else {
80
80
  try {
@@ -82,7 +82,7 @@ export async function doctorCommand() {
82
82
  findings.push({ level: 'ok', text: `콘솔 세션 유효 · ${profile.user.email} · ${profile.organization.name} (${profile.user.role})${session.source === 'env' ? ` · ${ENV_SESSION_NAME}` : ''}` });
83
83
  }
84
84
  catch (error) {
85
- findings.push({ level: 'warn', text: `콘솔 세션 무효: ${error instanceof Error ? error.message : String(error)}`, hint: '`bizrouter login` 으로 다시 로그인하십시오.' });
85
+ findings.push({ level: 'warn', text: `콘솔 세션 무효: ${error instanceof Error ? error.message : String(error)}`, hint: '`bizrouter login`으로 다시 로그인하십시오.' });
86
86
  }
87
87
  }
88
88
  const cred = resolveCredential();
@@ -104,7 +104,7 @@ export async function doctorCommand() {
104
104
  const claudeModels = await probe(`${base}/claude/v1/models?limit=1`, cred.apiKey ?? '');
105
105
  findings.push(claudeModels.startsWith('200')
106
106
  ? { level: 'ok', text: `GET ${base}/claude/v1/models → ${claudeModels} (Claude Code /model 선택창 자동 채움)` }
107
- : { level: 'warn', text: `GET ${base}/claude/v1/models → ${claudeModels}`, hint: 'Claude Code 의 /model 선택창이 내장 목록으로만 채워집니다.' });
107
+ : { level: 'warn', text: `GET ${base}/claude/v1/models → ${claudeModels}`, hint: 'Claude Code의 /model 선택창이 내장 목록으로만 채워집니다.' });
108
108
  }
109
109
  for (const h of HARNESSES) {
110
110
  const version = binaryVersion(h);
@@ -51,7 +51,7 @@ export async function waitForApproval(deviceCode, options) {
51
51
  }
52
52
  async function browserLogin(flags) {
53
53
  const started = await startDeviceLogin();
54
- print(c.bold('BizRouter 에 로그인합니다.'));
54
+ print(c.bold('BizRouter에 로그인합니다.'));
55
55
  print();
56
56
  print(`1. 브라우저에서 이 주소를 엽니다: ${c.cyan(started.verification_url_complete)}`);
57
57
  print(`2. 화면의 코드가 아래와 같은지 확인하고 「승인」을 누릅니다.`);
@@ -69,9 +69,9 @@ async function browserLogin(flags) {
69
69
  deadline: Date.now() + started.expires_in * 1000,
70
70
  });
71
71
  if (result.status === 'denied')
72
- throw new CliError('브라우저에서 로그인 요청을 거부했습니다.', { hint: '직접 실행한 로그인이었다면 `bizrouter login` 다시 실행하십시오.' });
72
+ throw new CliError('브라우저에서 로그인 요청을 거부했습니다.', { hint: '직접 실행한 로그인이었다면 `bizrouter login`을 다시 실행하십시오.' });
73
73
  if (result.status !== 'approved' || !result.session_token) {
74
- throw new CliError('로그인 코드가 만료되었습니다.', { hint: '`bizrouter login` 다시 실행하고 10분 안에 승인하십시오.' });
74
+ throw new CliError('로그인 코드가 만료되었습니다.', { hint: '`bizrouter login`을 다시 실행하고 10분 안에 승인하십시오.' });
75
75
  }
76
76
  const account = accountFromLogin(result);
77
77
  const path = saveCredentials({
@@ -99,7 +99,7 @@ async function browserLogin(flags) {
99
99
  if (existing.apiKey)
100
100
  info(`저장돼 있던 API 키(${maskKey(existing.apiKey)})는 그대로 사용합니다.`);
101
101
  else
102
- info(`코딩 에이전트 실행에는 API 키가 필요합니다. \`bizrouter keys create <이름>\` 또는 ${consoleUrl()}${KEYS_PAGE_PATH} 에서 만든 뒤 \`bizrouter login --with-key\` 저장하십시오.`);
102
+ info(`코딩 에이전트 실행에는 API 키가 필요합니다. \`bizrouter keys create <이름>\` 또는 ${consoleUrl()}${KEYS_PAGE_PATH}에서 만든 뒤 \`bizrouter login --with-key\`로 저장하십시오.`);
103
103
  }
104
104
  print();
105
105
  print('이제 바로 실행할 수 있습니다:');
@@ -124,7 +124,7 @@ async function keyLogin(flags) {
124
124
  throw new CliError('키가 입력되지 않았습니다.');
125
125
  if (!looksLikeApiKey(entered)) {
126
126
  throw new CliError('BizRouter API 키 형식이 아닙니다. 키는 sk-br- 로 시작합니다.', {
127
- hint: `${keysUrl} 에서 발급한 키를 그대로 붙여 넣으십시오.`,
127
+ hint: `${keysUrl}에서 발급한 키를 그대로 붙여 넣으십시오.`,
128
128
  });
129
129
  }
130
130
  info('키를 확인하는 중…');
@@ -134,7 +134,7 @@ async function keyLogin(flags) {
134
134
  ok(`키를 저장했습니다: ${path} (권한 600)`);
135
135
  ok(`이 키로 쓸 수 있는 모델 ${chatModels(catalog.models).length}개를 확인했습니다.`);
136
136
  if (!resolveSession().token)
137
- info('콘솔 관리 명령(`bizrouter api`, `keys`, `usage` …)까지 쓰려면 `bizrouter login` 으로 브라우저 승인도 받으십시오.');
137
+ info('콘솔 관리 명령(`bizrouter api`, `keys`, `usage` …)까지 쓰려면 `bizrouter login`으로 브라우저 승인도 받으십시오.');
138
138
  print();
139
139
  print(`이제 바로 실행할 수 있습니다:`);
140
140
  print(` ${c.bold('bizrouter claude')} Claude Code`);
@@ -167,7 +167,7 @@ export async function authCommand() {
167
167
  const session = resolveSession();
168
168
  let failed = false;
169
169
  if (session.source === 'none') {
170
- info('콘솔 세션: 없음 — `bizrouter login` 으로 브라우저 승인을 받으면 콘솔 관리 명령을 쓸 수 있습니다.');
170
+ info('콘솔 세션: 없음 — `bizrouter login`으로 브라우저 승인을 받으면 콘솔 관리 명령을 쓸 수 있습니다.');
171
171
  }
172
172
  else {
173
173
  const where = session.source === 'env' ? `환경 변수 ${ENV_SESSION_NAME}` : `저장된 파일 ${credentialsPath()}`;
@@ -178,7 +178,7 @@ export async function authCommand() {
178
178
  catch (error) {
179
179
  failed = true;
180
180
  fail(`콘솔 세션 무효: ${error instanceof Error ? error.message : String(error)}`);
181
- info('`bizrouter login` 으로 다시 로그인하십시오.');
181
+ info('`bizrouter login`으로 다시 로그인하십시오.');
182
182
  }
183
183
  }
184
184
  if (cred.source === 'none') {
@@ -205,7 +205,7 @@ export async function logoutCommand() {
205
205
  if (revoked)
206
206
  ok('콘솔 세션을 서버에서 종료했습니다.');
207
207
  else
208
- warn(`콘솔 세션을 서버에서 종료하지 못했습니다. ${consoleUrl()}${ACCOUNT_PAGE_PATH} 의 「로그인된 기기」에서 직접 종료할 수 있습니다.`);
208
+ warn(`콘솔 세션을 서버에서 종료하지 못했습니다. ${consoleUrl()}${ACCOUNT_PAGE_PATH}의 「로그인된 기기」에서 직접 종료할 수 있습니다.`);
209
209
  }
210
210
  const removed = clearCredential();
211
211
  if (removed)
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { ENV_KEY_NAME } from '../config.js';
3
3
  import { c, info, print } from '../ui.js';
4
4
  import { requireApiKey } from './shared.js';
5
- export const VERSION = '0.2.0';
5
+ export const VERSION = '0.2.1';
6
6
  /** `eval "$(bizrouter env)"` exports the saved key for tools configured by `bizrouter setup`. */
7
7
  export function envCommand() {
8
8
  const key = requireApiKey();
@@ -23,20 +23,21 @@ export function updateCommand() {
23
23
  });
24
24
  }
25
25
  export function helpText() {
26
- return `${c.bold('bizrouter')} — 코딩 에이전트를 BizRouter 로 연결하고, BizRouter 콘솔을 명령으로 다룹니다. (v${VERSION})
26
+ return `${c.bold('bizrouter')} — 코딩 에이전트를 BizRouter로 연결하고, BizRouter 콘솔을 명령으로 다룹니다. (v${VERSION})
27
27
 
28
28
  ${c.bold('시작하기')}
29
29
  bizrouter login 브라우저에서 승인하면 콘솔 세션과 API 키가 함께 저장됩니다 (한 번만)
30
- bizrouter claude Claude Code 를 BizRouter 로 실행합니다
31
- bizrouter codex Codex CLI 를 BizRouter 로 실행합니다
32
- bizrouter opencode OpenCode 를 BizRouter 로 실행합니다
33
- bizrouter hermes Hermes 를 BizRouter 로 실행합니다
30
+ bizrouter claude Claude Code를 BizRouter로 실행합니다
31
+ bizrouter codex Codex CLI를 BizRouter로 실행합니다
32
+ bizrouter opencode OpenCode를 BizRouter로 실행합니다
33
+ bizrouter hermes Hermes를 BizRouter로 실행합니다
34
+ 도구는 평소처럼 실행되고, 이번 실행의 모델 호출·과금만 BizRouter를 거칩니다. 설정 파일은 바뀌지 않습니다.
34
35
 
35
36
  ${c.bold('실행 옵션')} (실행 명령 뒤, 도구 자체 옵션 앞에 씁니다)
36
37
  --model <ID> BizRouter 모델 ID (예: anthropic/claude-sonnet-5, openai/gpt-5.5)
37
38
  --reasoning-effort <단계> minimal · low · medium · high · xhigh (Codex·OpenCode)
38
- --closed-network Claude Code 의 부수 트래픽(버전 확인·텔레메트리)을 끕니다
39
- --no-mcp 콘솔 MCP 서버(bizrouter) 를 에이전트에 연결하지 않습니다
39
+ --closed-network Claude Code의 부수 트래픽(버전 확인·텔레메트리)을 끕니다
40
+ --no-mcp 콘솔 MCP 서버(bizrouter)를 에이전트에 연결하지 않습니다
40
41
  --dry-run 실행하지 않고 넘겨줄 환경 변수와 명령만 보여줍니다
41
42
  나머지 옵션은 그대로 도구에 전달됩니다. 예: bizrouter claude -p "테스트 고쳐줘"
42
43
 
@@ -55,7 +56,7 @@ ${c.bold('그 밖에')}
55
56
  bizrouter models [검색어] 이 키로 쓸 수 있는 모델과 원화 단가
56
57
  bizrouter docs [주제] 에이전트 매뉴얼 · 개발 문서 (--list, --full)
57
58
  bizrouter mcp MCP 서버(stdio)로 실행 — 에이전트가 콘솔을 도구로 다룹니다
58
- bizrouter setup <도구> claude · codex · opencode 설정 파일에 BizRouter 를 영구 등록
59
+ bizrouter setup <도구> claude · codex · opencode 설정 파일에 BizRouter를 영구 등록
59
60
  bizrouter doctor 연결·키·세션·설치·충돌 점검
60
61
  bizrouter auth 지금 어떤 계정·키가 쓰이는지 확인
61
62
  bizrouter env eval "$(bizrouter env)" 로 ${ENV_KEY_NAME} 내보내기
@@ -92,7 +92,7 @@ function readJsonObject(path) {
92
92
  return parsed;
93
93
  }
94
94
  catch {
95
- throw new CliError(`${path} 를 JSON 으로 읽을 수 없어 수정하지 않았습니다.`, {
95
+ throw new CliError(`${path}를 JSON으로 읽을 수 없어 수정하지 않았습니다.`, {
96
96
  hint: '주석이나 문법 오류가 있으면 먼저 정리한 뒤 다시 실행하세요.',
97
97
  });
98
98
  }
@@ -105,7 +105,7 @@ export async function setupCommand(argv) {
105
105
  const known = ['claude', 'codex', 'opencode'];
106
106
  if (!target || !known.includes(target)) {
107
107
  throw new CliError('설정을 남길 도구를 지정하세요: bizrouter setup <claude|codex|opencode> [--model ID] [--dry-run]', {
108
- hint: 'Hermes 는 `bizrouter hermes` 실행할 때마다 연결되므로 별도 설정이 없습니다.',
108
+ hint: 'Hermes는 `bizrouter hermes`로 실행할 때마다 연결되므로 별도 설정이 없습니다.',
109
109
  });
110
110
  }
111
111
  const apiKey = requireApiKey();
@@ -120,9 +120,9 @@ export async function setupCommand(argv) {
120
120
  }
121
121
  const bak = backup(path);
122
122
  writeText(path, text);
123
- ok(`${path} 의 env 블록에 BizRouter 설정을 넣었습니다.${bak ? ` (백업: ${bak})` : ''}`);
124
- warn('이 파일에는 API 키가 그대로 저장됩니다. 저장소에 커밋되는 프로젝트 .claude/settings.json 이 아니라 사용자 설정이라 안전하지만, 기기를 공유한다면 `bizrouter claude` 실행 방식을 권장합니다.');
125
- info('이제 `claude` 실행해도 BizRouter 로 연결됩니다. 되돌리려면 백업 파일을 복원하세요.');
123
+ ok(`${path}의 env 블록에 BizRouter 설정을 넣었습니다.${bak ? ` (백업: ${bak})` : ''}`);
124
+ warn('이 파일에는 API 키가 그대로 저장됩니다. 저장소에 커밋되는 프로젝트 .claude/settings.json이 아니라 사용자 설정이라 안전하지만, 기기를 공유한다면 `bizrouter claude` 실행 방식을 권장합니다.');
125
+ info('이제 `claude`만 실행해도 BizRouter로 연결됩니다. 되돌리려면 백업 파일을 복원하세요.');
126
126
  return 0;
127
127
  }
128
128
  const catalog = await fetchCatalog(apiKey, { allowStale: true }).catch(() => undefined);
@@ -138,7 +138,7 @@ export async function setupCommand(argv) {
138
138
  }
139
139
  const bak = backup(path);
140
140
  writeText(path, text);
141
- ok(`${path} 에 BizRouter provider 를 넣고 기본 모델을 ${model} 로 맞췼습니다.${bak ? ` (백업: ${bak})` : ''}`);
141
+ ok(`${path}에 BizRouter provider를 넣고 기본 모델을 ${model}로 맞췼습니다.${bak ? ` (백업: ${bak})` : ''}`);
142
142
  printEnvHint();
143
143
  return 0;
144
144
  }
@@ -159,9 +159,9 @@ export async function setupCommand(argv) {
159
159
  }
160
160
  const bak = backup(path);
161
161
  writeText(path, text);
162
- ok(`${path} 에 BizRouter provider(모델 ${Object.keys(next.provider[OPENCODE_PROVIDER_ID]?.models ?? {}).length}개)를 넣고 기본 모델을 ${model} 로 맞췼습니다.${bak ? ` (백업: ${bak})` : ''}`);
162
+ ok(`${path}에 BizRouter provider(모델 ${Object.keys(next.provider[OPENCODE_PROVIDER_ID]?.models ?? {}).length}개)를 넣고 기본 모델을 ${model}로 맞췼습니다.${bak ? ` (백업: ${bak})` : ''}`);
163
163
  if (models.length === 0)
164
- warn('모델 목록을 가져오지 못해 provider 의 모델 표가 비어 있습니다. 네트워크가 되면 다시 실행하세요.');
164
+ warn('모델 목록을 가져오지 못해 provider의 모델 표가 비어 있습니다. 네트워크가 되면 다시 실행하세요.');
165
165
  printEnvHint();
166
166
  return 0;
167
167
  }
@@ -169,5 +169,5 @@ function printEnvHint() {
169
169
  print();
170
170
  print(`이 설정은 셸의 ${c.bold(ENV_KEY_NAME)} 환경 변수에서 키를 읽습니다. 셸 프로필(~/.zshrc 등)에 한 줄을 추가하세요:`);
171
171
  print(` ${c.cyan(`eval "$(bizrouter env)"`)}`);
172
- print(c.dim('또는 매번 `bizrouter codex` / `bizrouter opencode` 실행하면 이 단계가 필요 없습니다.'));
172
+ print(c.dim('또는 매번 `bizrouter codex` / `bizrouter opencode`로 실행하면 이 단계가 필요 없습니다.'));
173
173
  }
@@ -4,7 +4,7 @@ export function requireApiKey() {
4
4
  const cred = resolveCredential();
5
5
  if (!cred.apiKey) {
6
6
  throw new CliError('BizRouter API 키가 없습니다.', {
7
- hint: `\`bizrouter login\` 으로 키를 저장하거나 ${ENV_KEY_NAME} 환경 변수를 설정하세요.`,
7
+ hint: `\`bizrouter login\`으로 키를 저장하거나 ${ENV_KEY_NAME} 환경 변수를 설정하세요.`,
8
8
  });
9
9
  }
10
10
  return cred.apiKey;
package/dist/console.js CHANGED
@@ -39,7 +39,7 @@ export function requireSession() {
39
39
  const session = resolveSession();
40
40
  if (!session.token) {
41
41
  throw new CliError('BizRouter 콘솔 세션이 없습니다.', {
42
- hint: `\`bizrouter login\` 으로 브라우저에서 승인하면 콘솔 관리 명령을 쓸 수 있습니다. (CI 에서는 ${ENV_SESSION_NAME} 환경 변수)`,
42
+ hint: `\`bizrouter login\`으로 브라우저에서 승인하면 콘솔 관리 명령을 쓸 수 있습니다. (CI에서는 ${ENV_SESSION_NAME} 환경 변수)`,
43
43
  });
44
44
  }
45
45
  return session.token;
@@ -92,7 +92,7 @@ export async function consoleRequest(method, path, options = {}) {
92
92
  catch (error) {
93
93
  const reason = error instanceof Error && error.name === 'AbortError' ? `응답이 ${REQUEST_TIMEOUT_MS / 1000}초 안에 오지 않았습니다` : String(error);
94
94
  throw new CliError(`BizRouter 콘솔(${consoleUrl()})에 연결할 수 없습니다: ${reason}`, {
95
- hint: '네트워크나 프록시 설정을 확인하십시오. BIZROUTER_CONSOLE_URL 로 콘솔 주소를 바꿀 수 있습니다.',
95
+ hint: '네트워크나 프록시 설정을 확인하십시오. BIZROUTER_CONSOLE_URL로 콘솔 주소를 바꿀 수 있습니다.',
96
96
  });
97
97
  }
98
98
  finally {
@@ -112,18 +112,18 @@ export async function consoleRequest(method, path, options = {}) {
112
112
  return { status: response.status, data: data };
113
113
  const message = serverMessage(data);
114
114
  if (response.status === 401) {
115
- throw new ConsoleApiError(401, message ?? '콘솔 세션이 만료되었거나 유효하지 않습니다.', data, '`bizrouter login` 으로 다시 로그인하십시오.');
115
+ throw new ConsoleApiError(401, message ?? '콘솔 세션이 만료되었거나 유효하지 않습니다.', data, '`bizrouter login`으로 다시 로그인하십시오.');
116
116
  }
117
117
  if (response.status === 403) {
118
118
  throw new ConsoleApiError(403, message ?? '이 작업을 할 권한이 없습니다.', data, '조직의 소유자(owner) 권한이 필요하거나, 콘솔 접근 IP 제한에 걸렸을 수 있습니다.');
119
119
  }
120
120
  if (response.status === 404)
121
- throw new ConsoleApiError(404, message ?? `콘솔 API 경로를 찾을 수 없습니다: ${method} ${normalizeConsolePath(path)}`, data, '`bizrouter api --list <검색어>` 경로를 확인하십시오.');
121
+ throw new ConsoleApiError(404, message ?? `콘솔 API 경로를 찾을 수 없습니다: ${method} ${normalizeConsolePath(path)}`, data, '`bizrouter api --list <검색어>`로 경로를 확인하십시오.');
122
122
  if (response.status === 422)
123
- throw new ConsoleApiError(422, `요청 본문이 올바르지 않습니다: ${message ?? text}`, data, '`bizrouter api --schema <경로>` 필드를 확인하십시오.');
123
+ throw new ConsoleApiError(422, `요청 본문이 올바르지 않습니다: ${message ?? text}`, data, '`bizrouter api --schema <경로>`로 필드를 확인하십시오.');
124
124
  if (response.status === 429)
125
125
  throw new ConsoleApiError(429, message ?? '요청이 너무 잦습니다. 잠시 후 다시 시도하십시오.', data);
126
- throw new ConsoleApiError(response.status, message ?? `콘솔 API 가 ${response.status} 를 돌려주었습니다.`, data);
126
+ throw new ConsoleApiError(response.status, message ?? `콘솔 API가 ${response.status}를 돌려주었습니다.`, data);
127
127
  }
128
128
  export function deviceHostname() {
129
129
  return (process.env.BIZROUTER_DEVICE_NAME || hostname() || 'unknown-host').replace(/\.local$/i, '').slice(0, 64);
@@ -23,8 +23,8 @@ export function claudeEnv(options) {
23
23
  }
24
24
  export function assertClaudeModel(model) {
25
25
  if (!isAnthropicModelId(model)) {
26
- throw new CliError(`Claude Code 는 Claude 모델만 실행할 수 있습니다: ${model}`, {
27
- hint: 'Claude Code 는 thinking·cache_control 같은 Anthropic 전용 필드를 항상 보내서 다른 모델은 400 으로 거절됩니다. GPT·Gemini 는 `bizrouter codex` 또는 `bizrouter opencode` 실행하세요.',
26
+ throw new CliError(`Claude Code는 Claude 모델만 실행할 수 있습니다: ${model}`, {
27
+ hint: 'Claude Code는 thinking·cache_control 같은 Anthropic 전용 필드를 항상 보내서 다른 모델은 400으로 거절됩니다. GPT·Gemini는 `bizrouter codex` 또는 `bizrouter opencode`로 실행하세요.',
28
28
  });
29
29
  }
30
30
  }
@@ -40,7 +40,7 @@ export function buildCodexPlan(options) {
40
40
  env: { [ENV_KEY_NAME]: options.apiKey },
41
41
  notes: [
42
42
  `Codex → ${options.apiBase}/v1 (Responses API) · 모델 ${options.model}` + (options.reasoningEffort ? ` · 추론 강도 ${options.reasoningEffort}` : ''),
43
- ...(options.resetServiceTier ? ['config.toml 의 service_tier 는 BizRouter 에 요금 계약이 없어 이번 실행에서는 기본 등급으로 되돌립니다.'] : []),
43
+ ...(options.resetServiceTier ? ['config.toml의 service_tier는 BizRouter에 요금 계약이 없어 이번 실행에서는 기본 등급으로 되돌립니다.'] : []),
44
44
  ...(options.mcp ? ['콘솔 MCP 서버 「bizrouter」 를 이 세션에 연결합니다 (끄기: --no-mcp).'] : []),
45
45
  ],
46
46
  };
@@ -8,7 +8,7 @@ export const HERMES_FIXED_MAX_TOKENS = 65536;
8
8
  export function hermesOutputCapWarning(model, maxOutputTokens) {
9
9
  if (maxOutputTokens === undefined || maxOutputTokens >= HERMES_FIXED_MAX_TOKENS)
10
10
  return undefined;
11
- return `Hermes 는 max_tokens ${HERMES_FIXED_MAX_TOKENS.toLocaleString('en-US')} 을 고정으로 보내는데 ${model} 의 출력 한도는 ${maxOutputTokens.toLocaleString('en-US')} 입니다. 요청이 400 으로 거절되고 Hermes 가 이를 컨텍스트 초과로 오해합니다. 출력 한도가 65,536 이상인 모델(예: anthropic/claude-sonnet-5)을 쓰세요.`;
11
+ return `Hermes는 max_tokens ${HERMES_FIXED_MAX_TOKENS.toLocaleString('en-US')}을 고정으로 보내는데 ${model}의 출력 한도는 ${maxOutputTokens.toLocaleString('en-US')} 입니다. 요청이 400으로 거절되고 Hermes가 이를 컨텍스트 초과로 오해합니다. 출력 한도가 65,536 이상인 모델(예: anthropic/claude-sonnet-5)을 쓰세요.`;
12
12
  }
13
13
  const HERMES_SUBCOMMANDS = new Set(['chat']);
14
14
  /**
package/dist/index.js CHANGED
@@ -73,13 +73,13 @@ async function launch(harness, argv) {
73
73
  // With a console session the agent gets the console as MCP tools; without one there is nothing to expose.
74
74
  const mcp = !args.noMcp && harness !== 'hermes' && resolveSession().source !== 'none' ? bizrouterMcpServer() : undefined;
75
75
  if (!args.noMcp && harness !== 'hermes' && !mcp && !args.dryRun)
76
- info('콘솔 세션이 없어 MCP 서버(콘솔 조작 도구)는 연결하지 않습니다. `bizrouter login` 으로 브라우저 승인을 받으면 연결됩니다.');
76
+ info('콘솔 세션이 없어 MCP 서버(콘솔 조작 도구)는 연결하지 않습니다. `bizrouter login`으로 브라우저 승인을 받으면 연결됩니다.');
77
77
  let plan;
78
78
  switch (harness) {
79
79
  case 'claude':
80
80
  plan = buildClaudePlan({ apiKey, apiBase: base, model, closedNetwork: args.closedNetwork, passthrough: args.passthrough, mcp });
81
81
  if (args.reasoningEffort)
82
- warn('Claude Code 는 추론 강도를 실행 옵션으로 받지 않습니다. 세션 안에서 /effort 를 사용하세요.');
82
+ warn('Claude Code는 추론 강도를 실행 옵션으로 받지 않습니다. 세션 안에서 /effort를 사용하세요.');
83
83
  break;
84
84
  case 'codex':
85
85
  plan = buildCodexPlan({
@@ -92,19 +92,19 @@ async function launch(harness, argv) {
92
92
  mcp,
93
93
  });
94
94
  if (args.closedNetwork)
95
- warn('--closed-network 는 Claude Code 전용 옵션입니다.');
95
+ warn('--closed-network는 Claude Code 전용 옵션입니다.');
96
96
  break;
97
97
  case 'opencode':
98
98
  plan = buildOpencodePlan({ apiKey, apiBase: base, model: model ?? FALLBACK_DEFAULT.opencode, models, reasoningEffort: args.reasoningEffort, passthrough: args.passthrough, mcp });
99
99
  if (args.closedNetwork)
100
- warn('--closed-network 는 Claude Code 전용 옵션입니다.');
100
+ warn('--closed-network는 Claude Code 전용 옵션입니다.');
101
101
  break;
102
102
  case 'hermes':
103
103
  plan = buildHermesPlan({ apiKey, apiBase: base, model: model ?? FALLBACK_DEFAULT.hermes, passthrough: args.passthrough });
104
104
  if (args.reasoningEffort)
105
- warn('Hermes 는 추론 강도 옵션을 받지 않습니다.');
105
+ warn('Hermes는 추론 강도 옵션을 받지 않습니다.');
106
106
  if (!args.noMcp && resolveSession().source !== 'none')
107
- info('Hermes 에는 MCP 서버를 실행 옵션으로 넘길 수 없습니다. `bizrouter docs agent` 안내대로 `bizrouter api` 명령을 쓰십시오.');
107
+ info('Hermes에는 MCP 서버를 실행 옵션으로 넘길 수 없습니다. `bizrouter docs agent`의 안내대로 `bizrouter api` 명령을 쓰십시오.');
108
108
  {
109
109
  const hermesModel = model ?? FALLBACK_DEFAULT.hermes;
110
110
  const capWarning = hermesOutputCapWarning(hermesModel, models.find((m) => m.id === hermesModel)?.max_output_tokens);
@@ -176,7 +176,7 @@ async function main(argv) {
176
176
  case 'hermes':
177
177
  return launch(command, rest);
178
178
  default:
179
- throw new CliError(`알 수 없는 명령입니다: ${command}`, { hint: '`bizrouter help` 전체 명령을 볼 수 있습니다.' });
179
+ throw new CliError(`알 수 없는 명령입니다: ${command}`, { hint: '`bizrouter help`로 전체 명령을 볼 수 있습니다.' });
180
180
  }
181
181
  }
182
182
  // `bizrouter docs | head` closes our stdout early; that is not an error worth a stack trace.
package/dist/spawn.js CHANGED
@@ -55,7 +55,7 @@ export async function runPlan(plan, options) {
55
55
  const binPath = whichBinary(plan.bin);
56
56
  if (!binPath) {
57
57
  plan.cleanup?.();
58
- throw new CliError(`\`${plan.bin}\` 실행 파일을 PATH 에서 찾을 수 없습니다.`, {
58
+ throw new CliError(`\`${plan.bin}\` 실행 파일을 PATH에서 찾을 수 없습니다.`, {
59
59
  hint: INSTALL_HINTS[plan.bin] ? `먼저 설치하세요: ${INSTALL_HINTS[plan.bin]}` : undefined,
60
60
  });
61
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizrouter",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "BizRouter CLI - run Claude Code, Codex, OpenCode, and Hermes through BizRouter, and operate the BizRouter console (API keys, usage, policies, audit logs) from the terminal or as an MCP server",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",