@norman-else/dsh-claude 0.1.25 → 0.1.27

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,10 +1,10 @@
1
- import { C as CLAUDE_REVIEW_COMMENT_PATH, D as TASK_TOOL_NAMES, S as CLAUDE_REPOSITORY_STATUS_PATH, T as CLAUDE_UPDATE_PATH, _ as CLAUDE_JIRA_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_FEEDBACK_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_PROJECTION_PATH, w as CLAUDE_UPDATE_CHECK_PATH, x as CLAUDE_REPOSITORY_SETUP_PATH, y as CLAUDE_REPOSITORY_ACTION_PATH } from "./events-BhU0iJUi.mjs";
2
- import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-B48egrCr.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-Bzw8tuGI.mjs";
1
+ import { A as TASK_TOOL_NAMES, C as CLAUDE_REPOSITORY_SETUP_PATH, D as CLAUDE_UPDATE_PATH, E as CLAUDE_UPDATE_CHECK_PATH, O as CLAUDE_USAGE_PATH, S as CLAUDE_REPOSITORY_FILE_PATH, T as CLAUDE_REVIEW_COMMENT_PATH, _ as CLAUDE_GLOBAL_SETTINGS_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_ACTION_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, 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_JIRA_PATH, w as CLAUDE_REPOSITORY_STATUS_PATH, x as CLAUDE_REPOSITORY_FEEDBACK_PATH, y as CLAUDE_PROJECTION_PATH } from "./events-lDt9nTUw.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-BYj0VF4J.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-CPOH9lAr.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";
7
- import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
7
+ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
8
8
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
9
  import { homedir } from "node:os";
10
10
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -12,6 +12,7 @@ import { EventEmitter } from "node:events";
12
12
  import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
13
13
  import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
14
14
  import { fileURLToPath } from "node:url";
15
+ import { StringDecoder } from "node:string_decoder";
15
16
  //#region src/sidecar.ts
16
17
  const SIDECAR_SCHEMA_VERSION = 1;
17
18
  const MAX_ACTIVITIES = 1e4;
@@ -25,7 +26,7 @@ function emptyProjection() {
25
26
  activities: []
26
27
  };
27
28
  }
