devez-vibe 0.1.41 → 0.1.43

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 CHANGED
@@ -1,33 +1,33 @@
1
- DevezCode source code license
2
-
3
- The MIT License below applies only to the DevezCode source code and to files
4
- that are not listed in the exception below.
5
-
6
- Exceptions:
7
-
8
- * `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
9
- Studio Image Library materials, subject to the included Microsoft terms.
10
- * `Resources/Images/ShellPresets/` contains third-party brand assets, subject
11
- to their respective owners' terms.
12
-
13
- MIT License
14
-
15
- Copyright (c) 2026 DevezCode contributors
16
-
17
- Permission is hereby granted, free of charge, to any person obtaining a copy
18
- of this software and associated documentation files (the "Software"), to deal
19
- in the Software without restriction, including without limitation the rights
20
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
- copies of the Software, and to permit persons to whom the Software is
22
- furnished to do so, subject to the following conditions:
23
-
24
- The above copyright notice and this permission notice shall be included in all
25
- copies or substantial portions of the Software.
26
-
27
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
1
+ DevezCode source code license
2
+
3
+ The MIT License below applies only to the DevezCode source code and to files
4
+ that are not listed in the exception below.
5
+
6
+ Exceptions:
7
+
8
+ * `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
9
+ Studio Image Library materials, subject to the included Microsoft terms.
10
+ * `Resources/Images/ShellPresets/` contains third-party brand assets, subject
11
+ to their respective owners' terms.
12
+
13
+ MIT License
14
+
15
+ Copyright (c) 2026 DevezCode contributors
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
package/README.md CHANGED
@@ -1,56 +1,67 @@
1
- # Devez Vibe
2
-
3
- 공식 `codex app-server`를 위한 차분한 터미널 클라이언트입니다. 인증, 하네스 프롬프트,
4
- 도구, 스킬, `AGENTS.md`, 샌드박스는 Codex가 그대로 담당하고, 이 프로젝트는 화면과 입력
5
- 계층만 소유합니다.
6
-
7
- ## 설치
8
-
9
- ```powershell
10
- npm install -g devez-vibe
11
- ```
12
-
13
- 설치하면 `dvz` 명령을 사용할 수 있습니다.
14
-
15
- | 요건 | 값 |
16
- | --- | --- |
17
- | OS | Windows x64 |
18
- | Node.js | 18 이상 (설치용. 실행 자체는 네이티브 바이너리) |
19
- | 전제 | Codex CLI 설치 및 로그인 완료 |
20
-
21
- 현재 Windows x64 빌드만 배포합니다. 다른 플랫폼에서는 `EBADPLATFORM`으로 설치가 거부됩니다.
22
-
23
- ## 사용
24
-
25
- Codex CLI가 설치되고 로그인된 환경에서:
26
-
27
- ```powershell
28
- dvz
29
- ```
30
-
31
- 주요 옵션:
32
-
33
- ```text
34
- dvz [--resume [SESSION] | --continue] [--model MODEL] [--effort EFFORT]
35
- [--cwd PATH] [--codex PATH] [--theme THEME]
36
- ```
37
-
38
- `--resume`만 입력하면 검색 가능한 세션 피커를 열고, `--continue`는 현재 폴더의 가장 최근
39
- 세션을 바로 이어갑니다. 실행 전체 명령은 `/help`에서 확인합니다.
40
-
41
- ## 업데이트
42
-
43
- 새 버전이 배포되면 시작 시 안내 배너가 표시됩니다. 배너의 안내대로 실행하면 됩니다.
44
-
45
- ```powershell
46
- dvz update
47
- ```
48
-
49
- `npm install -g devez-vibe@latest`와 동일하며, 실행 중인 바이너리를 교체할 있도록
50
- 별도 창에서 설치가 진행됩니다.
51
-
52
- 업데이트 확인을 끄려면 `DEVEZ_VIBE_NO_UPDATE_CHECK` 환경변수를 설정합니다.
53
-
54
- ## 라이선스
55
-
56
- MIT. 자세한 내용은 [저장소](https://github.com/MrHoje/Devez-vibe)를 참고하세요.
1
+ # Devez Vibe
2
+
3
+ 공식 `codex app-server`와 Claude Agent SDK를 위한 차분한 터미널 클라이언트입니다.
4
+ 공식 런타임이 인증·도구·스킬·프로젝트 지침을 담당하고, 이 프로젝트는 공통 화면과
5
+ 입력 계층을 소유합니다.
6
+
7
+ ## 설치
8
+
9
+ ```powershell
10
+ npm install -g devez-vibe
11
+ ```
12
+
13
+ 설치하면 `dvz` 명령을 사용할 수 있습니다.
14
+
15
+ | 요건 | 값 |
16
+ | --- | --- |
17
+ | OS | Windows x64 |
18
+ | Node.js | 18 이상 (Claude Agent SDK 런타임 포함) |
19
+ | Codex 전제 | Codex CLI 설치 및 로그인 완료 |
20
+ | Claude 전제 | Claude Code 설치 후 `claude` 구독 로그인 완료 |
21
+
22
+ 현재 Windows x64 빌드만 배포합니다. 다른 플랫폼에서는 `EBADPLATFORM`으로 설치가 거부됩니다.
23
+
24
+ ## 사용
25
+
26
+ Codex CLI가 설치되고 로그인된 환경에서:
27
+
28
+ ```powershell
29
+ dvz
30
+ ```
31
+
32
+ 개인 Claude 구독 로그인 재사용:
33
+
34
+ ```powershell
35
+ dvz --model claude
36
+ dvz --model sonnet --effort high
37
+ ```
38
+
39
+ Claude SDK 자식 프로세스는 API 환경변수를 제거하고 설치된 Claude Code 로그인만 사용합니다.
40
+ 본인 계정의 로컬 개인 사용만 대상으로 하며 로그인 공유나 제3자 인증은 제공하지 않습니다.
41
+
42
+ 주요 옵션:
43
+
44
+ ```text
45
+ dvz [--resume [SESSION] | --continue] [--model MODEL] [--effort EFFORT]
46
+ [--cwd PATH] [--codex PATH] [--claude PATH] [--theme THEME]
47
+ ```
48
+
49
+ `--resume`만 입력하면 검색 가능한 세션 피커를 열고, `--continue`는 현재 폴더의 가장 최근
50
+ 세션을 바로 이어갑니다. 실행 중 전체 명령은 `/help`에서 확인합니다.
51
+
52
+ ## 업데이트
53
+
54
+ 버전이 배포되면 시작 시 안내 배너가 표시됩니다. 배너의 안내대로 실행하면 됩니다.
55
+
56
+ ```powershell
57
+ dvz update
58
+ ```
59
+
60
+ `npm install -g devez-vibe@latest`와 동일하며, 실행 중인 바이너리를 교체할 수 있도록
61
+ 별도 창에서 설치가 진행됩니다.
62
+
63
+ 업데이트 확인을 끄려면 `DEVEZ_VIBE_NO_UPDATE_CHECK` 환경변수를 설정합니다.
64
+
65
+ ## 라이선스
66
+
67
+ MIT. 자세한 내용은 [저장소](https://github.com/MrHoje/Devez-vibe)를 참고하세요.
package/bin/dvz.exe CHANGED
Binary file
@@ -0,0 +1,916 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { randomUUID } from "node:crypto";
4
+ import { readFile } from "node:fs/promises";
5
+ import { createInterface } from "node:readline";
6
+ import {
7
+ deleteSession,
8
+ forkSession,
9
+ getSessionInfo,
10
+ getSessionMessages,
11
+ listSessions,
12
+ query,
13
+ } from "@anthropic-ai/claude-agent-sdk";
14
+
15
+ const VERSION = process.env.DEVEZ_VIBE_VERSION || "dev";
16
+ const sessions = new Map();
17
+ const pendingHostRequests = new Map();
18
+ const modelCatalogs = new Map();
19
+ let nextHostRequest = 1;
20
+
21
+ class AsyncQueue {
22
+ constructor() {
23
+ this.values = [];
24
+ this.waiters = [];
25
+ this.closed = false;
26
+ }
27
+
28
+ push(value) {
29
+ if (this.closed) throw new Error("Claude 입력 큐가 종료되었습니다.");
30
+ const waiter = this.waiters.shift();
31
+ if (waiter) waiter({ value, done: false });
32
+ else this.values.push(value);
33
+ }
34
+
35
+ close() {
36
+ this.closed = true;
37
+ for (const waiter of this.waiters.splice(0)) waiter({ value: undefined, done: true });
38
+ }
39
+
40
+ [Symbol.asyncIterator]() {
41
+ return {
42
+ next: () => {
43
+ if (this.values.length) return Promise.resolve({ value: this.values.shift(), done: false });
44
+ if (this.closed) return Promise.resolve({ value: undefined, done: true });
45
+ return new Promise((resolve) => this.waiters.push(resolve));
46
+ },
47
+ };
48
+ }
49
+ }
50
+
51
+ function write(message) {
52
+ process.stdout.write(`${JSON.stringify(message)}\n`);
53
+ }
54
+
55
+ function notify(method, params) {
56
+ write({ method, params });
57
+ }
58
+
59
+ function rpcError(error) {
60
+ return {
61
+ code: -32000,
62
+ message: error instanceof Error ? error.message : String(error),
63
+ };
64
+ }
65
+
66
+ function hostRequest(method, params, signal) {
67
+ const id = `claude-host-${nextHostRequest++}`;
68
+ return new Promise((resolve, reject) => {
69
+ const abort = () => {
70
+ pendingHostRequests.delete(id);
71
+ reject(new Error("사용자 입력 요청이 취소되었습니다."));
72
+ };
73
+ if (signal?.aborted) return abort();
74
+ signal?.addEventListener("abort", abort, { once: true });
75
+ pendingHostRequests.set(id, {
76
+ resolve: (value) => {
77
+ signal?.removeEventListener("abort", abort);
78
+ resolve(value);
79
+ },
80
+ });
81
+ write({ id, method, params });
82
+ });
83
+ }
84
+
85
+ function sanitizedEnvironment() {
86
+ const env = { ...process.env };
87
+ delete env.ANTHROPIC_API_KEY;
88
+ delete env.ANTHROPIC_AUTH_TOKEN;
89
+ env.CLAUDE_AGENT_SDK_CLIENT_APP = `devez-vibe/${VERSION}`;
90
+ return env;
91
+ }
92
+
93
+ function modelCapabilities(models, model) {
94
+ const value = stripClaudeModel(model);
95
+ return models.find((candidate) => candidate.value === value || candidate.resolvedModel === value)
96
+ || (!value ? models.find((candidate) => candidate.value === "default") : undefined);
97
+ }
98
+
99
+ function supportedEffort(model, requested) {
100
+ if (!model) return requested || "high";
101
+ const levels = Array.isArray(model.supportedEffortLevels) ? model.supportedEffortLevels : [];
102
+ if (!model.supportsEffort || !levels.length) return "";
103
+ if (requested && levels.includes(requested)) return requested;
104
+ return levels.includes("high") ? "high" : levels.at(-1) || "";
105
+ }
106
+
107
+ function compactClaudeModelName(model) {
108
+ const clean = (value) => String(value || "")
109
+ .replace(/^Claude\s*(?:·|:|-)?\s*/i, "")
110
+ .replace(
111
+ /\s*(?:\[[^\]]*\bcontext\b[^\]]*\]|\([^)]*\bcontext\b[^)]*\)|\[\s*\d+(?:\.\d+)?\s*[mk]\s*\]|\(\s*\d+(?:\.\d+)?\s*[mk]\s*\))\s*$/i,
112
+ "",
113
+ )
114
+ .trim();
115
+ let fallback = "";
116
+ for (const candidate of [model.displayName, model.resolvedModel, model.value]) {
117
+ const source = clean(candidate);
118
+ const match = source.match(/\b(fable|opus|sonnet|haiku)\b(?:[\s-]+(\d+(?:[.-]\d+)?))?/i);
119
+ if (!match) continue;
120
+ const family = match[1][0].toUpperCase() + match[1].slice(1).toLowerCase();
121
+ const version = match[2]?.replace("-", ".");
122
+ if (version) return `${family} ${version}`;
123
+ fallback ||= family;
124
+ }
125
+ return fallback || clean(model.displayName || model.resolvedModel || model.value);
126
+ }
127
+
128
+ function catalogEntry(model) {
129
+ const value = String(model.value || "");
130
+ const resolved = String(model.resolvedModel || value);
131
+ const efforts = model.supportsEffort && Array.isArray(model.supportedEffortLevels)
132
+ ? model.supportedEffortLevels
133
+ : [];
134
+ const contextWindow = Number(model.contextWindow || model.contextWindowSize || 0);
135
+ return {
136
+ id: visibleModel(resolved),
137
+ model: visibleModel(value),
138
+ displayName: compactClaudeModelName(model),
139
+ defaultReasoningEffort: efforts.includes("high") ? "high" : efforts.at(-1) || "",
140
+ supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort })),
141
+ isDefault: false,
142
+ ...(contextWindow > 0 ? { contextWindow } : {}),
143
+ };
144
+ }
145
+
146
+ async function loadModelCatalog(params) {
147
+ const cacheKey = `${params.claudePath || "claude"}\n${params.cwd || process.cwd()}`;
148
+ if (modelCatalogs.has(cacheKey)) return modelCatalogs.get(cacheKey);
149
+ const pending = (async () => {
150
+ const input = new AsyncQueue();
151
+ const options = {
152
+ cwd: params.cwd || process.cwd(),
153
+ persistSession: false,
154
+ settingSources: [],
155
+ tools: [],
156
+ env: sanitizedEnvironment(),
157
+ stderr: (data) => process.stderr.write(data),
158
+ };
159
+ if (params.claudePath) options.pathToClaudeCodeExecutable = params.claudePath;
160
+ const agentQuery = query({ prompt: input, options });
161
+ const consumer = (async () => {
162
+ try { for await (const _message of agentQuery) { /* initialization only */ } }
163
+ catch { /* the caller receives the supportedModels error */ }
164
+ })();
165
+ try {
166
+ const models = await agentQuery.supportedModels();
167
+ return {
168
+ data: models
169
+ .filter((model) => model.value && model.value !== "default")
170
+ .map(catalogEntry),
171
+ };
172
+ } finally {
173
+ input.close();
174
+ agentQuery.close();
175
+ await Promise.race([consumer, new Promise((resolve) => setTimeout(resolve, 1000))]);
176
+ }
177
+ })();
178
+ modelCatalogs.set(cacheKey, pending);
179
+ try {
180
+ return await pending;
181
+ } catch (error) {
182
+ modelCatalogs.delete(cacheKey);
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ function stripClaudeModel(model) {
188
+ if (!model || model === "claude:default") return undefined;
189
+ return model.startsWith("claude:") ? model.slice("claude:".length) : model;
190
+ }
191
+
192
+ function visibleModel(model) {
193
+ if (!model) return "claude:default";
194
+ return model.startsWith("claude:") ? model : `claude:${model}`;
195
+ }
196
+
197
+ function visibleSession(id) {
198
+ return id.startsWith("claude:") ? id : `claude:${id}`;
199
+ }
200
+
201
+ function rawSession(id) {
202
+ return id.startsWith("claude:") ? id.slice("claude:".length) : id;
203
+ }
204
+
205
+ function makeOptions(params, sessionId, resume) {
206
+ const options = {
207
+ cwd: params.cwd || process.cwd(),
208
+ includePartialMessages: true,
209
+ permissionMode: "default",
210
+ enableFileCheckpointing: true,
211
+ persistSession: true,
212
+ settingSources: ["user", "project", "local"],
213
+ skills: "all",
214
+ tools: { type: "preset", preset: "claude_code" },
215
+ systemPrompt: {
216
+ type: "preset",
217
+ preset: "claude_code",
218
+ append: params.systemPrompt || "",
219
+ },
220
+ env: sanitizedEnvironment(),
221
+ stderr: (data) => process.stderr.write(data),
222
+ };
223
+ const model = stripClaudeModel(params.model);
224
+ if (model) options.model = model;
225
+ if (params.effort) options.effort = params.effort;
226
+ if (params.claudePath) options.pathToClaudeCodeExecutable = params.claudePath;
227
+ if (resume) options.resume = resume;
228
+ else options.sessionId = sessionId;
229
+ options.canUseTool = (toolName, input, permission) =>
230
+ requestToolPermission(toolName, input, permission);
231
+ return options;
232
+ }
233
+
234
+ async function requestToolPermission(toolName, input, permission) {
235
+ if (toolName === "AskUserQuestion") {
236
+ const questions = (input.questions || []).map((question, index) => ({
237
+ id: `q${index}`,
238
+ header: question.header || "질문",
239
+ question: question.question || "입력이 필요합니다.",
240
+ options: (question.options || []).map((option) => ({
241
+ label: option.label || String(option),
242
+ description: option.description || "",
243
+ })),
244
+ isOther: true,
245
+ multiSelect: Boolean(question.multiSelect),
246
+ }));
247
+ const response = await hostRequest(
248
+ "item/tool/requestUserInput",
249
+ { questions },
250
+ permission.signal,
251
+ );
252
+ const answers = {};
253
+ for (let index = 0; index < questions.length; index += 1) {
254
+ const selected = response?.answers?.[`q${index}`]?.answers;
255
+ if (Array.isArray(selected) && selected.length) {
256
+ answers[questions[index].question] = questions[index].multiSelect
257
+ ? selected.join(", ")
258
+ : selected[0];
259
+ }
260
+ }
261
+ return { behavior: "allow", updatedInput: { ...input, answers } };
262
+ }
263
+
264
+ // Devez Vibe runs in its existing full-access profile. Claude's callback is
265
+ // still kept so AskUserQuestion and explicit user-authored ask rules reach UI.
266
+ if (!permission.matchedAskRule) {
267
+ return { behavior: "allow", updatedInput: input };
268
+ }
269
+
270
+ let method = "item/permissions/requestApproval";
271
+ let params = {
272
+ reason: permission.decisionReason || permission.description || permission.title,
273
+ permissions: { tool: toolName, blockedPath: permission.blockedPath },
274
+ };
275
+ if (toolName === "Bash") {
276
+ method = "item/commandExecution/requestApproval";
277
+ params = {
278
+ command: input.command || "command",
279
+ cwd: input.cwd,
280
+ reason: permission.decisionReason || permission.description,
281
+ };
282
+ } else if (["Edit", "Write", "NotebookEdit"].includes(toolName)) {
283
+ method = "item/fileChange/requestApproval";
284
+ params = {
285
+ grantRoot: permission.blockedPath || input.file_path || input.notebook_path,
286
+ reason: permission.decisionReason || permission.description,
287
+ };
288
+ }
289
+ const response = await hostRequest(method, params, permission.signal);
290
+ const accepted = response?.decision === "accept" || response?.decision === "acceptForSession"
291
+ || response?.scope === "turn" || response?.scope === "session";
292
+ if (!accepted) return { behavior: "deny", message: "사용자가 작업을 거부했습니다." };
293
+ return {
294
+ behavior: "allow",
295
+ updatedInput: input,
296
+ ...(response?.decision === "acceptForSession" || response?.scope === "session"
297
+ ? { updatedPermissions: permission.suggestions }
298
+ : {}),
299
+ };
300
+ }
301
+
302
+ async function createSession(params, resumeId) {
303
+ const id = resumeId || randomUUID();
304
+ const queue = new AsyncQueue();
305
+ const session = {
306
+ id,
307
+ cwd: params.cwd || process.cwd(),
308
+ model: visibleModel(params.model),
309
+ effort: params.effort || "",
310
+ models: [],
311
+ queue,
312
+ query: null,
313
+ turn: null,
314
+ turnSequence: 1,
315
+ itemSequence: 1,
316
+ streamBlocks: new Map(),
317
+ tools: new Map(),
318
+ tasks: new Map(),
319
+ };
320
+ const agentQuery = query({
321
+ prompt: queue,
322
+ options: makeOptions(params, id, resumeId),
323
+ });
324
+ session.query = agentQuery;
325
+ sessions.set(id, session);
326
+ const consumer = consume(session).catch((error) => {
327
+ notify("error", {
328
+ threadId: id,
329
+ provider: "Claude",
330
+ error: { message: error instanceof Error ? error.message : String(error) },
331
+ willRetry: false,
332
+ });
333
+ if (session.turn) finishTurn(session, error);
334
+ });
335
+ session.consumer = consumer;
336
+ const initialization = await agentQuery.initializationResult();
337
+ session.models = Array.isArray(initialization.models) ? initialization.models : [];
338
+ session.effort = supportedEffort(modelCapabilities(session.models, params.model), params.effort);
339
+ const account = initialization.account || await safeAccount(agentQuery);
340
+ const usage = await safeUsage(agentQuery);
341
+ return { session, initialization, account, usage };
342
+ }
343
+
344
+ async function safeAccount(agentQuery) {
345
+ try { return await agentQuery.accountInfo(); } catch { return null; }
346
+ }
347
+
348
+ async function safeUsage(agentQuery) {
349
+ try { return await agentQuery.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(); }
350
+ catch { return null; }
351
+ }
352
+
353
+ function nextItemId(session, suffix) {
354
+ return `claude-item-${session.itemSequence++}-${suffix}`;
355
+ }
356
+
357
+ function emitItem(session, phase, item) {
358
+ notify(`item/${phase}`, { threadId: session.id, turnId: session.turn?.id, item });
359
+ }
360
+
361
+ function emitDelta(session, method, itemId, delta) {
362
+ notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
363
+ }
364
+
365
+ function processStreamEvent(session, message) {
366
+ if (!session.turn || message.parent_tool_use_id) return;
367
+ const event = message.event || {};
368
+ if (event.type === "message_start") session.streamBlocks.clear();
369
+ if (event.type === "content_block_start") {
370
+ const block = event.content_block || {};
371
+ if (block.type !== "text" && block.type !== "thinking") return;
372
+ const id = nextItemId(session, block.type);
373
+ const item = block.type === "text"
374
+ ? { id, type: "agentMessage", text: "", provider: "Claude" }
375
+ : { id, type: "reasoning", summary: [] };
376
+ session.streamBlocks.set(event.index, { id, type: block.type, text: "" });
377
+ emitItem(session, "started", item);
378
+ return;
379
+ }
380
+ if (event.type === "content_block_delta") {
381
+ const current = session.streamBlocks.get(event.index);
382
+ if (!current) return;
383
+ const delta = event.delta?.text || event.delta?.thinking || "";
384
+ if (!delta) return;
385
+ current.text += delta;
386
+ emitDelta(
387
+ session,
388
+ current.type === "text" ? "item/agentMessage/delta" : "item/reasoning/summaryTextDelta",
389
+ current.id,
390
+ delta,
391
+ );
392
+ return;
393
+ }
394
+ if (event.type === "content_block_stop") {
395
+ const current = session.streamBlocks.get(event.index);
396
+ if (!current) return;
397
+ const item = current.type === "text"
398
+ ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
399
+ : { id: current.id, type: "reasoning", summary: [current.text] };
400
+ emitItem(session, "completed", item);
401
+ session.streamBlocks.delete(event.index);
402
+ }
403
+ }
404
+
405
+ function processAssistant(session, message) {
406
+ if (!session.turn || message.parent_tool_use_id) return;
407
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
408
+ for (const block of content) {
409
+ if (block.type === "tool_use") processToolUse(session, block);
410
+ }
411
+ if (!session.streamBlocks.size && !session.turn.sawStreamText) {
412
+ for (const block of content) {
413
+ if (block.type !== "text" && block.type !== "thinking") continue;
414
+ const id = nextItemId(session, block.type);
415
+ const item = block.type === "text"
416
+ ? { id, type: "agentMessage", text: block.text || "", provider: "Claude" }
417
+ : { id, type: "reasoning", summary: [block.thinking || ""] };
418
+ emitItem(session, "started", item);
419
+ emitItem(session, "completed", item);
420
+ }
421
+ }
422
+ }
423
+
424
+ function processToolUse(session, block) {
425
+ const name = block.name || "Tool";
426
+ const input = block.input || {};
427
+ if (name === "TaskCreate" || name === "TaskUpdate" || name === "TaskList") {
428
+ updatePlanFromToolUse(session, name, block.id, input);
429
+ session.tools.set(block.id, { name, input, suppressed: true });
430
+ return;
431
+ }
432
+ if (name === "AskUserQuestion") {
433
+ session.tools.set(block.id, { name, input, suppressed: true });
434
+ return;
435
+ }
436
+ const item = toolItem(session, block.id, name, input);
437
+ session.tools.set(block.id, { name, input, item });
438
+ emitItem(session, "started", item);
439
+ }
440
+
441
+ function toolItem(session, id, name, input) {
442
+ if (name === "Bash") {
443
+ return { id, type: "commandExecution", command: input.command || "command", status: "inProgress", aggregatedOutput: "" };
444
+ }
445
+ if (["Edit", "Write", "NotebookEdit"].includes(name)) {
446
+ return { id, type: "fileChange", status: "inProgress", changes: fileChanges(name, input) };
447
+ }
448
+ if (name === "WebSearch") {
449
+ return { id, type: "webSearch", query: input.query || "" };
450
+ }
451
+ if (name === "Agent" || name === "Task") {
452
+ return { id, type: "collabAgentToolCall", tool: { name, arguments: input } };
453
+ }
454
+ if (name.startsWith("mcp__")) {
455
+ const [, server = "server", ...tool] = name.split("__");
456
+ return { id, type: "mcpToolCall", server, tool: tool.join("__") || name, arguments: input };
457
+ }
458
+ return { id, type: "dynamicToolCall", tool: name, arguments: input };
459
+ }
460
+
461
+ function fileChanges(name, input) {
462
+ const path = input.file_path || input.notebook_path || "unknown";
463
+ if (name === "Edit") {
464
+ const before = String(input.old_string || "").split("\n").map((line) => `-${line}`).join("\n");
465
+ const after = String(input.new_string || "").split("\n").map((line) => `+${line}`).join("\n");
466
+ return [{ path, kind: { type: "update" }, diff: `@@ -1 +1 @@\n${before}\n${after}` }];
467
+ }
468
+ const content = String(input.content || input.new_source || input.new_cell_source || "");
469
+ const additions = content.split("\n").map((line) => `+${line}`).join("\n");
470
+ return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
471
+ }
472
+
473
+ function updatePlanFromToolUse(session, name, toolUseId, input) {
474
+ if (name === "TaskCreate") {
475
+ session.tasks.set(`pending:${toolUseId}`, {
476
+ id: `pending:${toolUseId}`,
477
+ subject: input.subject || input.description || "작업",
478
+ status: "pending",
479
+ });
480
+ } else if (name === "TaskUpdate") {
481
+ const task = session.tasks.get(String(input.taskId));
482
+ if (task) {
483
+ if (input.subject) task.subject = input.subject;
484
+ if (input.status) task.status = input.status;
485
+ }
486
+ }
487
+ emitPlan(session);
488
+ }
489
+
490
+ function updatePlanFromToolResult(session, pending, message) {
491
+ const value = message.tool_use_result;
492
+ if (pending.name === "TaskCreate") {
493
+ const temporary = session.tasks.get(`pending:${pending.toolUseId}`);
494
+ const created = taskCreatedResult(value, message.message?.content);
495
+ if (temporary && created?.id) {
496
+ session.tasks.delete(temporary.id);
497
+ temporary.id = String(created.id);
498
+ temporary.subject = created.subject || temporary.subject;
499
+ temporary.status = created.status || temporary.status;
500
+ session.tasks.set(temporary.id, temporary);
501
+ }
502
+ } else if (pending.name === "TaskList" && Array.isArray(value?.tasks || value)) {
503
+ session.tasks.clear();
504
+ for (const task of value.tasks || value) {
505
+ session.tasks.set(String(task.id), {
506
+ id: String(task.id),
507
+ subject: task.subject || task.description || "작업",
508
+ status: task.status || "pending",
509
+ });
510
+ }
511
+ emitPlan(session);
512
+ }
513
+ }
514
+
515
+ function taskCreatedResult(structured, content) {
516
+ const created = structured?.task || structured;
517
+ if (created?.id) return created;
518
+ const blocks = Array.isArray(content) ? content : [];
519
+ const text = blocks.map((block) => toolOutput(block.content)).join("\n");
520
+ const match = text.match(/Task #(\S+) created successfully(?::\s*(.+))?/i);
521
+ return match ? { id: match[1], subject: match[2] } : null;
522
+ }
523
+
524
+ function planStatus(status) {
525
+ if (status === "completed") return "completed";
526
+ if (status === "in_progress" || status === "running") return "inProgress";
527
+ return "pending";
528
+ }
529
+
530
+ function emitPlan(session) {
531
+ notify("turn/plan/updated", {
532
+ threadId: session.id,
533
+ turnId: session.turn?.id,
534
+ plan: [...session.tasks.values()].map((task, index) => ({
535
+ step: numberedTaskSubject(task.subject, index),
536
+ status: planStatus(task.status),
537
+ })),
538
+ });
539
+ }
540
+
541
+ function numberedTaskSubject(subject, index) {
542
+ const text = String(subject || "작업").trim();
543
+ return /^\d+\.\s/.test(text) ? text : `${index + 1}. ${text}`;
544
+ }
545
+
546
+ function processUser(session, message) {
547
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
548
+ for (const block of content) {
549
+ if (block.type !== "tool_result") continue;
550
+ const pending = session.tools.get(block.tool_use_id);
551
+ if (!pending) continue;
552
+ pending.toolUseId = block.tool_use_id;
553
+ if (pending.suppressed) {
554
+ updatePlanFromToolResult(session, pending, message);
555
+ session.tools.delete(block.tool_use_id);
556
+ continue;
557
+ }
558
+ const output = toolOutput(block.content, message.tool_use_result);
559
+ const completed = { ...pending.item };
560
+ if (completed.type === "commandExecution") {
561
+ completed.status = block.is_error ? "failed" : "completed";
562
+ completed.aggregatedOutput = output;
563
+ const exitCode = message.tool_use_result?.exitCode ?? message.tool_use_result?.exit_code;
564
+ if (Number.isInteger(exitCode)) completed.exitCode = exitCode;
565
+ } else if (completed.type === "mcpToolCall") {
566
+ if (block.is_error) completed.error = output;
567
+ else completed.result = message.tool_use_result ?? output;
568
+ } else if (completed.type === "dynamicToolCall") {
569
+ completed.contentItems = message.tool_use_result ?? [{ type: "text", text: output }];
570
+ } else {
571
+ completed.result = message.tool_use_result ?? output;
572
+ }
573
+ emitItem(session, "completed", completed);
574
+ session.tools.delete(block.tool_use_id);
575
+ }
576
+ }
577
+
578
+ function toolOutput(content, structured) {
579
+ if (typeof content === "string") return content;
580
+ if (Array.isArray(content)) {
581
+ return content.map((part) => typeof part === "string" ? part : part.text || JSON.stringify(part)).join("\n");
582
+ }
583
+ if (structured != null) return typeof structured === "string" ? structured : JSON.stringify(structured, null, 2);
584
+ return content == null ? "" : JSON.stringify(content, null, 2);
585
+ }
586
+
587
+ async function processResult(session, message) {
588
+ if (!session.turn) return;
589
+ const interrupted = session.turn.interruptRequested === true;
590
+ const totals = [...Object.values(message.modelUsage || {})].reduce((sum, usage) => ({
591
+ inputTokens: sum.inputTokens + Number(usage.inputTokens || 0) + Number(usage.cacheReadInputTokens || 0) + Number(usage.cacheCreationInputTokens || 0),
592
+ cachedInputTokens: sum.cachedInputTokens + Number(usage.cacheReadInputTokens || 0),
593
+ cacheWriteInputTokens: sum.cacheWriteInputTokens + Number(usage.cacheCreationInputTokens || 0),
594
+ outputTokens: sum.outputTokens + Number(usage.outputTokens || 0),
595
+ totalTokens: sum.totalTokens + Number(usage.inputTokens || 0) + Number(usage.cacheReadInputTokens || 0) + Number(usage.cacheCreationInputTokens || 0) + Number(usage.outputTokens || 0),
596
+ contextWindow: Math.max(sum.contextWindow, Number(usage.contextWindow || 0)),
597
+ }), { inputTokens: 0, cachedInputTokens: 0, cacheWriteInputTokens: 0, outputTokens: 0, totalTokens: 0, contextWindow: 0 });
598
+ notify("thread/tokenUsage/updated", {
599
+ threadId: session.id,
600
+ tokenUsage: {
601
+ total: totals,
602
+ last: totals,
603
+ modelContextWindow: totals.contextWindow || undefined,
604
+ },
605
+ });
606
+ const error = message.is_error && !interrupted
607
+ ? { message: message.errors?.join("\n") || message.stop_reason || "Claude 실행 실패" }
608
+ : null;
609
+ finishTurn(session, error, message.duration_ms);
610
+ notify("claude/account/updated", {
611
+ threadId: session.id,
612
+ account: await safeAccount(session.query),
613
+ usage: await safeUsage(session.query),
614
+ });
615
+ }
616
+
617
+ function finishTurn(session, error, durationMs) {
618
+ if (!session.turn) return;
619
+ const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
620
+ if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
621
+ if (durationMs != null) turn.durationMs = durationMs;
622
+ notify("turn/completed", { threadId: session.id, turn });
623
+ session.turn = null;
624
+ session.streamBlocks.clear();
625
+ }
626
+
627
+ async function consume(session) {
628
+ for await (const message of session.query) {
629
+ if (message.type === "stream_event") {
630
+ if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
631
+ if (session.turn) session.turn.sawStreamText = true;
632
+ }
633
+ processStreamEvent(session, message);
634
+ } else if (message.type === "assistant") processAssistant(session, message);
635
+ else if (message.type === "user") processUser(session, message);
636
+ else if (message.type === "result") await processResult(session, message);
637
+ else if (message.type === "system" && message.subtype === "compact_boundary") {
638
+ notify("thread/compacted", { threadId: session.id });
639
+ } else if (message.type === "rate_limit_event") {
640
+ notify("claude/account/updated", { threadId: session.id, rateLimitInfo: message.rate_limit_info });
641
+ } else if (message.type === "system" && message.subtype === "api_retry") {
642
+ notify("warning", { threadId: session.id, provider: "Claude", message: `Claude API 재시도 ${message.attempt}/${message.max_retries}` });
643
+ }
644
+ }
645
+ }
646
+
647
+ async function inputContent(input) {
648
+ const content = [];
649
+ for (const item of Array.isArray(input) ? input : []) {
650
+ if (item.type === "text") content.push({ type: "text", text: item.text || "" });
651
+ else if (item.type === "localImage" && item.path) {
652
+ const bytes = await readFile(item.path);
653
+ const extension = item.path.split(".").pop()?.toLowerCase();
654
+ const mediaType = extension === "png" ? "image/png"
655
+ : extension === "gif" ? "image/gif"
656
+ : extension === "webp" ? "image/webp"
657
+ : "image/jpeg";
658
+ content.push({ type: "image", source: { type: "base64", media_type: mediaType, data: bytes.toString("base64") } });
659
+ }
660
+ }
661
+ return content.length ? content : [{ type: "text", text: "" }];
662
+ }
663
+
664
+ async function startPrompt(params) {
665
+ const id = rawSession(params.sessionId);
666
+ const session = sessions.get(id);
667
+ if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
668
+ if (session.turn) throw new Error("Claude가 이전 작업을 아직 실행 중입니다.");
669
+ if (params.model) {
670
+ const model = stripClaudeModel(params.model);
671
+ await session.query.setModel(model);
672
+ session.model = visibleModel(params.model);
673
+ }
674
+ const effort = supportedEffort(modelCapabilities(session.models, params.model || session.model), params.effort);
675
+ if (effort) {
676
+ await session.query.applyFlagSettings({ effortLevel: effort });
677
+ }
678
+ session.effort = effort;
679
+ const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
680
+ session.turn = { id: turnId, sawStreamText: false };
681
+ notify("turn/started", { threadId: id, turn: { id: turnId } });
682
+ session.queue.push({
683
+ type: "user",
684
+ message: { role: "user", content: await inputContent(params.input) },
685
+ parent_tool_use_id: null,
686
+ session_id: id,
687
+ origin: { kind: "human" },
688
+ });
689
+ return { turn: { id: turnId } };
690
+ }
691
+
692
+ function contentBlocks(message) {
693
+ const content = message?.content;
694
+ if (typeof content === "string") return [{ type: "text", text: content }];
695
+ return Array.isArray(content) ? content : [];
696
+ }
697
+
698
+ function historyTurns(messages) {
699
+ const turns = [];
700
+ let turn = null;
701
+ const tools = new Map();
702
+ const tasks = new Map();
703
+ for (const message of messages) {
704
+ const blocks = contentBlocks(message.message);
705
+ const userText = message.type === "user"
706
+ ? blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")
707
+ : "";
708
+ if (userText && !blocks.some((block) => block.type === "tool_result")) {
709
+ turn = {
710
+ id: `claude-turn-${message.uuid}`,
711
+ status: "completed",
712
+ items: [{ id: `claude-user-${message.uuid}`, type: "userMessage", content: [{ type: "text", text: userText }] }],
713
+ };
714
+ turns.push(turn);
715
+ }
716
+ if (!turn) continue;
717
+ if (message.type === "assistant") {
718
+ for (const block of blocks) {
719
+ if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
720
+ else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
721
+ else if (block.type === "tool_use") {
722
+ const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
723
+ tools.set(block.id, pending);
724
+ if (block.name === "TaskCreate") tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending" });
725
+ else if (block.name === "TaskUpdate") {
726
+ const task = tasks.get(String(block.input?.taskId));
727
+ if (task) Object.assign(task, block.input?.subject ? { subject: block.input.subject } : {}, block.input?.status ? { status: block.input.status } : {});
728
+ } else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
729
+ }
730
+ }
731
+ } else if (message.type === "user") {
732
+ for (const block of blocks.filter((candidate) => candidate.type === "tool_result")) {
733
+ const pending = tools.get(block.tool_use_id);
734
+ if (!pending) continue;
735
+ if (pending.name === "TaskCreate") {
736
+ const temporary = tasks.get(`pending:${block.tool_use_id}`);
737
+ const created = taskCreatedResult(message.tool_use_result, [block]);
738
+ if (temporary && created?.id) {
739
+ tasks.delete(temporary.id);
740
+ temporary.id = String(created.id);
741
+ temporary.subject = created.subject || temporary.subject;
742
+ tasks.set(temporary.id, temporary);
743
+ }
744
+ } else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
745
+ tasks.clear();
746
+ for (const task of message.tool_use_result.tasks) tasks.set(String(task.id), { id: String(task.id), subject: task.subject || "작업", status: task.status || "pending" });
747
+ } else if (pending.item) {
748
+ const output = toolOutput(block.content, message.tool_use_result);
749
+ Object.assign(pending.item, pending.item.type === "commandExecution"
750
+ ? { status: block.is_error ? "failed" : "completed", aggregatedOutput: output }
751
+ : pending.item.type === "mcpToolCall" ? { result: message.tool_use_result ?? output }
752
+ : pending.item.type === "dynamicToolCall" ? { contentItems: message.tool_use_result ?? [{ type: "text", text: output }] }
753
+ : { result: message.tool_use_result ?? output });
754
+ }
755
+ }
756
+ }
757
+ }
758
+ if (turn && tasks.size) {
759
+ const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "✓" : task.status === "in_progress" ? "▸" : "□"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
760
+ turn.items.push({ id: "claude-plan-latest", type: "plan", text });
761
+ }
762
+ return turns;
763
+ }
764
+
765
+ async function dispatch(method, params = {}) {
766
+ if (method === "model/list") return loadModelCatalog(params);
767
+ if (method === "session/start") {
768
+ const { session, account, usage } = await createSession(params);
769
+ return {
770
+ id: session.id,
771
+ thread: { id: session.id, turns: [] },
772
+ cwd: session.cwd,
773
+ model: session.model,
774
+ reasoningEffort: session.effort,
775
+ account,
776
+ usage,
777
+ };
778
+ }
779
+ if (method === "session/resume") {
780
+ const id = rawSession(params.sessionId);
781
+ const existing = sessions.get(id);
782
+ if (existing) {
783
+ return {
784
+ id,
785
+ thread: { id, turns: [] },
786
+ initialTurnsPage: { data: [], nextCursor: null },
787
+ cwd: existing.cwd,
788
+ model: existing.model,
789
+ reasoningEffort: existing.effort,
790
+ account: await safeAccount(existing.query),
791
+ usage: await safeUsage(existing.query),
792
+ };
793
+ }
794
+ const info = await getSessionInfo(id, { dir: params.cwd });
795
+ if (!info) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
796
+ const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
797
+ const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
798
+ const { session, account, usage } = await createSession({ ...params, cwd: info.cwd || params.cwd, model: params.model || lastModel }, id);
799
+ return {
800
+ id,
801
+ thread: { id, turns: [] },
802
+ initialTurnsPage: { data: [], nextCursor: null },
803
+ cwd: session.cwd,
804
+ model: session.model,
805
+ reasoningEffort: session.effort,
806
+ account,
807
+ usage,
808
+ };
809
+ }
810
+ if (method === "session/list") {
811
+ const found = await listSessions({
812
+ dir: params.cwd,
813
+ limit: params.limit || 100,
814
+ offset: params.offset || 0,
815
+ includeProgrammatic: true,
816
+ });
817
+ return {
818
+ data: found.map((session) => ({
819
+ id: visibleSession(session.sessionId),
820
+ name: session.customTitle || undefined,
821
+ preview: session.summary || session.firstPrompt || "Untitled Claude session",
822
+ cwd: session.cwd || params.cwd || "",
823
+ updatedAt: Math.floor((session.lastModified || 0) / 1000),
824
+ })),
825
+ nextCursor: null,
826
+ };
827
+ }
828
+ if (method === "session/history") {
829
+ const id = rawSession(params.sessionId);
830
+ const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
831
+ return { data: historyTurns(messages), nextCursor: null };
832
+ }
833
+ if (method === "session/prompt") return startPrompt(params);
834
+ if (method === "session/interrupt") {
835
+ const session = sessions.get(rawSession(params.sessionId));
836
+ if (session?.turn) {
837
+ const turn = session.turn;
838
+ turn.interruptRequested = true;
839
+ try {
840
+ await session.query.interrupt();
841
+ } catch (error) {
842
+ if (session.turn === turn) delete turn.interruptRequested;
843
+ throw error;
844
+ }
845
+ }
846
+ return {};
847
+ }
848
+ if (method === "session/compact") {
849
+ return startPrompt({ ...params, input: [{ type: "text", text: "/compact" }] });
850
+ }
851
+ if (method === "session/fork") {
852
+ const source = rawSession(params.sessionId);
853
+ const forked = await forkSession(source, { dir: params.cwd });
854
+ const id = forked.sessionId || forked;
855
+ const { session, account, usage } = await createSession(params, id);
856
+ return {
857
+ id,
858
+ thread: { id, turns: [] },
859
+ cwd: session.cwd,
860
+ model: session.model,
861
+ reasoningEffort: session.effort,
862
+ account,
863
+ usage,
864
+ };
865
+ }
866
+ if (method === "session/close") {
867
+ const session = sessions.get(rawSession(params.sessionId));
868
+ if (session) {
869
+ session.queue.close();
870
+ session.query.close();
871
+ sessions.delete(session.id);
872
+ if (params.delete) await deleteSession(session.id, { dir: session.cwd });
873
+ }
874
+ return {};
875
+ }
876
+ if (method === "account/usage") {
877
+ const session = sessions.get(rawSession(params.sessionId)) || [...sessions.values()][0];
878
+ if (!session) return { account: null, usage: null };
879
+ return { account: await safeAccount(session.query), usage: await safeUsage(session.query) };
880
+ }
881
+ if (method === "shutdown") {
882
+ for (const session of sessions.values()) {
883
+ session.queue.close();
884
+ session.query.close();
885
+ }
886
+ sessions.clear();
887
+ return {};
888
+ }
889
+ throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
890
+ }
891
+
892
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
893
+ lines.on("line", async (line) => {
894
+ if (!line.trim()) return;
895
+ let message;
896
+ try { message = JSON.parse(line); }
897
+ catch (error) {
898
+ write({ method: "warning", params: { provider: "Claude", message: `Claude 브리지 JSON 해석 실패: ${error.message}` } });
899
+ return;
900
+ }
901
+ if (typeof message.id === "string" && ("result" in message || "error" in message)) {
902
+ const pending = pendingHostRequests.get(message.id);
903
+ if (pending) {
904
+ pendingHostRequests.delete(message.id);
905
+ pending.resolve(message.result ?? { decision: "decline" });
906
+ }
907
+ return;
908
+ }
909
+ if (typeof message.id !== "number" || typeof message.method !== "string") return;
910
+ try { write({ id: message.id, result: await dispatch(message.method, message.params) }); }
911
+ catch (error) { write({ id: message.id, error: rpcError(error) }); }
912
+ });
913
+
914
+ lines.on("close", () => {
915
+ for (const session of sessions.values()) session.query.close();
916
+ });
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "0.1.41",
4
- "description": "Stable terminal UI for the official Codex app-server",
3
+ "version": "0.1.43",
4
+ "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",
7
7
  "cli",
8
8
  "tui",
9
9
  "terminal",
10
- "app-server"
10
+ "app-server",
11
+ "claude-agent-sdk"
11
12
  ],
12
13
  "homepage": "https://github.com/MrHoje/Devez-vibe#readme",
13
14
  "bugs": "https://github.com/MrHoje/Devez-vibe/issues",
@@ -21,6 +22,7 @@
21
22
  },
22
23
  "files": [
23
24
  "bin/dvz.exe",
25
+ "bridge/claude-agent-sdk-bridge.mjs",
24
26
  "README.md",
25
27
  "LICENSE"
26
28
  ],
@@ -32,5 +34,8 @@
32
34
  ],
33
35
  "engines": {
34
36
  "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "@anthropic-ai/claude-agent-sdk": "0.3.223"
35
40
  }
36
41
  }