@norman-else/dsh-claude 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { A as CLAUDE_USAGE_PATH, C as CLAUDE_REPOSITORY_FILE_PATH, D as CLAUDE_REWIND_PATH, E as CLAUDE_REVIEW_COMMENT_PATH, M as TASK_TOOL_NAMES, O as CLAUDE_UPDATE_CHECK_PATH, S as CLAUDE_REPOSITORY_FEEDBACK_PATH, T as CLAUDE_REPOSITORY_STATUS_PATH, _ as CLAUDE_EDITOR_OPEN_PATH, a as latestClaudeTasks, b as CLAUDE_PROJECTION_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_DOCTOR_PATH, h as CLAUDE_CODE_PROVIDER_IDS, i as latestClaudeSessionBinding, k as CLAUDE_UPDATE_PATH, l as redactText, m as CLAUDE_CODE_PROVIDER, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PRESET_ID, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_GLOBAL_SETTINGS_PATH, w as CLAUDE_REPOSITORY_SETUP_PATH, x as CLAUDE_REPOSITORY_ACTION_PATH, y as CLAUDE_JIRA_PATH } from "./events-CSDcXsWE.mjs";
2
- import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-tqsu15hR.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-CVdbU87E.mjs";
1
+ import { A as CLAUDE_UPDATE_PATH, C as CLAUDE_REPOSITORY_FEEDBACK_PATH, D as CLAUDE_REVIEW_COMMENT_PATH, E as CLAUDE_REPOSITORY_STATUS_PATH, N as TASK_TOOL_NAMES, O as CLAUDE_REWIND_PATH, S as CLAUDE_REPOSITORY_ACTION_PATH, T as CLAUDE_REPOSITORY_SETUP_PATH, _ as CLAUDE_DOCTOR_PATH, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_CODE_PROVIDER_IDS, h as CLAUDE_CODE_PROVIDER, i as latestClaudeSessionBinding, j as CLAUDE_USAGE_PATH, k as CLAUDE_UPDATE_CHECK_PATH, l as redactText, m as CLAUDE_CODE_PRESET_ID, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CLIENT_DIAGNOSTICS_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_EDITOR_OPEN_PATH, w as CLAUDE_REPOSITORY_FILE_PATH, x as CLAUDE_PROJECTION_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH } from "./events-DeSV0S1-.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-DXI6nWhB.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DbmlhBXI.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -87,7 +87,7 @@ function emptyProjection() {
87
87
  activities: []
88
88
  };
89
89
  }
