@norman-else/dsh-claude 0.1.18 → 0.1.20
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/INSTALL.md +1 -1
- package/README.md +3 -3
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +232 -120
- package/lib/client.js +5452 -882
- package/lib/client.js.map +1 -1
- package/lib/command-bridge-C10-lz6A.mjs +71 -0
- package/lib/command-bridge-C10-lz6A.mjs.map +1 -0
- package/lib/{events-6xTApIQw.mjs → events-BdDs9ebF.mjs} +7 -2
- package/lib/events-BdDs9ebF.mjs.map +1 -0
- package/lib/index.d.mts +3 -1
- package/lib/index.mjs +1760 -210
- package/lib/index.mjs.map +1 -1
- package/lib/{preset-installer-B0NrA4Hi.mjs → preset-installer-DMANIjwu.mjs} +2 -2
- package/lib/{preset-installer-B0NrA4Hi.mjs.map → preset-installer-DMANIjwu.mjs.map} +1 -1
- package/lib/preset-route.mjs +172 -2
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +68 -21
- package/lib/events-6xTApIQw.mjs.map +0 -1
- package/lib/presenters-BWXp9d3p.mjs +0 -257
- package/lib/presenters-BWXp9d3p.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as
|
|
2
|
-
import {
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { S as CLAUDE_UPDATE_PATH, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REVIEW_COMMENT_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_ACTION_PATH, w as TASK_TOOL_NAMES, x as CLAUDE_UPDATE_CHECK_PATH, y as CLAUDE_REPOSITORY_SETUP_PATH } from "./events-BdDs9ebF.mjs";
|
|
2
|
+
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-C10-lz6A.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DMANIjwu.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
-
import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
12
11
|
import { EventEmitter } from "node:events";
|
|
13
12
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
13
|
+
import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
//#region src/sidecar.ts
|
|
16
16
|
const SIDECAR_SCHEMA_VERSION = 1;
|
|
@@ -22,18 +22,18 @@ function emptyProjection() {
|
|
|
22
22
|
activities: []
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
function record$
|
|
25
|
+
function record$5(value) {
|
|
26
26
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
27
27
|
}
|
|
28
28
|
function finiteInteger(value) {
|
|
29
29
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
30
30
|
}
|
|
31
|
-
function string$
|
|
31
|
+
function string$3(value, max) {
|
|
32
32
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
33
33
|
}
|
|
34
34
|
function binding(value) {
|
|
35
|
-
const input = record$
|
|
36
|
-
if (input === void 0 || !string$
|
|
35
|
+
const input = record$5(value);
|
|
36
|
+
if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
|
|
37
37
|
return {
|
|
38
38
|
claudeSessionId: input.claudeSessionId,
|
|
39
39
|
sdkVersion: input.sdkVersion,
|
|
@@ -42,6 +42,7 @@ function binding(value) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
|
|
45
|
+
"text",
|
|
45
46
|
"status",
|
|
46
47
|
"thinking",
|
|
47
48
|
"tool-call",
|
|
@@ -61,22 +62,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
61
62
|
"failed"
|
|
62
63
|
]);
|
|
63
64
|
function activity(value) {
|
|
64
|
-
const input = record$
|
|
65
|
+
const input = record$5(value);
|
|
65
66
|
if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
|
|
66
67
|
return normalizeActivity(input);
|
|
67
68
|
}
|
|
68
69
|
function contextUsage(value) {
|
|
69
|
-
const input = record$
|
|
70
|
+
const input = record$5(value);
|
|
70
71
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
71
72
|
return normalizeContextUsage(input);
|
|
72
73
|
}
|
|
73
74
|
function tasks(value) {
|
|
74
|
-
const input = record$
|
|
75
|
+
const input = record$5(value);
|
|
75
76
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
76
77
|
return normalizeTasksEvent(input.tasks);
|
|
77
78
|
}
|
|
78
79
|
function parseClaudeSidecar(value) {
|
|
79
|
-
const input = record$
|
|
80
|
+
const input = record$5(value);
|
|
80
81
|
if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
|
|
81
82
|
const activities = input.activities.map(activity);
|
|
82
83
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
@@ -525,10 +526,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
525
526
|
}
|
|
526
527
|
//#endregion
|
|
527
528
|
//#region src/sdk-messages.ts
|
|
528
|
-
function record(value) {
|
|
529
|
+
function record$4(value) {
|
|
529
530
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
530
531
|
}
|
|
531
|
-
function string(value) {
|
|
532
|
+
function string$2(value) {
|
|
532
533
|
return typeof value === "string" ? value : void 0;
|
|
533
534
|
}
|
|
534
535
|
function taskUsageOf(usage) {
|
|
@@ -540,7 +541,7 @@ function taskUsageOf(usage) {
|
|
|
540
541
|
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
541
542
|
}
|
|
542
543
|
function resultUsage(message) {
|
|
543
|
-
const usage = record(message.usage);
|
|
544
|
+
const usage = record$4(message.usage);
|
|
544
545
|
const normalized = {};
|
|
545
546
|
if (usage !== void 0) {
|
|
546
547
|
if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
|
|
@@ -552,26 +553,26 @@ function resultUsage(message) {
|
|
|
552
553
|
return normalized;
|
|
553
554
|
}
|
|
554
555
|
function normalizeAssistant(message) {
|
|
555
|
-
const content = record(message.message)?.content;
|
|
556
|
+
const content = record$4(message.message)?.content;
|
|
556
557
|
if (!Array.isArray(content)) return [{
|
|
557
558
|
kind: "protocol-error",
|
|
558
559
|
title: "Malformed Claude assistant message",
|
|
559
560
|
detail: message
|
|
560
561
|
}];
|
|
561
|
-
const parentToolUseId = string(message.parent_tool_use_id);
|
|
562
|
+
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
562
563
|
const normalized = [];
|
|
563
564
|
for (const item of content) {
|
|
564
|
-
const block = record(item);
|
|
565
|
+
const block = record$4(item);
|
|
565
566
|
if (block === void 0) continue;
|
|
566
567
|
if (block.type === "text") {
|
|
567
|
-
const text = string(block.text);
|
|
568
|
+
const text = string$2(block.text);
|
|
568
569
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
569
570
|
kind: "assistant-text",
|
|
570
571
|
text,
|
|
571
572
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
572
573
|
});
|
|
573
574
|
} else if (block.type === "thinking") {
|
|
574
|
-
const text = string(block.thinking);
|
|
575
|
+
const text = string$2(block.thinking);
|
|
575
576
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
576
577
|
kind: "thinking",
|
|
577
578
|
text,
|
|
@@ -579,8 +580,8 @@ function normalizeAssistant(message) {
|
|
|
579
580
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
580
581
|
});
|
|
581
582
|
} else if (block.type === "tool_use") {
|
|
582
|
-
const toolUseId = string(block.id);
|
|
583
|
-
const toolName = string(block.name);
|
|
583
|
+
const toolUseId = string$2(block.id);
|
|
584
|
+
const toolName = string$2(block.name);
|
|
584
585
|
if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
|
|
585
586
|
kind: "tool-call",
|
|
586
587
|
toolUseId,
|
|
@@ -593,18 +594,18 @@ function normalizeAssistant(message) {
|
|
|
593
594
|
return normalized;
|
|
594
595
|
}
|
|
595
596
|
function normalizeUser(message) {
|
|
596
|
-
const content = record(message.message)?.content;
|
|
597
|
+
const content = record$4(message.message)?.content;
|
|
597
598
|
if (!Array.isArray(content)) return [{
|
|
598
599
|
kind: "protocol-error",
|
|
599
600
|
title: "Malformed Claude user message",
|
|
600
601
|
detail: message
|
|
601
602
|
}];
|
|
602
|
-
const parentToolUseId = string(message.parent_tool_use_id);
|
|
603
|
+
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
603
604
|
const normalized = [];
|
|
604
605
|
for (const item of content) {
|
|
605
|
-
const block = record(item);
|
|
606
|
+
const block = record$4(item);
|
|
606
607
|
if (block?.type !== "tool_result") continue;
|
|
607
|
-
const toolUseId = string(block.tool_use_id);
|
|
608
|
+
const toolUseId = string$2(block.tool_use_id);
|
|
608
609
|
if (toolUseId === void 0) continue;
|
|
609
610
|
normalized.push({
|
|
610
611
|
kind: "tool-result",
|
|
@@ -617,11 +618,11 @@ function normalizeUser(message) {
|
|
|
617
618
|
return normalized;
|
|
618
619
|
}
|
|
619
620
|
function normalizeSystem(message) {
|
|
620
|
-
const subtype = string(message.subtype);
|
|
621
|
+
const subtype = string$2(message.subtype);
|
|
621
622
|
if (subtype === "init") {
|
|
622
|
-
const sessionId = string(message.session_id);
|
|
623
|
-
const cliVersion = string(message.claude_code_version);
|
|
624
|
-
const cwd = string(message.cwd);
|
|
623
|
+
const sessionId = string$2(message.session_id);
|
|
624
|
+
const cliVersion = string$2(message.claude_code_version);
|
|
625
|
+
const cwd = string$2(message.cwd);
|
|
625
626
|
return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
|
|
626
627
|
kind: "init",
|
|
627
628
|
sessionId,
|
|
@@ -650,20 +651,20 @@ function normalizeSystem(message) {
|
|
|
650
651
|
title: `Claude session ${String(message.state)}`
|
|
651
652
|
}];
|
|
652
653
|
if (subtype === "permission_denied") {
|
|
653
|
-
const toolUseId = string(message.tool_use_id);
|
|
654
|
-
const toolName = string(message.tool_name);
|
|
654
|
+
const toolUseId = string$2(message.tool_use_id);
|
|
655
|
+
const toolName = string$2(message.tool_name);
|
|
655
656
|
if (toolUseId !== void 0 && toolName !== void 0) return [{
|
|
656
657
|
kind: "permission-denied",
|
|
657
658
|
toolUseId,
|
|
658
659
|
toolName,
|
|
659
|
-
summary: string(message.message) ?? "Claude Code denied the action"
|
|
660
|
+
summary: string$2(message.message) ?? "Claude Code denied the action"
|
|
660
661
|
}];
|
|
661
662
|
}
|
|
662
663
|
if (subtype === "task_started") {
|
|
663
|
-
const taskId = string(message.task_id);
|
|
664
|
-
const description = string(message.description);
|
|
665
|
-
const subagentType = string(message.subagent_type);
|
|
666
|
-
const taskType = string(message.task_type);
|
|
664
|
+
const taskId = string$2(message.task_id);
|
|
665
|
+
const description = string$2(message.description);
|
|
666
|
+
const subagentType = string$2(message.subagent_type);
|
|
667
|
+
const taskType = string$2(message.task_type);
|
|
667
668
|
return [{
|
|
668
669
|
kind: "subagent",
|
|
669
670
|
title: description ?? taskId ?? "Claude subagent started",
|
|
@@ -678,12 +679,12 @@ function normalizeSystem(message) {
|
|
|
678
679
|
}];
|
|
679
680
|
}
|
|
680
681
|
if (subtype === "task_progress") {
|
|
681
|
-
const taskId = string(message.task_id);
|
|
682
|
-
const description = string(message.description);
|
|
683
|
-
const summary = string(message.summary);
|
|
684
|
-
const subagentType = string(message.subagent_type);
|
|
685
|
-
const lastToolName = string(message.last_tool_name);
|
|
686
|
-
const usage = taskUsageOf(record(message.usage));
|
|
682
|
+
const taskId = string$2(message.task_id);
|
|
683
|
+
const description = string$2(message.description);
|
|
684
|
+
const summary = string$2(message.summary);
|
|
685
|
+
const subagentType = string$2(message.subagent_type);
|
|
686
|
+
const lastToolName = string$2(message.last_tool_name);
|
|
687
|
+
const usage = taskUsageOf(record$4(message.usage));
|
|
687
688
|
return [{
|
|
688
689
|
kind: "subagent",
|
|
689
690
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -699,11 +700,11 @@ function normalizeSystem(message) {
|
|
|
699
700
|
}];
|
|
700
701
|
}
|
|
701
702
|
if (subtype === "task_updated") {
|
|
702
|
-
const patch = record(message.patch);
|
|
703
|
-
const status = string(patch?.status);
|
|
704
|
-
const taskId = string(message.task_id);
|
|
705
|
-
const description = string(patch?.description);
|
|
706
|
-
const error = string(patch?.error);
|
|
703
|
+
const patch = record$4(message.patch);
|
|
704
|
+
const status = string$2(patch?.status);
|
|
705
|
+
const taskId = string$2(message.task_id);
|
|
706
|
+
const description = string$2(patch?.description);
|
|
707
|
+
const error = string$2(patch?.error);
|
|
707
708
|
const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
|
|
708
709
|
return [{
|
|
709
710
|
kind: "subagent",
|
|
@@ -719,10 +720,10 @@ function normalizeSystem(message) {
|
|
|
719
720
|
if (subtype === "task_notification") {
|
|
720
721
|
const failed = message.status === "failed";
|
|
721
722
|
const stopped = message.status === "stopped" || message.status === "cancelled";
|
|
722
|
-
const taskId = string(message.task_id);
|
|
723
|
-
const summary = string(message.summary);
|
|
723
|
+
const taskId = string$2(message.task_id);
|
|
724
|
+
const summary = string$2(message.summary);
|
|
724
725
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
725
|
-
const usage = taskUsageOf(record(message.usage));
|
|
726
|
+
const usage = taskUsageOf(record$4(message.usage));
|
|
726
727
|
return [{
|
|
727
728
|
kind: "subagent",
|
|
728
729
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -737,10 +738,10 @@ function normalizeSystem(message) {
|
|
|
737
738
|
if (subtype === "background_tasks_changed") return [{
|
|
738
739
|
kind: "background-tasks",
|
|
739
740
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
740
|
-
const entry = record(item);
|
|
741
|
-
const taskId = string(entry?.task_id);
|
|
742
|
-
const description = string(entry?.description);
|
|
743
|
-
const taskType = string(entry?.task_type);
|
|
741
|
+
const entry = record$4(item);
|
|
742
|
+
const taskId = string$2(entry?.task_id);
|
|
743
|
+
const description = string$2(entry?.description);
|
|
744
|
+
const taskType = string$2(entry?.task_type);
|
|
744
745
|
if (taskId === void 0 || description === void 0) return [];
|
|
745
746
|
return [{
|
|
746
747
|
taskId,
|
|
@@ -756,7 +757,7 @@ function normalizeSystem(message) {
|
|
|
756
757
|
}];
|
|
757
758
|
if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
|
|
758
759
|
kind: message.level === "warning" ? "warning" : "status",
|
|
759
|
-
title: string(message.content) ?? string(message.text) ?? "Claude Code notice",
|
|
760
|
+
title: string$2(message.content) ?? string$2(message.text) ?? "Claude Code notice",
|
|
760
761
|
detail: message
|
|
761
762
|
}];
|
|
762
763
|
if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
|
|
@@ -780,12 +781,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
780
781
|
function normalizeSdkMessage(message) {
|
|
781
782
|
const value = message;
|
|
782
783
|
if (value.type === "stream_event") {
|
|
783
|
-
const event = record(value.event);
|
|
784
|
-
const parentToolUseId = string(value.parent_tool_use_id);
|
|
784
|
+
const event = record$4(value.event);
|
|
785
|
+
const parentToolUseId = string$2(value.parent_tool_use_id);
|
|
785
786
|
if (event?.type === "content_block_delta") {
|
|
786
|
-
const delta = record(event.delta);
|
|
787
|
+
const delta = record$4(event.delta);
|
|
787
788
|
if (delta?.type === "text_delta") {
|
|
788
|
-
const text = string(delta.text);
|
|
789
|
+
const text = string$2(delta.text);
|
|
789
790
|
return text === void 0 ? [] : [{
|
|
790
791
|
kind: "text-delta",
|
|
791
792
|
text,
|
|
@@ -793,7 +794,7 @@ function normalizeSdkMessage(message) {
|
|
|
793
794
|
}];
|
|
794
795
|
}
|
|
795
796
|
if (delta?.type === "thinking_delta") {
|
|
796
|
-
const text = string(delta.thinking);
|
|
797
|
+
const text = string$2(delta.thinking);
|
|
797
798
|
return text === void 0 ? [] : [{
|
|
798
799
|
kind: "thinking",
|
|
799
800
|
text,
|
|
@@ -808,7 +809,7 @@ function normalizeSdkMessage(message) {
|
|
|
808
809
|
if (value.type === "user") return normalizeUser(value);
|
|
809
810
|
if (value.type === "system") return normalizeSystem(value);
|
|
810
811
|
if (value.type === "result") {
|
|
811
|
-
const sessionId = string(value.session_id);
|
|
812
|
+
const sessionId = string$2(value.session_id);
|
|
812
813
|
if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
|
|
813
814
|
kind: "protocol-error",
|
|
814
815
|
title: "Malformed Claude result message",
|
|
@@ -816,11 +817,11 @@ function normalizeSdkMessage(message) {
|
|
|
816
817
|
}];
|
|
817
818
|
const success = value.subtype === "success" && value.is_error !== true;
|
|
818
819
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
819
|
-
const terminalReason = string(value.terminal_reason);
|
|
820
|
-
const userMessageUuid = string(value.user_message_uuid);
|
|
821
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record(item)).filter((item) => item !== void 0).map((item) => {
|
|
822
|
-
const toolName = string(item.tool_name);
|
|
823
|
-
const toolUseId = string(item.tool_use_id);
|
|
820
|
+
const terminalReason = string$2(value.terminal_reason);
|
|
821
|
+
const userMessageUuid = string$2(value.user_message_uuid);
|
|
822
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$4(item)).filter((item) => item !== void 0).map((item) => {
|
|
823
|
+
const toolName = string$2(item.tool_name);
|
|
824
|
+
const toolUseId = string$2(item.tool_use_id);
|
|
824
825
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
825
826
|
toolName,
|
|
826
827
|
toolUseId
|
|
@@ -844,11 +845,10 @@ function normalizeSdkMessage(message) {
|
|
|
844
845
|
detail: value.error ?? value.output
|
|
845
846
|
}];
|
|
846
847
|
if (value.type === "rate_limit_event") {
|
|
847
|
-
const status = string(record(value.rate_limit_info)?.status);
|
|
848
|
-
const blocking = status !== void 0 && status !== "allowed";
|
|
848
|
+
const status = string$2(record$4(value.rate_limit_info)?.status);
|
|
849
849
|
return [{
|
|
850
|
-
kind:
|
|
851
|
-
title:
|
|
850
|
+
kind: "status",
|
|
851
|
+
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
852
852
|
detail: value.rate_limit_info
|
|
853
853
|
}];
|
|
854
854
|
}
|
|
@@ -1034,6 +1034,7 @@ function rootCallSummary(toolName, input) {
|
|
|
1034
1034
|
}
|
|
1035
1035
|
var ClaudeSupervisor = class {
|
|
1036
1036
|
#entries = /* @__PURE__ */ new Map();
|
|
1037
|
+
#interruptions = /* @__PURE__ */ new Map();
|
|
1037
1038
|
#runtime;
|
|
1038
1039
|
#approval;
|
|
1039
1040
|
#userQuestions;
|
|
@@ -1041,7 +1042,6 @@ var ClaudeSupervisor = class {
|
|
|
1041
1042
|
#queryFactory;
|
|
1042
1043
|
#runDetached;
|
|
1043
1044
|
#sidecar;
|
|
1044
|
-
#dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
|
|
1045
1045
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1046
1046
|
#disposed = false;
|
|
1047
1047
|
#admissionGate = Promise.resolve();
|
|
@@ -1082,6 +1082,8 @@ var ClaudeSupervisor = class {
|
|
|
1082
1082
|
return this.#contextWindows.get(model);
|
|
1083
1083
|
}
|
|
1084
1084
|
runTurn(request) {
|
|
1085
|
+
const interruption = this.#interruptions.get(request.agent.id);
|
|
1086
|
+
if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
|
|
1085
1087
|
const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
|
|
1086
1088
|
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
1087
1089
|
return operation;
|
|
@@ -1134,6 +1136,8 @@ var ClaudeSupervisor = class {
|
|
|
1134
1136
|
sawActivity: false,
|
|
1135
1137
|
sawTextDelta: false,
|
|
1136
1138
|
text: "",
|
|
1139
|
+
transcriptText: "",
|
|
1140
|
+
transcriptTextOrdinal: void 0,
|
|
1137
1141
|
thinking: "",
|
|
1138
1142
|
aborted: false,
|
|
1139
1143
|
deniedToolUseIds: /* @__PURE__ */ new Set(),
|
|
@@ -1172,7 +1176,7 @@ var ClaudeSupervisor = class {
|
|
|
1172
1176
|
}
|
|
1173
1177
|
if (request.signal !== void 0) {
|
|
1174
1178
|
const abortListener = () => {
|
|
1175
|
-
this.#
|
|
1179
|
+
this.#startInterrupt(entry);
|
|
1176
1180
|
};
|
|
1177
1181
|
active.abortListener = abortListener;
|
|
1178
1182
|
request.signal.addEventListener("abort", abortListener, { once: true });
|
|
@@ -1387,7 +1391,14 @@ var ClaudeSupervisor = class {
|
|
|
1387
1391
|
if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
|
|
1388
1392
|
if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
|
|
1389
1393
|
if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
|
|
1390
|
-
if (
|
|
1394
|
+
if (active.phase === "waiting-tasks") {
|
|
1395
|
+
await this.#completeProgressSegment(active, message);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) {
|
|
1399
|
+
if (active.phase === "follow-up") return;
|
|
1400
|
+
throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
|
|
1401
|
+
}
|
|
1391
1402
|
await this.#completeTurn(entry, active, message);
|
|
1392
1403
|
return;
|
|
1393
1404
|
}
|
|
@@ -1398,6 +1409,8 @@ var ClaudeSupervisor = class {
|
|
|
1398
1409
|
if (message.parentToolUseId !== void 0) return;
|
|
1399
1410
|
active.sawTextDelta = true;
|
|
1400
1411
|
active.text += message.text;
|
|
1412
|
+
active.transcriptText += message.text;
|
|
1413
|
+
await this.#upsertTranscriptText(active);
|
|
1401
1414
|
active.output.push({
|
|
1402
1415
|
type: "text-delta",
|
|
1403
1416
|
text: message.text
|
|
@@ -1408,6 +1421,8 @@ var ClaudeSupervisor = class {
|
|
|
1408
1421
|
if (!active.sawTextDelta) {
|
|
1409
1422
|
active.sawTextDelta = true;
|
|
1410
1423
|
active.text += message.text;
|
|
1424
|
+
active.transcriptText += message.text;
|
|
1425
|
+
await this.#upsertTranscriptText(active);
|
|
1411
1426
|
active.output.push({
|
|
1412
1427
|
type: "text-delta",
|
|
1413
1428
|
text: message.text
|
|
@@ -1429,6 +1444,7 @@ var ClaudeSupervisor = class {
|
|
|
1429
1444
|
});
|
|
1430
1445
|
return;
|
|
1431
1446
|
case "tool-call":
|
|
1447
|
+
if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
|
|
1432
1448
|
await this.#appendActivity(active, {
|
|
1433
1449
|
kind: message.parentToolUseId === void 0 ? "tool-call" : "subagent",
|
|
1434
1450
|
phase: "started",
|
|
@@ -1439,11 +1455,7 @@ var ClaudeSupervisor = class {
|
|
|
1439
1455
|
summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
|
|
1440
1456
|
detail: message.input
|
|
1441
1457
|
});
|
|
1442
|
-
if (message.parentToolUseId === void 0)
|
|
1443
|
-
active.callNames.set(message.toolUseId, message.toolName);
|
|
1444
|
-
this.#ensureDynamicPresenter(active.agent, message.toolName);
|
|
1445
|
-
if (!TASK_TOOL_NAMES.has(message.toolName)) await this.#appendNativeToolCall(active, message);
|
|
1446
|
-
}
|
|
1458
|
+
if (message.parentToolUseId === void 0) active.callNames.set(message.toolUseId, message.toolName);
|
|
1447
1459
|
return;
|
|
1448
1460
|
case "tool-result":
|
|
1449
1461
|
await this.#appendActivity(active, {
|
|
@@ -1455,7 +1467,6 @@ var ClaudeSupervisor = class {
|
|
|
1455
1467
|
detail: message.output,
|
|
1456
1468
|
isError: message.isError
|
|
1457
1469
|
});
|
|
1458
|
-
if (message.parentToolUseId === void 0 && !TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? "")) await this.#appendNativeToolResult(active, message);
|
|
1459
1470
|
return;
|
|
1460
1471
|
case "subagent":
|
|
1461
1472
|
await this.#appendActivity(active, {
|
|
@@ -1488,12 +1499,6 @@ var ClaudeSupervisor = class {
|
|
|
1488
1499
|
title: message.toolName,
|
|
1489
1500
|
summary: message.summary
|
|
1490
1501
|
});
|
|
1491
|
-
if (!TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? message.toolName)) await this.#appendNativeToolResult(active, {
|
|
1492
|
-
kind: "tool-result",
|
|
1493
|
-
toolUseId: message.toolUseId,
|
|
1494
|
-
output: message.summary,
|
|
1495
|
-
isError: true
|
|
1496
|
-
});
|
|
1497
1502
|
return;
|
|
1498
1503
|
}
|
|
1499
1504
|
}
|
|
@@ -1571,6 +1576,7 @@ var ClaudeSupervisor = class {
|
|
|
1571
1576
|
active.promptUuid = randomUUID();
|
|
1572
1577
|
active.sawTextDelta = false;
|
|
1573
1578
|
active.text = "";
|
|
1579
|
+
this.#closeTranscriptTextSegment(active);
|
|
1574
1580
|
active.thinking = "";
|
|
1575
1581
|
await this.#appendSafely(active, {
|
|
1576
1582
|
kind: "status",
|
|
@@ -1604,9 +1610,43 @@ var ClaudeSupervisor = class {
|
|
|
1604
1610
|
entry.taskSnapshotAt = Date.now();
|
|
1605
1611
|
await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
|
|
1606
1612
|
}
|
|
1613
|
+
async #completeProgressSegment(active, result, recordUsage = true) {
|
|
1614
|
+
if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
|
|
1615
|
+
await this.#appendSafely(active, {
|
|
1616
|
+
kind: "usage",
|
|
1617
|
+
phase: "completed",
|
|
1618
|
+
title: "Claude usage",
|
|
1619
|
+
summary: usageSummary(result.usage),
|
|
1620
|
+
usage: result.usage
|
|
1621
|
+
});
|
|
1622
|
+
active.output.push({
|
|
1623
|
+
type: "usage",
|
|
1624
|
+
usage: result.usage
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1628
|
+
active.text = result.text;
|
|
1629
|
+
active.transcriptText = result.text;
|
|
1630
|
+
await this.#upsertTranscriptText(active);
|
|
1631
|
+
active.output.push({
|
|
1632
|
+
type: "text-delta",
|
|
1633
|
+
text: result.text
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
await this.#upsertTranscriptText(active);
|
|
1637
|
+
active.output.push({
|
|
1638
|
+
type: "segment-complete",
|
|
1639
|
+
text: active.text
|
|
1640
|
+
});
|
|
1641
|
+
active.sawTextDelta = false;
|
|
1642
|
+
active.text = "";
|
|
1643
|
+
this.#closeTranscriptTextSegment(active);
|
|
1644
|
+
active.thinking = "";
|
|
1645
|
+
}
|
|
1607
1646
|
async #completeTurn(entry, active, result) {
|
|
1608
1647
|
if (entry.active !== active) return;
|
|
1609
1648
|
if (active.aborted) {
|
|
1649
|
+
await this.#upsertTranscriptText(active);
|
|
1610
1650
|
await this.#appendSafely(active, {
|
|
1611
1651
|
kind: "status",
|
|
1612
1652
|
phase: "failed",
|
|
@@ -1639,6 +1679,16 @@ var ClaudeSupervisor = class {
|
|
|
1639
1679
|
summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
|
|
1640
1680
|
});
|
|
1641
1681
|
if (!result.success) {
|
|
1682
|
+
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1683
|
+
active.text = result.text;
|
|
1684
|
+
active.transcriptText = result.text;
|
|
1685
|
+
await this.#upsertTranscriptText(active);
|
|
1686
|
+
active.output.push({
|
|
1687
|
+
type: "text-delta",
|
|
1688
|
+
text: result.text
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
await this.#upsertTranscriptText(active);
|
|
1642
1692
|
const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
|
|
1643
1693
|
await this.#appendSafely(active, {
|
|
1644
1694
|
kind: "error",
|
|
@@ -1651,11 +1701,14 @@ var ClaudeSupervisor = class {
|
|
|
1651
1701
|
} else {
|
|
1652
1702
|
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1653
1703
|
active.text = result.text;
|
|
1704
|
+
active.transcriptText = result.text;
|
|
1705
|
+
await this.#upsertTranscriptText(active);
|
|
1654
1706
|
active.output.push({
|
|
1655
1707
|
type: "text-delta",
|
|
1656
1708
|
text: result.text
|
|
1657
1709
|
});
|
|
1658
1710
|
}
|
|
1711
|
+
await this.#upsertTranscriptText(active);
|
|
1659
1712
|
if (active.phase === "primary" && this.#hasRunningTasks(entry, active)) {
|
|
1660
1713
|
active.phase = "waiting-tasks";
|
|
1661
1714
|
await this.#appendSafely(active, {
|
|
@@ -1663,10 +1716,7 @@ var ClaudeSupervisor = class {
|
|
|
1663
1716
|
phase: "updated",
|
|
1664
1717
|
title: "Claude Code is waiting for background tasks"
|
|
1665
1718
|
});
|
|
1666
|
-
active
|
|
1667
|
-
type: "segment-complete",
|
|
1668
|
-
text: active.text
|
|
1669
|
-
});
|
|
1719
|
+
await this.#completeProgressSegment(active, result, false);
|
|
1670
1720
|
return;
|
|
1671
1721
|
}
|
|
1672
1722
|
await this.#appendSafely(active, {
|
|
@@ -1686,6 +1736,25 @@ var ClaudeSupervisor = class {
|
|
|
1686
1736
|
entry.lastUsedAt = Date.now();
|
|
1687
1737
|
this.#armIdleTimer(entry);
|
|
1688
1738
|
}
|
|
1739
|
+
async #upsertTranscriptText(active) {
|
|
1740
|
+
if (active.transcriptText.length === 0) return;
|
|
1741
|
+
const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
|
|
1742
|
+
active.transcriptTextOrdinal = ordinal;
|
|
1743
|
+
try {
|
|
1744
|
+
await this.#sidecar.appendActivity(active.agent.id, {
|
|
1745
|
+
kind: "text",
|
|
1746
|
+
phase: "updated",
|
|
1747
|
+
text: active.transcriptText,
|
|
1748
|
+
turn: active.cursor.turn,
|
|
1749
|
+
step: active.cursor.step,
|
|
1750
|
+
ordinal
|
|
1751
|
+
});
|
|
1752
|
+
} catch {}
|
|
1753
|
+
}
|
|
1754
|
+
#closeTranscriptTextSegment(active) {
|
|
1755
|
+
active.transcriptText = "";
|
|
1756
|
+
active.transcriptTextOrdinal = void 0;
|
|
1757
|
+
}
|
|
1689
1758
|
async #appendActivity(active, activity) {
|
|
1690
1759
|
const ordinal = active.cursor.nextOrdinal++;
|
|
1691
1760
|
await this.#sidecar.appendActivity(active.agent.id, {
|
|
@@ -1700,53 +1769,14 @@ var ClaudeSupervisor = class {
|
|
|
1700
1769
|
async #appendSafely(active, activity) {
|
|
1701
1770
|
await this.#appendActivity(active, activity).catch(() => void 0);
|
|
1702
1771
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
known = /* @__PURE__ */ new Set();
|
|
1712
|
-
this.#dynamicPresenterNames.set(agent, known);
|
|
1713
|
-
}
|
|
1714
|
-
if (known.has(name)) return;
|
|
1715
|
-
try {
|
|
1716
|
-
agent.ctx.tools.register(dynamicPresenterDefinition(name));
|
|
1717
|
-
known.add(name);
|
|
1718
|
-
} catch {}
|
|
1719
|
-
}
|
|
1720
|
-
/** Mirror one root Claude tool call into the durable native tool channel so
|
|
1721
|
-
* the host's tool presentation renders it exactly like a DSH-executed call.
|
|
1722
|
-
* Presentation duplication is best-effort and never unsettles the turn. */
|
|
1723
|
-
async #appendNativeToolCall(active, message) {
|
|
1724
|
-
try {
|
|
1725
|
-
await active.agent.session.append("tool/call", {
|
|
1726
|
-
turn: active.cursor.turn,
|
|
1727
|
-
step: active.cursor.step,
|
|
1728
|
-
callId: CallId(message.toolUseId),
|
|
1729
|
-
name: message.toolName,
|
|
1730
|
-
arguments: safeDetail(message.input) ?? "{}"
|
|
1731
|
-
});
|
|
1732
|
-
} catch {}
|
|
1733
|
-
}
|
|
1734
|
-
async #appendNativeToolResult(active, message) {
|
|
1735
|
-
const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
|
|
1736
|
-
try {
|
|
1737
|
-
await active.agent.session.append("tool/result", {
|
|
1738
|
-
turn: active.cursor.turn,
|
|
1739
|
-
step: active.cursor.step,
|
|
1740
|
-
message: createToolResultMessage({
|
|
1741
|
-
callId: CallId(message.toolUseId),
|
|
1742
|
-
content: [{
|
|
1743
|
-
type: "text",
|
|
1744
|
-
text
|
|
1745
|
-
}],
|
|
1746
|
-
isError: message.isError
|
|
1747
|
-
})
|
|
1748
|
-
}, { surfaceOp: "append" });
|
|
1749
|
-
} catch {}
|
|
1772
|
+
#startInterrupt(entry) {
|
|
1773
|
+
const existing = this.#interruptions.get(entry.sessionId);
|
|
1774
|
+
if (existing !== void 0) return existing;
|
|
1775
|
+
const interruption = this.#interrupt(entry).finally(() => {
|
|
1776
|
+
this.#interruptions.delete(entry.sessionId);
|
|
1777
|
+
});
|
|
1778
|
+
this.#interruptions.set(entry.sessionId, interruption);
|
|
1779
|
+
return interruption;
|
|
1750
1780
|
}
|
|
1751
1781
|
async #interrupt(entry) {
|
|
1752
1782
|
const active = entry.active;
|
|
@@ -1754,6 +1784,7 @@ var ClaudeSupervisor = class {
|
|
|
1754
1784
|
entry.state = "interrupting";
|
|
1755
1785
|
active.aborted = true;
|
|
1756
1786
|
active.output.fail(abortFailure());
|
|
1787
|
+
await this.#upsertTranscriptText(active);
|
|
1757
1788
|
let interruptError;
|
|
1758
1789
|
try {
|
|
1759
1790
|
if (((await withTimeout(entry.query.interrupt(), 5e3, "Claude Code interrupt"))?.still_queued ?? []).includes(active.promptUuid)) throw new Error(`Claude Code interrupt left submitted prompt ${active.promptUuid} queued`);
|
|
@@ -1775,6 +1806,7 @@ var ClaudeSupervisor = class {
|
|
|
1775
1806
|
const active = entry.active;
|
|
1776
1807
|
const stderr = entry.process?.stderrTail();
|
|
1777
1808
|
if (active !== void 0) {
|
|
1809
|
+
await this.#upsertTranscriptText(active);
|
|
1778
1810
|
if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
|
|
1779
1811
|
const unknown = active.sawActivity;
|
|
1780
1812
|
entry.state = unknown ? "outcome-unknown" : "disconnected";
|
|
@@ -1830,6 +1862,79 @@ var ClaudeSupervisor = class {
|
|
|
1830
1862
|
}
|
|
1831
1863
|
};
|
|
1832
1864
|
//#endregion
|
|
1865
|
+
//#region src/review-comments.ts
|
|
1866
|
+
const MAX_COMMENTS_PER_SESSION = 50;
|
|
1867
|
+
const MAX_TEXT_CHARS$1 = 2e3;
|
|
1868
|
+
const MAX_PATH_CHARS$1 = 1024;
|
|
1869
|
+
const MAX_LINE = 1e7;
|
|
1870
|
+
var ReviewCommentError = class extends Error {
|
|
1871
|
+
code;
|
|
1872
|
+
constructor(code, message) {
|
|
1873
|
+
super(message);
|
|
1874
|
+
this.name = "ReviewCommentError";
|
|
1875
|
+
this.code = code;
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
function validSide(value) {
|
|
1879
|
+
return value === "old" || value === "new";
|
|
1880
|
+
}
|
|
1881
|
+
/** Pending line comments queued per session until the next user turn drains them. */
|
|
1882
|
+
var ReviewCommentStore = class {
|
|
1883
|
+
#comments = /* @__PURE__ */ new Map();
|
|
1884
|
+
add(sessionId, input) {
|
|
1885
|
+
const path = typeof input.path === "string" ? input.path.trim() : "";
|
|
1886
|
+
const text = typeof input.text === "string" ? input.text.trim() : "";
|
|
1887
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS$1 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
|
|
1888
|
+
if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
|
|
1889
|
+
if (typeof input.line !== "number" || !Number.isSafeInteger(input.line) || input.line < 1 || input.line > MAX_LINE) throw new ReviewCommentError("invalid-request", "The comment line is invalid.");
|
|
1890
|
+
if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
|
|
1891
|
+
const existing = this.#comments.get(sessionId) ?? [];
|
|
1892
|
+
if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
|
|
1893
|
+
const comment = {
|
|
1894
|
+
id: randomUUID(),
|
|
1895
|
+
path,
|
|
1896
|
+
line: input.line,
|
|
1897
|
+
side: input.side,
|
|
1898
|
+
text
|
|
1899
|
+
};
|
|
1900
|
+
this.#comments.set(sessionId, [...existing, comment]);
|
|
1901
|
+
return comment;
|
|
1902
|
+
}
|
|
1903
|
+
remove(sessionId, id) {
|
|
1904
|
+
const existing = this.#comments.get(sessionId);
|
|
1905
|
+
if (existing === void 0) return false;
|
|
1906
|
+
const next = existing.filter((comment) => comment.id !== id);
|
|
1907
|
+
if (next.length === existing.length) return false;
|
|
1908
|
+
if (next.length === 0) this.#comments.delete(sessionId);
|
|
1909
|
+
else this.#comments.set(sessionId, next);
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
list(sessionId) {
|
|
1913
|
+
return this.#comments.get(sessionId) ?? [];
|
|
1914
|
+
}
|
|
1915
|
+
/** Remove and return every pending comment; called when a user turn consumes them. */
|
|
1916
|
+
drain(sessionId) {
|
|
1917
|
+
const existing = this.#comments.get(sessionId) ?? [];
|
|
1918
|
+
this.#comments.delete(sessionId);
|
|
1919
|
+
return existing;
|
|
1920
|
+
}
|
|
1921
|
+
disposeSession(sessionId) {
|
|
1922
|
+
this.#comments.delete(sessionId);
|
|
1923
|
+
}
|
|
1924
|
+
dispose() {
|
|
1925
|
+
this.#comments.clear();
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
/** Render drained comments as the prompt block preceding the user's message text. */
|
|
1929
|
+
function formatReviewComments(comments) {
|
|
1930
|
+
return [
|
|
1931
|
+
"<user-review-comments>",
|
|
1932
|
+
"The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
|
|
1933
|
+
...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
|
|
1934
|
+
"</user-review-comments>"
|
|
1935
|
+
].join("\n");
|
|
1936
|
+
}
|
|
1937
|
+
//#endregion
|
|
1833
1938
|
//#region src/adapter.ts
|
|
1834
1939
|
const MODELS = [
|
|
1835
1940
|
{
|
|
@@ -1926,6 +2031,22 @@ function validateImageRef(ref, attachments, imageIndex) {
|
|
|
1926
2031
|
const maxDimension = "maxImageDimension" in limits && finiteNonNegative(limits.maxImageDimension) ? limits.maxImageDimension : void 0;
|
|
1927
2032
|
if (!finiteNonNegative(ref.width) || !finiteNonNegative(ref.height) || ref.width * ref.height > limits.maxImagePixels || maxDimension !== void 0 && (ref.width > maxDimension || ref.height > maxDimension)) throw new Error(`dsh-claude: image ${imageIndex} exceeds the configured dimension limit`);
|
|
1928
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Prepend the session's pending diff-review comments to the outgoing user
|
|
2036
|
+
* prompt. The comments are drained once per turn: they are formatted into one
|
|
2037
|
+
* `<user-review-comments>` block placed ahead of the user's own text (or as a
|
|
2038
|
+
* leading text block for multi-part prompts) so Claude reads them in the same
|
|
2039
|
+
* turn that consumed them.
|
|
2040
|
+
*/
|
|
2041
|
+
function injectReviewComments(prompt, comments) {
|
|
2042
|
+
if (comments.length === 0) return prompt;
|
|
2043
|
+
const block = formatReviewComments(comments);
|
|
2044
|
+
if (typeof prompt === "string") return `${block}\n\n${prompt}`;
|
|
2045
|
+
return [{
|
|
2046
|
+
type: "text",
|
|
2047
|
+
text: block
|
|
2048
|
+
}, ...prompt];
|
|
2049
|
+
}
|
|
1929
2050
|
function imageBlock(data, mediaType) {
|
|
1930
2051
|
return {
|
|
1931
2052
|
type: "image",
|
|
@@ -2008,12 +2129,14 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2008
2129
|
#agents;
|
|
2009
2130
|
#attachments;
|
|
2010
2131
|
#presetIdFor;
|
|
2011
|
-
|
|
2132
|
+
#drainReviewComments;
|
|
2133
|
+
constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2012
2134
|
super();
|
|
2013
2135
|
this.#supervisor = supervisor;
|
|
2014
2136
|
this.#agents = agents;
|
|
2015
2137
|
this.#attachments = attachments;
|
|
2016
2138
|
this.#presetIdFor = presetIdFor;
|
|
2139
|
+
this.#drainReviewComments = drainReviewComments;
|
|
2017
2140
|
}
|
|
2018
2141
|
providerInfo(provider) {
|
|
2019
2142
|
return {
|
|
@@ -2033,7 +2156,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2033
2156
|
inputModalities: ["text", "image"]
|
|
2034
2157
|
}));
|
|
2035
2158
|
}
|
|
2036
|
-
async resolveModel(provider, model) {
|
|
2159
|
+
async resolveModel(provider, model, _signal) {
|
|
2037
2160
|
const known = MODELS.find((item) => item.id === model);
|
|
2038
2161
|
const contextWindow = this.#supervisor.contextWindow(model) ?? (known !== void 0 && "contextWindow" in known ? known.contextWindow : void 0);
|
|
2039
2162
|
return {
|
|
@@ -2050,6 +2173,12 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2050
2173
|
})) }
|
|
2051
2174
|
};
|
|
2052
2175
|
}
|
|
2176
|
+
async prepareCall(provider, model, signal) {
|
|
2177
|
+
return {
|
|
2178
|
+
model: await this.resolveModel(provider, model, signal),
|
|
2179
|
+
stream: (options) => this.stream(options)
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2053
2182
|
async *stream(options) {
|
|
2054
2183
|
if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
|
|
2055
2184
|
const agent = resolveAgent(this.#agents, options);
|
|
@@ -2074,42 +2203,20 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2074
2203
|
}
|
|
2075
2204
|
const events = await this.#supervisor.runTurn({
|
|
2076
2205
|
agent,
|
|
2077
|
-
prompt,
|
|
2206
|
+
prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
|
|
2078
2207
|
model: options.model,
|
|
2079
2208
|
...thinkingMode === void 0 ? {} : { thinkingMode },
|
|
2080
2209
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2081
2210
|
});
|
|
2082
|
-
let text = "";
|
|
2083
2211
|
let pendingUsage;
|
|
2084
2212
|
let completed = false;
|
|
2085
|
-
let blockIndex = 0;
|
|
2086
2213
|
try {
|
|
2087
|
-
for await (const event of events)
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
type: "block-start",
|
|
2093
|
-
index: blockIndex,
|
|
2094
|
-
blockType: "text"
|
|
2095
|
-
};
|
|
2096
|
-
yield {
|
|
2097
|
-
type: "text-delta",
|
|
2098
|
-
index: blockIndex,
|
|
2099
|
-
text
|
|
2100
|
-
};
|
|
2101
|
-
yield {
|
|
2102
|
-
type: "block-end",
|
|
2103
|
-
index: blockIndex,
|
|
2104
|
-
block: {
|
|
2105
|
-
type: "text",
|
|
2106
|
-
text
|
|
2107
|
-
}
|
|
2108
|
-
};
|
|
2109
|
-
blockIndex += 1;
|
|
2214
|
+
for await (const event of events) {
|
|
2215
|
+
if (event.type === "text-delta" || event.type === "segment-complete") continue;
|
|
2216
|
+
if (event.type === "usage") {
|
|
2217
|
+
pendingUsage = tokenUsage(event.usage);
|
|
2218
|
+
continue;
|
|
2110
2219
|
}
|
|
2111
|
-
text = "";
|
|
2112
|
-
if (event.type === "segment-complete") continue;
|
|
2113
2220
|
completed = true;
|
|
2114
2221
|
if (pendingUsage !== void 0) yield {
|
|
2115
2222
|
type: "usage",
|
|
@@ -2123,26 +2230,6 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2123
2230
|
} catch (error) {
|
|
2124
2231
|
if (error.name === "AbortError") {
|
|
2125
2232
|
completed = true;
|
|
2126
|
-
if (text.length > 0) {
|
|
2127
|
-
yield {
|
|
2128
|
-
type: "block-start",
|
|
2129
|
-
index: blockIndex,
|
|
2130
|
-
blockType: "text"
|
|
2131
|
-
};
|
|
2132
|
-
yield {
|
|
2133
|
-
type: "text-delta",
|
|
2134
|
-
index: blockIndex,
|
|
2135
|
-
text
|
|
2136
|
-
};
|
|
2137
|
-
yield {
|
|
2138
|
-
type: "block-end",
|
|
2139
|
-
index: blockIndex,
|
|
2140
|
-
block: {
|
|
2141
|
-
type: "text",
|
|
2142
|
-
text
|
|
2143
|
-
}
|
|
2144
|
-
};
|
|
2145
|
-
}
|
|
2146
2233
|
yield {
|
|
2147
2234
|
type: "finish",
|
|
2148
2235
|
reason: {
|
|
@@ -2160,8 +2247,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2160
2247
|
if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
|
|
2161
2248
|
}
|
|
2162
2249
|
};
|
|
2163
|
-
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
|
|
2164
|
-
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
|
|
2250
|
+
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2251
|
+
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
|
|
2165
2252
|
}
|
|
2166
2253
|
//#endregion
|
|
2167
2254
|
//#region src/http.ts
|
|
@@ -2288,8 +2375,8 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2288
2375
|
}
|
|
2289
2376
|
//#endregion
|
|
2290
2377
|
//#region src/projection-routes.ts
|
|
2291
|
-
const MAX_SESSION_ID_CHARS = 1024;
|
|
2292
|
-
function sessionIdFromUrl(rawUrl) {
|
|
2378
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
2379
|
+
function sessionIdFromUrl$1(rawUrl) {
|
|
2293
2380
|
try {
|
|
2294
2381
|
const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
|
|
2295
2382
|
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
@@ -2297,32 +2384,36 @@ function sessionIdFromUrl(rawUrl) {
|
|
|
2297
2384
|
const encoded = pathname.slice(prefix.length);
|
|
2298
2385
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2299
2386
|
const sessionId = decodeURIComponent(encoded);
|
|
2300
|
-
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS) return void 0;
|
|
2387
|
+
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
|
|
2301
2388
|
return sessionId;
|
|
2302
2389
|
} catch {
|
|
2303
2390
|
return;
|
|
2304
2391
|
}
|
|
2305
2392
|
}
|
|
2306
2393
|
/** Register the browser-readable, credential-free sidecar projection endpoint. */
|
|
2307
|
-
function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => []) {
|
|
2394
|
+
function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
|
|
2308
2395
|
ctx.effect(() => ctx.webServer.register({
|
|
2309
2396
|
kind: "prefix",
|
|
2310
2397
|
path: CLAUDE_PROJECTION_PATH,
|
|
2311
2398
|
handler: async (req, res) => {
|
|
2312
2399
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
2313
2400
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
2314
|
-
const sessionId = sessionIdFromUrl(req.url);
|
|
2401
|
+
const sessionId = sessionIdFromUrl$1(req.url);
|
|
2315
2402
|
if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
|
|
2316
2403
|
try {
|
|
2317
2404
|
const projection = await sidecar.read(sessionId);
|
|
2405
|
+
const owned = ownsSession(sessionId);
|
|
2406
|
+
const repository = owned ? await repositoryForSession(sessionId) : void 0;
|
|
2318
2407
|
return json(res, 200, {
|
|
2319
2408
|
schemaVersion: projection.schemaVersion,
|
|
2320
2409
|
revision: projection.revision,
|
|
2321
|
-
owned
|
|
2410
|
+
owned,
|
|
2322
2411
|
commands: commandsForSession(sessionId),
|
|
2323
2412
|
activities: projection.activities,
|
|
2324
2413
|
...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
|
|
2325
|
-
...projection.tasks === void 0 ? {} : { tasks: projection.tasks }
|
|
2414
|
+
...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
|
|
2415
|
+
...repository === void 0 ? {} : { repository },
|
|
2416
|
+
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
2326
2417
|
});
|
|
2327
2418
|
} catch {
|
|
2328
2419
|
return json(res, 500, { error: "projection unavailable" });
|
|
@@ -2331,6 +2422,1394 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2331
2422
|
}), "dsh-claude: sidecar projection route");
|
|
2332
2423
|
}
|
|
2333
2424
|
//#endregion
|
|
2425
|
+
//#region src/repository-status.ts
|
|
2426
|
+
const MAX_OUTPUT_BYTES$2 = 65536;
|
|
2427
|
+
const MAX_DIFF_BYTES = 262144;
|
|
2428
|
+
const MAX_UNTRACKED_DIFFS = 50;
|
|
2429
|
+
const GIT_TIMEOUT_MS$2 = 5e3;
|
|
2430
|
+
const GH_TIMEOUT_MS = 8e3;
|
|
2431
|
+
const CACHE_TTL_MS = 5e3;
|
|
2432
|
+
const MAX_TEXT_CHARS = 1024;
|
|
2433
|
+
function bounded(value) {
|
|
2434
|
+
return value.trim().slice(0, MAX_TEXT_CHARS);
|
|
2435
|
+
}
|
|
2436
|
+
async function collect$2(handle) {
|
|
2437
|
+
const outcome = await handle.done;
|
|
2438
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
2439
|
+
return {
|
|
2440
|
+
exitCode: outcome.exitCode,
|
|
2441
|
+
stdout: stdout?.text ?? "",
|
|
2442
|
+
lossy: stdout?.lossy === true
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
|
|
2446
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
2447
|
+
return collect$2(runtime.spawn({
|
|
2448
|
+
argv: [executable, ...args],
|
|
2449
|
+
cwd,
|
|
2450
|
+
stdio: {
|
|
2451
|
+
stdin: "ignore",
|
|
2452
|
+
stdout: { maxBytes },
|
|
2453
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
|
|
2454
|
+
},
|
|
2455
|
+
graceMs: 1e3,
|
|
2456
|
+
signal,
|
|
2457
|
+
env: {}
|
|
2458
|
+
}));
|
|
2459
|
+
}
|
|
2460
|
+
function normalizedPath(value) {
|
|
2461
|
+
return value.replaceAll("\\", "/").replace(/\/+$/u, "").toLocaleLowerCase("en-US");
|
|
2462
|
+
}
|
|
2463
|
+
function parseGitStatus(output) {
|
|
2464
|
+
let branch;
|
|
2465
|
+
let detached = false;
|
|
2466
|
+
let dirty = false;
|
|
2467
|
+
let upstream = false;
|
|
2468
|
+
let ahead;
|
|
2469
|
+
let behind;
|
|
2470
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
2471
|
+
if (line.startsWith("# branch.head ")) {
|
|
2472
|
+
const head = bounded(line.slice(14));
|
|
2473
|
+
if (head === "(detached)") detached = true;
|
|
2474
|
+
else if (head.length > 0 && head !== "(unknown)") branch = head;
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
if (line.startsWith("# branch.upstream ")) {
|
|
2478
|
+
upstream = true;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (line.startsWith("# branch.ab ")) {
|
|
2482
|
+
const counts = /^# branch\.ab \+(\d+) -(\d+)$/u.exec(line);
|
|
2483
|
+
if (counts !== null) {
|
|
2484
|
+
ahead = Number(counts[1]);
|
|
2485
|
+
behind = Number(counts[2]);
|
|
2486
|
+
}
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (line.length > 0 && !line.startsWith("# ")) dirty = true;
|
|
2490
|
+
}
|
|
2491
|
+
return {
|
|
2492
|
+
...branch === void 0 ? {} : { branch },
|
|
2493
|
+
detached,
|
|
2494
|
+
dirty,
|
|
2495
|
+
upstream,
|
|
2496
|
+
...ahead === void 0 ? {} : { ahead },
|
|
2497
|
+
...behind === void 0 ? {} : { behind }
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function parseDiffNumstat(value) {
|
|
2501
|
+
let additions = 0;
|
|
2502
|
+
let deletions = 0;
|
|
2503
|
+
let files = 0;
|
|
2504
|
+
for (const line of value.split(/\r?\n/u)) {
|
|
2505
|
+
if (line.length === 0) continue;
|
|
2506
|
+
const [added, deleted] = line.split(" ");
|
|
2507
|
+
files += 1;
|
|
2508
|
+
if (added !== void 0 && /^\d+$/u.test(added)) additions += Number(added);
|
|
2509
|
+
if (deleted !== void 0 && /^\d+$/u.test(deleted)) deletions += Number(deleted);
|
|
2510
|
+
}
|
|
2511
|
+
return {
|
|
2512
|
+
additions,
|
|
2513
|
+
deletions,
|
|
2514
|
+
files
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
function parseGitHubRemote(value) {
|
|
2518
|
+
const remote = bounded(value);
|
|
2519
|
+
const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?$/iu.exec(remote);
|
|
2520
|
+
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
2521
|
+
return `${match[1]}/${match[2]}`;
|
|
2522
|
+
}
|
|
2523
|
+
function record$3(value) {
|
|
2524
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2525
|
+
}
|
|
2526
|
+
function aggregateChecks(value) {
|
|
2527
|
+
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
2528
|
+
let pending = false;
|
|
2529
|
+
for (const item of value) {
|
|
2530
|
+
const check = record$3(item);
|
|
2531
|
+
if (check === void 0) continue;
|
|
2532
|
+
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
2533
|
+
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
2534
|
+
const state = typeof check.state === "string" ? check.state.toUpperCase() : void 0;
|
|
2535
|
+
if ([
|
|
2536
|
+
"FAILURE",
|
|
2537
|
+
"ERROR",
|
|
2538
|
+
"CANCELLED",
|
|
2539
|
+
"TIMED_OUT",
|
|
2540
|
+
"ACTION_REQUIRED"
|
|
2541
|
+
].includes(conclusion ?? state ?? "")) return "failing";
|
|
2542
|
+
if (status !== void 0 && status !== "COMPLETED") pending = true;
|
|
2543
|
+
if (["PENDING", "EXPECTED"].includes(state ?? "")) pending = true;
|
|
2544
|
+
}
|
|
2545
|
+
return pending ? "pending" : "passing";
|
|
2546
|
+
}
|
|
2547
|
+
function reviewState(value) {
|
|
2548
|
+
if (value === "APPROVED") return "approved";
|
|
2549
|
+
if (value === "CHANGES_REQUESTED") return "changes-requested";
|
|
2550
|
+
if (value === "REVIEW_REQUIRED") return "review-required";
|
|
2551
|
+
return "none";
|
|
2552
|
+
}
|
|
2553
|
+
function parsePullRequest(value) {
|
|
2554
|
+
const input = record$3(value);
|
|
2555
|
+
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
2556
|
+
let url;
|
|
2557
|
+
try {
|
|
2558
|
+
url = new URL(input.url);
|
|
2559
|
+
} catch {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com") return void 0;
|
|
2563
|
+
const rawState = typeof input.state === "string" ? input.state.toUpperCase() : "";
|
|
2564
|
+
const state = input.mergedAt !== void 0 && input.mergedAt !== null ? "merged" : rawState === "OPEN" ? "open" : rawState === "CLOSED" ? "closed" : void 0;
|
|
2565
|
+
if (state === void 0) return void 0;
|
|
2566
|
+
return {
|
|
2567
|
+
number: Number(input.number),
|
|
2568
|
+
title: bounded(input.title),
|
|
2569
|
+
url: url.href,
|
|
2570
|
+
state,
|
|
2571
|
+
draft: input.isDraft === true,
|
|
2572
|
+
review: reviewState(input.reviewDecision),
|
|
2573
|
+
checks: aggregateChecks(input.statusCheckRollup),
|
|
2574
|
+
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
2575
|
+
...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
|
|
2576
|
+
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
2577
|
+
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
2578
|
+
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
var RepositoryStatusService = class {
|
|
2582
|
+
#runtime;
|
|
2583
|
+
#cacheTtlMs;
|
|
2584
|
+
#cache = /* @__PURE__ */ new Map();
|
|
2585
|
+
#lastReady = /* @__PURE__ */ new Map();
|
|
2586
|
+
#gitExecutable;
|
|
2587
|
+
#ghExecutable;
|
|
2588
|
+
constructor(runtime, cacheTtlMs = CACHE_TTL_MS) {
|
|
2589
|
+
this.#runtime = runtime;
|
|
2590
|
+
this.#cacheTtlMs = cacheTtlMs;
|
|
2591
|
+
}
|
|
2592
|
+
inspect(cwd) {
|
|
2593
|
+
const current = this.#cache.get(cwd);
|
|
2594
|
+
if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
|
|
2595
|
+
const value = this.#inspect(cwd).then((next) => this.#stabilize(cwd, next));
|
|
2596
|
+
this.#cache.set(cwd, {
|
|
2597
|
+
expiresAt: Date.now() + this.#cacheTtlMs,
|
|
2598
|
+
value
|
|
2599
|
+
});
|
|
2600
|
+
value.catch(() => this.#cache.delete(cwd));
|
|
2601
|
+
return value;
|
|
2602
|
+
}
|
|
2603
|
+
invalidate(cwd) {
|
|
2604
|
+
this.#cache.delete(cwd);
|
|
2605
|
+
this.#lastReady.delete(cwd);
|
|
2606
|
+
}
|
|
2607
|
+
dispose() {
|
|
2608
|
+
this.#cache.clear();
|
|
2609
|
+
this.#lastReady.clear();
|
|
2610
|
+
}
|
|
2611
|
+
#stabilize(cwd, next) {
|
|
2612
|
+
const previous = this.#lastReady.get(cwd);
|
|
2613
|
+
if (next.status !== "ready") return previous ?? next;
|
|
2614
|
+
const stable = previous?.status === "ready" && previous.root === next.root && previous.branch === next.branch ? {
|
|
2615
|
+
...next,
|
|
2616
|
+
...next.pullRequest === void 0 && previous.pullRequest !== void 0 ? { pullRequest: previous.pullRequest } : {},
|
|
2617
|
+
...next.pullRequest === void 0 && previous.pullRequest !== void 0 && previous.diff !== void 0 ? { diff: previous.diff } : next.diff === void 0 && previous.diff !== void 0 ? { diff: previous.diff } : {}
|
|
2618
|
+
} : next;
|
|
2619
|
+
this.#lastReady.set(cwd, stable);
|
|
2620
|
+
return stable;
|
|
2621
|
+
}
|
|
2622
|
+
async #git() {
|
|
2623
|
+
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
2624
|
+
return this.#gitExecutable;
|
|
2625
|
+
}
|
|
2626
|
+
async #gh() {
|
|
2627
|
+
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => void 0);
|
|
2628
|
+
return this.#ghExecutable;
|
|
2629
|
+
}
|
|
2630
|
+
async #inspect(cwd) {
|
|
2631
|
+
const safeCwd = bounded(cwd);
|
|
2632
|
+
let git;
|
|
2633
|
+
try {
|
|
2634
|
+
git = await this.#git();
|
|
2635
|
+
} catch {
|
|
2636
|
+
return {
|
|
2637
|
+
status: "unavailable",
|
|
2638
|
+
cwd: safeCwd
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
try {
|
|
2642
|
+
const paths = await run(this.#runtime, git, [
|
|
2643
|
+
"rev-parse",
|
|
2644
|
+
"--path-format=absolute",
|
|
2645
|
+
"--show-toplevel",
|
|
2646
|
+
"--absolute-git-dir",
|
|
2647
|
+
"--git-common-dir"
|
|
2648
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2649
|
+
if (paths.exitCode !== 0) return {
|
|
2650
|
+
status: "not-repository",
|
|
2651
|
+
cwd: safeCwd
|
|
2652
|
+
};
|
|
2653
|
+
const [rootValue, gitDirValue, commonDirValue] = paths.stdout.split(/\r?\n/u);
|
|
2654
|
+
const root = bounded(rootValue ?? "");
|
|
2655
|
+
const gitDir = bounded(gitDirValue ?? "");
|
|
2656
|
+
const commonDir = bounded(commonDirValue ?? "");
|
|
2657
|
+
if (root.length === 0 || gitDir.length === 0 || commonDir.length === 0) return {
|
|
2658
|
+
status: "unavailable",
|
|
2659
|
+
cwd: safeCwd
|
|
2660
|
+
};
|
|
2661
|
+
const statusResult = await run(this.#runtime, git, [
|
|
2662
|
+
"status",
|
|
2663
|
+
"--porcelain=v2",
|
|
2664
|
+
"--branch",
|
|
2665
|
+
"--untracked-files=normal"
|
|
2666
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2667
|
+
if (statusResult.exitCode !== 0) return {
|
|
2668
|
+
status: "unavailable",
|
|
2669
|
+
cwd: safeCwd
|
|
2670
|
+
};
|
|
2671
|
+
const status = parseGitStatus(statusResult.stdout);
|
|
2672
|
+
const remoteResult = await run(this.#runtime, git, [
|
|
2673
|
+
"remote",
|
|
2674
|
+
"get-url",
|
|
2675
|
+
"origin"
|
|
2676
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2677
|
+
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
2678
|
+
const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
|
|
2679
|
+
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
2680
|
+
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase) : {
|
|
2681
|
+
additions: 0,
|
|
2682
|
+
deletions: 0,
|
|
2683
|
+
files: 0,
|
|
2684
|
+
truncated: false
|
|
2685
|
+
};
|
|
2686
|
+
return {
|
|
2687
|
+
status: "ready",
|
|
2688
|
+
cwd: safeCwd,
|
|
2689
|
+
root,
|
|
2690
|
+
...status,
|
|
2691
|
+
worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
|
|
2692
|
+
...remote === void 0 ? {} : { remote },
|
|
2693
|
+
...pullRequest === void 0 ? {} : { pullRequest },
|
|
2694
|
+
...diff === void 0 ? {} : { diff }
|
|
2695
|
+
};
|
|
2696
|
+
} catch {
|
|
2697
|
+
return {
|
|
2698
|
+
status: "unavailable",
|
|
2699
|
+
cwd: safeCwd
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
async #mergeBase(cwd, git, baseBranch) {
|
|
2704
|
+
try {
|
|
2705
|
+
const result = await run(this.#runtime, git, [
|
|
2706
|
+
"merge-base",
|
|
2707
|
+
"HEAD",
|
|
2708
|
+
`refs/remotes/origin/${baseBranch}`
|
|
2709
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2710
|
+
if (result.exitCode !== 0 || result.lossy) return void 0;
|
|
2711
|
+
const oid = bounded(result.stdout);
|
|
2712
|
+
return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
|
|
2713
|
+
} catch {
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
async #diff(cwd, git, base) {
|
|
2718
|
+
try {
|
|
2719
|
+
const numstat = await run(this.#runtime, git, [
|
|
2720
|
+
"diff",
|
|
2721
|
+
"--no-ext-diff",
|
|
2722
|
+
"--numstat",
|
|
2723
|
+
base,
|
|
2724
|
+
"--"
|
|
2725
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2726
|
+
if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
|
|
2727
|
+
const summary = parseDiffNumstat(numstat.stdout);
|
|
2728
|
+
const patch = await run(this.#runtime, git, [
|
|
2729
|
+
"diff",
|
|
2730
|
+
"--no-ext-diff",
|
|
2731
|
+
"--no-color",
|
|
2732
|
+
"--unified=3",
|
|
2733
|
+
base,
|
|
2734
|
+
"--"
|
|
2735
|
+
], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
|
|
2736
|
+
if (patch.exitCode !== 0) return {
|
|
2737
|
+
...summary,
|
|
2738
|
+
truncated: true
|
|
2739
|
+
};
|
|
2740
|
+
const untracked = await this.#untrackedDiff(cwd, git);
|
|
2741
|
+
const combinedPatch = `${patch.stdout}${untracked.patch}`;
|
|
2742
|
+
return {
|
|
2743
|
+
additions: summary.additions + untracked.additions,
|
|
2744
|
+
deletions: summary.deletions,
|
|
2745
|
+
files: summary.files + untracked.files,
|
|
2746
|
+
...patch.lossy ? {} : { patch: combinedPatch.slice(0, MAX_DIFF_BYTES) },
|
|
2747
|
+
truncated: patch.lossy || untracked.truncated || combinedPatch.length > MAX_DIFF_BYTES
|
|
2748
|
+
};
|
|
2749
|
+
} catch {
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
async #untrackedDiff(cwd, git) {
|
|
2754
|
+
const listed = await run(this.#runtime, git, [
|
|
2755
|
+
"ls-files",
|
|
2756
|
+
"--others",
|
|
2757
|
+
"--exclude-standard",
|
|
2758
|
+
"-z"
|
|
2759
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2760
|
+
if (listed.exitCode !== 0 || listed.lossy) return {
|
|
2761
|
+
additions: 0,
|
|
2762
|
+
files: 0,
|
|
2763
|
+
patch: "",
|
|
2764
|
+
truncated: listed.lossy
|
|
2765
|
+
};
|
|
2766
|
+
const paths = listed.stdout.split("\0").filter((path) => path.length > 0);
|
|
2767
|
+
let additions = 0;
|
|
2768
|
+
let patch = "";
|
|
2769
|
+
let truncated = paths.length > MAX_UNTRACKED_DIFFS;
|
|
2770
|
+
for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
|
|
2771
|
+
const result = await run(this.#runtime, git, [
|
|
2772
|
+
"diff",
|
|
2773
|
+
"--no-ext-diff",
|
|
2774
|
+
"--no-color",
|
|
2775
|
+
"--unified=3",
|
|
2776
|
+
"--numstat",
|
|
2777
|
+
"--patch",
|
|
2778
|
+
"--no-index",
|
|
2779
|
+
"--",
|
|
2780
|
+
"/dev/null",
|
|
2781
|
+
path
|
|
2782
|
+
], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
|
|
2783
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
2784
|
+
truncated = true;
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const start = result.stdout.indexOf("diff --git ");
|
|
2788
|
+
additions += parseDiffNumstat(start >= 0 ? result.stdout.slice(0, start) : result.stdout).additions;
|
|
2789
|
+
if (result.lossy) truncated = true;
|
|
2790
|
+
else if (start >= 0) patch += result.stdout.slice(start);
|
|
2791
|
+
}
|
|
2792
|
+
return {
|
|
2793
|
+
additions,
|
|
2794
|
+
files: paths.length,
|
|
2795
|
+
patch,
|
|
2796
|
+
truncated
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
async #pullRequest(cwd, repository, branch) {
|
|
2800
|
+
const gh = await this.#gh();
|
|
2801
|
+
if (gh === void 0) return void 0;
|
|
2802
|
+
try {
|
|
2803
|
+
const result = await run(this.#runtime, gh, [
|
|
2804
|
+
"pr",
|
|
2805
|
+
"view",
|
|
2806
|
+
branch,
|
|
2807
|
+
"--repo",
|
|
2808
|
+
repository,
|
|
2809
|
+
"--json",
|
|
2810
|
+
"number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName"
|
|
2811
|
+
], cwd, GH_TIMEOUT_MS);
|
|
2812
|
+
if (result.exitCode !== 0) return void 0;
|
|
2813
|
+
return parsePullRequest(JSON.parse(result.stdout));
|
|
2814
|
+
} catch {
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
//#endregion
|
|
2820
|
+
//#region src/repository-setup.ts
|
|
2821
|
+
const MAX_OUTPUT_BYTES$1 = 131072;
|
|
2822
|
+
const GIT_TIMEOUT_MS$1 = 1e4;
|
|
2823
|
+
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
2824
|
+
const MAX_PATH_CHARS = 4096;
|
|
2825
|
+
const MAX_BRANCH_CHARS = 512;
|
|
2826
|
+
const LEASE_SCHEMA_VERSION = 1;
|
|
2827
|
+
var RepositorySetupError = class extends Error {
|
|
2828
|
+
code;
|
|
2829
|
+
constructor(code, message) {
|
|
2830
|
+
super(message);
|
|
2831
|
+
this.name = "RepositorySetupError";
|
|
2832
|
+
this.code = code;
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
async function collect$1(handle) {
|
|
2836
|
+
const outcome = await handle.done;
|
|
2837
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
2838
|
+
const stderr = handle.collected.stderr?.readFrom(0);
|
|
2839
|
+
return {
|
|
2840
|
+
exitCode: outcome.exitCode,
|
|
2841
|
+
stdout: stdout?.text ?? "",
|
|
2842
|
+
stderr: stderr?.text ?? "",
|
|
2843
|
+
lossy: stdout?.lossy === true || stderr?.lossy === true
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
function safePath(value) {
|
|
2847
|
+
if (value.length === 0 || value.length > MAX_PATH_CHARS || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
|
|
2848
|
+
return resolve(value);
|
|
2849
|
+
}
|
|
2850
|
+
function safeBranch(value) {
|
|
2851
|
+
if (value.length === 0 || value.length > MAX_BRANCH_CHARS || value.trim() !== value || /[\0\r\n]/u.test(value)) throw new RepositorySetupError("invalid-branch", "The branch name is invalid.");
|
|
2852
|
+
return value;
|
|
2853
|
+
}
|
|
2854
|
+
function parseCurrentBranch(value) {
|
|
2855
|
+
const branch = value.trim();
|
|
2856
|
+
return branch.length === 0 || branch === "HEAD" ? void 0 : branch;
|
|
2857
|
+
}
|
|
2858
|
+
function parseWorktreeBranches(value) {
|
|
2859
|
+
const occupied = /* @__PURE__ */ new Map();
|
|
2860
|
+
let path;
|
|
2861
|
+
for (const line of `${value}\n`.split(/\r?\n/u)) if (line.startsWith("worktree ")) path = line.slice(9);
|
|
2862
|
+
else if (line.startsWith("branch refs/heads/") && path !== void 0) occupied.set(line.slice(18), path);
|
|
2863
|
+
else if (line.length === 0) path = void 0;
|
|
2864
|
+
return occupied;
|
|
2865
|
+
}
|
|
2866
|
+
function slug(value, fallback) {
|
|
2867
|
+
return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
|
|
2868
|
+
}
|
|
2869
|
+
function lease(value) {
|
|
2870
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2871
|
+
const input = value;
|
|
2872
|
+
if (typeof input.id !== "string" || typeof input.root !== "string" || typeof input.path !== "string" || typeof input.branch !== "string" || typeof input.createdAt !== "string" || input.pluginGeneratedBranch !== void 0 && typeof input.pluginGeneratedBranch !== "boolean" || input.sessionId !== void 0 && typeof input.sessionId !== "string") return void 0;
|
|
2873
|
+
return {
|
|
2874
|
+
id: input.id,
|
|
2875
|
+
root: input.root,
|
|
2876
|
+
path: input.path,
|
|
2877
|
+
branch: input.branch,
|
|
2878
|
+
createdAt: input.createdAt,
|
|
2879
|
+
pluginGeneratedBranch: input.pluginGeneratedBranch === true,
|
|
2880
|
+
...input.sessionId === void 0 ? {} : { sessionId: input.sessionId }
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
var RepositorySetupService = class {
|
|
2884
|
+
#runtime;
|
|
2885
|
+
#leasePath;
|
|
2886
|
+
#worktreeRoot;
|
|
2887
|
+
#branchPrefix;
|
|
2888
|
+
#gitPath;
|
|
2889
|
+
#pending = Promise.resolve();
|
|
2890
|
+
constructor(runtime, options = {}) {
|
|
2891
|
+
this.#runtime = runtime;
|
|
2892
|
+
this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
|
|
2893
|
+
this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
|
|
2894
|
+
this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
|
|
2895
|
+
}
|
|
2896
|
+
async listBranches(cwd) {
|
|
2897
|
+
const git = await this.#git();
|
|
2898
|
+
const root = await this.#repositoryRoot(git, safePath(cwd));
|
|
2899
|
+
const [status, refs, remoteRefs] = await Promise.all([
|
|
2900
|
+
this.#run(git, [
|
|
2901
|
+
"status",
|
|
2902
|
+
"--porcelain=v2",
|
|
2903
|
+
"--branch",
|
|
2904
|
+
"--untracked-files=normal"
|
|
2905
|
+
], root),
|
|
2906
|
+
this.#run(git, [
|
|
2907
|
+
"for-each-ref",
|
|
2908
|
+
"--format=%(refname:short)",
|
|
2909
|
+
"refs/heads"
|
|
2910
|
+
], root),
|
|
2911
|
+
this.#run(git, [
|
|
2912
|
+
"for-each-ref",
|
|
2913
|
+
"--format=%(refname:short) %(symref)",
|
|
2914
|
+
"refs/remotes"
|
|
2915
|
+
], root)
|
|
2916
|
+
]);
|
|
2917
|
+
if (status.exitCode !== 0 || status.lossy || refs.exitCode !== 0 || refs.lossy || remoteRefs.exitCode !== 0 || remoteRefs.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
|
|
2918
|
+
const current = status.stdout.split(/\r?\n/u).find((line) => line.startsWith("# branch.head "))?.slice(14);
|
|
2919
|
+
const branches = refs.stdout.split(/\r?\n/u).filter(Boolean).sort((left, right) => left.localeCompare(right));
|
|
2920
|
+
const remoteBranches = remoteRefs.stdout.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)).filter((parts) => parts[0] !== void 0 && parts[0].length > 0 && parts.length === 1).map((parts) => parts[0]).sort((left, right) => left.localeCompare(right));
|
|
2921
|
+
const currentBranch = current === void 0 ? void 0 : parseCurrentBranch(current);
|
|
2922
|
+
return {
|
|
2923
|
+
root,
|
|
2924
|
+
...currentBranch === void 0 ? {} : { current: currentBranch },
|
|
2925
|
+
dirty: status.stdout.split(/\r?\n/u).some((line) => line.length > 0 && !line.startsWith("# ")),
|
|
2926
|
+
branches,
|
|
2927
|
+
remoteBranches
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}) {
|
|
2931
|
+
progress("inspecting");
|
|
2932
|
+
const branch = safeBranch(branchValue);
|
|
2933
|
+
const info = await this.listBranches(cwd);
|
|
2934
|
+
const local = info.branches.includes(branch);
|
|
2935
|
+
const remote = info.remoteBranches.includes(branch);
|
|
2936
|
+
if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
|
|
2937
|
+
const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
|
|
2938
|
+
if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, progress);
|
|
2939
|
+
progress("switching-branch");
|
|
2940
|
+
return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
|
|
2941
|
+
}
|
|
2942
|
+
bindLease(leaseId, sessionId) {
|
|
2943
|
+
if (leaseId.length === 0 || leaseId.length > 128 || sessionId.length === 0 || sessionId.length > 1024) return Promise.reject(new RepositorySetupError("invalid-lease", "The worktree lease is invalid."));
|
|
2944
|
+
return this.#serialize(async () => {
|
|
2945
|
+
const leases = await this.#readLeases();
|
|
2946
|
+
const index = leases.findIndex((item) => item.id === leaseId);
|
|
2947
|
+
if (index < 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
|
|
2948
|
+
const existing = leases[index];
|
|
2949
|
+
if (existing === void 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
|
|
2950
|
+
leases[index] = {
|
|
2951
|
+
...existing,
|
|
2952
|
+
sessionId
|
|
2953
|
+
};
|
|
2954
|
+
await this.#writeLeases(leases);
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
cleanupSession(sessionId) {
|
|
2958
|
+
return this.#serialize(async () => {
|
|
2959
|
+
const leases = await this.#readLeases();
|
|
2960
|
+
const retained = [];
|
|
2961
|
+
let changed = false;
|
|
2962
|
+
const git = await this.#git();
|
|
2963
|
+
for (const item of leases) {
|
|
2964
|
+
if (item.sessionId !== sessionId) {
|
|
2965
|
+
retained.push(item);
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
try {
|
|
2969
|
+
const status = await this.#run(git, [
|
|
2970
|
+
"status",
|
|
2971
|
+
"--porcelain=v1",
|
|
2972
|
+
"--untracked-files=normal"
|
|
2973
|
+
], item.path);
|
|
2974
|
+
if (status.exitCode !== 0 || status.lossy || status.stdout.trim().length > 0) {
|
|
2975
|
+
retained.push(item);
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
if ((await this.#run(git, [
|
|
2979
|
+
"worktree",
|
|
2980
|
+
"remove",
|
|
2981
|
+
"--",
|
|
2982
|
+
item.path
|
|
2983
|
+
], item.root)).exitCode !== 0) {
|
|
2984
|
+
retained.push(item);
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
if (item.pluginGeneratedBranch) await this.#run(git, [
|
|
2988
|
+
"branch",
|
|
2989
|
+
"-D",
|
|
2990
|
+
"--",
|
|
2991
|
+
item.branch
|
|
2992
|
+
], item.root).catch(() => void 0);
|
|
2993
|
+
changed = true;
|
|
2994
|
+
} catch {
|
|
2995
|
+
retained.push(item);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
if (changed) await this.#writeLeases(retained);
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
async #checkoutRemote(info, remoteBranch) {
|
|
3002
|
+
const separator = remoteBranch.indexOf("/");
|
|
3003
|
+
if (separator <= 0 || separator === remoteBranch.length - 1) throw new RepositorySetupError("invalid-branch", "The remote-tracking branch name is invalid.");
|
|
3004
|
+
const localBranch = safeBranch(remoteBranch.slice(separator + 1));
|
|
3005
|
+
if (info.branches.includes(localBranch)) {
|
|
3006
|
+
const git = await this.#git();
|
|
3007
|
+
const upstream = await this.#run(git, [
|
|
3008
|
+
"for-each-ref",
|
|
3009
|
+
"--format=%(upstream:short)",
|
|
3010
|
+
`refs/heads/${localBranch}`
|
|
3011
|
+
], info.root);
|
|
3012
|
+
if (upstream.exitCode !== 0 || upstream.lossy) throw new RepositorySetupError("repository-unavailable", "The local branch tracking state is unavailable.");
|
|
3013
|
+
if (upstream.stdout.trim() !== remoteBranch) throw new RepositorySetupError("branch-tracking-conflict", `The local branch ${localBranch} does not track ${remoteBranch}.`);
|
|
3014
|
+
return this.#checkout(info, localBranch);
|
|
3015
|
+
}
|
|
3016
|
+
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
3017
|
+
const git = await this.#git();
|
|
3018
|
+
if ((await this.#run(git, [
|
|
3019
|
+
"switch",
|
|
3020
|
+
"--track",
|
|
3021
|
+
"-c",
|
|
3022
|
+
localBranch,
|
|
3023
|
+
`refs/remotes/${remoteBranch}`
|
|
3024
|
+
], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not create the local tracking branch.");
|
|
3025
|
+
return {
|
|
3026
|
+
mode: "checkout",
|
|
3027
|
+
root: info.root,
|
|
3028
|
+
path: info.root,
|
|
3029
|
+
branch: localBranch
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
async #checkout(info, branch) {
|
|
3033
|
+
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
3034
|
+
if (info.current !== branch) {
|
|
3035
|
+
const git = await this.#git();
|
|
3036
|
+
const worktrees = await this.#run(git, [
|
|
3037
|
+
"worktree",
|
|
3038
|
+
"list",
|
|
3039
|
+
"--porcelain"
|
|
3040
|
+
], info.root);
|
|
3041
|
+
if (worktrees.exitCode !== 0 || worktrees.lossy) throw new RepositorySetupError("repository-unavailable", "Worktree state is unavailable.");
|
|
3042
|
+
const occupiedPath = parseWorktreeBranches(worktrees.stdout).get(branch);
|
|
3043
|
+
if (occupiedPath !== void 0 && resolve(occupiedPath) !== info.root) throw new RepositorySetupError("branch-occupied", "The selected branch is already checked out in another worktree.");
|
|
3044
|
+
if ((await this.#run(git, [
|
|
3045
|
+
"switch",
|
|
3046
|
+
"--",
|
|
3047
|
+
branch
|
|
3048
|
+
], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not switch to the selected branch.");
|
|
3049
|
+
}
|
|
3050
|
+
return {
|
|
3051
|
+
mode: "checkout",
|
|
3052
|
+
root: info.root,
|
|
3053
|
+
path: info.root,
|
|
3054
|
+
branch
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
async #createWorktree(root, baseBranch, baseRef, explicitBranchName, progress) {
|
|
3058
|
+
const git = await this.#git();
|
|
3059
|
+
progress("fetching");
|
|
3060
|
+
const fetched = await this.#run(git, [
|
|
3061
|
+
"-c",
|
|
3062
|
+
"credential.interactive=never",
|
|
3063
|
+
"fetch",
|
|
3064
|
+
"--all",
|
|
3065
|
+
"--prune"
|
|
3066
|
+
], root, GIT_FETCH_TIMEOUT_MS);
|
|
3067
|
+
if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references before creating the worktree.");
|
|
3068
|
+
const suffix = randomUUID().slice(0, 8);
|
|
3069
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
|
|
3070
|
+
const branch = explicitBranchName ?? `${safeBranch(await this.#branchPrefix())}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
|
|
3071
|
+
const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
|
|
3072
|
+
progress("creating-worktree");
|
|
3073
|
+
await mkdir(this.#worktreeRoot, { recursive: true });
|
|
3074
|
+
if ((await this.#run(git, [
|
|
3075
|
+
"worktree",
|
|
3076
|
+
"add",
|
|
3077
|
+
"-b",
|
|
3078
|
+
branch,
|
|
3079
|
+
path,
|
|
3080
|
+
baseRef
|
|
3081
|
+
], root)).exitCode !== 0) throw new RepositorySetupError("worktree-failed", "Git could not create the worktree.");
|
|
3082
|
+
const item = {
|
|
3083
|
+
id: randomUUID(),
|
|
3084
|
+
root,
|
|
3085
|
+
path,
|
|
3086
|
+
branch,
|
|
3087
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3088
|
+
pluginGeneratedBranch: explicitBranchName === void 0
|
|
3089
|
+
};
|
|
3090
|
+
progress("saving-worktree");
|
|
3091
|
+
try {
|
|
3092
|
+
await this.#serialize(async () => {
|
|
3093
|
+
const leases = await this.#readLeases();
|
|
3094
|
+
await this.#writeLeases([...leases, item]);
|
|
3095
|
+
});
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
await this.#run(git, [
|
|
3098
|
+
"worktree",
|
|
3099
|
+
"remove",
|
|
3100
|
+
"--force",
|
|
3101
|
+
"--",
|
|
3102
|
+
path
|
|
3103
|
+
], root).catch(() => void 0);
|
|
3104
|
+
throw error;
|
|
3105
|
+
}
|
|
3106
|
+
return {
|
|
3107
|
+
mode: "worktree",
|
|
3108
|
+
root,
|
|
3109
|
+
path,
|
|
3110
|
+
branch,
|
|
3111
|
+
leaseId: item.id
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
async #repositoryRoot(git, cwd) {
|
|
3115
|
+
const result = await this.#run(git, [
|
|
3116
|
+
"rev-parse",
|
|
3117
|
+
"--path-format=absolute",
|
|
3118
|
+
"--show-toplevel"
|
|
3119
|
+
], cwd);
|
|
3120
|
+
const root = result.stdout.trim();
|
|
3121
|
+
if (result.exitCode !== 0 || result.lossy || root.length === 0 || !isAbsolute(root)) throw new RepositorySetupError("not-repository", "The workspace is not a Git repository.");
|
|
3122
|
+
return resolve(root);
|
|
3123
|
+
}
|
|
3124
|
+
#git() {
|
|
3125
|
+
this.#gitPath ??= this.#runtime.resolveExecutable("git");
|
|
3126
|
+
return this.#gitPath;
|
|
3127
|
+
}
|
|
3128
|
+
async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
|
|
3129
|
+
return collect$1(this.#runtime.spawn({
|
|
3130
|
+
argv: [executable, ...args],
|
|
3131
|
+
cwd,
|
|
3132
|
+
stdio: {
|
|
3133
|
+
stdin: "ignore",
|
|
3134
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
|
|
3135
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
3136
|
+
},
|
|
3137
|
+
graceMs: 1e3,
|
|
3138
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3139
|
+
env: {}
|
|
3140
|
+
}));
|
|
3141
|
+
}
|
|
3142
|
+
async #readLeases() {
|
|
3143
|
+
try {
|
|
3144
|
+
const parsed = JSON.parse(await readFile(this.#leasePath, "utf8"));
|
|
3145
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
3146
|
+
const input = parsed;
|
|
3147
|
+
if (input.schemaVersion !== LEASE_SCHEMA_VERSION || !Array.isArray(input.leases)) return [];
|
|
3148
|
+
return input.leases.map(lease).filter((item) => item !== void 0);
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
if (error.code === "ENOENT") return [];
|
|
3151
|
+
throw error;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
async #writeLeases(leases) {
|
|
3155
|
+
await mkdir(resolve(this.#leasePath, ".."), { recursive: true });
|
|
3156
|
+
const temporary = `${this.#leasePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
3157
|
+
const document = {
|
|
3158
|
+
schemaVersion: LEASE_SCHEMA_VERSION,
|
|
3159
|
+
leases
|
|
3160
|
+
};
|
|
3161
|
+
try {
|
|
3162
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 384 });
|
|
3163
|
+
await rename(temporary, this.#leasePath);
|
|
3164
|
+
} finally {
|
|
3165
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
#serialize(operation) {
|
|
3169
|
+
const result = this.#pending.then(operation, operation);
|
|
3170
|
+
this.#pending = result.then(() => void 0, () => void 0);
|
|
3171
|
+
return result;
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
//#endregion
|
|
3175
|
+
//#region src/repository-actions.ts
|
|
3176
|
+
const MAX_OUTPUT_BYTES = 262144;
|
|
3177
|
+
const MAX_PATCH_CHARS = 65536;
|
|
3178
|
+
const MAX_MESSAGE_CHARS = 512;
|
|
3179
|
+
const MAX_PR_TEXT_CHARS = 8192;
|
|
3180
|
+
const MAX_UNPUSHED_COMMITS = 20;
|
|
3181
|
+
const GIT_TIMEOUT_MS = 15e3;
|
|
3182
|
+
const REMOTE_TIMEOUT_MS = 6e4;
|
|
3183
|
+
const GENERATE_TIMEOUT_MS = 6e4;
|
|
3184
|
+
var RepositoryActionError = class extends Error {
|
|
3185
|
+
code;
|
|
3186
|
+
commit;
|
|
3187
|
+
constructor(code, message, commit) {
|
|
3188
|
+
super(message);
|
|
3189
|
+
this.name = "RepositoryActionError";
|
|
3190
|
+
this.code = code;
|
|
3191
|
+
if (commit !== void 0) this.commit = commit;
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
async function collect(handle) {
|
|
3195
|
+
const outcome = await handle.done;
|
|
3196
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
3197
|
+
const stderr = handle.collected.stderr?.readFrom(0);
|
|
3198
|
+
return {
|
|
3199
|
+
exitCode: outcome.exitCode,
|
|
3200
|
+
stdout: stdout?.text ?? "",
|
|
3201
|
+
stderr: stderr?.text ?? "",
|
|
3202
|
+
lossy: stdout?.lossy === true || stderr?.lossy === true
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function safeText(value, maximum, label) {
|
|
3206
|
+
const text = value.trim();
|
|
3207
|
+
if (text.length === 0 || text.length > maximum || /[\0\r]/u.test(text)) throw new RepositoryActionError("invalid-request", `${label} is invalid.`);
|
|
3208
|
+
return text;
|
|
3209
|
+
}
|
|
3210
|
+
function isProtectedWarpPath(path) {
|
|
3211
|
+
return basename(path.replaceAll("\\", "/")).toLocaleLowerCase("en-US") === "warp.md";
|
|
3212
|
+
}
|
|
3213
|
+
function parseRepositoryActionStatus(output) {
|
|
3214
|
+
const files = /* @__PURE__ */ new Map();
|
|
3215
|
+
const records = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/u);
|
|
3216
|
+
for (let position = 0; position < records.length; position += 1) {
|
|
3217
|
+
const line = records[position] ?? "";
|
|
3218
|
+
if (line.length < 4) continue;
|
|
3219
|
+
const index = line[0] ?? " ";
|
|
3220
|
+
const worktree = line[1] ?? " ";
|
|
3221
|
+
let path = line.slice(3);
|
|
3222
|
+
const rename = path.lastIndexOf(" -> ");
|
|
3223
|
+
if (rename >= 0) path = path.slice(rename + 4);
|
|
3224
|
+
if (output.includes("\0") && (index === "R" || index === "C" || worktree === "R" || worktree === "C")) position += 1;
|
|
3225
|
+
if (path.length === 0 || path.includes("\0") || isProtectedWarpPath(path)) continue;
|
|
3226
|
+
files.set(path, {
|
|
3227
|
+
path,
|
|
3228
|
+
staged: index !== " " && index !== "?",
|
|
3229
|
+
unstaged: worktree !== " " && worktree !== "?",
|
|
3230
|
+
untracked: index === "?" && worktree === "?"
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
3234
|
+
}
|
|
3235
|
+
function fallbackCommitMessage(files) {
|
|
3236
|
+
if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
|
|
3237
|
+
return `Update ${files.length} repository files`;
|
|
3238
|
+
}
|
|
3239
|
+
function normalizedGeneratedMessage(value, fallback) {
|
|
3240
|
+
const first = value.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
|
|
3241
|
+
if (first === void 0) return fallback;
|
|
3242
|
+
const message = first.replace(/^['"`]+|['"`]+$/gu, "").replace(/[\0\r\n]/gu, " ").trim();
|
|
3243
|
+
return message.length === 0 || message.length > 72 ? fallback : message;
|
|
3244
|
+
}
|
|
3245
|
+
function validPrUrl(value) {
|
|
3246
|
+
try {
|
|
3247
|
+
const url = new URL(value.trim());
|
|
3248
|
+
return url.protocol === "https:" && url.hostname === "github.com" ? url.href : void 0;
|
|
3249
|
+
} catch {
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function validPullRequestBody(value) {
|
|
3254
|
+
const body = value.trim();
|
|
3255
|
+
const match = /^Summary:\s+([^\r\n]+)\r?\n\r?\nChanges:\s*\r?\n([\s\S]+)$/u.exec(body);
|
|
3256
|
+
const summary = match?.[1]?.trim();
|
|
3257
|
+
const changes = match?.[2]?.trim();
|
|
3258
|
+
if (summary === void 0 || summary.length === 0 || changes === void 0 || changes.length === 0) return false;
|
|
3259
|
+
return !/^#{1,6}\s|^[A-Za-z][A-Za-z ]+:\s*$/mu.test(changes);
|
|
3260
|
+
}
|
|
3261
|
+
var RepositoryActionService = class {
|
|
3262
|
+
#runtime;
|
|
3263
|
+
#claudeExecutable;
|
|
3264
|
+
#invalidate;
|
|
3265
|
+
#gitExecutable;
|
|
3266
|
+
#ghExecutable;
|
|
3267
|
+
#pending = Promise.resolve();
|
|
3268
|
+
constructor(runtime, claudeExecutable, invalidate = () => {}) {
|
|
3269
|
+
this.#runtime = runtime;
|
|
3270
|
+
this.#claudeExecutable = claudeExecutable;
|
|
3271
|
+
this.#invalidate = invalidate;
|
|
3272
|
+
}
|
|
3273
|
+
preview(cwd) {
|
|
3274
|
+
return this.#preview(cwd);
|
|
3275
|
+
}
|
|
3276
|
+
async generateMessage(cwd, fingerprint) {
|
|
3277
|
+
const preview = await this.#preview(cwd);
|
|
3278
|
+
if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
3279
|
+
const fallback = fallbackCommitMessage(preview.files);
|
|
3280
|
+
const prompt = [
|
|
3281
|
+
"Write one concise English git commit subject (imperative mood, maximum 72 characters).",
|
|
3282
|
+
"Return only the subject without quotes, markdown, body, or explanation.",
|
|
3283
|
+
`Files: ${preview.files.map((file) => file.path).join(", ")}`,
|
|
3284
|
+
`Diff:\n${preview.patch.slice(0, 24576)}`
|
|
3285
|
+
].join("\n");
|
|
3286
|
+
try {
|
|
3287
|
+
const result = await this.#run(this.#claudeExecutable, [
|
|
3288
|
+
"-p",
|
|
3289
|
+
"--tools",
|
|
3290
|
+
"",
|
|
3291
|
+
"--output-format",
|
|
3292
|
+
"text",
|
|
3293
|
+
prompt
|
|
3294
|
+
], preview.root, GENERATE_TIMEOUT_MS);
|
|
3295
|
+
return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
|
|
3296
|
+
} catch {
|
|
3297
|
+
return fallback;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
execute(cwd, request) {
|
|
3301
|
+
const operation = this.#pending.then(() => this.#execute(cwd, request));
|
|
3302
|
+
this.#pending = operation.then(() => void 0, () => void 0);
|
|
3303
|
+
return operation;
|
|
3304
|
+
}
|
|
3305
|
+
async #execute(cwd, request) {
|
|
3306
|
+
const before = await this.#preview(cwd);
|
|
3307
|
+
if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
3308
|
+
if (request.action === "push") {
|
|
3309
|
+
const git = await this.#git();
|
|
3310
|
+
try {
|
|
3311
|
+
await this.#push(git, before.root, before.branch);
|
|
3312
|
+
} catch (error) {
|
|
3313
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.");
|
|
3314
|
+
}
|
|
3315
|
+
this.#invalidate(before.root);
|
|
3316
|
+
return {
|
|
3317
|
+
commit: before.head,
|
|
3318
|
+
pushed: true
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
|
|
3322
|
+
if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
|
|
3323
|
+
const git = await this.#git();
|
|
3324
|
+
let oid = before.head;
|
|
3325
|
+
if (before.files.length > 0) {
|
|
3326
|
+
await this.#rejectStagedWarp(git, before.root);
|
|
3327
|
+
if (request.includeUnstaged) {
|
|
3328
|
+
const paths = before.files.filter((file) => file.unstaged || file.untracked).map((file) => file.path);
|
|
3329
|
+
if (paths.length > 0) await this.#mustRun(git, [
|
|
3330
|
+
"add",
|
|
3331
|
+
"--",
|
|
3332
|
+
...paths
|
|
3333
|
+
], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
|
|
3334
|
+
}
|
|
3335
|
+
await this.#rejectStagedWarp(git, before.root);
|
|
3336
|
+
const staged = await this.#run(git, [
|
|
3337
|
+
"diff",
|
|
3338
|
+
"--cached",
|
|
3339
|
+
"--quiet",
|
|
3340
|
+
"--exit-code",
|
|
3341
|
+
"--"
|
|
3342
|
+
], before.root, GIT_TIMEOUT_MS);
|
|
3343
|
+
if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
|
|
3344
|
+
if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
|
|
3345
|
+
await this.#mustRun(git, [
|
|
3346
|
+
"commit",
|
|
3347
|
+
"-m",
|
|
3348
|
+
message,
|
|
3349
|
+
"--"
|
|
3350
|
+
], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
|
|
3351
|
+
oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
|
|
3352
|
+
this.#invalidate(before.root);
|
|
3353
|
+
if (request.action === "commit") return {
|
|
3354
|
+
commit: oid,
|
|
3355
|
+
pushed: false
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
try {
|
|
3359
|
+
await this.#push(git, before.root, before.branch);
|
|
3360
|
+
} catch (error) {
|
|
3361
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", oid);
|
|
3362
|
+
}
|
|
3363
|
+
this.#invalidate(before.root);
|
|
3364
|
+
if (request.action === "commit-push") return {
|
|
3365
|
+
commit: oid,
|
|
3366
|
+
pushed: true
|
|
3367
|
+
};
|
|
3368
|
+
const title = safeText(request.prTitle ?? message, 256, "Pull request title");
|
|
3369
|
+
const body = safeText(request.prBody ?? "", MAX_PR_TEXT_CHARS, "Pull request description");
|
|
3370
|
+
if (!validPullRequestBody(body)) throw new RepositoryActionError("invalid-pr-description", "Pull request description must contain only Summary and Changes sections.", oid);
|
|
3371
|
+
let gh;
|
|
3372
|
+
try {
|
|
3373
|
+
gh = await this.#gh();
|
|
3374
|
+
} catch (error) {
|
|
3375
|
+
throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.", oid);
|
|
3376
|
+
}
|
|
3377
|
+
const args = [
|
|
3378
|
+
"pr",
|
|
3379
|
+
"create",
|
|
3380
|
+
"--title",
|
|
3381
|
+
title,
|
|
3382
|
+
"--body",
|
|
3383
|
+
body
|
|
3384
|
+
];
|
|
3385
|
+
if (request.draft !== false) args.push("--draft");
|
|
3386
|
+
if (request.baseBranch !== void 0) args.push("--base", safeText(request.baseBranch, 512, "Base branch"));
|
|
3387
|
+
const created = await this.#run(gh, args, before.root, REMOTE_TIMEOUT_MS);
|
|
3388
|
+
const url = created.exitCode === 0 && !created.lossy ? validPrUrl(created.stdout) : void 0;
|
|
3389
|
+
if (url === void 0) throw new RepositoryActionError("pr-failed", "The pull request could not be created.", oid);
|
|
3390
|
+
return {
|
|
3391
|
+
commit: oid,
|
|
3392
|
+
pushed: true,
|
|
3393
|
+
pullRequestUrl: url
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
async #preview(cwd) {
|
|
3397
|
+
const git = await this.#git();
|
|
3398
|
+
const root = (await this.#mustRun(git, [
|
|
3399
|
+
"rev-parse",
|
|
3400
|
+
"--path-format=absolute",
|
|
3401
|
+
"--show-toplevel"
|
|
3402
|
+
], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
|
|
3403
|
+
const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
|
|
3404
|
+
this.#run(git, [
|
|
3405
|
+
"symbolic-ref",
|
|
3406
|
+
"--quiet",
|
|
3407
|
+
"--short",
|
|
3408
|
+
"HEAD"
|
|
3409
|
+
], root, GIT_TIMEOUT_MS),
|
|
3410
|
+
this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
|
|
3411
|
+
this.#run(git, [
|
|
3412
|
+
"status",
|
|
3413
|
+
"--porcelain=v1",
|
|
3414
|
+
"-z",
|
|
3415
|
+
"--untracked-files=all"
|
|
3416
|
+
], root, GIT_TIMEOUT_MS),
|
|
3417
|
+
this.#run(git, [
|
|
3418
|
+
"diff",
|
|
3419
|
+
"--cached",
|
|
3420
|
+
"--no-ext-diff",
|
|
3421
|
+
"--no-color",
|
|
3422
|
+
"--unified=3",
|
|
3423
|
+
"--",
|
|
3424
|
+
":(exclude)WARP.md",
|
|
3425
|
+
":(exclude)**/WARP.md"
|
|
3426
|
+
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
|
|
3427
|
+
this.#run(git, [
|
|
3428
|
+
"diff",
|
|
3429
|
+
"--no-ext-diff",
|
|
3430
|
+
"--no-color",
|
|
3431
|
+
"--unified=3",
|
|
3432
|
+
"--",
|
|
3433
|
+
":(exclude)WARP.md",
|
|
3434
|
+
":(exclude)**/WARP.md"
|
|
3435
|
+
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
|
|
3436
|
+
]);
|
|
3437
|
+
if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
|
|
3438
|
+
if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
3439
|
+
const files = parseRepositoryActionStatus(statusResult.stdout);
|
|
3440
|
+
const patch = `${stagedPatch.stdout}${stagedPatch.stdout.length > 0 && unstagedPatch.stdout.length > 0 ? "\n" : ""}${unstagedPatch.stdout}`.slice(0, MAX_PATCH_CHARS);
|
|
3441
|
+
const branch = branchResult.stdout.trim();
|
|
3442
|
+
const head = headResult.stdout.trim();
|
|
3443
|
+
const fingerprint = createHash("sha256").update([
|
|
3444
|
+
head,
|
|
3445
|
+
branch,
|
|
3446
|
+
statusResult.stdout,
|
|
3447
|
+
stagedPatch.stdout,
|
|
3448
|
+
unstagedPatch.stdout
|
|
3449
|
+
].join("\0")).digest("hex");
|
|
3450
|
+
const upstreamResult = await this.#run(git, [
|
|
3451
|
+
"rev-parse",
|
|
3452
|
+
"--abbrev-ref",
|
|
3453
|
+
"--symbolic-full-name",
|
|
3454
|
+
"@{upstream}"
|
|
3455
|
+
], root, GIT_TIMEOUT_MS);
|
|
3456
|
+
const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
|
|
3457
|
+
const logResult = await this.#run(git, [
|
|
3458
|
+
"log",
|
|
3459
|
+
"--format=%H%x09%s",
|
|
3460
|
+
`-n`,
|
|
3461
|
+
String(21),
|
|
3462
|
+
upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
|
|
3463
|
+
"--"
|
|
3464
|
+
], root, GIT_TIMEOUT_MS);
|
|
3465
|
+
const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
|
|
3466
|
+
const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
|
|
3467
|
+
const tab = line.indexOf(" ");
|
|
3468
|
+
const hash = line.slice(0, tab);
|
|
3469
|
+
return /^[0-9a-f]{40}$/iu.test(hash) ? [{
|
|
3470
|
+
hash,
|
|
3471
|
+
subject: line.slice(tab + 1).slice(0, 140)
|
|
3472
|
+
}] : [];
|
|
3473
|
+
});
|
|
3474
|
+
return {
|
|
3475
|
+
root,
|
|
3476
|
+
branch,
|
|
3477
|
+
head,
|
|
3478
|
+
fingerprint,
|
|
3479
|
+
files,
|
|
3480
|
+
patch,
|
|
3481
|
+
truncated: stagedPatch.lossy || unstagedPatch.lossy || stagedPatch.stdout.length + unstagedPatch.stdout.length > MAX_PATCH_CHARS,
|
|
3482
|
+
hasStaged: files.some((file) => file.staged),
|
|
3483
|
+
hasUnstaged: files.some((file) => file.unstaged),
|
|
3484
|
+
hasUntracked: files.some((file) => file.untracked),
|
|
3485
|
+
...upstream === void 0 ? {} : { upstream },
|
|
3486
|
+
unpushedCommits,
|
|
3487
|
+
unpushedTruncated: commitLines.length > MAX_UNPUSHED_COMMITS
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
async #rejectStagedWarp(git, cwd) {
|
|
3491
|
+
const staged = await this.#run(git, [
|
|
3492
|
+
"diff",
|
|
3493
|
+
"--cached",
|
|
3494
|
+
"--name-only",
|
|
3495
|
+
"--"
|
|
3496
|
+
], cwd, GIT_TIMEOUT_MS);
|
|
3497
|
+
if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
|
|
3498
|
+
if (staged.stdout.split(/\r?\n/u).some((path) => path.length > 0 && isProtectedWarpPath(path))) throw new RepositoryActionError("protected-warp-file", "WARP.md files cannot be committed. Unstage them before continuing.");
|
|
3499
|
+
}
|
|
3500
|
+
async #push(git, cwd, branch) {
|
|
3501
|
+
const args = (await this.#run(git, [
|
|
3502
|
+
"rev-parse",
|
|
3503
|
+
"--abbrev-ref",
|
|
3504
|
+
"--symbolic-full-name",
|
|
3505
|
+
"@{upstream}"
|
|
3506
|
+
], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
|
|
3507
|
+
"push",
|
|
3508
|
+
"--set-upstream",
|
|
3509
|
+
"origin",
|
|
3510
|
+
branch
|
|
3511
|
+
];
|
|
3512
|
+
await this.#mustRun(git, args, cwd, REMOTE_TIMEOUT_MS, "push-failed", "Git push failed.");
|
|
3513
|
+
}
|
|
3514
|
+
#git() {
|
|
3515
|
+
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
3516
|
+
return this.#gitExecutable;
|
|
3517
|
+
}
|
|
3518
|
+
#gh() {
|
|
3519
|
+
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
|
|
3520
|
+
throw new RepositoryActionError("gh-unavailable", "GitHub CLI is unavailable.");
|
|
3521
|
+
});
|
|
3522
|
+
return this.#ghExecutable;
|
|
3523
|
+
}
|
|
3524
|
+
async #mustRun(executable, args, cwd, timeoutMs, code, message) {
|
|
3525
|
+
const result = await this.#run(executable, args, cwd, timeoutMs);
|
|
3526
|
+
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
3527
|
+
return result;
|
|
3528
|
+
}
|
|
3529
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
|
|
3530
|
+
return collect(this.#runtime.spawn({
|
|
3531
|
+
argv: [executable, ...args],
|
|
3532
|
+
cwd,
|
|
3533
|
+
stdio: {
|
|
3534
|
+
stdin: "ignore",
|
|
3535
|
+
stdout: { maxBytes },
|
|
3536
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES }
|
|
3537
|
+
},
|
|
3538
|
+
graceMs: 1e3,
|
|
3539
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3540
|
+
env: {}
|
|
3541
|
+
}));
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
//#endregion
|
|
3545
|
+
//#region src/repository-setup-routes.ts
|
|
3546
|
+
const MAX_BODY_BYTES$2 = 16384;
|
|
3547
|
+
function record$2(value) {
|
|
3548
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3549
|
+
}
|
|
3550
|
+
async function readJson$2(req) {
|
|
3551
|
+
const chunks = [];
|
|
3552
|
+
let size = 0;
|
|
3553
|
+
for await (const chunk of req) {
|
|
3554
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3555
|
+
size += buffer.length;
|
|
3556
|
+
if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
3557
|
+
chunks.push(buffer);
|
|
3558
|
+
}
|
|
3559
|
+
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3560
|
+
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
3561
|
+
return value;
|
|
3562
|
+
}
|
|
3563
|
+
function string$1(input, key) {
|
|
3564
|
+
const value = input[key];
|
|
3565
|
+
if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
|
|
3566
|
+
return value;
|
|
3567
|
+
}
|
|
3568
|
+
function optionalString$1(input, key) {
|
|
3569
|
+
const value = input[key];
|
|
3570
|
+
if (value === void 0) return void 0;
|
|
3571
|
+
if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
|
|
3572
|
+
return value;
|
|
3573
|
+
}
|
|
3574
|
+
function ndjsonHeaders(res) {
|
|
3575
|
+
res.writeHead(200, {
|
|
3576
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
3577
|
+
"cache-control": "no-store",
|
|
3578
|
+
"x-content-type-options": "nosniff"
|
|
3579
|
+
});
|
|
3580
|
+
res.flushHeaders?.();
|
|
3581
|
+
}
|
|
3582
|
+
function ndjson(res, value) {
|
|
3583
|
+
res.write(`${JSON.stringify(value)}\n`);
|
|
3584
|
+
}
|
|
3585
|
+
async function streamSetup(res, service, input) {
|
|
3586
|
+
ndjsonHeaders(res);
|
|
3587
|
+
let ended = false;
|
|
3588
|
+
try {
|
|
3589
|
+
ndjson(res, {
|
|
3590
|
+
type: "complete",
|
|
3591
|
+
result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
|
|
3592
|
+
if (!ended) ndjson(res, {
|
|
3593
|
+
type: "progress",
|
|
3594
|
+
stage
|
|
3595
|
+
});
|
|
3596
|
+
})
|
|
3597
|
+
});
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
const setupError = error instanceof RepositorySetupError ? error : void 0;
|
|
3600
|
+
ndjson(res, {
|
|
3601
|
+
type: "error",
|
|
3602
|
+
code: setupError?.code ?? "repository-setup-unavailable",
|
|
3603
|
+
message: setupError?.message ?? "Repository setup is unavailable."
|
|
3604
|
+
});
|
|
3605
|
+
} finally {
|
|
3606
|
+
ended = true;
|
|
3607
|
+
res.end();
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
/** Register trusted browser routes for safe pre-session Git setup. */
|
|
3611
|
+
function registerRepositorySetupRoute(ctx, service) {
|
|
3612
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3613
|
+
kind: "prefix",
|
|
3614
|
+
path: CLAUDE_REPOSITORY_SETUP_PATH,
|
|
3615
|
+
handler: async (req, res) => {
|
|
3616
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3617
|
+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
3618
|
+
try {
|
|
3619
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
|
|
3620
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
3621
|
+
const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
|
|
3622
|
+
if (cwd === null) throw new RepositorySetupError("invalid-request", "The cwd query parameter is required.");
|
|
3623
|
+
return json(res, 200, await service.listBranches(cwd));
|
|
3624
|
+
}
|
|
3625
|
+
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
3626
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3627
|
+
const input = await readJson$2(req);
|
|
3628
|
+
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
3629
|
+
await streamSetup(res, service, input);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
3633
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3634
|
+
const input = await readJson$2(req);
|
|
3635
|
+
await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
|
|
3636
|
+
return json(res, 200, { ok: true });
|
|
3637
|
+
}
|
|
3638
|
+
return json(res, 404, { error: "not found" });
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
if (error instanceof RepositorySetupError) return json(res, 409, {
|
|
3641
|
+
error: error.code,
|
|
3642
|
+
message: error.message
|
|
3643
|
+
});
|
|
3644
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid json" });
|
|
3645
|
+
return json(res, 500, { error: "repository setup unavailable" });
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
}), "dsh-claude: repository setup route");
|
|
3649
|
+
}
|
|
3650
|
+
//#endregion
|
|
3651
|
+
//#region src/repository-action-routes.ts
|
|
3652
|
+
const MAX_BODY_BYTES$1 = 16384;
|
|
3653
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
3654
|
+
const ACTIONS = /* @__PURE__ */ new Set([
|
|
3655
|
+
"commit",
|
|
3656
|
+
"commit-push",
|
|
3657
|
+
"push",
|
|
3658
|
+
"create-pr"
|
|
3659
|
+
]);
|
|
3660
|
+
function record$1(value) {
|
|
3661
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3662
|
+
}
|
|
3663
|
+
async function readJson$1(req) {
|
|
3664
|
+
const chunks = [];
|
|
3665
|
+
let size = 0;
|
|
3666
|
+
for await (const chunk of req) {
|
|
3667
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3668
|
+
size += buffer.length;
|
|
3669
|
+
if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
3670
|
+
chunks.push(buffer);
|
|
3671
|
+
}
|
|
3672
|
+
const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3673
|
+
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
3674
|
+
return value;
|
|
3675
|
+
}
|
|
3676
|
+
function sessionId(url) {
|
|
3677
|
+
const value = url.searchParams.get("sessionId");
|
|
3678
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
3679
|
+
return value;
|
|
3680
|
+
}
|
|
3681
|
+
function string(input, key) {
|
|
3682
|
+
const value = input[key];
|
|
3683
|
+
if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
|
|
3684
|
+
return value;
|
|
3685
|
+
}
|
|
3686
|
+
function optionalString(input, key) {
|
|
3687
|
+
const value = input[key];
|
|
3688
|
+
if (value === void 0) return void 0;
|
|
3689
|
+
if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field must be a string.`);
|
|
3690
|
+
return value;
|
|
3691
|
+
}
|
|
3692
|
+
function actionRequest(input) {
|
|
3693
|
+
const action = input.action;
|
|
3694
|
+
if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
|
|
3695
|
+
return {
|
|
3696
|
+
action,
|
|
3697
|
+
fingerprint: string(input, "fingerprint"),
|
|
3698
|
+
message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
3699
|
+
includeUnstaged: input.includeUnstaged,
|
|
3700
|
+
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
3701
|
+
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
3702
|
+
...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
|
|
3703
|
+
...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
|
|
3704
|
+
throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
|
|
3705
|
+
})()
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
3709
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3710
|
+
kind: "prefix",
|
|
3711
|
+
path: CLAUDE_REPOSITORY_ACTION_PATH,
|
|
3712
|
+
handler: async (req, res) => {
|
|
3713
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3714
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3715
|
+
try {
|
|
3716
|
+
const cwd = cwdForSession(sessionId(url));
|
|
3717
|
+
if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
|
|
3718
|
+
if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
|
|
3719
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
3720
|
+
return json(res, 200, await service.preview(cwd));
|
|
3721
|
+
}
|
|
3722
|
+
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
3723
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3724
|
+
const input = await readJson$1(req);
|
|
3725
|
+
return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
|
|
3726
|
+
}
|
|
3727
|
+
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
3728
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3729
|
+
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$1(req))));
|
|
3730
|
+
}
|
|
3731
|
+
return json(res, 404, { error: "not found" });
|
|
3732
|
+
} catch (error) {
|
|
3733
|
+
if (error instanceof RepositoryActionError) return json(res, 409, {
|
|
3734
|
+
error: error.code,
|
|
3735
|
+
message: error.message,
|
|
3736
|
+
...error.commit === void 0 ? {} : { commit: error.commit }
|
|
3737
|
+
});
|
|
3738
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
3739
|
+
return json(res, 500, {
|
|
3740
|
+
error: "repository-action-unavailable",
|
|
3741
|
+
message: "Repository action is unavailable."
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}), "dsh-claude: repository action route");
|
|
3746
|
+
}
|
|
3747
|
+
//#endregion
|
|
3748
|
+
//#region src/review-comment-routes.ts
|
|
3749
|
+
const MAX_BODY_BYTES = 16384;
|
|
3750
|
+
const MAX_SESSION_ID_CHARS = 1024;
|
|
3751
|
+
function record(value) {
|
|
3752
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3753
|
+
}
|
|
3754
|
+
async function readJson(req) {
|
|
3755
|
+
const chunks = [];
|
|
3756
|
+
let size = 0;
|
|
3757
|
+
for await (const chunk of req) {
|
|
3758
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3759
|
+
size += buffer.length;
|
|
3760
|
+
if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
3761
|
+
chunks.push(buffer);
|
|
3762
|
+
}
|
|
3763
|
+
const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3764
|
+
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
3765
|
+
return value;
|
|
3766
|
+
}
|
|
3767
|
+
function sessionIdFromUrl(url) {
|
|
3768
|
+
const value = url.searchParams.get("sessionId");
|
|
3769
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
|
|
3770
|
+
return value;
|
|
3771
|
+
}
|
|
3772
|
+
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
3773
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3774
|
+
kind: "prefix",
|
|
3775
|
+
path: CLAUDE_REVIEW_COMMENT_PATH,
|
|
3776
|
+
handler: async (req, res) => {
|
|
3777
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3778
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3779
|
+
try {
|
|
3780
|
+
const sessionId = sessionIdFromUrl(url);
|
|
3781
|
+
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
3782
|
+
if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
|
|
3783
|
+
const input = await readJson(req);
|
|
3784
|
+
return json(res, 200, { comment: store.add(sessionId, {
|
|
3785
|
+
path: input.path,
|
|
3786
|
+
line: input.line,
|
|
3787
|
+
side: input.side,
|
|
3788
|
+
text: input.text
|
|
3789
|
+
}) });
|
|
3790
|
+
}
|
|
3791
|
+
if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
|
|
3792
|
+
if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
|
|
3793
|
+
const input = await readJson(req);
|
|
3794
|
+
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
3795
|
+
return json(res, 200, { removed: store.remove(sessionId, input.id) });
|
|
3796
|
+
}
|
|
3797
|
+
return json(res, 404, { error: "not found" });
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
if (error instanceof ReviewCommentError) return json(res, 409, {
|
|
3800
|
+
error: error.code,
|
|
3801
|
+
message: error.message
|
|
3802
|
+
});
|
|
3803
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
3804
|
+
return json(res, 500, {
|
|
3805
|
+
error: "review-comment-unavailable",
|
|
3806
|
+
message: "Review comments are unavailable."
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}), "dsh-claude: review comment route");
|
|
3811
|
+
}
|
|
3812
|
+
//#endregion
|
|
2334
3813
|
//#region src/update-routes.ts
|
|
2335
3814
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
2336
3815
|
const UPDATE_TIMEOUT_MS = 3e4;
|
|
@@ -2604,11 +4083,14 @@ const BUILTIN_OUTPUT_STYLES = [
|
|
|
2604
4083
|
"Learning"
|
|
2605
4084
|
];
|
|
2606
4085
|
const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
|
|
4086
|
+
const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
|
|
4087
|
+
const MAX_BRANCH_PREFIX_CHARS = 128;
|
|
2607
4088
|
function pathsFor(deps) {
|
|
2608
4089
|
const root = join(homedir(), ".claude");
|
|
2609
4090
|
return {
|
|
2610
4091
|
settingsFile: deps.paths?.settingsFile ?? join(root, "settings.json"),
|
|
2611
|
-
outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles")
|
|
4092
|
+
outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles"),
|
|
4093
|
+
pluginSettingsFile: deps.paths?.pluginSettingsFile ?? dshHomePath("plugins", "dsh-claude", "settings.json")
|
|
2612
4094
|
};
|
|
2613
4095
|
}
|
|
2614
4096
|
function object(value) {
|
|
@@ -2664,9 +4146,10 @@ async function userOutputStyleOptions(directory) {
|
|
|
2664
4146
|
}
|
|
2665
4147
|
return options;
|
|
2666
4148
|
}
|
|
2667
|
-
const
|
|
4149
|
+
const OUTPUT_STYLE = {
|
|
2668
4150
|
key: "outputStyle",
|
|
2669
4151
|
kind: "select",
|
|
4152
|
+
document: "claude",
|
|
2670
4153
|
effect: "new-session",
|
|
2671
4154
|
async options(paths) {
|
|
2672
4155
|
const builtIn = BUILTIN_OUTPUT_STYLES.map((value) => ({
|
|
@@ -2687,11 +4170,42 @@ const DESCRIPTORS = [{
|
|
|
2687
4170
|
if (value === "Default") delete document.outputStyle;
|
|
2688
4171
|
else document.outputStyle = value;
|
|
2689
4172
|
}
|
|
2690
|
-
}
|
|
4173
|
+
};
|
|
4174
|
+
function isValidWorktreeBranchPrefix(value) {
|
|
4175
|
+
return value.length > 0 && value.length <= MAX_BRANCH_PREFIX_CHARS && value.trim() === value && !value.startsWith("/") && !value.endsWith("/") && !value.includes("//") && !value.includes("..") && !value.includes("@{") && !/[\0-\x20\x7f~^:?*[\\]/u.test(value) && value.split("/").every((segment) => segment.length > 0 && !segment.startsWith(".") && !segment.endsWith(".lock"));
|
|
4176
|
+
}
|
|
4177
|
+
const WORKTREE_BRANCH_PREFIX = {
|
|
4178
|
+
key: "worktreeBranchPrefix",
|
|
4179
|
+
kind: "text",
|
|
4180
|
+
document: "plugin",
|
|
4181
|
+
effect: "next-worktree",
|
|
4182
|
+
maxLength: MAX_BRANCH_PREFIX_CHARS,
|
|
4183
|
+
read(document) {
|
|
4184
|
+
const value = document.worktreeBranchPrefix;
|
|
4185
|
+
return typeof value === "string" && isValidWorktreeBranchPrefix(value) ? value : DEFAULT_WORKTREE_BRANCH_PREFIX;
|
|
4186
|
+
},
|
|
4187
|
+
apply(document, value) {
|
|
4188
|
+
if (typeof value !== "string" || !isValidWorktreeBranchPrefix(value)) throw new Error("Invalid value for global setting worktreeBranchPrefix");
|
|
4189
|
+
if (value === DEFAULT_WORKTREE_BRANCH_PREFIX) delete document.worktreeBranchPrefix;
|
|
4190
|
+
else document.worktreeBranchPrefix = value;
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
const DESCRIPTORS = [OUTPUT_STYLE, WORKTREE_BRANCH_PREFIX];
|
|
2691
4194
|
const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
|
|
2692
4195
|
let pendingWrite = Promise.resolve();
|
|
2693
|
-
|
|
4196
|
+
function documentFor(descriptor, documents) {
|
|
4197
|
+
return documents[descriptor.document];
|
|
4198
|
+
}
|
|
4199
|
+
async function views(documents, paths) {
|
|
2694
4200
|
return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
|
|
4201
|
+
const document = documentFor(descriptor, documents);
|
|
4202
|
+
if (descriptor.kind === "text") return {
|
|
4203
|
+
key: descriptor.key,
|
|
4204
|
+
kind: descriptor.kind,
|
|
4205
|
+
value: descriptor.read(document),
|
|
4206
|
+
maxLength: descriptor.maxLength,
|
|
4207
|
+
effect: descriptor.effect
|
|
4208
|
+
};
|
|
2695
4209
|
const discovered = await descriptor.options(paths);
|
|
2696
4210
|
const value = descriptor.read(document, discovered);
|
|
2697
4211
|
const options = discovered.some((option) => option.value === value) ? discovered : [...discovered, {
|
|
@@ -2708,9 +4222,20 @@ async function views(document, paths) {
|
|
|
2708
4222
|
};
|
|
2709
4223
|
})) };
|
|
2710
4224
|
}
|
|
4225
|
+
async function readDocuments(paths) {
|
|
4226
|
+
const [claude, plugin] = await Promise.all([readDocument(paths.settingsFile), readDocument(paths.pluginSettingsFile)]);
|
|
4227
|
+
return {
|
|
4228
|
+
claude,
|
|
4229
|
+
plugin
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
2711
4232
|
async function readGlobalSettings(deps = {}) {
|
|
2712
4233
|
const paths = pathsFor(deps);
|
|
2713
|
-
return views(await
|
|
4234
|
+
return views(await readDocuments(paths), paths);
|
|
4235
|
+
}
|
|
4236
|
+
async function readWorktreeBranchPrefix(deps = {}) {
|
|
4237
|
+
const paths = pathsFor(deps);
|
|
4238
|
+
return WORKTREE_BRANCH_PREFIX.read(await readDocument(paths.pluginSettingsFile));
|
|
2714
4239
|
}
|
|
2715
4240
|
async function atomicWrite(path, document) {
|
|
2716
4241
|
await mkdir(dirname(path), {
|
|
@@ -2737,13 +4262,18 @@ function updateGlobalSettings(changes, deps = {}) {
|
|
|
2737
4262
|
for (const key of Object.keys(changeObject)) if (!DESCRIPTOR_BY_KEY.has(key)) return Promise.reject(/* @__PURE__ */ new Error(`Unsupported global setting: ${key}`));
|
|
2738
4263
|
const paths = pathsFor(deps);
|
|
2739
4264
|
const operation = pendingWrite.catch(() => void 0).then(async () => {
|
|
2740
|
-
const
|
|
4265
|
+
const documents = await readDocuments(paths);
|
|
4266
|
+
const changedDocuments = /* @__PURE__ */ new Set();
|
|
2741
4267
|
for (const [key, value] of Object.entries(changeObject)) {
|
|
2742
4268
|
const descriptor = DESCRIPTOR_BY_KEY.get(key);
|
|
2743
|
-
|
|
4269
|
+
const document = documentFor(descriptor, documents);
|
|
4270
|
+
if (descriptor.kind === "select") descriptor.apply(document, value, await descriptor.options(paths));
|
|
4271
|
+
else descriptor.apply(document, value);
|
|
4272
|
+
changedDocuments.add(descriptor.document);
|
|
2744
4273
|
}
|
|
2745
|
-
await atomicWrite(paths.settingsFile,
|
|
2746
|
-
|
|
4274
|
+
if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
|
|
4275
|
+
if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
|
|
4276
|
+
return views(documents, paths);
|
|
2747
4277
|
});
|
|
2748
4278
|
pendingWrite = operation;
|
|
2749
4279
|
return operation;
|
|
@@ -2908,6 +4438,9 @@ async function apply(ctx, config) {
|
|
|
2908
4438
|
maxProcesses: config.maxProcesses ?? 4
|
|
2909
4439
|
};
|
|
2910
4440
|
const sidecar = new ClaudeSidecarRepository();
|
|
4441
|
+
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
4442
|
+
const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
|
|
4443
|
+
const reviewComments = new ReviewCommentStore();
|
|
2911
4444
|
const commandCatalogs = /* @__PURE__ */ new Map();
|
|
2912
4445
|
const supervisor = new ClaudeSupervisor({
|
|
2913
4446
|
runtime: ctx.subprocess,
|
|
@@ -2920,7 +4453,7 @@ async function apply(ctx, config) {
|
|
|
2920
4453
|
let resolutionError;
|
|
2921
4454
|
try {
|
|
2922
4455
|
supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
2923
|
-
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
|
|
4456
|
+
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
|
|
2924
4457
|
ctx.effect(() => {
|
|
2925
4458
|
const mounted = /* @__PURE__ */ new Map();
|
|
2926
4459
|
const pending = /* @__PURE__ */ new Set();
|
|
@@ -2981,18 +4514,35 @@ async function apply(ctx, config) {
|
|
|
2981
4514
|
resolutionError = error;
|
|
2982
4515
|
}
|
|
2983
4516
|
ctx.on("agent/disposed", async ({ agent }) => {
|
|
4517
|
+
reviewComments.disposeSession(agent.id);
|
|
2984
4518
|
await supervisor.disposeSession(agent.id);
|
|
4519
|
+
await repositorySetup.cleanupSession(agent.id);
|
|
2985
4520
|
});
|
|
4521
|
+
ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
|
|
2986
4522
|
ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
|
|
4523
|
+
ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
|
|
2987
4524
|
ctx.inject(["webServer"], (webCtx) => {
|
|
2988
4525
|
registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
|
|
2989
4526
|
const desktopActions = webCtx.get("desktopActions");
|
|
2990
4527
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
2991
4528
|
registerClaudeGlobalSettingsRoute(webCtx);
|
|
2992
|
-
|
|
4529
|
+
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
4530
|
+
registerRepositoryActionRoute(webCtx, new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd)), (sessionId) => {
|
|
4531
|
+
const agent = webCtx.agents.get(sessionId);
|
|
4532
|
+
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
4533
|
+
return agent.session.header.cwd;
|
|
4534
|
+
});
|
|
4535
|
+
const ownsClaudeSession = (sessionId) => {
|
|
2993
4536
|
const agent = webCtx.agents.get(sessionId);
|
|
2994
4537
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|
|
2995
|
-
}
|
|
4538
|
+
};
|
|
4539
|
+
registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
|
|
4540
|
+
registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
|
|
4541
|
+
const agent = webCtx.agents.get(sessionId);
|
|
4542
|
+
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
4543
|
+
const cwd = agent.session.header.cwd;
|
|
4544
|
+
return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
|
|
4545
|
+
}, (sessionId) => reviewComments.list(sessionId));
|
|
2996
4546
|
});
|
|
2997
4547
|
}
|
|
2998
4548
|
//#endregion
|