@norman-else/dsh-claude 0.1.24 → 0.1.26
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/lib/bin.mjs +1 -1
- package/lib/client.d.ts +59 -0
- package/lib/client.js +3298 -911
- package/lib/client.js.map +1 -1
- package/lib/{command-bridge-Br25ODdm.mjs → command-bridge-vyZoEeEO.mjs} +2 -2
- package/lib/{command-bridge-Br25ODdm.mjs.map → command-bridge-vyZoEeEO.mjs.map} +1 -1
- package/lib/{events-DhrAr5gh.mjs → events-CY6V9oQ4.mjs} +7 -3
- package/lib/events-CY6V9oQ4.mjs.map +1 -0
- package/lib/index.mjs +919 -163
- package/lib/index.mjs.map +1 -1
- package/lib/{preset-installer-B_Cb0RG6.mjs → preset-installer-BMRaIHLu.mjs} +2 -2
- package/lib/{preset-installer-B_Cb0RG6.mjs.map → preset-installer-BMRaIHLu.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +7 -7
- package/lib/events-DhrAr5gh.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { C as CLAUDE_UPDATE_PATH,
|
|
2
|
-
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { C as CLAUDE_REPOSITORY_SETUP_PATH, D as CLAUDE_UPDATE_PATH, E as CLAUDE_UPDATE_CHECK_PATH, S as CLAUDE_REPOSITORY_FILE_PATH, T as CLAUDE_REVIEW_COMMENT_PATH, _ as CLAUDE_GLOBAL_SETTINGS_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_ACTION_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_DOCTOR_PATH, h as CLAUDE_CODE_PROVIDER_IDS, i as latestClaudeSessionBinding, k as TASK_TOOL_NAMES, l as redactText, m as CLAUDE_CODE_PROVIDER, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PRESET_ID, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_JIRA_PATH, w as CLAUDE_REPOSITORY_STATUS_PATH, x as CLAUDE_REPOSITORY_FEEDBACK_PATH, y as CLAUDE_PROJECTION_PATH } from "./events-CY6V9oQ4.mjs";
|
|
2
|
+
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-vyZoEeEO.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-BMRaIHLu.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
-
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, extname, isAbsolute, join, resolve, sep } 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";
|
|
@@ -12,6 +12,7 @@ import { EventEmitter } from "node:events";
|
|
|
12
12
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
13
13
|
import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { StringDecoder } from "node:string_decoder";
|
|
15
16
|
//#region src/sidecar.ts
|
|
16
17
|
const SIDECAR_SCHEMA_VERSION = 1;
|
|
17
18
|
const MAX_ACTIVITIES = 1e4;
|
|
@@ -25,18 +26,18 @@ function emptyProjection() {
|
|
|
25
26
|
activities: []
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
|
-
function record$
|
|
29
|
+
function record$10(value) {
|
|
29
30
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
30
31
|
}
|
|
31
32
|
function finiteInteger(value) {
|
|
32
33
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
33
34
|
}
|
|
34
|
-
function string$
|
|
35
|
+
function string$4(value, max) {
|
|
35
36
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
36
37
|
}
|
|
37
38
|
function binding(value) {
|
|
38
|
-
const input = record$
|
|
39
|
-
if (input === void 0 || !string$
|
|
39
|
+
const input = record$10(value);
|
|
40
|
+
if (input === void 0 || !string$4(input.claudeSessionId, 512) || !string$4(input.sdkVersion, 128) || !string$4(input.cwd, 4096) || input.cliVersion !== void 0 && !string$4(input.cliVersion, 128)) return void 0;
|
|
40
41
|
return {
|
|
41
42
|
claudeSessionId: input.claudeSessionId,
|
|
42
43
|
sdkVersion: input.sdkVersion,
|
|
@@ -65,22 +66,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
65
66
|
"failed"
|
|
66
67
|
]);
|
|
67
68
|
function activity(value) {
|
|
68
|
-
const input = record$
|
|
69
|
+
const input = record$10(value);
|
|
69
70
|
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;
|
|
70
71
|
return normalizeActivity(input);
|
|
71
72
|
}
|
|
72
73
|
function contextUsage(value) {
|
|
73
|
-
const input = record$
|
|
74
|
+
const input = record$10(value);
|
|
74
75
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
75
76
|
return normalizeContextUsage(input);
|
|
76
77
|
}
|
|
77
78
|
function tasks(value) {
|
|
78
|
-
const input = record$
|
|
79
|
+
const input = record$10(value);
|
|
79
80
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
80
81
|
return normalizeTasksEvent(input.tasks);
|
|
81
82
|
}
|
|
82
83
|
function parseClaudeSidecar(value) {
|
|
83
|
-
const input = record$
|
|
84
|
+
const input = record$10(value);
|
|
84
85
|
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");
|
|
85
86
|
const activities = input.activities.map(activity);
|
|
86
87
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
@@ -111,7 +112,7 @@ function mergeActivities(existing, additions) {
|
|
|
111
112
|
function normalizeBinding(input) {
|
|
112
113
|
return {
|
|
113
114
|
claudeSessionId: redactText(input.claudeSessionId, 512),
|
|
114
|
-
sdkVersion: redactText(input.sdkVersion ?? "0.3.
|
|
115
|
+
sdkVersion: redactText(input.sdkVersion ?? "0.3.247", 128),
|
|
115
116
|
cwd: redactText(input.cwd, 4096),
|
|
116
117
|
...input.cliVersion === void 0 ? {} : { cliVersion: redactText(input.cliVersion, 128) }
|
|
117
118
|
};
|
|
@@ -653,10 +654,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
653
654
|
}
|
|
654
655
|
//#endregion
|
|
655
656
|
//#region src/sdk-messages.ts
|
|
656
|
-
function record$
|
|
657
|
+
function record$9(value) {
|
|
657
658
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
658
659
|
}
|
|
659
|
-
function string$
|
|
660
|
+
function string$3(value) {
|
|
660
661
|
return typeof value === "string" ? value : void 0;
|
|
661
662
|
}
|
|
662
663
|
function taskUsageOf(usage) {
|
|
@@ -668,7 +669,7 @@ function taskUsageOf(usage) {
|
|
|
668
669
|
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
669
670
|
}
|
|
670
671
|
function resultUsage(message) {
|
|
671
|
-
const usage = record$
|
|
672
|
+
const usage = record$9(message.usage);
|
|
672
673
|
const normalized = {};
|
|
673
674
|
if (usage !== void 0) {
|
|
674
675
|
if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
|
|
@@ -680,26 +681,26 @@ function resultUsage(message) {
|
|
|
680
681
|
return normalized;
|
|
681
682
|
}
|
|
682
683
|
function normalizeAssistant(message) {
|
|
683
|
-
const content = record$
|
|
684
|
+
const content = record$9(message.message)?.content;
|
|
684
685
|
if (!Array.isArray(content)) return [{
|
|
685
686
|
kind: "protocol-error",
|
|
686
687
|
title: "Malformed Claude assistant message",
|
|
687
688
|
detail: message
|
|
688
689
|
}];
|
|
689
|
-
const parentToolUseId = string$
|
|
690
|
+
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
690
691
|
const normalized = [];
|
|
691
692
|
for (const item of content) {
|
|
692
|
-
const block = record$
|
|
693
|
+
const block = record$9(item);
|
|
693
694
|
if (block === void 0) continue;
|
|
694
695
|
if (block.type === "text") {
|
|
695
|
-
const text = string$
|
|
696
|
+
const text = string$3(block.text);
|
|
696
697
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
697
698
|
kind: "assistant-text",
|
|
698
699
|
text,
|
|
699
700
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
700
701
|
});
|
|
701
702
|
} else if (block.type === "thinking") {
|
|
702
|
-
const text = string$
|
|
703
|
+
const text = string$3(block.thinking);
|
|
703
704
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
704
705
|
kind: "thinking",
|
|
705
706
|
text,
|
|
@@ -707,8 +708,8 @@ function normalizeAssistant(message) {
|
|
|
707
708
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
708
709
|
});
|
|
709
710
|
} else if (block.type === "tool_use") {
|
|
710
|
-
const toolUseId = string$
|
|
711
|
-
const toolName = string$
|
|
711
|
+
const toolUseId = string$3(block.id);
|
|
712
|
+
const toolName = string$3(block.name);
|
|
712
713
|
if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
|
|
713
714
|
kind: "tool-call",
|
|
714
715
|
toolUseId,
|
|
@@ -722,19 +723,19 @@ function normalizeAssistant(message) {
|
|
|
722
723
|
}
|
|
723
724
|
function normalizeUser(message) {
|
|
724
725
|
if (message.isReplay === true) return [];
|
|
725
|
-
const content = record$
|
|
726
|
+
const content = record$9(message.message)?.content;
|
|
726
727
|
if (typeof content === "string") return [];
|
|
727
728
|
if (!Array.isArray(content)) return [{
|
|
728
729
|
kind: "protocol-error",
|
|
729
730
|
title: "Malformed Claude user message",
|
|
730
731
|
detail: message
|
|
731
732
|
}];
|
|
732
|
-
const parentToolUseId = string$
|
|
733
|
+
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
733
734
|
const normalized = [];
|
|
734
735
|
for (const item of content) {
|
|
735
|
-
const block = record$
|
|
736
|
+
const block = record$9(item);
|
|
736
737
|
if (block?.type !== "tool_result") continue;
|
|
737
|
-
const toolUseId = string$
|
|
738
|
+
const toolUseId = string$3(block.tool_use_id);
|
|
738
739
|
if (toolUseId === void 0) continue;
|
|
739
740
|
normalized.push({
|
|
740
741
|
kind: "tool-result",
|
|
@@ -747,11 +748,11 @@ function normalizeUser(message) {
|
|
|
747
748
|
return normalized;
|
|
748
749
|
}
|
|
749
750
|
function normalizeSystem(message) {
|
|
750
|
-
const subtype = string$
|
|
751
|
+
const subtype = string$3(message.subtype);
|
|
751
752
|
if (subtype === "init") {
|
|
752
|
-
const sessionId = string$
|
|
753
|
-
const cliVersion = string$
|
|
754
|
-
const cwd = string$
|
|
753
|
+
const sessionId = string$3(message.session_id);
|
|
754
|
+
const cliVersion = string$3(message.claude_code_version);
|
|
755
|
+
const cwd = string$3(message.cwd);
|
|
755
756
|
return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
|
|
756
757
|
kind: "init",
|
|
757
758
|
sessionId,
|
|
@@ -780,20 +781,20 @@ function normalizeSystem(message) {
|
|
|
780
781
|
title: `Claude session ${String(message.state)}`
|
|
781
782
|
}];
|
|
782
783
|
if (subtype === "permission_denied") {
|
|
783
|
-
const toolUseId = string$
|
|
784
|
-
const toolName = string$
|
|
784
|
+
const toolUseId = string$3(message.tool_use_id);
|
|
785
|
+
const toolName = string$3(message.tool_name);
|
|
785
786
|
if (toolUseId !== void 0 && toolName !== void 0) return [{
|
|
786
787
|
kind: "permission-denied",
|
|
787
788
|
toolUseId,
|
|
788
789
|
toolName,
|
|
789
|
-
summary: string$
|
|
790
|
+
summary: string$3(message.message) ?? "Claude Code denied the action"
|
|
790
791
|
}];
|
|
791
792
|
}
|
|
792
793
|
if (subtype === "task_started") {
|
|
793
|
-
const taskId = string$
|
|
794
|
-
const description = string$
|
|
795
|
-
const subagentType = string$
|
|
796
|
-
const taskType = string$
|
|
794
|
+
const taskId = string$3(message.task_id);
|
|
795
|
+
const description = string$3(message.description);
|
|
796
|
+
const subagentType = string$3(message.subagent_type);
|
|
797
|
+
const taskType = string$3(message.task_type);
|
|
797
798
|
return [{
|
|
798
799
|
kind: "subagent",
|
|
799
800
|
title: description ?? taskId ?? "Claude subagent started",
|
|
@@ -808,12 +809,12 @@ function normalizeSystem(message) {
|
|
|
808
809
|
}];
|
|
809
810
|
}
|
|
810
811
|
if (subtype === "task_progress") {
|
|
811
|
-
const taskId = string$
|
|
812
|
-
const description = string$
|
|
813
|
-
const summary = string$
|
|
814
|
-
const subagentType = string$
|
|
815
|
-
const lastToolName = string$
|
|
816
|
-
const usage = taskUsageOf(record$
|
|
812
|
+
const taskId = string$3(message.task_id);
|
|
813
|
+
const description = string$3(message.description);
|
|
814
|
+
const summary = string$3(message.summary);
|
|
815
|
+
const subagentType = string$3(message.subagent_type);
|
|
816
|
+
const lastToolName = string$3(message.last_tool_name);
|
|
817
|
+
const usage = taskUsageOf(record$9(message.usage));
|
|
817
818
|
return [{
|
|
818
819
|
kind: "subagent",
|
|
819
820
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -829,11 +830,11 @@ function normalizeSystem(message) {
|
|
|
829
830
|
}];
|
|
830
831
|
}
|
|
831
832
|
if (subtype === "task_updated") {
|
|
832
|
-
const patch = record$
|
|
833
|
-
const status = string$
|
|
834
|
-
const taskId = string$
|
|
835
|
-
const description = string$
|
|
836
|
-
const error = string$
|
|
833
|
+
const patch = record$9(message.patch);
|
|
834
|
+
const status = string$3(patch?.status);
|
|
835
|
+
const taskId = string$3(message.task_id);
|
|
836
|
+
const description = string$3(patch?.description);
|
|
837
|
+
const error = string$3(patch?.error);
|
|
837
838
|
const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
|
|
838
839
|
return [{
|
|
839
840
|
kind: "subagent",
|
|
@@ -849,10 +850,10 @@ function normalizeSystem(message) {
|
|
|
849
850
|
if (subtype === "task_notification") {
|
|
850
851
|
const failed = message.status === "failed";
|
|
851
852
|
const stopped = message.status === "stopped" || message.status === "cancelled";
|
|
852
|
-
const taskId = string$
|
|
853
|
-
const summary = string$
|
|
853
|
+
const taskId = string$3(message.task_id);
|
|
854
|
+
const summary = string$3(message.summary);
|
|
854
855
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
855
|
-
const usage = taskUsageOf(record$
|
|
856
|
+
const usage = taskUsageOf(record$9(message.usage));
|
|
856
857
|
return [{
|
|
857
858
|
kind: "subagent",
|
|
858
859
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -867,10 +868,10 @@ function normalizeSystem(message) {
|
|
|
867
868
|
if (subtype === "background_tasks_changed") return [{
|
|
868
869
|
kind: "background-tasks",
|
|
869
870
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
870
|
-
const entry = record$
|
|
871
|
-
const taskId = string$
|
|
872
|
-
const description = string$
|
|
873
|
-
const taskType = string$
|
|
871
|
+
const entry = record$9(item);
|
|
872
|
+
const taskId = string$3(entry?.task_id);
|
|
873
|
+
const description = string$3(entry?.description);
|
|
874
|
+
const taskType = string$3(entry?.task_type);
|
|
874
875
|
if (taskId === void 0 || description === void 0) return [];
|
|
875
876
|
return [{
|
|
876
877
|
taskId,
|
|
@@ -886,7 +887,7 @@ function normalizeSystem(message) {
|
|
|
886
887
|
}];
|
|
887
888
|
if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
|
|
888
889
|
kind: message.level === "warning" ? "warning" : "status",
|
|
889
|
-
title: string$
|
|
890
|
+
title: string$3(message.content) ?? string$3(message.text) ?? "Claude Code notice",
|
|
890
891
|
detail: message
|
|
891
892
|
}];
|
|
892
893
|
if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
|
|
@@ -910,12 +911,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
910
911
|
function normalizeSdkMessage(message) {
|
|
911
912
|
const value = message;
|
|
912
913
|
if (value.type === "stream_event") {
|
|
913
|
-
const event = record$
|
|
914
|
-
const parentToolUseId = string$
|
|
914
|
+
const event = record$9(value.event);
|
|
915
|
+
const parentToolUseId = string$3(value.parent_tool_use_id);
|
|
915
916
|
if (event?.type === "content_block_delta") {
|
|
916
|
-
const delta = record$
|
|
917
|
+
const delta = record$9(event.delta);
|
|
917
918
|
if (delta?.type === "text_delta") {
|
|
918
|
-
const text = string$
|
|
919
|
+
const text = string$3(delta.text);
|
|
919
920
|
return text === void 0 ? [] : [{
|
|
920
921
|
kind: "text-delta",
|
|
921
922
|
text,
|
|
@@ -923,7 +924,7 @@ function normalizeSdkMessage(message) {
|
|
|
923
924
|
}];
|
|
924
925
|
}
|
|
925
926
|
if (delta?.type === "thinking_delta") {
|
|
926
|
-
const text = string$
|
|
927
|
+
const text = string$3(delta.thinking);
|
|
927
928
|
return text === void 0 ? [] : [{
|
|
928
929
|
kind: "thinking",
|
|
929
930
|
text,
|
|
@@ -938,7 +939,7 @@ function normalizeSdkMessage(message) {
|
|
|
938
939
|
if (value.type === "user") return normalizeUser(value);
|
|
939
940
|
if (value.type === "system") return normalizeSystem(value);
|
|
940
941
|
if (value.type === "result") {
|
|
941
|
-
const sessionId = string$
|
|
942
|
+
const sessionId = string$3(value.session_id);
|
|
942
943
|
if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
|
|
943
944
|
kind: "protocol-error",
|
|
944
945
|
title: "Malformed Claude result message",
|
|
@@ -946,11 +947,11 @@ function normalizeSdkMessage(message) {
|
|
|
946
947
|
}];
|
|
947
948
|
const success = value.subtype === "success" && value.is_error !== true;
|
|
948
949
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
949
|
-
const terminalReason = string$
|
|
950
|
-
const userMessageUuid = string$
|
|
951
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$
|
|
952
|
-
const toolName = string$
|
|
953
|
-
const toolUseId = string$
|
|
950
|
+
const terminalReason = string$3(value.terminal_reason);
|
|
951
|
+
const userMessageUuid = string$3(value.user_message_uuid);
|
|
952
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$9(item)).filter((item) => item !== void 0).map((item) => {
|
|
953
|
+
const toolName = string$3(item.tool_name);
|
|
954
|
+
const toolUseId = string$3(item.tool_use_id);
|
|
954
955
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
955
956
|
toolName,
|
|
956
957
|
toolUseId
|
|
@@ -974,7 +975,7 @@ function normalizeSdkMessage(message) {
|
|
|
974
975
|
detail: value.error ?? value.output
|
|
975
976
|
}];
|
|
976
977
|
if (value.type === "rate_limit_event") {
|
|
977
|
-
const status = string$
|
|
978
|
+
const status = string$3(record$9(value.rate_limit_info)?.status);
|
|
978
979
|
return [{
|
|
979
980
|
kind: "status",
|
|
980
981
|
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
@@ -1141,11 +1142,6 @@ const BACKGROUND_TASK_REPORT_PROMPT = [
|
|
|
1141
1142
|
"Report their final completed or failed outcomes concisely to the user.",
|
|
1142
1143
|
"Do not start new tools or tasks, and do not repeat the earlier progress update."
|
|
1143
1144
|
].join(" ");
|
|
1144
|
-
/** CLI ≥ 2.1.235 emits system/init only after its first stdin input, while
|
|
1145
|
-
* the supervisor must see init before submitting any real turn. A local
|
|
1146
|
-
* slash command nudges startup without costing a model call; its lifecycle
|
|
1147
|
-
* messages are ignored because no DSH turn is active while they arrive. */
|
|
1148
|
-
const CLAUDE_HANDSHAKE_PROMPT = "/status";
|
|
1149
1145
|
function usageSummary(usage) {
|
|
1150
1146
|
return `${usage.inputTokens ?? 0} input / ${usage.outputTokens ?? 0} output tokens${usage.cumulativeCostUsd === void 0 ? "" : ` · $${usage.cumulativeCostUsd.toFixed(4)} cumulative`}`;
|
|
1151
1147
|
}
|
|
@@ -1231,10 +1227,10 @@ var ClaudeSupervisor = class {
|
|
|
1231
1227
|
await this.#makeRoom();
|
|
1232
1228
|
entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
|
|
1233
1229
|
this.#entries.set(sessionId, entry);
|
|
1234
|
-
this.#armInitializationTimer(entry);
|
|
1235
1230
|
}
|
|
1236
1231
|
if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
1237
1232
|
if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
|
|
1233
|
+
await entry.sdkInitialization;
|
|
1238
1234
|
if (entry.idleTimer !== void 0) {
|
|
1239
1235
|
clearTimeout(entry.idleTimer);
|
|
1240
1236
|
entry.idleTimer = void 0;
|
|
@@ -1245,7 +1241,7 @@ var ClaudeSupervisor = class {
|
|
|
1245
1241
|
await this.#disposeEntry(entry);
|
|
1246
1242
|
entry = await this.#createEntry(request.agent, model, request.thinkingMode);
|
|
1247
1243
|
this.#entries.set(sessionId, entry);
|
|
1248
|
-
|
|
1244
|
+
await entry.sdkInitialization;
|
|
1249
1245
|
} else {
|
|
1250
1246
|
await this.#syncPermissionMode(entry);
|
|
1251
1247
|
if (model !== entry.model) {
|
|
@@ -1318,7 +1314,7 @@ var ClaudeSupervisor = class {
|
|
|
1318
1314
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
1319
1315
|
const entry = await this.#metadataEntry(agent, model);
|
|
1320
1316
|
try {
|
|
1321
|
-
await
|
|
1317
|
+
await entry.sdkInitialization;
|
|
1322
1318
|
return await withTimeout(operation(entry.query, entry), CLAUDE_METADATA_TIMEOUT_MS, "Claude metadata request");
|
|
1323
1319
|
} finally {
|
|
1324
1320
|
entry.lastUsedAt = Date.now();
|
|
@@ -1328,13 +1324,6 @@ var ClaudeSupervisor = class {
|
|
|
1328
1324
|
this.#admissionGate = admitted.then(() => void 0, () => void 0);
|
|
1329
1325
|
return admitted;
|
|
1330
1326
|
}
|
|
1331
|
-
#whenInitialized(entry) {
|
|
1332
|
-
if (entry.initialized) return Promise.resolve();
|
|
1333
|
-
if (entry.state === "disposed" || entry.state === "disconnected" || entry.state === "outcome-unknown") return Promise.reject(/* @__PURE__ */ new Error(`dsh-claude: session ${entry.sessionId} is ${entry.state}`));
|
|
1334
|
-
return new Promise((resolve, reject) => {
|
|
1335
|
-
entry.initWaiters.push((error) => error === void 0 ? resolve() : reject(error instanceof Error ? error : new Error(String(error))));
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
1327
|
async #metadataEntry(agent, model) {
|
|
1339
1328
|
const sessionId = agent.id;
|
|
1340
1329
|
let entry = this.#entries.get(sessionId);
|
|
@@ -1347,7 +1336,6 @@ var ClaudeSupervisor = class {
|
|
|
1347
1336
|
await this.#makeRoom();
|
|
1348
1337
|
entry = await this.#createEntry(agent, model);
|
|
1349
1338
|
this.#entries.set(sessionId, entry);
|
|
1350
|
-
this.#armInitializationTimer(entry);
|
|
1351
1339
|
}
|
|
1352
1340
|
if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
1353
1341
|
if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
|
|
@@ -1408,11 +1396,7 @@ var ClaudeSupervisor = class {
|
|
|
1408
1396
|
lifetime,
|
|
1409
1397
|
claudeSessionId: binding?.claudeSessionId,
|
|
1410
1398
|
expectedResume: binding?.claudeSessionId,
|
|
1411
|
-
handshakeUuid: randomUUID(),
|
|
1412
|
-
handshakePending: true,
|
|
1413
1399
|
initialized: false,
|
|
1414
|
-
initWaiters: [],
|
|
1415
|
-
initTimer: void 0,
|
|
1416
1400
|
idleTimer: void 0,
|
|
1417
1401
|
tasks: /* @__PURE__ */ new Map(),
|
|
1418
1402
|
taskSnapshotAt: 0,
|
|
@@ -1470,8 +1454,11 @@ var ClaudeSupervisor = class {
|
|
|
1470
1454
|
prompt: input,
|
|
1471
1455
|
options
|
|
1472
1456
|
});
|
|
1473
|
-
entry.input.push(sdkUserMessage(CLAUDE_HANDSHAKE_PROMPT, entry.handshakeUuid));
|
|
1474
1457
|
entry.pump = this.#runDetached(() => this.#pump(entry));
|
|
1458
|
+
entry.sdkInitialization = withTimeout(entry.query.initializationResult(), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude SDK initialization").then(() => {
|
|
1459
|
+
if (entry.state === "starting") entry.state = "idle";
|
|
1460
|
+
});
|
|
1461
|
+
entry.sdkInitialization.catch((error) => this.#handleDisconnect(entry, error));
|
|
1475
1462
|
return entry;
|
|
1476
1463
|
}
|
|
1477
1464
|
async #pump(entry) {
|
|
@@ -1487,14 +1474,9 @@ var ClaudeSupervisor = class {
|
|
|
1487
1474
|
const firstInitialization = !entry.initialized;
|
|
1488
1475
|
if (entry.expectedResume !== void 0 && message.sessionId !== entry.expectedResume) throw new ClaudeProtocolError(`Claude Code resumed unexpected session ${message.sessionId}; expected ${entry.expectedResume}`);
|
|
1489
1476
|
if (message.cwd !== entry.cwd) throw new ClaudeProtocolError(`Claude Code initialized in unexpected cwd ${message.cwd}; expected ${entry.cwd}`);
|
|
1490
|
-
if (entry.initTimer !== void 0) {
|
|
1491
|
-
clearTimeout(entry.initTimer);
|
|
1492
|
-
entry.initTimer = void 0;
|
|
1493
|
-
}
|
|
1494
1477
|
entry.initialized = true;
|
|
1495
1478
|
entry.claudeSessionId = message.sessionId;
|
|
1496
1479
|
entry.state = entry.active === void 0 ? "idle" : "running";
|
|
1497
|
-
for (const waiter of entry.initWaiters.splice(0)) waiter(void 0);
|
|
1498
1480
|
if (firstInitialization && entry.tasks.size > 0) {
|
|
1499
1481
|
entry.tasks.clear();
|
|
1500
1482
|
await this.#flushTasksSnapshot(entry);
|
|
@@ -1509,15 +1491,9 @@ var ClaudeSupervisor = class {
|
|
|
1509
1491
|
const taskId = message.kind === "subagent" ? message.taskId : void 0;
|
|
1510
1492
|
if (message.kind === "subagent" && taskId !== void 0) await this.#trackTask(entry, message, taskId, entry.active?.cursor.turn);
|
|
1511
1493
|
else if (message.kind === "background-tasks") await this.#trackBackgroundLevel(entry, message.tasks, entry.active?.cursor.turn);
|
|
1512
|
-
if (entry.handshakePending) {
|
|
1513
|
-
if (message.kind === "protocol-error") throw new ClaudeProtocolError(`${message.title}: ${JSON.stringify(message.detail).slice(0, 1e3)}`);
|
|
1514
|
-
if (message.kind === "result") entry.handshakePending = false;
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
1494
|
const active = entry.active;
|
|
1518
1495
|
if (active === void 0) return;
|
|
1519
1496
|
if (message.kind === "result") {
|
|
1520
|
-
if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
|
|
1521
1497
|
if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
|
|
1522
1498
|
if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
|
|
1523
1499
|
if (active.phase === "waiting-tasks") {
|
|
@@ -1957,20 +1933,9 @@ var ClaudeSupervisor = class {
|
|
|
1957
1933
|
active.output.fail(failure);
|
|
1958
1934
|
entry.active = void 0;
|
|
1959
1935
|
} else entry.state = "disconnected";
|
|
1960
|
-
const waiters = entry.initWaiters.splice(0);
|
|
1961
|
-
const waiterError = error instanceof Error ? error : new Error(String(error));
|
|
1962
|
-
for (const waiter of waiters) waiter(waiterError);
|
|
1963
1936
|
this.#entries.delete(entry.sessionId);
|
|
1964
1937
|
await this.#disposeEntry(entry);
|
|
1965
1938
|
}
|
|
1966
|
-
#armInitializationTimer(entry) {
|
|
1967
|
-
const timer = setTimeout(() => {
|
|
1968
|
-
if (entry.state !== "starting" || entry.initialized) return;
|
|
1969
|
-
this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code initialization timed out"));
|
|
1970
|
-
}, CLAUDE_INITIALIZATION_TIMEOUT_MS);
|
|
1971
|
-
timer.unref?.();
|
|
1972
|
-
entry.initTimer = timer;
|
|
1973
|
-
}
|
|
1974
1939
|
#armIdleTimer(entry) {
|
|
1975
1940
|
if (this.#config.idleTimeoutMs <= 0) return;
|
|
1976
1941
|
const timer = setTimeout(() => {
|
|
@@ -1984,7 +1949,6 @@ var ClaudeSupervisor = class {
|
|
|
1984
1949
|
async #disposeEntry(entry) {
|
|
1985
1950
|
if (entry.state === "disposed") return;
|
|
1986
1951
|
if (entry.idleTimer !== void 0) clearTimeout(entry.idleTimer);
|
|
1987
|
-
if (entry.initTimer !== void 0) clearTimeout(entry.initTimer);
|
|
1988
1952
|
entry.state = "disposed";
|
|
1989
1953
|
entry.input.discard(abortFailure());
|
|
1990
1954
|
entry.query.close();
|
|
@@ -2000,7 +1964,7 @@ var ClaudeSupervisor = class {
|
|
|
2000
1964
|
//#region src/review-comments.ts
|
|
2001
1965
|
const MAX_COMMENTS_PER_SESSION = 50;
|
|
2002
1966
|
const MAX_TEXT_CHARS$1 = 2e3;
|
|
2003
|
-
const MAX_PATH_CHARS$
|
|
1967
|
+
const MAX_PATH_CHARS$3 = 1024;
|
|
2004
1968
|
const MAX_LINE = 1e7;
|
|
2005
1969
|
var ReviewCommentError = class extends Error {
|
|
2006
1970
|
code;
|
|
@@ -2019,16 +1983,19 @@ var ReviewCommentStore = class {
|
|
|
2019
1983
|
add(sessionId, input) {
|
|
2020
1984
|
const path = typeof input.path === "string" ? input.path.trim() : "";
|
|
2021
1985
|
const text = typeof input.text === "string" ? input.text.trim() : "";
|
|
2022
|
-
if (path.length === 0 || path.length > MAX_PATH_CHARS$
|
|
1986
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS$3 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
|
|
2023
1987
|
if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
|
|
2024
1988
|
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.");
|
|
2025
1989
|
if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
|
|
1990
|
+
const startLine = input.startLine === void 0 || input.startLine === null ? void 0 : input.startLine;
|
|
1991
|
+
if (startLine !== void 0 && (typeof startLine !== "number" || !Number.isSafeInteger(startLine) || startLine < 1 || startLine > input.line)) throw new ReviewCommentError("invalid-request", "The comment start line is invalid.");
|
|
2026
1992
|
const existing = this.#comments.get(sessionId) ?? [];
|
|
2027
1993
|
if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
|
|
2028
1994
|
const comment = {
|
|
2029
1995
|
id: randomUUID(),
|
|
2030
1996
|
path,
|
|
2031
1997
|
line: input.line,
|
|
1998
|
+
...startLine === void 0 || startLine === input.line ? {} : { startLine },
|
|
2032
1999
|
side: input.side,
|
|
2033
2000
|
text
|
|
2034
2001
|
};
|
|
@@ -2064,8 +2031,8 @@ var ReviewCommentStore = class {
|
|
|
2064
2031
|
function formatReviewComments(comments) {
|
|
2065
2032
|
return [
|
|
2066
2033
|
"<user-review-comments>",
|
|
2067
|
-
"The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
|
|
2068
|
-
...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
|
|
2034
|
+
"The user attached these code review comments to this message. Each references a file and line (or line range) from the current working tree diff:",
|
|
2035
|
+
...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.startLine === void 0 ? comment.line : `${comment.startLine}-${comment.line}`}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
|
|
2069
2036
|
"</user-review-comments>"
|
|
2070
2037
|
].join("\n");
|
|
2071
2038
|
}
|
|
@@ -2510,7 +2477,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2510
2477
|
}
|
|
2511
2478
|
//#endregion
|
|
2512
2479
|
//#region src/projection-routes.ts
|
|
2513
|
-
const MAX_SESSION_ID_CHARS$
|
|
2480
|
+
const MAX_SESSION_ID_CHARS$4 = 1024;
|
|
2514
2481
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2515
2482
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2516
2483
|
const META_REFRESH_MS = 5e3;
|
|
@@ -2524,7 +2491,7 @@ function targetFromUrl(rawUrl) {
|
|
|
2524
2491
|
if (stream) encoded = encoded.slice(0, -7);
|
|
2525
2492
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2526
2493
|
const sessionId = decodeURIComponent(encoded);
|
|
2527
|
-
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$
|
|
2494
|
+
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$4) return void 0;
|
|
2528
2495
|
return {
|
|
2529
2496
|
sessionId,
|
|
2530
2497
|
stream
|
|
@@ -2686,6 +2653,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2686
2653
|
//#region src/repository-status.ts
|
|
2687
2654
|
const MAX_OUTPUT_BYTES$3 = 65536;
|
|
2688
2655
|
const MAX_DIFF_BYTES = 262144;
|
|
2656
|
+
const MAX_FILE_BYTES = 8388608;
|
|
2689
2657
|
const MAX_UNTRACKED_DIFFS = 50;
|
|
2690
2658
|
const GIT_TIMEOUT_MS$3 = 5e3;
|
|
2691
2659
|
const GH_TIMEOUT_MS$1 = 8e3;
|
|
@@ -2781,14 +2749,14 @@ function parseGitHubRemote(value) {
|
|
|
2781
2749
|
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
2782
2750
|
return `${match[1]}/${match[2]}`;
|
|
2783
2751
|
}
|
|
2784
|
-
function record$
|
|
2752
|
+
function record$8(value) {
|
|
2785
2753
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2786
2754
|
}
|
|
2787
2755
|
function aggregateChecks(value) {
|
|
2788
2756
|
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
2789
2757
|
let pending = false;
|
|
2790
2758
|
for (const item of value) {
|
|
2791
|
-
const check = record$
|
|
2759
|
+
const check = record$8(item);
|
|
2792
2760
|
if (check === void 0) continue;
|
|
2793
2761
|
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
2794
2762
|
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
@@ -2812,7 +2780,7 @@ function reviewState(value) {
|
|
|
2812
2780
|
return "none";
|
|
2813
2781
|
}
|
|
2814
2782
|
function parsePullRequest(value) {
|
|
2815
|
-
const input = record$
|
|
2783
|
+
const input = record$8(value);
|
|
2816
2784
|
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
2817
2785
|
let url;
|
|
2818
2786
|
try {
|
|
@@ -2833,12 +2801,20 @@ function parsePullRequest(value) {
|
|
|
2833
2801
|
review: reviewState(input.reviewDecision),
|
|
2834
2802
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
2835
2803
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
2836
|
-
...typeof record$
|
|
2804
|
+
...typeof record$8(input.author)?.login === "string" ? { author: bounded(String(record$8(input.author)?.login)) } : {},
|
|
2837
2805
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
2838
2806
|
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
2839
2807
|
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
2840
2808
|
};
|
|
2841
2809
|
}
|
|
2810
|
+
var RepositoryFileError = class extends Error {
|
|
2811
|
+
code;
|
|
2812
|
+
constructor(code, message) {
|
|
2813
|
+
super(message);
|
|
2814
|
+
this.name = "RepositoryFileError";
|
|
2815
|
+
this.code = code;
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2842
2818
|
var RepositoryStatusService = class {
|
|
2843
2819
|
#runtime;
|
|
2844
2820
|
#cacheTtlMs;
|
|
@@ -2880,6 +2856,29 @@ var RepositoryStatusService = class {
|
|
|
2880
2856
|
this.#lastReady.set(cwd, stable);
|
|
2881
2857
|
return stable;
|
|
2882
2858
|
}
|
|
2859
|
+
/** Lines [from, to] of a working-tree file, used to expand unmodified context around diff hunks. */
|
|
2860
|
+
async fileLines(cwd, relativePath, from, to) {
|
|
2861
|
+
if (relativePath.length === 0 || isAbsolute(relativePath) || relativePath.includes("\0") || relativePath.split(/[\\/]/u).includes("..")) throw new RepositoryFileError("invalid-request", "The file path must be relative to the repository.");
|
|
2862
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 1 || to < from || to - from >= 500) throw new RepositoryFileError("invalid-request", "The requested line range is invalid.");
|
|
2863
|
+
const git = await this.#git();
|
|
2864
|
+
const top = await run(this.#runtime, git, [
|
|
2865
|
+
"rev-parse",
|
|
2866
|
+
"--path-format=absolute",
|
|
2867
|
+
"--show-toplevel"
|
|
2868
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2869
|
+
if (top.exitCode !== 0) throw new RepositoryFileError("not-repository", "The directory is not inside a git repository.");
|
|
2870
|
+
const root = resolve(top.stdout.trim());
|
|
2871
|
+
const target = resolve(root, relativePath);
|
|
2872
|
+
if (target !== root && !target.startsWith(root + sep)) throw new RepositoryFileError("invalid-request", "The file path escapes the repository.");
|
|
2873
|
+
const content = await readFile(target, "utf8");
|
|
2874
|
+
if (content.length > MAX_FILE_BYTES) throw new RepositoryFileError("too-large", "The file is too large to expand.");
|
|
2875
|
+
const all = content.split(/\r?\n/u);
|
|
2876
|
+
if (all.at(-1) === "") all.pop();
|
|
2877
|
+
return {
|
|
2878
|
+
lines: all.slice(from - 1, to),
|
|
2879
|
+
total: all.length
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2883
2882
|
async #git() {
|
|
2884
2883
|
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
2885
2884
|
return this.#gitExecutable;
|
|
@@ -3099,7 +3098,7 @@ var RepositoryStatusService = class {
|
|
|
3099
3098
|
const MAX_OUTPUT_BYTES$2 = 131072;
|
|
3100
3099
|
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
3101
3100
|
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
3102
|
-
const MAX_PATH_CHARS = 4096;
|
|
3101
|
+
const MAX_PATH_CHARS$2 = 4096;
|
|
3103
3102
|
const MAX_BRANCH_CHARS = 512;
|
|
3104
3103
|
const LEASE_SCHEMA_VERSION = 1;
|
|
3105
3104
|
const CLEANUP_GRACE_MS = 12e4;
|
|
@@ -3123,7 +3122,7 @@ async function collect$2(handle) {
|
|
|
3123
3122
|
};
|
|
3124
3123
|
}
|
|
3125
3124
|
function safePath(value) {
|
|
3126
|
-
if (value.length === 0 || value.length > MAX_PATH_CHARS || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
|
|
3125
|
+
if (value.length === 0 || value.length > MAX_PATH_CHARS$2 || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
|
|
3127
3126
|
return resolve(value);
|
|
3128
3127
|
}
|
|
3129
3128
|
function safeBranch(value) {
|
|
@@ -3224,10 +3223,73 @@ var RepositorySetupService = class {
|
|
|
3224
3223
|
const remote = info.remoteBranches.includes(branch);
|
|
3225
3224
|
if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
|
|
3226
3225
|
const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
|
|
3227
|
-
if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, progress);
|
|
3226
|
+
if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, requestedBranch !== void 0 && info.branches.includes(requestedBranch), progress);
|
|
3228
3227
|
progress("switching-branch");
|
|
3229
3228
|
return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
|
|
3230
3229
|
}
|
|
3230
|
+
/** Tear down a merged branch: remove a plugin worktree (and its lease and
|
|
3231
|
+
* branch), or switch a plain checkout back to the base branch and delete
|
|
3232
|
+
* the merged branch. Refuses dirty trees. */
|
|
3233
|
+
async cleanupMerged(pathValue, baseBranch) {
|
|
3234
|
+
const path = safePath(pathValue);
|
|
3235
|
+
const base = safeBranch(baseBranch);
|
|
3236
|
+
const git = await this.#git();
|
|
3237
|
+
const status = await this.#run(git, [
|
|
3238
|
+
"status",
|
|
3239
|
+
"--porcelain=v1",
|
|
3240
|
+
"--untracked-files=normal"
|
|
3241
|
+
], path);
|
|
3242
|
+
if (status.exitCode !== 0 || status.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
|
|
3243
|
+
if (status.stdout.trim().length > 0) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before cleaning up.");
|
|
3244
|
+
const lease = (await this.#readLeases()).find((item) => comparablePath(item.path) === comparablePath(path));
|
|
3245
|
+
if (lease !== void 0) return this.#serialize(async () => {
|
|
3246
|
+
if ((await this.#run(git, [
|
|
3247
|
+
"worktree",
|
|
3248
|
+
"remove",
|
|
3249
|
+
"--",
|
|
3250
|
+
lease.path
|
|
3251
|
+
], lease.root)).exitCode !== 0) throw new RepositorySetupError("worktree-remove-failed", "Git could not remove the worktree.");
|
|
3252
|
+
await this.#run(git, [
|
|
3253
|
+
"branch",
|
|
3254
|
+
"-D",
|
|
3255
|
+
"--",
|
|
3256
|
+
lease.branch
|
|
3257
|
+
], lease.root).catch(() => void 0);
|
|
3258
|
+
const current = await this.#readLeases();
|
|
3259
|
+
await this.#writeLeases(current.filter((item) => item.id !== lease.id));
|
|
3260
|
+
return {
|
|
3261
|
+
mode: "worktree",
|
|
3262
|
+
root: lease.root,
|
|
3263
|
+
branch: lease.branch
|
|
3264
|
+
};
|
|
3265
|
+
});
|
|
3266
|
+
const root = await this.#repositoryRoot(git, path);
|
|
3267
|
+
const head = await this.#run(git, [
|
|
3268
|
+
"symbolic-ref",
|
|
3269
|
+
"--quiet",
|
|
3270
|
+
"--short",
|
|
3271
|
+
"HEAD"
|
|
3272
|
+
], root);
|
|
3273
|
+
const branch = head.exitCode === 0 ? head.stdout.trim() : "";
|
|
3274
|
+
if (branch.length === 0 || branch === base) throw new RepositorySetupError("nothing-to-clean", "The checkout is already on the base branch.");
|
|
3275
|
+
if ((await this.#run(git, [
|
|
3276
|
+
"switch",
|
|
3277
|
+
"--",
|
|
3278
|
+
base
|
|
3279
|
+
], root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not switch to the base branch.");
|
|
3280
|
+
await this.#run(git, [
|
|
3281
|
+
"branch",
|
|
3282
|
+
"-D",
|
|
3283
|
+
"--",
|
|
3284
|
+
branch
|
|
3285
|
+
], root).catch(() => void 0);
|
|
3286
|
+
await this.#run(git, ["pull", "--ff-only"], root, GIT_FETCH_TIMEOUT_MS).catch(() => void 0);
|
|
3287
|
+
return {
|
|
3288
|
+
mode: "checkout",
|
|
3289
|
+
root,
|
|
3290
|
+
branch
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3231
3293
|
bindLease(leaseId, sessionId) {
|
|
3232
3294
|
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."));
|
|
3233
3295
|
return this.#serialize(async () => {
|
|
@@ -3336,8 +3398,8 @@ var RepositorySetupService = class {
|
|
|
3336
3398
|
};
|
|
3337
3399
|
}
|
|
3338
3400
|
async #checkout(info, branch) {
|
|
3339
|
-
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
3340
3401
|
if (info.current !== branch) {
|
|
3402
|
+
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
3341
3403
|
const git = await this.#git();
|
|
3342
3404
|
const worktrees = await this.#run(git, [
|
|
3343
3405
|
"worktree",
|
|
@@ -3360,7 +3422,7 @@ var RepositorySetupService = class {
|
|
|
3360
3422
|
branch
|
|
3361
3423
|
};
|
|
3362
3424
|
}
|
|
3363
|
-
async #createWorktree(root, baseBranch, baseRef, explicitBranchName, progress) {
|
|
3425
|
+
async #createWorktree(root, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress) {
|
|
3364
3426
|
const git = await this.#git();
|
|
3365
3427
|
progress("fetching");
|
|
3366
3428
|
const fetched = await this.#run(git, [
|
|
@@ -3377,14 +3439,25 @@ var RepositorySetupService = class {
|
|
|
3377
3439
|
const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
|
|
3378
3440
|
progress("creating-worktree");
|
|
3379
3441
|
await mkdir(this.#worktreeRoot, { recursive: true });
|
|
3380
|
-
if (
|
|
3442
|
+
if (reuseExistingBranch) await this.#run(git, ["worktree", "prune"], root).catch(() => void 0);
|
|
3443
|
+
const created = await this.#run(git, reuseExistingBranch ? [
|
|
3444
|
+
"worktree",
|
|
3445
|
+
"add",
|
|
3446
|
+
"--",
|
|
3447
|
+
path,
|
|
3448
|
+
branch
|
|
3449
|
+
] : [
|
|
3381
3450
|
"worktree",
|
|
3382
3451
|
"add",
|
|
3383
3452
|
"-b",
|
|
3384
3453
|
branch,
|
|
3385
3454
|
path,
|
|
3386
3455
|
baseRef
|
|
3387
|
-
], root)
|
|
3456
|
+
], root);
|
|
3457
|
+
if (created.exitCode !== 0) {
|
|
3458
|
+
const detail = created.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
|
|
3459
|
+
throw new RepositorySetupError("worktree-failed", detail === void 0 ? "Git could not create the worktree." : `Git could not create the worktree: ${detail}`);
|
|
3460
|
+
}
|
|
3388
3461
|
const item = {
|
|
3389
3462
|
id: randomUUID(),
|
|
3390
3463
|
root,
|
|
@@ -3407,6 +3480,12 @@ var RepositorySetupService = class {
|
|
|
3407
3480
|
"--",
|
|
3408
3481
|
path
|
|
3409
3482
|
], root).catch(() => void 0);
|
|
3483
|
+
if (!reuseExistingBranch) await this.#run(git, [
|
|
3484
|
+
"branch",
|
|
3485
|
+
"-D",
|
|
3486
|
+
"--",
|
|
3487
|
+
branch
|
|
3488
|
+
], root).catch(() => void 0);
|
|
3410
3489
|
throw error;
|
|
3411
3490
|
}
|
|
3412
3491
|
return {
|
|
@@ -3920,24 +3999,24 @@ var RepositoryActionService = class {
|
|
|
3920
3999
|
};
|
|
3921
4000
|
//#endregion
|
|
3922
4001
|
//#region src/repository-setup-routes.ts
|
|
3923
|
-
const MAX_BODY_BYTES$
|
|
3924
|
-
function record$
|
|
4002
|
+
const MAX_BODY_BYTES$4 = 16384;
|
|
4003
|
+
function record$7(value) {
|
|
3925
4004
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3926
4005
|
}
|
|
3927
|
-
async function readJson$
|
|
4006
|
+
async function readJson$4(req) {
|
|
3928
4007
|
const chunks = [];
|
|
3929
4008
|
let size = 0;
|
|
3930
4009
|
for await (const chunk of req) {
|
|
3931
4010
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3932
4011
|
size += buffer.length;
|
|
3933
|
-
if (size > MAX_BODY_BYTES$
|
|
4012
|
+
if (size > MAX_BODY_BYTES$4) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
3934
4013
|
chunks.push(buffer);
|
|
3935
4014
|
}
|
|
3936
|
-
const value = record$
|
|
4015
|
+
const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3937
4016
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
3938
4017
|
return value;
|
|
3939
4018
|
}
|
|
3940
|
-
function string$
|
|
4019
|
+
function string$2(input, key) {
|
|
3941
4020
|
const value = input[key];
|
|
3942
4021
|
if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
|
|
3943
4022
|
return value;
|
|
@@ -3956,17 +4035,17 @@ function ndjsonHeaders(res) {
|
|
|
3956
4035
|
});
|
|
3957
4036
|
res.flushHeaders?.();
|
|
3958
4037
|
}
|
|
3959
|
-
function ndjson(res, value) {
|
|
4038
|
+
function ndjson$1(res, value) {
|
|
3960
4039
|
res.write(`${JSON.stringify(value)}\n`);
|
|
3961
4040
|
}
|
|
3962
4041
|
async function streamSetup(res, service, input) {
|
|
3963
4042
|
ndjsonHeaders(res);
|
|
3964
4043
|
let ended = false;
|
|
3965
4044
|
try {
|
|
3966
|
-
ndjson(res, {
|
|
4045
|
+
ndjson$1(res, {
|
|
3967
4046
|
type: "complete",
|
|
3968
|
-
result: await service.setup(string$
|
|
3969
|
-
if (!ended) ndjson(res, {
|
|
4047
|
+
result: await service.setup(string$2(input, "cwd"), string$2(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
|
|
4048
|
+
if (!ended) ndjson$1(res, {
|
|
3970
4049
|
type: "progress",
|
|
3971
4050
|
stage
|
|
3972
4051
|
});
|
|
@@ -3974,7 +4053,7 @@ async function streamSetup(res, service, input) {
|
|
|
3974
4053
|
});
|
|
3975
4054
|
} catch (error) {
|
|
3976
4055
|
const setupError = error instanceof RepositorySetupError ? error : void 0;
|
|
3977
|
-
ndjson(res, {
|
|
4056
|
+
ndjson$1(res, {
|
|
3978
4057
|
type: "error",
|
|
3979
4058
|
code: setupError?.code ?? "repository-setup-unavailable",
|
|
3980
4059
|
message: setupError?.message ?? "Repository setup is unavailable."
|
|
@@ -4001,15 +4080,20 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4001
4080
|
}
|
|
4002
4081
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
4003
4082
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4004
|
-
const input = await readJson$
|
|
4083
|
+
const input = await readJson$4(req);
|
|
4005
4084
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
4006
4085
|
await streamSetup(res, service, input);
|
|
4007
4086
|
return;
|
|
4008
4087
|
}
|
|
4088
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
|
|
4089
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4090
|
+
const input = await readJson$4(req);
|
|
4091
|
+
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
4092
|
+
}
|
|
4009
4093
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
4010
4094
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4011
|
-
const input = await readJson$
|
|
4012
|
-
await service.bindLease(string$
|
|
4095
|
+
const input = await readJson$4(req);
|
|
4096
|
+
await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
|
|
4013
4097
|
return json(res, 200, { ok: true });
|
|
4014
4098
|
}
|
|
4015
4099
|
return json(res, 404, { error: "not found" });
|
|
@@ -4026,8 +4110,8 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4026
4110
|
}
|
|
4027
4111
|
//#endregion
|
|
4028
4112
|
//#region src/repository-action-routes.ts
|
|
4029
|
-
const MAX_BODY_BYTES$
|
|
4030
|
-
const MAX_SESSION_ID_CHARS$
|
|
4113
|
+
const MAX_BODY_BYTES$3 = 16384;
|
|
4114
|
+
const MAX_SESSION_ID_CHARS$3 = 1024;
|
|
4031
4115
|
const ACTIONS = /* @__PURE__ */ new Set([
|
|
4032
4116
|
"commit",
|
|
4033
4117
|
"commit-push",
|
|
@@ -4036,28 +4120,28 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
4036
4120
|
"merge-pr",
|
|
4037
4121
|
"update-branch"
|
|
4038
4122
|
]);
|
|
4039
|
-
function record$
|
|
4123
|
+
function record$6(value) {
|
|
4040
4124
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4041
4125
|
}
|
|
4042
|
-
async function readJson$
|
|
4126
|
+
async function readJson$3(req) {
|
|
4043
4127
|
const chunks = [];
|
|
4044
4128
|
let size = 0;
|
|
4045
4129
|
for await (const chunk of req) {
|
|
4046
4130
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4047
4131
|
size += buffer.length;
|
|
4048
|
-
if (size > MAX_BODY_BYTES$
|
|
4132
|
+
if (size > MAX_BODY_BYTES$3) throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
4049
4133
|
chunks.push(buffer);
|
|
4050
4134
|
}
|
|
4051
|
-
const value = record$
|
|
4135
|
+
const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4052
4136
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
4053
4137
|
return value;
|
|
4054
4138
|
}
|
|
4055
4139
|
function sessionId$1(url) {
|
|
4056
4140
|
const value = url.searchParams.get("sessionId");
|
|
4057
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
4141
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
4058
4142
|
return value;
|
|
4059
4143
|
}
|
|
4060
|
-
function string(input, key) {
|
|
4144
|
+
function string$1(input, key) {
|
|
4061
4145
|
const value = input[key];
|
|
4062
4146
|
if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
|
|
4063
4147
|
return value;
|
|
@@ -4073,8 +4157,8 @@ function actionRequest(input) {
|
|
|
4073
4157
|
if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
|
|
4074
4158
|
return {
|
|
4075
4159
|
action,
|
|
4076
|
-
fingerprint: string(input, "fingerprint"),
|
|
4077
|
-
message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
4160
|
+
fingerprint: string$1(input, "fingerprint"),
|
|
4161
|
+
message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string$1(input, "message"),
|
|
4078
4162
|
includeUnstaged: input.includeUnstaged,
|
|
4079
4163
|
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
4080
4164
|
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
@@ -4103,12 +4187,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
4103
4187
|
}
|
|
4104
4188
|
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
4105
4189
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4106
|
-
const input = await readJson$
|
|
4107
|
-
return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
|
|
4190
|
+
const input = await readJson$3(req);
|
|
4191
|
+
return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
|
|
4108
4192
|
}
|
|
4109
4193
|
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
4110
4194
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4111
|
-
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$
|
|
4195
|
+
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$3(req))));
|
|
4112
4196
|
}
|
|
4113
4197
|
return json(res, 404, { error: "not found" });
|
|
4114
4198
|
} catch (error) {
|
|
@@ -4152,14 +4236,14 @@ async function collect(handle) {
|
|
|
4152
4236
|
lossy: stdout?.lossy === true
|
|
4153
4237
|
};
|
|
4154
4238
|
}
|
|
4155
|
-
function record$
|
|
4239
|
+
function record$5(value) {
|
|
4156
4240
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4157
4241
|
}
|
|
4158
4242
|
function parseReviewComments(value) {
|
|
4159
4243
|
if (!Array.isArray(value)) return [];
|
|
4160
4244
|
const comments = [];
|
|
4161
4245
|
for (const item of value) {
|
|
4162
|
-
const input = record$
|
|
4246
|
+
const input = record$5(item);
|
|
4163
4247
|
if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
|
|
4164
4248
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
4165
4249
|
const path = typeof input.path === "string" ? input.path : "";
|
|
@@ -4171,7 +4255,7 @@ function parseReviewComments(value) {
|
|
|
4171
4255
|
path,
|
|
4172
4256
|
...line === void 0 ? {} : { line },
|
|
4173
4257
|
side: input.side === "LEFT" ? "old" : "new",
|
|
4174
|
-
author: typeof record$
|
|
4258
|
+
author: typeof record$5(input.user)?.login === "string" ? String(record$5(input.user)?.login) : "unknown",
|
|
4175
4259
|
body: body.slice(0, MAX_COMMENT_CHARS),
|
|
4176
4260
|
url
|
|
4177
4261
|
});
|
|
@@ -4188,7 +4272,7 @@ function parseFailingChecks(value) {
|
|
|
4188
4272
|
if (!Array.isArray(value)) return [];
|
|
4189
4273
|
const failing = [];
|
|
4190
4274
|
for (const item of value) {
|
|
4191
|
-
const input = record$
|
|
4275
|
+
const input = record$5(item);
|
|
4192
4276
|
if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
|
|
4193
4277
|
failing.push({
|
|
4194
4278
|
name: input.name,
|
|
@@ -4300,10 +4384,10 @@ var PullRequestFeedbackService = class {
|
|
|
4300
4384
|
};
|
|
4301
4385
|
//#endregion
|
|
4302
4386
|
//#region src/pr-feedback-routes.ts
|
|
4303
|
-
const MAX_SESSION_ID_CHARS$
|
|
4387
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
4304
4388
|
function sessionId(url) {
|
|
4305
4389
|
const value = url.searchParams.get("sessionId");
|
|
4306
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
4390
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
|
|
4307
4391
|
return value;
|
|
4308
4392
|
}
|
|
4309
4393
|
function pullNumber(url) {
|
|
@@ -4339,6 +4423,667 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
|
4339
4423
|
}), "dsh-claude: pull request feedback route");
|
|
4340
4424
|
}
|
|
4341
4425
|
//#endregion
|
|
4426
|
+
//#region src/repository-status-routes.ts
|
|
4427
|
+
const MAX_PATH_CHARS$1 = 4096;
|
|
4428
|
+
/** Read-only repository status for an arbitrary directory (the overview panel
|
|
4429
|
+
* aggregates every Claude session's checkout through this). */
|
|
4430
|
+
function registerRepositoryStatusRoute(ctx, service) {
|
|
4431
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4432
|
+
kind: "exact",
|
|
4433
|
+
path: CLAUDE_REPOSITORY_STATUS_PATH,
|
|
4434
|
+
handler: async (req, res) => {
|
|
4435
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4436
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4437
|
+
const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
|
|
4438
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
|
|
4439
|
+
error: "invalid-request",
|
|
4440
|
+
message: "The cwd query parameter is invalid."
|
|
4441
|
+
});
|
|
4442
|
+
try {
|
|
4443
|
+
return json(res, 200, await service.inspect(cwd));
|
|
4444
|
+
} catch {
|
|
4445
|
+
return json(res, 500, {
|
|
4446
|
+
error: "repository-status-unavailable",
|
|
4447
|
+
message: "Repository status is unavailable."
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
}), "dsh-claude: repository status route");
|
|
4452
|
+
}
|
|
4453
|
+
//#endregion
|
|
4454
|
+
//#region src/repository-file-routes.ts
|
|
4455
|
+
const MAX_PATH_CHARS = 4096;
|
|
4456
|
+
/** Read-only slice of a working-tree file so the diff panel can expand unmodified lines around hunks. */
|
|
4457
|
+
function registerRepositoryFileRoute(ctx, service) {
|
|
4458
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4459
|
+
kind: "exact",
|
|
4460
|
+
path: CLAUDE_REPOSITORY_FILE_PATH,
|
|
4461
|
+
handler: async (req, res) => {
|
|
4462
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4463
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4464
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
4465
|
+
const cwd = params.get("cwd");
|
|
4466
|
+
const path = params.get("path");
|
|
4467
|
+
const from = Number(params.get("from"));
|
|
4468
|
+
const to = Number(params.get("to"));
|
|
4469
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
|
|
4470
|
+
error: "invalid-request",
|
|
4471
|
+
message: "The cwd query parameter is invalid."
|
|
4472
|
+
});
|
|
4473
|
+
if (path === null || path.length === 0 || path.length > MAX_PATH_CHARS) return json(res, 400, {
|
|
4474
|
+
error: "invalid-request",
|
|
4475
|
+
message: "The path query parameter is invalid."
|
|
4476
|
+
});
|
|
4477
|
+
try {
|
|
4478
|
+
return json(res, 200, await service.fileLines(cwd, path, from, to));
|
|
4479
|
+
} catch (error) {
|
|
4480
|
+
if (error instanceof RepositoryFileError) return json(res, error.code === "invalid-request" ? 400 : 409, {
|
|
4481
|
+
error: error.code,
|
|
4482
|
+
message: error.message
|
|
4483
|
+
});
|
|
4484
|
+
return json(res, 500, {
|
|
4485
|
+
error: "repository-file-unavailable",
|
|
4486
|
+
message: "The file could not be read."
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
}), "dsh-claude: repository file route");
|
|
4491
|
+
}
|
|
4492
|
+
//#endregion
|
|
4493
|
+
//#region src/jira.ts
|
|
4494
|
+
const REQUEST_TIMEOUT_MS = 15e3;
|
|
4495
|
+
const MAX_RESULTS = 20;
|
|
4496
|
+
const MAX_QUERY_CHARS = 200;
|
|
4497
|
+
const MAX_STORE_BYTES = 65536;
|
|
4498
|
+
var JiraError = class extends Error {
|
|
4499
|
+
code;
|
|
4500
|
+
constructor(code, message) {
|
|
4501
|
+
super(message);
|
|
4502
|
+
this.name = "JiraError";
|
|
4503
|
+
this.code = code;
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
function record$4(value) {
|
|
4507
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4508
|
+
}
|
|
4509
|
+
/** Jira Cloud sites are origins; Data Center may carry a context path. */
|
|
4510
|
+
function normalizeSiteUrl(value) {
|
|
4511
|
+
let url;
|
|
4512
|
+
try {
|
|
4513
|
+
url = new URL(value.trim());
|
|
4514
|
+
} catch {
|
|
4515
|
+
throw new JiraError("invalid-site", "The Jira site URL is invalid.");
|
|
4516
|
+
}
|
|
4517
|
+
if (url.protocol !== "https:") throw new JiraError("invalid-site", "The Jira site URL must use https.");
|
|
4518
|
+
return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
|
|
4519
|
+
}
|
|
4520
|
+
function ticketKeyOf(query) {
|
|
4521
|
+
const match = /^([A-Za-z][A-Za-z0-9_]*)-(\d+)$/u.exec(query.trim());
|
|
4522
|
+
return match === null ? void 0 : `${match[1].toUpperCase()}-${match[2]}`;
|
|
4523
|
+
}
|
|
4524
|
+
function buildJql(query) {
|
|
4525
|
+
const trimmed = query.trim().slice(0, MAX_QUERY_CHARS);
|
|
4526
|
+
if (trimmed.length === 0) return "statusCategory != Done ORDER BY updated DESC";
|
|
4527
|
+
const key = ticketKeyOf(trimmed);
|
|
4528
|
+
if (key !== void 0) return `key = "${key}"`;
|
|
4529
|
+
return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
|
|
4530
|
+
}
|
|
4531
|
+
function parseTickets(value, siteUrl) {
|
|
4532
|
+
const issues = record$4(value)?.issues;
|
|
4533
|
+
if (!Array.isArray(issues)) return [];
|
|
4534
|
+
const tickets = [];
|
|
4535
|
+
for (const item of issues) {
|
|
4536
|
+
const issue = record$4(item);
|
|
4537
|
+
const fields = record$4(issue?.fields);
|
|
4538
|
+
if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
|
|
4539
|
+
const status = record$4(fields.status)?.name;
|
|
4540
|
+
const type = record$4(fields.issuetype)?.name;
|
|
4541
|
+
tickets.push({
|
|
4542
|
+
key: issue.key,
|
|
4543
|
+
summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
|
|
4544
|
+
url: `${siteUrl}/browse/${issue.key}`,
|
|
4545
|
+
...typeof status === "string" ? { status } : {},
|
|
4546
|
+
...typeof type === "string" ? { type } : {}
|
|
4547
|
+
});
|
|
4548
|
+
if (tickets.length >= MAX_RESULTS) break;
|
|
4549
|
+
}
|
|
4550
|
+
return tickets;
|
|
4551
|
+
}
|
|
4552
|
+
function statusOf(store) {
|
|
4553
|
+
if (store === void 0) return { connected: false };
|
|
4554
|
+
return {
|
|
4555
|
+
connected: true,
|
|
4556
|
+
siteUrl: store.siteUrl,
|
|
4557
|
+
email: store.email,
|
|
4558
|
+
...store.displayName === void 0 ? {} : { displayName: store.displayName }
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
/** Jira Cloud access through the user's API token (Basic auth). */
|
|
4562
|
+
var JiraService = class {
|
|
4563
|
+
#storePath;
|
|
4564
|
+
#fetch;
|
|
4565
|
+
constructor(options = {}) {
|
|
4566
|
+
this.#storePath = options.storePath ?? dshHomePath("plugins", "dsh-claude", "jira.json");
|
|
4567
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
4568
|
+
}
|
|
4569
|
+
async status() {
|
|
4570
|
+
return statusOf(await this.#read());
|
|
4571
|
+
}
|
|
4572
|
+
async connect(input) {
|
|
4573
|
+
const siteUrl = normalizeSiteUrl(input.siteUrl);
|
|
4574
|
+
const email = input.email.trim();
|
|
4575
|
+
const apiToken = input.apiToken.trim();
|
|
4576
|
+
if (email.length === 0 || email.length > 320 || apiToken.length === 0 || apiToken.length > 4096) throw new JiraError("invalid-credentials", "An account email and API token are required.");
|
|
4577
|
+
const connection = {
|
|
4578
|
+
siteUrl,
|
|
4579
|
+
email,
|
|
4580
|
+
apiToken
|
|
4581
|
+
};
|
|
4582
|
+
const myself = record$4(await this.#json(connection, "/rest/api/3/myself"));
|
|
4583
|
+
const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
|
|
4584
|
+
const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
|
|
4585
|
+
const store = {
|
|
4586
|
+
...connection,
|
|
4587
|
+
...displayName === void 0 ? {} : { displayName },
|
|
4588
|
+
...accountId === void 0 ? {} : { accountId }
|
|
4589
|
+
};
|
|
4590
|
+
await this.#write(store);
|
|
4591
|
+
return statusOf(store);
|
|
4592
|
+
}
|
|
4593
|
+
/** Assign a ticket to the connected account (used once a ticket's worktree exists). */
|
|
4594
|
+
async assignToMe(key) {
|
|
4595
|
+
const store = await this.#read();
|
|
4596
|
+
if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
|
|
4597
|
+
const ticket = ticketKeyOf(key);
|
|
4598
|
+
if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
|
|
4599
|
+
let accountId = store.accountId;
|
|
4600
|
+
if (accountId === void 0) {
|
|
4601
|
+
const myself = record$4(await this.#json(store, "/rest/api/3/myself"));
|
|
4602
|
+
if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
|
|
4603
|
+
accountId = myself.accountId;
|
|
4604
|
+
await this.#write({
|
|
4605
|
+
...store,
|
|
4606
|
+
accountId
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
await this.#json(store, `/rest/api/3/issue/${ticket}/assignee`, {
|
|
4610
|
+
method: "PUT",
|
|
4611
|
+
body: { accountId }
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
async disconnect() {
|
|
4615
|
+
await rm(this.#storePath, { force: true });
|
|
4616
|
+
}
|
|
4617
|
+
async search(query) {
|
|
4618
|
+
const store = await this.#read();
|
|
4619
|
+
if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
|
|
4620
|
+
const params = new URLSearchParams({
|
|
4621
|
+
jql: buildJql(query),
|
|
4622
|
+
maxResults: String(MAX_RESULTS),
|
|
4623
|
+
fields: "summary,status,issuetype"
|
|
4624
|
+
});
|
|
4625
|
+
let body;
|
|
4626
|
+
try {
|
|
4627
|
+
body = await this.#json(store, `/rest/api/3/search/jql?${params.toString()}`);
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
if (!(error instanceof JiraError && error.code === "not-found")) throw error;
|
|
4630
|
+
body = await this.#json(store, `/rest/api/3/search?${params.toString()}`);
|
|
4631
|
+
}
|
|
4632
|
+
return parseTickets(body, store.siteUrl);
|
|
4633
|
+
}
|
|
4634
|
+
async #json(connection, path, init = {}) {
|
|
4635
|
+
let response;
|
|
4636
|
+
try {
|
|
4637
|
+
response = await this.#fetch(`${connection.siteUrl}${path}`, {
|
|
4638
|
+
method: init.method ?? "GET",
|
|
4639
|
+
headers: {
|
|
4640
|
+
accept: "application/json",
|
|
4641
|
+
authorization: `Basic ${Buffer.from(`${connection.email}:${connection.apiToken}`).toString("base64")}`,
|
|
4642
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" }
|
|
4643
|
+
},
|
|
4644
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
|
|
4645
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
4646
|
+
});
|
|
4647
|
+
} catch {
|
|
4648
|
+
throw new JiraError("unreachable", "The Jira site could not be reached.");
|
|
4649
|
+
}
|
|
4650
|
+
if (response.status === 401 || response.status === 403) throw new JiraError("unauthorized", "Jira rejected the credentials.");
|
|
4651
|
+
if (response.status === 404) throw new JiraError("not-found", "The Jira endpoint was not found.");
|
|
4652
|
+
if (!response.ok) throw new JiraError("jira-failed", `Jira responded with HTTP ${response.status}.`);
|
|
4653
|
+
if (response.status === 204) return void 0;
|
|
4654
|
+
try {
|
|
4655
|
+
return await response.json();
|
|
4656
|
+
} catch {
|
|
4657
|
+
throw new JiraError("jira-failed", "Jira returned an invalid response.");
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
async #read() {
|
|
4661
|
+
let text;
|
|
4662
|
+
try {
|
|
4663
|
+
text = await readFile(this.#storePath, "utf8");
|
|
4664
|
+
} catch (error) {
|
|
4665
|
+
if (error.code === "ENOENT") return void 0;
|
|
4666
|
+
throw error;
|
|
4667
|
+
}
|
|
4668
|
+
if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
|
|
4669
|
+
const input = record$4(JSON.parse(text));
|
|
4670
|
+
if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
|
|
4671
|
+
return {
|
|
4672
|
+
siteUrl: input.siteUrl,
|
|
4673
|
+
email: input.email,
|
|
4674
|
+
apiToken: input.apiToken,
|
|
4675
|
+
...typeof input.displayName === "string" ? { displayName: input.displayName } : {},
|
|
4676
|
+
...typeof input.accountId === "string" ? { accountId: input.accountId } : {}
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
async #write(store) {
|
|
4680
|
+
await mkdir(dirname(this.#storePath), {
|
|
4681
|
+
recursive: true,
|
|
4682
|
+
mode: 448
|
|
4683
|
+
});
|
|
4684
|
+
const temporary = `${this.#storePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
4685
|
+
try {
|
|
4686
|
+
await writeFile(temporary, `${JSON.stringify(store, null, 2)}\n`, {
|
|
4687
|
+
encoding: "utf8",
|
|
4688
|
+
mode: 384,
|
|
4689
|
+
flag: "wx"
|
|
4690
|
+
});
|
|
4691
|
+
await chmod(temporary, 384);
|
|
4692
|
+
await rename(temporary, this.#storePath);
|
|
4693
|
+
} finally {
|
|
4694
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
};
|
|
4698
|
+
//#endregion
|
|
4699
|
+
//#region src/jira-routes.ts
|
|
4700
|
+
const MAX_BODY_BYTES$2 = 8192;
|
|
4701
|
+
function record$3(value) {
|
|
4702
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4703
|
+
}
|
|
4704
|
+
async function readJson$2(req) {
|
|
4705
|
+
const chunks = [];
|
|
4706
|
+
let size = 0;
|
|
4707
|
+
for await (const chunk of req) {
|
|
4708
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4709
|
+
size += buffer.length;
|
|
4710
|
+
if (size > MAX_BODY_BYTES$2) throw new JiraError("body-too-large", "The request body is too large.");
|
|
4711
|
+
chunks.push(buffer);
|
|
4712
|
+
}
|
|
4713
|
+
const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4714
|
+
if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
|
|
4715
|
+
return value;
|
|
4716
|
+
}
|
|
4717
|
+
function string(input, key) {
|
|
4718
|
+
const value = input[key];
|
|
4719
|
+
if (typeof value !== "string") throw new JiraError("invalid-request", `The ${key} field is required.`);
|
|
4720
|
+
return value;
|
|
4721
|
+
}
|
|
4722
|
+
function registerJiraRoute(ctx, service) {
|
|
4723
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4724
|
+
kind: "prefix",
|
|
4725
|
+
path: CLAUDE_JIRA_PATH,
|
|
4726
|
+
handler: async (req, res) => {
|
|
4727
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4728
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
4729
|
+
try {
|
|
4730
|
+
if (url.pathname === `/plugins/dsh-claude/jira/status`) {
|
|
4731
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4732
|
+
return json(res, 200, await service.status());
|
|
4733
|
+
}
|
|
4734
|
+
if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
|
|
4735
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4736
|
+
const input = await readJson$2(req);
|
|
4737
|
+
return json(res, 200, await service.connect({
|
|
4738
|
+
siteUrl: string(input, "siteUrl"),
|
|
4739
|
+
email: string(input, "email"),
|
|
4740
|
+
apiToken: string(input, "apiToken")
|
|
4741
|
+
}));
|
|
4742
|
+
}
|
|
4743
|
+
if (url.pathname === `/plugins/dsh-claude/jira/disconnect`) {
|
|
4744
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4745
|
+
await service.disconnect();
|
|
4746
|
+
return json(res, 200, { connected: false });
|
|
4747
|
+
}
|
|
4748
|
+
if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
|
|
4749
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4750
|
+
const input = await readJson$2(req);
|
|
4751
|
+
await service.assignToMe(string(input, "key"));
|
|
4752
|
+
return json(res, 200, { assigned: true });
|
|
4753
|
+
}
|
|
4754
|
+
if (url.pathname === `/plugins/dsh-claude/jira/search`) {
|
|
4755
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4756
|
+
return json(res, 200, { tickets: await service.search(url.searchParams.get("query") ?? "") });
|
|
4757
|
+
}
|
|
4758
|
+
return json(res, 404, { error: "not found" });
|
|
4759
|
+
} catch (error) {
|
|
4760
|
+
if (error instanceof JiraError) return json(res, 409, {
|
|
4761
|
+
error: error.code,
|
|
4762
|
+
message: error.message
|
|
4763
|
+
});
|
|
4764
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
4765
|
+
return json(res, 500, {
|
|
4766
|
+
error: "jira-unavailable",
|
|
4767
|
+
message: "Jira is unavailable."
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
}), "dsh-claude: jira route");
|
|
4772
|
+
}
|
|
4773
|
+
//#endregion
|
|
4774
|
+
//#region src/ask.ts
|
|
4775
|
+
const MAX_SELECTION_CHARS = 8e3;
|
|
4776
|
+
const MAX_CONTEXT_CHARS = 16e3;
|
|
4777
|
+
const MAX_QUESTION_CHARS = 2e3;
|
|
4778
|
+
const ASK_TIMEOUT_MS = 18e4;
|
|
4779
|
+
const MAX_STDERR_BYTES = 65536;
|
|
4780
|
+
const MAX_TOOL_SUMMARY_CHARS = 160;
|
|
4781
|
+
const READ_ONLY_TOOLS = [
|
|
4782
|
+
"Read",
|
|
4783
|
+
"Grep",
|
|
4784
|
+
"Glob"
|
|
4785
|
+
];
|
|
4786
|
+
var AskError = class extends Error {
|
|
4787
|
+
code;
|
|
4788
|
+
constructor(code, message) {
|
|
4789
|
+
super(message);
|
|
4790
|
+
this.name = "AskError";
|
|
4791
|
+
this.code = code;
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
function fence(value) {
|
|
4795
|
+
return `"""\n${value.replaceAll("\"\"\"", "\" \" \"")}\n"""`;
|
|
4796
|
+
}
|
|
4797
|
+
function askPrompt(request) {
|
|
4798
|
+
const selection = request.selection.trim().slice(0, MAX_SELECTION_CHARS);
|
|
4799
|
+
const context = (request.context ?? "").trim().slice(0, MAX_CONTEXT_CHARS);
|
|
4800
|
+
const question = request.question.trim().slice(0, MAX_QUESTION_CHARS);
|
|
4801
|
+
return [
|
|
4802
|
+
"You are answering a follow-up question about part of an earlier assistant reply in this project.",
|
|
4803
|
+
"",
|
|
4804
|
+
"Selected passage:",
|
|
4805
|
+
fence(selection),
|
|
4806
|
+
...context.length === 0 || context === selection ? [] : [
|
|
4807
|
+
"",
|
|
4808
|
+
"Surrounding reply, for context only:",
|
|
4809
|
+
fence(context)
|
|
4810
|
+
],
|
|
4811
|
+
"",
|
|
4812
|
+
`Question: ${question}`,
|
|
4813
|
+
"",
|
|
4814
|
+
"Answer the question directly and concisely, in the same language as the question. Use Markdown.",
|
|
4815
|
+
"Only the read-only tools Read, Grep, and Glob are available; use them when the answer depends on project code, otherwise answer from the passage."
|
|
4816
|
+
].join("\n");
|
|
4817
|
+
}
|
|
4818
|
+
/** CLI effort for a session thinking mode; `off` and unknown modes send nothing. */
|
|
4819
|
+
function effortFor(thinkingMode) {
|
|
4820
|
+
if (thinkingMode === void 0 || thinkingMode === "off") return void 0;
|
|
4821
|
+
if (thinkingMode === "ultracode") return "max";
|
|
4822
|
+
return [
|
|
4823
|
+
"low",
|
|
4824
|
+
"medium",
|
|
4825
|
+
"high",
|
|
4826
|
+
"max"
|
|
4827
|
+
].includes(thinkingMode) ? thinkingMode : void 0;
|
|
4828
|
+
}
|
|
4829
|
+
function askArguments(preferences) {
|
|
4830
|
+
const effort = effortFor(preferences.thinkingMode);
|
|
4831
|
+
return [
|
|
4832
|
+
"-p",
|
|
4833
|
+
"--output-format",
|
|
4834
|
+
"stream-json",
|
|
4835
|
+
"--verbose",
|
|
4836
|
+
"--include-partial-messages",
|
|
4837
|
+
"--strict-mcp-config",
|
|
4838
|
+
"--mcp-config",
|
|
4839
|
+
"{\"mcpServers\":{}}",
|
|
4840
|
+
...preferences.model === void 0 || preferences.model === "default" ? [] : ["--model", preferences.model],
|
|
4841
|
+
...effort === void 0 ? [] : ["--effort", effort],
|
|
4842
|
+
"--tools",
|
|
4843
|
+
...READ_ONLY_TOOLS,
|
|
4844
|
+
"--allowedTools",
|
|
4845
|
+
...READ_ONLY_TOOLS
|
|
4846
|
+
];
|
|
4847
|
+
}
|
|
4848
|
+
function record$2(value) {
|
|
4849
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4850
|
+
}
|
|
4851
|
+
/** One-line description of a tool call, mirroring the main window's step titles. */
|
|
4852
|
+
function toolSummary(input) {
|
|
4853
|
+
const fields = record$2(input);
|
|
4854
|
+
if (fields === void 0) return void 0;
|
|
4855
|
+
const candidate = [
|
|
4856
|
+
fields.command,
|
|
4857
|
+
fields.pattern,
|
|
4858
|
+
fields.file_path,
|
|
4859
|
+
fields.path,
|
|
4860
|
+
fields.query
|
|
4861
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
4862
|
+
return (typeof candidate === "string" ? candidate : JSON.stringify(fields)).replaceAll(/\s+/gu, " ").trim().slice(0, MAX_TOOL_SUMMARY_CHARS);
|
|
4863
|
+
}
|
|
4864
|
+
function eventsOfStreamLine(line) {
|
|
4865
|
+
let parsed;
|
|
4866
|
+
try {
|
|
4867
|
+
parsed = record$2(JSON.parse(line));
|
|
4868
|
+
} catch {
|
|
4869
|
+
return [];
|
|
4870
|
+
}
|
|
4871
|
+
if (parsed === void 0) return [];
|
|
4872
|
+
if (parsed.type === "system" && parsed.subtype === "init") return [{
|
|
4873
|
+
type: "status",
|
|
4874
|
+
text: "ready"
|
|
4875
|
+
}];
|
|
4876
|
+
if (parsed.type === "stream_event") {
|
|
4877
|
+
const event = record$2(parsed.event);
|
|
4878
|
+
if (event?.type === "content_block_start") {
|
|
4879
|
+
const block = record$2(event.content_block);
|
|
4880
|
+
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
|
|
4881
|
+
type: "tool",
|
|
4882
|
+
id: block.id,
|
|
4883
|
+
phase: "start",
|
|
4884
|
+
name: block.name
|
|
4885
|
+
}];
|
|
4886
|
+
return [];
|
|
4887
|
+
}
|
|
4888
|
+
const delta = record$2(event?.delta);
|
|
4889
|
+
if (event?.type !== "content_block_delta" || delta === void 0) return [];
|
|
4890
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") return [{
|
|
4891
|
+
type: "text",
|
|
4892
|
+
text: delta.text
|
|
4893
|
+
}];
|
|
4894
|
+
if (delta.type === "thinking_delta" && typeof delta.thinking === "string") return [{
|
|
4895
|
+
type: "thinking",
|
|
4896
|
+
text: delta.thinking
|
|
4897
|
+
}];
|
|
4898
|
+
return [];
|
|
4899
|
+
}
|
|
4900
|
+
if (parsed.type === "assistant" || parsed.type === "user") {
|
|
4901
|
+
const content = record$2(parsed.message)?.content;
|
|
4902
|
+
if (!Array.isArray(content)) return [];
|
|
4903
|
+
const events = [];
|
|
4904
|
+
for (const item of content) {
|
|
4905
|
+
const block = record$2(item);
|
|
4906
|
+
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
4907
|
+
const summary = toolSummary(block.input);
|
|
4908
|
+
events.push({
|
|
4909
|
+
type: "tool",
|
|
4910
|
+
id: block.id,
|
|
4911
|
+
phase: "input",
|
|
4912
|
+
name: block.name,
|
|
4913
|
+
...summary === void 0 ? {} : { summary }
|
|
4914
|
+
});
|
|
4915
|
+
} else if (block?.type === "tool_result" && typeof block.tool_use_id === "string") events.push({
|
|
4916
|
+
type: "tool",
|
|
4917
|
+
id: block.tool_use_id,
|
|
4918
|
+
phase: "done",
|
|
4919
|
+
...block.is_error === true ? { error: true } : {}
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
return events;
|
|
4923
|
+
}
|
|
4924
|
+
if (parsed.type === "result" && typeof parsed.result === "string") return [{
|
|
4925
|
+
type: "result",
|
|
4926
|
+
text: parsed.result
|
|
4927
|
+
}];
|
|
4928
|
+
return [];
|
|
4929
|
+
}
|
|
4930
|
+
/** One-shot Claude Code query answering a question about selected reply text. */
|
|
4931
|
+
var AskService = class {
|
|
4932
|
+
#runtime;
|
|
4933
|
+
#executablePath;
|
|
4934
|
+
constructor(runtime, executablePath) {
|
|
4935
|
+
this.#runtime = runtime;
|
|
4936
|
+
this.#executablePath = executablePath;
|
|
4937
|
+
}
|
|
4938
|
+
async ask(cwd, request, preferences, onEvent, signal) {
|
|
4939
|
+
if (request.question.trim().length === 0 || request.selection.trim().length === 0) throw new AskError("invalid-request", "A selection and a question are required.");
|
|
4940
|
+
if (this.#executablePath.length === 0) throw new AskError("claude-unavailable", "Claude Code is unavailable.");
|
|
4941
|
+
const timeout = AbortSignal.timeout(ASK_TIMEOUT_MS);
|
|
4942
|
+
const handle = this.#runtime.spawn({
|
|
4943
|
+
argv: [this.#executablePath, ...askArguments(preferences)],
|
|
4944
|
+
cwd,
|
|
4945
|
+
stdio: {
|
|
4946
|
+
stdin: { data: askPrompt(request) },
|
|
4947
|
+
stdout: "pipe",
|
|
4948
|
+
stderr: { maxBytes: MAX_STDERR_BYTES }
|
|
4949
|
+
},
|
|
4950
|
+
graceMs: 1e3,
|
|
4951
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
|
|
4952
|
+
env: {}
|
|
4953
|
+
});
|
|
4954
|
+
const stdout = handle.stdout;
|
|
4955
|
+
if (stdout === void 0) throw new AskError("ask-failed", "Claude output is unavailable.");
|
|
4956
|
+
const decoder = new StringDecoder("utf8");
|
|
4957
|
+
let buffer = "";
|
|
4958
|
+
let emitted = false;
|
|
4959
|
+
let result;
|
|
4960
|
+
const consume = (line) => {
|
|
4961
|
+
for (const event of eventsOfStreamLine(line)) {
|
|
4962
|
+
if (event.type === "result") {
|
|
4963
|
+
result = event.text;
|
|
4964
|
+
continue;
|
|
4965
|
+
}
|
|
4966
|
+
if (event.type !== "tool" && event.text.length === 0) continue;
|
|
4967
|
+
if (event.type === "text") emitted = true;
|
|
4968
|
+
onEvent(event);
|
|
4969
|
+
}
|
|
4970
|
+
};
|
|
4971
|
+
for await (const chunk of stdout) {
|
|
4972
|
+
buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
4973
|
+
const lines = buffer.split("\n");
|
|
4974
|
+
buffer = lines.pop() ?? "";
|
|
4975
|
+
for (const line of lines) if (line.trim().length > 0) consume(line);
|
|
4976
|
+
}
|
|
4977
|
+
buffer += decoder.end();
|
|
4978
|
+
if (buffer.trim().length > 0) consume(buffer);
|
|
4979
|
+
const outcome = await handle.done;
|
|
4980
|
+
if (!emitted && result !== void 0 && result.length > 0) {
|
|
4981
|
+
emitted = true;
|
|
4982
|
+
onEvent({
|
|
4983
|
+
type: "text",
|
|
4984
|
+
text: result
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
if (!emitted) {
|
|
4988
|
+
if (signal?.aborted === true) throw new AskError("aborted", "The question was cancelled.");
|
|
4989
|
+
if (timeout.aborted) throw new AskError("timeout", "Claude took too long to answer.");
|
|
4990
|
+
const stderr = handle.collected.stderr?.readFrom(0).text.trim().split(/\r?\n/u).filter((line) => line.length > 0).at(-1);
|
|
4991
|
+
throw new AskError("ask-failed", stderr !== void 0 && stderr.length > 0 ? stderr : `Claude exited with code ${String(outcome.exitCode)}.`);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
};
|
|
4995
|
+
//#endregion
|
|
4996
|
+
//#region src/ask-routes.ts
|
|
4997
|
+
const MAX_BODY_BYTES$1 = 131072;
|
|
4998
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
4999
|
+
function record$1(value) {
|
|
5000
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5001
|
+
}
|
|
5002
|
+
async function readJson$1(req) {
|
|
5003
|
+
const chunks = [];
|
|
5004
|
+
let size = 0;
|
|
5005
|
+
for await (const chunk of req) {
|
|
5006
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5007
|
+
size += buffer.length;
|
|
5008
|
+
if (size > MAX_BODY_BYTES$1) throw new AskError("body-too-large", "The request body is too large.");
|
|
5009
|
+
chunks.push(buffer);
|
|
5010
|
+
}
|
|
5011
|
+
const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5012
|
+
if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
5013
|
+
return value;
|
|
5014
|
+
}
|
|
5015
|
+
function askRequest(input) {
|
|
5016
|
+
if (typeof input.selection !== "string" || typeof input.question !== "string" || input.context !== void 0 && typeof input.context !== "string") throw new AskError("invalid-request", "The selection and question fields are required.");
|
|
5017
|
+
return {
|
|
5018
|
+
selection: input.selection,
|
|
5019
|
+
question: input.question,
|
|
5020
|
+
...typeof input.context === "string" ? { context: input.context } : {}
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
function ndjson(res, value) {
|
|
5024
|
+
res.write(`${JSON.stringify(value)}\n`);
|
|
5025
|
+
}
|
|
5026
|
+
function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
5027
|
+
ctx.effect(() => ctx.webServer.register({
|
|
5028
|
+
kind: "exact",
|
|
5029
|
+
path: CLAUDE_ASK_PATH,
|
|
5030
|
+
handler: async (req, res) => {
|
|
5031
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
5032
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5033
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5034
|
+
let cwd;
|
|
5035
|
+
let request;
|
|
5036
|
+
let sessionId;
|
|
5037
|
+
try {
|
|
5038
|
+
const value = url.searchParams.get("sessionId");
|
|
5039
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new AskError("invalid-session", "The session is invalid.");
|
|
5040
|
+
sessionId = value;
|
|
5041
|
+
const resolved = cwdForSession(sessionId);
|
|
5042
|
+
if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
|
|
5043
|
+
cwd = resolved;
|
|
5044
|
+
request = askRequest(await readJson$1(req));
|
|
5045
|
+
} catch (error) {
|
|
5046
|
+
if (error instanceof AskError) return json(res, 409, {
|
|
5047
|
+
error: error.code,
|
|
5048
|
+
message: error.message
|
|
5049
|
+
});
|
|
5050
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
5051
|
+
return json(res, 500, {
|
|
5052
|
+
error: "ask-unavailable",
|
|
5053
|
+
message: "The question could not be sent."
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
res.writeHead(200, {
|
|
5057
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
5058
|
+
"cache-control": "no-store",
|
|
5059
|
+
"x-content-type-options": "nosniff"
|
|
5060
|
+
});
|
|
5061
|
+
res.flushHeaders?.();
|
|
5062
|
+
const controller = new AbortController();
|
|
5063
|
+
req.on("close", () => {
|
|
5064
|
+
controller.abort();
|
|
5065
|
+
});
|
|
5066
|
+
try {
|
|
5067
|
+
await service.ask(cwd, request, preferencesFor(sessionId) ?? {}, (event) => {
|
|
5068
|
+
ndjson(res, event.type === "text" ? {
|
|
5069
|
+
type: "delta",
|
|
5070
|
+
text: event.text
|
|
5071
|
+
} : event);
|
|
5072
|
+
}, controller.signal);
|
|
5073
|
+
ndjson(res, { type: "done" });
|
|
5074
|
+
} catch (error) {
|
|
5075
|
+
ndjson(res, {
|
|
5076
|
+
type: "error",
|
|
5077
|
+
code: error instanceof AskError ? error.code : "ask-unavailable",
|
|
5078
|
+
message: error instanceof AskError ? error.message : "The question could not be answered."
|
|
5079
|
+
});
|
|
5080
|
+
} finally {
|
|
5081
|
+
res.end();
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
}), "dsh-claude: ask route");
|
|
5085
|
+
}
|
|
5086
|
+
//#endregion
|
|
4342
5087
|
//#region src/review-comment-routes.ts
|
|
4343
5088
|
const MAX_BODY_BYTES = 16384;
|
|
4344
5089
|
const MAX_SESSION_ID_CHARS = 1024;
|
|
@@ -4378,6 +5123,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
4378
5123
|
return json(res, 200, { comment: store.add(sessionId, {
|
|
4379
5124
|
path: input.path,
|
|
4380
5125
|
line: input.line,
|
|
5126
|
+
startLine: input.startLine,
|
|
4381
5127
|
side: input.side,
|
|
4382
5128
|
text: input.text
|
|
4383
5129
|
}) });
|
|
@@ -5196,6 +5942,9 @@ async function apply(ctx, config) {
|
|
|
5196
5942
|
onUpdated: applyLimitOverrides
|
|
5197
5943
|
});
|
|
5198
5944
|
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
5945
|
+
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
5946
|
+
registerRepositoryFileRoute(webCtx, repositoryStatus);
|
|
5947
|
+
registerJiraRoute(webCtx, new JiraService());
|
|
5199
5948
|
const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
|
|
5200
5949
|
const cwdForClaudeSession = (sessionId) => {
|
|
5201
5950
|
const agent = webCtx.agents.get(sessionId);
|
|
@@ -5204,6 +5953,13 @@ async function apply(ctx, config) {
|
|
|
5204
5953
|
};
|
|
5205
5954
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
5206
5955
|
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
5956
|
+
registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
5957
|
+
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|
|
5958
|
+
return snapshot === void 0 ? void 0 : {
|
|
5959
|
+
model: snapshot.model,
|
|
5960
|
+
...snapshot.thinkingMode === void 0 ? {} : { thinkingMode: snapshot.thinkingMode }
|
|
5961
|
+
};
|
|
5962
|
+
});
|
|
5207
5963
|
const ownsClaudeSession = (sessionId) => {
|
|
5208
5964
|
const agent = webCtx.agents.get(sessionId);
|
|
5209
5965
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|