devez-vibe 0.1.46 → 0.1.47
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/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +294 -97
- package/package.json +41 -41
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
deleteSession,
|
|
8
8
|
forkSession,
|
|
9
9
|
getSessionInfo,
|
|
10
|
-
getSessionMessages,
|
|
11
|
-
listSessions,
|
|
12
|
-
startup,
|
|
13
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
10
|
+
getSessionMessages,
|
|
11
|
+
listSessions,
|
|
12
|
+
startup,
|
|
13
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
14
14
|
|
|
15
15
|
const VERSION = process.env.DEVEZ_VIBE_VERSION || "dev";
|
|
16
16
|
const sessions = new Map();
|
|
@@ -82,23 +82,23 @@ function hostRequest(method, params, signal) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function sanitizedEnvironment() {
|
|
85
|
+
function sanitizedEnvironment() {
|
|
86
86
|
const env = { ...process.env };
|
|
87
87
|
delete env.ANTHROPIC_API_KEY;
|
|
88
88
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
89
89
|
env.CLAUDE_AGENT_SDK_CLIENT_APP = `devez-vibe/${VERSION}`;
|
|
90
|
-
return env;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function applyClaudeExecutable(options, params) {
|
|
94
|
-
const executable = String(params.claudePath || "").trim();
|
|
95
|
-
if (!executable) return;
|
|
96
|
-
// On Windows use the SDK's matching native CLI bundle. It reads the same
|
|
97
|
-
// ~/.claude credentials, settings, hooks, skills, and plugins, while avoiding
|
|
98
|
-
// EINVAL from command shims and incompatible separately-installed CLI builds.
|
|
99
|
-
if (process.platform === "win32") return;
|
|
100
|
-
options.pathToClaudeCodeExecutable = executable;
|
|
101
|
-
}
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function applyClaudeExecutable(options, params) {
|
|
94
|
+
const executable = String(params.claudePath || "").trim();
|
|
95
|
+
if (!executable) return;
|
|
96
|
+
// On Windows use the SDK's matching native CLI bundle. It reads the same
|
|
97
|
+
// ~/.claude credentials, settings, hooks, skills, and plugins, while avoiding
|
|
98
|
+
// EINVAL from command shims and incompatible separately-installed CLI builds.
|
|
99
|
+
if (process.platform === "win32") return;
|
|
100
|
+
options.pathToClaudeCodeExecutable = executable;
|
|
101
|
+
}
|
|
102
102
|
|
|
103
103
|
function modelCapabilities(models, model) {
|
|
104
104
|
const value = stripClaudeModel(model);
|
|
@@ -135,7 +135,7 @@ function compactClaudeModelName(model) {
|
|
|
135
135
|
return fallback || clean(model.displayName || model.resolvedModel || model.value);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function catalogEntry(model, defaultResolvedModel) {
|
|
138
|
+
function catalogEntry(model, defaultResolvedModel) {
|
|
139
139
|
const value = String(model.value || "");
|
|
140
140
|
const resolved = String(model.resolvedModel || value);
|
|
141
141
|
const efforts = model.supportsEffort && Array.isArray(model.supportedEffortLevels)
|
|
@@ -148,7 +148,7 @@ function catalogEntry(model, defaultResolvedModel) {
|
|
|
148
148
|
displayName: compactClaudeModelName(model),
|
|
149
149
|
defaultReasoningEffort: efforts.includes("high") ? "high" : efforts.at(-1) || "",
|
|
150
150
|
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort })),
|
|
151
|
-
isDefault: Boolean(defaultResolvedModel) && resolved === defaultResolvedModel,
|
|
151
|
+
isDefault: Boolean(defaultResolvedModel) && resolved === defaultResolvedModel,
|
|
152
152
|
...(contextWindow > 0 ? { contextWindow } : {}),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
@@ -166,21 +166,21 @@ async function loadModelCatalog(params) {
|
|
|
166
166
|
env: sanitizedEnvironment(),
|
|
167
167
|
stderr: (data) => process.stderr.write(data),
|
|
168
168
|
};
|
|
169
|
-
applyClaudeExecutable(options, params);
|
|
170
|
-
const agentQuery = await startAgentQuery(input, options);
|
|
169
|
+
applyClaudeExecutable(options, params);
|
|
170
|
+
const agentQuery = await startAgentQuery(input, options);
|
|
171
171
|
const consumer = (async () => {
|
|
172
172
|
try { for await (const _message of agentQuery) { /* initialization only */ } }
|
|
173
173
|
catch { /* the caller receives the supportedModels error */ }
|
|
174
174
|
})();
|
|
175
175
|
try {
|
|
176
|
-
const models = await agentQuery.supportedModels();
|
|
177
|
-
const defaultResolvedModel = String(
|
|
178
|
-
models.find((model) => model.value === "default")?.resolvedModel || "",
|
|
179
|
-
);
|
|
180
|
-
return {
|
|
181
|
-
data: models
|
|
182
|
-
.filter((model) => model.value && model.value !== "default")
|
|
183
|
-
.map((model) => catalogEntry(model, defaultResolvedModel)),
|
|
176
|
+
const models = await agentQuery.supportedModels();
|
|
177
|
+
const defaultResolvedModel = String(
|
|
178
|
+
models.find((model) => model.value === "default")?.resolvedModel || "",
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
data: models
|
|
182
|
+
.filter((model) => model.value && model.value !== "default")
|
|
183
|
+
.map((model) => catalogEntry(model, defaultResolvedModel)),
|
|
184
184
|
};
|
|
185
185
|
} finally {
|
|
186
186
|
input.close();
|
|
@@ -215,7 +215,7 @@ function rawSession(id) {
|
|
|
215
215
|
return id.startsWith("claude:") ? id.slice("claude:".length) : id;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function makeOptions(params, sessionId, resume) {
|
|
218
|
+
function makeOptions(params, sessionId, resume) {
|
|
219
219
|
const options = {
|
|
220
220
|
cwd: params.cwd || process.cwd(),
|
|
221
221
|
includePartialMessages: true,
|
|
@@ -236,23 +236,23 @@ function makeOptions(params, sessionId, resume) {
|
|
|
236
236
|
const model = stripClaudeModel(params.model);
|
|
237
237
|
if (model) options.model = model;
|
|
238
238
|
if (params.effort) options.effort = params.effort;
|
|
239
|
-
applyClaudeExecutable(options, params);
|
|
239
|
+
applyClaudeExecutable(options, params);
|
|
240
240
|
if (resume) options.resume = resume;
|
|
241
241
|
else options.sessionId = sessionId;
|
|
242
242
|
options.canUseTool = (toolName, input, permission) =>
|
|
243
243
|
requestToolPermission(toolName, input, permission);
|
|
244
|
-
return options;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function startAgentQuery(prompt, options) {
|
|
248
|
-
const warm = await startup({ options });
|
|
249
|
-
try {
|
|
250
|
-
return warm.query(prompt);
|
|
251
|
-
} catch (error) {
|
|
252
|
-
warm.close();
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
244
|
+
return options;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function startAgentQuery(prompt, options) {
|
|
248
|
+
const warm = await startup({ options });
|
|
249
|
+
try {
|
|
250
|
+
return warm.query(prompt);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
warm.close();
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
256
|
|
|
257
257
|
async function requestToolPermission(toolName, input, permission) {
|
|
258
258
|
if (toolName === "AskUserQuestion") {
|
|
@@ -334,13 +334,17 @@ async function createSession(params, resumeId) {
|
|
|
334
334
|
queue,
|
|
335
335
|
query: null,
|
|
336
336
|
turn: null,
|
|
337
|
+
// Prompts that arrived while a turn was running, run in order afterwards.
|
|
338
|
+
pendingPrompts: [],
|
|
337
339
|
turnSequence: 1,
|
|
338
340
|
itemSequence: 1,
|
|
339
341
|
streamBlocks: new Map(),
|
|
340
342
|
tools: new Map(),
|
|
341
343
|
tasks: new Map(),
|
|
344
|
+
lastContextUsage: null,
|
|
345
|
+
lastContextWindow: 0,
|
|
342
346
|
};
|
|
343
|
-
const agentQuery = await startAgentQuery(queue, makeOptions(params, id, resumeId));
|
|
347
|
+
const agentQuery = await startAgentQuery(queue, makeOptions(params, id, resumeId));
|
|
344
348
|
session.query = agentQuery;
|
|
345
349
|
sessions.set(id, session);
|
|
346
350
|
const consumer = consume(session).catch((error) => {
|
|
@@ -353,16 +357,16 @@ async function createSession(params, resumeId) {
|
|
|
353
357
|
if (session.turn) finishTurn(session, error);
|
|
354
358
|
});
|
|
355
359
|
session.consumer = consumer;
|
|
356
|
-
let initialization;
|
|
357
|
-
try {
|
|
358
|
-
initialization = await agentQuery.initializationResult();
|
|
359
|
-
} catch (error) {
|
|
360
|
-
sessions.delete(id);
|
|
361
|
-
queue.close();
|
|
362
|
-
agentQuery.close();
|
|
363
|
-
await Promise.race([consumer, new Promise((resolve) => setTimeout(resolve, 1000))]);
|
|
364
|
-
throw error;
|
|
365
|
-
}
|
|
360
|
+
let initialization;
|
|
361
|
+
try {
|
|
362
|
+
initialization = await agentQuery.initializationResult();
|
|
363
|
+
} catch (error) {
|
|
364
|
+
sessions.delete(id);
|
|
365
|
+
queue.close();
|
|
366
|
+
agentQuery.close();
|
|
367
|
+
await Promise.race([consumer, new Promise((resolve) => setTimeout(resolve, 1000))]);
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
366
370
|
session.models = Array.isArray(initialization.models) ? initialization.models : [];
|
|
367
371
|
session.effort = supportedEffort(modelCapabilities(session.models, params.model), params.effort);
|
|
368
372
|
const account = initialization.account || await safeAccount(agentQuery);
|
|
@@ -391,6 +395,52 @@ function emitDelta(session, method, itemId, delta) {
|
|
|
391
395
|
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
392
396
|
}
|
|
393
397
|
|
|
398
|
+
function tokenBreakdown(usage) {
|
|
399
|
+
if (!usage) return null;
|
|
400
|
+
const input = Number(usage.input_tokens ?? usage.inputTokens ?? 0);
|
|
401
|
+
const cached = Number(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? 0);
|
|
402
|
+
const cacheWrite = Number(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens ?? 0);
|
|
403
|
+
const output = Number(usage.output_tokens ?? usage.outputTokens ?? 0);
|
|
404
|
+
return {
|
|
405
|
+
inputTokens: input + cached + cacheWrite,
|
|
406
|
+
cachedInputTokens: cached,
|
|
407
|
+
cacheWriteInputTokens: cacheWrite,
|
|
408
|
+
outputTokens: output,
|
|
409
|
+
totalTokens: input + cached + cacheWrite + output,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// A resumed session has no turn yet, so the status line would show no context
|
|
414
|
+
// until the next result. Rebuild both figures from the stored transcript.
|
|
415
|
+
function historyTokenUsage(messages, models, model) {
|
|
416
|
+
const total = {
|
|
417
|
+
inputTokens: 0,
|
|
418
|
+
cachedInputTokens: 0,
|
|
419
|
+
cacheWriteInputTokens: 0,
|
|
420
|
+
outputTokens: 0,
|
|
421
|
+
totalTokens: 0,
|
|
422
|
+
};
|
|
423
|
+
let last = null;
|
|
424
|
+
let counted = false;
|
|
425
|
+
for (const message of messages) {
|
|
426
|
+
if (message.type !== "assistant" || message.message?.model === "<synthetic>") continue;
|
|
427
|
+
const breakdown = tokenBreakdown(message.message?.usage);
|
|
428
|
+
if (!breakdown) continue;
|
|
429
|
+
counted = true;
|
|
430
|
+
for (const key of Object.keys(total)) total[key] += breakdown[key];
|
|
431
|
+
// Only the main thread occupies the context window; subagents run their own.
|
|
432
|
+
if (!message.parent_tool_use_id) last = breakdown;
|
|
433
|
+
}
|
|
434
|
+
if (!counted) return null;
|
|
435
|
+
const capabilities = modelCapabilities(models, model);
|
|
436
|
+
const contextWindow = Number(capabilities?.contextWindow || capabilities?.contextWindowSize || 0);
|
|
437
|
+
return {
|
|
438
|
+
total,
|
|
439
|
+
...(last ? { last } : {}),
|
|
440
|
+
...(contextWindow > 0 ? { modelContextWindow: contextWindow } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
394
444
|
function processStreamEvent(session, message) {
|
|
395
445
|
if (!session.turn || message.parent_tool_use_id) return;
|
|
396
446
|
const event = message.event || {};
|
|
@@ -433,6 +483,14 @@ function processStreamEvent(session, message) {
|
|
|
433
483
|
|
|
434
484
|
function processAssistant(session, message) {
|
|
435
485
|
if (!session.turn || message.parent_tool_use_id) return;
|
|
486
|
+
session.lastContextUsage = tokenBreakdown(message.message?.usage);
|
|
487
|
+
const capabilities = modelCapabilities(
|
|
488
|
+
session.models,
|
|
489
|
+
message.message?.model || session.model,
|
|
490
|
+
);
|
|
491
|
+
session.lastContextWindow = Number(
|
|
492
|
+
capabilities?.contextWindow || capabilities?.contextWindowSize || 0,
|
|
493
|
+
);
|
|
436
494
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
437
495
|
for (const block of content) {
|
|
438
496
|
if (block.type === "tool_use") processToolUse(session, block);
|
|
@@ -499,18 +557,30 @@ function fileChanges(name, input) {
|
|
|
499
557
|
return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
|
|
500
558
|
}
|
|
501
559
|
|
|
560
|
+
// Claude는 Codex의 update_plan처럼 계획 전체를 다시 보내지 않으므로, 새 계획이 시작될 때
|
|
561
|
+
// 이전 턴에서 이미 끝난 작업을 직접 걷어내야 목록이 턴마다 쌓이지 않는다.
|
|
562
|
+
function pruneFinishedTasks(tasks, turnId) {
|
|
563
|
+
for (const [key, task] of tasks) {
|
|
564
|
+
if (task.status === "completed" && task.turnId !== turnId) tasks.delete(key);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
502
568
|
function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
569
|
+
const turnId = session.turn?.id;
|
|
503
570
|
if (name === "TaskCreate") {
|
|
571
|
+
pruneFinishedTasks(session.tasks, turnId);
|
|
504
572
|
session.tasks.set(`pending:${toolUseId}`, {
|
|
505
573
|
id: `pending:${toolUseId}`,
|
|
506
574
|
subject: input.subject || input.description || "작업",
|
|
507
575
|
status: "pending",
|
|
576
|
+
turnId,
|
|
508
577
|
});
|
|
509
578
|
} else if (name === "TaskUpdate") {
|
|
510
579
|
const task = session.tasks.get(String(input.taskId));
|
|
511
580
|
if (task) {
|
|
512
581
|
if (input.subject) task.subject = input.subject;
|
|
513
582
|
if (input.status) task.status = input.status;
|
|
583
|
+
task.turnId = turnId;
|
|
514
584
|
}
|
|
515
585
|
}
|
|
516
586
|
emitPlan(session);
|
|
@@ -535,6 +605,7 @@ function updatePlanFromToolResult(session, pending, message) {
|
|
|
535
605
|
id: String(task.id),
|
|
536
606
|
subject: task.subject || task.description || "작업",
|
|
537
607
|
status: task.status || "pending",
|
|
608
|
+
turnId: session.turn?.id,
|
|
538
609
|
});
|
|
539
610
|
}
|
|
540
611
|
emitPlan(session);
|
|
@@ -628,8 +699,8 @@ async function processResult(session, message) {
|
|
|
628
699
|
threadId: session.id,
|
|
629
700
|
tokenUsage: {
|
|
630
701
|
total: totals,
|
|
631
|
-
last:
|
|
632
|
-
modelContextWindow: totals.contextWindow || undefined,
|
|
702
|
+
...(session.lastContextUsage ? { last: session.lastContextUsage } : {}),
|
|
703
|
+
modelContextWindow: session.lastContextWindow || totals.contextWindow || undefined,
|
|
633
704
|
},
|
|
634
705
|
});
|
|
635
706
|
const error = message.is_error && !interrupted
|
|
@@ -641,6 +712,25 @@ async function processResult(session, message) {
|
|
|
641
712
|
account: await safeAccount(session.query),
|
|
642
713
|
usage: await safeUsage(session.query),
|
|
643
714
|
});
|
|
715
|
+
await runPendingPrompt(session);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// The turn that was waiting starts on its own, so the host sees it exactly like a
|
|
719
|
+
// prompt sent the moment the previous turn ended.
|
|
720
|
+
async function runPendingPrompt(session) {
|
|
721
|
+
const next = session.pendingPrompts.shift();
|
|
722
|
+
if (!next) return;
|
|
723
|
+
try {
|
|
724
|
+
await runPrompt(session, next);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
notify("error", {
|
|
727
|
+
threadId: session.id,
|
|
728
|
+
provider: "Claude",
|
|
729
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
730
|
+
willRetry: false,
|
|
731
|
+
});
|
|
732
|
+
await runPendingPrompt(session);
|
|
733
|
+
}
|
|
644
734
|
}
|
|
645
735
|
|
|
646
736
|
function finishTurn(session, error, durationMs) {
|
|
@@ -673,34 +763,34 @@ async function consume(session) {
|
|
|
673
763
|
}
|
|
674
764
|
}
|
|
675
765
|
|
|
676
|
-
const HANDOFF_HEADER = '<devez_provider_handoff chars="';
|
|
677
|
-
const HANDOFF_SEPARATOR = '">\n';
|
|
678
|
-
const HANDOFF_FOOTER = "\n</devez_provider_handoff>\n\n";
|
|
679
|
-
|
|
680
|
-
function prependHandoff(content, handoffContext) {
|
|
681
|
-
if (!handoffContext) return content;
|
|
682
|
-
const handoff = String(handoffContext);
|
|
683
|
-
const prefix = `${HANDOFF_HEADER}${handoff.length}${HANDOFF_SEPARATOR}${handoff}${HANDOFF_FOOTER}`;
|
|
684
|
-
const firstText = content.find((item) => item.type === "text");
|
|
685
|
-
if (firstText) firstText.text = `${prefix}${firstText.text || ""}`;
|
|
686
|
-
else content.unshift({ type: "text", text: prefix });
|
|
687
|
-
return content;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function stripHandoff(text) {
|
|
691
|
-
if (!text.startsWith(HANDOFF_HEADER)) return text;
|
|
692
|
-
const separator = text.indexOf(HANDOFF_SEPARATOR, HANDOFF_HEADER.length);
|
|
693
|
-
if (separator < 0) return text;
|
|
694
|
-
const length = Number(text.slice(HANDOFF_HEADER.length, separator));
|
|
695
|
-
if (!Number.isSafeInteger(length) || length < 0) return text;
|
|
696
|
-
const contextStart = separator + HANDOFF_SEPARATOR.length;
|
|
697
|
-
const contextEnd = contextStart + length;
|
|
698
|
-
if (text.slice(contextEnd, contextEnd + HANDOFF_FOOTER.length) !== HANDOFF_FOOTER) return text;
|
|
699
|
-
return text.slice(contextEnd + HANDOFF_FOOTER.length);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
async function inputContent(input, handoffContext) {
|
|
703
|
-
const content = [];
|
|
766
|
+
const HANDOFF_HEADER = '<devez_provider_handoff chars="';
|
|
767
|
+
const HANDOFF_SEPARATOR = '">\n';
|
|
768
|
+
const HANDOFF_FOOTER = "\n</devez_provider_handoff>\n\n";
|
|
769
|
+
|
|
770
|
+
function prependHandoff(content, handoffContext) {
|
|
771
|
+
if (!handoffContext) return content;
|
|
772
|
+
const handoff = String(handoffContext);
|
|
773
|
+
const prefix = `${HANDOFF_HEADER}${handoff.length}${HANDOFF_SEPARATOR}${handoff}${HANDOFF_FOOTER}`;
|
|
774
|
+
const firstText = content.find((item) => item.type === "text");
|
|
775
|
+
if (firstText) firstText.text = `${prefix}${firstText.text || ""}`;
|
|
776
|
+
else content.unshift({ type: "text", text: prefix });
|
|
777
|
+
return content;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function stripHandoff(text) {
|
|
781
|
+
if (!text.startsWith(HANDOFF_HEADER)) return text;
|
|
782
|
+
const separator = text.indexOf(HANDOFF_SEPARATOR, HANDOFF_HEADER.length);
|
|
783
|
+
if (separator < 0) return text;
|
|
784
|
+
const length = Number(text.slice(HANDOFF_HEADER.length, separator));
|
|
785
|
+
if (!Number.isSafeInteger(length) || length < 0) return text;
|
|
786
|
+
const contextStart = separator + HANDOFF_SEPARATOR.length;
|
|
787
|
+
const contextEnd = contextStart + length;
|
|
788
|
+
if (text.slice(contextEnd, contextEnd + HANDOFF_FOOTER.length) !== HANDOFF_FOOTER) return text;
|
|
789
|
+
return text.slice(contextEnd + HANDOFF_FOOTER.length);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function inputContent(input, handoffContext) {
|
|
793
|
+
const content = [];
|
|
704
794
|
for (const item of Array.isArray(input) ? input : []) {
|
|
705
795
|
if (item.type === "text") content.push({ type: "text", text: item.text || "" });
|
|
706
796
|
else if (item.type === "localImage" && item.path) {
|
|
@@ -713,14 +803,24 @@ async function inputContent(input, handoffContext) {
|
|
|
713
803
|
content.push({ type: "image", source: { type: "base64", media_type: mediaType, data: bytes.toString("base64") } });
|
|
714
804
|
}
|
|
715
805
|
}
|
|
716
|
-
return prependHandoff(content.length ? content : [{ type: "text", text: "" }], handoffContext);
|
|
806
|
+
return prependHandoff(content.length ? content : [{ type: "text", text: "" }], handoffContext);
|
|
717
807
|
}
|
|
718
808
|
|
|
719
809
|
async function startPrompt(params) {
|
|
720
810
|
const id = rawSession(params.sessionId);
|
|
721
811
|
const session = sessions.get(id);
|
|
722
812
|
if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
|
|
723
|
-
|
|
813
|
+
// Claude runs one turn at a time, so extra input waits its turn instead of
|
|
814
|
+
// failing — the same queueing the CLI does for a prompt typed while it works.
|
|
815
|
+
if (session.turn) {
|
|
816
|
+
session.pendingPrompts.push(params);
|
|
817
|
+
return { turn: { id: session.turn.id }, queued: true };
|
|
818
|
+
}
|
|
819
|
+
return runPrompt(session, params);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function runPrompt(session, params) {
|
|
823
|
+
const id = session.id;
|
|
724
824
|
if (params.model) {
|
|
725
825
|
const model = stripClaudeModel(params.model);
|
|
726
826
|
await session.query.setModel(model);
|
|
@@ -733,10 +833,11 @@ async function startPrompt(params) {
|
|
|
733
833
|
session.effort = effort;
|
|
734
834
|
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
735
835
|
session.turn = { id: turnId, sawStreamText: false };
|
|
836
|
+
session.lastContextUsage = null;
|
|
736
837
|
notify("turn/started", { threadId: id, turn: { id: turnId } });
|
|
737
|
-
session.queue.push({
|
|
738
|
-
type: "user",
|
|
739
|
-
message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
|
|
838
|
+
session.queue.push({
|
|
839
|
+
type: "user",
|
|
840
|
+
message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
|
|
740
841
|
parent_tool_use_id: null,
|
|
741
842
|
session_id: id,
|
|
742
843
|
origin: { kind: "human" },
|
|
@@ -750,6 +851,23 @@ function contentBlocks(message) {
|
|
|
750
851
|
return Array.isArray(content) ? content : [];
|
|
751
852
|
}
|
|
752
853
|
|
|
854
|
+
function isInternalHistoryText(message, text) {
|
|
855
|
+
if (message.isMeta || message.subtype === "local_command") return true;
|
|
856
|
+
const trimmed = text.trim();
|
|
857
|
+
const tag = trimmed.match(/^<([a-z0-9-]+)>/i)?.[1]?.toLowerCase();
|
|
858
|
+
return trimmed === "[Request interrupted by user]"
|
|
859
|
+
|| [
|
|
860
|
+
"bash-input",
|
|
861
|
+
"bash-stdout",
|
|
862
|
+
"bash-stderr",
|
|
863
|
+
"command-name",
|
|
864
|
+
"local-command-caveat",
|
|
865
|
+
"local-command-stdout",
|
|
866
|
+
"local-command-stderr",
|
|
867
|
+
"task-notification",
|
|
868
|
+
].includes(tag);
|
|
869
|
+
}
|
|
870
|
+
|
|
753
871
|
function historyTurns(messages) {
|
|
754
872
|
const turns = [];
|
|
755
873
|
let turn = null;
|
|
@@ -757,10 +875,12 @@ function historyTurns(messages) {
|
|
|
757
875
|
const tasks = new Map();
|
|
758
876
|
for (const message of messages) {
|
|
759
877
|
const blocks = contentBlocks(message.message);
|
|
760
|
-
const userText = message.type === "user"
|
|
761
|
-
? stripHandoff(blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n"))
|
|
878
|
+
const userText = message.type === "user"
|
|
879
|
+
? stripHandoff(blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n"))
|
|
762
880
|
: "";
|
|
763
|
-
if (userText
|
|
881
|
+
if (userText
|
|
882
|
+
&& !blocks.some((block) => block.type === "tool_result")
|
|
883
|
+
&& !isInternalHistoryText(message, userText)) {
|
|
764
884
|
turn = {
|
|
765
885
|
id: `claude-turn-${message.uuid}`,
|
|
766
886
|
status: "completed",
|
|
@@ -770,16 +890,28 @@ function historyTurns(messages) {
|
|
|
770
890
|
}
|
|
771
891
|
if (!turn) continue;
|
|
772
892
|
if (message.type === "assistant") {
|
|
893
|
+
if (message.message?.model === "<synthetic>") {
|
|
894
|
+
turn.synthetic = true;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
turn.synthetic = false;
|
|
898
|
+
if (!turn.model && message.message?.model) {
|
|
899
|
+
turn.model = visibleModel(message.message.model);
|
|
900
|
+
const prompt = turn.items.find((item) => item.type === "userMessage");
|
|
901
|
+
if (prompt) prompt.model = turn.model;
|
|
902
|
+
}
|
|
773
903
|
for (const block of blocks) {
|
|
774
904
|
if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
|
|
775
905
|
else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
|
|
776
906
|
else if (block.type === "tool_use") {
|
|
777
907
|
const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
|
|
778
908
|
tools.set(block.id, pending);
|
|
779
|
-
if (block.name === "TaskCreate")
|
|
780
|
-
|
|
909
|
+
if (block.name === "TaskCreate") {
|
|
910
|
+
pruneFinishedTasks(tasks, turn.id);
|
|
911
|
+
tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
|
|
912
|
+
} else if (block.name === "TaskUpdate") {
|
|
781
913
|
const task = tasks.get(String(block.input?.taskId));
|
|
782
|
-
if (task) Object.assign(task, block.input?.subject ? { subject: block.input.subject } : {}, block.input?.status ? { status: block.input.status } : {});
|
|
914
|
+
if (task) Object.assign(task, block.input?.subject ? { subject: block.input.subject } : {}, block.input?.status ? { status: block.input.status } : {}, { turnId: turn.id });
|
|
783
915
|
} else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
|
|
784
916
|
}
|
|
785
917
|
}
|
|
@@ -798,7 +930,7 @@ function historyTurns(messages) {
|
|
|
798
930
|
}
|
|
799
931
|
} else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
|
|
800
932
|
tasks.clear();
|
|
801
|
-
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" });
|
|
933
|
+
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", turnId: turn.id });
|
|
802
934
|
} else if (pending.item) {
|
|
803
935
|
const output = toolOutput(block.content, message.tool_use_result);
|
|
804
936
|
Object.assign(pending.item, pending.item.type === "commandExecution"
|
|
@@ -814,7 +946,9 @@ function historyTurns(messages) {
|
|
|
814
946
|
const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "✓" : task.status === "in_progress" ? "▸" : "□"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
|
|
815
947
|
turn.items.push({ id: "claude-plan-latest", type: "plan", text });
|
|
816
948
|
}
|
|
817
|
-
return turns
|
|
949
|
+
return turns
|
|
950
|
+
.filter((candidate) => !candidate.synthetic)
|
|
951
|
+
.map(({ synthetic: _, ...candidate }) => candidate);
|
|
818
952
|
}
|
|
819
953
|
|
|
820
954
|
async function dispatch(method, params = {}) {
|
|
@@ -835,6 +969,7 @@ async function dispatch(method, params = {}) {
|
|
|
835
969
|
const id = rawSession(params.sessionId);
|
|
836
970
|
const existing = sessions.get(id);
|
|
837
971
|
if (existing) {
|
|
972
|
+
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
838
973
|
return {
|
|
839
974
|
id,
|
|
840
975
|
thread: { id, turns: [] },
|
|
@@ -844,6 +979,7 @@ async function dispatch(method, params = {}) {
|
|
|
844
979
|
reasoningEffort: existing.effort,
|
|
845
980
|
account: await safeAccount(existing.query),
|
|
846
981
|
usage: await safeUsage(existing.query),
|
|
982
|
+
tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
|
|
847
983
|
};
|
|
848
984
|
}
|
|
849
985
|
const info = await getSessionInfo(id, { dir: params.cwd });
|
|
@@ -851,6 +987,10 @@ async function dispatch(method, params = {}) {
|
|
|
851
987
|
const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
|
|
852
988
|
const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
|
|
853
989
|
const { session, account, usage } = await createSession({ ...params, cwd: info.cwd || params.cwd, model: params.model || lastModel }, id);
|
|
990
|
+
const tokenUsage = historyTokenUsage(messages, session.models, session.model);
|
|
991
|
+
// Seed the live session so the next turn keeps reporting a full context.
|
|
992
|
+
session.lastContextUsage = tokenUsage?.last || null;
|
|
993
|
+
session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
|
|
854
994
|
return {
|
|
855
995
|
id,
|
|
856
996
|
thread: { id, turns: [] },
|
|
@@ -860,6 +1000,7 @@ async function dispatch(method, params = {}) {
|
|
|
860
1000
|
reasoningEffort: session.effort,
|
|
861
1001
|
account,
|
|
862
1002
|
usage,
|
|
1003
|
+
tokenUsage,
|
|
863
1004
|
};
|
|
864
1005
|
}
|
|
865
1006
|
if (method === "session/list") {
|
|
@@ -888,6 +1029,9 @@ async function dispatch(method, params = {}) {
|
|
|
888
1029
|
if (method === "session/prompt") return startPrompt(params);
|
|
889
1030
|
if (method === "session/interrupt") {
|
|
890
1031
|
const session = sessions.get(rawSession(params.sessionId));
|
|
1032
|
+
// Stopping the run drops what was waiting behind it too, so nothing the user
|
|
1033
|
+
// just cancelled starts on its own afterwards.
|
|
1034
|
+
if (session) session.pendingPrompts.length = 0;
|
|
891
1035
|
if (session?.turn) {
|
|
892
1036
|
const turn = session.turn;
|
|
893
1037
|
turn.interruptRequested = true;
|
|
@@ -944,6 +1088,59 @@ async function dispatch(method, params = {}) {
|
|
|
944
1088
|
throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
|
|
945
1089
|
}
|
|
946
1090
|
|
|
1091
|
+
function runSelfTest() {
|
|
1092
|
+
const user = (uuid, text) => ({
|
|
1093
|
+
type: "user",
|
|
1094
|
+
uuid,
|
|
1095
|
+
message: { role: "user", content: [{ type: "text", text }] },
|
|
1096
|
+
origin: { kind: "human" },
|
|
1097
|
+
});
|
|
1098
|
+
const assistant = (uuid, model, text) => ({
|
|
1099
|
+
type: "assistant",
|
|
1100
|
+
uuid,
|
|
1101
|
+
message: { role: "assistant", model, content: [{ type: "text", text }] },
|
|
1102
|
+
});
|
|
1103
|
+
const turns = historyTurns([
|
|
1104
|
+
user("u1", "say hi"),
|
|
1105
|
+
assistant("a1", "claude-opus-5", "hi."),
|
|
1106
|
+
user("command", "<command-name>/model</command-name>"),
|
|
1107
|
+
user("stdout", "<local-command-stdout>Set model to sonnet</local-command-stdout>"),
|
|
1108
|
+
user("u2", "say hello"),
|
|
1109
|
+
assistant("a2", "claude-sonnet-5", "Hello."),
|
|
1110
|
+
user("bash", "<bash-stdout>hidden</bash-stdout>"),
|
|
1111
|
+
user("u3", "hay zzz"),
|
|
1112
|
+
assistant("a3", "claude-haiku-4-5-20251001", "hey."),
|
|
1113
|
+
user("synthetic-user", "duplicate"),
|
|
1114
|
+
assistant("synthetic", "<synthetic>", "No response requested."),
|
|
1115
|
+
]);
|
|
1116
|
+
const prompts = turns.map((turn) => turn.items.find((item) => item.type === "userMessage"));
|
|
1117
|
+
const expected = [
|
|
1118
|
+
["say hi", "claude:claude-opus-5"],
|
|
1119
|
+
["say hello", "claude:claude-sonnet-5"],
|
|
1120
|
+
["hay zzz", "claude:claude-haiku-4-5-20251001"],
|
|
1121
|
+
];
|
|
1122
|
+
if (turns.length !== expected.length
|
|
1123
|
+
|| prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
|
|
1124
|
+
|| prompt.model !== expected[index][1])) {
|
|
1125
|
+
throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
|
|
1126
|
+
}
|
|
1127
|
+
const usage = tokenBreakdown({
|
|
1128
|
+
input_tokens: 2,
|
|
1129
|
+
cache_read_input_tokens: 68_000,
|
|
1130
|
+
cache_creation_input_tokens: 500,
|
|
1131
|
+
output_tokens: 300,
|
|
1132
|
+
});
|
|
1133
|
+
if (usage.totalTokens !== 68_802 || usage.inputTokens !== 68_502) {
|
|
1134
|
+
throw new Error(`Claude usage self-test failed: ${JSON.stringify(usage)}`);
|
|
1135
|
+
}
|
|
1136
|
+
process.stdout.write("Claude bridge self-test passed\n");
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (process.argv.includes("--self-test")) {
|
|
1140
|
+
runSelfTest();
|
|
1141
|
+
process.exit(0);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
947
1144
|
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
948
1145
|
lines.on("line", async (line) => {
|
|
949
1146
|
if (!line.trim()) return;
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "devez-vibe",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"codex",
|
|
7
|
-
"cli",
|
|
8
|
-
"tui",
|
|
9
|
-
"terminal",
|
|
10
|
-
"app-server",
|
|
11
|
-
"claude-agent-sdk"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
-
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
-
},
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"bin": {
|
|
21
|
-
"dvz": "bin/dvz.exe"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"bin/dvz.exe",
|
|
25
|
-
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
-
"README.md",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
],
|
|
29
|
-
"os": [
|
|
30
|
-
"win32"
|
|
31
|
-
],
|
|
32
|
-
"cpu": [
|
|
33
|
-
"x64"
|
|
34
|
-
],
|
|
35
|
-
"engines": {
|
|
36
|
-
"node": ">=18"
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.223"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "devez-vibe",
|
|
3
|
+
"version": "0.1.47",
|
|
4
|
+
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codex",
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"terminal",
|
|
10
|
+
"app-server",
|
|
11
|
+
"claude-agent-sdk"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
+
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bin": {
|
|
21
|
+
"dvz": "bin/dvz.exe"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/dvz.exe",
|
|
25
|
+
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"os": [
|
|
30
|
+
"win32"
|
|
31
|
+
],
|
|
32
|
+
"cpu": [
|
|
33
|
+
"x64"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.223"
|
|
40
|
+
}
|
|
41
|
+
}
|