@norman-else/dsh-claude 0.1.25 → 0.1.26

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 { C as CLAUDE_REPOSITORY_SETUP_PATH, D as CLAUDE_UPDATE_PATH, E as CLAUDE_UPDATE_CHECK_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, k as TASK_TOOL_NAMES, 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-CY6V9oQ4.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-vyZoEeEO.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-BMRaIHLu.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$10(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$10(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$10(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$10(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$10(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$10(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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$9(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",
@@ -1141,11 +1142,6 @@ const BACKGROUND_TASK_REPORT_PROMPT = [
1141
1142
  "Report their final completed or failed outcomes concisely to the user.",
1142
1143
  "Do not start new tools or tasks, and do not repeat the earlier progress update."
1143
1144
  ].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
1145
  function usageSummary(usage) {
1150
1146
  return `${usage.inputTokens ?? 0} input / ${usage.outputTokens ?? 0} output tokens${usage.cumulativeCostUsd === void 0 ? "" : ` · $${usage.cumulativeCostUsd.toFixed(4)} cumulative`}`;
1151
1147
  }
@@ -1231,10 +1227,10 @@ var ClaudeSupervisor = class {
1231
1227
  await this.#makeRoom();
1232
1228
  entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
1233
1229
  this.#entries.set(sessionId, entry);
1234
- this.#armInitializationTimer(entry);
1235
1230
  }
1236
1231
  if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
1237
1232
  if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
1233
+ await entry.sdkInitialization;
1238
1234
  if (entry.idleTimer !== void 0) {
1239
1235
  clearTimeout(entry.idleTimer);
1240
1236
  entry.idleTimer = void 0;
@@ -1245,7 +1241,7 @@ var ClaudeSupervisor = class {
1245
1241
  await this.#disposeEntry(entry);
1246
1242
  entry = await this.#createEntry(request.agent, model, request.thinkingMode);
1247
1243
  this.#entries.set(sessionId, entry);
1248
- this.#armInitializationTimer(entry);
1244
+ await entry.sdkInitialization;
1249
1245
  } else {
1250
1246
  await this.#syncPermissionMode(entry);
1251
1247
  if (model !== entry.model) {
@@ -1318,7 +1314,7 @@ var ClaudeSupervisor = class {
1318
1314
  if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
1319
1315
  const entry = await this.#metadataEntry(agent, model);
1320
1316
  try {
1321
- await withTimeout(this.#whenInitialized(entry), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude metadata initialization");
1317
+ await entry.sdkInitialization;
1322
1318
  return await withTimeout(operation(entry.query, entry), CLAUDE_METADATA_TIMEOUT_MS, "Claude metadata request");
1323
1319
  } finally {
1324
1320
  entry.lastUsedAt = Date.now();
@@ -1328,13 +1324,6 @@ var ClaudeSupervisor = class {
1328
1324
  this.#admissionGate = admitted.then(() => void 0, () => void 0);
1329
1325
  return admitted;
1330
1326
  }
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
1327
  async #metadataEntry(agent, model) {
1339
1328
  const sessionId = agent.id;
1340
1329
  let entry = this.#entries.get(sessionId);
@@ -1347,7 +1336,6 @@ var ClaudeSupervisor = class {
1347
1336
  await this.#makeRoom();
1348
1337
  entry = await this.#createEntry(agent, model);
1349
1338
  this.#entries.set(sessionId, entry);
1350
- this.#armInitializationTimer(entry);
1351
1339
  }
1352
1340
  if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
1353
1341
  if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
@@ -1408,11 +1396,7 @@ var ClaudeSupervisor = class {
1408
1396
  lifetime,
1409
1397
  claudeSessionId: binding?.claudeSessionId,
1410
1398
  expectedResume: binding?.claudeSessionId,
1411
- handshakeUuid: randomUUID(),
1412
- handshakePending: true,
1413
1399
  initialized: false,
1414
- initWaiters: [],
1415
- initTimer: void 0,
1416
1400
  idleTimer: void 0,
1417
1401
  tasks: /* @__PURE__ */ new Map(),
1418
1402
  taskSnapshotAt: 0,
@@ -1470,8 +1454,11 @@ var ClaudeSupervisor = class {
1470
1454
  prompt: input,
1471
1455
  options
1472
1456
  });
1473
- entry.input.push(sdkUserMessage(CLAUDE_HANDSHAKE_PROMPT, entry.handshakeUuid));
1474
1457
  entry.pump = this.#runDetached(() => this.#pump(entry));
1458
+ entry.sdkInitialization = withTimeout(entry.query.initializationResult(), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude SDK initialization").then(() => {
1459
+ if (entry.state === "starting") entry.state = "idle";
1460
+ });
1461
+ entry.sdkInitialization.catch((error) => this.#handleDisconnect(entry, error));
1475
1462
  return entry;
1476
1463
  }
1477
1464
  async #pump(entry) {
@@ -1487,14 +1474,9 @@ var ClaudeSupervisor = class {
1487
1474
  const firstInitialization = !entry.initialized;
1488
1475
  if (entry.expectedResume !== void 0 && message.sessionId !== entry.expectedResume) throw new ClaudeProtocolError(`Claude Code resumed unexpected session ${message.sessionId}; expected ${entry.expectedResume}`);
1489
1476
  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
1477
  entry.initialized = true;
1495
1478
  entry.claudeSessionId = message.sessionId;
1496
1479
  entry.state = entry.active === void 0 ? "idle" : "running";
1497
- for (const waiter of entry.initWaiters.splice(0)) waiter(void 0);
1498
1480
  if (firstInitialization && entry.tasks.size > 0) {
1499
1481
  entry.tasks.clear();
1500
1482
  await this.#flushTasksSnapshot(entry);
@@ -1509,15 +1491,9 @@ var ClaudeSupervisor = class {
1509
1491
  const taskId = message.kind === "subagent" ? message.taskId : void 0;
1510
1492
  if (message.kind === "subagent" && taskId !== void 0) await this.#trackTask(entry, message, taskId, entry.active?.cursor.turn);
1511
1493
  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
1494
  const active = entry.active;
1518
1495
  if (active === void 0) return;
1519
1496
  if (message.kind === "result") {
1520
- if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
1521
1497
  if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
1522
1498
  if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
1523
1499
  if (active.phase === "waiting-tasks") {
@@ -1957,20 +1933,9 @@ var ClaudeSupervisor = class {
1957
1933
  active.output.fail(failure);
1958
1934
  entry.active = void 0;
1959
1935
  } 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
1936
  this.#entries.delete(entry.sessionId);
1964
1937
  await this.#disposeEntry(entry);
1965
1938
  }
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
1939
  #armIdleTimer(entry) {
1975
1940
  if (this.#config.idleTimeoutMs <= 0) return;
1976
1941
  const timer = setTimeout(() => {
@@ -1984,7 +1949,6 @@ var ClaudeSupervisor = class {
1984
1949
  async #disposeEntry(entry) {
1985
1950
  if (entry.state === "disposed") return;
1986
1951
  if (entry.idleTimer !== void 0) clearTimeout(entry.idleTimer);
1987
- if (entry.initTimer !== void 0) clearTimeout(entry.initTimer);
1988
1952
  entry.state = "disposed";
1989
1953
  entry.input.discard(abortFailure());
1990
1954
  entry.query.close();
@@ -2000,7 +1964,7 @@ var ClaudeSupervisor = class {
2000
1964
  //#region src/review-comments.ts
2001
1965
  const MAX_COMMENTS_PER_SESSION = 50;
2002
1966
  const MAX_TEXT_CHARS$1 = 2e3;
2003
- const MAX_PATH_CHARS$2 = 1024;
1967
+ const MAX_PATH_CHARS$3 = 1024;
2004
1968
  const MAX_LINE = 1e7;
2005
1969
  var ReviewCommentError = class extends Error {
2006
1970
  code;
@@ -2019,16 +1983,19 @@ var ReviewCommentStore = class {
2019
1983
  add(sessionId, input) {
2020
1984
  const path = typeof input.path === "string" ? input.path.trim() : "";
2021
1985
  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.");
1986
+ 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
1987
  if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
2024
1988
  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
1989
  if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
1990
+ const startLine = input.startLine === void 0 || input.startLine === null ? void 0 : input.startLine;
1991
+ 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
1992
  const existing = this.#comments.get(sessionId) ?? [];
2027
1993
  if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
2028
1994
  const comment = {
2029
1995
  id: randomUUID(),
2030
1996
  path,
2031
1997
  line: input.line,
1998
+ ...startLine === void 0 || startLine === input.line ? {} : { startLine },
2032
1999
  side: input.side,
2033
2000
  text
2034
2001
  };
@@ -2064,8 +2031,8 @@ var ReviewCommentStore = class {
2064
2031
  function formatReviewComments(comments) {
2065
2032
  return [
2066
2033
  "<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}`),
2034
+ "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:",
2035
+ ...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
2036
  "</user-review-comments>"
2070
2037
  ].join("\n");
2071
2038
  }
@@ -2510,7 +2477,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2510
2477
  }
2511
2478
  //#endregion
2512
2479
  //#region src/projection-routes.ts
2513
- const MAX_SESSION_ID_CHARS$3 = 1024;
2480
+ const MAX_SESSION_ID_CHARS$4 = 1024;
2514
2481
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
2515
2482
  * the transcript hot path so git/gh latency never delays visible text. */
2516
2483
  const META_REFRESH_MS = 5e3;
@@ -2524,7 +2491,7 @@ function targetFromUrl(rawUrl) {
2524
2491
  if (stream) encoded = encoded.slice(0, -7);
2525
2492
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2526
2493
  const sessionId = decodeURIComponent(encoded);
2527
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$3) return void 0;
2494
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$4) return void 0;
2528
2495
  return {
2529
2496
  sessionId,
2530
2497
  stream
@@ -2686,6 +2653,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2686
2653
  //#region src/repository-status.ts
2687
2654
  const MAX_OUTPUT_BYTES$3 = 65536;
2688
2655
  const MAX_DIFF_BYTES = 262144;
2656
+ const MAX_FILE_BYTES = 8388608;
2689
2657
  const MAX_UNTRACKED_DIFFS = 50;
2690
2658
  const GIT_TIMEOUT_MS$3 = 5e3;
2691
2659
  const GH_TIMEOUT_MS$1 = 8e3;
@@ -2781,14 +2749,14 @@ function parseGitHubRemote(value) {
2781
2749
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2782
2750
  return `${match[1]}/${match[2]}`;
2783
2751
  }
2784
- function record$6(value) {
2752
+ function record$8(value) {
2785
2753
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2786
2754
  }
2787
2755
  function aggregateChecks(value) {
2788
2756
  if (!Array.isArray(value) || value.length === 0) return "none";
2789
2757
  let pending = false;
2790
2758
  for (const item of value) {
2791
- const check = record$6(item);
2759
+ const check = record$8(item);
2792
2760
  if (check === void 0) continue;
2793
2761
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2794
2762
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2812,7 +2780,7 @@ function reviewState(value) {
2812
2780
  return "none";
2813
2781
  }
2814
2782
  function parsePullRequest(value) {
2815
- const input = record$6(value);
2783
+ const input = record$8(value);
2816
2784
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2817
2785
  let url;
2818
2786
  try {
@@ -2833,12 +2801,20 @@ function parsePullRequest(value) {
2833
2801
  review: reviewState(input.reviewDecision),
2834
2802
  checks: aggregateChecks(input.statusCheckRollup),
2835
2803
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2836
- ...typeof record$6(input.author)?.login === "string" ? { author: bounded(String(record$6(input.author)?.login)) } : {},
2804
+ ...typeof record$8(input.author)?.login === "string" ? { author: bounded(String(record$8(input.author)?.login)) } : {},
2837
2805
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2838
2806
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2839
2807
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2840
2808
  };
2841
2809
  }
2810
+ var RepositoryFileError = class extends Error {
2811
+ code;
2812
+ constructor(code, message) {
2813
+ super(message);
2814
+ this.name = "RepositoryFileError";
2815
+ this.code = code;
2816
+ }
2817
+ };
2842
2818
  var RepositoryStatusService = class {
2843
2819
  #runtime;
2844
2820
  #cacheTtlMs;
@@ -2880,6 +2856,29 @@ var RepositoryStatusService = class {
2880
2856
  this.#lastReady.set(cwd, stable);
2881
2857
  return stable;
2882
2858
  }
2859
+ /** Lines [from, to] of a working-tree file, used to expand unmodified context around diff hunks. */
2860
+ async fileLines(cwd, relativePath, from, to) {
2861
+ 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.");
2862
+ 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.");
2863
+ const git = await this.#git();
2864
+ const top = await run(this.#runtime, git, [
2865
+ "rev-parse",
2866
+ "--path-format=absolute",
2867
+ "--show-toplevel"
2868
+ ], cwd, GIT_TIMEOUT_MS$3);
2869
+ if (top.exitCode !== 0) throw new RepositoryFileError("not-repository", "The directory is not inside a git repository.");
2870
+ const root = resolve(top.stdout.trim());
2871
+ const target = resolve(root, relativePath);
2872
+ if (target !== root && !target.startsWith(root + sep)) throw new RepositoryFileError("invalid-request", "The file path escapes the repository.");
2873
+ const content = await readFile(target, "utf8");
2874
+ if (content.length > MAX_FILE_BYTES) throw new RepositoryFileError("too-large", "The file is too large to expand.");
2875
+ const all = content.split(/\r?\n/u);
2876
+ if (all.at(-1) === "") all.pop();
2877
+ return {
2878
+ lines: all.slice(from - 1, to),
2879
+ total: all.length
2880
+ };
2881
+ }
2883
2882
  async #git() {
2884
2883
  this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
2885
2884
  return this.#gitExecutable;
@@ -3099,7 +3098,7 @@ var RepositoryStatusService = class {
3099
3098
  const MAX_OUTPUT_BYTES$2 = 131072;
3100
3099
  const GIT_TIMEOUT_MS$2 = 1e4;
3101
3100
  const GIT_FETCH_TIMEOUT_MS = 6e4;
3102
- const MAX_PATH_CHARS$1 = 4096;
3101
+ const MAX_PATH_CHARS$2 = 4096;
3103
3102
  const MAX_BRANCH_CHARS = 512;
3104
3103
  const LEASE_SCHEMA_VERSION = 1;
3105
3104
  const CLEANUP_GRACE_MS = 12e4;
@@ -3123,7 +3122,7 @@ async function collect$2(handle) {
3123
3122
  };
3124
3123
  }
3125
3124
  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.");
3125
+ 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
3126
  return resolve(value);
3128
3127
  }
3129
3128
  function safeBranch(value) {
@@ -4000,20 +3999,20 @@ var RepositoryActionService = class {
4000
3999
  };
4001
4000
  //#endregion
4002
4001
  //#region src/repository-setup-routes.ts
4003
- const MAX_BODY_BYTES$3 = 16384;
4004
- function record$5(value) {
4002
+ const MAX_BODY_BYTES$4 = 16384;
4003
+ function record$7(value) {
4005
4004
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4006
4005
  }
4007
- async function readJson$3(req) {
4006
+ async function readJson$4(req) {
4008
4007
  const chunks = [];
4009
4008
  let size = 0;
4010
4009
  for await (const chunk of req) {
4011
4010
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4012
4011
  size += buffer.length;
4013
- if (size > MAX_BODY_BYTES$3) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4012
+ if (size > MAX_BODY_BYTES$4) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4014
4013
  chunks.push(buffer);
4015
4014
  }
4016
- const value = record$5(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4015
+ const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4017
4016
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
4018
4017
  return value;
4019
4018
  }
@@ -4036,17 +4035,17 @@ function ndjsonHeaders(res) {
4036
4035
  });
4037
4036
  res.flushHeaders?.();
4038
4037
  }
4039
- function ndjson(res, value) {
4038
+ function ndjson$1(res, value) {
4040
4039
  res.write(`${JSON.stringify(value)}\n`);
4041
4040
  }
4042
4041
  async function streamSetup(res, service, input) {
4043
4042
  ndjsonHeaders(res);
4044
4043
  let ended = false;
4045
4044
  try {
4046
- ndjson(res, {
4045
+ ndjson$1(res, {
4047
4046
  type: "complete",
4048
4047
  result: await service.setup(string$2(input, "cwd"), string$2(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
4049
- if (!ended) ndjson(res, {
4048
+ if (!ended) ndjson$1(res, {
4050
4049
  type: "progress",
4051
4050
  stage
4052
4051
  });
@@ -4054,7 +4053,7 @@ async function streamSetup(res, service, input) {
4054
4053
  });
4055
4054
  } catch (error) {
4056
4055
  const setupError = error instanceof RepositorySetupError ? error : void 0;
4057
- ndjson(res, {
4056
+ ndjson$1(res, {
4058
4057
  type: "error",
4059
4058
  code: setupError?.code ?? "repository-setup-unavailable",
4060
4059
  message: setupError?.message ?? "Repository setup is unavailable."
@@ -4081,19 +4080,19 @@ function registerRepositorySetupRoute(ctx, service) {
4081
4080
  }
4082
4081
  if (pathname === "/plugins/dsh-claude/repository/setup") {
4083
4082
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4084
- const input = await readJson$3(req);
4083
+ const input = await readJson$4(req);
4085
4084
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
4086
4085
  await streamSetup(res, service, input);
4087
4086
  return;
4088
4087
  }
4089
4088
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
4090
4089
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4091
- const input = await readJson$3(req);
4090
+ const input = await readJson$4(req);
4092
4091
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
4093
4092
  }
4094
4093
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
4095
4094
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4096
- const input = await readJson$3(req);
4095
+ const input = await readJson$4(req);
4097
4096
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
4098
4097
  return json(res, 200, { ok: true });
4099
4098
  }
@@ -4111,8 +4110,8 @@ function registerRepositorySetupRoute(ctx, service) {
4111
4110
  }
4112
4111
  //#endregion
4113
4112
  //#region src/repository-action-routes.ts
4114
- const MAX_BODY_BYTES$2 = 16384;
4115
- const MAX_SESSION_ID_CHARS$2 = 1024;
4113
+ const MAX_BODY_BYTES$3 = 16384;
4114
+ const MAX_SESSION_ID_CHARS$3 = 1024;
4116
4115
  const ACTIONS = /* @__PURE__ */ new Set([
4117
4116
  "commit",
4118
4117
  "commit-push",
@@ -4121,25 +4120,25 @@ const ACTIONS = /* @__PURE__ */ new Set([
4121
4120
  "merge-pr",
4122
4121
  "update-branch"
4123
4122
  ]);
4124
- function record$4(value) {
4123
+ function record$6(value) {
4125
4124
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4126
4125
  }
4127
- async function readJson$2(req) {
4126
+ async function readJson$3(req) {
4128
4127
  const chunks = [];
4129
4128
  let size = 0;
4130
4129
  for await (const chunk of req) {
4131
4130
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4132
4131
  size += buffer.length;
4133
- if (size > MAX_BODY_BYTES$2) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4132
+ if (size > MAX_BODY_BYTES$3) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4134
4133
  chunks.push(buffer);
4135
4134
  }
4136
- const value = record$4(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4135
+ const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4137
4136
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
4138
4137
  return value;
4139
4138
  }
4140
4139
  function sessionId$1(url) {
4141
4140
  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.");
4141
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4143
4142
  return value;
4144
4143
  }
4145
4144
  function string$1(input, key) {
@@ -4188,12 +4187,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4188
4187
  }
4189
4188
  if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
4190
4189
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4191
- const input = await readJson$2(req);
4190
+ const input = await readJson$3(req);
4192
4191
  return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
4193
4192
  }
4194
4193
  if (url.pathname === "/plugins/dsh-claude/repository/action") {
4195
4194
  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))));
4195
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$3(req))));
4197
4196
  }
4198
4197
  return json(res, 404, { error: "not found" });
4199
4198
  } catch (error) {
@@ -4237,14 +4236,14 @@ async function collect(handle) {
4237
4236
  lossy: stdout?.lossy === true
4238
4237
  };
4239
4238
  }
4240
- function record$3(value) {
4239
+ function record$5(value) {
4241
4240
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4242
4241
  }
4243
4242
  function parseReviewComments(value) {
4244
4243
  if (!Array.isArray(value)) return [];
4245
4244
  const comments = [];
4246
4245
  for (const item of value) {
4247
- const input = record$3(item);
4246
+ const input = record$5(item);
4248
4247
  if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4249
4248
  const body = typeof input.body === "string" ? input.body.trim() : "";
4250
4249
  const path = typeof input.path === "string" ? input.path : "";
@@ -4256,7 +4255,7 @@ function parseReviewComments(value) {
4256
4255
  path,
4257
4256
  ...line === void 0 ? {} : { line },
4258
4257
  side: input.side === "LEFT" ? "old" : "new",
4259
- author: typeof record$3(input.user)?.login === "string" ? String(record$3(input.user)?.login) : "unknown",
4258
+ author: typeof record$5(input.user)?.login === "string" ? String(record$5(input.user)?.login) : "unknown",
4260
4259
  body: body.slice(0, MAX_COMMENT_CHARS),
4261
4260
  url
4262
4261
  });
@@ -4273,7 +4272,7 @@ function parseFailingChecks(value) {
4273
4272
  if (!Array.isArray(value)) return [];
4274
4273
  const failing = [];
4275
4274
  for (const item of value) {
4276
- const input = record$3(item);
4275
+ const input = record$5(item);
4277
4276
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4278
4277
  failing.push({
4279
4278
  name: input.name,
@@ -4385,10 +4384,10 @@ var PullRequestFeedbackService = class {
4385
4384
  };
4386
4385
  //#endregion
4387
4386
  //#region src/pr-feedback-routes.ts
4388
- const MAX_SESSION_ID_CHARS$1 = 1024;
4387
+ const MAX_SESSION_ID_CHARS$2 = 1024;
4389
4388
  function sessionId(url) {
4390
4389
  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.");
4390
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4392
4391
  return value;
4393
4392
  }
4394
4393
  function pullNumber(url) {
@@ -4425,7 +4424,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
4425
4424
  }
4426
4425
  //#endregion
4427
4426
  //#region src/repository-status-routes.ts
4428
- const MAX_PATH_CHARS = 4096;
4427
+ const MAX_PATH_CHARS$1 = 4096;
4429
4428
  /** Read-only repository status for an arbitrary directory (the overview panel
4430
4429
  * aggregates every Claude session's checkout through this). */
4431
4430
  function registerRepositoryStatusRoute(ctx, service) {
@@ -4436,7 +4435,7 @@ function registerRepositoryStatusRoute(ctx, service) {
4436
4435
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4437
4436
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4438
4437
  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, {
4438
+ if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4440
4439
  error: "invalid-request",
4441
4440
  message: "The cwd query parameter is invalid."
4442
4441
  });
@@ -4452,6 +4451,45 @@ function registerRepositoryStatusRoute(ctx, service) {
4452
4451
  }), "dsh-claude: repository status route");
4453
4452
  }
4454
4453
  //#endregion
4454
+ //#region src/repository-file-routes.ts
4455
+ const MAX_PATH_CHARS = 4096;
4456
+ /** Read-only slice of a working-tree file so the diff panel can expand unmodified lines around hunks. */
4457
+ function registerRepositoryFileRoute(ctx, service) {
4458
+ ctx.effect(() => ctx.webServer.register({
4459
+ kind: "exact",
4460
+ path: CLAUDE_REPOSITORY_FILE_PATH,
4461
+ handler: async (req, res) => {
4462
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4463
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4464
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
4465
+ const cwd = params.get("cwd");
4466
+ const path = params.get("path");
4467
+ const from = Number(params.get("from"));
4468
+ const to = Number(params.get("to"));
4469
+ if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4470
+ error: "invalid-request",
4471
+ message: "The cwd query parameter is invalid."
4472
+ });
4473
+ if (path === null || path.length === 0 || path.length > MAX_PATH_CHARS) return json(res, 400, {
4474
+ error: "invalid-request",
4475
+ message: "The path query parameter is invalid."
4476
+ });
4477
+ try {
4478
+ return json(res, 200, await service.fileLines(cwd, path, from, to));
4479
+ } catch (error) {
4480
+ if (error instanceof RepositoryFileError) return json(res, error.code === "invalid-request" ? 400 : 409, {
4481
+ error: error.code,
4482
+ message: error.message
4483
+ });
4484
+ return json(res, 500, {
4485
+ error: "repository-file-unavailable",
4486
+ message: "The file could not be read."
4487
+ });
4488
+ }
4489
+ }
4490
+ }), "dsh-claude: repository file route");
4491
+ }
4492
+ //#endregion
4455
4493
  //#region src/jira.ts
4456
4494
  const REQUEST_TIMEOUT_MS = 15e3;
4457
4495
  const MAX_RESULTS = 20;
@@ -4465,7 +4503,7 @@ var JiraError = class extends Error {
4465
4503
  this.code = code;
4466
4504
  }
4467
4505
  };
4468
- function record$2(value) {
4506
+ function record$4(value) {
4469
4507
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4470
4508
  }
4471
4509
  /** Jira Cloud sites are origins; Data Center may carry a context path. */
@@ -4491,15 +4529,15 @@ function buildJql(query) {
4491
4529
  return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
4492
4530
  }
4493
4531
  function parseTickets(value, siteUrl) {
4494
- const issues = record$2(value)?.issues;
4532
+ const issues = record$4(value)?.issues;
4495
4533
  if (!Array.isArray(issues)) return [];
4496
4534
  const tickets = [];
4497
4535
  for (const item of issues) {
4498
- const issue = record$2(item);
4499
- const fields = record$2(issue?.fields);
4536
+ const issue = record$4(item);
4537
+ const fields = record$4(issue?.fields);
4500
4538
  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;
4539
+ const status = record$4(fields.status)?.name;
4540
+ const type = record$4(fields.issuetype)?.name;
4503
4541
  tickets.push({
4504
4542
  key: issue.key,
4505
4543
  summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
@@ -4541,7 +4579,7 @@ var JiraService = class {
4541
4579
  email,
4542
4580
  apiToken
4543
4581
  };
4544
- const myself = record$2(await this.#json(connection, "/rest/api/3/myself"));
4582
+ const myself = record$4(await this.#json(connection, "/rest/api/3/myself"));
4545
4583
  const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
4546
4584
  const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
4547
4585
  const store = {
@@ -4560,7 +4598,7 @@ var JiraService = class {
4560
4598
  if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
4561
4599
  let accountId = store.accountId;
4562
4600
  if (accountId === void 0) {
4563
- const myself = record$2(await this.#json(store, "/rest/api/3/myself"));
4601
+ const myself = record$4(await this.#json(store, "/rest/api/3/myself"));
4564
4602
  if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
4565
4603
  accountId = myself.accountId;
4566
4604
  await this.#write({
@@ -4628,7 +4666,7 @@ var JiraService = class {
4628
4666
  throw error;
4629
4667
  }
4630
4668
  if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
4631
- const input = record$2(JSON.parse(text));
4669
+ const input = record$4(JSON.parse(text));
4632
4670
  if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
4633
4671
  return {
4634
4672
  siteUrl: input.siteUrl,
@@ -4659,20 +4697,20 @@ var JiraService = class {
4659
4697
  };
4660
4698
  //#endregion
4661
4699
  //#region src/jira-routes.ts
4662
- const MAX_BODY_BYTES$1 = 8192;
4663
- function record$1(value) {
4700
+ const MAX_BODY_BYTES$2 = 8192;
4701
+ function record$3(value) {
4664
4702
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4665
4703
  }
4666
- async function readJson$1(req) {
4704
+ async function readJson$2(req) {
4667
4705
  const chunks = [];
4668
4706
  let size = 0;
4669
4707
  for await (const chunk of req) {
4670
4708
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4671
4709
  size += buffer.length;
4672
- if (size > MAX_BODY_BYTES$1) throw new JiraError("body-too-large", "The request body is too large.");
4710
+ if (size > MAX_BODY_BYTES$2) throw new JiraError("body-too-large", "The request body is too large.");
4673
4711
  chunks.push(buffer);
4674
4712
  }
4675
- const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4713
+ const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4676
4714
  if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
4677
4715
  return value;
4678
4716
  }
@@ -4695,7 +4733,7 @@ function registerJiraRoute(ctx, service) {
4695
4733
  }
4696
4734
  if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
4697
4735
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4698
- const input = await readJson$1(req);
4736
+ const input = await readJson$2(req);
4699
4737
  return json(res, 200, await service.connect({
4700
4738
  siteUrl: string(input, "siteUrl"),
4701
4739
  email: string(input, "email"),
@@ -4709,7 +4747,7 @@ function registerJiraRoute(ctx, service) {
4709
4747
  }
4710
4748
  if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
4711
4749
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4712
- const input = await readJson$1(req);
4750
+ const input = await readJson$2(req);
4713
4751
  await service.assignToMe(string(input, "key"));
4714
4752
  return json(res, 200, { assigned: true });
4715
4753
  }
@@ -4733,6 +4771,319 @@ function registerJiraRoute(ctx, service) {
4733
4771
  }), "dsh-claude: jira route");
4734
4772
  }
4735
4773
  //#endregion
4774
+ //#region src/ask.ts
4775
+ const MAX_SELECTION_CHARS = 8e3;
4776
+ const MAX_CONTEXT_CHARS = 16e3;
4777
+ const MAX_QUESTION_CHARS = 2e3;
4778
+ const ASK_TIMEOUT_MS = 18e4;
4779
+ const MAX_STDERR_BYTES = 65536;
4780
+ const MAX_TOOL_SUMMARY_CHARS = 160;
4781
+ const READ_ONLY_TOOLS = [
4782
+ "Read",
4783
+ "Grep",
4784
+ "Glob"
4785
+ ];
4786
+ var AskError = class extends Error {
4787
+ code;
4788
+ constructor(code, message) {
4789
+ super(message);
4790
+ this.name = "AskError";
4791
+ this.code = code;
4792
+ }
4793
+ };
4794
+ function fence(value) {
4795
+ return `"""\n${value.replaceAll("\"\"\"", "\" \" \"")}\n"""`;
4796
+ }
4797
+ function askPrompt(request) {
4798
+ const selection = request.selection.trim().slice(0, MAX_SELECTION_CHARS);
4799
+ const context = (request.context ?? "").trim().slice(0, MAX_CONTEXT_CHARS);
4800
+ const question = request.question.trim().slice(0, MAX_QUESTION_CHARS);
4801
+ return [
4802
+ "You are answering a follow-up question about part of an earlier assistant reply in this project.",
4803
+ "",
4804
+ "Selected passage:",
4805
+ fence(selection),
4806
+ ...context.length === 0 || context === selection ? [] : [
4807
+ "",
4808
+ "Surrounding reply, for context only:",
4809
+ fence(context)
4810
+ ],
4811
+ "",
4812
+ `Question: ${question}`,
4813
+ "",
4814
+ "Answer the question directly and concisely, in the same language as the question. Use Markdown.",
4815
+ "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."
4816
+ ].join("\n");
4817
+ }
4818
+ /** CLI effort for a session thinking mode; `off` and unknown modes send nothing. */
4819
+ function effortFor(thinkingMode) {
4820
+ if (thinkingMode === void 0 || thinkingMode === "off") return void 0;
4821
+ if (thinkingMode === "ultracode") return "max";
4822
+ return [
4823
+ "low",
4824
+ "medium",
4825
+ "high",
4826
+ "max"
4827
+ ].includes(thinkingMode) ? thinkingMode : void 0;
4828
+ }
4829
+ function askArguments(preferences) {
4830
+ const effort = effortFor(preferences.thinkingMode);
4831
+ return [
4832
+ "-p",
4833
+ "--output-format",
4834
+ "stream-json",
4835
+ "--verbose",
4836
+ "--include-partial-messages",
4837
+ "--strict-mcp-config",
4838
+ "--mcp-config",
4839
+ "{\"mcpServers\":{}}",
4840
+ ...preferences.model === void 0 || preferences.model === "default" ? [] : ["--model", preferences.model],
4841
+ ...effort === void 0 ? [] : ["--effort", effort],
4842
+ "--tools",
4843
+ ...READ_ONLY_TOOLS,
4844
+ "--allowedTools",
4845
+ ...READ_ONLY_TOOLS
4846
+ ];
4847
+ }
4848
+ function record$2(value) {
4849
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4850
+ }
4851
+ /** One-line description of a tool call, mirroring the main window's step titles. */
4852
+ function toolSummary(input) {
4853
+ const fields = record$2(input);
4854
+ if (fields === void 0) return void 0;
4855
+ const candidate = [
4856
+ fields.command,
4857
+ fields.pattern,
4858
+ fields.file_path,
4859
+ fields.path,
4860
+ fields.query
4861
+ ].find((value) => typeof value === "string" && value.length > 0);
4862
+ return (typeof candidate === "string" ? candidate : JSON.stringify(fields)).replaceAll(/\s+/gu, " ").trim().slice(0, MAX_TOOL_SUMMARY_CHARS);
4863
+ }
4864
+ function eventsOfStreamLine(line) {
4865
+ let parsed;
4866
+ try {
4867
+ parsed = record$2(JSON.parse(line));
4868
+ } catch {
4869
+ return [];
4870
+ }
4871
+ if (parsed === void 0) return [];
4872
+ if (parsed.type === "system" && parsed.subtype === "init") return [{
4873
+ type: "status",
4874
+ text: "ready"
4875
+ }];
4876
+ if (parsed.type === "stream_event") {
4877
+ const event = record$2(parsed.event);
4878
+ if (event?.type === "content_block_start") {
4879
+ const block = record$2(event.content_block);
4880
+ if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
4881
+ type: "tool",
4882
+ id: block.id,
4883
+ phase: "start",
4884
+ name: block.name
4885
+ }];
4886
+ return [];
4887
+ }
4888
+ const delta = record$2(event?.delta);
4889
+ if (event?.type !== "content_block_delta" || delta === void 0) return [];
4890
+ if (delta.type === "text_delta" && typeof delta.text === "string") return [{
4891
+ type: "text",
4892
+ text: delta.text
4893
+ }];
4894
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") return [{
4895
+ type: "thinking",
4896
+ text: delta.thinking
4897
+ }];
4898
+ return [];
4899
+ }
4900
+ if (parsed.type === "assistant" || parsed.type === "user") {
4901
+ const content = record$2(parsed.message)?.content;
4902
+ if (!Array.isArray(content)) return [];
4903
+ const events = [];
4904
+ for (const item of content) {
4905
+ const block = record$2(item);
4906
+ if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
4907
+ const summary = toolSummary(block.input);
4908
+ events.push({
4909
+ type: "tool",
4910
+ id: block.id,
4911
+ phase: "input",
4912
+ name: block.name,
4913
+ ...summary === void 0 ? {} : { summary }
4914
+ });
4915
+ } else if (block?.type === "tool_result" && typeof block.tool_use_id === "string") events.push({
4916
+ type: "tool",
4917
+ id: block.tool_use_id,
4918
+ phase: "done",
4919
+ ...block.is_error === true ? { error: true } : {}
4920
+ });
4921
+ }
4922
+ return events;
4923
+ }
4924
+ if (parsed.type === "result" && typeof parsed.result === "string") return [{
4925
+ type: "result",
4926
+ text: parsed.result
4927
+ }];
4928
+ return [];
4929
+ }
4930
+ /** One-shot Claude Code query answering a question about selected reply text. */
4931
+ var AskService = class {
4932
+ #runtime;
4933
+ #executablePath;
4934
+ constructor(runtime, executablePath) {
4935
+ this.#runtime = runtime;
4936
+ this.#executablePath = executablePath;
4937
+ }
4938
+ async ask(cwd, request, preferences, onEvent, signal) {
4939
+ if (request.question.trim().length === 0 || request.selection.trim().length === 0) throw new AskError("invalid-request", "A selection and a question are required.");
4940
+ if (this.#executablePath.length === 0) throw new AskError("claude-unavailable", "Claude Code is unavailable.");
4941
+ const timeout = AbortSignal.timeout(ASK_TIMEOUT_MS);
4942
+ const handle = this.#runtime.spawn({
4943
+ argv: [this.#executablePath, ...askArguments(preferences)],
4944
+ cwd,
4945
+ stdio: {
4946
+ stdin: { data: askPrompt(request) },
4947
+ stdout: "pipe",
4948
+ stderr: { maxBytes: MAX_STDERR_BYTES }
4949
+ },
4950
+ graceMs: 1e3,
4951
+ signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
4952
+ env: {}
4953
+ });
4954
+ const stdout = handle.stdout;
4955
+ if (stdout === void 0) throw new AskError("ask-failed", "Claude output is unavailable.");
4956
+ const decoder = new StringDecoder("utf8");
4957
+ let buffer = "";
4958
+ let emitted = false;
4959
+ let result;
4960
+ const consume = (line) => {
4961
+ for (const event of eventsOfStreamLine(line)) {
4962
+ if (event.type === "result") {
4963
+ result = event.text;
4964
+ continue;
4965
+ }
4966
+ if (event.type !== "tool" && event.text.length === 0) continue;
4967
+ if (event.type === "text") emitted = true;
4968
+ onEvent(event);
4969
+ }
4970
+ };
4971
+ for await (const chunk of stdout) {
4972
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
4973
+ const lines = buffer.split("\n");
4974
+ buffer = lines.pop() ?? "";
4975
+ for (const line of lines) if (line.trim().length > 0) consume(line);
4976
+ }
4977
+ buffer += decoder.end();
4978
+ if (buffer.trim().length > 0) consume(buffer);
4979
+ const outcome = await handle.done;
4980
+ if (!emitted && result !== void 0 && result.length > 0) {
4981
+ emitted = true;
4982
+ onEvent({
4983
+ type: "text",
4984
+ text: result
4985
+ });
4986
+ }
4987
+ if (!emitted) {
4988
+ if (signal?.aborted === true) throw new AskError("aborted", "The question was cancelled.");
4989
+ if (timeout.aborted) throw new AskError("timeout", "Claude took too long to answer.");
4990
+ const stderr = handle.collected.stderr?.readFrom(0).text.trim().split(/\r?\n/u).filter((line) => line.length > 0).at(-1);
4991
+ throw new AskError("ask-failed", stderr !== void 0 && stderr.length > 0 ? stderr : `Claude exited with code ${String(outcome.exitCode)}.`);
4992
+ }
4993
+ }
4994
+ };
4995
+ //#endregion
4996
+ //#region src/ask-routes.ts
4997
+ const MAX_BODY_BYTES$1 = 131072;
4998
+ const MAX_SESSION_ID_CHARS$1 = 1024;
4999
+ function record$1(value) {
5000
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5001
+ }
5002
+ async function readJson$1(req) {
5003
+ const chunks = [];
5004
+ let size = 0;
5005
+ for await (const chunk of req) {
5006
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5007
+ size += buffer.length;
5008
+ if (size > MAX_BODY_BYTES$1) throw new AskError("body-too-large", "The request body is too large.");
5009
+ chunks.push(buffer);
5010
+ }
5011
+ const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5012
+ if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
5013
+ return value;
5014
+ }
5015
+ function askRequest(input) {
5016
+ 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.");
5017
+ return {
5018
+ selection: input.selection,
5019
+ question: input.question,
5020
+ ...typeof input.context === "string" ? { context: input.context } : {}
5021
+ };
5022
+ }
5023
+ function ndjson(res, value) {
5024
+ res.write(`${JSON.stringify(value)}\n`);
5025
+ }
5026
+ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
5027
+ ctx.effect(() => ctx.webServer.register({
5028
+ kind: "exact",
5029
+ path: CLAUDE_ASK_PATH,
5030
+ handler: async (req, res) => {
5031
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
5032
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5033
+ const url = new URL(req.url ?? "/", "http://localhost");
5034
+ let cwd;
5035
+ let request;
5036
+ let sessionId;
5037
+ try {
5038
+ const value = url.searchParams.get("sessionId");
5039
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new AskError("invalid-session", "The session is invalid.");
5040
+ sessionId = value;
5041
+ const resolved = cwdForSession(sessionId);
5042
+ if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
5043
+ cwd = resolved;
5044
+ request = askRequest(await readJson$1(req));
5045
+ } catch (error) {
5046
+ if (error instanceof AskError) return json(res, 409, {
5047
+ error: error.code,
5048
+ message: error.message
5049
+ });
5050
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
5051
+ return json(res, 500, {
5052
+ error: "ask-unavailable",
5053
+ message: "The question could not be sent."
5054
+ });
5055
+ }
5056
+ res.writeHead(200, {
5057
+ "content-type": "application/x-ndjson; charset=utf-8",
5058
+ "cache-control": "no-store",
5059
+ "x-content-type-options": "nosniff"
5060
+ });
5061
+ res.flushHeaders?.();
5062
+ const controller = new AbortController();
5063
+ req.on("close", () => {
5064
+ controller.abort();
5065
+ });
5066
+ try {
5067
+ await service.ask(cwd, request, preferencesFor(sessionId) ?? {}, (event) => {
5068
+ ndjson(res, event.type === "text" ? {
5069
+ type: "delta",
5070
+ text: event.text
5071
+ } : event);
5072
+ }, controller.signal);
5073
+ ndjson(res, { type: "done" });
5074
+ } catch (error) {
5075
+ ndjson(res, {
5076
+ type: "error",
5077
+ code: error instanceof AskError ? error.code : "ask-unavailable",
5078
+ message: error instanceof AskError ? error.message : "The question could not be answered."
5079
+ });
5080
+ } finally {
5081
+ res.end();
5082
+ }
5083
+ }
5084
+ }), "dsh-claude: ask route");
5085
+ }
5086
+ //#endregion
4736
5087
  //#region src/review-comment-routes.ts
4737
5088
  const MAX_BODY_BYTES = 16384;
4738
5089
  const MAX_SESSION_ID_CHARS = 1024;
@@ -4772,6 +5123,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
4772
5123
  return json(res, 200, { comment: store.add(sessionId, {
4773
5124
  path: input.path,
4774
5125
  line: input.line,
5126
+ startLine: input.startLine,
4775
5127
  side: input.side,
4776
5128
  text: input.text
4777
5129
  }) });
@@ -5591,6 +5943,7 @@ async function apply(ctx, config) {
5591
5943
  });
5592
5944
  registerRepositorySetupRoute(webCtx, repositorySetup);
5593
5945
  registerRepositoryStatusRoute(webCtx, repositoryStatus);
5946
+ registerRepositoryFileRoute(webCtx, repositoryStatus);
5594
5947
  registerJiraRoute(webCtx, new JiraService());
5595
5948
  const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
5596
5949
  const cwdForClaudeSession = (sessionId) => {
@@ -5600,6 +5953,13 @@ async function apply(ctx, config) {
5600
5953
  };
5601
5954
  registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
5602
5955
  registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
5956
+ registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
5957
+ const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
5958
+ return snapshot === void 0 ? void 0 : {
5959
+ model: snapshot.model,
5960
+ ...snapshot.thinkingMode === void 0 ? {} : { thinkingMode: snapshot.thinkingMode }
5961
+ };
5962
+ });
5603
5963
  const ownsClaudeSession = (sessionId) => {
5604
5964
  const agent = webCtx.agents.get(sessionId);
5605
5965
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";