open-claude-p 1.0.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/LICENSE +21 -0
- package/README.ja.md +708 -0
- package/README.ko.md +713 -0
- package/README.md +850 -0
- package/README.zh.md +708 -0
- package/bin/cli.js +782 -0
- package/package.json +68 -0
- package/scripts/postinstall.js +60 -0
- package/src/chat/event-filters.js +116 -0
- package/src/chat/index.js +1225 -0
- package/src/completion/detector.js +163 -0
- package/src/daemon/client.js +172 -0
- package/src/daemon/server.js +267 -0
- package/src/daemon/socket.js +78 -0
- package/src/index.js +908 -0
- package/src/options/index.js +4 -0
- package/src/options/parse-argv.js +214 -0
- package/src/options/spec.js +519 -0
- package/src/options/validate.js +104 -0
- package/src/output/index.js +8 -0
- package/src/output/json.js +83 -0
- package/src/output/registry.js +35 -0
- package/src/output/stream-json.js +111 -0
- package/src/output/text.js +94 -0
- package/src/parsers/ansi-strip.js +94 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/pipeline.js +50 -0
- package/src/parsers/registry.js +43 -0
- package/src/parsers/sentinel.js +41 -0
- package/src/parsers/tui-frame.js +256 -0
- package/src/print-mode.js +214 -0
- package/src/pty/index.js +3 -0
- package/src/pty/pool.js +127 -0
- package/src/pty/session.js +88 -0
- package/src/session-log.js +124 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
[English](README.md) · **한국어** · [中文](README.zh.md) · [日本語](README.ja.md)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/open-claude-p)
|
|
4
|
+
[](https://www.npmjs.com/package/open-claude-p)
|
|
5
|
+
[](https://github.com/empty-user77/open-claude-p/stargazers)
|
|
6
|
+
[](https://github.com/empty-user77/open-claude-p/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/sponsors/empty-user77)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# open-claude-p (ocp)
|
|
12
|
+
|
|
13
|
+
`claude -p`(헤드리스 프린트 모드)를 사용할 수 없는 환경에서, 인터랙티브 `claude` CLI를 **node-pty로 직접 구동**하여 동일한 기능을 제공하는 PTY 기반 호환 레이어입니다.
|
|
14
|
+
|
|
15
|
+
> **핵심 차이**: `claude -p`는 Claude Code의 비대화형 모드로 내부 API를 통해 동작하지만, 특정 플랜/환경에서는 사용할 수 없습니다. `open-claude-p`는 실제 TUI 클라이언트를 PTY로 실행하고 출력 스트림을 파싱하여 동일한 결과를 얻습니다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 목차
|
|
20
|
+
|
|
21
|
+
- [설치](#설치)
|
|
22
|
+
- [CLI 사용법](#cli-사용법)
|
|
23
|
+
- [데몬 (세션 유지)](#데몬-세션-유지)
|
|
24
|
+
- [라이브러리 API](#라이브러리-api)
|
|
25
|
+
- [createDriver()](#createdriveropts)
|
|
26
|
+
- [runOneShot()](#runoneshotreq)
|
|
27
|
+
- [반환값: OneShotResult](#반환값-oneshotresult)
|
|
28
|
+
- [이벤트 타입 (onEvent 콜백)](#이벤트-타입-onevent-콜백)
|
|
29
|
+
- [세션 관리](#세션-관리)
|
|
30
|
+
- [JSONL 세션 파일 활용](#jsonl-세션-파일-활용)
|
|
31
|
+
- [출력 파싱에 대하여](#출력-파싱에-대하여)
|
|
32
|
+
- [환경변수](#환경변수)
|
|
33
|
+
- [옵션 전체 목록](#옵션-전체-목록)
|
|
34
|
+
- [샘플 앱 사용법](#샘플-앱-사용법)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 설치
|
|
39
|
+
|
|
40
|
+
### npm
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 프로젝트 의존성으로 설치
|
|
44
|
+
npm install open-claude-p
|
|
45
|
+
|
|
46
|
+
# 또는 어디서든 `ocp` CLI를 쓰려면 전역 설치
|
|
47
|
+
npm install -g open-claude-p
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 소스 빌드 (개발용)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 저장소를 clone 후 프로젝트 루트에서 symlink 설치
|
|
54
|
+
git clone https://github.com/empty-user77/open-claude-p.git
|
|
55
|
+
cd open-claude-p
|
|
56
|
+
npm link
|
|
57
|
+
|
|
58
|
+
# 또는 다른 프로젝트에서 로컬 경로로 설치
|
|
59
|
+
npm install /path/to/open-claude-p
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**전제 조건**: `claude` CLI가 `PATH`에 설치되어 있어야 합니다.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Claude Code CLI 설치 확인
|
|
66
|
+
claude --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## CLI 사용법
|
|
72
|
+
|
|
73
|
+
패키지는 단일 바이너리 **`ocp`** 를 설치합니다.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 기본 사용
|
|
77
|
+
ocp "안녕하세요"
|
|
78
|
+
|
|
79
|
+
# claude -p 와 동일한 argv 형식 지원 (-p 플래그는 호환성을 위해 무시됨)
|
|
80
|
+
ocp -p "안녕하세요"
|
|
81
|
+
|
|
82
|
+
# stdin에서 프롬프트 읽기
|
|
83
|
+
echo "서울 날씨 알려줘" | ocp
|
|
84
|
+
|
|
85
|
+
# 출력 포맷 지정
|
|
86
|
+
ocp --output-format json "한 단어로 대답: 사과"
|
|
87
|
+
ocp --output-format stream-json "안녕"
|
|
88
|
+
|
|
89
|
+
# 모델 지정
|
|
90
|
+
ocp --model sonnet "복잡한 질문..."
|
|
91
|
+
ocp --model claude-opus-4-7 "설계 리뷰..."
|
|
92
|
+
|
|
93
|
+
# 시스템 프롬프트 추가
|
|
94
|
+
ocp --append-system-prompt "항상 한국어로 답변하세요" "what's the weather?"
|
|
95
|
+
|
|
96
|
+
# 세션 재개 — sessionId는 stderr에 출력됨
|
|
97
|
+
SID=$(ocp "키위라고만 대답해" 2>&1 >/dev/null | grep sessionId | grep -oE '[0-9a-f-]{36}')
|
|
98
|
+
ocp --resume "$SID" "방금 뭐라고 했어?"
|
|
99
|
+
|
|
100
|
+
# 또는 가장 최근 세션을 자동으로 이어받기
|
|
101
|
+
ocp --continue "방금 뭐라고 했어?"
|
|
102
|
+
|
|
103
|
+
# 권한 검사 건너뜀 (자동화 환경)
|
|
104
|
+
ocp --dangerously-skip-permissions "파일을 읽어서 분석해줘"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 출력 포맷
|
|
108
|
+
|
|
109
|
+
#### `text` (기본값)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
안녕하세요! 무엇을 도와드릴까요?
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `json`
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"result": "안녕하세요! 무엇을 도와드릴까요?",
|
|
120
|
+
"session_id": "a1b2c3d4-...",
|
|
121
|
+
"is_error": false,
|
|
122
|
+
"cost_usd": null,
|
|
123
|
+
"duration_ms": 4200,
|
|
124
|
+
"num_turns": 1
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `stream-json` (NDJSON)
|
|
129
|
+
|
|
130
|
+
응답이 한 줄씩 스트리밍됩니다:
|
|
131
|
+
|
|
132
|
+
```jsonl
|
|
133
|
+
{"type":"system","subtype":"init","session_id":"a1b2c3d4-...","tools":[],"mcp_servers":[]}
|
|
134
|
+
{"type":"assistant","session_id":"a1b2c3d4-...","message":{"role":"assistant","content":[{"type":"text","text":"안녕하세요!"}]}}
|
|
135
|
+
{"type":"result","subtype":"success","session_id":"a1b2c3d4-...","is_error":false,"duration_ms":4200}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 데몬 (세션 유지)
|
|
141
|
+
|
|
142
|
+
`ocp` CLI는 기본적으로 **백그라운드 데몬**을 통해 PTY를 살려두고 세션을 이어갑니다.
|
|
143
|
+
같은 디렉토리에서 반복 호출 시 매번 2.5초 워밍업을 기다리지 않아도 되고, 대화 컨텍스트가 자동으로 유지됩니다.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
ocp "첫 번째 질문" → 데몬 없으면 새로 시작, 있으면 재사용
|
|
147
|
+
ocp "두 번째 질문" → 같은 데몬에 연결, 컨텍스트 유지
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
데몬 소켓은 `~/.ocp/` 디렉토리에 **작업 디렉토리별로** 하나씩 생성됩니다.
|
|
151
|
+
|
|
152
|
+
### 데몬 비활성화
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
OCP_NO_DAEMON=1 ocp "한 번만 실행" # 데몬 없이 직접 PTY 실행
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
데몬을 쓰지 않아야 하는 경우:
|
|
159
|
+
- `--resume`, `--continue`, `--fork-session` 플래그 사용 시 (자동으로 직접 모드 전환됨)
|
|
160
|
+
- `--input-format=stream-json` 사용 시
|
|
161
|
+
- CI/CD 등 격리된 환경에서 단건 실행 시
|
|
162
|
+
|
|
163
|
+
### 데몬 관련 환경변수
|
|
164
|
+
|
|
165
|
+
| 변수명 | 설명 | 기본값 |
|
|
166
|
+
|--------|------|--------|
|
|
167
|
+
| `OCP_NO_DAEMON` | `1`로 설정 시 데몬 비활성화 | — |
|
|
168
|
+
| `OCP_DAEMON_IDLE_MS` | 유휴 상태 지속 시 데몬 자동 종료 | `600000` (10분) |
|
|
169
|
+
| `OCP_MAX_DAEMONS` | 동시에 유지할 최대 데몬 수 | `30` |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 라이브러리 API
|
|
174
|
+
|
|
175
|
+
### `createDriver(opts?)`
|
|
176
|
+
|
|
177
|
+
드라이버를 생성합니다. 드라이버는 애플리케이션 전체에서 공유해서 사용합니다.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { createDriver } from 'open-claude-p';
|
|
181
|
+
|
|
182
|
+
const driver = createDriver({
|
|
183
|
+
claudeBin: 'claude', // claude 바이너리 경로 (기본: PATH의 claude)
|
|
184
|
+
warmupMs: 2500, // PTY 초기화 대기 시간 (ms)
|
|
185
|
+
reuseWarmupMs: 200, // 풀에서 재사용 시 대기 시간 (ms)
|
|
186
|
+
idleMs: 1500, // 응답 완료 후 침묵 대기 (ms)
|
|
187
|
+
preIdleMs: 8000, // sentinel 매칭 전 최소 대기 (ms)
|
|
188
|
+
maxResponseMs: 60_000, // 최대 응답 대기 시간 (ms), 초과 시 timeout
|
|
189
|
+
poolSize: 0, // PTY 풀 크기 (0=비활성, N>0=N개 워밍업 유지)
|
|
190
|
+
poolMaxAgeMs: 600_000, // 풀 세션 최대 수명 (ms)
|
|
191
|
+
cwd: process.cwd(), // 작업 디렉토리
|
|
192
|
+
env: {}, // 추가 환경변수
|
|
193
|
+
debug: false, // stderr에 디버그 로그 출력
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `runOneShot(req)`
|
|
198
|
+
|
|
199
|
+
단일 프롬프트를 Claude에게 전송하고 응답을 기다립니다.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const result = await driver.runOneShot({
|
|
203
|
+
prompt: '서울의 현재 날씨를 알려줘',
|
|
204
|
+
|
|
205
|
+
// ── 모델 / 동작 ──────────────────────────
|
|
206
|
+
model: 'sonnet', // 모델 지정
|
|
207
|
+
effort: 'high', // 'low' | 'medium' | 'high' | 'max'
|
|
208
|
+
thinking: 'adaptive', // 'enabled' | 'adaptive' | 'disabled'
|
|
209
|
+
maxTurns: 5, // 최대 에이전트 턴 수 (shim 강제)
|
|
210
|
+
|
|
211
|
+
// ── 시스템 프롬프트 ───────────────────────
|
|
212
|
+
systemPrompt: '너는 날씨 전문가야', // 시스템 프롬프트 전체 대체
|
|
213
|
+
appendSystemPrompt: '항상 한국어로', // 기본 프롬프트에 추가
|
|
214
|
+
|
|
215
|
+
// ── 권한 / 도구 ───────────────────────────
|
|
216
|
+
dangerouslySkipPermissions: true, // 권한 검사 건너뜀
|
|
217
|
+
allowedTools: ['WebSearch', 'Read'], // 허용 도구 화이트리스트
|
|
218
|
+
disallowedTools: ['Bash'], // 차단 도구 블랙리스트
|
|
219
|
+
|
|
220
|
+
// ── 세션 ──────────────────────────────────
|
|
221
|
+
resume: 'a1b2c3d4-...', // 이전 세션 UUID로 재개
|
|
222
|
+
continue: false, // 가장 최근 세션 계속
|
|
223
|
+
forkSession: false, // resume 시 새 세션 ID 생성
|
|
224
|
+
|
|
225
|
+
// ── 작업 디렉토리 ─────────────────────────
|
|
226
|
+
cwd: '/path/to/project',
|
|
227
|
+
|
|
228
|
+
// ── 취소 ──────────────────────────────────
|
|
229
|
+
abortSignal: controller.signal,
|
|
230
|
+
|
|
231
|
+
// ── 실시간 이벤트 콜백 ────────────────────
|
|
232
|
+
onEvent(ev) {
|
|
233
|
+
// 응답이 생성되는 동안 실시간으로 호출됨
|
|
234
|
+
// 이벤트 타입은 아래 "이벤트 타입" 섹션 참고
|
|
235
|
+
if (ev.type === 'assistant-text') {
|
|
236
|
+
process.stdout.write(ev.text);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 반환값: OneShotResult
|
|
243
|
+
|
|
244
|
+
`runOneShot()`이 resolve되면 다음 구조의 객체를 반환합니다:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
{
|
|
248
|
+
// ── 핵심 결과 ──────────────────────────────────────────────────────
|
|
249
|
+
text: string,
|
|
250
|
+
// Claude의 최종 응답 텍스트 (TUI 아티팩트 제거됨).
|
|
251
|
+
// 마크다운, HTML, 코드블록 등 Claude가 생성한 원본 텍스트.
|
|
252
|
+
// 렌더링/파싱은 호출하는 쪽에서 직접 해야 함.
|
|
253
|
+
|
|
254
|
+
sessionId: string | null,
|
|
255
|
+
// 이 요청에 해당하는 Claude 세션 UUID.
|
|
256
|
+
// --resume <sessionId> 로 대화를 이어갈 수 있음.
|
|
257
|
+
// 배너 캡처 실패 시 ~/.claude/projects/ 파일시스템으로 폴백.
|
|
258
|
+
|
|
259
|
+
isError: boolean,
|
|
260
|
+
// true = 오류 또는 timeout으로 완료
|
|
261
|
+
|
|
262
|
+
completionReason: string,
|
|
263
|
+
// 완료 이유:
|
|
264
|
+
// 'sentinel' 정상 완료 (sentinel 문자열 감지)
|
|
265
|
+
// 'idle' 응답 후 침묵 타임아웃
|
|
266
|
+
// 'prompt-box' TUI 입력 박스 재등장 감지
|
|
267
|
+
// 'timeout' maxResponseMs 초과
|
|
268
|
+
// 'max-turns' maxTurns 한도 도달
|
|
269
|
+
// 'upstream-exited' claude 프로세스가 먼저 종료
|
|
270
|
+
// 'write-failed' PTY write 실패
|
|
271
|
+
// 'cancelled' AbortSignal로 취소됨
|
|
272
|
+
|
|
273
|
+
exitCode: number,
|
|
274
|
+
// 0 = 정상, 1 = 오류
|
|
275
|
+
|
|
276
|
+
// ── 이벤트 배열 ────────────────────────────────────────────────────
|
|
277
|
+
events: Array<object>,
|
|
278
|
+
// 파이프라인이 생성한 이벤트 전체 배열 (onEvent 콜백과 동일한 객체들).
|
|
279
|
+
// 아래 "이벤트 타입" 섹션 참고.
|
|
280
|
+
|
|
281
|
+
// ── 성능 메트릭 ────────────────────────────────────────────────────
|
|
282
|
+
durationMs: number,
|
|
283
|
+
// 총 소요 시간 (ms)
|
|
284
|
+
|
|
285
|
+
cost: { totalUsd: number | null, numTurns: number | null },
|
|
286
|
+
// 현재 null (PTY에서는 비용 정보를 직접 획득할 수 없음).
|
|
287
|
+
// 정확한 토큰/비용 정보는 JSONL 세션 파일에서 읽어야 함 (아래 참고).
|
|
288
|
+
|
|
289
|
+
diagnostics: { rawBytes: number, strippedBytes: number },
|
|
290
|
+
// PTY에서 받은 원시 바이트 수 / ANSI 제거 후 바이트 수
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 이벤트 타입 (onEvent 콜백)
|
|
295
|
+
|
|
296
|
+
`onEvent` 콜백과 `result.events` 배열에는 다음 타입의 이벤트가 포함됩니다:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// Claude가 응답을 시작했을 때 (⏺ 마커 감지)
|
|
300
|
+
{ type: 'assistant-region-entered', n: number }
|
|
301
|
+
|
|
302
|
+
// 응답 영역이 닫혔을 때 (hr 또는 sentinel 감지)
|
|
303
|
+
{ type: 'assistant-region-exited', n: number }
|
|
304
|
+
|
|
305
|
+
// 응답 텍스트 한 줄 (실시간 스트리밍)
|
|
306
|
+
{
|
|
307
|
+
type: 'assistant-text',
|
|
308
|
+
text: string, // 한 줄의 텍스트 (마크다운 원문 그대로)
|
|
309
|
+
region: number // 몇 번째 응답 영역인지 (resume 시 이전 기록이 높은 번호로 필터됨)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Claude 세션 UUID 감지 (배너 또는 exit 메시지에서)
|
|
313
|
+
{ type: 'session-id', id: string }
|
|
314
|
+
|
|
315
|
+
// TUI 스피너 (작업 중 상태 표시)
|
|
316
|
+
// label: "Searching the web...", "Reading file...", "Cogitated for 25s" 등
|
|
317
|
+
{ type: 'spinner', label: string }
|
|
318
|
+
|
|
319
|
+
// TUI 입력 박스가 화면에 나타남 (완료 신호 중 하나)
|
|
320
|
+
{ type: 'prompt-box-shown' }
|
|
321
|
+
|
|
322
|
+
// sentinel 문자열이 감지됨 (정상 완료)
|
|
323
|
+
{ type: 'sentinel' }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### 이벤트 활용 예시
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const result = await driver.runOneShot({
|
|
330
|
+
prompt: '긴 문서를 분석해줘',
|
|
331
|
+
onEvent(ev) {
|
|
332
|
+
switch (ev.type) {
|
|
333
|
+
case 'assistant-text':
|
|
334
|
+
// 실시간 스트리밍 — 줄 단위로 화면에 출력
|
|
335
|
+
process.stdout.write(ev.text + '\n');
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case 'spinner':
|
|
339
|
+
// 스피너 라벨 — 도구 사용 중 표시 (예: "Searching the web...")
|
|
340
|
+
process.stderr.write(`\r⏳ ${ev.label} `);
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'session-id':
|
|
344
|
+
// 세션 ID를 미리 저장해두면 timeout 시에도 재개 가능
|
|
345
|
+
saveSessionId(ev.id);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// 전체 텍스트는 events에서도 재조합 가능
|
|
352
|
+
const lines = result.events
|
|
353
|
+
.filter(e => e.type === 'assistant-text' && e.region === Math.max(...result.events.filter(e => e.type === 'assistant-text').map(e => e.region)))
|
|
354
|
+
.map(e => e.text);
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 세션 관리
|
|
360
|
+
|
|
361
|
+
Claude는 각 세션을 UUID로 구분하고, 세션 ID를 이용해 이전 대화를 이어갈 수 있습니다.
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
// 1. 첫 번째 요청 — 새 세션 시작
|
|
365
|
+
const result1 = await driver.runOneShot({
|
|
366
|
+
prompt: '파이썬으로 피보나치를 구현해줘',
|
|
367
|
+
});
|
|
368
|
+
console.log('세션 ID:', result1.sessionId);
|
|
369
|
+
// → "a1b2c3d4-5678-..."
|
|
370
|
+
|
|
371
|
+
// 2. 세션 재개 — 이전 대화 컨텍스트가 유지됨
|
|
372
|
+
const result2 = await driver.runOneShot({
|
|
373
|
+
prompt: '그 코드를 재귀가 아닌 반복으로 바꿔줘',
|
|
374
|
+
resume: result1.sessionId,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// 3. 세션 분기 — 원본 보존하면서 다른 방향 탐색
|
|
378
|
+
const result3 = await driver.runOneShot({
|
|
379
|
+
prompt: '대신 제너레이터 버전으로 만들어줘',
|
|
380
|
+
resume: result1.sessionId,
|
|
381
|
+
forkSession: true, // 새 UUID 할당, 원본 세션 보존
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## JSONL 세션 파일 활용
|
|
388
|
+
|
|
389
|
+
Claude CLI는 각 세션을 아래 경로에 JSONL 파일로 저장합니다:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
~/.claude/projects/<cwd를-로-인코딩된-경로>/<session-uuid>.jsonl
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
예: cwd가 `/Users/alice/myproject`이면
|
|
396
|
+
→ `~/.claude/projects/-Users-alice-myproject/<uuid>.jsonl`
|
|
397
|
+
|
|
398
|
+
이 파일에는 PTY 출력에는 없는 **토큰 사용량, 비용, 도구 사용 내역** 등의 메타데이터가 포함되어 있습니다.
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
import { readFile } from 'node:fs/promises';
|
|
402
|
+
import path from 'node:path';
|
|
403
|
+
import os from 'node:os';
|
|
404
|
+
|
|
405
|
+
async function readSessionMeta(sessionId, cwd = process.cwd()) {
|
|
406
|
+
const key = path.resolve(cwd).replace(/\//g, '-');
|
|
407
|
+
const filePath = path.join(os.homedir(), '.claude', 'projects', key, `${sessionId}.jsonl`);
|
|
408
|
+
const lines = (await readFile(filePath, 'utf8')).split('\n').filter(Boolean);
|
|
409
|
+
|
|
410
|
+
// 마지막 assistant 메시지에서 usage 추출
|
|
411
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
412
|
+
try {
|
|
413
|
+
const ev = JSON.parse(lines[i]);
|
|
414
|
+
if (ev.message?.role === 'assistant') {
|
|
415
|
+
const textBlock = ev.message.content?.find(c => c.type === 'text');
|
|
416
|
+
return {
|
|
417
|
+
text: textBlock?.text, // 클린 마크다운 텍스트 (TUI 아티팩트 없음)
|
|
418
|
+
usage: ev.message.usage, // { input_tokens, output_tokens, cache_read_input_tokens, ... }
|
|
419
|
+
timestamp: ev.timestamp,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
} catch {}
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const meta = await readSessionMeta(result.sessionId);
|
|
428
|
+
// meta.usage.input_tokens → 입력 토큰
|
|
429
|
+
// meta.usage.output_tokens → 출력 토큰
|
|
430
|
+
// meta.usage.cache_read_input_tokens → 캐시 읽기 토큰
|
|
431
|
+
// meta.usage.server_tool_use.web_search_requests → 웹 검색 횟수
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### JSONL에서 얻을 수 있는 것
|
|
435
|
+
|
|
436
|
+
| 항목 | PTY result.text | JSONL |
|
|
437
|
+
|------|----------------|-------|
|
|
438
|
+
| 응답 텍스트 | ✅ (TUI 아티팩트 포함 가능) | ✅ (클린 마크다운) |
|
|
439
|
+
| 입력 토큰 수 | ❌ | ✅ |
|
|
440
|
+
| 출력 토큰 수 | ❌ | ✅ |
|
|
441
|
+
| 캐시 토큰 수 | ❌ | ✅ |
|
|
442
|
+
| 비용 계산 | ❌ | ✅ (토큰 × 단가) |
|
|
443
|
+
| 웹 검색 횟수 | ❌ | ✅ |
|
|
444
|
+
| 타임스탬프 | ❌ | ✅ |
|
|
445
|
+
| 도구 사용 내역 | 부분적 (이벤트) | ✅ |
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## 출력 파싱에 대하여
|
|
450
|
+
|
|
451
|
+
**`result.text`는 Claude가 생성한 원시 마크다운/텍스트입니다.**
|
|
452
|
+
오픈 포맷이라 렌더링, 파싱, 표시 방법은 각 프로젝트에서 직접 구현해야 합니다.
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
result.text 예시:
|
|
456
|
+
─────────────────────────────────────
|
|
457
|
+
# 피보나치 수열
|
|
458
|
+
|
|
459
|
+
파이썬으로 피보나치를 구현하는 방법입니다:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
def fib(n):
|
|
463
|
+
a, b = 0, 1
|
|
464
|
+
for _ in range(n):
|
|
465
|
+
a, b = b, a + b
|
|
466
|
+
return a
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
- 시간 복잡도: O(n)
|
|
470
|
+
- 공간 복잡도: O(1)
|
|
471
|
+
─────────────────────────────────────
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 파싱 구현 참고
|
|
475
|
+
|
|
476
|
+
`sample/public/app.js`의 `renderMarkdown()` 함수는 웹 UI를 위한 파싱 예시입니다.
|
|
477
|
+
실제 사용 환경에 맞게 직접 구현하세요:
|
|
478
|
+
|
|
479
|
+
```js
|
|
480
|
+
// 웹 UI → HTML 렌더링 (예시)
|
|
481
|
+
import { marked } from 'marked';
|
|
482
|
+
const html = marked.parse(result.text);
|
|
483
|
+
|
|
484
|
+
// 터미널 → ANSI 컬러 렌더링 (예시)
|
|
485
|
+
import { renderMarkdown } from 'cli-markdown';
|
|
486
|
+
console.log(renderMarkdown(result.text));
|
|
487
|
+
|
|
488
|
+
// 다른 LLM에 전달 → 그대로 사용
|
|
489
|
+
const nextPrompt = `이전 응답: ${result.text}\n\n이제 다음 단계를 진행해줘`;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### TUI 아티팩트에 대하여
|
|
493
|
+
|
|
494
|
+
`result.text`는 ocp가 최대한 TUI 렌더링 잔재를 제거하지만, 완벽하지 않을 수 있습니다.
|
|
495
|
+
더 클린한 텍스트가 필요하면 **JSONL 세션 파일**에서 읽는 것을 권장합니다 (위 참고).
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 환경변수
|
|
500
|
+
|
|
501
|
+
### 드라이버 옵션
|
|
502
|
+
|
|
503
|
+
| 변수명 | 대응 옵션 | 기본값 |
|
|
504
|
+
|--------|-----------|--------|
|
|
505
|
+
| `OCP_CLAUDE_BIN` | `claudeBin` | `'claude'` |
|
|
506
|
+
| `OCP_WARMUP_MS` | `warmupMs` | `2500` |
|
|
507
|
+
| `OCP_REUSE_WARMUP_MS` | `reuseWarmupMs` | `200` |
|
|
508
|
+
| `OCP_IDLE_MS` | `idleMs` | `1500` |
|
|
509
|
+
| `OCP_PRE_IDLE_MS` | `preIdleMs` | `8000` |
|
|
510
|
+
| `OCP_MAX_RESPONSE_MS` | `maxResponseMs` | `60000` |
|
|
511
|
+
| `OCP_POOL_SIZE` | `poolSize` | `0` |
|
|
512
|
+
| `OCP_POOL_MAX_AGE_MS` | `poolMaxAgeMs` | `600000` |
|
|
513
|
+
|
|
514
|
+
### 데몬 (CLI 전용)
|
|
515
|
+
|
|
516
|
+
| 변수명 | 설명 | 기본값 |
|
|
517
|
+
|--------|------|--------|
|
|
518
|
+
| `OCP_NO_DAEMON` | `1`로 설정 시 데몬 비활성화, 직접 PTY 실행 | — |
|
|
519
|
+
| `OCP_DAEMON_IDLE_MS` | 유휴 상태 지속 시 데몬 자동 종료 대기 시간 | `600000` |
|
|
520
|
+
| `OCP_MAX_DAEMONS` | 동시에 유지할 최대 데몬 수 | `30` |
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
# 응답 제한 시간을 10분으로 늘리기
|
|
524
|
+
OCP_MAX_RESPONSE_MS=600000 ocp "복잡한 작업..."
|
|
525
|
+
|
|
526
|
+
# 데몬 없이 단건 실행
|
|
527
|
+
OCP_NO_DAEMON=1 ocp "한 번만 실행"
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 옵션 전체 목록
|
|
533
|
+
|
|
534
|
+
`runOneShot(req)` 요청 객체와 CLI 플래그 대응표:
|
|
535
|
+
|
|
536
|
+
| req 필드 | CLI 플래그 | 타입 | 설명 |
|
|
537
|
+
|----------|-----------|------|------|
|
|
538
|
+
| `model` | `--model` | string | 모델 이름 (예: `sonnet`, `claude-sonnet-4-6`) |
|
|
539
|
+
| `systemPrompt` | `--system-prompt` | string | 시스템 프롬프트 전체 대체 |
|
|
540
|
+
| `appendSystemPrompt` | `--append-system-prompt` | string | 기본 시스템 프롬프트에 추가 |
|
|
541
|
+
| `dangerouslySkipPermissions` | `--dangerously-skip-permissions` | boolean | 권한 검사 건너뜀 |
|
|
542
|
+
| `allowedTools` | `--allowed-tools` | string[] | 허용 도구 화이트리스트 |
|
|
543
|
+
| `disallowedTools` | `--disallowed-tools` | string[] | 차단 도구 블랙리스트 |
|
|
544
|
+
| `resume` | `--resume` / `-r` | string | 세션 UUID로 재개 |
|
|
545
|
+
| `continue` | `--continue` / `-c` | boolean | 가장 최근 세션 계속 |
|
|
546
|
+
| `forkSession` | `--fork-session` | boolean | resume 시 새 세션 ID 생성 |
|
|
547
|
+
| `sessionId` | `--session-id` | string | 새 세션에 특정 UUID 지정 |
|
|
548
|
+
| `noSessionPersistence` | `--no-session-persistence` | boolean | 세션 저장 비활성화 |
|
|
549
|
+
| `effort` | `--effort` | enum | `low` \| `medium` \| `high` \| `max` |
|
|
550
|
+
| `thinking` | `--thinking` | enum | `enabled` \| `adaptive` \| `disabled` |
|
|
551
|
+
| `maxTurns` | `--max-turns` | number | 최대 에이전트 턴 수 |
|
|
552
|
+
| `fallbackModel` | `--fallback-model` | string | 기본 모델 과부하 시 폴백 |
|
|
553
|
+
| `permissionMode` | `--permission-mode` | string | `default` \| `plan` \| `acceptEdits` \| `bypassPermissions` |
|
|
554
|
+
| `mcpConfig` | `--mcp-config` | string[] | MCP 설정 경로 |
|
|
555
|
+
| `addDir` | `--add-dir` | string[] | 도구가 접근할 추가 디렉토리 |
|
|
556
|
+
| `bare` | `--bare` | boolean | 최소 모드 (hooks, LSP, 플러그인 등 비활성) |
|
|
557
|
+
| `debug` | `--debug` | boolean | 디버그 로그를 stderr에 출력 |
|
|
558
|
+
| `verbose` | `--verbose` | boolean | 상세 출력 |
|
|
559
|
+
| `cwd` | `--cwd` | string | PTY 프로세스 작업 디렉토리 |
|
|
560
|
+
| `abortSignal` | — | AbortSignal | 요청 취소 신호 |
|
|
561
|
+
| `onEvent` | — | function | 실시간 이벤트 콜백 |
|
|
562
|
+
| `passThroughArgv` | — | string[] | claude에 그대로 전달할 추가 argv |
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 샘플 앱 사용법
|
|
567
|
+
|
|
568
|
+
`sample/` 디렉토리에는 ocp를 활용한 웹 기반 채팅 UI가 포함되어 있습니다.
|
|
569
|
+
|
|
570
|
+
### 실행
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
cd sample
|
|
574
|
+
node server.js
|
|
575
|
+
# → http://localhost:3000
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### 샘플 앱 구조
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
sample/
|
|
582
|
+
server.js Express 서버 — ocp 드라이버 래핑, SSE 스트리밍
|
|
583
|
+
data/
|
|
584
|
+
conversations.json 대화 기록 (자동 생성)
|
|
585
|
+
public/
|
|
586
|
+
index.html 채팅 UI
|
|
587
|
+
app.js 클라이언트 JavaScript
|
|
588
|
+
style.css 스타일시트
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### 샘플 서버 API
|
|
592
|
+
|
|
593
|
+
| 엔드포인트 | 메서드 | 설명 |
|
|
594
|
+
|-----------|--------|------|
|
|
595
|
+
| `/api/conversations` | GET | 대화 목록 |
|
|
596
|
+
| `/api/conversations/:id` | GET | 대화 상세 (전체 메시지) |
|
|
597
|
+
| `/api/conversations/:id` | DELETE | 대화 삭제 |
|
|
598
|
+
| `/api/chat` | POST | 메시지 전송 (SSE 스트리밍) |
|
|
599
|
+
| `/api/monitor` | GET | PTY 이벤트 모니터 (SSE) |
|
|
600
|
+
| `/api/skills` | GET | `~/.claude/skills/` 스킬 목록 |
|
|
601
|
+
| `/api/processes` | GET | 진행 중인 요청 목록 (`id`, `prompt`, `elapsedMs`) |
|
|
602
|
+
| `/api/processes/:id` | DELETE | 특정 요청 abort (`all`로 전체 종료) |
|
|
603
|
+
|
|
604
|
+
### `/api/chat` SSE 이벤트
|
|
605
|
+
|
|
606
|
+
채팅 요청(`POST /api/chat`)은 Server-Sent Events로 응답을 스트리밍합니다:
|
|
607
|
+
|
|
608
|
+
```js
|
|
609
|
+
// 클라이언트 요청
|
|
610
|
+
const resp = await fetch('/api/chat', {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
message: '서울 날씨 알려줘',
|
|
615
|
+
conversationId: null, // null이면 새 대화 시작
|
|
616
|
+
skillName: 'my-skill', // 선택사항: ~/.claude/skills/ 의 스킬 이름
|
|
617
|
+
}),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// SSE 이벤트 종류
|
|
621
|
+
{ type: 'spinner', label: 'Searching the web...' } // 작업 중 상태
|
|
622
|
+
{ type: 'text', text: '안녕하세요...' } // 스트리밍 텍스트 (조각)
|
|
623
|
+
{ type: 'error', error: '오류 메시지' } // 오류
|
|
624
|
+
{
|
|
625
|
+
type: 'done',
|
|
626
|
+
conversationId: 'uuid', // 대화 ID (저장됨)
|
|
627
|
+
text: '최종 전체 응답', // 완전한 최종 텍스트 (JSONL에서 읽은 클린 마크다운)
|
|
628
|
+
isNew: true, // 새 대화 여부
|
|
629
|
+
meta: {
|
|
630
|
+
elapsedMs: 4200, // 소요 시간 (ms)
|
|
631
|
+
inputTokens: 1500, // 인풋 토큰 (cache 포함)
|
|
632
|
+
outputTokens: 320, // 아웃풋 토큰
|
|
633
|
+
costUsd: 0.0042, // 비용 (USD)
|
|
634
|
+
tools: ['WebSearch'], // 사용된 도구 목록
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### 샘플의 마크다운 파싱
|
|
640
|
+
|
|
641
|
+
샘플 앱(`sample/public/app.js`)은 `renderMarkdown()` 함수로 `result.text`를 HTML로 변환합니다.
|
|
642
|
+
|
|
643
|
+
**이 파싱 코드는 샘플 전용입니다.** 실제 프로젝트에서는:
|
|
644
|
+
- 웹: `marked`, `markdown-it` 등 라이브러리 사용
|
|
645
|
+
- 터미널: `cli-markdown`, `terminal-link` 등 사용
|
|
646
|
+
- React: `react-markdown` 사용
|
|
647
|
+
- 다른 LLM 입력: 그대로 사용
|
|
648
|
+
|
|
649
|
+
### 프로세스 매니저 (`ocp-ps`)
|
|
650
|
+
|
|
651
|
+
샘플 앱은 `/api/processes` API를 이용해 진행 중인 요청을 조회·취소할 수 있는 CLI 도구를 함께 제공합니다.
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
cd sample
|
|
655
|
+
|
|
656
|
+
node ocp-ps.js # 실행 중인 요청 목록
|
|
657
|
+
node ocp-ps.js kill <id> # 특정 요청 abort
|
|
658
|
+
node ocp-ps.js kill all # 전체 abort
|
|
659
|
+
node ocp-ps.js watch # 1초마다 자동 갱신
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
> **참고**: `ocp-ps`는 샘플 앱의 HTTP API(`/api/processes`)를 사용하는 샘플 구현체입니다.
|
|
663
|
+
> ocp 라이브러리를 사용해 서버를 직접 구현할 때는 같은 패턴으로 프로세스 관리 API를 구성할 수 있습니다.
|
|
664
|
+
|
|
665
|
+
### 스킬 호출 (`/스킬명`)
|
|
666
|
+
|
|
667
|
+
채팅 입력창에서 `/`를 타이핑하면 `~/.claude/skills/` 의 스킬 목록이 드롭다운으로 표시됩니다.
|
|
668
|
+
|
|
669
|
+
```
|
|
670
|
+
사용자 입력: /my-skill 이 문서 분석해서 결과 정리해줘
|
|
671
|
+
↓
|
|
672
|
+
서버: SKILL.md 내용을 appendSystemPrompt로 주입
|
|
673
|
+
↓
|
|
674
|
+
Claude: 스킬 지시에 따라 실행
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## 모듈 구조
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
src/
|
|
683
|
+
index.js 라이브러리 공개 API (createDriver, runOneShot)
|
|
684
|
+
options/
|
|
685
|
+
spec.js 모든 옵션 정의 (단일 소스)
|
|
686
|
+
parse-argv.js CLI argv 파서
|
|
687
|
+
validate.js 교차 옵션 유효성 검사
|
|
688
|
+
parsers/
|
|
689
|
+
ansi-strip.js ANSI 이스케이프 제거
|
|
690
|
+
tui-frame.js TUI 프레임 파서 (이벤트 생성)
|
|
691
|
+
sentinel.js 완료 sentinel 감지
|
|
692
|
+
pipeline.js 파서 파이프라인 조합
|
|
693
|
+
output/
|
|
694
|
+
text.js --output-format text 어댑터
|
|
695
|
+
json.js --output-format json 어댑터
|
|
696
|
+
stream-json.js --output-format stream-json 어댑터
|
|
697
|
+
pty/
|
|
698
|
+
session.js 단일 PTY 세션 생명주기
|
|
699
|
+
pool.js 워밍업 PTY 풀
|
|
700
|
+
completion/
|
|
701
|
+
detector.js 완료 감지 (sentinel + idle + prompt-box)
|
|
702
|
+
bin/
|
|
703
|
+
cli.js ocp CLI 진입점
|
|
704
|
+
sample/
|
|
705
|
+
server.js 예제 웹 서버
|
|
706
|
+
public/ 채팅 UI
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## 라이선스
|
|
712
|
+
|
|
713
|
+
MIT
|