@norman-else/dsh-claude 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +55 -55
- package/LICENSE +21 -21
- package/README.md +82 -191
- package/cordis.patch.yml +12 -12
- package/legacy-preset/agent.cordis.yml +11 -11
- package/legacy-preset/preset.yml +4 -4
- package/lib/bin.mjs +0 -0
- package/lib/bin.mjs.map +1 -1
- package/lib/client.d.ts +7 -0
- package/lib/client.js +815 -350
- package/lib/client.js.map +1 -1
- package/lib/command-bridge-C10-lz6A.mjs.map +1 -1
- package/lib/events-BdDs9ebF.mjs.map +1 -1
- package/lib/index.d.mts +33 -0
- package/lib/index.mjs +322 -33
- package/lib/index.mjs.map +1 -1
- package/lib/preset-installer-DMANIjwu.mjs.map +1 -1
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +181 -181
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
package/lib/index.mjs
CHANGED
|
@@ -15,6 +15,9 @@ 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,
|
|
@@ -117,13 +120,126 @@ var ClaudeSidecarRepository = class {
|
|
|
117
120
|
root;
|
|
118
121
|
legacyRoot;
|
|
119
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();
|
|
120
131
|
constructor(options = {}) {
|
|
121
132
|
this.root = options.root ?? dshHomePath("plugins", "dsh-claude", "sessions");
|
|
122
133
|
this.legacyRoot = options.legacyRoot ?? (options.root === void 0 ? dshHomePath("plugins", "dsh-claude-code", "sessions") : void 0);
|
|
123
134
|
}
|
|
124
135
|
async read(sessionId) {
|
|
125
136
|
await this.#pending.get(sessionId)?.catch(() => void 0);
|
|
126
|
-
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);
|
|
127
243
|
}
|
|
128
244
|
writeBinding(sessionId, value) {
|
|
129
245
|
const normalized = normalizeBinding(value);
|
|
@@ -137,21 +253,30 @@ var ClaudeSidecarRepository = class {
|
|
|
137
253
|
return this.#update(sessionId, (current) => ({
|
|
138
254
|
...current,
|
|
139
255
|
activities: mergeActivities(current.activities, [normalized])
|
|
140
|
-
})
|
|
256
|
+
}), false, {
|
|
257
|
+
kind: "activity",
|
|
258
|
+
activity: normalized
|
|
259
|
+
});
|
|
141
260
|
}
|
|
142
261
|
writeContextUsage(sessionId, value) {
|
|
143
262
|
const normalized = normalizeContextUsage(value);
|
|
144
263
|
return this.#update(sessionId, (current) => ({
|
|
145
264
|
...current,
|
|
146
265
|
contextUsage: normalized
|
|
147
|
-
})
|
|
266
|
+
}), false, {
|
|
267
|
+
kind: "contextUsage",
|
|
268
|
+
value: normalized
|
|
269
|
+
});
|
|
148
270
|
}
|
|
149
271
|
writeTasks(sessionId, value) {
|
|
150
272
|
const normalized = normalizeTasksEvent(value);
|
|
151
273
|
return this.#update(sessionId, (current) => ({
|
|
152
274
|
...current,
|
|
153
275
|
tasks: normalized
|
|
154
|
-
})
|
|
276
|
+
}), false, {
|
|
277
|
+
kind: "tasks",
|
|
278
|
+
value: normalized
|
|
279
|
+
});
|
|
155
280
|
}
|
|
156
281
|
importLegacy(sessionId, events) {
|
|
157
282
|
const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
|
|
@@ -164,15 +289,15 @@ var ClaudeSidecarRepository = class {
|
|
|
164
289
|
...current.binding !== void 0 || importedBinding === void 0 ? {} : { binding: normalizeBinding(importedBinding) },
|
|
165
290
|
...current.contextUsage !== void 0 || importedUsage === void 0 ? {} : { contextUsage: normalizeContextUsage(importedUsage) },
|
|
166
291
|
...current.tasks !== void 0 || importedTasks === void 0 ? {} : { tasks: normalizeTasksEvent(importedTasks.tasks) }
|
|
167
|
-
}), true);
|
|
292
|
+
}), true, { kind: "sync" });
|
|
168
293
|
}
|
|
169
294
|
#path(sessionId, root = this.root) {
|
|
170
295
|
if (sessionId.length === 0 || sessionId.length > 1024) throw new Error("dsh-claude: invalid session id");
|
|
171
296
|
return join(root, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
172
297
|
}
|
|
173
|
-
#update(sessionId, change, skipUnchanged = false) {
|
|
298
|
+
#update(sessionId, change, skipUnchanged = false, delta) {
|
|
174
299
|
const operation = (this.#pending.get(sessionId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
175
|
-
const current = await this.#
|
|
300
|
+
const current = await this.#base(sessionId);
|
|
176
301
|
const changed = parseClaudeSidecar({
|
|
177
302
|
...change(current),
|
|
178
303
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
@@ -184,6 +309,8 @@ var ClaudeSidecarRepository = class {
|
|
|
184
309
|
revision: current.revision + 1
|
|
185
310
|
};
|
|
186
311
|
await this.#writeNow(sessionId, next);
|
|
312
|
+
this.#latest.set(sessionId, next);
|
|
313
|
+
if (delta !== void 0) this.#notify(sessionId, delta);
|
|
187
314
|
return next;
|
|
188
315
|
});
|
|
189
316
|
this.#pending.set(sessionId, operation);
|
|
@@ -594,7 +721,9 @@ function normalizeAssistant(message) {
|
|
|
594
721
|
return normalized;
|
|
595
722
|
}
|
|
596
723
|
function normalizeUser(message) {
|
|
724
|
+
if (message.isReplay === true) return [];
|
|
597
725
|
const content = record$4(message.message)?.content;
|
|
726
|
+
if (typeof content === "string") return [];
|
|
598
727
|
if (!Array.isArray(content)) return [{
|
|
599
728
|
kind: "protocol-error",
|
|
600
729
|
title: "Malformed Claude user message",
|
|
@@ -1647,6 +1776,7 @@ var ClaudeSupervisor = class {
|
|
|
1647
1776
|
if (entry.active !== active) return;
|
|
1648
1777
|
if (active.aborted) {
|
|
1649
1778
|
await this.#upsertTranscriptText(active);
|
|
1779
|
+
await this.#flushTranscript(active);
|
|
1650
1780
|
await this.#appendSafely(active, {
|
|
1651
1781
|
kind: "status",
|
|
1652
1782
|
phase: "failed",
|
|
@@ -1689,6 +1819,7 @@ var ClaudeSupervisor = class {
|
|
|
1689
1819
|
});
|
|
1690
1820
|
}
|
|
1691
1821
|
await this.#upsertTranscriptText(active);
|
|
1822
|
+
await this.#flushTranscript(active);
|
|
1692
1823
|
const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
|
|
1693
1824
|
await this.#appendSafely(active, {
|
|
1694
1825
|
kind: "error",
|
|
@@ -1724,6 +1855,7 @@ var ClaudeSupervisor = class {
|
|
|
1724
1855
|
phase: "completed",
|
|
1725
1856
|
title: "Claude Code turn completed"
|
|
1726
1857
|
});
|
|
1858
|
+
await this.#flushTranscript(active);
|
|
1727
1859
|
active.output.push({
|
|
1728
1860
|
type: "complete",
|
|
1729
1861
|
text: active.text
|
|
@@ -1741,9 +1873,7 @@ var ClaudeSupervisor = class {
|
|
|
1741
1873
|
const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
|
|
1742
1874
|
active.transcriptTextOrdinal = ordinal;
|
|
1743
1875
|
try {
|
|
1744
|
-
|
|
1745
|
-
kind: "text",
|
|
1746
|
-
phase: "updated",
|
|
1876
|
+
this.#sidecar.appendTranscriptText(active.agent.id, {
|
|
1747
1877
|
text: active.transcriptText,
|
|
1748
1878
|
turn: active.cursor.turn,
|
|
1749
1879
|
step: active.cursor.step,
|
|
@@ -1751,7 +1881,11 @@ var ClaudeSupervisor = class {
|
|
|
1751
1881
|
});
|
|
1752
1882
|
} catch {}
|
|
1753
1883
|
}
|
|
1884
|
+
async #flushTranscript(active) {
|
|
1885
|
+
await this.#sidecar.flushTranscriptText(active.agent.id).catch(() => void 0);
|
|
1886
|
+
}
|
|
1754
1887
|
#closeTranscriptTextSegment(active) {
|
|
1888
|
+
this.#flushTranscript(active);
|
|
1755
1889
|
active.transcriptText = "";
|
|
1756
1890
|
active.transcriptTextOrdinal = void 0;
|
|
1757
1891
|
}
|
|
@@ -1807,6 +1941,7 @@ var ClaudeSupervisor = class {
|
|
|
1807
1941
|
const stderr = entry.process?.stderrTail();
|
|
1808
1942
|
if (active !== void 0) {
|
|
1809
1943
|
await this.#upsertTranscriptText(active);
|
|
1944
|
+
await this.#flushTranscript(active);
|
|
1810
1945
|
if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
|
|
1811
1946
|
const unknown = active.sawActivity;
|
|
1812
1947
|
entry.state = unknown ? "outcome-unknown" : "disconnected";
|
|
@@ -2376,47 +2511,173 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2376
2511
|
//#endregion
|
|
2377
2512
|
//#region src/projection-routes.ts
|
|
2378
2513
|
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
2379
|
-
|
|
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) {
|
|
2380
2518
|
try {
|
|
2381
2519
|
const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
|
|
2382
2520
|
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
2383
2521
|
if (!pathname.startsWith(prefix)) return void 0;
|
|
2384
|
-
|
|
2522
|
+
let encoded = pathname.slice(prefix.length);
|
|
2523
|
+
const stream = encoded.endsWith("/stream");
|
|
2524
|
+
if (stream) encoded = encoded.slice(0, -7);
|
|
2385
2525
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2386
2526
|
const sessionId = decodeURIComponent(encoded);
|
|
2387
2527
|
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
|
|
2388
|
-
return
|
|
2528
|
+
return {
|
|
2529
|
+
sessionId,
|
|
2530
|
+
stream
|
|
2531
|
+
};
|
|
2389
2532
|
} catch {
|
|
2390
2533
|
return;
|
|
2391
2534
|
}
|
|
2392
2535
|
}
|
|
2393
|
-
|
|
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. */
|
|
2394
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
|
+
};
|
|
2395
2666
|
ctx.effect(() => ctx.webServer.register({
|
|
2396
2667
|
kind: "prefix",
|
|
2397
2668
|
path: CLAUDE_PROJECTION_PATH,
|
|
2398
2669
|
handler: async (req, res) => {
|
|
2399
2670
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
2400
2671
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
2401
|
-
const
|
|
2402
|
-
if (
|
|
2672
|
+
const target = targetFromUrl(req.url);
|
|
2673
|
+
if (target === void 0) return json(res, 400, { error: "invalid session id" });
|
|
2403
2674
|
try {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
return json(res, 200, {
|
|
2408
|
-
schemaVersion: projection.schemaVersion,
|
|
2409
|
-
revision: projection.revision,
|
|
2410
|
-
owned,
|
|
2411
|
-
commands: commandsForSession(sessionId),
|
|
2412
|
-
activities: projection.activities,
|
|
2413
|
-
...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
|
|
2414
|
-
...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
|
|
2415
|
-
...repository === void 0 ? {} : { repository },
|
|
2416
|
-
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
2417
|
-
});
|
|
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)));
|
|
2418
2678
|
} catch {
|
|
2419
|
-
return json(res, 500, { error: "projection unavailable" });
|
|
2679
|
+
if (!res.headersSent) return json(res, 500, { error: "projection unavailable" });
|
|
2680
|
+
res.end();
|
|
2420
2681
|
}
|
|
2421
2682
|
}
|
|
2422
2683
|
}), "dsh-claude: sidecar projection route");
|
|
@@ -3318,6 +3579,30 @@ var RepositoryActionService = class {
|
|
|
3318
3579
|
pushed: true
|
|
3319
3580
|
};
|
|
3320
3581
|
}
|
|
3582
|
+
if (request.action === "merge-pr") {
|
|
3583
|
+
const method = request.mergeMethod;
|
|
3584
|
+
if (method !== "merge" && method !== "squash" && method !== "rebase") throw new RepositoryActionError("invalid-request", "The merge method is invalid.");
|
|
3585
|
+
let gh;
|
|
3586
|
+
try {
|
|
3587
|
+
gh = await this.#gh();
|
|
3588
|
+
} catch (error) {
|
|
3589
|
+
throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.");
|
|
3590
|
+
}
|
|
3591
|
+
const merged = await this.#run(gh, [
|
|
3592
|
+
"pr",
|
|
3593
|
+
"merge",
|
|
3594
|
+
`--${method}`
|
|
3595
|
+
], before.root, REMOTE_TIMEOUT_MS);
|
|
3596
|
+
if (merged.exitCode !== 0 || merged.lossy) {
|
|
3597
|
+
const reason = merged.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
|
|
3598
|
+
throw new RepositoryActionError("merge-failed", reason === void 0 || reason.length === 0 ? "The pull request could not be merged." : reason);
|
|
3599
|
+
}
|
|
3600
|
+
this.#invalidate(before.root);
|
|
3601
|
+
return {
|
|
3602
|
+
commit: before.head,
|
|
3603
|
+
pushed: true
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3321
3606
|
const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
|
|
3322
3607
|
if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
|
|
3323
3608
|
const git = await this.#git();
|
|
@@ -3655,7 +3940,8 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
3655
3940
|
"commit",
|
|
3656
3941
|
"commit-push",
|
|
3657
3942
|
"push",
|
|
3658
|
-
"create-pr"
|
|
3943
|
+
"create-pr",
|
|
3944
|
+
"merge-pr"
|
|
3659
3945
|
]);
|
|
3660
3946
|
function record$1(value) {
|
|
3661
3947
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -3695,13 +3981,16 @@ function actionRequest(input) {
|
|
|
3695
3981
|
return {
|
|
3696
3982
|
action,
|
|
3697
3983
|
fingerprint: string(input, "fingerprint"),
|
|
3698
|
-
message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
3984
|
+
message: action === "push" || action === "merge-pr" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
3699
3985
|
includeUnstaged: input.includeUnstaged,
|
|
3700
3986
|
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
3701
3987
|
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
3702
3988
|
...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
|
|
3703
3989
|
...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
|
|
3704
3990
|
throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
|
|
3991
|
+
})(),
|
|
3992
|
+
...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
|
|
3993
|
+
throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
|
|
3705
3994
|
})()
|
|
3706
3995
|
};
|
|
3707
3996
|
}
|