relay-dsh-plugin-claude 0.1.0
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 +202 -0
- package/README.zh.md +191 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/dsh-new-session-backends.jpg +0 -0
- package/lib/client.js +177 -0
- package/lib/client.js.map +1 -0
- package/lib/host-plugin.js +2262 -0
- package/lib/host-plugin.js.map +1 -0
- package/lib/typert.host.js +16 -0
- package/lib/typert.host.js.map +1 -0
- package/package.json +42 -0
- package/presets/relay-claude/.relay-managed +1 -0
- package/presets/relay-claude/agent.cordis.yml +4 -0
- package/presets/relay-claude/preset.yml +3 -0
|
@@ -0,0 +1,2262 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { KNOWN_SESSION_EVENT_TYPES } from "@deepseek-ai/dsh-session";
|
|
11
|
+
import { LlmAdapter } from "@deepseek-ai/dsh-llm";
|
|
12
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { cp, mkdir, stat } from "node:fs/promises";
|
|
14
|
+
//#region internal/plugin-sdk.mjs
|
|
15
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
16
|
+
const PLUGIN_ID_PATTERN = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
17
|
+
const CAPABILITY_ID_PATTERN = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
18
|
+
var CapabilityRegistry = class {
|
|
19
|
+
#entries = /* @__PURE__ */ new Map();
|
|
20
|
+
register(name, version, value, providerId) {
|
|
21
|
+
assertCapabilityName(name);
|
|
22
|
+
assertSemanticVersion(version, `capability ${name}`);
|
|
23
|
+
if (this.#entries.has(name)) throw new Error(`capability ${name} is already available`);
|
|
24
|
+
this.#entries.set(name, Object.freeze({
|
|
25
|
+
name,
|
|
26
|
+
version,
|
|
27
|
+
value,
|
|
28
|
+
providerId
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
unregisterProvider(providerId) {
|
|
32
|
+
for (const [name, entry] of this.#entries) if (entry.providerId === providerId) this.#entries.delete(name);
|
|
33
|
+
}
|
|
34
|
+
require(name, range = "*") {
|
|
35
|
+
const entry = this.#entries.get(name);
|
|
36
|
+
if (!entry) throw new Error(`capability ${name} is not available`);
|
|
37
|
+
if (!satisfiesVersion(entry.version, range)) throw new Error(`capability ${name} ${entry.version} does not satisfy ${range}`);
|
|
38
|
+
return entry.value;
|
|
39
|
+
}
|
|
40
|
+
optional(name, range = "*") {
|
|
41
|
+
if (!this.#entries.has(name)) return void 0;
|
|
42
|
+
return this.require(name, range);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var PluginHost = class {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.capabilities = new CapabilityRegistry();
|
|
48
|
+
this.active = [];
|
|
49
|
+
this.disposed = false;
|
|
50
|
+
}
|
|
51
|
+
async activate(definitions) {
|
|
52
|
+
if (this.active.length > 0) throw new Error("plugin host is already active");
|
|
53
|
+
if (this.disposed) throw new Error("plugin host is disposed");
|
|
54
|
+
const ordered = resolveActivationOrder(definitions);
|
|
55
|
+
let current = null;
|
|
56
|
+
try {
|
|
57
|
+
for (const definition of ordered) {
|
|
58
|
+
const access = createCapabilityAccess(definition.manifest, this.capabilities);
|
|
59
|
+
const cleanups = [];
|
|
60
|
+
let acceptingCleanups = true;
|
|
61
|
+
const defer = (cleanup) => {
|
|
62
|
+
assert.equal(typeof cleanup, "function", `plugin ${definition.manifest.id} cleanup must be a function`);
|
|
63
|
+
assert.ok(acceptingCleanups, `plugin ${definition.manifest.id} cannot defer cleanup after activation`);
|
|
64
|
+
cleanups.push(cleanup);
|
|
65
|
+
return cleanup;
|
|
66
|
+
};
|
|
67
|
+
current = {
|
|
68
|
+
id: definition.manifest.id,
|
|
69
|
+
cleanups
|
|
70
|
+
};
|
|
71
|
+
let activation;
|
|
72
|
+
try {
|
|
73
|
+
activation = await definition.activate(Object.freeze({
|
|
74
|
+
plugin: definition.manifest,
|
|
75
|
+
capabilities: access,
|
|
76
|
+
defer
|
|
77
|
+
})) ?? {};
|
|
78
|
+
} finally {
|
|
79
|
+
acceptingCleanups = false;
|
|
80
|
+
}
|
|
81
|
+
if (typeof activation.dispose === "function") cleanups.push(activation.dispose);
|
|
82
|
+
const provided = activation.capabilities ?? {};
|
|
83
|
+
validateProvidedCapabilities(definition.manifest, provided);
|
|
84
|
+
for (const [name, version] of Object.entries(definition.manifest.provides)) this.capabilities.register(name, version, provided[name], definition.manifest.id);
|
|
85
|
+
this.active.push(current);
|
|
86
|
+
current = null;
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const rollbackErrors = [];
|
|
90
|
+
if (current) {
|
|
91
|
+
this.capabilities.unregisterProvider(current.id);
|
|
92
|
+
rollbackErrors.push(...await disposeCleanups(current.cleanups));
|
|
93
|
+
}
|
|
94
|
+
rollbackErrors.push(...await this.#drainActive());
|
|
95
|
+
if (rollbackErrors.length > 0) throw new AggregateError([error, ...rollbackErrors], `plugin activation failed: ${error?.message ?? error}; rollback also failed`, { cause: error });
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
async dispose() {
|
|
101
|
+
if (this.disposed) return;
|
|
102
|
+
this.disposed = true;
|
|
103
|
+
await this.#disposeActive();
|
|
104
|
+
}
|
|
105
|
+
async #disposeActive() {
|
|
106
|
+
const errors = await this.#drainActive();
|
|
107
|
+
if (errors.length === 1) throw errors[0];
|
|
108
|
+
if (errors.length > 1) throw new AggregateError(errors, "multiple plugin cleanup operations failed");
|
|
109
|
+
}
|
|
110
|
+
async #drainActive() {
|
|
111
|
+
const errors = [];
|
|
112
|
+
while (this.active.length > 0) {
|
|
113
|
+
const plugin = this.active.pop();
|
|
114
|
+
try {
|
|
115
|
+
errors.push(...await disposeCleanups(plugin.cleanups));
|
|
116
|
+
} finally {
|
|
117
|
+
this.capabilities.unregisterProvider(plugin.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
async function disposeCleanups(cleanups) {
|
|
124
|
+
const errors = [];
|
|
125
|
+
for (const cleanup of cleanups.reverse()) try {
|
|
126
|
+
await cleanup();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
errors.push(error);
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
function definePlugin(definition) {
|
|
133
|
+
assert.equal(typeof definition?.activate, "function", "plugin activate must be a function");
|
|
134
|
+
const manifest = validateManifest(definition.manifest);
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
manifest,
|
|
137
|
+
activate: definition.activate
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function validateManifest(input) {
|
|
141
|
+
assert.ok(input && typeof input === "object" && !Array.isArray(input), "plugin manifest is required");
|
|
142
|
+
assert.match(input.id ?? "", PLUGIN_ID_PATTERN, "plugin id must be lowercase and stable");
|
|
143
|
+
assertSemanticVersion(input.version, `plugin ${input.id}`);
|
|
144
|
+
const provides = validateCapabilityMap(input.provides, "provides", { ranges: false });
|
|
145
|
+
const requires = validateCapabilityMap(input.requires, "requires", { ranges: true });
|
|
146
|
+
const optional = validateCapabilityMap(input.optional, "optional", { ranges: true });
|
|
147
|
+
for (const name of Object.keys(requires)) assert.ok(!(name in optional), `capability ${name} cannot be both required and optional`);
|
|
148
|
+
const permissions = input.permissions ?? [];
|
|
149
|
+
assert.ok(Array.isArray(permissions), "plugin permissions must be an array");
|
|
150
|
+
assert.ok(permissions.every((permission) => typeof permission === "string" && permission.length > 0), "plugin permissions must contain non-empty strings");
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
id: input.id,
|
|
153
|
+
version: input.version,
|
|
154
|
+
provides: Object.freeze(provides),
|
|
155
|
+
requires: Object.freeze(requires),
|
|
156
|
+
optional: Object.freeze(optional),
|
|
157
|
+
permissions: Object.freeze([...permissions])
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function satisfiesVersion(version, range) {
|
|
161
|
+
const current = parseVersion(version);
|
|
162
|
+
if (range === "*" || range === void 0) return true;
|
|
163
|
+
if (SEMVER_PATTERN.test(range)) return compareVersions(current, parseVersion(range)) === 0;
|
|
164
|
+
const majorWildcard = /^(0|[1-9]\d*)\.x$/.exec(range);
|
|
165
|
+
if (majorWildcard) return current.major === Number(majorWildcard[1]);
|
|
166
|
+
if (range.startsWith("^")) {
|
|
167
|
+
const minimum = parseVersion(range.slice(1));
|
|
168
|
+
const upper = minimum.major > 0 ? {
|
|
169
|
+
major: minimum.major + 1,
|
|
170
|
+
minor: 0,
|
|
171
|
+
patch: 0
|
|
172
|
+
} : minimum.minor > 0 ? {
|
|
173
|
+
major: 0,
|
|
174
|
+
minor: minimum.minor + 1,
|
|
175
|
+
patch: 0
|
|
176
|
+
} : {
|
|
177
|
+
major: 0,
|
|
178
|
+
minor: 0,
|
|
179
|
+
patch: minimum.patch + 1
|
|
180
|
+
};
|
|
181
|
+
return compareVersions(current, minimum) >= 0 && compareVersions(current, upper) < 0;
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`unsupported semantic version range ${range}`);
|
|
184
|
+
}
|
|
185
|
+
function resolveActivationOrder(definitions) {
|
|
186
|
+
assert.ok(Array.isArray(definitions), "plugin definitions must be an array");
|
|
187
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
188
|
+
const providers = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const definition of definitions) {
|
|
190
|
+
assert.ok(definition?.manifest && typeof definition.activate === "function", "invalid plugin definition");
|
|
191
|
+
const manifest = validateManifest(definition.manifest);
|
|
192
|
+
if (plugins.has(manifest.id)) throw new Error(`duplicate plugin id ${manifest.id}`);
|
|
193
|
+
plugins.set(manifest.id, definition);
|
|
194
|
+
for (const [name, version] of Object.entries(manifest.provides)) {
|
|
195
|
+
if (providers.has(name)) throw new Error(`capability ${name} is provided by both ${providers.get(name).id} and ${manifest.id}`);
|
|
196
|
+
providers.set(name, {
|
|
197
|
+
id: manifest.id,
|
|
198
|
+
version
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const dependencies = new Map([...plugins.keys()].map((id) => [id, /* @__PURE__ */ new Set()]));
|
|
203
|
+
for (const definition of plugins.values()) {
|
|
204
|
+
const { manifest } = definition;
|
|
205
|
+
for (const [name, range] of Object.entries(manifest.requires)) {
|
|
206
|
+
const provider = providers.get(name);
|
|
207
|
+
if (!provider || !satisfiesVersion(provider.version, range)) {
|
|
208
|
+
const found = provider ? ` (found ${provider.version})` : "";
|
|
209
|
+
throw new Error(`plugin ${manifest.id} requires ${name} ${range}${found}`);
|
|
210
|
+
}
|
|
211
|
+
dependencies.get(manifest.id).add(provider.id);
|
|
212
|
+
}
|
|
213
|
+
for (const [name, range] of Object.entries(manifest.optional)) {
|
|
214
|
+
const provider = providers.get(name);
|
|
215
|
+
if (!provider) continue;
|
|
216
|
+
if (!satisfiesVersion(provider.version, range)) throw new Error(`plugin ${manifest.id} optional capability ${name} requires ${range} (found ${provider.version})`);
|
|
217
|
+
dependencies.get(manifest.id).add(provider.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const ordered = [];
|
|
221
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
222
|
+
const visited = /* @__PURE__ */ new Set();
|
|
223
|
+
const visit = (id) => {
|
|
224
|
+
if (visiting.has(id)) throw new Error(`plugin dependency cycle includes ${id}`);
|
|
225
|
+
if (visited.has(id)) return;
|
|
226
|
+
visiting.add(id);
|
|
227
|
+
for (const dependency of dependencies.get(id)) visit(dependency);
|
|
228
|
+
visiting.delete(id);
|
|
229
|
+
visited.add(id);
|
|
230
|
+
ordered.push(plugins.get(id));
|
|
231
|
+
};
|
|
232
|
+
for (const id of plugins.keys()) visit(id);
|
|
233
|
+
return ordered;
|
|
234
|
+
}
|
|
235
|
+
function createCapabilityAccess(manifest, registry) {
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
require(name) {
|
|
238
|
+
const range = manifest.requires[name];
|
|
239
|
+
if (!range) throw new Error(`plugin ${manifest.id} did not declare required capability ${name}`);
|
|
240
|
+
return registry.require(name, range);
|
|
241
|
+
},
|
|
242
|
+
optional(name) {
|
|
243
|
+
const range = manifest.optional[name];
|
|
244
|
+
if (!range) throw new Error(`plugin ${manifest.id} did not declare optional capability ${name}`);
|
|
245
|
+
return registry.optional(name, range);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function validateProvidedCapabilities(manifest, provided) {
|
|
250
|
+
assert.ok(provided && typeof provided === "object" && !Array.isArray(provided), `plugin ${manifest.id} capabilities must be an object`);
|
|
251
|
+
const expected = Object.keys(manifest.provides).sort();
|
|
252
|
+
const actual = Object.keys(provided).sort();
|
|
253
|
+
assert.deepEqual(actual, expected, `plugin ${manifest.id} provided capabilities do not match its manifest`);
|
|
254
|
+
for (const name of expected) assert.notEqual(provided[name], void 0, `plugin ${manifest.id} did not provide ${name}`);
|
|
255
|
+
}
|
|
256
|
+
function validateCapabilityMap(input, label, { ranges }) {
|
|
257
|
+
const map = input ?? {};
|
|
258
|
+
assert.ok(map && typeof map === "object" && !Array.isArray(map), `plugin ${label} must be an object`);
|
|
259
|
+
const result = {};
|
|
260
|
+
for (const [name, version] of Object.entries(map)) {
|
|
261
|
+
assertCapabilityName(name);
|
|
262
|
+
if (ranges) satisfiesVersion("0.0.0", version);
|
|
263
|
+
else assertSemanticVersion(version, `capability ${name}`);
|
|
264
|
+
result[name] = version;
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function assertCapabilityName(name) {
|
|
269
|
+
assert.match(name ?? "", CAPABILITY_ID_PATTERN, "capability id must be lowercase and stable");
|
|
270
|
+
}
|
|
271
|
+
function assertSemanticVersion(version, label) {
|
|
272
|
+
assert.match(version ?? "", SEMVER_PATTERN, `${label} must use a semantic version`);
|
|
273
|
+
}
|
|
274
|
+
function parseVersion(version) {
|
|
275
|
+
assertSemanticVersion(version, "version");
|
|
276
|
+
const [, major, minor, patch] = SEMVER_PATTERN.exec(version);
|
|
277
|
+
return {
|
|
278
|
+
major: Number(major),
|
|
279
|
+
minor: Number(minor),
|
|
280
|
+
patch: Number(patch)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function compareVersions(left, right) {
|
|
284
|
+
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region cli-client.mjs
|
|
288
|
+
const DEFAULT_MODELS$2 = [
|
|
289
|
+
{
|
|
290
|
+
id: "sonnet",
|
|
291
|
+
displayName: "Claude Sonnet",
|
|
292
|
+
isDefault: true,
|
|
293
|
+
defaultReasoningEffort: "medium"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "opus",
|
|
297
|
+
displayName: "Claude Opus",
|
|
298
|
+
isDefault: false,
|
|
299
|
+
defaultReasoningEffort: "high"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "haiku",
|
|
303
|
+
displayName: "Claude Haiku",
|
|
304
|
+
isDefault: false,
|
|
305
|
+
defaultReasoningEffort: "low"
|
|
306
|
+
}
|
|
307
|
+
];
|
|
308
|
+
var ClaudeCliClient = class extends EventEmitter {
|
|
309
|
+
constructor({ command = "claude", args = [], requestTimeoutMs = 30 * 6e4 } = {}) {
|
|
310
|
+
super();
|
|
311
|
+
this.command = command;
|
|
312
|
+
this.args = args;
|
|
313
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
314
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
315
|
+
this.processes = /* @__PURE__ */ new Map();
|
|
316
|
+
this.closed = false;
|
|
317
|
+
}
|
|
318
|
+
async start() {
|
|
319
|
+
this.closed = false;
|
|
320
|
+
}
|
|
321
|
+
async listModels() {
|
|
322
|
+
return DEFAULT_MODELS$2;
|
|
323
|
+
}
|
|
324
|
+
async createSession(config = {}) {
|
|
325
|
+
const id = config.sessionId ?? randomUUID();
|
|
326
|
+
const session = {
|
|
327
|
+
id,
|
|
328
|
+
cwd: config.cwd ?? process.cwd(),
|
|
329
|
+
created: false,
|
|
330
|
+
config: structuredClone(config)
|
|
331
|
+
};
|
|
332
|
+
this.sessions.set(id, session);
|
|
333
|
+
return {
|
|
334
|
+
id,
|
|
335
|
+
cwd: session.cwd,
|
|
336
|
+
turns: []
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async resumeSession(sessionId, config = {}) {
|
|
340
|
+
const existing = this.sessions.get(sessionId) ?? {
|
|
341
|
+
id: sessionId,
|
|
342
|
+
created: true
|
|
343
|
+
};
|
|
344
|
+
const session = {
|
|
345
|
+
...existing,
|
|
346
|
+
cwd: config.cwd ?? existing.cwd ?? process.cwd(),
|
|
347
|
+
config: {
|
|
348
|
+
...existing.config,
|
|
349
|
+
...config
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
this.sessions.set(sessionId, session);
|
|
353
|
+
return {
|
|
354
|
+
id: sessionId,
|
|
355
|
+
cwd: session.cwd,
|
|
356
|
+
turns: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async sendMessage(sessionId, message = {}) {
|
|
360
|
+
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");
|
|
361
|
+
const session = this.sessions.get(sessionId) ?? await this.resumeSession(sessionId, message);
|
|
362
|
+
const turnId = randomUUID();
|
|
363
|
+
const child = this.spawnTurn(session, turnId, message);
|
|
364
|
+
this.processes.set(turnId, child);
|
|
365
|
+
return {
|
|
366
|
+
id: turnId,
|
|
367
|
+
status: "inProgress",
|
|
368
|
+
items: []
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async interruptTurn(_sessionId, turnId) {
|
|
372
|
+
const child = this.processes.get(turnId);
|
|
373
|
+
if (!child) return;
|
|
374
|
+
child.kill("SIGTERM");
|
|
375
|
+
}
|
|
376
|
+
async releaseSession(sessionId) {
|
|
377
|
+
for (const [turnId, child] of this.processes) if (child.relayClaudeSessionId === sessionId) {
|
|
378
|
+
child.kill("SIGTERM");
|
|
379
|
+
this.processes.delete(turnId);
|
|
380
|
+
}
|
|
381
|
+
this.sessions.delete(sessionId);
|
|
382
|
+
}
|
|
383
|
+
async close() {
|
|
384
|
+
this.closed = true;
|
|
385
|
+
for (const child of this.processes.values()) child.kill("SIGTERM");
|
|
386
|
+
this.processes.clear();
|
|
387
|
+
}
|
|
388
|
+
spawnTurn(session, turnId, message) {
|
|
389
|
+
const settingSourceArgs = settingSourceArguments(message.settingSources ?? session.config?.settingSources);
|
|
390
|
+
const systemPromptArgs = systemPromptArguments(message.systemPrompt ?? session.config?.systemPrompt);
|
|
391
|
+
const cliArgs = [
|
|
392
|
+
...this.args,
|
|
393
|
+
"-p",
|
|
394
|
+
message.text,
|
|
395
|
+
"--output-format",
|
|
396
|
+
"stream-json",
|
|
397
|
+
"--verbose",
|
|
398
|
+
"--include-partial-messages",
|
|
399
|
+
"--model",
|
|
400
|
+
message.model ?? session.config?.model ?? "sonnet",
|
|
401
|
+
"--effort",
|
|
402
|
+
message.effort ?? session.config?.effort ?? "medium",
|
|
403
|
+
"--permission-mode",
|
|
404
|
+
permissionMode(message),
|
|
405
|
+
...settingSourceArgs,
|
|
406
|
+
...systemPromptArgs,
|
|
407
|
+
...session.created ? ["--resume", session.id] : ["--session-id", session.id]
|
|
408
|
+
];
|
|
409
|
+
const child = spawn(this.command, cliArgs, {
|
|
410
|
+
cwd: message.cwd ?? session.cwd ?? process.cwd(),
|
|
411
|
+
stdio: [
|
|
412
|
+
"ignore",
|
|
413
|
+
"pipe",
|
|
414
|
+
"pipe"
|
|
415
|
+
],
|
|
416
|
+
env: {
|
|
417
|
+
...process.env,
|
|
418
|
+
...message.env ?? {}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
child.relayClaudeSessionId = session.id;
|
|
422
|
+
session.created = true;
|
|
423
|
+
const state = {
|
|
424
|
+
textItemId: null,
|
|
425
|
+
text: "",
|
|
426
|
+
activities: /* @__PURE__ */ new Set()
|
|
427
|
+
};
|
|
428
|
+
readline.createInterface({ input: child.stdout }).on("line", (line) => this.handleLine(session.id, turnId, line, state));
|
|
429
|
+
child.stderr.setEncoding("utf8");
|
|
430
|
+
child.stderr.on("data", (chunk) => this.emit("diagnostic", String(chunk)));
|
|
431
|
+
child.once("error", (error) => {
|
|
432
|
+
this.emit("diagnostic", `Claude CLI failed: ${error.message}`);
|
|
433
|
+
this.completeTurn(session.id, turnId, "failed", error);
|
|
434
|
+
});
|
|
435
|
+
child.once("exit", (code, signal) => {
|
|
436
|
+
this.processes.delete(turnId);
|
|
437
|
+
if (signal || code) this.completeTurn(session.id, turnId, "failed", /* @__PURE__ */ new Error(`claude exited (${signal ?? code})`));
|
|
438
|
+
else this.completeTurn(session.id, turnId, "completed");
|
|
439
|
+
});
|
|
440
|
+
return child;
|
|
441
|
+
}
|
|
442
|
+
handleLine(sessionId, turnId, line, state) {
|
|
443
|
+
let message;
|
|
444
|
+
try {
|
|
445
|
+
message = JSON.parse(line);
|
|
446
|
+
} catch {
|
|
447
|
+
this.emitText(sessionId, turnId, state, line);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const event of normalizeClaudeStreamMessage(message, state)) this.emit("activity", {
|
|
451
|
+
method: event.method,
|
|
452
|
+
params: {
|
|
453
|
+
sessionId,
|
|
454
|
+
turnId,
|
|
455
|
+
...event.params
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
emitText(sessionId, turnId, state, text) {
|
|
460
|
+
if (!text) return;
|
|
461
|
+
state.textItemId ??= `answer-${turnId}`;
|
|
462
|
+
this.emit("activity", {
|
|
463
|
+
method: "item/agentMessage/delta",
|
|
464
|
+
params: {
|
|
465
|
+
sessionId,
|
|
466
|
+
turnId,
|
|
467
|
+
itemId: state.textItemId,
|
|
468
|
+
delta: `${text}\n`
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
completeTurn(sessionId, turnId, status, error = null) {
|
|
473
|
+
this.emit("activity", {
|
|
474
|
+
method: "turn/completed",
|
|
475
|
+
params: {
|
|
476
|
+
sessionId,
|
|
477
|
+
turn: {
|
|
478
|
+
id: turnId,
|
|
479
|
+
status,
|
|
480
|
+
error: error ? { message: error.message } : null,
|
|
481
|
+
items: []
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
function normalizeClaudeStreamMessage(message, state) {
|
|
488
|
+
const events = [];
|
|
489
|
+
const content = message.message?.content ?? message.content ?? [];
|
|
490
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
491
|
+
if (block.type === "text" && block.text) {
|
|
492
|
+
state.textItemId ??= block.id ?? `answer-${message.message?.id ?? "latest"}`;
|
|
493
|
+
const delta = block.text.startsWith(state.text) ? block.text.slice(state.text.length) : block.text;
|
|
494
|
+
state.text = block.text;
|
|
495
|
+
if (delta) events.push({
|
|
496
|
+
method: "item/agentMessage/delta",
|
|
497
|
+
params: {
|
|
498
|
+
itemId: state.textItemId,
|
|
499
|
+
delta
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if ((block.type === "text_delta" || block.type === "content_block_delta") && (block.text ?? block.delta?.text)) {
|
|
504
|
+
state.textItemId ??= block.id ?? `answer-${message.message?.id ?? "latest"}`;
|
|
505
|
+
const delta = block.text ?? block.delta.text;
|
|
506
|
+
state.text += delta;
|
|
507
|
+
events.push({
|
|
508
|
+
method: "item/agentMessage/delta",
|
|
509
|
+
params: {
|
|
510
|
+
itemId: state.textItemId,
|
|
511
|
+
delta
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (block.type === "thinking" && block.thinking) events.push({
|
|
516
|
+
method: "item/reasoning/summaryTextDelta",
|
|
517
|
+
params: {
|
|
518
|
+
itemId: block.id ?? "reasoning",
|
|
519
|
+
delta: block.thinking
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
if (block.type === "tool_use") {
|
|
523
|
+
const item = {
|
|
524
|
+
type: "toolUse",
|
|
525
|
+
id: block.id ?? block.name,
|
|
526
|
+
name: block.name,
|
|
527
|
+
input: block.input,
|
|
528
|
+
status: "inProgress"
|
|
529
|
+
};
|
|
530
|
+
if (!state.activities.has(item.id)) {
|
|
531
|
+
state.activities.add(item.id);
|
|
532
|
+
events.push({
|
|
533
|
+
method: "item/started",
|
|
534
|
+
params: { item }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (block.type === "tool_result") events.push({
|
|
539
|
+
method: "item/completed",
|
|
540
|
+
params: { item: {
|
|
541
|
+
type: "toolUse",
|
|
542
|
+
id: block.tool_use_id ?? block.id,
|
|
543
|
+
name: block.name,
|
|
544
|
+
output: block.content,
|
|
545
|
+
status: block.is_error ? "failed" : "completed"
|
|
546
|
+
} }
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (message.type === "result" && message.result) {
|
|
550
|
+
state.textItemId ??= `answer-${message.session_id ?? "latest"}`;
|
|
551
|
+
const delta = String(message.result).startsWith(state.text) ? String(message.result).slice(state.text.length) : String(message.result);
|
|
552
|
+
state.text = String(message.result);
|
|
553
|
+
if (delta) events.push({
|
|
554
|
+
method: "item/agentMessage/delta",
|
|
555
|
+
params: {
|
|
556
|
+
itemId: state.textItemId,
|
|
557
|
+
delta
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return events;
|
|
562
|
+
}
|
|
563
|
+
function permissionMode(message) {
|
|
564
|
+
if (message.permissionMode) return message.permissionMode;
|
|
565
|
+
if (message.approvalPolicy === "never") return "plan";
|
|
566
|
+
if (message.sandbox === "read-only") return "plan";
|
|
567
|
+
return "manual";
|
|
568
|
+
}
|
|
569
|
+
function settingSourceArguments(value) {
|
|
570
|
+
if (Array.isArray(value) && value.length === 0) return ["--safe-mode"];
|
|
571
|
+
if (Array.isArray(value)) return ["--setting-sources", value.join(",")];
|
|
572
|
+
if (typeof value === "string" && value.trim()) return ["--setting-sources", value];
|
|
573
|
+
return ["--setting-sources", "user,project,local"];
|
|
574
|
+
}
|
|
575
|
+
function systemPromptArguments(value) {
|
|
576
|
+
if (typeof value === "string" && value.trim()) return ["--system-prompt", value];
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region sdk-client.mjs
|
|
581
|
+
const DEFAULT_MODELS$1 = [
|
|
582
|
+
{
|
|
583
|
+
id: "sonnet",
|
|
584
|
+
displayName: "Claude Sonnet",
|
|
585
|
+
isDefault: true,
|
|
586
|
+
defaultReasoningEffort: "medium",
|
|
587
|
+
supportedReasoningEfforts: reasoningEfforts()
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
id: "opus",
|
|
591
|
+
displayName: "Claude Opus",
|
|
592
|
+
isDefault: false,
|
|
593
|
+
defaultReasoningEffort: "high",
|
|
594
|
+
supportedReasoningEfforts: reasoningEfforts()
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
id: "haiku",
|
|
598
|
+
displayName: "Claude Haiku",
|
|
599
|
+
isDefault: false,
|
|
600
|
+
defaultReasoningEffort: "low",
|
|
601
|
+
supportedReasoningEfforts: reasoningEfforts()
|
|
602
|
+
}
|
|
603
|
+
];
|
|
604
|
+
function reasoningEfforts() {
|
|
605
|
+
return [
|
|
606
|
+
"low",
|
|
607
|
+
"medium",
|
|
608
|
+
"high"
|
|
609
|
+
].map((reasoningEffort) => ({ reasoningEffort }));
|
|
610
|
+
}
|
|
611
|
+
var ClaudeSdkClient = class extends EventEmitter {
|
|
612
|
+
constructor({ sdk = null, pathToClaudeCodeExecutable = void 0, requestTimeoutMs = 30 * 6e4 } = {}) {
|
|
613
|
+
super();
|
|
614
|
+
this.sdk = sdk;
|
|
615
|
+
this.pathToClaudeCodeExecutable = pathToClaudeCodeExecutable;
|
|
616
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
617
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
618
|
+
this.queries = /* @__PURE__ */ new Map();
|
|
619
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
620
|
+
this.closed = false;
|
|
621
|
+
}
|
|
622
|
+
async start() {
|
|
623
|
+
this.sdk ??= await import("@anthropic-ai/claude-agent-sdk");
|
|
624
|
+
if (typeof this.sdk.query !== "function") throw new Error("Claude Agent SDK query() is unavailable");
|
|
625
|
+
this.closed = false;
|
|
626
|
+
}
|
|
627
|
+
async listModels() {
|
|
628
|
+
return DEFAULT_MODELS$1;
|
|
629
|
+
}
|
|
630
|
+
async createSession(config = {}) {
|
|
631
|
+
const id = config.sessionId ?? randomUUID();
|
|
632
|
+
this.sessions.set(id, {
|
|
633
|
+
id,
|
|
634
|
+
cwd: config.cwd ?? process.cwd(),
|
|
635
|
+
created: false,
|
|
636
|
+
config: structuredClone(config)
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
id,
|
|
640
|
+
cwd: config.cwd ?? process.cwd(),
|
|
641
|
+
turns: []
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async resumeSession(sessionId, config = {}) {
|
|
645
|
+
const existing = this.sessions.get(sessionId) ?? {
|
|
646
|
+
id: sessionId,
|
|
647
|
+
created: true,
|
|
648
|
+
config: {}
|
|
649
|
+
};
|
|
650
|
+
this.sessions.set(sessionId, {
|
|
651
|
+
...existing,
|
|
652
|
+
cwd: config.cwd ?? existing.cwd ?? process.cwd(),
|
|
653
|
+
config: {
|
|
654
|
+
...existing.config,
|
|
655
|
+
...structuredClone(config)
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return {
|
|
659
|
+
id: sessionId,
|
|
660
|
+
cwd: config.cwd ?? existing.cwd ?? process.cwd(),
|
|
661
|
+
turns: []
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
async sendMessage(sessionId, message = {}) {
|
|
665
|
+
const session = this.sessions.get(sessionId) ?? await this.resumeSession(sessionId, message);
|
|
666
|
+
const turnId = randomUUID();
|
|
667
|
+
const abortController = new AbortController();
|
|
668
|
+
const options = this.queryOptions(session, message, abortController);
|
|
669
|
+
const query = this.sdk.query({
|
|
670
|
+
prompt: message.text,
|
|
671
|
+
options
|
|
672
|
+
});
|
|
673
|
+
this.queries.set(turnId, {
|
|
674
|
+
query,
|
|
675
|
+
abortController,
|
|
676
|
+
sessionId
|
|
677
|
+
});
|
|
678
|
+
this.consumeQuery(session, turnId, query).catch((error) => {
|
|
679
|
+
this.emit("diagnostic", `Claude SDK query failed: ${error?.stack ?? error}`);
|
|
680
|
+
this.completeTurn(session.id, turnId, "failed", error);
|
|
681
|
+
});
|
|
682
|
+
session.created = true;
|
|
683
|
+
return {
|
|
684
|
+
id: turnId,
|
|
685
|
+
status: "inProgress",
|
|
686
|
+
items: []
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async interruptTurn(_sessionId, turnId) {
|
|
690
|
+
const record = this.queries.get(turnId);
|
|
691
|
+
if (!record) return;
|
|
692
|
+
await record.query.interrupt?.().catch(() => {});
|
|
693
|
+
record.abortController.abort();
|
|
694
|
+
record.query.close?.();
|
|
695
|
+
}
|
|
696
|
+
async releaseSession(sessionId) {
|
|
697
|
+
for (const [turnId, record] of this.queries) if (record.sessionId === sessionId) {
|
|
698
|
+
record.abortController.abort();
|
|
699
|
+
record.query.close?.();
|
|
700
|
+
this.queries.delete(turnId);
|
|
701
|
+
}
|
|
702
|
+
this.sessions.delete(sessionId);
|
|
703
|
+
}
|
|
704
|
+
async close() {
|
|
705
|
+
this.closed = true;
|
|
706
|
+
for (const record of this.queries.values()) {
|
|
707
|
+
record.abortController.abort();
|
|
708
|
+
record.query.close?.();
|
|
709
|
+
}
|
|
710
|
+
this.queries.clear();
|
|
711
|
+
for (const request of this.pendingRequests.values()) request.resolve({
|
|
712
|
+
behavior: "deny",
|
|
713
|
+
message: "Relay Claude SDK client closed"
|
|
714
|
+
});
|
|
715
|
+
this.pendingRequests.clear();
|
|
716
|
+
}
|
|
717
|
+
resolveRequest(requestId, response = {}) {
|
|
718
|
+
const request = this.pendingRequests.get(String(requestId));
|
|
719
|
+
if (!request) throw new Error(`unknown pending Claude request ${requestId}`);
|
|
720
|
+
this.pendingRequests.delete(String(requestId));
|
|
721
|
+
request.resolve(responseForRequest(request, response));
|
|
722
|
+
}
|
|
723
|
+
rejectRequest(requestId, error) {
|
|
724
|
+
const request = this.pendingRequests.get(String(requestId));
|
|
725
|
+
if (!request) return;
|
|
726
|
+
this.pendingRequests.delete(String(requestId));
|
|
727
|
+
request.resolve({
|
|
728
|
+
behavior: "deny",
|
|
729
|
+
message: error?.message ?? String(error)
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
queryOptions(session, message, abortController) {
|
|
733
|
+
return {
|
|
734
|
+
abortController,
|
|
735
|
+
cwd: message.cwd ?? session.cwd ?? process.cwd(),
|
|
736
|
+
model: message.model ?? session.config?.model,
|
|
737
|
+
effort: message.effort ?? session.config?.effort,
|
|
738
|
+
permissionMode: sdkPermissionMode(message),
|
|
739
|
+
settingSources: message.settingSources ?? session.config?.settingSources ?? [
|
|
740
|
+
"user",
|
|
741
|
+
"project",
|
|
742
|
+
"local"
|
|
743
|
+
],
|
|
744
|
+
systemPrompt: message.systemPrompt ?? session.config?.systemPrompt,
|
|
745
|
+
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
|
746
|
+
includePartialMessages: true,
|
|
747
|
+
...session.created ? { resume: session.id } : { sessionId: session.id },
|
|
748
|
+
canUseTool: (toolName, input, options) => this.requestPermission(session.id, toolName, input, options),
|
|
749
|
+
...dshMcpOptions(this.sdk, message.dshTools, message.executeDshTool, abortController.signal)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
requestPermission(sessionId, toolName, input, options = {}) {
|
|
753
|
+
const id = options.requestId ?? randomUUID();
|
|
754
|
+
return new Promise((resolve) => {
|
|
755
|
+
const request = {
|
|
756
|
+
id,
|
|
757
|
+
method: toolName === "AskUserQuestion" ? "tool/requestUserInput" : "tool/requestApproval",
|
|
758
|
+
signal: options.signal,
|
|
759
|
+
params: {
|
|
760
|
+
sessionId,
|
|
761
|
+
toolName,
|
|
762
|
+
input: structuredClone(input ?? {}),
|
|
763
|
+
title: options.title,
|
|
764
|
+
displayName: options.displayName,
|
|
765
|
+
description: options.description,
|
|
766
|
+
decisionReason: options.decisionReason,
|
|
767
|
+
blockedPath: options.blockedPath,
|
|
768
|
+
toolUseID: options.toolUseID,
|
|
769
|
+
suggestions: structuredClone(options.suggestions ?? [])
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
this.pendingRequests.set(String(id), {
|
|
773
|
+
request,
|
|
774
|
+
resolve,
|
|
775
|
+
input
|
|
776
|
+
});
|
|
777
|
+
options.signal?.addEventListener("abort", () => this.rejectRequest(id, /* @__PURE__ */ new Error("Claude permission request was cancelled")), { once: true });
|
|
778
|
+
this.emit("request", request);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
async consumeQuery(session, turnId, query) {
|
|
782
|
+
const state = {
|
|
783
|
+
currentMessageId: null,
|
|
784
|
+
text: /* @__PURE__ */ new Map(),
|
|
785
|
+
reasoning: /* @__PURE__ */ new Map(),
|
|
786
|
+
activities: /* @__PURE__ */ new Set()
|
|
787
|
+
};
|
|
788
|
+
let completed = false;
|
|
789
|
+
try {
|
|
790
|
+
for await (const message of query) {
|
|
791
|
+
for (const event of normalizeSdkMessage(message, state)) this.emit("activity", {
|
|
792
|
+
method: event.method,
|
|
793
|
+
params: {
|
|
794
|
+
sessionId: session.id,
|
|
795
|
+
turnId,
|
|
796
|
+
...event.params
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
if (message.type === "result") {
|
|
800
|
+
completed = true;
|
|
801
|
+
this.completeTurn(session.id, turnId, message.is_error ? "failed" : "completed", resultError(message));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (!completed) this.completeTurn(session.id, turnId, "completed");
|
|
805
|
+
} finally {
|
|
806
|
+
this.queries.delete(turnId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
completeTurn(sessionId, turnId, status, error = null) {
|
|
810
|
+
this.emit("activity", {
|
|
811
|
+
method: "turn/completed",
|
|
812
|
+
params: {
|
|
813
|
+
sessionId,
|
|
814
|
+
turn: {
|
|
815
|
+
id: turnId,
|
|
816
|
+
status,
|
|
817
|
+
error: error ? { message: error.message } : null,
|
|
818
|
+
items: []
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
function dshMcpOptions(sdk, schemas, execute, signal) {
|
|
825
|
+
if (!Array.isArray(schemas) || schemas.length === 0) return {};
|
|
826
|
+
if (typeof execute !== "function") throw new Error("Claude DSH tools require an execution callback");
|
|
827
|
+
if (typeof sdk.createSdkMcpServer !== "function" || typeof sdk.tool !== "function") throw new Error("This Claude Agent SDK does not support in-process DSH tools");
|
|
828
|
+
const tools = schemas.map((schema) => sdk.tool(schema.name, schema.description, jsonSchemaShape(schema.parameters), async (args, extra = {}) => dshToolResult(await execute({
|
|
829
|
+
name: schema.name,
|
|
830
|
+
arguments: args,
|
|
831
|
+
callId: extra.toolUseID ?? extra.toolUseId ?? randomUUID(),
|
|
832
|
+
signal: extra.signal ?? signal
|
|
833
|
+
}))));
|
|
834
|
+
return {
|
|
835
|
+
mcpServers: { dsh: sdk.createSdkMcpServer({
|
|
836
|
+
name: "dsh",
|
|
837
|
+
version: "1.0.0",
|
|
838
|
+
tools,
|
|
839
|
+
alwaysLoad: true
|
|
840
|
+
}) },
|
|
841
|
+
allowedTools: schemas.map((schema) => `mcp__dsh__${schema.name}`)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function jsonSchemaShape(schema) {
|
|
845
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object" || schema.properties === null) {
|
|
846
|
+
if (schema?.type === "object" && schema.properties === void 0) return {};
|
|
847
|
+
throw new Error("DSH tool parameters must use an object JSON Schema");
|
|
848
|
+
}
|
|
849
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
850
|
+
return Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
|
|
851
|
+
let field;
|
|
852
|
+
try {
|
|
853
|
+
field = z.fromJSONSchema(property);
|
|
854
|
+
} catch {
|
|
855
|
+
field = z.unknown();
|
|
856
|
+
}
|
|
857
|
+
return [name, required.has(name) ? field : field.optional()];
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
function dshToolResult(result) {
|
|
861
|
+
const content = (result.content ?? []).map((block) => {
|
|
862
|
+
if (block?.type === "text") return {
|
|
863
|
+
type: "text",
|
|
864
|
+
text: String(block.text ?? "")
|
|
865
|
+
};
|
|
866
|
+
if (block?.type === "image" && typeof block.data === "string" && typeof block.mediaType === "string") return {
|
|
867
|
+
type: "image",
|
|
868
|
+
data: block.data,
|
|
869
|
+
mimeType: block.mediaType
|
|
870
|
+
};
|
|
871
|
+
return {
|
|
872
|
+
type: "text",
|
|
873
|
+
text: JSON.stringify(block)
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
if (content.length === 0) content.push({
|
|
877
|
+
type: "text",
|
|
878
|
+
text: result.isError ? "DSH tool failed" : "DSH tool completed."
|
|
879
|
+
});
|
|
880
|
+
return {
|
|
881
|
+
content,
|
|
882
|
+
isError: Boolean(result.isError)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function normalizeSdkMessage(message, state) {
|
|
886
|
+
const events = [];
|
|
887
|
+
if (message.type === "stream_event") {
|
|
888
|
+
const event = message.event;
|
|
889
|
+
if (event?.type === "message_start") {
|
|
890
|
+
state.currentMessageId = event.message?.id ?? message.uuid ?? null;
|
|
891
|
+
return events;
|
|
892
|
+
}
|
|
893
|
+
if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
894
|
+
const itemId = streamItemId(state, "text", event.index);
|
|
895
|
+
state.text.set(itemId, `${state.text.get(itemId) ?? ""}${event.delta.text}`);
|
|
896
|
+
events.push({
|
|
897
|
+
method: "item/agentMessage/delta",
|
|
898
|
+
params: {
|
|
899
|
+
itemId,
|
|
900
|
+
delta: event.delta.text
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (event?.type === "content_block_delta" && event.delta?.type === "thinking_delta") {
|
|
905
|
+
const itemId = streamItemId(state, "reason", event.index);
|
|
906
|
+
state.reasoning.set(itemId, `${state.reasoning.get(itemId) ?? ""}${event.delta.thinking}`);
|
|
907
|
+
events.push({
|
|
908
|
+
method: "item/reasoning/summaryTextDelta",
|
|
909
|
+
params: {
|
|
910
|
+
itemId,
|
|
911
|
+
delta: event.delta.thinking
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return events;
|
|
916
|
+
}
|
|
917
|
+
if (message.type === "assistant") {
|
|
918
|
+
const content = message.message?.content ?? [];
|
|
919
|
+
for (const [index, block] of content.entries()) {
|
|
920
|
+
if (block.type === "text" && block.text) {
|
|
921
|
+
const itemId = block.id ?? messageItemId(state.text, message, "text", content, index);
|
|
922
|
+
const previous = state.text.get(itemId) ?? "";
|
|
923
|
+
const delta = block.text.startsWith(previous) ? block.text.slice(previous.length) : block.text;
|
|
924
|
+
state.text.set(itemId, block.text);
|
|
925
|
+
if (delta) events.push({
|
|
926
|
+
method: "item/agentMessage/delta",
|
|
927
|
+
params: {
|
|
928
|
+
itemId,
|
|
929
|
+
delta
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
if (block.type === "thinking" && block.thinking) {
|
|
934
|
+
const itemId = block.id ?? messageItemId(state.reasoning, message, "reason", content, index);
|
|
935
|
+
const previous = state.reasoning.get(itemId) ?? "";
|
|
936
|
+
const delta = block.thinking.startsWith(previous) ? block.thinking.slice(previous.length) : block.thinking;
|
|
937
|
+
state.reasoning.set(itemId, block.thinking);
|
|
938
|
+
if (delta) events.push({
|
|
939
|
+
method: "item/reasoning/summaryTextDelta",
|
|
940
|
+
params: {
|
|
941
|
+
itemId,
|
|
942
|
+
delta
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (block.type === "tool_use") {
|
|
947
|
+
const item = {
|
|
948
|
+
type: "toolUse",
|
|
949
|
+
id: block.id,
|
|
950
|
+
name: block.name,
|
|
951
|
+
input: block.input,
|
|
952
|
+
status: "inProgress"
|
|
953
|
+
};
|
|
954
|
+
if (!state.activities.has(item.id)) {
|
|
955
|
+
state.activities.add(item.id);
|
|
956
|
+
events.push({
|
|
957
|
+
method: "item/started",
|
|
958
|
+
params: { item }
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (message.type === "user") for (const block of message.message?.content ?? []) {
|
|
965
|
+
if (block.type !== "tool_result") continue;
|
|
966
|
+
events.push({
|
|
967
|
+
method: "item/completed",
|
|
968
|
+
params: { item: {
|
|
969
|
+
type: "toolUse",
|
|
970
|
+
id: block.tool_use_id,
|
|
971
|
+
output: block.content,
|
|
972
|
+
status: block.is_error ? "failed" : "completed"
|
|
973
|
+
} }
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (message.type === "system" && message.subtype === "permission_denied") events.push({
|
|
977
|
+
method: "item/completed",
|
|
978
|
+
params: { item: {
|
|
979
|
+
type: "toolUse",
|
|
980
|
+
id: message.tool_use_id,
|
|
981
|
+
name: message.tool_name,
|
|
982
|
+
output: message.message,
|
|
983
|
+
status: "failed"
|
|
984
|
+
} }
|
|
985
|
+
});
|
|
986
|
+
return events;
|
|
987
|
+
}
|
|
988
|
+
function streamItemId(state, type, index) {
|
|
989
|
+
return `${state.currentMessageId ?? "message"}-${type}-${index ?? 0}`;
|
|
990
|
+
}
|
|
991
|
+
function messageItemId(items, message, type, content, index) {
|
|
992
|
+
const prefix = `${message.message?.id ?? message.uuid ?? "message"}-${type}-`;
|
|
993
|
+
const ordinal = content.slice(0, index).filter((block) => block.type === (type === "reason" ? "thinking" : type)).length;
|
|
994
|
+
return [...items.keys()].filter((itemId) => itemId.startsWith(prefix)).sort((left, right) => Number(left.slice(prefix.length)) - Number(right.slice(prefix.length)))[ordinal] ?? `${prefix}${index}`;
|
|
995
|
+
}
|
|
996
|
+
function responseForRequest(pending, response) {
|
|
997
|
+
if (response.action === "accept" || response.action === "allow") return {
|
|
998
|
+
behavior: "allow",
|
|
999
|
+
updatedInput: response.updatedInput ?? pending.input
|
|
1000
|
+
};
|
|
1001
|
+
if (response.action === "answer") return {
|
|
1002
|
+
behavior: "allow",
|
|
1003
|
+
updatedInput: {
|
|
1004
|
+
...pending.input,
|
|
1005
|
+
answers: response.answers ?? {}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
return {
|
|
1009
|
+
behavior: "deny",
|
|
1010
|
+
message: response.message ?? "User declined this Claude tool request"
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function resultError(message) {
|
|
1014
|
+
if (!message?.is_error) return null;
|
|
1015
|
+
return new Error(message.errors?.join("\n") || message.subtype || "Claude SDK turn failed");
|
|
1016
|
+
}
|
|
1017
|
+
function sdkPermissionMode(message) {
|
|
1018
|
+
if (message.permissionMode) return message.permissionMode;
|
|
1019
|
+
if (message.approvalPolicy === "never") return "dontAsk";
|
|
1020
|
+
if (message.sandbox === "read-only") return "plan";
|
|
1021
|
+
return "default";
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
1024
|
+
//#region session-runtime.mjs
|
|
1025
|
+
const DEFAULT_MODELS = [
|
|
1026
|
+
{
|
|
1027
|
+
id: "sonnet",
|
|
1028
|
+
displayName: "Claude Sonnet",
|
|
1029
|
+
description: "Claude Code default balanced model",
|
|
1030
|
+
isDefault: true,
|
|
1031
|
+
defaultReasoningEffort: "medium",
|
|
1032
|
+
supportedReasoningEfforts: [
|
|
1033
|
+
{ reasoningEffort: "low" },
|
|
1034
|
+
{ reasoningEffort: "medium" },
|
|
1035
|
+
{ reasoningEffort: "high" }
|
|
1036
|
+
]
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
id: "opus",
|
|
1040
|
+
displayName: "Claude Opus",
|
|
1041
|
+
description: "Claude Code high-capability model",
|
|
1042
|
+
isDefault: false,
|
|
1043
|
+
defaultReasoningEffort: "high",
|
|
1044
|
+
supportedReasoningEfforts: [{ reasoningEffort: "medium" }, { reasoningEffort: "high" }]
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
id: "haiku",
|
|
1048
|
+
displayName: "Claude Haiku",
|
|
1049
|
+
description: "Claude Code fast model",
|
|
1050
|
+
isDefault: false,
|
|
1051
|
+
defaultReasoningEffort: "low",
|
|
1052
|
+
supportedReasoningEfforts: [{ reasoningEffort: "low" }, { reasoningEffort: "medium" }]
|
|
1053
|
+
}
|
|
1054
|
+
];
|
|
1055
|
+
var ClaudeSessionRuntime = class extends EventEmitter {
|
|
1056
|
+
constructor({ client = new ClaudeCliClient(), cwd = process.cwd() } = {}) {
|
|
1057
|
+
super();
|
|
1058
|
+
this.client = client;
|
|
1059
|
+
this.cwd = cwd;
|
|
1060
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
1061
|
+
this.models = DEFAULT_MODELS;
|
|
1062
|
+
this.selectedSessionId = null;
|
|
1063
|
+
this.diagnostics = [];
|
|
1064
|
+
this.closed = false;
|
|
1065
|
+
this.client.on?.("activity", (message) => this.handleActivity(message));
|
|
1066
|
+
this.client.on?.("request", (request) => this.emit("request", request));
|
|
1067
|
+
this.client.on?.("diagnostic", (message) => this.addDiagnostic(message));
|
|
1068
|
+
this.client.on?.("exit", (details) => {
|
|
1069
|
+
this.addDiagnostic(`Claude backend exited: ${JSON.stringify(details)}`);
|
|
1070
|
+
this.emitChange();
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
async initialize() {
|
|
1074
|
+
await this.client.start?.();
|
|
1075
|
+
const models = await this.client.listModels?.().catch((error) => {
|
|
1076
|
+
this.addDiagnostic(`Claude model list failed: ${error.message}`);
|
|
1077
|
+
return null;
|
|
1078
|
+
});
|
|
1079
|
+
if (Array.isArray(models) && models.length > 0) this.models = models;
|
|
1080
|
+
this.emitChange();
|
|
1081
|
+
return this.snapshot();
|
|
1082
|
+
}
|
|
1083
|
+
async createSession({ model, effort, sandbox = "workspace-write", approvalPolicy = "on-request", cwd = this.cwd, ephemeral = false, settingSources = [
|
|
1084
|
+
"user",
|
|
1085
|
+
"project",
|
|
1086
|
+
"local"
|
|
1087
|
+
], systemPrompt = {
|
|
1088
|
+
type: "preset",
|
|
1089
|
+
preset: "claude_code"
|
|
1090
|
+
} } = {}) {
|
|
1091
|
+
const selectedModel = model ?? this.models.find((candidate) => candidate.isDefault)?.id ?? "sonnet";
|
|
1092
|
+
const selectedEffort = effort ?? this.models.find((candidate) => candidate.id === selectedModel)?.defaultReasoningEffort ?? "medium";
|
|
1093
|
+
const created = await this.client.createSession?.({
|
|
1094
|
+
model: selectedModel,
|
|
1095
|
+
effort: selectedEffort,
|
|
1096
|
+
sandbox,
|
|
1097
|
+
approvalPolicy,
|
|
1098
|
+
cwd,
|
|
1099
|
+
ephemeral,
|
|
1100
|
+
settingSources,
|
|
1101
|
+
systemPrompt
|
|
1102
|
+
});
|
|
1103
|
+
const session = this.upsertSession(created ?? {}, {
|
|
1104
|
+
id: created?.id,
|
|
1105
|
+
model: selectedModel,
|
|
1106
|
+
effort: selectedEffort,
|
|
1107
|
+
sandbox,
|
|
1108
|
+
approvalPolicy,
|
|
1109
|
+
cwd,
|
|
1110
|
+
ephemeral
|
|
1111
|
+
});
|
|
1112
|
+
if (!session.ephemeral) this.selectedSessionId = session.id;
|
|
1113
|
+
this.emitChange();
|
|
1114
|
+
return publicSession(session);
|
|
1115
|
+
}
|
|
1116
|
+
async resumeSession(sessionId, defaults = {}) {
|
|
1117
|
+
if (!sessionId?.trim()) throw new Error("sessionId is required");
|
|
1118
|
+
const resumed = await this.client.resumeSession?.(sessionId, {
|
|
1119
|
+
cwd: defaults.cwd ?? this.cwd,
|
|
1120
|
+
...defaults
|
|
1121
|
+
});
|
|
1122
|
+
const session = this.upsertSession(resumed ?? {}, {
|
|
1123
|
+
id: sessionId,
|
|
1124
|
+
...defaults
|
|
1125
|
+
});
|
|
1126
|
+
if (Array.isArray(resumed?.turns) && resumed.turns.length > 0) session.turns = structuredClone(resumed.turns);
|
|
1127
|
+
if (!session.ephemeral) this.selectedSessionId = sessionId;
|
|
1128
|
+
this.emitChange();
|
|
1129
|
+
return publicSession(session);
|
|
1130
|
+
}
|
|
1131
|
+
async sendMessage(sessionId, { text, model, effort, sandbox, approvalPolicy, cwd } = {}) {
|
|
1132
|
+
const session = this.requireSession(sessionId);
|
|
1133
|
+
if (!text?.trim()) throw new Error("message text is required");
|
|
1134
|
+
const next = {
|
|
1135
|
+
model: model ?? session.model,
|
|
1136
|
+
effort: effort ?? session.effort,
|
|
1137
|
+
sandbox: sandbox ?? session.sandbox,
|
|
1138
|
+
approvalPolicy: approvalPolicy ?? session.approvalPolicy,
|
|
1139
|
+
cwd: cwd ?? session.cwd
|
|
1140
|
+
};
|
|
1141
|
+
Object.assign(session, next, { updatedAt: nowSeconds() });
|
|
1142
|
+
const turn = await this.client.sendMessage(sessionId, {
|
|
1143
|
+
text,
|
|
1144
|
+
...next
|
|
1145
|
+
});
|
|
1146
|
+
this.ensureTurn(session, turn);
|
|
1147
|
+
this.emitChange();
|
|
1148
|
+
return structuredClone(turn);
|
|
1149
|
+
}
|
|
1150
|
+
async interruptTurn(sessionId, turnId) {
|
|
1151
|
+
await this.client.interruptTurn?.(sessionId, turnId);
|
|
1152
|
+
}
|
|
1153
|
+
async resolveRequest(requestId, response = {}) {
|
|
1154
|
+
if (typeof this.client.resolveRequest !== "function") throw new Error("Claude client does not support interactive request resolution");
|
|
1155
|
+
this.client.resolveRequest(requestId, response);
|
|
1156
|
+
}
|
|
1157
|
+
rejectRequest(requestId, error) {
|
|
1158
|
+
this.client.rejectRequest?.(requestId, error);
|
|
1159
|
+
}
|
|
1160
|
+
async releaseSession(sessionId) {
|
|
1161
|
+
if (!sessionId) return;
|
|
1162
|
+
await this.client.releaseSession?.(sessionId).catch((error) => {
|
|
1163
|
+
this.addDiagnostic(`Claude session release failed for ${sessionId}: ${error.message}`);
|
|
1164
|
+
});
|
|
1165
|
+
this.sessions.delete(sessionId);
|
|
1166
|
+
if (this.selectedSessionId === sessionId) this.selectedSessionId = null;
|
|
1167
|
+
this.emitChange();
|
|
1168
|
+
}
|
|
1169
|
+
getSession(sessionId) {
|
|
1170
|
+
const session = this.sessions.get(sessionId);
|
|
1171
|
+
return session ? publicSession(session) : null;
|
|
1172
|
+
}
|
|
1173
|
+
snapshot() {
|
|
1174
|
+
return {
|
|
1175
|
+
connected: !this.closed,
|
|
1176
|
+
selectedSessionId: this.selectedSessionId,
|
|
1177
|
+
cwd: this.cwd,
|
|
1178
|
+
models: structuredClone(this.models),
|
|
1179
|
+
sessions: [...this.sessions.values()].sort((left, right) => right.updatedAt - left.updatedAt).map((session) => publicSession(session)),
|
|
1180
|
+
diagnostics: this.diagnostics.slice(-20)
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
async close() {
|
|
1184
|
+
if (this.closed) return;
|
|
1185
|
+
this.closed = true;
|
|
1186
|
+
await this.client.close?.();
|
|
1187
|
+
}
|
|
1188
|
+
handleActivity(message) {
|
|
1189
|
+
const params = message.params ?? {};
|
|
1190
|
+
const sessionId = params.sessionId ?? params.session?.id ?? null;
|
|
1191
|
+
const session = sessionId ? this.sessions.get(sessionId) : null;
|
|
1192
|
+
if (message.method === "turn/completed" && session) {
|
|
1193
|
+
const turn = params.turn;
|
|
1194
|
+
if (turn?.id) this.ensureTurn(session, turn);
|
|
1195
|
+
}
|
|
1196
|
+
this.emit("activity", message);
|
|
1197
|
+
this.emitChange();
|
|
1198
|
+
}
|
|
1199
|
+
upsertSession(input = {}, defaults = {}) {
|
|
1200
|
+
const id = input.id ?? defaults.id;
|
|
1201
|
+
if (!id) throw new Error("Claude session id is required");
|
|
1202
|
+
const existing = this.sessions.get(id) ?? {
|
|
1203
|
+
id,
|
|
1204
|
+
turns: [],
|
|
1205
|
+
createdAt: nowSeconds()
|
|
1206
|
+
};
|
|
1207
|
+
Object.assign(existing, {
|
|
1208
|
+
model: defaults.model ?? input.model ?? existing.model ?? this.models.find((candidate) => candidate.isDefault)?.id ?? "sonnet",
|
|
1209
|
+
effort: defaults.effort ?? input.effort ?? existing.effort ?? "medium",
|
|
1210
|
+
sandbox: defaults.sandbox ?? input.sandbox ?? existing.sandbox ?? "workspace-write",
|
|
1211
|
+
approvalPolicy: defaults.approvalPolicy ?? input.approvalPolicy ?? existing.approvalPolicy ?? "on-request",
|
|
1212
|
+
cwd: defaults.cwd ?? input.cwd ?? existing.cwd ?? this.cwd,
|
|
1213
|
+
ephemeral: Boolean(defaults.ephemeral ?? input.ephemeral ?? existing.ephemeral),
|
|
1214
|
+
updatedAt: input.updatedAt ?? nowSeconds()
|
|
1215
|
+
});
|
|
1216
|
+
if (Array.isArray(input.turns) && input.turns.length > 0) existing.turns = structuredClone(input.turns);
|
|
1217
|
+
this.sessions.set(id, existing);
|
|
1218
|
+
return existing;
|
|
1219
|
+
}
|
|
1220
|
+
requireSession(sessionId) {
|
|
1221
|
+
const session = this.sessions.get(sessionId);
|
|
1222
|
+
if (!session) throw new Error(`unknown Claude session ${sessionId}`);
|
|
1223
|
+
return session;
|
|
1224
|
+
}
|
|
1225
|
+
ensureTurn(session, turn) {
|
|
1226
|
+
if (!turn?.id) return;
|
|
1227
|
+
const existing = session.turns.findIndex((candidate) => candidate.id === turn.id);
|
|
1228
|
+
if (existing >= 0) {
|
|
1229
|
+
if (session.turns[existing].status !== "inProgress" && turn.status === "inProgress") return;
|
|
1230
|
+
session.turns[existing] = structuredClone(turn);
|
|
1231
|
+
} else session.turns.push(structuredClone(turn));
|
|
1232
|
+
session.updatedAt = nowSeconds();
|
|
1233
|
+
}
|
|
1234
|
+
addDiagnostic(message) {
|
|
1235
|
+
this.diagnostics.push(String(message));
|
|
1236
|
+
this.diagnostics.splice(0, Math.max(0, this.diagnostics.length - 50));
|
|
1237
|
+
}
|
|
1238
|
+
emitChange() {
|
|
1239
|
+
this.emit("change", this.snapshot());
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
function publicSession(session) {
|
|
1243
|
+
return structuredClone({
|
|
1244
|
+
id: session.id,
|
|
1245
|
+
model: session.model,
|
|
1246
|
+
effort: session.effort,
|
|
1247
|
+
sandbox: session.sandbox,
|
|
1248
|
+
approvalPolicy: session.approvalPolicy,
|
|
1249
|
+
cwd: session.cwd,
|
|
1250
|
+
ephemeral: session.ephemeral,
|
|
1251
|
+
turns: session.turns ?? []
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function nowSeconds() {
|
|
1255
|
+
return Date.now() / 1e3;
|
|
1256
|
+
}
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region plugin.mjs
|
|
1259
|
+
const CLAUDE_EXECUTION_CAPABILITY = "relay.execution.claude.v1";
|
|
1260
|
+
function createClaudeExecutionPlugin(config = {}) {
|
|
1261
|
+
return definePlugin({
|
|
1262
|
+
manifest: {
|
|
1263
|
+
id: "relay.execution.claude",
|
|
1264
|
+
version: "1.0.0",
|
|
1265
|
+
provides: { [CLAUDE_EXECUTION_CAPABILITY]: "1.0.0" },
|
|
1266
|
+
optional: { "relay.logging.v1": "^1.0.0" },
|
|
1267
|
+
permissions: ["process:claude", "filesystem:workspace"]
|
|
1268
|
+
},
|
|
1269
|
+
activate({ capabilities, defer }) {
|
|
1270
|
+
const logger = capabilities.optional("relay.logging.v1") ?? console;
|
|
1271
|
+
const runtime = new ClaudeSessionRuntime({
|
|
1272
|
+
client: config.client ?? createClaudeClient(config),
|
|
1273
|
+
cwd: config.cwd ?? process.cwd()
|
|
1274
|
+
});
|
|
1275
|
+
defer(() => runtime.close());
|
|
1276
|
+
const ready = runtime.initialize();
|
|
1277
|
+
ready.catch((error) => {
|
|
1278
|
+
logger.error?.(`Relay Claude backend failed to initialize: ${error?.stack ?? error}`);
|
|
1279
|
+
});
|
|
1280
|
+
return { capabilities: { [CLAUDE_EXECUTION_CAPABILITY]: executionCapability(runtime, ready) } };
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function executionCapability(runtime, ready) {
|
|
1285
|
+
return Object.freeze({
|
|
1286
|
+
whenReady: () => ready,
|
|
1287
|
+
listModels: () => structuredClone(runtime.models),
|
|
1288
|
+
hasSession: (sessionId) => runtime.sessions.has(sessionId),
|
|
1289
|
+
getSession: runtime.getSession.bind(runtime),
|
|
1290
|
+
patchSession(sessionId, patch) {
|
|
1291
|
+
const session = runtime.sessions.get(sessionId);
|
|
1292
|
+
if (session) Object.assign(session, structuredClone(patch));
|
|
1293
|
+
return Boolean(session);
|
|
1294
|
+
},
|
|
1295
|
+
createSession: runtime.createSession.bind(runtime),
|
|
1296
|
+
resumeSession: runtime.resumeSession.bind(runtime),
|
|
1297
|
+
sendMessage: runtime.sendMessage.bind(runtime),
|
|
1298
|
+
interruptTurn: runtime.interruptTurn.bind(runtime),
|
|
1299
|
+
releaseSession: runtime.releaseSession.bind(runtime),
|
|
1300
|
+
resolveRequest: runtime.resolveRequest.bind(runtime),
|
|
1301
|
+
rejectRequest: runtime.rejectRequest.bind(runtime),
|
|
1302
|
+
subscribeActivity: (listener) => subscribe(runtime, "activity", listener),
|
|
1303
|
+
subscribeRequest: (listener) => subscribe(runtime, "request", listener)
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
function createClaudeClient(config) {
|
|
1307
|
+
const backend = config.backend ?? "auto";
|
|
1308
|
+
if (backend === "cli") return createClaudeCliClient(config);
|
|
1309
|
+
const sdkClient = new ClaudeSdkClient({
|
|
1310
|
+
pathToClaudeCodeExecutable: config.codeExecutablePath,
|
|
1311
|
+
requestTimeoutMs: positiveInteger(config.requestTimeoutMs, 30 * 6e4)
|
|
1312
|
+
});
|
|
1313
|
+
if (backend === "sdk") return sdkClient;
|
|
1314
|
+
return new FallbackClaudeClient({
|
|
1315
|
+
primary: sdkClient,
|
|
1316
|
+
fallback: createClaudeCliClient(config)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function createClaudeCliClient(config) {
|
|
1320
|
+
return new ClaudeCliClient({
|
|
1321
|
+
command: config.command ?? "claude",
|
|
1322
|
+
args: config.args ?? [],
|
|
1323
|
+
requestTimeoutMs: positiveInteger(config.requestTimeoutMs, 30 * 6e4)
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
var FallbackClaudeClient = class extends ClaudeCliClient {
|
|
1327
|
+
constructor({ primary, fallback }) {
|
|
1328
|
+
super();
|
|
1329
|
+
this.primary = primary;
|
|
1330
|
+
this.fallback = fallback;
|
|
1331
|
+
this.active = primary;
|
|
1332
|
+
for (const event of [
|
|
1333
|
+
"activity",
|
|
1334
|
+
"request",
|
|
1335
|
+
"diagnostic",
|
|
1336
|
+
"exit"
|
|
1337
|
+
]) {
|
|
1338
|
+
primary.on(event, (...args) => this.emit(event, ...args));
|
|
1339
|
+
fallback.on(event, (...args) => this.emit(event, ...args));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async start() {
|
|
1343
|
+
try {
|
|
1344
|
+
await this.primary.start();
|
|
1345
|
+
this.active = this.primary;
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
this.emit("diagnostic", `Claude Agent SDK unavailable; falling back to CLI: ${error.message}`);
|
|
1348
|
+
await this.fallback.start();
|
|
1349
|
+
this.active = this.fallback;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
listModels(...args) {
|
|
1353
|
+
return this.active.listModels(...args);
|
|
1354
|
+
}
|
|
1355
|
+
createSession(...args) {
|
|
1356
|
+
return this.active.createSession(...args);
|
|
1357
|
+
}
|
|
1358
|
+
resumeSession(...args) {
|
|
1359
|
+
return this.active.resumeSession(...args);
|
|
1360
|
+
}
|
|
1361
|
+
sendMessage(...args) {
|
|
1362
|
+
return this.active.sendMessage(...args);
|
|
1363
|
+
}
|
|
1364
|
+
interruptTurn(...args) {
|
|
1365
|
+
return this.active.interruptTurn(...args);
|
|
1366
|
+
}
|
|
1367
|
+
releaseSession(...args) {
|
|
1368
|
+
return this.active.releaseSession(...args);
|
|
1369
|
+
}
|
|
1370
|
+
resolveRequest(...args) {
|
|
1371
|
+
return this.active.resolveRequest?.(...args);
|
|
1372
|
+
}
|
|
1373
|
+
rejectRequest(...args) {
|
|
1374
|
+
return this.active.rejectRequest?.(...args);
|
|
1375
|
+
}
|
|
1376
|
+
close(...args) {
|
|
1377
|
+
return this.active.close(...args);
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
function subscribe(emitter, event, listener) {
|
|
1381
|
+
emitter.on(event, listener);
|
|
1382
|
+
let active = true;
|
|
1383
|
+
return () => {
|
|
1384
|
+
if (!active) return;
|
|
1385
|
+
active = false;
|
|
1386
|
+
emitter.off(event, listener);
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function positiveInteger(value, fallback) {
|
|
1390
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
1391
|
+
}
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region claude-adapter.js
|
|
1394
|
+
const CLAUDE_PRESET = "relay-claude";
|
|
1395
|
+
const CLAUDE_PROVIDER = "relay-claude";
|
|
1396
|
+
const CLAUDE_ACTIVITY_EVENT = "relay-claude/activity";
|
|
1397
|
+
var ClaudeDshAdapter = class extends LlmAdapter {
|
|
1398
|
+
constructor({ runtime, ready, linkStore = null, logger = console }) {
|
|
1399
|
+
super();
|
|
1400
|
+
this.runtime = runtime;
|
|
1401
|
+
this.ready = ready;
|
|
1402
|
+
this.logger = logger;
|
|
1403
|
+
this.linkStore = linkStore;
|
|
1404
|
+
this.links = /* @__PURE__ */ new Map();
|
|
1405
|
+
this.settings = /* @__PURE__ */ new Map();
|
|
1406
|
+
this.pendingSessions = /* @__PURE__ */ new Map();
|
|
1407
|
+
this.agents = /* @__PURE__ */ new Map();
|
|
1408
|
+
for (const [sessionId, record] of linkStore?.entries() ?? []) {
|
|
1409
|
+
const claudeSessionId = record.claudeSessionId ?? record.sessionId ?? record.threadId;
|
|
1410
|
+
if (claudeSessionId) this.links.set(sessionId, claudeSessionId);
|
|
1411
|
+
this.settings.set(sessionId, record.config);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
providerInfo() {
|
|
1415
|
+
return {
|
|
1416
|
+
id: CLAUDE_PROVIDER,
|
|
1417
|
+
name: "Claude Code"
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
async listModels() {
|
|
1421
|
+
await this.ready;
|
|
1422
|
+
return runtimeModels(this.runtime).sort((left, right) => Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault))).map((model) => ({
|
|
1423
|
+
provider: CLAUDE_PROVIDER,
|
|
1424
|
+
id: model.id,
|
|
1425
|
+
name: model.displayName ?? model.id,
|
|
1426
|
+
description: model.description,
|
|
1427
|
+
inputModalities: ["text", "image"]
|
|
1428
|
+
}));
|
|
1429
|
+
}
|
|
1430
|
+
async resolveModel(provider, model) {
|
|
1431
|
+
await this.ready;
|
|
1432
|
+
const info = runtimeModels(this.runtime).find((candidate) => candidate.id === model);
|
|
1433
|
+
return {
|
|
1434
|
+
provider,
|
|
1435
|
+
id: model,
|
|
1436
|
+
name: info?.displayName ?? model,
|
|
1437
|
+
inputModalities: ["text", "image"],
|
|
1438
|
+
...Array.isArray(info?.supportedReasoningEfforts) ? { reasoning: {
|
|
1439
|
+
efforts: info.supportedReasoningEfforts.map((effort) => ({
|
|
1440
|
+
id: effort.reasoningEffort ?? effort.id ?? effort,
|
|
1441
|
+
name: reasoningEffortName(effort.reasoningEffort ?? effort.id ?? effort)
|
|
1442
|
+
})),
|
|
1443
|
+
defaultEffort: info.defaultReasoningEffort
|
|
1444
|
+
} } : {}
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
attachAgent(agent, requestedPreset = effectivePreset(agent.session)) {
|
|
1448
|
+
this.agents.set(String(agent.id), agent);
|
|
1449
|
+
if (requestedPreset !== "relay-claude") return false;
|
|
1450
|
+
this.configuration(agent.id, agent.session.header.cwd);
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
servesAgent(agent) {
|
|
1454
|
+
return effectivePreset(agent.session) === CLAUDE_PRESET;
|
|
1455
|
+
}
|
|
1456
|
+
detachAgent(sessionId) {
|
|
1457
|
+
this.agents.delete(String(sessionId));
|
|
1458
|
+
}
|
|
1459
|
+
configuration(sessionId, cwd) {
|
|
1460
|
+
const key = String(sessionId);
|
|
1461
|
+
const existing = this.settings.get(key);
|
|
1462
|
+
if (existing) return existing;
|
|
1463
|
+
const models = runtimeModels(this.runtime);
|
|
1464
|
+
const model = models.find((candidate) => candidate.isDefault) ?? models[0];
|
|
1465
|
+
const config = {
|
|
1466
|
+
model: model?.id ?? "sonnet",
|
|
1467
|
+
effort: model?.defaultReasoningEffort ?? "medium",
|
|
1468
|
+
sandbox: "workspace-write",
|
|
1469
|
+
approvalPolicy: "on-request",
|
|
1470
|
+
cwd: cwd ?? process.cwd(),
|
|
1471
|
+
settingSources: [
|
|
1472
|
+
"user",
|
|
1473
|
+
"project",
|
|
1474
|
+
"local"
|
|
1475
|
+
],
|
|
1476
|
+
systemPrompt: {
|
|
1477
|
+
type: "preset",
|
|
1478
|
+
preset: "claude_code"
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
this.settings.set(key, config);
|
|
1482
|
+
return config;
|
|
1483
|
+
}
|
|
1484
|
+
configure(sessionId, patch = {}) {
|
|
1485
|
+
const key = String(sessionId);
|
|
1486
|
+
const next = {
|
|
1487
|
+
...this.configuration(key),
|
|
1488
|
+
...compact(patch)
|
|
1489
|
+
};
|
|
1490
|
+
this.settings.set(key, next);
|
|
1491
|
+
const claudeSessionId = this.links.get(key);
|
|
1492
|
+
if (claudeSessionId) patchRuntimeSession(this.runtime, claudeSessionId, next);
|
|
1493
|
+
this.persistLink(key);
|
|
1494
|
+
return structuredClone(next);
|
|
1495
|
+
}
|
|
1496
|
+
async ensureSession(sessionId) {
|
|
1497
|
+
const key = String(sessionId);
|
|
1498
|
+
const pending = this.pendingSessions.get(key);
|
|
1499
|
+
if (pending) return pending;
|
|
1500
|
+
const operation = this.createOrResumeSession(key).finally(() => {
|
|
1501
|
+
this.pendingSessions.delete(key);
|
|
1502
|
+
});
|
|
1503
|
+
this.pendingSessions.set(key, operation);
|
|
1504
|
+
return operation;
|
|
1505
|
+
}
|
|
1506
|
+
async createOrResumeSession(sessionId) {
|
|
1507
|
+
await this.ready;
|
|
1508
|
+
const settings = { ...this.configuration(sessionId) };
|
|
1509
|
+
const linked = this.links.get(sessionId);
|
|
1510
|
+
if (linked && hasRuntimeSession(this.runtime, linked)) return linked;
|
|
1511
|
+
if (linked) try {
|
|
1512
|
+
await this.runtime.resumeSession(linked, settings);
|
|
1513
|
+
return linked;
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
this.logger.warn(`Relay could not resume Claude session ${linked}; creating a replacement: ${error.message}`);
|
|
1516
|
+
this.links.delete(sessionId);
|
|
1517
|
+
}
|
|
1518
|
+
const created = await this.runtime.createSession(settings);
|
|
1519
|
+
this.links.set(sessionId, created.id);
|
|
1520
|
+
this.persistLink(sessionId);
|
|
1521
|
+
return created.id;
|
|
1522
|
+
}
|
|
1523
|
+
persistLink(sessionId) {
|
|
1524
|
+
this.linkStore?.set(sessionId, {
|
|
1525
|
+
claudeSessionId: this.links.get(sessionId) ?? null,
|
|
1526
|
+
config: this.configuration(sessionId)
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
sessionFor(sessionId) {
|
|
1530
|
+
return this.links.get(String(sessionId)) ?? null;
|
|
1531
|
+
}
|
|
1532
|
+
dshSessionForClaudeSession(claudeSessionId) {
|
|
1533
|
+
for (const [sessionId, candidate] of this.links) if (candidate === claudeSessionId) return sessionId;
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
async *stream(options) {
|
|
1537
|
+
if (options.purpose) {
|
|
1538
|
+
yield* this.streamAuxiliary(options);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const sessionId = String(options.sessionId ?? "");
|
|
1542
|
+
if (!sessionId) throw new Error("Relay Claude adapter requires a DSH session id");
|
|
1543
|
+
const text = latestUserText(options.messages);
|
|
1544
|
+
if (!text) throw new Error("Relay Claude adapter received no user text");
|
|
1545
|
+
const agent = this.agents.get(sessionId);
|
|
1546
|
+
if (!agent) throw new Error(`Relay Claude adapter has no attached agent for ${sessionId}`);
|
|
1547
|
+
const nativePermissions = permissionConfiguration(agent.session.events);
|
|
1548
|
+
const config = this.configure(sessionId, {
|
|
1549
|
+
...options.provider === "relay-claude" ? { model: options.model } : {},
|
|
1550
|
+
...options.provider === "relay-claude" ? { effort: options.reasoningEffort } : {},
|
|
1551
|
+
...nativePermissions,
|
|
1552
|
+
cwd: agent.session.header.cwd
|
|
1553
|
+
});
|
|
1554
|
+
const dshTools = structuredClone(options.tools ?? []);
|
|
1555
|
+
const availableTools = new Set(dshTools.map((tool) => tool.name));
|
|
1556
|
+
const executeDshTool = async ({ name, arguments: args, callId, signal }) => {
|
|
1557
|
+
if (!availableTools.has(name)) throw new Error(`DSH tool ${name} is not available for this DSH turn.`);
|
|
1558
|
+
if (!agent.ctx?.tools?.execute) throw new Error("The owning DSH Agent has no tool runtime");
|
|
1559
|
+
return agent.ctx.tools.execute({
|
|
1560
|
+
callId,
|
|
1561
|
+
name,
|
|
1562
|
+
arguments: args,
|
|
1563
|
+
agent,
|
|
1564
|
+
signal: signal ?? options.signal ?? new AbortController().signal
|
|
1565
|
+
});
|
|
1566
|
+
};
|
|
1567
|
+
const claudeSessionId = await this.ensureSession(sessionId);
|
|
1568
|
+
const queue = new ActivityQueue(options.signal, "Claude");
|
|
1569
|
+
const onActivity = (message) => {
|
|
1570
|
+
if ((message.params?.sessionId ?? message.params?.session?.id) === claudeSessionId) queue.push(message);
|
|
1571
|
+
};
|
|
1572
|
+
const stopActivity = subscribeRuntimeActivity(this.runtime, onActivity);
|
|
1573
|
+
let turnId = null;
|
|
1574
|
+
try {
|
|
1575
|
+
turnId = (await this.runtime.sendMessage(claudeSessionId, {
|
|
1576
|
+
text,
|
|
1577
|
+
...config,
|
|
1578
|
+
dshTools,
|
|
1579
|
+
executeDshTool
|
|
1580
|
+
})).id;
|
|
1581
|
+
const state = createStreamState();
|
|
1582
|
+
let completedTurn = null;
|
|
1583
|
+
while (!completedTurn) {
|
|
1584
|
+
const message = await queue.next();
|
|
1585
|
+
const params = message.params ?? {};
|
|
1586
|
+
if (params.turnId && params.turnId !== turnId) continue;
|
|
1587
|
+
if (message.method === "turn/completed") {
|
|
1588
|
+
if (params.turn?.id !== turnId) continue;
|
|
1589
|
+
for (const item of params.turn.items ?? []) for (const chunk of this.completeItem(agent, claudeSessionId, turnId, item, state)) yield chunk;
|
|
1590
|
+
completedTurn = params.turn;
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
for (const chunk of this.projectActivity(agent, claudeSessionId, turnId, message, state)) yield chunk;
|
|
1594
|
+
}
|
|
1595
|
+
for (const block of state.blocks.values()) {
|
|
1596
|
+
if (block.closed) continue;
|
|
1597
|
+
block.closed = true;
|
|
1598
|
+
yield {
|
|
1599
|
+
type: "block-end",
|
|
1600
|
+
index: block.index,
|
|
1601
|
+
block: {
|
|
1602
|
+
type: block.type,
|
|
1603
|
+
text: block.text
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (completedTurn.status === "failed") yield {
|
|
1608
|
+
type: "finish",
|
|
1609
|
+
reason: {
|
|
1610
|
+
kind: "error",
|
|
1611
|
+
failure: {
|
|
1612
|
+
message: completedTurn.error?.message ?? "Claude turn failed",
|
|
1613
|
+
code: "CLAUDE_TURN_FAILED"
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
else yield {
|
|
1618
|
+
type: "finish",
|
|
1619
|
+
reason: { kind: "stop" },
|
|
1620
|
+
replayState: {
|
|
1621
|
+
claudeSessionId,
|
|
1622
|
+
turnId
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (options.signal?.aborted) {
|
|
1627
|
+
if (turnId) await this.runtime.interruptTurn(claudeSessionId, turnId).catch(() => {});
|
|
1628
|
+
yield {
|
|
1629
|
+
type: "finish",
|
|
1630
|
+
reason: {
|
|
1631
|
+
kind: "aborted",
|
|
1632
|
+
failure: {
|
|
1633
|
+
message: "Claude turn cancelled",
|
|
1634
|
+
code: "ABORTED"
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
throw error;
|
|
1641
|
+
} finally {
|
|
1642
|
+
stopActivity();
|
|
1643
|
+
queue.close();
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
async *streamAuxiliary(options) {
|
|
1647
|
+
await this.ready;
|
|
1648
|
+
const text = auxiliaryInput(options.messages);
|
|
1649
|
+
if (!text) throw new Error(`Relay Claude adapter received no ${options.purpose} input`);
|
|
1650
|
+
const sessionId = String(options.sessionId ?? "");
|
|
1651
|
+
const cwd = this.agents.get(sessionId)?.session.header.cwd ?? this.settings.get(sessionId)?.cwd ?? process.cwd();
|
|
1652
|
+
const claudeSessionId = (await this.runtime.createSession({
|
|
1653
|
+
model: options.model,
|
|
1654
|
+
effort: options.reasoningEffort,
|
|
1655
|
+
sandbox: "read-only",
|
|
1656
|
+
approvalPolicy: "never",
|
|
1657
|
+
cwd,
|
|
1658
|
+
ephemeral: true,
|
|
1659
|
+
settingSources: ["user"],
|
|
1660
|
+
systemPrompt: options.system
|
|
1661
|
+
})).id;
|
|
1662
|
+
const queue = new ActivityQueue(options.signal, "Claude");
|
|
1663
|
+
const onActivity = (message) => {
|
|
1664
|
+
if ((message.params?.sessionId ?? message.params?.session?.id) === claudeSessionId) queue.push(message);
|
|
1665
|
+
};
|
|
1666
|
+
const stopActivity = subscribeRuntimeActivity(this.runtime, onActivity);
|
|
1667
|
+
let turnId = null;
|
|
1668
|
+
try {
|
|
1669
|
+
turnId = (await this.runtime.sendMessage(claudeSessionId, {
|
|
1670
|
+
text,
|
|
1671
|
+
model: options.model,
|
|
1672
|
+
effort: options.reasoningEffort,
|
|
1673
|
+
sandbox: "read-only",
|
|
1674
|
+
approvalPolicy: "never"
|
|
1675
|
+
})).id;
|
|
1676
|
+
const state = createStreamState();
|
|
1677
|
+
let completedTurn = null;
|
|
1678
|
+
while (!completedTurn) {
|
|
1679
|
+
const message = await queue.next();
|
|
1680
|
+
const params = message.params ?? {};
|
|
1681
|
+
if (params.turnId && params.turnId !== turnId) continue;
|
|
1682
|
+
if (message.method === "turn/completed") {
|
|
1683
|
+
if (params.turn?.id !== turnId) continue;
|
|
1684
|
+
for (const item of params.turn.items ?? []) for (const chunk of completeAuxiliaryItem(state, item)) yield chunk;
|
|
1685
|
+
completedTurn = params.turn;
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
for (const chunk of projectAuxiliaryActivity(message, state)) yield chunk;
|
|
1689
|
+
}
|
|
1690
|
+
for (const block of state.blocks.values()) {
|
|
1691
|
+
if (block.closed) continue;
|
|
1692
|
+
block.closed = true;
|
|
1693
|
+
yield {
|
|
1694
|
+
type: "block-end",
|
|
1695
|
+
index: block.index,
|
|
1696
|
+
block: {
|
|
1697
|
+
type: block.type,
|
|
1698
|
+
text: block.text
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
yield completedTurn.status === "failed" ? {
|
|
1703
|
+
type: "finish",
|
|
1704
|
+
reason: {
|
|
1705
|
+
kind: "error",
|
|
1706
|
+
failure: {
|
|
1707
|
+
message: completedTurn.error?.message ?? `Claude ${options.purpose} failed`,
|
|
1708
|
+
code: "CLAUDE_AUXILIARY_FAILED"
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
} : {
|
|
1712
|
+
type: "finish",
|
|
1713
|
+
reason: { kind: "stop" }
|
|
1714
|
+
};
|
|
1715
|
+
} finally {
|
|
1716
|
+
stopActivity();
|
|
1717
|
+
queue.close();
|
|
1718
|
+
await this.runtime.releaseSession(claudeSessionId);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
projectActivity(agent, claudeSessionId, turnId, message, state) {
|
|
1722
|
+
const params = message.params ?? {};
|
|
1723
|
+
if (message.method === "item/reasoning/summaryTextDelta" || message.method === "item/reasoning/textDelta") return textDelta(state, params.itemId, "reasoning", params.delta ?? "");
|
|
1724
|
+
if (message.method === "item/agentMessage/delta") return textDelta(state, params.itemId, "text", params.delta ?? "");
|
|
1725
|
+
if (message.method === "item/started") {
|
|
1726
|
+
if (isActivityItem(params.item)) this.appendActivity(agent, claudeSessionId, turnId, params.item, "started", state);
|
|
1727
|
+
return [];
|
|
1728
|
+
}
|
|
1729
|
+
if (message.method === "item/completed") return this.completeItem(agent, claudeSessionId, turnId, params.item, state);
|
|
1730
|
+
return [];
|
|
1731
|
+
}
|
|
1732
|
+
completeItem(agent, claudeSessionId, turnId, item, state) {
|
|
1733
|
+
if (!item?.id || state.completed.has(item.id)) return [];
|
|
1734
|
+
state.completed.add(item.id);
|
|
1735
|
+
if (item.type === "reasoning") return completeTextItem(state, item.id, "reasoning", reasoningText(item));
|
|
1736
|
+
if (item.type === "agentMessage") return completeTextItem(state, item.id, "text", item.text ?? "");
|
|
1737
|
+
if (isActivityItem(item)) this.appendActivity(agent, claudeSessionId, turnId, item, "completed", state);
|
|
1738
|
+
return [];
|
|
1739
|
+
}
|
|
1740
|
+
appendActivity(agent, claudeSessionId, turnId, item, phase, state) {
|
|
1741
|
+
const previous = state.activityItems.get(item.id) ?? {};
|
|
1742
|
+
const merged = {
|
|
1743
|
+
...previous,
|
|
1744
|
+
...item,
|
|
1745
|
+
input: item.input ?? previous.input,
|
|
1746
|
+
arguments: item.arguments ?? previous.arguments,
|
|
1747
|
+
name: item.name ?? previous.name,
|
|
1748
|
+
tool: item.tool ?? previous.tool
|
|
1749
|
+
};
|
|
1750
|
+
state.activityItems.set(item.id, merged);
|
|
1751
|
+
if (!state.startedActivities.has(item.id)) {
|
|
1752
|
+
state.startedActivities.add(item.id);
|
|
1753
|
+
agent.session.append(CLAUDE_ACTIVITY_EVENT, activityPayload(claudeSessionId, turnId, merged, "started"));
|
|
1754
|
+
}
|
|
1755
|
+
if (phase === "completed" && !state.completedActivities.has(item.id)) {
|
|
1756
|
+
state.completedActivities.add(item.id);
|
|
1757
|
+
agent.session.append(CLAUDE_ACTIVITY_EVENT, activityPayload(claudeSessionId, turnId, merged, "completed"));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
var ActivityQueue = class {
|
|
1762
|
+
constructor(signal, label) {
|
|
1763
|
+
this.signal = signal;
|
|
1764
|
+
this.label = label;
|
|
1765
|
+
this.values = [];
|
|
1766
|
+
this.waiters = [];
|
|
1767
|
+
this.closed = false;
|
|
1768
|
+
}
|
|
1769
|
+
push(value) {
|
|
1770
|
+
if (this.closed) return;
|
|
1771
|
+
const waiter = this.waiters.shift();
|
|
1772
|
+
if (waiter) waiter.resolve(value);
|
|
1773
|
+
else this.values.push(value);
|
|
1774
|
+
}
|
|
1775
|
+
next() {
|
|
1776
|
+
if (this.values.length) return Promise.resolve(this.values.shift());
|
|
1777
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error(`${this.label} activity stream closed`));
|
|
1778
|
+
if (this.signal?.aborted) return Promise.reject(this.signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
1779
|
+
return new Promise((resolve, reject) => {
|
|
1780
|
+
const waiter = {
|
|
1781
|
+
resolve,
|
|
1782
|
+
reject
|
|
1783
|
+
};
|
|
1784
|
+
this.waiters.push(waiter);
|
|
1785
|
+
if (this.signal) {
|
|
1786
|
+
const abort = () => {
|
|
1787
|
+
const index = this.waiters.indexOf(waiter);
|
|
1788
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
1789
|
+
reject(this.signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
1790
|
+
};
|
|
1791
|
+
this.signal.addEventListener("abort", abort, { once: true });
|
|
1792
|
+
waiter.resolve = (value) => {
|
|
1793
|
+
this.signal.removeEventListener("abort", abort);
|
|
1794
|
+
resolve(value);
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
close() {
|
|
1800
|
+
this.closed = true;
|
|
1801
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(/* @__PURE__ */ new Error(`${this.label} activity stream closed`));
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
function createStreamState() {
|
|
1805
|
+
return {
|
|
1806
|
+
nextIndex: 0,
|
|
1807
|
+
blocks: /* @__PURE__ */ new Map(),
|
|
1808
|
+
completed: /* @__PURE__ */ new Set(),
|
|
1809
|
+
activityItems: /* @__PURE__ */ new Map(),
|
|
1810
|
+
startedActivities: /* @__PURE__ */ new Set(),
|
|
1811
|
+
completedActivities: /* @__PURE__ */ new Set()
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function textDelta(state, id, type, delta) {
|
|
1815
|
+
if (!id || !delta) return [];
|
|
1816
|
+
let block = state.blocks.get(id);
|
|
1817
|
+
const chunks = [];
|
|
1818
|
+
if (!block) {
|
|
1819
|
+
block = {
|
|
1820
|
+
index: state.nextIndex++,
|
|
1821
|
+
type,
|
|
1822
|
+
text: "",
|
|
1823
|
+
closed: false
|
|
1824
|
+
};
|
|
1825
|
+
state.blocks.set(id, block);
|
|
1826
|
+
chunks.push({
|
|
1827
|
+
type: "block-start",
|
|
1828
|
+
index: block.index,
|
|
1829
|
+
blockType: type
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
if (block.closed) return chunks;
|
|
1833
|
+
block.text += delta;
|
|
1834
|
+
chunks.push({
|
|
1835
|
+
type: type === "reasoning" ? "reasoning-delta" : "text-delta",
|
|
1836
|
+
index: block.index,
|
|
1837
|
+
text: delta
|
|
1838
|
+
});
|
|
1839
|
+
return chunks;
|
|
1840
|
+
}
|
|
1841
|
+
function completeTextItem(state, id, type, completeText) {
|
|
1842
|
+
const chunks = [];
|
|
1843
|
+
let block = state.blocks.get(id);
|
|
1844
|
+
if (!block) {
|
|
1845
|
+
block = {
|
|
1846
|
+
index: state.nextIndex++,
|
|
1847
|
+
type,
|
|
1848
|
+
text: "",
|
|
1849
|
+
closed: false
|
|
1850
|
+
};
|
|
1851
|
+
state.blocks.set(id, block);
|
|
1852
|
+
chunks.push({
|
|
1853
|
+
type: "block-start",
|
|
1854
|
+
index: block.index,
|
|
1855
|
+
blockType: type
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
if (completeText && completeText.startsWith(block.text) && completeText.length > block.text.length) {
|
|
1859
|
+
const delta = completeText.slice(block.text.length);
|
|
1860
|
+
block.text = completeText;
|
|
1861
|
+
chunks.push({
|
|
1862
|
+
type: type === "reasoning" ? "reasoning-delta" : "text-delta",
|
|
1863
|
+
index: block.index,
|
|
1864
|
+
text: delta
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
if (!block.closed) {
|
|
1868
|
+
block.closed = true;
|
|
1869
|
+
chunks.push({
|
|
1870
|
+
type: "block-end",
|
|
1871
|
+
index: block.index,
|
|
1872
|
+
block: {
|
|
1873
|
+
type,
|
|
1874
|
+
text: block.text
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
return chunks;
|
|
1879
|
+
}
|
|
1880
|
+
function activityPayload(claudeSessionId, turnId, item, phase) {
|
|
1881
|
+
const activity = normalizeActivity(item, phase);
|
|
1882
|
+
return {
|
|
1883
|
+
version: 1,
|
|
1884
|
+
claudeSessionId,
|
|
1885
|
+
turnId,
|
|
1886
|
+
itemId: String(item.id),
|
|
1887
|
+
phase,
|
|
1888
|
+
activity
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function normalizeActivity(item, phase) {
|
|
1892
|
+
const type = String(item.type ?? "toolUse");
|
|
1893
|
+
return bounded({
|
|
1894
|
+
type,
|
|
1895
|
+
status: phase === "started" ? "running" : item.status === "failed" ? "error" : "completed",
|
|
1896
|
+
title: item.tool ?? item.name ?? humanize(type),
|
|
1897
|
+
summary: summarizeValue(item.arguments ?? item.input ?? item.prompt),
|
|
1898
|
+
input: item.arguments ?? item.input,
|
|
1899
|
+
output: item.output ?? item.result ?? item.error
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
function bounded(value) {
|
|
1903
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
|
|
1904
|
+
if (entry === void 0 || entry === null || entry === "") return [];
|
|
1905
|
+
const text = typeof entry === "string" ? entry : JSON.stringify(entry, null, 2);
|
|
1906
|
+
return [[key, text.length > 2e4 ? `${text.slice(0, 2e4)}\n...` : text]];
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
function isActivityItem(item) {
|
|
1910
|
+
return item?.id && ![
|
|
1911
|
+
"userMessage",
|
|
1912
|
+
"agentMessage",
|
|
1913
|
+
"reasoning"
|
|
1914
|
+
].includes(item.type);
|
|
1915
|
+
}
|
|
1916
|
+
function permissionConfiguration(events) {
|
|
1917
|
+
let sandbox = "workspace-write";
|
|
1918
|
+
let approvalPolicy = "on-request";
|
|
1919
|
+
for (const event of events) {
|
|
1920
|
+
if (event.type === "sandbox/mode") sandbox = event.data.mode;
|
|
1921
|
+
if (event.type === "approval/policy") approvalPolicy = event.data.policy === "never" ? "never" : "on-request";
|
|
1922
|
+
if (event.type === "permission/preset") sandbox = event.data.preset;
|
|
1923
|
+
}
|
|
1924
|
+
return {
|
|
1925
|
+
sandbox,
|
|
1926
|
+
approvalPolicy
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function reasoningText(item) {
|
|
1930
|
+
return [...item.summary ?? [], ...item.content ?? []].filter(Boolean).join("\n\n");
|
|
1931
|
+
}
|
|
1932
|
+
function summarizeValue(value) {
|
|
1933
|
+
if (value === void 0 || value === null) return "";
|
|
1934
|
+
return firstLine(typeof value === "string" ? value : JSON.stringify(value));
|
|
1935
|
+
}
|
|
1936
|
+
function firstLine(value) {
|
|
1937
|
+
return String(value ?? "").split("\n")[0].slice(0, 240);
|
|
1938
|
+
}
|
|
1939
|
+
function humanize(value) {
|
|
1940
|
+
return String(value).replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (letter) => letter.toUpperCase());
|
|
1941
|
+
}
|
|
1942
|
+
function reasoningEffortName(value) {
|
|
1943
|
+
return String(value) === "xhigh" ? "Extra high" : humanize(value);
|
|
1944
|
+
}
|
|
1945
|
+
function latestUserText(messages) {
|
|
1946
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1947
|
+
const message = messages[index];
|
|
1948
|
+
if (message?.role !== "user") continue;
|
|
1949
|
+
const text = (message.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
1950
|
+
if (!text) continue;
|
|
1951
|
+
if (message.source?.kind === "user" || isRelayActivation(message.source)) return text;
|
|
1952
|
+
}
|
|
1953
|
+
return "";
|
|
1954
|
+
}
|
|
1955
|
+
function auxiliaryInput(messages) {
|
|
1956
|
+
return messages.map((message) => {
|
|
1957
|
+
const text = (message?.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
1958
|
+
return text ? `${message.role ?? "user"}: ${text}` : "";
|
|
1959
|
+
}).filter(Boolean).join("\n\n");
|
|
1960
|
+
}
|
|
1961
|
+
function projectAuxiliaryActivity(message, state) {
|
|
1962
|
+
const params = message.params ?? {};
|
|
1963
|
+
if (message.method === "item/reasoning/summaryTextDelta" || message.method === "item/reasoning/textDelta") return textDelta(state, params.itemId, "reasoning", params.delta ?? "");
|
|
1964
|
+
if (message.method === "item/agentMessage/delta") return textDelta(state, params.itemId, "text", params.delta ?? "");
|
|
1965
|
+
if (message.method === "item/completed") return completeAuxiliaryItem(state, params.item);
|
|
1966
|
+
return [];
|
|
1967
|
+
}
|
|
1968
|
+
function completeAuxiliaryItem(state, item) {
|
|
1969
|
+
if (!item?.id || state.completed.has(item.id)) return [];
|
|
1970
|
+
state.completed.add(item.id);
|
|
1971
|
+
if (item.type === "reasoning") return completeTextItem(state, item.id, "reasoning", reasoningText(item));
|
|
1972
|
+
if (item.type === "agentMessage") return completeTextItem(state, item.id, "text", item.text ?? "");
|
|
1973
|
+
return [];
|
|
1974
|
+
}
|
|
1975
|
+
function isRelayActivation(source) {
|
|
1976
|
+
return source?.kind === "plugin" && source.plugin === "relay";
|
|
1977
|
+
}
|
|
1978
|
+
function compact(value) {
|
|
1979
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0 && item !== null));
|
|
1980
|
+
}
|
|
1981
|
+
function runtimeModels(runtime) {
|
|
1982
|
+
return typeof runtime.listModels === "function" ? runtime.listModels() : [...runtime.models];
|
|
1983
|
+
}
|
|
1984
|
+
function hasRuntimeSession(runtime, sessionId) {
|
|
1985
|
+
return typeof runtime.hasSession === "function" ? runtime.hasSession(sessionId) : runtime.sessions.has(sessionId);
|
|
1986
|
+
}
|
|
1987
|
+
function patchRuntimeSession(runtime, sessionId, patch) {
|
|
1988
|
+
if (typeof runtime.patchSession === "function") return runtime.patchSession(sessionId, patch);
|
|
1989
|
+
const session = runtime.sessions.get(sessionId);
|
|
1990
|
+
if (session) Object.assign(session, patch);
|
|
1991
|
+
return Boolean(session);
|
|
1992
|
+
}
|
|
1993
|
+
function subscribeRuntimeActivity(runtime, listener) {
|
|
1994
|
+
if (typeof runtime.subscribeActivity === "function") return runtime.subscribeActivity(listener);
|
|
1995
|
+
runtime.on("activity", listener);
|
|
1996
|
+
return () => runtime.off("activity", listener);
|
|
1997
|
+
}
|
|
1998
|
+
function effectivePreset(session) {
|
|
1999
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
2000
|
+
const event = session.events[index];
|
|
2001
|
+
if (event.type === "agent-preset/selected") return event.data.agentPreset;
|
|
2002
|
+
}
|
|
2003
|
+
return session.header.agentPreset;
|
|
2004
|
+
}
|
|
2005
|
+
//#endregion
|
|
2006
|
+
//#region claude-link-store.js
|
|
2007
|
+
var ClaudeLinkStore = class {
|
|
2008
|
+
constructor(path) {
|
|
2009
|
+
this.path = path;
|
|
2010
|
+
this.records = loadRecords(path);
|
|
2011
|
+
}
|
|
2012
|
+
entries() {
|
|
2013
|
+
return [...this.records.entries()].map(([sessionId, record]) => [sessionId, structuredClone(record)]);
|
|
2014
|
+
}
|
|
2015
|
+
set(sessionId, record) {
|
|
2016
|
+
this.records.set(String(sessionId), structuredClone(record));
|
|
2017
|
+
this.persist();
|
|
2018
|
+
}
|
|
2019
|
+
delete(sessionId) {
|
|
2020
|
+
if (!this.records.delete(String(sessionId))) return;
|
|
2021
|
+
this.persist();
|
|
2022
|
+
}
|
|
2023
|
+
persist() {
|
|
2024
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
2025
|
+
const temporary = `${this.path}.${process.pid}.tmp`;
|
|
2026
|
+
const value = Object.fromEntries([...this.records.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
2027
|
+
writeFileSync(temporary, `${JSON.stringify({
|
|
2028
|
+
version: 1,
|
|
2029
|
+
sessions: value
|
|
2030
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
2031
|
+
renameSync(temporary, this.path);
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
function loadRecords(path) {
|
|
2035
|
+
try {
|
|
2036
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2037
|
+
if (parsed?.version !== 1 || !isObject(parsed.sessions)) return /* @__PURE__ */ new Map();
|
|
2038
|
+
return new Map(Object.entries(parsed.sessions).filter(([, record]) => validRecord(record)));
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
if (error?.code === "ENOENT") return /* @__PURE__ */ new Map();
|
|
2041
|
+
throw new Error(`Unable to read Claude DSH links from ${path}: ${error.message}`, { cause: error });
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function validRecord(record) {
|
|
2045
|
+
return isObject(record) && (record.claudeSessionId === null || typeof record.claudeSessionId === "string") && isObject(record.config);
|
|
2046
|
+
}
|
|
2047
|
+
function isObject(value) {
|
|
2048
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2049
|
+
}
|
|
2050
|
+
//#endregion
|
|
2051
|
+
//#region claude-tools.js
|
|
2052
|
+
async function handleClaudeSdkRequest(ctx, { adapter, runtime, request }) {
|
|
2053
|
+
const claudeSessionId = request.params?.sessionId;
|
|
2054
|
+
const dshSessionId = claudeSessionId ? adapter.dshSessionForClaudeSession(claudeSessionId) : null;
|
|
2055
|
+
const agent = dshSessionId ? ctx.agents.get(dshSessionId) : null;
|
|
2056
|
+
if (!agent) {
|
|
2057
|
+
runtime.rejectRequest(request.id, /* @__PURE__ */ new Error("Claude request has no owning live DSH Session"));
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
if (request.method === "tool/requestApproval") {
|
|
2062
|
+
const outcome = await ctx.approval.request({
|
|
2063
|
+
agent,
|
|
2064
|
+
toolName: approvalToolName(request),
|
|
2065
|
+
reason: approvalReason(request),
|
|
2066
|
+
signal: request.signal
|
|
2067
|
+
});
|
|
2068
|
+
await runtime.resolveRequest(request.id, {
|
|
2069
|
+
action: outcome === "allowed-once" ? "accept" : "decline",
|
|
2070
|
+
updatedInput: request.params?.input,
|
|
2071
|
+
message: `DSH approval returned ${outcome}.`
|
|
2072
|
+
});
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (request.method === "tool/requestUserInput") {
|
|
2076
|
+
const questions = normalizeQuestions(request.params?.input?.questions ?? []);
|
|
2077
|
+
const answer = await ctx.userQuestions.ask({
|
|
2078
|
+
agent,
|
|
2079
|
+
questions,
|
|
2080
|
+
signal: request.signal
|
|
2081
|
+
});
|
|
2082
|
+
await runtime.resolveRequest(request.id, {
|
|
2083
|
+
action: "answer",
|
|
2084
|
+
answers: normalizeAnswers(answer, questions)
|
|
2085
|
+
});
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
runtime.rejectRequest(request.id, /* @__PURE__ */ new Error(`Unsupported Claude interaction ${request.method}`));
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
runtime.rejectRequest(request.id, error);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
function approvalToolName(request) {
|
|
2094
|
+
const display = request.params?.displayName;
|
|
2095
|
+
if (typeof display === "string" && display.trim()) return `Claude ${display.trim()}`;
|
|
2096
|
+
const tool = request.params?.toolName;
|
|
2097
|
+
return tool ? `Claude ${tool}` : "Claude tool";
|
|
2098
|
+
}
|
|
2099
|
+
function approvalReason(request) {
|
|
2100
|
+
const params = request.params ?? {};
|
|
2101
|
+
const input = plainObject(params.input);
|
|
2102
|
+
if (typeof params.title === "string" && params.title.trim()) return params.title.trim();
|
|
2103
|
+
if (typeof params.description === "string" && params.description.trim()) return params.description.trim();
|
|
2104
|
+
if (typeof params.decisionReason === "string" && params.decisionReason.trim()) return params.decisionReason.trim();
|
|
2105
|
+
if (typeof input.command === "string" && input.command.trim()) return input.command.trim();
|
|
2106
|
+
if (typeof input.file_path === "string" && input.file_path.trim()) return input.file_path.trim();
|
|
2107
|
+
return `${params.toolName ?? "Claude"} requires permission to continue.`;
|
|
2108
|
+
}
|
|
2109
|
+
function normalizeQuestions(input) {
|
|
2110
|
+
return input.slice(0, 3).map((question, index) => ({
|
|
2111
|
+
id: `question-${index + 1}`,
|
|
2112
|
+
question: requiredString(question.question ?? question.header ?? `Question ${index + 1}`, "question"),
|
|
2113
|
+
header: String(question.header ?? "Claude").slice(0, 12),
|
|
2114
|
+
options: normalizeOptions(question.options ?? []),
|
|
2115
|
+
multiSelect: Boolean(question.multiSelect),
|
|
2116
|
+
detail: typeof question.detail === "string" ? question.detail : void 0
|
|
2117
|
+
}));
|
|
2118
|
+
}
|
|
2119
|
+
function normalizeOptions(input) {
|
|
2120
|
+
if (!Array.isArray(input) || input.length === 0) return [{ label: "Continue" }, { label: "Cancel" }];
|
|
2121
|
+
return input.slice(0, 4).map((option) => ({
|
|
2122
|
+
label: requiredString(option.label ?? option, "option label"),
|
|
2123
|
+
description: typeof option.description === "string" ? option.description : void 0
|
|
2124
|
+
}));
|
|
2125
|
+
}
|
|
2126
|
+
function normalizeAnswers(answer, questions) {
|
|
2127
|
+
const byId = new Map(questions.map((question) => [question.id, question]));
|
|
2128
|
+
return Object.fromEntries((answer.answers ?? []).flatMap((entry) => {
|
|
2129
|
+
const question = byId.get(entry.id);
|
|
2130
|
+
if (!question) return [];
|
|
2131
|
+
const selected = [...entry.selected ?? [], ...entry.custom ? [entry.custom] : []].filter(Boolean);
|
|
2132
|
+
return [[question.question, question.multiSelect ? selected : selected[0] ?? ""]];
|
|
2133
|
+
}));
|
|
2134
|
+
}
|
|
2135
|
+
function requiredString(value, label) {
|
|
2136
|
+
const text = String(value ?? "").trim();
|
|
2137
|
+
if (!text) throw new Error(`Claude ${label} is required`);
|
|
2138
|
+
return text;
|
|
2139
|
+
}
|
|
2140
|
+
function plainObject(value) {
|
|
2141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2142
|
+
}
|
|
2143
|
+
//#endregion
|
|
2144
|
+
//#region dsh-plugin.js
|
|
2145
|
+
function createDshClaudePlugin(ctx, config = {}) {
|
|
2146
|
+
return definePlugin({
|
|
2147
|
+
manifest: {
|
|
2148
|
+
id: "relay.dsh.claude",
|
|
2149
|
+
version: "1.0.0",
|
|
2150
|
+
provides: { "relay.dsh.claude.v1": "1.0.0" },
|
|
2151
|
+
requires: { "relay.execution.claude.v1": "^1.0.0" },
|
|
2152
|
+
permissions: ["dsh:llm", "dsh:agents"]
|
|
2153
|
+
},
|
|
2154
|
+
async activate({ capabilities, defer }) {
|
|
2155
|
+
installClaudeSessionEventType();
|
|
2156
|
+
const runtime = capabilities.require("relay.execution.claude.v1");
|
|
2157
|
+
const adapter = new ClaudeDshAdapter({
|
|
2158
|
+
runtime,
|
|
2159
|
+
ready: runtime.whenReady(),
|
|
2160
|
+
linkStore: new ClaudeLinkStore(resolveLinkPath(config.claudeLinkPath)),
|
|
2161
|
+
logger: ctx.logger
|
|
2162
|
+
});
|
|
2163
|
+
defer(ctx.llm.registerAdapter([CLAUDE_PROVIDER], adapter));
|
|
2164
|
+
defer(runtime.subscribeRequest((request) => {
|
|
2165
|
+
handleClaudeSdkRequest(ctx, {
|
|
2166
|
+
adapter,
|
|
2167
|
+
runtime,
|
|
2168
|
+
request
|
|
2169
|
+
}).catch((error) => ctx.logger.error(`Relay failed to handle a Claude interaction: ${error?.stack ?? error}`));
|
|
2170
|
+
}));
|
|
2171
|
+
defer(ctx.on("llm/stream", (options, next) => {
|
|
2172
|
+
if (options.purpose || !options.sessionId) return next();
|
|
2173
|
+
const agent = ctx.agents.get(options.sessionId);
|
|
2174
|
+
return agent && adapter.servesAgent(agent) ? adapter.stream(options) : next();
|
|
2175
|
+
}, {
|
|
2176
|
+
global: true,
|
|
2177
|
+
prepend: true
|
|
2178
|
+
}));
|
|
2179
|
+
defer(ctx.on("agent/created", ({ agent }) => {
|
|
2180
|
+
adapter.attachAgent(agent);
|
|
2181
|
+
}));
|
|
2182
|
+
defer(ctx.on("agent-preset/selected", (sessionId, preset) => {
|
|
2183
|
+
const agent = ctx.agents.get(sessionId);
|
|
2184
|
+
if (agent) adapter.attachAgent(agent, preset);
|
|
2185
|
+
}, { global: true }));
|
|
2186
|
+
defer(ctx.on("agent/disposed", ({ agent }) => {
|
|
2187
|
+
adapter.detachAgent(agent.id);
|
|
2188
|
+
}));
|
|
2189
|
+
for (const agent of ctx.agents.list()) adapter.attachAgent(agent);
|
|
2190
|
+
return { capabilities: { "relay.dsh.claude.v1": Object.freeze({ provider: CLAUDE_PROVIDER }) } };
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
function installClaudeSessionEventType() {
|
|
2195
|
+
if (KNOWN_SESSION_EVENT_TYPES.has("relay-claude/activity")) return;
|
|
2196
|
+
if (typeof KNOWN_SESSION_EVENT_TYPES.add !== "function") throw new Error("This DSH build cannot register Relay Claude session events");
|
|
2197
|
+
KNOWN_SESSION_EVENT_TYPES.add(CLAUDE_ACTIVITY_EVENT);
|
|
2198
|
+
}
|
|
2199
|
+
function resolveLinkPath(value) {
|
|
2200
|
+
const configured = value ?? process.env.RELAY_CLAUDE_LINK_PATH;
|
|
2201
|
+
return configured ? resolve(configured) : join(homedir(), ".relay", "claude-dsh-links.json");
|
|
2202
|
+
}
|
|
2203
|
+
//#endregion
|
|
2204
|
+
//#region preset.js
|
|
2205
|
+
async function installManagedPreset(source, id) {
|
|
2206
|
+
const home = resolve(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"));
|
|
2207
|
+
const target = join(home, ".agent-presets", id);
|
|
2208
|
+
await mkdir(join(home, ".agent-presets"), { recursive: true });
|
|
2209
|
+
if (await exists(target)) {
|
|
2210
|
+
if (!await exists(join(target, ".relay-managed"))) throw new Error(`Relay preset ${id} already exists and is not Relay-managed`);
|
|
2211
|
+
} else await mkdir(target, { recursive: true });
|
|
2212
|
+
for (const file of [
|
|
2213
|
+
"agent.cordis.yml",
|
|
2214
|
+
"preset.yml",
|
|
2215
|
+
".relay-managed"
|
|
2216
|
+
]) await cp(join(source, file), join(target, file));
|
|
2217
|
+
return target;
|
|
2218
|
+
}
|
|
2219
|
+
async function exists(path) {
|
|
2220
|
+
try {
|
|
2221
|
+
await stat(path);
|
|
2222
|
+
return true;
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
if (error?.code === "ENOENT") return false;
|
|
2225
|
+
throw error;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region host-plugin.js
|
|
2230
|
+
const name = "relay-dsh-plugin-claude";
|
|
2231
|
+
const inject = [
|
|
2232
|
+
"agents",
|
|
2233
|
+
"llm",
|
|
2234
|
+
"sessions",
|
|
2235
|
+
"sessionPersistence",
|
|
2236
|
+
"tools",
|
|
2237
|
+
"typert",
|
|
2238
|
+
"webServer"
|
|
2239
|
+
];
|
|
2240
|
+
async function apply(ctx, config = {}) {
|
|
2241
|
+
const host = new PluginHost();
|
|
2242
|
+
const release = ctx.effect(() => () => host.dispose(), "relay.claude()");
|
|
2243
|
+
try {
|
|
2244
|
+
await installManagedPreset(fileURLToPath(new URL("../presets/relay-claude", import.meta.url)), "relay-claude");
|
|
2245
|
+
await host.activate([createClaudeExecutionPlugin({
|
|
2246
|
+
client: config.claude?.client,
|
|
2247
|
+
backend: config.claudeBackend,
|
|
2248
|
+
command: config.claudeCommand,
|
|
2249
|
+
args: config.claudeArgs,
|
|
2250
|
+
codeExecutablePath: config.claudeCodeExecutablePath,
|
|
2251
|
+
requestTimeoutMs: config.claudeRequestTimeoutMs,
|
|
2252
|
+
cwd: config.cwd
|
|
2253
|
+
}), createDshClaudePlugin(ctx, config)]);
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
await release();
|
|
2256
|
+
throw error;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
//#endregion
|
|
2260
|
+
export { apply, inject, installClaudeSessionEventType, name };
|
|
2261
|
+
|
|
2262
|
+
//# sourceMappingURL=host-plugin.js.map
|