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.
@@ -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