devez-vibe 1.2.7 → 1.2.9
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 +33 -33
- package/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +331 -93
- package/package.json +1 -1
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/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
5
8
|
import { createInterface } from "node:readline";
|
|
6
9
|
import {
|
|
7
10
|
deleteSession,
|
|
@@ -501,6 +504,45 @@ function emitDelta(session, method, itemId, delta) {
|
|
|
501
504
|
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
502
505
|
}
|
|
503
506
|
|
|
507
|
+
function isKoreanPrompt(input) {
|
|
508
|
+
const prompt = (Array.isArray(input) ? input : [])
|
|
509
|
+
.filter((item) => item?.type === "text")
|
|
510
|
+
.map((item) => String(item.text || ""))
|
|
511
|
+
.join("\n");
|
|
512
|
+
return /[\uac00-\ud7a3]/.test(prompt);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function openingNotice(input) {
|
|
516
|
+
return isKoreanPrompt(input)
|
|
517
|
+
? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
|
|
518
|
+
: "I’ll review the request and proceed with the necessary work.";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function normalizeProgressText(turn, text) {
|
|
522
|
+
const value = String(text || "");
|
|
523
|
+
const trimmed = value.trim();
|
|
524
|
+
if (turn?.koreanRequest
|
|
525
|
+
&& trimmed.length <= 160
|
|
526
|
+
&& !/[\uac00-\ud7a3]/.test(trimmed)
|
|
527
|
+
&& /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
|
|
528
|
+
return "다음 부분을 이어서 확인하겠습니다.";
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Claude can answer with a tool_use as its first and only content block even
|
|
534
|
+
// when the prompt asks for an opening update. Keep the visible contract stable
|
|
535
|
+
// without duplicating a real model-written update.
|
|
536
|
+
function emitOpeningNotice(session) {
|
|
537
|
+
if (!session.turn || session.turn.openingNoticeEmitted) return;
|
|
538
|
+
const text = session.turn.openingNotice;
|
|
539
|
+
const id = nextItemId(session, "opening");
|
|
540
|
+
const item = { id, type: "agentMessage", text, provider: "Claude" };
|
|
541
|
+
emitItem(session, "started", item);
|
|
542
|
+
emitItem(session, "completed", item);
|
|
543
|
+
session.turn.openingNoticeEmitted = true;
|
|
544
|
+
}
|
|
545
|
+
|
|
504
546
|
// Windows rounds larger timer delays up to the next scheduler slice. Ten
|
|
505
547
|
// milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
|
|
506
548
|
const SMOOTH_TEXT_INTERVAL_MS = 10;
|
|
@@ -642,7 +684,14 @@ async function processStreamEvent(session, message) {
|
|
|
642
684
|
const smooth = block.type === "text"
|
|
643
685
|
? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
|
|
644
686
|
: null;
|
|
645
|
-
session.streamBlocks.set(event.index, {
|
|
687
|
+
session.streamBlocks.set(event.index, {
|
|
688
|
+
id,
|
|
689
|
+
type: block.type,
|
|
690
|
+
text: "",
|
|
691
|
+
smooth,
|
|
692
|
+
languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
|
|
693
|
+
holdEnglishProgress: false,
|
|
694
|
+
});
|
|
646
695
|
emitItem(session, "started", item);
|
|
647
696
|
return;
|
|
648
697
|
}
|
|
@@ -652,13 +701,36 @@ async function processStreamEvent(session, message) {
|
|
|
652
701
|
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
653
702
|
if (!delta) return;
|
|
654
703
|
current.text += delta;
|
|
655
|
-
if (current.
|
|
656
|
-
|
|
704
|
+
if (current.type === "text") session.turn.sawVisibleText = true;
|
|
705
|
+
if (!current.smooth) {
|
|
706
|
+
emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (current.languagePending != null) {
|
|
710
|
+
current.languagePending += delta;
|
|
711
|
+
const probe = current.languagePending.trimStart();
|
|
712
|
+
const lower = probe.toLowerCase();
|
|
713
|
+
if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
|
|
714
|
+
if (/^now(?:\s|$)/i.test(probe)) {
|
|
715
|
+
current.holdEnglishProgress = true;
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
current.smooth.push(current.languagePending);
|
|
719
|
+
current.languagePending = null;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
current.smooth.push(delta);
|
|
657
723
|
return;
|
|
658
724
|
}
|
|
659
725
|
if (event.type === "content_block_stop") {
|
|
660
726
|
const current = session.streamBlocks.get(event.index);
|
|
661
727
|
if (!current) return;
|
|
728
|
+
if (current.languagePending != null) {
|
|
729
|
+
const visible = normalizeProgressText(session.turn, current.languagePending);
|
|
730
|
+
current.text = visible;
|
|
731
|
+
current.smooth?.push(visible);
|
|
732
|
+
current.languagePending = null;
|
|
733
|
+
}
|
|
662
734
|
await current.smooth?.finish();
|
|
663
735
|
const item = current.type === "text"
|
|
664
736
|
? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
|
|
@@ -685,20 +757,27 @@ function processAssistant(session, message) {
|
|
|
685
757
|
session.model,
|
|
686
758
|
);
|
|
687
759
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
760
|
+
const hasToolUse = content.some((block) => block.type === "tool_use");
|
|
761
|
+
const hasVisibleText = session.turn.sawVisibleText || content.some(
|
|
762
|
+
(block) => block.type === "text" && String(block.text || "").trim(),
|
|
763
|
+
);
|
|
764
|
+
// Without partial SDK events, replay completed text before tool items so the
|
|
765
|
+
// visible order still matches the assistant content order.
|
|
691
766
|
if (!session.streamBlocks.size && !session.turn.sawStreamText) {
|
|
692
767
|
for (const block of content) {
|
|
693
768
|
if (block.type !== "text" && block.type !== "thinking") continue;
|
|
694
769
|
const id = nextItemId(session, block.type);
|
|
695
770
|
const item = block.type === "text"
|
|
696
|
-
? { id, type: "agentMessage", text: block.text
|
|
771
|
+
? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
|
|
697
772
|
: { id, type: "reasoning", summary: [block.thinking || ""] };
|
|
698
773
|
emitItem(session, "started", item);
|
|
699
774
|
emitItem(session, "completed", item);
|
|
700
775
|
}
|
|
701
776
|
}
|
|
777
|
+
if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
|
|
778
|
+
for (const block of content) {
|
|
779
|
+
if (block.type === "tool_use") processToolUse(session, block);
|
|
780
|
+
}
|
|
702
781
|
}
|
|
703
782
|
|
|
704
783
|
function processToolUse(session, block) {
|
|
@@ -762,51 +841,51 @@ function prepareTaskPlanForCreate(tasks, subject) {
|
|
|
762
841
|
if (numberedTaskIndex(subject) === 1) tasks.clear();
|
|
763
842
|
}
|
|
764
843
|
|
|
765
|
-
function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
|
|
766
|
-
const task = tasks.get(String(input.taskId));
|
|
767
|
-
if (!task) return false;
|
|
768
|
-
if (input.subject) task.subject = input.subject;
|
|
769
|
-
const status = input.status;
|
|
770
|
-
if (status === "in_progress" || status === "completed") {
|
|
771
|
-
const entries = [...tasks.values()];
|
|
772
|
-
const targetIndex = entries.indexOf(task);
|
|
773
|
-
|
|
774
|
-
// Claude occasionally closes a later pending task at the end of a turn
|
|
775
|
-
// without ever starting it. Keep the visible plan truthful and sequential:
|
|
776
|
-
// every skipped predecessor and the target itself pass through in_progress.
|
|
777
|
-
for (let index = 0; index < targetIndex; index++) {
|
|
778
|
-
const previous = entries[index];
|
|
779
|
-
if (previous.status === "completed") continue;
|
|
780
|
-
if (previous.status !== "in_progress") {
|
|
781
|
-
previous.status = "in_progress";
|
|
782
|
-
previous.turnId = turnId;
|
|
783
|
-
onIntermediate?.();
|
|
784
|
-
}
|
|
785
|
-
previous.status = "completed";
|
|
786
|
-
previous.turnId = turnId;
|
|
787
|
-
onIntermediate?.();
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
for (let index = 0; index < entries.length; index++) {
|
|
791
|
-
const other = entries[index];
|
|
792
|
-
if (other === task || other.status !== "in_progress") continue;
|
|
793
|
-
other.status = index < targetIndex ? "completed" : "pending";
|
|
794
|
-
other.turnId = turnId;
|
|
795
|
-
onIntermediate?.();
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
|
|
799
|
-
task.status = "in_progress";
|
|
800
|
-
task.turnId = turnId;
|
|
801
|
-
onIntermediate?.();
|
|
802
|
-
}
|
|
803
|
-
task.status = status;
|
|
804
|
-
} else if (status) {
|
|
805
|
-
task.status = status;
|
|
806
|
-
}
|
|
807
|
-
task.turnId = turnId;
|
|
808
|
-
return true;
|
|
809
|
-
}
|
|
844
|
+
function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
|
|
845
|
+
const task = tasks.get(String(input.taskId));
|
|
846
|
+
if (!task) return false;
|
|
847
|
+
if (input.subject) task.subject = input.subject;
|
|
848
|
+
const status = input.status;
|
|
849
|
+
if (status === "in_progress" || status === "completed") {
|
|
850
|
+
const entries = [...tasks.values()];
|
|
851
|
+
const targetIndex = entries.indexOf(task);
|
|
852
|
+
|
|
853
|
+
// Claude occasionally closes a later pending task at the end of a turn
|
|
854
|
+
// without ever starting it. Keep the visible plan truthful and sequential:
|
|
855
|
+
// every skipped predecessor and the target itself pass through in_progress.
|
|
856
|
+
for (let index = 0; index < targetIndex; index++) {
|
|
857
|
+
const previous = entries[index];
|
|
858
|
+
if (previous.status === "completed") continue;
|
|
859
|
+
if (previous.status !== "in_progress") {
|
|
860
|
+
previous.status = "in_progress";
|
|
861
|
+
previous.turnId = turnId;
|
|
862
|
+
onIntermediate?.();
|
|
863
|
+
}
|
|
864
|
+
previous.status = "completed";
|
|
865
|
+
previous.turnId = turnId;
|
|
866
|
+
onIntermediate?.();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
for (let index = 0; index < entries.length; index++) {
|
|
870
|
+
const other = entries[index];
|
|
871
|
+
if (other === task || other.status !== "in_progress") continue;
|
|
872
|
+
other.status = index < targetIndex ? "completed" : "pending";
|
|
873
|
+
other.turnId = turnId;
|
|
874
|
+
onIntermediate?.();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
|
|
878
|
+
task.status = "in_progress";
|
|
879
|
+
task.turnId = turnId;
|
|
880
|
+
onIntermediate?.();
|
|
881
|
+
}
|
|
882
|
+
task.status = status;
|
|
883
|
+
} else if (status) {
|
|
884
|
+
task.status = status;
|
|
885
|
+
}
|
|
886
|
+
task.turnId = turnId;
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
810
889
|
|
|
811
890
|
// TaskList에는 예전 계획까지 함께 들어올 수 있다. 마지막으로 번호가 1부터
|
|
812
891
|
// 시작한 묶음만 현재 계획으로 삼되, 번호가 없는 목록은 손실 없이 그대로 둔다.
|
|
@@ -829,9 +908,9 @@ function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
|
829
908
|
status: "pending",
|
|
830
909
|
turnId,
|
|
831
910
|
});
|
|
832
|
-
} else if (name === "TaskUpdate") {
|
|
833
|
-
applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
|
|
834
|
-
}
|
|
911
|
+
} else if (name === "TaskUpdate") {
|
|
912
|
+
applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
|
|
913
|
+
}
|
|
835
914
|
emitPlan(session);
|
|
836
915
|
}
|
|
837
916
|
|
|
@@ -1348,10 +1427,11 @@ async function runPrompt(session, params) {
|
|
|
1348
1427
|
}
|
|
1349
1428
|
session.effort = effort;
|
|
1350
1429
|
await applyPermissionMode(session, params.permissionMode);
|
|
1351
|
-
const
|
|
1430
|
+
const content = await inputContent(params.input, params.handoffContext);
|
|
1431
|
+
const turnId = beginTurn(session, params.input);
|
|
1352
1432
|
session.queue.push({
|
|
1353
1433
|
type: "user",
|
|
1354
|
-
message: { role: "user", content
|
|
1434
|
+
message: { role: "user", content },
|
|
1355
1435
|
parent_tool_use_id: null,
|
|
1356
1436
|
session_id: id,
|
|
1357
1437
|
origin: { kind: "human" },
|
|
@@ -1361,9 +1441,16 @@ async function runPrompt(session, params) {
|
|
|
1361
1441
|
|
|
1362
1442
|
// A background task notification is an internal user message that starts its
|
|
1363
1443
|
// own Claude response even though the host did not submit a new prompt.
|
|
1364
|
-
function beginTurn(session) {
|
|
1444
|
+
function beginTurn(session, input = []) {
|
|
1365
1445
|
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
1366
|
-
session.turn = {
|
|
1446
|
+
session.turn = {
|
|
1447
|
+
id: turnId,
|
|
1448
|
+
sawStreamText: false,
|
|
1449
|
+
sawVisibleText: false,
|
|
1450
|
+
koreanRequest: isKoreanPrompt(input),
|
|
1451
|
+
openingNotice: openingNotice(input),
|
|
1452
|
+
openingNoticeEmitted: false,
|
|
1453
|
+
};
|
|
1367
1454
|
session.lastContextUsage = null;
|
|
1368
1455
|
notify("turn/started", { threadId: session.id, turn: { id: turnId } });
|
|
1369
1456
|
return turnId;
|
|
@@ -1485,6 +1572,75 @@ function historyTurns(messages) {
|
|
|
1485
1572
|
return historyState(messages).turns;
|
|
1486
1573
|
}
|
|
1487
1574
|
|
|
1575
|
+
// A transcript lives in a folder encoded from the cwd string, so two spellings of
|
|
1576
|
+
// the same path (Windows differs only in case) resolve to different folders and a
|
|
1577
|
+
// session recorded under one spelling is invisible to the other. Remember the
|
|
1578
|
+
// spelling the transcript was written with, keyed by session id.
|
|
1579
|
+
const transcriptCwds = new Map();
|
|
1580
|
+
|
|
1581
|
+
function claudeProjectsDir() {
|
|
1582
|
+
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects");
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Read the cwd a transcript records. Only the head is scanned: the opening
|
|
1586
|
+
* records carry no cwd, but a real turn shows up long before the limit. */
|
|
1587
|
+
async function readTranscriptCwd(path, limit = 200) {
|
|
1588
|
+
const stream = createReadStream(path, { encoding: "utf8" });
|
|
1589
|
+
try {
|
|
1590
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
1591
|
+
try {
|
|
1592
|
+
let seen = 0;
|
|
1593
|
+
for await (const line of reader) {
|
|
1594
|
+
if (++seen > limit) break;
|
|
1595
|
+
let cwd;
|
|
1596
|
+
try {
|
|
1597
|
+
cwd = JSON.parse(line)?.cwd;
|
|
1598
|
+
} catch {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (typeof cwd === "string" && cwd) return cwd;
|
|
1602
|
+
}
|
|
1603
|
+
return null;
|
|
1604
|
+
} finally {
|
|
1605
|
+
reader.close();
|
|
1606
|
+
}
|
|
1607
|
+
} finally {
|
|
1608
|
+
stream.destroy();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/** Locate the transcript of `id` under any project folder and report the cwd it
|
|
1613
|
+
* records, which is the spelling the SDK needs to find it again. */
|
|
1614
|
+
async function transcriptCwd(id) {
|
|
1615
|
+
if (transcriptCwds.has(id)) return transcriptCwds.get(id);
|
|
1616
|
+
let entries;
|
|
1617
|
+
try {
|
|
1618
|
+
entries = await readdir(claudeProjectsDir(), { withFileTypes: true });
|
|
1619
|
+
} catch {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
for (const entry of entries) {
|
|
1623
|
+
if (!entry.isDirectory()) continue;
|
|
1624
|
+
let cwd;
|
|
1625
|
+
try {
|
|
1626
|
+
cwd = await readTranscriptCwd(join(claudeProjectsDir(), entry.name, `${id}.jsonl`));
|
|
1627
|
+
} catch {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
if (cwd) {
|
|
1631
|
+
transcriptCwds.set(id, cwd);
|
|
1632
|
+
return cwd;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** The cwd to read `id`'s transcript with: the one the transcript itself records,
|
|
1639
|
+
* falling back to the host's when no transcript is on disk. */
|
|
1640
|
+
async function readableCwd(id, cwd) {
|
|
1641
|
+
return await transcriptCwd(id) || cwd;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1488
1644
|
async function dispatch(method, params = {}) {
|
|
1489
1645
|
if (method === "model/list") return loadModelCatalog(params);
|
|
1490
1646
|
if (method === "session/permissionMode") {
|
|
@@ -1524,15 +1680,19 @@ async function dispatch(method, params = {}) {
|
|
|
1524
1680
|
tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
|
|
1525
1681
|
};
|
|
1526
1682
|
}
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1683
|
+
const dir = await readableCwd(id, params.cwd);
|
|
1684
|
+
// The transcript itself decides whether there is anything to resume:
|
|
1685
|
+
// getSessionInfo only sees sessions the CLI indexed, and a bridge-run session
|
|
1686
|
+
// whose transcript is intact can be missing from that index.
|
|
1687
|
+
const messages = await getSessionMessages(id, { dir, includeSystemMessages: true });
|
|
1688
|
+
if (!messages.length) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
|
|
1689
|
+
const info = await getSessionInfo(id, { dir });
|
|
1530
1690
|
const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
|
|
1531
1691
|
// The transcript's own model outranks the host's fallback, which is only what
|
|
1532
1692
|
// a new session would have opened on.
|
|
1533
1693
|
const { session, account, usage } = await createSession({
|
|
1534
1694
|
...params,
|
|
1535
|
-
cwd: info
|
|
1695
|
+
cwd: info?.cwd || dir,
|
|
1536
1696
|
model: params.model || lastModel || params.fallbackModel,
|
|
1537
1697
|
effort: params.effort || params.fallbackEffort,
|
|
1538
1698
|
}, id);
|
|
@@ -1573,7 +1733,7 @@ async function dispatch(method, params = {}) {
|
|
|
1573
1733
|
}
|
|
1574
1734
|
if (method === "session/history") {
|
|
1575
1735
|
const id = liveSessionId(params.sessionId);
|
|
1576
|
-
const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
|
|
1736
|
+
const messages = await getSessionMessages(id, { dir: await readableCwd(id, params.cwd), includeSystemMessages: true });
|
|
1577
1737
|
return { data: historyTurns(messages), nextCursor: null };
|
|
1578
1738
|
}
|
|
1579
1739
|
if (method === "session/prompt") return startPrompt(params);
|
|
@@ -1599,7 +1759,7 @@ async function dispatch(method, params = {}) {
|
|
|
1599
1759
|
}
|
|
1600
1760
|
if (method === "session/fork") {
|
|
1601
1761
|
const source = liveSessionId(params.sessionId);
|
|
1602
|
-
const forked = await forkSession(source, { dir: params.cwd });
|
|
1762
|
+
const forked = await forkSession(source, { dir: await readableCwd(source, params.cwd) });
|
|
1603
1763
|
const id = forked.sessionId || forked;
|
|
1604
1764
|
const { session, account, usage } = await createSession(params, id);
|
|
1605
1765
|
return {
|
|
@@ -1717,33 +1877,33 @@ async function runSelfTest() {
|
|
|
1717
1877
|
}
|
|
1718
1878
|
applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
|
|
1719
1879
|
applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
|
|
1720
|
-
if (restoredTasks.size !== 6
|
|
1721
|
-
|| restoredTasks.get("25")?.status !== "completed"
|
|
1722
|
-
|| restoredTasks.get("26")?.status !== "completed"
|
|
1723
|
-
|| restoredTasks.get("27")?.status !== "in_progress"
|
|
1724
|
-
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1725
|
-
throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1726
|
-
}
|
|
1727
|
-
const skippedTasks = new Map([
|
|
1728
|
-
["1", { id: "1", subject: "1. 조사", status: "pending" }],
|
|
1729
|
-
["2", { id: "2", subject: "2. 분석", status: "pending" }],
|
|
1730
|
-
["3", { id: "3", subject: "3. 검증", status: "pending" }],
|
|
1731
|
-
]);
|
|
1732
|
-
const transitions = [];
|
|
1733
|
-
const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
|
|
1734
|
-
applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
|
|
1735
|
-
snapshot();
|
|
1736
|
-
if (transitions.join("|") !== [
|
|
1737
|
-
"in_progress,pending,pending",
|
|
1738
|
-
"completed,pending,pending",
|
|
1739
|
-
"completed,in_progress,pending",
|
|
1740
|
-
"completed,completed,pending",
|
|
1741
|
-
"completed,completed,in_progress",
|
|
1742
|
-
"completed,completed,completed",
|
|
1743
|
-
].join("|")) {
|
|
1744
|
-
throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
|
|
1745
|
-
}
|
|
1746
|
-
const mixedPlans = new Map([
|
|
1880
|
+
if (restoredTasks.size !== 6
|
|
1881
|
+
|| restoredTasks.get("25")?.status !== "completed"
|
|
1882
|
+
|| restoredTasks.get("26")?.status !== "completed"
|
|
1883
|
+
|| restoredTasks.get("27")?.status !== "in_progress"
|
|
1884
|
+
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1885
|
+
throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1886
|
+
}
|
|
1887
|
+
const skippedTasks = new Map([
|
|
1888
|
+
["1", { id: "1", subject: "1. 조사", status: "pending" }],
|
|
1889
|
+
["2", { id: "2", subject: "2. 분석", status: "pending" }],
|
|
1890
|
+
["3", { id: "3", subject: "3. 검증", status: "pending" }],
|
|
1891
|
+
]);
|
|
1892
|
+
const transitions = [];
|
|
1893
|
+
const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
|
|
1894
|
+
applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
|
|
1895
|
+
snapshot();
|
|
1896
|
+
if (transitions.join("|") !== [
|
|
1897
|
+
"in_progress,pending,pending",
|
|
1898
|
+
"completed,pending,pending",
|
|
1899
|
+
"completed,in_progress,pending",
|
|
1900
|
+
"completed,completed,pending",
|
|
1901
|
+
"completed,completed,in_progress",
|
|
1902
|
+
"completed,completed,completed",
|
|
1903
|
+
].join("|")) {
|
|
1904
|
+
throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
|
|
1905
|
+
}
|
|
1906
|
+
const mixedPlans = new Map([
|
|
1747
1907
|
["old-1", { subject: "1. 이전 작업", status: "completed" }],
|
|
1748
1908
|
["old-2", { subject: "2. 이전 검증", status: "completed" }],
|
|
1749
1909
|
...[...restoredTasks],
|
|
@@ -1888,6 +2048,84 @@ async function runSelfTest() {
|
|
|
1888
2048
|
&& event.params?.line?.kind === "error")) {
|
|
1889
2049
|
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
1890
2050
|
}
|
|
2051
|
+
const openingSession = {
|
|
2052
|
+
id: "opening-self-test",
|
|
2053
|
+
model: "claude:default",
|
|
2054
|
+
models: [],
|
|
2055
|
+
turn: null,
|
|
2056
|
+
turnSequence: 1,
|
|
2057
|
+
itemSequence: 1,
|
|
2058
|
+
streamBlocks: new Map(),
|
|
2059
|
+
tools: new Map(),
|
|
2060
|
+
tasks: new Map(),
|
|
2061
|
+
subagents: new Map(),
|
|
2062
|
+
knownSubagents: new Map(),
|
|
2063
|
+
lastContextUsage: null,
|
|
2064
|
+
lastContextWindow: 0,
|
|
2065
|
+
};
|
|
2066
|
+
const openingCaptured = [];
|
|
2067
|
+
process.stdout.write = (chunk) => {
|
|
2068
|
+
openingCaptured.push(String(chunk));
|
|
2069
|
+
return true;
|
|
2070
|
+
};
|
|
2071
|
+
try {
|
|
2072
|
+
beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
|
|
2073
|
+
processAssistant(openingSession, {
|
|
2074
|
+
message: {
|
|
2075
|
+
content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
2078
|
+
} finally {
|
|
2079
|
+
process.stdout.write = stdoutWrite;
|
|
2080
|
+
}
|
|
2081
|
+
const openingEvents = openingCaptured
|
|
2082
|
+
.join("")
|
|
2083
|
+
.trim()
|
|
2084
|
+
.split("\n")
|
|
2085
|
+
.filter(Boolean)
|
|
2086
|
+
.map((line) => JSON.parse(line));
|
|
2087
|
+
const openingMessageIndex = openingEvents.findIndex((event) =>
|
|
2088
|
+
event.method === "item/completed"
|
|
2089
|
+
&& event.params?.item?.type === "agentMessage"
|
|
2090
|
+
&& event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
|
|
2091
|
+
const openingToolIndex = openingEvents.findIndex((event) =>
|
|
2092
|
+
event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
|
|
2093
|
+
if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
|
|
2094
|
+
throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
|
|
2095
|
+
}
|
|
2096
|
+
const languageCaptured = [];
|
|
2097
|
+
process.stdout.write = (chunk) => {
|
|
2098
|
+
languageCaptured.push(String(chunk));
|
|
2099
|
+
return true;
|
|
2100
|
+
};
|
|
2101
|
+
try {
|
|
2102
|
+
openingSession.streamBlocks.clear();
|
|
2103
|
+
await processStreamEvent(openingSession, {
|
|
2104
|
+
event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
|
|
2105
|
+
});
|
|
2106
|
+
await processStreamEvent(openingSession, {
|
|
2107
|
+
event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
|
|
2108
|
+
});
|
|
2109
|
+
await processStreamEvent(openingSession, {
|
|
2110
|
+
event: { type: "content_block_stop", index: 0 },
|
|
2111
|
+
});
|
|
2112
|
+
} finally {
|
|
2113
|
+
process.stdout.write = stdoutWrite;
|
|
2114
|
+
}
|
|
2115
|
+
const languageEvents = languageCaptured
|
|
2116
|
+
.join("")
|
|
2117
|
+
.trim()
|
|
2118
|
+
.split("\n")
|
|
2119
|
+
.filter(Boolean)
|
|
2120
|
+
.map((line) => JSON.parse(line));
|
|
2121
|
+
const visibleLanguage = languageEvents
|
|
2122
|
+
.filter((event) => event.method === "item/agentMessage/delta")
|
|
2123
|
+
.map((event) => event.params?.delta || "")
|
|
2124
|
+
.join("");
|
|
2125
|
+
if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
|
|
2126
|
+
|| languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
|
|
2127
|
+
throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
|
|
2128
|
+
}
|
|
1891
2129
|
const smoothText = "Claude가 👨👩👧👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
|
|
1892
2130
|
const emitted = [];
|
|
1893
2131
|
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|