90
- function record$12(value) {
90
+ function record$13(value) {
91
91
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
92
92
  }
93
93
  function finiteInteger(value) {
@@ -97,7 +97,7 @@ function string$4(value, max) {
97
97
  return typeof value === "string" && value.length > 0 && value.length <= max;
98
98
  }
99
99
  function binding(value) {
100
- const input = record$12(value);
100
+ const input = record$13(value);
101
101
  if (input === void 0 || !string$4(input.claudeSessionId, 512) || !string$4(input.sdkVersion, 128) || !string$4(input.cwd, 4096) || input.cliVersion !== void 0 && !string$4(input.cliVersion, 128)) return void 0;
102
102
  return {
103
103
  claudeSessionId: input.claudeSessionId,
@@ -128,21 +128,21 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
128
128
  "failed"
129
129
  ]);
130
130
  function activity(value) {
131
- const input = record$12(value);
131
+ const input = record$13(value);
132
132
  if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
133
133
  return normalizeActivity(input);
134
134
  }
135
135
  function contextUsage(value) {
136
- const input = record$12(value);
136
+ const input = record$13(value);
137
137
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
138
138
  return normalizeContextUsage(input);
139
139
  }
140
140
  function rewind(value) {
141
- const input = record$12(value);
141
+ const input = record$13(value);
142
142
  if (input === void 0 || !Array.isArray(input.ranges) || input.ranges.length > 200 || !Array.isArray(input.anchors) || input.anchors.length > 2e3) return void 0;
143
143
  const ranges = [];
144
144
  for (const item of input.ranges) {
145
- const range = record$12(item);
145
+ const range = record$13(item);
146
146
  if (range === void 0 || !finiteInteger(range.start) || !finiteInteger(range.end) || range.end < range.start) return void 0;
147
147
  ranges.push({
148
148
  start: range.start,
@@ -151,14 +151,14 @@ function rewind(value) {
151
151
  }
152
152
  const anchors = [];
153
153
  for (const item of input.anchors) {
154
- const anchor = record$12(item);
154
+ const anchor = record$13(item);
155
155
  if (anchor === void 0 || !finiteInteger(anchor.turn) || !string$4(anchor.uuid, 128)) return void 0;
156
156
  anchors.push({
157
157
  turn: anchor.turn,
158
158
  uuid: anchor.uuid
159
159
  });
160
160
  }
161
- const pending = record$12(input.pending);
161
+ const pending = record$13(input.pending);
162
162
  if (input.pending !== void 0 && pending === void 0) return void 0;
163
163
  if (pending === void 0) return {
164
164
  ranges,
@@ -177,12 +177,12 @@ function rewind(value) {
177
177
  };
178
178
  }
179
179
  function tasks(value) {
180
- const input = record$12(value);
180
+ const input = record$13(value);
181
181
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
182
182
  return normalizeTasksEvent(input.tasks);
183
183
  }
184
184
  function parseClaudeSidecar(value) {
185
- const input = record$12(value);
185
+ const input = record$13(value);
186
186
  if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
187
187
  const activities = input.activities.map(activity);
188
188
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -786,7 +786,7 @@ function createUserQuestionBridge(userQuestions, activeContext) {
786
786
  }
787
787
  //#endregion
788
788
  //#region src/sdk-messages.ts
789
- function record$11(value) {
789
+ function record$12(value) {
790
790
  return value !== null && typeof value === "object" ? value : void 0;
791
791
  }
792
792
  function string$3(value) {
@@ -818,12 +818,12 @@ function hasUsageCounts(usage) {
818
818
  return usage.inputTokens !== void 0 || usage.outputTokens !== void 0 || usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0;
819
819
  }
820
820
  function resultUsage(message) {
821
- const normalized = usageOf(record$11(message.usage));
821
+ const normalized = usageOf(record$12(message.usage));
822
822
  if (typeof message.total_cost_usd === "number") normalized.cumulativeCostUsd = message.total_cost_usd;
823
823
  return normalized;
824
824
  }
825
825
  function normalizeAssistant(message) {
826
- const envelope = record$11(message.message);
826
+ const envelope = record$12(message.message);
827
827
  const content = envelope?.content;
828
828
  if (!Array.isArray(content)) return [{
829
829
  kind: "protocol-error",
@@ -833,7 +833,7 @@ function normalizeAssistant(message) {
833
833
  const parentToolUseId = string$3(message.parent_tool_use_id);
834
834
  const normalized = [];
835
835
  for (const item of content) {
836
- const block = record$11(item);
836
+ const block = record$12(item);
837
837
  if (block === void 0) continue;
838
838
  if (block.type === "text") {
839
839
  const text = string$3(block.text);
@@ -862,7 +862,7 @@ function normalizeAssistant(message) {
862
862
  });
863
863
  }
864
864
  }
865
- const usage = usageOf(record$11(envelope?.usage));
865
+ const usage = usageOf(record$12(envelope?.usage));
866
866
  if (hasUsageCounts(usage)) normalized.push({
867
867
  kind: "request-usage",
868
868
  usage,
@@ -872,7 +872,7 @@ function normalizeAssistant(message) {
872
872
  }
873
873
  function normalizeUser(message) {
874
874
  if (message.isReplay === true) return [];
875
- const content = record$11(message.message)?.content;
875
+ const content = record$12(message.message)?.content;
876
876
  if (typeof content === "string") return [];
877
877
  if (!Array.isArray(content)) return [{
878
878
  kind: "protocol-error",
@@ -882,7 +882,7 @@ function normalizeUser(message) {
882
882
  const parentToolUseId = string$3(message.parent_tool_use_id);
883
883
  const normalized = [];
884
884
  for (const item of content) {
885
- const block = record$11(item);
885
+ const block = record$12(item);
886
886
  if (block?.type !== "tool_result") continue;
887
887
  const toolUseId = string$3(block.tool_use_id);
888
888
  if (toolUseId === void 0) continue;
@@ -963,7 +963,7 @@ function normalizeSystem(message) {
963
963
  const summary = string$3(message.summary);
964
964
  const subagentType = string$3(message.subagent_type);
965
965
  const lastToolName = string$3(message.last_tool_name);
966
- const usage = taskUsageOf(record$11(message.usage));
966
+ const usage = taskUsageOf(record$12(message.usage));
967
967
  return [{
968
968
  kind: "subagent",
969
969
  title: summary ?? description ?? "Claude subagent update",
@@ -979,7 +979,7 @@ function normalizeSystem(message) {
979
979
  }];
980
980
  }
981
981
  if (subtype === "task_updated") {
982
- const patch = record$11(message.patch);
982
+ const patch = record$12(message.patch);
983
983
  const status = string$3(patch?.status);
984
984
  const taskId = string$3(message.task_id);
985
985
  const description = string$3(patch?.description);
@@ -1002,7 +1002,7 @@ function normalizeSystem(message) {
1002
1002
  const taskId = string$3(message.task_id);
1003
1003
  const summary = string$3(message.summary);
1004
1004
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
1005
- const usage = taskUsageOf(record$11(message.usage));
1005
+ const usage = taskUsageOf(record$12(message.usage));
1006
1006
  return [{
1007
1007
  kind: "subagent",
1008
1008
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -1017,7 +1017,7 @@ function normalizeSystem(message) {
1017
1017
  if (subtype === "background_tasks_changed") return [{
1018
1018
  kind: "background-tasks",
1019
1019
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
1020
- const entry = record$11(item);
1020
+ const entry = record$12(item);
1021
1021
  const taskId = string$3(entry?.task_id);
1022
1022
  const description = string$3(entry?.description);
1023
1023
  const taskType = string$3(entry?.task_type);
@@ -1035,7 +1035,7 @@ function normalizeSystem(message) {
1035
1035
  detail: message
1036
1036
  }];
1037
1037
  if (subtype === "compact_boundary") {
1038
- const metadata = record$11(message.compact_metadata);
1038
+ const metadata = record$12(message.compact_metadata);
1039
1039
  const trigger = metadata?.trigger === "auto" || metadata?.trigger === "manual" ? metadata.trigger : void 0;
1040
1040
  const preTokens = finiteNumber(metadata?.pre_tokens);
1041
1041
  const postTokens = finiteNumber(metadata?.post_tokens);
@@ -1074,10 +1074,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
1074
1074
  function normalizeSdkMessage(message) {
1075
1075
  const value = message;
1076
1076
  if (value.type === "stream_event") {
1077
- const event = record$11(value.event);
1077
+ const event = record$12(value.event);
1078
1078
  const parentToolUseId = string$3(value.parent_tool_use_id);
1079
1079
  if (event?.type === "content_block_delta") {
1080
- const delta = record$11(event.delta);
1080
+ const delta = record$12(event.delta);
1081
1081
  if (delta?.type === "text_delta") {
1082
1082
  const text = string$3(delta.text);
1083
1083
  return text === void 0 ? [] : [{
@@ -1112,7 +1112,7 @@ function normalizeSdkMessage(message) {
1112
1112
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
1113
1113
  const terminalReason = string$3(value.terminal_reason);
1114
1114
  const userMessageUuid = string$3(value.user_message_uuid);
1115
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$11(item)).filter((item) => item !== void 0).map((item) => {
1115
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$12(item)).filter((item) => item !== void 0).map((item) => {
1116
1116
  const toolName = string$3(item.tool_name);
1117
1117
  const toolUseId = string$3(item.tool_use_id);
1118
1118
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
@@ -1138,7 +1138,7 @@ function normalizeSdkMessage(message) {
1138
1138
  detail: value.error ?? value.output
1139
1139
  }];
1140
1140
  if (value.type === "rate_limit_event") {
1141
- const status = string$3(record$11(value.rate_limit_info)?.status);
1141
+ const status = string$3(record$12(value.rate_limit_info)?.status);
1142
1142
  return [{
1143
1143
  kind: "status",
1144
1144
  title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
@@ -1177,7 +1177,7 @@ const FIXED_WINDOWS = [
1177
1177
  "seven_day_opus",
1178
1178
  "seven_day_sonnet"
1179
1179
  ];
1180
- function record$10(value) {
1180
+ function record$11(value) {
1181
1181
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1182
1182
  }
1183
1183
  /** Utilization is documented as 0-100; clamp so a server glitch cannot render
@@ -1189,7 +1189,7 @@ function resetsAt(value) {
1189
1189
  return typeof value === "string" && value.length > 0 ? value : void 0;
1190
1190
  }
1191
1191
  function window(id, source, label) {
1192
- const entry = record$10(source);
1192
+ const entry = record$11(source);
1193
1193
  if (entry === void 0) return void 0;
1194
1194
  const used = utilization(entry.utilization);
1195
1195
  const reset = resetsAt(entry.resets_at);
@@ -1203,9 +1203,9 @@ function window(id, source, label) {
1203
1203
  }
1204
1204
  /** Project the SDK's `/usage` response onto the windows the settings card shows. */
1205
1205
  function normalizePlanUsage(value, fetchedAt) {
1206
- const response = record$10(value);
1206
+ const response = record$11(value);
1207
1207
  const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
1208
- const limits = record$10(response?.rate_limits);
1208
+ const limits = record$11(response?.rate_limits);
1209
1209
  if (response?.rate_limits_available !== true || limits === void 0) return {
1210
1210
  available: false,
1211
1211
  ...subscription === void 0 ? {} : { subscription },
@@ -1213,7 +1213,7 @@ function normalizePlanUsage(value, fetchedAt) {
1213
1213
  fetchedAt
1214
1214
  };
1215
1215
  const windows = [...FIXED_WINDOWS.map((id) => window(id, limits[id])), ...(Array.isArray(limits.model_scoped) ? limits.model_scoped : []).map((entry, index) => {
1216
- const name = record$10(entry)?.display_name;
1216
+ const name = record$11(entry)?.display_name;
1217
1217
  return window(`model:${typeof name === "string" ? name : index}`, entry, typeof name === "string" ? name : void 0);
1218
1218
  })].filter((entry) => entry !== void 0);
1219
1219
  return {
@@ -2085,7 +2085,7 @@ var ClaudeSupervisor = class {
2085
2085
  entry.taskSnapshotAt = Date.now();
2086
2086
  await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
2087
2087
  }
2088
- /** What DSH is told about token usage.
2088
+ /** What DSH is told about token usage: newest call's prompt, whole turn's output.
2089
2089
  *
2090
2090
  * `TokenUsage` is documented as "token accounting for ONE model call", and
2091
2091
  * DSH's token meter divides `uncachedInput + cacheRead + cacheWrite` by the
@@ -2095,10 +2095,20 @@ var ClaudeSupervisor = class {
2095
2095
  * times, which sums past the window without the conversation ever growing.
2096
2096
  * The newest single call answers "how big is this conversation now".
2097
2097
  *
2098
- * The sidecar activity keeps the turn total instead that is the audit and
2099
- * cost record, and nothing divides it by a window. */
2098
+ * Output is deliberately excluded from that pressure sum, so the same
2099
+ * argument never applied to it — and taking it from the newest call reported
2100
+ * whatever the wrap-up message happened to cost, which is a couple of tokens
2101
+ * after a turn that wrote thousands. The turn total is the honest figure.
2102
+ *
2103
+ * The sidecar activity keeps the whole turn total for both — that is the
2104
+ * audit and cost record, and nothing divides it by a window. */
2100
2105
  #reportedUsage(active, result) {
2101
- return active.requestUsage ?? result.usage;
2106
+ const prompt = active.requestUsage;
2107
+ if (prompt === void 0) return result.usage;
2108
+ return {
2109
+ ...prompt,
2110
+ ...result.usage.outputTokens === void 0 ? {} : { outputTokens: result.usage.outputTokens }
2111
+ };
2102
2112
  }
2103
2113
  async #completeProgressSegment(active, result, recordUsage = true) {
2104
2114
  if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
@@ -3148,14 +3158,14 @@ function parseGitHubRemote(value) {
3148
3158
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
3149
3159
  return `${match[1]}/${match[2]}`;
3150
3160
  }
3151
- function record$9(value) {
3161
+ function record$10(value) {
3152
3162
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3153
3163
  }
3154
3164
  function aggregateChecks(value) {
3155
3165
  if (!Array.isArray(value) || value.length === 0) return "none";
3156
3166
  let pending = false;
3157
3167
  for (const item of value) {
3158
- const check = record$9(item);
3168
+ const check = record$10(item);
3159
3169
  if (check === void 0) continue;
3160
3170
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
3161
3171
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -3179,7 +3189,7 @@ function reviewState(value) {
3179
3189
  return "none";
3180
3190
  }
3181
3191
  function parsePullRequest(value) {
3182
- const input = record$9(value);
3192
+ const input = record$10(value);
3183
3193
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
3184
3194
  let url;
3185
3195
  try {
@@ -3200,7 +3210,7 @@ function parsePullRequest(value) {
3200
3210
  review: reviewState(input.reviewDecision),
3201
3211
  checks: aggregateChecks(input.statusCheckRollup),
3202
3212
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
3203
- ...typeof record$9(input.author)?.login === "string" ? { author: bounded(String(record$9(input.author)?.login)) } : {},
3213
+ ...typeof record$10(input.author)?.login === "string" ? { author: bounded(String(record$10(input.author)?.login)) } : {},
3204
3214
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
3205
3215
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
3206
3216
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
@@ -3981,6 +3991,24 @@ const MAX_UNPUSHED_COMMITS = 20;
3981
3991
  const GIT_TIMEOUT_MS$1 = 15e3;
3982
3992
  const REMOTE_TIMEOUT_MS = 6e4;
3983
3993
  const GENERATE_TIMEOUT_MS = 6e4;
3994
+ /** One cold `claude -p` per generation, so everything the subject line cannot
3995
+ * use is cost: MCP servers (which `ask` already skips for the same reason,
3996
+ * and which stall for as long as an unreachable one takes to give up) and the
3997
+ * user's own hooks and settings. Project settings stay: a repository's commit
3998
+ * conventions belong in the message. `--tools ''` keeps `--output-format`
3999
+ * between it and the prompt -- both flags are variadic. */
4000
+ const GENERATE_ARGUMENTS = [
4001
+ "-p",
4002
+ "--strict-mcp-config",
4003
+ "--mcp-config",
4004
+ "{\"mcpServers\":{}}",
4005
+ "--setting-sources",
4006
+ "project,local",
4007
+ "--tools",
4008
+ "",
4009
+ "--output-format",
4010
+ "text"
4011
+ ];
3984
4012
  var RepositoryActionError = class extends Error {
3985
4013
  code;
3986
4014
  commit;
@@ -4084,14 +4112,7 @@ var RepositoryActionService = class {
4084
4112
  `Diff:\n${preview.patch.slice(0, 24576)}`
4085
4113
  ].join("\n");
4086
4114
  try {
4087
- const result = await this.#run(this.#claudeExecutable, [
4088
- "-p",
4089
- "--tools",
4090
- "",
4091
- "--output-format",
4092
- "text",
4093
- prompt
4094
- ], preview.root, GENERATE_TIMEOUT_MS);
4115
+ const result = await this.#run(this.#claudeExecutable, GENERATE_ARGUMENTS.concat(prompt), preview.root, GENERATE_TIMEOUT_MS);
4095
4116
  return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
4096
4117
  } catch {
4097
4118
  return fallback;
@@ -4420,20 +4441,20 @@ var RepositoryActionService = class {
4420
4441
  };
4421
4442
  //#endregion
4422
4443
  //#region src/repository-setup-routes.ts
4423
- const MAX_BODY_BYTES$5 = 16384;
4424
- function record$8(value) {
4444
+ const MAX_BODY_BYTES$6 = 16384;
4445
+ function record$9(value) {
4425
4446
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4426
4447
  }
4427
- async function readJson$5(req) {
4448
+ async function readJson$6(req) {
4428
4449
  const chunks = [];
4429
4450
  let size = 0;
4430
4451
  for await (const chunk of req) {
4431
4452
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4432
4453
  size += buffer.length;
4433
- if (size > MAX_BODY_BYTES$5) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4454
+ if (size > MAX_BODY_BYTES$6) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4434
4455
  chunks.push(buffer);
4435
4456
  }
4436
- const value = record$8(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4457
+ const value = record$9(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4437
4458
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
4438
4459
  return value;
4439
4460
  }
@@ -4495,7 +4516,7 @@ function registerRepositorySetupRoute(ctx, service) {
4495
4516
  try {
4496
4517
  if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
4497
4518
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4498
- const input = await readJson$5(req);
4519
+ const input = await readJson$6(req);
4499
4520
  return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
4500
4521
  }
4501
4522
  if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
@@ -4506,19 +4527,19 @@ function registerRepositorySetupRoute(ctx, service) {
4506
4527
  }
4507
4528
  if (pathname === "/plugins/dsh-claude/repository/setup") {
4508
4529
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4509
- const input = await readJson$5(req);
4530
+ const input = await readJson$6(req);
4510
4531
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
4511
4532
  await streamSetup(res, service, input);
4512
4533
  return;
4513
4534
  }
4514
4535
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
4515
4536
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4516
- const input = await readJson$5(req);
4537
+ const input = await readJson$6(req);
4517
4538
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
4518
4539
  }
4519
4540
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
4520
4541
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4521
- const input = await readJson$5(req);
4542
+ const input = await readJson$6(req);
4522
4543
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
4523
4544
  return json(res, 200, { ok: true });
4524
4545
  }
@@ -4536,7 +4557,7 @@ function registerRepositorySetupRoute(ctx, service) {
4536
4557
  }
4537
4558
  //#endregion
4538
4559
  //#region src/repository-action-routes.ts
4539
- const MAX_BODY_BYTES$4 = 16384;
4560
+ const MAX_BODY_BYTES$5 = 16384;
4540
4561
  const MAX_SESSION_ID_CHARS$5 = 1024;
4541
4562
  const ACTIONS = /* @__PURE__ */ new Set([
4542
4563
  "commit",
@@ -4546,19 +4567,19 @@ const ACTIONS = /* @__PURE__ */ new Set([
4546
4567
  "merge-pr",
4547
4568
  "update-branch"
4548
4569
  ]);
4549
- function record$7(value) {
4570
+ function record$8(value) {
4550
4571
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4551
4572
  }
4552
- async function readJson$4(req) {
4573
+ async function readJson$5(req) {
4553
4574
  const chunks = [];
4554
4575
  let size = 0;
4555
4576
  for await (const chunk of req) {
4556
4577
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4557
4578
  size += buffer.length;
4558
- if (size > MAX_BODY_BYTES$4) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4579
+ if (size > MAX_BODY_BYTES$5) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4559
4580
  chunks.push(buffer);
4560
4581
  }
4561
- const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4582
+ const value = record$8(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4562
4583
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
4563
4584
  return value;
4564
4585
  }
@@ -4613,12 +4634,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4613
4634
  }
4614
4635
  if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
4615
4636
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4616
- const input = await readJson$4(req);
4637
+ const input = await readJson$5(req);
4617
4638
  return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
4618
4639
  }
4619
4640
  if (url.pathname === "/plugins/dsh-claude/repository/action") {
4620
4641
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4621
- return json(res, 200, await service.execute(cwd, actionRequest(await readJson$4(req))));
4642
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$5(req))));
4622
4643
  }
4623
4644
  return json(res, 404, { error: "not found" });
4624
4645
  } catch (error) {
@@ -4818,6 +4839,9 @@ const MAX_OUTPUT_BYTES = 524288;
4818
4839
  const GIT_TIMEOUT_MS = 1e4;
4819
4840
  const GH_TIMEOUT_MS = 6e4;
4820
4841
  const MAX_COMMENTS = 100;
4842
+ const MAX_THREADS = 100;
4843
+ const MAX_MENTIONABLE_USERS = 20;
4844
+ const MAX_REPLY_CHARS$1 = 2e3;
4821
4845
  const MAX_COMMENT_CHARS = 4096;
4822
4846
  const MAX_FAILING_CHECKS = 3;
4823
4847
  const MAX_LOG_CHARS = 8192;
@@ -4829,6 +4853,31 @@ var PullRequestFeedbackError = class extends Error {
4829
4853
  this.code = code;
4830
4854
  }
4831
4855
  };
4856
+ /** Threads carry the anchor; comments carry the prose. `isOutdated` marks a
4857
+ * thread whose lines the branch has since moved past. */
4858
+ const REVIEW_THREADS_QUERY = `query($owner:String!,$name:String!,$number:Int!){
4859
+ repository(owner:$owner,name:$name){
4860
+ pullRequest(number:$number){
4861
+ reviewThreads(first:100){
4862
+ nodes{
4863
+ id isResolved isOutdated path line originalLine diffSide
4864
+ comments(first:50){ nodes{ databaseId body url createdAt author{ __typename login avatarUrl } } }
4865
+ }
4866
+ }
4867
+ }
4868
+ }
4869
+ }`;
4870
+ const RESOLVE_MUTATION = `mutation($threadId:ID!){
4871
+ resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
4872
+ }`;
4873
+ const UNRESOLVE_MUTATION = `mutation($threadId:ID!){
4874
+ unresolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
4875
+ }`;
4876
+ const MENTIONABLE_USERS_QUERY = `query($owner:String!,$name:String!,$q:String!){
4877
+ repository(owner:$owner,name:$name){
4878
+ mentionableUsers(first:20,query:$q){ nodes{ login avatarUrl } }
4879
+ }
4880
+ }`;
4832
4881
  async function collect(handle) {
4833
4882
  const outcome = await handle.done;
4834
4883
  const stdout = handle.collected.stdout?.readFrom(0);
@@ -4838,34 +4887,100 @@ async function collect(handle) {
4838
4887
  lossy: stdout?.lossy === true
4839
4888
  };
4840
4889
  }
4841
- function record$6(value) {
4890
+ function record$7(value) {
4842
4891
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4843
4892
  }
4844
- function parseReviewComments(value) {
4845
- if (!Array.isArray(value)) return [];
4846
- const comments = [];
4847
- for (const item of value) {
4848
- const input = record$6(item);
4849
- if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4850
- const body = typeof input.body === "string" ? input.body.trim() : "";
4893
+ /** Read `repository.pullRequest.reviewThreads.nodes` out of a GraphQL response.
4894
+ * A payload carrying `errors` instead of data yields nothing rather than
4895
+ * throwing: the caller distinguishes "no threads" from "call failed" by the
4896
+ * process exit code. */
4897
+ function parseReviewThreads(value) {
4898
+ const nodes = record$7(record$7(record$7(record$7(record$7(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
4899
+ if (!Array.isArray(nodes)) return [];
4900
+ const threads = [];
4901
+ let total = 0;
4902
+ for (const item of nodes) {
4903
+ const input = record$7(item);
4904
+ if (input === void 0 || typeof input.id !== "string" || input.id.length === 0) continue;
4851
4905
  const path = typeof input.path === "string" ? input.path : "";
4852
- const url = typeof input.html_url === "string" ? input.html_url : "";
4853
- if (body.length === 0 || path.length === 0) continue;
4854
- const avatarUrl = githubAvatarUrl(record$6(input.user)?.avatar_url);
4855
- const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.original_line) ? Number(input.original_line) : void 0;
4856
- comments.push({
4857
- id: Number(input.id),
4906
+ if (path.length === 0) continue;
4907
+ const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.originalLine) ? Number(input.originalLine) : void 0;
4908
+ const side = input.diffSide === "LEFT" ? "old" : "new";
4909
+ const anchor = {
4858
4910
  path,
4859
4911
  ...line === void 0 ? {} : { line },
4860
- side: input.side === "LEFT" ? "old" : "new",
4861
- author: typeof record$6(input.user)?.login === "string" ? String(record$6(input.user)?.login) : "unknown",
4862
- ...avatarUrl === void 0 ? {} : { avatarUrl },
4863
- body: body.slice(0, MAX_COMMENT_CHARS),
4864
- url
4912
+ side
4913
+ };
4914
+ const comments = [];
4915
+ const commentNodes = record$7(input.comments)?.nodes;
4916
+ for (const node of Array.isArray(commentNodes) ? commentNodes : []) {
4917
+ if (total >= MAX_COMMENTS) break;
4918
+ const comment = record$7(node);
4919
+ if (comment === void 0 || !Number.isSafeInteger(comment.databaseId)) continue;
4920
+ const body = typeof comment.body === "string" ? comment.body.trim() : "";
4921
+ if (body.length === 0) continue;
4922
+ const author = record$7(comment.author);
4923
+ const avatarUrl = githubAvatarUrl(author?.avatarUrl);
4924
+ const login = typeof author?.login === "string" ? author.login : "unknown";
4925
+ comments.push({
4926
+ id: Number(comment.databaseId),
4927
+ ...anchor,
4928
+ author: login,
4929
+ ...author?.__typename === "Bot" || login.endsWith("[bot]") ? { bot: true } : {},
4930
+ ...avatarUrl === void 0 ? {} : { avatarUrl },
4931
+ body: body.slice(0, MAX_COMMENT_CHARS),
4932
+ url: typeof comment.url === "string" ? comment.url : "",
4933
+ ...typeof comment.createdAt === "string" ? { createdAt: comment.createdAt } : {}
4934
+ });
4935
+ total += 1;
4936
+ }
4937
+ if (comments.length === 0) continue;
4938
+ threads.push({
4939
+ id: input.id,
4940
+ ...anchor,
4941
+ resolved: input.isResolved === true,
4942
+ outdated: input.isOutdated === true,
4943
+ comments
4865
4944
  });
4866
- if (comments.length >= MAX_COMMENTS) break;
4945
+ if (threads.length >= MAX_THREADS || total >= MAX_COMMENTS) break;
4946
+ }
4947
+ return threads;
4948
+ }
4949
+ /** One posted reply, shaped like the thread comments it joins. */
4950
+ function parseReplyComment(value, anchor) {
4951
+ const input = record$7(value);
4952
+ if (input === void 0 || !Number.isSafeInteger(input.id)) return void 0;
4953
+ const body = typeof input.body === "string" ? input.body.trim() : "";
4954
+ if (body.length === 0) return void 0;
4955
+ const user = record$7(input.user);
4956
+ const avatarUrl = githubAvatarUrl(user?.avatar_url);
4957
+ const login = typeof user?.login === "string" ? user.login : "unknown";
4958
+ return {
4959
+ id: Number(input.id),
4960
+ ...anchor,
4961
+ author: login,
4962
+ ...user?.type === "Bot" || login.endsWith("[bot]") ? { bot: true } : {},
4963
+ ...avatarUrl === void 0 ? {} : { avatarUrl },
4964
+ body: body.slice(0, MAX_COMMENT_CHARS),
4965
+ url: typeof input.html_url === "string" ? input.html_url : "",
4966
+ ...typeof input.created_at === "string" ? { createdAt: input.created_at } : {}
4967
+ };
4968
+ }
4969
+ function parseMentionableUsers(value) {
4970
+ const nodes = record$7(record$7(record$7(record$7(value)?.data)?.repository)?.mentionableUsers)?.nodes;
4971
+ if (!Array.isArray(nodes)) return [];
4972
+ const users = [];
4973
+ for (const item of nodes) {
4974
+ const input = record$7(item);
4975
+ if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
4976
+ const avatarUrl = githubAvatarUrl(input.avatarUrl);
4977
+ users.push({
4978
+ login: input.login,
4979
+ ...avatarUrl === void 0 ? {} : { avatarUrl }
4980
+ });
4981
+ if (users.length >= MAX_MENTIONABLE_USERS) break;
4867
4982
  }
4868
- return comments;
4983
+ return users;
4869
4984
  }
4870
4985
  /** Extract the Actions job id from a check run link, when it is one. */
4871
4986
  function actionsJobId(link) {
@@ -4876,7 +4991,7 @@ function parseFailingChecks(value) {
4876
4991
  if (!Array.isArray(value)) return [];
4877
4992
  const failing = [];
4878
4993
  for (const item of value) {
4879
- const input = record$6(item);
4994
+ const input = record$7(item);
4880
4995
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4881
4996
  failing.push({
4882
4997
  name: input.name,
@@ -4898,22 +5013,103 @@ var PullRequestFeedbackService = class {
4898
5013
  constructor(runtime) {
4899
5014
  this.#runtime = runtime;
4900
5015
  }
4901
- async comments(cwd, pullNumber) {
4902
- const repository = await this.#repository(cwd);
5016
+ async threads(cwd, pullNumber) {
5017
+ const { owner, name } = await this.#repositoryParts(cwd);
4903
5018
  const gh = await this.#gh();
4904
5019
  const result = await this.#run(gh, [
4905
5020
  "api",
4906
- `repos/${repository}/pulls/${pullNumber}/comments`,
4907
- "--paginate"
5021
+ "graphql",
5022
+ "-f",
5023
+ `owner=${owner}`,
5024
+ "-f",
5025
+ `name=${name}`,
5026
+ "-F",
5027
+ `number=${pullNumber}`,
5028
+ "-f",
5029
+ `query=${REVIEW_THREADS_QUERY}`
4908
5030
  ], cwd, GH_TIMEOUT_MS);
4909
5031
  if (result.exitCode !== 0) throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be loaded.");
4910
5032
  try {
4911
- const pages = result.stdout.replaceAll("][", ",");
4912
- return parseReviewComments(JSON.parse(pages));
5033
+ return parseReviewThreads(JSON.parse(result.stdout));
4913
5034
  } catch {
4914
5035
  throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be parsed.");
4915
5036
  }
4916
5037
  }
5038
+ /** Post a reply into the thread the given comment belongs to. GitHub parses
5039
+ * `@login` in the body server-side, so mentions need nothing from us. */
5040
+ async reply(cwd, pullNumber, commentId, body) {
5041
+ const text = body.trim();
5042
+ if (text.length === 0 || text.length > MAX_REPLY_CHARS$1) throw new PullRequestFeedbackError("invalid-request", "The reply body is invalid.");
5043
+ const repository = await this.#repository(cwd);
5044
+ const gh = await this.#gh();
5045
+ const result = await this.#run(gh, [
5046
+ "api",
5047
+ "-X",
5048
+ "POST",
5049
+ `repos/${repository}/pulls/${pullNumber}/comments/${commentId}/replies`,
5050
+ "--input",
5051
+ "-"
5052
+ ], cwd, GH_TIMEOUT_MS, JSON.stringify({ body: text }));
5053
+ if (result.exitCode !== 0) throw new PullRequestFeedbackError("reply-failed", "The reply could not be posted.");
5054
+ let posted;
5055
+ try {
5056
+ const parsed = JSON.parse(result.stdout);
5057
+ const path = typeof record$7(parsed)?.path === "string" ? String(record$7(parsed)?.path) : "";
5058
+ const line = Number.isSafeInteger(record$7(parsed)?.line) ? Number(record$7(parsed)?.line) : void 0;
5059
+ posted = parseReplyComment(parsed, {
5060
+ path,
5061
+ ...line === void 0 ? {} : { line },
5062
+ side: record$7(parsed)?.side === "LEFT" ? "old" : "new"
5063
+ });
5064
+ } catch {
5065
+ posted = void 0;
5066
+ }
5067
+ if (posted === void 0) throw new PullRequestFeedbackError("reply-failed", "The posted reply could not be read back.");
5068
+ return posted;
5069
+ }
5070
+ /** Resolve or reopen a thread. Returns the state GitHub reports afterwards. */
5071
+ async setResolved(cwd, threadId, resolved) {
5072
+ const gh = await this.#gh();
5073
+ const result = await this.#run(gh, [
5074
+ "api",
5075
+ "graphql",
5076
+ "-f",
5077
+ `threadId=${threadId}`,
5078
+ "-f",
5079
+ `query=${resolved ? RESOLVE_MUTATION : UNRESOLVE_MUTATION}`
5080
+ ], cwd, GH_TIMEOUT_MS);
5081
+ if (result.exitCode !== 0) throw new PullRequestFeedbackError("resolve-failed", "The thread could not be updated.");
5082
+ try {
5083
+ const thread = record$7(record$7(record$7(record$7(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
5084
+ if (typeof thread?.isResolved !== "boolean") throw new Error("missing state");
5085
+ return thread.isResolved;
5086
+ } catch {
5087
+ throw new PullRequestFeedbackError("resolve-failed", "The thread state could not be read back.");
5088
+ }
5089
+ }
5090
+ /** Logins GitHub would notify from this repository, for the reply composer. */
5091
+ async mentionables(cwd, query) {
5092
+ const { owner, name } = await this.#repositoryParts(cwd);
5093
+ const gh = await this.#gh();
5094
+ const result = await this.#run(gh, [
5095
+ "api",
5096
+ "graphql",
5097
+ "-f",
5098
+ `owner=${owner}`,
5099
+ "-f",
5100
+ `name=${name}`,
5101
+ "-f",
5102
+ `q=${query}`,
5103
+ "-f",
5104
+ `query=${MENTIONABLE_USERS_QUERY}`
5105
+ ], cwd, GH_TIMEOUT_MS);
5106
+ if (result.exitCode !== 0) return [];
5107
+ try {
5108
+ return parseMentionableUsers(JSON.parse(result.stdout));
5109
+ } catch {
5110
+ return [];
5111
+ }
5112
+ }
4917
5113
  async failingChecks(cwd, pullNumber) {
4918
5114
  const gh = await this.#gh();
4919
5115
  const result = await this.#run(gh, [
@@ -4961,6 +5157,16 @@ var PullRequestFeedbackService = class {
4961
5157
  if (repository === void 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
4962
5158
  return repository;
4963
5159
  }
5160
+ /** GraphQL takes the owner and the name as separate variables, so they never
5161
+ * reach the query text itself. */
5162
+ async #repositoryParts(cwd) {
5163
+ const [owner, name] = (await this.#repository(cwd)).split("/");
5164
+ if (owner === void 0 || name === void 0 || owner.length === 0 || name.length === 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
5165
+ return {
5166
+ owner,
5167
+ name
5168
+ };
5169
+ }
4964
5170
  #git() {
4965
5171
  this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
4966
5172
  return this.#gitExecutable;
@@ -4971,24 +5177,61 @@ var PullRequestFeedbackService = class {
4971
5177
  });
4972
5178
  return this.#ghExecutable;
4973
5179
  }
4974
- #run(executable, args, cwd, timeoutMs) {
4975
- return collect(this.#runtime.spawn({
5180
+ #run(executable, args, cwd, timeoutMs, input) {
5181
+ const handle = this.#runtime.spawn({
4976
5182
  argv: [executable, ...args],
4977
5183
  cwd,
4978
5184
  stdio: {
4979
- stdin: "ignore",
5185
+ stdin: input === void 0 ? "ignore" : "pipe",
4980
5186
  stdout: { maxBytes: MAX_OUTPUT_BYTES },
4981
5187
  stderr: { maxBytes: 65536 }
4982
5188
  },
4983
5189
  graceMs: 1e3,
4984
5190
  signal: AbortSignal.timeout(timeoutMs),
4985
5191
  env: {}
4986
- }));
5192
+ });
5193
+ if (input !== void 0) handle.stdin?.end(input);
5194
+ return collect(handle);
4987
5195
  }
4988
5196
  };
4989
5197
  //#endregion
4990
5198
  //#region src/pr-feedback-routes.ts
4991
5199
  const MAX_SESSION_ID_CHARS$3 = 1024;
5200
+ const MAX_BODY_BYTES$4 = 16384;
5201
+ const MAX_REPLY_CHARS = 2e3;
5202
+ const MAX_THREAD_ID_CHARS = 512;
5203
+ const MAX_MENTION_QUERY_CHARS = 64;
5204
+ function record$6(value) {
5205
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5206
+ }
5207
+ async function readJson$4(req) {
5208
+ const chunks = [];
5209
+ let size = 0;
5210
+ for await (const chunk of req) {
5211
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5212
+ size += buffer.length;
5213
+ if (size > MAX_BODY_BYTES$4) throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
5214
+ chunks.push(buffer);
5215
+ }
5216
+ const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5217
+ if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
5218
+ return value;
5219
+ }
5220
+ function replyBody(input) {
5221
+ const body = typeof input.body === "string" ? input.body.trim() : "";
5222
+ if (body.length === 0 || body.length > MAX_REPLY_CHARS || body.includes("\0")) throw new PullRequestFeedbackError("invalid-request", "The reply body is invalid.");
5223
+ return body;
5224
+ }
5225
+ function commentId(input) {
5226
+ const value = input.commentId;
5227
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new PullRequestFeedbackError("invalid-request", "The comment id is invalid.");
5228
+ return value;
5229
+ }
5230
+ function threadId(input) {
5231
+ const value = input.threadId;
5232
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_THREAD_ID_CHARS) throw new PullRequestFeedbackError("invalid-request", "The thread id is invalid.");
5233
+ return value;
5234
+ }
4992
5235
  function sessionId(url) {
4993
5236
  const value = url.searchParams.get("sessionId");
4994
5237
  if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
@@ -5005,19 +5248,43 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
5005
5248
  path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
5006
5249
  handler: async (req, res) => {
5007
5250
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
5008
- if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
5009
5251
  const url = new URL(req.url ?? "/", "http://localhost");
5252
+ const reads = req.method === "GET";
5253
+ const writes = req.method === "POST";
5010
5254
  try {
5011
5255
  const cwd = cwdForSession(sessionId(url));
5012
5256
  if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
5013
- if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) return json(res, 200, { comments: await service.comments(cwd, pullNumber(url)) });
5014
- if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) return json(res, 200, { checks: await service.failingChecks(cwd, pullNumber(url)) });
5257
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) {
5258
+ if (!reads) return json(res, 405, { error: "method not allowed" });
5259
+ return json(res, 200, { threads: await service.threads(cwd, pullNumber(url)) });
5260
+ }
5261
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) {
5262
+ if (!reads) return json(res, 405, { error: "method not allowed" });
5263
+ return json(res, 200, { checks: await service.failingChecks(cwd, pullNumber(url)) });
5264
+ }
5265
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/mentionables`) {
5266
+ if (!reads) return json(res, 405, { error: "method not allowed" });
5267
+ const query = (url.searchParams.get("q") ?? "").slice(0, MAX_MENTION_QUERY_CHARS);
5268
+ return json(res, 200, { users: await service.mentionables(cwd, query) });
5269
+ }
5270
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/reply`) {
5271
+ if (!writes) return json(res, 405, { error: "method not allowed" });
5272
+ const input = await readJson$4(req);
5273
+ return json(res, 200, { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) });
5274
+ }
5275
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/resolve`) {
5276
+ if (!writes) return json(res, 405, { error: "method not allowed" });
5277
+ const input = await readJson$4(req);
5278
+ if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
5279
+ return json(res, 200, { resolved: await service.setResolved(cwd, threadId(input), input.resolved) });
5280
+ }
5015
5281
  return json(res, 404, { error: "not found" });
5016
5282
  } catch (error) {
5017
5283
  if (error instanceof PullRequestFeedbackError) return json(res, 409, {
5018
5284
  error: error.code,
5019
5285
  message: error.message
5020
5286
  });
5287
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
5021
5288
  return json(res, 500, {
5022
5289
  error: "pr-feedback-unavailable",
5023
5290
  message: "Pull request feedback is unavailable."
@@ -5786,6 +6053,52 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
5786
6053
  }), "dsh-claude: review comment route");
5787
6054
  }
5788
6055
  //#endregion
6056
+ //#region src/client-diagnostics-routes.ts
6057
+ /** Enough for a message plus a trimmed stack; the client caps its own volume. */
6058
+ const MAX_DIAGNOSTIC_BYTES = 8192;
6059
+ const MAX_DETAIL_CHARS = 2e3;
6060
+ const MAX_KIND_CHARS = 60;
6061
+ async function readBody(req) {
6062
+ const chunks = [];
6063
+ let size = 0;
6064
+ for await (const chunk of req) {
6065
+ const buffer = chunk;
6066
+ size += buffer.length;
6067
+ if (size > MAX_DIAGNOSTIC_BYTES) throw new Error("diagnostic too large");
6068
+ chunks.push(buffer);
6069
+ }
6070
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
6071
+ }
6072
+ /** `POST <path>` with `{ kind, detail }`: write one renderer finding to the Host log.
6073
+ *
6074
+ * The renderer has no other way to speak. A Slot entry that throws is caught
6075
+ * by the Host's Slot system and dropped, the shipped Desktop opens no
6076
+ * DevTools, and startup still reports `rendererStatus: "healthy"` — so a
6077
+ * plugin whose UI died silently is indistinguishable from a working one.
6078
+ * Every finding here is data written by this package's own client half, but
6079
+ * it is still bounded and redacted like any other untrusted input. */
6080
+ function registerClaudeClientDiagnosticsRoute(ctx) {
6081
+ ctx.effect(() => ctx.webServer.register({
6082
+ kind: "exact",
6083
+ path: CLAUDE_CLIENT_DIAGNOSTICS_PATH,
6084
+ handler: async (req, res) => {
6085
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
6086
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
6087
+ try {
6088
+ const body = await readBody(req);
6089
+ const kind = typeof body?.kind === "string" ? body.kind.slice(0, MAX_KIND_CHARS) : "unknown";
6090
+ const detail = typeof body?.detail === "string" ? body.detail : "";
6091
+ if (detail === "") return json(res, 400, { error: "invalid-request" });
6092
+ ctx.logger.warn(`dsh-claude client [${kind}]: ${redactText(detail, MAX_DETAIL_CHARS)}`);
6093
+ return json(res, 200, { ok: true });
6094
+ } catch (error) {
6095
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
6096
+ return json(res, 400, { error: "invalid-request" });
6097
+ }
6098
+ }
6099
+ }), "dsh-claude: client diagnostics route");
6100
+ }
6101
+ //#endregion
5789
6102
  //#region src/rewind-routes.ts
5790
6103
  const MAX_BODY_BYTES = 4096;
5791
6104
  const MAX_SESSION_ID_CHARS = 1024;
@@ -6656,6 +6969,7 @@ async function apply(ctx, config) {
6656
6969
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
6657
6970
  ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
6658
6971
  ctx.inject(["webServer"], (webCtx) => {
6972
+ registerClaudeClientDiagnosticsRoute(webCtx);
6659
6973
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
6660
6974
  const desktopActions = webCtx.get("desktopActions");
6661
6975
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });