@norman-else/dsh-claude 0.1.29 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +46 -3
- package/lib/client.js +6717 -567
- package/lib/client.js.map +1 -1
- package/lib/{command-bridge-BYj0VF4J.mjs → command-bridge-DXI6nWhB.mjs} +2 -2
- package/lib/{command-bridge-BYj0VF4J.mjs.map → command-bridge-DXI6nWhB.mjs.map} +1 -1
- package/lib/{events-lDt9nTUw.mjs → events-DeSV0S1-.mjs} +5 -2
- package/lib/events-DeSV0S1-.mjs.map +1 -0
- package/lib/index.d.mts +38 -1
- package/lib/index.mjs +1067 -187
- package/lib/index.mjs.map +1 -1
- package/lib/{preset-installer-CPOH9lAr.mjs → preset-installer-DbmlhBXI.mjs} +9 -11
- package/lib/preset-installer-DbmlhBXI.mjs.map +1 -0
- package/lib/preset-route.mjs +2 -2
- package/package.json +188 -181
- package/lib/events-lDt9nTUw.mjs.map +0 -1
- package/lib/preset-installer-CPOH9lAr.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
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 { A as CLAUDE_UPDATE_PATH, C as CLAUDE_REPOSITORY_FEEDBACK_PATH, D as CLAUDE_REVIEW_COMMENT_PATH, E as CLAUDE_REPOSITORY_STATUS_PATH, N as TASK_TOOL_NAMES, O as CLAUDE_REWIND_PATH, S as CLAUDE_REPOSITORY_ACTION_PATH, T as CLAUDE_REPOSITORY_SETUP_PATH, _ as CLAUDE_DOCTOR_PATH, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_CODE_PROVIDER_IDS, h as CLAUDE_CODE_PROVIDER, i as latestClaudeSessionBinding, j as CLAUDE_USAGE_PATH, k as CLAUDE_UPDATE_CHECK_PATH, l as redactText, m as CLAUDE_CODE_PRESET_ID, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CLIENT_DIAGNOSTICS_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_EDITOR_OPEN_PATH, w as CLAUDE_REPOSITORY_FILE_PATH, x as CLAUDE_PROJECTION_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH } from "./events-DeSV0S1-.mjs";
|
|
2
|
+
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-DXI6nWhB.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DbmlhBXI.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";
|
|
@@ -13,6 +13,67 @@ import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subproce
|
|
|
13
13
|
import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { StringDecoder } from "node:string_decoder";
|
|
16
|
+
//#region src/rewind.ts
|
|
17
|
+
const EMPTY_REWIND_STATE = {
|
|
18
|
+
ranges: [],
|
|
19
|
+
anchors: []
|
|
20
|
+
};
|
|
21
|
+
/** Absorb one span into an ascending, non-overlapping range list. Adjacent
|
|
22
|
+
* spans merge so a rewind of a rewind reads as one hidden block. */
|
|
23
|
+
function mergeRewindRanges(ranges, addition) {
|
|
24
|
+
const merged = [];
|
|
25
|
+
let { start, end } = addition;
|
|
26
|
+
for (const range of [...ranges].sort((left, right) => left.start - right.start)) if (range.end + 1 < start) merged.push(range);
|
|
27
|
+
else if (range.start > end + 1) {
|
|
28
|
+
merged.push({
|
|
29
|
+
start,
|
|
30
|
+
end
|
|
31
|
+
});
|
|
32
|
+
start = range.start;
|
|
33
|
+
end = range.end;
|
|
34
|
+
} else {
|
|
35
|
+
start = Math.min(start, range.start);
|
|
36
|
+
end = Math.max(end, range.end);
|
|
37
|
+
}
|
|
38
|
+
merged.push({
|
|
39
|
+
start,
|
|
40
|
+
end
|
|
41
|
+
});
|
|
42
|
+
return merged.slice(-200);
|
|
43
|
+
}
|
|
44
|
+
/** Record one completed turn's last chain entry, replacing a re-run turn. */
|
|
45
|
+
function recordRewindAnchor(state, anchor) {
|
|
46
|
+
const anchors = [...state.anchors.filter((item) => item.turn !== anchor.turn), anchor].sort((left, right) => left.turn - right.turn).slice(-2e3);
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
anchors
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** The turn a surface seq belongs to: the first turn opened at or after it.
|
|
53
|
+
* A message accepted but never run belongs to no logged turn, so nothing
|
|
54
|
+
* Claude holds is discarded and every anchor stays valid. */
|
|
55
|
+
function turnAtOrAfter(events, seq) {
|
|
56
|
+
for (const event of events) if (event.seq >= seq && event.type === "turn/start") return event.data.turn;
|
|
57
|
+
}
|
|
58
|
+
/** Plan one rewind at `seq`, or undefined when the seq is not in the log.
|
|
59
|
+
* Anchors of the discarded turns go with them: after this rewind Claude no
|
|
60
|
+
* longer holds those entries, so a later rewind must never fork at one. */
|
|
61
|
+
function planRewind(state, events, seq) {
|
|
62
|
+
const last = events.at(-1)?.seq;
|
|
63
|
+
if (last === void 0 || seq > last) return void 0;
|
|
64
|
+
const turn = turnAtOrAfter(events, seq) ?? Number.MAX_SAFE_INTEGER;
|
|
65
|
+
const anchors = state.anchors.filter((anchor) => anchor.turn < turn);
|
|
66
|
+
const kept = anchors.at(-1);
|
|
67
|
+
return {
|
|
68
|
+
ranges: mergeRewindRanges(state.ranges, {
|
|
69
|
+
start: seq,
|
|
70
|
+
end: last
|
|
71
|
+
}),
|
|
72
|
+
anchors,
|
|
73
|
+
pending: kept === void 0 ? { fresh: true } : { resumeAt: kept.uuid }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
16
77
|
//#region src/sidecar.ts
|
|
17
78
|
const SIDECAR_SCHEMA_VERSION = 1;
|
|
18
79
|
const MAX_ACTIVITIES = 1e4;
|
|
@@ -26,7 +87,7 @@ function emptyProjection() {
|
|
|
26
87
|
activities: []
|
|
27
88
|
};
|
|
28
89
|
}
|
|
29
|
-
function record$
|
|
90
|
+
function record$13(value) {
|
|
30
91
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
31
92
|
}
|
|
32
93
|
function finiteInteger(value) {
|
|
@@ -36,7 +97,7 @@ function string$4(value, max) {
|
|
|
36
97
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
37
98
|
}
|
|
38
99
|
function binding(value) {
|
|
39
|
-
const input = record$
|
|
100
|
+
const input = record$13(value);
|
|
40
101
|
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;
|
|
41
102
|
return {
|
|
42
103
|
claudeSessionId: input.claudeSessionId,
|
|
@@ -48,6 +109,7 @@ function binding(value) {
|
|
|
48
109
|
const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
|
|
49
110
|
"text",
|
|
50
111
|
"status",
|
|
112
|
+
"compaction",
|
|
51
113
|
"thinking",
|
|
52
114
|
"tool-call",
|
|
53
115
|
"tool-result",
|
|
@@ -66,36 +128,77 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
66
128
|
"failed"
|
|
67
129
|
]);
|
|
68
130
|
function activity(value) {
|
|
69
|
-
const input = record$
|
|
131
|
+
const input = record$13(value);
|
|
70
132
|
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;
|
|
71
133
|
return normalizeActivity(input);
|
|
72
134
|
}
|
|
73
135
|
function contextUsage(value) {
|
|
74
|
-
const input = record$
|
|
136
|
+
const input = record$13(value);
|
|
75
137
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
76
138
|
return normalizeContextUsage(input);
|
|
77
139
|
}
|
|
140
|
+
function rewind(value) {
|
|
141
|
+
const input = record$13(value);
|
|
142
|
+
if (input === void 0 || !Array.isArray(input.ranges) || input.ranges.length > 200 || !Array.isArray(input.anchors) || input.anchors.length > 2e3) return void 0;
|
|
143
|
+
const ranges = [];
|
|
144
|
+
for (const item of input.ranges) {
|
|
145
|
+
const range = record$13(item);
|
|
146
|
+
if (range === void 0 || !finiteInteger(range.start) || !finiteInteger(range.end) || range.end < range.start) return void 0;
|
|
147
|
+
ranges.push({
|
|
148
|
+
start: range.start,
|
|
149
|
+
end: range.end
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const anchors = [];
|
|
153
|
+
for (const item of input.anchors) {
|
|
154
|
+
const anchor = record$13(item);
|
|
155
|
+
if (anchor === void 0 || !finiteInteger(anchor.turn) || !string$4(anchor.uuid, 128)) return void 0;
|
|
156
|
+
anchors.push({
|
|
157
|
+
turn: anchor.turn,
|
|
158
|
+
uuid: anchor.uuid
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const pending = record$13(input.pending);
|
|
162
|
+
if (input.pending !== void 0 && pending === void 0) return void 0;
|
|
163
|
+
if (pending === void 0) return {
|
|
164
|
+
ranges,
|
|
165
|
+
anchors
|
|
166
|
+
};
|
|
167
|
+
if (pending.fresh === true) return {
|
|
168
|
+
ranges,
|
|
169
|
+
anchors,
|
|
170
|
+
pending: { fresh: true }
|
|
171
|
+
};
|
|
172
|
+
if (!string$4(pending.resumeAt, 128)) return void 0;
|
|
173
|
+
return {
|
|
174
|
+
ranges,
|
|
175
|
+
anchors,
|
|
176
|
+
pending: { resumeAt: pending.resumeAt }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
78
179
|
function tasks(value) {
|
|
79
|
-
const input = record$
|
|
180
|
+
const input = record$13(value);
|
|
80
181
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
81
182
|
return normalizeTasksEvent(input.tasks);
|
|
82
183
|
}
|
|
83
184
|
function parseClaudeSidecar(value) {
|
|
84
|
-
const input = record$
|
|
185
|
+
const input = record$13(value);
|
|
85
186
|
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");
|
|
86
187
|
const activities = input.activities.map(activity);
|
|
87
188
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
88
189
|
const parsedBinding = input.binding === void 0 ? void 0 : binding(input.binding);
|
|
89
190
|
const parsedUsage = input.contextUsage === void 0 ? void 0 : contextUsage(input.contextUsage);
|
|
90
191
|
const parsedTasks = input.tasks === void 0 ? void 0 : tasks(input.tasks);
|
|
91
|
-
|
|
192
|
+
const parsedRewind = input.rewind === void 0 ? void 0 : rewind(input.rewind);
|
|
193
|
+
if (input.binding !== void 0 && parsedBinding === void 0 || input.contextUsage !== void 0 && parsedUsage === void 0 || input.tasks !== void 0 && parsedTasks === void 0 || input.rewind !== void 0 && parsedRewind === void 0) throw new Error("dsh-claude: invalid sidecar projection");
|
|
92
194
|
return {
|
|
93
195
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
94
196
|
revision: input.revision,
|
|
95
197
|
activities,
|
|
96
198
|
...parsedBinding === void 0 ? {} : { binding: parsedBinding },
|
|
97
199
|
...parsedUsage === void 0 ? {} : { contextUsage: parsedUsage },
|
|
98
|
-
...parsedTasks === void 0 ? {} : { tasks: parsedTasks }
|
|
200
|
+
...parsedTasks === void 0 ? {} : { tasks: parsedTasks },
|
|
201
|
+
...parsedRewind === void 0 ? {} : { rewind: parsedRewind }
|
|
99
202
|
};
|
|
100
203
|
}
|
|
101
204
|
function compareActivity(left, right) {
|
|
@@ -279,6 +382,35 @@ var ClaudeSidecarRepository = class {
|
|
|
279
382
|
value: normalized
|
|
280
383
|
});
|
|
281
384
|
}
|
|
385
|
+
/** Land one planned rewind: hidden ranges, surviving anchors, and the fork
|
|
386
|
+
* target the next Claude spawn consumes. */
|
|
387
|
+
writeRewind(sessionId, value) {
|
|
388
|
+
return this.#update(sessionId, (current) => ({
|
|
389
|
+
...current,
|
|
390
|
+
rewind: value
|
|
391
|
+
}), false, { kind: "sync" });
|
|
392
|
+
}
|
|
393
|
+
/** Remember where Claude's chain ended for one completed DSH turn. */
|
|
394
|
+
recordRewindAnchor(sessionId, turn, uuid) {
|
|
395
|
+
return this.#update(sessionId, (current) => ({
|
|
396
|
+
...current,
|
|
397
|
+
rewind: recordRewindAnchor(current.rewind ?? EMPTY_REWIND_STATE, {
|
|
398
|
+
turn,
|
|
399
|
+
uuid
|
|
400
|
+
})
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
/** Disarm the fork target once a Claude process has resumed at it, so a
|
|
404
|
+
* later respawn continues the rewound session instead of re-truncating it. */
|
|
405
|
+
clearRewindPending(sessionId) {
|
|
406
|
+
return this.#update(sessionId, (current) => current.rewind?.pending === void 0 ? current : {
|
|
407
|
+
...current,
|
|
408
|
+
rewind: {
|
|
409
|
+
ranges: current.rewind.ranges,
|
|
410
|
+
anchors: current.rewind.anchors
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
282
414
|
importLegacy(sessionId, events) {
|
|
283
415
|
const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
|
|
284
416
|
const importedBinding = latestClaudeSessionBinding(events);
|
|
@@ -654,12 +786,15 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
654
786
|
}
|
|
655
787
|
//#endregion
|
|
656
788
|
//#region src/sdk-messages.ts
|
|
657
|
-
function record$
|
|
789
|
+
function record$12(value) {
|
|
658
790
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
659
791
|
}
|
|
660
792
|
function string$3(value) {
|
|
661
793
|
return typeof value === "string" ? value : void 0;
|
|
662
794
|
}
|
|
795
|
+
function finiteNumber(value) {
|
|
796
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
797
|
+
}
|
|
663
798
|
function taskUsageOf(usage) {
|
|
664
799
|
if (usage === void 0) return void 0;
|
|
665
800
|
const normalized = {};
|
|
@@ -668,20 +803,28 @@ function taskUsageOf(usage) {
|
|
|
668
803
|
if (typeof usage.duration_ms === "number") normalized.durationMs = usage.duration_ms;
|
|
669
804
|
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
670
805
|
}
|
|
671
|
-
|
|
672
|
-
|
|
806
|
+
/** Read one Anthropic `usage` envelope. Shared by the per-call sample on an
|
|
807
|
+
* assistant message and the turn total on a result message. */
|
|
808
|
+
function usageOf(usage) {
|
|
673
809
|
const normalized = {};
|
|
674
|
-
if (usage
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
810
|
+
if (usage === void 0) return normalized;
|
|
811
|
+
if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
|
|
812
|
+
if (typeof usage.output_tokens === "number") normalized.outputTokens = usage.output_tokens;
|
|
813
|
+
if (typeof usage.cache_read_input_tokens === "number") normalized.cacheReadTokens = usage.cache_read_input_tokens;
|
|
814
|
+
if (typeof usage.cache_creation_input_tokens === "number") normalized.cacheCreationTokens = usage.cache_creation_input_tokens;
|
|
815
|
+
return normalized;
|
|
816
|
+
}
|
|
817
|
+
function hasUsageCounts(usage) {
|
|
818
|
+
return usage.inputTokens !== void 0 || usage.outputTokens !== void 0 || usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0;
|
|
819
|
+
}
|
|
820
|
+
function resultUsage(message) {
|
|
821
|
+
const normalized = usageOf(record$12(message.usage));
|
|
680
822
|
if (typeof message.total_cost_usd === "number") normalized.cumulativeCostUsd = message.total_cost_usd;
|
|
681
823
|
return normalized;
|
|
682
824
|
}
|
|
683
825
|
function normalizeAssistant(message) {
|
|
684
|
-
const
|
|
826
|
+
const envelope = record$12(message.message);
|
|
827
|
+
const content = envelope?.content;
|
|
685
828
|
if (!Array.isArray(content)) return [{
|
|
686
829
|
kind: "protocol-error",
|
|
687
830
|
title: "Malformed Claude assistant message",
|
|
@@ -690,7 +833,7 @@ function normalizeAssistant(message) {
|
|
|
690
833
|
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
691
834
|
const normalized = [];
|
|
692
835
|
for (const item of content) {
|
|
693
|
-
const block = record$
|
|
836
|
+
const block = record$12(item);
|
|
694
837
|
if (block === void 0) continue;
|
|
695
838
|
if (block.type === "text") {
|
|
696
839
|
const text = string$3(block.text);
|
|
@@ -719,11 +862,17 @@ function normalizeAssistant(message) {
|
|
|
719
862
|
});
|
|
720
863
|
}
|
|
721
864
|
}
|
|
865
|
+
const usage = usageOf(record$12(envelope?.usage));
|
|
866
|
+
if (hasUsageCounts(usage)) normalized.push({
|
|
867
|
+
kind: "request-usage",
|
|
868
|
+
usage,
|
|
869
|
+
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
870
|
+
});
|
|
722
871
|
return normalized;
|
|
723
872
|
}
|
|
724
873
|
function normalizeUser(message) {
|
|
725
874
|
if (message.isReplay === true) return [];
|
|
726
|
-
const content = record$
|
|
875
|
+
const content = record$12(message.message)?.content;
|
|
727
876
|
if (typeof content === "string") return [];
|
|
728
877
|
if (!Array.isArray(content)) return [{
|
|
729
878
|
kind: "protocol-error",
|
|
@@ -733,7 +882,7 @@ function normalizeUser(message) {
|
|
|
733
882
|
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
734
883
|
const normalized = [];
|
|
735
884
|
for (const item of content) {
|
|
736
|
-
const block = record$
|
|
885
|
+
const block = record$12(item);
|
|
737
886
|
if (block?.type !== "tool_result") continue;
|
|
738
887
|
const toolUseId = string$3(block.tool_use_id);
|
|
739
888
|
if (toolUseId === void 0) continue;
|
|
@@ -814,7 +963,7 @@ function normalizeSystem(message) {
|
|
|
814
963
|
const summary = string$3(message.summary);
|
|
815
964
|
const subagentType = string$3(message.subagent_type);
|
|
816
965
|
const lastToolName = string$3(message.last_tool_name);
|
|
817
|
-
const usage = taskUsageOf(record$
|
|
966
|
+
const usage = taskUsageOf(record$12(message.usage));
|
|
818
967
|
return [{
|
|
819
968
|
kind: "subagent",
|
|
820
969
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -830,7 +979,7 @@ function normalizeSystem(message) {
|
|
|
830
979
|
}];
|
|
831
980
|
}
|
|
832
981
|
if (subtype === "task_updated") {
|
|
833
|
-
const patch = record$
|
|
982
|
+
const patch = record$12(message.patch);
|
|
834
983
|
const status = string$3(patch?.status);
|
|
835
984
|
const taskId = string$3(message.task_id);
|
|
836
985
|
const description = string$3(patch?.description);
|
|
@@ -853,7 +1002,7 @@ function normalizeSystem(message) {
|
|
|
853
1002
|
const taskId = string$3(message.task_id);
|
|
854
1003
|
const summary = string$3(message.summary);
|
|
855
1004
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
856
|
-
const usage = taskUsageOf(record$
|
|
1005
|
+
const usage = taskUsageOf(record$12(message.usage));
|
|
857
1006
|
return [{
|
|
858
1007
|
kind: "subagent",
|
|
859
1008
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -868,7 +1017,7 @@ function normalizeSystem(message) {
|
|
|
868
1017
|
if (subtype === "background_tasks_changed") return [{
|
|
869
1018
|
kind: "background-tasks",
|
|
870
1019
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
871
|
-
const entry = record$
|
|
1020
|
+
const entry = record$12(item);
|
|
872
1021
|
const taskId = string$3(entry?.task_id);
|
|
873
1022
|
const description = string$3(entry?.description);
|
|
874
1023
|
const taskType = string$3(entry?.task_type);
|
|
@@ -885,6 +1034,20 @@ function normalizeSystem(message) {
|
|
|
885
1034
|
title: "Claude API retry",
|
|
886
1035
|
detail: message
|
|
887
1036
|
}];
|
|
1037
|
+
if (subtype === "compact_boundary") {
|
|
1038
|
+
const metadata = record$12(message.compact_metadata);
|
|
1039
|
+
const trigger = metadata?.trigger === "auto" || metadata?.trigger === "manual" ? metadata.trigger : void 0;
|
|
1040
|
+
const preTokens = finiteNumber(metadata?.pre_tokens);
|
|
1041
|
+
const postTokens = finiteNumber(metadata?.post_tokens);
|
|
1042
|
+
const durationMs = finiteNumber(metadata?.duration_ms);
|
|
1043
|
+
return [{
|
|
1044
|
+
kind: "compaction",
|
|
1045
|
+
...trigger === void 0 ? {} : { trigger },
|
|
1046
|
+
...preTokens === void 0 ? {} : { preTokens },
|
|
1047
|
+
...postTokens === void 0 ? {} : { postTokens },
|
|
1048
|
+
...durationMs === void 0 ? {} : { durationMs }
|
|
1049
|
+
}];
|
|
1050
|
+
}
|
|
888
1051
|
if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
|
|
889
1052
|
kind: message.level === "warning" ? "warning" : "status",
|
|
890
1053
|
title: string$3(message.content) ?? string$3(message.text) ?? "Claude Code notice",
|
|
@@ -911,10 +1074,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
911
1074
|
function normalizeSdkMessage(message) {
|
|
912
1075
|
const value = message;
|
|
913
1076
|
if (value.type === "stream_event") {
|
|
914
|
-
const event = record$
|
|
1077
|
+
const event = record$12(value.event);
|
|
915
1078
|
const parentToolUseId = string$3(value.parent_tool_use_id);
|
|
916
1079
|
if (event?.type === "content_block_delta") {
|
|
917
|
-
const delta = record$
|
|
1080
|
+
const delta = record$12(event.delta);
|
|
918
1081
|
if (delta?.type === "text_delta") {
|
|
919
1082
|
const text = string$3(delta.text);
|
|
920
1083
|
return text === void 0 ? [] : [{
|
|
@@ -949,7 +1112,7 @@ function normalizeSdkMessage(message) {
|
|
|
949
1112
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
950
1113
|
const terminalReason = string$3(value.terminal_reason);
|
|
951
1114
|
const userMessageUuid = string$3(value.user_message_uuid);
|
|
952
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$
|
|
1115
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$12(item)).filter((item) => item !== void 0).map((item) => {
|
|
953
1116
|
const toolName = string$3(item.tool_name);
|
|
954
1117
|
const toolUseId = string$3(item.tool_use_id);
|
|
955
1118
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
@@ -975,7 +1138,7 @@ function normalizeSdkMessage(message) {
|
|
|
975
1138
|
detail: value.error ?? value.output
|
|
976
1139
|
}];
|
|
977
1140
|
if (value.type === "rate_limit_event") {
|
|
978
|
-
const status = string$3(record$
|
|
1141
|
+
const status = string$3(record$12(value.rate_limit_info)?.status);
|
|
979
1142
|
return [{
|
|
980
1143
|
kind: "status",
|
|
981
1144
|
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
@@ -1014,7 +1177,7 @@ const FIXED_WINDOWS = [
|
|
|
1014
1177
|
"seven_day_opus",
|
|
1015
1178
|
"seven_day_sonnet"
|
|
1016
1179
|
];
|
|
1017
|
-
function record$
|
|
1180
|
+
function record$11(value) {
|
|
1018
1181
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1019
1182
|
}
|
|
1020
1183
|
/** Utilization is documented as 0-100; clamp so a server glitch cannot render
|
|
@@ -1026,7 +1189,7 @@ function resetsAt(value) {
|
|
|
1026
1189
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1027
1190
|
}
|
|
1028
1191
|
function window(id, source, label) {
|
|
1029
|
-
const entry = record$
|
|
1192
|
+
const entry = record$11(source);
|
|
1030
1193
|
if (entry === void 0) return void 0;
|
|
1031
1194
|
const used = utilization(entry.utilization);
|
|
1032
1195
|
const reset = resetsAt(entry.resets_at);
|
|
@@ -1040,9 +1203,9 @@ function window(id, source, label) {
|
|
|
1040
1203
|
}
|
|
1041
1204
|
/** Project the SDK's `/usage` response onto the windows the settings card shows. */
|
|
1042
1205
|
function normalizePlanUsage(value, fetchedAt) {
|
|
1043
|
-
const response = record$
|
|
1206
|
+
const response = record$11(value);
|
|
1044
1207
|
const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
|
|
1045
|
-
const limits = record$
|
|
1208
|
+
const limits = record$11(response?.rate_limits);
|
|
1046
1209
|
if (response?.rate_limits_available !== true || limits === void 0) return {
|
|
1047
1210
|
available: false,
|
|
1048
1211
|
...subscription === void 0 ? {} : { subscription },
|
|
@@ -1050,7 +1213,7 @@ function normalizePlanUsage(value, fetchedAt) {
|
|
|
1050
1213
|
fetchedAt
|
|
1051
1214
|
};
|
|
1052
1215
|
const windows = [...FIXED_WINDOWS.map((id) => window(id, limits[id])), ...(Array.isArray(limits.model_scoped) ? limits.model_scoped : []).map((entry, index) => {
|
|
1053
|
-
const name = record$
|
|
1216
|
+
const name = record$11(entry)?.display_name;
|
|
1054
1217
|
return window(`model:${typeof name === "string" ? name : index}`, entry, typeof name === "string" ? name : void 0);
|
|
1055
1218
|
})].filter((entry) => entry !== void 0);
|
|
1056
1219
|
return {
|
|
@@ -1086,7 +1249,9 @@ async function probePlanUsage(executablePath, fetchedAt, factory = query) {
|
|
|
1086
1249
|
(async () => {
|
|
1087
1250
|
for await (const _ of query$1);
|
|
1088
1251
|
})().catch(() => void 0);
|
|
1089
|
-
return normalizePlanUsage(await readPlanUsageFrom(query$1),
|
|
1252
|
+
return normalizePlanUsage(await Promise.race([readPlanUsageFrom(query$1), new Promise((_resolve, reject) => {
|
|
1253
|
+
setTimeout(() => reject(/* @__PURE__ */ new Error("dsh-claude: the plan usage request did not answer in time")), PLAN_USAGE_TIMEOUT_MS).unref?.();
|
|
1254
|
+
})]), fetchedAt);
|
|
1090
1255
|
} finally {
|
|
1091
1256
|
clearTimeout(timer);
|
|
1092
1257
|
lifetime.abort();
|
|
@@ -1237,6 +1402,14 @@ async function withTimeout(operation, timeoutMs, label) {
|
|
|
1237
1402
|
if (timer !== void 0) clearTimeout(timer);
|
|
1238
1403
|
}
|
|
1239
1404
|
}
|
|
1405
|
+
/** The uuid of one main-chain transcript entry, or undefined for anything a
|
|
1406
|
+
* rewind must not fork at: stream partials, results, and sidechain traffic. */
|
|
1407
|
+
function chainEntryUuid(message) {
|
|
1408
|
+
if (message.type !== "assistant" && message.type !== "user") return void 0;
|
|
1409
|
+
const envelope = message;
|
|
1410
|
+
if (typeof envelope.parent_tool_use_id === "string") return void 0;
|
|
1411
|
+
return typeof envelope.uuid === "string" && envelope.uuid.length > 0 ? envelope.uuid : void 0;
|
|
1412
|
+
}
|
|
1240
1413
|
function sdkUserMessage(prompt, uuid) {
|
|
1241
1414
|
return {
|
|
1242
1415
|
type: "user",
|
|
@@ -1307,13 +1480,39 @@ var ClaudeSupervisor = class {
|
|
|
1307
1480
|
}
|
|
1308
1481
|
async contextUsage(agent, model = this.#config.defaultModel) {
|
|
1309
1482
|
const usage = await this.#runMetadata(agent, model, (query) => query.getContextUsage());
|
|
1310
|
-
|
|
1311
|
-
if (contextWindow > 0) {
|
|
1312
|
-
this.#contextWindows.set(model, contextWindow);
|
|
1313
|
-
this.#contextWindows.set(usage.model, contextWindow);
|
|
1314
|
-
}
|
|
1483
|
+
this.#recordContextWindow(model, usage);
|
|
1315
1484
|
return usage;
|
|
1316
1485
|
}
|
|
1486
|
+
/** Cache a window under both the selector id the caller asked for and the
|
|
1487
|
+
* concrete model the CLI reports, so either name resolves it later. */
|
|
1488
|
+
#recordContextWindow(model, usage) {
|
|
1489
|
+
const contextWindow = usage.rawMaxTokens > 0 ? usage.rawMaxTokens : usage.maxTokens;
|
|
1490
|
+
if (contextWindow <= 0) return;
|
|
1491
|
+
this.#contextWindows.set(model, contextWindow);
|
|
1492
|
+
this.#contextWindows.set(usage.model, contextWindow);
|
|
1493
|
+
}
|
|
1494
|
+
/** Learn a model's context window the first time a turn finishes on it.
|
|
1495
|
+
*
|
|
1496
|
+
* DSH hides its context meter entirely unless the route publishes a
|
|
1497
|
+
* capacity, and these numbers move with Claude releases — so none are
|
|
1498
|
+
* hardcoded; the CLI is asked over the session's own live process.
|
|
1499
|
+
*
|
|
1500
|
+
* Turn completion is the earliest honest moment to ask. `entry.model` is
|
|
1501
|
+
* already the model that just ran, so no model switch is provoked — which
|
|
1502
|
+
* rules out asking from `resolveModel`, since DSH resolves every model in
|
|
1503
|
+
* the catalog to build its picker and `#metadataEntry` would switch the live
|
|
1504
|
+
* session once per entry. And nothing is lost by waiting: the meter needs a
|
|
1505
|
+
* usage sample too, and no turn has reported one before the first turn ends.
|
|
1506
|
+
*
|
|
1507
|
+
* Best-effort — a failure leaves the window unknown (the meter stays hidden,
|
|
1508
|
+
* exactly as before) and the next completed turn tries again. */
|
|
1509
|
+
async #learnContextWindow(entry) {
|
|
1510
|
+
if (this.#contextWindows.has(entry.model)) return;
|
|
1511
|
+
try {
|
|
1512
|
+
const usage = await withTimeout(entry.query.getContextUsage(), CLAUDE_METADATA_TIMEOUT_MS, "Claude context window probe");
|
|
1513
|
+
this.#recordContextWindow(entry.model, usage);
|
|
1514
|
+
} catch {}
|
|
1515
|
+
}
|
|
1317
1516
|
contextWindow(model) {
|
|
1318
1517
|
return this.#contextWindows.get(model);
|
|
1319
1518
|
}
|
|
@@ -1362,7 +1561,7 @@ var ClaudeSupervisor = class {
|
|
|
1362
1561
|
} else {
|
|
1363
1562
|
await this.#syncPermissionMode(entry);
|
|
1364
1563
|
if (model !== entry.model) {
|
|
1365
|
-
await entry.query.setModel(model);
|
|
1564
|
+
await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
|
|
1366
1565
|
entry.model = model;
|
|
1367
1566
|
}
|
|
1368
1567
|
}
|
|
@@ -1381,6 +1580,7 @@ var ClaudeSupervisor = class {
|
|
|
1381
1580
|
transcriptText: "",
|
|
1382
1581
|
transcriptTextOrdinal: void 0,
|
|
1383
1582
|
thinking: "",
|
|
1583
|
+
requestUsage: void 0,
|
|
1384
1584
|
aborted: false,
|
|
1385
1585
|
deniedToolUseIds: /* @__PURE__ */ new Set(),
|
|
1386
1586
|
callNames: /* @__PURE__ */ new Map(),
|
|
@@ -1432,7 +1632,7 @@ var ClaudeSupervisor = class {
|
|
|
1432
1632
|
const entry = await this.#metadataEntry(agent, model);
|
|
1433
1633
|
try {
|
|
1434
1634
|
await entry.sdkInitialization;
|
|
1435
|
-
return await
|
|
1635
|
+
return await this.#control(entry, operation(entry.query, entry), "Claude metadata request");
|
|
1436
1636
|
} finally {
|
|
1437
1637
|
entry.lastUsedAt = Date.now();
|
|
1438
1638
|
if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
|
|
@@ -1462,15 +1662,34 @@ var ClaudeSupervisor = class {
|
|
|
1462
1662
|
}
|
|
1463
1663
|
await this.#syncPermissionMode(entry);
|
|
1464
1664
|
if (model !== entry.model) {
|
|
1465
|
-
await entry.query.setModel(model);
|
|
1665
|
+
await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
|
|
1466
1666
|
entry.model = model;
|
|
1467
1667
|
}
|
|
1468
1668
|
return entry;
|
|
1469
1669
|
}
|
|
1670
|
+
/** Run one SDK control request against a live entry, and discard the entry if
|
|
1671
|
+
* it does not answer.
|
|
1672
|
+
*
|
|
1673
|
+
* Turn admission and every metadata read share one process-wide gate, so an
|
|
1674
|
+
* unbounded control request stalls every session until the Host restarts.
|
|
1675
|
+
* Bounding it is only half the cure: a timeout also proves this query has
|
|
1676
|
+
* stopped answering, and keeping the entry means the next caller reuses the
|
|
1677
|
+
* same dead process — timing out again, forever. Discarding it lets the next
|
|
1678
|
+
* attempt spawn a fresh one. Every control request goes through here so a
|
|
1679
|
+
* new call site cannot quietly reintroduce either half. */
|
|
1680
|
+
async #control(entry, operation, label, timeoutMs = CLAUDE_METADATA_TIMEOUT_MS) {
|
|
1681
|
+
try {
|
|
1682
|
+
return await withTimeout(operation, timeoutMs, label);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (this.#entries.get(entry.sessionId) === entry) this.#entries.delete(entry.sessionId);
|
|
1685
|
+
await this.#disposeEntry(entry);
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1470
1689
|
async #syncPermissionMode(entry) {
|
|
1471
1690
|
const mode = claudePermissionMode(entry.ownerAgent.session.events);
|
|
1472
1691
|
if (mode === entry.permissionMode) return;
|
|
1473
|
-
await entry.query.setPermissionMode(mode);
|
|
1692
|
+
await this.#control(entry, entry.query.setPermissionMode(mode), "Claude Code permission mode switch");
|
|
1474
1693
|
entry.permissionMode = mode;
|
|
1475
1694
|
}
|
|
1476
1695
|
async disposeSession(sessionId) {
|
|
@@ -1498,7 +1717,11 @@ var ClaudeSupervisor = class {
|
|
|
1498
1717
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
1499
1718
|
const input = new AsyncQueue();
|
|
1500
1719
|
const lifetime = new AbortController();
|
|
1501
|
-
const
|
|
1720
|
+
const projection = await this.#sidecar.importLegacy(sessionId, agent.session.events);
|
|
1721
|
+
const binding = projection.binding;
|
|
1722
|
+
const pendingRewind = projection.rewind?.pending;
|
|
1723
|
+
const forkAt = pendingRewind !== void 0 && "resumeAt" in pendingRewind ? pendingRewind.resumeAt : void 0;
|
|
1724
|
+
const startFresh = pendingRewind !== void 0 && "fresh" in pendingRewind;
|
|
1502
1725
|
const permissionMode = claudePermissionMode(agent.session.events);
|
|
1503
1726
|
const entry = {
|
|
1504
1727
|
sessionId,
|
|
@@ -1511,8 +1734,10 @@ var ClaudeSupervisor = class {
|
|
|
1511
1734
|
lastUsedAt: Date.now(),
|
|
1512
1735
|
input,
|
|
1513
1736
|
lifetime,
|
|
1514
|
-
claudeSessionId: binding?.claudeSessionId,
|
|
1515
|
-
expectedResume: binding?.claudeSessionId,
|
|
1737
|
+
claudeSessionId: startFresh ? void 0 : binding?.claudeSessionId,
|
|
1738
|
+
expectedResume: startFresh || forkAt !== void 0 ? void 0 : binding?.claudeSessionId,
|
|
1739
|
+
lastChainUuid: void 0,
|
|
1740
|
+
consumedRewind: pendingRewind !== void 0,
|
|
1516
1741
|
initialized: false,
|
|
1517
1742
|
idleTimer: void 0,
|
|
1518
1743
|
tasks: /* @__PURE__ */ new Map(),
|
|
@@ -1563,7 +1788,10 @@ var ClaudeSupervisor = class {
|
|
|
1563
1788
|
spawnClaudeCodeProcess: createManagedClaudeSpawner(this.#runtime, this.#config.executablePath, (process) => {
|
|
1564
1789
|
entry.process = process;
|
|
1565
1790
|
}),
|
|
1566
|
-
...binding === void 0 ? {} : {
|
|
1791
|
+
...binding === void 0 || startFresh ? {} : {
|
|
1792
|
+
resume: binding.claudeSessionId,
|
|
1793
|
+
...forkAt === void 0 ? {} : { resumeSessionAt: forkAt }
|
|
1794
|
+
},
|
|
1567
1795
|
model,
|
|
1568
1796
|
...thinkingMode === void 0 ? {} : thinkingMode === "off" ? { thinking: { type: "disabled" } } : thinkingMode === "ultracode" ? { settings: { ultracode: true } } : { effort: thinkingMode }
|
|
1569
1797
|
};
|
|
@@ -1580,7 +1808,11 @@ var ClaudeSupervisor = class {
|
|
|
1580
1808
|
}
|
|
1581
1809
|
async #pump(entry) {
|
|
1582
1810
|
try {
|
|
1583
|
-
for await (const sdkMessage of entry.query)
|
|
1811
|
+
for await (const sdkMessage of entry.query) {
|
|
1812
|
+
const chainUuid = chainEntryUuid(sdkMessage);
|
|
1813
|
+
if (chainUuid !== void 0) entry.lastChainUuid = chainUuid;
|
|
1814
|
+
for (const message of normalizeSdkMessage(sdkMessage)) await this.#handleMessage(entry, message);
|
|
1815
|
+
}
|
|
1584
1816
|
if (entry.state !== "disposed") await this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code stream ended"));
|
|
1585
1817
|
} catch (error) {
|
|
1586
1818
|
if (entry.state !== "disposed") await this.#handleDisconnect(entry, error);
|
|
@@ -1603,6 +1835,10 @@ var ClaudeSupervisor = class {
|
|
|
1603
1835
|
cliVersion: message.cliVersion,
|
|
1604
1836
|
cwd: message.cwd
|
|
1605
1837
|
});
|
|
1838
|
+
if (entry.consumedRewind) {
|
|
1839
|
+
entry.consumedRewind = false;
|
|
1840
|
+
await this.#sidecar.clearRewindPending(entry.sessionId);
|
|
1841
|
+
}
|
|
1606
1842
|
return;
|
|
1607
1843
|
}
|
|
1608
1844
|
const taskId = message.kind === "subagent" ? message.taskId : void 0;
|
|
@@ -1701,6 +1937,23 @@ var ClaudeSupervisor = class {
|
|
|
1701
1937
|
isError: message.phase === "failed"
|
|
1702
1938
|
});
|
|
1703
1939
|
return;
|
|
1940
|
+
case "request-usage":
|
|
1941
|
+
if (message.parentToolUseId === void 0) active.requestUsage = message.usage;
|
|
1942
|
+
return;
|
|
1943
|
+
case "compaction":
|
|
1944
|
+
this.#closeTranscriptTextSegment(active);
|
|
1945
|
+
await this.#appendActivity(active, {
|
|
1946
|
+
kind: "compaction",
|
|
1947
|
+
phase: "completed",
|
|
1948
|
+
title: "Claude compacted the conversation",
|
|
1949
|
+
detail: {
|
|
1950
|
+
...message.trigger === void 0 ? {} : { trigger: message.trigger },
|
|
1951
|
+
...message.preTokens === void 0 ? {} : { preTokens: message.preTokens },
|
|
1952
|
+
...message.postTokens === void 0 ? {} : { postTokens: message.postTokens },
|
|
1953
|
+
...message.durationMs === void 0 ? {} : { durationMs: message.durationMs }
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
return;
|
|
1704
1957
|
case "status":
|
|
1705
1958
|
case "warning":
|
|
1706
1959
|
case "unknown":
|
|
@@ -1832,6 +2085,31 @@ var ClaudeSupervisor = class {
|
|
|
1832
2085
|
entry.taskSnapshotAt = Date.now();
|
|
1833
2086
|
await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
|
|
1834
2087
|
}
|
|
2088
|
+
/** What DSH is told about token usage: newest call's prompt, whole turn's output.
|
|
2089
|
+
*
|
|
2090
|
+
* `TokenUsage` is documented as "token accounting for ONE model call", and
|
|
2091
|
+
* DSH's token meter divides `uncachedInput + cacheRead + cacheWrite` by the
|
|
2092
|
+
* context window to draw context pressure. One Claude turn makes many calls
|
|
2093
|
+
* and the CLI's result usage sums all of them, so reporting that sum pinned
|
|
2094
|
+
* the meter at 100%: a 35-call turn reads the same prompt from cache 35
|
|
2095
|
+
* times, which sums past the window without the conversation ever growing.
|
|
2096
|
+
* The newest single call answers "how big is this conversation now".
|
|
2097
|
+
*
|
|
2098
|
+
* Output is deliberately excluded from that pressure sum, so the same
|
|
2099
|
+
* argument never applied to it — and taking it from the newest call reported
|
|
2100
|
+
* whatever the wrap-up message happened to cost, which is a couple of tokens
|
|
2101
|
+
* after a turn that wrote thousands. The turn total is the honest figure.
|
|
2102
|
+
*
|
|
2103
|
+
* The sidecar activity keeps the whole turn total for both — that is the
|
|
2104
|
+
* audit and cost record, and nothing divides it by a window. */
|
|
2105
|
+
#reportedUsage(active, result) {
|
|
2106
|
+
const prompt = active.requestUsage;
|
|
2107
|
+
if (prompt === void 0) return result.usage;
|
|
2108
|
+
return {
|
|
2109
|
+
...prompt,
|
|
2110
|
+
...result.usage.outputTokens === void 0 ? {} : { outputTokens: result.usage.outputTokens }
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
1835
2113
|
async #completeProgressSegment(active, result, recordUsage = true) {
|
|
1836
2114
|
if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
|
|
1837
2115
|
await this.#appendSafely(active, {
|
|
@@ -1843,7 +2121,7 @@ var ClaudeSupervisor = class {
|
|
|
1843
2121
|
});
|
|
1844
2122
|
active.output.push({
|
|
1845
2123
|
type: "usage",
|
|
1846
|
-
usage: result
|
|
2124
|
+
usage: this.#reportedUsage(active, result)
|
|
1847
2125
|
});
|
|
1848
2126
|
}
|
|
1849
2127
|
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
@@ -1878,6 +2156,7 @@ var ClaudeSupervisor = class {
|
|
|
1878
2156
|
entry.active = void 0;
|
|
1879
2157
|
entry.state = "idle";
|
|
1880
2158
|
entry.lastUsedAt = Date.now();
|
|
2159
|
+
await this.#recordChainAnchor(entry, active);
|
|
1881
2160
|
this.#armIdleTimer(entry);
|
|
1882
2161
|
return;
|
|
1883
2162
|
}
|
|
@@ -1891,7 +2170,7 @@ var ClaudeSupervisor = class {
|
|
|
1891
2170
|
});
|
|
1892
2171
|
active.output.push({
|
|
1893
2172
|
type: "usage",
|
|
1894
|
-
usage: result
|
|
2173
|
+
usage: this.#reportedUsage(active, result)
|
|
1895
2174
|
});
|
|
1896
2175
|
}
|
|
1897
2176
|
const unmatchedDenials = (result.permissionDenials ?? []).filter((denial) => !active.deniedToolUseIds.has(denial.toolUseId));
|
|
@@ -1959,8 +2238,20 @@ var ClaudeSupervisor = class {
|
|
|
1959
2238
|
entry.active = void 0;
|
|
1960
2239
|
entry.state = "idle";
|
|
1961
2240
|
entry.lastUsedAt = Date.now();
|
|
2241
|
+
await this.#recordChainAnchor(entry, active);
|
|
2242
|
+
await this.#learnContextWindow(entry);
|
|
1962
2243
|
this.#armIdleTimer(entry);
|
|
1963
2244
|
}
|
|
2245
|
+
/** Pin where Claude's chain ended for the DSH turn that just settled, so a
|
|
2246
|
+
* later rewind of the following turn can fork exactly here. Best effort:
|
|
2247
|
+
* a missing anchor only makes a rewind fall back to an earlier turn. */
|
|
2248
|
+
async #recordChainAnchor(entry, active) {
|
|
2249
|
+
const uuid = entry.lastChainUuid;
|
|
2250
|
+
if (uuid === void 0) return;
|
|
2251
|
+
try {
|
|
2252
|
+
await this.#sidecar.recordRewindAnchor(entry.sessionId, active.cursor.turn, uuid);
|
|
2253
|
+
} catch {}
|
|
2254
|
+
}
|
|
1964
2255
|
async #upsertTranscriptText(active) {
|
|
1965
2256
|
if (active.transcriptText.length === 0) return;
|
|
1966
2257
|
const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
|
|
@@ -2159,28 +2450,28 @@ const MODELS = [
|
|
|
2159
2450
|
{
|
|
2160
2451
|
id: "default",
|
|
2161
2452
|
name: "Default (recommended)",
|
|
2162
|
-
description: "
|
|
2453
|
+
description: ""
|
|
2163
2454
|
},
|
|
2164
2455
|
{
|
|
2165
2456
|
id: "opus[1m]",
|
|
2166
2457
|
name: "Opus (1M context)",
|
|
2167
|
-
description: "
|
|
2458
|
+
description: "",
|
|
2168
2459
|
contextWindow: 1e6
|
|
2169
2460
|
},
|
|
2170
2461
|
{
|
|
2171
2462
|
id: "fable",
|
|
2172
2463
|
name: "Fable",
|
|
2173
|
-
description: "
|
|
2464
|
+
description: ""
|
|
2174
2465
|
},
|
|
2175
2466
|
{
|
|
2176
2467
|
id: "sonnet",
|
|
2177
2468
|
name: "Sonnet",
|
|
2178
|
-
description: "
|
|
2469
|
+
description: ""
|
|
2179
2470
|
},
|
|
2180
2471
|
{
|
|
2181
2472
|
id: "haiku",
|
|
2182
2473
|
name: "Haiku",
|
|
2183
|
-
description: "
|
|
2474
|
+
description: ""
|
|
2184
2475
|
}
|
|
2185
2476
|
];
|
|
2186
2477
|
const THINKING_MODES = [
|
|
@@ -2594,7 +2885,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2594
2885
|
}
|
|
2595
2886
|
//#endregion
|
|
2596
2887
|
//#region src/projection-routes.ts
|
|
2597
|
-
const MAX_SESSION_ID_CHARS$
|
|
2888
|
+
const MAX_SESSION_ID_CHARS$6 = 1024;
|
|
2598
2889
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2599
2890
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2600
2891
|
const META_REFRESH_MS = 5e3;
|
|
@@ -2608,7 +2899,7 @@ function targetFromUrl(rawUrl) {
|
|
|
2608
2899
|
if (stream) encoded = encoded.slice(0, -7);
|
|
2609
2900
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2610
2901
|
const sessionId = decodeURIComponent(encoded);
|
|
2611
|
-
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$
|
|
2902
|
+
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$6) return void 0;
|
|
2612
2903
|
return {
|
|
2613
2904
|
sessionId,
|
|
2614
2905
|
stream
|
|
@@ -2627,7 +2918,8 @@ function envelope(projection, meta) {
|
|
|
2627
2918
|
...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
|
|
2628
2919
|
...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
|
|
2629
2920
|
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
2630
|
-
reviewComments: meta.reviewComments
|
|
2921
|
+
reviewComments: meta.reviewComments,
|
|
2922
|
+
...projection.rewind === void 0 ? {} : { rewind: { ranges: projection.rewind.ranges } }
|
|
2631
2923
|
};
|
|
2632
2924
|
}
|
|
2633
2925
|
/** Register the browser-readable, credential-free sidecar projection endpoint.
|
|
@@ -2768,7 +3060,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2768
3060
|
}
|
|
2769
3061
|
//#endregion
|
|
2770
3062
|
//#region src/repository-status.ts
|
|
2771
|
-
const MAX_OUTPUT_BYTES$
|
|
3063
|
+
const MAX_OUTPUT_BYTES$4 = 65536;
|
|
2772
3064
|
const MAX_DIFF_BYTES = 262144;
|
|
2773
3065
|
const MAX_FILE_BYTES = 8388608;
|
|
2774
3066
|
const MAX_UNTRACKED_DIFFS = 50;
|
|
@@ -2788,7 +3080,7 @@ async function collect$3(handle) {
|
|
|
2788
3080
|
lossy: stdout?.lossy === true
|
|
2789
3081
|
};
|
|
2790
3082
|
}
|
|
2791
|
-
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
3083
|
+
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$4) {
|
|
2792
3084
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
2793
3085
|
return collect$3(runtime.spawn({
|
|
2794
3086
|
argv: [executable, ...args],
|
|
@@ -2796,7 +3088,7 @@ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUT
|
|
|
2796
3088
|
stdio: {
|
|
2797
3089
|
stdin: "ignore",
|
|
2798
3090
|
stdout: { maxBytes },
|
|
2799
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
3091
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$4 }
|
|
2800
3092
|
},
|
|
2801
3093
|
graceMs: 1e3,
|
|
2802
3094
|
signal,
|
|
@@ -2866,14 +3158,14 @@ function parseGitHubRemote(value) {
|
|
|
2866
3158
|
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
2867
3159
|
return `${match[1]}/${match[2]}`;
|
|
2868
3160
|
}
|
|
2869
|
-
function record$
|
|
3161
|
+
function record$10(value) {
|
|
2870
3162
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2871
3163
|
}
|
|
2872
3164
|
function aggregateChecks(value) {
|
|
2873
3165
|
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
2874
3166
|
let pending = false;
|
|
2875
3167
|
for (const item of value) {
|
|
2876
|
-
const check = record$
|
|
3168
|
+
const check = record$10(item);
|
|
2877
3169
|
if (check === void 0) continue;
|
|
2878
3170
|
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
2879
3171
|
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
@@ -2897,7 +3189,7 @@ function reviewState(value) {
|
|
|
2897
3189
|
return "none";
|
|
2898
3190
|
}
|
|
2899
3191
|
function parsePullRequest(value) {
|
|
2900
|
-
const input = record$
|
|
3192
|
+
const input = record$10(value);
|
|
2901
3193
|
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
2902
3194
|
let url;
|
|
2903
3195
|
try {
|
|
@@ -2918,7 +3210,7 @@ function parsePullRequest(value) {
|
|
|
2918
3210
|
review: reviewState(input.reviewDecision),
|
|
2919
3211
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
2920
3212
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
2921
|
-
...typeof record$
|
|
3213
|
+
...typeof record$10(input.author)?.login === "string" ? { author: bounded(String(record$10(input.author)?.login)) } : {},
|
|
2922
3214
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
2923
3215
|
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
2924
3216
|
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
@@ -3212,7 +3504,7 @@ var RepositoryStatusService = class {
|
|
|
3212
3504
|
};
|
|
3213
3505
|
//#endregion
|
|
3214
3506
|
//#region src/repository-setup.ts
|
|
3215
|
-
const MAX_OUTPUT_BYTES$
|
|
3507
|
+
const MAX_OUTPUT_BYTES$3 = 131072;
|
|
3216
3508
|
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
3217
3509
|
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
3218
3510
|
const MAX_PATH_CHARS$2 = 4096;
|
|
@@ -3299,8 +3591,18 @@ var RepositorySetupService = class {
|
|
|
3299
3591
|
this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
|
|
3300
3592
|
}
|
|
3301
3593
|
async listBranches(cwd) {
|
|
3594
|
+
const git = await this.#git();
|
|
3595
|
+
return this.#listBranches(git, await this.#repositoryRoot(git, safePath(cwd)));
|
|
3596
|
+
}
|
|
3597
|
+
/** Refresh remote-tracking refs before listing: a branch pushed after this
|
|
3598
|
+
* checkout last fetched has no local ref, so the picker cannot offer it. */
|
|
3599
|
+
async refreshBranches(cwd) {
|
|
3302
3600
|
const git = await this.#git();
|
|
3303
3601
|
const root = await this.#repositoryRoot(git, safePath(cwd));
|
|
3602
|
+
await this.#fetchRemotes(git, root);
|
|
3603
|
+
return this.#listBranches(git, root);
|
|
3604
|
+
}
|
|
3605
|
+
async #listBranches(git, root) {
|
|
3304
3606
|
const [status, refs, remoteRefs] = await Promise.all([
|
|
3305
3607
|
this.#run(git, [
|
|
3306
3608
|
"status",
|
|
@@ -3514,6 +3816,16 @@ var RepositorySetupService = class {
|
|
|
3514
3816
|
branch: localBranch
|
|
3515
3817
|
};
|
|
3516
3818
|
}
|
|
3819
|
+
async #fetchRemotes(git, root) {
|
|
3820
|
+
const fetched = await this.#run(git, [
|
|
3821
|
+
"-c",
|
|
3822
|
+
"credential.interactive=never",
|
|
3823
|
+
"fetch",
|
|
3824
|
+
"--all",
|
|
3825
|
+
"--prune"
|
|
3826
|
+
], root, GIT_FETCH_TIMEOUT_MS);
|
|
3827
|
+
if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references.");
|
|
3828
|
+
}
|
|
3517
3829
|
async #checkout(info, branch) {
|
|
3518
3830
|
if (info.current !== branch) {
|
|
3519
3831
|
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
@@ -3542,14 +3854,7 @@ var RepositorySetupService = class {
|
|
|
3542
3854
|
async #createWorktree(root, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress) {
|
|
3543
3855
|
const git = await this.#git();
|
|
3544
3856
|
progress("fetching");
|
|
3545
|
-
|
|
3546
|
-
"-c",
|
|
3547
|
-
"credential.interactive=never",
|
|
3548
|
-
"fetch",
|
|
3549
|
-
"--all",
|
|
3550
|
-
"--prune"
|
|
3551
|
-
], root, GIT_FETCH_TIMEOUT_MS);
|
|
3552
|
-
if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references before creating the worktree.");
|
|
3857
|
+
await this.#fetchRemotes(git, root);
|
|
3553
3858
|
const suffix = randomUUID().slice(0, 8);
|
|
3554
3859
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
|
|
3555
3860
|
const branch = explicitBranchName ?? `${safeBranch(await this.#branchPrefix())}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
|
|
@@ -3636,8 +3941,8 @@ var RepositorySetupService = class {
|
|
|
3636
3941
|
cwd,
|
|
3637
3942
|
stdio: {
|
|
3638
3943
|
stdin: "ignore",
|
|
3639
|
-
stdout: { maxBytes: MAX_OUTPUT_BYTES$
|
|
3640
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
3944
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$3 },
|
|
3945
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
|
|
3641
3946
|
},
|
|
3642
3947
|
graceMs: 1e3,
|
|
3643
3948
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -3678,7 +3983,7 @@ var RepositorySetupService = class {
|
|
|
3678
3983
|
};
|
|
3679
3984
|
//#endregion
|
|
3680
3985
|
//#region src/repository-actions.ts
|
|
3681
|
-
const MAX_OUTPUT_BYTES$
|
|
3986
|
+
const MAX_OUTPUT_BYTES$2 = 262144;
|
|
3682
3987
|
const MAX_PATCH_CHARS = 65536;
|
|
3683
3988
|
const MAX_MESSAGE_CHARS = 512;
|
|
3684
3989
|
const MAX_PR_TEXT_CHARS = 8192;
|
|
@@ -3686,6 +3991,24 @@ const MAX_UNPUSHED_COMMITS = 20;
|
|
|
3686
3991
|
const GIT_TIMEOUT_MS$1 = 15e3;
|
|
3687
3992
|
const REMOTE_TIMEOUT_MS = 6e4;
|
|
3688
3993
|
const GENERATE_TIMEOUT_MS = 6e4;
|
|
3994
|
+
/** One cold `claude -p` per generation, so everything the subject line cannot
|
|
3995
|
+
* use is cost: MCP servers (which `ask` already skips for the same reason,
|
|
3996
|
+
* and which stall for as long as an unreachable one takes to give up) and the
|
|
3997
|
+
* user's own hooks and settings. Project settings stay: a repository's commit
|
|
3998
|
+
* conventions belong in the message. `--tools ''` keeps `--output-format`
|
|
3999
|
+
* between it and the prompt -- both flags are variadic. */
|
|
4000
|
+
const GENERATE_ARGUMENTS = [
|
|
4001
|
+
"-p",
|
|
4002
|
+
"--strict-mcp-config",
|
|
4003
|
+
"--mcp-config",
|
|
4004
|
+
"{\"mcpServers\":{}}",
|
|
4005
|
+
"--setting-sources",
|
|
4006
|
+
"project,local",
|
|
4007
|
+
"--tools",
|
|
4008
|
+
"",
|
|
4009
|
+
"--output-format",
|
|
4010
|
+
"text"
|
|
4011
|
+
];
|
|
3689
4012
|
var RepositoryActionError = class extends Error {
|
|
3690
4013
|
code;
|
|
3691
4014
|
commit;
|
|
@@ -3789,14 +4112,7 @@ var RepositoryActionService = class {
|
|
|
3789
4112
|
`Diff:\n${preview.patch.slice(0, 24576)}`
|
|
3790
4113
|
].join("\n");
|
|
3791
4114
|
try {
|
|
3792
|
-
const result = await this.#run(this.#claudeExecutable,
|
|
3793
|
-
"-p",
|
|
3794
|
-
"--tools",
|
|
3795
|
-
"",
|
|
3796
|
-
"--output-format",
|
|
3797
|
-
"text",
|
|
3798
|
-
prompt
|
|
3799
|
-
], preview.root, GENERATE_TIMEOUT_MS);
|
|
4115
|
+
const result = await this.#run(this.#claudeExecutable, GENERATE_ARGUMENTS.concat(prompt), preview.root, GENERATE_TIMEOUT_MS);
|
|
3800
4116
|
return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
|
|
3801
4117
|
} catch {
|
|
3802
4118
|
return fallback;
|
|
@@ -4005,7 +4321,7 @@ var RepositoryActionService = class {
|
|
|
4005
4321
|
"--",
|
|
4006
4322
|
":(exclude)WARP.md",
|
|
4007
4323
|
":(exclude)**/WARP.md"
|
|
4008
|
-
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$
|
|
4324
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2),
|
|
4009
4325
|
this.#run(git, [
|
|
4010
4326
|
"diff",
|
|
4011
4327
|
"--no-ext-diff",
|
|
@@ -4014,7 +4330,7 @@ var RepositoryActionService = class {
|
|
|
4014
4330
|
"--",
|
|
4015
4331
|
":(exclude)WARP.md",
|
|
4016
4332
|
":(exclude)**/WARP.md"
|
|
4017
|
-
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$
|
|
4333
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2)
|
|
4018
4334
|
]);
|
|
4019
4335
|
if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
|
|
4020
4336
|
if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
@@ -4108,14 +4424,14 @@ var RepositoryActionService = class {
|
|
|
4108
4424
|
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
4109
4425
|
return result;
|
|
4110
4426
|
}
|
|
4111
|
-
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
4427
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
|
|
4112
4428
|
return collect$1(this.#runtime.spawn({
|
|
4113
4429
|
argv: [executable, ...args],
|
|
4114
4430
|
cwd,
|
|
4115
4431
|
stdio: {
|
|
4116
4432
|
stdin: "ignore",
|
|
4117
4433
|
stdout: { maxBytes },
|
|
4118
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
4434
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
|
|
4119
4435
|
},
|
|
4120
4436
|
graceMs: 1e3,
|
|
4121
4437
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -4125,20 +4441,20 @@ var RepositoryActionService = class {
|
|
|
4125
4441
|
};
|
|
4126
4442
|
//#endregion
|
|
4127
4443
|
//#region src/repository-setup-routes.ts
|
|
4128
|
-
const MAX_BODY_BYTES$
|
|
4129
|
-
function record$
|
|
4444
|
+
const MAX_BODY_BYTES$6 = 16384;
|
|
4445
|
+
function record$9(value) {
|
|
4130
4446
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4131
4447
|
}
|
|
4132
|
-
async function readJson$
|
|
4448
|
+
async function readJson$6(req) {
|
|
4133
4449
|
const chunks = [];
|
|
4134
4450
|
let size = 0;
|
|
4135
4451
|
for await (const chunk of req) {
|
|
4136
4452
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4137
4453
|
size += buffer.length;
|
|
4138
|
-
if (size > MAX_BODY_BYTES$
|
|
4454
|
+
if (size > MAX_BODY_BYTES$6) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
4139
4455
|
chunks.push(buffer);
|
|
4140
4456
|
}
|
|
4141
|
-
const value = record$
|
|
4457
|
+
const value = record$9(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4142
4458
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
4143
4459
|
return value;
|
|
4144
4460
|
}
|
|
@@ -4198,6 +4514,11 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4198
4514
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4199
4515
|
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
4200
4516
|
try {
|
|
4517
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
|
|
4518
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4519
|
+
const input = await readJson$6(req);
|
|
4520
|
+
return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
|
|
4521
|
+
}
|
|
4201
4522
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
|
|
4202
4523
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4203
4524
|
const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
|
|
@@ -4206,19 +4527,19 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4206
4527
|
}
|
|
4207
4528
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
4208
4529
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4209
|
-
const input = await readJson$
|
|
4530
|
+
const input = await readJson$6(req);
|
|
4210
4531
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
4211
4532
|
await streamSetup(res, service, input);
|
|
4212
4533
|
return;
|
|
4213
4534
|
}
|
|
4214
4535
|
if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
|
|
4215
4536
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4216
|
-
const input = await readJson$
|
|
4537
|
+
const input = await readJson$6(req);
|
|
4217
4538
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
4218
4539
|
}
|
|
4219
4540
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
4220
4541
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4221
|
-
const input = await readJson$
|
|
4542
|
+
const input = await readJson$6(req);
|
|
4222
4543
|
await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
|
|
4223
4544
|
return json(res, 200, { ok: true });
|
|
4224
4545
|
}
|
|
@@ -4236,8 +4557,8 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4236
4557
|
}
|
|
4237
4558
|
//#endregion
|
|
4238
4559
|
//#region src/repository-action-routes.ts
|
|
4239
|
-
const MAX_BODY_BYTES$
|
|
4240
|
-
const MAX_SESSION_ID_CHARS$
|
|
4560
|
+
const MAX_BODY_BYTES$5 = 16384;
|
|
4561
|
+
const MAX_SESSION_ID_CHARS$5 = 1024;
|
|
4241
4562
|
const ACTIONS = /* @__PURE__ */ new Set([
|
|
4242
4563
|
"commit",
|
|
4243
4564
|
"commit-push",
|
|
@@ -4246,25 +4567,25 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
4246
4567
|
"merge-pr",
|
|
4247
4568
|
"update-branch"
|
|
4248
4569
|
]);
|
|
4249
|
-
function record$
|
|
4570
|
+
function record$8(value) {
|
|
4250
4571
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4251
4572
|
}
|
|
4252
|
-
async function readJson$
|
|
4573
|
+
async function readJson$5(req) {
|
|
4253
4574
|
const chunks = [];
|
|
4254
4575
|
let size = 0;
|
|
4255
4576
|
for await (const chunk of req) {
|
|
4256
4577
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4257
4578
|
size += buffer.length;
|
|
4258
|
-
if (size > MAX_BODY_BYTES$
|
|
4579
|
+
if (size > MAX_BODY_BYTES$5) throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
4259
4580
|
chunks.push(buffer);
|
|
4260
4581
|
}
|
|
4261
|
-
const value = record$
|
|
4582
|
+
const value = record$8(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4262
4583
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
4263
4584
|
return value;
|
|
4264
4585
|
}
|
|
4265
4586
|
function sessionId$1(url) {
|
|
4266
4587
|
const value = url.searchParams.get("sessionId");
|
|
4267
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
4588
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
4268
4589
|
return value;
|
|
4269
4590
|
}
|
|
4270
4591
|
function string$1(input, key) {
|
|
@@ -4313,12 +4634,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
4313
4634
|
}
|
|
4314
4635
|
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
4315
4636
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4316
|
-
const input = await readJson$
|
|
4637
|
+
const input = await readJson$5(req);
|
|
4317
4638
|
return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
|
|
4318
4639
|
}
|
|
4319
4640
|
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
4320
4641
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4321
|
-
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$
|
|
4642
|
+
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$5(req))));
|
|
4322
4643
|
}
|
|
4323
4644
|
return json(res, 404, { error: "not found" });
|
|
4324
4645
|
} catch (error) {
|
|
@@ -4337,11 +4658,190 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
4337
4658
|
}), "dsh-claude: repository action route");
|
|
4338
4659
|
}
|
|
4339
4660
|
//#endregion
|
|
4661
|
+
//#region src/editor-open.ts
|
|
4662
|
+
const MAX_OUTPUT_BYTES$1 = 8192;
|
|
4663
|
+
/** Long enough for a launcher shim to fail loudly, short enough that the
|
|
4664
|
+
* request returns while the IDE is still booting. On Windows and Linux the
|
|
4665
|
+
* IDE binary IS the launched process, so still running past this is success. */
|
|
4666
|
+
const SETTLE_MS = 1500;
|
|
4667
|
+
const EDITOR_IDS = /* @__PURE__ */ new Set(["cursor", "idea"]);
|
|
4668
|
+
/** Launch commands tried in order, first success wins. macOS keeps `open -a`
|
|
4669
|
+
* behind the CLI shim because both shims are opt-in installs there, while
|
|
4670
|
+
* `open -a` finds the bundle wherever Toolbox or the DMG dropped it. */
|
|
4671
|
+
const LAUNCHERS = {
|
|
4672
|
+
cursor: {
|
|
4673
|
+
darwin: [["cursor"], [
|
|
4674
|
+
"open",
|
|
4675
|
+
"-a",
|
|
4676
|
+
"Cursor"
|
|
4677
|
+
]],
|
|
4678
|
+
win32: [["cursor"]],
|
|
4679
|
+
linux: [["cursor"]]
|
|
4680
|
+
},
|
|
4681
|
+
idea: {
|
|
4682
|
+
darwin: [
|
|
4683
|
+
["idea"],
|
|
4684
|
+
[
|
|
4685
|
+
"open",
|
|
4686
|
+
"-a",
|
|
4687
|
+
"IntelliJ IDEA"
|
|
4688
|
+
],
|
|
4689
|
+
[
|
|
4690
|
+
"open",
|
|
4691
|
+
"-a",
|
|
4692
|
+
"IntelliJ IDEA CE"
|
|
4693
|
+
]
|
|
4694
|
+
],
|
|
4695
|
+
win32: [["idea"], ["idea64.exe"]],
|
|
4696
|
+
linux: [["idea"], ["idea.sh"]]
|
|
4697
|
+
}
|
|
4698
|
+
};
|
|
4699
|
+
/** cmd.exe re-interprets these, and a mis-parsed path opens the wrong project
|
|
4700
|
+
* rather than failing. Refuse instead of guessing. */
|
|
4701
|
+
const WINDOWS_UNSAFE = /["&|<>^%]/u;
|
|
4702
|
+
var EditorOpenError = class extends Error {
|
|
4703
|
+
code;
|
|
4704
|
+
constructor(code, message) {
|
|
4705
|
+
super(message);
|
|
4706
|
+
this.name = "EditorOpenError";
|
|
4707
|
+
this.code = code;
|
|
4708
|
+
}
|
|
4709
|
+
};
|
|
4710
|
+
/** Open a session's working directory in a desktop editor. */
|
|
4711
|
+
var EditorOpenService = class {
|
|
4712
|
+
#runtime;
|
|
4713
|
+
#platform;
|
|
4714
|
+
#settleMs;
|
|
4715
|
+
constructor(runtime, platform = process.platform, settleMs = SETTLE_MS) {
|
|
4716
|
+
this.#runtime = runtime;
|
|
4717
|
+
this.#platform = platform;
|
|
4718
|
+
this.#settleMs = settleMs;
|
|
4719
|
+
}
|
|
4720
|
+
async open(cwd, editor) {
|
|
4721
|
+
if (this.#platform === "win32" && WINDOWS_UNSAFE.test(cwd)) throw new EditorOpenError("unsupported-path", "The project path cannot be opened through the Windows shell.");
|
|
4722
|
+
const candidates = LAUNCHERS[editor][this.#platform] ?? LAUNCHERS[editor].linux ?? [];
|
|
4723
|
+
let found = false;
|
|
4724
|
+
for (const candidate of candidates) {
|
|
4725
|
+
const argv = await this.#argv(candidate, cwd);
|
|
4726
|
+
if (argv === void 0) continue;
|
|
4727
|
+
found = true;
|
|
4728
|
+
if (await this.#launch(argv, cwd)) return;
|
|
4729
|
+
}
|
|
4730
|
+
throw found ? new EditorOpenError("launch-failed", "The editor refused to open the project.") : new EditorOpenError("editor-unavailable", "The editor was not found on PATH.");
|
|
4731
|
+
}
|
|
4732
|
+
async #argv(candidate, cwd) {
|
|
4733
|
+
const [program, ...rest] = candidate;
|
|
4734
|
+
if (program === void 0) return void 0;
|
|
4735
|
+
if (this.#platform === "win32") return [
|
|
4736
|
+
"cmd.exe",
|
|
4737
|
+
"/d",
|
|
4738
|
+
"/s",
|
|
4739
|
+
"/c",
|
|
4740
|
+
program,
|
|
4741
|
+
...rest,
|
|
4742
|
+
cwd
|
|
4743
|
+
];
|
|
4744
|
+
try {
|
|
4745
|
+
return [
|
|
4746
|
+
await this.#runtime.resolveExecutable(program),
|
|
4747
|
+
...rest,
|
|
4748
|
+
cwd
|
|
4749
|
+
];
|
|
4750
|
+
} catch {
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
/** True once the editor is launched: either the shim exited cleanly or the
|
|
4755
|
+
* process is still alive past the settle window. */
|
|
4756
|
+
async #launch(argv, cwd) {
|
|
4757
|
+
let handle;
|
|
4758
|
+
try {
|
|
4759
|
+
handle = this.#runtime.spawn({
|
|
4760
|
+
argv,
|
|
4761
|
+
cwd,
|
|
4762
|
+
stdio: {
|
|
4763
|
+
stdin: "ignore",
|
|
4764
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
|
|
4765
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
4766
|
+
},
|
|
4767
|
+
graceMs: 1e3,
|
|
4768
|
+
env: {}
|
|
4769
|
+
});
|
|
4770
|
+
} catch {
|
|
4771
|
+
return false;
|
|
4772
|
+
}
|
|
4773
|
+
return await Promise.race([handle.done.then((outcome) => outcome.exitCode === 0), new Promise((resolve) => {
|
|
4774
|
+
setTimeout(() => resolve(true), this.#settleMs).unref?.();
|
|
4775
|
+
})]);
|
|
4776
|
+
}
|
|
4777
|
+
};
|
|
4778
|
+
//#endregion
|
|
4779
|
+
//#region src/editor-open-routes.ts
|
|
4780
|
+
const MAX_SESSION_ID_CHARS$4 = 1024;
|
|
4781
|
+
/** Open the session's working directory in a desktop editor. Query-only: the
|
|
4782
|
+
* request carries two enum-ish values, so there is no body to parse. */
|
|
4783
|
+
function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
4784
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4785
|
+
kind: "exact",
|
|
4786
|
+
path: CLAUDE_EDITOR_OPEN_PATH,
|
|
4787
|
+
handler: async (req, res) => {
|
|
4788
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4789
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4790
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
4791
|
+
const id = params.get("sessionId");
|
|
4792
|
+
const editor = params.get("editor");
|
|
4793
|
+
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return json(res, 400, {
|
|
4794
|
+
error: "invalid-session",
|
|
4795
|
+
message: "The session is invalid."
|
|
4796
|
+
});
|
|
4797
|
+
if (editor === null || !EDITOR_IDS.has(editor)) return json(res, 400, {
|
|
4798
|
+
error: "invalid-editor",
|
|
4799
|
+
message: "The editor is invalid."
|
|
4800
|
+
});
|
|
4801
|
+
const cwd = cwdForSession(id);
|
|
4802
|
+
if (cwd === void 0) return json(res, 409, {
|
|
4803
|
+
error: "session-unavailable",
|
|
4804
|
+
message: "The Claude session is unavailable."
|
|
4805
|
+
});
|
|
4806
|
+
try {
|
|
4807
|
+
await service.open(cwd, editor);
|
|
4808
|
+
return json(res, 200, { opened: true });
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
if (error instanceof EditorOpenError) return json(res, 409, {
|
|
4811
|
+
error: error.code,
|
|
4812
|
+
message: error.message
|
|
4813
|
+
});
|
|
4814
|
+
return json(res, 500, {
|
|
4815
|
+
error: "editor-open-unavailable",
|
|
4816
|
+
message: "The editor could not be launched."
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
}), "dsh-claude: editor open route");
|
|
4821
|
+
}
|
|
4822
|
+
//#endregion
|
|
4823
|
+
//#region src/github-url.ts
|
|
4824
|
+
/** Only GitHub's own image hosts; the browser loads these directly, so a URL
|
|
4825
|
+
* the API did not vouch for must never become an outbound request. */
|
|
4826
|
+
function githubAvatarUrl(value) {
|
|
4827
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
|
|
4828
|
+
try {
|
|
4829
|
+
const url = new URL(value);
|
|
4830
|
+
const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
|
|
4831
|
+
return url.protocol === "https:" && allowed ? url.href : void 0;
|
|
4832
|
+
} catch {
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
//#endregion
|
|
4340
4837
|
//#region src/pr-feedback.ts
|
|
4341
4838
|
const MAX_OUTPUT_BYTES = 524288;
|
|
4342
4839
|
const GIT_TIMEOUT_MS = 1e4;
|
|
4343
4840
|
const GH_TIMEOUT_MS = 6e4;
|
|
4344
4841
|
const MAX_COMMENTS = 100;
|
|
4842
|
+
const MAX_THREADS = 100;
|
|
4843
|
+
const MAX_MENTIONABLE_USERS = 20;
|
|
4844
|
+
const MAX_REPLY_CHARS$1 = 2e3;
|
|
4345
4845
|
const MAX_COMMENT_CHARS = 4096;
|
|
4346
4846
|
const MAX_FAILING_CHECKS = 3;
|
|
4347
4847
|
const MAX_LOG_CHARS = 8192;
|
|
@@ -4353,6 +4853,31 @@ var PullRequestFeedbackError = class extends Error {
|
|
|
4353
4853
|
this.code = code;
|
|
4354
4854
|
}
|
|
4355
4855
|
};
|
|
4856
|
+
/** Threads carry the anchor; comments carry the prose. `isOutdated` marks a
|
|
4857
|
+
* thread whose lines the branch has since moved past. */
|
|
4858
|
+
const REVIEW_THREADS_QUERY = `query($owner:String!,$name:String!,$number:Int!){
|
|
4859
|
+
repository(owner:$owner,name:$name){
|
|
4860
|
+
pullRequest(number:$number){
|
|
4861
|
+
reviewThreads(first:100){
|
|
4862
|
+
nodes{
|
|
4863
|
+
id isResolved isOutdated path line originalLine diffSide
|
|
4864
|
+
comments(first:50){ nodes{ databaseId body url createdAt author{ __typename login avatarUrl } } }
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
}`;
|
|
4870
|
+
const RESOLVE_MUTATION = `mutation($threadId:ID!){
|
|
4871
|
+
resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
|
|
4872
|
+
}`;
|
|
4873
|
+
const UNRESOLVE_MUTATION = `mutation($threadId:ID!){
|
|
4874
|
+
unresolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
|
|
4875
|
+
}`;
|
|
4876
|
+
const MENTIONABLE_USERS_QUERY = `query($owner:String!,$name:String!,$q:String!){
|
|
4877
|
+
repository(owner:$owner,name:$name){
|
|
4878
|
+
mentionableUsers(first:20,query:$q){ nodes{ login avatarUrl } }
|
|
4879
|
+
}
|
|
4880
|
+
}`;
|
|
4356
4881
|
async function collect(handle) {
|
|
4357
4882
|
const outcome = await handle.done;
|
|
4358
4883
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
@@ -4362,32 +4887,100 @@ async function collect(handle) {
|
|
|
4362
4887
|
lossy: stdout?.lossy === true
|
|
4363
4888
|
};
|
|
4364
4889
|
}
|
|
4365
|
-
function record$
|
|
4890
|
+
function record$7(value) {
|
|
4366
4891
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4367
4892
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4893
|
+
/** Read `repository.pullRequest.reviewThreads.nodes` out of a GraphQL response.
|
|
4894
|
+
* A payload carrying `errors` instead of data yields nothing rather than
|
|
4895
|
+
* throwing: the caller distinguishes "no threads" from "call failed" by the
|
|
4896
|
+
* process exit code. */
|
|
4897
|
+
function parseReviewThreads(value) {
|
|
4898
|
+
const nodes = record$7(record$7(record$7(record$7(record$7(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
|
|
4899
|
+
if (!Array.isArray(nodes)) return [];
|
|
4900
|
+
const threads = [];
|
|
4901
|
+
let total = 0;
|
|
4902
|
+
for (const item of nodes) {
|
|
4903
|
+
const input = record$7(item);
|
|
4904
|
+
if (input === void 0 || typeof input.id !== "string" || input.id.length === 0) continue;
|
|
4375
4905
|
const path = typeof input.path === "string" ? input.path : "";
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
id: Number(input.id),
|
|
4906
|
+
if (path.length === 0) continue;
|
|
4907
|
+
const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.originalLine) ? Number(input.originalLine) : void 0;
|
|
4908
|
+
const side = input.diffSide === "LEFT" ? "old" : "new";
|
|
4909
|
+
const anchor = {
|
|
4381
4910
|
path,
|
|
4382
4911
|
...line === void 0 ? {} : { line },
|
|
4383
|
-
side
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4912
|
+
side
|
|
4913
|
+
};
|
|
4914
|
+
const comments = [];
|
|
4915
|
+
const commentNodes = record$7(input.comments)?.nodes;
|
|
4916
|
+
for (const node of Array.isArray(commentNodes) ? commentNodes : []) {
|
|
4917
|
+
if (total >= MAX_COMMENTS) break;
|
|
4918
|
+
const comment = record$7(node);
|
|
4919
|
+
if (comment === void 0 || !Number.isSafeInteger(comment.databaseId)) continue;
|
|
4920
|
+
const body = typeof comment.body === "string" ? comment.body.trim() : "";
|
|
4921
|
+
if (body.length === 0) continue;
|
|
4922
|
+
const author = record$7(comment.author);
|
|
4923
|
+
const avatarUrl = githubAvatarUrl(author?.avatarUrl);
|
|
4924
|
+
const login = typeof author?.login === "string" ? author.login : "unknown";
|
|
4925
|
+
comments.push({
|
|
4926
|
+
id: Number(comment.databaseId),
|
|
4927
|
+
...anchor,
|
|
4928
|
+
author: login,
|
|
4929
|
+
...author?.__typename === "Bot" || login.endsWith("[bot]") ? { bot: true } : {},
|
|
4930
|
+
...avatarUrl === void 0 ? {} : { avatarUrl },
|
|
4931
|
+
body: body.slice(0, MAX_COMMENT_CHARS),
|
|
4932
|
+
url: typeof comment.url === "string" ? comment.url : "",
|
|
4933
|
+
...typeof comment.createdAt === "string" ? { createdAt: comment.createdAt } : {}
|
|
4934
|
+
});
|
|
4935
|
+
total += 1;
|
|
4936
|
+
}
|
|
4937
|
+
if (comments.length === 0) continue;
|
|
4938
|
+
threads.push({
|
|
4939
|
+
id: input.id,
|
|
4940
|
+
...anchor,
|
|
4941
|
+
resolved: input.isResolved === true,
|
|
4942
|
+
outdated: input.isOutdated === true,
|
|
4943
|
+
comments
|
|
4944
|
+
});
|
|
4945
|
+
if (threads.length >= MAX_THREADS || total >= MAX_COMMENTS) break;
|
|
4946
|
+
}
|
|
4947
|
+
return threads;
|
|
4948
|
+
}
|
|
4949
|
+
/** One posted reply, shaped like the thread comments it joins. */
|
|
4950
|
+
function parseReplyComment(value, anchor) {
|
|
4951
|
+
const input = record$7(value);
|
|
4952
|
+
if (input === void 0 || !Number.isSafeInteger(input.id)) return void 0;
|
|
4953
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
4954
|
+
if (body.length === 0) return void 0;
|
|
4955
|
+
const user = record$7(input.user);
|
|
4956
|
+
const avatarUrl = githubAvatarUrl(user?.avatar_url);
|
|
4957
|
+
const login = typeof user?.login === "string" ? user.login : "unknown";
|
|
4958
|
+
return {
|
|
4959
|
+
id: Number(input.id),
|
|
4960
|
+
...anchor,
|
|
4961
|
+
author: login,
|
|
4962
|
+
...user?.type === "Bot" || login.endsWith("[bot]") ? { bot: true } : {},
|
|
4963
|
+
...avatarUrl === void 0 ? {} : { avatarUrl },
|
|
4964
|
+
body: body.slice(0, MAX_COMMENT_CHARS),
|
|
4965
|
+
url: typeof input.html_url === "string" ? input.html_url : "",
|
|
4966
|
+
...typeof input.created_at === "string" ? { createdAt: input.created_at } : {}
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
function parseMentionableUsers(value) {
|
|
4970
|
+
const nodes = record$7(record$7(record$7(record$7(value)?.data)?.repository)?.mentionableUsers)?.nodes;
|
|
4971
|
+
if (!Array.isArray(nodes)) return [];
|
|
4972
|
+
const users = [];
|
|
4973
|
+
for (const item of nodes) {
|
|
4974
|
+
const input = record$7(item);
|
|
4975
|
+
if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
|
|
4976
|
+
const avatarUrl = githubAvatarUrl(input.avatarUrl);
|
|
4977
|
+
users.push({
|
|
4978
|
+
login: input.login,
|
|
4979
|
+
...avatarUrl === void 0 ? {} : { avatarUrl }
|
|
4387
4980
|
});
|
|
4388
|
-
if (
|
|
4981
|
+
if (users.length >= MAX_MENTIONABLE_USERS) break;
|
|
4389
4982
|
}
|
|
4390
|
-
return
|
|
4983
|
+
return users;
|
|
4391
4984
|
}
|
|
4392
4985
|
/** Extract the Actions job id from a check run link, when it is one. */
|
|
4393
4986
|
function actionsJobId(link) {
|
|
@@ -4398,7 +4991,7 @@ function parseFailingChecks(value) {
|
|
|
4398
4991
|
if (!Array.isArray(value)) return [];
|
|
4399
4992
|
const failing = [];
|
|
4400
4993
|
for (const item of value) {
|
|
4401
|
-
const input = record$
|
|
4994
|
+
const input = record$7(item);
|
|
4402
4995
|
if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
|
|
4403
4996
|
failing.push({
|
|
4404
4997
|
name: input.name,
|
|
@@ -4420,22 +5013,103 @@ var PullRequestFeedbackService = class {
|
|
|
4420
5013
|
constructor(runtime) {
|
|
4421
5014
|
this.#runtime = runtime;
|
|
4422
5015
|
}
|
|
4423
|
-
async
|
|
4424
|
-
const
|
|
5016
|
+
async threads(cwd, pullNumber) {
|
|
5017
|
+
const { owner, name } = await this.#repositoryParts(cwd);
|
|
4425
5018
|
const gh = await this.#gh();
|
|
4426
5019
|
const result = await this.#run(gh, [
|
|
4427
5020
|
"api",
|
|
4428
|
-
|
|
4429
|
-
"
|
|
5021
|
+
"graphql",
|
|
5022
|
+
"-f",
|
|
5023
|
+
`owner=${owner}`,
|
|
5024
|
+
"-f",
|
|
5025
|
+
`name=${name}`,
|
|
5026
|
+
"-F",
|
|
5027
|
+
`number=${pullNumber}`,
|
|
5028
|
+
"-f",
|
|
5029
|
+
`query=${REVIEW_THREADS_QUERY}`
|
|
4430
5030
|
], cwd, GH_TIMEOUT_MS);
|
|
4431
5031
|
if (result.exitCode !== 0) throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be loaded.");
|
|
4432
5032
|
try {
|
|
4433
|
-
|
|
4434
|
-
return parseReviewComments(JSON.parse(pages));
|
|
5033
|
+
return parseReviewThreads(JSON.parse(result.stdout));
|
|
4435
5034
|
} catch {
|
|
4436
5035
|
throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be parsed.");
|
|
4437
5036
|
}
|
|
4438
5037
|
}
|
|
5038
|
+
/** Post a reply into the thread the given comment belongs to. GitHub parses
|
|
5039
|
+
* `@login` in the body server-side, so mentions need nothing from us. */
|
|
5040
|
+
async reply(cwd, pullNumber, commentId, body) {
|
|
5041
|
+
const text = body.trim();
|
|
5042
|
+
if (text.length === 0 || text.length > MAX_REPLY_CHARS$1) throw new PullRequestFeedbackError("invalid-request", "The reply body is invalid.");
|
|
5043
|
+
const repository = await this.#repository(cwd);
|
|
5044
|
+
const gh = await this.#gh();
|
|
5045
|
+
const result = await this.#run(gh, [
|
|
5046
|
+
"api",
|
|
5047
|
+
"-X",
|
|
5048
|
+
"POST",
|
|
5049
|
+
`repos/${repository}/pulls/${pullNumber}/comments/${commentId}/replies`,
|
|
5050
|
+
"--input",
|
|
5051
|
+
"-"
|
|
5052
|
+
], cwd, GH_TIMEOUT_MS, JSON.stringify({ body: text }));
|
|
5053
|
+
if (result.exitCode !== 0) throw new PullRequestFeedbackError("reply-failed", "The reply could not be posted.");
|
|
5054
|
+
let posted;
|
|
5055
|
+
try {
|
|
5056
|
+
const parsed = JSON.parse(result.stdout);
|
|
5057
|
+
const path = typeof record$7(parsed)?.path === "string" ? String(record$7(parsed)?.path) : "";
|
|
5058
|
+
const line = Number.isSafeInteger(record$7(parsed)?.line) ? Number(record$7(parsed)?.line) : void 0;
|
|
5059
|
+
posted = parseReplyComment(parsed, {
|
|
5060
|
+
path,
|
|
5061
|
+
...line === void 0 ? {} : { line },
|
|
5062
|
+
side: record$7(parsed)?.side === "LEFT" ? "old" : "new"
|
|
5063
|
+
});
|
|
5064
|
+
} catch {
|
|
5065
|
+
posted = void 0;
|
|
5066
|
+
}
|
|
5067
|
+
if (posted === void 0) throw new PullRequestFeedbackError("reply-failed", "The posted reply could not be read back.");
|
|
5068
|
+
return posted;
|
|
5069
|
+
}
|
|
5070
|
+
/** Resolve or reopen a thread. Returns the state GitHub reports afterwards. */
|
|
5071
|
+
async setResolved(cwd, threadId, resolved) {
|
|
5072
|
+
const gh = await this.#gh();
|
|
5073
|
+
const result = await this.#run(gh, [
|
|
5074
|
+
"api",
|
|
5075
|
+
"graphql",
|
|
5076
|
+
"-f",
|
|
5077
|
+
`threadId=${threadId}`,
|
|
5078
|
+
"-f",
|
|
5079
|
+
`query=${resolved ? RESOLVE_MUTATION : UNRESOLVE_MUTATION}`
|
|
5080
|
+
], cwd, GH_TIMEOUT_MS);
|
|
5081
|
+
if (result.exitCode !== 0) throw new PullRequestFeedbackError("resolve-failed", "The thread could not be updated.");
|
|
5082
|
+
try {
|
|
5083
|
+
const thread = record$7(record$7(record$7(record$7(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
|
|
5084
|
+
if (typeof thread?.isResolved !== "boolean") throw new Error("missing state");
|
|
5085
|
+
return thread.isResolved;
|
|
5086
|
+
} catch {
|
|
5087
|
+
throw new PullRequestFeedbackError("resolve-failed", "The thread state could not be read back.");
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
/** Logins GitHub would notify from this repository, for the reply composer. */
|
|
5091
|
+
async mentionables(cwd, query) {
|
|
5092
|
+
const { owner, name } = await this.#repositoryParts(cwd);
|
|
5093
|
+
const gh = await this.#gh();
|
|
5094
|
+
const result = await this.#run(gh, [
|
|
5095
|
+
"api",
|
|
5096
|
+
"graphql",
|
|
5097
|
+
"-f",
|
|
5098
|
+
`owner=${owner}`,
|
|
5099
|
+
"-f",
|
|
5100
|
+
`name=${name}`,
|
|
5101
|
+
"-f",
|
|
5102
|
+
`q=${query}`,
|
|
5103
|
+
"-f",
|
|
5104
|
+
`query=${MENTIONABLE_USERS_QUERY}`
|
|
5105
|
+
], cwd, GH_TIMEOUT_MS);
|
|
5106
|
+
if (result.exitCode !== 0) return [];
|
|
5107
|
+
try {
|
|
5108
|
+
return parseMentionableUsers(JSON.parse(result.stdout));
|
|
5109
|
+
} catch {
|
|
5110
|
+
return [];
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
4439
5113
|
async failingChecks(cwd, pullNumber) {
|
|
4440
5114
|
const gh = await this.#gh();
|
|
4441
5115
|
const result = await this.#run(gh, [
|
|
@@ -4483,6 +5157,16 @@ var PullRequestFeedbackService = class {
|
|
|
4483
5157
|
if (repository === void 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
|
|
4484
5158
|
return repository;
|
|
4485
5159
|
}
|
|
5160
|
+
/** GraphQL takes the owner and the name as separate variables, so they never
|
|
5161
|
+
* reach the query text itself. */
|
|
5162
|
+
async #repositoryParts(cwd) {
|
|
5163
|
+
const [owner, name] = (await this.#repository(cwd)).split("/");
|
|
5164
|
+
if (owner === void 0 || name === void 0 || owner.length === 0 || name.length === 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
|
|
5165
|
+
return {
|
|
5166
|
+
owner,
|
|
5167
|
+
name
|
|
5168
|
+
};
|
|
5169
|
+
}
|
|
4486
5170
|
#git() {
|
|
4487
5171
|
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
4488
5172
|
return this.#gitExecutable;
|
|
@@ -4493,27 +5177,64 @@ var PullRequestFeedbackService = class {
|
|
|
4493
5177
|
});
|
|
4494
5178
|
return this.#ghExecutable;
|
|
4495
5179
|
}
|
|
4496
|
-
#run(executable, args, cwd, timeoutMs) {
|
|
4497
|
-
|
|
5180
|
+
#run(executable, args, cwd, timeoutMs, input) {
|
|
5181
|
+
const handle = this.#runtime.spawn({
|
|
4498
5182
|
argv: [executable, ...args],
|
|
4499
5183
|
cwd,
|
|
4500
5184
|
stdio: {
|
|
4501
|
-
stdin: "ignore",
|
|
5185
|
+
stdin: input === void 0 ? "ignore" : "pipe",
|
|
4502
5186
|
stdout: { maxBytes: MAX_OUTPUT_BYTES },
|
|
4503
5187
|
stderr: { maxBytes: 65536 }
|
|
4504
5188
|
},
|
|
4505
5189
|
graceMs: 1e3,
|
|
4506
5190
|
signal: AbortSignal.timeout(timeoutMs),
|
|
4507
5191
|
env: {}
|
|
4508
|
-
})
|
|
5192
|
+
});
|
|
5193
|
+
if (input !== void 0) handle.stdin?.end(input);
|
|
5194
|
+
return collect(handle);
|
|
4509
5195
|
}
|
|
4510
5196
|
};
|
|
4511
5197
|
//#endregion
|
|
4512
5198
|
//#region src/pr-feedback-routes.ts
|
|
4513
|
-
const MAX_SESSION_ID_CHARS$
|
|
5199
|
+
const MAX_SESSION_ID_CHARS$3 = 1024;
|
|
5200
|
+
const MAX_BODY_BYTES$4 = 16384;
|
|
5201
|
+
const MAX_REPLY_CHARS = 2e3;
|
|
5202
|
+
const MAX_THREAD_ID_CHARS = 512;
|
|
5203
|
+
const MAX_MENTION_QUERY_CHARS = 64;
|
|
5204
|
+
function record$6(value) {
|
|
5205
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5206
|
+
}
|
|
5207
|
+
async function readJson$4(req) {
|
|
5208
|
+
const chunks = [];
|
|
5209
|
+
let size = 0;
|
|
5210
|
+
for await (const chunk of req) {
|
|
5211
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5212
|
+
size += buffer.length;
|
|
5213
|
+
if (size > MAX_BODY_BYTES$4) throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
|
|
5214
|
+
chunks.push(buffer);
|
|
5215
|
+
}
|
|
5216
|
+
const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5217
|
+
if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
|
|
5218
|
+
return value;
|
|
5219
|
+
}
|
|
5220
|
+
function replyBody(input) {
|
|
5221
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5222
|
+
if (body.length === 0 || body.length > MAX_REPLY_CHARS || body.includes("\0")) throw new PullRequestFeedbackError("invalid-request", "The reply body is invalid.");
|
|
5223
|
+
return body;
|
|
5224
|
+
}
|
|
5225
|
+
function commentId(input) {
|
|
5226
|
+
const value = input.commentId;
|
|
5227
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new PullRequestFeedbackError("invalid-request", "The comment id is invalid.");
|
|
5228
|
+
return value;
|
|
5229
|
+
}
|
|
5230
|
+
function threadId(input) {
|
|
5231
|
+
const value = input.threadId;
|
|
5232
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_THREAD_ID_CHARS) throw new PullRequestFeedbackError("invalid-request", "The thread id is invalid.");
|
|
5233
|
+
return value;
|
|
5234
|
+
}
|
|
4514
5235
|
function sessionId(url) {
|
|
4515
5236
|
const value = url.searchParams.get("sessionId");
|
|
4516
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
5237
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
|
|
4517
5238
|
return value;
|
|
4518
5239
|
}
|
|
4519
5240
|
function pullNumber(url) {
|
|
@@ -4527,19 +5248,43 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
|
4527
5248
|
path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
|
|
4528
5249
|
handler: async (req, res) => {
|
|
4529
5250
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4530
|
-
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4531
5251
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5252
|
+
const reads = req.method === "GET";
|
|
5253
|
+
const writes = req.method === "POST";
|
|
4532
5254
|
try {
|
|
4533
5255
|
const cwd = cwdForSession(sessionId(url));
|
|
4534
5256
|
if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
|
|
4535
|
-
if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`)
|
|
4536
|
-
|
|
5257
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) {
|
|
5258
|
+
if (!reads) return json(res, 405, { error: "method not allowed" });
|
|
5259
|
+
return json(res, 200, { threads: await service.threads(cwd, pullNumber(url)) });
|
|
5260
|
+
}
|
|
5261
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) {
|
|
5262
|
+
if (!reads) return json(res, 405, { error: "method not allowed" });
|
|
5263
|
+
return json(res, 200, { checks: await service.failingChecks(cwd, pullNumber(url)) });
|
|
5264
|
+
}
|
|
5265
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/mentionables`) {
|
|
5266
|
+
if (!reads) return json(res, 405, { error: "method not allowed" });
|
|
5267
|
+
const query = (url.searchParams.get("q") ?? "").slice(0, MAX_MENTION_QUERY_CHARS);
|
|
5268
|
+
return json(res, 200, { users: await service.mentionables(cwd, query) });
|
|
5269
|
+
}
|
|
5270
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/reply`) {
|
|
5271
|
+
if (!writes) return json(res, 405, { error: "method not allowed" });
|
|
5272
|
+
const input = await readJson$4(req);
|
|
5273
|
+
return json(res, 200, { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) });
|
|
5274
|
+
}
|
|
5275
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/resolve`) {
|
|
5276
|
+
if (!writes) return json(res, 405, { error: "method not allowed" });
|
|
5277
|
+
const input = await readJson$4(req);
|
|
5278
|
+
if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
|
|
5279
|
+
return json(res, 200, { resolved: await service.setResolved(cwd, threadId(input), input.resolved) });
|
|
5280
|
+
}
|
|
4537
5281
|
return json(res, 404, { error: "not found" });
|
|
4538
5282
|
} catch (error) {
|
|
4539
5283
|
if (error instanceof PullRequestFeedbackError) return json(res, 409, {
|
|
4540
5284
|
error: error.code,
|
|
4541
5285
|
message: error.message
|
|
4542
5286
|
});
|
|
5287
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
4543
5288
|
return json(res, 500, {
|
|
4544
5289
|
error: "pr-feedback-unavailable",
|
|
4545
5290
|
message: "Pull request feedback is unavailable."
|
|
@@ -4629,7 +5374,7 @@ var JiraError = class extends Error {
|
|
|
4629
5374
|
this.code = code;
|
|
4630
5375
|
}
|
|
4631
5376
|
};
|
|
4632
|
-
function record$
|
|
5377
|
+
function record$5(value) {
|
|
4633
5378
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4634
5379
|
}
|
|
4635
5380
|
/** Jira Cloud sites are origins; Data Center may carry a context path. */
|
|
@@ -4647,6 +5392,30 @@ function ticketKeyOf(query) {
|
|
|
4647
5392
|
const match = /^([A-Za-z][A-Za-z0-9_]*)-(\d+)$/u.exec(query.trim());
|
|
4648
5393
|
return match === null ? void 0 : `${match[1].toUpperCase()}-${match[2]}`;
|
|
4649
5394
|
}
|
|
5395
|
+
/** Jira's text index carries summary, description and comments -- never the
|
|
5396
|
+
* issue key -- so a bare ticket number ("5697") can never reach PSOS-5697
|
|
5397
|
+
* through `text ~`. The issue picker is the endpoint behind Jira's own quick
|
|
5398
|
+
* search and does match keys; its hits become the key filter the normal
|
|
5399
|
+
* search then reads the display fields from. */
|
|
5400
|
+
function pickerKeys(value, number) {
|
|
5401
|
+
const sections = record$5(value)?.sections;
|
|
5402
|
+
if (!Array.isArray(sections)) return [];
|
|
5403
|
+
const keys = [];
|
|
5404
|
+
for (const section of sections) {
|
|
5405
|
+
const issues = record$5(section)?.issues;
|
|
5406
|
+
if (!Array.isArray(issues)) continue;
|
|
5407
|
+
for (const issue of issues) {
|
|
5408
|
+
const raw = record$5(issue)?.key;
|
|
5409
|
+
const key = ticketKeyOf(typeof raw === "string" ? raw : "");
|
|
5410
|
+
if (key !== void 0 && key.endsWith(`-${number}`) && !keys.includes(key)) keys.push(key);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
return keys;
|
|
5414
|
+
}
|
|
5415
|
+
/** Keys arrive validated by `ticketKeyOf`, so they cannot break out of the quotes. */
|
|
5416
|
+
function keyJql(keys) {
|
|
5417
|
+
return `key in (${keys.map((key) => `"${key}"`).join(", ")}) ORDER BY updated DESC`;
|
|
5418
|
+
}
|
|
4650
5419
|
function buildJql(query) {
|
|
4651
5420
|
const trimmed = query.trim().slice(0, MAX_QUERY_CHARS);
|
|
4652
5421
|
if (trimmed.length === 0) return "statusCategory != Done ORDER BY updated DESC";
|
|
@@ -4655,15 +5424,15 @@ function buildJql(query) {
|
|
|
4655
5424
|
return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
|
|
4656
5425
|
}
|
|
4657
5426
|
function parseTickets(value, siteUrl) {
|
|
4658
|
-
const issues = record$
|
|
5427
|
+
const issues = record$5(value)?.issues;
|
|
4659
5428
|
if (!Array.isArray(issues)) return [];
|
|
4660
5429
|
const tickets = [];
|
|
4661
5430
|
for (const item of issues) {
|
|
4662
|
-
const issue = record$
|
|
4663
|
-
const fields = record$
|
|
5431
|
+
const issue = record$5(item);
|
|
5432
|
+
const fields = record$5(issue?.fields);
|
|
4664
5433
|
if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
|
|
4665
|
-
const status = record$
|
|
4666
|
-
const type = record$
|
|
5434
|
+
const status = record$5(fields.status)?.name;
|
|
5435
|
+
const type = record$5(fields.issuetype)?.name;
|
|
4667
5436
|
tickets.push({
|
|
4668
5437
|
key: issue.key,
|
|
4669
5438
|
summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
|
|
@@ -4705,7 +5474,7 @@ var JiraService = class {
|
|
|
4705
5474
|
email,
|
|
4706
5475
|
apiToken
|
|
4707
5476
|
};
|
|
4708
|
-
const myself = record$
|
|
5477
|
+
const myself = record$5(await this.#json(connection, "/rest/api/3/myself"));
|
|
4709
5478
|
const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
|
|
4710
5479
|
const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
|
|
4711
5480
|
const store = {
|
|
@@ -4724,7 +5493,7 @@ var JiraService = class {
|
|
|
4724
5493
|
if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
|
|
4725
5494
|
let accountId = store.accountId;
|
|
4726
5495
|
if (accountId === void 0) {
|
|
4727
|
-
const myself = record$
|
|
5496
|
+
const myself = record$5(await this.#json(store, "/rest/api/3/myself"));
|
|
4728
5497
|
if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
|
|
4729
5498
|
accountId = myself.accountId;
|
|
4730
5499
|
await this.#write({
|
|
@@ -4744,7 +5513,7 @@ var JiraService = class {
|
|
|
4744
5513
|
const store = await this.#read();
|
|
4745
5514
|
if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
|
|
4746
5515
|
const params = new URLSearchParams({
|
|
4747
|
-
jql:
|
|
5516
|
+
jql: await this.#searchJql(store, query.trim().slice(0, MAX_QUERY_CHARS)),
|
|
4748
5517
|
maxResults: String(MAX_RESULTS),
|
|
4749
5518
|
fields: "summary,status,issuetype"
|
|
4750
5519
|
});
|
|
@@ -4757,6 +5526,14 @@ var JiraService = class {
|
|
|
4757
5526
|
}
|
|
4758
5527
|
return parseTickets(body, store.siteUrl);
|
|
4759
5528
|
}
|
|
5529
|
+
/** A bare ticket number resolves through the picker; everything else is JQL.
|
|
5530
|
+
* A site that cannot serve the picker falls back to the text search rather
|
|
5531
|
+
* than failing the whole lookup. */
|
|
5532
|
+
async #searchJql(store, query) {
|
|
5533
|
+
if (!/^\d+$/u.test(query)) return buildJql(query);
|
|
5534
|
+
const keys = await this.#json(store, `/rest/api/3/issue/picker?query=${encodeURIComponent(query)}`).then((body) => pickerKeys(body, query), () => []);
|
|
5535
|
+
return keys.length === 0 ? buildJql(query) : keyJql(keys);
|
|
5536
|
+
}
|
|
4760
5537
|
async #json(connection, path, init = {}) {
|
|
4761
5538
|
let response;
|
|
4762
5539
|
try {
|
|
@@ -4792,7 +5569,7 @@ var JiraService = class {
|
|
|
4792
5569
|
throw error;
|
|
4793
5570
|
}
|
|
4794
5571
|
if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
|
|
4795
|
-
const input = record$
|
|
5572
|
+
const input = record$5(JSON.parse(text));
|
|
4796
5573
|
if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
|
|
4797
5574
|
return {
|
|
4798
5575
|
siteUrl: input.siteUrl,
|
|
@@ -4823,20 +5600,20 @@ var JiraService = class {
|
|
|
4823
5600
|
};
|
|
4824
5601
|
//#endregion
|
|
4825
5602
|
//#region src/jira-routes.ts
|
|
4826
|
-
const MAX_BODY_BYTES$
|
|
4827
|
-
function record$
|
|
5603
|
+
const MAX_BODY_BYTES$3 = 8192;
|
|
5604
|
+
function record$4(value) {
|
|
4828
5605
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4829
5606
|
}
|
|
4830
|
-
async function readJson$
|
|
5607
|
+
async function readJson$3(req) {
|
|
4831
5608
|
const chunks = [];
|
|
4832
5609
|
let size = 0;
|
|
4833
5610
|
for await (const chunk of req) {
|
|
4834
5611
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4835
5612
|
size += buffer.length;
|
|
4836
|
-
if (size > MAX_BODY_BYTES$
|
|
5613
|
+
if (size > MAX_BODY_BYTES$3) throw new JiraError("body-too-large", "The request body is too large.");
|
|
4837
5614
|
chunks.push(buffer);
|
|
4838
5615
|
}
|
|
4839
|
-
const value = record$
|
|
5616
|
+
const value = record$4(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4840
5617
|
if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
|
|
4841
5618
|
return value;
|
|
4842
5619
|
}
|
|
@@ -4859,7 +5636,7 @@ function registerJiraRoute(ctx, service) {
|
|
|
4859
5636
|
}
|
|
4860
5637
|
if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
|
|
4861
5638
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4862
|
-
const input = await readJson$
|
|
5639
|
+
const input = await readJson$3(req);
|
|
4863
5640
|
return json(res, 200, await service.connect({
|
|
4864
5641
|
siteUrl: string(input, "siteUrl"),
|
|
4865
5642
|
email: string(input, "email"),
|
|
@@ -4873,7 +5650,7 @@ function registerJiraRoute(ctx, service) {
|
|
|
4873
5650
|
}
|
|
4874
5651
|
if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
|
|
4875
5652
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4876
|
-
const input = await readJson$
|
|
5653
|
+
const input = await readJson$3(req);
|
|
4877
5654
|
await service.assignToMe(string(input, "key"));
|
|
4878
5655
|
return json(res, 200, { assigned: true });
|
|
4879
5656
|
}
|
|
@@ -4971,12 +5748,12 @@ function askArguments(preferences) {
|
|
|
4971
5748
|
...READ_ONLY_TOOLS
|
|
4972
5749
|
];
|
|
4973
5750
|
}
|
|
4974
|
-
function record$
|
|
5751
|
+
function record$3(value) {
|
|
4975
5752
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4976
5753
|
}
|
|
4977
5754
|
/** One-line description of a tool call, mirroring the main window's step titles. */
|
|
4978
5755
|
function toolSummary(input) {
|
|
4979
|
-
const fields = record$
|
|
5756
|
+
const fields = record$3(input);
|
|
4980
5757
|
if (fields === void 0) return void 0;
|
|
4981
5758
|
const candidate = [
|
|
4982
5759
|
fields.command,
|
|
@@ -4990,7 +5767,7 @@ function toolSummary(input) {
|
|
|
4990
5767
|
function eventsOfStreamLine(line) {
|
|
4991
5768
|
let parsed;
|
|
4992
5769
|
try {
|
|
4993
|
-
parsed = record$
|
|
5770
|
+
parsed = record$3(JSON.parse(line));
|
|
4994
5771
|
} catch {
|
|
4995
5772
|
return [];
|
|
4996
5773
|
}
|
|
@@ -5000,9 +5777,9 @@ function eventsOfStreamLine(line) {
|
|
|
5000
5777
|
text: "ready"
|
|
5001
5778
|
}];
|
|
5002
5779
|
if (parsed.type === "stream_event") {
|
|
5003
|
-
const event = record$
|
|
5780
|
+
const event = record$3(parsed.event);
|
|
5004
5781
|
if (event?.type === "content_block_start") {
|
|
5005
|
-
const block = record$
|
|
5782
|
+
const block = record$3(event.content_block);
|
|
5006
5783
|
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
|
|
5007
5784
|
type: "tool",
|
|
5008
5785
|
id: block.id,
|
|
@@ -5011,7 +5788,7 @@ function eventsOfStreamLine(line) {
|
|
|
5011
5788
|
}];
|
|
5012
5789
|
return [];
|
|
5013
5790
|
}
|
|
5014
|
-
const delta = record$
|
|
5791
|
+
const delta = record$3(event?.delta);
|
|
5015
5792
|
if (event?.type !== "content_block_delta" || delta === void 0) return [];
|
|
5016
5793
|
if (delta.type === "text_delta" && typeof delta.text === "string") return [{
|
|
5017
5794
|
type: "text",
|
|
@@ -5024,11 +5801,11 @@ function eventsOfStreamLine(line) {
|
|
|
5024
5801
|
return [];
|
|
5025
5802
|
}
|
|
5026
5803
|
if (parsed.type === "assistant" || parsed.type === "user") {
|
|
5027
|
-
const content = record$
|
|
5804
|
+
const content = record$3(parsed.message)?.content;
|
|
5028
5805
|
if (!Array.isArray(content)) return [];
|
|
5029
5806
|
const events = [];
|
|
5030
5807
|
for (const item of content) {
|
|
5031
|
-
const block = record$
|
|
5808
|
+
const block = record$3(item);
|
|
5032
5809
|
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
5033
5810
|
const summary = toolSummary(block.input);
|
|
5034
5811
|
events.push({
|
|
@@ -5120,21 +5897,21 @@ var AskService = class {
|
|
|
5120
5897
|
};
|
|
5121
5898
|
//#endregion
|
|
5122
5899
|
//#region src/ask-routes.ts
|
|
5123
|
-
const MAX_BODY_BYTES$
|
|
5124
|
-
const MAX_SESSION_ID_CHARS$
|
|
5125
|
-
function record$
|
|
5900
|
+
const MAX_BODY_BYTES$2 = 131072;
|
|
5901
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
5902
|
+
function record$2(value) {
|
|
5126
5903
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5127
5904
|
}
|
|
5128
|
-
async function readJson$
|
|
5905
|
+
async function readJson$2(req) {
|
|
5129
5906
|
const chunks = [];
|
|
5130
5907
|
let size = 0;
|
|
5131
5908
|
for await (const chunk of req) {
|
|
5132
5909
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5133
5910
|
size += buffer.length;
|
|
5134
|
-
if (size > MAX_BODY_BYTES$
|
|
5911
|
+
if (size > MAX_BODY_BYTES$2) throw new AskError("body-too-large", "The request body is too large.");
|
|
5135
5912
|
chunks.push(buffer);
|
|
5136
5913
|
}
|
|
5137
|
-
const value = record$
|
|
5914
|
+
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5138
5915
|
if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
5139
5916
|
return value;
|
|
5140
5917
|
}
|
|
@@ -5162,12 +5939,12 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5162
5939
|
let sessionId;
|
|
5163
5940
|
try {
|
|
5164
5941
|
const value = url.searchParams.get("sessionId");
|
|
5165
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
5942
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
|
|
5166
5943
|
sessionId = value;
|
|
5167
5944
|
const resolved = cwdForSession(sessionId);
|
|
5168
5945
|
if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
|
|
5169
5946
|
cwd = resolved;
|
|
5170
|
-
request = askRequest(await readJson$
|
|
5947
|
+
request = askRequest(await readJson$2(req));
|
|
5171
5948
|
} catch (error) {
|
|
5172
5949
|
if (error instanceof AskError) return json(res, 409, {
|
|
5173
5950
|
error: error.code,
|
|
@@ -5211,27 +5988,27 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5211
5988
|
}
|
|
5212
5989
|
//#endregion
|
|
5213
5990
|
//#region src/review-comment-routes.ts
|
|
5214
|
-
const MAX_BODY_BYTES = 16384;
|
|
5215
|
-
const MAX_SESSION_ID_CHARS = 1024;
|
|
5216
|
-
function record(value) {
|
|
5991
|
+
const MAX_BODY_BYTES$1 = 16384;
|
|
5992
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
5993
|
+
function record$1(value) {
|
|
5217
5994
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5218
5995
|
}
|
|
5219
|
-
async function readJson(req) {
|
|
5996
|
+
async function readJson$1(req) {
|
|
5220
5997
|
const chunks = [];
|
|
5221
5998
|
let size = 0;
|
|
5222
5999
|
for await (const chunk of req) {
|
|
5223
6000
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5224
6001
|
size += buffer.length;
|
|
5225
|
-
if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
6002
|
+
if (size > MAX_BODY_BYTES$1) throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
5226
6003
|
chunks.push(buffer);
|
|
5227
6004
|
}
|
|
5228
|
-
const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
6005
|
+
const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5229
6006
|
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
5230
6007
|
return value;
|
|
5231
6008
|
}
|
|
5232
6009
|
function sessionIdFromUrl(url) {
|
|
5233
6010
|
const value = url.searchParams.get("sessionId");
|
|
5234
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
|
|
6011
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new ReviewCommentError("invalid-session", "The session is invalid.");
|
|
5235
6012
|
return value;
|
|
5236
6013
|
}
|
|
5237
6014
|
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
@@ -5245,7 +6022,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
5245
6022
|
const sessionId = sessionIdFromUrl(url);
|
|
5246
6023
|
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
5247
6024
|
if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
|
|
5248
|
-
const input = await readJson(req);
|
|
6025
|
+
const input = await readJson$1(req);
|
|
5249
6026
|
return json(res, 200, { comment: store.add(sessionId, {
|
|
5250
6027
|
path: input.path,
|
|
5251
6028
|
line: input.line,
|
|
@@ -5256,7 +6033,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
5256
6033
|
}
|
|
5257
6034
|
if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
|
|
5258
6035
|
if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
|
|
5259
|
-
const input = await readJson(req);
|
|
6036
|
+
const input = await readJson$1(req);
|
|
5260
6037
|
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
5261
6038
|
return json(res, 200, { removed: store.remove(sessionId, input.id) });
|
|
5262
6039
|
}
|
|
@@ -5276,6 +6053,99 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
5276
6053
|
}), "dsh-claude: review comment route");
|
|
5277
6054
|
}
|
|
5278
6055
|
//#endregion
|
|
6056
|
+
//#region src/client-diagnostics-routes.ts
|
|
6057
|
+
/** Enough for a message plus a trimmed stack; the client caps its own volume. */
|
|
6058
|
+
const MAX_DIAGNOSTIC_BYTES = 8192;
|
|
6059
|
+
const MAX_DETAIL_CHARS = 2e3;
|
|
6060
|
+
const MAX_KIND_CHARS = 60;
|
|
6061
|
+
async function readBody(req) {
|
|
6062
|
+
const chunks = [];
|
|
6063
|
+
let size = 0;
|
|
6064
|
+
for await (const chunk of req) {
|
|
6065
|
+
const buffer = chunk;
|
|
6066
|
+
size += buffer.length;
|
|
6067
|
+
if (size > MAX_DIAGNOSTIC_BYTES) throw new Error("diagnostic too large");
|
|
6068
|
+
chunks.push(buffer);
|
|
6069
|
+
}
|
|
6070
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
6071
|
+
}
|
|
6072
|
+
/** `POST <path>` with `{ kind, detail }`: write one renderer finding to the Host log.
|
|
6073
|
+
*
|
|
6074
|
+
* The renderer has no other way to speak. A Slot entry that throws is caught
|
|
6075
|
+
* by the Host's Slot system and dropped, the shipped Desktop opens no
|
|
6076
|
+
* DevTools, and startup still reports `rendererStatus: "healthy"` — so a
|
|
6077
|
+
* plugin whose UI died silently is indistinguishable from a working one.
|
|
6078
|
+
* Every finding here is data written by this package's own client half, but
|
|
6079
|
+
* it is still bounded and redacted like any other untrusted input. */
|
|
6080
|
+
function registerClaudeClientDiagnosticsRoute(ctx) {
|
|
6081
|
+
ctx.effect(() => ctx.webServer.register({
|
|
6082
|
+
kind: "exact",
|
|
6083
|
+
path: CLAUDE_CLIENT_DIAGNOSTICS_PATH,
|
|
6084
|
+
handler: async (req, res) => {
|
|
6085
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
6086
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
6087
|
+
try {
|
|
6088
|
+
const body = await readBody(req);
|
|
6089
|
+
const kind = typeof body?.kind === "string" ? body.kind.slice(0, MAX_KIND_CHARS) : "unknown";
|
|
6090
|
+
const detail = typeof body?.detail === "string" ? body.detail : "";
|
|
6091
|
+
if (detail === "") return json(res, 400, { error: "invalid-request" });
|
|
6092
|
+
ctx.logger.warn(`dsh-claude client [${kind}]: ${redactText(detail, MAX_DETAIL_CHARS)}`);
|
|
6093
|
+
return json(res, 200, { ok: true });
|
|
6094
|
+
} catch (error) {
|
|
6095
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
6096
|
+
return json(res, 400, { error: "invalid-request" });
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
}), "dsh-claude: client diagnostics route");
|
|
6100
|
+
}
|
|
6101
|
+
//#endregion
|
|
6102
|
+
//#region src/rewind-routes.ts
|
|
6103
|
+
const MAX_BODY_BYTES = 4096;
|
|
6104
|
+
const MAX_SESSION_ID_CHARS = 1024;
|
|
6105
|
+
function record(value) {
|
|
6106
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6107
|
+
}
|
|
6108
|
+
async function readJson(req) {
|
|
6109
|
+
const chunks = [];
|
|
6110
|
+
let size = 0;
|
|
6111
|
+
for await (const chunk of req) {
|
|
6112
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6113
|
+
size += buffer.length;
|
|
6114
|
+
if (size > MAX_BODY_BYTES) return void 0;
|
|
6115
|
+
chunks.push(buffer);
|
|
6116
|
+
}
|
|
6117
|
+
return record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
6118
|
+
}
|
|
6119
|
+
/** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
|
|
6120
|
+
* later one, and arm Claude to resume before the turn it opened. */
|
|
6121
|
+
function registerClaudeRewindRoute(ctx, sidecar, access) {
|
|
6122
|
+
ctx.effect(() => ctx.webServer.register({
|
|
6123
|
+
kind: "exact",
|
|
6124
|
+
path: CLAUDE_REWIND_PATH,
|
|
6125
|
+
handler: async (req, res) => {
|
|
6126
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
6127
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
6128
|
+
try {
|
|
6129
|
+
const input = await readJson(req);
|
|
6130
|
+
const sessionId = input?.sessionId;
|
|
6131
|
+
const seq = input?.seq;
|
|
6132
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return json(res, 400, { error: "invalid-request" });
|
|
6133
|
+
const events = access.eventsFor(sessionId);
|
|
6134
|
+
if (events === void 0) return json(res, 409, { error: "session-unavailable" });
|
|
6135
|
+
if (access.busy(sessionId)) return json(res, 409, { error: "session-busy" });
|
|
6136
|
+
const planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
|
|
6137
|
+
if (planned === void 0) return json(res, 409, { error: "seq-unavailable" });
|
|
6138
|
+
await sidecar.writeRewind(sessionId, planned);
|
|
6139
|
+
await access.reset(sessionId);
|
|
6140
|
+
return json(res, 200, { ranges: planned.ranges });
|
|
6141
|
+
} catch (error) {
|
|
6142
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
6143
|
+
return json(res, 500, { error: "rewind-unavailable" });
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
}), "dsh-claude: rewind route");
|
|
6147
|
+
}
|
|
6148
|
+
//#endregion
|
|
5279
6149
|
//#region src/update-routes.ts
|
|
5280
6150
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
5281
6151
|
const UPDATE_TIMEOUT_MS = 3e4;
|
|
@@ -6099,6 +6969,7 @@ async function apply(ctx, config) {
|
|
|
6099
6969
|
ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
|
|
6100
6970
|
ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
|
|
6101
6971
|
ctx.inject(["webServer"], (webCtx) => {
|
|
6972
|
+
registerClaudeClientDiagnosticsRoute(webCtx);
|
|
6102
6973
|
registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
|
|
6103
6974
|
const desktopActions = webCtx.get("desktopActions");
|
|
6104
6975
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
@@ -6117,6 +6988,7 @@ async function apply(ctx, config) {
|
|
|
6117
6988
|
return agent.session.header.cwd;
|
|
6118
6989
|
};
|
|
6119
6990
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
6991
|
+
registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
|
|
6120
6992
|
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
6121
6993
|
registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
6122
6994
|
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|
|
@@ -6130,6 +7002,14 @@ async function apply(ctx, config) {
|
|
|
6130
7002
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|
|
6131
7003
|
};
|
|
6132
7004
|
registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
|
|
7005
|
+
registerClaudeRewindRoute(webCtx, sidecar, {
|
|
7006
|
+
eventsFor: (sessionId) => {
|
|
7007
|
+
const agent = webCtx.agents.get(sessionId);
|
|
7008
|
+
return agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude" ? void 0 : agent.session.events;
|
|
7009
|
+
},
|
|
7010
|
+
busy: (sessionId) => supervisor.snapshots().some((item) => item.sessionId === sessionId && (item.state === "running" || item.state === "interrupting")),
|
|
7011
|
+
reset: (sessionId) => supervisor.disposeSession(sessionId)
|
|
7012
|
+
});
|
|
6133
7013
|
registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
|
|
6134
7014
|
registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
|
|
6135
7015
|
const agent = webCtx.agents.get(sessionId);
|