@norman-else/dsh-claude 0.1.24 → 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_UPDATE_PATH, S as CLAUDE_UPDATE_CHECK_PATH, T as TASK_TOOL_NAMES, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_SETUP_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_REPOSITORY_ACTION_PATH, x as CLAUDE_REVIEW_COMMENT_PATH, y as CLAUDE_REPOSITORY_FEEDBACK_PATH } from "./events-DhrAr5gh.mjs";
2
- import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-Br25ODdm.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-B_Cb0RG6.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,18 +26,18 @@ function emptyProjection() {
25
26
  activities: []
26
27
  };
27
28
  }
28
- function record$6(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) {
32
33
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
33
34
  }
34
- function string$3(value, max) {
35
+ 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$6(value);
39
- if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
39
+ const input = record$10(value);
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,
42
43
  sdkVersion: input.sdkVersion,
@@ -65,22 +66,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
65
66
  "failed"
66
67
  ]);
67
68
  function activity(value) {
68
- const input = record$6(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$6(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$6(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$6(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,10 +654,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
653
654
  }
654
655
  //#endregion
655
656
  //#region src/sdk-messages.ts
656
- function record$5(value) {
657
+ function record$9(value) {
657
658
  return value !== null && typeof value === "object" ? value : void 0;
658
659
  }
659
- function string$2(value) {
660
+ function string$3(value) {
660
661
  return typeof value === "string" ? value : void 0;
661
662
  }
662
663
  function taskUsageOf(usage) {
@@ -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$5(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,26 +681,26 @@ function resultUsage(message) {
680
681
  return normalized;
681
682
  }
682
683
  function normalizeAssistant(message) {
683
- const content = record$5(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",
687
688
  detail: message
688
689
  }];
689
- const parentToolUseId = string$2(message.parent_tool_use_id);
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$5(item);
693
+ const block = record$9(item);
693
694
  if (block === void 0) continue;
694
695
  if (block.type === "text") {
695
- const text = string$2(block.text);
696
+ const text = string$3(block.text);
696
697
  if (text !== void 0 && text.length > 0) normalized.push({
697
698
  kind: "assistant-text",
698
699
  text,
699
700
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
700
701
  });
701
702
  } else if (block.type === "thinking") {
702
- const text = string$2(block.thinking);
703
+ const text = string$3(block.thinking);
703
704
  if (text !== void 0 && text.length > 0) normalized.push({
704
705
  kind: "thinking",
705
706
  text,
@@ -707,8 +708,8 @@ function normalizeAssistant(message) {
707
708
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
708
709
  });
709
710
  } else if (block.type === "tool_use") {
710
- const toolUseId = string$2(block.id);
711
- const toolName = string$2(block.name);
711
+ const toolUseId = string$3(block.id);
712
+ const toolName = string$3(block.name);
712
713
  if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
713
714
  kind: "tool-call",
714
715
  toolUseId,
@@ -722,19 +723,19 @@ function normalizeAssistant(message) {
722
723
  }
723
724
  function normalizeUser(message) {
724
725
  if (message.isReplay === true) return [];
725
- const content = record$5(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",
729
730
  title: "Malformed Claude user message",
730
731
  detail: message
731
732
  }];
732
- const parentToolUseId = string$2(message.parent_tool_use_id);
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$5(item);
736
+ const block = record$9(item);
736
737
  if (block?.type !== "tool_result") continue;
737
- const toolUseId = string$2(block.tool_use_id);
738
+ const toolUseId = string$3(block.tool_use_id);
738
739
  if (toolUseId === void 0) continue;
739
740
  normalized.push({
740
741
  kind: "tool-result",
@@ -747,11 +748,11 @@ function normalizeUser(message) {
747
748
  return normalized;
748
749
  }
749
750
  function normalizeSystem(message) {
750
- const subtype = string$2(message.subtype);
751
+ const subtype = string$3(message.subtype);
751
752
  if (subtype === "init") {
752
- const sessionId = string$2(message.session_id);
753
- const cliVersion = string$2(message.claude_code_version);
754
- const cwd = string$2(message.cwd);
753
+ const sessionId = string$3(message.session_id);
754
+ const cliVersion = string$3(message.claude_code_version);
755
+ const cwd = string$3(message.cwd);
755
756
  return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
756
757
  kind: "init",
757
758
  sessionId,
@@ -780,20 +781,20 @@ function normalizeSystem(message) {
780
781
  title: `Claude session ${String(message.state)}`
781
782
  }];
782
783
  if (subtype === "permission_denied") {
783
- const toolUseId = string$2(message.tool_use_id);
784
- const toolName = string$2(message.tool_name);
784
+ const toolUseId = string$3(message.tool_use_id);
785
+ const toolName = string$3(message.tool_name);
785
786
  if (toolUseId !== void 0 && toolName !== void 0) return [{
786
787
  kind: "permission-denied",
787
788
  toolUseId,
788
789
  toolName,
789
- summary: string$2(message.message) ?? "Claude Code denied the action"
790
+ summary: string$3(message.message) ?? "Claude Code denied the action"
790
791
  }];
791
792
  }
792
793
  if (subtype === "task_started") {
793
- const taskId = string$2(message.task_id);
794
- const description = string$2(message.description);
795
- const subagentType = string$2(message.subagent_type);
796
- const taskType = string$2(message.task_type);
794
+ const taskId = string$3(message.task_id);
795
+ const description = string$3(message.description);
796
+ const subagentType = string$3(message.subagent_type);
797
+ const taskType = string$3(message.task_type);
797
798
  return [{
798
799
  kind: "subagent",
799
800
  title: description ?? taskId ?? "Claude subagent started",
@@ -808,12 +809,12 @@ function normalizeSystem(message) {
808
809
  }];
809
810
  }
810
811
  if (subtype === "task_progress") {
811
- const taskId = string$2(message.task_id);
812
- const description = string$2(message.description);
813
- const summary = string$2(message.summary);
814
- const subagentType = string$2(message.subagent_type);
815
- const lastToolName = string$2(message.last_tool_name);
816
- const usage = taskUsageOf(record$5(message.usage));
812
+ const taskId = string$3(message.task_id);
813
+ const description = string$3(message.description);
814
+ const summary = string$3(message.summary);
815
+ const subagentType = string$3(message.subagent_type);
816
+ const lastToolName = string$3(message.last_tool_name);
817
+ const usage = taskUsageOf(record$9(message.usage));
817
818
  return [{
818
819
  kind: "subagent",
819
820
  title: summary ?? description ?? "Claude subagent update",
@@ -829,11 +830,11 @@ function normalizeSystem(message) {
829
830
  }];
830
831
  }
831
832
  if (subtype === "task_updated") {
832
- const patch = record$5(message.patch);
833
- const status = string$2(patch?.status);
834
- const taskId = string$2(message.task_id);
835
- const description = string$2(patch?.description);
836
- const error = string$2(patch?.error);
833
+ const patch = record$9(message.patch);
834
+ const status = string$3(patch?.status);
835
+ const taskId = string$3(message.task_id);
836
+ const description = string$3(patch?.description);
837
+ const error = string$3(patch?.error);
837
838
  const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
838
839
  return [{
839
840
  kind: "subagent",
@@ -849,10 +850,10 @@ function normalizeSystem(message) {
849
850
  if (subtype === "task_notification") {
850
851
  const failed = message.status === "failed";
851
852
  const stopped = message.status === "stopped" || message.status === "cancelled";
852
- const taskId = string$2(message.task_id);
853
- const summary = string$2(message.summary);
853
+ const taskId = string$3(message.task_id);
854
+ const summary = string$3(message.summary);
854
855
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
855
- const usage = taskUsageOf(record$5(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,10 +868,10 @@ 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$5(item);
871
- const taskId = string$2(entry?.task_id);
872
- const description = string$2(entry?.description);
873
- const taskType = string$2(entry?.task_type);
871
+ const entry = record$9(item);
872
+ const taskId = string$3(entry?.task_id);
873
+ const description = string$3(entry?.description);
874
+ const taskType = string$3(entry?.task_type);
874
875
  if (taskId === void 0 || description === void 0) return [];
875
876
  return [{
876
877
  taskId,
@@ -886,7 +887,7 @@ function normalizeSystem(message) {
886
887
  }];
887
888
  if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
888
889
  kind: message.level === "warning" ? "warning" : "status",
889
- title: string$2(message.content) ?? string$2(message.text) ?? "Claude Code notice",
890
+ title: string$3(message.content) ?? string$3(message.text) ?? "Claude Code notice",
890
891
  detail: message
891
892
  }];
892
893
  if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
@@ -910,12 +911,12 @@ 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$5(value.event);
914
- const parentToolUseId = string$2(value.parent_tool_use_id);
914
+ const event = record$9(value.event);
915
+ const parentToolUseId = string$3(value.parent_tool_use_id);
915
916
  if (event?.type === "content_block_delta") {
916
- const delta = record$5(event.delta);
917
+ const delta = record$9(event.delta);
917
918
  if (delta?.type === "text_delta") {
918
- const text = string$2(delta.text);
919
+ const text = string$3(delta.text);
919
920
  return text === void 0 ? [] : [{
920
921
  kind: "text-delta",
921
922
  text,
@@ -923,7 +924,7 @@ function normalizeSdkMessage(message) {
923
924
  }];
924
925
  }
925
926
  if (delta?.type === "thinking_delta") {
926
- const text = string$2(delta.thinking);
927
+ const text = string$3(delta.thinking);
927
928
  return text === void 0 ? [] : [{
928
929
  kind: "thinking",
929
930
  text,
@@ -938,7 +939,7 @@ function normalizeSdkMessage(message) {
938
939
  if (value.type === "user") return normalizeUser(value);
939
940
  if (value.type === "system") return normalizeSystem(value);
940
941
  if (value.type === "result") {
941
- const sessionId = string$2(value.session_id);
942
+ const sessionId = string$3(value.session_id);
942
943
  if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
943
944
  kind: "protocol-error",
944
945
  title: "Malformed Claude result message",
@@ -946,11 +947,11 @@ function normalizeSdkMessage(message) {
946
947
  }];
947
948
  const success = value.subtype === "success" && value.is_error !== true;
948
949
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
949
- const terminalReason = string$2(value.terminal_reason);
950
- const userMessageUuid = string$2(value.user_message_uuid);
951
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$5(item)).filter((item) => item !== void 0).map((item) => {
952
- const toolName = string$2(item.tool_name);
953
- const toolUseId = string$2(item.tool_use_id);
950
+ const terminalReason = string$3(value.terminal_reason);
951
+ const userMessageUuid = string$3(value.user_message_uuid);
952
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$9(item)).filter((item) => item !== void 0).map((item) => {
953
+ const toolName = string$3(item.tool_name);
954
+ const toolUseId = string$3(item.tool_use_id);
954
955
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
955
956
  toolName,
956
957
  toolUseId
@@ -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$2(record$5(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$1 = 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$1 || /[\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$4(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$4(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$4(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$4(input.author)?.login === "string" ? { author: bounded(String(record$4(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 = 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 || !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) {
@@ -3224,10 +3223,73 @@ var RepositorySetupService = class {
3224
3223
  const remote = info.remoteBranches.includes(branch);
3225
3224
  if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
3226
3225
  const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
3227
- if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, progress);
3226
+ if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, requestedBranch !== void 0 && info.branches.includes(requestedBranch), progress);
3228
3227
  progress("switching-branch");
3229
3228
  return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
3230
3229
  }
3230
+ /** Tear down a merged branch: remove a plugin worktree (and its lease and
3231
+ * branch), or switch a plain checkout back to the base branch and delete
3232
+ * the merged branch. Refuses dirty trees. */
3233
+ async cleanupMerged(pathValue, baseBranch) {
3234
+ const path = safePath(pathValue);
3235
+ const base = safeBranch(baseBranch);
3236
+ const git = await this.#git();
3237
+ const status = await this.#run(git, [
3238
+ "status",
3239
+ "--porcelain=v1",
3240
+ "--untracked-files=normal"
3241
+ ], path);
3242
+ if (status.exitCode !== 0 || status.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
3243
+ if (status.stdout.trim().length > 0) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before cleaning up.");
3244
+ const lease = (await this.#readLeases()).find((item) => comparablePath(item.path) === comparablePath(path));
3245
+ if (lease !== void 0) return this.#serialize(async () => {
3246
+ if ((await this.#run(git, [
3247
+ "worktree",
3248
+ "remove",
3249
+ "--",
3250
+ lease.path
3251
+ ], lease.root)).exitCode !== 0) throw new RepositorySetupError("worktree-remove-failed", "Git could not remove the worktree.");
3252
+ await this.#run(git, [
3253
+ "branch",
3254
+ "-D",
3255
+ "--",
3256
+ lease.branch
3257
+ ], lease.root).catch(() => void 0);
3258
+ const current = await this.#readLeases();
3259
+ await this.#writeLeases(current.filter((item) => item.id !== lease.id));
3260
+ return {
3261
+ mode: "worktree",
3262
+ root: lease.root,
3263
+ branch: lease.branch
3264
+ };
3265
+ });
3266
+ const root = await this.#repositoryRoot(git, path);
3267
+ const head = await this.#run(git, [
3268
+ "symbolic-ref",
3269
+ "--quiet",
3270
+ "--short",
3271
+ "HEAD"
3272
+ ], root);
3273
+ const branch = head.exitCode === 0 ? head.stdout.trim() : "";
3274
+ if (branch.length === 0 || branch === base) throw new RepositorySetupError("nothing-to-clean", "The checkout is already on the base branch.");
3275
+ if ((await this.#run(git, [
3276
+ "switch",
3277
+ "--",
3278
+ base
3279
+ ], root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not switch to the base branch.");
3280
+ await this.#run(git, [
3281
+ "branch",
3282
+ "-D",
3283
+ "--",
3284
+ branch
3285
+ ], root).catch(() => void 0);
3286
+ await this.#run(git, ["pull", "--ff-only"], root, GIT_FETCH_TIMEOUT_MS).catch(() => void 0);
3287
+ return {
3288
+ mode: "checkout",
3289
+ root,
3290
+ branch
3291
+ };
3292
+ }
3231
3293
  bindLease(leaseId, sessionId) {
3232
3294
  if (leaseId.length === 0 || leaseId.length > 128 || sessionId.length === 0 || sessionId.length > 1024) return Promise.reject(new RepositorySetupError("invalid-lease", "The worktree lease is invalid."));
3233
3295
  return this.#serialize(async () => {
@@ -3336,8 +3398,8 @@ var RepositorySetupService = class {
3336
3398
  };
3337
3399
  }
3338
3400
  async #checkout(info, branch) {
3339
- if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
3340
3401
  if (info.current !== branch) {
3402
+ if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
3341
3403
  const git = await this.#git();
3342
3404
  const worktrees = await this.#run(git, [
3343
3405
  "worktree",
@@ -3360,7 +3422,7 @@ var RepositorySetupService = class {
3360
3422
  branch
3361
3423
  };
3362
3424
  }
3363
- async #createWorktree(root, baseBranch, baseRef, explicitBranchName, progress) {
3425
+ async #createWorktree(root, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress) {
3364
3426
  const git = await this.#git();
3365
3427
  progress("fetching");
3366
3428
  const fetched = await this.#run(git, [
@@ -3377,14 +3439,25 @@ var RepositorySetupService = class {
3377
3439
  const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
3378
3440
  progress("creating-worktree");
3379
3441
  await mkdir(this.#worktreeRoot, { recursive: true });
3380
- if ((await this.#run(git, [
3442
+ if (reuseExistingBranch) await this.#run(git, ["worktree", "prune"], root).catch(() => void 0);
3443
+ const created = await this.#run(git, reuseExistingBranch ? [
3444
+ "worktree",
3445
+ "add",
3446
+ "--",
3447
+ path,
3448
+ branch
3449
+ ] : [
3381
3450
  "worktree",
3382
3451
  "add",
3383
3452
  "-b",
3384
3453
  branch,
3385
3454
  path,
3386
3455
  baseRef
3387
- ], root)).exitCode !== 0) throw new RepositorySetupError("worktree-failed", "Git could not create the worktree.");
3456
+ ], root);
3457
+ if (created.exitCode !== 0) {
3458
+ const detail = created.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
3459
+ throw new RepositorySetupError("worktree-failed", detail === void 0 ? "Git could not create the worktree." : `Git could not create the worktree: ${detail}`);
3460
+ }
3388
3461
  const item = {
3389
3462
  id: randomUUID(),
3390
3463
  root,
@@ -3407,6 +3480,12 @@ var RepositorySetupService = class {
3407
3480
  "--",
3408
3481
  path
3409
3482
  ], root).catch(() => void 0);
3483
+ if (!reuseExistingBranch) await this.#run(git, [
3484
+ "branch",
3485
+ "-D",
3486
+ "--",
3487
+ branch
3488
+ ], root).catch(() => void 0);
3410
3489
  throw error;
3411
3490
  }
3412
3491
  return {
@@ -3920,24 +3999,24 @@ var RepositoryActionService = class {
3920
3999
  };
3921
4000
  //#endregion
3922
4001
  //#region src/repository-setup-routes.ts
3923
- const MAX_BODY_BYTES$2 = 16384;
3924
- function record$3(value) {
4002
+ const MAX_BODY_BYTES$4 = 16384;
4003
+ function record$7(value) {
3925
4004
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3926
4005
  }
3927
- async function readJson$2(req) {
4006
+ async function readJson$4(req) {
3928
4007
  const chunks = [];
3929
4008
  let size = 0;
3930
4009
  for await (const chunk of req) {
3931
4010
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3932
4011
  size += buffer.length;
3933
- if (size > MAX_BODY_BYTES$2) 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.");
3934
4013
  chunks.push(buffer);
3935
4014
  }
3936
- const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4015
+ const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3937
4016
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3938
4017
  return value;
3939
4018
  }
3940
- function string$1(input, key) {
4019
+ function string$2(input, key) {
3941
4020
  const value = input[key];
3942
4021
  if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
3943
4022
  return value;
@@ -3956,17 +4035,17 @@ function ndjsonHeaders(res) {
3956
4035
  });
3957
4036
  res.flushHeaders?.();
3958
4037
  }
3959
- function ndjson(res, value) {
4038
+ function ndjson$1(res, value) {
3960
4039
  res.write(`${JSON.stringify(value)}\n`);
3961
4040
  }
3962
4041
  async function streamSetup(res, service, input) {
3963
4042
  ndjsonHeaders(res);
3964
4043
  let ended = false;
3965
4044
  try {
3966
- ndjson(res, {
4045
+ ndjson$1(res, {
3967
4046
  type: "complete",
3968
- result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
3969
- if (!ended) ndjson(res, {
4047
+ result: await service.setup(string$2(input, "cwd"), string$2(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
4048
+ if (!ended) ndjson$1(res, {
3970
4049
  type: "progress",
3971
4050
  stage
3972
4051
  });
@@ -3974,7 +4053,7 @@ async function streamSetup(res, service, input) {
3974
4053
  });
3975
4054
  } catch (error) {
3976
4055
  const setupError = error instanceof RepositorySetupError ? error : void 0;
3977
- ndjson(res, {
4056
+ ndjson$1(res, {
3978
4057
  type: "error",
3979
4058
  code: setupError?.code ?? "repository-setup-unavailable",
3980
4059
  message: setupError?.message ?? "Repository setup is unavailable."
@@ -4001,15 +4080,20 @@ function registerRepositorySetupRoute(ctx, service) {
4001
4080
  }
4002
4081
  if (pathname === "/plugins/dsh-claude/repository/setup") {
4003
4082
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4004
- const input = await readJson$2(req);
4083
+ const input = await readJson$4(req);
4005
4084
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
4006
4085
  await streamSetup(res, service, input);
4007
4086
  return;
4008
4087
  }
4088
+ if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
4089
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4090
+ const input = await readJson$4(req);
4091
+ return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
4092
+ }
4009
4093
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
4010
4094
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4011
- const input = await readJson$2(req);
4012
- await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
4095
+ const input = await readJson$4(req);
4096
+ await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
4013
4097
  return json(res, 200, { ok: true });
4014
4098
  }
4015
4099
  return json(res, 404, { error: "not found" });
@@ -4026,8 +4110,8 @@ function registerRepositorySetupRoute(ctx, service) {
4026
4110
  }
4027
4111
  //#endregion
4028
4112
  //#region src/repository-action-routes.ts
4029
- const MAX_BODY_BYTES$1 = 16384;
4030
- const MAX_SESSION_ID_CHARS$2 = 1024;
4113
+ const MAX_BODY_BYTES$3 = 16384;
4114
+ const MAX_SESSION_ID_CHARS$3 = 1024;
4031
4115
  const ACTIONS = /* @__PURE__ */ new Set([
4032
4116
  "commit",
4033
4117
  "commit-push",
@@ -4036,28 +4120,28 @@ const ACTIONS = /* @__PURE__ */ new Set([
4036
4120
  "merge-pr",
4037
4121
  "update-branch"
4038
4122
  ]);
4039
- function record$2(value) {
4123
+ function record$6(value) {
4040
4124
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4041
4125
  }
4042
- async function readJson$1(req) {
4126
+ async function readJson$3(req) {
4043
4127
  const chunks = [];
4044
4128
  let size = 0;
4045
4129
  for await (const chunk of req) {
4046
4130
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4047
4131
  size += buffer.length;
4048
- if (size > MAX_BODY_BYTES$1) 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.");
4049
4133
  chunks.push(buffer);
4050
4134
  }
4051
- const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4135
+ const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4052
4136
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
4053
4137
  return value;
4054
4138
  }
4055
4139
  function sessionId$1(url) {
4056
4140
  const value = url.searchParams.get("sessionId");
4057
- 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.");
4058
4142
  return value;
4059
4143
  }
4060
- function string(input, key) {
4144
+ function string$1(input, key) {
4061
4145
  const value = input[key];
4062
4146
  if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
4063
4147
  return value;
@@ -4073,8 +4157,8 @@ function actionRequest(input) {
4073
4157
  if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
4074
4158
  return {
4075
4159
  action,
4076
- fingerprint: string(input, "fingerprint"),
4077
- message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string(input, "message"),
4160
+ fingerprint: string$1(input, "fingerprint"),
4161
+ message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string$1(input, "message"),
4078
4162
  includeUnstaged: input.includeUnstaged,
4079
4163
  ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
4080
4164
  ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
@@ -4103,12 +4187,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4103
4187
  }
4104
4188
  if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
4105
4189
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4106
- const input = await readJson$1(req);
4107
- return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
4190
+ const input = await readJson$3(req);
4191
+ return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
4108
4192
  }
4109
4193
  if (url.pathname === "/plugins/dsh-claude/repository/action") {
4110
4194
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4111
- return json(res, 200, await service.execute(cwd, actionRequest(await readJson$1(req))));
4195
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$3(req))));
4112
4196
  }
4113
4197
  return json(res, 404, { error: "not found" });
4114
4198
  } catch (error) {
@@ -4152,14 +4236,14 @@ async function collect(handle) {
4152
4236
  lossy: stdout?.lossy === true
4153
4237
  };
4154
4238
  }
4155
- function record$1(value) {
4239
+ function record$5(value) {
4156
4240
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4157
4241
  }
4158
4242
  function parseReviewComments(value) {
4159
4243
  if (!Array.isArray(value)) return [];
4160
4244
  const comments = [];
4161
4245
  for (const item of value) {
4162
- const input = record$1(item);
4246
+ const input = record$5(item);
4163
4247
  if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4164
4248
  const body = typeof input.body === "string" ? input.body.trim() : "";
4165
4249
  const path = typeof input.path === "string" ? input.path : "";
@@ -4171,7 +4255,7 @@ function parseReviewComments(value) {
4171
4255
  path,
4172
4256
  ...line === void 0 ? {} : { line },
4173
4257
  side: input.side === "LEFT" ? "old" : "new",
4174
- author: typeof record$1(input.user)?.login === "string" ? String(record$1(input.user)?.login) : "unknown",
4258
+ author: typeof record$5(input.user)?.login === "string" ? String(record$5(input.user)?.login) : "unknown",
4175
4259
  body: body.slice(0, MAX_COMMENT_CHARS),
4176
4260
  url
4177
4261
  });
@@ -4188,7 +4272,7 @@ function parseFailingChecks(value) {
4188
4272
  if (!Array.isArray(value)) return [];
4189
4273
  const failing = [];
4190
4274
  for (const item of value) {
4191
- const input = record$1(item);
4275
+ const input = record$5(item);
4192
4276
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4193
4277
  failing.push({
4194
4278
  name: input.name,
@@ -4300,10 +4384,10 @@ var PullRequestFeedbackService = class {
4300
4384
  };
4301
4385
  //#endregion
4302
4386
  //#region src/pr-feedback-routes.ts
4303
- const MAX_SESSION_ID_CHARS$1 = 1024;
4387
+ const MAX_SESSION_ID_CHARS$2 = 1024;
4304
4388
  function sessionId(url) {
4305
4389
  const value = url.searchParams.get("sessionId");
4306
- 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.");
4307
4391
  return value;
4308
4392
  }
4309
4393
  function pullNumber(url) {
@@ -4339,6 +4423,667 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
4339
4423
  }), "dsh-claude: pull request feedback route");
4340
4424
  }
4341
4425
  //#endregion
4426
+ //#region src/repository-status-routes.ts
4427
+ const MAX_PATH_CHARS$1 = 4096;
4428
+ /** Read-only repository status for an arbitrary directory (the overview panel
4429
+ * aggregates every Claude session's checkout through this). */
4430
+ function registerRepositoryStatusRoute(ctx, service) {
4431
+ ctx.effect(() => ctx.webServer.register({
4432
+ kind: "exact",
4433
+ path: CLAUDE_REPOSITORY_STATUS_PATH,
4434
+ handler: async (req, res) => {
4435
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4436
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4437
+ const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
4438
+ if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return json(res, 400, {
4439
+ error: "invalid-request",
4440
+ message: "The cwd query parameter is invalid."
4441
+ });
4442
+ try {
4443
+ return json(res, 200, await service.inspect(cwd));
4444
+ } catch {
4445
+ return json(res, 500, {
4446
+ error: "repository-status-unavailable",
4447
+ message: "Repository status is unavailable."
4448
+ });
4449
+ }
4450
+ }
4451
+ }), "dsh-claude: repository status route");
4452
+ }
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
4493
+ //#region src/jira.ts
4494
+ const REQUEST_TIMEOUT_MS = 15e3;
4495
+ const MAX_RESULTS = 20;
4496
+ const MAX_QUERY_CHARS = 200;
4497
+ const MAX_STORE_BYTES = 65536;
4498
+ var JiraError = class extends Error {
4499
+ code;
4500
+ constructor(code, message) {
4501
+ super(message);
4502
+ this.name = "JiraError";
4503
+ this.code = code;
4504
+ }
4505
+ };
4506
+ function record$4(value) {
4507
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4508
+ }
4509
+ /** Jira Cloud sites are origins; Data Center may carry a context path. */
4510
+ function normalizeSiteUrl(value) {
4511
+ let url;
4512
+ try {
4513
+ url = new URL(value.trim());
4514
+ } catch {
4515
+ throw new JiraError("invalid-site", "The Jira site URL is invalid.");
4516
+ }
4517
+ if (url.protocol !== "https:") throw new JiraError("invalid-site", "The Jira site URL must use https.");
4518
+ return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
4519
+ }
4520
+ function ticketKeyOf(query) {
4521
+ const match = /^([A-Za-z][A-Za-z0-9_]*)-(\d+)$/u.exec(query.trim());
4522
+ return match === null ? void 0 : `${match[1].toUpperCase()}-${match[2]}`;
4523
+ }
4524
+ function buildJql(query) {
4525
+ const trimmed = query.trim().slice(0, MAX_QUERY_CHARS);
4526
+ if (trimmed.length === 0) return "statusCategory != Done ORDER BY updated DESC";
4527
+ const key = ticketKeyOf(trimmed);
4528
+ if (key !== void 0) return `key = "${key}"`;
4529
+ return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
4530
+ }
4531
+ function parseTickets(value, siteUrl) {
4532
+ const issues = record$4(value)?.issues;
4533
+ if (!Array.isArray(issues)) return [];
4534
+ const tickets = [];
4535
+ for (const item of issues) {
4536
+ const issue = record$4(item);
4537
+ const fields = record$4(issue?.fields);
4538
+ if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
4539
+ const status = record$4(fields.status)?.name;
4540
+ const type = record$4(fields.issuetype)?.name;
4541
+ tickets.push({
4542
+ key: issue.key,
4543
+ summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
4544
+ url: `${siteUrl}/browse/${issue.key}`,
4545
+ ...typeof status === "string" ? { status } : {},
4546
+ ...typeof type === "string" ? { type } : {}
4547
+ });
4548
+ if (tickets.length >= MAX_RESULTS) break;
4549
+ }
4550
+ return tickets;
4551
+ }
4552
+ function statusOf(store) {
4553
+ if (store === void 0) return { connected: false };
4554
+ return {
4555
+ connected: true,
4556
+ siteUrl: store.siteUrl,
4557
+ email: store.email,
4558
+ ...store.displayName === void 0 ? {} : { displayName: store.displayName }
4559
+ };
4560
+ }
4561
+ /** Jira Cloud access through the user's API token (Basic auth). */
4562
+ var JiraService = class {
4563
+ #storePath;
4564
+ #fetch;
4565
+ constructor(options = {}) {
4566
+ this.#storePath = options.storePath ?? dshHomePath("plugins", "dsh-claude", "jira.json");
4567
+ this.#fetch = options.fetch ?? globalThis.fetch;
4568
+ }
4569
+ async status() {
4570
+ return statusOf(await this.#read());
4571
+ }
4572
+ async connect(input) {
4573
+ const siteUrl = normalizeSiteUrl(input.siteUrl);
4574
+ const email = input.email.trim();
4575
+ const apiToken = input.apiToken.trim();
4576
+ if (email.length === 0 || email.length > 320 || apiToken.length === 0 || apiToken.length > 4096) throw new JiraError("invalid-credentials", "An account email and API token are required.");
4577
+ const connection = {
4578
+ siteUrl,
4579
+ email,
4580
+ apiToken
4581
+ };
4582
+ const myself = record$4(await this.#json(connection, "/rest/api/3/myself"));
4583
+ const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
4584
+ const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
4585
+ const store = {
4586
+ ...connection,
4587
+ ...displayName === void 0 ? {} : { displayName },
4588
+ ...accountId === void 0 ? {} : { accountId }
4589
+ };
4590
+ await this.#write(store);
4591
+ return statusOf(store);
4592
+ }
4593
+ /** Assign a ticket to the connected account (used once a ticket's worktree exists). */
4594
+ async assignToMe(key) {
4595
+ const store = await this.#read();
4596
+ if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
4597
+ const ticket = ticketKeyOf(key);
4598
+ if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
4599
+ let accountId = store.accountId;
4600
+ if (accountId === void 0) {
4601
+ const myself = record$4(await this.#json(store, "/rest/api/3/myself"));
4602
+ if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
4603
+ accountId = myself.accountId;
4604
+ await this.#write({
4605
+ ...store,
4606
+ accountId
4607
+ });
4608
+ }
4609
+ await this.#json(store, `/rest/api/3/issue/${ticket}/assignee`, {
4610
+ method: "PUT",
4611
+ body: { accountId }
4612
+ });
4613
+ }
4614
+ async disconnect() {
4615
+ await rm(this.#storePath, { force: true });
4616
+ }
4617
+ async search(query) {
4618
+ const store = await this.#read();
4619
+ if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
4620
+ const params = new URLSearchParams({
4621
+ jql: buildJql(query),
4622
+ maxResults: String(MAX_RESULTS),
4623
+ fields: "summary,status,issuetype"
4624
+ });
4625
+ let body;
4626
+ try {
4627
+ body = await this.#json(store, `/rest/api/3/search/jql?${params.toString()}`);
4628
+ } catch (error) {
4629
+ if (!(error instanceof JiraError && error.code === "not-found")) throw error;
4630
+ body = await this.#json(store, `/rest/api/3/search?${params.toString()}`);
4631
+ }
4632
+ return parseTickets(body, store.siteUrl);
4633
+ }
4634
+ async #json(connection, path, init = {}) {
4635
+ let response;
4636
+ try {
4637
+ response = await this.#fetch(`${connection.siteUrl}${path}`, {
4638
+ method: init.method ?? "GET",
4639
+ headers: {
4640
+ accept: "application/json",
4641
+ authorization: `Basic ${Buffer.from(`${connection.email}:${connection.apiToken}`).toString("base64")}`,
4642
+ ...init.body === void 0 ? {} : { "content-type": "application/json" }
4643
+ },
4644
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
4645
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
4646
+ });
4647
+ } catch {
4648
+ throw new JiraError("unreachable", "The Jira site could not be reached.");
4649
+ }
4650
+ if (response.status === 401 || response.status === 403) throw new JiraError("unauthorized", "Jira rejected the credentials.");
4651
+ if (response.status === 404) throw new JiraError("not-found", "The Jira endpoint was not found.");
4652
+ if (!response.ok) throw new JiraError("jira-failed", `Jira responded with HTTP ${response.status}.`);
4653
+ if (response.status === 204) return void 0;
4654
+ try {
4655
+ return await response.json();
4656
+ } catch {
4657
+ throw new JiraError("jira-failed", "Jira returned an invalid response.");
4658
+ }
4659
+ }
4660
+ async #read() {
4661
+ let text;
4662
+ try {
4663
+ text = await readFile(this.#storePath, "utf8");
4664
+ } catch (error) {
4665
+ if (error.code === "ENOENT") return void 0;
4666
+ throw error;
4667
+ }
4668
+ if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
4669
+ const input = record$4(JSON.parse(text));
4670
+ if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
4671
+ return {
4672
+ siteUrl: input.siteUrl,
4673
+ email: input.email,
4674
+ apiToken: input.apiToken,
4675
+ ...typeof input.displayName === "string" ? { displayName: input.displayName } : {},
4676
+ ...typeof input.accountId === "string" ? { accountId: input.accountId } : {}
4677
+ };
4678
+ }
4679
+ async #write(store) {
4680
+ await mkdir(dirname(this.#storePath), {
4681
+ recursive: true,
4682
+ mode: 448
4683
+ });
4684
+ const temporary = `${this.#storePath}.${process.pid}.${randomUUID()}.tmp`;
4685
+ try {
4686
+ await writeFile(temporary, `${JSON.stringify(store, null, 2)}\n`, {
4687
+ encoding: "utf8",
4688
+ mode: 384,
4689
+ flag: "wx"
4690
+ });
4691
+ await chmod(temporary, 384);
4692
+ await rename(temporary, this.#storePath);
4693
+ } finally {
4694
+ await rm(temporary, { force: true }).catch(() => void 0);
4695
+ }
4696
+ }
4697
+ };
4698
+ //#endregion
4699
+ //#region src/jira-routes.ts
4700
+ const MAX_BODY_BYTES$2 = 8192;
4701
+ function record$3(value) {
4702
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4703
+ }
4704
+ async function readJson$2(req) {
4705
+ const chunks = [];
4706
+ let size = 0;
4707
+ for await (const chunk of req) {
4708
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4709
+ size += buffer.length;
4710
+ if (size > MAX_BODY_BYTES$2) throw new JiraError("body-too-large", "The request body is too large.");
4711
+ chunks.push(buffer);
4712
+ }
4713
+ const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4714
+ if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
4715
+ return value;
4716
+ }
4717
+ function string(input, key) {
4718
+ const value = input[key];
4719
+ if (typeof value !== "string") throw new JiraError("invalid-request", `The ${key} field is required.`);
4720
+ return value;
4721
+ }
4722
+ function registerJiraRoute(ctx, service) {
4723
+ ctx.effect(() => ctx.webServer.register({
4724
+ kind: "prefix",
4725
+ path: CLAUDE_JIRA_PATH,
4726
+ handler: async (req, res) => {
4727
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4728
+ const url = new URL(req.url ?? "/", "http://localhost");
4729
+ try {
4730
+ if (url.pathname === `/plugins/dsh-claude/jira/status`) {
4731
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4732
+ return json(res, 200, await service.status());
4733
+ }
4734
+ if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
4735
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4736
+ const input = await readJson$2(req);
4737
+ return json(res, 200, await service.connect({
4738
+ siteUrl: string(input, "siteUrl"),
4739
+ email: string(input, "email"),
4740
+ apiToken: string(input, "apiToken")
4741
+ }));
4742
+ }
4743
+ if (url.pathname === `/plugins/dsh-claude/jira/disconnect`) {
4744
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4745
+ await service.disconnect();
4746
+ return json(res, 200, { connected: false });
4747
+ }
4748
+ if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
4749
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4750
+ const input = await readJson$2(req);
4751
+ await service.assignToMe(string(input, "key"));
4752
+ return json(res, 200, { assigned: true });
4753
+ }
4754
+ if (url.pathname === `/plugins/dsh-claude/jira/search`) {
4755
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4756
+ return json(res, 200, { tickets: await service.search(url.searchParams.get("query") ?? "") });
4757
+ }
4758
+ return json(res, 404, { error: "not found" });
4759
+ } catch (error) {
4760
+ if (error instanceof JiraError) return json(res, 409, {
4761
+ error: error.code,
4762
+ message: error.message
4763
+ });
4764
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
4765
+ return json(res, 500, {
4766
+ error: "jira-unavailable",
4767
+ message: "Jira is unavailable."
4768
+ });
4769
+ }
4770
+ }
4771
+ }), "dsh-claude: jira route");
4772
+ }
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
4342
5087
  //#region src/review-comment-routes.ts
4343
5088
  const MAX_BODY_BYTES = 16384;
4344
5089
  const MAX_SESSION_ID_CHARS = 1024;
@@ -4378,6 +5123,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
4378
5123
  return json(res, 200, { comment: store.add(sessionId, {
4379
5124
  path: input.path,
4380
5125
  line: input.line,
5126
+ startLine: input.startLine,
4381
5127
  side: input.side,
4382
5128
  text: input.text
4383
5129
  }) });
@@ -5196,6 +5942,9 @@ async function apply(ctx, config) {
5196
5942
  onUpdated: applyLimitOverrides
5197
5943
  });
5198
5944
  registerRepositorySetupRoute(webCtx, repositorySetup);
5945
+ registerRepositoryStatusRoute(webCtx, repositoryStatus);
5946
+ registerRepositoryFileRoute(webCtx, repositoryStatus);
5947
+ registerJiraRoute(webCtx, new JiraService());
5199
5948
  const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
5200
5949
  const cwdForClaudeSession = (sessionId) => {
5201
5950
  const agent = webCtx.agents.get(sessionId);
@@ -5204,6 +5953,13 @@ async function apply(ctx, config) {
5204
5953
  };
5205
5954
  registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
5206
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
+ });
5207
5963
  const ownsClaudeSession = (sessionId) => {
5208
5964
  const agent = webCtx.agents.get(sessionId);
5209
5965
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";