@norman-else/dsh-claude 0.1.19 → 0.1.21
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/README.md +38 -147
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +232 -188
- package/lib/client.js +3060 -442
- package/lib/client.js.map +1 -1
- package/lib/command-bridge-C10-lz6A.mjs +71 -0
- package/lib/command-bridge-C10-lz6A.mjs.map +1 -0
- package/lib/{events-DPJaBReT.mjs → events-BdDs9ebF.mjs} +6 -2
- package/lib/events-BdDs9ebF.mjs.map +1 -0
- package/lib/index.d.mts +36 -1
- package/lib/index.mjs +1221 -262
- package/lib/index.mjs.map +1 -1
- package/lib/{preset-installer-DnoSEKGm.mjs → preset-installer-DMANIjwu.mjs} +2 -2
- package/lib/{preset-installer-DnoSEKGm.mjs.map → preset-installer-DMANIjwu.mjs.map} +1 -1
- package/lib/preset-route.mjs +172 -2
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/events-DPJaBReT.mjs.map +0 -1
- package/lib/presenters-DBRIOgJs.mjs +0 -257
- package/lib/presenters-DBRIOgJs.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { S as
|
|
2
|
-
import {
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { S as CLAUDE_UPDATE_PATH, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REVIEW_COMMENT_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_ACTION_PATH, w as TASK_TOOL_NAMES, x as CLAUDE_UPDATE_CHECK_PATH, y as CLAUDE_REPOSITORY_SETUP_PATH } from "./events-BdDs9ebF.mjs";
|
|
2
|
+
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-C10-lz6A.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DMANIjwu.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
-
import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
12
11
|
import { EventEmitter } from "node:events";
|
|
13
12
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
13
|
+
import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
//#region src/sidecar.ts
|
|
16
16
|
const SIDECAR_SCHEMA_VERSION = 1;
|
|
17
17
|
const MAX_ACTIVITIES = 1e4;
|
|
18
|
+
/** Trailing window that coalesces per-token transcript persistence into one
|
|
19
|
+
* atomic disk write; live subscribers are notified synchronously regardless. */
|
|
20
|
+
const TEXT_FLUSH_MS = 150;
|
|
18
21
|
function emptyProjection() {
|
|
19
22
|
return {
|
|
20
23
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
@@ -22,18 +25,18 @@ function emptyProjection() {
|
|
|
22
25
|
activities: []
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
|
-
function record$
|
|
28
|
+
function record$5(value) {
|
|
26
29
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
27
30
|
}
|
|
28
31
|
function finiteInteger(value) {
|
|
29
32
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
30
33
|
}
|
|
31
|
-
function string$
|
|
34
|
+
function string$3(value, max) {
|
|
32
35
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
33
36
|
}
|
|
34
37
|
function binding(value) {
|
|
35
|
-
const input = record$
|
|
36
|
-
if (input === void 0 || !string$
|
|
38
|
+
const input = record$5(value);
|
|
39
|
+
if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
|
|
37
40
|
return {
|
|
38
41
|
claudeSessionId: input.claudeSessionId,
|
|
39
42
|
sdkVersion: input.sdkVersion,
|
|
@@ -42,6 +45,7 @@ function binding(value) {
|
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
|
|
48
|
+
"text",
|
|
45
49
|
"status",
|
|
46
50
|
"thinking",
|
|
47
51
|
"tool-call",
|
|
@@ -61,22 +65,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
61
65
|
"failed"
|
|
62
66
|
]);
|
|
63
67
|
function activity(value) {
|
|
64
|
-
const input = record$
|
|
68
|
+
const input = record$5(value);
|
|
65
69
|
if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
|
|
66
70
|
return normalizeActivity(input);
|
|
67
71
|
}
|
|
68
72
|
function contextUsage(value) {
|
|
69
|
-
const input = record$
|
|
73
|
+
const input = record$5(value);
|
|
70
74
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
71
75
|
return normalizeContextUsage(input);
|
|
72
76
|
}
|
|
73
77
|
function tasks(value) {
|
|
74
|
-
const input = record$
|
|
78
|
+
const input = record$5(value);
|
|
75
79
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
76
80
|
return normalizeTasksEvent(input.tasks);
|
|
77
81
|
}
|
|
78
82
|
function parseClaudeSidecar(value) {
|
|
79
|
-
const input = record$
|
|
83
|
+
const input = record$5(value);
|
|
80
84
|
if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
|
|
81
85
|
const activities = input.activities.map(activity);
|
|
82
86
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
@@ -116,13 +120,126 @@ var ClaudeSidecarRepository = class {
|
|
|
116
120
|
root;
|
|
117
121
|
legacyRoot;
|
|
118
122
|
#pending = /* @__PURE__ */ new Map();
|
|
123
|
+
/** Latest durable projection per session; disk is read once and written through. */
|
|
124
|
+
#latest = /* @__PURE__ */ new Map();
|
|
125
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
126
|
+
/** Streaming transcript segments not yet persisted, keyed by activity key. */
|
|
127
|
+
#live = /* @__PURE__ */ new Map();
|
|
128
|
+
/** Monotonic revision boost so merged reads advance while text stays in memory. */
|
|
129
|
+
#boost = /* @__PURE__ */ new Map();
|
|
130
|
+
#flushTimers = /* @__PURE__ */ new Map();
|
|
119
131
|
constructor(options = {}) {
|
|
120
132
|
this.root = options.root ?? dshHomePath("plugins", "dsh-claude", "sessions");
|
|
121
133
|
this.legacyRoot = options.legacyRoot ?? (options.root === void 0 ? dshHomePath("plugins", "dsh-claude-code", "sessions") : void 0);
|
|
122
134
|
}
|
|
123
135
|
async read(sessionId) {
|
|
124
136
|
await this.#pending.get(sessionId)?.catch(() => void 0);
|
|
125
|
-
return this.#
|
|
137
|
+
return this.#merged(sessionId, await this.#base(sessionId));
|
|
138
|
+
}
|
|
139
|
+
/** Observe accepted changes for one session; returns the unsubscriber. */
|
|
140
|
+
subscribe(sessionId, listener) {
|
|
141
|
+
let set = this.#listeners.get(sessionId);
|
|
142
|
+
if (set === void 0) {
|
|
143
|
+
set = /* @__PURE__ */ new Set();
|
|
144
|
+
this.#listeners.set(sessionId, set);
|
|
145
|
+
}
|
|
146
|
+
set.add(listener);
|
|
147
|
+
return () => {
|
|
148
|
+
set.delete(listener);
|
|
149
|
+
if (set.size === 0) this.#listeners.delete(sessionId);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Record streaming assistant prose without touching the disk on the hot
|
|
153
|
+
* path: subscribers are notified synchronously (as an append when the
|
|
154
|
+
* redacted text grows in place) and persistence is coalesced. */
|
|
155
|
+
appendTranscriptText(sessionId, value) {
|
|
156
|
+
const normalized = normalizeActivity({
|
|
157
|
+
kind: "text",
|
|
158
|
+
phase: "updated",
|
|
159
|
+
...value
|
|
160
|
+
});
|
|
161
|
+
const key = activityKey(normalized);
|
|
162
|
+
let overlay = this.#live.get(sessionId);
|
|
163
|
+
if (overlay === void 0) {
|
|
164
|
+
overlay = /* @__PURE__ */ new Map();
|
|
165
|
+
this.#live.set(sessionId, overlay);
|
|
166
|
+
}
|
|
167
|
+
const previous = overlay.get(key);
|
|
168
|
+
overlay.set(key, normalized);
|
|
169
|
+
this.#boost.set(sessionId, (this.#boost.get(sessionId) ?? 0) + 1);
|
|
170
|
+
const text = normalized.text ?? "";
|
|
171
|
+
const base = {
|
|
172
|
+
turn: normalized.turn,
|
|
173
|
+
step: normalized.step,
|
|
174
|
+
ordinal: normalized.ordinal
|
|
175
|
+
};
|
|
176
|
+
this.#notify(sessionId, previous?.text !== void 0 && text.startsWith(previous.text) ? {
|
|
177
|
+
kind: "text",
|
|
178
|
+
...base,
|
|
179
|
+
append: text.slice(previous.text.length)
|
|
180
|
+
} : {
|
|
181
|
+
kind: "text",
|
|
182
|
+
...base,
|
|
183
|
+
text
|
|
184
|
+
});
|
|
185
|
+
this.#scheduleTextFlush(sessionId);
|
|
186
|
+
}
|
|
187
|
+
/** Persist any pending streaming transcript now (segment close, turn end). */
|
|
188
|
+
flushTranscriptText(sessionId) {
|
|
189
|
+
const timer = this.#flushTimers.get(sessionId);
|
|
190
|
+
if (timer !== void 0) {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
this.#flushTimers.delete(sessionId);
|
|
193
|
+
}
|
|
194
|
+
return this.#flushLive(sessionId);
|
|
195
|
+
}
|
|
196
|
+
#notify(sessionId, delta) {
|
|
197
|
+
const set = this.#listeners.get(sessionId);
|
|
198
|
+
if (set === void 0) return;
|
|
199
|
+
for (const listener of [...set]) try {
|
|
200
|
+
listener(delta);
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
#merged(sessionId, base) {
|
|
204
|
+
const overlay = this.#live.get(sessionId);
|
|
205
|
+
const boost = this.#boost.get(sessionId) ?? 0;
|
|
206
|
+
if ((overlay === void 0 || overlay.size === 0) && boost === 0) return base;
|
|
207
|
+
return {
|
|
208
|
+
...base,
|
|
209
|
+
revision: base.revision + boost,
|
|
210
|
+
...overlay === void 0 || overlay.size === 0 ? {} : { activities: mergeActivities(base.activities, [...overlay.values()]) }
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async #base(sessionId) {
|
|
214
|
+
const cached = this.#latest.get(sessionId);
|
|
215
|
+
if (cached !== void 0) return cached;
|
|
216
|
+
const loaded = await this.#readNow(sessionId);
|
|
217
|
+
this.#latest.set(sessionId, loaded);
|
|
218
|
+
return loaded;
|
|
219
|
+
}
|
|
220
|
+
#scheduleTextFlush(sessionId) {
|
|
221
|
+
if (this.#flushTimers.has(sessionId)) return;
|
|
222
|
+
const timer = setTimeout(() => {
|
|
223
|
+
this.#flushTimers.delete(sessionId);
|
|
224
|
+
this.#flushLive(sessionId);
|
|
225
|
+
}, TEXT_FLUSH_MS);
|
|
226
|
+
timer.unref?.();
|
|
227
|
+
this.#flushTimers.set(sessionId, timer);
|
|
228
|
+
}
|
|
229
|
+
async #flushLive(sessionId) {
|
|
230
|
+
const overlay = this.#live.get(sessionId);
|
|
231
|
+
if (overlay === void 0 || overlay.size === 0) return;
|
|
232
|
+
const entries = [...overlay.entries()];
|
|
233
|
+
try {
|
|
234
|
+
await this.#update(sessionId, (current) => ({
|
|
235
|
+
...current,
|
|
236
|
+
activities: mergeActivities(current.activities, entries.map(([, value]) => value))
|
|
237
|
+
}));
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const [key, value] of entries) if (overlay.get(key) === value) overlay.delete(key);
|
|
242
|
+
if (overlay.size === 0) this.#live.delete(sessionId);
|
|
126
243
|
}
|
|
127
244
|
writeBinding(sessionId, value) {
|
|
128
245
|
const normalized = normalizeBinding(value);
|
|
@@ -136,21 +253,30 @@ var ClaudeSidecarRepository = class {
|
|
|
136
253
|
return this.#update(sessionId, (current) => ({
|
|
137
254
|
...current,
|
|
138
255
|
activities: mergeActivities(current.activities, [normalized])
|
|
139
|
-
})
|
|
256
|
+
}), false, {
|
|
257
|
+
kind: "activity",
|
|
258
|
+
activity: normalized
|
|
259
|
+
});
|
|
140
260
|
}
|
|
141
261
|
writeContextUsage(sessionId, value) {
|
|
142
262
|
const normalized = normalizeContextUsage(value);
|
|
143
263
|
return this.#update(sessionId, (current) => ({
|
|
144
264
|
...current,
|
|
145
265
|
contextUsage: normalized
|
|
146
|
-
})
|
|
266
|
+
}), false, {
|
|
267
|
+
kind: "contextUsage",
|
|
268
|
+
value: normalized
|
|
269
|
+
});
|
|
147
270
|
}
|
|
148
271
|
writeTasks(sessionId, value) {
|
|
149
272
|
const normalized = normalizeTasksEvent(value);
|
|
150
273
|
return this.#update(sessionId, (current) => ({
|
|
151
274
|
...current,
|
|
152
275
|
tasks: normalized
|
|
153
|
-
})
|
|
276
|
+
}), false, {
|
|
277
|
+
kind: "tasks",
|
|
278
|
+
value: normalized
|
|
279
|
+
});
|
|
154
280
|
}
|
|
155
281
|
importLegacy(sessionId, events) {
|
|
156
282
|
const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
|
|
@@ -163,15 +289,15 @@ var ClaudeSidecarRepository = class {
|
|
|
163
289
|
...current.binding !== void 0 || importedBinding === void 0 ? {} : { binding: normalizeBinding(importedBinding) },
|
|
164
290
|
...current.contextUsage !== void 0 || importedUsage === void 0 ? {} : { contextUsage: normalizeContextUsage(importedUsage) },
|
|
165
291
|
...current.tasks !== void 0 || importedTasks === void 0 ? {} : { tasks: normalizeTasksEvent(importedTasks.tasks) }
|
|
166
|
-
}), true);
|
|
292
|
+
}), true, { kind: "sync" });
|
|
167
293
|
}
|
|
168
294
|
#path(sessionId, root = this.root) {
|
|
169
295
|
if (sessionId.length === 0 || sessionId.length > 1024) throw new Error("dsh-claude: invalid session id");
|
|
170
296
|
return join(root, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
171
297
|
}
|
|
172
|
-
#update(sessionId, change, skipUnchanged = false) {
|
|
298
|
+
#update(sessionId, change, skipUnchanged = false, delta) {
|
|
173
299
|
const operation = (this.#pending.get(sessionId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
174
|
-
const current = await this.#
|
|
300
|
+
const current = await this.#base(sessionId);
|
|
175
301
|
const changed = parseClaudeSidecar({
|
|
176
302
|
...change(current),
|
|
177
303
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
@@ -183,6 +309,8 @@ var ClaudeSidecarRepository = class {
|
|
|
183
309
|
revision: current.revision + 1
|
|
184
310
|
};
|
|
185
311
|
await this.#writeNow(sessionId, next);
|
|
312
|
+
this.#latest.set(sessionId, next);
|
|
313
|
+
if (delta !== void 0) this.#notify(sessionId, delta);
|
|
186
314
|
return next;
|
|
187
315
|
});
|
|
188
316
|
this.#pending.set(sessionId, operation);
|
|
@@ -525,10 +653,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
525
653
|
}
|
|
526
654
|
//#endregion
|
|
527
655
|
//#region src/sdk-messages.ts
|
|
528
|
-
function record$
|
|
656
|
+
function record$4(value) {
|
|
529
657
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
530
658
|
}
|
|
531
|
-
function string$
|
|
659
|
+
function string$2(value) {
|
|
532
660
|
return typeof value === "string" ? value : void 0;
|
|
533
661
|
}
|
|
534
662
|
function taskUsageOf(usage) {
|
|
@@ -540,7 +668,7 @@ function taskUsageOf(usage) {
|
|
|
540
668
|
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
541
669
|
}
|
|
542
670
|
function resultUsage(message) {
|
|
543
|
-
const usage = record$
|
|
671
|
+
const usage = record$4(message.usage);
|
|
544
672
|
const normalized = {};
|
|
545
673
|
if (usage !== void 0) {
|
|
546
674
|
if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
|
|
@@ -552,26 +680,26 @@ function resultUsage(message) {
|
|
|
552
680
|
return normalized;
|
|
553
681
|
}
|
|
554
682
|
function normalizeAssistant(message) {
|
|
555
|
-
const content = record$
|
|
683
|
+
const content = record$4(message.message)?.content;
|
|
556
684
|
if (!Array.isArray(content)) return [{
|
|
557
685
|
kind: "protocol-error",
|
|
558
686
|
title: "Malformed Claude assistant message",
|
|
559
687
|
detail: message
|
|
560
688
|
}];
|
|
561
|
-
const parentToolUseId = string$
|
|
689
|
+
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
562
690
|
const normalized = [];
|
|
563
691
|
for (const item of content) {
|
|
564
|
-
const block = record$
|
|
692
|
+
const block = record$4(item);
|
|
565
693
|
if (block === void 0) continue;
|
|
566
694
|
if (block.type === "text") {
|
|
567
|
-
const text = string$
|
|
695
|
+
const text = string$2(block.text);
|
|
568
696
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
569
697
|
kind: "assistant-text",
|
|
570
698
|
text,
|
|
571
699
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
572
700
|
});
|
|
573
701
|
} else if (block.type === "thinking") {
|
|
574
|
-
const text = string$
|
|
702
|
+
const text = string$2(block.thinking);
|
|
575
703
|
if (text !== void 0 && text.length > 0) normalized.push({
|
|
576
704
|
kind: "thinking",
|
|
577
705
|
text,
|
|
@@ -579,8 +707,8 @@ function normalizeAssistant(message) {
|
|
|
579
707
|
...parentToolUseId === void 0 ? {} : { parentToolUseId }
|
|
580
708
|
});
|
|
581
709
|
} else if (block.type === "tool_use") {
|
|
582
|
-
const toolUseId = string$
|
|
583
|
-
const toolName = string$
|
|
710
|
+
const toolUseId = string$2(block.id);
|
|
711
|
+
const toolName = string$2(block.name);
|
|
584
712
|
if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
|
|
585
713
|
kind: "tool-call",
|
|
586
714
|
toolUseId,
|
|
@@ -593,18 +721,20 @@ function normalizeAssistant(message) {
|
|
|
593
721
|
return normalized;
|
|
594
722
|
}
|
|
595
723
|
function normalizeUser(message) {
|
|
596
|
-
|
|
724
|
+
if (message.isReplay === true) return [];
|
|
725
|
+
const content = record$4(message.message)?.content;
|
|
726
|
+
if (typeof content === "string") return [];
|
|
597
727
|
if (!Array.isArray(content)) return [{
|
|
598
728
|
kind: "protocol-error",
|
|
599
729
|
title: "Malformed Claude user message",
|
|
600
730
|
detail: message
|
|
601
731
|
}];
|
|
602
|
-
const parentToolUseId = string$
|
|
732
|
+
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
603
733
|
const normalized = [];
|
|
604
734
|
for (const item of content) {
|
|
605
|
-
const block = record$
|
|
735
|
+
const block = record$4(item);
|
|
606
736
|
if (block?.type !== "tool_result") continue;
|
|
607
|
-
const toolUseId = string$
|
|
737
|
+
const toolUseId = string$2(block.tool_use_id);
|
|
608
738
|
if (toolUseId === void 0) continue;
|
|
609
739
|
normalized.push({
|
|
610
740
|
kind: "tool-result",
|
|
@@ -617,11 +747,11 @@ function normalizeUser(message) {
|
|
|
617
747
|
return normalized;
|
|
618
748
|
}
|
|
619
749
|
function normalizeSystem(message) {
|
|
620
|
-
const subtype = string$
|
|
750
|
+
const subtype = string$2(message.subtype);
|
|
621
751
|
if (subtype === "init") {
|
|
622
|
-
const sessionId = string$
|
|
623
|
-
const cliVersion = string$
|
|
624
|
-
const cwd = string$
|
|
752
|
+
const sessionId = string$2(message.session_id);
|
|
753
|
+
const cliVersion = string$2(message.claude_code_version);
|
|
754
|
+
const cwd = string$2(message.cwd);
|
|
625
755
|
return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
|
|
626
756
|
kind: "init",
|
|
627
757
|
sessionId,
|
|
@@ -650,20 +780,20 @@ function normalizeSystem(message) {
|
|
|
650
780
|
title: `Claude session ${String(message.state)}`
|
|
651
781
|
}];
|
|
652
782
|
if (subtype === "permission_denied") {
|
|
653
|
-
const toolUseId = string$
|
|
654
|
-
const toolName = string$
|
|
783
|
+
const toolUseId = string$2(message.tool_use_id);
|
|
784
|
+
const toolName = string$2(message.tool_name);
|
|
655
785
|
if (toolUseId !== void 0 && toolName !== void 0) return [{
|
|
656
786
|
kind: "permission-denied",
|
|
657
787
|
toolUseId,
|
|
658
788
|
toolName,
|
|
659
|
-
summary: string$
|
|
789
|
+
summary: string$2(message.message) ?? "Claude Code denied the action"
|
|
660
790
|
}];
|
|
661
791
|
}
|
|
662
792
|
if (subtype === "task_started") {
|
|
663
|
-
const taskId = string$
|
|
664
|
-
const description = string$
|
|
665
|
-
const subagentType = string$
|
|
666
|
-
const taskType = string$
|
|
793
|
+
const taskId = string$2(message.task_id);
|
|
794
|
+
const description = string$2(message.description);
|
|
795
|
+
const subagentType = string$2(message.subagent_type);
|
|
796
|
+
const taskType = string$2(message.task_type);
|
|
667
797
|
return [{
|
|
668
798
|
kind: "subagent",
|
|
669
799
|
title: description ?? taskId ?? "Claude subagent started",
|
|
@@ -678,12 +808,12 @@ function normalizeSystem(message) {
|
|
|
678
808
|
}];
|
|
679
809
|
}
|
|
680
810
|
if (subtype === "task_progress") {
|
|
681
|
-
const taskId = string$
|
|
682
|
-
const description = string$
|
|
683
|
-
const summary = string$
|
|
684
|
-
const subagentType = string$
|
|
685
|
-
const lastToolName = string$
|
|
686
|
-
const usage = taskUsageOf(record$
|
|
811
|
+
const taskId = string$2(message.task_id);
|
|
812
|
+
const description = string$2(message.description);
|
|
813
|
+
const summary = string$2(message.summary);
|
|
814
|
+
const subagentType = string$2(message.subagent_type);
|
|
815
|
+
const lastToolName = string$2(message.last_tool_name);
|
|
816
|
+
const usage = taskUsageOf(record$4(message.usage));
|
|
687
817
|
return [{
|
|
688
818
|
kind: "subagent",
|
|
689
819
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -699,11 +829,11 @@ function normalizeSystem(message) {
|
|
|
699
829
|
}];
|
|
700
830
|
}
|
|
701
831
|
if (subtype === "task_updated") {
|
|
702
|
-
const patch = record$
|
|
703
|
-
const status = string$
|
|
704
|
-
const taskId = string$
|
|
705
|
-
const description = string$
|
|
706
|
-
const error = string$
|
|
832
|
+
const patch = record$4(message.patch);
|
|
833
|
+
const status = string$2(patch?.status);
|
|
834
|
+
const taskId = string$2(message.task_id);
|
|
835
|
+
const description = string$2(patch?.description);
|
|
836
|
+
const error = string$2(patch?.error);
|
|
707
837
|
const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
|
|
708
838
|
return [{
|
|
709
839
|
kind: "subagent",
|
|
@@ -719,10 +849,10 @@ function normalizeSystem(message) {
|
|
|
719
849
|
if (subtype === "task_notification") {
|
|
720
850
|
const failed = message.status === "failed";
|
|
721
851
|
const stopped = message.status === "stopped" || message.status === "cancelled";
|
|
722
|
-
const taskId = string$
|
|
723
|
-
const summary = string$
|
|
852
|
+
const taskId = string$2(message.task_id);
|
|
853
|
+
const summary = string$2(message.summary);
|
|
724
854
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
725
|
-
const usage = taskUsageOf(record$
|
|
855
|
+
const usage = taskUsageOf(record$4(message.usage));
|
|
726
856
|
return [{
|
|
727
857
|
kind: "subagent",
|
|
728
858
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -737,10 +867,10 @@ function normalizeSystem(message) {
|
|
|
737
867
|
if (subtype === "background_tasks_changed") return [{
|
|
738
868
|
kind: "background-tasks",
|
|
739
869
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
740
|
-
const entry = record$
|
|
741
|
-
const taskId = string$
|
|
742
|
-
const description = string$
|
|
743
|
-
const taskType = string$
|
|
870
|
+
const entry = record$4(item);
|
|
871
|
+
const taskId = string$2(entry?.task_id);
|
|
872
|
+
const description = string$2(entry?.description);
|
|
873
|
+
const taskType = string$2(entry?.task_type);
|
|
744
874
|
if (taskId === void 0 || description === void 0) return [];
|
|
745
875
|
return [{
|
|
746
876
|
taskId,
|
|
@@ -756,7 +886,7 @@ function normalizeSystem(message) {
|
|
|
756
886
|
}];
|
|
757
887
|
if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
|
|
758
888
|
kind: message.level === "warning" ? "warning" : "status",
|
|
759
|
-
title: string$
|
|
889
|
+
title: string$2(message.content) ?? string$2(message.text) ?? "Claude Code notice",
|
|
760
890
|
detail: message
|
|
761
891
|
}];
|
|
762
892
|
if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
|
|
@@ -780,12 +910,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
780
910
|
function normalizeSdkMessage(message) {
|
|
781
911
|
const value = message;
|
|
782
912
|
if (value.type === "stream_event") {
|
|
783
|
-
const event = record$
|
|
784
|
-
const parentToolUseId = string$
|
|
913
|
+
const event = record$4(value.event);
|
|
914
|
+
const parentToolUseId = string$2(value.parent_tool_use_id);
|
|
785
915
|
if (event?.type === "content_block_delta") {
|
|
786
|
-
const delta = record$
|
|
916
|
+
const delta = record$4(event.delta);
|
|
787
917
|
if (delta?.type === "text_delta") {
|
|
788
|
-
const text = string$
|
|
918
|
+
const text = string$2(delta.text);
|
|
789
919
|
return text === void 0 ? [] : [{
|
|
790
920
|
kind: "text-delta",
|
|
791
921
|
text,
|
|
@@ -793,7 +923,7 @@ function normalizeSdkMessage(message) {
|
|
|
793
923
|
}];
|
|
794
924
|
}
|
|
795
925
|
if (delta?.type === "thinking_delta") {
|
|
796
|
-
const text = string$
|
|
926
|
+
const text = string$2(delta.thinking);
|
|
797
927
|
return text === void 0 ? [] : [{
|
|
798
928
|
kind: "thinking",
|
|
799
929
|
text,
|
|
@@ -808,7 +938,7 @@ function normalizeSdkMessage(message) {
|
|
|
808
938
|
if (value.type === "user") return normalizeUser(value);
|
|
809
939
|
if (value.type === "system") return normalizeSystem(value);
|
|
810
940
|
if (value.type === "result") {
|
|
811
|
-
const sessionId = string$
|
|
941
|
+
const sessionId = string$2(value.session_id);
|
|
812
942
|
if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
|
|
813
943
|
kind: "protocol-error",
|
|
814
944
|
title: "Malformed Claude result message",
|
|
@@ -816,11 +946,11 @@ function normalizeSdkMessage(message) {
|
|
|
816
946
|
}];
|
|
817
947
|
const success = value.subtype === "success" && value.is_error !== true;
|
|
818
948
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
819
|
-
const terminalReason = string$
|
|
820
|
-
const userMessageUuid = string$
|
|
821
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$
|
|
822
|
-
const toolName = string$
|
|
823
|
-
const toolUseId = string$
|
|
949
|
+
const terminalReason = string$2(value.terminal_reason);
|
|
950
|
+
const userMessageUuid = string$2(value.user_message_uuid);
|
|
951
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$4(item)).filter((item) => item !== void 0).map((item) => {
|
|
952
|
+
const toolName = string$2(item.tool_name);
|
|
953
|
+
const toolUseId = string$2(item.tool_use_id);
|
|
824
954
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
825
955
|
toolName,
|
|
826
956
|
toolUseId
|
|
@@ -844,11 +974,10 @@ function normalizeSdkMessage(message) {
|
|
|
844
974
|
detail: value.error ?? value.output
|
|
845
975
|
}];
|
|
846
976
|
if (value.type === "rate_limit_event") {
|
|
847
|
-
const status = string$
|
|
848
|
-
const blocking = status !== void 0 && status !== "allowed";
|
|
977
|
+
const status = string$2(record$4(value.rate_limit_info)?.status);
|
|
849
978
|
return [{
|
|
850
|
-
kind:
|
|
851
|
-
title:
|
|
979
|
+
kind: "status",
|
|
980
|
+
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
852
981
|
detail: value.rate_limit_info
|
|
853
982
|
}];
|
|
854
983
|
}
|
|
@@ -1034,6 +1163,7 @@ function rootCallSummary(toolName, input) {
|
|
|
1034
1163
|
}
|
|
1035
1164
|
var ClaudeSupervisor = class {
|
|
1036
1165
|
#entries = /* @__PURE__ */ new Map();
|
|
1166
|
+
#interruptions = /* @__PURE__ */ new Map();
|
|
1037
1167
|
#runtime;
|
|
1038
1168
|
#approval;
|
|
1039
1169
|
#userQuestions;
|
|
@@ -1041,7 +1171,6 @@ var ClaudeSupervisor = class {
|
|
|
1041
1171
|
#queryFactory;
|
|
1042
1172
|
#runDetached;
|
|
1043
1173
|
#sidecar;
|
|
1044
|
-
#dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
|
|
1045
1174
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1046
1175
|
#disposed = false;
|
|
1047
1176
|
#admissionGate = Promise.resolve();
|
|
@@ -1082,6 +1211,8 @@ var ClaudeSupervisor = class {
|
|
|
1082
1211
|
return this.#contextWindows.get(model);
|
|
1083
1212
|
}
|
|
1084
1213
|
runTurn(request) {
|
|
1214
|
+
const interruption = this.#interruptions.get(request.agent.id);
|
|
1215
|
+
if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
|
|
1085
1216
|
const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
|
|
1086
1217
|
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
1087
1218
|
return operation;
|
|
@@ -1134,6 +1265,8 @@ var ClaudeSupervisor = class {
|
|
|
1134
1265
|
sawActivity: false,
|
|
1135
1266
|
sawTextDelta: false,
|
|
1136
1267
|
text: "",
|
|
1268
|
+
transcriptText: "",
|
|
1269
|
+
transcriptTextOrdinal: void 0,
|
|
1137
1270
|
thinking: "",
|
|
1138
1271
|
aborted: false,
|
|
1139
1272
|
deniedToolUseIds: /* @__PURE__ */ new Set(),
|
|
@@ -1172,7 +1305,7 @@ var ClaudeSupervisor = class {
|
|
|
1172
1305
|
}
|
|
1173
1306
|
if (request.signal !== void 0) {
|
|
1174
1307
|
const abortListener = () => {
|
|
1175
|
-
this.#
|
|
1308
|
+
this.#startInterrupt(entry);
|
|
1176
1309
|
};
|
|
1177
1310
|
active.abortListener = abortListener;
|
|
1178
1311
|
request.signal.addEventListener("abort", abortListener, { once: true });
|
|
@@ -1387,7 +1520,14 @@ var ClaudeSupervisor = class {
|
|
|
1387
1520
|
if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
|
|
1388
1521
|
if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
|
|
1389
1522
|
if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
|
|
1390
|
-
if (
|
|
1523
|
+
if (active.phase === "waiting-tasks") {
|
|
1524
|
+
await this.#completeProgressSegment(active, message);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) {
|
|
1528
|
+
if (active.phase === "follow-up") return;
|
|
1529
|
+
throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
|
|
1530
|
+
}
|
|
1391
1531
|
await this.#completeTurn(entry, active, message);
|
|
1392
1532
|
return;
|
|
1393
1533
|
}
|
|
@@ -1398,6 +1538,8 @@ var ClaudeSupervisor = class {
|
|
|
1398
1538
|
if (message.parentToolUseId !== void 0) return;
|
|
1399
1539
|
active.sawTextDelta = true;
|
|
1400
1540
|
active.text += message.text;
|
|
1541
|
+
active.transcriptText += message.text;
|
|
1542
|
+
await this.#upsertTranscriptText(active);
|
|
1401
1543
|
active.output.push({
|
|
1402
1544
|
type: "text-delta",
|
|
1403
1545
|
text: message.text
|
|
@@ -1408,6 +1550,8 @@ var ClaudeSupervisor = class {
|
|
|
1408
1550
|
if (!active.sawTextDelta) {
|
|
1409
1551
|
active.sawTextDelta = true;
|
|
1410
1552
|
active.text += message.text;
|
|
1553
|
+
active.transcriptText += message.text;
|
|
1554
|
+
await this.#upsertTranscriptText(active);
|
|
1411
1555
|
active.output.push({
|
|
1412
1556
|
type: "text-delta",
|
|
1413
1557
|
text: message.text
|
|
@@ -1429,6 +1573,7 @@ var ClaudeSupervisor = class {
|
|
|
1429
1573
|
});
|
|
1430
1574
|
return;
|
|
1431
1575
|
case "tool-call":
|
|
1576
|
+
if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
|
|
1432
1577
|
await this.#appendActivity(active, {
|
|
1433
1578
|
kind: message.parentToolUseId === void 0 ? "tool-call" : "subagent",
|
|
1434
1579
|
phase: "started",
|
|
@@ -1439,11 +1584,7 @@ var ClaudeSupervisor = class {
|
|
|
1439
1584
|
summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
|
|
1440
1585
|
detail: message.input
|
|
1441
1586
|
});
|
|
1442
|
-
if (message.parentToolUseId === void 0)
|
|
1443
|
-
active.callNames.set(message.toolUseId, message.toolName);
|
|
1444
|
-
this.#ensureDynamicPresenter(active.agent, message.toolName);
|
|
1445
|
-
if (!TASK_TOOL_NAMES.has(message.toolName)) await this.#appendNativeToolCall(active, message);
|
|
1446
|
-
}
|
|
1587
|
+
if (message.parentToolUseId === void 0) active.callNames.set(message.toolUseId, message.toolName);
|
|
1447
1588
|
return;
|
|
1448
1589
|
case "tool-result":
|
|
1449
1590
|
await this.#appendActivity(active, {
|
|
@@ -1455,7 +1596,6 @@ var ClaudeSupervisor = class {
|
|
|
1455
1596
|
detail: message.output,
|
|
1456
1597
|
isError: message.isError
|
|
1457
1598
|
});
|
|
1458
|
-
if (message.parentToolUseId === void 0 && !TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? "")) await this.#appendNativeToolResult(active, message);
|
|
1459
1599
|
return;
|
|
1460
1600
|
case "subagent":
|
|
1461
1601
|
await this.#appendActivity(active, {
|
|
@@ -1488,12 +1628,6 @@ var ClaudeSupervisor = class {
|
|
|
1488
1628
|
title: message.toolName,
|
|
1489
1629
|
summary: message.summary
|
|
1490
1630
|
});
|
|
1491
|
-
if (!TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? message.toolName)) await this.#appendNativeToolResult(active, {
|
|
1492
|
-
kind: "tool-result",
|
|
1493
|
-
toolUseId: message.toolUseId,
|
|
1494
|
-
output: message.summary,
|
|
1495
|
-
isError: true
|
|
1496
|
-
});
|
|
1497
1631
|
return;
|
|
1498
1632
|
}
|
|
1499
1633
|
}
|
|
@@ -1571,6 +1705,7 @@ var ClaudeSupervisor = class {
|
|
|
1571
1705
|
active.promptUuid = randomUUID();
|
|
1572
1706
|
active.sawTextDelta = false;
|
|
1573
1707
|
active.text = "";
|
|
1708
|
+
this.#closeTranscriptTextSegment(active);
|
|
1574
1709
|
active.thinking = "";
|
|
1575
1710
|
await this.#appendSafely(active, {
|
|
1576
1711
|
kind: "status",
|
|
@@ -1604,9 +1739,44 @@ var ClaudeSupervisor = class {
|
|
|
1604
1739
|
entry.taskSnapshotAt = Date.now();
|
|
1605
1740
|
await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
|
|
1606
1741
|
}
|
|
1742
|
+
async #completeProgressSegment(active, result, recordUsage = true) {
|
|
1743
|
+
if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
|
|
1744
|
+
await this.#appendSafely(active, {
|
|
1745
|
+
kind: "usage",
|
|
1746
|
+
phase: "completed",
|
|
1747
|
+
title: "Claude usage",
|
|
1748
|
+
summary: usageSummary(result.usage),
|
|
1749
|
+
usage: result.usage
|
|
1750
|
+
});
|
|
1751
|
+
active.output.push({
|
|
1752
|
+
type: "usage",
|
|
1753
|
+
usage: result.usage
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1757
|
+
active.text = result.text;
|
|
1758
|
+
active.transcriptText = result.text;
|
|
1759
|
+
await this.#upsertTranscriptText(active);
|
|
1760
|
+
active.output.push({
|
|
1761
|
+
type: "text-delta",
|
|
1762
|
+
text: result.text
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
await this.#upsertTranscriptText(active);
|
|
1766
|
+
active.output.push({
|
|
1767
|
+
type: "segment-complete",
|
|
1768
|
+
text: active.text
|
|
1769
|
+
});
|
|
1770
|
+
active.sawTextDelta = false;
|
|
1771
|
+
active.text = "";
|
|
1772
|
+
this.#closeTranscriptTextSegment(active);
|
|
1773
|
+
active.thinking = "";
|
|
1774
|
+
}
|
|
1607
1775
|
async #completeTurn(entry, active, result) {
|
|
1608
1776
|
if (entry.active !== active) return;
|
|
1609
1777
|
if (active.aborted) {
|
|
1778
|
+
await this.#upsertTranscriptText(active);
|
|
1779
|
+
await this.#flushTranscript(active);
|
|
1610
1780
|
await this.#appendSafely(active, {
|
|
1611
1781
|
kind: "status",
|
|
1612
1782
|
phase: "failed",
|
|
@@ -1639,6 +1809,17 @@ var ClaudeSupervisor = class {
|
|
|
1639
1809
|
summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
|
|
1640
1810
|
});
|
|
1641
1811
|
if (!result.success) {
|
|
1812
|
+
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1813
|
+
active.text = result.text;
|
|
1814
|
+
active.transcriptText = result.text;
|
|
1815
|
+
await this.#upsertTranscriptText(active);
|
|
1816
|
+
active.output.push({
|
|
1817
|
+
type: "text-delta",
|
|
1818
|
+
text: result.text
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
await this.#upsertTranscriptText(active);
|
|
1822
|
+
await this.#flushTranscript(active);
|
|
1642
1823
|
const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
|
|
1643
1824
|
await this.#appendSafely(active, {
|
|
1644
1825
|
kind: "error",
|
|
@@ -1651,11 +1832,14 @@ var ClaudeSupervisor = class {
|
|
|
1651
1832
|
} else {
|
|
1652
1833
|
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
1653
1834
|
active.text = result.text;
|
|
1835
|
+
active.transcriptText = result.text;
|
|
1836
|
+
await this.#upsertTranscriptText(active);
|
|
1654
1837
|
active.output.push({
|
|
1655
1838
|
type: "text-delta",
|
|
1656
1839
|
text: result.text
|
|
1657
1840
|
});
|
|
1658
1841
|
}
|
|
1842
|
+
await this.#upsertTranscriptText(active);
|
|
1659
1843
|
if (active.phase === "primary" && this.#hasRunningTasks(entry, active)) {
|
|
1660
1844
|
active.phase = "waiting-tasks";
|
|
1661
1845
|
await this.#appendSafely(active, {
|
|
@@ -1663,10 +1847,7 @@ var ClaudeSupervisor = class {
|
|
|
1663
1847
|
phase: "updated",
|
|
1664
1848
|
title: "Claude Code is waiting for background tasks"
|
|
1665
1849
|
});
|
|
1666
|
-
active
|
|
1667
|
-
type: "segment-complete",
|
|
1668
|
-
text: active.text
|
|
1669
|
-
});
|
|
1850
|
+
await this.#completeProgressSegment(active, result, false);
|
|
1670
1851
|
return;
|
|
1671
1852
|
}
|
|
1672
1853
|
await this.#appendSafely(active, {
|
|
@@ -1674,6 +1855,7 @@ var ClaudeSupervisor = class {
|
|
|
1674
1855
|
phase: "completed",
|
|
1675
1856
|
title: "Claude Code turn completed"
|
|
1676
1857
|
});
|
|
1858
|
+
await this.#flushTranscript(active);
|
|
1677
1859
|
active.output.push({
|
|
1678
1860
|
type: "complete",
|
|
1679
1861
|
text: active.text
|
|
@@ -1686,6 +1868,27 @@ var ClaudeSupervisor = class {
|
|
|
1686
1868
|
entry.lastUsedAt = Date.now();
|
|
1687
1869
|
this.#armIdleTimer(entry);
|
|
1688
1870
|
}
|
|
1871
|
+
async #upsertTranscriptText(active) {
|
|
1872
|
+
if (active.transcriptText.length === 0) return;
|
|
1873
|
+
const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
|
|
1874
|
+
active.transcriptTextOrdinal = ordinal;
|
|
1875
|
+
try {
|
|
1876
|
+
this.#sidecar.appendTranscriptText(active.agent.id, {
|
|
1877
|
+
text: active.transcriptText,
|
|
1878
|
+
turn: active.cursor.turn,
|
|
1879
|
+
step: active.cursor.step,
|
|
1880
|
+
ordinal
|
|
1881
|
+
});
|
|
1882
|
+
} catch {}
|
|
1883
|
+
}
|
|
1884
|
+
async #flushTranscript(active) {
|
|
1885
|
+
await this.#sidecar.flushTranscriptText(active.agent.id).catch(() => void 0);
|
|
1886
|
+
}
|
|
1887
|
+
#closeTranscriptTextSegment(active) {
|
|
1888
|
+
this.#flushTranscript(active);
|
|
1889
|
+
active.transcriptText = "";
|
|
1890
|
+
active.transcriptTextOrdinal = void 0;
|
|
1891
|
+
}
|
|
1689
1892
|
async #appendActivity(active, activity) {
|
|
1690
1893
|
const ordinal = active.cursor.nextOrdinal++;
|
|
1691
1894
|
await this.#sidecar.appendActivity(active.agent.id, {
|
|
@@ -1700,53 +1903,14 @@ var ClaudeSupervisor = class {
|
|
|
1700
1903
|
async #appendSafely(active, activity) {
|
|
1701
1904
|
await this.#appendActivity(active, activity).catch(() => void 0);
|
|
1702
1905
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
known = /* @__PURE__ */ new Set();
|
|
1712
|
-
this.#dynamicPresenterNames.set(agent, known);
|
|
1713
|
-
}
|
|
1714
|
-
if (known.has(name)) return;
|
|
1715
|
-
try {
|
|
1716
|
-
agent.ctx.tools.register(dynamicPresenterDefinition(name));
|
|
1717
|
-
known.add(name);
|
|
1718
|
-
} catch {}
|
|
1719
|
-
}
|
|
1720
|
-
/** Mirror one root Claude tool call into the durable native tool channel so
|
|
1721
|
-
* the host's tool presentation renders it exactly like a DSH-executed call.
|
|
1722
|
-
* Presentation duplication is best-effort and never unsettles the turn. */
|
|
1723
|
-
async #appendNativeToolCall(active, message) {
|
|
1724
|
-
try {
|
|
1725
|
-
await active.agent.session.append("tool/call", {
|
|
1726
|
-
turn: active.cursor.turn,
|
|
1727
|
-
step: active.cursor.step,
|
|
1728
|
-
callId: CallId(message.toolUseId),
|
|
1729
|
-
name: message.toolName,
|
|
1730
|
-
arguments: safeDetail(message.input) ?? "{}"
|
|
1731
|
-
});
|
|
1732
|
-
} catch {}
|
|
1733
|
-
}
|
|
1734
|
-
async #appendNativeToolResult(active, message) {
|
|
1735
|
-
const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
|
|
1736
|
-
try {
|
|
1737
|
-
await active.agent.session.append("tool/result", {
|
|
1738
|
-
turn: active.cursor.turn,
|
|
1739
|
-
step: active.cursor.step,
|
|
1740
|
-
message: createToolResultMessage({
|
|
1741
|
-
callId: CallId(message.toolUseId),
|
|
1742
|
-
content: [{
|
|
1743
|
-
type: "text",
|
|
1744
|
-
text
|
|
1745
|
-
}],
|
|
1746
|
-
isError: message.isError
|
|
1747
|
-
})
|
|
1748
|
-
}, { surfaceOp: "append" });
|
|
1749
|
-
} catch {}
|
|
1906
|
+
#startInterrupt(entry) {
|
|
1907
|
+
const existing = this.#interruptions.get(entry.sessionId);
|
|
1908
|
+
if (existing !== void 0) return existing;
|
|
1909
|
+
const interruption = this.#interrupt(entry).finally(() => {
|
|
1910
|
+
this.#interruptions.delete(entry.sessionId);
|
|
1911
|
+
});
|
|
1912
|
+
this.#interruptions.set(entry.sessionId, interruption);
|
|
1913
|
+
return interruption;
|
|
1750
1914
|
}
|
|
1751
1915
|
async #interrupt(entry) {
|
|
1752
1916
|
const active = entry.active;
|
|
@@ -1754,6 +1918,7 @@ var ClaudeSupervisor = class {
|
|
|
1754
1918
|
entry.state = "interrupting";
|
|
1755
1919
|
active.aborted = true;
|
|
1756
1920
|
active.output.fail(abortFailure());
|
|
1921
|
+
await this.#upsertTranscriptText(active);
|
|
1757
1922
|
let interruptError;
|
|
1758
1923
|
try {
|
|
1759
1924
|
if (((await withTimeout(entry.query.interrupt(), 5e3, "Claude Code interrupt"))?.still_queued ?? []).includes(active.promptUuid)) throw new Error(`Claude Code interrupt left submitted prompt ${active.promptUuid} queued`);
|
|
@@ -1775,6 +1940,8 @@ var ClaudeSupervisor = class {
|
|
|
1775
1940
|
const active = entry.active;
|
|
1776
1941
|
const stderr = entry.process?.stderrTail();
|
|
1777
1942
|
if (active !== void 0) {
|
|
1943
|
+
await this.#upsertTranscriptText(active);
|
|
1944
|
+
await this.#flushTranscript(active);
|
|
1778
1945
|
if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
|
|
1779
1946
|
const unknown = active.sawActivity;
|
|
1780
1947
|
entry.state = unknown ? "outcome-unknown" : "disconnected";
|
|
@@ -1830,6 +1997,79 @@ var ClaudeSupervisor = class {
|
|
|
1830
1997
|
}
|
|
1831
1998
|
};
|
|
1832
1999
|
//#endregion
|
|
2000
|
+
//#region src/review-comments.ts
|
|
2001
|
+
const MAX_COMMENTS_PER_SESSION = 50;
|
|
2002
|
+
const MAX_TEXT_CHARS$1 = 2e3;
|
|
2003
|
+
const MAX_PATH_CHARS$1 = 1024;
|
|
2004
|
+
const MAX_LINE = 1e7;
|
|
2005
|
+
var ReviewCommentError = class extends Error {
|
|
2006
|
+
code;
|
|
2007
|
+
constructor(code, message) {
|
|
2008
|
+
super(message);
|
|
2009
|
+
this.name = "ReviewCommentError";
|
|
2010
|
+
this.code = code;
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
function validSide(value) {
|
|
2014
|
+
return value === "old" || value === "new";
|
|
2015
|
+
}
|
|
2016
|
+
/** Pending line comments queued per session until the next user turn drains them. */
|
|
2017
|
+
var ReviewCommentStore = class {
|
|
2018
|
+
#comments = /* @__PURE__ */ new Map();
|
|
2019
|
+
add(sessionId, input) {
|
|
2020
|
+
const path = typeof input.path === "string" ? input.path.trim() : "";
|
|
2021
|
+
const text = typeof input.text === "string" ? input.text.trim() : "";
|
|
2022
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS$1 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
|
|
2023
|
+
if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
|
|
2024
|
+
if (typeof input.line !== "number" || !Number.isSafeInteger(input.line) || input.line < 1 || input.line > MAX_LINE) throw new ReviewCommentError("invalid-request", "The comment line is invalid.");
|
|
2025
|
+
if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
|
|
2026
|
+
const existing = this.#comments.get(sessionId) ?? [];
|
|
2027
|
+
if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
|
|
2028
|
+
const comment = {
|
|
2029
|
+
id: randomUUID(),
|
|
2030
|
+
path,
|
|
2031
|
+
line: input.line,
|
|
2032
|
+
side: input.side,
|
|
2033
|
+
text
|
|
2034
|
+
};
|
|
2035
|
+
this.#comments.set(sessionId, [...existing, comment]);
|
|
2036
|
+
return comment;
|
|
2037
|
+
}
|
|
2038
|
+
remove(sessionId, id) {
|
|
2039
|
+
const existing = this.#comments.get(sessionId);
|
|
2040
|
+
if (existing === void 0) return false;
|
|
2041
|
+
const next = existing.filter((comment) => comment.id !== id);
|
|
2042
|
+
if (next.length === existing.length) return false;
|
|
2043
|
+
if (next.length === 0) this.#comments.delete(sessionId);
|
|
2044
|
+
else this.#comments.set(sessionId, next);
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
list(sessionId) {
|
|
2048
|
+
return this.#comments.get(sessionId) ?? [];
|
|
2049
|
+
}
|
|
2050
|
+
/** Remove and return every pending comment; called when a user turn consumes them. */
|
|
2051
|
+
drain(sessionId) {
|
|
2052
|
+
const existing = this.#comments.get(sessionId) ?? [];
|
|
2053
|
+
this.#comments.delete(sessionId);
|
|
2054
|
+
return existing;
|
|
2055
|
+
}
|
|
2056
|
+
disposeSession(sessionId) {
|
|
2057
|
+
this.#comments.delete(sessionId);
|
|
2058
|
+
}
|
|
2059
|
+
dispose() {
|
|
2060
|
+
this.#comments.clear();
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
/** Render drained comments as the prompt block preceding the user's message text. */
|
|
2064
|
+
function formatReviewComments(comments) {
|
|
2065
|
+
return [
|
|
2066
|
+
"<user-review-comments>",
|
|
2067
|
+
"The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
|
|
2068
|
+
...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
|
|
2069
|
+
"</user-review-comments>"
|
|
2070
|
+
].join("\n");
|
|
2071
|
+
}
|
|
2072
|
+
//#endregion
|
|
1833
2073
|
//#region src/adapter.ts
|
|
1834
2074
|
const MODELS = [
|
|
1835
2075
|
{
|
|
@@ -1926,6 +2166,22 @@ function validateImageRef(ref, attachments, imageIndex) {
|
|
|
1926
2166
|
const maxDimension = "maxImageDimension" in limits && finiteNonNegative(limits.maxImageDimension) ? limits.maxImageDimension : void 0;
|
|
1927
2167
|
if (!finiteNonNegative(ref.width) || !finiteNonNegative(ref.height) || ref.width * ref.height > limits.maxImagePixels || maxDimension !== void 0 && (ref.width > maxDimension || ref.height > maxDimension)) throw new Error(`dsh-claude: image ${imageIndex} exceeds the configured dimension limit`);
|
|
1928
2168
|
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Prepend the session's pending diff-review comments to the outgoing user
|
|
2171
|
+
* prompt. The comments are drained once per turn: they are formatted into one
|
|
2172
|
+
* `<user-review-comments>` block placed ahead of the user's own text (or as a
|
|
2173
|
+
* leading text block for multi-part prompts) so Claude reads them in the same
|
|
2174
|
+
* turn that consumed them.
|
|
2175
|
+
*/
|
|
2176
|
+
function injectReviewComments(prompt, comments) {
|
|
2177
|
+
if (comments.length === 0) return prompt;
|
|
2178
|
+
const block = formatReviewComments(comments);
|
|
2179
|
+
if (typeof prompt === "string") return `${block}\n\n${prompt}`;
|
|
2180
|
+
return [{
|
|
2181
|
+
type: "text",
|
|
2182
|
+
text: block
|
|
2183
|
+
}, ...prompt];
|
|
2184
|
+
}
|
|
1929
2185
|
function imageBlock(data, mediaType) {
|
|
1930
2186
|
return {
|
|
1931
2187
|
type: "image",
|
|
@@ -2008,12 +2264,14 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2008
2264
|
#agents;
|
|
2009
2265
|
#attachments;
|
|
2010
2266
|
#presetIdFor;
|
|
2011
|
-
|
|
2267
|
+
#drainReviewComments;
|
|
2268
|
+
constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2012
2269
|
super();
|
|
2013
2270
|
this.#supervisor = supervisor;
|
|
2014
2271
|
this.#agents = agents;
|
|
2015
2272
|
this.#attachments = attachments;
|
|
2016
2273
|
this.#presetIdFor = presetIdFor;
|
|
2274
|
+
this.#drainReviewComments = drainReviewComments;
|
|
2017
2275
|
}
|
|
2018
2276
|
providerInfo(provider) {
|
|
2019
2277
|
return {
|
|
@@ -2080,42 +2338,20 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2080
2338
|
}
|
|
2081
2339
|
const events = await this.#supervisor.runTurn({
|
|
2082
2340
|
agent,
|
|
2083
|
-
prompt,
|
|
2341
|
+
prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
|
|
2084
2342
|
model: options.model,
|
|
2085
2343
|
...thinkingMode === void 0 ? {} : { thinkingMode },
|
|
2086
2344
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2087
2345
|
});
|
|
2088
|
-
let text = "";
|
|
2089
2346
|
let pendingUsage;
|
|
2090
2347
|
let completed = false;
|
|
2091
|
-
let blockIndex = 0;
|
|
2092
2348
|
try {
|
|
2093
|
-
for await (const event of events)
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
type: "block-start",
|
|
2099
|
-
index: blockIndex,
|
|
2100
|
-
blockType: "text"
|
|
2101
|
-
};
|
|
2102
|
-
yield {
|
|
2103
|
-
type: "text-delta",
|
|
2104
|
-
index: blockIndex,
|
|
2105
|
-
text
|
|
2106
|
-
};
|
|
2107
|
-
yield {
|
|
2108
|
-
type: "block-end",
|
|
2109
|
-
index: blockIndex,
|
|
2110
|
-
block: {
|
|
2111
|
-
type: "text",
|
|
2112
|
-
text
|
|
2113
|
-
}
|
|
2114
|
-
};
|
|
2115
|
-
blockIndex += 1;
|
|
2349
|
+
for await (const event of events) {
|
|
2350
|
+
if (event.type === "text-delta" || event.type === "segment-complete") continue;
|
|
2351
|
+
if (event.type === "usage") {
|
|
2352
|
+
pendingUsage = tokenUsage(event.usage);
|
|
2353
|
+
continue;
|
|
2116
2354
|
}
|
|
2117
|
-
text = "";
|
|
2118
|
-
if (event.type === "segment-complete") continue;
|
|
2119
2355
|
completed = true;
|
|
2120
2356
|
if (pendingUsage !== void 0) yield {
|
|
2121
2357
|
type: "usage",
|
|
@@ -2129,26 +2365,6 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2129
2365
|
} catch (error) {
|
|
2130
2366
|
if (error.name === "AbortError") {
|
|
2131
2367
|
completed = true;
|
|
2132
|
-
if (text.length > 0) {
|
|
2133
|
-
yield {
|
|
2134
|
-
type: "block-start",
|
|
2135
|
-
index: blockIndex,
|
|
2136
|
-
blockType: "text"
|
|
2137
|
-
};
|
|
2138
|
-
yield {
|
|
2139
|
-
type: "text-delta",
|
|
2140
|
-
index: blockIndex,
|
|
2141
|
-
text
|
|
2142
|
-
};
|
|
2143
|
-
yield {
|
|
2144
|
-
type: "block-end",
|
|
2145
|
-
index: blockIndex,
|
|
2146
|
-
block: {
|
|
2147
|
-
type: "text",
|
|
2148
|
-
text
|
|
2149
|
-
}
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
2368
|
yield {
|
|
2153
2369
|
type: "finish",
|
|
2154
2370
|
reason: {
|
|
@@ -2166,8 +2382,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2166
2382
|
if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
|
|
2167
2383
|
}
|
|
2168
2384
|
};
|
|
2169
|
-
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
|
|
2170
|
-
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
|
|
2385
|
+
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2386
|
+
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
|
|
2171
2387
|
}
|
|
2172
2388
|
//#endregion
|
|
2173
2389
|
//#region src/http.ts
|
|
@@ -2294,63 +2510,191 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2294
2510
|
}
|
|
2295
2511
|
//#endregion
|
|
2296
2512
|
//#region src/projection-routes.ts
|
|
2297
|
-
const MAX_SESSION_ID_CHARS = 1024;
|
|
2298
|
-
|
|
2513
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
2514
|
+
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2515
|
+
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2516
|
+
const META_REFRESH_MS = 5e3;
|
|
2517
|
+
function targetFromUrl(rawUrl) {
|
|
2299
2518
|
try {
|
|
2300
2519
|
const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
|
|
2301
2520
|
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
2302
2521
|
if (!pathname.startsWith(prefix)) return void 0;
|
|
2303
|
-
|
|
2522
|
+
let encoded = pathname.slice(prefix.length);
|
|
2523
|
+
const stream = encoded.endsWith("/stream");
|
|
2524
|
+
if (stream) encoded = encoded.slice(0, -7);
|
|
2304
2525
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2305
2526
|
const sessionId = decodeURIComponent(encoded);
|
|
2306
|
-
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS) return void 0;
|
|
2307
|
-
return
|
|
2527
|
+
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
|
|
2528
|
+
return {
|
|
2529
|
+
sessionId,
|
|
2530
|
+
stream
|
|
2531
|
+
};
|
|
2308
2532
|
} catch {
|
|
2309
2533
|
return;
|
|
2310
2534
|
}
|
|
2311
2535
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2536
|
+
function envelope(projection, meta) {
|
|
2537
|
+
return {
|
|
2538
|
+
schemaVersion: projection.schemaVersion,
|
|
2539
|
+
revision: projection.revision,
|
|
2540
|
+
owned: meta.owned,
|
|
2541
|
+
commands: meta.commands,
|
|
2542
|
+
activities: projection.activities,
|
|
2543
|
+
...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
|
|
2544
|
+
...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
|
|
2545
|
+
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
2546
|
+
reviewComments: meta.reviewComments
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
/** Register the browser-readable, credential-free sidecar projection endpoint.
|
|
2550
|
+
* `GET <path>/:sessionId` returns one snapshot; `GET <path>/:sessionId/stream`
|
|
2551
|
+
* returns an NDJSON stream: a full snapshot line followed by incremental
|
|
2552
|
+
* transcript/activity deltas and periodic metadata/heartbeat lines. */
|
|
2553
|
+
function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
|
|
2554
|
+
const info = (message) => {
|
|
2555
|
+
ctx.logger?.info?.(message);
|
|
2556
|
+
};
|
|
2557
|
+
const assembleMeta = async (sessionId) => {
|
|
2558
|
+
const owned = ownsSession(sessionId);
|
|
2559
|
+
const repository = owned ? await repositoryForSession(sessionId) : void 0;
|
|
2560
|
+
return {
|
|
2561
|
+
owned,
|
|
2562
|
+
commands: commandsForSession(sessionId),
|
|
2563
|
+
...repository === void 0 ? {} : { repository },
|
|
2564
|
+
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
2565
|
+
};
|
|
2566
|
+
};
|
|
2567
|
+
const streamProjection = async (res, sessionId) => {
|
|
2568
|
+
info(`dsh-claude: projection stream opened for ${sessionId.slice(0, 64)}`);
|
|
2569
|
+
let textDeltas = 0;
|
|
2570
|
+
let textBytes = 0;
|
|
2571
|
+
let textSince = Date.now();
|
|
2572
|
+
let meta = await assembleMeta(sessionId);
|
|
2573
|
+
res.writeHead(200, {
|
|
2574
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
2575
|
+
"cache-control": "no-store",
|
|
2576
|
+
"x-content-type-options": "nosniff"
|
|
2577
|
+
});
|
|
2578
|
+
res.flushHeaders?.();
|
|
2579
|
+
let closed = false;
|
|
2580
|
+
const writeLine = (value) => {
|
|
2581
|
+
if (closed) return;
|
|
2582
|
+
try {
|
|
2583
|
+
res.write(`${JSON.stringify(value)}\n`);
|
|
2584
|
+
} catch {
|
|
2585
|
+
closed = true;
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
const writeSnapshot = async () => {
|
|
2589
|
+
const projection = await sidecar.read(sessionId);
|
|
2590
|
+
writeLine({
|
|
2591
|
+
type: "snapshot",
|
|
2592
|
+
...envelope(projection, meta)
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2595
|
+
await writeSnapshot();
|
|
2596
|
+
const unsubscribe = sidecar.subscribe(sessionId, (delta) => {
|
|
2597
|
+
switch (delta.kind) {
|
|
2598
|
+
case "text":
|
|
2599
|
+
textDeltas += 1;
|
|
2600
|
+
textBytes += (delta.append ?? delta.text ?? "").length;
|
|
2601
|
+
if (textDeltas % 25 === 0) {
|
|
2602
|
+
const elapsed = Date.now() - textSince;
|
|
2603
|
+
info(`dsh-claude: stream ${sessionId.slice(0, 24)} 25 text deltas ${textBytes}B in ${elapsed}ms`);
|
|
2604
|
+
textBytes = 0;
|
|
2605
|
+
textSince = Date.now();
|
|
2606
|
+
}
|
|
2607
|
+
writeLine({
|
|
2608
|
+
type: "text",
|
|
2609
|
+
turn: delta.turn,
|
|
2610
|
+
step: delta.step,
|
|
2611
|
+
ordinal: delta.ordinal,
|
|
2612
|
+
...delta.append === void 0 ? {} : { append: delta.append },
|
|
2613
|
+
...delta.text === void 0 ? {} : { text: delta.text }
|
|
2614
|
+
});
|
|
2615
|
+
return;
|
|
2616
|
+
case "activity":
|
|
2617
|
+
writeLine({
|
|
2618
|
+
type: "activity",
|
|
2619
|
+
activity: delta.activity
|
|
2620
|
+
});
|
|
2621
|
+
return;
|
|
2622
|
+
case "contextUsage":
|
|
2623
|
+
writeLine({
|
|
2624
|
+
type: "contextUsage",
|
|
2625
|
+
value: delta.value
|
|
2626
|
+
});
|
|
2627
|
+
return;
|
|
2628
|
+
case "tasks":
|
|
2629
|
+
writeLine({
|
|
2630
|
+
type: "tasks",
|
|
2631
|
+
value: delta.value
|
|
2632
|
+
});
|
|
2633
|
+
return;
|
|
2634
|
+
case "sync": writeSnapshot().catch(() => void 0);
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
const timer = setInterval(() => {
|
|
2638
|
+
(async () => {
|
|
2639
|
+
const next = await assembleMeta(sessionId);
|
|
2640
|
+
if (closed) return;
|
|
2641
|
+
if (JSON.stringify(next) === JSON.stringify(meta)) {
|
|
2642
|
+
writeLine({ type: "ping" });
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
meta = next;
|
|
2646
|
+
writeLine({
|
|
2647
|
+
type: "meta",
|
|
2648
|
+
owned: meta.owned,
|
|
2649
|
+
commands: meta.commands,
|
|
2650
|
+
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
2651
|
+
reviewComments: meta.reviewComments
|
|
2652
|
+
});
|
|
2653
|
+
})().catch(() => void 0);
|
|
2654
|
+
}, META_REFRESH_MS);
|
|
2655
|
+
timer.unref?.();
|
|
2656
|
+
await new Promise((resolve) => {
|
|
2657
|
+
res.on("close", () => {
|
|
2658
|
+
closed = true;
|
|
2659
|
+
clearInterval(timer);
|
|
2660
|
+
unsubscribe();
|
|
2661
|
+
info(`dsh-claude: projection stream closed for ${sessionId.slice(0, 64)} after ${textDeltas} text deltas`);
|
|
2662
|
+
resolve();
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
};
|
|
2314
2666
|
ctx.effect(() => ctx.webServer.register({
|
|
2315
2667
|
kind: "prefix",
|
|
2316
2668
|
path: CLAUDE_PROJECTION_PATH,
|
|
2317
2669
|
handler: async (req, res) => {
|
|
2318
2670
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
2319
2671
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
2320
|
-
const
|
|
2321
|
-
if (
|
|
2672
|
+
const target = targetFromUrl(req.url);
|
|
2673
|
+
if (target === void 0) return json(res, 400, { error: "invalid session id" });
|
|
2322
2674
|
try {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
return json(res, 200, {
|
|
2327
|
-
schemaVersion: projection.schemaVersion,
|
|
2328
|
-
revision: projection.revision,
|
|
2329
|
-
owned,
|
|
2330
|
-
commands: commandsForSession(sessionId),
|
|
2331
|
-
activities: projection.activities,
|
|
2332
|
-
...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
|
|
2333
|
-
...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
|
|
2334
|
-
...repository === void 0 ? {} : { repository }
|
|
2335
|
-
});
|
|
2675
|
+
if (target.stream) return await streamProjection(res, target.sessionId);
|
|
2676
|
+
info(`dsh-claude: projection poll for ${target.sessionId.slice(0, 64)}`);
|
|
2677
|
+
return json(res, 200, envelope(await sidecar.read(target.sessionId), await assembleMeta(target.sessionId)));
|
|
2336
2678
|
} catch {
|
|
2337
|
-
return json(res, 500, { error: "projection unavailable" });
|
|
2679
|
+
if (!res.headersSent) return json(res, 500, { error: "projection unavailable" });
|
|
2680
|
+
res.end();
|
|
2338
2681
|
}
|
|
2339
2682
|
}
|
|
2340
2683
|
}), "dsh-claude: sidecar projection route");
|
|
2341
2684
|
}
|
|
2342
2685
|
//#endregion
|
|
2343
2686
|
//#region src/repository-status.ts
|
|
2344
|
-
const MAX_OUTPUT_BYTES$
|
|
2687
|
+
const MAX_OUTPUT_BYTES$2 = 65536;
|
|
2345
2688
|
const MAX_DIFF_BYTES = 262144;
|
|
2346
|
-
const
|
|
2689
|
+
const MAX_UNTRACKED_DIFFS = 50;
|
|
2690
|
+
const GIT_TIMEOUT_MS$2 = 5e3;
|
|
2347
2691
|
const GH_TIMEOUT_MS = 8e3;
|
|
2348
2692
|
const CACHE_TTL_MS = 5e3;
|
|
2349
2693
|
const MAX_TEXT_CHARS = 1024;
|
|
2350
2694
|
function bounded(value) {
|
|
2351
2695
|
return value.trim().slice(0, MAX_TEXT_CHARS);
|
|
2352
2696
|
}
|
|
2353
|
-
async function collect$
|
|
2697
|
+
async function collect$2(handle) {
|
|
2354
2698
|
const outcome = await handle.done;
|
|
2355
2699
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
2356
2700
|
return {
|
|
@@ -2359,15 +2703,15 @@ async function collect$1(handle) {
|
|
|
2359
2703
|
lossy: stdout?.lossy === true
|
|
2360
2704
|
};
|
|
2361
2705
|
}
|
|
2362
|
-
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
2706
|
+
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
|
|
2363
2707
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
2364
|
-
return collect$
|
|
2708
|
+
return collect$2(runtime.spawn({
|
|
2365
2709
|
argv: [executable, ...args],
|
|
2366
2710
|
cwd,
|
|
2367
2711
|
stdio: {
|
|
2368
2712
|
stdin: "ignore",
|
|
2369
2713
|
stdout: { maxBytes },
|
|
2370
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
2714
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
|
|
2371
2715
|
},
|
|
2372
2716
|
graceMs: 1e3,
|
|
2373
2717
|
signal,
|
|
@@ -2381,6 +2725,9 @@ function parseGitStatus(output) {
|
|
|
2381
2725
|
let branch;
|
|
2382
2726
|
let detached = false;
|
|
2383
2727
|
let dirty = false;
|
|
2728
|
+
let upstream = false;
|
|
2729
|
+
let ahead;
|
|
2730
|
+
let behind;
|
|
2384
2731
|
for (const line of output.split(/\r?\n/u)) {
|
|
2385
2732
|
if (line.startsWith("# branch.head ")) {
|
|
2386
2733
|
const head = bounded(line.slice(14));
|
|
@@ -2388,12 +2735,27 @@ function parseGitStatus(output) {
|
|
|
2388
2735
|
else if (head.length > 0 && head !== "(unknown)") branch = head;
|
|
2389
2736
|
continue;
|
|
2390
2737
|
}
|
|
2738
|
+
if (line.startsWith("# branch.upstream ")) {
|
|
2739
|
+
upstream = true;
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
if (line.startsWith("# branch.ab ")) {
|
|
2743
|
+
const counts = /^# branch\.ab \+(\d+) -(\d+)$/u.exec(line);
|
|
2744
|
+
if (counts !== null) {
|
|
2745
|
+
ahead = Number(counts[1]);
|
|
2746
|
+
behind = Number(counts[2]);
|
|
2747
|
+
}
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2391
2750
|
if (line.length > 0 && !line.startsWith("# ")) dirty = true;
|
|
2392
2751
|
}
|
|
2393
2752
|
return {
|
|
2394
2753
|
...branch === void 0 ? {} : { branch },
|
|
2395
2754
|
detached,
|
|
2396
|
-
dirty
|
|
2755
|
+
dirty,
|
|
2756
|
+
upstream,
|
|
2757
|
+
...ahead === void 0 ? {} : { ahead },
|
|
2758
|
+
...behind === void 0 ? {} : { behind }
|
|
2397
2759
|
};
|
|
2398
2760
|
}
|
|
2399
2761
|
function parseDiffNumstat(value) {
|
|
@@ -2419,14 +2781,14 @@ function parseGitHubRemote(value) {
|
|
|
2419
2781
|
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
2420
2782
|
return `${match[1]}/${match[2]}`;
|
|
2421
2783
|
}
|
|
2422
|
-
function record$
|
|
2784
|
+
function record$3(value) {
|
|
2423
2785
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2424
2786
|
}
|
|
2425
2787
|
function aggregateChecks(value) {
|
|
2426
2788
|
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
2427
2789
|
let pending = false;
|
|
2428
2790
|
for (const item of value) {
|
|
2429
|
-
const check = record$
|
|
2791
|
+
const check = record$3(item);
|
|
2430
2792
|
if (check === void 0) continue;
|
|
2431
2793
|
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
2432
2794
|
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
@@ -2450,7 +2812,7 @@ function reviewState(value) {
|
|
|
2450
2812
|
return "none";
|
|
2451
2813
|
}
|
|
2452
2814
|
function parsePullRequest(value) {
|
|
2453
|
-
const input = record$
|
|
2815
|
+
const input = record$3(value);
|
|
2454
2816
|
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
2455
2817
|
let url;
|
|
2456
2818
|
try {
|
|
@@ -2471,8 +2833,9 @@ function parsePullRequest(value) {
|
|
|
2471
2833
|
review: reviewState(input.reviewDecision),
|
|
2472
2834
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
2473
2835
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
2474
|
-
...typeof record$
|
|
2836
|
+
...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
|
|
2475
2837
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
2838
|
+
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
2476
2839
|
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
2477
2840
|
};
|
|
2478
2841
|
}
|
|
@@ -2498,6 +2861,10 @@ var RepositoryStatusService = class {
|
|
|
2498
2861
|
value.catch(() => this.#cache.delete(cwd));
|
|
2499
2862
|
return value;
|
|
2500
2863
|
}
|
|
2864
|
+
invalidate(cwd) {
|
|
2865
|
+
this.#cache.delete(cwd);
|
|
2866
|
+
this.#lastReady.delete(cwd);
|
|
2867
|
+
}
|
|
2501
2868
|
dispose() {
|
|
2502
2869
|
this.#cache.clear();
|
|
2503
2870
|
this.#lastReady.clear();
|
|
@@ -2539,7 +2906,7 @@ var RepositoryStatusService = class {
|
|
|
2539
2906
|
"--show-toplevel",
|
|
2540
2907
|
"--absolute-git-dir",
|
|
2541
2908
|
"--git-common-dir"
|
|
2542
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2909
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2543
2910
|
if (paths.exitCode !== 0) return {
|
|
2544
2911
|
status: "not-repository",
|
|
2545
2912
|
cwd: safeCwd
|
|
@@ -2556,8 +2923,8 @@ var RepositoryStatusService = class {
|
|
|
2556
2923
|
"status",
|
|
2557
2924
|
"--porcelain=v2",
|
|
2558
2925
|
"--branch",
|
|
2559
|
-
"--untracked-files=
|
|
2560
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2926
|
+
"--untracked-files=normal"
|
|
2927
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2561
2928
|
if (statusResult.exitCode !== 0) return {
|
|
2562
2929
|
status: "unavailable",
|
|
2563
2930
|
cwd: safeCwd
|
|
@@ -2567,7 +2934,7 @@ var RepositoryStatusService = class {
|
|
|
2567
2934
|
"remote",
|
|
2568
2935
|
"get-url",
|
|
2569
2936
|
"origin"
|
|
2570
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2937
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2571
2938
|
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
2572
2939
|
const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
|
|
2573
2940
|
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
@@ -2600,7 +2967,7 @@ var RepositoryStatusService = class {
|
|
|
2600
2967
|
"merge-base",
|
|
2601
2968
|
"HEAD",
|
|
2602
2969
|
`refs/remotes/origin/${baseBranch}`
|
|
2603
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2970
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2604
2971
|
if (result.exitCode !== 0 || result.lossy) return void 0;
|
|
2605
2972
|
const oid = bounded(result.stdout);
|
|
2606
2973
|
return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
|
|
@@ -2616,7 +2983,7 @@ var RepositoryStatusService = class {
|
|
|
2616
2983
|
"--numstat",
|
|
2617
2984
|
base,
|
|
2618
2985
|
"--"
|
|
2619
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2986
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
2620
2987
|
if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
|
|
2621
2988
|
const summary = parseDiffNumstat(numstat.stdout);
|
|
2622
2989
|
const patch = await run(this.#runtime, git, [
|
|
@@ -2626,20 +2993,70 @@ var RepositoryStatusService = class {
|
|
|
2626
2993
|
"--unified=3",
|
|
2627
2994
|
base,
|
|
2628
2995
|
"--"
|
|
2629
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2996
|
+
], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
|
|
2630
2997
|
if (patch.exitCode !== 0) return {
|
|
2631
2998
|
...summary,
|
|
2632
2999
|
truncated: true
|
|
2633
3000
|
};
|
|
3001
|
+
const untracked = await this.#untrackedDiff(cwd, git);
|
|
3002
|
+
const combinedPatch = `${patch.stdout}${untracked.patch}`;
|
|
2634
3003
|
return {
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3004
|
+
additions: summary.additions + untracked.additions,
|
|
3005
|
+
deletions: summary.deletions,
|
|
3006
|
+
files: summary.files + untracked.files,
|
|
3007
|
+
...patch.lossy ? {} : { patch: combinedPatch.slice(0, MAX_DIFF_BYTES) },
|
|
3008
|
+
truncated: patch.lossy || untracked.truncated || combinedPatch.length > MAX_DIFF_BYTES
|
|
2638
3009
|
};
|
|
2639
3010
|
} catch {
|
|
2640
3011
|
return;
|
|
2641
3012
|
}
|
|
2642
3013
|
}
|
|
3014
|
+
async #untrackedDiff(cwd, git) {
|
|
3015
|
+
const listed = await run(this.#runtime, git, [
|
|
3016
|
+
"ls-files",
|
|
3017
|
+
"--others",
|
|
3018
|
+
"--exclude-standard",
|
|
3019
|
+
"-z"
|
|
3020
|
+
], cwd, GIT_TIMEOUT_MS$2);
|
|
3021
|
+
if (listed.exitCode !== 0 || listed.lossy) return {
|
|
3022
|
+
additions: 0,
|
|
3023
|
+
files: 0,
|
|
3024
|
+
patch: "",
|
|
3025
|
+
truncated: listed.lossy
|
|
3026
|
+
};
|
|
3027
|
+
const paths = listed.stdout.split("\0").filter((path) => path.length > 0);
|
|
3028
|
+
let additions = 0;
|
|
3029
|
+
let patch = "";
|
|
3030
|
+
let truncated = paths.length > MAX_UNTRACKED_DIFFS;
|
|
3031
|
+
for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
|
|
3032
|
+
const result = await run(this.#runtime, git, [
|
|
3033
|
+
"diff",
|
|
3034
|
+
"--no-ext-diff",
|
|
3035
|
+
"--no-color",
|
|
3036
|
+
"--unified=3",
|
|
3037
|
+
"--numstat",
|
|
3038
|
+
"--patch",
|
|
3039
|
+
"--no-index",
|
|
3040
|
+
"--",
|
|
3041
|
+
"/dev/null",
|
|
3042
|
+
path
|
|
3043
|
+
], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
|
|
3044
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
3045
|
+
truncated = true;
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
const start = result.stdout.indexOf("diff --git ");
|
|
3049
|
+
additions += parseDiffNumstat(start >= 0 ? result.stdout.slice(0, start) : result.stdout).additions;
|
|
3050
|
+
if (result.lossy) truncated = true;
|
|
3051
|
+
else if (start >= 0) patch += result.stdout.slice(start);
|
|
3052
|
+
}
|
|
3053
|
+
return {
|
|
3054
|
+
additions,
|
|
3055
|
+
files: paths.length,
|
|
3056
|
+
patch,
|
|
3057
|
+
truncated
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
2643
3060
|
async #pullRequest(cwd, repository, branch) {
|
|
2644
3061
|
const gh = await this.#gh();
|
|
2645
3062
|
if (gh === void 0) return void 0;
|
|
@@ -2662,8 +3079,8 @@ var RepositoryStatusService = class {
|
|
|
2662
3079
|
};
|
|
2663
3080
|
//#endregion
|
|
2664
3081
|
//#region src/repository-setup.ts
|
|
2665
|
-
const MAX_OUTPUT_BYTES = 131072;
|
|
2666
|
-
const GIT_TIMEOUT_MS = 1e4;
|
|
3082
|
+
const MAX_OUTPUT_BYTES$1 = 131072;
|
|
3083
|
+
const GIT_TIMEOUT_MS$1 = 1e4;
|
|
2667
3084
|
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
2668
3085
|
const MAX_PATH_CHARS = 4096;
|
|
2669
3086
|
const MAX_BRANCH_CHARS = 512;
|
|
@@ -2676,7 +3093,7 @@ var RepositorySetupError = class extends Error {
|
|
|
2676
3093
|
this.code = code;
|
|
2677
3094
|
}
|
|
2678
3095
|
};
|
|
2679
|
-
async function collect(handle) {
|
|
3096
|
+
async function collect$1(handle) {
|
|
2680
3097
|
const outcome = await handle.done;
|
|
2681
3098
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
2682
3099
|
const stderr = handle.collected.stderr?.readFrom(0);
|
|
@@ -2969,14 +3386,14 @@ var RepositorySetupService = class {
|
|
|
2969
3386
|
this.#gitPath ??= this.#runtime.resolveExecutable("git");
|
|
2970
3387
|
return this.#gitPath;
|
|
2971
3388
|
}
|
|
2972
|
-
async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
2973
|
-
return collect(this.#runtime.spawn({
|
|
3389
|
+
async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
|
|
3390
|
+
return collect$1(this.#runtime.spawn({
|
|
2974
3391
|
argv: [executable, ...args],
|
|
2975
3392
|
cwd,
|
|
2976
3393
|
stdio: {
|
|
2977
3394
|
stdin: "ignore",
|
|
2978
|
-
stdout: { maxBytes: MAX_OUTPUT_BYTES },
|
|
2979
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES }
|
|
3395
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
|
|
3396
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
2980
3397
|
},
|
|
2981
3398
|
graceMs: 1e3,
|
|
2982
3399
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -3016,30 +3433,400 @@ var RepositorySetupService = class {
|
|
|
3016
3433
|
}
|
|
3017
3434
|
};
|
|
3018
3435
|
//#endregion
|
|
3436
|
+
//#region src/repository-actions.ts
|
|
3437
|
+
const MAX_OUTPUT_BYTES = 262144;
|
|
3438
|
+
const MAX_PATCH_CHARS = 65536;
|
|
3439
|
+
const MAX_MESSAGE_CHARS = 512;
|
|
3440
|
+
const MAX_PR_TEXT_CHARS = 8192;
|
|
3441
|
+
const MAX_UNPUSHED_COMMITS = 20;
|
|
3442
|
+
const GIT_TIMEOUT_MS = 15e3;
|
|
3443
|
+
const REMOTE_TIMEOUT_MS = 6e4;
|
|
3444
|
+
const GENERATE_TIMEOUT_MS = 6e4;
|
|
3445
|
+
var RepositoryActionError = class extends Error {
|
|
3446
|
+
code;
|
|
3447
|
+
commit;
|
|
3448
|
+
constructor(code, message, commit) {
|
|
3449
|
+
super(message);
|
|
3450
|
+
this.name = "RepositoryActionError";
|
|
3451
|
+
this.code = code;
|
|
3452
|
+
if (commit !== void 0) this.commit = commit;
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
async function collect(handle) {
|
|
3456
|
+
const outcome = await handle.done;
|
|
3457
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
3458
|
+
const stderr = handle.collected.stderr?.readFrom(0);
|
|
3459
|
+
return {
|
|
3460
|
+
exitCode: outcome.exitCode,
|
|
3461
|
+
stdout: stdout?.text ?? "",
|
|
3462
|
+
stderr: stderr?.text ?? "",
|
|
3463
|
+
lossy: stdout?.lossy === true || stderr?.lossy === true
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function safeText(value, maximum, label) {
|
|
3467
|
+
const text = value.trim();
|
|
3468
|
+
if (text.length === 0 || text.length > maximum || /[\0\r]/u.test(text)) throw new RepositoryActionError("invalid-request", `${label} is invalid.`);
|
|
3469
|
+
return text;
|
|
3470
|
+
}
|
|
3471
|
+
function isProtectedWarpPath(path) {
|
|
3472
|
+
return basename(path.replaceAll("\\", "/")).toLocaleLowerCase("en-US") === "warp.md";
|
|
3473
|
+
}
|
|
3474
|
+
function parseRepositoryActionStatus(output) {
|
|
3475
|
+
const files = /* @__PURE__ */ new Map();
|
|
3476
|
+
const records = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/u);
|
|
3477
|
+
for (let position = 0; position < records.length; position += 1) {
|
|
3478
|
+
const line = records[position] ?? "";
|
|
3479
|
+
if (line.length < 4) continue;
|
|
3480
|
+
const index = line[0] ?? " ";
|
|
3481
|
+
const worktree = line[1] ?? " ";
|
|
3482
|
+
let path = line.slice(3);
|
|
3483
|
+
const rename = path.lastIndexOf(" -> ");
|
|
3484
|
+
if (rename >= 0) path = path.slice(rename + 4);
|
|
3485
|
+
if (output.includes("\0") && (index === "R" || index === "C" || worktree === "R" || worktree === "C")) position += 1;
|
|
3486
|
+
if (path.length === 0 || path.includes("\0") || isProtectedWarpPath(path)) continue;
|
|
3487
|
+
files.set(path, {
|
|
3488
|
+
path,
|
|
3489
|
+
staged: index !== " " && index !== "?",
|
|
3490
|
+
unstaged: worktree !== " " && worktree !== "?",
|
|
3491
|
+
untracked: index === "?" && worktree === "?"
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
3495
|
+
}
|
|
3496
|
+
function fallbackCommitMessage(files) {
|
|
3497
|
+
if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
|
|
3498
|
+
return `Update ${files.length} repository files`;
|
|
3499
|
+
}
|
|
3500
|
+
function normalizedGeneratedMessage(value, fallback) {
|
|
3501
|
+
const first = value.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
|
|
3502
|
+
if (first === void 0) return fallback;
|
|
3503
|
+
const message = first.replace(/^['"`]+|['"`]+$/gu, "").replace(/[\0\r\n]/gu, " ").trim();
|
|
3504
|
+
return message.length === 0 || message.length > 72 ? fallback : message;
|
|
3505
|
+
}
|
|
3506
|
+
function validPrUrl(value) {
|
|
3507
|
+
try {
|
|
3508
|
+
const url = new URL(value.trim());
|
|
3509
|
+
return url.protocol === "https:" && url.hostname === "github.com" ? url.href : void 0;
|
|
3510
|
+
} catch {
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function validPullRequestBody(value) {
|
|
3515
|
+
const body = value.trim();
|
|
3516
|
+
const match = /^Summary:\s+([^\r\n]+)\r?\n\r?\nChanges:\s*\r?\n([\s\S]+)$/u.exec(body);
|
|
3517
|
+
const summary = match?.[1]?.trim();
|
|
3518
|
+
const changes = match?.[2]?.trim();
|
|
3519
|
+
if (summary === void 0 || summary.length === 0 || changes === void 0 || changes.length === 0) return false;
|
|
3520
|
+
return !/^#{1,6}\s|^[A-Za-z][A-Za-z ]+:\s*$/mu.test(changes);
|
|
3521
|
+
}
|
|
3522
|
+
var RepositoryActionService = class {
|
|
3523
|
+
#runtime;
|
|
3524
|
+
#claudeExecutable;
|
|
3525
|
+
#invalidate;
|
|
3526
|
+
#gitExecutable;
|
|
3527
|
+
#ghExecutable;
|
|
3528
|
+
#pending = Promise.resolve();
|
|
3529
|
+
constructor(runtime, claudeExecutable, invalidate = () => {}) {
|
|
3530
|
+
this.#runtime = runtime;
|
|
3531
|
+
this.#claudeExecutable = claudeExecutable;
|
|
3532
|
+
this.#invalidate = invalidate;
|
|
3533
|
+
}
|
|
3534
|
+
preview(cwd) {
|
|
3535
|
+
return this.#preview(cwd);
|
|
3536
|
+
}
|
|
3537
|
+
async generateMessage(cwd, fingerprint) {
|
|
3538
|
+
const preview = await this.#preview(cwd);
|
|
3539
|
+
if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
3540
|
+
const fallback = fallbackCommitMessage(preview.files);
|
|
3541
|
+
const prompt = [
|
|
3542
|
+
"Write one concise English git commit subject (imperative mood, maximum 72 characters).",
|
|
3543
|
+
"Return only the subject without quotes, markdown, body, or explanation.",
|
|
3544
|
+
`Files: ${preview.files.map((file) => file.path).join(", ")}`,
|
|
3545
|
+
`Diff:\n${preview.patch.slice(0, 24576)}`
|
|
3546
|
+
].join("\n");
|
|
3547
|
+
try {
|
|
3548
|
+
const result = await this.#run(this.#claudeExecutable, [
|
|
3549
|
+
"-p",
|
|
3550
|
+
"--tools",
|
|
3551
|
+
"",
|
|
3552
|
+
"--output-format",
|
|
3553
|
+
"text",
|
|
3554
|
+
prompt
|
|
3555
|
+
], preview.root, GENERATE_TIMEOUT_MS);
|
|
3556
|
+
return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
|
|
3557
|
+
} catch {
|
|
3558
|
+
return fallback;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
execute(cwd, request) {
|
|
3562
|
+
const operation = this.#pending.then(() => this.#execute(cwd, request));
|
|
3563
|
+
this.#pending = operation.then(() => void 0, () => void 0);
|
|
3564
|
+
return operation;
|
|
3565
|
+
}
|
|
3566
|
+
async #execute(cwd, request) {
|
|
3567
|
+
const before = await this.#preview(cwd);
|
|
3568
|
+
if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
3569
|
+
if (request.action === "push") {
|
|
3570
|
+
const git = await this.#git();
|
|
3571
|
+
try {
|
|
3572
|
+
await this.#push(git, before.root, before.branch);
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.");
|
|
3575
|
+
}
|
|
3576
|
+
this.#invalidate(before.root);
|
|
3577
|
+
return {
|
|
3578
|
+
commit: before.head,
|
|
3579
|
+
pushed: true
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
|
|
3583
|
+
if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
|
|
3584
|
+
const git = await this.#git();
|
|
3585
|
+
let oid = before.head;
|
|
3586
|
+
if (before.files.length > 0) {
|
|
3587
|
+
await this.#rejectStagedWarp(git, before.root);
|
|
3588
|
+
if (request.includeUnstaged) {
|
|
3589
|
+
const paths = before.files.filter((file) => file.unstaged || file.untracked).map((file) => file.path);
|
|
3590
|
+
if (paths.length > 0) await this.#mustRun(git, [
|
|
3591
|
+
"add",
|
|
3592
|
+
"--",
|
|
3593
|
+
...paths
|
|
3594
|
+
], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
|
|
3595
|
+
}
|
|
3596
|
+
await this.#rejectStagedWarp(git, before.root);
|
|
3597
|
+
const staged = await this.#run(git, [
|
|
3598
|
+
"diff",
|
|
3599
|
+
"--cached",
|
|
3600
|
+
"--quiet",
|
|
3601
|
+
"--exit-code",
|
|
3602
|
+
"--"
|
|
3603
|
+
], before.root, GIT_TIMEOUT_MS);
|
|
3604
|
+
if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
|
|
3605
|
+
if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
|
|
3606
|
+
await this.#mustRun(git, [
|
|
3607
|
+
"commit",
|
|
3608
|
+
"-m",
|
|
3609
|
+
message,
|
|
3610
|
+
"--"
|
|
3611
|
+
], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
|
|
3612
|
+
oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
|
|
3613
|
+
this.#invalidate(before.root);
|
|
3614
|
+
if (request.action === "commit") return {
|
|
3615
|
+
commit: oid,
|
|
3616
|
+
pushed: false
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
try {
|
|
3620
|
+
await this.#push(git, before.root, before.branch);
|
|
3621
|
+
} catch (error) {
|
|
3622
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", oid);
|
|
3623
|
+
}
|
|
3624
|
+
this.#invalidate(before.root);
|
|
3625
|
+
if (request.action === "commit-push") return {
|
|
3626
|
+
commit: oid,
|
|
3627
|
+
pushed: true
|
|
3628
|
+
};
|
|
3629
|
+
const title = safeText(request.prTitle ?? message, 256, "Pull request title");
|
|
3630
|
+
const body = safeText(request.prBody ?? "", MAX_PR_TEXT_CHARS, "Pull request description");
|
|
3631
|
+
if (!validPullRequestBody(body)) throw new RepositoryActionError("invalid-pr-description", "Pull request description must contain only Summary and Changes sections.", oid);
|
|
3632
|
+
let gh;
|
|
3633
|
+
try {
|
|
3634
|
+
gh = await this.#gh();
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.", oid);
|
|
3637
|
+
}
|
|
3638
|
+
const args = [
|
|
3639
|
+
"pr",
|
|
3640
|
+
"create",
|
|
3641
|
+
"--title",
|
|
3642
|
+
title,
|
|
3643
|
+
"--body",
|
|
3644
|
+
body
|
|
3645
|
+
];
|
|
3646
|
+
if (request.draft !== false) args.push("--draft");
|
|
3647
|
+
if (request.baseBranch !== void 0) args.push("--base", safeText(request.baseBranch, 512, "Base branch"));
|
|
3648
|
+
const created = await this.#run(gh, args, before.root, REMOTE_TIMEOUT_MS);
|
|
3649
|
+
const url = created.exitCode === 0 && !created.lossy ? validPrUrl(created.stdout) : void 0;
|
|
3650
|
+
if (url === void 0) throw new RepositoryActionError("pr-failed", "The pull request could not be created.", oid);
|
|
3651
|
+
return {
|
|
3652
|
+
commit: oid,
|
|
3653
|
+
pushed: true,
|
|
3654
|
+
pullRequestUrl: url
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
async #preview(cwd) {
|
|
3658
|
+
const git = await this.#git();
|
|
3659
|
+
const root = (await this.#mustRun(git, [
|
|
3660
|
+
"rev-parse",
|
|
3661
|
+
"--path-format=absolute",
|
|
3662
|
+
"--show-toplevel"
|
|
3663
|
+
], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
|
|
3664
|
+
const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
|
|
3665
|
+
this.#run(git, [
|
|
3666
|
+
"symbolic-ref",
|
|
3667
|
+
"--quiet",
|
|
3668
|
+
"--short",
|
|
3669
|
+
"HEAD"
|
|
3670
|
+
], root, GIT_TIMEOUT_MS),
|
|
3671
|
+
this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
|
|
3672
|
+
this.#run(git, [
|
|
3673
|
+
"status",
|
|
3674
|
+
"--porcelain=v1",
|
|
3675
|
+
"-z",
|
|
3676
|
+
"--untracked-files=all"
|
|
3677
|
+
], root, GIT_TIMEOUT_MS),
|
|
3678
|
+
this.#run(git, [
|
|
3679
|
+
"diff",
|
|
3680
|
+
"--cached",
|
|
3681
|
+
"--no-ext-diff",
|
|
3682
|
+
"--no-color",
|
|
3683
|
+
"--unified=3",
|
|
3684
|
+
"--",
|
|
3685
|
+
":(exclude)WARP.md",
|
|
3686
|
+
":(exclude)**/WARP.md"
|
|
3687
|
+
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
|
|
3688
|
+
this.#run(git, [
|
|
3689
|
+
"diff",
|
|
3690
|
+
"--no-ext-diff",
|
|
3691
|
+
"--no-color",
|
|
3692
|
+
"--unified=3",
|
|
3693
|
+
"--",
|
|
3694
|
+
":(exclude)WARP.md",
|
|
3695
|
+
":(exclude)**/WARP.md"
|
|
3696
|
+
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
|
|
3697
|
+
]);
|
|
3698
|
+
if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
|
|
3699
|
+
if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
3700
|
+
const files = parseRepositoryActionStatus(statusResult.stdout);
|
|
3701
|
+
const patch = `${stagedPatch.stdout}${stagedPatch.stdout.length > 0 && unstagedPatch.stdout.length > 0 ? "\n" : ""}${unstagedPatch.stdout}`.slice(0, MAX_PATCH_CHARS);
|
|
3702
|
+
const branch = branchResult.stdout.trim();
|
|
3703
|
+
const head = headResult.stdout.trim();
|
|
3704
|
+
const fingerprint = createHash("sha256").update([
|
|
3705
|
+
head,
|
|
3706
|
+
branch,
|
|
3707
|
+
statusResult.stdout,
|
|
3708
|
+
stagedPatch.stdout,
|
|
3709
|
+
unstagedPatch.stdout
|
|
3710
|
+
].join("\0")).digest("hex");
|
|
3711
|
+
const upstreamResult = await this.#run(git, [
|
|
3712
|
+
"rev-parse",
|
|
3713
|
+
"--abbrev-ref",
|
|
3714
|
+
"--symbolic-full-name",
|
|
3715
|
+
"@{upstream}"
|
|
3716
|
+
], root, GIT_TIMEOUT_MS);
|
|
3717
|
+
const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
|
|
3718
|
+
const logResult = await this.#run(git, [
|
|
3719
|
+
"log",
|
|
3720
|
+
"--format=%H%x09%s",
|
|
3721
|
+
`-n`,
|
|
3722
|
+
String(21),
|
|
3723
|
+
upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
|
|
3724
|
+
"--"
|
|
3725
|
+
], root, GIT_TIMEOUT_MS);
|
|
3726
|
+
const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
|
|
3727
|
+
const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
|
|
3728
|
+
const tab = line.indexOf(" ");
|
|
3729
|
+
const hash = line.slice(0, tab);
|
|
3730
|
+
return /^[0-9a-f]{40}$/iu.test(hash) ? [{
|
|
3731
|
+
hash,
|
|
3732
|
+
subject: line.slice(tab + 1).slice(0, 140)
|
|
3733
|
+
}] : [];
|
|
3734
|
+
});
|
|
3735
|
+
return {
|
|
3736
|
+
root,
|
|
3737
|
+
branch,
|
|
3738
|
+
head,
|
|
3739
|
+
fingerprint,
|
|
3740
|
+
files,
|
|
3741
|
+
patch,
|
|
3742
|
+
truncated: stagedPatch.lossy || unstagedPatch.lossy || stagedPatch.stdout.length + unstagedPatch.stdout.length > MAX_PATCH_CHARS,
|
|
3743
|
+
hasStaged: files.some((file) => file.staged),
|
|
3744
|
+
hasUnstaged: files.some((file) => file.unstaged),
|
|
3745
|
+
hasUntracked: files.some((file) => file.untracked),
|
|
3746
|
+
...upstream === void 0 ? {} : { upstream },
|
|
3747
|
+
unpushedCommits,
|
|
3748
|
+
unpushedTruncated: commitLines.length > MAX_UNPUSHED_COMMITS
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
async #rejectStagedWarp(git, cwd) {
|
|
3752
|
+
const staged = await this.#run(git, [
|
|
3753
|
+
"diff",
|
|
3754
|
+
"--cached",
|
|
3755
|
+
"--name-only",
|
|
3756
|
+
"--"
|
|
3757
|
+
], cwd, GIT_TIMEOUT_MS);
|
|
3758
|
+
if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
|
|
3759
|
+
if (staged.stdout.split(/\r?\n/u).some((path) => path.length > 0 && isProtectedWarpPath(path))) throw new RepositoryActionError("protected-warp-file", "WARP.md files cannot be committed. Unstage them before continuing.");
|
|
3760
|
+
}
|
|
3761
|
+
async #push(git, cwd, branch) {
|
|
3762
|
+
const args = (await this.#run(git, [
|
|
3763
|
+
"rev-parse",
|
|
3764
|
+
"--abbrev-ref",
|
|
3765
|
+
"--symbolic-full-name",
|
|
3766
|
+
"@{upstream}"
|
|
3767
|
+
], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
|
|
3768
|
+
"push",
|
|
3769
|
+
"--set-upstream",
|
|
3770
|
+
"origin",
|
|
3771
|
+
branch
|
|
3772
|
+
];
|
|
3773
|
+
await this.#mustRun(git, args, cwd, REMOTE_TIMEOUT_MS, "push-failed", "Git push failed.");
|
|
3774
|
+
}
|
|
3775
|
+
#git() {
|
|
3776
|
+
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
3777
|
+
return this.#gitExecutable;
|
|
3778
|
+
}
|
|
3779
|
+
#gh() {
|
|
3780
|
+
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
|
|
3781
|
+
throw new RepositoryActionError("gh-unavailable", "GitHub CLI is unavailable.");
|
|
3782
|
+
});
|
|
3783
|
+
return this.#ghExecutable;
|
|
3784
|
+
}
|
|
3785
|
+
async #mustRun(executable, args, cwd, timeoutMs, code, message) {
|
|
3786
|
+
const result = await this.#run(executable, args, cwd, timeoutMs);
|
|
3787
|
+
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
3788
|
+
return result;
|
|
3789
|
+
}
|
|
3790
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
|
|
3791
|
+
return collect(this.#runtime.spawn({
|
|
3792
|
+
argv: [executable, ...args],
|
|
3793
|
+
cwd,
|
|
3794
|
+
stdio: {
|
|
3795
|
+
stdin: "ignore",
|
|
3796
|
+
stdout: { maxBytes },
|
|
3797
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES }
|
|
3798
|
+
},
|
|
3799
|
+
graceMs: 1e3,
|
|
3800
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3801
|
+
env: {}
|
|
3802
|
+
}));
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
//#endregion
|
|
3019
3806
|
//#region src/repository-setup-routes.ts
|
|
3020
|
-
const MAX_BODY_BYTES = 16384;
|
|
3021
|
-
function record(value) {
|
|
3807
|
+
const MAX_BODY_BYTES$2 = 16384;
|
|
3808
|
+
function record$2(value) {
|
|
3022
3809
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3023
3810
|
}
|
|
3024
|
-
async function readJson(req) {
|
|
3811
|
+
async function readJson$2(req) {
|
|
3025
3812
|
const chunks = [];
|
|
3026
3813
|
let size = 0;
|
|
3027
3814
|
for await (const chunk of req) {
|
|
3028
3815
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3029
3816
|
size += buffer.length;
|
|
3030
|
-
if (size > MAX_BODY_BYTES) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
3817
|
+
if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
3031
3818
|
chunks.push(buffer);
|
|
3032
3819
|
}
|
|
3033
|
-
const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3820
|
+
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3034
3821
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
3035
3822
|
return value;
|
|
3036
3823
|
}
|
|
3037
|
-
function string(input, key) {
|
|
3824
|
+
function string$1(input, key) {
|
|
3038
3825
|
const value = input[key];
|
|
3039
3826
|
if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
|
|
3040
3827
|
return value;
|
|
3041
3828
|
}
|
|
3042
|
-
function optionalString(input, key) {
|
|
3829
|
+
function optionalString$1(input, key) {
|
|
3043
3830
|
const value = input[key];
|
|
3044
3831
|
if (value === void 0) return void 0;
|
|
3045
3832
|
if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
|
|
@@ -3062,7 +3849,7 @@ async function streamSetup(res, service, input) {
|
|
|
3062
3849
|
try {
|
|
3063
3850
|
ndjson(res, {
|
|
3064
3851
|
type: "complete",
|
|
3065
|
-
result: await service.setup(string(input, "cwd"), string(input, "branch"), input.worktree, optionalString(input, "branchName"), (stage) => {
|
|
3852
|
+
result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
|
|
3066
3853
|
if (!ended) ndjson(res, {
|
|
3067
3854
|
type: "progress",
|
|
3068
3855
|
stage
|
|
@@ -3098,15 +3885,15 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
3098
3885
|
}
|
|
3099
3886
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
3100
3887
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3101
|
-
const input = await readJson(req);
|
|
3888
|
+
const input = await readJson$2(req);
|
|
3102
3889
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
3103
3890
|
await streamSetup(res, service, input);
|
|
3104
3891
|
return;
|
|
3105
3892
|
}
|
|
3106
3893
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
3107
3894
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3108
|
-
const input = await readJson(req);
|
|
3109
|
-
await service.bindLease(string(input, "leaseId"), string(input, "sessionId"));
|
|
3895
|
+
const input = await readJson$2(req);
|
|
3896
|
+
await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
|
|
3110
3897
|
return json(res, 200, { ok: true });
|
|
3111
3898
|
}
|
|
3112
3899
|
return json(res, 404, { error: "not found" });
|
|
@@ -3122,6 +3909,168 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
3122
3909
|
}), "dsh-claude: repository setup route");
|
|
3123
3910
|
}
|
|
3124
3911
|
//#endregion
|
|
3912
|
+
//#region src/repository-action-routes.ts
|
|
3913
|
+
const MAX_BODY_BYTES$1 = 16384;
|
|
3914
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
3915
|
+
const ACTIONS = /* @__PURE__ */ new Set([
|
|
3916
|
+
"commit",
|
|
3917
|
+
"commit-push",
|
|
3918
|
+
"push",
|
|
3919
|
+
"create-pr"
|
|
3920
|
+
]);
|
|
3921
|
+
function record$1(value) {
|
|
3922
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3923
|
+
}
|
|
3924
|
+
async function readJson$1(req) {
|
|
3925
|
+
const chunks = [];
|
|
3926
|
+
let size = 0;
|
|
3927
|
+
for await (const chunk of req) {
|
|
3928
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3929
|
+
size += buffer.length;
|
|
3930
|
+
if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
3931
|
+
chunks.push(buffer);
|
|
3932
|
+
}
|
|
3933
|
+
const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3934
|
+
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
3935
|
+
return value;
|
|
3936
|
+
}
|
|
3937
|
+
function sessionId(url) {
|
|
3938
|
+
const value = url.searchParams.get("sessionId");
|
|
3939
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
3940
|
+
return value;
|
|
3941
|
+
}
|
|
3942
|
+
function string(input, key) {
|
|
3943
|
+
const value = input[key];
|
|
3944
|
+
if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
|
|
3945
|
+
return value;
|
|
3946
|
+
}
|
|
3947
|
+
function optionalString(input, key) {
|
|
3948
|
+
const value = input[key];
|
|
3949
|
+
if (value === void 0) return void 0;
|
|
3950
|
+
if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field must be a string.`);
|
|
3951
|
+
return value;
|
|
3952
|
+
}
|
|
3953
|
+
function actionRequest(input) {
|
|
3954
|
+
const action = input.action;
|
|
3955
|
+
if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
|
|
3956
|
+
return {
|
|
3957
|
+
action,
|
|
3958
|
+
fingerprint: string(input, "fingerprint"),
|
|
3959
|
+
message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
3960
|
+
includeUnstaged: input.includeUnstaged,
|
|
3961
|
+
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
3962
|
+
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
3963
|
+
...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
|
|
3964
|
+
...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
|
|
3965
|
+
throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
|
|
3966
|
+
})()
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
3970
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3971
|
+
kind: "prefix",
|
|
3972
|
+
path: CLAUDE_REPOSITORY_ACTION_PATH,
|
|
3973
|
+
handler: async (req, res) => {
|
|
3974
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3975
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3976
|
+
try {
|
|
3977
|
+
const cwd = cwdForSession(sessionId(url));
|
|
3978
|
+
if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
|
|
3979
|
+
if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
|
|
3980
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
3981
|
+
return json(res, 200, await service.preview(cwd));
|
|
3982
|
+
}
|
|
3983
|
+
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
3984
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3985
|
+
const input = await readJson$1(req);
|
|
3986
|
+
return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
|
|
3987
|
+
}
|
|
3988
|
+
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
3989
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
3990
|
+
return json(res, 200, await service.execute(cwd, actionRequest(await readJson$1(req))));
|
|
3991
|
+
}
|
|
3992
|
+
return json(res, 404, { error: "not found" });
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
if (error instanceof RepositoryActionError) return json(res, 409, {
|
|
3995
|
+
error: error.code,
|
|
3996
|
+
message: error.message,
|
|
3997
|
+
...error.commit === void 0 ? {} : { commit: error.commit }
|
|
3998
|
+
});
|
|
3999
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
4000
|
+
return json(res, 500, {
|
|
4001
|
+
error: "repository-action-unavailable",
|
|
4002
|
+
message: "Repository action is unavailable."
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}), "dsh-claude: repository action route");
|
|
4007
|
+
}
|
|
4008
|
+
//#endregion
|
|
4009
|
+
//#region src/review-comment-routes.ts
|
|
4010
|
+
const MAX_BODY_BYTES = 16384;
|
|
4011
|
+
const MAX_SESSION_ID_CHARS = 1024;
|
|
4012
|
+
function record(value) {
|
|
4013
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4014
|
+
}
|
|
4015
|
+
async function readJson(req) {
|
|
4016
|
+
const chunks = [];
|
|
4017
|
+
let size = 0;
|
|
4018
|
+
for await (const chunk of req) {
|
|
4019
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4020
|
+
size += buffer.length;
|
|
4021
|
+
if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
4022
|
+
chunks.push(buffer);
|
|
4023
|
+
}
|
|
4024
|
+
const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
4025
|
+
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
4026
|
+
return value;
|
|
4027
|
+
}
|
|
4028
|
+
function sessionIdFromUrl(url) {
|
|
4029
|
+
const value = url.searchParams.get("sessionId");
|
|
4030
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
|
|
4031
|
+
return value;
|
|
4032
|
+
}
|
|
4033
|
+
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
4034
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4035
|
+
kind: "prefix",
|
|
4036
|
+
path: CLAUDE_REVIEW_COMMENT_PATH,
|
|
4037
|
+
handler: async (req, res) => {
|
|
4038
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4039
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
4040
|
+
try {
|
|
4041
|
+
const sessionId = sessionIdFromUrl(url);
|
|
4042
|
+
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
4043
|
+
if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
|
|
4044
|
+
const input = await readJson(req);
|
|
4045
|
+
return json(res, 200, { comment: store.add(sessionId, {
|
|
4046
|
+
path: input.path,
|
|
4047
|
+
line: input.line,
|
|
4048
|
+
side: input.side,
|
|
4049
|
+
text: input.text
|
|
4050
|
+
}) });
|
|
4051
|
+
}
|
|
4052
|
+
if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
|
|
4053
|
+
if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
|
|
4054
|
+
const input = await readJson(req);
|
|
4055
|
+
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
4056
|
+
return json(res, 200, { removed: store.remove(sessionId, input.id) });
|
|
4057
|
+
}
|
|
4058
|
+
return json(res, 404, { error: "not found" });
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
if (error instanceof ReviewCommentError) return json(res, 409, {
|
|
4061
|
+
error: error.code,
|
|
4062
|
+
message: error.message
|
|
4063
|
+
});
|
|
4064
|
+
if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
|
|
4065
|
+
return json(res, 500, {
|
|
4066
|
+
error: "review-comment-unavailable",
|
|
4067
|
+
message: "Review comments are unavailable."
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}), "dsh-claude: review comment route");
|
|
4072
|
+
}
|
|
4073
|
+
//#endregion
|
|
3125
4074
|
//#region src/update-routes.ts
|
|
3126
4075
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
3127
4076
|
const UPDATE_TIMEOUT_MS = 3e4;
|
|
@@ -3752,6 +4701,7 @@ async function apply(ctx, config) {
|
|
|
3752
4701
|
const sidecar = new ClaudeSidecarRepository();
|
|
3753
4702
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
3754
4703
|
const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
|
|
4704
|
+
const reviewComments = new ReviewCommentStore();
|
|
3755
4705
|
const commandCatalogs = /* @__PURE__ */ new Map();
|
|
3756
4706
|
const supervisor = new ClaudeSupervisor({
|
|
3757
4707
|
runtime: ctx.subprocess,
|
|
@@ -3764,7 +4714,7 @@ async function apply(ctx, config) {
|
|
|
3764
4714
|
let resolutionError;
|
|
3765
4715
|
try {
|
|
3766
4716
|
supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
3767
|
-
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
|
|
4717
|
+
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
|
|
3768
4718
|
ctx.effect(() => {
|
|
3769
4719
|
const mounted = /* @__PURE__ */ new Map();
|
|
3770
4720
|
const pending = /* @__PURE__ */ new Set();
|
|
@@ -3825,9 +4775,11 @@ async function apply(ctx, config) {
|
|
|
3825
4775
|
resolutionError = error;
|
|
3826
4776
|
}
|
|
3827
4777
|
ctx.on("agent/disposed", async ({ agent }) => {
|
|
4778
|
+
reviewComments.disposeSession(agent.id);
|
|
3828
4779
|
await supervisor.disposeSession(agent.id);
|
|
3829
4780
|
await repositorySetup.cleanupSession(agent.id);
|
|
3830
4781
|
});
|
|
4782
|
+
ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
|
|
3831
4783
|
ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
|
|
3832
4784
|
ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
|
|
3833
4785
|
ctx.inject(["webServer"], (webCtx) => {
|
|
@@ -3836,15 +4788,22 @@ async function apply(ctx, config) {
|
|
|
3836
4788
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
3837
4789
|
registerClaudeGlobalSettingsRoute(webCtx);
|
|
3838
4790
|
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
3839
|
-
|
|
4791
|
+
registerRepositoryActionRoute(webCtx, new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd)), (sessionId) => {
|
|
4792
|
+
const agent = webCtx.agents.get(sessionId);
|
|
4793
|
+
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
4794
|
+
return agent.session.header.cwd;
|
|
4795
|
+
});
|
|
4796
|
+
const ownsClaudeSession = (sessionId) => {
|
|
3840
4797
|
const agent = webCtx.agents.get(sessionId);
|
|
3841
4798
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|
|
3842
|
-
}
|
|
4799
|
+
};
|
|
4800
|
+
registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
|
|
4801
|
+
registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
|
|
3843
4802
|
const agent = webCtx.agents.get(sessionId);
|
|
3844
4803
|
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
3845
4804
|
const cwd = agent.session.header.cwd;
|
|
3846
4805
|
return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
|
|
3847
|
-
});
|
|
4806
|
+
}, (sessionId) => reviewComments.list(sessionId));
|
|
3848
4807
|
});
|
|
3849
4808
|
}
|
|
3850
4809
|
//#endregion
|