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.
- package/README.md +98 -13
- package/README.zh.md +85 -12
- package/docs/spec/claude-local-plugins.md +56 -0
- package/docs/spec/claude-native-session-import.md +114 -0
- package/docs/spec/claude-search-tools.md +40 -0
- package/docs/spec/claude-tool-output-redaction.md +68 -0
- package/lib/client.js +575 -0
- package/lib/client.js.map +1 -1
- package/lib/host-plugin.js +1032 -34
- package/lib/host-plugin.js.map +1 -1
- package/package.json +4 -4
package/lib/host-plugin.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
1511
|
-
collectMatches(matches, visible,
|
|
1512
|
-
collectMatches(matches, visible,
|
|
1513
|
-
collectMatches(matches, visible, new RegExp(
|
|
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
|
-
|
|
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: [
|
|
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
|
|
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();
|