bizrouter 0.1.0 → 0.2.0

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.
@@ -0,0 +1,195 @@
1
+ # BizRouter CLI — manual for coding agents
2
+
3
+ `bizrouter docs` prints this file. It is written for an AI coding agent (Claude
4
+ Code, Codex, OpenCode, Hermes, …) that has the `bizrouter` CLI installed and
5
+ needs to operate a BizRouter account on the user's behalf. The human-facing
6
+ documentation at https://bizrouter.ai/docs is mirrored, from the same source,
7
+ at https://bizrouter.ai/llms-full.txt (`bizrouter docs <topic>` reads it).
8
+
9
+ ## 1. What BizRouter is
10
+
11
+ BizRouter (https://bizrouter.ai) is an LLM gateway for Korean companies: one
12
+ API key, one bill in KRW, many providers (OpenAI, Anthropic, Google, xAI,
13
+ Perplexity, Upstage, z.AI, domestic serving). Around the gateway sits a console
14
+ where an organization manages API keys, model access, Smart Routing, security
15
+ filters (PII masking, forbidden words, Corepin), audit logs, statistics,
16
+ billing and members. **Everything the console does is an HTTP API**, and this
17
+ CLI can call all of it.
18
+
19
+ ## 2. Two credentials, two purposes
20
+
21
+ | Credential | Used for | Where it comes from |
22
+ | --- | --- | --- |
23
+ | API key `sk-br-v1-…` | Model calls: `https://api.bizrouter.ai/v1/*`, `/v1/messages`, `/claude/*`. Also what the launched coding agents use. | Issued during `bizrouter login` (browser approval) or pasted with `bizrouter login --with-key`. Env override: `BIZROUTER_API_KEY`. |
24
+ | Console session token | Console management API `https://bizrouter.ai/api/web/*` (`bizrouter api`, `keys`, `usage`, …). 30 days. | Minted when the user approves `bizrouter login` in the browser. Env override: `BIZROUTER_SESSION_TOKEN`. |
25
+
26
+ Both are stored in `~/.bizrouter/credentials.json` (mode 600;
27
+ `BIZROUTER_CONFIG_DIR` relocates it). `bizrouter auth` shows what is present
28
+ and whether it is still valid. The session carries the **user's role**:
29
+ `owner` can manage the whole organization, `member` sees only their own keys
30
+ and usage.
31
+
32
+ An API key cannot manage the console. A console session cannot call models.
33
+ If a management command fails with "콘솔 세션이 없습니다", ask the user to run
34
+ `bizrouter login` (it opens a browser; the user approves an 8-character code).
35
+ You cannot complete that approval yourself — it is deliberately a human step.
36
+
37
+ ## 3. Command cheat sheet
38
+
39
+ ```
40
+ bizrouter login [--with-key] [--no-browser] sign in (browser approval; --with-key pastes an API key)
41
+ bizrouter auth which credentials exist and whether they work
42
+ bizrouter logout revoke the console session, delete local credentials
43
+ bizrouter doctor connectivity, key, session, installed agents, conflicts
44
+ bizrouter models [query] [--json] models this key may use, KRW prices per 1M tokens
45
+
46
+ bizrouter claude|codex|opencode|hermes [--model ID] [--reasoning-effort L] [--no-mcp] [tool args…]
47
+ run a coding agent through BizRouter (config files untouched)
48
+
49
+ bizrouter api --list [keyword] every console API operation (from the live OpenAPI)
50
+ bizrouter api --schema [METHOD] <path> request/response fields of one operation
51
+ bizrouter api [METHOD] <path> [-q k=v]… [-d JSON|@file] [--yes]
52
+ call any console API; JSON on stdout
53
+
54
+ bizrouter keys list|create|update|delete API keys (name, limits, allowed models/IPs)
55
+ bizrouter usage [--from D --to D] [--by key|model|user] spend and tokens (statistics)
56
+ bizrouter billing credits, monthly bills
57
+ bizrouter org show|members|settings [k=v…] organization, members, org-level toggles
58
+ bizrouter audit list|show <id>|stats request audit logs
59
+ bizrouter routing policy|presets|profiles|logs Smart Routing
60
+ bizrouter security show|access content filters, console access (MFA, IP allowlist)
61
+
62
+ bizrouter docs [topic|--list|--full] this manual; developer docs from llms-full.txt
63
+ bizrouter mcp run as an MCP server (stdio) exposing the above as tools
64
+ ```
65
+
66
+ All management commands accept `--json` and print exactly the server's JSON,
67
+ so you can pipe into `jq`. Without `--json` they print a compact table.
68
+ Exit code 0 = success, 1 = error (message on stderr; for `api`, the server's
69
+ structured error is also printed on stdout as `{"status":…,"error":…}`).
70
+
71
+ ## 4. Rules of engagement (read before mutating anything)
72
+
73
+ 1. **Reads are free; writes need the user's OK.** Listing keys, usage,
74
+ policies and logs is fine. Creating, updating or deleting anything (keys,
75
+ policies, security settings, members, credits) must be confirmed with the
76
+ user first — quote what you are about to change. `bizrouter api` enforces
77
+ this: `POST/PUT/PATCH/DELETE` prompt for confirmation in a terminal and
78
+ refuse without `--yes` in a pipe. Add `--yes` only after the user agreed.
79
+ 2. **Never print a full API key into a file, log, commit or chat reply.**
80
+ `keys create` returns `full_key` once; hand it to the place it belongs
81
+ (env var, secret store) and show the user only the masked preview.
82
+ 3. **Do not touch money without an explicit request**: `/payments/*`,
83
+ `/organizations/credits` (PATCH), auto-charge settings, referral payouts.
84
+ 4. **Console access controls can lock people out.** Enabling the IP
85
+ allowlist (`/security/console-access`) or MFA requirement affects every
86
+ member's sign-in. The server refuses changes that would lock out the
87
+ caller, but not changes that lock out others. Confirm the exact list of
88
+ CIDRs with the user.
89
+ 5. **Audit log on/off is an organization setting**: `skip_audit_log=true`
90
+ stops storing prompts/responses for new requests (billing rows remain).
91
+ Turning it on again does not recover skipped content. Say so.
92
+ 6. **Members** can only see and manage their own API keys; commands that
93
+ need `owner` return 403 — report it, do not retry with tricks.
94
+ 7. Prefer the wrapped commands (`keys`, `usage`, …) for common tasks; use
95
+ `bizrouter api` for everything else. Look the operation up with
96
+ `--list`/`--schema` before guessing a field name.
97
+
98
+ ## 5. Console API map (paths are relative to https://bizrouter.ai/api/web)
99
+
100
+ | Area | Paths | Notes |
101
+ | --- | --- | --- |
102
+ | Account, sessions | `GET /auth/profile` · `GET /auth/sessions` · `DELETE /auth/sessions/{id}` · `GET/POST /auth/mfa…` | who am I; devices logged in |
103
+ | API keys | `GET/POST /api-keys` · `PATCH/DELETE /api-keys/{key_id}` | body: `name`, `is_active`, `credit_limit` (KRW), `credit_limit_period` lifetime/daily/monthly, `limit_alert_enabled`, `limit_alert_threshold` (%), `limit_alert_recipient_emails[]`, `allowed_ips[]` (CIDR), `allowed_model_codes[]` (empty = all) |
104
+ | Usage API keys | `/usage-api-keys` | keys for the read-only Usage API (`https://api.bizrouter.ai/usage/v1/*`) |
105
+ | Models | `GET /models` · `GET/POST/DELETE /model-blacklist…` | catalog; org-wide model blocklist |
106
+ | Organization | `GET /organizations/{id}` · `GET/PATCH /organizations/settings` · `GET /organizations/{id}/users` · `PATCH/DELETE /organizations/{id}/users/{user_id}` · `POST /organizations/invitations` | settings: `skip_audit_log`, `block_file_upload`, Claude Code model mappings (`claude_code_*_model_id`) |
107
+ | Billing | `GET /organizations/credits` · `GET /organizations/billing/monthly?months=N` · `GET /organizations/billing/evidence…` · `/payments/*` | read freely; never change without being asked |
108
+ | Statistics | `GET /statistics/?start_date&end_date&models&api_keys&users&limit&offset` · `GET /statistics/filter-options` · `GET /statistics/charts/spend|tokens|requests|api-key-model-distribution|api-key-daily-trend` · `…/export` | dates `YYYY-MM-DD`; comma-separated filters |
109
+ | Audit logs | `GET /audit-logs/?api_key&model&status&start_date&end_date&search&cursor&limit` · `GET /audit-logs/{log_id}` · `GET /audit-logs/stats/summary` · `POST /audit-logs/exports` | request-level records incl. prompts unless `skip_audit_log` |
110
+ | Smart Routing | `GET/PATCH /smart-routing/policy` · `POST /smart-routing/policy/presets` · `GET /smart-routing/policy/diagnostics` · `GET/POST /smart-routing/profiles` · `GET/PATCH/DELETE /smart-routing/profiles/{id}` · `GET /smart-routing/pool-models` · `GET /smart-routing/logs` · `/smart-routing/taxonomy-rules…` · `/smart-routing/review-candidates…` | the virtual model `bizrouter/route` follows the default policy |
111
+ | Security | `GET/PATCH /security` · `GET/POST/DELETE /security/forbidden-words…` · `GET /security/corepin/catalog` · `GET/PATCH /security/console-access` · `POST/DELETE /security/console-access/allowed-ips…` | PII masking, forbidden words, Corepin; MFA/IP policy |
112
+ | BYOK | `GET /byok/` · `PUT/DELETE /byok/{provider_name}` · `POST /byok/test` | customer's own provider keys |
113
+ | Evals | `/evals/*` | datasets, rubrics, runs (owner-enabled feature) |
114
+ | Release notes | `GET /release-notes` | what changed in the product |
115
+
116
+ `bizrouter api --list` is authoritative — the table above is a map, not the
117
+ contract. `bizrouter api --schema PATCH /organizations/settings` shows the
118
+ exact fields.
119
+
120
+ ## 6. Worked examples
121
+
122
+ Create a key for a project, limited to two models and ₩300,000 a month:
123
+
124
+ ```
125
+ bizrouter api --schema POST /api-keys
126
+ bizrouter keys create "growth-bot" --limit 300000 --period monthly \
127
+ --models openai/gpt-5.6-sol,anthropic/claude-sonnet-5 --json
128
+ # → full_key appears once. Put it where it belongs; show the user key_preview only.
129
+ ```
130
+
131
+ This month's spend by API key, then by model:
132
+
133
+ ```
134
+ bizrouter usage --from 2026-09-01 --to 2026-09-30 --by key
135
+ bizrouter usage --from 2026-09-01 --to 2026-09-30 --by model --json | jq '.items[] | {model, cost}'
136
+ ```
137
+
138
+ Turn audit logging off for the organization (after the user confirmed):
139
+
140
+ ```
141
+ bizrouter org settings skip_audit_log=true --yes
142
+ ```
143
+
144
+ Why did requests fail this morning?
145
+
146
+ ```
147
+ bizrouter audit list --status error --from 2026-09-06 --limit 20
148
+ bizrouter audit show <log_id>
149
+ ```
150
+
151
+ Recommend and apply a Smart Routing preset:
152
+
153
+ ```
154
+ bizrouter routing presets # server suggests presets for the org's traffic
155
+ bizrouter routing policy # current default policy
156
+ bizrouter api PATCH /smart-routing/policy -d '{"strategy":"cost_optimized"}' --yes
157
+ ```
158
+
159
+ Anything not wrapped:
160
+
161
+ ```
162
+ bizrouter api --list byok
163
+ bizrouter api PUT /byok/openai -d '{"api_key":"sk-…"}' --yes
164
+ ```
165
+
166
+ ## 7. Running coding agents
167
+
168
+ `bizrouter claude|codex|opencode|hermes` launches the tool with BizRouter
169
+ injected for that process only (env + the tool's own override mechanism); the
170
+ user's config files are not modified. `--model <id>` picks a BizRouter model
171
+ (`bizrouter models` lists them), `--reasoning-effort` maps to the tool's
172
+ setting, everything after our flags is passed through untouched.
173
+
174
+ When a console session exists, the launcher also registers this CLI as an MCP
175
+ server named `bizrouter` inside Claude Code, Codex and OpenCode for that run
176
+ (`--no-mcp` disables it). The agent then has tools `bizrouter_api`,
177
+ `bizrouter_openapi`, `bizrouter_docs`, `bizrouter_models` and `bizrouter_whoami`
178
+ with the same rules as above — `bizrouter_api` refuses mutating calls unless
179
+ `confirm: true` is passed after the user agreed.
180
+
181
+ Claude Code can only run Anthropic models (it always sends Anthropic-only
182
+ fields); use Codex/OpenCode/Hermes for GPT or Gemini.
183
+
184
+ ## 8. Troubleshooting
185
+
186
+ - `콘솔 세션이 없습니다` / 401 → `bizrouter login` (human step).
187
+ - 403 with a message about IP → the organization's console IP allowlist blocks
188
+ this machine; the user must add the address in the console or from an
189
+ allowed network.
190
+ - 403 on management calls → the account is a `member`; needs an `owner`.
191
+ - Model calls return 400 `model not found` → the key's allowed-model list or
192
+ the organization's model policy excludes it (`bizrouter models` shows what the
193
+ key may use).
194
+ - Hermes + model with output cap under 65536 → pick another model
195
+ (`bizrouter hermes --model anthropic/claude-sonnet-5`).
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # bizrouter CLI
2
2
 
3
- 쓰던 코딩 에이전트를 그대로 두고, 명령 앞에 `bizrouter` 만 붙이면 BizRouter 로 연결됩니다.
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
7
- bizrouter login # 브라우저에서 API 키를 발급받아 붙여 넣습니다 (한 번만)
7
+ bizrouter login # 브라우저에서 코드를 확인하고 승인합니다 (한 번만)
8
8
  bizrouter claude # Claude Code
9
9
  bizrouter codex # Codex CLI
10
10
  bizrouter opencode # OpenCode
@@ -13,11 +13,20 @@ bizrouter hermes # Hermes
13
13
 
14
14
  설치 없이 바로 써 볼 수도 있습니다: `npx bizrouter claude`
15
15
 
16
- ## 무엇을주나
16
+ ## 로그인이주는 것
17
+
18
+ `bizrouter login` 은 터미널에 8자리 코드를 보여주고 브라우저에서 BizRouter 콘솔을 엽니다. 콘솔에 로그인(비밀번호·Google·웍스AI·2차 인증 그대로)한 뒤 코드를 확인하고 「승인」하면
19
+
20
+ - 이 기기용 **콘솔 세션**(30일)과
21
+ - 이 기기 이름의 **API 키**(모델 호출용)가 함께 발급되어 `~/.bizrouter/credentials.json` 에 권한 600 으로 저장됩니다.
22
+
23
+ 키만 붙여 넣던 방식도 그대로 됩니다: `bizrouter login --with-key`. CI·컨테이너에서는 `BIZROUTER_API_KEY`(모델 호출)·`BIZROUTER_SESSION_TOKEN`(콘솔) 환경 변수가 저장된 값보다 우선합니다. 세션은 콘솔 「계정 → 로그인된 기기」에서 언제든 종료할 수 있습니다.
24
+
25
+ ## 코딩 에이전트 실행
17
26
 
18
27
  | 도구 | 실행 시 하는 일 | 설정 파일 변경 |
19
28
  | --- | --- | --- |
20
- | Claude Code | `ANTHROPIC_BASE_URL=https://api.bizrouter.ai/claude`, 토큰, 빈 `ANTHROPIC_API_KEY`, 모델 선택창 자동 채움(`CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY`)을 넘기고, 사용자 settings.json 의 다른 게이트웨이 설정이 이기지 못하도록 명령행 settings 로 고정 | 없음 |
29
+ | Claude Code | `ANTHROPIC_BASE_URL=https://api.bizrouter.ai/claude`, 토큰, 빈 `ANTHROPIC_API_KEY`, 모델 선택창 자동 채움을 넘기고, 사용자 settings.json 의 다른 게이트웨이 설정이 이기지 못하도록 명령행 settings 로 고정 | 없음 |
21
30
  | Codex CLI | `-c` 오버라이드로 BizRouter provider(Responses API)와 모델·추론 강도 지정 | 없음 |
22
31
  | OpenCode | `OPENCODE_CONFIG_CONTENT` 로 이 키가 쓸 수 있는 모델 전체가 담긴 `bizrouter` provider 를 주입 | 없음 |
23
32
  | Hermes | `custom` provider(`CUSTOM_BASE_URL`/`CUSTOM_API_KEY`)와 모델을 이번 실행에만 지정 | 없음 |
@@ -31,20 +40,44 @@ bizrouter opencode --model google/gemini-3.5-pro
31
40
  bizrouter claude --dry-run # 실행 대신 넘겨줄 환경 변수와 명령을 보여줍니다
32
41
  ```
33
42
 
34
- ## 관리 명령
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
+
45
+ ## 콘솔 관리 명령
46
+
47
+ 모두 `--json` 을 지원하고, 데이터를 바꾸는 명령은 터미널에서 확인을 묻거나 `--yes` 를 요구합니다.
48
+
49
+ ```bash
50
+ bizrouter api --list [검색어] # 콘솔 API 전체 목록 (라이브 OpenAPI 기준)
51
+ bizrouter api --schema PATCH /organizations/settings # 필드 확인
52
+ bizrouter api /statistics/ -q start_date=2026-09-01 # 아무 콘솔 API 호출
53
+ bizrouter api POST /api-keys -d '{"name":"ci"}' --yes
54
+
55
+ bizrouter keys list|create|update|delete # API 키 (한도·기간·허용 모델·허용 IP)
56
+ bizrouter usage [--from --to] [--by model|key|day] # 사용 금액·요청 수
57
+ bizrouter billing # 크레딧·월별 청구
58
+ bizrouter org show|members|settings [k=v] # 조직·구성원·감사 로그 저장 등 조직 설정
59
+ bizrouter audit list|show <id>|stats # 요청 감사 로그
60
+ bizrouter routing policy|presets|profiles|logs # 스마트 라우팅
61
+ bizrouter security show|set|access # 보안 필터·콘솔 접근 통제
62
+ bizrouter docs [주제] # 에이전트 매뉴얼(AGENT_MANUAL.md)·개발 문서
63
+ bizrouter mcp # MCP 서버(stdio)로 직접 실행
64
+ ```
65
+
66
+ 에이전트에게 맡길 때는 `bizrouter docs` 가 출력하는 매뉴얼을 먼저 읽히면 됩니다. 사람용 개발 문서(https://bizrouter.ai/docs)는 같은 소스에서 만든 https://bizrouter.ai/llms-full.txt 로도 제공됩니다.
67
+
68
+ ## 그 밖의 명령
35
69
 
36
70
  ```bash
37
71
  bizrouter models [검색어] # 이 키로 쓸 수 있는 모델·컨텍스트·원화 단가
38
- bizrouter doctor # 키·연결·설치·충돌 점검
72
+ bizrouter doctor # 키·세션·연결·설치·충돌 점검
39
73
  bizrouter setup claude|codex|opencode # 도구 설정 파일에 BizRouter 를 영구 등록 (백업 생성)
40
74
  bizrouter auth / logout / env / update
41
75
  ```
42
76
 
43
- `BIZROUTER_API_KEY` 환경 변수가 있으면 저장된 키보다 우선합니다(CI·컨테이너). 설정은 `~/.bizrouter/`(`BIZROUTER_CONFIG_DIR` 로 변경) 에 권한 600 으로 저장됩니다.
44
-
45
77
  ## 알아둘 것
46
78
 
47
- - 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
+ - 콘솔 세션은 로그인한 사용자의 권한(owner/member)을 그대로 따릅니다. 구성원(member)은 자기 API 키와 사용량만 다룰 수 있습니다.
48
81
  - 요구 사항: Node.js 18 이상. 각 코딩 에이전트는 따로 설치돼 있어야 합니다.
49
82
 
50
83
  문서: https://bizrouter.ai/docs/cli
package/dist/args.js CHANGED
@@ -7,7 +7,7 @@ export class ArgError extends Error {
7
7
  * the harness untouched, so `bizrouter claude -p "hi"` still works.
8
8
  */
9
9
  export function parseLaunchArgs(argv) {
10
- const out = { closedNetwork: false, dryRun: false, help: false, passthrough: [] };
10
+ const out = { closedNetwork: false, dryRun: false, help: false, noMcp: false, passthrough: [] };
11
11
  let i = 0;
12
12
  const takeValue = (flag) => {
13
13
  const value = argv[i + 1];
@@ -47,6 +47,10 @@ export function parseLaunchArgs(argv) {
47
47
  out.dryRun = true;
48
48
  i += 1;
49
49
  continue;
50
+ case '--no-mcp':
51
+ out.noMcp = true;
52
+ i += 1;
53
+ continue;
50
54
  case '--help':
51
55
  case '-h':
52
56
  out.help = true;
@@ -0,0 +1,152 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { ConsoleApiError, HTTP_METHODS, MUTATING_METHODS, consoleRequest, fetchOpenApi, findOperation, listOperations, normalizeConsolePath } from '../console.js';
3
+ import { readLine } from '../prompt.js';
4
+ import { c, CliError, info, padEnd, print, warn } from '../ui.js';
5
+ const USAGE = [
6
+ 'bizrouter api --list [검색어] 콘솔 API 경로 목록',
7
+ 'bizrouter api --schema [METHOD] <경로> 요청·응답 필드',
8
+ 'bizrouter api [METHOD] <경로> [-q k=v]… [-d JSON|@파일] [--yes]',
9
+ ].join('\n');
10
+ function parseData(raw) {
11
+ const text = raw.startsWith('@') ? readFileSync(raw.slice(1) === '-' ? 0 : raw.slice(1), 'utf8') : raw;
12
+ try {
13
+ return JSON.parse(text);
14
+ }
15
+ catch (error) {
16
+ throw new CliError(`-d 값이 JSON 이 아닙니다: ${error instanceof Error ? error.message : String(error)}`, {
17
+ hint: `예: -d '{"name":"ci-key"}' 또는 -d @body.json (표준 입력은 -d @-)`,
18
+ });
19
+ }
20
+ }
21
+ export function parseApiArgs(argv) {
22
+ const flags = { query: {}, yes: false, refresh: false, raw: false };
23
+ const positional = [];
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const token = argv[i];
26
+ const next = () => {
27
+ const value = argv[i + 1];
28
+ if (value === undefined)
29
+ throw new CliError(`${token} 뒤에 값이 필요합니다.`, { hint: USAGE });
30
+ i += 1;
31
+ return value;
32
+ };
33
+ if (token === '--list' || token === '-l') {
34
+ flags.list = argv[i + 1] && !argv[i + 1].startsWith('-') ? (i += 1, argv[i]) : true;
35
+ }
36
+ else if (token.startsWith('--list='))
37
+ flags.list = token.slice(7) || true;
38
+ else if (token === '--schema' || token === '-s')
39
+ flags.schema = '';
40
+ else if (token === '-d' || token === '--data')
41
+ flags.data = parseData(next());
42
+ else if (token.startsWith('--data='))
43
+ flags.data = parseData(token.slice(7));
44
+ else if (token === '-q' || token === '--query') {
45
+ const pair = next();
46
+ const eq = pair.indexOf('=');
47
+ if (eq <= 0)
48
+ throw new CliError(`-q 값은 key=value 형식이어야 합니다: ${pair}`);
49
+ flags.query[pair.slice(0, eq)] = pair.slice(eq + 1);
50
+ }
51
+ else if (token === '--yes' || token === '-y')
52
+ flags.yes = true;
53
+ else if (token === '--refresh')
54
+ flags.refresh = true;
55
+ else if (token === '--raw')
56
+ flags.raw = true;
57
+ else if (token === '-h' || token === '--help')
58
+ flags.list = flags.list ?? true;
59
+ else if (token.startsWith('-') && !/^-\d/.test(token))
60
+ throw new CliError(`알 수 없는 옵션입니다: ${token}`, { hint: USAGE });
61
+ else
62
+ positional.push(token);
63
+ }
64
+ // `api GET /x`, `api /x` (GET), `api POST /x`
65
+ const first = positional[0]?.toUpperCase();
66
+ if (first && HTTP_METHODS.includes(first)) {
67
+ flags.method = first;
68
+ flags.path = positional[1];
69
+ }
70
+ else if (positional[0]) {
71
+ flags.path = positional[0];
72
+ }
73
+ if (flags.schema === '')
74
+ flags.schema = flags.path ?? '';
75
+ if (!flags.method && flags.path)
76
+ flags.method = flags.data !== undefined ? 'POST' : 'GET';
77
+ return flags;
78
+ }
79
+ function printOperations(rows, keyword) {
80
+ if (!rows.length) {
81
+ print(keyword ? `「${keyword}」에 맞는 콘솔 API 가 없습니다.` : '콘솔 API 목록이 비어 있습니다.');
82
+ return;
83
+ }
84
+ const pathWidth = Math.min(56, Math.max(...rows.map((r) => r.path.length), 10));
85
+ print(c.dim(`${padEnd('METHOD', 7)} ${padEnd('경로 (/api/web 기준)', pathWidth)} 설명 · 질의 인자(*=필수)`));
86
+ for (const r of rows) {
87
+ const method = MUTATING_METHODS.has(r.method) ? c.yellow(padEnd(r.method, 7)) : c.green(padEnd(r.method, 7));
88
+ const extras = [r.params.length ? `?${r.params.join('&')}` : '', r.hasBody ? c.dim('[본문]') : ''].filter(Boolean).join(' ');
89
+ print(`${method} ${padEnd(r.path, pathWidth)} ${r.summary}${extras ? ` ${c.dim(extras)}` : ''}`);
90
+ }
91
+ print();
92
+ print(c.dim(`${rows.length}개 · 자세한 필드: bizrouter api --schema <METHOD> <경로> · 호출: bizrouter api <METHOD> <경로> [-q k=v] [-d JSON]`));
93
+ }
94
+ export async function apiCommand(argv) {
95
+ const flags = parseApiArgs(argv);
96
+ if (flags.list !== undefined && !flags.path) {
97
+ const spec = await fetchOpenApi({ force: flags.refresh });
98
+ const keyword = flags.list === true ? undefined : flags.list;
99
+ printOperations(listOperations(spec, keyword), keyword);
100
+ return 0;
101
+ }
102
+ if (flags.schema !== undefined) {
103
+ if (!flags.schema)
104
+ throw new CliError('--schema 뒤에 경로가 필요합니다.', { hint: USAGE });
105
+ const spec = await fetchOpenApi({ force: flags.refresh });
106
+ const method = flags.method && argv.some((a) => a.toUpperCase() === flags.method) ? flags.method : undefined;
107
+ const matches = findOperation(spec, method, flags.schema);
108
+ if (!matches.length)
109
+ throw new CliError(`콘솔 API 에 없는 경로입니다: ${normalizeConsolePath(flags.schema)}`, { hint: '`bizrouter api --list <검색어>` 로 찾아보십시오.' });
110
+ print(JSON.stringify(matches.length === 1 ? matches[0] : matches, null, 2));
111
+ return 0;
112
+ }
113
+ if (!flags.path || !flags.method) {
114
+ print(USAGE);
115
+ print();
116
+ print(c.dim('예: bizrouter api /api-keys · bizrouter api /statistics/ -q start_date=2026-09-01 · bizrouter api POST /api-keys -d \'{"name":"ci"}\''));
117
+ return 0;
118
+ }
119
+ const path = normalizeConsolePath(flags.path);
120
+ if (MUTATING_METHODS.has(flags.method) && !flags.yes) {
121
+ if (!process.stdin.isTTY) {
122
+ throw new CliError(`${flags.method} ${path} 는 데이터를 바꾸는 요청입니다. 확인 없이 실행하려면 --yes 를 붙이십시오.`, {
123
+ hint: '에이전트가 실행하는 경우, 사람에게 먼저 확인을 받은 뒤 --yes 를 붙여 다시 실행하십시오.',
124
+ });
125
+ }
126
+ warn(`${flags.method} ${path} 는 데이터를 바꾸는 요청입니다.`);
127
+ if (flags.data !== undefined)
128
+ print(c.dim(JSON.stringify(flags.data, null, 2)));
129
+ const answer = await readLine('실행할까요? (y/N) ');
130
+ if (!/^y(es)?$/i.test(answer)) {
131
+ info('취소했습니다.');
132
+ return 1;
133
+ }
134
+ }
135
+ try {
136
+ const { status, data } = await consoleRequest(flags.method, path, { query: flags.query, body: flags.data });
137
+ if (data === undefined)
138
+ print(JSON.stringify({ status }));
139
+ else if (typeof data === 'string')
140
+ print(flags.raw ? data : JSON.stringify({ status, body: data }));
141
+ else
142
+ print(JSON.stringify(data, null, 2));
143
+ return 0;
144
+ }
145
+ catch (error) {
146
+ if (error instanceof ConsoleApiError && error.body !== undefined && typeof error.body === 'object') {
147
+ // Keep the server's structured error on stdout too so agents can parse it.
148
+ print(JSON.stringify({ status: error.status, error: error.body }, null, 2));
149
+ }
150
+ throw error;
151
+ }
152
+ }