@anureo/adapter-claude 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1000 @@
1
+ // packages/claude/src/file-changes.ts
2
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
+ function claudeFileChanges(name, input, result) {
4
+ if (!["Edit", "MultiEdit", "Write"].includes(name))
5
+ return;
6
+ const data = record(input), output = record(result);
7
+ const path = typeof output.filePath === "string" ? output.filePath : typeof data.file_path === "string" ? data.file_path : undefined;
8
+ if (path === undefined)
9
+ return [];
10
+ if (typeof output.patch === "string")
11
+ return [{ path, operation: "modify", materials: [{ format: "unified", patch: output.patch }], completeness: "complete" }];
12
+ if ((output.oldText === null || typeof output.oldText === "string") && typeof output.newText === "string")
13
+ return [{ path, operation: output.oldText === null ? "add" : "modify", materials: [{ format: "text", scope: "file", oldText: output.oldText, newText: output.newText }], completeness: "complete" }];
14
+ if (Array.isArray(output.structuredPatch) && output.structuredPatch.length) {
15
+ const hunks = output.structuredPatch.map(record);
16
+ const valid = hunks.every((hunk) => ["oldStart", "oldLines", "newStart", "newLines"].every((key) => Number.isInteger(hunk[key]) && hunk[key] >= 0) && Array.isArray(hunk.lines) && hunk.lines.every((line) => typeof line === "string" && /^[ +\\-]/.test(line)));
17
+ if (valid) {
18
+ const patch = hunks.map((hunk) => `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@
19
+ ${hunk.lines.join(`
20
+ `)}
21
+ `).join("");
22
+ return [{ path, operation: output.type === "create" ? "add" : "modify", materials: [{ format: "unified", patch }], completeness: "complete" }];
23
+ }
24
+ }
25
+ if (name === "Write" && typeof output.content === "string" && (typeof output.originalFile === "string" || output.type === "create" && output.originalFile === null)) {
26
+ return [{ path, operation: output.type === "create" ? "add" : "modify", materials: [{ format: "text", scope: "file", oldText: output.originalFile, newText: output.content }], completeness: "complete" }];
27
+ }
28
+ if (name === "Write")
29
+ return [{ path, operation: "unknown", materials: [{ format: "unavailable", reason: "notProvided" }], completeness: "partial" }];
30
+ const edits = name === "MultiEdit" && Array.isArray(data.edits) ? data.edits : [data];
31
+ const materials = edits.flatMap((value) => {
32
+ const edit = record(value);
33
+ return typeof edit.old_string === "string" && typeof edit.new_string === "string" ? [{ format: "text", scope: "fragment", oldText: edit.old_string, newText: edit.new_string }] : [];
34
+ });
35
+ const partial = name === "MultiEdit" || edits.some((value) => record(value).replace_all === true) || materials.length !== edits.length;
36
+ return [{ path, operation: "modify", materials: materials.length ? materials : [{ format: "unavailable", reason: "notProvided" }], completeness: partial || !materials.length ? "partial" : "complete" }];
37
+ }
38
+ function completeClaudeTool(previous, block) {
39
+ const fileChanges = claudeFileChanges(previous.nativeType ?? previous.title, previous.input, block.structuredContent ?? block.content);
40
+ return { ...previous, status: previous.status === "declined" || previous.status === "interrupted" ? previous.status : block.is_error ? "failed" : "completed", output: block.content, ...fileChanges !== undefined ? { fileChanges } : {} };
41
+ }
42
+
43
+ // packages/claude/src/index.ts
44
+ import { randomUUID as randomUUID2 } from "node:crypto";
45
+ import { spawn } from "node:child_process";
46
+ import { join as join2 } from "node:path";
47
+ import {
48
+ query,
49
+ getSessionMessages,
50
+ getSessionInfo,
51
+ forkSession
52
+ } from "@anthropic-ai/claude-agent-sdk";
53
+ import {
54
+ BoundedEventQueue,
55
+ HarnessSdkError as HarnessSdkError2,
56
+ sdkError
57
+ } from "@anureo/sdk";
58
+ import {
59
+ negotiateVersion,
60
+ publicValue,
61
+ validateJsonSchema
62
+ } from "@anureo/protocol";
63
+
64
+ // packages/claude/src/store.ts
65
+ import { createHash, randomUUID } from "node:crypto";
66
+ import { mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
67
+ import { join } from "node:path";
68
+ import { HarnessSdkError } from "@anureo/sdk";
69
+
70
+ class FileSessionStore {
71
+ directory;
72
+ constructor(directory) {
73
+ this.directory = directory;
74
+ }
75
+ file(key) {
76
+ return join(this.directory, `${createHash("sha256").update(JSON.stringify([key.projectKey, key.sessionId, key.subpath ?? null])).digest("hex")}.json`);
77
+ }
78
+ async read(file) {
79
+ try {
80
+ const value = JSON.parse(await readFile(file, "utf8"));
81
+ if (!value || !value.key || !Array.isArray(value.entries))
82
+ throw new HarnessSdkError("SESSION_STORE_INVALID");
83
+ return value;
84
+ } catch (cause) {
85
+ if (cause.code === "ENOENT")
86
+ return null;
87
+ throw cause;
88
+ }
89
+ }
90
+ async append(key, entries) {
91
+ await mkdir(this.directory, { recursive: true, mode: 448 });
92
+ const file = this.file(key);
93
+ const lockPath = `${file}.lock`;
94
+ const expires = Date.now() + 1e4;
95
+ let lock;
96
+ while (!lock) {
97
+ try {
98
+ lock = await open(lockPath, "wx", 384);
99
+ await lock.writeFile(String(process.pid));
100
+ } catch (cause) {
101
+ if (cause.code !== "EEXIST")
102
+ throw cause;
103
+ const pid = Number(await readFile(lockPath, "utf8").catch(() => ""));
104
+ let alive = true;
105
+ if (Number.isSafeInteger(pid) && pid > 0) {
106
+ try {
107
+ process.kill(pid, 0);
108
+ } catch (error) {
109
+ alive = error.code !== "ESRCH";
110
+ }
111
+ }
112
+ if (!alive) {
113
+ await rm(lockPath, { force: true });
114
+ continue;
115
+ }
116
+ if (Date.now() >= expires)
117
+ throw new HarnessSdkError("SESSION_STORE_BUSY");
118
+ await new Promise((resolve) => setTimeout(resolve, 10));
119
+ }
120
+ }
121
+ const temporary = `${file}.${randomUUID()}.tmp`;
122
+ try {
123
+ const stored = await this.read(file) ?? { key: structuredClone(key), entries: [], mtime: 0 };
124
+ const byId = new Map(stored.entries.flatMap((entry, index) => entry.uuid ? [[entry.uuid, index]] : []));
125
+ for (const entry of entries) {
126
+ const previous = entry.uuid ? byId.get(entry.uuid) : undefined;
127
+ if (previous !== undefined)
128
+ stored.entries[previous] = structuredClone(entry);
129
+ else {
130
+ if (entry.uuid)
131
+ byId.set(entry.uuid, stored.entries.length);
132
+ stored.entries.push(structuredClone(entry));
133
+ }
134
+ }
135
+ stored.mtime = Math.max(Date.now(), stored.mtime + 1);
136
+ const target = await open(temporary, "wx", 384);
137
+ try {
138
+ await target.writeFile(JSON.stringify(stored));
139
+ await target.sync();
140
+ } finally {
141
+ await target.close();
142
+ }
143
+ await rename(temporary, file);
144
+ } finally {
145
+ await rm(temporary, { force: true });
146
+ await lock.close();
147
+ await rm(lockPath, { force: true });
148
+ }
149
+ }
150
+ async load(key) {
151
+ const value = await this.read(this.file(key));
152
+ return value ? structuredClone(value.entries) : null;
153
+ }
154
+ async all() {
155
+ const names = await readdir(this.directory).catch((cause) => {
156
+ if (cause.code === "ENOENT")
157
+ return [];
158
+ throw cause;
159
+ });
160
+ const entries = await Promise.all(names.filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).map((name) => this.read(join(this.directory, name))));
161
+ return entries.filter((entry) => entry !== null);
162
+ }
163
+ async listSessions(projectKey) {
164
+ return (await this.all()).filter((entry) => entry.key.projectKey === projectKey && !entry.key.subpath).map(({ key, mtime }) => ({ sessionId: key.sessionId, mtime }));
165
+ }
166
+ async listSubkeys(key) {
167
+ return (await this.all()).filter((entry) => entry.key.projectKey === key.projectKey && entry.key.sessionId === key.sessionId && entry.key.subpath).map((entry) => entry.key.subpath);
168
+ }
169
+ async delete(key) {
170
+ await rm(this.file(key), { force: true });
171
+ }
172
+ turnMapPath(sessionId) {
173
+ return join(this.directory, `${createHash("sha256").update(`turns:${sessionId}`).digest("hex")}.turns.json`);
174
+ }
175
+ async loadTurnMap(sessionId) {
176
+ try {
177
+ return JSON.parse(await readFile(this.turnMapPath(sessionId), "utf8"));
178
+ } catch (cause) {
179
+ if (cause.code === "ENOENT")
180
+ return {};
181
+ throw cause;
182
+ }
183
+ }
184
+ async saveTurnMap(sessionId, turns) {
185
+ await mkdir(this.directory, { recursive: true, mode: 448 });
186
+ const path = this.turnMapPath(sessionId);
187
+ const temporary = `${path}.${randomUUID()}.tmp`;
188
+ try {
189
+ const file = await open(temporary, "wx", 384);
190
+ try {
191
+ await file.writeFile(JSON.stringify(turns));
192
+ await file.sync();
193
+ } finally {
194
+ await file.close();
195
+ }
196
+ await rename(temporary, path);
197
+ } finally {
198
+ await rm(temporary, { force: true });
199
+ }
200
+ }
201
+ }
202
+
203
+ // packages/claude/src/index.ts
204
+ function record2(value) {
205
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
206
+ }
207
+ function str(value) {
208
+ return typeof value === "string" ? value : undefined;
209
+ }
210
+ function json(value) {
211
+ try {
212
+ return JSON.parse(JSON.stringify(value ?? null));
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function safe(value) {
218
+ return publicValue(json(value));
219
+ }
220
+ function elicitationContent(value) {
221
+ const content = {};
222
+ for (const [key, field] of Object.entries(value)) {
223
+ if (typeof field === "string" || typeof field === "number" || typeof field === "boolean" || Array.isArray(field) && field.every((entry) => typeof entry === "string"))
224
+ content[key] = field;
225
+ else
226
+ throw new HarnessSdkError2("INVALID_PARAMS", "Elicitation response cannot be represented by MCP");
227
+ }
228
+ return content;
229
+ }
230
+ var PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
231
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
232
+ var isEffort = (value) => typeof value === "string" && EFFORT_LEVELS.includes(value);
233
+ function permission(value) {
234
+ if (typeof value !== "string" || !PERMISSION_MODES.includes(value))
235
+ throw new HarnessSdkError2("INVALID_PARAMS", "Unsupported permission mode");
236
+ return value;
237
+ }
238
+ function capabilities(store, checkpoints) {
239
+ return {
240
+ session: { resume: store, fork: store ? ["before", "through"] : [], revert: [...store ? ["conversation"] : [], ...checkpoints ? ["files"] : [], ...store && checkpoints ? ["both"] : []], history: store },
241
+ turn: { steer: false, interrupt: true, outputSchema: false, overrides: [], inputModalities: ["text", "image"] },
242
+ interactions: ["approval", "userInput", "elicitation"],
243
+ subagents: { events: "nested" },
244
+ mcp: { stdio: true, http: true, elicitation: ["form", "url"] },
245
+ config: { options: [{ id: "model", scope: "session", schema: { type: "string", minLength: 1 } }, { id: "permissionMode", title: "Permissions", scope: "session", schema: { type: "string", enum: PERMISSION_MODES, default: "default" } }, { id: "effort", title: "Effort", scope: "session", schema: { type: "string", enum: [...EFFORT_LEVELS], default: "high" } }, { id: "fastMode", title: "Fast mode", scope: "session", schema: { type: "boolean", default: false } }, { id: "maxThinkingTokens", scope: "session", schema: { type: ["integer", "null"], minimum: 0 } }] }
246
+ };
247
+ }
248
+ function basicEnvironment() {
249
+ const env = {};
250
+ for (const key of ["PATH", "Path", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE", "USER", "LOGNAME", "LANG", "LC_ALL"])
251
+ if (process.env[key] !== undefined)
252
+ env[key] = process.env[key];
253
+ return env;
254
+ }
255
+ function content(input) {
256
+ return input.map((block) => {
257
+ if (block.type === "text")
258
+ return { type: "text", text: block.text };
259
+ if (block.type === "image" && block.data && block.mimeType && ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(block.mimeType))
260
+ return { type: "image", source: { type: "base64", data: block.data, media_type: block.mimeType } };
261
+ if (block.type === "image" && block.url)
262
+ return { type: "image", source: { type: "url", url: block.url } };
263
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED", "Input cannot be represented by the Claude SDK");
264
+ });
265
+ }
266
+ function isPrompt(message) {
267
+ if (message.type !== "user")
268
+ return false;
269
+ const body = record2(message.message).content;
270
+ return typeof body === "string" || Array.isArray(body) && body.some((part) => record2(part).type !== "tool_result");
271
+ }
272
+
273
+ class ClaudeNativeAdapter {
274
+ options;
275
+ sessionStore;
276
+ defaultStore;
277
+ sessions = new Set;
278
+ turnMaps = new Map;
279
+ disposed = false;
280
+ env;
281
+ constructor(options = {}) {
282
+ this.options = options;
283
+ const stateDir = options.stateDir ?? options.env?.CLAUDE_CONFIG_DIR;
284
+ this.env = { ...basicEnvironment(), ...options.env, ...stateDir && !options.defaultHome ? { CLAUDE_CONFIG_DIR: stateDir } : {} };
285
+ if (!options.sessionStore && stateDir)
286
+ this.defaultStore = new FileSessionStore(join2(stateDir, "ahp-session-store"));
287
+ this.sessionStore = options.sessionStore ?? this.defaultStore;
288
+ }
289
+ get capabilities() {
290
+ return capabilities(!!this.sessionStore, !!this.options.enableFileCheckpointing);
291
+ }
292
+ async initialize(params) {
293
+ negotiateVersion(params.protocolVersions);
294
+ if (this.disposed)
295
+ throw new HarnessSdkError2("RUNTIME_DISPOSED");
296
+ if (!this.env.CLAUDE_CONFIG_DIR && !this.options.defaultHome && !this.options.queryFactory)
297
+ throw new HarnessSdkError2("HARNESS_CONFIG_INVALID", "Claude requires an explicit isolated CLAUDE_CONFIG_DIR");
298
+ return { protocolVersion: 1, runtime: { name: "claude", version: "0.3.257", adapter: "claude-native/0.1.0" }, capabilities: this.capabilities };
299
+ }
300
+ async openSession(params, sink) {
301
+ if (this.disposed)
302
+ throw new HarnessSdkError2("RUNTIME_DISPOSED");
303
+ if (params.binding.adapterId !== "claude" && params.binding.adapterId !== "claude-native")
304
+ throw new HarnessSdkError2("SESSION_BINDING_MISMATCH");
305
+ const session = new ClaudeBackendSession(this, params, sink, this.defaultStore);
306
+ await session.open();
307
+ this.sessions.add(session);
308
+ return session;
309
+ }
310
+ async listModels(cwd = process.cwd()) {
311
+ const input = new BoundedEventQueue;
312
+ const stream = (this.options.queryFactory ?? query)({ prompt: input, options: this.queryOptions(cwd) });
313
+ try {
314
+ return await stream.supportedModels();
315
+ } finally {
316
+ input.end();
317
+ stream.close();
318
+ }
319
+ }
320
+ queryOptions(cwd) {
321
+ return {
322
+ cwd,
323
+ env: this.env,
324
+ settingSources: [],
325
+ persistSession: true,
326
+ includePartialMessages: true,
327
+ includeHookEvents: true,
328
+ ...this.options.command ?? this.options.executable ? { pathToClaudeCodeExecutable: this.options.command ?? this.options.executable } : {},
329
+ ...this.options.executableRuntime ? { executable: this.options.executableRuntime } : {},
330
+ ...this.options.args?.length ? { spawnClaudeCodeProcess: (options) => spawn(options.command, [...options.args, ...this.options.args], { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ["pipe", "pipe", "pipe"], shell: false, windowsHide: true }) } : {},
331
+ ...this.sessionStore ? { sessionStore: this.sessionStore, sessionStoreFlush: "eager" } : {},
332
+ enableFileCheckpointing: this.options.enableFileCheckpointing ?? false
333
+ };
334
+ }
335
+ async dispose() {
336
+ if (this.disposed)
337
+ return;
338
+ this.disposed = true;
339
+ await Promise.allSettled([...this.sessions].map((session) => session.dispose()));
340
+ this.sessions.clear();
341
+ }
342
+ }
343
+
344
+ class ClaudeBackendSession {
345
+ adapter;
346
+ params;
347
+ sink;
348
+ fileStore;
349
+ binding;
350
+ capabilities;
351
+ query;
352
+ input;
353
+ active;
354
+ generation = 0;
355
+ closed = false;
356
+ config;
357
+ capabilityRevision = 1;
358
+ model;
359
+ turns = {};
360
+ historySnapshots = new Map;
361
+ fastMode;
362
+ storeKeys = new Map;
363
+ sessionTitle;
364
+ metadataTail = Promise.resolve();
365
+ metadataPending;
366
+ observedStore(store, generation) {
367
+ return {
368
+ append: async (key, entries) => {
369
+ await store.append(key, entries);
370
+ if (!key.subpath && key.sessionId === this.nativeId && generation === this.generation && !this.closed)
371
+ this.refreshMetadata();
372
+ },
373
+ load: store.load.bind(store),
374
+ ...store.listSessions ? { listSessions: store.listSessions.bind(store) } : {},
375
+ ...store.listSubkeys ? { listSubkeys: store.listSubkeys.bind(store) } : {},
376
+ ...store.delete ? { delete: store.delete.bind(store) } : {},
377
+ ...store.listSessionSummaries ? { listSessionSummaries: store.listSessionSummaries.bind(store) } : {}
378
+ };
379
+ }
380
+ refreshMetadata() {
381
+ const generation = this.generation, nativeId = this.nativeId;
382
+ if (this.metadataPending?.generation === generation) {
383
+ this.metadataPending.dirty = true;
384
+ return this.metadataPending.promise;
385
+ }
386
+ const pending = { generation, dirty: true, promise: Promise.resolve() };
387
+ pending.promise = this.metadataTail.then(async () => {
388
+ try {
389
+ while (pending.dirty && !this.closed && generation === this.generation && nativeId === this.nativeId) {
390
+ pending.dirty = false;
391
+ try {
392
+ const info = await (this.adapter.options.getSessionInfo ?? getSessionInfo)(nativeId, { dir: this.params.cwd, sessionStore: this.adapter.sessionStore });
393
+ if (!info || this.closed || generation !== this.generation || nativeId !== this.nativeId)
394
+ continue;
395
+ const title = info.summary.trim() || null;
396
+ if (this.sessionTitle === title)
397
+ continue;
398
+ this.sessionTitle = title;
399
+ await this.sink.emit({ type: "session/metadata", payload: { title } });
400
+ } catch {
401
+ if (!this.closed && generation === this.generation)
402
+ await this.sink.emit({ type: "notice", payload: { severity: "warning", code: "SESSION_METADATA_UNAVAILABLE", message: "Session title could not be refreshed" } }).catch(() => {});
403
+ }
404
+ }
405
+ } finally {
406
+ if (this.metadataPending === pending)
407
+ this.metadataPending = undefined;
408
+ }
409
+ });
410
+ this.metadataPending = pending;
411
+ this.metadataTail = pending.promise;
412
+ return pending.promise;
413
+ }
414
+ constructor(adapter, params, sink, fileStore) {
415
+ this.adapter = adapter;
416
+ this.params = params;
417
+ this.sink = sink;
418
+ this.fileStore = fileStore;
419
+ this.binding = { ...params.binding, nativeRef: params.binding.nativeRef ?? { provider: "claude", value: randomUUID2() } };
420
+ this.capabilities = adapter.capabilities;
421
+ this.config = structuredClone(params.config ?? {});
422
+ this.model = params.model ?? str(this.config.model);
423
+ for (const [key, value] of Object.entries(this.config)) {
424
+ const option = this.capabilities.config?.options.find((entry) => entry.id === key);
425
+ if (!option)
426
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED", "Unknown Claude config option");
427
+ if (!validateJsonSchema(option.schema, value))
428
+ throw new HarnessSdkError2("INVALID_PARAMS", "Invalid Claude config value");
429
+ }
430
+ }
431
+ get nativeId() {
432
+ return this.binding.nativeRef.value;
433
+ }
434
+ async open() {
435
+ if (this.params.mode === "resume" && !this.params.binding.nativeRef)
436
+ throw new HarnessSdkError2("SESSION_NOT_FOUND");
437
+ if (this.params.mode === "fork") {
438
+ if (!this.params.fork?.nativeRef)
439
+ throw new HarnessSdkError2("SESSION_NOT_FOUND");
440
+ const source = this.params.fork.nativeRef.value;
441
+ const point = await this.boundaryPoint(source, this.params.fork.boundary);
442
+ const forked = point.uuid ? await (this.adapter.options.forkSession ?? forkSession)(source, { dir: this.params.cwd, sessionStore: this.adapter.sessionStore, upToMessageId: point.uuid }) : { sessionId: randomUUID2() };
443
+ this.binding = { ...this.binding, nativeRef: { provider: "claude", value: forked.sessionId } };
444
+ this.turns = this.fileStore ? await this.fileStore.loadTurnMap(source) : structuredClone(this.adapter.turnMaps.get(source) ?? {});
445
+ } else
446
+ this.turns = this.fileStore ? await this.fileStore.loadTurnMap(this.nativeId) : this.adapter.turnMaps.get(this.nativeId) ?? {};
447
+ this.adapter.turnMaps.set(this.nativeId, this.turns);
448
+ await this.fileStore?.saveTurnMap(this.nativeId, this.turns);
449
+ await this.startQuery(this.params.mode === "new" || this.params.mode === "fork" && !await this.hasHistory(this.nativeId));
450
+ }
451
+ async hasHistory(id) {
452
+ return (await this.messages(id)).length > 0;
453
+ }
454
+ trackFastMode(value) {
455
+ const state = str(value.fast_mode_state);
456
+ if (state !== "off" && state !== "cooldown" && state !== "on")
457
+ return false;
458
+ const reason = str(value.fast_mode_disabled_reason);
459
+ const next = { state, ...reason ? { reason } : {} };
460
+ const changed = JSON.stringify(next) !== JSON.stringify(this.fastMode);
461
+ this.fastMode = next;
462
+ return changed;
463
+ }
464
+ snapshot() {
465
+ return { ...this.sessionTitle !== undefined ? { metadata: { title: this.sessionTitle } } : {}, binding: structuredClone(this.binding), status: this.active ? "running" : "ready", config: structuredClone(this.config), capabilities: this.capabilities, capabilityRevision: this.capabilityRevision, ...this.model ? { model: this.model } : {}, ...this.fastMode ? { fastMode: this.fastMode } : {}, ...this.active ? { activeTurnId: this.active.id } : {} };
466
+ }
467
+ async startQuery(fresh, resumeAt) {
468
+ const generation = ++this.generation;
469
+ this.input = new BoundedEventQueue({ maxEvents: 1 });
470
+ const mcpServers = {};
471
+ for (const server of this.params.mcpServers ?? [])
472
+ mcpServers[server.name] = server.type === "stdio" ? { type: "stdio", command: server.command, args: server.args, env: server.env } : { type: "http", url: server.url, headers: server.headers };
473
+ const options = {
474
+ ...this.adapter.queryOptions(this.params.cwd),
475
+ ...this.adapter.sessionStore ? { sessionStore: this.observedStore(this.adapter.sessionStore, generation) } : {},
476
+ ...fresh ? { sessionId: this.nativeId } : { resume: this.nativeId },
477
+ ...resumeAt ? { resumeSessionAt: resumeAt } : {},
478
+ ...this.model ? { model: this.model } : {},
479
+ ...this.params.instructions ? { systemPrompt: this.params.instructions } : {},
480
+ ...this.config.permissionMode !== undefined || this.params.permissionMode ? { permissionMode: permission(this.config.permissionMode ?? this.params.permissionMode) } : {},
481
+ allowDangerouslySkipPermissions: true,
482
+ ...typeof this.config.maxThinkingTokens === "number" ? { maxThinkingTokens: this.config.maxThinkingTokens } : {},
483
+ ...isEffort(this.config.effort) ? { effort: this.config.effort } : {},
484
+ ...this.config.fastMode === true ? { settings: { fastMode: true } } : {},
485
+ mcpServers,
486
+ canUseTool: async (toolName, input, details) => this.permissionRequest(toolName, input, details),
487
+ onElicitation: async (request, details) => this.elicitationRequest(request, details)
488
+ };
489
+ this.query = (this.adapter.options.queryFactory ?? query)({ prompt: this.input, options });
490
+ this.pump(this.query, generation);
491
+ if (!fresh)
492
+ await this.refreshMetadata();
493
+ }
494
+ async pump(stream, generation) {
495
+ try {
496
+ for await (const message of stream) {
497
+ if (this.closed || generation !== this.generation)
498
+ return;
499
+ await this.mapMessage(message);
500
+ }
501
+ if (!this.closed && generation === this.generation && this.active)
502
+ await this.sink.fail(new HarnessSdkError2("CONNECTION_CLOSED", "Claude query ended before a result", false, true));
503
+ } catch (cause) {
504
+ if (!this.closed && generation === this.generation)
505
+ await this.sink.fail(sdkError(cause, "CLAUDE_QUERY_FAILED", true));
506
+ }
507
+ }
508
+ async mapMessage(message) {
509
+ const value = record2(message);
510
+ const active = this.active;
511
+ if (message.type === "system" && message.subtype === "init") {
512
+ this.trackFastMode(value);
513
+ if (message.session_id !== this.nativeId) {
514
+ this.binding = { ...this.binding, nativeRef: { provider: "claude", value: message.session_id } };
515
+ this.adapter.turnMaps.set(this.nativeId, this.turns);
516
+ await this.fileStore?.saveTurnMap(this.nativeId, this.turns);
517
+ }
518
+ await this.sink.emit({ type: "session/state", payload: this.snapshot() });
519
+ return;
520
+ }
521
+ if (!active) {
522
+ await this.notice(value);
523
+ return;
524
+ }
525
+ if (value.user_message_uuid && value.user_message_uuid !== active.userUuid && !value.parent_tool_use_id)
526
+ throw new HarnessSdkError2("TURN_MISMATCH", "Claude replied to a different input UUID");
527
+ if (message.type === "assistant") {
528
+ for (const block of message.message.content)
529
+ await this.completeBlock(record2(block), message.message.id, message.parent_tool_use_id !== null);
530
+ return;
531
+ }
532
+ if (message.type === "stream_event") {
533
+ await this.streamEvent(record2(message.event));
534
+ return;
535
+ }
536
+ if (message.type === "user") {
537
+ const body = record2(message.message).content;
538
+ if (Array.isArray(body))
539
+ for (const part of body) {
540
+ const block = record2(part);
541
+ if (block.type !== "tool_result")
542
+ continue;
543
+ const toolId = str(block.tool_use_id);
544
+ if (!toolId)
545
+ continue;
546
+ const previous = active.output.get(toolId);
547
+ if (previous?.type === "toolCall") {
548
+ const item = completeClaudeTool(previous, { ...block, content: safe(block.content), ...value.tool_use_result !== undefined ? { structuredContent: safe(value.tool_use_result) } : {} });
549
+ active.output.set(toolId, item);
550
+ await this.sink.emit({ type: "item/completed", payload: { item } }, { turnId: active.id });
551
+ }
552
+ }
553
+ return;
554
+ }
555
+ if (message.type === "result") {
556
+ const resultText = message.subtype === "success" ? message.result : undefined;
557
+ const textItems = [...active.output.values()].filter((item) => item.type === "agentMessage" && item.status !== "completed");
558
+ for (let index = 0;index < textItems.length; index++) {
559
+ const item = textItems[index];
560
+ if (item.type !== "agentMessage")
561
+ continue;
562
+ const completed = { ...item, text: index === textItems.length - 1 && resultText !== undefined ? resultText : item.text, phase: index === textItems.length - 1 ? "final" : "commentary", status: "completed" };
563
+ active.output.set(item.id, completed);
564
+ await this.sink.emit({ type: "item/completed", payload: { item: completed } }, { turnId: active.id });
565
+ }
566
+ if (!textItems.length && resultText) {
567
+ const item = { id: `${active.userUuid}:final`, type: "agentMessage", phase: "final", text: resultText, status: "completed" };
568
+ await this.sink.emit({ type: "item/completed", payload: { item } }, { turnId: active.id });
569
+ }
570
+ const nativeUsage = record2(message.usage);
571
+ const usage = { provider: "claude", scope: "turn", source: "native", ...typeof nativeUsage.input_tokens === "number" ? { input: nativeUsage.input_tokens } : {}, ...typeof nativeUsage.output_tokens === "number" ? { output: nativeUsage.output_tokens } : {}, ...typeof nativeUsage.cache_read_input_tokens === "number" ? { cacheRead: nativeUsage.cache_read_input_tokens } : {}, ...typeof nativeUsage.cache_creation_input_tokens === "number" ? { cacheWrite: nativeUsage.cache_creation_input_tokens } : {} };
572
+ await this.sink.emit({ type: "usage/updated", payload: usage }, { turnId: active.id });
573
+ const interrupted = active.interrupted || str(message.stop_reason) === "interrupted" || str(value.terminal_reason) === "interrupted";
574
+ const failed = message.subtype !== "success" || message.is_error;
575
+ for (const pending of active.output.values()) {
576
+ if (pending.type !== "toolCall" || pending.kind !== "fileChange" || pending.status === "completed" || pending.status === "failed")
577
+ continue;
578
+ const item = { ...pending, status: active.stoppedTools.get(pending.id) ?? (interrupted ? "interrupted" : failed ? "failed" : undefined) };
579
+ active.output.set(item.id, item);
580
+ await this.sink.emit({ type: "item/completed", payload: { item } }, { turnId: active.id });
581
+ }
582
+ this.active = undefined;
583
+ this.trackFastMode(value);
584
+ await this.sink.emit({ type: "turn/completed", payload: { turnId: active.id, status: interrupted ? "interrupted" : failed ? "failed" : "completed", usage, ...failed && !interrupted ? { error: { code: "CLAUDE_TURN_FAILED", message: "Claude turn failed", retryable: false, outcomeUnknown: false } } : {} } });
585
+ this.refreshMetadata();
586
+ await this.sink.emit({ type: "session/state", payload: this.snapshot() });
587
+ return;
588
+ }
589
+ if (message.type === "system" && typeof value.subtype === "string") {
590
+ const subtype = value.subtype;
591
+ if (subtype.startsWith("task_")) {
592
+ const id = str(value.task_id);
593
+ if (!id)
594
+ return this.notice(value);
595
+ const previous = active.output.get(`task:${id}`);
596
+ const patch = record2(value.patch);
597
+ const status = str(value.status) ?? str(patch.status) ?? (subtype === "task_started" ? "running" : "running");
598
+ const final = ["completed", "failed", "stopped", "killed"].includes(status);
599
+ const item = { id: `task:${id}`, type: "subagent", subagentId: id, state: status === "completed" ? "completed" : status === "failed" ? "failed" : final ? "interrupted" : "running", status: final ? status === "completed" ? "completed" : "failed" : "running", task: str(value.description) ?? str(patch.description) ?? str(value.summary), ...str(value.tool_use_id) ? { parentItemId: str(value.tool_use_id) } : {} };
600
+ active.output.set(item.id, item);
601
+ if (final)
602
+ await this.sink.emit({ type: "item/completed", payload: { item } }, { turnId: active.id });
603
+ else if (!previous)
604
+ await this.sink.emit({ type: "item/started", payload: { item } }, { turnId: active.id });
605
+ else
606
+ await this.sink.emit({ type: "item/delta", payload: { itemId: item.id, kind: "patch", value: safe(item) } }, { turnId: active.id });
607
+ return;
608
+ }
609
+ if (subtype.startsWith("hook_")) {
610
+ const id = str(value.hook_id);
611
+ if (!id)
612
+ return this.notice(value);
613
+ const previous = active.output.get(`hook:${id}`);
614
+ const item = { id: `hook:${id}`, type: "hook", name: str(value.hook_name) ?? "hook", status: subtype === "hook_response" ? value.outcome === "success" ? "completed" : "failed" : "running", ...value.output !== undefined ? { output: safe(value.output) } : {} };
615
+ active.output.set(item.id, item);
616
+ if (subtype === "hook_progress") {
617
+ if (!previous)
618
+ await this.sink.emit({ type: "item/started", payload: { item } }, { turnId: active.id });
619
+ await this.sink.emit({ type: "item/delta", payload: { itemId: item.id, kind: "output", text: str(value.output) ?? "" } }, { turnId: active.id });
620
+ } else
621
+ await this.sink.emit({ type: previous || subtype === "hook_response" ? "item/completed" : "item/started", payload: { item } }, { turnId: active.id });
622
+ return;
623
+ }
624
+ if (subtype === "compact_boundary") {
625
+ await this.sink.emit({ type: "item/completed", payload: { item: { id: str(value.uuid) ?? randomUUID2(), type: "compaction", status: "completed", summary: str(value.summary), trigger: str(record2(value.compact_metadata).trigger) } } }, { turnId: active.id });
626
+ return;
627
+ }
628
+ }
629
+ await this.notice(value);
630
+ }
631
+ async streamEvent(event) {
632
+ const active = this.active;
633
+ const type = event.type;
634
+ if (type === "message_start") {
635
+ active.streamMessageId = str(record2(event.message).id);
636
+ return;
637
+ }
638
+ const index = typeof event.index === "number" ? event.index : 0;
639
+ const id = `${active.streamMessageId ?? active.userUuid}:${index}`;
640
+ if (type === "content_block_start") {
641
+ const block = record2(event.content_block);
642
+ const item = this.blockItem(block, str(block.id) ?? id, true);
643
+ active.output.set(item.id, item);
644
+ await this.sink.emit({ type: "item/started", payload: { item } }, { turnId: active.id });
645
+ } else if (type === "content_block_delta") {
646
+ const delta = record2(event.delta);
647
+ const previous = active.output.get(id);
648
+ if (!previous || previous.type !== "agentMessage" && previous.type !== "reasoning")
649
+ return;
650
+ const text = str(delta.text) ?? str(delta.thinking) ?? "";
651
+ if (previous.type === "agentMessage")
652
+ previous.text += text;
653
+ else
654
+ previous.text = (previous.text ?? "") + text;
655
+ await this.sink.emit({ type: "item/delta", payload: { itemId: id, kind: previous.type === "reasoning" ? "reasoningSummary" : "text", text } }, { turnId: active.id });
656
+ }
657
+ }
658
+ blockItem(block, id, running) {
659
+ const status = this.active?.stoppedTools.get(id) ?? (running ? "running" : "completed");
660
+ if (block.type === "text")
661
+ return { id, type: "agentMessage", text: str(block.text) ?? "", phase: "commentary", status };
662
+ if (block.type === "thinking")
663
+ return { id, type: "reasoning", text: str(block.thinking) ?? "", status };
664
+ if (block.type === "tool_use") {
665
+ const name = str(block.name) ?? "unknown";
666
+ return { id, type: "toolCall", kind: name === "Bash" ? "command" : ["Write", "Edit", "MultiEdit"].includes(name) ? "fileChange" : name.startsWith("mcp__") ? "mcp" : "other", title: name, input: safe(block.input), status, nativeType: name, ...claudeFileChanges(name, block.input) !== undefined ? { fileChanges: claudeFileChanges(name, block.input) } : {} };
667
+ }
668
+ return { id, type: "toolCall", kind: "other", title: "Unknown Claude content", nativeType: str(block.type) ?? "unknown", raw: safe(block), status };
669
+ }
670
+ async completeBlock(block, messageId, subagent) {
671
+ const active = this.active;
672
+ let id = str(block.id);
673
+ if (!id)
674
+ id = [...active.output.values()].find((item) => item.id.startsWith(`${messageId}:`) && item.status !== "completed" && (block.type === "text" ? item.type === "agentMessage" : block.type === "thinking" ? item.type === "reasoning" : false))?.id ?? `${messageId}:${active.blockIndex++}`;
675
+ const previous = active.output.get(id);
676
+ const item = this.blockItem(block, id, block.type === "text" || block.type === "tool_use");
677
+ if (subagent)
678
+ item.extensions = { ...item.extensions ?? {}, nativeParentTool: "subagent" };
679
+ active.output.set(id, item);
680
+ if (!previous)
681
+ await this.sink.emit({ type: "item/started", payload: { item: { ...item, status: "running" } } }, { turnId: active.id });
682
+ else if (block.type === "tool_use")
683
+ await this.sink.emit({ type: "item/delta", payload: { itemId: item.id, kind: "patch", value: safe(item) } }, { turnId: active.id });
684
+ if (item.type !== "agentMessage" && block.type !== "tool_use")
685
+ await this.sink.emit({ type: "item/completed", payload: { item } }, { turnId: active.id });
686
+ }
687
+ async notice(value) {
688
+ const subtype = str(value.subtype) ?? str(value.type) ?? "unknown";
689
+ await this.sink.emit({ type: "notice", payload: { severity: /error|denied|retry|rate_limit/.test(subtype) ? "warning" : "info", code: `CLAUDE_${subtype.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`, message: str(value.content) ?? str(value.message) ?? subtype, raw: safe(value) } }, this.active ? { turnId: this.active.id } : {});
690
+ }
691
+ async permissionRequest(toolName, input, details) {
692
+ const turnId = this.active?.id;
693
+ if (!turnId)
694
+ return { behavior: "deny", message: "No active AHP turn" };
695
+ const interactionId = details.toolUseID;
696
+ const rawQuestions = Array.isArray(input.questions) ? input.questions.map(record2) : [];
697
+ const questionMode = toolName === "AskUserQuestion" && rawQuestions.length > 0 && rawQuestions.every((question) => typeof question.question === "string");
698
+ const request = questionMode ? { interactionId, kind: "userInput", turnId, itemId: details.toolUseID, questions: rawQuestions.map((question, index) => ({ id: `q${index}`, prompt: str(question.question), ...Array.isArray(question.options) ? { options: question.options.map((option) => ({ id: str(record2(option).label) ?? String(option), label: str(record2(option).label) ?? String(option) })) } : {}, ...question.secret === true ? { secret: true } : {} })) } : { interactionId, kind: "approval", turnId, itemId: details.toolUseID, title: details.title ?? toolName, input: safe(input), options: [{ id: "allow-once", label: "Allow once", semantic: "allowOnce" }, ...details.suggestions?.some((entry) => entry.destination === "session") ? [{ id: "allow-session", label: "Allow for this session", semantic: "allowSession" }] : [], { id: "deny", label: "Deny", semantic: "deny" }] };
699
+ return new Promise((resolve) => {
700
+ let answered = false;
701
+ const finish = (result) => {
702
+ if (!answered) {
703
+ answered = true;
704
+ details.signal.removeEventListener("abort", abort);
705
+ resolve(result);
706
+ }
707
+ };
708
+ const abort = () => {
709
+ this.sink.resolveInteraction({ interactionId, outcome: "expired", reason: "native_cancelled" });
710
+ finish({ behavior: "deny", message: "Request cancelled" });
711
+ };
712
+ details.signal.addEventListener("abort", abort, { once: true });
713
+ this.sink.requestInteraction(request, async (answer) => {
714
+ const stopped = !answer ? "interrupted" : answer.kind === "approval" && answer.optionId === "deny" ? "declined" : undefined;
715
+ if (stopped && this.active?.id === turnId) {
716
+ this.active.stoppedTools.set(details.toolUseID, stopped);
717
+ const previous = this.active.output.get(details.toolUseID);
718
+ if (previous?.type === "toolCall") {
719
+ const item = { ...previous, status: stopped };
720
+ this.active.output.set(item.id, item);
721
+ await this.sink.emit({ type: "item/delta", payload: { itemId: item.id, kind: "patch", value: safe(item) } }, { turnId });
722
+ }
723
+ }
724
+ if (!answer)
725
+ finish({ behavior: "deny", message: "Request cancelled" });
726
+ else if (answer.kind === "approval")
727
+ finish(answer.optionId === "deny" ? { behavior: "deny", message: "User denied tool execution" } : { behavior: "allow", updatedInput: input, ...answer.optionId === "allow-session" ? { updatedPermissions: details.suggestions?.filter((entry) => entry.destination === "session") } : {} });
728
+ else if (answer.kind === "userInput")
729
+ finish({ behavior: "allow", updatedInput: { ...input, answers: Object.fromEntries(rawQuestions.map((question, index) => [str(question.question), answer.answers[`q${index}`]])) } });
730
+ else
731
+ finish({ behavior: "deny", message: "Invalid response" });
732
+ }).catch(() => finish({ behavior: "deny", message: "Interaction unavailable" }));
733
+ });
734
+ }
735
+ async elicitationRequest(request, details) {
736
+ const interactionId = details.requestId;
737
+ const ahp = { interactionId, kind: "elicitation", mode: request.mode ?? "form", ...this.active ? { turnId: this.active.id } : {}, message: request.message, ...request.requestedSchema ? { schema: json(request.requestedSchema) } : {}, ...request.url ? { privatePayload: { url: request.url }, privatePayloadAvailable: true } : {} };
738
+ return new Promise((resolve) => {
739
+ let answered = false;
740
+ const finish = (value) => {
741
+ if (!answered) {
742
+ answered = true;
743
+ details.signal.removeEventListener("abort", abort);
744
+ resolve(value);
745
+ }
746
+ };
747
+ const abort = () => {
748
+ this.sink.resolveInteraction({ interactionId, outcome: "expired", reason: "native_cancelled" });
749
+ finish({ action: "cancel" });
750
+ };
751
+ details.signal.addEventListener("abort", abort, { once: true });
752
+ this.sink.requestInteraction(ahp, async (answer) => finish(answer?.kind === "elicitation" ? { action: answer.action, ...answer.content ? { content: elicitationContent(answer.content) } : {} } : { action: "cancel" })).catch(() => finish({ action: "cancel" }));
753
+ });
754
+ }
755
+ async messages(nativeId = this.nativeId) {
756
+ const store = this.adapter.sessionStore;
757
+ if (!store)
758
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED");
759
+ const scoped = {
760
+ append: store.append.bind(store),
761
+ load: async (key) => {
762
+ if (key.sessionId === nativeId && !key.subpath)
763
+ this.storeKeys.set(nativeId, key);
764
+ return store.load(key);
765
+ },
766
+ ...store.listSessions ? { listSessions: store.listSessions.bind(store) } : {},
767
+ ...store.listSubkeys ? { listSubkeys: store.listSubkeys.bind(store) } : {}
768
+ };
769
+ return (this.adapter.options.getSessionMessages ?? getSessionMessages)(nativeId, { dir: this.params.cwd, sessionStore: scoped, includeSystemMessages: true });
770
+ }
771
+ async boundaryPoint(nativeId, boundary) {
772
+ if (!this.adapter.sessionStore) {
773
+ const promptUuid = boundary.nativeTurnId ?? Object.entries(this.turns).find(([, turn]) => turn === boundary.turnId)?.[0];
774
+ if (!promptUuid)
775
+ throw new HarnessSdkError2("TURN_NOT_FOUND");
776
+ const prompts = Object.keys(this.turns);
777
+ const index = prompts.indexOf(promptUuid);
778
+ return { promptUuid, ...index >= 0 && prompts[index + 1] ? { nextPromptUuid: prompts[index + 1] } : {} };
779
+ }
780
+ const messages = await this.messages(nativeId);
781
+ const turns = this.fileStore ? await this.fileStore.loadTurnMap(nativeId) : this.adapter.turnMaps.get(nativeId) ?? this.turns;
782
+ const promptUuid = boundary.nativeTurnId ?? Object.entries(turns).find(([, id]) => id === boundary.turnId)?.[0];
783
+ if (!promptUuid)
784
+ throw new HarnessSdkError2("TURN_NOT_FOUND", "No verified native turn boundary");
785
+ const index = messages.findIndex((message) => message.uuid === promptUuid && isPrompt(message));
786
+ if (index < 0)
787
+ throw new HarnessSdkError2("TURN_NOT_FOUND");
788
+ const next = messages.findIndex((message, position) => position > index && isPrompt(message));
789
+ let uuid = boundary.relation === "before" ? index > 0 ? messages[index - 1].uuid : undefined : messages[(next >= 0 ? next : messages.length) - 1].uuid;
790
+ const key = this.storeKeys.get(nativeId);
791
+ if (boundary.relation === "through" && key) {
792
+ const entries = await this.adapter.sessionStore.load(key);
793
+ if (entries) {
794
+ const start = entries.findIndex((entry) => entry.uuid === promptUuid);
795
+ const end = next >= 0 ? entries.findIndex((entry) => entry.uuid === messages[next].uuid) : entries.length;
796
+ if (start < 0 || end < start)
797
+ throw new HarnessSdkError2("TURN_NOT_FOUND");
798
+ const last = entries.slice(start, end).reverse().find((entry) => typeof entry.uuid === "string");
799
+ if (last?.uuid)
800
+ uuid = last.uuid;
801
+ }
802
+ }
803
+ return { promptUuid, ...next >= 0 ? { nextPromptUuid: messages[next].uuid } : {}, ...uuid ? { uuid } : {} };
804
+ }
805
+ async execute(method, params) {
806
+ if (this.closed || !this.query)
807
+ throw new HarnessSdkError2("SESSION_CLOSED");
808
+ let result;
809
+ switch (method) {
810
+ case "turn/start": {
811
+ const start = params;
812
+ if (this.active)
813
+ throw new HarnessSdkError2("SESSION_BUSY");
814
+ const userUuid = randomUUID2();
815
+ const message = { type: "user", uuid: userUuid, session_id: this.nativeId, message: { role: "user", content: content(start.input) }, parent_tool_use_id: null };
816
+ this.active = { id: start.turnId, userUuid, interrupted: false, output: new Map, stoppedTools: new Map, blockIndex: 0 };
817
+ this.turns[userUuid] = start.turnId;
818
+ this.adapter.turnMaps.set(this.nativeId, this.turns);
819
+ await this.fileStore?.saveTurnMap(this.nativeId, this.turns);
820
+ await this.sink.emit({ type: "turn/started", payload: { turnId: start.turnId, nativeTurnId: userUuid } });
821
+ this.input.push(message);
822
+ result = { accepted: true, turnId: start.turnId };
823
+ break;
824
+ }
825
+ case "turn/interrupt": {
826
+ const request = params;
827
+ if (this.active?.id !== request.turnId)
828
+ throw new HarnessSdkError2("TURN_MISMATCH");
829
+ this.active.interrupted = true;
830
+ await this.query.interrupt();
831
+ result = { accepted: true, turnId: request.turnId };
832
+ break;
833
+ }
834
+ case "turn/steer":
835
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED", "Claude queued input is not current-turn steering");
836
+ case "session/config/set": {
837
+ const patch = params.patch;
838
+ try {
839
+ if (patch.model !== undefined) {
840
+ if (typeof patch.model !== "string")
841
+ throw new HarnessSdkError2("INVALID_PARAMS");
842
+ await this.query.setModel(patch.model);
843
+ this.model = patch.model;
844
+ }
845
+ if (patch.permissionMode !== undefined)
846
+ await this.query.setPermissionMode(permission(patch.permissionMode));
847
+ if (patch.fastMode !== undefined) {
848
+ if (typeof patch.fastMode !== "boolean")
849
+ throw new HarnessSdkError2("INVALID_PARAMS");
850
+ await this.query.applyFlagSettings({ fastMode: patch.fastMode });
851
+ }
852
+ if (patch.effort !== undefined) {
853
+ if (patch.effort !== null && !isEffort(patch.effort))
854
+ throw new HarnessSdkError2("INVALID_PARAMS");
855
+ await this.query.applyFlagSettings({ effortLevel: patch.effort });
856
+ }
857
+ if (patch.maxThinkingTokens !== undefined) {
858
+ if (patch.maxThinkingTokens !== null && typeof patch.maxThinkingTokens !== "number")
859
+ throw new HarnessSdkError2("INVALID_PARAMS");
860
+ await this.query.setMaxThinkingTokens(patch.maxThinkingTokens);
861
+ }
862
+ } catch (cause) {
863
+ throw sdkError(cause, "CLAUDE_CONFIG_FAILED", true);
864
+ }
865
+ this.config = { ...this.config, ...patch };
866
+ this.capabilityRevision++;
867
+ result = this.snapshot();
868
+ await this.sink.emit({ type: "session/state", payload: result });
869
+ break;
870
+ }
871
+ case "history/read": {
872
+ const request = params;
873
+ result = await this.readHistory(request);
874
+ break;
875
+ }
876
+ case "session/revert": {
877
+ const request = params;
878
+ const point = await this.boundaryPoint(this.nativeId, request.boundary);
879
+ let appliedFiles = false;
880
+ if (request.scope === "files" || request.scope === "both") {
881
+ if (!this.adapter.options.enableFileCheckpointing)
882
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED");
883
+ const checkpoint = request.boundary.relation === "before" ? point.promptUuid : point.nextPromptUuid;
884
+ if (!checkpoint)
885
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED", "No verified post-turn checkpoint exists");
886
+ const preview = await this.query.rewindFiles(checkpoint, { dryRun: true });
887
+ if (!preview.canRewind)
888
+ throw new HarnessSdkError2("CHECKPOINT_UNAVAILABLE");
889
+ const rewound = await this.query.rewindFiles(checkpoint);
890
+ if (!rewound.canRewind || rewound.skippedLinks)
891
+ throw new HarnessSdkError2("REVERT_PARTIAL", "Some files could not be restored", false, true, { appliedScope: "files" });
892
+ appliedFiles = true;
893
+ }
894
+ if (request.scope !== "files") {
895
+ try {
896
+ const native = point.uuid ? await (this.adapter.options.forkSession ?? forkSession)(this.nativeId, { dir: this.params.cwd, sessionStore: this.adapter.sessionStore, upToMessageId: point.uuid }) : { sessionId: randomUUID2() };
897
+ this.generation++;
898
+ this.input?.end();
899
+ this.query.close();
900
+ this.binding = { ...this.binding, nativeRef: { provider: "claude", value: native.sessionId } };
901
+ this.adapter.turnMaps.set(this.nativeId, this.turns);
902
+ await this.fileStore?.saveTurnMap(this.nativeId, this.turns);
903
+ await this.startQuery(!point.uuid);
904
+ await this.sink.emit({ type: "session/state", payload: this.snapshot() });
905
+ } catch (cause) {
906
+ if (appliedFiles)
907
+ throw new HarnessSdkError2("REVERT_PARTIAL", "Files were restored but conversation restore could not be confirmed", false, true, { appliedScope: "files" });
908
+ throw cause;
909
+ }
910
+ }
911
+ result = { appliedScope: request.scope, boundary: request.boundary, nativeRef: this.binding.nativeRef };
912
+ break;
913
+ }
914
+ case "session/close":
915
+ await this.dispose();
916
+ result = { closed: true };
917
+ break;
918
+ default:
919
+ throw new HarnessSdkError2("CAPABILITY_UNSUPPORTED");
920
+ }
921
+ return result;
922
+ }
923
+ async readHistory(request) {
924
+ let snapshotToken = request.snapshotToken;
925
+ if (!snapshotToken) {
926
+ const messages = await this.messages();
927
+ const items = [];
928
+ let currentTurn = "native:session";
929
+ let currentNativeTurn;
930
+ const blockCounts = new Map;
931
+ for (const message of messages) {
932
+ if (isPrompt(message)) {
933
+ currentTurn = this.turns[message.uuid] ?? `native:${message.uuid}`;
934
+ currentNativeTurn = message.uuid;
935
+ }
936
+ const body = record2(message.message).content;
937
+ const blocks = typeof body === "string" ? [{ type: "text", text: body }] : Array.isArray(body) ? body.map(record2) : [];
938
+ if (message.type === "user" && isPrompt(message))
939
+ items.push({ turnId: currentTurn, nativeTurnId: message.uuid, nativeItemId: message.uuid, item: { id: message.uuid, type: "userMessage", content: blocks.filter((block) => block.type === "text").map((block) => ({ type: "text", text: str(block.text) ?? "" })), status: "completed" } });
940
+ else
941
+ for (let index = 0;index < blocks.length; index++) {
942
+ const block = blocks[index];
943
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
944
+ let previous;
945
+ for (let position = items.length - 1;position >= 0; position--) {
946
+ const candidate = items[position];
947
+ if (candidate.turnId === currentTurn && candidate.item.id === block.tool_use_id && candidate.item.type === "toolCall") {
948
+ previous = candidate;
949
+ break;
950
+ }
951
+ }
952
+ if (previous?.item.type === "toolCall") {
953
+ previous.item = completeClaudeTool(previous.item, { ...block, content: safe(block.content), ...record2(message).tool_use_result !== undefined ? { structuredContent: safe(record2(message).tool_use_result) } : {} });
954
+ continue;
955
+ }
956
+ }
957
+ const apiMessageId = str(record2(message.message).id);
958
+ const blockIndex = apiMessageId ? blockCounts.get(apiMessageId) ?? 0 : index;
959
+ if (apiMessageId)
960
+ blockCounts.set(apiMessageId, blockIndex + 1);
961
+ const item = this.blockItem(block, str(block.id) ?? `${apiMessageId ?? message.uuid}:${blockIndex}`, block.type === "tool_use");
962
+ if (block.type === "tool_use")
963
+ delete item.status;
964
+ items.push({ turnId: currentTurn, ...currentNativeTurn ? { nativeTurnId: currentNativeTurn } : {}, nativeItemId: message.uuid, item });
965
+ }
966
+ }
967
+ snapshotToken = randomUUID2();
968
+ this.historySnapshots.set(snapshotToken, items);
969
+ if (this.historySnapshots.size > 8)
970
+ this.historySnapshots.delete(this.historySnapshots.keys().next().value);
971
+ }
972
+ const snapshot = this.historySnapshots.get(snapshotToken);
973
+ if (!snapshot)
974
+ throw new HarnessSdkError2("CURSOR_EXPIRED");
975
+ const offset = request.cursor === undefined ? 0 : Number(request.cursor);
976
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > snapshot.length)
977
+ throw new HarnessSdkError2("CURSOR_INVALID");
978
+ const limit = Math.min(request.limit ?? 200, 1000);
979
+ const page = snapshot.slice(offset, offset + limit);
980
+ return { items: structuredClone(page), ...offset + page.length < snapshot.length ? { nextCursor: String(offset + page.length) } : {}, snapshotToken, source: this.binding.nativeRef };
981
+ }
982
+ async dispose() {
983
+ if (this.closed)
984
+ return;
985
+ this.closed = true;
986
+ this.generation++;
987
+ this.input?.end();
988
+ this.query?.close();
989
+ await this.query?.return?.();
990
+ }
991
+ }
992
+ function createClaudeAdapter(options) {
993
+ return new ClaudeNativeAdapter(options);
994
+ }
995
+ export {
996
+ ClaudeBackendSession,
997
+ ClaudeNativeAdapter,
998
+ FileSessionStore,
999
+ createClaudeAdapter
1000
+ };