28
- function record$8(value) {
29
+ function record$11(value) {
29
30
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
30
31
  }
31
32
  function finiteInteger(value) {
@@ -35,7 +36,7 @@ function string$4(value, max) {
35
36
  return typeof value === "string" && value.length > 0 && value.length <= max;
36
37
  }
37
38
  function binding(value) {
38
- const input = record$8(value);
39
+ const input = record$11(value);
39
40
  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;
40
41
  return {
41
42
  claudeSessionId: input.claudeSessionId,
@@ -65,22 +66,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
65
66
  "failed"
66
67
  ]);
67
68
  function activity(value) {
68
- const input = record$8(value);
69
+ const input = record$11(value);
69
70
  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;
70
71
  return normalizeActivity(input);
71
72
  }
72
73
  function contextUsage(value) {
73
- const input = record$8(value);
74
+ const input = record$11(value);
74
75
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
75
76
  return normalizeContextUsage(input);
76
77
  }
77
78
  function tasks(value) {
78
- const input = record$8(value);
79
+ const input = record$11(value);
79
80
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
80
81
  return normalizeTasksEvent(input.tasks);
81
82
  }
82
83
  function parseClaudeSidecar(value) {
83
- const input = record$8(value);
84
+ const input = record$11(value);
84
85
  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");
85
86
  const activities = input.activities.map(activity);
86
87
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -111,7 +112,7 @@ function mergeActivities(existing, additions) {
111
112
  function normalizeBinding(input) {
112
113
  return {
113
114
  claudeSessionId: redactText(input.claudeSessionId, 512),
114
- sdkVersion: redactText(input.sdkVersion ?? "0.3.233", 128),
115
+ sdkVersion: redactText(input.sdkVersion ?? "0.3.247", 128),
115
116
  cwd: redactText(input.cwd, 4096),
116
117
  ...input.cliVersion === void 0 ? {} : { cliVersion: redactText(input.cliVersion, 128) }
117
118
  };
@@ -653,7 +654,7 @@ function createUserQuestionBridge(userQuestions, activeContext) {
653
654
  }
654
655
  //#endregion
655
656
  //#region src/sdk-messages.ts
656
- function record$7(value) {
657
+ function record$10(value) {
657
658
  return value !== null && typeof value === "object" ? value : void 0;
658
659
  }
659
660
  function string$3(value) {
@@ -668,7 +669,7 @@ function taskUsageOf(usage) {
668
669
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
669
670
  }
670
671
  function resultUsage(message) {
671
- const usage = record$7(message.usage);
672
+ const usage = record$10(message.usage);
672
673
  const normalized = {};
673
674
  if (usage !== void 0) {
674
675
  if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
@@ -680,7 +681,7 @@ function resultUsage(message) {
680
681
  return normalized;
681
682
  }
682
683
  function normalizeAssistant(message) {
683
- const content = record$7(message.message)?.content;
684
+ const content = record$10(message.message)?.content;
684
685
  if (!Array.isArray(content)) return [{
685
686
  kind: "protocol-error",
686
687
  title: "Malformed Claude assistant message",
@@ -689,7 +690,7 @@ function normalizeAssistant(message) {
689
690
  const parentToolUseId = string$3(message.parent_tool_use_id);
690
691
  const normalized = [];
691
692
  for (const item of content) {
692
- const block = record$7(item);
693
+ const block = record$10(item);
693
694
  if (block === void 0) continue;
694
695
  if (block.type === "text") {
695
696
  const text = string$3(block.text);
@@ -722,7 +723,7 @@ function normalizeAssistant(message) {
722
723
  }
723
724
  function normalizeUser(message) {
724
725
  if (message.isReplay === true) return [];
725
- const content = record$7(message.message)?.content;
726
+ const content = record$10(message.message)?.content;
726
727
  if (typeof content === "string") return [];
727
728
  if (!Array.isArray(content)) return [{
728
729
  kind: "protocol-error",
@@ -732,7 +733,7 @@ function normalizeUser(message) {
732
733
  const parentToolUseId = string$3(message.parent_tool_use_id);
733
734
  const normalized = [];
734
735
  for (const item of content) {
735
- const block = record$7(item);
736
+ const block = record$10(item);
736
737
  if (block?.type !== "tool_result") continue;
737
738
  const toolUseId = string$3(block.tool_use_id);
738
739
  if (toolUseId === void 0) continue;
@@ -813,7 +814,7 @@ function normalizeSystem(message) {
813
814
  const summary = string$3(message.summary);
814
815
  const subagentType = string$3(message.subagent_type);
815
816
  const lastToolName = string$3(message.last_tool_name);
816
- const usage = taskUsageOf(record$7(message.usage));
817
+ const usage = taskUsageOf(record$10(message.usage));
817
818
  return [{
818
819
  kind: "subagent",
819
820
  title: summary ?? description ?? "Claude subagent update",
@@ -829,7 +830,7 @@ function normalizeSystem(message) {
829
830
  }];
830
831
  }
831
832
  if (subtype === "task_updated") {
832
- const patch = record$7(message.patch);
833
+ const patch = record$10(message.patch);
833
834
  const status = string$3(patch?.status);
834
835
  const taskId = string$3(message.task_id);
835
836
  const description = string$3(patch?.description);
@@ -852,7 +853,7 @@ function normalizeSystem(message) {
852
853
  const taskId = string$3(message.task_id);
853
854
  const summary = string$3(message.summary);
854
855
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
855
- const usage = taskUsageOf(record$7(message.usage));
856
+ const usage = taskUsageOf(record$10(message.usage));
856
857
  return [{
857
858
  kind: "subagent",
858
859
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -867,7 +868,7 @@ function normalizeSystem(message) {
867
868
  if (subtype === "background_tasks_changed") return [{
868
869
  kind: "background-tasks",
869
870
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
870
- const entry = record$7(item);
871
+ const entry = record$10(item);
871
872
  const taskId = string$3(entry?.task_id);
872
873
  const description = string$3(entry?.description);
873
874
  const taskType = string$3(entry?.task_type);
@@ -910,10 +911,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
910
911
  function normalizeSdkMessage(message) {
911
912
  const value = message;
912
913
  if (value.type === "stream_event") {
913
- const event = record$7(value.event);
914
+ const event = record$10(value.event);
914
915
  const parentToolUseId = string$3(value.parent_tool_use_id);
915
916
  if (event?.type === "content_block_delta") {
916
- const delta = record$7(event.delta);
917
+ const delta = record$10(event.delta);
917
918
  if (delta?.type === "text_delta") {
918
919
  const text = string$3(delta.text);
919
920
  return text === void 0 ? [] : [{
@@ -948,7 +949,7 @@ function normalizeSdkMessage(message) {
948
949
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
949
950
  const terminalReason = string$3(value.terminal_reason);
950
951
  const userMessageUuid = string$3(value.user_message_uuid);
951
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$7(item)).filter((item) => item !== void 0).map((item) => {
952
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$10(item)).filter((item) => item !== void 0).map((item) => {
952
953
  const toolName = string$3(item.tool_name);
953
954
  const toolUseId = string$3(item.tool_use_id);
954
955
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
@@ -974,7 +975,7 @@ function normalizeSdkMessage(message) {
974
975
  detail: value.error ?? value.output
975
976
  }];
976
977
  if (value.type === "rate_limit_event") {
977
- const status = string$3(record$7(value.rate_limit_info)?.status);
978
+ const status = string$3(record$10(value.rate_limit_info)?.status);
978
979
  return [{
979
980
  kind: "status",
980
981
  title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
@@ -988,6 +989,117 @@ function normalizeSdkMessage(message) {
988
989
  }];
989
990
  }
990
991
  //#endregion
992
+ //#region src/plan-usage.ts
993
+ /** Claude.ai plan rate-limit windows behind the CLI's `/usage` panel.
994
+ *
995
+ * The Agent SDK exposes them through an experimental query method whose name
996
+ * advertises its own instability, so every read is guarded: a missing method,
997
+ * a shape change, or an API-key session all degrade to `available: false`
998
+ * instead of breaking the settings page. */
999
+ /** The SDK's own name for the unstable `/usage` control request; it is
1000
+ * documented to change when the API stabilizes, so it lives in one place. */
1001
+ const PLAN_USAGE_METHOD = "usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET";
1002
+ /** A throwaway probe should not outlive a wedged control request. */
1003
+ const PLAN_USAGE_TIMEOUT_MS = 2e4;
1004
+ /** Invoke the experimental usage control request, or say why we cannot. */
1005
+ function readPlanUsageFrom(query) {
1006
+ const read = query[PLAN_USAGE_METHOD];
1007
+ if (typeof read !== "function") throw new Error("dsh-claude: this Claude Agent SDK build exposes no plan usage API");
1008
+ return read.call(query);
1009
+ }
1010
+ /** Fixed windows in display order; the SDK omits the ones a plan does not have. */
1011
+ const FIXED_WINDOWS = [
1012
+ "five_hour",
1013
+ "seven_day",
1014
+ "seven_day_opus",
1015
+ "seven_day_sonnet"
1016
+ ];
1017
+ function record$9(value) {
1018
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1019
+ }
1020
+ /** Utilization is documented as 0-100; clamp so a server glitch cannot render
1021
+ * a bar wider than its track or a negative width. */
1022
+ function utilization(value) {
1023
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : void 0;
1024
+ }
1025
+ function resetsAt(value) {
1026
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1027
+ }
1028
+ function window(id, source, label) {
1029
+ const entry = record$9(source);
1030
+ if (entry === void 0) return void 0;
1031
+ const used = utilization(entry.utilization);
1032
+ const reset = resetsAt(entry.resets_at);
1033
+ if (used === void 0 && reset === void 0) return void 0;
1034
+ return {
1035
+ id,
1036
+ ...label === void 0 ? {} : { label },
1037
+ ...used === void 0 ? {} : { utilization: used },
1038
+ ...reset === void 0 ? {} : { resetsAt: reset }
1039
+ };
1040
+ }
1041
+ /** Project the SDK's `/usage` response onto the windows the settings card shows. */
1042
+ function normalizePlanUsage(value, fetchedAt) {
1043
+ const response = record$9(value);
1044
+ const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
1045
+ const limits = record$9(response?.rate_limits);
1046
+ if (response?.rate_limits_available !== true || limits === void 0) return {
1047
+ available: false,
1048
+ ...subscription === void 0 ? {} : { subscription },
1049
+ windows: [],
1050
+ fetchedAt
1051
+ };
1052
+ const windows = [...FIXED_WINDOWS.map((id) => window(id, limits[id])), ...(Array.isArray(limits.model_scoped) ? limits.model_scoped : []).map((entry, index) => {
1053
+ const name = record$9(entry)?.display_name;
1054
+ return window(`model:${typeof name === "string" ? name : index}`, entry, typeof name === "string" ? name : void 0);
1055
+ })].filter((entry) => entry !== void 0);
1056
+ return {
1057
+ available: windows.length > 0,
1058
+ ...subscription === void 0 ? {} : { subscription },
1059
+ windows,
1060
+ fetchedAt
1061
+ };
1062
+ }
1063
+ /** Read plan limits from a throwaway Claude process.
1064
+ *
1065
+ * Deliberately NOT routed through the supervisor. Borrowing a session's
1066
+ * process fails outright while that session is mid-turn, and for a session
1067
+ * with no process yet it would resume the whole conversation — and possibly
1068
+ * evict another idle session to make room — just to read three numbers. This
1069
+ * query carries no tools, no permission bridge, and no session binding: it
1070
+ * starts, answers one control request, and is killed. */
1071
+ async function probePlanUsage(executablePath, fetchedAt, factory = query) {
1072
+ const lifetime = new AbortController();
1073
+ const timer = setTimeout(() => lifetime.abort(), PLAN_USAGE_TIMEOUT_MS);
1074
+ timer.unref?.();
1075
+ const query$1 = factory({
1076
+ prompt: (async function* () {
1077
+ await new Promise(() => {});
1078
+ })(),
1079
+ options: {
1080
+ cwd: process.cwd(),
1081
+ abortController: lifetime,
1082
+ ...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
1083
+ }
1084
+ });
1085
+ try {
1086
+ (async () => {
1087
+ for await (const _ of query$1);
1088
+ })().catch(() => void 0);
1089
+ return normalizePlanUsage(await readPlanUsageFrom(query$1), fetchedAt);
1090
+ } finally {
1091
+ clearTimeout(timer);
1092
+ lifetime.abort();
1093
+ }
1094
+ }
1095
+ let latest;
1096
+ function recordPlanUsage(report) {
1097
+ latest = report;
1098
+ }
1099
+ function latestPlanUsage() {
1100
+ return latest;
1101
+ }
1102
+ //#endregion
991
1103
  //#region src/spawn.ts
992
1104
  const CLAUDE_PROCESS_GRACE_MS = 2e3;
993
1105
  const CLAUDE_STDERR_TAIL_BYTES = 32768;
@@ -1141,11 +1253,6 @@ const BACKGROUND_TASK_REPORT_PROMPT = [
1141
1253
  "Report their final completed or failed outcomes concisely to the user.",
1142
1254
  "Do not start new tools or tasks, and do not repeat the earlier progress update."
1143
1255
  ].join(" ");
1144
- /** CLI ≥ 2.1.235 emits system/init only after its first stdin input, while
1145
- * the supervisor must see init before submitting any real turn. A local
1146
- * slash command nudges startup without costing a model call; its lifecycle
1147
- * messages are ignored because no DSH turn is active while they arrive. */
1148
- const CLAUDE_HANDSHAKE_PROMPT = "/status";
1149
1256
  function usageSummary(usage) {
1150
1257
  return `${usage.inputTokens ?? 0} input / ${usage.outputTokens ?? 0} output tokens${usage.cumulativeCostUsd === void 0 ? "" : ` · $${usage.cumulativeCostUsd.toFixed(4)} cumulative`}`;
1151
1258
  }
@@ -1210,6 +1317,12 @@ var ClaudeSupervisor = class {
1210
1317
  contextWindow(model) {
1211
1318
  return this.#contextWindows.get(model);
1212
1319
  }
1320
+ /** Raw `/usage` payload, read over a session's existing process. Only the
1321
+ * idle-time metadata bridge uses this; a user-triggered refresh runs
1322
+ * probePlanUsage instead so it never waits on, or perturbs, a session. */
1323
+ planUsage(agent, model = this.#config.defaultModel) {
1324
+ return this.#runMetadata(agent, model, (query) => readPlanUsageFrom(query));
1325
+ }
1213
1326
  runTurn(request) {
1214
1327
  const interruption = this.#interruptions.get(request.agent.id);
1215
1328
  if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
@@ -1231,10 +1344,10 @@ var ClaudeSupervisor = class {
1231
1344
  await this.#makeRoom();
1232
1345
  entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
1233
1346
  this.#entries.set(sessionId, entry);
1234
- this.#armInitializationTimer(entry);
1235
1347
  }
1236
1348
  if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
1237
1349
  if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
1350
+ await entry.sdkInitialization;
1238
1351
  if (entry.idleTimer !== void 0) {
1239
1352
  clearTimeout(entry.idleTimer);
1240
1353
  entry.idleTimer = void 0;
@@ -1245,7 +1358,7 @@ var ClaudeSupervisor = class {
1245
1358
  await this.#disposeEntry(entry);
1246
1359
  entry = await this.#createEntry(request.agent, model, request.thinkingMode);
1247
1360
  this.#entries.set(sessionId, entry);
1248
- this.#armInitializationTimer(entry);
1361
+ await entry.sdkInitialization;
1249
1362
  } else {
1250
1363
  await this.#syncPermissionMode(entry);
1251
1364
  if (model !== entry.model) {
@@ -1318,7 +1431,7 @@ var ClaudeSupervisor = class {
1318
1431
  if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
1319
1432
  const entry = await this.#metadataEntry(agent, model);
1320
1433
  try {
1321
- await withTimeout(this.#whenInitialized(entry), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude metadata initialization");
1434
+ await entry.sdkInitialization;
1322
1435
  return await withTimeout(operation(entry.query, entry), CLAUDE_METADATA_TIMEOUT_MS, "Claude metadata request");
1323
1436
  } finally {
1324
1437
  entry.lastUsedAt = Date.now();
@@ -1328,13 +1441,6 @@ var ClaudeSupervisor = class {
1328
1441
  this.#admissionGate = admitted.then(() => void 0, () => void 0);
1329
1442
  return admitted;
1330
1443
  }
1331
- #whenInitialized(entry) {
1332
- if (entry.initialized) return Promise.resolve();
1333
- if (entry.state === "disposed" || entry.state === "disconnected" || entry.state === "outcome-unknown") return Promise.reject(/* @__PURE__ */ new Error(`dsh-claude: session ${entry.sessionId} is ${entry.state}`));
1334
- return new Promise((resolve, reject) => {
1335
- entry.initWaiters.push((error) => error === void 0 ? resolve() : reject(error instanceof Error ? error : new Error(String(error))));
1336
- });
1337
- }
1338
1444
  async #metadataEntry(agent, model) {
1339
1445
  const sessionId = agent.id;
1340
1446
  let entry = this.#entries.get(sessionId);
@@ -1347,7 +1453,6 @@ var ClaudeSupervisor = class {
1347
1453
  await this.#makeRoom();
1348
1454
  entry = await this.#createEntry(agent, model);
1349
1455
  this.#entries.set(sessionId, entry);
1350
- this.#armInitializationTimer(entry);
1351
1456
  }
1352
1457
  if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
1353
1458
  if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
@@ -1408,11 +1513,7 @@ var ClaudeSupervisor = class {
1408
1513
  lifetime,
1409
1514
  claudeSessionId: binding?.claudeSessionId,
1410
1515
  expectedResume: binding?.claudeSessionId,
1411
- handshakeUuid: randomUUID(),
1412
- handshakePending: true,
1413
1516
  initialized: false,
1414
- initWaiters: [],
1415
- initTimer: void 0,
1416
1517
  idleTimer: void 0,
1417
1518
  tasks: /* @__PURE__ */ new Map(),
1418
1519
  taskSnapshotAt: 0,
@@ -1470,8 +1571,11 @@ var ClaudeSupervisor = class {
1470
1571
  prompt: input,
1471
1572
  options
1472
1573
  });
1473
- entry.input.push(sdkUserMessage(CLAUDE_HANDSHAKE_PROMPT, entry.handshakeUuid));
1474
1574
  entry.pump = this.#runDetached(() => this.#pump(entry));
1575
+ entry.sdkInitialization = withTimeout(entry.query.initializationResult(), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude SDK initialization").then(() => {
1576
+ if (entry.state === "starting") entry.state = "idle";
1577
+ });
1578
+ entry.sdkInitialization.catch((error) => this.#handleDisconnect(entry, error));
1475
1579
  return entry;
1476
1580
  }
1477
1581
  async #pump(entry) {
@@ -1487,14 +1591,9 @@ var ClaudeSupervisor = class {
1487
1591
  const firstInitialization = !entry.initialized;
1488
1592
  if (entry.expectedResume !== void 0 && message.sessionId !== entry.expectedResume) throw new ClaudeProtocolError(`Claude Code resumed unexpected session ${message.sessionId}; expected ${entry.expectedResume}`);
1489
1593
  if (message.cwd !== entry.cwd) throw new ClaudeProtocolError(`Claude Code initialized in unexpected cwd ${message.cwd}; expected ${entry.cwd}`);
1490
- if (entry.initTimer !== void 0) {
1491
- clearTimeout(entry.initTimer);
1492
- entry.initTimer = void 0;
1493
- }
1494
1594
  entry.initialized = true;
1495
1595
  entry.claudeSessionId = message.sessionId;
1496
1596
  entry.state = entry.active === void 0 ? "idle" : "running";
1497
- for (const waiter of entry.initWaiters.splice(0)) waiter(void 0);
1498
1597
  if (firstInitialization && entry.tasks.size > 0) {
1499
1598
  entry.tasks.clear();
1500
1599
  await this.#flushTasksSnapshot(entry);
@@ -1509,15 +1608,9 @@ var ClaudeSupervisor = class {
1509
1608
  const taskId = message.kind === "subagent" ? message.taskId : void 0;
1510
1609
  if (message.kind === "subagent" && taskId !== void 0) await this.#trackTask(entry, message, taskId, entry.active?.cursor.turn);
1511
1610
  else if (message.kind === "background-tasks") await this.#trackBackgroundLevel(entry, message.tasks, entry.active?.cursor.turn);
1512
- if (entry.handshakePending) {
1513
- if (message.kind === "protocol-error") throw new ClaudeProtocolError(`${message.title}: ${JSON.stringify(message.detail).slice(0, 1e3)}`);
1514
- if (message.kind === "result") entry.handshakePending = false;
1515
- return;
1516
- }
1517
1611
  const active = entry.active;
1518
1612
  if (active === void 0) return;
1519
1613
  if (message.kind === "result") {
1520
- if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
1521
1614
  if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
1522
1615
  if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
1523
1616
  if (active.phase === "waiting-tasks") {
@@ -1957,20 +2050,9 @@ var ClaudeSupervisor = class {
1957
2050
  active.output.fail(failure);
1958
2051
  entry.active = void 0;
1959
2052
  } else entry.state = "disconnected";
1960
- const waiters = entry.initWaiters.splice(0);
1961
- const waiterError = error instanceof Error ? error : new Error(String(error));
1962
- for (const waiter of waiters) waiter(waiterError);
1963
2053
  this.#entries.delete(entry.sessionId);
1964
2054
  await this.#disposeEntry(entry);
1965
2055
  }
1966
- #armInitializationTimer(entry) {
1967
- const timer = setTimeout(() => {
1968
- if (entry.state !== "starting" || entry.initialized) return;
1969
- this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code initialization timed out"));
1970
- }, CLAUDE_INITIALIZATION_TIMEOUT_MS);
1971
- timer.unref?.();
1972
- entry.initTimer = timer;
1973
- }
1974
2056
  #armIdleTimer(entry) {
1975
2057
  if (this.#config.idleTimeoutMs <= 0) return;
1976
2058
  const timer = setTimeout(() => {
@@ -1984,7 +2066,6 @@ var ClaudeSupervisor = class {
1984
2066
  async #disposeEntry(entry) {
1985
2067
  if (entry.state === "disposed") return;
1986
2068
  if (entry.idleTimer !== void 0) clearTimeout(entry.idleTimer);
1987
- if (entry.initTimer !== void 0) clearTimeout(entry.initTimer);
1988
2069
  entry.state = "disposed";
1989
2070
  entry.input.discard(abortFailure());
1990
2071
  entry.query.close();
@@ -2000,7 +2081,7 @@ var ClaudeSupervisor = class {
2000
2081
  //#region src/review-comments.ts
2001
2082
  const MAX_COMMENTS_PER_SESSION = 50;
2002
2083
  const MAX_TEXT_CHARS$1 = 2e3;
2003
- const MAX_PATH_CHARS$2 = 1024;
2084
+ const MAX_PATH_CHARS$3 = 1024;
2004
2085
  const MAX_LINE = 1e7;
2005
2086
  var ReviewCommentError = class extends Error {
2006
2087
  code;
@@ -2019,16 +2100,19 @@ var ReviewCommentStore = class {
2019
2100
  add(sessionId, input) {
2020
2101
  const path = typeof input.path === "string" ? input.path.trim() : "";
2021
2102
  const text = typeof input.text === "string" ? input.text.trim() : "";
2022
- if (path.length === 0 || path.length > MAX_PATH_CHARS$2 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
2103
+ if (path.length === 0 || path.length > MAX_PATH_CHARS$3 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
2023
2104
  if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
2024
2105
  if (typeof input.line !== "number" || !Number.isSafeInteger(input.line) || input.line < 1 || input.line > MAX_LINE) throw new ReviewCommentError("invalid-request", "The comment line is invalid.");
2025
2106
  if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
2107
+ const startLine = input.startLine === void 0 || input.startLine === null ? void 0 : input.startLine;
2108
+ if (startLine !== void 0 && (typeof startLine !== "number" || !Number.isSafeInteger(startLine) || startLine < 1 || startLine > input.line)) throw new ReviewCommentError("invalid-request", "The comment start line is invalid.");
2026
2109
  const existing = this.#comments.get(sessionId) ?? [];
2027
2110
  if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
2028
2111
  const comment = {
2029
2112
  id: randomUUID(),
2030
2113
  path,
2031
2114
  line: input.line,
2115
+ ...startLine === void 0 || startLine === input.line ? {} : { startLine },
2032
2116
  side: input.side,
2033
2117
  text
2034
2118
  };
@@ -2064,8 +2148,8 @@ var ReviewCommentStore = class {
2064
2148
  function formatReviewComments(comments) {
2065
2149
  return [
2066
2150
  "<user-review-comments>",
2067
- "The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
2068
- ...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
2151
+ "The user attached these code review comments to this message. Each references a file and line (or line range) from the current working tree diff:",
2152
+ ...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.startLine === void 0 ? comment.line : `${comment.startLine}-${comment.line}`}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
2069
2153
  "</user-review-comments>"
2070
2154
  ].join("\n");
2071
2155
  }
@@ -2418,7 +2502,7 @@ function json(res, status, value) {
2418
2502
  //#region src/doctor-routes.ts
2419
2503
  const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
2420
2504
  const claudeBridgeDiagnostics = /* @__PURE__ */ new WeakMap();
2421
- function safeMessage$1(error) {
2505
+ function safeMessage$2(error) {
2422
2506
  return redactText(error instanceof Error ? error.message : String(error), 1e3);
2423
2507
  }
2424
2508
  /** Live command-bridge diagnostics: which agents exist, their presets, and how
@@ -2434,14 +2518,14 @@ function commandDiagnostics(ctx) {
2434
2518
  const preset = ctx.agentPresets.composedPreset(agent.ctx);
2435
2519
  if (preset !== void 0) info.preset = preset;
2436
2520
  } catch (error) {
2437
- info.error = safeMessage$1(error);
2521
+ info.error = safeMessage$2(error);
2438
2522
  }
2439
2523
  try {
2440
2524
  const list = ctx.commands.list(agent);
2441
2525
  info.commandCount = list.length;
2442
2526
  info.sample = list.slice(0, 10).map((command) => command.name);
2443
2527
  } catch (error) {
2444
- info.error = info.error === void 0 ? safeMessage$1(error) : `${info.error}; ${safeMessage$1(error)}`;
2528
+ info.error = info.error === void 0 ? safeMessage$2(error) : `${info.error}; ${safeMessage$2(error)}`;
2445
2529
  }
2446
2530
  const bridge = claudeBridgeDiagnostics.get(agent);
2447
2531
  if (bridge !== void 0) info.bridge = bridge;
@@ -2449,7 +2533,7 @@ function commandDiagnostics(ctx) {
2449
2533
  })
2450
2534
  };
2451
2535
  } catch (error) {
2452
- return { error: safeMessage$1(error) };
2536
+ return { error: safeMessage$2(error) };
2453
2537
  }
2454
2538
  }
2455
2539
  function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
@@ -2473,7 +2557,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2473
2557
  version: { status: "not-run" },
2474
2558
  authentication: { status: "not-run" },
2475
2559
  handshake: "not-run",
2476
- message: safeMessage$1(resolutionError),
2560
+ message: safeMessage$2(resolutionError),
2477
2561
  limits: {
2478
2562
  idleTimeoutMs: config.idleTimeoutMs,
2479
2563
  maxProcesses: config.maxProcesses
@@ -2503,14 +2587,14 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2503
2587
  commandBridge: commandDiagnostics(ctx)
2504
2588
  });
2505
2589
  } catch (error) {
2506
- json(res, 500, { error: safeMessage$1(error) });
2590
+ json(res, 500, { error: safeMessage$2(error) });
2507
2591
  }
2508
2592
  }
2509
2593
  }), "dsh-claude: Doctor route");
2510
2594
  }
2511
2595
  //#endregion
2512
2596
  //#region src/projection-routes.ts
2513
- const MAX_SESSION_ID_CHARS$3 = 1024;
2597
+ const MAX_SESSION_ID_CHARS$4 = 1024;
2514
2598
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
2515
2599
  * the transcript hot path so git/gh latency never delays visible text. */
2516
2600
  const META_REFRESH_MS = 5e3;
@@ -2524,7 +2608,7 @@ function targetFromUrl(rawUrl) {
2524
2608
  if (stream) encoded = encoded.slice(0, -7);
2525
2609
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2526
2610
  const sessionId = decodeURIComponent(encoded);
2527
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$3) return void 0;
2611
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$4) return void 0;
2528
2612
  return {
2529
2613
  sessionId,
2530
2614
  stream
@@ -2686,6 +2770,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2686
2770
  //#region src/repository-status.ts
2687
2771
  const MAX_OUTPUT_BYTES$3 = 65536;
2688
2772
  const MAX_DIFF_BYTES = 262144;
2773
+ const MAX_FILE_BYTES = 8388608;
2689
2774
  const MAX_UNTRACKED_DIFFS = 50;
2690
2775
  const GIT_TIMEOUT_MS$3 = 5e3;
2691
2776
  const GH_TIMEOUT_MS$1 = 8e3;
@@ -2781,14 +2866,14 @@ function parseGitHubRemote(value) {
2781
2866
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2782
2867
  return `${match[1]}/${match[2]}`;
2783
2868
  }
2784
- function record$6(value) {
2869
+ function record$8(value) {
2785
2870
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2786
2871
  }
2787
2872
  function aggregateChecks(value) {
2788
2873
  if (!Array.isArray(value) || value.length === 0) return "none";
2789
2874
  let pending = false;
2790
2875
  for (const item of value) {
2791
- const check = record$6(item);
2876
+ const check = record$8(item);
2792
2877
  if (check === void 0) continue;
2793
2878
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2794
2879
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2812,7 +2897,7 @@ function reviewState(value) {
2812
2897
  return "none";
2813
2898
  }
2814
2899
  function parsePullRequest(value) {
2815
- const input = record$6(value);
2900
+ const input = record$8(value);
2816
2901
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2817
2902
  let url;
2818
2903
  try {
@@ -2833,12 +2918,20 @@ function parsePullRequest(value) {
2833
2918
  review: reviewState(input.reviewDecision),
2834
2919
  checks: aggregateChecks(input.statusCheckRollup),
2835
2920
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2836
- ...typeof record$6(input.author)?.login === "string" ? { author: bounded(String(record$6(input.author)?.login)) } : {},
2921
+ ...typeof record$8(input.author)?.login === "string" ? { author: bounded(String(record$8(input.author)?.login)) } : {},
2837
2922
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2838
2923
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2839
2924
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2840
2925
  };
2841
2926
  }
2927
+ var RepositoryFileError = class extends Error {
2928
+ code;
2929
+ constructor(code, message) {
2930
+ super(message);
2931
+ this.name = "RepositoryFileError";
2932
+ this.code = code;
2933
+ }
2934
+ };
2842
2935
  var RepositoryStatusService = class {
2843
2936
  #runtime;
2844
2937
  #cacheTtlMs;
@@ -2880,6 +2973,29 @@ var RepositoryStatusService = class {
2880
2973
  this.#lastReady.set(cwd, stable);
2881
2974
  return stable;
2882
2975
  }
2976
+ /** Lines [from, to] of a working-tree file, used to expand unmodified context around diff hunks. */
2977
+ async fileLines(cwd, relativePath, from, to) {
2978
+ if (relativePath.length === 0 || isAbsolute(relativePath) || relativePath.includes("\0") || relativePath.split(/[\\/]/u).includes("..")) throw new RepositoryFileError("invalid-request", "The file path must be relative to the repository.");
2979
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 1 || to < from || to - from >= 500) throw new RepositoryFileError("invalid-request", "The requested line range is invalid.");
2980
+ const git = await this.#git();
2981
+ const top = await run(this.#runtime, git, [
2982
+ "rev-parse",
2983
+ "--path-format=absolute",
2984
+ "--show-toplevel"
2985
+ ], cwd, GIT_TIMEOUT_MS$3);
2986
+ if (top.exitCode !== 0) throw new RepositoryFileError("not-repository", "The directory is not inside a git repository.");
2987
+ const root = resolve(top.stdout.trim());
2988
+ const target = resolve(root, relativePath);
2989
+ if (target !== root && !target.startsWith(root + sep)) throw new RepositoryFileError("invalid-request", "The file path escapes the repository.");
2990
+ const content = await readFile(target, "utf8");
2991
+ if (content.length > MAX_FILE_BYTES) throw new RepositoryFileError("too-large", "The file is too large to expand.");
2992
+ const all = content.split(/\r?\n/u);
2993
+ if (all.at(-1) === "") all.pop();
2994
+ return {
2995
+ lines: all.slice(from - 1, to),
2996
+ total: all.length
2997
+ };
2998
+ }
2883
2999
  async #git() {
2884
3000
  this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
2885
3001
  return this.#gitExecutable;
@@ -3099,7 +3215,7 @@ var RepositoryStatusService = class {
3099
3215
  const MAX_OUTPUT_BYTES$2 = 131072;
3100
3216
  const GIT_TIMEOUT_MS$2 = 1e4;
3101
3217
  const GIT_FETCH_TIMEOUT_MS = 6e4;
3102
- const MAX_PATH_CHARS$1 = 4096;
3218
+ const MAX_PATH_CHARS$2 = 4096;
3103
3219
  const MAX_BRANCH_CHARS = 512;
3104
3220
  const LEASE_SCHEMA_VERSION = 1;
3105
3221
  const CLEANUP_GRACE_MS = 12e4;
@@ -3123,7 +3239,7 @@ async function collect$2(handle) {
3123
3239
  };
3124
3240
  }
3125
3241
  function safePath(value) {
3126
- if (value.length === 0 || value.length > MAX_PATH_CHARS$1 || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
3242
+ if (value.length === 0 || value.length > MAX_PATH_CHARS$2 || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
3127
3243
  return resolve(value);
3128
3244
  }
3129
3245
  function safeBranch(value) {
@@ -3508,7 +3624,10 @@ var RepositorySetupService = class {
3508
3624
  return resolve(root);
3509
3625
  }
3510
3626
  #git() {
3511
- this.#gitPath ??= this.#runtime.resolveExecutable("git");
3627
+ this.#gitPath ??= this.#runtime.resolveExecutable("git", void 0, AbortSignal.timeout(GIT_TIMEOUT_MS$2)).catch((error) => {
3628
+ this.#gitPath = void 0;
3629
+ throw error;
3630
+ });
3512
3631
  return this.#gitPath;
3513
3632
  }
3514
3633
  async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$2) {
@@ -4000,20 +4119,20 @@ var RepositoryActionService = class {
4000
4119
  };
4001
4120
  //#endregion
4002
4121
  //#region src/repository-setup-routes.ts
4003
- const MAX_BODY_BYTES$3 = 16384;
4004
- function record$5(value) {
4122
+ const MAX_BODY_BYTES$4 = 16384;
4123
+ function record$7(value) {
4005
4124
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4006
4125
  }
4007
- async function readJson$3(req) {
4126
+ async function readJson$4(req) {
4008
4127
  const chunks = [];
4009
4128
  let size = 0;
4010
4129
  for await (const chunk of req) {
4011
4130
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4012
4131
  size += buffer.length;
4013
- if (size > MAX_BODY_BYTES$3) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4132
+ if (size > MAX_BODY_BYTES$4) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4014
4133
  chunks.push(buffer);
4015
4134
  }
4016
- const value = record$5(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4135
+ const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4017
4136
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
4018
4137
  return value;
4019
4138
  }
@@ -4036,17 +4155,17 @@ function ndjsonHeaders(res) {
4036
4155
  });
4037
4156
  res.flushHeaders?.();
4038
4157
  }
4039
- function ndjson(res, value) {
4158
+ function ndjson$1(res, value) {
4040
4159
  res.write(`${JSON.stringify(value)}\n`);
4041
4160
  }
4042
4161
  async function streamSetup(res, service, input) {
4043
4162
  ndjsonHeaders(res);
4044
4163
  let ended = false;
4045
4164
  try {
4046
- ndjson(res, {
4165
+ ndjson$1(res, {
4047
4166
  type: "complete",
4048
4167
  result: await service.setup(string$2(input, "cwd"), string$2(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
4049
- if (!ended) ndjson(res, {
4168
+ if (!ended) ndjson$1(res, {
4050
4169
  type: "progress",
4051
4170
  stage
4052
4171
  });
@@ -4054,7 +4173,7 @@ async function streamSetup(res, service, input) {
4054
4173
  });
4055
4174
  } catch (error) {
4056
4175
  const setupError = error instanceof RepositorySetupError ? error : void 0;
4057
- ndjson(res, {
4176
+ ndjson$1(res, {
4058
4177
  type: "error",
4059
4178
  code: setupError?.code ?? "repository-setup-unavailable",
4060
4179
  message: setupError?.message ?? "Repository setup is unavailable."
@@ -4081,19 +4200,19 @@ function registerRepositorySetupRoute(ctx, service) {
4081
4200
  }
4082
4201
  if (pathname === "/plugins/dsh-claude/repository/setup") {
4083
4202
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4084
- const input = await readJson$3(req);
4203
+ const input = await readJson$4(req);
4085
4204
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
4086
4205
  await streamSetup(res, service, input);
4087
4206
  return;
4088
4207
  }
4089
4208
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
4090
4209
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4091
- const input = await readJson$3(req);
4210
+ const input = await readJson$4(req);
4092
4211
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
4093
4212
  }
4094
4213
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
4095
4214
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4096
- const input = await readJson$3(req);
4215
+ const input = await readJson$4(req);
4097
4216
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
4098
4217
  return json(res, 200, { ok: true });
4099
4218
  }
@@ -4111,8 +4230,8 @@ function registerRepositorySetupRoute(ctx, service) {
4111
4230
  }
4112
4231
  //#endregion
4113
4232
  //#region src/repository-action-routes.ts
4114
- const MAX_BODY_BYTES$2 = 16384;
4115
- const MAX_SESSION_ID_CHARS$2 = 1024;
4233
+ const MAX_BODY_BYTES$3 = 16384;
4234
+ const MAX_SESSION_ID_CHARS$3 = 1024;
4116
4235
  const ACTIONS = /* @__PURE__ */ new Set([
4117
4236
  "commit",
4118
4237
  "commit-push",
@@ -4121,25 +4240,25 @@ const ACTIONS = /* @__PURE__ */ new Set([
4121
4240
  "merge-pr",
4122
4241
  "update-branch"
4123
4242
  ]);
4124
- function record$4(value) {
4243
+ function record$6(value) {
4125
4244
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4126
4245
  }
4127
- async function readJson$2(req) {
4246
+ async function readJson$3(req) {
4128
4247
  const chunks = [];
4129
4248
  let size = 0;
4130
4249
  for await (const chunk of req) {
4131
4250
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4132
4251
  size += buffer.length;
4133
- if (size > MAX_BODY_BYTES$2) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4252
+ if (size > MAX_BODY_BYTES$3) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4134
4253
  chunks.push(buffer);
4135
4254
  }
4136
- const value = record$4(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4255
+ const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4137
4256
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
4138
4257
  return value;
4139
4258
  }
4140
4259
  function sessionId$1(url) {
4141
4260
  const value = url.searchParams.get("sessionId");
4142
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4261
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4143
4262
  return value;
4144
4263
  }
4145
4264
  function string$1(input, key) {
@@ -4188,12 +4307,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4188
4307
  }
4189
4308
  if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
4190
4309
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4191
- const input = await readJson$2(req);
4310
+ const input = await readJson$3(req);
4192
4311
  return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
4193
4312
  }
4194
4313
  if (url.pathname === "/plugins/dsh-claude/repository/action") {
4195
4314
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4196
- return json(res, 200, await service.execute(cwd, actionRequest(await readJson$2(req))));
4315
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$3(req))));
4197
4316
  }
4198
4317
  return json(res, 404, { error: "not found" });
4199
4318
  } catch (error) {
@@ -4237,14 +4356,14 @@ async function collect(handle) {
4237
4356
  lossy: stdout?.lossy === true
4238
4357
  };
4239
4358
  }
4240
- function record$3(value) {
4359
+ function record$5(value) {
4241
4360
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4242
4361
  }
4243
4362
  function parseReviewComments(value) {
4244
4363
  if (!Array.isArray(value)) return [];
4245
4364
  const comments = [];
4246
4365
  for (const item of value) {
4247
- const input = record$3(item);
4366
+ const input = record$5(item);
4248
4367
  if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4249
4368
  const body = typeof input.body === "string" ? input.body.trim() : "";
4250
4369
  const path = typeof input.path === "string" ? input.path : "";
@@ -4256,7 +4375,7 @@ function parseReviewComments(value) {
4256
4375
  path,
4257
4376
  ...line === void 0 ? {} : { line },
4258
4377
  side: input.side === "LEFT" ? "old" : "new",
4259
- author: typeof record$3(input.user)?.login === "string" ? String(record$3(input.user)?.login) : "unknown",
4378
+ author: typeof record$5(input.user)?.login === "string" ? String(record$5(input.user)?.login) : "unknown",
4260
4379
  body: body.slice(0, MAX_COMMENT_CHARS),
4261
4380
  url
4262
4381
  });
@@ -4273,7 +4392,7 @@ function parseFailingChecks(value) {
4273
4392
  if (!Array.isArray(value)) return [];
4274
4393
  const failing = [];
4275
4394
  for (const item of value) {
4276
- const input = record$3(item);
4395
+ const input = record$5(item);
4277
4396
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4278
4397
  failing.push({
4279
4398
  name: input.name,
@@ -4385,10 +4504,10 @@ var PullRequestFeedbackService = class {
4385
4504
  };
4386
4505
  //#endregion
4387
4506
  //#region src/pr-feedback-routes.ts
4388
- const MAX_SESSION_ID_CHARS$1 = 1024;
4507
+ const MAX_SESSION_ID_CHARS$2 = 1024;
4389
4508
  function sessionId(url) {
4390
4509
  const value = url.searchParams.get("sessionId");
4391
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4510
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4392
4511
  return value;
4393
4512
  }
4394
4513
  function pullNumber(url) {
@@ -4425,7 +4544,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
4425
4544
  }
4426
4545
  //#endregion
4427
4546
  //#region src/repository-status-routes.ts
4428
- const MAX_PATH_CHARS = 4096;
4547
+ const MAX_PATH_CHARS$1 = 4096;
4429
4548
  /** Read-only repository status for an arbitrary directory (the overview panel
4430
4549
  * aggregates every Claude session's checkout through this). */
4431
4550
  function registerRepositoryStatusRoute(ctx, service) {
@@ -4436,7 +4555,7 @@ function registerRepositoryStatusRoute(ctx, service) {
4436
4555
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4437
4556
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4438
4557
  const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
4439
- if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4558
+ if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4440
4559
  error: "invalid-request",
4441
4560
  message: "The cwd query parameter is invalid."
4442
4561
  });
@@ -4452,6 +4571,45 @@ function registerRepositoryStatusRoute(ctx, service) {
4452
4571
  }), "dsh-claude: repository status route");
4453
4572
  }
4454
4573
  //#endregion
4574
+ //#region src/repository-file-routes.ts
4575
+ const MAX_PATH_CHARS = 4096;
4576
+ /** Read-only slice of a working-tree file so the diff panel can expand unmodified lines around hunks. */
4577
+ function registerRepositoryFileRoute(ctx, service) {
4578
+ ctx.effect(() => ctx.webServer.register({
4579
+ kind: "exact",
4580
+ path: CLAUDE_REPOSITORY_FILE_PATH,
4581
+ handler: async (req, res) => {
4582
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4583
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4584
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
4585
+ const cwd = params.get("cwd");
4586
+ const path = params.get("path");
4587
+ const from = Number(params.get("from"));
4588
+ const to = Number(params.get("to"));
4589
+ if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4590
+ error: "invalid-request",
4591
+ message: "The cwd query parameter is invalid."
4592
+ });
4593
+ if (path === null || path.length === 0 || path.length > MAX_PATH_CHARS) return json(res, 400, {
4594
+ error: "invalid-request",
4595
+ message: "The path query parameter is invalid."
4596
+ });
4597
+ try {
4598
+ return json(res, 200, await service.fileLines(cwd, path, from, to));
4599
+ } catch (error) {
4600
+ if (error instanceof RepositoryFileError) return json(res, error.code === "invalid-request" ? 400 : 409, {
4601
+ error: error.code,
4602
+ message: error.message
4603
+ });
4604
+ return json(res, 500, {
4605
+ error: "repository-file-unavailable",
4606
+ message: "The file could not be read."
4607
+ });
4608
+ }
4609
+ }
4610
+ }), "dsh-claude: repository file route");
4611
+ }
4612
+ //#endregion
4455
4613
  //#region src/jira.ts
4456
4614
  const REQUEST_TIMEOUT_MS = 15e3;
4457
4615
  const MAX_RESULTS = 20;
@@ -4465,7 +4623,7 @@ var JiraError = class extends Error {
4465
4623
  this.code = code;
4466
4624
  }
4467
4625
  };
4468
- function record$2(value) {
4626
+ function record$4(value) {
4469
4627
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4470
4628
  }
4471
4629
  /** Jira Cloud sites are origins; Data Center may carry a context path. */
@@ -4491,15 +4649,15 @@ function buildJql(query) {
4491
4649
  return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
4492
4650
  }
4493
4651
  function parseTickets(value, siteUrl) {
4494
- const issues = record$2(value)?.issues;
4652
+ const issues = record$4(value)?.issues;
4495
4653
  if (!Array.isArray(issues)) return [];
4496
4654
  const tickets = [];
4497
4655
  for (const item of issues) {
4498
- const issue = record$2(item);
4499
- const fields = record$2(issue?.fields);
4656
+ const issue = record$4(item);
4657
+ const fields = record$4(issue?.fields);
4500
4658
  if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
4501
- const status = record$2(fields.status)?.name;
4502
- const type = record$2(fields.issuetype)?.name;
4659
+ const status = record$4(fields.status)?.name;
4660
+ const type = record$4(fields.issuetype)?.name;
4503
4661
  tickets.push({
4504
4662
  key: issue.key,
4505
4663
  summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
@@ -4541,7 +4699,7 @@ var JiraService = class {
4541
4699
  email,
4542
4700
  apiToken
4543
4701
  };
4544
- const myself = record$2(await this.#json(connection, "/rest/api/3/myself"));
4702
+ const myself = record$4(await this.#json(connection, "/rest/api/3/myself"));
4545
4703
  const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
4546
4704
  const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
4547
4705
  const store = {
@@ -4560,7 +4718,7 @@ var JiraService = class {
4560
4718
  if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
4561
4719
  let accountId = store.accountId;
4562
4720
  if (accountId === void 0) {
4563
- const myself = record$2(await this.#json(store, "/rest/api/3/myself"));
4721
+ const myself = record$4(await this.#json(store, "/rest/api/3/myself"));
4564
4722
  if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
4565
4723
  accountId = myself.accountId;
4566
4724
  await this.#write({
@@ -4628,7 +4786,7 @@ var JiraService = class {
4628
4786
  throw error;
4629
4787
  }
4630
4788
  if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
4631
- const input = record$2(JSON.parse(text));
4789
+ const input = record$4(JSON.parse(text));
4632
4790
  if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
4633
4791
  return {
4634
4792
  siteUrl: input.siteUrl,
@@ -4659,20 +4817,20 @@ var JiraService = class {
4659
4817
  };
4660
4818
  //#endregion
4661
4819
  //#region src/jira-routes.ts
4662
- const MAX_BODY_BYTES$1 = 8192;
4663
- function record$1(value) {
4820
+ const MAX_BODY_BYTES$2 = 8192;
4821
+ function record$3(value) {
4664
4822
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4665
4823
  }
4666
- async function readJson$1(req) {
4824
+ async function readJson$2(req) {
4667
4825
  const chunks = [];
4668
4826
  let size = 0;
4669
4827
  for await (const chunk of req) {
4670
4828
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4671
4829
  size += buffer.length;
4672
- if (size > MAX_BODY_BYTES$1) throw new JiraError("body-too-large", "The request body is too large.");
4830
+ if (size > MAX_BODY_BYTES$2) throw new JiraError("body-too-large", "The request body is too large.");
4673
4831
  chunks.push(buffer);
4674
4832
  }
4675
- const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4833
+ const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4676
4834
  if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
4677
4835
  return value;
4678
4836
  }
@@ -4695,7 +4853,7 @@ function registerJiraRoute(ctx, service) {
4695
4853
  }
4696
4854
  if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
4697
4855
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4698
- const input = await readJson$1(req);
4856
+ const input = await readJson$2(req);
4699
4857
  return json(res, 200, await service.connect({
4700
4858
  siteUrl: string(input, "siteUrl"),
4701
4859
  email: string(input, "email"),
@@ -4709,7 +4867,7 @@ function registerJiraRoute(ctx, service) {
4709
4867
  }
4710
4868
  if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
4711
4869
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4712
- const input = await readJson$1(req);
4870
+ const input = await readJson$2(req);
4713
4871
  await service.assignToMe(string(input, "key"));
4714
4872
  return json(res, 200, { assigned: true });
4715
4873
  }
@@ -4733,6 +4891,319 @@ function registerJiraRoute(ctx, service) {
4733
4891
  }), "dsh-claude: jira route");
4734
4892
  }
4735
4893
  //#endregion
4894
+ //#region src/ask.ts
4895
+ const MAX_SELECTION_CHARS = 8e3;
4896
+ const MAX_CONTEXT_CHARS = 16e3;
4897
+ const MAX_QUESTION_CHARS = 2e3;
4898
+ const ASK_TIMEOUT_MS = 18e4;
4899
+ const MAX_STDERR_BYTES = 65536;
4900
+ const MAX_TOOL_SUMMARY_CHARS = 160;
4901
+ const READ_ONLY_TOOLS = [
4902
+ "Read",
4903
+ "Grep",
4904
+ "Glob"
4905
+ ];
4906
+ var AskError = class extends Error {
4907
+ code;
4908
+ constructor(code, message) {
4909
+ super(message);
4910
+ this.name = "AskError";
4911
+ this.code = code;
4912
+ }
4913
+ };
4914
+ function fence(value) {
4915
+ return `"""\n${value.replaceAll("\"\"\"", "\" \" \"")}\n"""`;
4916
+ }
4917
+ function askPrompt(request) {
4918
+ const selection = request.selection.trim().slice(0, MAX_SELECTION_CHARS);
4919
+ const context = (request.context ?? "").trim().slice(0, MAX_CONTEXT_CHARS);
4920
+ const question = request.question.trim().slice(0, MAX_QUESTION_CHARS);
4921
+ return [
4922
+ "You are answering a follow-up question about part of an earlier assistant reply in this project.",
4923
+ "",
4924
+ "Selected passage:",
4925
+ fence(selection),
4926
+ ...context.length === 0 || context === selection ? [] : [
4927
+ "",
4928
+ "Surrounding reply, for context only:",
4929
+ fence(context)
4930
+ ],
4931
+ "",
4932
+ `Question: ${question}`,
4933
+ "",
4934
+ "Answer the question directly and concisely, in the same language as the question. Use Markdown.",
4935
+ "Only the read-only tools Read, Grep, and Glob are available; use them when the answer depends on project code, otherwise answer from the passage."
4936
+ ].join("\n");
4937
+ }
4938
+ /** CLI effort for a session thinking mode; `off` and unknown modes send nothing. */
4939
+ function effortFor(thinkingMode) {
4940
+ if (thinkingMode === void 0 || thinkingMode === "off") return void 0;
4941
+ if (thinkingMode === "ultracode") return "max";
4942
+ return [
4943
+ "low",
4944
+ "medium",
4945
+ "high",
4946
+ "max"
4947
+ ].includes(thinkingMode) ? thinkingMode : void 0;
4948
+ }
4949
+ function askArguments(preferences) {
4950
+ const effort = effortFor(preferences.thinkingMode);
4951
+ return [
4952
+ "-p",
4953
+ "--output-format",
4954
+ "stream-json",
4955
+ "--verbose",
4956
+ "--include-partial-messages",
4957
+ "--strict-mcp-config",
4958
+ "--mcp-config",
4959
+ "{\"mcpServers\":{}}",
4960
+ ...preferences.model === void 0 || preferences.model === "default" ? [] : ["--model", preferences.model],
4961
+ ...effort === void 0 ? [] : ["--effort", effort],
4962
+ "--tools",
4963
+ ...READ_ONLY_TOOLS,
4964
+ "--allowedTools",
4965
+ ...READ_ONLY_TOOLS
4966
+ ];
4967
+ }
4968
+ function record$2(value) {
4969
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4970
+ }
4971
+ /** One-line description of a tool call, mirroring the main window's step titles. */
4972
+ function toolSummary(input) {
4973
+ const fields = record$2(input);
4974
+ if (fields === void 0) return void 0;
4975
+ const candidate = [
4976
+ fields.command,
4977
+ fields.pattern,
4978
+ fields.file_path,
4979
+ fields.path,
4980
+ fields.query
4981
+ ].find((value) => typeof value === "string" && value.length > 0);
4982
+ return (typeof candidate === "string" ? candidate : JSON.stringify(fields)).replaceAll(/\s+/gu, " ").trim().slice(0, MAX_TOOL_SUMMARY_CHARS);
4983
+ }
4984
+ function eventsOfStreamLine(line) {
4985
+ let parsed;
4986
+ try {
4987
+ parsed = record$2(JSON.parse(line));
4988
+ } catch {
4989
+ return [];
4990
+ }
4991
+ if (parsed === void 0) return [];
4992
+ if (parsed.type === "system" && parsed.subtype === "init") return [{
4993
+ type: "status",
4994
+ text: "ready"
4995
+ }];
4996
+ if (parsed.type === "stream_event") {
4997
+ const event = record$2(parsed.event);
4998
+ if (event?.type === "content_block_start") {
4999
+ const block = record$2(event.content_block);
5000
+ if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
5001
+ type: "tool",
5002
+ id: block.id,
5003
+ phase: "start",
5004
+ name: block.name
5005
+ }];
5006
+ return [];
5007
+ }
5008
+ const delta = record$2(event?.delta);
5009
+ if (event?.type !== "content_block_delta" || delta === void 0) return [];
5010
+ if (delta.type === "text_delta" && typeof delta.text === "string") return [{
5011
+ type: "text",
5012
+ text: delta.text
5013
+ }];
5014
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") return [{
5015
+ type: "thinking",
5016
+ text: delta.thinking
5017
+ }];
5018
+ return [];
5019
+ }
5020
+ if (parsed.type === "assistant" || parsed.type === "user") {
5021
+ const content = record$2(parsed.message)?.content;
5022
+ if (!Array.isArray(content)) return [];
5023
+ const events = [];
5024
+ for (const item of content) {
5025
+ const block = record$2(item);
5026
+ if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
5027
+ const summary = toolSummary(block.input);
5028
+ events.push({
5029
+ type: "tool",
5030
+ id: block.id,
5031
+ phase: "input",
5032
+ name: block.name,
5033
+ ...summary === void 0 ? {} : { summary }
5034
+ });
5035
+ } else if (block?.type === "tool_result" && typeof block.tool_use_id === "string") events.push({
5036
+ type: "tool",
5037
+ id: block.tool_use_id,
5038
+ phase: "done",
5039
+ ...block.is_error === true ? { error: true } : {}
5040
+ });
5041
+ }
5042
+ return events;
5043
+ }
5044
+ if (parsed.type === "result" && typeof parsed.result === "string") return [{
5045
+ type: "result",
5046
+ text: parsed.result
5047
+ }];
5048
+ return [];
5049
+ }
5050
+ /** One-shot Claude Code query answering a question about selected reply text. */
5051
+ var AskService = class {
5052
+ #runtime;
5053
+ #executablePath;
5054
+ constructor(runtime, executablePath) {
5055
+ this.#runtime = runtime;
5056
+ this.#executablePath = executablePath;
5057
+ }
5058
+ async ask(cwd, request, preferences, onEvent, signal) {
5059
+ if (request.question.trim().length === 0 || request.selection.trim().length === 0) throw new AskError("invalid-request", "A selection and a question are required.");
5060
+ if (this.#executablePath.length === 0) throw new AskError("claude-unavailable", "Claude Code is unavailable.");
5061
+ const timeout = AbortSignal.timeout(ASK_TIMEOUT_MS);
5062
+ const handle = this.#runtime.spawn({
5063
+ argv: [this.#executablePath, ...askArguments(preferences)],
5064
+ cwd,
5065
+ stdio: {
5066
+ stdin: { data: askPrompt(request) },
5067
+ stdout: "pipe",
5068
+ stderr: { maxBytes: MAX_STDERR_BYTES }
5069
+ },
5070
+ graceMs: 1e3,
5071
+ signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
5072
+ env: {}
5073
+ });
5074
+ const stdout = handle.stdout;
5075
+ if (stdout === void 0) throw new AskError("ask-failed", "Claude output is unavailable.");
5076
+ const decoder = new StringDecoder("utf8");
5077
+ let buffer = "";
5078
+ let emitted = false;
5079
+ let result;
5080
+ const consume = (line) => {
5081
+ for (const event of eventsOfStreamLine(line)) {
5082
+ if (event.type === "result") {
5083
+ result = event.text;
5084
+ continue;
5085
+ }
5086
+ if (event.type !== "tool" && event.text.length === 0) continue;
5087
+ if (event.type === "text") emitted = true;
5088
+ onEvent(event);
5089
+ }
5090
+ };
5091
+ for await (const chunk of stdout) {
5092
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
5093
+ const lines = buffer.split("\n");
5094
+ buffer = lines.pop() ?? "";
5095
+ for (const line of lines) if (line.trim().length > 0) consume(line);
5096
+ }
5097
+ buffer += decoder.end();
5098
+ if (buffer.trim().length > 0) consume(buffer);
5099
+ const outcome = await handle.done;
5100
+ if (!emitted && result !== void 0 && result.length > 0) {
5101
+ emitted = true;
5102
+ onEvent({
5103
+ type: "text",
5104
+ text: result
5105
+ });
5106
+ }
5107
+ if (!emitted) {
5108
+ if (signal?.aborted === true) throw new AskError("aborted", "The question was cancelled.");
5109
+ if (timeout.aborted) throw new AskError("timeout", "Claude took too long to answer.");
5110
+ const stderr = handle.collected.stderr?.readFrom(0).text.trim().split(/\r?\n/u).filter((line) => line.length > 0).at(-1);
5111
+ throw new AskError("ask-failed", stderr !== void 0 && stderr.length > 0 ? stderr : `Claude exited with code ${String(outcome.exitCode)}.`);
5112
+ }
5113
+ }
5114
+ };
5115
+ //#endregion
5116
+ //#region src/ask-routes.ts
5117
+ const MAX_BODY_BYTES$1 = 131072;
5118
+ const MAX_SESSION_ID_CHARS$1 = 1024;
5119
+ function record$1(value) {
5120
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5121
+ }
5122
+ async function readJson$1(req) {
5123
+ const chunks = [];
5124
+ let size = 0;
5125
+ for await (const chunk of req) {
5126
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5127
+ size += buffer.length;
5128
+ if (size > MAX_BODY_BYTES$1) throw new AskError("body-too-large", "The request body is too large.");
5129
+ chunks.push(buffer);
5130
+ }
5131
+ const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5132
+ if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
5133
+ return value;
5134
+ }
5135
+ function askRequest(input) {
5136
+ 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.");
5137
+ return {
5138
+ selection: input.selection,
5139
+ question: input.question,
5140
+ ...typeof input.context === "string" ? { context: input.context } : {}
5141
+ };
5142
+ }
5143
+ function ndjson(res, value) {
5144
+ res.write(`${JSON.stringify(value)}\n`);
5145
+ }
5146
+ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
5147
+ ctx.effect(() => ctx.webServer.register({
5148
+ kind: "exact",
5149
+ path: CLAUDE_ASK_PATH,
5150
+ handler: async (req, res) => {
5151
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
5152
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5153
+ const url = new URL(req.url ?? "/", "http://localhost");
5154
+ let cwd;
5155
+ let request;
5156
+ let sessionId;
5157
+ try {
5158
+ const value = url.searchParams.get("sessionId");
5159
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new AskError("invalid-session", "The session is invalid.");
5160
+ sessionId = value;
5161
+ const resolved = cwdForSession(sessionId);
5162
+ if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
5163
+ cwd = resolved;
5164
+ request = askRequest(await readJson$1(req));
5165
+ } catch (error) {
5166
+ if (error instanceof AskError) return json(res, 409, {
5167
+ error: error.code,
5168
+ message: error.message
5169
+ });
5170
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
5171
+ return json(res, 500, {
5172
+ error: "ask-unavailable",
5173
+ message: "The question could not be sent."
5174
+ });
5175
+ }
5176
+ res.writeHead(200, {
5177
+ "content-type": "application/x-ndjson; charset=utf-8",
5178
+ "cache-control": "no-store",
5179
+ "x-content-type-options": "nosniff"
5180
+ });
5181
+ res.flushHeaders?.();
5182
+ const controller = new AbortController();
5183
+ req.on("close", () => {
5184
+ controller.abort();
5185
+ });
5186
+ try {
5187
+ await service.ask(cwd, request, preferencesFor(sessionId) ?? {}, (event) => {
5188
+ ndjson(res, event.type === "text" ? {
5189
+ type: "delta",
5190
+ text: event.text
5191
+ } : event);
5192
+ }, controller.signal);
5193
+ ndjson(res, { type: "done" });
5194
+ } catch (error) {
5195
+ ndjson(res, {
5196
+ type: "error",
5197
+ code: error instanceof AskError ? error.code : "ask-unavailable",
5198
+ message: error instanceof AskError ? error.message : "The question could not be answered."
5199
+ });
5200
+ } finally {
5201
+ res.end();
5202
+ }
5203
+ }
5204
+ }), "dsh-claude: ask route");
5205
+ }
5206
+ //#endregion
4736
5207
  //#region src/review-comment-routes.ts
4737
5208
  const MAX_BODY_BYTES = 16384;
4738
5209
  const MAX_SESSION_ID_CHARS = 1024;
@@ -4772,6 +5243,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
4772
5243
  return json(res, 200, { comment: store.add(sessionId, {
4773
5244
  path: input.path,
4774
5245
  line: input.line,
5246
+ startLine: input.startLine,
4775
5247
  side: input.side,
4776
5248
  text: input.text
4777
5249
  }) });
@@ -4806,7 +5278,7 @@ const MAX_MANIFEST_BYTES = 262144;
4806
5278
  const MAX_UPDATE_OUTPUT_BYTES = 32768;
4807
5279
  const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
4808
5280
  const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4809
- function safeMessage(error) {
5281
+ function safeMessage$1(error) {
4810
5282
  return redactText(error instanceof Error ? error.message : String(error), 500);
4811
5283
  }
4812
5284
  async function readManifest(path) {
@@ -4968,7 +5440,7 @@ async function checkPluginUpdate(deps = {}) {
4968
5440
  state: "error",
4969
5441
  canUpdate: false,
4970
5442
  restartRequired: false,
4971
- message: safeMessage(error)
5443
+ message: safeMessage$1(error)
4972
5444
  };
4973
5445
  }
4974
5446
  }
@@ -5015,7 +5487,7 @@ async function updatePlugin(deps = {}) {
5015
5487
  const outcome = await handle.done;
5016
5488
  if (outcome.exitCode !== 0) {
5017
5489
  const detail = handle.collected.stderr?.readFrom(0).text ?? "";
5018
- throw new Error(`DSH plugin update failed (${outcome.exitCode ?? outcome.signal ?? "unknown exit"}): ${safeMessage(detail)}`);
5490
+ throw new Error(`DSH plugin update failed (${outcome.exitCode ?? outcome.signal ?? "unknown exit"}): ${safeMessage$1(detail)}`);
5019
5491
  }
5020
5492
  await verifyInstalledVersion(installation, latest);
5021
5493
  if (deps.requestRestart !== void 0) setTimeout(() => deps.requestRestart?.(), 100).unref?.();
@@ -5052,12 +5524,44 @@ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
5052
5524
  try {
5053
5525
  json(res, 200, await route.run());
5054
5526
  } catch (error) {
5055
- json(res, 500, { error: safeMessage(error) });
5527
+ json(res, 500, { error: safeMessage$1(error) });
5056
5528
  }
5057
5529
  }
5058
5530
  }), `dsh-claude: ${route.method} ${route.path}`);
5059
5531
  }
5060
5532
  //#endregion
5533
+ //#region src/plan-usage-routes.ts
5534
+ const EMPTY = {
5535
+ available: false,
5536
+ windows: [],
5537
+ fetchedAt: 0
5538
+ };
5539
+ function safeMessage(error) {
5540
+ return redactText(error instanceof Error ? error.message : String(error), 1e3);
5541
+ }
5542
+ /** Plan usage: GET serves the value the metadata bridge last cached (free —
5543
+ * it rides an already-running session), POST reads fresh numbers from a
5544
+ * throwaway probe process so a refresh never depends on, or disturbs, a live
5545
+ * Claude session. */
5546
+ function registerPlanUsageRoute(ctx, probe, now = Date.now) {
5547
+ ctx.effect(() => ctx.webServer.register({
5548
+ kind: "exact",
5549
+ path: CLAUDE_USAGE_PATH,
5550
+ handler: async (req, res) => {
5551
+ if (req.method !== "GET" && req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5552
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
5553
+ if (req.method === "GET") return json(res, 200, latestPlanUsage() ?? EMPTY);
5554
+ try {
5555
+ const report = await probe(now());
5556
+ recordPlanUsage(report);
5557
+ json(res, 200, report);
5558
+ } catch (error) {
5559
+ json(res, 500, { error: safeMessage(error) });
5560
+ }
5561
+ }
5562
+ }), "dsh-claude: plan usage route");
5563
+ }
5564
+ //#endregion
5061
5565
  //#region src/global-settings.ts
5062
5566
  const MAX_SETTINGS_BYTES = 262144;
5063
5567
  const MAX_REQUEST_BYTES = 8192;
@@ -5441,6 +5945,13 @@ function mountClaudeMetadata(ctx, supervisor, agent, model, sidecar, publishComm
5441
5945
  } catch (error) {
5442
5946
  warn("context usage", error);
5443
5947
  }
5948
+ if (stopped) return;
5949
+ try {
5950
+ const plan = await supervisor.planUsage(agent, model);
5951
+ if (!stopped) recordPlanUsage(normalizePlanUsage(plan, Date.now()));
5952
+ } catch (error) {
5953
+ warn("plan usage", error);
5954
+ }
5444
5955
  });
5445
5956
  };
5446
5957
  return agent.ctx.effect(() => {
@@ -5591,6 +6102,7 @@ async function apply(ctx, config) {
5591
6102
  });
5592
6103
  registerRepositorySetupRoute(webCtx, repositorySetup);
5593
6104
  registerRepositoryStatusRoute(webCtx, repositoryStatus);
6105
+ registerRepositoryFileRoute(webCtx, repositoryStatus);
5594
6106
  registerJiraRoute(webCtx, new JiraService());
5595
6107
  const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
5596
6108
  const cwdForClaudeSession = (sessionId) => {
@@ -5600,11 +6112,19 @@ async function apply(ctx, config) {
5600
6112
  };
5601
6113
  registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
5602
6114
  registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
6115
+ registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
6116
+ const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
6117
+ return snapshot === void 0 ? void 0 : {
6118
+ model: snapshot.model,
6119
+ ...snapshot.thinkingMode === void 0 ? {} : { thinkingMode: snapshot.thinkingMode }
6120
+ };
6121
+ });
5603
6122
  const ownsClaudeSession = (sessionId) => {
5604
6123
  const agent = webCtx.agents.get(sessionId);
5605
6124
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
5606
6125
  };
5607
6126
  registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
6127
+ registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
5608
6128
  registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
5609
6129
  const agent = webCtx.agents.get(sessionId);
5610
6130
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;