@norman-else/dsh-claude 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -1,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 TASK_TOOL_NAMES } from "./events-DP0Ov_Re.mjs";
2
- import { a as ClaudeCommandBridge, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-YMR2WMjn.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-BKw-SdOm.mjs";
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;
@@ -1865,15 +1866,81 @@ const NO_RETRY_POLICY = Object.freeze({
1865
1866
  maxDelayMs: 1e4,
1866
1867
  jitterRatio: .1
1867
1868
  });
1868
- function extractDirectUserText(messages) {
1869
- for (let index = messages.length - 1; index >= 0; index -= 1) {
1870
- const message = messages[index];
1871
- if (message?.role !== "user" || message.source.kind !== "user") continue;
1869
+ function abortIfRequested(signal) {
1870
+ if (signal?.aborted !== true) return;
1871
+ if (signal.reason instanceof Error) throw signal.reason;
1872
+ const error = /* @__PURE__ */ new Error("Claude Code input resolution aborted");
1873
+ error.name = "AbortError";
1874
+ throw error;
1875
+ }
1876
+ function finiteNonNegative(value) {
1877
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1878
+ }
1879
+ function validateImageRef(ref, attachments, imageIndex) {
1880
+ const limits = attachments.imageLimits;
1881
+ if (!limits.mediaTypes.includes(ref.mediaType)) throw new Error(`dsh-claude: image ${imageIndex} has an unsupported media type`);
1882
+ if (!finiteNonNegative(ref.bytes) || ref.bytes > limits.maxImageBytes) throw new Error(`dsh-claude: image ${imageIndex} exceeds the configured byte limit`);
1883
+ const maxDimension = "maxImageDimension" in limits && finiteNonNegative(limits.maxImageDimension) ? limits.maxImageDimension : void 0;
1884
+ if (!finiteNonNegative(ref.width) || !finiteNonNegative(ref.height) || ref.width * ref.height > limits.maxImagePixels || maxDimension !== void 0 && (ref.width > maxDimension || ref.height > maxDimension)) throw new Error(`dsh-claude: image ${imageIndex} exceeds the configured dimension limit`);
1885
+ }
1886
+ function imageBlock(data, mediaType) {
1887
+ return {
1888
+ type: "image",
1889
+ source: {
1890
+ type: "base64",
1891
+ media_type: mediaType,
1892
+ data: Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64")
1893
+ }
1894
+ };
1895
+ }
1896
+ /** Resolve only the newest direct human message; Claude's session owns history. */
1897
+ async function resolveDirectUserPrompt(messages, attachments, signal) {
1898
+ const message = [...messages].reverse().find((candidate) => candidate.role === "user" && candidate.source.kind === "user");
1899
+ if (message === void 0) throw new Error("dsh-claude: no direct human input was present in this model step");
1900
+ const imageRefs = message.content.filter((block) => block.type === "image").map((block) => block.attachment);
1901
+ const limits = attachments.imageLimits;
1902
+ if (imageRefs.length > limits.maxImagesPerMessage) throw new Error("dsh-claude: prompt exceeds the configured image-count limit");
1903
+ let declaredBytes = 0;
1904
+ imageRefs.forEach((ref, index) => {
1905
+ validateImageRef(ref, attachments, index + 1);
1906
+ declaredBytes += ref.bytes;
1907
+ if (!Number.isSafeInteger(declaredBytes) || declaredBytes > limits.maxMessageImageBytes) throw new Error("dsh-claude: prompt exceeds the configured aggregate image-byte limit");
1908
+ });
1909
+ if (imageRefs.length === 0) {
1872
1910
  const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
1873
1911
  if (text.length > 0) return text;
1874
- if (message.content.some((block) => block.type === "image")) throw new Error("dsh-claude: image-only prompts are not supported in v0.1; include a text prompt");
1912
+ throw new Error("dsh-claude: the newest direct human message has no supported content");
1875
1913
  }
1876
- throw new Error("dsh-claude: no direct human text was present in this model step");
1914
+ const content = [];
1915
+ let imageIndex = 0;
1916
+ let verifiedBytes = 0;
1917
+ for (const block of message.content) {
1918
+ abortIfRequested(signal);
1919
+ if (block.type === "text") {
1920
+ content.push({
1921
+ type: "text",
1922
+ text: block.text
1923
+ });
1924
+ continue;
1925
+ }
1926
+ if (block.type !== "image") continue;
1927
+ imageIndex += 1;
1928
+ let stored;
1929
+ try {
1930
+ stored = await attachments.readImage(block.attachment, signal);
1931
+ } catch {
1932
+ abortIfRequested(signal);
1933
+ throw new Error(`dsh-claude: image ${imageIndex} could not be read or verified`);
1934
+ }
1935
+ abortIfRequested(signal);
1936
+ validateImageRef(stored.ref, attachments, imageIndex);
1937
+ if (stored.data.byteLength !== stored.ref.bytes || stored.ref.mediaType !== block.attachment.mediaType) throw new Error(`dsh-claude: image ${imageIndex} failed attachment verification`);
1938
+ verifiedBytes += stored.data.byteLength;
1939
+ if (!Number.isSafeInteger(verifiedBytes) || verifiedBytes > limits.maxMessageImageBytes) throw new Error("dsh-claude: prompt exceeds the configured aggregate image-byte limit");
1940
+ content.push(imageBlock(stored.data, stored.ref.mediaType));
1941
+ }
1942
+ if (content.length === 0) throw new Error("dsh-claude: the newest direct human message has no supported content");
1943
+ return content;
1877
1944
  }
1878
1945
  function tokenUsage(usage) {
1879
1946
  const normalized = {
@@ -1896,11 +1963,13 @@ function resolveAgent(agents, options) {
1896
1963
  var ClaudeCodeAdapter = class extends LlmAdapter {
1897
1964
  #supervisor;
1898
1965
  #agents;
1966
+ #attachments;
1899
1967
  #presetIdFor;
1900
- constructor(supervisor, agents, presetIdFor) {
1968
+ constructor(supervisor, agents, attachments, presetIdFor) {
1901
1969
  super();
1902
1970
  this.#supervisor = supervisor;
1903
1971
  this.#agents = agents;
1972
+ this.#attachments = attachments;
1904
1973
  this.#presetIdFor = presetIdFor;
1905
1974
  }
1906
1975
  providerInfo(provider) {
@@ -1918,7 +1987,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
1918
1987
  id: model.id,
1919
1988
  name: model.name,
1920
1989
  description: model.description,
1921
- inputModalities: ["text"]
1990
+ inputModalities: ["text", "image"]
1922
1991
  }));
1923
1992
  }
1924
1993
  async resolveModel(provider, model) {
@@ -1930,7 +1999,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
1930
1999
  name: known?.name ?? `Claude Code ${model}`,
1931
2000
  ...known === void 0 ? {} : { description: known.description },
1932
2001
  ...contextWindow === void 0 ? {} : { context: { contextWindow } },
1933
- inputModalities: ["text"],
2002
+ inputModalities: ["text", "image"],
1934
2003
  reasoning: { efforts: THINKING_MODES.map((mode) => ({
1935
2004
  id: ReasoningEffortId(mode.id),
1936
2005
  name: mode.name,
@@ -1943,7 +2012,23 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
1943
2012
  const agent = resolveAgent(this.#agents, options);
1944
2013
  if (this.#presetIdFor(agent) !== "claude") throw new Error(`dsh-claude: provider ${CLAUDE_CODE_PROVIDER} is available only to the ${CLAUDE_CODE_PRESET_ID} preset`);
1945
2014
  const thinkingMode = thinkingModeFor(options.reasoningEffort);
1946
- const prompt = extractDirectUserText(options.messages);
2015
+ let prompt;
2016
+ try {
2017
+ prompt = await resolveDirectUserPrompt(options.messages, this.#attachments, options.signal);
2018
+ } catch (error) {
2019
+ if (error.name !== "AbortError") throw error;
2020
+ yield {
2021
+ type: "finish",
2022
+ reason: {
2023
+ kind: "aborted",
2024
+ failure: {
2025
+ code: "aborted",
2026
+ message: error instanceof Error ? error.message : "Claude Code input resolution aborted"
2027
+ }
2028
+ }
2029
+ };
2030
+ return;
2031
+ }
1947
2032
  const events = await this.#supervisor.runTurn({
1948
2033
  agent,
1949
2034
  prompt,
@@ -2028,8 +2113,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2028
2113
  if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
2029
2114
  }
2030
2115
  };
2031
- function createClaudeCodeAdapter(supervisor, agents, presetIdFor) {
2032
- return new ClaudeCodeAdapter(supervisor, agents, presetIdFor);
2116
+ function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
2117
+ return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
2033
2118
  }
2034
2119
  //#endregion
2035
2120
  //#region src/http.ts
@@ -2064,7 +2149,7 @@ function json(res, status, value) {
2064
2149
  //#region src/doctor-routes.ts
2065
2150
  const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
2066
2151
  const claudeBridgeDiagnostics = /* @__PURE__ */ new WeakMap();
2067
- function safeMessage(error) {
2152
+ function safeMessage$1(error) {
2068
2153
  return redactText(error instanceof Error ? error.message : String(error), 1e3);
2069
2154
  }
2070
2155
  /** Live command-bridge diagnostics: which agents exist, their presets, and how
@@ -2080,14 +2165,14 @@ function commandDiagnostics(ctx) {
2080
2165
  const preset = ctx.agentPresets.composedPreset(agent.ctx);
2081
2166
  if (preset !== void 0) info.preset = preset;
2082
2167
  } catch (error) {
2083
- info.error = safeMessage(error);
2168
+ info.error = safeMessage$1(error);
2084
2169
  }
2085
2170
  try {
2086
2171
  const list = ctx.commands.list(agent);
2087
2172
  info.commandCount = list.length;
2088
2173
  info.sample = list.slice(0, 10).map((command) => command.name);
2089
2174
  } catch (error) {
2090
- info.error = info.error === void 0 ? safeMessage(error) : `${info.error}; ${safeMessage(error)}`;
2175
+ info.error = info.error === void 0 ? safeMessage$1(error) : `${info.error}; ${safeMessage$1(error)}`;
2091
2176
  }
2092
2177
  const bridge = claudeBridgeDiagnostics.get(agent);
2093
2178
  if (bridge !== void 0) info.bridge = bridge;
@@ -2095,7 +2180,7 @@ function commandDiagnostics(ctx) {
2095
2180
  })
2096
2181
  };
2097
2182
  } catch (error) {
2098
- return { error: safeMessage(error) };
2183
+ return { error: safeMessage$1(error) };
2099
2184
  }
2100
2185
  }
2101
2186
  function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
@@ -2119,7 +2204,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2119
2204
  version: { status: "not-run" },
2120
2205
  authentication: { status: "not-run" },
2121
2206
  handshake: "not-run",
2122
- message: safeMessage(resolutionError),
2207
+ message: safeMessage$1(resolutionError),
2123
2208
  limits: {
2124
2209
  idleTimeoutMs: config.idleTimeoutMs,
2125
2210
  maxProcesses: config.maxProcesses
@@ -2149,7 +2234,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2149
2234
  commandBridge: commandDiagnostics(ctx)
2150
2235
  });
2151
2236
  } catch (error) {
2152
- json(res, 500, { error: safeMessage(error) });
2237
+ json(res, 500, { error: safeMessage$1(error) });
2153
2238
  }
2154
2239
  }
2155
2240
  }), "dsh-claude: Doctor route");
@@ -2198,6 +2283,259 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession) {
2198
2283
  }), "dsh-claude: sidecar projection route");
2199
2284
  }
2200
2285
  //#endregion
2286
+ //#region src/update-routes.ts
2287
+ const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
2288
+ const UPDATE_TIMEOUT_MS = 3e4;
2289
+ const CHECK_TIMEOUT_MS = 1e4;
2290
+ const MAX_MANIFEST_BYTES = 262144;
2291
+ const MAX_UPDATE_OUTPUT_BYTES = 32768;
2292
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
2293
+ const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
2294
+ function safeMessage(error) {
2295
+ return redactText(error instanceof Error ? error.message : String(error), 500);
2296
+ }
2297
+ async function readManifest(path) {
2298
+ const text = await readFile(path, "utf8");
2299
+ if (Buffer.byteLength(text) > MAX_MANIFEST_BYTES) throw new Error("package manifest is too large");
2300
+ const value = JSON.parse(text);
2301
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("package manifest is invalid");
2302
+ return value;
2303
+ }
2304
+ function dependencySpec(manifest) {
2305
+ if (typeof manifest.dependencies !== "object" || manifest.dependencies === null || Array.isArray(manifest.dependencies)) return void 0;
2306
+ const value = manifest.dependencies[PLUGIN_PACKAGE_NAME];
2307
+ return typeof value === "string" && value.length <= 2e3 ? value : void 0;
2308
+ }
2309
+ function classifyInstallSpec(spec) {
2310
+ if (/^(?:link|file|workspace):/i.test(spec)) return "link";
2311
+ if (/^(?:git(?:\+[^:]+)?:|github:|https?:|npm:)/i.test(spec) || /\.git(?:#|$)/i.test(spec)) return "unsupported";
2312
+ 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";
2313
+ }
2314
+ async function samePath(left, right) {
2315
+ try {
2316
+ const [a, b] = await Promise.all([realpath(left), realpath(right)]);
2317
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
2318
+ } catch {
2319
+ return false;
2320
+ }
2321
+ }
2322
+ function linkedTarget(profileDir, spec) {
2323
+ const match = /^(?:link|file):(.*)$/i.exec(spec);
2324
+ return match?.[1] === void 0 ? void 0 : resolve(profileDir, match[1]);
2325
+ }
2326
+ async function discoverInstallation(dshHome, packageDir) {
2327
+ const profilesDir = join(dshHome, "profiles");
2328
+ const matches = [];
2329
+ let profiles;
2330
+ try {
2331
+ profiles = await opendir(profilesDir);
2332
+ } catch (error) {
2333
+ if (error.code === "ENOENT") return void 0;
2334
+ throw error;
2335
+ }
2336
+ for await (const entry of profiles) {
2337
+ if (!entry.isDirectory() || !PROFILE_NAME.test(entry.name)) continue;
2338
+ const profileDir = join(profilesDir, entry.name);
2339
+ let manifest;
2340
+ try {
2341
+ manifest = await readManifest(join(profileDir, "package.json"));
2342
+ } catch {
2343
+ continue;
2344
+ }
2345
+ const spec = dependencySpec(manifest);
2346
+ if (spec === void 0) continue;
2347
+ const source = classifyInstallSpec(spec);
2348
+ 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({
2349
+ profile: entry.name,
2350
+ profileDir,
2351
+ source,
2352
+ spec
2353
+ });
2354
+ }
2355
+ return matches.length === 1 ? matches[0] : void 0;
2356
+ }
2357
+ function parseVersion(version) {
2358
+ const match = SEMVER.exec(version);
2359
+ if (match === null) throw new Error("package version is not valid semver");
2360
+ return match;
2361
+ }
2362
+ function compareVersions(left, right) {
2363
+ const a = parseVersion(left);
2364
+ const b = parseVersion(right);
2365
+ for (const index of [
2366
+ 1,
2367
+ 2,
2368
+ 3
2369
+ ]) {
2370
+ const difference = Number(a[index]) - Number(b[index]);
2371
+ if (difference !== 0) return Math.sign(difference);
2372
+ }
2373
+ const aPre = a[4];
2374
+ const bPre = b[4];
2375
+ if (aPre === void 0) return bPre === void 0 ? 0 : 1;
2376
+ if (bPre === void 0) return -1;
2377
+ return aPre.localeCompare(bPre, "en", { numeric: true });
2378
+ }
2379
+ async function registryLatest(signal) {
2380
+ const response = await fetch("https://registry.npmjs.org/%40norman-else%2Fdsh-claude", {
2381
+ headers: { accept: "application/vnd.npm.install-v1+json" },
2382
+ signal
2383
+ });
2384
+ if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`);
2385
+ const latest = (await response.json())["dist-tags"]?.latest;
2386
+ if (typeof latest !== "string") throw new Error("npm registry response has no latest version");
2387
+ parseVersion(latest);
2388
+ return latest;
2389
+ }
2390
+ async function resolvePackageContextDir(configured) {
2391
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
2392
+ const candidates = configured === void 0 ? [moduleDir, dirname(moduleDir)] : [configured];
2393
+ for (const packageDir of candidates) try {
2394
+ const manifest = await readManifest(join(packageDir, "package.json"));
2395
+ if (manifest.name === "@norman-else/dsh-claude") return {
2396
+ packageDir,
2397
+ manifest
2398
+ };
2399
+ } catch {}
2400
+ throw new Error("plugin package manifest is invalid");
2401
+ }
2402
+ async function packageContext(deps) {
2403
+ const { packageDir, manifest } = await resolvePackageContextDir(deps.packageDir);
2404
+ if (typeof manifest.version !== "string") throw new Error("plugin package manifest is invalid");
2405
+ parseVersion(manifest.version);
2406
+ const installation = await discoverInstallation(deps.dshHome ?? resolveDshHome(), packageDir);
2407
+ return {
2408
+ version: manifest.version,
2409
+ ...installation === void 0 ? {} : { installation }
2410
+ };
2411
+ }
2412
+ async function checkPluginUpdate(deps = {}) {
2413
+ try {
2414
+ const { version, installation } = await packageContext(deps);
2415
+ if (installation === void 0) return {
2416
+ currentVersion: version,
2417
+ source: "unknown",
2418
+ state: "unavailable",
2419
+ canUpdate: false,
2420
+ restartRequired: false,
2421
+ message: "Active DSH profile could not be identified uniquely"
2422
+ };
2423
+ if (installation.source === "link") return {
2424
+ currentVersion: version,
2425
+ source: "link",
2426
+ state: "linked",
2427
+ canUpdate: false,
2428
+ restartRequired: false,
2429
+ message: "Local development link; updates come from the linked checkout"
2430
+ };
2431
+ if (installation.source !== "registry") return {
2432
+ currentVersion: version,
2433
+ source: installation.source,
2434
+ state: "unsupported",
2435
+ canUpdate: false,
2436
+ restartRequired: false,
2437
+ message: "This installation source cannot be updated from the npm registry"
2438
+ };
2439
+ const latest = await (deps.fetchLatest ?? registryLatest)(AbortSignal.timeout(CHECK_TIMEOUT_MS));
2440
+ const comparison = compareVersions(version, latest);
2441
+ return {
2442
+ currentVersion: version,
2443
+ latestVersion: latest,
2444
+ source: "registry",
2445
+ state: comparison < 0 ? "available" : "current",
2446
+ canUpdate: comparison < 0,
2447
+ restartRequired: comparison < 0
2448
+ };
2449
+ } catch (error) {
2450
+ return {
2451
+ currentVersion: "unknown",
2452
+ source: "unknown",
2453
+ state: "error",
2454
+ canUpdate: false,
2455
+ restartRequired: false,
2456
+ message: safeMessage(error)
2457
+ };
2458
+ }
2459
+ }
2460
+ async function updatePlugin(deps = {}) {
2461
+ const { version, installation } = await packageContext(deps);
2462
+ if (installation === void 0 || installation.source !== "registry") throw new Error("Plugin update is unavailable for this installation");
2463
+ const latest = await (deps.fetchLatest ?? registryLatest)(AbortSignal.timeout(CHECK_TIMEOUT_MS));
2464
+ if (compareVersions(version, latest) >= 0) return {
2465
+ currentVersion: version,
2466
+ latestVersion: latest,
2467
+ source: "registry",
2468
+ state: "current",
2469
+ canUpdate: false,
2470
+ restartRequired: false
2471
+ };
2472
+ const resolveExecutable = deps.resolveExecutable;
2473
+ const spawn = deps.spawn;
2474
+ if (resolveExecutable === void 0 || spawn === void 0) throw new Error("DSH update runtime is unavailable");
2475
+ const signal = AbortSignal.timeout(UPDATE_TIMEOUT_MS);
2476
+ const handle = spawn({
2477
+ argv: [
2478
+ await resolveExecutable("dsh", {}, signal),
2479
+ "plugin",
2480
+ "--profile",
2481
+ installation.profile,
2482
+ "update",
2483
+ PLUGIN_PACKAGE_NAME
2484
+ ],
2485
+ cwd: installation.profileDir,
2486
+ env: {},
2487
+ stdio: {
2488
+ stdin: "ignore",
2489
+ stdout: { maxBytes: MAX_UPDATE_OUTPUT_BYTES },
2490
+ stderr: { maxBytes: MAX_UPDATE_OUTPUT_BYTES }
2491
+ },
2492
+ graceMs: 2e3,
2493
+ signal
2494
+ });
2495
+ const outcome = await handle.done;
2496
+ if (outcome.exitCode !== 0) {
2497
+ const detail = handle.collected.stderr?.readFrom(0).text ?? "";
2498
+ throw new Error(`DSH plugin update failed (${outcome.exitCode ?? outcome.signal ?? "unknown exit"}): ${safeMessage(detail)}`);
2499
+ }
2500
+ return {
2501
+ currentVersion: version,
2502
+ latestVersion: latest,
2503
+ source: "registry",
2504
+ state: "current",
2505
+ canUpdate: false,
2506
+ restartRequired: true,
2507
+ message: "Update installed; restart DSH Desktop to load it"
2508
+ };
2509
+ }
2510
+ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
2511
+ const shared = {
2512
+ ...deps,
2513
+ resolveExecutable: runtime.resolveExecutable.bind(runtime),
2514
+ spawn: runtime.spawn.bind(runtime)
2515
+ };
2516
+ for (const route of [{
2517
+ path: CLAUDE_UPDATE_CHECK_PATH,
2518
+ method: "GET",
2519
+ run: () => checkPluginUpdate(shared)
2520
+ }, {
2521
+ path: CLAUDE_UPDATE_PATH,
2522
+ method: "POST",
2523
+ run: () => updatePlugin(shared)
2524
+ }]) ctx.effect(() => ctx.webServer.register({
2525
+ kind: "exact",
2526
+ path: route.path,
2527
+ handler: async (req, res) => {
2528
+ if (req.method !== route.method) return json(res, 405, { error: "method not allowed" });
2529
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
2530
+ try {
2531
+ json(res, 200, await route.run());
2532
+ } catch (error) {
2533
+ json(res, 500, { error: safeMessage(error) });
2534
+ }
2535
+ }
2536
+ }), `dsh-claude: ${route.method} ${route.path}`);
2537
+ }
2538
+ //#endregion
2201
2539
  //#region src/index.ts
2202
2540
  const name = "llm-claude";
2203
2541
  const inject = [
@@ -2207,7 +2545,8 @@ const inject = [
2207
2545
  "commands",
2208
2546
  "subprocess",
2209
2547
  "approval",
2210
- "userQuestions"
2548
+ "userQuestions",
2549
+ "attachments"
2211
2550
  ];
2212
2551
  const Config = z.object({
2213
2552
  executablePath: z.string().default(""),
@@ -2347,7 +2686,7 @@ async function apply(ctx, config) {
2347
2686
  let resolutionError;
2348
2687
  try {
2349
2688
  supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
2350
- ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
2689
+ ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
2351
2690
  ctx.effect(() => {
2352
2691
  const mounted = /* @__PURE__ */ new Map();
2353
2692
  const pending = /* @__PURE__ */ new Set();
@@ -2409,6 +2748,7 @@ async function apply(ctx, config) {
2409
2748
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
2410
2749
  ctx.inject(["webServer"], (webCtx) => {
2411
2750
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
2751
+ registerClaudeUpdateRoutes(webCtx, webCtx.subprocess);
2412
2752
  registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
2413
2753
  const agent = webCtx.agents.get(sessionId);
2414
2754
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";