relay-dsh-plugin-claude 0.1.2 → 0.1.4

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.
@@ -1,14 +1,14 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import assert from "node:assert/strict";
3
3
  import { spawn } from "node:child_process";
4
- import { randomUUID } from "node:crypto";
4
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import readline from "node:readline";
7
7
  import { z } from "zod";
8
8
  import { homedir } from "node:os";
9
9
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
- import { KNOWN_SESSION_EVENT_TYPES } from "@deepseek-ai/dsh-session";
11
- import { LlmAdapter } from "@deepseek-ai/dsh-llm";
10
+ import { KNOWN_SESSION_EVENT_TYPES, SessionId } from "@deepseek-ai/dsh-session";
11
+ import { CallId, LlmAdapter, MessageId, freezeMessage } from "@deepseek-ai/dsh-llm";
12
12
  import { cp, mkdir, open, realpath, stat } from "node:fs/promises";
13
13
  import sharp from "sharp";
14
14
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -326,6 +326,7 @@ var ClaudeCliClient = class extends EventEmitter {
326
326
  return DEFAULT_MODELS$2;
327
327
  }
328
328
  async createSession(config = {}) {
329
+ assertSdkOnlyPluginsAbsent(config);
329
330
  const id = config.sessionId ?? randomUUID();
330
331
  const session = {
331
332
  id,
@@ -341,6 +342,7 @@ var ClaudeCliClient = class extends EventEmitter {
341
342
  };
342
343
  }
343
344
  async resumeSession(sessionId, config = {}) {
345
+ assertSdkOnlyPluginsAbsent(config);
344
346
  const existing = this.sessions.get(sessionId) ?? {
345
347
  id: sessionId,
346
348
  created: true
@@ -361,6 +363,7 @@ var ClaudeCliClient = class extends EventEmitter {
361
363
  };
362
364
  }
363
365
  async sendMessage(sessionId, message = {}) {
366
+ assertSdkOnlyPluginsAbsent(message);
364
367
  if (message.content?.some((block) => block?.type === "image")) throw new Error("The Claude CLI backend cannot accept image input; use the Claude Agent SDK backend");
365
368
  if (Array.isArray(message.dshTools) && message.dshTools.length > 0) throw new Error("The Claude CLI backend cannot expose DSH tools; use the Claude Agent SDK backend");
366
369
  const session = this.sessions.get(sessionId) ?? await this.resumeSession(sessionId, message);
@@ -486,6 +489,10 @@ var ClaudeCliClient = class extends EventEmitter {
486
489
  });
487
490
  }
488
491
  };
