@norman-else/dsh-claude 0.1.33 → 0.1.35
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 +117 -4
- package/lib/bin.mjs +2 -2
- package/lib/client.d.ts +4 -0
- package/lib/client.js +1160 -614
- package/lib/client.js.map +1 -1
- package/lib/{events-DeSV0S1-.mjs → events-Doid-tq7.mjs} +8 -2
- package/lib/events-Doid-tq7.mjs.map +1 -0
- package/lib/index.d.mts +54 -2
- package/lib/index.mjs +1185 -480
- package/lib/index.mjs.map +1 -1
- package/lib/presenters-CXtE8qve.mjs +300 -0
- package/lib/presenters-CXtE8qve.mjs.map +1 -0
- package/lib/{preset-installer-DbmlhBXI.mjs → preset-installer-BMyKr5eQ.mjs} +36 -4
- package/lib/preset-installer-BMyKr5eQ.mjs.map +1 -0
- package/lib/preset-route.mjs +2 -172
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +189 -188
- package/lib/command-bridge-DXI6nWhB.mjs +0 -71
- package/lib/command-bridge-DXI6nWhB.mjs.map +0 -1
- package/lib/events-DeSV0S1-.mjs.map +0 -1
- package/lib/preset-installer-DbmlhBXI.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import {
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { A as CLAUDE_UPDATE_CHECK_PATH, C as CLAUDE_REPOSITORY_ACTION_PATH, D as CLAUDE_REPOSITORY_STATUS_PATH, E as CLAUDE_REPOSITORY_SETUP_PATH, F as TASK_TOOL_NAMES, I as isClaudeRenderMode, M as CLAUDE_USAGE_PATH, N as DEFAULT_CLAUDE_RENDER_MODE, O as CLAUDE_REVIEW_COMMENT_PATH, S as CLAUDE_RENDER_MODES, T as CLAUDE_REPOSITORY_FILE_PATH, _ as CLAUDE_DOCTOR_PATH, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_CODE_PROVIDER_IDS, h as CLAUDE_CODE_PROVIDER, i as latestClaudeSessionBinding, j as CLAUDE_UPDATE_PATH, k as CLAUDE_REWIND_PATH, l as redactText, m as CLAUDE_CODE_PRESET_ID, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CLIENT_DIAGNOSTICS_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_EDITOR_OPEN_PATH, w as CLAUDE_REPOSITORY_FEEDBACK_PATH, x as CLAUDE_PROJECTION_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH } from "./events-Doid-tq7.mjs";
|
|
2
|
+
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-CXtE8qve.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-BMyKr5eQ.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -8,10 +8,11 @@ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node
|
|
|
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";
|
|
11
12
|
import { EventEmitter } from "node:events";
|
|
12
13
|
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
|
+
import { deadline } from "@deepseek-ai/dsh-timeout";
|
|
15
16
|
import { StringDecoder } from "node:string_decoder";
|
|
16
17
|
//#region src/rewind.ts
|
|
17
18
|
const EMPTY_REWIND_STATE = {
|
|
@@ -227,6 +228,8 @@ var ClaudeSidecarRepository = class {
|
|
|
227
228
|
/** Latest durable projection per session; disk is read once and written through. */
|
|
228
229
|
#latest = /* @__PURE__ */ new Map();
|
|
229
230
|
#listeners = /* @__PURE__ */ new Map();
|
|
231
|
+
/** Notifications published per session, so a reader can spot a hole. */
|
|
232
|
+
#seq = /* @__PURE__ */ new Map();
|
|
230
233
|
/** Streaming transcript segments not yet persisted, keyed by activity key. */
|
|
231
234
|
#live = /* @__PURE__ */ new Map();
|
|
232
235
|
/** Monotonic revision boost so merged reads advance while text stays in memory. */
|
|
@@ -275,7 +278,8 @@ var ClaudeSidecarRepository = class {
|
|
|
275
278
|
const base = {
|
|
276
279
|
turn: normalized.turn,
|
|
277
280
|
step: normalized.step,
|
|
278
|
-
ordinal: normalized.ordinal
|
|
281
|
+
ordinal: normalized.ordinal,
|
|
282
|
+
...normalized.renderer === void 0 ? {} : { renderer: normalized.renderer }
|
|
279
283
|
};
|
|
280
284
|
this.#notify(sessionId, previous?.text !== void 0 && text.startsWith(previous.text) ? {
|
|
281
285
|
kind: "text",
|
|
@@ -297,11 +301,35 @@ var ClaudeSidecarRepository = class {
|
|
|
297
301
|
}
|
|
298
302
|
return this.#flushLive(sessionId);
|
|
299
303
|
}
|
|
304
|
+
/** How many notifications this session has published. */
|
|
305
|
+
sequence(sessionId) {
|
|
306
|
+
return this.#seq.get(sessionId) ?? 0;
|
|
307
|
+
}
|
|
308
|
+
/** Restate where the stream stands without changing anything.
|
|
309
|
+
*
|
|
310
|
+
* A reader detects a lost delta from the hole the NEXT one leaves, which
|
|
311
|
+
* never comes when the lost delta was the last of a turn -- exactly the
|
|
312
|
+
* case that leaves a finished tool group pulsing forever. Called at turn
|
|
313
|
+
* settlement, this gives that reader the one line it needs to disagree. */
|
|
314
|
+
checkpoint(sessionId) {
|
|
315
|
+
this.#deliver(sessionId, {
|
|
316
|
+
kind: "checkpoint",
|
|
317
|
+
seq: this.sequence(sessionId)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
300
320
|
#notify(sessionId, delta) {
|
|
321
|
+
const seq = this.sequence(sessionId) + 1;
|
|
322
|
+
this.#seq.set(sessionId, seq);
|
|
323
|
+
this.#deliver(sessionId, {
|
|
324
|
+
...delta,
|
|
325
|
+
seq
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
#deliver(sessionId, notification) {
|
|
301
329
|
const set = this.#listeners.get(sessionId);
|
|
302
330
|
if (set === void 0) return;
|
|
303
331
|
for (const listener of [...set]) try {
|
|
304
|
-
listener(
|
|
332
|
+
listener(notification);
|
|
305
333
|
} catch {}
|
|
306
334
|
}
|
|
307
335
|
#merged(sessionId, base) {
|
|
@@ -1451,6 +1479,7 @@ var ClaudeSupervisor = class {
|
|
|
1451
1479
|
#queryFactory;
|
|
1452
1480
|
#runDetached;
|
|
1453
1481
|
#sidecar;
|
|
1482
|
+
#dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
|
|
1454
1483
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1455
1484
|
#disposed = false;
|
|
1456
1485
|
#admissionGate = Promise.resolve();
|
|
@@ -1900,6 +1929,10 @@ var ClaudeSupervisor = class {
|
|
|
1900
1929
|
title: "Claude thinking",
|
|
1901
1930
|
summary: message.text
|
|
1902
1931
|
});
|
|
1932
|
+
if (this.#nativeRendering()) active.output.push({
|
|
1933
|
+
type: "thinking",
|
|
1934
|
+
text: message.text
|
|
1935
|
+
});
|
|
1903
1936
|
return;
|
|
1904
1937
|
case "tool-call":
|
|
1905
1938
|
if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
|
|
@@ -1913,7 +1946,13 @@ var ClaudeSupervisor = class {
|
|
|
1913
1946
|
summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
|
|
1914
1947
|
detail: message.input
|
|
1915
1948
|
});
|
|
1916
|
-
if (message.parentToolUseId === void 0)
|
|
1949
|
+
if (message.parentToolUseId === void 0) {
|
|
1950
|
+
active.callNames.set(message.toolUseId, message.toolName);
|
|
1951
|
+
if (this.#nativeRendering()) {
|
|
1952
|
+
this.#ensureDynamicPresenter(active.agent, message.toolName);
|
|
1953
|
+
await this.#appendNativeToolCall(active, message);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1917
1956
|
return;
|
|
1918
1957
|
case "tool-result":
|
|
1919
1958
|
await this.#appendActivity(active, {
|
|
@@ -1925,6 +1964,7 @@ var ClaudeSupervisor = class {
|
|
|
1925
1964
|
detail: message.output,
|
|
1926
1965
|
isError: message.isError
|
|
1927
1966
|
});
|
|
1967
|
+
if (message.parentToolUseId === void 0 && this.#nativeRendering()) await this.#appendNativeToolResult(active, message);
|
|
1928
1968
|
return;
|
|
1929
1969
|
case "subagent":
|
|
1930
1970
|
await this.#appendActivity(active, {
|
|
@@ -1974,9 +2014,66 @@ var ClaudeSupervisor = class {
|
|
|
1974
2014
|
title: message.toolName,
|
|
1975
2015
|
summary: message.summary
|
|
1976
2016
|
});
|
|
2017
|
+
if (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
|
|
2018
|
+
kind: "tool-result",
|
|
2019
|
+
toolUseId: message.toolUseId,
|
|
2020
|
+
output: message.summary,
|
|
2021
|
+
isError: true
|
|
2022
|
+
});
|
|
1977
2023
|
return;
|
|
1978
2024
|
}
|
|
1979
2025
|
}
|
|
2026
|
+
#nativeRendering() {
|
|
2027
|
+
return (this.#config.renderMode ?? "plugin") === "native";
|
|
2028
|
+
}
|
|
2029
|
+
/** Register one presenter-only mirror for a tool name the static preset
|
|
2030
|
+
* registry does not cover (MCP tools, newly added built-ins). Runs in the
|
|
2031
|
+
* agent scope so the mirror is visible only to this preset's sessions and
|
|
2032
|
+
* unwinds with the agent; failure keeps the generic card, never the turn. */
|
|
2033
|
+
#ensureDynamicPresenter(agent, name) {
|
|
2034
|
+
if (CLAUDE_PRESENTER_NAMES.has(name)) return;
|
|
2035
|
+
let known = this.#dynamicPresenterNames.get(agent);
|
|
2036
|
+
if (known === void 0) {
|
|
2037
|
+
known = /* @__PURE__ */ new Set();
|
|
2038
|
+
this.#dynamicPresenterNames.set(agent, known);
|
|
2039
|
+
}
|
|
2040
|
+
if (known.has(name)) return;
|
|
2041
|
+
try {
|
|
2042
|
+
agent.ctx.tools.register(dynamicPresenterDefinition(name));
|
|
2043
|
+
known.add(name);
|
|
2044
|
+
} catch {}
|
|
2045
|
+
}
|
|
2046
|
+
/** Mirror one root Claude tool call into the durable native tool channel so
|
|
2047
|
+
* the host's tool presentation renders it exactly like a DSH-executed call.
|
|
2048
|
+
* Presentation duplication is best-effort and never unsettles the turn. */
|
|
2049
|
+
async #appendNativeToolCall(active, message) {
|
|
2050
|
+
try {
|
|
2051
|
+
await active.agent.session.append("tool/call", {
|
|
2052
|
+
turn: active.cursor.turn,
|
|
2053
|
+
step: active.cursor.step,
|
|
2054
|
+
callId: CallId(message.toolUseId),
|
|
2055
|
+
name: message.toolName,
|
|
2056
|
+
arguments: safeDetail(message.input) ?? "{}"
|
|
2057
|
+
});
|
|
2058
|
+
} catch {}
|
|
2059
|
+
}
|
|
2060
|
+
async #appendNativeToolResult(active, message) {
|
|
2061
|
+
const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
|
|
2062
|
+
try {
|
|
2063
|
+
await active.agent.session.append("tool/result", {
|
|
2064
|
+
turn: active.cursor.turn,
|
|
2065
|
+
step: active.cursor.step,
|
|
2066
|
+
message: createToolResultMessage({
|
|
2067
|
+
callId: CallId(message.toolUseId),
|
|
2068
|
+
content: [{
|
|
2069
|
+
type: "text",
|
|
2070
|
+
text
|
|
2071
|
+
}],
|
|
2072
|
+
isError: message.isError
|
|
2073
|
+
})
|
|
2074
|
+
}, { surfaceOp: "append" });
|
|
2075
|
+
} catch {}
|
|
2076
|
+
}
|
|
1980
2077
|
/** Merge one task lifecycle message into the session's task board. */
|
|
1981
2078
|
async #trackTask(entry, message, taskId, originTurn) {
|
|
1982
2079
|
const previous = entry.tasks.get(taskId);
|
|
@@ -2157,6 +2254,7 @@ var ClaudeSupervisor = class {
|
|
|
2157
2254
|
entry.state = "idle";
|
|
2158
2255
|
entry.lastUsedAt = Date.now();
|
|
2159
2256
|
await this.#recordChainAnchor(entry, active);
|
|
2257
|
+
this.#checkpointProjection(entry);
|
|
2160
2258
|
this.#armIdleTimer(entry);
|
|
2161
2259
|
return;
|
|
2162
2260
|
}
|
|
@@ -2240,8 +2338,20 @@ var ClaudeSupervisor = class {
|
|
|
2240
2338
|
entry.lastUsedAt = Date.now();
|
|
2241
2339
|
await this.#recordChainAnchor(entry, active);
|
|
2242
2340
|
await this.#learnContextWindow(entry);
|
|
2341
|
+
this.#checkpointProjection(entry);
|
|
2243
2342
|
this.#armIdleTimer(entry);
|
|
2244
2343
|
}
|
|
2344
|
+
/** Tell every reader where this session's delta stream ended.
|
|
2345
|
+
*
|
|
2346
|
+
* A reader that lost the turn's last delta has nothing later to reveal the
|
|
2347
|
+
* hole, and a settled turn produces nothing further -- so a finished tool
|
|
2348
|
+
* group would keep pulsing until the session was reopened by hand. Last
|
|
2349
|
+
* line of the turn, best effort: presentation must never unsettle it. */
|
|
2350
|
+
#checkpointProjection(entry) {
|
|
2351
|
+
try {
|
|
2352
|
+
this.#sidecar.checkpoint(entry.sessionId);
|
|
2353
|
+
} catch {}
|
|
2354
|
+
}
|
|
2245
2355
|
/** Pin where Claude's chain ended for the DSH turn that just settled, so a
|
|
2246
2356
|
* later rewind of the following turn can fork exactly here. Best effort:
|
|
2247
2357
|
* a missing anchor only makes a rewind fall back to an earlier turn. */
|
|
@@ -2259,6 +2369,7 @@ var ClaudeSupervisor = class {
|
|
|
2259
2369
|
try {
|
|
2260
2370
|
this.#sidecar.appendTranscriptText(active.agent.id, {
|
|
2261
2371
|
text: active.transcriptText,
|
|
2372
|
+
...this.#nativeRendering() ? { renderer: "native" } : {},
|
|
2262
2373
|
turn: active.cursor.turn,
|
|
2263
2374
|
step: active.cursor.step,
|
|
2264
2375
|
ordinal
|
|
@@ -2277,6 +2388,7 @@ var ClaudeSupervisor = class {
|
|
|
2277
2388
|
const ordinal = active.cursor.nextOrdinal++;
|
|
2278
2389
|
await this.#sidecar.appendActivity(active.agent.id, {
|
|
2279
2390
|
...activity,
|
|
2391
|
+
...this.#nativeRendering() ? { renderer: "native" } : {},
|
|
2280
2392
|
turn: active.cursor.turn,
|
|
2281
2393
|
step: active.cursor.step,
|
|
2282
2394
|
ordinal
|
|
@@ -2640,13 +2752,15 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2640
2752
|
#attachments;
|
|
2641
2753
|
#presetIdFor;
|
|
2642
2754
|
#drainReviewComments;
|
|
2643
|
-
|
|
2755
|
+
#renderMode;
|
|
2756
|
+
constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
|
|
2644
2757
|
super();
|
|
2645
2758
|
this.#supervisor = supervisor;
|
|
2646
2759
|
this.#agents = agents;
|
|
2647
2760
|
this.#attachments = attachments;
|
|
2648
2761
|
this.#presetIdFor = presetIdFor;
|
|
2649
2762
|
this.#drainReviewComments = drainReviewComments;
|
|
2763
|
+
this.#renderMode = renderMode;
|
|
2650
2764
|
}
|
|
2651
2765
|
providerInfo(provider) {
|
|
2652
2766
|
return {
|
|
@@ -2718,15 +2832,74 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2718
2832
|
...thinkingMode === void 0 ? {} : { thinkingMode },
|
|
2719
2833
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2720
2834
|
});
|
|
2835
|
+
const native = this.#renderMode() === "native";
|
|
2721
2836
|
let pendingUsage;
|
|
2722
2837
|
let completed = false;
|
|
2838
|
+
let blockIndex = 0;
|
|
2839
|
+
let text = "";
|
|
2840
|
+
/** Settle the buffered prose as one text block. Each Claude result is one
|
|
2841
|
+
* block so tool activity stays ahead of the prose it explains and a
|
|
2842
|
+
* background-task report can follow in a block of its own. */
|
|
2843
|
+
function* flushText() {
|
|
2844
|
+
if (text.length === 0) return;
|
|
2845
|
+
const settled = text;
|
|
2846
|
+
text = "";
|
|
2847
|
+
yield {
|
|
2848
|
+
type: "block-start",
|
|
2849
|
+
index: blockIndex,
|
|
2850
|
+
blockType: "text"
|
|
2851
|
+
};
|
|
2852
|
+
yield {
|
|
2853
|
+
type: "text-delta",
|
|
2854
|
+
index: blockIndex,
|
|
2855
|
+
text: settled
|
|
2856
|
+
};
|
|
2857
|
+
yield {
|
|
2858
|
+
type: "block-end",
|
|
2859
|
+
index: blockIndex,
|
|
2860
|
+
block: {
|
|
2861
|
+
type: "text",
|
|
2862
|
+
text: settled
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
blockIndex += 1;
|
|
2866
|
+
}
|
|
2723
2867
|
try {
|
|
2724
2868
|
for await (const event of events) {
|
|
2725
|
-
if (event.type === "text-delta" || event.type === "segment-complete") continue;
|
|
2726
2869
|
if (event.type === "usage") {
|
|
2727
2870
|
pendingUsage = tokenUsage(event.usage);
|
|
2728
2871
|
continue;
|
|
2729
2872
|
}
|
|
2873
|
+
if (event.type === "text-delta") {
|
|
2874
|
+
if (native) text += event.text;
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (event.type === "thinking") {
|
|
2878
|
+
if (!native) continue;
|
|
2879
|
+
yield* flushText();
|
|
2880
|
+
yield {
|
|
2881
|
+
type: "block-start",
|
|
2882
|
+
index: blockIndex,
|
|
2883
|
+
blockType: "reasoning"
|
|
2884
|
+
};
|
|
2885
|
+
yield {
|
|
2886
|
+
type: "reasoning-delta",
|
|
2887
|
+
index: blockIndex,
|
|
2888
|
+
text: event.text
|
|
2889
|
+
};
|
|
2890
|
+
yield {
|
|
2891
|
+
type: "block-end",
|
|
2892
|
+
index: blockIndex,
|
|
2893
|
+
block: {
|
|
2894
|
+
type: "reasoning",
|
|
2895
|
+
text: event.text
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
blockIndex += 1;
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
yield* flushText();
|
|
2902
|
+
if (event.type === "segment-complete") continue;
|
|
2730
2903
|
completed = true;
|
|
2731
2904
|
if (pendingUsage !== void 0) yield {
|
|
2732
2905
|
type: "usage",
|
|
@@ -2740,6 +2913,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2740
2913
|
} catch (error) {
|
|
2741
2914
|
if (error.name === "AbortError") {
|
|
2742
2915
|
completed = true;
|
|
2916
|
+
yield* flushText();
|
|
2743
2917
|
yield {
|
|
2744
2918
|
type: "finish",
|
|
2745
2919
|
reason: {
|
|
@@ -2757,10 +2931,41 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2757
2931
|
if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
|
|
2758
2932
|
}
|
|
2759
2933
|
};
|
|
2760
|
-
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2761
|
-
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
|
|
2934
|
+
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
|
|
2935
|
+
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode);
|
|
2762
2936
|
}
|
|
2763
2937
|
//#endregion
|
|
2938
|
+
//#region src/plugin-budget.ts
|
|
2939
|
+
/**
|
|
2940
|
+
* The plugin's connection budget, in one table.
|
|
2941
|
+
*
|
|
2942
|
+
* A browser opens a small fixed number of connections to one origin — six for
|
|
2943
|
+
* HTTP/1.1 in Chromium — and shares them with the Host's own traffic. Every
|
|
2944
|
+
* response this plugin holds open costs one of them for its lifetime, and a
|
|
2945
|
+
* request that cannot get one waits in the browser's queue where no server-side
|
|
2946
|
+
* deadline can reach it. When the pool is exhausted the panels that would
|
|
2947
|
+
* *diagnose* the problem are the first thing to stop answering, which is how
|
|
2948
|
+
* this failure has always presented: four settings cards timing out at once
|
|
2949
|
+
* against a Host that is demonstrably healthy.
|
|
2950
|
+
*
|
|
2951
|
+
* So the budget is a fixed constant rather than a function of how much work is
|
|
2952
|
+
* in flight. Steady state is one connection (the multiplexed projection
|
|
2953
|
+
* carrier); the peak is `PLUGIN_GLOBAL_PERMITS`, whatever the session count.
|
|
2954
|
+
*
|
|
2955
|
+
* Both halves read this file, which is the point: a route declares a budget
|
|
2956
|
+
* class and the client derives its wait from the same entry, so a server
|
|
2957
|
+
* deadline can never be quietly longer than the client's patience.
|
|
2958
|
+
*/
|
|
2959
|
+
/** Server-side budget classes. A route declares a class, never a number. */
|
|
2960
|
+
const ROUTE_BUDGET_MS = {
|
|
2961
|
+
/** Answers from memory or a single bounded probe. */
|
|
2962
|
+
fast: 5e3,
|
|
2963
|
+
/** Chains local Git work. */
|
|
2964
|
+
git: 45e3,
|
|
2965
|
+
/** Reaches the network: remote Git, `gh`, the npm registry. */
|
|
2966
|
+
remote: 15e4
|
|
2967
|
+
};
|
|
2968
|
+
//#endregion
|
|
2764
2969
|
//#region src/http.ts
|
|
2765
2970
|
/** Accept only loopback, same-origin browser requests to plugin-private routes. */
|
|
2766
2971
|
function trustedRequest(req) {
|
|
@@ -2789,6 +2994,147 @@ function json(res, status, value) {
|
|
|
2789
2994
|
});
|
|
2790
2995
|
res.end(JSON.stringify(value));
|
|
2791
2996
|
}
|
|
2997
|
+
const ROUTE_TIMEOUT_CODE = "DSH_CLAUDE_ROUTE";
|
|
2998
|
+
const DEFAULT_BODY_BYTES = 1048576;
|
|
2999
|
+
/** Bounds live streaming responses per path, so a teardown bug cannot become a
|
|
3000
|
+
* permanent connection leak. Registration is per path, not global, because a
|
|
3001
|
+
* wedged projection stream must not evict an in-flight repository setup. */
|
|
3002
|
+
var StreamRegistry = class {
|
|
3003
|
+
#open = /* @__PURE__ */ new Map();
|
|
3004
|
+
admit(path, key, max, close) {
|
|
3005
|
+
let live = this.#open.get(path);
|
|
3006
|
+
if (live === void 0) {
|
|
3007
|
+
live = /* @__PURE__ */ new Map();
|
|
3008
|
+
this.#open.set(path, live);
|
|
3009
|
+
}
|
|
3010
|
+
live.get(key)?.();
|
|
3011
|
+
live.set(key, close);
|
|
3012
|
+
while (live.size > max) {
|
|
3013
|
+
const oldest = live.keys().next();
|
|
3014
|
+
if (oldest.done === true) break;
|
|
3015
|
+
const evict = live.get(oldest.value);
|
|
3016
|
+
live.delete(oldest.value);
|
|
3017
|
+
evict?.();
|
|
3018
|
+
}
|
|
3019
|
+
return () => {
|
|
3020
|
+
const current = this.#open.get(path);
|
|
3021
|
+
if (current?.get(key) === close) current.delete(key);
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
const streams = new StreamRegistry();
|
|
3026
|
+
function isPluginMethod(value) {
|
|
3027
|
+
return value === "GET" || value === "POST" || value === "PATCH" || value === "DELETE";
|
|
3028
|
+
}
|
|
3029
|
+
/** Distinguishable so the wrapper can answer 413 rather than folding an
|
|
3030
|
+
* oversized body into whatever generic failure the handler reports. */
|
|
3031
|
+
var PluginBodyTooLargeError = class extends Error {
|
|
3032
|
+
constructor() {
|
|
3033
|
+
super("Request body is too large");
|
|
3034
|
+
this.name = "PluginBodyTooLargeError";
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
async function readBody(req, maxBytes) {
|
|
3038
|
+
const declared = Number(req.headers["content-length"] ?? 0);
|
|
3039
|
+
if (!Number.isFinite(declared) || declared < 0 || declared > maxBytes) throw new PluginBodyTooLargeError();
|
|
3040
|
+
const chunks = [];
|
|
3041
|
+
let bytes = 0;
|
|
3042
|
+
for await (const chunk of req) {
|
|
3043
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3044
|
+
bytes += data.byteLength;
|
|
3045
|
+
if (bytes > maxBytes) throw new PluginBodyTooLargeError();
|
|
3046
|
+
chunks.push(data);
|
|
3047
|
+
}
|
|
3048
|
+
if (bytes === 0) return {};
|
|
3049
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3050
|
+
}
|
|
3051
|
+
function rejectOn(signal) {
|
|
3052
|
+
return new Promise((_resolve, reject) => {
|
|
3053
|
+
if (signal.aborted) {
|
|
3054
|
+
reject(signal.reason);
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
/**
|
|
3061
|
+
* Register one plugin route.
|
|
3062
|
+
*
|
|
3063
|
+
* This is the only place in the package that calls `ctx.webServer.register`,
|
|
3064
|
+
* and the ordering inside it is the fix rather than an implementation detail:
|
|
3065
|
+
* the disconnect listeners are attached before the first `await`, so a client
|
|
3066
|
+
* that goes away while the handler is still assembling its first response
|
|
3067
|
+
* still tears the route's work down. The previous per-route code attached
|
|
3068
|
+
* `res.on('close')` after awaiting an unbounded repository probe, which is how
|
|
3069
|
+
* the projection stream reached 53 opens against 33 closes.
|
|
3070
|
+
*/
|
|
3071
|
+
function registerPluginRoute(ctx, route) {
|
|
3072
|
+
const label = `dsh-claude: ${route.path}`;
|
|
3073
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3074
|
+
kind: route.kind,
|
|
3075
|
+
path: route.path,
|
|
3076
|
+
handler: async (req, res) => {
|
|
3077
|
+
const method = req.method;
|
|
3078
|
+
if (!isPluginMethod(method) || !route.methods.includes(method)) return json(res, 405, { error: "method not allowed" });
|
|
3079
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3080
|
+
const aborted = new AbortController();
|
|
3081
|
+
const abort = () => {
|
|
3082
|
+
aborted.abort();
|
|
3083
|
+
};
|
|
3084
|
+
req.on("close", () => {
|
|
3085
|
+
if (!req.complete) abort();
|
|
3086
|
+
});
|
|
3087
|
+
res.on("close", abort);
|
|
3088
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3089
|
+
if (route.mode === "stream") {
|
|
3090
|
+
const release = streams.admit(route.path, route.streamKey(url), route.maxConcurrent, abort);
|
|
3091
|
+
const io = {
|
|
3092
|
+
signal: aborted.signal,
|
|
3093
|
+
method,
|
|
3094
|
+
url,
|
|
3095
|
+
body: async (maxBytes = DEFAULT_BODY_BYTES) => await readBody(req, maxBytes)
|
|
3096
|
+
};
|
|
3097
|
+
try {
|
|
3098
|
+
await route.handler(res, io);
|
|
3099
|
+
} catch (error) {
|
|
3100
|
+
if (!res.headersSent) json(res, 500, { error: "stream-failed" });
|
|
3101
|
+
ctx.logger.warn(`dsh-claude: ${route.path} stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3102
|
+
} finally {
|
|
3103
|
+
release();
|
|
3104
|
+
res.end();
|
|
3105
|
+
}
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const budgetMs = ROUTE_BUDGET_MS[route.budget];
|
|
3109
|
+
const bounded = deadline(aborted.signal, budgetMs, ROUTE_TIMEOUT_CODE);
|
|
3110
|
+
const io = {
|
|
3111
|
+
signal: bounded.signal,
|
|
3112
|
+
method,
|
|
3113
|
+
url,
|
|
3114
|
+
body: async (maxBytes = DEFAULT_BODY_BYTES) => await readBody(req, maxBytes)
|
|
3115
|
+
};
|
|
3116
|
+
try {
|
|
3117
|
+
const result = await Promise.race([route.handler(io), rejectOn(bounded.signal)]);
|
|
3118
|
+
if (!res.writableEnded) json(res, result.status, result.value);
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
if (res.writableEnded || res.headersSent) return;
|
|
3121
|
+
if (bounded.signal.aborted && !aborted.signal.aborted) return json(res, 504, {
|
|
3122
|
+
error: "deadline",
|
|
3123
|
+
budget: route.budget,
|
|
3124
|
+
ms: budgetMs
|
|
3125
|
+
});
|
|
3126
|
+
if (aborted.signal.aborted) return;
|
|
3127
|
+
if (error instanceof PluginBodyTooLargeError) return json(res, 413, {
|
|
3128
|
+
error: "body-too-large",
|
|
3129
|
+
message: "The request body is too large."
|
|
3130
|
+
});
|
|
3131
|
+
json(res, 400, { error: error instanceof Error ? error.message : "request failed" });
|
|
3132
|
+
} finally {
|
|
3133
|
+
bounded[Symbol.dispose]();
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
}), label);
|
|
3137
|
+
}
|
|
2792
3138
|
//#endregion
|
|
2793
3139
|
//#region src/doctor-routes.ts
|
|
2794
3140
|
const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
|
|
@@ -2828,60 +3174,70 @@ function commandDiagnostics(ctx) {
|
|
|
2828
3174
|
}
|
|
2829
3175
|
}
|
|
2830
3176
|
function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
|
|
2831
|
-
ctx
|
|
3177
|
+
registerPluginRoute(ctx, {
|
|
3178
|
+
mode: "unary",
|
|
2832
3179
|
kind: "exact",
|
|
2833
3180
|
path: CLAUDE_DOCTOR_PATH,
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3181
|
+
methods: ["GET"],
|
|
3182
|
+
budget: "git",
|
|
3183
|
+
handler: async (io) => {
|
|
2837
3184
|
try {
|
|
2838
|
-
if (resolutionError !== void 0) return
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
"
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3185
|
+
if (resolutionError !== void 0) return {
|
|
3186
|
+
status: 200,
|
|
3187
|
+
value: {
|
|
3188
|
+
executable: {
|
|
3189
|
+
status: "missing",
|
|
3190
|
+
searched: config.executablePath.length > 0 ? [config.executablePath] : [
|
|
3191
|
+
"claude",
|
|
3192
|
+
"~/.local/bin/claude",
|
|
3193
|
+
"/opt/homebrew/bin/claude",
|
|
3194
|
+
"/usr/local/bin/claude"
|
|
3195
|
+
]
|
|
3196
|
+
},
|
|
3197
|
+
version: { status: "not-run" },
|
|
3198
|
+
authentication: { status: "not-run" },
|
|
3199
|
+
handshake: "not-run",
|
|
3200
|
+
message: safeMessage$2(resolutionError),
|
|
3201
|
+
limits: {
|
|
3202
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
3203
|
+
maxProcesses: config.maxProcesses
|
|
3204
|
+
},
|
|
3205
|
+
processes: {
|
|
3206
|
+
count: 0,
|
|
3207
|
+
active: 0
|
|
3208
|
+
}
|
|
2859
3209
|
}
|
|
2860
|
-
}
|
|
3210
|
+
};
|
|
2861
3211
|
const report = await runClaudeDoctor(runtime, {
|
|
2862
3212
|
configuredPath: config.executablePath,
|
|
2863
3213
|
cwd: process.cwd(),
|
|
2864
|
-
signal: AbortSignal.timeout(CLAUDE_DOCTOR_PROBE_TIMEOUT_MS)
|
|
3214
|
+
signal: AbortSignal.any([io.signal, AbortSignal.timeout(CLAUDE_DOCTOR_PROBE_TIMEOUT_MS)])
|
|
2865
3215
|
});
|
|
2866
3216
|
const processes = supervisor.snapshots();
|
|
2867
3217
|
if (processes.some((process) => process.claudeSessionId !== void 0)) report.handshake = "ok";
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3218
|
+
return {
|
|
3219
|
+
status: 200,
|
|
3220
|
+
value: {
|
|
3221
|
+
...report,
|
|
3222
|
+
limits: {
|
|
3223
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
3224
|
+
maxProcesses: config.maxProcesses
|
|
3225
|
+
},
|
|
3226
|
+
processes: {
|
|
3227
|
+
count: processes.length,
|
|
3228
|
+
active: processes.filter((process) => process.state === "running" || process.state === "starting").length
|
|
3229
|
+
},
|
|
3230
|
+
commandBridge: commandDiagnostics(ctx)
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
2880
3233
|
} catch (error) {
|
|
2881
|
-
|
|
3234
|
+
return {
|
|
3235
|
+
status: 500,
|
|
3236
|
+
value: { error: safeMessage$2(error) }
|
|
3237
|
+
};
|
|
2882
3238
|
}
|
|
2883
3239
|
}
|
|
2884
|
-
})
|
|
3240
|
+
});
|
|
2885
3241
|
}
|
|
2886
3242
|
//#endregion
|
|
2887
3243
|
//#region src/projection-routes.ts
|
|
@@ -2889,21 +3245,34 @@ const MAX_SESSION_ID_CHARS$6 = 1024;
|
|
|
2889
3245
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2890
3246
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2891
3247
|
const META_REFRESH_MS = 5e3;
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3248
|
+
/** The multiplexed carrier's path segment. Session ids are `session-<uuid>`,
|
|
3249
|
+
* so this cannot collide with one. */
|
|
3250
|
+
const MULTI_SEGMENT = "multi";
|
|
3251
|
+
function validSessionId(value) {
|
|
3252
|
+
return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$6;
|
|
3253
|
+
}
|
|
3254
|
+
function targetFromUrl(url) {
|
|
3255
|
+
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
3256
|
+
if (!url.pathname.startsWith(prefix)) return void 0;
|
|
3257
|
+
const encoded = url.pathname.slice(prefix.length);
|
|
3258
|
+
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
3259
|
+
if (encoded === MULTI_SEGMENT) {
|
|
3260
|
+
const raw = url.searchParams.get("sessions");
|
|
3261
|
+
if (raw === null) return void 0;
|
|
3262
|
+
const sessionIds = [...new Set(raw.split(",").filter((id) => id.length > 0))];
|
|
3263
|
+
if (sessionIds.length === 0 || sessionIds.length > 16) return void 0;
|
|
3264
|
+
if (!sessionIds.every(validSessionId)) return void 0;
|
|
2903
3265
|
return {
|
|
2904
|
-
|
|
2905
|
-
|
|
3266
|
+
kind: "multi",
|
|
3267
|
+
sessionIds
|
|
2906
3268
|
};
|
|
3269
|
+
}
|
|
3270
|
+
try {
|
|
3271
|
+
const sessionId = decodeURIComponent(encoded);
|
|
3272
|
+
return validSessionId(sessionId) ? {
|
|
3273
|
+
kind: "snapshot",
|
|
3274
|
+
sessionId
|
|
3275
|
+
} : void 0;
|
|
2907
3276
|
} catch {
|
|
2908
3277
|
return;
|
|
2909
3278
|
}
|
|
@@ -2923,9 +3292,16 @@ function envelope(projection, meta) {
|
|
|
2923
3292
|
};
|
|
2924
3293
|
}
|
|
2925
3294
|
/** Register the browser-readable, credential-free sidecar projection endpoint.
|
|
2926
|
-
*
|
|
2927
|
-
*
|
|
2928
|
-
*
|
|
3295
|
+
*
|
|
3296
|
+
* `GET <path>/:sessionId` returns one snapshot. `GET <path>/multi?sessions=a,b`
|
|
3297
|
+
* returns ONE NDJSON stream carrying every listed session, each line stamped
|
|
3298
|
+
* with its `session`.
|
|
3299
|
+
*
|
|
3300
|
+
* There is deliberately no per-session stream URL. The browser shares a small
|
|
3301
|
+
* fixed connection budget between this plugin and the Host, and a stream per
|
|
3302
|
+
* session spent it in proportion to how many Claude sessions existed — which
|
|
3303
|
+
* is what left the settings panel unable to get a connection at all. One
|
|
3304
|
+
* carrier is one connection, whatever the session count. */
|
|
2929
3305
|
function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
|
|
2930
3306
|
const info = (message) => {
|
|
2931
3307
|
ctx.logger?.info?.(message);
|
|
@@ -2940,15 +3316,14 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2940
3316
|
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
2941
3317
|
};
|
|
2942
3318
|
};
|
|
2943
|
-
const
|
|
2944
|
-
info(`dsh-claude: projection stream opened for ${
|
|
3319
|
+
const streamMulti = async (res, io, sessionIds) => {
|
|
3320
|
+
info(`dsh-claude: projection stream opened for ${sessionIds.length} session(s)`);
|
|
2945
3321
|
let textDeltas = 0;
|
|
2946
3322
|
let textBytes = 0;
|
|
2947
3323
|
let textSince = Date.now();
|
|
2948
|
-
let meta = await assembleMeta(sessionId);
|
|
2949
3324
|
res.writeHead(200, {
|
|
2950
3325
|
"content-type": "application/x-ndjson; charset=utf-8",
|
|
2951
|
-
"cache-control": "no-store",
|
|
3326
|
+
"cache-control": "no-store, no-transform",
|
|
2952
3327
|
"x-content-type-options": "nosniff"
|
|
2953
3328
|
});
|
|
2954
3329
|
res.flushHeaders?.();
|
|
@@ -2961,102 +3336,146 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2961
3336
|
closed = true;
|
|
2962
3337
|
}
|
|
2963
3338
|
};
|
|
2964
|
-
const
|
|
2965
|
-
|
|
3339
|
+
const metas = /* @__PURE__ */ new Map();
|
|
3340
|
+
const writeSnapshot = async (sessionId) => {
|
|
3341
|
+
const [projection, meta] = await Promise.all([sidecar.read(sessionId), assembleMeta(sessionId)]);
|
|
3342
|
+
metas.set(sessionId, meta);
|
|
2966
3343
|
writeLine({
|
|
2967
3344
|
type: "snapshot",
|
|
3345
|
+
session: sessionId,
|
|
3346
|
+
seq: sidecar.sequence(sessionId),
|
|
2968
3347
|
...envelope(projection, meta)
|
|
2969
3348
|
});
|
|
2970
3349
|
};
|
|
2971
|
-
|
|
2972
|
-
const unsubscribe = sidecar.subscribe(sessionId, (delta) => {
|
|
3350
|
+
const unsubscribes = sessionIds.map((sessionId) => sidecar.subscribe(sessionId, (delta) => {
|
|
2973
3351
|
switch (delta.kind) {
|
|
2974
3352
|
case "text":
|
|
2975
3353
|
textDeltas += 1;
|
|
2976
3354
|
textBytes += (delta.append ?? delta.text ?? "").length;
|
|
2977
3355
|
if (textDeltas % 25 === 0) {
|
|
2978
3356
|
const elapsed = Date.now() - textSince;
|
|
2979
|
-
info(`dsh-claude: stream
|
|
3357
|
+
info(`dsh-claude: stream 25 text deltas ${textBytes}B in ${elapsed}ms`);
|
|
2980
3358
|
textBytes = 0;
|
|
2981
3359
|
textSince = Date.now();
|
|
2982
3360
|
}
|
|
2983
3361
|
writeLine({
|
|
2984
3362
|
type: "text",
|
|
3363
|
+
session: sessionId,
|
|
2985
3364
|
turn: delta.turn,
|
|
2986
3365
|
step: delta.step,
|
|
2987
3366
|
ordinal: delta.ordinal,
|
|
2988
3367
|
...delta.append === void 0 ? {} : { append: delta.append },
|
|
2989
|
-
...delta.text === void 0 ? {} : { text: delta.text }
|
|
3368
|
+
...delta.text === void 0 ? {} : { text: delta.text },
|
|
3369
|
+
...delta.renderer === void 0 ? {} : { renderer: delta.renderer },
|
|
3370
|
+
seq: delta.seq
|
|
2990
3371
|
});
|
|
2991
3372
|
return;
|
|
2992
3373
|
case "activity":
|
|
2993
3374
|
writeLine({
|
|
2994
3375
|
type: "activity",
|
|
2995
|
-
|
|
3376
|
+
session: sessionId,
|
|
3377
|
+
activity: delta.activity,
|
|
3378
|
+
seq: delta.seq
|
|
2996
3379
|
});
|
|
2997
3380
|
return;
|
|
2998
3381
|
case "contextUsage":
|
|
2999
3382
|
writeLine({
|
|
3000
3383
|
type: "contextUsage",
|
|
3001
|
-
|
|
3384
|
+
session: sessionId,
|
|
3385
|
+
value: delta.value,
|
|
3386
|
+
seq: delta.seq
|
|
3002
3387
|
});
|
|
3003
3388
|
return;
|
|
3004
3389
|
case "tasks":
|
|
3005
3390
|
writeLine({
|
|
3006
3391
|
type: "tasks",
|
|
3007
|
-
|
|
3392
|
+
session: sessionId,
|
|
3393
|
+
value: delta.value,
|
|
3394
|
+
seq: delta.seq
|
|
3008
3395
|
});
|
|
3009
3396
|
return;
|
|
3010
|
-
case "
|
|
3397
|
+
case "checkpoint":
|
|
3398
|
+
writeLine({
|
|
3399
|
+
type: "checkpoint",
|
|
3400
|
+
session: sessionId,
|
|
3401
|
+
seq: delta.seq
|
|
3402
|
+
});
|
|
3403
|
+
return;
|
|
3404
|
+
case "sync": writeSnapshot(sessionId).catch(() => void 0);
|
|
3011
3405
|
}
|
|
3012
|
-
});
|
|
3406
|
+
}));
|
|
3407
|
+
for (const sessionId of sessionIds) writeSnapshot(sessionId).catch(() => void 0);
|
|
3013
3408
|
const timer = setInterval(() => {
|
|
3014
3409
|
(async () => {
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3410
|
+
for (const sessionId of sessionIds) {
|
|
3411
|
+
if (closed) return;
|
|
3412
|
+
const next = await assembleMeta(sessionId);
|
|
3413
|
+
if (closed) return;
|
|
3414
|
+
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
3415
|
+
metas.set(sessionId, next);
|
|
3416
|
+
writeLine({
|
|
3417
|
+
type: "meta",
|
|
3418
|
+
session: sessionId,
|
|
3419
|
+
owned: next.owned,
|
|
3420
|
+
commands: next.commands,
|
|
3421
|
+
...next.repository === void 0 ? {} : { repository: next.repository },
|
|
3422
|
+
reviewComments: next.reviewComments
|
|
3423
|
+
});
|
|
3020
3424
|
}
|
|
3021
|
-
|
|
3022
|
-
writeLine({
|
|
3023
|
-
type: "meta",
|
|
3024
|
-
owned: meta.owned,
|
|
3025
|
-
commands: meta.commands,
|
|
3026
|
-
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
3027
|
-
reviewComments: meta.reviewComments
|
|
3028
|
-
});
|
|
3425
|
+
writeLine({ type: "ping" });
|
|
3029
3426
|
})().catch(() => void 0);
|
|
3030
3427
|
}, META_REFRESH_MS);
|
|
3031
3428
|
timer.unref?.();
|
|
3032
3429
|
await new Promise((resolve) => {
|
|
3033
|
-
|
|
3430
|
+
const finish = () => {
|
|
3431
|
+
if (closed) return;
|
|
3034
3432
|
closed = true;
|
|
3035
3433
|
clearInterval(timer);
|
|
3036
|
-
unsubscribe();
|
|
3037
|
-
info(`dsh-claude: projection stream closed
|
|
3434
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
3435
|
+
info(`dsh-claude: projection stream closed after ${textDeltas} text deltas`);
|
|
3038
3436
|
resolve();
|
|
3039
|
-
}
|
|
3437
|
+
};
|
|
3438
|
+
if (io.signal.aborted) finish();
|
|
3439
|
+
else io.signal.addEventListener("abort", finish, { once: true });
|
|
3040
3440
|
});
|
|
3041
3441
|
};
|
|
3042
|
-
ctx
|
|
3442
|
+
registerPluginRoute(ctx, {
|
|
3443
|
+
mode: "stream",
|
|
3043
3444
|
kind: "prefix",
|
|
3044
3445
|
path: CLAUDE_PROJECTION_PATH,
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3446
|
+
methods: ["GET"],
|
|
3447
|
+
maxConcurrent: 1,
|
|
3448
|
+
streamKey: () => MULTI_SEGMENT,
|
|
3449
|
+
handler: async (res, io) => {
|
|
3450
|
+
const target = targetFromUrl(io.url);
|
|
3451
|
+
if (target === void 0) {
|
|
3452
|
+
res.writeHead(400, {
|
|
3453
|
+
"content-type": "application/json; charset=utf-8",
|
|
3454
|
+
"cache-control": "no-store"
|
|
3455
|
+
});
|
|
3456
|
+
res.write(JSON.stringify({ error: "invalid session id" }));
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
if (target.kind === "multi") return await streamMulti(res, io, target.sessionIds);
|
|
3460
|
+
info(`dsh-claude: projection poll for ${target.sessionId.slice(0, 64)}`);
|
|
3050
3461
|
try {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3462
|
+
const body = envelope(await sidecar.read(target.sessionId), await assembleMeta(target.sessionId));
|
|
3463
|
+
res.writeHead(200, {
|
|
3464
|
+
"content-type": "application/json; charset=utf-8",
|
|
3465
|
+
"cache-control": "no-store",
|
|
3466
|
+
"x-content-type-options": "nosniff"
|
|
3467
|
+
});
|
|
3468
|
+
res.write(JSON.stringify(body));
|
|
3054
3469
|
} catch {
|
|
3055
|
-
if (
|
|
3056
|
-
res.
|
|
3470
|
+
if (res.headersSent) return;
|
|
3471
|
+
res.writeHead(500, {
|
|
3472
|
+
"content-type": "application/json; charset=utf-8",
|
|
3473
|
+
"cache-control": "no-store"
|
|
3474
|
+
});
|
|
3475
|
+
res.write(JSON.stringify({ error: "projection unavailable" }));
|
|
3057
3476
|
}
|
|
3058
3477
|
}
|
|
3059
|
-
})
|
|
3478
|
+
});
|
|
3060
3479
|
}
|
|
3061
3480
|
//#endregion
|
|
3062
3481
|
//#region src/repository-status.ts
|
|
@@ -3235,10 +3654,13 @@ var RepositoryStatusService = class {
|
|
|
3235
3654
|
this.#runtime = runtime;
|
|
3236
3655
|
this.#cacheTtlMs = cacheTtlMs;
|
|
3237
3656
|
}
|
|
3238
|
-
|
|
3657
|
+
/** `signal` bounds the scan itself, not just the caller's patience: the
|
|
3658
|
+
* untracked-file diff below is a serial spawn loop that can outlive any
|
|
3659
|
+
* route budget, and work nobody is waiting for still occupies the process. */
|
|
3660
|
+
inspect(cwd, signal) {
|
|
3239
3661
|
const current = this.#cache.get(cwd);
|
|
3240
3662
|
if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
|
|
3241
|
-
const value = this.#inspect(cwd).then((next) => this.#stabilize(cwd, next));
|
|
3663
|
+
const value = this.#inspect(cwd, signal).then((next) => this.#stabilize(cwd, next));
|
|
3242
3664
|
this.#cache.set(cwd, {
|
|
3243
3665
|
expiresAt: Date.now() + this.#cacheTtlMs,
|
|
3244
3666
|
value
|
|
@@ -3296,7 +3718,7 @@ var RepositoryStatusService = class {
|
|
|
3296
3718
|
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => void 0);
|
|
3297
3719
|
return this.#ghExecutable;
|
|
3298
3720
|
}
|
|
3299
|
-
async #inspect(cwd) {
|
|
3721
|
+
async #inspect(cwd, signal) {
|
|
3300
3722
|
const safeCwd = bounded(cwd);
|
|
3301
3723
|
let git;
|
|
3302
3724
|
try {
|
|
@@ -3346,7 +3768,7 @@ var RepositoryStatusService = class {
|
|
|
3346
3768
|
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
3347
3769
|
const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
|
|
3348
3770
|
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
3349
|
-
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase) : {
|
|
3771
|
+
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase, signal) : {
|
|
3350
3772
|
additions: 0,
|
|
3351
3773
|
deletions: 0,
|
|
3352
3774
|
files: 0,
|
|
@@ -3400,7 +3822,7 @@ var RepositoryStatusService = class {
|
|
|
3400
3822
|
return;
|
|
3401
3823
|
}
|
|
3402
3824
|
}
|
|
3403
|
-
async #diff(cwd, git, base) {
|
|
3825
|
+
async #diff(cwd, git, base, signal) {
|
|
3404
3826
|
try {
|
|
3405
3827
|
const numstat = await run(this.#runtime, git, [
|
|
3406
3828
|
"diff",
|
|
@@ -3423,7 +3845,7 @@ var RepositoryStatusService = class {
|
|
|
3423
3845
|
...summary,
|
|
3424
3846
|
truncated: true
|
|
3425
3847
|
};
|
|
3426
|
-
const untracked = await this.#untrackedDiff(cwd, git);
|
|
3848
|
+
const untracked = await this.#untrackedDiff(cwd, git, signal);
|
|
3427
3849
|
const combinedPatch = `${patch.stdout}${untracked.patch}`;
|
|
3428
3850
|
return {
|
|
3429
3851
|
additions: summary.additions + untracked.additions,
|
|
@@ -3436,7 +3858,7 @@ var RepositoryStatusService = class {
|
|
|
3436
3858
|
return;
|
|
3437
3859
|
}
|
|
3438
3860
|
}
|
|
3439
|
-
async #untrackedDiff(cwd, git) {
|
|
3861
|
+
async #untrackedDiff(cwd, git, signal) {
|
|
3440
3862
|
const listed = await run(this.#runtime, git, [
|
|
3441
3863
|
"ls-files",
|
|
3442
3864
|
"--others",
|
|
@@ -3454,6 +3876,12 @@ var RepositoryStatusService = class {
|
|
|
3454
3876
|
let patch = "";
|
|
3455
3877
|
let truncated = paths.length > MAX_UNTRACKED_DIFFS;
|
|
3456
3878
|
for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
|
|
3879
|
+
if (signal?.aborted === true) return {
|
|
3880
|
+
additions,
|
|
3881
|
+
files: paths.length,
|
|
3882
|
+
patch,
|
|
3883
|
+
truncated: true
|
|
3884
|
+
};
|
|
3457
3885
|
const result = await run(this.#runtime, git, [
|
|
3458
3886
|
"diff",
|
|
3459
3887
|
"--no-ext-diff",
|
|
@@ -4445,16 +4873,15 @@ const MAX_BODY_BYTES$6 = 16384;
|
|
|
4445
4873
|
function record$9(value) {
|
|
4446
4874
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4447
4875
|
}
|
|
4448
|
-
async function readJson$
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
chunks.push(buffer);
|
|
4876
|
+
async function readJson$5(io) {
|
|
4877
|
+
let parsed;
|
|
4878
|
+
try {
|
|
4879
|
+
parsed = await io.body(MAX_BODY_BYTES$6);
|
|
4880
|
+
} catch (error) {
|
|
4881
|
+
if (error instanceof SyntaxError) throw error;
|
|
4882
|
+
throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
4456
4883
|
}
|
|
4457
|
-
const value = record$9(
|
|
4884
|
+
const value = record$9(parsed);
|
|
4458
4885
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
4459
4886
|
return value;
|
|
4460
4887
|
}
|
|
@@ -4505,41 +4932,48 @@ async function streamSetup(res, service, input) {
|
|
|
4505
4932
|
res.end();
|
|
4506
4933
|
}
|
|
4507
4934
|
}
|
|
4508
|
-
/** Register trusted browser routes for safe pre-session Git setup.
|
|
4935
|
+
/** Register trusted browser routes for safe pre-session Git setup.
|
|
4936
|
+
*
|
|
4937
|
+
* The prefix is registered as a stream because the setup POST holds its
|
|
4938
|
+
* connection open for the whole worktree build; the short sibling paths ride
|
|
4939
|
+
* the same registration and answer with `json` before releasing it. */
|
|
4509
4940
|
function registerRepositorySetupRoute(ctx, service) {
|
|
4510
|
-
ctx
|
|
4941
|
+
registerPluginRoute(ctx, {
|
|
4942
|
+
mode: "stream",
|
|
4511
4943
|
kind: "prefix",
|
|
4512
4944
|
path: CLAUDE_REPOSITORY_SETUP_PATH,
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4945
|
+
methods: ["GET", "POST"],
|
|
4946
|
+
maxConcurrent: 2,
|
|
4947
|
+
streamKey: (url) => `${url.pathname}?${url.searchParams.get("cwd") ?? url.searchParams.get("branch") ?? ""}`,
|
|
4948
|
+
handler: async (res, io) => {
|
|
4949
|
+
const pathname = io.url.pathname;
|
|
4516
4950
|
try {
|
|
4517
4951
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
|
|
4518
|
-
if (
|
|
4519
|
-
const input = await readJson$
|
|
4952
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4953
|
+
const input = await readJson$5(io);
|
|
4520
4954
|
return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
|
|
4521
4955
|
}
|
|
4522
4956
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
|
|
4523
|
-
if (
|
|
4524
|
-
const cwd =
|
|
4957
|
+
if (io.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4958
|
+
const cwd = io.url.searchParams.get("cwd");
|
|
4525
4959
|
if (cwd === null) throw new RepositorySetupError("invalid-request", "The cwd query parameter is required.");
|
|
4526
4960
|
return json(res, 200, await service.listBranches(cwd));
|
|
4527
4961
|
}
|
|
4528
4962
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
4529
|
-
if (
|
|
4530
|
-
const input = await readJson$
|
|
4963
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4964
|
+
const input = await readJson$5(io);
|
|
4531
4965
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
4532
4966
|
await streamSetup(res, service, input);
|
|
4533
4967
|
return;
|
|
4534
4968
|
}
|
|
4535
4969
|
if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
|
|
4536
|
-
if (
|
|
4537
|
-
const input = await readJson$
|
|
4970
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4971
|
+
const input = await readJson$5(io);
|
|
4538
4972
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
4539
4973
|
}
|
|
4540
4974
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
4541
|
-
if (
|
|
4542
|
-
const input = await readJson$
|
|
4975
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4976
|
+
const input = await readJson$5(io);
|
|
4543
4977
|
await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
|
|
4544
4978
|
return json(res, 200, { ok: true });
|
|
4545
4979
|
}
|
|
@@ -4553,7 +4987,7 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4553
4987
|
return json(res, 500, { error: "repository setup unavailable" });
|
|
4554
4988
|
}
|
|
4555
4989
|
}
|
|
4556
|
-
})
|
|
4990
|
+
});
|
|
4557
4991
|
}
|
|
4558
4992
|
//#endregion
|
|
4559
4993
|
//#region src/repository-action-routes.ts
|
|
@@ -4570,16 +5004,15 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
4570
5004
|
function record$8(value) {
|
|
4571
5005
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4572
5006
|
}
|
|
4573
|
-
async function readJson$
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
chunks.push(buffer);
|
|
5007
|
+
async function readJson$4(io) {
|
|
5008
|
+
let parsed;
|
|
5009
|
+
try {
|
|
5010
|
+
parsed = await io.body(MAX_BODY_BYTES$5);
|
|
5011
|
+
} catch (error) {
|
|
5012
|
+
if (error instanceof SyntaxError) throw error;
|
|
5013
|
+
throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
4581
5014
|
}
|
|
4582
|
-
const value = record$8(
|
|
5015
|
+
const value = record$8(parsed);
|
|
4583
5016
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
4584
5017
|
return value;
|
|
4585
5018
|
}
|
|
@@ -4618,44 +5051,80 @@ function actionRequest(input) {
|
|
|
4618
5051
|
})()
|
|
4619
5052
|
};
|
|
4620
5053
|
}
|
|
5054
|
+
/** One prefix serves the local-Git preview and the POST arms that push, open
|
|
5055
|
+
* and merge pull requests, so the registration takes the wider of the two
|
|
5056
|
+
* budgets: a `remote` arm cut short at the `git` deadline would abandon work
|
|
5057
|
+
* that had already reached GitHub. */
|
|
4621
5058
|
function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
4622
|
-
ctx
|
|
5059
|
+
registerPluginRoute(ctx, {
|
|
5060
|
+
mode: "unary",
|
|
4623
5061
|
kind: "prefix",
|
|
4624
5062
|
path: CLAUDE_REPOSITORY_ACTION_PATH,
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
5063
|
+
methods: ["GET", "POST"],
|
|
5064
|
+
budget: "remote",
|
|
5065
|
+
handler: async (io) => {
|
|
5066
|
+
const url = io.url;
|
|
4628
5067
|
try {
|
|
4629
5068
|
const cwd = cwdForSession(sessionId$1(url));
|
|
4630
5069
|
if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
|
|
4631
5070
|
if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
|
|
4632
|
-
if (
|
|
4633
|
-
|
|
5071
|
+
if (io.method !== "GET") return {
|
|
5072
|
+
status: 405,
|
|
5073
|
+
value: { error: "method not allowed" }
|
|
5074
|
+
};
|
|
5075
|
+
return {
|
|
5076
|
+
status: 200,
|
|
5077
|
+
value: await service.preview(cwd)
|
|
5078
|
+
};
|
|
4634
5079
|
}
|
|
4635
5080
|
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
4636
|
-
if (
|
|
4637
|
-
|
|
4638
|
-
|
|
5081
|
+
if (io.method !== "POST") return {
|
|
5082
|
+
status: 405,
|
|
5083
|
+
value: { error: "method not allowed" }
|
|
5084
|
+
};
|
|
5085
|
+
const input = await readJson$4(io);
|
|
5086
|
+
return {
|
|
5087
|
+
status: 200,
|
|
5088
|
+
value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
|
|
5089
|
+
};
|
|
4639
5090
|
}
|
|
4640
5091
|
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
4641
|
-
if (
|
|
4642
|
-
|
|
5092
|
+
if (io.method !== "POST") return {
|
|
5093
|
+
status: 405,
|
|
5094
|
+
value: { error: "method not allowed" }
|
|
5095
|
+
};
|
|
5096
|
+
return {
|
|
5097
|
+
status: 200,
|
|
5098
|
+
value: await service.execute(cwd, actionRequest(await readJson$4(io)))
|
|
5099
|
+
};
|
|
4643
5100
|
}
|
|
4644
|
-
return
|
|
5101
|
+
return {
|
|
5102
|
+
status: 404,
|
|
5103
|
+
value: { error: "not found" }
|
|
5104
|
+
};
|
|
4645
5105
|
} catch (error) {
|
|
4646
|
-
if (error instanceof RepositoryActionError) return
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
5106
|
+
if (error instanceof RepositoryActionError) return {
|
|
5107
|
+
status: 409,
|
|
5108
|
+
value: {
|
|
5109
|
+
error: error.code,
|
|
5110
|
+
message: error.message,
|
|
5111
|
+
...error.commit === void 0 ? {} : { commit: error.commit }
|
|
5112
|
+
}
|
|
5113
|
+
};
|
|
5114
|
+
if (error instanceof SyntaxError) return {
|
|
5115
|
+
status: 400,
|
|
5116
|
+
value: { error: "invalid-json" }
|
|
5117
|
+
};
|
|
5118
|
+
return {
|
|
5119
|
+
status: 500,
|
|
5120
|
+
value: {
|
|
5121
|
+
error: "repository-action-unavailable",
|
|
5122
|
+
message: "Repository action is unavailable."
|
|
5123
|
+
}
|
|
5124
|
+
};
|
|
4656
5125
|
}
|
|
4657
5126
|
}
|
|
4658
|
-
})
|
|
5127
|
+
});
|
|
4659
5128
|
}
|
|
4660
5129
|
//#endregion
|
|
4661
5130
|
//#region src/editor-open.ts
|
|
@@ -4781,43 +5250,62 @@ const MAX_SESSION_ID_CHARS$4 = 1024;
|
|
|
4781
5250
|
/** Open the session's working directory in a desktop editor. Query-only: the
|
|
4782
5251
|
* request carries two enum-ish values, so there is no body to parse. */
|
|
4783
5252
|
function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
4784
|
-
ctx
|
|
5253
|
+
registerPluginRoute(ctx, {
|
|
5254
|
+
mode: "unary",
|
|
4785
5255
|
kind: "exact",
|
|
4786
5256
|
path: CLAUDE_EDITOR_OPEN_PATH,
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
const params =
|
|
5257
|
+
methods: ["POST"],
|
|
5258
|
+
budget: "fast",
|
|
5259
|
+
handler: async (io) => {
|
|
5260
|
+
const params = io.url.searchParams;
|
|
4791
5261
|
const id = params.get("sessionId");
|
|
4792
5262
|
const editor = params.get("editor");
|
|
4793
|
-
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
5263
|
+
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return {
|
|
5264
|
+
status: 400,
|
|
5265
|
+
value: {
|
|
5266
|
+
error: "invalid-session",
|
|
5267
|
+
message: "The session is invalid."
|
|
5268
|
+
}
|
|
5269
|
+
};
|
|
5270
|
+
if (editor === null || !EDITOR_IDS.has(editor)) return {
|
|
5271
|
+
status: 400,
|
|
5272
|
+
value: {
|
|
5273
|
+
error: "invalid-editor",
|
|
5274
|
+
message: "The editor is invalid."
|
|
5275
|
+
}
|
|
5276
|
+
};
|
|
4801
5277
|
const cwd = cwdForSession(id);
|
|
4802
|
-
if (cwd === void 0) return
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
5278
|
+
if (cwd === void 0) return {
|
|
5279
|
+
status: 409,
|
|
5280
|
+
value: {
|
|
5281
|
+
error: "session-unavailable",
|
|
5282
|
+
message: "The Claude session is unavailable."
|
|
5283
|
+
}
|
|
5284
|
+
};
|
|
4806
5285
|
try {
|
|
4807
5286
|
await service.open(cwd, editor);
|
|
4808
|
-
return
|
|
5287
|
+
return {
|
|
5288
|
+
status: 200,
|
|
5289
|
+
value: { opened: true }
|
|
5290
|
+
};
|
|
4809
5291
|
} catch (error) {
|
|
4810
|
-
if (error instanceof EditorOpenError) return
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
5292
|
+
if (error instanceof EditorOpenError) return {
|
|
5293
|
+
status: 409,
|
|
5294
|
+
value: {
|
|
5295
|
+
error: error.code,
|
|
5296
|
+
message: error.message
|
|
5297
|
+
}
|
|
5298
|
+
};
|
|
5299
|
+
return {
|
|
5300
|
+
status: 500,
|
|
5301
|
+
value: {
|
|
5302
|
+
error: "editor-open-unavailable",
|
|
5303
|
+
message: "The editor could not be launched."
|
|
5304
|
+
}
|
|
5305
|
+
};
|
|
4818
5306
|
}
|
|
4819
5307
|
}
|
|
4820
|
-
})
|
|
5308
|
+
});
|
|
4821
5309
|
}
|
|
4822
5310
|
//#endregion
|
|
4823
5311
|
//#region src/github-url.ts
|
|
@@ -5110,7 +5598,10 @@ var PullRequestFeedbackService = class {
|
|
|
5110
5598
|
return [];
|
|
5111
5599
|
}
|
|
5112
5600
|
}
|
|
5113
|
-
|
|
5601
|
+
/** `signal` bounds the log fetches below: each is capped on its own, but they
|
|
5602
|
+
* run one after another, so the honest worst case is their sum rather than
|
|
5603
|
+
* any single ceiling — more than a route is allowed to hold a connection. */
|
|
5604
|
+
async failingChecks(cwd, pullNumber, signal) {
|
|
5114
5605
|
const gh = await this.#gh();
|
|
5115
5606
|
const result = await this.#run(gh, [
|
|
5116
5607
|
"pr",
|
|
@@ -5132,6 +5623,10 @@ var PullRequestFeedbackService = class {
|
|
|
5132
5623
|
detailed.push(check);
|
|
5133
5624
|
continue;
|
|
5134
5625
|
}
|
|
5626
|
+
if (signal?.aborted === true) {
|
|
5627
|
+
detailed.push(check);
|
|
5628
|
+
continue;
|
|
5629
|
+
}
|
|
5135
5630
|
const log = await this.#run(gh, [
|
|
5136
5631
|
"run",
|
|
5137
5632
|
"view",
|
|
@@ -5204,16 +5699,15 @@ const MAX_MENTION_QUERY_CHARS = 64;
|
|
|
5204
5699
|
function record$6(value) {
|
|
5205
5700
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5206
5701
|
}
|
|
5207
|
-
async function readJson$
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
chunks.push(buffer);
|
|
5702
|
+
async function readJson$3(io) {
|
|
5703
|
+
let parsed;
|
|
5704
|
+
try {
|
|
5705
|
+
parsed = await io.body(MAX_BODY_BYTES$4);
|
|
5706
|
+
} catch (error) {
|
|
5707
|
+
if (error instanceof SyntaxError) throw error;
|
|
5708
|
+
throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
|
|
5215
5709
|
}
|
|
5216
|
-
const value = record$6(
|
|
5710
|
+
const value = record$6(parsed);
|
|
5217
5711
|
if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
|
|
5218
5712
|
return value;
|
|
5219
5713
|
}
|
|
@@ -5242,123 +5736,199 @@ function pullNumber(url) {
|
|
|
5242
5736
|
if (!Number.isSafeInteger(value) || value <= 0 || value > 1e9) throw new PullRequestFeedbackError("invalid-request", "The pull request number is invalid.");
|
|
5243
5737
|
return value;
|
|
5244
5738
|
}
|
|
5739
|
+
/** Every arm shells out to `gh`, so the whole prefix carries the network budget. */
|
|
5245
5740
|
function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
5246
|
-
ctx
|
|
5741
|
+
registerPluginRoute(ctx, {
|
|
5742
|
+
mode: "unary",
|
|
5247
5743
|
kind: "prefix",
|
|
5248
5744
|
path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
const
|
|
5253
|
-
const
|
|
5745
|
+
methods: ["GET", "POST"],
|
|
5746
|
+
budget: "remote",
|
|
5747
|
+
handler: async (io) => {
|
|
5748
|
+
const url = io.url;
|
|
5749
|
+
const reads = io.method === "GET";
|
|
5750
|
+
const writes = io.method === "POST";
|
|
5254
5751
|
try {
|
|
5255
5752
|
const cwd = cwdForSession(sessionId(url));
|
|
5256
5753
|
if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
|
|
5257
5754
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) {
|
|
5258
|
-
if (!reads) return
|
|
5259
|
-
|
|
5755
|
+
if (!reads) return {
|
|
5756
|
+
status: 405,
|
|
5757
|
+
value: { error: "method not allowed" }
|
|
5758
|
+
};
|
|
5759
|
+
return {
|
|
5760
|
+
status: 200,
|
|
5761
|
+
value: { threads: await service.threads(cwd, pullNumber(url)) }
|
|
5762
|
+
};
|
|
5260
5763
|
}
|
|
5261
5764
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) {
|
|
5262
|
-
if (!reads) return
|
|
5263
|
-
|
|
5765
|
+
if (!reads) return {
|
|
5766
|
+
status: 405,
|
|
5767
|
+
value: { error: "method not allowed" }
|
|
5768
|
+
};
|
|
5769
|
+
return {
|
|
5770
|
+
status: 200,
|
|
5771
|
+
value: { checks: await service.failingChecks(cwd, pullNumber(url), io.signal) }
|
|
5772
|
+
};
|
|
5264
5773
|
}
|
|
5265
5774
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/mentionables`) {
|
|
5266
|
-
if (!reads) return
|
|
5775
|
+
if (!reads) return {
|
|
5776
|
+
status: 405,
|
|
5777
|
+
value: { error: "method not allowed" }
|
|
5778
|
+
};
|
|
5267
5779
|
const query = (url.searchParams.get("q") ?? "").slice(0, MAX_MENTION_QUERY_CHARS);
|
|
5268
|
-
return
|
|
5780
|
+
return {
|
|
5781
|
+
status: 200,
|
|
5782
|
+
value: { users: await service.mentionables(cwd, query) }
|
|
5783
|
+
};
|
|
5269
5784
|
}
|
|
5270
5785
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/reply`) {
|
|
5271
|
-
if (!writes) return
|
|
5272
|
-
|
|
5273
|
-
|
|
5786
|
+
if (!writes) return {
|
|
5787
|
+
status: 405,
|
|
5788
|
+
value: { error: "method not allowed" }
|
|
5789
|
+
};
|
|
5790
|
+
const input = await readJson$3(io);
|
|
5791
|
+
return {
|
|
5792
|
+
status: 200,
|
|
5793
|
+
value: { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) }
|
|
5794
|
+
};
|
|
5274
5795
|
}
|
|
5275
5796
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/resolve`) {
|
|
5276
|
-
if (!writes) return
|
|
5277
|
-
|
|
5797
|
+
if (!writes) return {
|
|
5798
|
+
status: 405,
|
|
5799
|
+
value: { error: "method not allowed" }
|
|
5800
|
+
};
|
|
5801
|
+
const input = await readJson$3(io);
|
|
5278
5802
|
if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
|
|
5279
|
-
return
|
|
5803
|
+
return {
|
|
5804
|
+
status: 200,
|
|
5805
|
+
value: { resolved: await service.setResolved(cwd, threadId(input), input.resolved) }
|
|
5806
|
+
};
|
|
5280
5807
|
}
|
|
5281
|
-
return
|
|
5808
|
+
return {
|
|
5809
|
+
status: 404,
|
|
5810
|
+
value: { error: "not found" }
|
|
5811
|
+
};
|
|
5282
5812
|
} catch (error) {
|
|
5283
|
-
if (error instanceof PullRequestFeedbackError) return
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5813
|
+
if (error instanceof PullRequestFeedbackError) return {
|
|
5814
|
+
status: 409,
|
|
5815
|
+
value: {
|
|
5816
|
+
error: error.code,
|
|
5817
|
+
message: error.message
|
|
5818
|
+
}
|
|
5819
|
+
};
|
|
5820
|
+
if (error instanceof SyntaxError) return {
|
|
5821
|
+
status: 400,
|
|
5822
|
+
value: { error: "invalid-json" }
|
|
5823
|
+
};
|
|
5824
|
+
return {
|
|
5825
|
+
status: 500,
|
|
5826
|
+
value: {
|
|
5827
|
+
error: "pr-feedback-unavailable",
|
|
5828
|
+
message: "Pull request feedback is unavailable."
|
|
5829
|
+
}
|
|
5830
|
+
};
|
|
5292
5831
|
}
|
|
5293
5832
|
}
|
|
5294
|
-
})
|
|
5833
|
+
});
|
|
5295
5834
|
}
|
|
5296
5835
|
//#endregion
|
|
5297
5836
|
//#region src/repository-status-routes.ts
|
|
5298
5837
|
const MAX_PATH_CHARS$1 = 4096;
|
|
5299
5838
|
/** Read-only repository status for an arbitrary directory (the overview panel
|
|
5300
|
-
* aggregates every Claude session's checkout through this).
|
|
5839
|
+
* aggregates every Claude session's checkout through this).
|
|
5840
|
+
*
|
|
5841
|
+
* The route signal is threaded into the service because this is the one read
|
|
5842
|
+
* the overview polls per distinct checkout every 30 seconds: freeing the
|
|
5843
|
+
* socket at the budget while the Host kept scanning would just grow a queue
|
|
5844
|
+
* of work nobody is waiting for any more. */
|
|
5301
5845
|
function registerRepositoryStatusRoute(ctx, service) {
|
|
5302
|
-
ctx
|
|
5846
|
+
registerPluginRoute(ctx, {
|
|
5847
|
+
mode: "unary",
|
|
5303
5848
|
kind: "exact",
|
|
5304
5849
|
path: CLAUDE_REPOSITORY_STATUS_PATH,
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
const cwd =
|
|
5309
|
-
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5850
|
+
methods: ["GET"],
|
|
5851
|
+
budget: "git",
|
|
5852
|
+
handler: async (io) => {
|
|
5853
|
+
const cwd = io.url.searchParams.get("cwd");
|
|
5854
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return {
|
|
5855
|
+
status: 400,
|
|
5856
|
+
value: {
|
|
5857
|
+
error: "invalid-request",
|
|
5858
|
+
message: "The cwd query parameter is invalid."
|
|
5859
|
+
}
|
|
5860
|
+
};
|
|
5313
5861
|
try {
|
|
5314
|
-
return
|
|
5862
|
+
return {
|
|
5863
|
+
status: 200,
|
|
5864
|
+
value: await service.inspect(cwd, io.signal)
|
|
5865
|
+
};
|
|
5315
5866
|
} catch {
|
|
5316
|
-
return
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5867
|
+
return {
|
|
5868
|
+
status: 500,
|
|
5869
|
+
value: {
|
|
5870
|
+
error: "repository-status-unavailable",
|
|
5871
|
+
message: "Repository status is unavailable."
|
|
5872
|
+
}
|
|
5873
|
+
};
|
|
5320
5874
|
}
|
|
5321
5875
|
}
|
|
5322
|
-
})
|
|
5876
|
+
});
|
|
5323
5877
|
}
|
|
5324
5878
|
//#endregion
|
|
5325
5879
|
//#region src/repository-file-routes.ts
|
|
5326
5880
|
const MAX_PATH_CHARS = 4096;
|
|
5327
5881
|
/** Read-only slice of a working-tree file so the diff panel can expand unmodified lines around hunks. */
|
|
5328
5882
|
function registerRepositoryFileRoute(ctx, service) {
|
|
5329
|
-
ctx
|
|
5883
|
+
registerPluginRoute(ctx, {
|
|
5884
|
+
mode: "unary",
|
|
5330
5885
|
kind: "exact",
|
|
5331
5886
|
path: CLAUDE_REPOSITORY_FILE_PATH,
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
const params =
|
|
5887
|
+
methods: ["GET"],
|
|
5888
|
+
budget: "git",
|
|
5889
|
+
handler: async (io) => {
|
|
5890
|
+
const params = io.url.searchParams;
|
|
5336
5891
|
const cwd = params.get("cwd");
|
|
5337
5892
|
const path = params.get("path");
|
|
5338
5893
|
const from = Number(params.get("from"));
|
|
5339
5894
|
const to = Number(params.get("to"));
|
|
5340
|
-
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5895
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return {
|
|
5896
|
+
status: 400,
|
|
5897
|
+
value: {
|
|
5898
|
+
error: "invalid-request",
|
|
5899
|
+
message: "The cwd query parameter is invalid."
|
|
5900
|
+
}
|
|
5901
|
+
};
|
|
5902
|
+
if (path === null || path.length === 0 || path.length > MAX_PATH_CHARS) return {
|
|
5903
|
+
status: 400,
|
|
5904
|
+
value: {
|
|
5905
|
+
error: "invalid-request",
|
|
5906
|
+
message: "The path query parameter is invalid."
|
|
5907
|
+
}
|
|
5908
|
+
};
|
|
5348
5909
|
try {
|
|
5349
|
-
return
|
|
5910
|
+
return {
|
|
5911
|
+
status: 200,
|
|
5912
|
+
value: await service.fileLines(cwd, path, from, to)
|
|
5913
|
+
};
|
|
5350
5914
|
} catch (error) {
|
|
5351
|
-
if (error instanceof RepositoryFileError) return
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5915
|
+
if (error instanceof RepositoryFileError) return {
|
|
5916
|
+
status: error.code === "invalid-request" ? 400 : 409,
|
|
5917
|
+
value: {
|
|
5918
|
+
error: error.code,
|
|
5919
|
+
message: error.message
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
return {
|
|
5923
|
+
status: 500,
|
|
5924
|
+
value: {
|
|
5925
|
+
error: "repository-file-unavailable",
|
|
5926
|
+
message: "The file could not be read."
|
|
5927
|
+
}
|
|
5928
|
+
};
|
|
5359
5929
|
}
|
|
5360
5930
|
}
|
|
5361
|
-
})
|
|
5931
|
+
});
|
|
5362
5932
|
}
|
|
5363
5933
|
//#endregion
|
|
5364
5934
|
//#region src/jira.ts
|
|
@@ -5604,16 +6174,17 @@ const MAX_BODY_BYTES$3 = 8192;
|
|
|
5604
6174
|
function record$4(value) {
|
|
5605
6175
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5606
6176
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
6177
|
+
/** The wrapper enforces the byte cap; its plain rejection is translated back
|
|
6178
|
+
* into the JiraError shape the panel already knows how to render. */
|
|
6179
|
+
async function readJson$2(io) {
|
|
6180
|
+
let body;
|
|
6181
|
+
try {
|
|
6182
|
+
body = await io.body(MAX_BODY_BYTES$3);
|
|
6183
|
+
} catch (error) {
|
|
6184
|
+
if (error instanceof SyntaxError) throw error;
|
|
6185
|
+
throw new JiraError("body-too-large", "The request body is too large.");
|
|
5615
6186
|
}
|
|
5616
|
-
const value = record$4(
|
|
6187
|
+
const value = record$4(body);
|
|
5617
6188
|
if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
|
|
5618
6189
|
return value;
|
|
5619
6190
|
}
|
|
@@ -5623,55 +6194,99 @@ function string(input, key) {
|
|
|
5623
6194
|
return value;
|
|
5624
6195
|
}
|
|
5625
6196
|
function registerJiraRoute(ctx, service) {
|
|
5626
|
-
ctx
|
|
6197
|
+
registerPluginRoute(ctx, {
|
|
6198
|
+
mode: "unary",
|
|
5627
6199
|
kind: "prefix",
|
|
5628
6200
|
path: CLAUDE_JIRA_PATH,
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
6201
|
+
methods: ["GET", "POST"],
|
|
6202
|
+
budget: "git",
|
|
6203
|
+
handler: async (io) => {
|
|
6204
|
+
const url = io.url;
|
|
5632
6205
|
try {
|
|
5633
6206
|
if (url.pathname === `/plugins/dsh-claude/jira/status`) {
|
|
5634
|
-
if (
|
|
5635
|
-
|
|
6207
|
+
if (io.method !== "GET") return {
|
|
6208
|
+
status: 405,
|
|
6209
|
+
value: { error: "method not allowed" }
|
|
6210
|
+
};
|
|
6211
|
+
return {
|
|
6212
|
+
status: 200,
|
|
6213
|
+
value: await service.status()
|
|
6214
|
+
};
|
|
5636
6215
|
}
|
|
5637
6216
|
if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
|
|
5638
|
-
if (
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
6217
|
+
if (io.method !== "POST") return {
|
|
6218
|
+
status: 405,
|
|
6219
|
+
value: { error: "method not allowed" }
|
|
6220
|
+
};
|
|
6221
|
+
const input = await readJson$2(io);
|
|
6222
|
+
return {
|
|
6223
|
+
status: 200,
|
|
6224
|
+
value: await service.connect({
|
|
6225
|
+
siteUrl: string(input, "siteUrl"),
|
|
6226
|
+
email: string(input, "email"),
|
|
6227
|
+
apiToken: string(input, "apiToken")
|
|
6228
|
+
})
|
|
6229
|
+
};
|
|
5645
6230
|
}
|
|
5646
6231
|
if (url.pathname === `/plugins/dsh-claude/jira/disconnect`) {
|
|
5647
|
-
if (
|
|
6232
|
+
if (io.method !== "POST") return {
|
|
6233
|
+
status: 405,
|
|
6234
|
+
value: { error: "method not allowed" }
|
|
6235
|
+
};
|
|
5648
6236
|
await service.disconnect();
|
|
5649
|
-
return
|
|
6237
|
+
return {
|
|
6238
|
+
status: 200,
|
|
6239
|
+
value: { connected: false }
|
|
6240
|
+
};
|
|
5650
6241
|
}
|
|
5651
6242
|
if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
|
|
5652
|
-
if (
|
|
5653
|
-
|
|
6243
|
+
if (io.method !== "POST") return {
|
|
6244
|
+
status: 405,
|
|
6245
|
+
value: { error: "method not allowed" }
|
|
6246
|
+
};
|
|
6247
|
+
const input = await readJson$2(io);
|
|
5654
6248
|
await service.assignToMe(string(input, "key"));
|
|
5655
|
-
return
|
|
6249
|
+
return {
|
|
6250
|
+
status: 200,
|
|
6251
|
+
value: { assigned: true }
|
|
6252
|
+
};
|
|
5656
6253
|
}
|
|
5657
6254
|
if (url.pathname === `/plugins/dsh-claude/jira/search`) {
|
|
5658
|
-
if (
|
|
5659
|
-
|
|
6255
|
+
if (io.method !== "GET") return {
|
|
6256
|
+
status: 405,
|
|
6257
|
+
value: { error: "method not allowed" }
|
|
6258
|
+
};
|
|
6259
|
+
return {
|
|
6260
|
+
status: 200,
|
|
6261
|
+
value: { tickets: await service.search(url.searchParams.get("query") ?? "") }
|
|
6262
|
+
};
|
|
5660
6263
|
}
|
|
5661
|
-
return
|
|
6264
|
+
return {
|
|
6265
|
+
status: 404,
|
|
6266
|
+
value: { error: "not found" }
|
|
6267
|
+
};
|
|
5662
6268
|
} catch (error) {
|
|
5663
|
-
if (error instanceof JiraError) return
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
6269
|
+
if (error instanceof JiraError) return {
|
|
6270
|
+
status: 409,
|
|
6271
|
+
value: {
|
|
6272
|
+
error: error.code,
|
|
6273
|
+
message: error.message
|
|
6274
|
+
}
|
|
6275
|
+
};
|
|
6276
|
+
if (error instanceof SyntaxError) return {
|
|
6277
|
+
status: 400,
|
|
6278
|
+
value: { error: "invalid-json" }
|
|
6279
|
+
};
|
|
6280
|
+
return {
|
|
6281
|
+
status: 500,
|
|
6282
|
+
value: {
|
|
6283
|
+
error: "jira-unavailable",
|
|
6284
|
+
message: "Jira is unavailable."
|
|
6285
|
+
}
|
|
6286
|
+
};
|
|
5672
6287
|
}
|
|
5673
6288
|
}
|
|
5674
|
-
})
|
|
6289
|
+
});
|
|
5675
6290
|
}
|
|
5676
6291
|
//#endregion
|
|
5677
6292
|
//#region src/ask.ts
|
|
@@ -5899,22 +6514,11 @@ var AskService = class {
|
|
|
5899
6514
|
//#region src/ask-routes.ts
|
|
5900
6515
|
const MAX_BODY_BYTES$2 = 131072;
|
|
5901
6516
|
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
6517
|
+
/** Two sessions may await an answer at once; a third evicts the oldest. */
|
|
6518
|
+
const MAX_CONCURRENT_ASKS = 2;
|
|
5902
6519
|
function record$2(value) {
|
|
5903
6520
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5904
6521
|
}
|
|
5905
|
-
async function readJson$2(req) {
|
|
5906
|
-
const chunks = [];
|
|
5907
|
-
let size = 0;
|
|
5908
|
-
for await (const chunk of req) {
|
|
5909
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5910
|
-
size += buffer.length;
|
|
5911
|
-
if (size > MAX_BODY_BYTES$2) throw new AskError("body-too-large", "The request body is too large.");
|
|
5912
|
-
chunks.push(buffer);
|
|
5913
|
-
}
|
|
5914
|
-
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5915
|
-
if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
5916
|
-
return value;
|
|
5917
|
-
}
|
|
5918
6522
|
function askRequest(input) {
|
|
5919
6523
|
if (typeof input.selection !== "string" || typeof input.question !== "string" || input.context !== void 0 && typeof input.context !== "string") throw new AskError("invalid-request", "The selection and question fields are required.");
|
|
5920
6524
|
return {
|
|
@@ -5926,25 +6530,32 @@ function askRequest(input) {
|
|
|
5926
6530
|
function ndjson(res, value) {
|
|
5927
6531
|
res.write(`${JSON.stringify(value)}\n`);
|
|
5928
6532
|
}
|
|
6533
|
+
/** A stream route rather than a unary one: the answer is written as it arrives,
|
|
6534
|
+
* so the deadline stays where the work is — `ASK_TIMEOUT_MS` inside the
|
|
6535
|
+
* service, fused there with the disconnect signal — instead of a route budget
|
|
6536
|
+
* that would cut a legitimate long answer off mid-sentence. */
|
|
5929
6537
|
function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
5930
|
-
ctx
|
|
6538
|
+
registerPluginRoute(ctx, {
|
|
6539
|
+
mode: "stream",
|
|
5931
6540
|
kind: "exact",
|
|
5932
6541
|
path: CLAUDE_ASK_PATH,
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
6542
|
+
methods: ["POST"],
|
|
6543
|
+
maxConcurrent: MAX_CONCURRENT_ASKS,
|
|
6544
|
+
streamKey: (url) => url.searchParams.get("sessionId") ?? "",
|
|
6545
|
+
handler: async (res, io) => {
|
|
5937
6546
|
let cwd;
|
|
5938
6547
|
let request;
|
|
5939
6548
|
let sessionId;
|
|
5940
6549
|
try {
|
|
5941
|
-
const value = url.searchParams.get("sessionId");
|
|
6550
|
+
const value = io.url.searchParams.get("sessionId");
|
|
5942
6551
|
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
|
|
5943
6552
|
sessionId = value;
|
|
5944
6553
|
const resolved = cwdForSession(sessionId);
|
|
5945
6554
|
if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
|
|
5946
6555
|
cwd = resolved;
|
|
5947
|
-
|
|
6556
|
+
const body = record$2(await io.body(MAX_BODY_BYTES$2));
|
|
6557
|
+
if (body === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
6558
|
+
request = askRequest(body);
|
|
5948
6559
|
} catch (error) {
|
|
5949
6560
|
if (error instanceof AskError) return json(res, 409, {
|
|
5950
6561
|
error: error.code,
|
|
@@ -5962,17 +6573,13 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5962
6573
|
"x-content-type-options": "nosniff"
|
|
5963
6574
|
});
|
|
5964
6575
|
res.flushHeaders?.();
|
|
5965
|
-
const controller = new AbortController();
|
|
5966
|
-
req.on("close", () => {
|
|
5967
|
-
controller.abort();
|
|
5968
|
-
});
|
|
5969
6576
|
try {
|
|
5970
6577
|
await service.ask(cwd, request, preferencesFor(sessionId) ?? {}, (event) => {
|
|
5971
6578
|
ndjson(res, event.type === "text" ? {
|
|
5972
6579
|
type: "delta",
|
|
5973
6580
|
text: event.text
|
|
5974
6581
|
} : event);
|
|
5975
|
-
},
|
|
6582
|
+
}, io.signal);
|
|
5976
6583
|
ndjson(res, { type: "done" });
|
|
5977
6584
|
} catch (error) {
|
|
5978
6585
|
ndjson(res, {
|
|
@@ -5980,11 +6587,9 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5980
6587
|
code: error instanceof AskError ? error.code : "ask-unavailable",
|
|
5981
6588
|
message: error instanceof AskError ? error.message : "The question could not be answered."
|
|
5982
6589
|
});
|
|
5983
|
-
} finally {
|
|
5984
|
-
res.end();
|
|
5985
6590
|
}
|
|
5986
6591
|
}
|
|
5987
|
-
})
|
|
6592
|
+
});
|
|
5988
6593
|
}
|
|
5989
6594
|
//#endregion
|
|
5990
6595
|
//#region src/review-comment-routes.ts
|
|
@@ -5993,16 +6598,15 @@ const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
|
5993
6598
|
function record$1(value) {
|
|
5994
6599
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5995
6600
|
}
|
|
5996
|
-
async function readJson$1(
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
chunks.push(buffer);
|
|
6601
|
+
async function readJson$1(io) {
|
|
6602
|
+
let parsed;
|
|
6603
|
+
try {
|
|
6604
|
+
parsed = await io.body(MAX_BODY_BYTES$1);
|
|
6605
|
+
} catch (error) {
|
|
6606
|
+
if (error instanceof SyntaxError) throw error;
|
|
6607
|
+
throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
6004
6608
|
}
|
|
6005
|
-
const value = record$1(
|
|
6609
|
+
const value = record$1(parsed);
|
|
6006
6610
|
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
6007
6611
|
return value;
|
|
6008
6612
|
}
|
|
@@ -6012,45 +6616,68 @@ function sessionIdFromUrl(url) {
|
|
|
6012
6616
|
return value;
|
|
6013
6617
|
}
|
|
6014
6618
|
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
6015
|
-
ctx
|
|
6619
|
+
registerPluginRoute(ctx, {
|
|
6620
|
+
mode: "unary",
|
|
6016
6621
|
kind: "prefix",
|
|
6017
6622
|
path: CLAUDE_REVIEW_COMMENT_PATH,
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6623
|
+
methods: ["POST"],
|
|
6624
|
+
budget: "fast",
|
|
6625
|
+
handler: async (io) => {
|
|
6021
6626
|
try {
|
|
6022
|
-
const sessionId = sessionIdFromUrl(url);
|
|
6627
|
+
const sessionId = sessionIdFromUrl(io.url);
|
|
6023
6628
|
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6629
|
+
const pathname = io.url.pathname;
|
|
6630
|
+
if (pathname === "/plugins/dsh-claude/review-comments") {
|
|
6631
|
+
const input = await readJson$1(io);
|
|
6632
|
+
return {
|
|
6633
|
+
status: 200,
|
|
6634
|
+
value: { comment: store.add(sessionId, {
|
|
6635
|
+
path: input.path,
|
|
6636
|
+
line: input.line,
|
|
6637
|
+
startLine: input.startLine,
|
|
6638
|
+
side: input.side,
|
|
6639
|
+
text: input.text
|
|
6640
|
+
}) }
|
|
6641
|
+
};
|
|
6033
6642
|
}
|
|
6034
|
-
if (
|
|
6035
|
-
|
|
6036
|
-
|
|
6643
|
+
if (pathname === `/plugins/dsh-claude/review-comments/clear`) return {
|
|
6644
|
+
status: 200,
|
|
6645
|
+
value: { removed: store.drain(sessionId).length }
|
|
6646
|
+
};
|
|
6647
|
+
if (pathname === `/plugins/dsh-claude/review-comments/remove`) {
|
|
6648
|
+
const input = await readJson$1(io);
|
|
6037
6649
|
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
6038
|
-
return
|
|
6650
|
+
return {
|
|
6651
|
+
status: 200,
|
|
6652
|
+
value: { removed: store.remove(sessionId, input.id) }
|
|
6653
|
+
};
|
|
6039
6654
|
}
|
|
6040
|
-
return
|
|
6655
|
+
return {
|
|
6656
|
+
status: 404,
|
|
6657
|
+
value: { error: "not found" }
|
|
6658
|
+
};
|
|
6041
6659
|
} catch (error) {
|
|
6042
|
-
if (error instanceof ReviewCommentError) return
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6660
|
+
if (error instanceof ReviewCommentError) return {
|
|
6661
|
+
status: 409,
|
|
6662
|
+
value: {
|
|
6663
|
+
error: error.code,
|
|
6664
|
+
message: error.message
|
|
6665
|
+
}
|
|
6666
|
+
};
|
|
6667
|
+
if (error instanceof SyntaxError) return {
|
|
6668
|
+
status: 400,
|
|
6669
|
+
value: { error: "invalid-json" }
|
|
6670
|
+
};
|
|
6671
|
+
return {
|
|
6672
|
+
status: 500,
|
|
6673
|
+
value: {
|
|
6674
|
+
error: "review-comment-unavailable",
|
|
6675
|
+
message: "Review comments are unavailable."
|
|
6676
|
+
}
|
|
6677
|
+
};
|
|
6051
6678
|
}
|
|
6052
6679
|
}
|
|
6053
|
-
})
|
|
6680
|
+
});
|
|
6054
6681
|
}
|
|
6055
6682
|
//#endregion
|
|
6056
6683
|
//#region src/client-diagnostics-routes.ts
|
|
@@ -6058,17 +6685,6 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
6058
6685
|
const MAX_DIAGNOSTIC_BYTES = 8192;
|
|
6059
6686
|
const MAX_DETAIL_CHARS = 2e3;
|
|
6060
6687
|
const MAX_KIND_CHARS = 60;
|
|
6061
|
-
async function readBody(req) {
|
|
6062
|
-
const chunks = [];
|
|
6063
|
-
let size = 0;
|
|
6064
|
-
for await (const chunk of req) {
|
|
6065
|
-
const buffer = chunk;
|
|
6066
|
-
size += buffer.length;
|
|
6067
|
-
if (size > MAX_DIAGNOSTIC_BYTES) throw new Error("diagnostic too large");
|
|
6068
|
-
chunks.push(buffer);
|
|
6069
|
-
}
|
|
6070
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
6071
|
-
}
|
|
6072
6688
|
/** `POST <path>` with `{ kind, detail }`: write one renderer finding to the Host log.
|
|
6073
6689
|
*
|
|
6074
6690
|
* The renderer has no other way to speak. A Slot entry that throws is caught
|
|
@@ -6078,25 +6694,38 @@ async function readBody(req) {
|
|
|
6078
6694
|
* Every finding here is data written by this package's own client half, but
|
|
6079
6695
|
* it is still bounded and redacted like any other untrusted input. */
|
|
6080
6696
|
function registerClaudeClientDiagnosticsRoute(ctx) {
|
|
6081
|
-
ctx
|
|
6697
|
+
registerPluginRoute(ctx, {
|
|
6698
|
+
mode: "unary",
|
|
6082
6699
|
kind: "exact",
|
|
6083
6700
|
path: CLAUDE_CLIENT_DIAGNOSTICS_PATH,
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6701
|
+
methods: ["POST"],
|
|
6702
|
+
budget: "fast",
|
|
6703
|
+
handler: async (io) => {
|
|
6087
6704
|
try {
|
|
6088
|
-
const body = await
|
|
6705
|
+
const body = await io.body(MAX_DIAGNOSTIC_BYTES);
|
|
6089
6706
|
const kind = typeof body?.kind === "string" ? body.kind.slice(0, MAX_KIND_CHARS) : "unknown";
|
|
6090
6707
|
const detail = typeof body?.detail === "string" ? body.detail : "";
|
|
6091
|
-
if (detail === "") return
|
|
6708
|
+
if (detail === "") return {
|
|
6709
|
+
status: 400,
|
|
6710
|
+
value: { error: "invalid-request" }
|
|
6711
|
+
};
|
|
6092
6712
|
ctx.logger.warn(`dsh-claude client [${kind}]: ${redactText(detail, MAX_DETAIL_CHARS)}`);
|
|
6093
|
-
return
|
|
6713
|
+
return {
|
|
6714
|
+
status: 200,
|
|
6715
|
+
value: { ok: true }
|
|
6716
|
+
};
|
|
6094
6717
|
} catch (error) {
|
|
6095
|
-
if (error instanceof SyntaxError) return
|
|
6096
|
-
|
|
6718
|
+
if (error instanceof SyntaxError) return {
|
|
6719
|
+
status: 400,
|
|
6720
|
+
value: { error: "invalid-json" }
|
|
6721
|
+
};
|
|
6722
|
+
return {
|
|
6723
|
+
status: 400,
|
|
6724
|
+
value: { error: "invalid-request" }
|
|
6725
|
+
};
|
|
6097
6726
|
}
|
|
6098
6727
|
}
|
|
6099
|
-
})
|
|
6728
|
+
});
|
|
6100
6729
|
}
|
|
6101
6730
|
//#endregion
|
|
6102
6731
|
//#region src/rewind-routes.ts
|
|
@@ -6105,55 +6734,78 @@ const MAX_SESSION_ID_CHARS = 1024;
|
|
|
6105
6734
|
function record(value) {
|
|
6106
6735
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6107
6736
|
}
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
if (
|
|
6115
|
-
|
|
6737
|
+
/** An oversized body fails the same field checks as a missing one; only
|
|
6738
|
+
* malformed JSON is worth reporting separately. */
|
|
6739
|
+
async function readJson(io) {
|
|
6740
|
+
try {
|
|
6741
|
+
return record(await io.body(MAX_BODY_BYTES));
|
|
6742
|
+
} catch (error) {
|
|
6743
|
+
if (error instanceof SyntaxError) throw error;
|
|
6744
|
+
return;
|
|
6116
6745
|
}
|
|
6117
|
-
return record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
6118
6746
|
}
|
|
6119
6747
|
/** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
|
|
6120
6748
|
* later one, and arm Claude to resume before the turn it opened. */
|
|
6121
6749
|
function registerClaudeRewindRoute(ctx, sidecar, access) {
|
|
6122
|
-
ctx
|
|
6750
|
+
registerPluginRoute(ctx, {
|
|
6751
|
+
mode: "unary",
|
|
6123
6752
|
kind: "exact",
|
|
6124
6753
|
path: CLAUDE_REWIND_PATH,
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6754
|
+
methods: ["POST"],
|
|
6755
|
+
budget: "git",
|
|
6756
|
+
handler: async (io) => {
|
|
6128
6757
|
try {
|
|
6129
|
-
const input = await readJson(
|
|
6758
|
+
const input = await readJson(io);
|
|
6130
6759
|
const sessionId = input?.sessionId;
|
|
6131
6760
|
const seq = input?.seq;
|
|
6132
|
-
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return
|
|
6761
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return {
|
|
6762
|
+
status: 400,
|
|
6763
|
+
value: { error: "invalid-request" }
|
|
6764
|
+
};
|
|
6133
6765
|
const events = access.eventsFor(sessionId);
|
|
6134
|
-
if (events === void 0) return
|
|
6135
|
-
|
|
6766
|
+
if (events === void 0) return {
|
|
6767
|
+
status: 409,
|
|
6768
|
+
value: { error: "session-unavailable" }
|
|
6769
|
+
};
|
|
6770
|
+
if (access.busy(sessionId)) return {
|
|
6771
|
+
status: 409,
|
|
6772
|
+
value: { error: "session-busy" }
|
|
6773
|
+
};
|
|
6136
6774
|
const planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
|
|
6137
|
-
if (planned === void 0) return
|
|
6775
|
+
if (planned === void 0) return {
|
|
6776
|
+
status: 409,
|
|
6777
|
+
value: { error: "seq-unavailable" }
|
|
6778
|
+
};
|
|
6138
6779
|
await sidecar.writeRewind(sessionId, planned);
|
|
6139
6780
|
await access.reset(sessionId);
|
|
6140
|
-
return
|
|
6781
|
+
return {
|
|
6782
|
+
status: 200,
|
|
6783
|
+
value: { ranges: planned.ranges }
|
|
6784
|
+
};
|
|
6141
6785
|
} catch (error) {
|
|
6142
|
-
if (error instanceof SyntaxError) return
|
|
6143
|
-
|
|
6786
|
+
if (error instanceof SyntaxError) return {
|
|
6787
|
+
status: 400,
|
|
6788
|
+
value: { error: "invalid-json" }
|
|
6789
|
+
};
|
|
6790
|
+
return {
|
|
6791
|
+
status: 500,
|
|
6792
|
+
value: { error: "rewind-unavailable" }
|
|
6793
|
+
};
|
|
6144
6794
|
}
|
|
6145
6795
|
}
|
|
6146
|
-
})
|
|
6796
|
+
});
|
|
6147
6797
|
}
|
|
6148
6798
|
//#endregion
|
|
6149
6799
|
//#region src/update-routes.ts
|
|
6150
6800
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
6151
|
-
const UPDATE_TIMEOUT_MS = 3e4;
|
|
6152
|
-
const CHECK_TIMEOUT_MS = 1e4;
|
|
6153
6801
|
const MAX_MANIFEST_BYTES = 262144;
|
|
6154
6802
|
const MAX_UPDATE_OUTPUT_BYTES = 32768;
|
|
6155
6803
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6156
6804
|
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6805
|
+
/** The route owns the deadline now. A direct caller with nothing to cancel
|
|
6806
|
+
* against gets a signal that never fires, rather than a second timeout
|
|
6807
|
+
* competing with the budget the route already declared. */
|
|
6808
|
+
const NEVER_ABORTS = new AbortController().signal;
|
|
6157
6809
|
function safeMessage$1(error) {
|
|
6158
6810
|
return redactText(error instanceof Error ? error.message : String(error), 500);
|
|
6159
6811
|
}
|
|
@@ -6272,7 +6924,7 @@ async function packageContext(deps) {
|
|
|
6272
6924
|
...installation === void 0 ? {} : { installation }
|
|
6273
6925
|
};
|
|
6274
6926
|
}
|
|
6275
|
-
async function checkPluginUpdate(deps = {}) {
|
|
6927
|
+
async function checkPluginUpdate(deps = {}, signal = NEVER_ABORTS) {
|
|
6276
6928
|
try {
|
|
6277
6929
|
const { version, installation } = await packageContext(deps);
|
|
6278
6930
|
if (installation === void 0) return {
|
|
@@ -6299,7 +6951,7 @@ async function checkPluginUpdate(deps = {}) {
|
|
|
6299
6951
|
restartRequired: false,
|
|
6300
6952
|
message: "This installation source cannot be updated from the npm registry"
|
|
6301
6953
|
};
|
|
6302
|
-
const latest = await (deps.fetchLatest ?? registryLatest)(
|
|
6954
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(signal);
|
|
6303
6955
|
const comparison = compareVersions(version, latest);
|
|
6304
6956
|
return {
|
|
6305
6957
|
currentVersion: version,
|
|
@@ -6325,10 +6977,10 @@ async function verifyInstalledVersion(installation, expectedVersion) {
|
|
|
6325
6977
|
const installedManifest = await readManifest(join(installation.profileDir, "node_modules", ...PLUGIN_PACKAGE_NAME.split("/"), "package.json"));
|
|
6326
6978
|
if (installedManifest.name !== "@norman-else/dsh-claude" || installedManifest.version !== expectedVersion) throw new Error("DSH plugin update completed without installing the requested version");
|
|
6327
6979
|
}
|
|
6328
|
-
async function updatePlugin(deps = {}) {
|
|
6980
|
+
async function updatePlugin(deps = {}, signal = NEVER_ABORTS) {
|
|
6329
6981
|
const { version, installation } = await packageContext(deps);
|
|
6330
6982
|
if (installation === void 0 || installation.source !== "registry") throw new Error("Plugin update is unavailable for this installation");
|
|
6331
|
-
const latest = await (deps.fetchLatest ?? registryLatest)(
|
|
6983
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(signal);
|
|
6332
6984
|
if (compareVersions(version, latest) >= 0) return {
|
|
6333
6985
|
currentVersion: version,
|
|
6334
6986
|
latestVersion: latest,
|
|
@@ -6340,7 +6992,6 @@ async function updatePlugin(deps = {}) {
|
|
|
6340
6992
|
const resolveExecutable = deps.resolveExecutable;
|
|
6341
6993
|
const spawn = deps.spawn;
|
|
6342
6994
|
if (resolveExecutable === void 0 || spawn === void 0) throw new Error("DSH update runtime is unavailable");
|
|
6343
|
-
const signal = AbortSignal.timeout(UPDATE_TIMEOUT_MS);
|
|
6344
6995
|
const handle = spawn({
|
|
6345
6996
|
argv: [
|
|
6346
6997
|
await resolveExecutable("dsh", {}, signal),
|
|
@@ -6383,27 +7034,35 @@ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
|
|
|
6383
7034
|
resolveExecutable: runtime.resolveExecutable.bind(runtime),
|
|
6384
7035
|
spawn: runtime.spawn.bind(runtime)
|
|
6385
7036
|
};
|
|
6386
|
-
|
|
7037
|
+
const routes = [{
|
|
6387
7038
|
path: CLAUDE_UPDATE_CHECK_PATH,
|
|
6388
7039
|
method: "GET",
|
|
6389
|
-
run: () => checkPluginUpdate(shared)
|
|
7040
|
+
run: (signal) => checkPluginUpdate(shared, signal)
|
|
6390
7041
|
}, {
|
|
6391
7042
|
path: CLAUDE_UPDATE_PATH,
|
|
6392
7043
|
method: "POST",
|
|
6393
|
-
run: () => updatePlugin(shared)
|
|
6394
|
-
}]
|
|
7044
|
+
run: (signal) => updatePlugin(shared, signal)
|
|
7045
|
+
}];
|
|
7046
|
+
for (const route of routes) registerPluginRoute(ctx, {
|
|
7047
|
+
mode: "unary",
|
|
6395
7048
|
kind: "exact",
|
|
6396
7049
|
path: route.path,
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
7050
|
+
methods: [route.method],
|
|
7051
|
+
budget: "remote",
|
|
7052
|
+
handler: async (io) => {
|
|
6400
7053
|
try {
|
|
6401
|
-
|
|
7054
|
+
return {
|
|
7055
|
+
status: 200,
|
|
7056
|
+
value: await route.run(io.signal)
|
|
7057
|
+
};
|
|
6402
7058
|
} catch (error) {
|
|
6403
|
-
|
|
7059
|
+
return {
|
|
7060
|
+
status: 500,
|
|
7061
|
+
value: { error: safeMessage$1(error) }
|
|
7062
|
+
};
|
|
6404
7063
|
}
|
|
6405
7064
|
}
|
|
6406
|
-
})
|
|
7065
|
+
});
|
|
6407
7066
|
}
|
|
6408
7067
|
//#endregion
|
|
6409
7068
|
//#region src/plan-usage-routes.ts
|
|
@@ -6420,22 +7079,32 @@ function safeMessage(error) {
|
|
|
6420
7079
|
* throwaway probe process so a refresh never depends on, or disturbs, a live
|
|
6421
7080
|
* Claude session. */
|
|
6422
7081
|
function registerPlanUsageRoute(ctx, probe, now = Date.now) {
|
|
6423
|
-
ctx
|
|
7082
|
+
registerPluginRoute(ctx, {
|
|
7083
|
+
mode: "unary",
|
|
6424
7084
|
kind: "exact",
|
|
6425
7085
|
path: CLAUDE_USAGE_PATH,
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
if (
|
|
7086
|
+
methods: ["GET", "POST"],
|
|
7087
|
+
budget: "git",
|
|
7088
|
+
handler: async (io) => {
|
|
7089
|
+
if (io.method === "GET") return {
|
|
7090
|
+
status: 200,
|
|
7091
|
+
value: latestPlanUsage() ?? EMPTY
|
|
7092
|
+
};
|
|
6430
7093
|
try {
|
|
6431
7094
|
const report = await probe(now());
|
|
6432
7095
|
recordPlanUsage(report);
|
|
6433
|
-
|
|
7096
|
+
return {
|
|
7097
|
+
status: 200,
|
|
7098
|
+
value: report
|
|
7099
|
+
};
|
|
6434
7100
|
} catch (error) {
|
|
6435
|
-
|
|
7101
|
+
return {
|
|
7102
|
+
status: 500,
|
|
7103
|
+
value: { error: safeMessage(error) }
|
|
7104
|
+
};
|
|
6436
7105
|
}
|
|
6437
7106
|
}
|
|
6438
|
-
})
|
|
7107
|
+
});
|
|
6439
7108
|
}
|
|
6440
7109
|
//#endregion
|
|
6441
7110
|
//#region src/global-settings.ts
|
|
@@ -6564,6 +7233,31 @@ const WORKTREE_BRANCH_PREFIX = {
|
|
|
6564
7233
|
else document.worktreeBranchPrefix = value;
|
|
6565
7234
|
}
|
|
6566
7235
|
};
|
|
7236
|
+
/** Which renderer draws Claude's visible output. Plugin settings, not Claude's:
|
|
7237
|
+
* the CLI has no opinion about how DSH paints a turn. The option labels stay
|
|
7238
|
+
* machine-readable ids; the Client translates the two known values. */
|
|
7239
|
+
const RENDERER = {
|
|
7240
|
+
key: "renderer",
|
|
7241
|
+
kind: "select",
|
|
7242
|
+
document: "plugin",
|
|
7243
|
+
effect: "next-turn",
|
|
7244
|
+
async options() {
|
|
7245
|
+
return CLAUDE_RENDER_MODES.map((value) => ({
|
|
7246
|
+
value,
|
|
7247
|
+
label: value,
|
|
7248
|
+
source: "built-in"
|
|
7249
|
+
}));
|
|
7250
|
+
},
|
|
7251
|
+
read(document) {
|
|
7252
|
+
const value = document.renderer;
|
|
7253
|
+
return isClaudeRenderMode(value) ? value : DEFAULT_CLAUDE_RENDER_MODE;
|
|
7254
|
+
},
|
|
7255
|
+
apply(document, value) {
|
|
7256
|
+
if (!isClaudeRenderMode(value)) throw new Error("Invalid value for global setting renderer");
|
|
7257
|
+
if (value === "plugin") delete document.renderer;
|
|
7258
|
+
else document.renderer = value;
|
|
7259
|
+
}
|
|
7260
|
+
};
|
|
6567
7261
|
function isBoundedInteger(value, min, max) {
|
|
6568
7262
|
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
6569
7263
|
}
|
|
@@ -6587,6 +7281,7 @@ function integerSetting(key, min, max, defaultFor) {
|
|
|
6587
7281
|
}
|
|
6588
7282
|
const DESCRIPTORS = [
|
|
6589
7283
|
OUTPUT_STYLE,
|
|
7284
|
+
RENDERER,
|
|
6590
7285
|
WORKTREE_BRANCH_PREFIX,
|
|
6591
7286
|
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
6592
7287
|
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
@@ -6648,6 +7343,17 @@ async function readSupervisorLimitOverrides(deps = {}) {
|
|
|
6648
7343
|
...isBoundedInteger(idleTimeoutMinutes, 1, MAX_IDLE_TIMEOUT_MINUTES) ? { idleTimeoutMs: idleTimeoutMinutes * 6e4 } : {}
|
|
6649
7344
|
};
|
|
6650
7345
|
}
|
|
7346
|
+
/** The renderer the Host should produce output for. A missing, unreadable, or
|
|
7347
|
+
* malformed plugin settings file keeps today's plugin-owned transcript. */
|
|
7348
|
+
async function readRenderMode(deps = {}) {
|
|
7349
|
+
let document;
|
|
7350
|
+
try {
|
|
7351
|
+
document = await readDocument(pathsFor(deps).pluginSettingsFile);
|
|
7352
|
+
} catch {
|
|
7353
|
+
return DEFAULT_CLAUDE_RENDER_MODE;
|
|
7354
|
+
}
|
|
7355
|
+
return isClaudeRenderMode(document.renderer) ? document.renderer : DEFAULT_CLAUDE_RENDER_MODE;
|
|
7356
|
+
}
|
|
6651
7357
|
async function readWorktreeBranchPrefix(deps = {}) {
|
|
6652
7358
|
const paths = pathsFor(deps);
|
|
6653
7359
|
return WORKTREE_BRANCH_PREFIX.read(await readDocument(paths.pluginSettingsFile));
|
|
@@ -6693,37 +7399,34 @@ function updateGlobalSettings(changes, deps = {}) {
|
|
|
6693
7399
|
pendingWrite = operation;
|
|
6694
7400
|
return operation;
|
|
6695
7401
|
}
|
|
6696
|
-
async function requestJson(
|
|
6697
|
-
const
|
|
6698
|
-
if (!Number.isFinite(declared) || declared < 0 || declared > MAX_REQUEST_BYTES) throw new Error("Request body is too large");
|
|
6699
|
-
const chunks = [];
|
|
6700
|
-
let bytes = 0;
|
|
6701
|
-
for await (const chunk of req) {
|
|
6702
|
-
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6703
|
-
bytes += data.byteLength;
|
|
6704
|
-
if (bytes > MAX_REQUEST_BYTES) throw new Error("Request body is too large");
|
|
6705
|
-
chunks.push(data);
|
|
6706
|
-
}
|
|
6707
|
-
const body = object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
7402
|
+
async function requestJson(io) {
|
|
7403
|
+
const body = object(await io.body(MAX_REQUEST_BYTES));
|
|
6708
7404
|
if (body === void 0 || Object.keys(body).some((key) => key !== "changes")) throw new Error("Invalid global settings request");
|
|
6709
7405
|
return body.changes;
|
|
6710
7406
|
}
|
|
6711
7407
|
function registerClaudeGlobalSettingsRoute(ctx, deps = {}) {
|
|
6712
|
-
ctx
|
|
7408
|
+
registerPluginRoute(ctx, {
|
|
7409
|
+
mode: "unary",
|
|
6713
7410
|
kind: "exact",
|
|
6714
7411
|
path: CLAUDE_GLOBAL_SETTINGS_PATH,
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
7412
|
+
methods: ["GET", "PATCH"],
|
|
7413
|
+
budget: "fast",
|
|
7414
|
+
handler: async (io) => {
|
|
6718
7415
|
try {
|
|
6719
|
-
const result =
|
|
6720
|
-
if (
|
|
6721
|
-
|
|
7416
|
+
const result = io.method === "GET" ? await readGlobalSettings(deps) : await updateGlobalSettings(await requestJson(io), deps);
|
|
7417
|
+
if (io.method === "PATCH") await deps.onUpdated?.();
|
|
7418
|
+
return {
|
|
7419
|
+
status: 200,
|
|
7420
|
+
value: result
|
|
7421
|
+
};
|
|
6722
7422
|
} catch (error) {
|
|
6723
|
-
|
|
7423
|
+
return {
|
|
7424
|
+
status: 400,
|
|
7425
|
+
value: { error: error instanceof Error ? error.message : "Invalid global settings request" }
|
|
7426
|
+
};
|
|
6724
7427
|
}
|
|
6725
7428
|
}
|
|
6726
|
-
})
|
|
7429
|
+
});
|
|
6727
7430
|
}
|
|
6728
7431
|
//#endregion
|
|
6729
7432
|
//#region src/index.ts
|
|
@@ -6862,14 +7565,16 @@ async function apply(ctx, config) {
|
|
|
6862
7565
|
const supervisorConfig = {
|
|
6863
7566
|
executablePath: "",
|
|
6864
7567
|
defaultModel: config.model ?? "default",
|
|
7568
|
+
renderMode: DEFAULT_CLAUDE_RENDER_MODE,
|
|
6865
7569
|
...defaultLimits
|
|
6866
7570
|
};
|
|
6867
|
-
const
|
|
7571
|
+
const applySettingsOverrides = async () => {
|
|
6868
7572
|
const overrides = await readSupervisorLimitOverrides();
|
|
6869
7573
|
supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
|
|
6870
7574
|
supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
|
|
7575
|
+
supervisorConfig.renderMode = await readRenderMode();
|
|
6871
7576
|
};
|
|
6872
|
-
await
|
|
7577
|
+
await applySettingsOverrides();
|
|
6873
7578
|
const sidecar = new ClaudeSidecarRepository();
|
|
6874
7579
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
6875
7580
|
const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
|
|
@@ -6886,7 +7591,7 @@ async function apply(ctx, config) {
|
|
|
6886
7591
|
let resolutionError;
|
|
6887
7592
|
try {
|
|
6888
7593
|
supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
6889
|
-
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
|
|
7594
|
+
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => supervisorConfig.renderMode));
|
|
6890
7595
|
ctx.effect(() => {
|
|
6891
7596
|
const mounted = /* @__PURE__ */ new Map();
|
|
6892
7597
|
const pending = /* @__PURE__ */ new Set();
|
|
@@ -6975,7 +7680,7 @@ async function apply(ctx, config) {
|
|
|
6975
7680
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
6976
7681
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
6977
7682
|
defaultLimits,
|
|
6978
|
-
onUpdated:
|
|
7683
|
+
onUpdated: applySettingsOverrides
|
|
6979
7684
|
});
|
|
6980
7685
|
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
6981
7686
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|