History

Integrate Claude agent SDK into server provider adapter

- add `@anthropic-ai/claude-agent-sdk` dependency for `apps/server` - replace placeholder Claude adapter with live session/query/event handling - add comprehensive adapter tests for runtime events, approvals, resume, rollback, and model overrides

Julius Marminge committed 8 months ago

Commit e5f12e61 parent f00d4145 files · +1666 −206Browse files

Unified · Split

5 files
  1. apps/server/package.json +1 −0
  2. apps/server/src/main.test.ts +1 −1
  3. apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +465 −123
  4. apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +1164 −82
  5. bun.lock +35 −0
apps/server/package.json+1−0
@@ -18,6 +18,7 @@ · 17 unmodified lines
18 "test": "vitest run"18 "test": "vitest run"
19 },19 },
20 "dependencies": {20 "dependencies": {
21 "@anthropic-ai/claude-agent-sdk": "^0.2.62",
21 "@effect/platform-node": "catalog:",22 "@effect/platform-node": "catalog:",
22 "@effect/sql-sqlite-bun": "catalog:",23 "@effect/sql-sqlite-bun": "catalog:",
23 "@pierre/diffs": "^1.1.0-beta.16",24 "@pierre/diffs": "^1.1.0-beta.16",
apps/server/src/main.test.ts+1−1
@@ -62,7 +62,7 @@ beforeEach(() => { · 61 unmodified lines
62 findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred));62 findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred));
63});63});
64 64
65it.layer(testLayer)("server cli", (it) => {65it.layer(testLayer)("server CLI command", (it) => {
66 it.effect("parses all CLI flags and wires scoped start/stop", () =>66 it.effect("parses all CLI flags and wires scoped start/stop", () =>
67 Effect.gen(function* () {67 Effect.gen(function* () {
68 yield* runCli([68 yield* runCli([
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+465−123
@@ -1,27 +1,157 @@
1import { EventId, ProviderSessionId, ProviderThreadId, ProviderTurnId } from "@t3tools/contracts";1import type {
2 Options as ClaudeQueryOptions,
3 PermissionMode,
4 PermissionResult,
5 SDKMessage,
6 SDKUserMessage,
7} from "@anthropic-ai/claude-agent-sdk";
8import { ApprovalRequestId, type ProviderTurnId } from "@t3tools/contracts";
2import { assert, describe, it } from "@effect/vitest";9import { assert, describe, it } from "@effect/vitest";
3import { Effect, Layer, Stream } from "effect";10import { Effect, Fiber, Random, Stream } from "effect";
4 11
5import {12import {
6 ProviderAdapterProcessError,
7 ProviderAdapterRequestError,
8 ProviderAdapterValidationError,13 ProviderAdapterValidationError,
9} from "../Errors.ts";14} from "../Errors.ts";
15import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
10import {16import {
11 ClaudeCodeAdapter,17 makeClaudeCodeAdapterLive,
12 type ClaudeCodeAdapterShape,18 type ClaudeCodeAdapterLiveOptions,
13} from "../Services/ClaudeCodeAdapter.ts";19} from "./ClaudeCodeAdapter.ts";
14import { makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
15 20
16const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value);21class FakeClaudeQuery implements AsyncIterable<SDKMessage> {
17const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);22 private readonly queue: Array<SDKMessage> = [];
18const asEventId = (value: string): EventId => EventId.makeUnsafe(value);23 private readonly resolvers: Array<(value: IteratorResult<SDKMessage>) => void> = [];
24 private done = false;
19 25
20describe("ClaudeCodeAdapterLive", () => {26 public readonly interruptCalls: Array<void> = [];
21 const layer = makeClaudeCodeAdapterLive();27 public readonly setModelCalls: Array<string | undefined> = [];
28 public readonly setPermissionModeCalls: Array<string> = [];
29 public readonly setMaxThinkingTokensCalls: Array<number | null> = [];
30 public closeCalls = 0;
31
32 emit(message: SDKMessage): void {
33 if (this.done) {
34 return;
35 }
36 const resolver = this.resolvers.shift();
37 if (resolver) {
38 resolver({ done: false, value: message });
39 return;
40 }
41 this.queue.push(message);
42 }
43
44 finish(): void {
45 if (this.done) {
46 return;
47 }
48 this.done = true;
49 for (const resolver of this.resolvers.splice(0)) {
50 resolver({ done: true, value: undefined });
51 }
52 }
53
54 readonly interrupt = async (): Promise<void> => {
55 this.interruptCalls.push(undefined);
56 };
57
58 readonly setModel = async (model?: string): Promise<void> => {
59 this.setModelCalls.push(model);
60 };
61
62 readonly setPermissionMode = async (mode: PermissionMode): Promise<void> => {
63 this.setPermissionModeCalls.push(mode);
64 };
65
66 readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise<void> => {
67 this.setMaxThinkingTokensCalls.push(maxThinkingTokens);
68 };
69
70 readonly close = (): void => {
71 this.closeCalls += 1;
72 this.finish();
73 };
74
75 [Symbol.asyncIterator](): AsyncIterator<SDKMessage> {
76 return {
77 next: () => {
78 if (this.queue.length > 0) {
79 const value = this.queue.shift();
80 if (value) {
81 return Promise.resolve({
82 done: false,
83 value,
84 });
85 }
86 }
87 if (this.done) {
88 return Promise.resolve({
89 done: true,
90 value: undefined,
91 });
92 }
93 return new Promise((resolve) => {
94 this.resolvers.push(resolve);
95 });
96 },
97 };
98 }
99}
100
101interface Harness {
102 readonly layer: ReturnType<typeof makeClaudeCodeAdapterLive>;
103 readonly query: FakeClaudeQuery;
104 readonly getLastCreateQueryInput: () =>
105 | {
106 readonly prompt: AsyncIterable<SDKUserMessage>;
107 readonly options: ClaudeQueryOptions;
108 }
109 | undefined;
110}
111
112function makeHarness(): Harness {
113 const query = new FakeClaudeQuery();
114 let createInput:
115 | {
116 readonly prompt: AsyncIterable<SDKUserMessage>;
117 readonly options: ClaudeQueryOptions;
118 }
119 | undefined;
120
121 const options: ClaudeCodeAdapterLiveOptions = {
122 createQuery: (input) => {
123 createInput = input;
124 return query;
125 },
126 };
127
128 return {
129 layer: makeClaudeCodeAdapterLive(options),
130 query,
131 getLastCreateQueryInput: () => createInput,
132 };
133}
22 134
23 it.effect("returns validation error for non-claudeCode provider on startSession", () =>135function makeDeterministicRandomService(seed = 0x1234_5678): {
24 Effect.gen(function* () {136 nextIntUnsafe: () => number;
137 nextDoubleUnsafe: () => number;
138} {
139 let state = seed >>> 0;
140 const nextIntUnsafe = (): number => {
141 state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0;
142 return state;
143 };
144
145 return {
146 nextIntUnsafe,
147 nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000,
148 };
149}
150
151describe("ClaudeCodeAdapterLive", () => {
152 it.effect("returns validation error for non-claudeCode provider on startSession", () => {
153 const harness = makeHarness();
154 return Effect.gen(function* () {
25 const adapter = yield* ClaudeCodeAdapter;155 const adapter = yield* ClaudeCodeAdapter;
26 const result = yield* adapter.startSession({ provider: "codex" }).pipe(Effect.result);156 const result = yield* adapter.startSession({ provider: "codex" }).pipe(Effect.result);
27 157
@@ -29,10 +159,6 @@ describe("ClaudeCodeAdapterLive", () => { · 1 unmodified line
29 if (result._tag !== "Failure") {159 if (result._tag !== "Failure") {
30 return;160 return;
31 }161 }
32 assert.equal(result.failure._tag, "ProviderAdapterValidationError");
33 if (result.failure._tag !== "ProviderAdapterValidationError") {
34 return;
35 }
36 assert.deepEqual(162 assert.deepEqual(
37 result.failure,163 result.failure,
38 new ProviderAdapterValidationError({164 new ProviderAdapterValidationError({
@@ -41,126 +167,342 @@ describe("ClaudeCodeAdapterLive", () => { · 2 unmodified lines
41 issue: "Expected provider 'claudeCode' but received 'codex'.",167 issue: "Expected provider 'claudeCode' but received 'codex'.",
42 }),168 }),
43 );169 );
44 }).pipe(Effect.provide(layer)),170 }).pipe(
45 );171 Effect.provideService(Random.Random, makeDeterministicRandomService()),
172 Effect.provide(harness.layer),
173 );
174 });
46 175
47 it.effect("returns typed process error when runtime is not configured", () =>176 it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => {
48 Effect.gen(function* () {177 const harness = makeHarness();
178 return Effect.gen(function* () {
49 const adapter = yield* ClaudeCodeAdapter;179 const adapter = yield* ClaudeCodeAdapter;
50 const result = yield* adapter.startSession({ provider: "claudeCode" }).pipe(Effect.result);
51 180
52 assert.equal(result._tag, "Failure");181 const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe(
53 if (result._tag !== "Failure") {182 Stream.runCollect,
54 return;183 Effect.forkChild,
55 }184 );
56 assert.equal(result.failure._tag, "ProviderAdapterProcessError");185
57 if (result.failure._tag !== "ProviderAdapterProcessError") {186 const session = yield* adapter.startSession({
58 return;187 provider: "claudeCode",
59 }188 model: "claude-sonnet-4-5",
189 });
190
191 const turn = yield* adapter.sendTurn({
192 sessionId: session.sessionId,
193 input: "hello",
194 attachments: [],
195 });
196
197 harness.query.emit({
198 type: "stream_event",
199 session_id: "sdk-session-1",
200 uuid: "stream-1",
201 parent_tool_use_id: null,
202 event: {
203 type: "content_block_delta",
204 index: 0,
205 delta: {
206 type: "text_delta",
207 text: "Hi",
208 },
209 },
210 } as unknown as SDKMessage);
211
212 harness.query.emit({
213 type: "stream_event",
214 session_id: "sdk-session-1",
215 uuid: "stream-2",
216 parent_tool_use_id: null,
217 event: {
218 type: "content_block_start",
219 index: 1,
220 content_block: {
221 type: "tool_use",
222 id: "tool-1",
223 name: "Bash",
224 input: {
225 command: "ls",
226 },
227 },
228 },
229 } as unknown as SDKMessage);
230
231 harness.query.emit({
232 type: "stream_event",
233 session_id: "sdk-session-1",
234 uuid: "stream-3",
235 parent_tool_use_id: null,
236 event: {
237 type: "content_block_stop",
238 index: 1,
239 },
240 } as unknown as SDKMessage);
241
242 harness.query.emit({
243 type: "assistant",
244 session_id: "sdk-session-1",
245 uuid: "assistant-1",
246 parent_tool_use_id: null,
247 message: {
248 id: "assistant-message-1",
249 content: [{ type: "text", text: "Hi" }],
250 },
251 } as unknown as SDKMessage);
252
253 harness.query.emit({
254 type: "result",
255 subtype: "success",
256 is_error: false,
257 errors: [],
258 session_id: "sdk-session-1",
259 uuid: "result-1",
260 } as unknown as SDKMessage);
261
262 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
60 assert.deepEqual(263 assert.deepEqual(
61 result.failure,264 runtimeEvents.map((event) => event.type),
62 new ProviderAdapterProcessError({265 [
63 provider: "claudeCode",266 "session.started",
64 sessionId: "pending",267 "turn.started",
65 detail: "Claude Code runtime is not configured.",268 "thread.started",
66 }),269 "message.delta",
270 "tool.started",
271 "tool.completed",
272 "message.completed",
273 "turn.completed",
274 ],
67 );275 );
68 }).pipe(Effect.provide(layer)),
69 );
70 276
71 it.effect("returns typed request errors for turn operations when runtime is not configured", () =>277 const turnStarted = runtimeEvents[1];
72 Effect.gen(function* () {278 assert.equal(turnStarted?.type, "turn.started");
279 if (turnStarted?.type === "turn.started") {
280 assert.equal(turnStarted.turnId, turn.turnId);
281 }
282
283 const deltaEvent = runtimeEvents[3];
284 assert.equal(deltaEvent?.type, "message.delta");
285 if (deltaEvent?.type === "message.delta") {
286 assert.equal(deltaEvent.delta, "Hi");
287 assert.equal(deltaEvent.turnId, turn.turnId);
288 }
289
290 const toolStarted = runtimeEvents[4];
291 assert.equal(toolStarted?.type, "tool.started");
292 if (toolStarted?.type === "tool.started") {
293 assert.equal(toolStarted.toolKind, "command");
294 }
295
296 const turnCompleted = runtimeEvents[7];
297 assert.equal(turnCompleted?.type, "turn.completed");
298 if (turnCompleted?.type === "turn.completed") {
299 assert.equal(turnCompleted.turnId, turn.turnId);
300 assert.equal(turnCompleted.status, "completed");
301 }
302 }).pipe(
303 Effect.provideService(Random.Random, makeDeterministicRandomService()),
304 Effect.provide(harness.layer),
305 );
306 });
307
308 it.effect("bridges approval request/response lifecycle through canUseTool", () => {
309 const harness = makeHarness();
310 return Effect.gen(function* () {
73 const adapter = yield* ClaudeCodeAdapter;311 const adapter = yield* ClaudeCodeAdapter;
74 const result = yield* adapter
75 .sendTurn({
76 sessionId: asSessionId("claude-sess-1"),
77 input: "hello",
78 attachments: [],
79 })
80 .pipe(Effect.result);
81 312
82 assert.equal(result._tag, "Failure");313 const session = yield* adapter.startSession({
83 if (result._tag !== "Failure") {314 provider: "claudeCode",
315 approvalPolicy: "on-request",
316 });
317
318 const consumeSessionStarted = yield* Stream.runHead(adapter.streamEvents);
319 assert.equal(consumeSessionStarted._tag, "Some");
320
321 const createInput = harness.getLastCreateQueryInput();
322 const canUseTool = createInput?.options.canUseTool;
323 assert.equal(typeof canUseTool, "function");
324 if (!canUseTool) {
84 return;325 return;
85 }326 }
86 assert.equal(result.failure._tag, "ProviderAdapterRequestError");327
87 if (result.failure._tag !== "ProviderAdapterRequestError") {328 const permissionPromise = canUseTool(
329 "Bash",
330 { command: "pwd" },
331 {
332 signal: new AbortController().signal,
333 suggestions: [
334 {
335 type: "setMode",
336 mode: "default",
337 destination: "session",
338 },
339 ],
340 toolUseID: "tool-use-1",
341 },
342 );
343
344 const requested = yield* Stream.runHead(adapter.streamEvents);
345 assert.equal(requested._tag, "Some");
346 if (requested._tag !== "Some") {
88 return;347 return;
89 }348 }
90 assert.deepEqual(349 assert.equal(requested.value.type, "approval.requested");
91 result.failure,350 if (requested.value.type !== "approval.requested") {
92 new ProviderAdapterRequestError({351 return;
93 provider: "claudeCode",352 }
94 method: "turn/start",353
95 detail: "Claude Code runtime is not configured. (session claude-sess-1)",354 yield* adapter.respondToRequest(
96 }),355 session.sessionId,
356 ApprovalRequestId.makeUnsafe(requested.value.requestId),
357 "accept",
97 );358 );
98 }).pipe(Effect.provide(layer)),
99 );
100 359
101 it.effect("emits no runtime events by default", () =>360 const resolved = yield* Stream.runHead(adapter.streamEvents);
102 Effect.gen(function* () {361 assert.equal(resolved._tag, "Some");
362 if (resolved._tag !== "Some") {
363 return;
364 }
365 assert.equal(resolved.value.type, "approval.resolved");
366 if (resolved.value.type !== "approval.resolved") {
367 return;
368 }
369 assert.equal(resolved.value.requestId, requested.value.requestId);
370 assert.equal(resolved.value.decision, "accept");
371
372 const permissionResult = yield* Effect.promise(() => permissionPromise);
373 assert.equal((permissionResult as PermissionResult).behavior, "allow");
374 }).pipe(
375 Effect.provideService(Random.Random, makeDeterministicRandomService()),
376 Effect.provide(harness.layer),
377 );
378 });
379
380 it.effect("passes parsed resume cursor values to Claude query options", () => {
381 const harness = makeHarness();
382 return Effect.gen(function* () {
103 const adapter = yield* ClaudeCodeAdapter;383 const adapter = yield* ClaudeCodeAdapter;
104 const firstEvent = yield* Stream.runHead(adapter.streamEvents);
105 assert.equal(firstEvent._tag, "None");
106 }).pipe(Effect.provide(layer)),
107 );
108});
109 384
110describe("ClaudeCodeAdapter mocked layer", () => {385 const session = yield* adapter.startSession({
111 it.effect("supports replacing the adapter layer directly in tests", () =>386 provider: "claudeCode",
112 Effect.gen(function* () {387 resumeCursor: {
388 threadId: "resume-thread-1",
389 resume: "resume-session-1",
390 resumeSessionAt: "assistant-99",
391 turnCount: 3,
392 },
393 });
394
395 assert.equal(session.threadId, "resume-thread-1");
396 assert.deepEqual(session.resumeCursor, {
397 threadId: "resume-thread-1",
398 resume: "resume-session-1",
399 resumeSessionAt: "assistant-99",
400 turnCount: 3,
401 });
402
403 const createInput = harness.getLastCreateQueryInput();
404 assert.equal(createInput?.options.resume, "resume-session-1");
405 assert.equal(createInput?.options.resumeSessionAt, "assistant-99");
406 }).pipe(
407 Effect.provideService(Random.Random, makeDeterministicRandomService()),
408 Effect.provide(harness.layer),
409 );
410 });
411
412 it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => {
413 const harness = makeHarness();
414 return Effect.gen(function* () {
415 const adapter = yield* ClaudeCodeAdapter;
416
417 const session = yield* adapter.startSession({
418 provider: "claudeCode",
419 });
420
421 const firstTurn = yield* adapter.sendTurn({
422 sessionId: session.sessionId,
423 input: "first",
424 attachments: [],
425 });
426
427 const firstCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe(
428 Stream.runHead,
429 Effect.forkChild,
430 );
431
432 harness.query.emit({
433 type: "result",
434 subtype: "success",
435 is_error: false,
436 errors: [],
437 session_id: "sdk-session-rollback",
438 uuid: "result-first",
439 } as unknown as SDKMessage);
440
441 const firstCompleted = yield* Fiber.join(firstCompletedFiber);
442 assert.equal(firstCompleted._tag, "Some");
443 if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") {
444 assert.equal(firstCompleted.value.turnId, firstTurn.turnId);
445 }
446
447 const secondTurn = yield* adapter.sendTurn({
448 sessionId: session.sessionId,
449 input: "second",
450 attachments: [],
451 });
452
453 const secondCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe(
454 Stream.runHead,
455 Effect.forkChild,
456 );
457
458 harness.query.emit({
459 type: "result",
460 subtype: "success",
461 is_error: false,
462 errors: [],
463 session_id: "sdk-session-rollback",
464 uuid: "result-second",
465 } as unknown as SDKMessage);
466
467 const secondCompleted = yield* Fiber.join(secondCompletedFiber);
468 assert.equal(secondCompleted._tag, "Some");
469 if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") {
470 assert.equal(secondCompleted.value.turnId, secondTurn.turnId);
471 }
472
473 const threadBeforeRollback = yield* adapter.readThread(session.sessionId);
474 assert.equal(threadBeforeRollback.turns.length, 2);
475
476 const rolledBack = yield* adapter.rollbackThread(session.sessionId, 1);
477 assert.equal(rolledBack.turns.length, 1);
478 assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId as ProviderTurnId);
479
480 const threadAfterRollback = yield* adapter.readThread(session.sessionId);
481 assert.equal(threadAfterRollback.turns.length, 1);
482 assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId as ProviderTurnId);
483 }).pipe(
484 Effect.provideService(Random.Random, makeDeterministicRandomService()),
485 Effect.provide(harness.layer),
486 );
487 });
488
489 it.effect("updates model on sendTurn when model override is provided", () => {
490 const harness = makeHarness();
491 return Effect.gen(function* () {
113 const adapter = yield* ClaudeCodeAdapter;492 const adapter = yield* ClaudeCodeAdapter;
114 const started = yield* adapter.startSession({ provider: "claudeCode" });493
115 assert.equal(started.sessionId, "mock-sess-1");494 const session = yield* adapter.startSession({ provider: "claudeCode" });
116 const event = yield* Stream.runHead(adapter.streamEvents);495 yield* adapter.sendTurn({
117 assert.equal(event._tag, "Some");496 sessionId: session.sessionId,
497 input: "hello",
498 model: "claude-opus-4-6",
499 attachments: [],
500 });
501
502 assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]);
118 }).pipe(503 }).pipe(
119 Effect.provide(504 Effect.provideService(Random.Random, makeDeterministicRandomService()),
120 Layer.succeed(ClaudeCodeAdapter, {505 Effect.provide(harness.layer),
121 provider: "claudeCode",506 );
122 startSession: () =>507 });
123 Effect.succeed({
124 sessionId: asSessionId("mock-sess-1"),
125 provider: "claudeCode",
126 status: "ready",
127 threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
128 createdAt: new Date().toISOString(),
129 updatedAt: new Date().toISOString(),
130 }),
131 sendTurn: () =>
132 Effect.succeed({
133 threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
134 turnId: asTurnId("mock-turn-1"),
135 }),
136 interruptTurn: () => Effect.void,
137 readThread: () =>
138 Effect.succeed({
139 threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
140 turns: [],
141 }),
142 rollbackThread: () =>
143 Effect.succeed({
144 threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
145 turns: [],
146 }),
147 respondToRequest: () => Effect.void,
148 stopSession: () => Effect.void,
149 listSessions: () => Effect.succeed([]),
150 hasSession: () => Effect.succeed(false),
151 stopAll: () => Effect.void,
152 streamEvents: Stream.make({
153 type: "turn.completed",
154 eventId: asEventId("evt-mock-1"),
155 provider: "claudeCode",
156 sessionId: asSessionId("mock-sess-1"),
157 createdAt: new Date().toISOString(),
158 threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
159 turnId: asTurnId("mock-turn-1"),
160 status: "completed",
161 }),
162 } satisfies ClaudeCodeAdapterShape),
163 ),
164 ),
165 );
166});508});
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+1164−82
@@ -1,105 +1,1187 @@
1/**1/**
2 * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter.2 * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter.
3 *3 *
4 * Until the Claude runtime bridge is implemented, this adapter is wired4 * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic
5 * directly and returns typed "not configured" errors for runtime operations.5 * provider adapter contract and emits canonical runtime events.
6 *6 *
7 * @module ClaudeCodeAdapterLive7 * @module ClaudeCodeAdapterLive
8 */8 */
9import type { ProviderRuntimeEvent } from "@t3tools/contracts";9import {
10import { Effect, Layer, Stream } from "effect";10 type CanUseTool,
11 query,
12 type Options as ClaudeQueryOptions,
13 type PermissionMode,
14 type PermissionResult,
15 type PermissionUpdate,
16 type SDKMessage,
17 type SDKResultMessage,
18 type SDKUserMessage,
19} from "@anthropic-ai/claude-agent-sdk";
20import {
21 ApprovalRequestId,
22 EventId,
23 type ProviderApprovalDecision,
24 ProviderItemId,
25 type ProviderRuntimeEvent,
26 type ProviderRuntimeToolKind,
27 type ProviderRuntimeTurnStatus,
28 type ProviderSendTurnInput,
29 ProviderSessionId,
30 type ProviderSession,
31 ProviderThreadId,
32 ProviderTurnId,
33} from "@t3tools/contracts";
34import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Stream } from "effect";
11 35
12import {36import {
13 ProviderAdapterProcessError,37 ProviderAdapterProcessError,
14 ProviderAdapterRequestError,38 ProviderAdapterRequestError,
39 ProviderAdapterSessionClosedError,
40 ProviderAdapterSessionNotFoundError,
15 ProviderAdapterValidationError,41 ProviderAdapterValidationError,
42 type ProviderAdapterError,
16} from "../Errors.ts";43} from "../Errors.ts";
17import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";44import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
18 45
19const PROVIDER = "claudeCode" as const;46const PROVIDER = "claudeCode" as const;
20const CLAUDE_RUNTIME_NOT_CONFIGURED = "Claude Code runtime is not configured.";47
21 48type PromptQueueItem =
22const makeClaudeCodeAdapter = Effect.succeed({49 | {
23 provider: PROVIDER,50 readonly type: "message";
24 startSession: (input) => {51 readonly message: SDKUserMessage;
25 if (input.provider !== undefined && input.provider !== PROVIDER) {52 }
26 return Effect.fail(53 | {
27 new ProviderAdapterValidationError({54 readonly type: "terminate";
28 provider: PROVIDER,55 };
29 operation: "startSession",56
30 issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,57interface ClaudeResumeState {
31 }),58 readonly threadId?: ProviderThreadId;
59 readonly resume?: string;
60 readonly resumeSessionAt?: string;
61 readonly turnCount?: number;
62}
63
64interface ClaudeTurnState {
65 readonly turnId: ProviderTurnId;
66 readonly assistantItemId: string;
67 readonly startedAt: string;
68 readonly items: Array<unknown>;
69 readonly messageCompleted: boolean;
70}
71
72interface PendingApproval {
73 readonly requestKind: "command" | "file-change";
74 readonly detail?: string;
75 readonly suggestions?: ReadonlyArray<PermissionUpdate>;
76 readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
77}
78
79interface ToolInFlight {
80 readonly itemId: string;
81 readonly toolKind: ProviderRuntimeToolKind;
82 readonly title: string;
83 readonly detail?: string;
84}
85
86interface ClaudeSessionContext {
87 session: ProviderSession;
88 readonly promptQueue: Queue.Queue<PromptQueueItem>;
89 readonly query: ClaudeQueryRuntime;
90 readonly startedAt: string;
91 readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
92 readonly turns: Array<{
93 id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
94 items: Array<unknown>;
95 }>;
96 readonly inFlightTools: Map<number, ToolInFlight>;
97 turnState: ClaudeTurnState | undefined;
98 lastAssistantUuid: string | undefined;
99 lastThreadStartedId: ProviderThreadId | undefined;
100 stopped: boolean;
101}
102
103interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
104 readonly interrupt: () => Promise<void>;
105 readonly setModel: (model?: string) => Promise<void>;
106 readonly setPermissionMode: (mode: PermissionMode) => Promise<void>;
107 readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>;
108 readonly close: () => void;
109}
110
111export interface ClaudeCodeAdapterLiveOptions {
112 readonly createQuery?: (input: {
113 readonly prompt: AsyncIterable<SDKUserMessage>;
114 readonly options: ClaudeQueryOptions;
115 }) => ClaudeQueryRuntime;
116}
117
118function toMessage(cause: unknown, fallback: string): string {
119 if (cause instanceof Error && cause.message.length > 0) {
120 return cause.message;
121 }
122 return fallback;
123}
124
125function toPermissionMode(value: unknown): PermissionMode | undefined {
126 switch (value) {
127 case "default":
128 case "acceptEdits":
129 case "bypassPermissions":
130 case "plan":
131 case "dontAsk":
132 return value;
133 default:
134 return undefined;
135 }
136}
137
138function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined {
139 if (!resumeCursor || typeof resumeCursor !== "object") {
140 return undefined;
141 }
142 const cursor = resumeCursor as {
143 threadId?: unknown;
144 resume?: unknown;
145 sessionId?: unknown;
146 resumeSessionAt?: unknown;
147 turnCount?: unknown;
148 };
149
150 const threadId = typeof cursor.threadId === "string" ? cursor.threadId : undefined;
151 const resume =
152 typeof cursor.resume === "string"
153 ? cursor.resume
154 : typeof cursor.sessionId === "string"
155 ? cursor.sessionId
156 : undefined;
157 const resumeSessionAt =
158 typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined;
159 const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined;
160
161 return {
162 ...(threadId ? { threadId: ProviderThreadId.makeUnsafe(threadId) } : {}),
163 ...(resume ? { resume } : {}),
164 ...(resumeSessionAt ? { resumeSessionAt } : {}),
165 ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0
166 ? { turnCount: turnCountValue }
167 : {}),
168 };
169}
170
171function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
172 const normalized = toolName.toLowerCase();
173 if (
174 normalized.includes("bash") ||
175 normalized.includes("command") ||
176 normalized.includes("shell") ||
177 normalized.includes("terminal")
178 ) {
179 return "command";
180 }
181 if (
182 normalized.includes("edit") ||
183 normalized.includes("write") ||
184 normalized.includes("file") ||
185 normalized.includes("patch") ||
186 normalized.includes("replace") ||
187 normalized.includes("create") ||
188 normalized.includes("delete")
189 ) {
190 return "file-change";
191 }
192 return "other";
193}
194
195function classifyRequestKind(toolName: string): "command" | "file-change" {
196 return classifyToolKind(toolName) === "command" ? "command" : "file-change";
197}
198
199function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
200 const commandValue = input.command ?? input.cmd;
201 const command = typeof commandValue === "string" ? commandValue : undefined;
202 if (command && command.trim().length > 0) {
203 return `${toolName}: ${command.trim().slice(0, 400)}`;
204 }
205
206 const serialized = JSON.stringify(input);
207 if (serialized.length <= 400) {
208 return `${toolName}: ${serialized}`;
209 }
210 return `${toolName}: ${serialized.slice(0, 397)}...`;
211}
212
213function titleForTool(kind: ProviderRuntimeToolKind): string {
214 switch (kind) {
215 case "command":
216 return "Command run";
217 case "file-change":
218 return "File change";
219 case "other":
220 return "Tool call";
221 }
222}
223
224function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage {
225 const fragments: string[] = [];
226
227 if (input.input && input.input.trim().length > 0) {
228 fragments.push(input.input.trim());
229 }
230
231 for (const attachment of input.attachments ?? []) {
232 if (attachment.type === "image") {
233 fragments.push(
234 `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`,
32 );235 );
33 }236 }
237 }
34 238
35 return Effect.fail(239 const text = fragments.join("\n\n");
36 new ProviderAdapterProcessError({240
37 provider: PROVIDER,241 return {
38 sessionId: "pending",242 type: "user",
39 detail: CLAUDE_RUNTIME_NOT_CONFIGURED,243 session_id: "",
40 }),244 parent_tool_use_id: null,
41 );245 message: {
42 },246 role: "user",
43 sendTurn: (input) =>247 content: [{ type: "text", text }],
44 Effect.fail(248 },
45 new ProviderAdapterRequestError({249 } as SDKUserMessage;
46 provider: PROVIDER,250}
47 method: "turn/start",251
48 detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${input.sessionId})`,252function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {
49 }),253 if (result.subtype === "success") {
50 ),254 return "completed";
51 interruptTurn: (sessionId, _turnId) =>255 }
52 Effect.fail(256
53 new ProviderAdapterRequestError({257 const errors = result.errors.join(" ").toLowerCase();
54 provider: PROVIDER,258 if (errors.includes("interrupt")) {
55 method: "turn/interrupt",259 return "interrupted";
56 detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,260 }
57 }),261 if (errors.includes("cancel")) {
58 ),262 return "cancelled";
59 readThread: (sessionId) =>263 }
60 Effect.fail(264 return "failed";
61 new ProviderAdapterRequestError({265}
62 provider: PROVIDER,266
63 method: "thread/read",267function toSessionError(
64 detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,268 sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
65 }),269 cause: unknown,
66 ),270): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
67 rollbackThread: (sessionId, numTurns) => {271 const normalized = toMessage(cause, "").toLowerCase();
68 if (!Number.isInteger(numTurns) || numTurns < 1) {272 if (normalized.includes("unknown session") || normalized.includes("not found")) {
69 return Effect.fail(273 return new ProviderAdapterSessionNotFoundError({
70 new ProviderAdapterValidationError({274 provider: PROVIDER,
275 sessionId,
276 cause,
277 });
278 }
279 if (normalized.includes("closed")) {
280 return new ProviderAdapterSessionClosedError({
281 provider: PROVIDER,
282 sessionId,
283 cause,
284 });
285 }
286 return undefined;
287}
288
289function toRequestError(
290 sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
291 method: string,
292 cause: unknown,
293): ProviderAdapterError {
294 const sessionError = toSessionError(sessionId, cause);
295 if (sessionError) {
296 return sessionError;
297 }
298 return new ProviderAdapterRequestError({
299 provider: PROVIDER,
300 method,
301 detail: toMessage(cause, `${method} failed`),
302 cause,
303 });
304}
305
306function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
307 return Effect.gen(function* () {
308 const createQuery =
309 options?.createQuery ??
310 ((input: {
311 readonly prompt: AsyncIterable<SDKUserMessage>;
312 readonly options: ClaudeQueryOptions;
313 }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);
314
315 const sessions = new Map<
316 ReturnType<typeof ProviderSessionId.makeUnsafe>,
317 ClaudeSessionContext
318 >();
319 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
320
321 const now = (): Effect.Effect<string> =>
322 DateTime.now.pipe(Effect.map(DateTime.formatIso));
323 const randomId = (): Effect.Effect<string> => Random.nextUUIDv4;
324 const makeEventId = (): Effect.Effect<ReturnType<typeof EventId.makeUnsafe>> =>
325 randomId().pipe(Effect.map((id) => EventId.makeUnsafe(id)));
326 const makeEventStamp = (): Effect.Effect<{
327 readonly eventId: ReturnType<typeof EventId.makeUnsafe>;
328 readonly createdAt: string;
329 }> =>
330 Effect.all({
331 eventId: makeEventId(),
332 createdAt: now(),
333 });
334
335 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
336 Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);
337
338 const snapshotThread = (
339 context: ClaudeSessionContext,
340 ): Effect.Effect<{
341 threadId: ReturnType<typeof ProviderThreadId.makeUnsafe>;
342 turns: ReadonlyArray<{
343 id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
344 items: ReadonlyArray<unknown>;
345 }>;
346 }> =>
347 Effect.gen(function* () {
348 const threadId =
349 context.session.threadId ??
350 ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);
351 return {
352 threadId,
353 turns: context.turns.map((turn) => ({
354 id: turn.id,
355 items: [...turn.items],
356 })),
357 };
358 });
359
360 const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> =>
361 Effect.gen(function* () {
362 const threadId = context.session.threadId;
363 if (!threadId) {
364 return;
365 }
366
367 const resumeCursor = {
368 threadId,
369 resume: threadId,
370 ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}),
371 turnCount: context.turns.length,
372 };
373
374 context.session = {
375 ...context.session,
376 resumeCursor,
377 updatedAt: yield* now(),
378 };
379 });
380
381 const ensureThreadId = (
382 context: ClaudeSessionContext,
383 message: SDKMessage,
384 ): Effect.Effect<void> =>
385 Effect.gen(function* () {
386 const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id);
387 const changed = context.session.threadId !== nextThreadId;
388
389 if (changed) {
390 const updatedAt = yield* now();
391 context.session = {
392 ...context.session,
393 threadId: nextThreadId,
394 updatedAt,
395 };
396 yield* updateResumeCursor(context);
397 }
398
399 if (context.lastThreadStartedId !== nextThreadId) {
400 context.lastThreadStartedId = nextThreadId;
401 const stamp = yield* makeEventStamp();
402 yield* offerRuntimeEvent({
403 type: "thread.started",
404 eventId: stamp.eventId,
405 provider: PROVIDER,
406 sessionId: context.session.sessionId,
407 createdAt: stamp.createdAt,
408 threadId: nextThreadId,
409 });
410 }
411 });
412
413 const emitRuntimeError = (
414 context: ClaudeSessionContext,
415 message: string,
416 cause?: unknown,
417 ): Effect.Effect<void> =>
418 Effect.gen(function* () {
419 if (cause !== undefined) {
420 void cause;
421 }
422 const turnState = context.turnState;
423 const stamp = yield* makeEventStamp();
424 yield* offerRuntimeEvent({
425 type: "runtime.error",
426 eventId: stamp.eventId,
427 provider: PROVIDER,
428 sessionId: context.session.sessionId,
429 createdAt: stamp.createdAt,
430 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
431 ...(turnState ? { turnId: turnState.turnId } : {}),
432 message,
433 });
434 });
435
436 const completeTurn = (
437 context: ClaudeSessionContext,
438 status: ProviderRuntimeTurnStatus,
439 errorMessage?: string,
440 ): Effect.Effect<void> =>
441 Effect.gen(function* () {
442 const turnState = context.turnState;
443 if (!turnState) {
444 const stamp = yield* makeEventStamp();
445 yield* offerRuntimeEvent({
446 type: "turn.completed",
447 eventId: stamp.eventId,
448 provider: PROVIDER,
449 sessionId: context.session.sessionId,
450 createdAt: stamp.createdAt,
451 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
452 status,
453 ...(errorMessage ? { errorMessage } : {}),
454 });
455 return;
456 }
457
458 if (!turnState.messageCompleted) {
459 const stamp = yield* makeEventStamp();
460 yield* offerRuntimeEvent({
461 type: "message.completed",
462 eventId: stamp.eventId,
463 provider: PROVIDER,
464 sessionId: context.session.sessionId,
465 createdAt: stamp.createdAt,
466 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
467 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
468 turnId: turnState.turnId,
469 });
470 }
471
472 context.turns.push({
473 id: turnState.turnId,
474 items: [...turnState.items],
475 });
476
477 const stamp = yield* makeEventStamp();
478 yield* offerRuntimeEvent({
479 type: "turn.completed",
480 eventId: stamp.eventId,
71 provider: PROVIDER,481 provider: PROVIDER,
72 operation: "rollbackThread",482 sessionId: context.session.sessionId,
73 issue: "numTurns must be an integer >= 1.",483 createdAt: stamp.createdAt,
74 }),484 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
485 turnId: turnState.turnId,
486 status,
487 ...(errorMessage ? { errorMessage } : {}),
488 });
489
490 const updatedAt = yield* now();
491 context.turnState = undefined;
492 context.session = {
493 ...context.session,
494 status: "ready",
495 activeTurnId: undefined,
496 updatedAt,
497 ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}),
498 };
499 yield* updateResumeCursor(context);
500 });
501
502 const handleStreamEvent = (
503 context: ClaudeSessionContext,
504 message: SDKMessage,
505 ): Effect.Effect<void> =>
506 Effect.gen(function* () {
507 if (message.type !== "stream_event") {
508 return;
509 }
510
511 const { event } = message;
512
513 if (event.type === "content_block_delta") {
514 if (
515 event.delta.type === "text_delta" &&
516 event.delta.text.length > 0 &&
517 context.turnState
518 ) {
519 const stamp = yield* makeEventStamp();
520 yield* offerRuntimeEvent({
521 type: "message.delta",
522 eventId: stamp.eventId,
523 provider: PROVIDER,
524 sessionId: context.session.sessionId,
525 createdAt: stamp.createdAt,
526 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
527 turnId: context.turnState.turnId,
528 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
529 delta: event.delta.text,
530 });
531 }
532 return;
533 }
534
535 if (event.type === "content_block_start") {
536 const { index, content_block: block } = event;
537 if (
538 block.type !== "tool_use" &&
539 block.type !== "server_tool_use" &&
540 block.type !== "mcp_tool_use"
541 ) {
542 return;
543 }
544
545 const toolName = block.name;
546 const toolKind = classifyToolKind(toolName);
547 const toolInput =
548 typeof block.input === "object" && block.input !== null
549 ? (block.input as Record<string, unknown>)
550 : {};
551 const itemId = block.id;
552 const detail = summarizeToolRequest(toolName, toolInput);
553
554 const tool: ToolInFlight = {
555 itemId,
556 toolKind,
557 title: titleForTool(toolKind),
558 detail,
559 };
560 context.inFlightTools.set(index, tool);
561
562 const stamp = yield* makeEventStamp();
563 yield* offerRuntimeEvent({
564 type: "tool.started",
565 eventId: stamp.eventId,
566 provider: PROVIDER,
567 sessionId: context.session.sessionId,
568 createdAt: stamp.createdAt,
569 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
570 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
571 itemId: ProviderItemId.makeUnsafe(tool.itemId),
572 toolKind: tool.toolKind,
573 title: tool.title,
574 ...(tool.detail ? { detail: tool.detail } : {}),
575 });
576 return;
577 }
578
579 if (event.type === "content_block_stop") {
580 const { index } = event;
581 const tool = context.inFlightTools.get(index);
582 if (!tool) {
583 return;
584 }
585 context.inFlightTools.delete(index);
586
587 const stamp = yield* makeEventStamp();
588 yield* offerRuntimeEvent({
589 type: "tool.completed",
590 eventId: stamp.eventId,
591 provider: PROVIDER,
592 sessionId: context.session.sessionId,
593 createdAt: stamp.createdAt,
594 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
595 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
596 itemId: ProviderItemId.makeUnsafe(tool.itemId),
597 toolKind: tool.toolKind,
598 title: tool.title,
599 ...(tool.detail ? { detail: tool.detail } : {}),
600 });
601 }
602 });
603
604 const handleAssistantMessage = (
605 context: ClaudeSessionContext,
606 message: SDKMessage,
607 ): Effect.Effect<void> =>
608 Effect.gen(function* () {
609 if (message.type !== "assistant") {
610 return;
611 }
612
613 if (context.turnState) {
614 context.turnState.items.push(message.message);
615 if (!context.turnState.messageCompleted) {
616 context.turnState = {
617 ...context.turnState,
618 messageCompleted: true,
619 };
620 const stamp = yield* makeEventStamp();
621 yield* offerRuntimeEvent({
622 type: "message.completed",
623 eventId: stamp.eventId,
624 provider: PROVIDER,
625 sessionId: context.session.sessionId,
626 createdAt: stamp.createdAt,
627 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
628 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
629 turnId: context.turnState.turnId,
630 });
631 }
632 }
633
634 context.lastAssistantUuid = message.uuid;
635 yield* updateResumeCursor(context);
636 });
637
638 const handleResultMessage = (
639 context: ClaudeSessionContext,
640 message: SDKMessage,
641 ): Effect.Effect<void> =>
642 Effect.gen(function* () {
643 if (message.type !== "result") {
644 return;
645 }
646
647 const status = turnStatusFromResult(message);
648 const errorMessage = message.subtype === "success" ? undefined : message.errors[0];
649
650 if (status === "failed") {
651 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
652 }
653
654 yield* completeTurn(context, status, errorMessage);
655 });
656
657 const handleSdkMessage = (
658 context: ClaudeSessionContext,
659 message: SDKMessage,
660 ): Effect.Effect<void> =>
661 Effect.gen(function* () {
662 yield* ensureThreadId(context, message);
663
664 switch (message.type) {
665 case "stream_event":
666 yield* handleStreamEvent(context, message);
667 return;
668 case "assistant":
669 yield* handleAssistantMessage(context, message);
670 return;
671 case "result":
672 yield* handleResultMessage(context, message);
673 return;
674 default:
675 return;
676 }
677 });
678
679 const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void> =>
680 Stream.fromAsyncIterable(context.query, (cause) => cause).pipe(
681 Stream.takeWhile(() => !context.stopped),
682 Stream.runForEach((message) => handleSdkMessage(context, message)),
683 Effect.catchCause((cause) =>
684 Effect.gen(function* () {
685 if (Cause.hasInterruptsOnly(cause) || context.stopped) {
686 return;
687 }
688 const message = toMessage(Cause.squash(cause), "Claude runtime stream failed.");
689 yield* emitRuntimeError(context, message, cause);
690 yield* completeTurn(context, "failed", message);
691 }),
692 ),
75 );693 );
76 }
77 694
78 return Effect.fail(695 const stopSessionInternal = (
79 new ProviderAdapterRequestError({696 context: ClaudeSessionContext,
80 provider: PROVIDER,697 options?: { readonly emitExitEvent?: boolean },
81 method: "thread/rollback",698 ): Effect.Effect<void> =>
82 detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,699 Effect.gen(function* () {
83 }),700 if (context.stopped) {
701 return;
702 }
703
704 context.stopped = true;
705
706 for (const [requestId, pending] of context.pendingApprovals) {
707 yield* Deferred.succeed(pending.decision, "cancel");
708 const stamp = yield* makeEventStamp();
709 yield* offerRuntimeEvent({
710 type: "approval.resolved",
711 eventId: stamp.eventId,
712 provider: PROVIDER,
713 sessionId: context.session.sessionId,
714 createdAt: stamp.createdAt,
715 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
716 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
717 requestId,
718 requestKind: pending.requestKind,
719 decision: "cancel",
720 });
721 }
722 context.pendingApprovals.clear();
723
724 if (context.turnState) {
725 yield* completeTurn(context, "interrupted", "Session stopped.");
726 }
727
728 yield* Queue.offer(context.promptQueue, { type: "terminate" }).pipe(Effect.ignore);
729
730 yield* Effect.sync(() => context.query.close()).pipe(Effect.ignoreCause);
731
732 const updatedAt = yield* now();
733 context.session = {
734 ...context.session,
735 status: "closed",
736 activeTurnId: undefined,
737 updatedAt,
738 };
739
740 if (options?.emitExitEvent !== false) {
741 const stamp = yield* makeEventStamp();
742 yield* offerRuntimeEvent({
743 type: "session.exited",
744 eventId: stamp.eventId,
745 provider: PROVIDER,
746 sessionId: context.session.sessionId,
747 createdAt: stamp.createdAt,
748 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
749 message: "Session stopped",
750 });
751 }
752
753 sessions.delete(context.session.sessionId);
754 });
755
756 const requireSession = (
757 sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
758 ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => {
759 const context = sessions.get(sessionId);
760 if (!context) {
761 return Effect.fail(
762 new ProviderAdapterSessionNotFoundError({
763 provider: PROVIDER,
764 sessionId,
765 }),
766 );
767 }
768 if (context.stopped || context.session.status === "closed") {
769 return Effect.fail(
770 new ProviderAdapterSessionClosedError({
771 provider: PROVIDER,
772 sessionId,
773 }),
774 );
775 }
776 return Effect.succeed(context);
777 };
778
779 const startSession: ClaudeCodeAdapterShape["startSession"] = (input) =>
780 Effect.gen(function* () {
781 if (input.provider !== undefined && input.provider !== PROVIDER) {
782 return yield* new ProviderAdapterValidationError({
783 provider: PROVIDER,
784 operation: "startSession",
785 issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
786 });
787 }
788
789 const startedAt = yield* now();
790 const sessionId = ProviderSessionId.makeUnsafe(`claude-session-${yield* randomId()}`);
791 const resumeState = readClaudeResumeState(input.resumeCursor);
792 const threadId =
793 resumeState?.threadId ??
794 ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);
795
796 const promptQueue = yield* Queue.unbounded<PromptQueueItem>();
797 const prompt: AsyncIterable<SDKUserMessage> = {
798 [Symbol.asyncIterator]() {
799 return {
800 next: async (): Promise<IteratorResult<SDKUserMessage>> => {
801 const item = await Effect.runPromise(Queue.take(promptQueue));
802 if (item.type === "terminate") {
803 return {
804 done: true,
805 value: undefined,
806 };
807 }
808
809 return {
810 done: false,
811 value: item.message,
812 };
813 },
814 };
815 },
816 };
817
818 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
819 const inFlightTools = new Map<number, ToolInFlight>();
820
821 let contextRef: ClaudeSessionContext | undefined;
822
823 const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>
824 Effect.runPromise(
825 Effect.gen(function* () {
826 const context = contextRef;
827 if (!context) {
828 return {
829 behavior: "deny",
830 message: "Claude session context is unavailable.",
831 } satisfies PermissionResult;
832 }
833
834 const approvalPolicy = input.approvalPolicy ?? "never";
835 if (approvalPolicy === "never") {
836 return {
837 behavior: "allow",
838 updatedInput: toolInput,
839 } satisfies PermissionResult;
840 }
841
842 const requestId = ApprovalRequestId.makeUnsafe(yield* randomId());
843 const requestKind = classifyRequestKind(toolName);
844 const detail = summarizeToolRequest(toolName, toolInput);
845 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
846 const pendingApproval: PendingApproval = {
847 requestKind,
848 detail,
849 decision: decisionDeferred,
850 ...(callbackOptions.suggestions
851 ? { suggestions: callbackOptions.suggestions }
852 : {}),
853 };
854
855 const requestedStamp = yield* makeEventStamp();
856 yield* offerRuntimeEvent({
857 type: "approval.requested",
858 eventId: requestedStamp.eventId,
859 provider: PROVIDER,
860 sessionId: context.session.sessionId,
861 createdAt: requestedStamp.createdAt,
862 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
863 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
864 requestId,
865 requestKind,
866 detail,
867 });
868
869 pendingApprovals.set(requestId, pendingApproval);
870
871 const onAbort = () => {
872 if (!pendingApprovals.has(requestId)) {
873 return;
874 }
875 pendingApprovals.delete(requestId);
876 Effect.runFork(Deferred.succeed(decisionDeferred, "cancel"));
877 };
878
879 callbackOptions.signal.addEventListener("abort", onAbort, {
880 once: true,
881 });
882
883 const decision = yield* Deferred.await(decisionDeferred);
884 pendingApprovals.delete(requestId);
885
886 const resolvedStamp = yield* makeEventStamp();
887 yield* offerRuntimeEvent({
888 type: "approval.resolved",
889 eventId: resolvedStamp.eventId,
890 provider: PROVIDER,
891 sessionId: context.session.sessionId,
892 createdAt: resolvedStamp.createdAt,
893 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
894 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
895 requestId,
896 requestKind,
897 decision,
898 });
899
900 if (decision === "accept" || decision === "acceptForSession") {
901 return {
902 behavior: "allow",
903 updatedInput: toolInput,
904 ...(decision === "acceptForSession" && pendingApproval.suggestions
905 ? { updatedPermissions: [...pendingApproval.suggestions] }
906 : {}),
907 } satisfies PermissionResult;
908 }
909
910 return {
911 behavior: "deny",
912 message:
913 decision === "cancel"
914 ? "User cancelled tool execution."
915 : "User declined tool execution.",
916 } satisfies PermissionResult;
917 }),
918 );
919
920 const providerOptions = input.providerOptions?.claudeCode;
921 const permissionMode = toPermissionMode(providerOptions?.permissionMode);
922
923 const queryOptions: ClaudeQueryOptions = {
924 ...(input.cwd ? { cwd: input.cwd } : {}),
925 ...(input.model ? { model: input.model } : {}),
926 ...(providerOptions?.binaryPath
927 ? { pathToClaudeCodeExecutable: providerOptions.binaryPath }
928 : {}),
929 ...(permissionMode ? { permissionMode } : {}),
930 ...(permissionMode === "bypassPermissions"
931 ? { allowDangerouslySkipPermissions: true }
932 : {}),
933 ...(providerOptions?.maxThinkingTokens !== undefined
934 ? { maxThinkingTokens: providerOptions.maxThinkingTokens }
935 : {}),
936 ...(resumeState?.resume ? { resume: resumeState.resume } : {}),
937 ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}),
938 includePartialMessages: true,
939 canUseTool,
940 env: process.env,
941 ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
942 };
943
944 const queryRuntime = yield* Effect.try({
945 try: () =>
946 createQuery({
947 prompt,
948 options: queryOptions,
949 }),
950 catch: (cause) =>
951 new ProviderAdapterProcessError({
952 provider: PROVIDER,
953 sessionId,
954 detail: toMessage(cause, "Failed to start Claude runtime session."),
955 cause,
956 }),
957 });
958
959 const session: ProviderSession = {
960 sessionId,
961 provider: PROVIDER,
962 status: "ready",
963 ...(input.cwd ? { cwd: input.cwd } : {}),
964 ...(input.model ? { model: input.model } : {}),
965 threadId,
966 resumeCursor: {
967 threadId,
968 resume: resumeState?.resume ?? threadId,
969 ...(resumeState?.resumeSessionAt
970 ? { resumeSessionAt: resumeState.resumeSessionAt }
971 : {}),
972 turnCount: resumeState?.turnCount ?? 0,
973 },
974 createdAt: startedAt,
975 updatedAt: startedAt,
976 };
977
978 const context: ClaudeSessionContext = {
979 session,
980 promptQueue,
981 query: queryRuntime,
982 startedAt,
983 pendingApprovals,
984 turns: [],
985 inFlightTools,
986 turnState: undefined,
987 lastAssistantUuid: resumeState?.resumeSessionAt,
988 lastThreadStartedId: undefined,
989 stopped: false,
990 };
991 contextRef = context;
992 sessions.set(sessionId, context);
993
994 const sessionStartedStamp = yield* makeEventStamp();
995 yield* offerRuntimeEvent({
996 type: "session.started",
997 eventId: sessionStartedStamp.eventId,
998 provider: PROVIDER,
999 sessionId,
1000 createdAt: sessionStartedStamp.createdAt,
1001 threadId,
1002 });
1003
1004 Effect.runFork(runSdkStream(context));
1005
1006 return {
1007 ...session,
1008 };
1009 });
1010
1011 const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) =>
1012 Effect.gen(function* () {
1013 const context = yield* requireSession(input.sessionId);
1014
1015 if (context.turnState) {
1016 return yield* new ProviderAdapterValidationError({
1017 provider: PROVIDER,
1018 operation: "sendTurn",
1019 issue: `Session '${input.sessionId}' already has an active turn '${context.turnState.turnId}'.`,
1020 });
1021 }
1022
1023 if (input.model) {
1024 yield* Effect.tryPromise({
1025 try: () => context.query.setModel(input.model),
1026 catch: (cause) => toRequestError(input.sessionId, "turn/setModel", cause),
1027 });
1028 }
1029
1030 const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* randomId()}`);
1031 const turnState: ClaudeTurnState = {
1032 turnId,
1033 assistantItemId: `claude-message-${yield* randomId()}`,
1034 startedAt: yield* now(),
1035 items: [],
1036 messageCompleted: false,
1037 };
1038
1039 const updatedAt = yield* now();
1040 context.turnState = turnState;
1041 context.session = {
1042 ...context.session,
1043 status: "running",
1044 activeTurnId: turnId,
1045 updatedAt,
1046 };
1047
1048 const turnStartedStamp = yield* makeEventStamp();
1049 yield* offerRuntimeEvent({
1050 type: "turn.started",
1051 eventId: turnStartedStamp.eventId,
1052 provider: PROVIDER,
1053 sessionId: context.session.sessionId,
1054 createdAt: turnStartedStamp.createdAt,
1055 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1056 turnId,
1057 });
1058
1059 const message = buildUserMessage(input);
1060
1061 yield* Queue.offer(context.promptQueue, {
1062 type: "message",
1063 message,
1064 }).pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)));
1065
1066 const threadId = context.session.threadId;
1067 if (!threadId) {
1068 return yield* new ProviderAdapterValidationError({
1069 provider: PROVIDER,
1070 operation: "sendTurn",
1071 issue: "Session thread id is not initialized.",
1072 });
1073 }
1074
1075 return {
1076 threadId,
1077 turnId,
1078 ...(context.session.resumeCursor !== undefined
1079 ? { resumeCursor: context.session.resumeCursor }
1080 : {}),
1081 };
1082 });
1083
1084 const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, _turnId) =>
1085 Effect.gen(function* () {
1086 const context = yield* requireSession(sessionId);
1087 yield* Effect.tryPromise({
1088 try: () => context.query.interrupt(),
1089 catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause),
1090 });
1091 });
1092
1093 const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) =>
1094 Effect.gen(function* () {
1095 const context = yield* requireSession(sessionId);
1096 return yield* snapshotThread(context);
1097 });
1098
1099 const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) =>
1100 Effect.gen(function* () {
1101 const context = yield* requireSession(sessionId);
1102 const nextLength = Math.max(0, context.turns.length - numTurns);
1103 context.turns.splice(nextLength);
1104 yield* updateResumeCursor(context);
1105 return yield* snapshotThread(context);
1106 });
1107
1108 const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = (
1109 sessionId,
1110 requestId,
1111 decision,
1112 ) =>
1113 Effect.gen(function* () {
1114 const context = yield* requireSession(sessionId);
1115 const pending = context.pendingApprovals.get(requestId);
1116 if (!pending) {
1117 return yield* new ProviderAdapterRequestError({
1118 provider: PROVIDER,
1119 method: "item/requestApproval/decision",
1120 detail: `Unknown pending approval request: ${requestId}`,
1121 });
1122 }
1123
1124 context.pendingApprovals.delete(requestId);
1125 yield* Deferred.succeed(pending.decision, decision);
1126 });
1127
1128 const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) =>
1129 Effect.gen(function* () {
1130 const context = yield* requireSession(sessionId);
1131 yield* stopSessionInternal(context, {
1132 emitExitEvent: true,
1133 });
1134 });
1135
1136 const listSessions: ClaudeCodeAdapterShape["listSessions"] = () =>
1137 Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session })));
1138
1139 const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) =>
1140 Effect.sync(() => {
1141 const context = sessions.get(sessionId);
1142 return context !== undefined && !context.stopped;
1143 });
1144
1145 const stopAll: ClaudeCodeAdapterShape["stopAll"] = () =>
1146 Effect.forEach(
1147 sessions,
1148 ([, context]) =>
1149 stopSessionInternal(context, {
1150 emitExitEvent: true,
1151 }),
1152 { discard: true },
1153 );
1154
1155 yield* Effect.addFinalizer(() =>
1156 Effect.forEach(
1157 sessions,
1158 ([, context]) =>
1159 stopSessionInternal(context, {
1160 emitExitEvent: false,
1161 }),
1162 { discard: true },
1163 ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
84 );1164 );
85 },1165
86 respondToRequest: (sessionId, _requestId, _decision) =>1166 return {
87 Effect.fail(1167 provider: PROVIDER,
88 new ProviderAdapterRequestError({1168 startSession,
89 provider: PROVIDER,1169 sendTurn,
90 method: "item/requestApproval/decision",1170 interruptTurn,
91 detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,1171 readThread,
92 }),1172 rollbackThread,
93 ),1173 respondToRequest,
94 stopSession: (_sessionId) => Effect.void,1174 stopSession,
95 listSessions: () => Effect.succeed([]),1175 listSessions,
96 hasSession: (_sessionId) => Effect.succeed(false),1176 hasSession,
97 stopAll: () => Effect.void,1177 stopAll,
98 streamEvents: Stream.empty as Stream.Stream<ProviderRuntimeEvent>,1178 streamEvents: Stream.fromQueue(runtimeEventQueue),
99} satisfies ClaudeCodeAdapterShape);1179 } satisfies ClaudeCodeAdapterShape;
100 1180 });
101export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);1181}
102 1182
103export function makeClaudeCodeAdapterLive() {1183export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter());
104 return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);1184
1185export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) {
1186 return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options));
105}1187}
bun.lock+35−0
@@ -35,6 +35,7 @@ · 34 unmodified lines
35 "t3": "./dist/index.mjs",35 "t3": "./dist/index.mjs",
36 },36 },
37 "dependencies": {37 "dependencies": {
38 "@anthropic-ai/claude-agent-sdk": "^0.2.62",
38 "@effect/platform-node": "catalog:",39 "@effect/platform-node": "catalog:",
39 "@effect/sql-sqlite-bun": "catalog:",40 "@effect/sql-sqlite-bun": "catalog:",
40 "@pierre/diffs": "^1.1.0-beta.16",41 "@pierre/diffs": "^1.1.0-beta.16",
@@ -115,6 +116,8 @@ · 74 unmodified lines
115 "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@f591738",116 "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@f591738",
116 },117 },
117 "packages": {118 "packages": {
119 "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.62", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-exRJ2djKP6erRpUrtnVf5iXzqeS9dAie9d1ghODZVVXdLqieSqCpaAhZhe0hL1yvP33OL0J5CgT9RAyPzhYY9A=="],
120
118 "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],121 "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
119 122
120 "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],123 "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
@@ -253,6 +256,38 @@ · 132 unmodified lines
253 256
254 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],257 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
255 258
259 "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
260
261 "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
262
263 "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
264
265 "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
266
267 "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
268
269 "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
270
271 "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
272
273 "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
274
275 "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
276
277 "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
278
279 "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
280
281 "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
282
283 "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
284
285 "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
286
287 "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
288
289 "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
290
256 "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],291 "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
257 292
258 "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],293 "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],