clearclaw 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -7
- package/dist/channel/slack.d.ts +18 -1
- package/dist/channel/slack.js +227 -27
- package/dist/channel/telegram.d.ts +20 -1
- package/dist/channel/telegram.js +146 -33
- package/dist/config.d.ts +37 -1
- package/dist/config.js +118 -0
- package/dist/engine/acp.d.ts +9 -4
- package/dist/engine/acp.js +301 -29
- package/dist/engine/claude-code.d.ts +19 -2
- package/dist/engine/claude-code.js +121 -33
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +10 -7
- package/dist/engine/registry.js +99 -16
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +1 -1
- package/dist/format.js +42 -7
- package/dist/index.js +18 -23
- package/dist/orchestrator.d.ts +38 -4
- package/dist/orchestrator.js +854 -167
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +6 -2
- package/dist/prompt.js +75 -21
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/transcribe.d.ts +13 -0
- package/dist/transcribe.js +157 -0
- package/dist/types.d.ts +77 -5
- package/package.json +11 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +66 -16
- package/scripts/patch-codex-acp.mjs +32 -0
- package/prompts/ONBOARDING.md +0 -56
package/dist/engine/acp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Readable, Writable } from "node:stream";
|
|
|
3
3
|
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
4
4
|
import log from "../logger.js";
|
|
5
5
|
import { AsyncQueue } from "./async-queue.js";
|
|
6
|
+
import { serveMcpOverHttp } from "./mcp-http.js";
|
|
6
7
|
export class AcpEngine {
|
|
7
8
|
name;
|
|
8
9
|
spawnConfig;
|
|
@@ -13,12 +14,29 @@ export class AcpEngine {
|
|
|
13
14
|
async *runTurn(opts) {
|
|
14
15
|
const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, signal, } = opts;
|
|
15
16
|
let proc;
|
|
17
|
+
const bridges = [];
|
|
16
18
|
const queue = new AsyncQueue();
|
|
17
19
|
const toolCalls = {};
|
|
20
|
+
let contextUsed = 0;
|
|
21
|
+
let contextWindow = 0;
|
|
22
|
+
let model = null;
|
|
18
23
|
// tool_call events carry content that requestPermission lacks — cache by ID
|
|
19
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
|
+
};
|
|
20
35
|
try {
|
|
21
|
-
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
38
|
+
proc = spawnAgent(this.spawnConfig, opts);
|
|
39
|
+
proc.once("error", stop);
|
|
22
40
|
// Log stderr for debugging
|
|
23
41
|
proc.stderr?.on("data", (chunk) => {
|
|
24
42
|
log.debug("[acp:%s] %s", this.name, chunk.toString().trim());
|
|
@@ -26,15 +44,35 @@ export class AcpEngine {
|
|
|
26
44
|
// If the process dies unexpectedly, close the queue
|
|
27
45
|
proc.on("exit", (code) => {
|
|
28
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"})`));
|
|
29
48
|
queue.close();
|
|
30
49
|
});
|
|
31
50
|
// Build Client — sessionUpdate pushes to queue after session setup
|
|
32
51
|
let live = false;
|
|
33
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 () => { },
|
|
34
56
|
requestPermission: async (params) => {
|
|
35
57
|
return handlePermission(params, permissionMode, onPermissionRequest, pendingTools);
|
|
36
58
|
},
|
|
37
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
|
+
}
|
|
38
76
|
if (!live)
|
|
39
77
|
return; // Suppress replay events from loadSession
|
|
40
78
|
const event = mapSessionUpdate(notification, toolCalls, pendingTools);
|
|
@@ -46,42 +84,98 @@ export class AcpEngine {
|
|
|
46
84
|
const stream = ndJsonStream(Writable.toWeb(proc.stdin), Readable.toWeb(proc.stdout));
|
|
47
85
|
const conn = new ClientSideConnection((_agent) => client, stream);
|
|
48
86
|
// Initialize and read agent capabilities
|
|
49
|
-
const initResponse = await conn.initialize({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
const initResponse = await Promise.race([conn.initialize({
|
|
88
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
89
|
+
clientInfo: { name: "clearclaw", version: "0.4.0" },
|
|
90
|
+
clientCapabilities: {},
|
|
91
|
+
}), stopped]);
|
|
54
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();
|
|
55
124
|
// Create or resume session
|
|
56
125
|
let acpSessionId;
|
|
126
|
+
let configOptions;
|
|
57
127
|
if (sessionId) {
|
|
58
|
-
await conn.loadSession({ sessionId, cwd, mcpServers
|
|
128
|
+
const loadedSession = await Promise.race([conn.loadSession({ sessionId, cwd, mcpServers }), stopped]);
|
|
129
|
+
configOptions = loadedSession.configOptions;
|
|
59
130
|
acpSessionId = sessionId;
|
|
60
131
|
}
|
|
61
132
|
else {
|
|
62
|
-
const newSession = await conn.newSession({ cwd, mcpServers
|
|
133
|
+
const newSession = await Promise.race([conn.newSession({ cwd, mcpServers }), stopped]);
|
|
63
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;
|
|
64
150
|
}
|
|
65
151
|
// Now start accepting live events
|
|
66
152
|
live = true;
|
|
67
153
|
// Wire cancellation
|
|
68
|
-
|
|
154
|
+
cancelSession = () => {
|
|
69
155
|
conn.cancel({ sessionId: acpSessionId }).catch(() => { });
|
|
70
156
|
};
|
|
71
|
-
signal?.
|
|
157
|
+
signal?.throwIfAborted();
|
|
72
158
|
// Build prompt content blocks: text + any image attachments
|
|
73
159
|
const promptBlocks = [
|
|
74
160
|
{ type: "text", text: textPrompt },
|
|
75
161
|
];
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 });
|
|
82
171
|
}
|
|
83
172
|
else {
|
|
84
|
-
|
|
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 });
|
|
85
179
|
}
|
|
86
180
|
}
|
|
87
181
|
// Send prompt — resolves when turn completes
|
|
@@ -95,7 +189,7 @@ export class AcpEngine {
|
|
|
95
189
|
queue.push({
|
|
96
190
|
type: "done",
|
|
97
191
|
sessionId: acpSessionId,
|
|
98
|
-
stats: { model
|
|
192
|
+
stats: { model, contextUsed, contextWindow, toolCalls },
|
|
99
193
|
});
|
|
100
194
|
queue.close();
|
|
101
195
|
})
|
|
@@ -108,39 +202,179 @@ export class AcpEngine {
|
|
|
108
202
|
});
|
|
109
203
|
// Yield events as they arrive
|
|
110
204
|
yield* queue;
|
|
111
|
-
// Cleanup
|
|
112
|
-
signal?.removeEventListener("abort", onAbort);
|
|
113
205
|
}
|
|
114
206
|
catch (err) {
|
|
115
|
-
if (!(err instanceof Error && err.name === "AbortError")) {
|
|
207
|
+
if (!signal?.aborted && !(err instanceof Error && err.name === "AbortError")) {
|
|
116
208
|
yield {
|
|
117
209
|
type: "error",
|
|
118
|
-
message:
|
|
210
|
+
message: errorMessage(err),
|
|
119
211
|
};
|
|
120
212
|
}
|
|
121
213
|
queue.close();
|
|
122
214
|
}
|
|
123
215
|
finally {
|
|
216
|
+
signal?.removeEventListener("abort", onAbort);
|
|
124
217
|
if (proc && !proc.killed) {
|
|
125
218
|
proc.kill();
|
|
126
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
|
+
}
|
|
127
225
|
}
|
|
128
226
|
}
|
|
129
|
-
async
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
}
|
|
132
352
|
}
|
|
133
353
|
}
|
|
134
354
|
// --- Helpers ---
|
|
135
|
-
function
|
|
136
|
-
|
|
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, {
|
|
137
370
|
stdio: ["pipe", "pipe", "pipe"],
|
|
371
|
+
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
138
372
|
});
|
|
139
373
|
proc.on("error", (err) => {
|
|
140
|
-
log.error({ err }, "[acp] failed to spawn %s",
|
|
374
|
+
log.error({ err }, "[acp] failed to spawn %s", command);
|
|
141
375
|
});
|
|
142
376
|
if (!proc.stdin || !proc.stdout) {
|
|
143
|
-
throw new Error(`Failed to spawn ${
|
|
377
|
+
throw new Error(`Failed to spawn ${command} — stdin/stdout not available`);
|
|
144
378
|
}
|
|
145
379
|
return proc;
|
|
146
380
|
}
|
|
@@ -244,4 +478,42 @@ async function handlePermission(params, permissionMode, onPermissionRequest, pen
|
|
|
244
478
|
}
|
|
245
479
|
return { outcome: { outcome: "cancelled" } };
|
|
246
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
|
+
}
|
|
247
519
|
//# sourceMappingURL=acp.js.map
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
3
4
|
private readonly executablePath?;
|
|
5
|
+
private readonly configPath?;
|
|
4
6
|
name: string;
|
|
5
|
-
|
|
7
|
+
readonly planUsageWindows: readonly PlanUsageWindow[];
|
|
8
|
+
constructor(executablePath?: string | undefined, configPath?: string | undefined);
|
|
9
|
+
getSessionMessages({ sessionId, cwd, signal }: SessionHistoryOpts): Promise<SessionMessage[]>;
|
|
6
10
|
listSessions(cwd: string): Promise<SessionInfo[]>;
|
|
7
11
|
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
8
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 {};
|