@norman-else/dsh-claude 0.1.6 → 0.1.8
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 +191 -190
- 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 +26 -0
- package/lib/client.js +329 -52
- package/lib/client.js.map +1 -1
- package/lib/{events-DP0Ov_Re.mjs → events-BVQkPmjg.mjs} +4 -2
- package/lib/events-BVQkPmjg.mjs.map +1 -0
- package/lib/index.d.mts +3 -1
- package/lib/index.mjs +398 -16
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-YMR2WMjn.mjs → presenters-CQ9dBVyY.mjs} +2 -2
- package/lib/presenters-CQ9dBVyY.mjs.map +1 -0
- package/lib/{preset-installer-BKw-SdOm.mjs → preset-installer-DUmmDvBm.mjs} +4 -4
- package/lib/preset-installer-DUmmDvBm.mjs.map +1 -0
- package/lib/preset-route.mjs +2 -2
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +129 -127
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
- package/lib/events-DP0Ov_Re.mjs.map +0 -1
- package/lib/presenters-YMR2WMjn.mjs.map +0 -1
- package/lib/preset-installer-BKw-SdOm.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { a as latestClaudeTasks, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_PROJECTION_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
|
|
2
|
-
import { a as ClaudeCommandBridge, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { _ as CLAUDE_UPDATE_CHECK_PATH, a as latestClaudeTasks, b as TASK_TOOL_NAMES, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_PROJECTION_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_UPDATE_PATH } from "./events-BVQkPmjg.mjs";
|
|
2
|
+
import { a as ClaudeCommandBridge, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-CQ9dBVyY.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DUmmDvBm.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
7
|
+
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
//#region src/sidecar.ts
|
|
14
15
|
const SIDECAR_SCHEMA_VERSION = 1;
|
|
15
16
|
const MAX_ACTIVITIES = 1e4;
|
|
@@ -45,6 +46,7 @@ const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
|
|
|
45
46
|
"tool-call",
|
|
46
47
|
"tool-result",
|
|
47
48
|
"permission",
|
|
49
|
+
"question",
|
|
48
50
|
"subagent",
|
|
49
51
|
"usage",
|
|
50
52
|
"warning",
|
|
@@ -336,8 +338,14 @@ function mapApprovalOutcome(outcome, input, toolUseID) {
|
|
|
336
338
|
decisionClassification: "user_reject"
|
|
337
339
|
};
|
|
338
340
|
}
|
|
339
|
-
function createPermissionBridge(approval, activeContext) {
|
|
341
|
+
function createPermissionBridge(approval, activeContext, userQuestion) {
|
|
340
342
|
return async (toolName, input, options) => {
|
|
343
|
+
if (toolName === "AskUserQuestion") return userQuestion === void 0 ? {
|
|
344
|
+
behavior: "deny",
|
|
345
|
+
message: "DeepSeek Harness user questions are unavailable; the question was cancelled.",
|
|
346
|
+
toolUseID: options.toolUseID,
|
|
347
|
+
decisionClassification: "user_reject"
|
|
348
|
+
} : userQuestion(input, options);
|
|
341
349
|
const active = activeContext();
|
|
342
350
|
if (active === void 0) return {
|
|
343
351
|
behavior: "deny",
|
|
@@ -401,6 +409,120 @@ function createPermissionBridge(approval, activeContext) {
|
|
|
401
409
|
};
|
|
402
410
|
}
|
|
403
411
|
//#endregion
|
|
412
|
+
//#region src/user-question.ts
|
|
413
|
+
const FAILURE_MESSAGE = "DeepSeek Harness could not collect an answer; the question was cancelled.";
|
|
414
|
+
const INVALID_MESSAGE = "Claude Code sent an invalid user-question request; the question was cancelled.";
|
|
415
|
+
function failureMessage(error) {
|
|
416
|
+
const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
417
|
+
return typeof code === "string" && /^[A-Z][A-Z0-9_]*$/.test(code) ? `${FAILURE_MESSAGE} (${code})` : FAILURE_MESSAGE;
|
|
418
|
+
}
|
|
419
|
+
function deny(message, toolUseID) {
|
|
420
|
+
return {
|
|
421
|
+
behavior: "deny",
|
|
422
|
+
message,
|
|
423
|
+
toolUseID,
|
|
424
|
+
decisionClassification: "user_reject"
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function optionalText(value) {
|
|
428
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
429
|
+
}
|
|
430
|
+
function parseQuestions(input, toolUseID) {
|
|
431
|
+
if (!Array.isArray(input.questions) || input.questions.length === 0 || input.questions.length > 20) return void 0;
|
|
432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
433
|
+
const questions = [];
|
|
434
|
+
for (const [index, value] of input.questions.entries()) {
|
|
435
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
436
|
+
const item = value;
|
|
437
|
+
if (typeof item.question !== "string" || item.question.length === 0 || seen.has(item.question)) return void 0;
|
|
438
|
+
seen.add(item.question);
|
|
439
|
+
if (item.options !== void 0 && !Array.isArray(item.options)) return void 0;
|
|
440
|
+
const options = (item.options ?? []).map((option) => {
|
|
441
|
+
if (option === null || typeof option !== "object" || Array.isArray(option)) return void 0;
|
|
442
|
+
const record = option;
|
|
443
|
+
if (typeof record.label !== "string" || record.label.length === 0) return void 0;
|
|
444
|
+
const description = optionalText(record.description);
|
|
445
|
+
return {
|
|
446
|
+
label: record.label,
|
|
447
|
+
...description === void 0 ? {} : { description }
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
if (options.some((option) => option === void 0)) return void 0;
|
|
451
|
+
const header = optionalText(item.header);
|
|
452
|
+
questions.push({
|
|
453
|
+
id: `${toolUseID}:${index}`,
|
|
454
|
+
question: item.question,
|
|
455
|
+
...header === void 0 ? {} : { header },
|
|
456
|
+
options,
|
|
457
|
+
multiSelect: item.multiSelect === true
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return questions;
|
|
461
|
+
}
|
|
462
|
+
function answerText(answer, multiSelect) {
|
|
463
|
+
if (answer === void 0) return "";
|
|
464
|
+
const custom = optionalText(answer.custom);
|
|
465
|
+
if (!multiSelect && custom !== void 0) return custom;
|
|
466
|
+
return [...answer.selected, ...custom === void 0 ? [] : [custom]].join(", ");
|
|
467
|
+
}
|
|
468
|
+
function createUserQuestionBridge(userQuestions, activeContext) {
|
|
469
|
+
return async (input, options) => {
|
|
470
|
+
const active = activeContext();
|
|
471
|
+
if (active === void 0) return deny("No active DeepSeek Harness turn owns this Claude Code question.", options.toolUseID);
|
|
472
|
+
const questions = parseQuestions(input, options.toolUseID);
|
|
473
|
+
if (questions === void 0) return deny(INVALID_MESSAGE, options.toolUseID);
|
|
474
|
+
active.markActivity?.();
|
|
475
|
+
try {
|
|
476
|
+
await active.appendActivity({
|
|
477
|
+
kind: "question",
|
|
478
|
+
phase: "started",
|
|
479
|
+
toolUseId: options.toolUseID,
|
|
480
|
+
toolName: "AskUserQuestion",
|
|
481
|
+
title: "Claude asked a question",
|
|
482
|
+
summary: questions.length === 1 ? questions[0].question : `Claude asked ${questions.length} questions`
|
|
483
|
+
});
|
|
484
|
+
const response = await userQuestions.ask({
|
|
485
|
+
questions,
|
|
486
|
+
agent: active.agent,
|
|
487
|
+
signal: options.signal
|
|
488
|
+
});
|
|
489
|
+
const answersById = new Map(response.answers.map((answer) => [answer.id, answer]));
|
|
490
|
+
const answers = Object.fromEntries(questions.map((question) => [question.question, answerText(answersById.get(question.id), question.multiSelect === true)]));
|
|
491
|
+
await active.appendActivity({
|
|
492
|
+
kind: "question",
|
|
493
|
+
phase: "completed",
|
|
494
|
+
toolUseId: options.toolUseID,
|
|
495
|
+
toolName: "AskUserQuestion",
|
|
496
|
+
title: "Claude asked a question",
|
|
497
|
+
summary: "Answered in DeepSeek Harness"
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
behavior: "allow",
|
|
501
|
+
updatedInput: {
|
|
502
|
+
...input,
|
|
503
|
+
answers
|
|
504
|
+
},
|
|
505
|
+
toolUseID: options.toolUseID,
|
|
506
|
+
decisionClassification: "user_temporary"
|
|
507
|
+
};
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const message = failureMessage(error);
|
|
510
|
+
try {
|
|
511
|
+
await active.appendActivity({
|
|
512
|
+
kind: "question",
|
|
513
|
+
phase: "failed",
|
|
514
|
+
toolUseId: options.toolUseID,
|
|
515
|
+
toolName: "AskUserQuestion",
|
|
516
|
+
title: "Claude asked a question",
|
|
517
|
+
summary: message,
|
|
518
|
+
isError: true
|
|
519
|
+
});
|
|
520
|
+
} catch {}
|
|
521
|
+
return deny(message, options.toolUseID);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
404
526
|
//#region src/sdk-messages.ts
|
|
405
527
|
function record(value) {
|
|
406
528
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
@@ -908,6 +1030,7 @@ var ClaudeSupervisor = class {
|
|
|
908
1030
|
#entries = /* @__PURE__ */ new Map();
|
|
909
1031
|
#runtime;
|
|
910
1032
|
#approval;
|
|
1033
|
+
#userQuestions;
|
|
911
1034
|
#config;
|
|
912
1035
|
#queryFactory;
|
|
913
1036
|
#runDetached;
|
|
@@ -919,6 +1042,7 @@ var ClaudeSupervisor = class {
|
|
|
919
1042
|
constructor(dependencies) {
|
|
920
1043
|
this.#runtime = dependencies.runtime;
|
|
921
1044
|
this.#approval = dependencies.approval;
|
|
1045
|
+
this.#userQuestions = dependencies.userQuestions;
|
|
922
1046
|
this.#config = dependencies.config;
|
|
923
1047
|
this.#queryFactory = dependencies.queryFactory ?? ((params) => query(params));
|
|
924
1048
|
this.#runDetached = dependencies.runDetached ?? ((operation) => operation());
|
|
@@ -1154,7 +1278,7 @@ var ClaudeSupervisor = class {
|
|
|
1154
1278
|
taskSnapshotAt: 0,
|
|
1155
1279
|
taskSnapshotTimer: void 0
|
|
1156
1280
|
};
|
|
1157
|
-
const
|
|
1281
|
+
const activeInteraction = () => {
|
|
1158
1282
|
const active = entry.active;
|
|
1159
1283
|
return active === void 0 ? void 0 : {
|
|
1160
1284
|
agent: active.agent,
|
|
@@ -1171,7 +1295,9 @@ var ClaudeSupervisor = class {
|
|
|
1171
1295
|
},
|
|
1172
1296
|
appendActivity: (activity) => this.#appendActivity(active, activity)
|
|
1173
1297
|
};
|
|
1174
|
-
}
|
|
1298
|
+
};
|
|
1299
|
+
const userQuestion = createUserQuestionBridge(this.#userQuestions, activeInteraction);
|
|
1300
|
+
const canUseTool = createPermissionBridge(this.#approval, activeInteraction, userQuestion);
|
|
1175
1301
|
const options = {
|
|
1176
1302
|
pathToClaudeCodeExecutable: this.#config.executablePath,
|
|
1177
1303
|
cwd,
|
|
@@ -1939,7 +2065,7 @@ function json(res, status, value) {
|
|
|
1939
2065
|
//#region src/doctor-routes.ts
|
|
1940
2066
|
const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
|
|
1941
2067
|
const claudeBridgeDiagnostics = /* @__PURE__ */ new WeakMap();
|
|
1942
|
-
function safeMessage(error) {
|
|
2068
|
+
function safeMessage$1(error) {
|
|
1943
2069
|
return redactText(error instanceof Error ? error.message : String(error), 1e3);
|
|
1944
2070
|
}
|
|
1945
2071
|
/** Live command-bridge diagnostics: which agents exist, their presets, and how
|
|
@@ -1955,14 +2081,14 @@ function commandDiagnostics(ctx) {
|
|
|
1955
2081
|
const preset = ctx.agentPresets.composedPreset(agent.ctx);
|
|
1956
2082
|
if (preset !== void 0) info.preset = preset;
|
|
1957
2083
|
} catch (error) {
|
|
1958
|
-
info.error = safeMessage(error);
|
|
2084
|
+
info.error = safeMessage$1(error);
|
|
1959
2085
|
}
|
|
1960
2086
|
try {
|
|
1961
2087
|
const list = ctx.commands.list(agent);
|
|
1962
2088
|
info.commandCount = list.length;
|
|
1963
2089
|
info.sample = list.slice(0, 10).map((command) => command.name);
|
|
1964
2090
|
} catch (error) {
|
|
1965
|
-
info.error = info.error === void 0 ? safeMessage(error) : `${info.error}; ${safeMessage(error)}`;
|
|
2091
|
+
info.error = info.error === void 0 ? safeMessage$1(error) : `${info.error}; ${safeMessage$1(error)}`;
|
|
1966
2092
|
}
|
|
1967
2093
|
const bridge = claudeBridgeDiagnostics.get(agent);
|
|
1968
2094
|
if (bridge !== void 0) info.bridge = bridge;
|
|
@@ -1970,7 +2096,7 @@ function commandDiagnostics(ctx) {
|
|
|
1970
2096
|
})
|
|
1971
2097
|
};
|
|
1972
2098
|
} catch (error) {
|
|
1973
|
-
return { error: safeMessage(error) };
|
|
2099
|
+
return { error: safeMessage$1(error) };
|
|
1974
2100
|
}
|
|
1975
2101
|
}
|
|
1976
2102
|
function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
|
|
@@ -1994,7 +2120,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
1994
2120
|
version: { status: "not-run" },
|
|
1995
2121
|
authentication: { status: "not-run" },
|
|
1996
2122
|
handshake: "not-run",
|
|
1997
|
-
message: safeMessage(resolutionError),
|
|
2123
|
+
message: safeMessage$1(resolutionError),
|
|
1998
2124
|
limits: {
|
|
1999
2125
|
idleTimeoutMs: config.idleTimeoutMs,
|
|
2000
2126
|
maxProcesses: config.maxProcesses
|
|
@@ -2024,7 +2150,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2024
2150
|
commandBridge: commandDiagnostics(ctx)
|
|
2025
2151
|
});
|
|
2026
2152
|
} catch (error) {
|
|
2027
|
-
json(res, 500, { error: safeMessage(error) });
|
|
2153
|
+
json(res, 500, { error: safeMessage$1(error) });
|
|
2028
2154
|
}
|
|
2029
2155
|
}
|
|
2030
2156
|
}), "dsh-claude: Doctor route");
|
|
@@ -2073,6 +2199,259 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession) {
|
|
|
2073
2199
|
}), "dsh-claude: sidecar projection route");
|
|
2074
2200
|
}
|
|
2075
2201
|
//#endregion
|
|
2202
|
+
//#region src/update-routes.ts
|
|
2203
|
+
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
2204
|
+
const UPDATE_TIMEOUT_MS = 3e4;
|
|
2205
|
+
const CHECK_TIMEOUT_MS = 1e4;
|
|
2206
|
+
const MAX_MANIFEST_BYTES = 262144;
|
|
2207
|
+
const MAX_UPDATE_OUTPUT_BYTES = 32768;
|
|
2208
|
+
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2209
|
+
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
2210
|
+
function safeMessage(error) {
|
|
2211
|
+
return redactText(error instanceof Error ? error.message : String(error), 500);
|
|
2212
|
+
}
|
|
2213
|
+
async function readManifest(path) {
|
|
2214
|
+
const text = await readFile(path, "utf8");
|
|
2215
|
+
if (Buffer.byteLength(text) > MAX_MANIFEST_BYTES) throw new Error("package manifest is too large");
|
|
2216
|
+
const value = JSON.parse(text);
|
|
2217
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("package manifest is invalid");
|
|
2218
|
+
return value;
|
|
2219
|
+
}
|
|
2220
|
+
function dependencySpec(manifest) {
|
|
2221
|
+
if (typeof manifest.dependencies !== "object" || manifest.dependencies === null || Array.isArray(manifest.dependencies)) return void 0;
|
|
2222
|
+
const value = manifest.dependencies[PLUGIN_PACKAGE_NAME];
|
|
2223
|
+
return typeof value === "string" && value.length <= 2e3 ? value : void 0;
|
|
2224
|
+
}
|
|
2225
|
+
function classifyInstallSpec(spec) {
|
|
2226
|
+
if (/^(?:link|file|workspace):/i.test(spec)) return "link";
|
|
2227
|
+
if (/^(?:git(?:\+[^:]+)?:|github:|https?:|npm:)/i.test(spec) || /\.git(?:#|$)/i.test(spec)) return "unsupported";
|
|
2228
|
+
return /^(?:\^|~|>=?|<=?|=)?\s*v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\s*\|\|\s*(?:\^|~|>=?|<=?|=)?\s*v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)*$/.test(spec.trim()) ? "registry" : "unsupported";
|
|
2229
|
+
}
|
|
2230
|
+
async function samePath(left, right) {
|
|
2231
|
+
try {
|
|
2232
|
+
const [a, b] = await Promise.all([realpath(left), realpath(right)]);
|
|
2233
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2234
|
+
} catch {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function linkedTarget(profileDir, spec) {
|
|
2239
|
+
const match = /^(?:link|file):(.*)$/i.exec(spec);
|
|
2240
|
+
return match?.[1] === void 0 ? void 0 : resolve(profileDir, match[1]);
|
|
2241
|
+
}
|
|
2242
|
+
async function discoverInstallation(dshHome, packageDir) {
|
|
2243
|
+
const profilesDir = join(dshHome, "profiles");
|
|
2244
|
+
const matches = [];
|
|
2245
|
+
let profiles;
|
|
2246
|
+
try {
|
|
2247
|
+
profiles = await opendir(profilesDir);
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
if (error.code === "ENOENT") return void 0;
|
|
2250
|
+
throw error;
|
|
2251
|
+
}
|
|
2252
|
+
for await (const entry of profiles) {
|
|
2253
|
+
if (!entry.isDirectory() || !PROFILE_NAME.test(entry.name)) continue;
|
|
2254
|
+
const profileDir = join(profilesDir, entry.name);
|
|
2255
|
+
let manifest;
|
|
2256
|
+
try {
|
|
2257
|
+
manifest = await readManifest(join(profileDir, "package.json"));
|
|
2258
|
+
} catch {
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
const spec = dependencySpec(manifest);
|
|
2262
|
+
if (spec === void 0) continue;
|
|
2263
|
+
const source = classifyInstallSpec(spec);
|
|
2264
|
+
if (source === "link" ? linkedTarget(profileDir, spec) !== void 0 && await samePath(linkedTarget(profileDir, spec), packageDir) : await samePath(join(profileDir, "node_modules", ..."@norman-else/dsh-claude".split("/")), packageDir)) matches.push({
|
|
2265
|
+
profile: entry.name,
|
|
2266
|
+
profileDir,
|
|
2267
|
+
source,
|
|
2268
|
+
spec
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2272
|
+
}
|
|
2273
|
+
function parseVersion(version) {
|
|
2274
|
+
const match = SEMVER.exec(version);
|
|
2275
|
+
if (match === null) throw new Error("package version is not valid semver");
|
|
2276
|
+
return match;
|
|
2277
|
+
}
|
|
2278
|
+
function compareVersions(left, right) {
|
|
2279
|
+
const a = parseVersion(left);
|
|
2280
|
+
const b = parseVersion(right);
|
|
2281
|
+
for (const index of [
|
|
2282
|
+
1,
|
|
2283
|
+
2,
|
|
2284
|
+
3
|
|
2285
|
+
]) {
|
|
2286
|
+
const difference = Number(a[index]) - Number(b[index]);
|
|
2287
|
+
if (difference !== 0) return Math.sign(difference);
|
|
2288
|
+
}
|
|
2289
|
+
const aPre = a[4];
|
|
2290
|
+
const bPre = b[4];
|
|
2291
|
+
if (aPre === void 0) return bPre === void 0 ? 0 : 1;
|
|
2292
|
+
if (bPre === void 0) return -1;
|
|
2293
|
+
return aPre.localeCompare(bPre, "en", { numeric: true });
|
|
2294
|
+
}
|
|
2295
|
+
async function registryLatest(signal) {
|
|
2296
|
+
const response = await fetch("https://registry.npmjs.org/%40norman-else%2Fdsh-claude", {
|
|
2297
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
2298
|
+
signal
|
|
2299
|
+
});
|
|
2300
|
+
if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
2301
|
+
const latest = (await response.json())["dist-tags"]?.latest;
|
|
2302
|
+
if (typeof latest !== "string") throw new Error("npm registry response has no latest version");
|
|
2303
|
+
parseVersion(latest);
|
|
2304
|
+
return latest;
|
|
2305
|
+
}
|
|
2306
|
+
async function resolvePackageContextDir(configured) {
|
|
2307
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
2308
|
+
const candidates = configured === void 0 ? [moduleDir, dirname(moduleDir)] : [configured];
|
|
2309
|
+
for (const packageDir of candidates) try {
|
|
2310
|
+
const manifest = await readManifest(join(packageDir, "package.json"));
|
|
2311
|
+
if (manifest.name === "@norman-else/dsh-claude") return {
|
|
2312
|
+
packageDir,
|
|
2313
|
+
manifest
|
|
2314
|
+
};
|
|
2315
|
+
} catch {}
|
|
2316
|
+
throw new Error("plugin package manifest is invalid");
|
|
2317
|
+
}
|
|
2318
|
+
async function packageContext(deps) {
|
|
2319
|
+
const { packageDir, manifest } = await resolvePackageContextDir(deps.packageDir);
|
|
2320
|
+
if (typeof manifest.version !== "string") throw new Error("plugin package manifest is invalid");
|
|
2321
|
+
parseVersion(manifest.version);
|
|
2322
|
+
const installation = await discoverInstallation(deps.dshHome ?? resolveDshHome(), packageDir);
|
|
2323
|
+
return {
|
|
2324
|
+
version: manifest.version,
|
|
2325
|
+
...installation === void 0 ? {} : { installation }
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
async function checkPluginUpdate(deps = {}) {
|
|
2329
|
+
try {
|
|
2330
|
+
const { version, installation } = await packageContext(deps);
|
|
2331
|
+
if (installation === void 0) return {
|
|
2332
|
+
currentVersion: version,
|
|
2333
|
+
source: "unknown",
|
|
2334
|
+
state: "unavailable",
|
|
2335
|
+
canUpdate: false,
|
|
2336
|
+
restartRequired: false,
|
|
2337
|
+
message: "Active DSH profile could not be identified uniquely"
|
|
2338
|
+
};
|
|
2339
|
+
if (installation.source === "link") return {
|
|
2340
|
+
currentVersion: version,
|
|
2341
|
+
source: "link",
|
|
2342
|
+
state: "linked",
|
|
2343
|
+
canUpdate: false,
|
|
2344
|
+
restartRequired: false,
|
|
2345
|
+
message: "Local development link; updates come from the linked checkout"
|
|
2346
|
+
};
|
|
2347
|
+
if (installation.source !== "registry") return {
|
|
2348
|
+
currentVersion: version,
|
|
2349
|
+
source: installation.source,
|
|
2350
|
+
state: "unsupported",
|
|
2351
|
+
canUpdate: false,
|
|
2352
|
+
restartRequired: false,
|
|
2353
|
+
message: "This installation source cannot be updated from the npm registry"
|
|
2354
|
+
};
|
|
2355
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2356
|
+
const comparison = compareVersions(version, latest);
|
|
2357
|
+
return {
|
|
2358
|
+
currentVersion: version,
|
|
2359
|
+
latestVersion: latest,
|
|
2360
|
+
source: "registry",
|
|
2361
|
+
state: comparison < 0 ? "available" : "current",
|
|
2362
|
+
canUpdate: comparison < 0,
|
|
2363
|
+
restartRequired: comparison < 0
|
|
2364
|
+
};
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
return {
|
|
2367
|
+
currentVersion: "unknown",
|
|
2368
|
+
source: "unknown",
|
|
2369
|
+
state: "error",
|
|
2370
|
+
canUpdate: false,
|
|
2371
|
+
restartRequired: false,
|
|
2372
|
+
message: safeMessage(error)
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
async function updatePlugin(deps = {}) {
|
|
2377
|
+
const { version, installation } = await packageContext(deps);
|
|
2378
|
+
if (installation === void 0 || installation.source !== "registry") throw new Error("Plugin update is unavailable for this installation");
|
|
2379
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2380
|
+
if (compareVersions(version, latest) >= 0) return {
|
|
2381
|
+
currentVersion: version,
|
|
2382
|
+
latestVersion: latest,
|
|
2383
|
+
source: "registry",
|
|
2384
|
+
state: "current",
|
|
2385
|
+
canUpdate: false,
|
|
2386
|
+
restartRequired: false
|
|
2387
|
+
};
|
|
2388
|
+
const resolveExecutable = deps.resolveExecutable;
|
|
2389
|
+
const spawn = deps.spawn;
|
|
2390
|
+
if (resolveExecutable === void 0 || spawn === void 0) throw new Error("DSH update runtime is unavailable");
|
|
2391
|
+
const signal = AbortSignal.timeout(UPDATE_TIMEOUT_MS);
|
|
2392
|
+
const handle = spawn({
|
|
2393
|
+
argv: [
|
|
2394
|
+
await resolveExecutable("dsh", {}, signal),
|
|
2395
|
+
"plugin",
|
|
2396
|
+
"--profile",
|
|
2397
|
+
installation.profile,
|
|
2398
|
+
"update",
|
|
2399
|
+
PLUGIN_PACKAGE_NAME
|
|
2400
|
+
],
|
|
2401
|
+
cwd: installation.profileDir,
|
|
2402
|
+
env: {},
|
|
2403
|
+
stdio: {
|
|
2404
|
+
stdin: "ignore",
|
|
2405
|
+
stdout: { maxBytes: MAX_UPDATE_OUTPUT_BYTES },
|
|
2406
|
+
stderr: { maxBytes: MAX_UPDATE_OUTPUT_BYTES }
|
|
2407
|
+
},
|
|
2408
|
+
graceMs: 2e3,
|
|
2409
|
+
signal
|
|
2410
|
+
});
|
|
2411
|
+
const outcome = await handle.done;
|
|
2412
|
+
if (outcome.exitCode !== 0) {
|
|
2413
|
+
const detail = handle.collected.stderr?.readFrom(0).text ?? "";
|
|
2414
|
+
throw new Error(`DSH plugin update failed (${outcome.exitCode ?? outcome.signal ?? "unknown exit"}): ${safeMessage(detail)}`);
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
currentVersion: version,
|
|
2418
|
+
latestVersion: latest,
|
|
2419
|
+
source: "registry",
|
|
2420
|
+
state: "current",
|
|
2421
|
+
canUpdate: false,
|
|
2422
|
+
restartRequired: true,
|
|
2423
|
+
message: "Update installed; restart DSH Desktop to load it"
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
|
|
2427
|
+
const shared = {
|
|
2428
|
+
...deps,
|
|
2429
|
+
resolveExecutable: runtime.resolveExecutable.bind(runtime),
|
|
2430
|
+
spawn: runtime.spawn.bind(runtime)
|
|
2431
|
+
};
|
|
2432
|
+
for (const route of [{
|
|
2433
|
+
path: CLAUDE_UPDATE_CHECK_PATH,
|
|
2434
|
+
method: "GET",
|
|
2435
|
+
run: () => checkPluginUpdate(shared)
|
|
2436
|
+
}, {
|
|
2437
|
+
path: CLAUDE_UPDATE_PATH,
|
|
2438
|
+
method: "POST",
|
|
2439
|
+
run: () => updatePlugin(shared)
|
|
2440
|
+
}]) ctx.effect(() => ctx.webServer.register({
|
|
2441
|
+
kind: "exact",
|
|
2442
|
+
path: route.path,
|
|
2443
|
+
handler: async (req, res) => {
|
|
2444
|
+
if (req.method !== route.method) return json(res, 405, { error: "method not allowed" });
|
|
2445
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
2446
|
+
try {
|
|
2447
|
+
json(res, 200, await route.run());
|
|
2448
|
+
} catch (error) {
|
|
2449
|
+
json(res, 500, { error: safeMessage(error) });
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}), `dsh-claude: ${route.method} ${route.path}`);
|
|
2453
|
+
}
|
|
2454
|
+
//#endregion
|
|
2076
2455
|
//#region src/index.ts
|
|
2077
2456
|
const name = "llm-claude";
|
|
2078
2457
|
const inject = [
|
|
@@ -2081,7 +2460,8 @@ const inject = [
|
|
|
2081
2460
|
"agentPresets",
|
|
2082
2461
|
"commands",
|
|
2083
2462
|
"subprocess",
|
|
2084
|
-
"approval"
|
|
2463
|
+
"approval",
|
|
2464
|
+
"userQuestions"
|
|
2085
2465
|
];
|
|
2086
2466
|
const Config = z.object({
|
|
2087
2467
|
executablePath: z.string().default(""),
|
|
@@ -2213,6 +2593,7 @@ async function apply(ctx, config) {
|
|
|
2213
2593
|
const supervisor = new ClaudeSupervisor({
|
|
2214
2594
|
runtime: ctx.subprocess,
|
|
2215
2595
|
approval: ctx.approval,
|
|
2596
|
+
userQuestions: ctx.userQuestions,
|
|
2216
2597
|
config: supervisorConfig,
|
|
2217
2598
|
runDetached: (operation) => ctx.agents.withoutInitiator(operation),
|
|
2218
2599
|
sidecar
|
|
@@ -2282,6 +2663,7 @@ async function apply(ctx, config) {
|
|
|
2282
2663
|
ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
|
|
2283
2664
|
ctx.inject(["webServer"], (webCtx) => {
|
|
2284
2665
|
registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
|
|
2666
|
+
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess);
|
|
2285
2667
|
registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
|
|
2286
2668
|
const agent = webCtx.agents.get(sessionId);
|
|
2287
2669
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|