clearclaw 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,519 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
4
+ import log from "../logger.js";
5
+ import { AsyncQueue } from "./async-queue.js";
6
+ import { serveMcpOverHttp } from "./mcp-http.js";
7
+ export class AcpEngine {
8
+ name;
9
+ spawnConfig;
10
+ constructor(name, spawnConfig) {
11
+ this.name = name;
12
+ this.spawnConfig = spawnConfig;
13
+ }
14
+ async *runTurn(opts) {
15
+ const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, signal, } = opts;
16
+ let proc;
17
+ const bridges = [];
18
+ const queue = new AsyncQueue();
19
+ const toolCalls = {};
20
+ let contextUsed = 0;
21
+ let contextWindow = 0;
22
+ let model = null;
23
+ // tool_call events carry content that requestPermission lacks — cache by ID
24
+ const pendingTools = new Map();
25
+ // ACP 0.16 does not reject pending setup requests when the process exits.
26
+ let stop;
27
+ const stopped = new Promise((_resolve, reject) => { stop = reject; });
28
+ void stopped.catch(() => { });
29
+ let cancelSession;
30
+ const onAbort = () => {
31
+ cancelSession?.();
32
+ stop(new DOMException("Turn cancelled", "AbortError"));
33
+ queue.close();
34
+ };
35
+ try {
36
+ signal?.throwIfAborted();
37
+ signal?.addEventListener("abort", onAbort, { once: true });
38
+ proc = spawnAgent(this.spawnConfig, opts);
39
+ proc.once("error", stop);
40
+ // Log stderr for debugging
41
+ proc.stderr?.on("data", (chunk) => {
42
+ log.debug("[acp:%s] %s", this.name, chunk.toString().trim());
43
+ });
44
+ // If the process dies unexpectedly, close the queue
45
+ proc.on("exit", (code) => {
46
+ log.info("[acp:%s] process exited with code %d", this.name, code ?? -1);
47
+ stop(new Error(`ACP process exited during session setup (${code ?? "signal"})`));
48
+ queue.close();
49
+ });
50
+ // Build Client — sessionUpdate pushes to queue after session setup
51
+ let live = false;
52
+ const client = {
53
+ // Newer adapters send optional auth/status extensions. Ignore unsupported
54
+ // notifications instead of letting the SDK print account metadata.
55
+ extNotification: async () => { },
56
+ requestPermission: async (params) => {
57
+ return handlePermission(params, permissionMode, onPermissionRequest, pendingTools);
58
+ },
59
+ sessionUpdate: async (notification) => {
60
+ if (notification.update.sessionUpdate === "config_option_update") {
61
+ model = modelConfigOption(notification.update.configOptions)?.currentValue ?? null;
62
+ return;
63
+ }
64
+ // Usage is session state, including updates sent during session loading.
65
+ if (notification.update.sessionUpdate === "usage_update") {
66
+ contextUsed = notification.update.used;
67
+ contextWindow = notification.update.size;
68
+ // Loaded history is not a fresh account observation.
69
+ if (live) {
70
+ const windows = codexPlanUsage(notification.update._meta);
71
+ if (windows.length)
72
+ queue.push({ type: "plan_usage", windows });
73
+ }
74
+ return;
75
+ }
76
+ if (!live)
77
+ return; // Suppress replay events from loadSession
78
+ const event = mapSessionUpdate(notification, toolCalls, pendingTools);
79
+ if (event)
80
+ queue.push(event);
81
+ },
82
+ };
83
+ // Create ACP connection over ndjson stdio
84
+ const stream = ndJsonStream(Writable.toWeb(proc.stdin), Readable.toWeb(proc.stdout));
85
+ const conn = new ClientSideConnection((_agent) => client, stream);
86
+ // Initialize and read agent capabilities
87
+ const initResponse = await Promise.race([conn.initialize({
88
+ protocolVersion: PROTOCOL_VERSION,
89
+ clientInfo: { name: "clearclaw", version: "0.4.0" },
90
+ clientCapabilities: {},
91
+ }), stopped]);
92
+ const supportsImages = initResponse.agentCapabilities?.promptCapabilities?.image ?? false;
93
+ // Hand the agent every server it advertises a transport for; skip the rest.
94
+ const httpCapable = initResponse.agentCapabilities?.mcpCapabilities?.http ?? false;
95
+ const mcpServers = [];
96
+ for (const [name, server] of Object.entries(opts.mcpServers ?? {})) {
97
+ signal?.throwIfAborted();
98
+ if (server.type === "sdk" || server.type === "http") {
99
+ // ponytail: agents without HTTP MCP run toolless, as every ACP engine did before.
100
+ if (!httpCapable) {
101
+ log.warn("[acp:%s] skipping MCP server %s: agent does not support HTTP", this.name, name);
102
+ continue;
103
+ }
104
+ if (server.type === "sdk") {
105
+ const bridge = await serveMcpOverHttp(name, server);
106
+ bridges.push(bridge);
107
+ mcpServers.push(bridge.config);
108
+ }
109
+ else {
110
+ mcpServers.push({ type: "http", name, url: server.url,
111
+ headers: Object.entries(server.headers ?? {}).map(([name, value]) => ({ name, value })) });
112
+ }
113
+ }
114
+ else if (server.type === "sse") {
115
+ // ClearClaw never configures SSE; no reason to build a passthrough for it.
116
+ log.warn("[acp:%s] skipping MCP server %s: SSE is not supported", this.name, name);
117
+ }
118
+ else {
119
+ mcpServers.push({ name, command: server.command, args: server.args ?? [],
120
+ env: Object.entries(server.env ?? {}).map(([name, value]) => ({ name, value })) });
121
+ }
122
+ }
123
+ signal?.throwIfAborted();
124
+ // Create or resume session
125
+ let acpSessionId;
126
+ let configOptions;
127
+ if (sessionId) {
128
+ const loadedSession = await Promise.race([conn.loadSession({ sessionId, cwd, mcpServers }), stopped]);
129
+ configOptions = loadedSession.configOptions;
130
+ acpSessionId = sessionId;
131
+ }
132
+ else {
133
+ const newSession = await Promise.race([conn.newSession({ cwd, mcpServers }), stopped]);
134
+ acpSessionId = newSession.sessionId;
135
+ configOptions = newSession.configOptions;
136
+ }
137
+ if (configOptions)
138
+ model = modelConfigOption(configOptions)?.currentValue ?? null;
139
+ // Persist before model selection so a rejected selection doesn't lose the session.
140
+ yield { type: "session", sessionId: acpSessionId };
141
+ if (opts.model) {
142
+ signal?.throwIfAborted();
143
+ const modelOption = modelConfigOption(configOptions);
144
+ if (!modelOption)
145
+ throw new Error(`${this.name} did not advertise a model selector`);
146
+ const selected = await Promise.race([conn.setSessionConfigOption({
147
+ sessionId: acpSessionId, configId: modelOption.id, value: opts.model,
148
+ }), stopped]);
149
+ model = modelConfigOption(selected.configOptions)?.currentValue ?? null;
150
+ }
151
+ // Now start accepting live events
152
+ live = true;
153
+ // Wire cancellation
154
+ cancelSession = () => {
155
+ conn.cancel({ sessionId: acpSessionId }).catch(() => { });
156
+ };
157
+ signal?.throwIfAborted();
158
+ // Build prompt content blocks: text + any image attachments
159
+ const promptBlocks = [
160
+ { type: "text", text: textPrompt },
161
+ ];
162
+ for (const att of attachments ?? []) {
163
+ const name = att.filename ?? att.mimeType;
164
+ const isImage = att.mimeType.startsWith("image/");
165
+ if (isImage && supportsImages) {
166
+ promptBlocks.push({ type: "image", data: att.buffer.toString("base64"), mimeType: att.mimeType });
167
+ const label = att.savedAs
168
+ ? `[Attachment: ${name} — saved to ${att.savedAs}. Content already included; do not re-read from disk.]`
169
+ : `[Attachment: ${name}]`;
170
+ promptBlocks.push({ type: "text", text: label });
171
+ }
172
+ else {
173
+ // Content not included (unsupported format or agent lacks image support)
174
+ const reason = isImage ? "agent does not support images" : "unsupported format";
175
+ const label = att.savedAs
176
+ ? `[Attachment: ${name} (${reason}) — saved to ${att.savedAs}]`
177
+ : `[Attachment: ${name} (${reason})]`;
178
+ promptBlocks.push({ type: "text", text: label });
179
+ }
180
+ }
181
+ // Send prompt — resolves when turn completes
182
+ const promptResponse = conn.prompt({
183
+ sessionId: acpSessionId,
184
+ prompt: promptBlocks,
185
+ });
186
+ // When prompt completes, push done event and close queue
187
+ promptResponse
188
+ .then(() => {
189
+ queue.push({
190
+ type: "done",
191
+ sessionId: acpSessionId,
192
+ stats: { model, contextUsed, contextWindow, toolCalls },
193
+ });
194
+ queue.close();
195
+ })
196
+ .catch((err) => {
197
+ queue.push({
198
+ type: "error",
199
+ message: err instanceof Error ? err.message : String(err),
200
+ });
201
+ queue.close();
202
+ });
203
+ // Yield events as they arrive
204
+ yield* queue;
205
+ }
206
+ catch (err) {
207
+ if (!signal?.aborted && !(err instanceof Error && err.name === "AbortError")) {
208
+ yield {
209
+ type: "error",
210
+ message: errorMessage(err),
211
+ };
212
+ }
213
+ queue.close();
214
+ }
215
+ finally {
216
+ signal?.removeEventListener("abort", onAbort);
217
+ if (proc && !proc.killed) {
218
+ proc.kill();
219
+ }
220
+ const closed = await Promise.allSettled(bridges.map((bridge) => bridge.close()));
221
+ for (const result of closed) {
222
+ if (result.status === "rejected")
223
+ log.error({ err: result.reason }, "[mcp] failed to close turn transport");
224
+ }
225
+ }
226
+ }
227
+ async getSessionMessages(opts) {
228
+ const messages = [];
229
+ let previousId;
230
+ let previousRole;
231
+ const client = {
232
+ extNotification: async () => { },
233
+ requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
234
+ sessionUpdate: async ({ sessionId, update }) => {
235
+ if (sessionId !== opts.sessionId)
236
+ return;
237
+ if (update.sessionUpdate !== "user_message_chunk" && update.sessionUpdate !== "agent_message_chunk") {
238
+ // A tool/thought between text chunks ends the adjacent text segment.
239
+ previousRole = undefined;
240
+ return;
241
+ }
242
+ const role = update.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
243
+ if (update.content.type !== "text") {
244
+ previousRole = undefined;
245
+ previousId = undefined;
246
+ return;
247
+ }
248
+ const last = messages.at(-1);
249
+ // ACP message IDs preserve separate consecutive messages from the same role.
250
+ // Older agents without IDs can only be normalized into adjacent role segments.
251
+ if (last && previousRole === role && previousId === update.messageId) {
252
+ last.text += update.content.text;
253
+ }
254
+ else {
255
+ messages.push({ role, text: update.content.text });
256
+ }
257
+ previousId = update.messageId;
258
+ previousRole = role;
259
+ },
260
+ };
261
+ await this.readSessionData(opts, client, async (conn, init) => {
262
+ if (!init.agentCapabilities?.loadSession) {
263
+ throw new Error(`${this.name} does not support loading session history`);
264
+ }
265
+ await conn.loadSession({ sessionId: opts.sessionId, cwd: opts.cwd, mcpServers: [] });
266
+ });
267
+ return messages.filter((message) => message.text.length > 0);
268
+ }
269
+ async listSessions(cwd) {
270
+ return this.readSessionData({ cwd }, {
271
+ extNotification: async () => { },
272
+ requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
273
+ sessionUpdate: async () => { },
274
+ }, async (conn, init, signal) => {
275
+ // Listing is optional in ACP. Unsupported agents deliberately have no picker entries.
276
+ if (!init.agentCapabilities?.sessionCapabilities?.list)
277
+ return [];
278
+ // ACP does not promise page ordering; sort only after exhausting the cursor.
279
+ const sessions = new Map();
280
+ const cursors = new Set();
281
+ let cursor;
282
+ do {
283
+ signal.throwIfAborted();
284
+ const page = await conn.listSessions({ cwd, ...(cursor !== undefined ? { cursor } : {}) });
285
+ for (const session of page.sessions) {
286
+ if (session.cwd !== cwd)
287
+ continue; // Never mix sibling worktrees into this picker.
288
+ const timestamp = session.updatedAt ? Date.parse(session.updatedAt) : NaN;
289
+ sessions.set(session.sessionId, {
290
+ sessionId: session.sessionId,
291
+ // ACP offers a title, not a last-prompt summary or a standard git branch.
292
+ summary: session.title?.trim() || session.sessionId,
293
+ lastModified: Number.isFinite(timestamp) ? timestamp : 0,
294
+ });
295
+ }
296
+ cursor = page.nextCursor ?? undefined;
297
+ if (cursor === undefined)
298
+ break;
299
+ if (cursors.has(cursor))
300
+ throw new Error(`${this.name} repeated a session list cursor`);
301
+ cursors.add(cursor);
302
+ } while (cursor !== undefined);
303
+ return [...sessions.values()].sort((a, b) => b.lastModified - a.lastModified).slice(0, 10);
304
+ });
305
+ }
306
+ /** Short-lived, read-only ACP queries share one deadline and always release the child. */
307
+ async readSessionData(opts, client, query) {
308
+ opts.signal?.throwIfAborted();
309
+ const signal = AbortSignal.any([
310
+ ...(opts.signal ? [opts.signal] : []),
311
+ AbortSignal.timeout(30_000),
312
+ ]);
313
+ const proc = spawnAgent(this.spawnConfig, {
314
+ ...opts,
315
+ sessionId: opts.sessionId ?? null,
316
+ prompt: "",
317
+ permissionMode: "default",
318
+ onPermissionRequest: async () => ({ decision: "deny", message: "Session queries cannot run tools" }),
319
+ });
320
+ let rejectStopped;
321
+ const stopped = new Promise((_resolve, reject) => { rejectStopped = reject; });
322
+ const onAbort = () => rejectStopped(signal.reason);
323
+ const onError = (err) => rejectStopped(err);
324
+ const onExit = () => rejectStopped(new Error(`${this.name} exited while querying sessions`));
325
+ signal.addEventListener("abort", onAbort, { once: true });
326
+ proc.once("error", onError);
327
+ proc.once("exit", onExit);
328
+ // Drain stderr so verbose adapters cannot block waiting for a full pipe.
329
+ proc.stderr?.resume();
330
+ try {
331
+ const conn = new ClientSideConnection(() => client, ndJsonStream(Writable.toWeb(proc.stdin), Readable.toWeb(proc.stdout)));
332
+ const init = await Promise.race([stopped, conn.initialize({
333
+ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {},
334
+ })]);
335
+ signal.throwIfAborted();
336
+ const result = await Promise.race([stopped, query(conn, init, signal)]);
337
+ signal.throwIfAborted();
338
+ return result;
339
+ }
340
+ catch (err) {
341
+ if (err instanceof Error)
342
+ throw err;
343
+ throw new Error(errorMessage(err));
344
+ }
345
+ finally {
346
+ signal.removeEventListener("abort", onAbort);
347
+ proc.removeListener("error", onError);
348
+ proc.removeListener("exit", onExit);
349
+ proc.stdin?.end();
350
+ proc.kill();
351
+ }
352
+ }
353
+ }
354
+ // --- Helpers ---
355
+ function modelConfigOption(options) {
356
+ // Categories are optional; accept an advertised "model" ID as a fallback.
357
+ const option = options?.find((option) => option.category === "model")
358
+ ?? options?.find((option) => option.id === "model" && !option.category);
359
+ return option?.type === "select" ? option : undefined;
360
+ }
361
+ function errorMessage(err) {
362
+ if (err && typeof err === "object" && "message" in err && typeof err.message === "string")
363
+ return err.message;
364
+ return String(err);
365
+ }
366
+ function spawnAgent(config, opts) {
367
+ const extraEnv = typeof config.env === "function" ? config.env(opts) : config.env;
368
+ const [command, ...args] = config.command;
369
+ const proc = spawn(command, args, {
370
+ stdio: ["pipe", "pipe", "pipe"],
371
+ env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
372
+ });
373
+ proc.on("error", (err) => {
374
+ log.error({ err }, "[acp] failed to spawn %s", command);
375
+ });
376
+ if (!proc.stdin || !proc.stdout) {
377
+ throw new Error(`Failed to spawn ${command} — stdin/stdout not available`);
378
+ }
379
+ return proc;
380
+ }
381
+ function mapSessionUpdate(notification, toolCalls, pendingTools) {
382
+ const update = notification.update;
383
+ switch (update.sessionUpdate) {
384
+ case "agent_message_chunk": {
385
+ const content = update.content;
386
+ if (content.type === "text") {
387
+ return { type: "text_chunk", text: content.text };
388
+ }
389
+ return null;
390
+ }
391
+ case "tool_call": {
392
+ const kind = update.kind ?? "tool";
393
+ toolCalls[kind] = (toolCalls[kind] ?? 0) + 1;
394
+ const tool = acpToolCall(update.title, update.toolCallId, kind, update.locations, update.content);
395
+ pendingTools.set(update.toolCallId, tool);
396
+ return { type: "tool_use", tool };
397
+ }
398
+ case "tool_call_update":
399
+ return null;
400
+ default:
401
+ return null;
402
+ }
403
+ }
404
+ /** Map ACP tool call fields to ToolCall. */
405
+ function acpToolCall(title, toolCallId, kind, locations, content) {
406
+ const toolName = title;
407
+ const toolUseId = toolCallId;
408
+ const paths = locations?.map((l) => l.line ? `${l.path}:${l.line}` : l.path);
409
+ switch (kind) {
410
+ case "edit": {
411
+ const diff = findDiff(content);
412
+ return {
413
+ action: "edit", toolName, toolUseId,
414
+ path: diff?.path ?? paths?.[0] ?? "",
415
+ before: diff?.oldText ?? "",
416
+ after: diff?.newText ?? "",
417
+ };
418
+ }
419
+ case "write": {
420
+ const diff = findDiff(content);
421
+ return {
422
+ action: "write", toolName, toolUseId,
423
+ path: diff?.path ?? paths?.[0] ?? "",
424
+ content: diff?.newText ?? "",
425
+ };
426
+ }
427
+ case "read":
428
+ return { action: "read", toolName, toolUseId, paths: paths ?? [] };
429
+ case "execute":
430
+ return { action: "execute", toolName, toolUseId, command: title };
431
+ case "search":
432
+ return { action: "search", toolName, toolUseId, pattern: title, paths };
433
+ case "fetch":
434
+ return { action: "fetch", toolName, toolUseId, url: title };
435
+ default: {
436
+ const detail = findTextContent(content);
437
+ return { action: kind, toolName, toolUseId, paths, detail };
438
+ }
439
+ }
440
+ }
441
+ /** Find the first diff content block from ACP tool call content. */
442
+ function findDiff(content) {
443
+ const entry = content?.find((c) => c.type === "diff");
444
+ return entry?.type === "diff" ? entry : undefined;
445
+ }
446
+ /** Extract text from the first content block. */
447
+ function findTextContent(content) {
448
+ const entry = content?.find((c) => c.type === "content");
449
+ if (entry?.type === "content" && entry.content.type === "text")
450
+ return entry.content.text;
451
+ return undefined;
452
+ }
453
+ async function handlePermission(params, permissionMode, onPermissionRequest, pendingTools) {
454
+ const allowOption = params.options.find((o) => o.kind === "allow_once");
455
+ const rejectOption = params.options.find((o) => o.kind === "reject_once");
456
+ // Auto-approve in bypass mode
457
+ if (permissionMode === "bypassPermissions" || permissionMode === "dontAsk") {
458
+ return {
459
+ outcome: allowOption
460
+ ? { outcome: "selected", optionId: allowOption.optionId }
461
+ : { outcome: "cancelled" },
462
+ };
463
+ }
464
+ // Use cached tool_call data (has content/locations) over the sparse permission request
465
+ const toolCall = params.toolCall;
466
+ const tool = pendingTools.get(toolCall.toolCallId)
467
+ ?? acpToolCall(toolCall.title ?? "Unknown tool", toolCall.toolCallId, toolCall.kind ?? "other", toolCall.locations, toolCall.content);
468
+ const resp = await onPermissionRequest(tool);
469
+ if (resp.decision === "allow" && allowOption) {
470
+ return {
471
+ outcome: { outcome: "selected", optionId: allowOption.optionId },
472
+ };
473
+ }
474
+ if (rejectOption) {
475
+ return {
476
+ outcome: { outcome: "selected", optionId: rejectOption.optionId },
477
+ };
478
+ }
479
+ return { outcome: { outcome: "cancelled" } };
480
+ }
481
+ function object(value) {
482
+ return value !== null && typeof value === "object" && !Array.isArray(value)
483
+ ? value : undefined;
484
+ }
485
+ function finite(value) {
486
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
487
+ }
488
+ /** PR #334's provider extension. Never treat standard ACP used/size as plan usage. */
489
+ export function codexPlanUsage(meta) {
490
+ const limits = object(meta)?.["_codex/rateLimits"];
491
+ if (!Array.isArray(limits))
492
+ return [];
493
+ const windows = [];
494
+ for (const value of limits) {
495
+ const limit = object(value);
496
+ if (!limit || typeof limit.limitId !== "string")
497
+ continue;
498
+ const prefix = limit.limitId === "codex" ? "" : `${limit.limitId.replace(/[\r\n|]/g, " ").slice(0, 24)} `;
499
+ for (const key of ["primary", "secondary"]) {
500
+ const window = object(limit[key]);
501
+ if (!window)
502
+ continue;
503
+ const minutes = finite(window.windowDurationMins);
504
+ const percent = finite(window.usedPercent);
505
+ const reset = finite(window.resetsAt);
506
+ if (minutes === undefined || minutes <= 0)
507
+ continue;
508
+ const duration = minutes % 1440 === 0 ? `${minutes / 1440}d`
509
+ : minutes % 60 === 0 ? `${minutes / 60}h` : `${minutes}m`;
510
+ windows.push({
511
+ id: `${limit.limitId}/${key}`, label: `${prefix}${duration}`,
512
+ usedPercent: percent !== undefined && percent >= 0 && percent <= 100 ? percent : undefined,
513
+ resetsAt: reset !== undefined && reset > 0 ? reset : undefined,
514
+ });
515
+ }
516
+ }
517
+ return windows;
518
+ }
519
+ //# sourceMappingURL=acp.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bridges callback-driven event producers to AsyncIterable consumers.
3
+ *
4
+ * Used by AcpEngine to convert ACP's sessionUpdate callbacks into
5
+ * the AsyncIterable<EngineEvent> that ClearClaw's Engine interface requires.
6
+ */
7
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
8
+ private buffer;
9
+ private waiting;
10
+ private closed;
11
+ push(value: T): void;
12
+ close(): void;
13
+ [Symbol.asyncIterator](): AsyncIterator<T>;
14
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bridges callback-driven event producers to AsyncIterable consumers.
3
+ *
4
+ * Used by AcpEngine to convert ACP's sessionUpdate callbacks into
5
+ * the AsyncIterable<EngineEvent> that ClearClaw's Engine interface requires.
6
+ */
7
+ export class AsyncQueue {
8
+ buffer = [];
9
+ waiting = [];
10
+ closed = false;
11
+ push(value) {
12
+ if (this.closed)
13
+ return;
14
+ const waiter = this.waiting.shift();
15
+ if (waiter) {
16
+ waiter({ value, done: false });
17
+ }
18
+ else {
19
+ this.buffer.push(value);
20
+ }
21
+ }
22
+ close() {
23
+ this.closed = true;
24
+ for (const waiter of this.waiting) {
25
+ waiter({ value: undefined, done: true });
26
+ }
27
+ this.waiting.length = 0;
28
+ }
29
+ [Symbol.asyncIterator]() {
30
+ return {
31
+ next: () => {
32
+ if (this.buffer.length > 0) {
33
+ return Promise.resolve({ value: this.buffer.shift(), done: false });
34
+ }
35
+ if (this.closed) {
36
+ return Promise.resolve({ value: undefined, done: true });
37
+ }
38
+ return new Promise((resolve) => {
39
+ this.waiting.push(resolve);
40
+ });
41
+ },
42
+ };
43
+ }
44
+ }
45
+ //# sourceMappingURL=async-queue.js.map
@@ -1,6 +1,25 @@
1
- import type { Engine, EngineEvent, RunTurnOpts, SessionInfo } from "../types.js";
1
+ import { type SDKRateLimitInfo } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { Engine, EngineEvent, PlanUsageWindow, RunTurnOpts, SessionInfo, SessionHistoryOpts, SessionMessage } from "../types.js";
2
3
  export declare class ClaudeCodeEngine implements Engine {
4
+ private readonly executablePath?;
5
+ private readonly configPath?;
3
6
  name: string;
7
+ readonly planUsageWindows: readonly PlanUsageWindow[];
8
+ constructor(executablePath?: string | undefined, configPath?: string | undefined);
9
+ getSessionMessages({ sessionId, cwd, signal }: SessionHistoryOpts): Promise<SessionMessage[]>;
4
10
  listSessions(cwd: string): Promise<SessionInfo[]>;
5
11
  runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
6
12
  }
13
+ /** Claude runtime fields newer than the bundled SDK declarations. */
14
+ type ClaudeRateLimitInfo = Omit<SDKRateLimitInfo, "rateLimitType"> & {
15
+ rateLimitType?: string;
16
+ unifiedWindows?: Record<string, {
17
+ utilization: number;
18
+ resetsAt: number;
19
+ }>;
20
+ };
21
+ export declare const CLAUDE_PLAN_WINDOWS: readonly PlanUsageWindow[];
22
+ export declare function claudePlanUsage(info: ClaudeRateLimitInfo): Extract<EngineEvent, {
23
+ type: "plan_usage";
24
+ }>;
25
+ export {};