byuckchon-frontend-cli 1.1.0 → 1.4.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.
package/README.md CHANGED
@@ -1,26 +1,204 @@
1
1
  # byuckchon-frontend-cli
2
2
 
3
- [byuckchon](https://www.byuckchon.com) 컨벤션에 맞게 React(Vite) 또는 Next.js(App Router) TypeScript 프로젝트를 생성하는 CLI.
3
+ [byuckchon](https://www.byuckchon.com) 프론트엔드 팀의 **프로젝트 스타터 + AI 어시스턴트** CLI.
4
+ React(Vite) / Next.js(App Router) TypeScript 프로젝트를 만들고, `bc chat` 으로 AI 와 코드 이야기를 나눌 수 있습니다.
4
5
 
5
6
  ## 요구 사항
6
7
 
7
- - [Node.js](https://nodejs.org/) (LTS 권장)
8
+ - [Node.js](https://nodejs.org/) 18+ (LTS 권장)
9
+ - AI 사용 시: Anthropic 또는 OpenAI API 키
8
10
 
9
- ## 사용법
11
+ ## 설치
10
12
 
11
13
  ```bash
12
- npx byuckchon-frontend-cli
14
+ npm install -g byuckchon-frontend-cli
15
+ # 짧은 alias 도 동시에 설치됨: `bc`
13
16
  ```
14
17
 
15
- 프로젝트 이름과 프레임워크를 묻는 프롬프트에 따라 답하면, 현재 디렉터리에 새 폴더가 만들어집니다.
18
+ ## 명령
16
19
 
17
- 생성이 끝나면 안내에 따라 다음을 실행하세요.
20
+ ### `bc init` 프로젝트 만들기
18
21
 
19
22
  ```bash
20
- cd <프로젝트-이름>
21
- npm run dev
23
+ bc init
22
24
  ```
23
25
 
26
+ 프로젝트 이름, 프레임워크, **기본 AI 모델, Figma URL, OpenAPI URL** 을 묻고
27
+ 새 폴더에 코드 + `bc.config.json` 까지 만들어 줍니다.
28
+
29
+ ### OpenAPI / 코드 컨텍스트 — 자동 주입 (v1.4+)
30
+
31
+ `bc.config.json` 의 `api.openapi` 와 코드 인덱스는 **chat 시작할 때 알아서 준비됩니다.**
32
+ 즉, 명령을 외울 필요 없이 그냥 `bc` 만 치고 자연어로 일을 시키면 됩니다.
33
+
34
+ - **OpenAPI**: chat 시작 시 자동 fetch + 1시간 디스크 캐시 → 엔드포인트 요약을 시스템 프롬프트에 박음.
35
+ - 헤더에 `openapi` 줄로 표시. 캐시 hit 면 `(cached)`, fresh fetch 면 `(live)`.
36
+ - **코드 인덱스**: chat 시작 시 인덱스 파일이 없으면 **백그라운드에서 자동 빌드**.
37
+ - 빌드 중에는 화면에 `📚 인덱싱 중 ...` 진행 표시. 끝나면 `✓` 메시지 한 줄.
38
+ - OpenAI 키가 없으면 빌드를 건너뛰고 도움 메시지를 띄움 (Anthropic 은 임베딩 API 미제공).
39
+ - **수동 컨트롤이 필요할 때:**
40
+
41
+ | 시나리오 | 명령 |
42
+ | --------------------------------------- | --------------------------------------------------- |
43
+ | 인덱스 다시 빌드 (chat 안에서) | `/index` 또는 `/index rebuild` |
44
+ | 인덱스 다시 빌드 (chat 밖에서) | `bc index` / `bc index --rebuild` |
45
+ | 인덱스 상태/검색 | `bc index status` / `bc index search "토큰 갱신"` |
46
+ | OpenAPI → `*.gen.ts` 결정론 생성 | `bc gen api-types` (필요할 때만, AI 가 권하기도 함) |
47
+ | RAG 잠시 끄기 | chat 안에서 `/rag off` |
48
+
49
+ #### 예시 — 진짜로 명령 안 외우고 시키기
50
+
51
+ `bc.config.json` 에 Swagger URL 만 박혀 있으면:
52
+
53
+ ```text
54
+ you › api/seller 부분 GET~POST 내 api 폴더 구조 참고해서 코드 짜줘
55
+ ```
56
+
57
+ → 모델이 자동 주입된 OpenAPI 요약 + RAG 로 가져온 `src/api/*` 컨텍스트를 보고
58
+ 해당 프로젝트 컨벤션(예: 기존 fetch 래퍼, axios 인스턴스, TanStack Query 훅 패턴)에 맞춰 코드를 짜 줍니다.
59
+ 타입이 부족하면 모델이 **"`bc gen api-types` 한 번 돌려달라"** 고 직접 안내해 줍니다.
60
+
61
+ > 비결정론적 코드 생성보다 결정론적인 타입 생성이 안전한 부분(예: `*.gen.ts`) 만 별도 명령으로 빼두고,
62
+ > 컴포넌트/엔드포인트 호출 코드는 채팅으로 처리하는 하이브리드 구조입니다.
63
+
64
+ #### `bc gen api-types` (선택) — OpenAPI → TS 타입 결정론 생성
65
+
66
+ ```bash
67
+ bc gen api-types # bc.config.json 의 api.openapi 사용
68
+ bc gen api-types --source https://api.dev/openapi.json # URL 직접
69
+ bc gen api-types --source ./openapi.yaml # 로컬 파일
70
+ bc gen api-types --out src/api/types.gen.ts # 출력 경로 지정 (기본값)
71
+ ```
72
+
73
+ ```ts
74
+ import type { paths, components } from '@/api/types.gen';
75
+
76
+ type ListUsersResponse =
77
+ paths['/users']['get']['responses']['200']['content']['application/json'];
78
+ type User = components['schemas']['User'];
79
+ ```
80
+
81
+ ### `bc adopt` — 기존 프로젝트에 bc 설정만 깔기
82
+
83
+ ```bash
84
+ cd 내-Expo-프로젝트
85
+ bc adopt
86
+ ```
87
+
88
+ `package.json` 과 디렉터리를 스캔해서 **프레임워크/언어/스타일/라우팅/패키지 매니저** 를 자동 감지하고,
89
+ Figma·OpenAPI URL 만 추가로 묻고 `bc.config.json` 만 떨궈줍니다.
90
+ **소스 코드는 절대 건드리지 않습니다.**
91
+
92
+ 지원 감지: Next.js · Expo · Electron · Vite+React · Remix · CRA · 일반 React.
93
+
94
+ ### `bc chat` — AI 와 대화 (ink TUI)
95
+
96
+ ```bash
97
+ bc # 인자 없이도 chat 진입 (제일 짧은 단축키)
98
+ bc start # chat 의 alias
99
+ bc chat # ink 풀 TUI (기본)
100
+ bc chat --model claude-haiku-4 # 이번 세션만 모델 지정
101
+ bc chat --plain # 단순 readline 모드
102
+ bc chat --once "useEffect 의존성 배열 누락된 거 어떻게 찾아?" # 1회성 호출 (CI/스크립트)
103
+ bc chat -c # 가장 최근 세션 이어가기
104
+ bc chat --list-history # 저장된 세션 목록
105
+ bc chat --resume 2026-06-19_15-23-45 # 특정 세션 이어가기
106
+ ```
107
+
108
+ **슬래시 명령 자동완성:** 입력창에서 `/` 만 쳐도 사용 가능한 명령이 메뉴로 펼쳐집니다.
109
+ 계속 타이핑하면 필터링되고, `↑↓` 로 이동, `Enter` 또는 `Tab` 으로 자동완성, `Esc` 로 취소.
110
+
111
+ 대화 세션은 자동으로 디스크에 저장됩니다:
112
+
113
+ - 프로젝트 안에서 실행 → `<projectRoot>/.bc/history/<id>.json` (`.bc/` 는 gitignore 됨)
114
+ - 그 외 → `~/.bc/history/<cwd-hash>/<id>.json`
115
+
116
+ 매 턴마다 자동 저장돼서 터미널이 닫히거나 충돌해도 `bc chat -c` 로 바로 복구할 수 있습니다.
117
+
118
+ TTY 안에서 자동으로 ink 모드로 뜨고, 파이프/CI 같은 비-TTY 환경에서는
119
+ `--plain` 모드로 자동 폴백합니다.
120
+
121
+ 세션 내 슬래시 명령:
122
+
123
+ | 명령 | 동작 |
124
+ | ------------------- | ------------------------------------------ |
125
+ | `/help` | 도움말 |
126
+ | `/clear` | 대화 컨텍스트 초기화 |
127
+ | `/model [id]` | 세션 모델 변경 (인자 없으면 목록) |
128
+ | `/cost` | 누적 토큰/비용 |
129
+ | `/image <path>` | 다음 메시지에 이미지 첨부 (Vision 모델 권장) |
130
+ | `/paste` | 클립보드 이미지 첨부 (macOS, `pngpaste` 필요) |
131
+ | `/attachments` | 현재 첨부 목록 |
132
+ | `/clear-attach` | 첨부 비우기 |
133
+ | `/index [rebuild]` | 코드 인덱스 빌드/재빌드 (자동 빌드된 거 갱신) |
134
+ | `/rag on\|off` | RAG 컨텍스트 주입 즉석 토글 |
135
+ | `/exit` | 종료 (`Ctrl+C` 도 가능) |
136
+
137
+ 이미지 첨부는 png / jpg / jpeg / gif / webp 만 지원하며,
138
+ Claude / GPT 비전 모델에 멀티파트 메시지로 전달됩니다.
139
+
140
+ **이미지 첨부 3가지 방법:**
141
+ 1. `/image ./shot.png` — 경로 직접
142
+ 2. **드래그 & 드롭** — `/image ` 까지 입력 후, Finder 에서 파일을 터미널 위로 끌어다 놓으면 절대경로가 자동 입력됩니다. Enter.
143
+ 3. `/paste` — **macOS 한정**, 클립보드의 이미지(예: `Cmd+Shift+4` 스크린샷)를 바로 첨부.
144
+ - 사전에 `brew install pngpaste` 한 번 필요.
145
+
146
+ ### 한글 입력이 안 보일 때
147
+
148
+ ink 가 터미널 커서를 숨겨버려서 macOS 한글 IME 의 조합 미리보기가 안 보이는 이슈가 있었습니다.
149
+ v1.4 부터는 ink 시작 후 커서를 강제로 다시 켜고 가짜 커서를 끄는 방식으로 수정되어 정상 동작해야 합니다.
150
+ 혹시 그래도 문제가 보이면 `bc chat --plain` 으로 readline 모드를 쓸 수 있습니다 (TUI 기능은 일부 제한).
151
+
152
+ ### `bc config` — 설정
153
+
154
+ ```bash
155
+ bc config show # 현재 적용 중인 설정 확인
156
+ bc config set-model # 대화형 모델 선택
157
+ bc config set-model claude-sonnet-4-5 # 직접 지정
158
+ bc config set-key anthropic # 키 안전 입력 (가려짐)
159
+ bc config set-key anthropic sk-ant-... # 직접 지정
160
+ bc config set-gateway https://ai.example.com # 사내 게이트웨이 모드
161
+ bc config set-gateway # 게이트웨이 해제 (BYOK 모드)
162
+ ```
163
+
164
+ ## 설정 위치
165
+
166
+ ```
167
+ ~/.bc/config.json # 글로벌 — API 키, 기본 모델 (chmod 600)
168
+ <project>/bc.config.json # 프로젝트별 — Figma/OpenAPI 링크, 기본 모델 강제
169
+ .env # 프로젝트 — ANTHROPIC_API_KEY 등 (자동 로드)
170
+ ```
171
+
172
+ 우선순위: **환경변수 > 글로벌 키**, **프로젝트 모델 > 글로벌 모델**.
173
+
174
+ ## 지원 모델
175
+
176
+ | id | provider | 추천 용도 |
177
+ | ------------------- | --------- | ---------------------------------- |
178
+ | `claude-sonnet-4-5` | anthropic | 기본 — 코드 Q&A, 리팩터, 컴포넌트 |
179
+ | `claude-haiku-4` | anthropic | 짧은 작업, 커밋 메시지 (저렴) |
180
+ | `claude-opus-4-5` | anthropic | 큰 리팩터, 아키텍처 설계 (고가) |
181
+ | `gpt-5` | openai | 일반 코드 |
182
+ | `gpt-5-mini` | openai | 저렴한 OpenAI |
183
+
184
+ ## 토큰/비용 안전장치
185
+
186
+ - 세션 누적이 `limits.warnAtTokens` 를 넘으면 경고 출력.
187
+ - 한 요청 추정 토큰이 `limits.confirmAtTokens` 를 넘으면 확인.
188
+ - BYOK 가 기본 — 외부에서 깔아도 우리 비용은 0.
189
+ - 사내에서는 게이트웨이 모드로 사용량 모니터링 가능.
190
+
191
+ ## 로드맵
192
+
193
+ - [x] Phase 1: provider 추상화, 글로벌/프로젝트 설정, 스트리밍 REPL
194
+ - [x] Phase 2a: ink 기반 풀 TUI, 이미지 첨부 (`/image`)
195
+ - [x] Phase 2b: 프로젝트 자동 감지(`bc adopt`), 세션 영구 저장(`-c`/`-r`/`--list-history`)
196
+ - [x] Phase 3a: 코드베이스 RAG (`bc index`, `/rag` 토글, 자동 컨텍스트 주입)
197
+ - [x] Phase 3b: `bc gen api-types` (OpenAPI → TS 타입), `/paste` 클립보드 이미지, 한글 IME 수정
198
+ - [x] Phase 3c-1: chat 시작 시 인덱스 자동 빌드, OpenAPI 자동 fetch+캐시+시스템 프롬프트 주입
199
+ - [ ] Phase 3c-2: Figma 실 fetch (URL → 노드 트리 → 컴포넌트 인텐트)
200
+ - [ ] Phase 4: `bc gen component/page` (AST 편집 + 검증 루프), `/apply` diff 미리보기
201
+
24
202
  ## 라이선스
25
203
 
26
204
  MIT
package/bin/index.js CHANGED
@@ -1,5 +1,161 @@
1
1
  #!/usr/bin/env node
2
+ import { config as loadDotenv } from 'dotenv';
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
2
5
 
3
6
  import { initCommand } from '../src/commands/init.js';
7
+ import { chatCommand } from '../src/commands/chat.js';
8
+ import { adoptCommand } from '../src/commands/adopt.js';
9
+ import {
10
+ indexBuildCommand,
11
+ indexStatusCommand,
12
+ indexSearchCommand,
13
+ } from '../src/commands/indexCmd.js';
14
+ import { genApiTypesCommand } from '../src/commands/genCmd.js';
15
+ import {
16
+ configShowCommand,
17
+ configSetModelCommand,
18
+ configSetKeyCommand,
19
+ configSetGatewayCommand,
20
+ } from '../src/commands/config.js';
4
21
 
5
- initCommand();
22
+ // 프로젝트 .env 가 있으면 자동 로드 (ANTHROPIC_API_KEY, OPENAI_API_KEY 등).
23
+ loadDotenv({ quiet: true });
24
+
25
+ const program = new Command();
26
+
27
+ program
28
+ .name('bc')
29
+ .description('Byuckchon Frontend Workbench — 프로젝트 스타터 + AI 어시스턴트')
30
+ .version('1.4.0');
31
+
32
+ program
33
+ .command('init')
34
+ .description('새 프론트엔드 프로젝트 생성 (React/Next.js)')
35
+ .action(async () => {
36
+ await initCommand();
37
+ });
38
+
39
+ program
40
+ .command('adopt')
41
+ .description('기존 프로젝트에 bc 설정만 추가 (Expo/Electron/Next/Vite/CRA 자동 감지)')
42
+ .option('-f, --force', '기존 bc.config.json 이 있어도 묻지 않고 덮어쓰기')
43
+ .action(async (opts) => {
44
+ await adoptCommand(opts);
45
+ });
46
+
47
+ const idx = program
48
+ .command('index')
49
+ .description('코드베이스 임베딩 인덱스 빌드 (chat 의 RAG 컨텍스트로 사용됨)')
50
+ .option('--rebuild', '캐시 무시하고 처음부터 다시 빌드')
51
+ .action(async (opts) => {
52
+ await indexBuildCommand(opts);
53
+ });
54
+
55
+ idx.command('status').description('현재 인덱스 상태').action(async () => {
56
+ await indexStatusCommand();
57
+ });
58
+
59
+ idx
60
+ .command('search <query>')
61
+ .description('인덱스에서 코드 검색 (디버그용)')
62
+ .option('-k, --top-k <n>', '상위 K 개', '5')
63
+ .action(async (query, opts) => {
64
+ await indexSearchCommand(query, opts);
65
+ });
66
+
67
+ const gen = program
68
+ .command('gen')
69
+ .description('자동 코드 생성 (OpenAPI → TS 타입 등)');
70
+
71
+ gen
72
+ .command('api-types')
73
+ .description('bc.config.json 의 api.openapi 또는 --source 에서 TS 타입 생성')
74
+ .option('--source <urlOrPath>', 'OpenAPI URL 또는 로컬 파일 경로')
75
+ .option('--out <path>', '출력 파일 경로 (기본 src/api/types.gen.ts)')
76
+ .action(async (opts) => {
77
+ await genApiTypesCommand(opts);
78
+ });
79
+
80
+ // chat 본체와 alias 들을 한 번에 등록하기 위한 헬퍼.
81
+ function registerChatCommand(name, descSuffix = '') {
82
+ return program
83
+ .command(name)
84
+ .description('AI 와 대화하며 코드 묻고/고치기 (ink TUI)' + descSuffix)
85
+ .option('-m, --model <id>', '이번 세션에 사용할 모델 id')
86
+ .option('--once <prompt>', 'REPL 없이 한 번만 호출하고 종료')
87
+ .option('--plain', 'ink TUI 대신 평문 readline 모드')
88
+ .option('-c, --continue', '가장 최근 세션 이어가기', false)
89
+ .option('-r, --resume <id>', '특정 세션 id 이어가기')
90
+ .option('--list-history', '저장된 세션 목록 출력 후 종료', false)
91
+ .action(async (opts) => {
92
+ await chatCommand({ ...opts, continueLast: opts.continue });
93
+ });
94
+ }
95
+
96
+ registerChatCommand('chat');
97
+ registerChatCommand('start', ' · chat 의 alias');
98
+
99
+ const cfg = program
100
+ .command('config')
101
+ .description('CLI 설정 (모델, API 키, 게이트웨이)');
102
+
103
+ cfg
104
+ .command('show')
105
+ .description('현재 적용 중인 설정 보기')
106
+ .action(async () => {
107
+ await configShowCommand();
108
+ });
109
+
110
+ cfg
111
+ .command('set-model [id]')
112
+ .description('기본 AI 모델 변경 (인자 없으면 대화형)')
113
+ .action(async (id) => {
114
+ await configSetModelCommand(id);
115
+ });
116
+
117
+ cfg
118
+ .command('set-key <provider> [key]')
119
+ .description('API 키 저장 (provider: anthropic | openai). key 생략 시 안전 입력.')
120
+ .action(async (provider, key) => {
121
+ await configSetKeyCommand(provider, key);
122
+ });
123
+
124
+ cfg
125
+ .command('set-gateway [url]')
126
+ .description('사내 AI 게이트웨이 URL 설정 (인자 없으면 해제)')
127
+ .action(async (url) => {
128
+ await configSetGatewayCommand(url);
129
+ });
130
+
131
+ program.exitOverride((err) => {
132
+ if (
133
+ err.code === 'commander.help' ||
134
+ err.code === 'commander.helpDisplayed' ||
135
+ err.code === 'commander.version'
136
+ ) {
137
+ process.exit(0);
138
+ }
139
+ if (err.code === 'commander.missingArgument' || err.code === 'commander.unknownCommand') {
140
+ console.error(chalk.red('\n ' + err.message + '\n'));
141
+ process.exit(1);
142
+ }
143
+ throw err;
144
+ });
145
+
146
+ // 인자 없이 `bc` 만 친 경우엔 chat 으로 자동 진입 (codex CLI 와 같은 UX).
147
+ // 단, --version / --help 같이 명시 플래그가 있으면 commander 가 처리하도록 둔다.
148
+ const NO_ARGS = process.argv.length <= 2;
149
+
150
+ (async () => {
151
+ try {
152
+ if (NO_ARGS) {
153
+ await chatCommand({});
154
+ return;
155
+ }
156
+ await program.parseAsync(process.argv);
157
+ } catch (err) {
158
+ console.error(chalk.red('\n 실행 중 오류: ') + (err?.message ?? err) + '\n');
159
+ process.exit(1);
160
+ }
161
+ })();
package/package.json CHANGED
@@ -1,17 +1,35 @@
1
1
  {
2
2
  "name": "byuckchon-frontend-cli",
3
- "version": "1.1.0",
4
- "description": "byuckchon frontend CLI for creating React and Next.js projects",
3
+ "version": "1.4.0",
4
+ "description": "Byuckchon Frontend Workbench project starter + AI chat + codebase RAG + OpenAPI codegen",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=18.18"
8
+ },
6
9
  "bin": {
7
- "byuckchon-frontend-cli": "./bin/index.js"
10
+ "byuckchon-frontend-cli": "./bin/index.js",
11
+ "bc": "./bin/index.js"
8
12
  },
9
13
  "scripts": {
10
- "start": "node bin/index.js"
14
+ "start": "node bin/index.js",
15
+ "chat": "node bin/index.js chat",
16
+ "init": "node bin/index.js init",
17
+ "config": "node bin/index.js config show"
11
18
  },
12
19
  "dependencies": {
20
+ "@ai-sdk/anthropic": "^3.0.85",
21
+ "@ai-sdk/openai": "^3.0.73",
22
+ "ai": "^6.0.208",
13
23
  "chalk": "^5.3.0",
14
- "inquirer": "^9.3.0"
24
+ "commander": "^15.0.0",
25
+ "dotenv": "^17.4.2",
26
+ "fast-glob": "^3.3.3",
27
+ "ink": "^7.1.0",
28
+ "ink-spinner": "^5.0.0",
29
+ "ink-text-input": "^6.0.0",
30
+ "inquirer": "^9.3.0",
31
+ "openapi-typescript": "^7.13.0",
32
+ "react": "^19.2.7"
15
33
  },
16
34
  "files": [
17
35
  "bin",
@@ -23,8 +41,16 @@
23
41
  "byuckchon",
24
42
  "frontend",
25
43
  "cli",
44
+ "ai",
45
+ "claude",
46
+ "anthropic",
47
+ "openai",
48
+ "rag",
49
+ "ink",
26
50
  "react",
27
- "next"
51
+ "next",
52
+ "expo",
53
+ "electron"
28
54
  ],
29
55
  "author": "Byuckchon Frontend Team",
30
56
  "license": "MIT",
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const IMAGE_EXT_TO_MIME = {
5
+ '.png': 'image/png',
6
+ '.jpg': 'image/jpeg',
7
+ '.jpeg': 'image/jpeg',
8
+ '.gif': 'image/gif',
9
+ '.webp': 'image/webp',
10
+ };
11
+
12
+ export function isImagePath(p) {
13
+ if (!p) return false;
14
+ return Object.keys(IMAGE_EXT_TO_MIME).includes(path.extname(p).toLowerCase());
15
+ }
16
+
17
+ /**
18
+ * 파일 경로 → AI SDK 가 받는 image content part 로 변환.
19
+ *
20
+ * AI SDK v6 의 multi-modal 메시지 형식:
21
+ * { role: 'user', content: [
22
+ * { type: 'text', text: '...' },
23
+ * { type: 'image', image: <Buffer|base64-data-url|URL> },
24
+ * ]}
25
+ *
26
+ * Buffer 로 넘기면 SDK 가 알아서 base64 + mime 처리해 준다.
27
+ */
28
+ export async function imagePartFromFile(filePath) {
29
+ const abs = path.resolve(filePath);
30
+ const stat = await fs.stat(abs);
31
+ if (!stat.isFile()) {
32
+ const e = new Error(`이미지 파일이 아닙니다: ${filePath}`);
33
+ e.code = 'BC_NOT_A_FILE';
34
+ throw e;
35
+ }
36
+ if (!isImagePath(abs)) {
37
+ const e = new Error(
38
+ `지원 안 하는 확장자: ${path.extname(abs)} (png, jpg, jpeg, gif, webp 만)`,
39
+ );
40
+ e.code = 'BC_UNSUPPORTED_IMAGE';
41
+ throw e;
42
+ }
43
+ const buf = await fs.readFile(abs);
44
+ return {
45
+ type: 'image',
46
+ image: buf,
47
+ // SDK 가 mediaType 을 추론해주기는 하지만 명시적으로 박아둔다.
48
+ mediaType: IMAGE_EXT_TO_MIME[path.extname(abs).toLowerCase()],
49
+ };
50
+ }
51
+
52
+ /**
53
+ * UI 가 들고 있는 단순한 메시지 형태:
54
+ * { role, text, attachments: [{ kind:'image', path, sizeKb }] }
55
+ * 를 AI SDK 가 받는 messages 배열로 변환한다.
56
+ *
57
+ * 반드시 거르는 것들 (이걸 안 거르면 SDK 가 ModelMessage[] 스키마 위반으로 거절한다):
58
+ * - 'system-info' / 'system-error' 같은 UI 전용 role (e.g. /help, /clear, 에러 표시)
59
+ * - 비어있는 assistant 메시지 (스트리밍 중단, 에러 등으로 텍스트가 한 글자도 없는 경우)
60
+ * - 비어있고 첨부도 없는 user 메시지 (방어적 — 정상 입력에선 일어나지 않음)
61
+ */
62
+ export async function toSdkMessages(uiMessages) {
63
+ const out = [];
64
+ for (const m of uiMessages) {
65
+ if (m.role !== 'user' && m.role !== 'assistant') continue; // UI 전용 role 제거
66
+ if (m.role === 'assistant' && !m.text?.trim()) continue; // 빈/실패 응답 제거
67
+
68
+ if (m.role === 'user' && m.attachments?.length) {
69
+ const parts = [];
70
+ if (m.text) parts.push({ type: 'text', text: m.text });
71
+ for (const att of m.attachments) {
72
+ if (att.kind === 'image') {
73
+ parts.push(await imagePartFromFile(att.path));
74
+ }
75
+ }
76
+ if (parts.length === 0) continue;
77
+ out.push({ role: 'user', content: parts });
78
+ } else {
79
+ if (m.role === 'user' && !m.text?.trim()) continue;
80
+ out.push({ role: m.role, content: m.text ?? '' });
81
+ }
82
+ }
83
+ return out;
84
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 지원하는 AI 모델 카탈로그.
3
+ *
4
+ * 새 모델/프로바이더를 추가할 때는 이 파일만 수정하면 된다.
5
+ * id: 사용자가 config 에 저장하는 안정적인 키.
6
+ * apiModel: 실제 SDK 가 호출할 때 쓰는 모델 식별자.
7
+ *
8
+ * 가격은 1M 토큰 기준 USD. 토큰 카운터에서 비용 추정에 쓴다.
9
+ */
10
+ export const MODEL_CATALOG = [
11
+ {
12
+ id: 'claude-sonnet-4-5',
13
+ label: 'Claude Sonnet 4.5 (권장 · 코드 품질·속도 밸런스)',
14
+ provider: 'anthropic',
15
+ apiModel: 'claude-sonnet-4-5',
16
+ contextWindow: 200_000,
17
+ pricing: { input: 3, output: 15, cachedInput: 0.3 },
18
+ tier: 'balanced',
19
+ },
20
+ {
21
+ id: 'claude-haiku-4',
22
+ label: 'Claude Haiku 4 (저렴 · 짧은 작업·커밋 메시지)',
23
+ provider: 'anthropic',
24
+ apiModel: 'claude-haiku-4',
25
+ contextWindow: 200_000,
26
+ pricing: { input: 0.8, output: 4, cachedInput: 0.08 },
27
+ tier: 'fast',
28
+ },
29
+ {
30
+ id: 'claude-opus-4-5',
31
+ label: 'Claude Opus 4.5 (고성능 · 큰 리팩터·아키텍처)',
32
+ provider: 'anthropic',
33
+ apiModel: 'claude-opus-4-5',
34
+ contextWindow: 200_000,
35
+ pricing: { input: 15, output: 75, cachedInput: 1.5 },
36
+ tier: 'powerful',
37
+ },
38
+ {
39
+ id: 'gpt-5',
40
+ label: 'GPT-5 (OpenAI · 일반 코드)',
41
+ provider: 'openai',
42
+ apiModel: 'gpt-5',
43
+ contextWindow: 400_000,
44
+ pricing: { input: 5, output: 20, cachedInput: 0.5 },
45
+ tier: 'balanced',
46
+ },
47
+ {
48
+ id: 'gpt-5-mini',
49
+ label: 'GPT-5 mini (OpenAI · 저렴)',
50
+ provider: 'openai',
51
+ apiModel: 'gpt-5-mini',
52
+ contextWindow: 400_000,
53
+ pricing: { input: 0.5, output: 2, cachedInput: 0.05 },
54
+ tier: 'fast',
55
+ },
56
+ ];
57
+
58
+ export const DEFAULT_MODEL_ID = 'claude-sonnet-4-5';
59
+
60
+ export function findModel(id) {
61
+ return MODEL_CATALOG.find((m) => m.id === id);
62
+ }
63
+
64
+ export function modelChoices() {
65
+ return MODEL_CATALOG.map((m) => ({ name: m.label, value: m.id }));
66
+ }
@@ -0,0 +1,54 @@
1
+ import { createAnthropic } from '@ai-sdk/anthropic';
2
+ import { createOpenAI } from '@ai-sdk/openai';
3
+
4
+ import { findModel } from './models.js';
5
+
6
+ /**
7
+ * 설정에서 받은 effective 정보를 가지고 AI SDK 가 바로 쓸 수 있는
8
+ * `model` 객체를 만들어 돌려준다.
9
+ *
10
+ * - gateway 가 있으면 baseURL 로 주입 (사내 프록시 / OpenRouter / LiteLLM 등).
11
+ * - 키가 없으면 명확한 에러 메시지로 실패 (chat 명령에서 user-friendly 처리).
12
+ */
13
+ export function resolveModel(effective) {
14
+ const meta = findModel(effective.model);
15
+ if (!meta) {
16
+ const err = new Error(
17
+ `알 수 없는 모델 '${effective.model}'. \`bc config set-model\` 으로 다시 골라주세요.`,
18
+ );
19
+ err.code = 'BC_UNKNOWN_MODEL';
20
+ throw err;
21
+ }
22
+
23
+ const apiKey = effective.apiKeys?.[meta.provider];
24
+ if (!apiKey && !effective.gateway) {
25
+ const envName =
26
+ meta.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
27
+ const err = new Error(
28
+ `${meta.provider} API 키가 없습니다.\n` +
29
+ ` - 환경변수 ${envName} 로 넣거나\n` +
30
+ ` - \`bc config set-key ${meta.provider} <key>\` 로 저장하세요.`,
31
+ );
32
+ err.code = 'BC_NO_API_KEY';
33
+ throw err;
34
+ }
35
+
36
+ if (meta.provider === 'anthropic') {
37
+ const anthropic = createAnthropic({
38
+ apiKey: apiKey ?? 'gateway',
39
+ baseURL: effective.gateway ?? undefined,
40
+ });
41
+ return { meta, model: anthropic(meta.apiModel) };
42
+ }
43
+ if (meta.provider === 'openai') {
44
+ const openai = createOpenAI({
45
+ apiKey: apiKey ?? 'gateway',
46
+ baseURL: effective.gateway ?? undefined,
47
+ });
48
+ return { meta, model: openai(meta.apiModel) };
49
+ }
50
+
51
+ const err = new Error(`지원하지 않는 provider: ${meta.provider}`);
52
+ err.code = 'BC_UNKNOWN_PROVIDER';
53
+ throw err;
54
+ }