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