492
+ function assertSdkOnlyPluginsAbsent(config) {
493
+ if (Array.isArray(config.plugins) && config.plugins.length === 0) return;
494
+ if (config.plugins !== void 0) throw Object.assign(/* @__PURE__ */ new Error("Local Claude plugins require the Claude Agent SDK backend"), { code: "CLAUDE_LOCAL_PLUGINS_REQUIRE_SDK" });
495
+ }
489
496
  function normalizeClaudeStreamMessage(message, state) {
490
497
  const events = [];
491
498
  const content = message.message?.content ?? message.content ?? [];
@@ -579,6 +586,30 @@ function systemPromptArguments(value) {
579
586
  return [];
580
587
  }
581
588
  //#endregion
589
+ //#region claude-plugin-config.mjs
590
+ const CLAUDE_PLUGIN_KEYS = /* @__PURE__ */ new Set([
591
+ "type",
592
+ "path",
593
+ "skipMcpDiscovery"
594
+ ]);
595
+ function normalizeClaudePlugins(value, label = "Claude plugins") {
596
+ if (value === void 0) return void 0;
597
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
598
+ return value.map((plugin, index) => {
599
+ const entryLabel = `${label}[${index}]`;
600
+ if (!plugin || typeof plugin !== "object" || Array.isArray(plugin)) throw new TypeError(`${entryLabel} must be an object`);
601
+ for (const key of Object.keys(plugin)) if (!CLAUDE_PLUGIN_KEYS.has(key)) throw new TypeError(`${entryLabel}.${key} is not supported`);
602
+ if (plugin.type !== "local") throw new TypeError(`${entryLabel}.type must be "local"`);
603
+ if (typeof plugin.path !== "string" || !plugin.path.trim()) throw new TypeError(`${entryLabel}.path must be a non-empty string`);
604
+ if (plugin.skipMcpDiscovery !== void 0 && typeof plugin.skipMcpDiscovery !== "boolean") throw new TypeError(`${entryLabel}.skipMcpDiscovery must be a boolean`);
605
+ return {
606
+ type: "local",
607
+ path: plugin.path,
608
+ ...plugin.skipMcpDiscovery === void 0 ? {} : { skipMcpDiscovery: plugin.skipMcpDiscovery }
609
+ };
610
+ });
611
+ }
612
+ //#endregion
582
613
  //#region sdk-client.mjs
583
614
  const DEFAULT_MODELS$1 = [
584
615
  {
@@ -606,6 +637,8 @@ const DEFAULT_MODELS$1 = [
606
637
  inputModalities: ["text", "image"]
607
638
  }
608
639
  ];
640
+ const CLAUDE_SEARCH_TOOLS = ["Glob", "Grep"];
641
+ const SENSITIVE_ENV_NAME = /(?:^|_)(?:API_?KEY|ACCESS_?KEY|SECRET(?:_ACCESS_?KEY)?|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|PRIVATE_?KEY)(?:$|_)/i;
609
642
  function reasoningEfforts() {
610
643
  return [
611
644
  "low",
@@ -632,7 +665,29 @@ var ClaudeSdkClient = class extends EventEmitter {
632
665
  async listModels() {
633
666
  return DEFAULT_MODELS$1;
634
667
  }
668
+ supportsSessionImport() {
669
+ return typeof this.sdk?.listSessions === "function" && typeof this.sdk?.getSessionInfo === "function" && typeof this.sdk?.getSessionMessages === "function";
670
+ }
671
+ async listWorkspaceSessions({ cwd }) {
672
+ if (!this.supportsSessionImport()) throw sessionImportUnavailable$1();
673
+ return this.sdk.listSessions({
674
+ dir: cwd,
675
+ includeWorktrees: false,
676
+ includeProgrammatic: false
677
+ });
678
+ }
679
+ async readSession(sessionId, { cwd }) {
680
+ if (!this.supportsSessionImport()) throw sessionImportUnavailable$1();
681
+ const info = await this.sdk.getSessionInfo(sessionId, { dir: cwd });
682
+ if (!info) return null;
683
+ const messages = await this.sdk.getSessionMessages(sessionId, { dir: cwd });
684
+ return {
685
+ ...info,
686
+ messages
687
+ };
688
+ }
635
689
  async createSession(config = {}) {
690
+ config = normalizedSessionConfig(config);
636
691
  const id = config.sessionId ?? randomUUID();
637
692
  this.sessions.set(id, {
638
693
  id,
@@ -647,6 +702,7 @@ var ClaudeSdkClient = class extends EventEmitter {
647
702
  };
648
703
  }
649
704
  async resumeSession(sessionId, config = {}) {
705
+ config = normalizedSessionConfig(config);
650
706
  const existing = this.sessions.get(sessionId) ?? {
651
707
  id: sessionId,
652
708
  created: true,
@@ -667,10 +723,11 @@ var ClaudeSdkClient = class extends EventEmitter {
667
723
  };
668
724
  }
669
725
  async sendMessage(sessionId, message = {}) {
726
+ if (message.plugins !== void 0) throw new TypeError("Claude plugins must be configured when the Session is created or resumed");
670
727
  const session = this.sessions.get(sessionId) ?? await this.resumeSession(sessionId, message);
671
728
  const turnId = randomUUID();
672
729
  const abortController = new AbortController();
673
- const options = this.queryOptions(session, message, abortController);
730
+ const options = await this.queryOptions(session, message, abortController);
674
731
  const query = this.sdk.query({
675
732
  prompt: claudePrompt(message),
676
733
  options
@@ -734,10 +791,19 @@ var ClaudeSdkClient = class extends EventEmitter {
734
791
  message: error?.message ?? String(error)
735
792
  });
736
793
  }
737
- queryOptions(session, message, abortController) {
794
+ async queryOptions(session, message, abortController) {
795
+ const plugins = normalizeClaudePlugins(session.config?.plugins);
796
+ const cwd = message.cwd ?? session.cwd ?? process.cwd();
797
+ const settingSources = message.settingSources ?? session.config?.settingSources ?? [
798
+ "user",
799
+ "project",
800
+ "local"
801
+ ];
802
+ const dshOptions = dshMcpOptions(this.sdk, message.dshTools, message.executeDshTool, abortController.signal);
803
+ const redactions = await sensitiveEnvironmentRedactions(this.sdk, cwd, settingSources);
738
804
  return {
739
805
  abortController,
740
- cwd: message.cwd ?? session.cwd ?? process.cwd(),
806
+ cwd,
741
807
  model: message.model ?? session.config?.model,
742
808
  effort: message.effort ?? session.config?.effort,
743
809
  thinking: {
@@ -745,17 +811,16 @@ var ClaudeSdkClient = class extends EventEmitter {
745
811
  display: "summarized"
746
812
  },
747
813
  permissionMode: sdkPermissionMode(message),
748
- settingSources: message.settingSources ?? session.config?.settingSources ?? [
749
- "user",
750
- "project",
751
- "local"
752
- ],
814
+ settingSources,
753
815
  systemPrompt: message.systemPrompt ?? session.config?.systemPrompt,
754
816
  pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
755
817
  includePartialMessages: true,
818
+ ...plugins === void 0 ? {} : { plugins },
756
819
  ...session.created ? { resume: session.id } : { sessionId: session.id },
757
820
  canUseTool: (toolName, input, options) => this.requestPermission(session.id, toolName, input, options),
758
- ...dshMcpOptions(this.sdk, message.dshTools, message.executeDshTool, abortController.signal)
821
+ ...redactions.length === 0 ? {} : { hooks: sensitiveOutputHooks(redactions) },
822
+ ...dshOptions,
823
+ allowedTools: [...CLAUDE_SEARCH_TOOLS, ...dshOptions.allowedTools ?? []]
759
824
  };
760
825
  }
761
826
  requestPermission(sessionId, toolName, input, options = {}) {
@@ -831,6 +896,87 @@ var ClaudeSdkClient = class extends EventEmitter {
831
896
  });
832
897
  }
833
898
  };
899
+ async function sensitiveEnvironmentRedactions(sdk, cwd, settingSources) {
900
+ const environment = { ...process.env };
901
+ const explicitSensitiveNames = /* @__PURE__ */ new Set();
902
+ if (typeof sdk.resolveSettings === "function") {
903
+ const resolved = await sdk.resolveSettings({
904
+ cwd,
905
+ settingSources
906
+ });
907
+ Object.assign(environment, resolved?.effective?.env ?? {});
908
+ for (const entry of resolved?.effective?.sandbox?.credentials?.envVars ?? []) if (typeof entry?.name === "string") explicitSensitiveNames.add(entry.name);
909
+ }
910
+ const byValue = /* @__PURE__ */ new Map();
911
+ for (const [name, value] of Object.entries(environment)) {
912
+ if (!SENSITIVE_ENV_NAME.test(name) && !explicitSensitiveNames.has(name)) continue;
913
+ if (typeof value !== "string" || value.length === 0) continue;
914
+ const current = byValue.get(value);
915
+ if (current === void 0 || name < current) byValue.set(value, name);
916
+ }
917
+ return [...byValue].map(([value, name]) => ({
918
+ value,
919
+ replacement: `[REDACTED_ENV:${name}]`
920
+ })).sort((left, right) => right.value.length - left.value.length || left.replacement.localeCompare(right.replacement));
921
+ }
922
+ function sensitiveOutputHooks(redactions) {
923
+ return { PostToolUse: [{ hooks: [async (input) => {
924
+ const redacted = redactSensitiveToolOutput(input.tool_response, redactions);
925
+ if (!redacted.changed) return { continue: true };
926
+ return {
927
+ continue: true,
928
+ hookSpecificOutput: {
929
+ hookEventName: "PostToolUse",
930
+ updatedToolOutput: redacted.value
931
+ }
932
+ };
933
+ }] }] };
934
+ }
935
+ function redactSensitiveToolOutput(value, redactions) {
936
+ if (typeof value === "string") {
937
+ let redacted = value;
938
+ for (const entry of redactions) redacted = redacted.replaceAll(entry.value, entry.replacement);
939
+ return {
940
+ value: redacted,
941
+ changed: redacted !== value
942
+ };
943
+ }
944
+ if (Array.isArray(value)) {
945
+ let changed = false;
946
+ const redacted = value.map((item) => {
947
+ const result = redactSensitiveToolOutput(item, redactions);
948
+ changed ||= result.changed;
949
+ return result.value;
950
+ });
951
+ return {
952
+ value: changed ? redacted : value,
953
+ changed
954
+ };
955
+ }
956
+ if (!value || typeof value !== "object") return {
957
+ value,
958
+ changed: false
959
+ };
960
+ let changed = false;
961
+ const entries = Object.entries(value).map(([key, item]) => {
962
+ const result = redactSensitiveToolOutput(item, redactions);
963
+ changed ||= result.changed;
964
+ return [key, result.value];
965
+ });
966
+ return {
967
+ value: changed ? Object.fromEntries(entries) : value,
968
+ changed
969
+ };
970
+ }
971
+ function normalizedSessionConfig(config) {
972
+ const plugins = normalizeClaudePlugins(config.plugins);
973
+ const normalized = {
974
+ ...config,
975
+ ...plugins === void 0 ? {} : { plugins }
976
+ };
977
+ if (plugins === void 0) delete normalized.plugins;
978
+ return normalized;
979
+ }
834
980
  function claudePrompt(message) {
835
981
  const content = Array.isArray(message.content) ? message.content : [];
836
982
  if (!content.some((block) => block?.type === "image")) return message.text;
@@ -1109,6 +1255,9 @@ function resultError(message) {
1109
1255
  if (!message?.is_error) return null;
1110
1256
  return new Error(message.errors?.join("\n") || message.subtype || "Claude SDK turn failed");
1111
1257
  }
1258
+ function sessionImportUnavailable$1() {
1259
+ return Object.assign(/* @__PURE__ */ new Error("The active Claude Agent SDK does not support native Session discovery and history import"), { code: "CLAUDE_SESSION_IMPORT_UNAVAILABLE" });
1260
+ }
1112
1261
  function sdkPermissionMode(message) {
1113
1262
  if (message.permissionMode) return message.permissionMode;
1114
1263
  if (message.approvalPolicy === "never") return "dontAsk";
@@ -1148,10 +1297,11 @@ const DEFAULT_MODELS = [
1148
1297
  }
1149
1298
  ];
1150
1299
  var ClaudeSessionRuntime = class extends EventEmitter {
1151
- constructor({ client = new ClaudeCliClient(), cwd = process.cwd() } = {}) {
1300
+ constructor({ client = new ClaudeCliClient(), cwd = process.cwd(), plugins = void 0 } = {}) {
1152
1301
  super();
1153
1302
  this.client = client;
1154
1303
  this.cwd = cwd;
1304
+ this.plugins = normalizeClaudePlugins(plugins);
1155
1305
  this.sessions = /* @__PURE__ */ new Map();
1156
1306
  this.models = DEFAULT_MODELS;
1157
1307
  this.selectedSessionId = null;
@@ -1182,9 +1332,10 @@ var ClaudeSessionRuntime = class extends EventEmitter {
1182
1332
  ], systemPrompt = {
1183
1333
  type: "preset",
1184
1334
  preset: "claude_code"
1185
- } } = {}) {
1335
+ }, plugins = this.plugins } = {}) {
1186
1336
  const selectedModel = model ?? this.models.find((candidate) => candidate.isDefault)?.id ?? "sonnet";
1187
1337
  const selectedEffort = effort ?? this.models.find((candidate) => candidate.id === selectedModel)?.defaultReasoningEffort ?? "medium";
1338
+ const normalizedPlugins = normalizeClaudePlugins(plugins);
1188
1339
  const created = await this.client.createSession?.({
1189
1340
  model: selectedModel,
1190
1341
  effort: selectedEffort,
@@ -1193,7 +1344,8 @@ var ClaudeSessionRuntime = class extends EventEmitter {
1193
1344
  cwd,
1194
1345
  ephemeral,
1195
1346
  settingSources,
1196
- systemPrompt
1347
+ systemPrompt,
1348
+ ...normalizedPlugins === void 0 ? {} : { plugins: normalizedPlugins }
1197
1349
  });
1198
1350
  const session = this.upsertSession(created ?? {}, {
1199
1351
  id: created?.id,
@@ -1202,21 +1354,38 @@ var ClaudeSessionRuntime = class extends EventEmitter {
1202
1354
  sandbox,
1203
1355
  approvalPolicy,
1204
1356
  cwd,
1205
- ephemeral
1357
+ ephemeral,
1358
+ plugins: normalizedPlugins
1206
1359
  });
1207
1360
  if (!session.ephemeral) this.selectedSessionId = session.id;
1208
1361
  this.emitChange();
1209
1362
  return publicSession(session);
1210
1363
  }
1364
+ supportsSessionImport() {
1365
+ return this.client.supportsSessionImport?.() === true;
1366
+ }
1367
+ async listWorkspaceSessions({ cwd }) {
1368
+ if (!this.supportsSessionImport() || typeof this.client.listWorkspaceSessions !== "function") throw sessionImportUnavailable();
1369
+ return structuredClone(await this.client.listWorkspaceSessions({ cwd }));
1370
+ }
1371
+ async readSession(sessionId, { cwd }) {
1372
+ if (!this.supportsSessionImport() || typeof this.client.readSession !== "function") throw sessionImportUnavailable();
1373
+ const source = await this.client.readSession(sessionId, { cwd });
1374
+ return source === null ? null : structuredClone(source);
1375
+ }
1211
1376
  async resumeSession(sessionId, defaults = {}) {
1212
1377
  if (!sessionId?.trim()) throw new Error("sessionId is required");
1213
- const resumed = await this.client.resumeSession?.(sessionId, {
1378
+ const normalizedPlugins = normalizeClaudePlugins(defaults.plugins === void 0 ? this.plugins : defaults.plugins);
1379
+ const resumeConfig = {
1214
1380
  cwd: defaults.cwd ?? this.cwd,
1215
- ...defaults
1216
- });
1381
+ ...defaults,
1382
+ ...normalizedPlugins === void 0 ? {} : { plugins: normalizedPlugins }
1383
+ };
1384
+ if (normalizedPlugins === void 0) delete resumeConfig.plugins;
1385
+ const resumed = await this.client.resumeSession?.(sessionId, resumeConfig);
1217
1386
  const session = this.upsertSession(resumed ?? {}, {
1218
1387
  id: sessionId,
1219
- ...defaults
1388
+ ...resumeConfig
1220
1389
  });
1221
1390
  if (Array.isArray(resumed?.turns) && resumed.turns.length > 0) session.turns = structuredClone(resumed.turns);
1222
1391
  if (!session.ephemeral) this.selectedSessionId = sessionId;
@@ -1228,6 +1397,7 @@ var ClaudeSessionRuntime = class extends EventEmitter {
1228
1397
  const session = this.requireSession(sessionId);
1229
1398
  const hasContent = Array.isArray(content) && content.some((block) => block?.type === "image" || block?.type === "text" && String(block.text ?? "").trim());
1230
1399
  if (!text?.trim() && !hasContent) throw new Error("message content is required");
1400
+ if (message.plugins !== void 0) throw new TypeError("Claude plugins must be configured when the Session is created or resumed");
1231
1401
  const next = {
1232
1402
  model: model ?? session.model,
1233
1403
  effort: effort ?? session.effort,
@@ -1310,6 +1480,7 @@ var ClaudeSessionRuntime = class extends EventEmitter {
1310
1480
  approvalPolicy: defaults.approvalPolicy ?? input.approvalPolicy ?? existing.approvalPolicy ?? "on-request",
1311
1481
  cwd: defaults.cwd ?? input.cwd ?? existing.cwd ?? this.cwd,
1312
1482
  ephemeral: Boolean(defaults.ephemeral ?? input.ephemeral ?? existing.ephemeral),
1483
+ plugins: defaults.plugins ?? input.plugins ?? existing.plugins,
1313
1484
  updatedAt: input.updatedAt ?? nowSeconds()
1314
1485
  });
1315
1486
  if (Array.isArray(input.turns) && input.turns.length > 0) existing.turns = structuredClone(input.turns);
@@ -1353,6 +1524,9 @@ function publicSession(session) {
1353
1524
  function nowSeconds() {
1354
1525
  return Date.now() / 1e3;
1355
1526
  }
1527
+ function sessionImportUnavailable() {
1528
+ return Object.assign(/* @__PURE__ */ new Error("Native Claude Session import requires the Claude Agent SDK backend"), { code: "CLAUDE_SESSION_IMPORT_UNAVAILABLE" });
1529
+ }
1356
1530
  //#endregion
1357
1531
  //#region plugin.mjs
1358
1532
  const CLAUDE_EXECUTION_CAPABILITY = "relay.execution.claude.v1";
@@ -1369,7 +1543,8 @@ function createClaudeExecutionPlugin(config = {}) {
1369
1543
  const logger = capabilities.optional("relay.logging.v1") ?? console;
1370
1544
  const runtime = new ClaudeSessionRuntime({
1371
1545
  client: config.client ?? createClaudeClient(config),
1372
- cwd: config.cwd ?? process.cwd()
1546
+ cwd: config.cwd ?? process.cwd(),
1547
+ plugins: config.plugins
1373
1548
  });
1374
1549
  defer(() => runtime.close());
1375
1550
  const ready = runtime.initialize();
@@ -1384,6 +1559,9 @@ function executionCapability(runtime, ready) {
1384
1559
  return Object.freeze({
1385
1560
  whenReady: () => ready,
1386
1561
  listModels: () => structuredClone(runtime.models),
1562
+ supportsSessionImport: () => runtime.supportsSessionImport(),
1563
+ listWorkspaceSessions: runtime.listWorkspaceSessions.bind(runtime),
1564
+ readSession: runtime.readSession.bind(runtime),
1387
1565
  hasSession: (sessionId) => runtime.sessions.has(sessionId),
1388
1566
  getSession: runtime.getSession.bind(runtime),
1389
1567
  patchSession(sessionId, patch) {
@@ -1451,6 +1629,15 @@ var FallbackClaudeClient = class extends ClaudeCliClient {
1451
1629
  listModels(...args) {
1452
1630
  return this.active.listModels(...args);
1453
1631
  }
1632
+ supportsSessionImport(...args) {
1633
+ return this.active.supportsSessionImport?.(...args) === true;
1634
+ }
1635
+ listWorkspaceSessions(...args) {
1636
+ return this.active.listWorkspaceSessions?.(...args);
1637
+ }
1638
+ readSession(...args) {
1639
+ return this.active.readSession?.(...args);
1640
+ }
1454
1641
  createSession(...args) {
1455
1642
  return this.active.createSession(...args);
1456
1643
  }
@@ -1498,6 +1685,15 @@ const MEDIA_TYPES = /* @__PURE__ */ new Map([
1498
1685
  [".webp", "image/webp"]
1499
1686
  ]);
1500
1687
  const IMAGE_SUFFIX = String.raw`\.(?:png|jpe?g|webp|gif|bmp|svg|tiff?|avif|heic)`;
1688
+ const IMAGE_PATH_END = new RegExp(`${IMAGE_SUFFIX}$`, "i");
1689
+ const BARE_PATH_BOUNDARY = String.raw`[\s(\[{:*=~(【《「『“‘,。;:!?、]`;
1690
+ const BARE_PATH_END_CHAR = String.raw`[\s)\]}>,;:!?*~,。;:!?、)】》」』”’]`;
1691
+ const BARE_PATH_END = String.raw`(?:${BARE_PATH_END_CHAR}|\.(?=$|${BARE_PATH_END_CHAR}))`;
1692
+ const BARE_PATH_CONTENT = String.raw`[^\s<>"'“”‘’「」『』*~()\[\]{}=:,;!?,。;:!?、()【】《》]+?`;
1693
+ const MARKDOWN_PATH = new RegExp(String.raw`!?\[[^\]\r\n]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)`, "gi");
1694
+ const INLINE_CODE_PATH = new RegExp("`([^`\\r\\n]+" + IMAGE_SUFFIX + ")`", "gi");
1695
+ const QUOTED_PATH = new RegExp("[\"']([^\"'\\r\\n]+" + IMAGE_SUFFIX + ")[\"']", "gi");
1696
+ const URI_REFERENCE = /(?!(?:[a-z]:[\\/]))(?:[a-z][a-z0-9+.-]*:\/\/|data:|file:|blob:|mailto:|\/\/)[^\s<>"'“”‘’]+/gi;
1501
1697
  const SVG_EXTENSION = ".svg";
1502
1698
  const SVG_SOURCE_MAX_BYTES = 2 * 1024 * 1024;
1503
1699
  const SVG_RASTER_DENSITY = 72;
@@ -1507,15 +1703,15 @@ const DEFAULT_MAX_IMAGE_DIMENSION = 8192;
1507
1703
  function extractFinalAnswerImagePaths(text) {
1508
1704
  const visible = withoutFencedCode(String(text ?? ""));
1509
1705
  const matches = [];
1510
- collectMatches(matches, visible, new RegExp(String.raw`!?\[[^\]\r\n]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)`, "gi"), 1, 2);
1511
- collectMatches(matches, visible, new RegExp("`([^`\\r\\n]+" + IMAGE_SUFFIX + ")`", "gi"), 1);
1512
- collectMatches(matches, visible, new RegExp("[\"']([^\"'\\r\\n]+" + IMAGE_SUFFIX + ")[\"']", "gi"), 1);
1513
- collectMatches(matches, visible, new RegExp("(?:^|[\\s(\\[])((?:\\.{0,2}/|/)[^\\s<>\"'`]+?" + IMAGE_SUFFIX + ")(?=$|[\\s)\\],.;:!?])", "gi"), 1);
1706
+ collectMatches(matches, visible, MARKDOWN_PATH, 1, 2);
1707
+ collectMatches(matches, visible, INLINE_CODE_PATH, 1);
1708
+ collectMatches(matches, visible, QUOTED_PATH, 1);
1709
+ collectMatches(matches, maskMatches(visible, MARKDOWN_PATH, INLINE_CODE_PATH, QUOTED_PATH, URI_REFERENCE), new RegExp(String.raw`(?:^|${BARE_PATH_BOUNDARY})((?:[a-z]:[\\/])?${BARE_PATH_CONTENT}${IMAGE_SUFFIX})(?=$|${BARE_PATH_END})`, "gi"), 1);
1514
1710
  matches.sort((left, right) => left.index - right.index);
1515
1711
  const seen = /* @__PURE__ */ new Set();
1516
1712
  return matches.flatMap(({ path }) => {
1517
1713
  const normalized = normalizeMention(path);
1518
- if (!normalized || isRemoteReference(normalized) || seen.has(normalized)) return [];
1714
+ if (!normalized || !IMAGE_PATH_END.test(normalized) || isRemoteReference(normalized) || seen.has(normalized)) return [];
1519
1715
  seen.add(normalized);
1520
1716
  return [normalized];
1521
1717
  });
@@ -1707,11 +1903,15 @@ function collectMatches(matches, text, expression, ...groups) {
1707
1903
  function withoutFencedCode(text) {
1708
1904
  return text.replace(/(^|\n)[ \t]*(```|~~~)[^\n]*\n[\s\S]*?(?:\n[ \t]*\2(?=\n|$)|$)/g, (match) => match.replace(/[^\n]/g, " "));
1709
1905
  }
1906
+ function maskMatches(text, ...expressions) {
1907
+ return expressions.reduce((masked, expression) => masked.replace(expression, (match) => match.replace(/[^\n]/g, " ")), text);
1908
+ }
1710
1909
  function normalizeMention(path) {
1711
1910
  return String(path ?? "").trim().replace(/^<|>$/g, "");
1712
1911
  }
1713
1912
  function isRemoteReference(path) {
1714
- return /^(?:https?:|data:|file:)/i.test(path);
1913
+ if (/^[a-z]:[\\/]/i.test(path)) return false;
1914
+ return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(path);
1715
1915
  }
1716
1916
  function deduplicateAttachments(images) {
1717
1917
  const seen = /* @__PURE__ */ new Set();
@@ -1786,6 +1986,13 @@ function outputImageError(message, code) {
1786
1986
  const CLAUDE_PRESET = "relay-claude";
1787
1987
  const CLAUDE_PROVIDER = "relay-claude";
1788
1988
  const CLAUDE_ACTIVITY_EVENT = "relay-claude/activity";
1989
+ const IMPORT_STATES = Object.freeze([
1990
+ "reserved",
1991
+ "session-created",
1992
+ "hydrated",
1993
+ "attached",
1994
+ "committed"
1995
+ ]);
1789
1996
  var ClaudeDshAdapter = class extends LlmAdapter {
1790
1997
  constructor({ runtime, ready, linkStore = null, attachments = null, logger = console }) {
1791
1998
  super();
@@ -1796,12 +2003,16 @@ var ClaudeDshAdapter = class extends LlmAdapter {
1796
2003
  this.attachments = attachments;
1797
2004
  this.links = /* @__PURE__ */ new Map();
1798
2005
  this.settings = /* @__PURE__ */ new Map();
2006
+ this.bindingModes = /* @__PURE__ */ new Map();
2007
+ this.importStates = /* @__PURE__ */ new Map();
1799
2008
  this.pendingSessions = /* @__PURE__ */ new Map();
1800
2009
  this.agents = /* @__PURE__ */ new Map();
1801
2010
  for (const [sessionId, record] of linkStore?.entries() ?? []) {
1802
2011
  const claudeSessionId = record.claudeSessionId ?? record.sessionId ?? record.threadId;
1803
2012
  if (claudeSessionId) this.links.set(sessionId, claudeSessionId);
1804
2013
  this.settings.set(sessionId, record.config);
2014
+ this.bindingModes.set(sessionId, record.bindingMode === "imported" ? "imported" : "native");
2015
+ if (record.bindingMode === "imported" && IMPORT_STATES.includes(record.importState)) this.importStates.set(sessionId, record.importState);
1805
2016
  }
1806
2017
  }
1807
2018
  providerInfo() {
@@ -1905,18 +2116,25 @@ var ClaudeDshAdapter = class extends LlmAdapter {
1905
2116
  await this.runtime.resumeSession(linked, settings);
1906
2117
  return linked;
1907
2118
  } catch (error) {
2119
+ if (this.bindingModes.get(sessionId) === "imported") throw Object.assign(new Error(`Relay could not resume imported Claude Session ${linked}`, { cause: error }), {
2120
+ code: "CLAUDE_IMPORTED_SESSION_RESUME_FAILED",
2121
+ claudeSessionId: linked
2122
+ });
1908
2123
  this.logger.warn(`Relay could not resume Claude session ${linked}; creating a replacement: ${error.message}`);
1909
2124
  this.links.delete(sessionId);
1910
2125
  }
1911
2126
  const created = await this.runtime.createSession(settings);
1912
2127
  this.links.set(sessionId, created.id);
2128
+ this.bindingModes.set(sessionId, "native");
1913
2129
  this.persistLink(sessionId);
1914
2130
  return created.id;
1915
2131
  }
1916
2132
  persistLink(sessionId) {
1917
2133
  this.linkStore?.set(sessionId, {
1918
2134
  claudeSessionId: this.links.get(sessionId) ?? null,
1919
- config: this.configuration(sessionId)
2135
+ config: this.configuration(sessionId),
2136
+ bindingMode: this.bindingModes.get(sessionId) ?? "native",
2137
+ ...this.importStates.has(sessionId) ? { importState: this.importStates.get(sessionId) } : {}
1920
2138
  });
1921
2139
  }
1922
2140
  sessionFor(sessionId) {
@@ -1926,6 +2144,47 @@ var ClaudeDshAdapter = class extends LlmAdapter {
1926
2144
  for (const [sessionId, candidate] of this.links) if (candidate === claudeSessionId) return sessionId;
1927
2145
  return null;
1928
2146
  }
2147
+ bindingForClaudeSession(claudeSessionId) {
2148
+ const sessionId = this.dshSessionForClaudeSession(claudeSessionId);
2149
+ if (!sessionId) return null;
2150
+ return {
2151
+ sessionId,
2152
+ claudeSessionId,
2153
+ config: structuredClone(this.configuration(sessionId)),
2154
+ bindingMode: this.bindingModes.get(sessionId) ?? "native",
2155
+ importState: this.importStates.get(sessionId) ?? null
2156
+ };
2157
+ }
2158
+ bindImportedSession(sessionId, claudeSessionId, config = {}) {
2159
+ const key = String(sessionId).trim();
2160
+ const source = String(claudeSessionId).trim();
2161
+ if (!key) throw new Error("DSH sessionId is required for an imported Claude binding");
2162
+ if (!source) throw new Error("Claude sessionId is required for an imported binding");
2163
+ const owner = this.dshSessionForClaudeSession(source);
2164
+ if (owner && owner !== key) throw new Error(`Claude Session ${source} is already bound to DSH session ${owner}`);
2165
+ const current = this.links.get(key);
2166
+ if (current && current !== source) throw new Error(`DSH session ${key} is already bound to Claude Session ${current}`);
2167
+ this.links.set(key, source);
2168
+ this.settings.set(key, {
2169
+ ...this.configuration(key, config.cwd),
2170
+ ...compact(config)
2171
+ });
2172
+ this.bindingModes.set(key, "imported");
2173
+ if (!this.importStates.has(key)) this.importStates.set(key, "reserved");
2174
+ this.persistLink(key);
2175
+ return this.bindingForClaudeSession(source);
2176
+ }
2177
+ markImportState(sessionId, state) {
2178
+ const key = String(sessionId);
2179
+ if (this.bindingModes.get(key) !== "imported") throw new Error(`DSH session ${key} is not an imported Claude binding`);
2180
+ const next = IMPORT_STATES.indexOf(state);
2181
+ if (next === -1) throw new Error(`unknown Claude import state ${state}`);
2182
+ if (next > IMPORT_STATES.indexOf(this.importStates.get(key) ?? "reserved")) {
2183
+ this.importStates.set(key, state);
2184
+ this.persistLink(key);
2185
+ }
2186
+ return this.bindingForClaudeSession(this.links.get(key));
2187
+ }
1929
2188
  async *stream(options) {
1930
2189
  if (options.purpose) {
1931
2190
  yield* this.streamAuxiliary(options);
@@ -2074,7 +2333,8 @@ var ClaudeDshAdapter = class extends LlmAdapter {
2074
2333
  cwd,
2075
2334
  ephemeral: true,
2076
2335
  settingSources: ["user"],
2077
- systemPrompt: options.system
2336
+ systemPrompt: options.system,
2337
+ plugins: []
2078
2338
  })).id;
2079
2339
  const queue = new ActivityQueue(options.signal, "Claude");
2080
2340
  const onActivity = (message) => {
@@ -2519,6 +2779,370 @@ function effectivePreset(session) {
2519
2779
  return session.header.agentPreset;
2520
2780
  }
2521
2781
  //#endregion
2782
+ //#region claude-import.mjs
2783
+ const IMPORT_STATE_ORDER = Object.freeze([
2784
+ "reserved",
2785
+ "session-created",
2786
+ "hydrated",
2787
+ "attached",
2788
+ "committed"
2789
+ ]);
2790
+ var ClaudeWorkspaceImporter = class {
2791
+ constructor({ runtime, adapter, target, logger = console }) {
2792
+ if (!runtime?.listWorkspaceSessions) throw new Error("Claude import requires Workspace Session inventory");
2793
+ if (!adapter?.bindImportedSession) throw new Error("Claude import requires a DSH binding adapter");
2794
+ if (![
2795
+ "prepare",
2796
+ "hydrate",
2797
+ "attach",
2798
+ "finalize"
2799
+ ].every((method) => typeof target?.[method] === "function")) throw new Error("Claude import requires a complete DSH Session target");
2800
+ this.runtime = runtime;
2801
+ this.adapter = adapter;
2802
+ this.target = target;
2803
+ this.logger = logger;
2804
+ this.pendingSessions = /* @__PURE__ */ new Map();
2805
+ }
2806
+ async scanWorkspace(cwd) {
2807
+ const canonicalCwd = await canonicalPath(cwd);
2808
+ const discovered = await this.runtime.listWorkspaceSessions({ cwd: canonicalCwd });
2809
+ const sessions = [];
2810
+ for (const session of discovered) {
2811
+ if (!session || typeof session.sessionId !== "string" || typeof session.cwd !== "string" || !session.cwd.trim()) continue;
2812
+ const sessionCwd = await canonicalPath(session.cwd);
2813
+ if (sessionCwd !== canonicalCwd) continue;
2814
+ sessions.push({
2815
+ ...session,
2816
+ cwd: sessionCwd
2817
+ });
2818
+ }
2819
+ sessions.sort(compareInventorySessions);
2820
+ const entries = sessions.map((session) => {
2821
+ const binding = this.adapter.bindingForClaudeSession(session.sessionId);
2822
+ if (!binding) return {
2823
+ session,
2824
+ binding: null,
2825
+ status: "ready"
2826
+ };
2827
+ if (binding.bindingMode === "imported" && binding.importState !== "committed") return {
2828
+ session,
2829
+ binding,
2830
+ status: "recoverable"
2831
+ };
2832
+ return {
2833
+ session,
2834
+ binding,
2835
+ status: "existing"
2836
+ };
2837
+ });
2838
+ const existing = entries.filter((entry) => entry.status === "existing").length;
2839
+ const recoverable = entries.filter((entry) => entry.status === "recoverable").length;
2840
+ return {
2841
+ cwd: canonicalCwd,
2842
+ entries,
2843
+ summary: {
2844
+ found: entries.length,
2845
+ existing,
2846
+ recoverable,
2847
+ ready: entries.length - existing
2848
+ }
2849
+ };
2850
+ }
2851
+ async importWorkspace(cwd, { sessionIds, onProgress } = {}) {
2852
+ const inventory = await this.scanWorkspace(cwd);
2853
+ const entries = selectedImportEntries(inventory.entries, sessionIds);
2854
+ const result = {
2855
+ found: sessionIds === void 0 ? inventory.summary.found : entries.length,
2856
+ imported: 0,
2857
+ existing: 0,
2858
+ failed: 0,
2859
+ failures: []
2860
+ };
2861
+ let completed = 0;
2862
+ for (const entry of entries) {
2863
+ if (entry.status === "existing") result.existing += 1;
2864
+ else try {
2865
+ await this.importSession(entry.session, cwd, entry.binding);
2866
+ result.imported += 1;
2867
+ } catch (error) {
2868
+ result.failed += 1;
2869
+ const session = shortSessionId(entry.session.sessionId);
2870
+ const message = publicErrorMessage(error, entry.session.sessionId, session);
2871
+ result.failures.push({
2872
+ session,
2873
+ message
2874
+ });
2875
+ this.logger.warn?.(`Claude import failed for ${session}: ${message}`);
2876
+ }
2877
+ completed += 1;
2878
+ onProgress?.({
2879
+ completed,
2880
+ total: entries.length,
2881
+ ...result
2882
+ });
2883
+ }
2884
+ return result;
2885
+ }
2886
+ async importSession(session, workspaceCwd, existingBinding = null) {
2887
+ const pending = this.pendingSessions.get(session.sessionId);
2888
+ if (pending) return pending;
2889
+ const operation = this.runImportSession(session, workspaceCwd, existingBinding).finally(() => {
2890
+ this.pendingSessions.delete(session.sessionId);
2891
+ });
2892
+ this.pendingSessions.set(session.sessionId, operation);
2893
+ return operation;
2894
+ }
2895
+ async runImportSession(session, workspaceCwd, existingBinding = null) {
2896
+ const source = await this.runtime.readSession(session.sessionId, { cwd: workspaceCwd });
2897
+ if (!source || source.sessionId !== session.sessionId) throw new Error(`Claude Session ${shortSessionId(session.sessionId)} is no longer available in this Workspace`);
2898
+ if (await canonicalPath(source.cwd) !== await canonicalPath(workspaceCwd)) throw new Error(`Claude Session ${shortSessionId(session.sessionId)} no longer belongs to this Workspace`);
2899
+ let binding = existingBinding;
2900
+ if (!binding) {
2901
+ const sessionId = importedDshSessionId(session.sessionId);
2902
+ binding = this.adapter.bindImportedSession(sessionId, session.sessionId, {
2903
+ ...this.adapter.configuration(sessionId, session.cwd),
2904
+ cwd: session.cwd
2905
+ });
2906
+ }
2907
+ if (binding.bindingMode !== "imported") return binding.sessionId;
2908
+ if (binding.importState === "committed") return binding.sessionId;
2909
+ let transaction = null;
2910
+ try {
2911
+ transaction = await this.target.prepare({
2912
+ session,
2913
+ source,
2914
+ binding,
2915
+ workspaceCwd
2916
+ });
2917
+ binding = this.adapter.markImportState(binding.sessionId, "session-created");
2918
+ if (before(binding.importState, "hydrated")) {
2919
+ await this.target.hydrate(transaction);
2920
+ binding = this.adapter.markImportState(binding.sessionId, "hydrated");
2921
+ }
2922
+ if (before(binding.importState, "attached")) {
2923
+ await this.target.attach(transaction);
2924
+ binding = this.adapter.markImportState(binding.sessionId, "attached");
2925
+ }
2926
+ if (before(binding.importState, "committed")) {
2927
+ await this.target.finalize(transaction);
2928
+ binding = this.adapter.markImportState(binding.sessionId, "committed");
2929
+ }
2930
+ return binding.sessionId;
2931
+ } finally {
2932
+ if (transaction !== null) await this.target.release?.(transaction);
2933
+ }
2934
+ }
2935
+ };
2936
+ function importedDshSessionId(sessionId) {
2937
+ return `claude-import-${createHash("sha256").update(String(sessionId)).digest("hex").slice(0, 24)}`;
2938
+ }
2939
+ function before(current, target) {
2940
+ return IMPORT_STATE_ORDER.indexOf(current) < IMPORT_STATE_ORDER.indexOf(target);
2941
+ }
2942
+ function compareInventorySessions(left, right) {
2943
+ const updated = timestampValue(right.lastModified) - timestampValue(left.lastModified);
2944
+ return updated === 0 ? String(left.sessionId).localeCompare(String(right.sessionId)) : updated;
2945
+ }
2946
+ function timestampValue(value) {
2947
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2948
+ const parsed = Date.parse(String(value ?? ""));
2949
+ return Number.isFinite(parsed) ? parsed : 0;
2950
+ }
2951
+ function selectedImportEntries(entries, sessionIds) {
2952
+ if (sessionIds === void 0) return entries;
2953
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) throw new Error("At least one Claude Session must be selected");
2954
+ const selected = /* @__PURE__ */ new Set();
2955
+ for (const rawId of sessionIds) {
2956
+ if (typeof rawId !== "string") throw new Error("Selected Claude Session IDs must be non-empty strings");
2957
+ const sessionId = rawId.trim();
2958
+ if (!sessionId) throw new Error("Selected Claude Session IDs must be non-empty strings");
2959
+ if (selected.has(sessionId)) throw new Error(`Claude Session ${shortSessionId(sessionId)} was selected more than once`);
2960
+ selected.add(sessionId);
2961
+ }
2962
+ const inventory = new Map(entries.map((entry) => [entry.session.sessionId, entry]));
2963
+ for (const sessionId of selected) {
2964
+ const entry = inventory.get(sessionId);
2965
+ if (!entry) throw new Error(`Claude Session ${shortSessionId(sessionId)} is not available in this Workspace`);
2966
+ if (entry.status === "existing") throw new Error(`Claude Session ${shortSessionId(sessionId)} is already bound to DSH`);
2967
+ }
2968
+ return entries.filter((entry) => selected.has(entry.session.sessionId));
2969
+ }
2970
+ async function canonicalPath(path) {
2971
+ const absolute = resolve(path);
2972
+ try {
2973
+ return await realpath(absolute);
2974
+ } catch {
2975
+ return absolute;
2976
+ }
2977
+ }
2978
+ function shortSessionId(sessionId) {
2979
+ const value = String(sessionId);
2980
+ return value.length > 12 ? `${value.slice(0, 8)}...${value.slice(-4)}` : value;
2981
+ }
2982
+ function publicErrorMessage(error, sessionId, shortId) {
2983
+ const message = error?.message ?? String(error);
2984
+ return String(message).replaceAll(String(sessionId), shortId);
2985
+ }
2986
+ //#endregion
2987
+ //#region claude-import-contract.mjs
2988
+ const CLAUDE_IMPORT_PATH = "/api/relay/claude/import";
2989
+ //#endregion
2990
+ //#region claude-import-route.js
2991
+ function registerClaudeImportRoute(ctx, options) {
2992
+ return ctx.webServer.register({
2993
+ kind: "exact",
2994
+ path: CLAUDE_IMPORT_PATH,
2995
+ handler: createClaudeImportHandler({
2996
+ workspaceRegistry: ctx.workspaceRegistry,
2997
+ token: process.env.RELAY_CLAUDE_IMPORT_TOKEN,
2998
+ ...options
2999
+ })
3000
+ });
3001
+ }
3002
+ function createClaudeImportHandler({ importer, workspaceRegistry, token, maxBodyBytes = 16384 }) {
3003
+ if (!importer || !workspaceRegistry) throw new Error("Claude import route requires importer and Workspace registry");
3004
+ return async (request, response) => {
3005
+ if (request.method !== "POST") {
3006
+ writeJson(response, 405, { error: "method_not_allowed" }, { allow: "POST" });
3007
+ return;
3008
+ }
3009
+ if (!authorized(request, token)) {
3010
+ writeJson(response, 403, { error: "forbidden" });
3011
+ return;
3012
+ }
3013
+ try {
3014
+ const body = await readJson(request, maxBodyBytes);
3015
+ const action = body?.action;
3016
+ const cwd = requiredString$1(body?.cwd, "cwd");
3017
+ if (action !== "scan" && action !== "import") throw new ImportRouteError(400, "action must be scan or import");
3018
+ const workspace = await workspaceRegistry.resolveByPath(cwd);
3019
+ if (!workspace) throw new ImportRouteError(404, "Workspace is not registered in DSH");
3020
+ if (action === "scan") {
3021
+ const inventory = await importer.scanWorkspace(workspace.path);
3022
+ writeJson(response, 200, {
3023
+ workspace: {
3024
+ title: workspace.title,
3025
+ path: workspace.path
3026
+ },
3027
+ summary: inventory.summary,
3028
+ candidates: publicCandidates(inventory.entries)
3029
+ });
3030
+ return;
3031
+ }
3032
+ const sessionIds = optionalSessionIds(body?.sessionIds);
3033
+ response.writeHead(200, {
3034
+ "content-type": "application/x-ndjson; charset=utf-8",
3035
+ "cache-control": "no-store",
3036
+ "x-content-type-options": "nosniff"
3037
+ });
3038
+ try {
3039
+ writeLine(response, {
3040
+ type: "complete",
3041
+ result: await importer.importWorkspace(workspace.path, {
3042
+ sessionIds,
3043
+ onProgress: (progress) => writeLine(response, {
3044
+ type: "progress",
3045
+ ...progress
3046
+ })
3047
+ })
3048
+ });
3049
+ } catch (error) {
3050
+ writeLine(response, {
3051
+ type: "error",
3052
+ error: "import_failed",
3053
+ message: error?.message ?? String(error)
3054
+ });
3055
+ }
3056
+ response.end();
3057
+ } catch (error) {
3058
+ const status = error instanceof ImportRouteError ? error.statusCode : 500;
3059
+ writeJson(response, status, {
3060
+ error: status === 413 ? "payload_too_large" : status === 404 ? "workspace_not_found" : status < 500 ? "invalid_request" : "import_failed",
3061
+ message: error?.message ?? String(error)
3062
+ });
3063
+ }
3064
+ };
3065
+ }
3066
+ async function readJson(request, maxBodyBytes) {
3067
+ if (String(request.headers?.["content-type"] ?? "").split(";", 1)[0].trim() !== "application/json") throw new ImportRouteError(400, "content-type must be application/json");
3068
+ const chunks = [];
3069
+ let size = 0;
3070
+ for await (const chunk of request) {
3071
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3072
+ size += buffer.length;
3073
+ if (size > maxBodyBytes) throw new ImportRouteError(413, `request exceeds ${maxBodyBytes} bytes`);
3074
+ chunks.push(buffer);
3075
+ }
3076
+ if (size === 0) throw new ImportRouteError(400, "request body is empty");
3077
+ try {
3078
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
3079
+ } catch {
3080
+ throw new ImportRouteError(400, "request body is not valid JSON");
3081
+ }
3082
+ }
3083
+ function authorized(request, token) {
3084
+ if (isLoopback(request.socket?.remoteAddress)) return true;
3085
+ if (!token) return false;
3086
+ const authorization = String(request.headers?.authorization ?? "");
3087
+ if (!authorization.startsWith("Bearer ")) return false;
3088
+ const supplied = Buffer.from(authorization.slice(7));
3089
+ const expected = Buffer.from(String(token));
3090
+ return supplied.length === expected.length && timingSafeEqual(supplied, expected);
3091
+ }
3092
+ function isLoopback(address) {
3093
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
3094
+ }
3095
+ function requiredString$1(value, name) {
3096
+ if (typeof value !== "string" || !value.trim()) throw new ImportRouteError(400, `${name} is required`);
3097
+ return value.trim();
3098
+ }
3099
+ function optionalSessionIds(value) {
3100
+ if (value === void 0) return void 0;
3101
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100) throw new ImportRouteError(400, "sessionIds must contain between 1 and 100 IDs");
3102
+ const ids = value.map((sessionId) => requiredString$1(sessionId, "sessionId"));
3103
+ if (new Set(ids).size !== ids.length) throw new ImportRouteError(400, "sessionIds must be unique");
3104
+ return ids;
3105
+ }
3106
+ function publicCandidates(entries = []) {
3107
+ return entries.filter((entry) => entry.status === "ready" || entry.status === "recoverable").map(({ session, status }) => ({
3108
+ id: session.sessionId,
3109
+ title: publicSessionTitle(session),
3110
+ cwd: session.cwd,
3111
+ updatedAt: session.lastModified ?? session.createdAt ?? null,
3112
+ status
3113
+ }));
3114
+ }
3115
+ function publicSessionTitle(session) {
3116
+ for (const value of [
3117
+ session.customTitle,
3118
+ session.summary,
3119
+ session.firstPrompt
3120
+ ]) {
3121
+ if (typeof value !== "string") continue;
3122
+ const normalized = value.replace(/\s+/g, " ").trim();
3123
+ if (normalized) return normalized.slice(0, 160);
3124
+ }
3125
+ return String(session.sessionId);
3126
+ }
3127
+ function writeLine(response, value) {
3128
+ response.write(`${JSON.stringify(value)}\n`);
3129
+ }
3130
+ function writeJson(response, status, value, headers = {}) {
3131
+ response.writeHead(status, {
3132
+ "content-type": "application/json; charset=utf-8",
3133
+ "cache-control": "no-store",
3134
+ "x-content-type-options": "nosniff",
3135
+ ...headers
3136
+ });
3137
+ response.end(`${JSON.stringify(value)}\n`);
3138
+ }
3139
+ var ImportRouteError = class extends Error {
3140
+ constructor(statusCode, message) {
3141
+ super(message);
3142
+ this.statusCode = statusCode;
3143
+ }
3144
+ };
3145
+ //#endregion
2522
3146
  //#region claude-link-store.js
2523
3147
  var ClaudeLinkStore = class {
2524
3148
  constructor(path) {
@@ -2558,7 +3182,13 @@ function loadRecords(path) {
2558
3182
  }
2559
3183
  }
2560
3184
  function validRecord(record) {
2561
- return isObject(record) && (record.claudeSessionId === null || typeof record.claudeSessionId === "string") && isObject(record.config);
3185
+ return isObject(record) && (record.claudeSessionId === null || typeof record.claudeSessionId === "string") && isObject(record.config) && (record.bindingMode === void 0 || record.bindingMode === "native" || record.bindingMode === "imported") && (record.importState === void 0 || [
3186
+ "reserved",
3187
+ "session-created",
3188
+ "hydrated",
3189
+ "attached",
3190
+ "committed"
3191
+ ].includes(record.importState));
2562
3192
  }
2563
3193
  function isObject(value) {
2564
3194
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2657,6 +3287,353 @@ function plainObject(value) {
2657
3287
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2658
3288
  }
2659
3289
  //#endregion
3290
+ //#region dsh-import-target.js
3291
+ var DshClaudeImportTarget = class {
3292
+ constructor({ ctx, logger = console }) {
3293
+ this.ctx = ctx;
3294
+ this.logger = logger;
3295
+ this.persistedIds = null;
3296
+ }
3297
+ async prepare(input) {
3298
+ const sessionId = SessionId(input.binding.sessionId);
3299
+ const projection = claudeHistoryProjection(input.source.messages ?? []);
3300
+ const seed = buildClaudeHistorySeed(projection, input.source.lastModified, input.binding.config);
3301
+ const resident = this.ctx.agents.get(sessionId);
3302
+ if (resident) return {
3303
+ ...input,
3304
+ projection,
3305
+ seed,
3306
+ agent: resident,
3307
+ handle: null
3308
+ };
3309
+ const persistedIds = await this.loadPersistedIds();
3310
+ const agentOptions = {
3311
+ provider: CLAUDE_PROVIDER,
3312
+ model: input.binding.config.model
3313
+ };
3314
+ const handle = persistedIds.has(sessionId) ? await this.ctx.agents.resume({
3315
+ resumeSessionId: sessionId,
3316
+ agentOptions
3317
+ }) : await this.ctx.agents.create({
3318
+ sessionId,
3319
+ seed,
3320
+ agentOptions,
3321
+ meta: {
3322
+ cwd: input.source.cwd,
3323
+ createdAt: importedCreatedAt(input.source, seed),
3324
+ agentPreset: CLAUDE_PRESET
3325
+ }
3326
+ });
3327
+ return {
3328
+ ...input,
3329
+ projection,
3330
+ seed,
3331
+ agent: handle.agent,
3332
+ handle
3333
+ };
3334
+ }
3335
+ async hydrate(transaction) {
3336
+ applySessionTitle(this.ctx, transaction.agent.session, transaction.source);
3337
+ await this.ctx.sessions.flush(transaction.agent.session);
3338
+ (await this.loadPersistedIds()).add(SessionId(transaction.binding.sessionId));
3339
+ const projectionCache = this.ctx.get?.("sessionProjectionCache");
3340
+ if (!projectionCache?.write) throw new Error("Claude Session import requires DSH's sessionProjectionCache service");
3341
+ await projectionCache.write(transaction.agent.session);
3342
+ return projectionSummary(transaction.projection);
3343
+ }
3344
+ async attach(transaction) {
3345
+ const workspace = await this.ctx.workspaceRegistry.resolveByPath(transaction.workspaceCwd);
3346
+ if (!workspace) throw new Error(`No registered DSH Workspace matches ${transaction.workspaceCwd}`);
3347
+ await workspace.attachSession(SessionId(transaction.binding.sessionId));
3348
+ }
3349
+ async finalize(transaction) {
3350
+ await this.ctx.sessions.flush(transaction.agent.session);
3351
+ (await this.loadPersistedIds()).add(SessionId(transaction.binding.sessionId));
3352
+ }
3353
+ async release(transaction) {
3354
+ await transaction.handle?.dispose();
3355
+ }
3356
+ async loadPersistedIds() {
3357
+ if (this.persistedIds === null) this.persistedIds = new Set((await this.ctx.sessionPersistence.list()).map((header) => SessionId(header.id)));
3358
+ return this.persistedIds;
3359
+ }
3360
+ };
3361
+ function buildClaudeHistorySeed(projection, updatedAt, config) {
3362
+ const time = timestampMs(updatedAt);
3363
+ const events = [];
3364
+ const append = (type, data, surfaceOp = null) => {
3365
+ events.push({
3366
+ type,
3367
+ seq: events.length,
3368
+ time,
3369
+ data,
3370
+ ...surfaceOp === null ? {} : { surfaceOp }
3371
+ });
3372
+ };
3373
+ append("request/header", {
3374
+ header: { config: {
3375
+ provider: CLAUDE_PROVIDER,
3376
+ model: config.model,
3377
+ ...config.effort ? { reasoningEffort: config.effort } : {}
3378
+ } },
3379
+ reason: "initial"
3380
+ });
3381
+ let turn = 1;
3382
+ for (const sourceTurn of projection.turns) {
3383
+ if (sourceTurn.timeline.length === 0) continue;
3384
+ append("turn/start", { turn });
3385
+ let step = 0;
3386
+ for (const entry of sourceTurn.timeline) {
3387
+ if (entry.kind === "message" && entry.role === "user") {
3388
+ append("user/message", freezeMessage({
3389
+ id: MessageId(entry.id),
3390
+ role: "user",
3391
+ content: entry.content,
3392
+ source: { kind: "user" }
3393
+ }), "append");
3394
+ continue;
3395
+ }
3396
+ step += 1;
3397
+ append("step/start", {
3398
+ turn,
3399
+ step
3400
+ });
3401
+ if (entry.kind === "message") append("assistant/message", {
3402
+ turn,
3403
+ step,
3404
+ message: freezeMessage({
3405
+ id: MessageId(entry.id),
3406
+ role: "assistant",
3407
+ content: entry.content,
3408
+ source: {
3409
+ kind: "model",
3410
+ provider: CLAUDE_PROVIDER,
3411
+ model: "imported"
3412
+ }
3413
+ })
3414
+ }, "append");
3415
+ else {
3416
+ const callId = CallId(entry.callId);
3417
+ append("assistant/message", {
3418
+ turn,
3419
+ step,
3420
+ message: freezeMessage({
3421
+ id: MessageId(entry.requestId),
3422
+ role: "assistant",
3423
+ content: [{
3424
+ type: "tool-call",
3425
+ id: callId,
3426
+ name: entry.name,
3427
+ arguments: entry.arguments
3428
+ }],
3429
+ source: {
3430
+ kind: "model",
3431
+ provider: CLAUDE_PROVIDER,
3432
+ model: "imported"
3433
+ }
3434
+ })
3435
+ }, "append");
3436
+ append("tool/call", {
3437
+ turn,
3438
+ step,
3439
+ callId,
3440
+ name: entry.name,
3441
+ arguments: entry.arguments
3442
+ });
3443
+ append("tool/result", {
3444
+ turn,
3445
+ step,
3446
+ message: freezeMessage({
3447
+ id: MessageId(entry.resultId),
3448
+ role: "user",
3449
+ content: [{
3450
+ type: "tool-result",
3451
+ toolCallId: callId,
3452
+ content: entry.content,
3453
+ isError: entry.isError
3454
+ }],
3455
+ source: {
3456
+ kind: "tool",
3457
+ callId
3458
+ }
3459
+ })
3460
+ }, "append");
3461
+ }
3462
+ append("step/end", {
3463
+ turn,
3464
+ step
3465
+ });
3466
+ }
3467
+ append("turn/end", {
3468
+ turn,
3469
+ reason: { kind: "stop" }
3470
+ });
3471
+ turn += 1;
3472
+ }
3473
+ return events;
3474
+ }
3475
+ function claudeHistoryProjection(messages) {
3476
+ const turns = [];
3477
+ const pendingTools = /* @__PURE__ */ new Map();
3478
+ let current = null;
3479
+ let skippedBlocks = 0;
3480
+ for (const source of messages) {
3481
+ const content = sourceContentBlocks(source);
3482
+ if (source?.type === "user" && source.parent_tool_use_id == null) {
3483
+ const blocks = content.filter((block) => block?.type === "text" && normalizedText(block.text)).map((block) => ({
3484
+ type: "text",
3485
+ text: normalizedText(block.text)
3486
+ }));
3487
+ if (blocks.length > 0) {
3488
+ skippedBlocks += content.filter((block) => block?.type !== "text").length;
3489
+ current = {
3490
+ sourceId: source.uuid,
3491
+ timeline: []
3492
+ };
3493
+ current.timeline.push({
3494
+ kind: "message",
3495
+ role: "user",
3496
+ id: projectionId(source, "user"),
3497
+ content: blocks
3498
+ });
3499
+ turns.push(current);
3500
+ continue;
3501
+ }
3502
+ }
3503
+ if (!current) {
3504
+ skippedBlocks += content.length;
3505
+ continue;
3506
+ }
3507
+ if (source?.type === "assistant") {
3508
+ for (const [index, block] of content.entries()) if (block?.type === "thinking" && normalizedText(block.thinking)) current.timeline.push({
3509
+ kind: "message",
3510
+ role: "assistant",
3511
+ id: projectionId(source, `thinking-${index}`),
3512
+ content: [{
3513
+ type: "reasoning",
3514
+ text: normalizedText(block.thinking)
3515
+ }]
3516
+ });
3517
+ else if (block?.type === "text" && normalizedText(block.text)) current.timeline.push({
3518
+ kind: "message",
3519
+ role: "assistant",
3520
+ id: projectionId(source, `text-${index}`),
3521
+ content: [{
3522
+ type: "text",
3523
+ text: normalizedText(block.text)
3524
+ }]
3525
+ });
3526
+ else if (block?.type === "tool_use" && normalizedText(block.id) && normalizedText(block.name)) pendingTools.set(block.id, {
3527
+ turn: current,
3528
+ source,
3529
+ index,
3530
+ name: block.name,
3531
+ arguments: jsonText(block.input)
3532
+ });
3533
+ else skippedBlocks += 1;
3534
+ continue;
3535
+ }
3536
+ if (source?.type === "user") {
3537
+ for (const block of content) {
3538
+ if (block?.type !== "tool_result" || !pendingTools.has(block.tool_use_id)) {
3539
+ skippedBlocks += 1;
3540
+ continue;
3541
+ }
3542
+ const tool = pendingTools.get(block.tool_use_id);
3543
+ pendingTools.delete(block.tool_use_id);
3544
+ const result = toolResultContent(block.content);
3545
+ skippedBlocks += result.skipped;
3546
+ tool.turn.timeline.push({
3547
+ kind: "activity",
3548
+ requestId: projectionId(tool.source, `tool-${tool.index}-request`),
3549
+ resultId: projectionId(source, `tool-${tool.index}-result`),
3550
+ callId: `claude:${block.tool_use_id}`,
3551
+ name: tool.name,
3552
+ arguments: tool.arguments,
3553
+ content: result.content,
3554
+ isError: block.is_error === true
3555
+ });
3556
+ }
3557
+ continue;
3558
+ }
3559
+ skippedBlocks += content.length;
3560
+ }
3561
+ skippedBlocks += pendingTools.size;
3562
+ return {
3563
+ turns,
3564
+ skippedBlocks
3565
+ };
3566
+ }
3567
+ function sourceContentBlocks(source) {
3568
+ const value = source?.message?.content;
3569
+ if (Array.isArray(value)) return value;
3570
+ if (source?.type === "user" && normalizedText(value)) return [{
3571
+ type: "text",
3572
+ text: value
3573
+ }];
3574
+ return [];
3575
+ }
3576
+ function toolResultContent(value) {
3577
+ if (typeof value === "string") return {
3578
+ content: [{
3579
+ type: "text",
3580
+ text: value
3581
+ }],
3582
+ skipped: 0
3583
+ };
3584
+ if (!Array.isArray(value)) return {
3585
+ content: [],
3586
+ skipped: value == null ? 0 : 1
3587
+ };
3588
+ const content = value.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => ({
3589
+ type: "text",
3590
+ text: block.text
3591
+ }));
3592
+ return {
3593
+ content,
3594
+ skipped: value.length - content.length
3595
+ };
3596
+ }
3597
+ function projectionSummary(projection) {
3598
+ return {
3599
+ projectedTurns: projection.turns.length,
3600
+ projectedMessages: projection.turns.reduce((count, turn) => count + turn.timeline.length, 0),
3601
+ skippedBlocks: projection.skippedBlocks
3602
+ };
3603
+ }
3604
+ function projectionId(message, suffix) {
3605
+ return `claude:${createHash("sha256").update(`${message.session_id ?? "session"}:${message.uuid ?? "message"}:${suffix}`).digest("hex").slice(0, 24)}`;
3606
+ }
3607
+ function importedCreatedAt(source, seed) {
3608
+ const updated = timestampMs(source.lastModified);
3609
+ if (!seed.some((event) => event.type === "user/message")) return updated;
3610
+ return Math.min(timestampMs(source.createdAt, updated), updated);
3611
+ }
3612
+ function timestampMs(value, fallback = Date.now()) {
3613
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return fallback;
3614
+ return Math.trunc(value < 0xe8d4a51000 ? value * 1e3 : value);
3615
+ }
3616
+ function applySessionTitle(ctx, session, source) {
3617
+ const titles = ctx.get?.("sessionTitle");
3618
+ if (!titles) throw new Error("Claude Session import requires DSH's sessionTitle service");
3619
+ const title = summarizeTitle(source.customTitle) || summarizeTitle(source.summary) || summarizeTitle(source.firstPrompt) || `Claude ${String(source.sessionId).slice(0, 8)}`;
3620
+ if (titles.get(session)?.title !== title) titles.rename(session, title);
3621
+ }
3622
+ function summarizeTitle(value) {
3623
+ const text = normalizedText(value).replace(/\s+/g, " ");
3624
+ return text.length > 80 ? `${text.slice(0, 79)}...` : text;
3625
+ }
3626
+ function normalizedText(value) {
3627
+ return typeof value === "string" ? value.trim() : "";
3628
+ }
3629
+ function jsonText(value) {
3630
+ try {
3631
+ return JSON.stringify(value ?? {}, null, 2);
3632
+ } catch {
3633
+ return "{}";
3634
+ }
3635
+ }
3636
+ //#endregion
2660
3637
  //#region dsh-plugin.js
2661
3638
  function createDshClaudePlugin(ctx, config = {}) {
2662
3639
  return definePlugin({
@@ -2665,19 +3642,37 @@ function createDshClaudePlugin(ctx, config = {}) {
2665
3642
  version: "1.0.0",
2666
3643
  provides: { "relay.dsh.claude.v1": "1.0.0" },
2667
3644
  requires: { "relay.execution.claude.v1": "^1.0.0" },
2668
- permissions: ["dsh:llm", "dsh:agents"]
3645
+ permissions: [
3646
+ "dsh:llm",
3647
+ "dsh:agents",
3648
+ "dsh:web-server"
3649
+ ]
2669
3650
  },
2670
3651
  async activate({ capabilities, defer }) {
2671
3652
  installClaudeSessionEventType();
2672
3653
  const runtime = capabilities.require("relay.execution.claude.v1");
3654
+ const linkStore = new ClaudeLinkStore(resolveLinkPath(config.claudeLinkPath));
2673
3655
  const adapter = new ClaudeDshAdapter({
2674
3656
  runtime,
2675
3657
  ready: runtime.whenReady(),
2676
- linkStore: new ClaudeLinkStore(resolveLinkPath(config.claudeLinkPath)),
3658
+ linkStore,
2677
3659
  attachments: ctx.attachments,
2678
3660
  logger: ctx.logger
2679
3661
  });
3662
+ const importer = new ClaudeWorkspaceImporter({
3663
+ runtime,
3664
+ adapter,
3665
+ target: new DshClaudeImportTarget({
3666
+ ctx,
3667
+ logger: ctx.logger
3668
+ }),
3669
+ logger: ctx.logger
3670
+ });
2680
3671
  defer(ctx.llm.registerAdapter([CLAUDE_PROVIDER], adapter));
3672
+ defer(registerClaudeImportRoute(ctx, {
3673
+ importer,
3674
+ token: config.claudeImportToken ?? process.env.RELAY_CLAUDE_IMPORT_TOKEN
3675
+ }));
2681
3676
  defer(runtime.subscribeRequest((request) => {
2682
3677
  handleClaudeSdkRequest(ctx, {
2683
3678
  adapter,
@@ -2755,7 +3750,9 @@ const inject = [
2755
3750
  "tools",
2756
3751
  "typert",
2757
3752
  "userQuestions",
2758
- "webServer"
3753
+ "webServer",
3754
+ "workspaceRegistry",
3755
+ "sessionTitle"
2759
3756
  ];
2760
3757
  async function apply(ctx, config = {}) {
2761
3758
  const host = new PluginHost();
@@ -2769,7 +3766,8 @@ async function apply(ctx, config = {}) {
2769
3766
  args: config.claudeArgs,
2770
3767
  codeExecutablePath: config.claudeCodeExecutablePath,
2771
3768
  requestTimeoutMs: config.claudeRequestTimeoutMs,
2772
- cwd: config.cwd
3769
+ cwd: config.cwd,
3770
+ plugins: config.claudePlugins
2773
3771
  }), createDshClaudePlugin(ctx, config)]);
2774
3772
  } catch (error) {
2775
3773
  await release();