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.
- package/README.md +53 -7
- package/dist/channel/slack.d.ts +21 -4
- package/dist/channel/slack.js +270 -44
- package/dist/channel/telegram.d.ts +22 -3
- package/dist/channel/telegram.js +159 -35
- package/dist/config.d.ts +49 -5
- package/dist/config.js +159 -33
- package/dist/engine/acp.d.ts +14 -0
- package/dist/engine/acp.js +519 -0
- package/dist/engine/async-queue.d.ts +14 -0
- package/dist/engine/async-queue.js +45 -0
- package/dist/engine/claude-code.d.ts +20 -1
- package/dist/engine/claude-code.js +174 -44
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +19 -0
- package/dist/engine/registry.js +115 -0
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +8 -10
- package/dist/format.js +106 -82
- package/dist/index.js +33 -9
- package/dist/orchestrator.d.ts +34 -8
- package/dist/orchestrator.js +866 -153
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +10 -0
- package/dist/prompt.js +78 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/tool-handlers.d.ts +1 -1
- package/dist/tool-handlers.js +2 -2
- package/dist/types.d.ts +128 -17
- package/dist/types.js +12 -1
- package/package.json +13 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +214 -0
- package/scripts/patch-codex-acp.mjs +32 -0
|
@@ -1,10 +1,84 @@
|
|
|
1
|
-
import { query, listSessions, } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import { query, listSessions, getSessionMessages, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import log from "../logger.js";
|
|
3
|
-
|
|
3
|
+
/** Map Claude Code SDK tool call to ToolCall. */
|
|
4
|
+
function toToolCall(toolName, input, toolUseId) {
|
|
5
|
+
switch (toolName) {
|
|
6
|
+
case "Edit":
|
|
7
|
+
return {
|
|
8
|
+
action: "edit", toolName, toolUseId,
|
|
9
|
+
path: String(input.file_path ?? ""),
|
|
10
|
+
before: String(input.old_string ?? ""),
|
|
11
|
+
after: String(input.new_string ?? ""),
|
|
12
|
+
};
|
|
13
|
+
case "Write":
|
|
14
|
+
return {
|
|
15
|
+
action: "write", toolName, toolUseId,
|
|
16
|
+
path: String(input.file_path ?? ""),
|
|
17
|
+
content: String(input.content ?? ""),
|
|
18
|
+
};
|
|
19
|
+
case "Bash":
|
|
20
|
+
return {
|
|
21
|
+
action: "execute", toolName, toolUseId,
|
|
22
|
+
command: String(input.command ?? ""),
|
|
23
|
+
};
|
|
24
|
+
case "Read":
|
|
25
|
+
case "Glob":
|
|
26
|
+
return {
|
|
27
|
+
action: "read", toolName, toolUseId,
|
|
28
|
+
paths: [String(input.file_path ?? input.pattern ?? "")],
|
|
29
|
+
};
|
|
30
|
+
case "Grep":
|
|
31
|
+
return {
|
|
32
|
+
action: "search", toolName, toolUseId,
|
|
33
|
+
pattern: String(input.pattern ?? ""),
|
|
34
|
+
paths: input.path ? [String(input.path)] : undefined,
|
|
35
|
+
};
|
|
36
|
+
case "WebSearch":
|
|
37
|
+
return {
|
|
38
|
+
action: "search", toolName, toolUseId,
|
|
39
|
+
pattern: String(input.query ?? ""),
|
|
40
|
+
};
|
|
41
|
+
case "WebFetch":
|
|
42
|
+
return {
|
|
43
|
+
action: "fetch", toolName, toolUseId,
|
|
44
|
+
url: String(input.url ?? ""),
|
|
45
|
+
};
|
|
46
|
+
default:
|
|
47
|
+
return { action: "other", toolName, toolUseId, ...input };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
4
50
|
export class ClaudeCodeEngine {
|
|
51
|
+
executablePath;
|
|
52
|
+
configPath;
|
|
5
53
|
name = "claude-code";
|
|
54
|
+
planUsageWindows = CLAUDE_PLAN_WINDOWS;
|
|
55
|
+
constructor(executablePath, configPath) {
|
|
56
|
+
this.executablePath = executablePath;
|
|
57
|
+
this.configPath = configPath;
|
|
58
|
+
}
|
|
59
|
+
async getSessionMessages({ sessionId, cwd, signal }) {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const messages = await getSessionMessages(sessionId, { dir: cwd });
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
// The SDK returns [] for missing sessions as well as sessions with no records.
|
|
64
|
+
if (!messages.length)
|
|
65
|
+
throw new Error("No conversation history available for this session");
|
|
66
|
+
return messages.flatMap(({ type, message }) => {
|
|
67
|
+
if ((type !== "user" && type !== "assistant") || !isRecord(message))
|
|
68
|
+
return [];
|
|
69
|
+
const content = message.content;
|
|
70
|
+
const text = typeof content === "string" ? content : Array.isArray(content)
|
|
71
|
+
? content.filter((block) => isRecord(block) && block.type === "text" && typeof block.text === "string")
|
|
72
|
+
.map((block) => block.text).join("\n")
|
|
73
|
+
: "";
|
|
74
|
+
return text.trim() ? [{ role: type, text }] : [];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
6
77
|
async listSessions(cwd) {
|
|
7
|
-
|
|
78
|
+
// includeWorktrees:false — SDK defaults to true, which merges sessions
|
|
79
|
+
// from all git worktrees of the same repo into one pool. We want only
|
|
80
|
+
// this cwd's own sessions.
|
|
81
|
+
const sessions = await listSessions({ dir: cwd, limit: 10, includeWorktrees: false });
|
|
8
82
|
return sessions
|
|
9
83
|
.sort((a, b) => b.lastModified - a.lastModified)
|
|
10
84
|
.map((s) => ({
|
|
@@ -15,7 +89,7 @@ export class ClaudeCodeEngine {
|
|
|
15
89
|
}));
|
|
16
90
|
}
|
|
17
91
|
async *runTurn(opts) {
|
|
18
|
-
const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, appendSystemPrompt, mcpServers, signal, } = opts;
|
|
92
|
+
const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, appendSystemPrompt, mcpServers, signal, model, } = opts;
|
|
19
93
|
const abortController = new AbortController();
|
|
20
94
|
// Wire external signal to our abort controller
|
|
21
95
|
if (signal) {
|
|
@@ -27,14 +101,7 @@ export class ClaudeCodeEngine {
|
|
|
27
101
|
? { resume: sessionId }
|
|
28
102
|
: {};
|
|
29
103
|
const canUseTool = async (toolName, input, options) => {
|
|
30
|
-
const
|
|
31
|
-
const resp = await onPermissionRequest({
|
|
32
|
-
toolName,
|
|
33
|
-
input,
|
|
34
|
-
description,
|
|
35
|
-
reason: options.decisionReason,
|
|
36
|
-
toolUseId: options.toolUseID,
|
|
37
|
-
});
|
|
104
|
+
const resp = await onPermissionRequest(toToolCall(toolName, input, options.toolUseID));
|
|
38
105
|
if (resp.decision === "allow") {
|
|
39
106
|
return { behavior: "allow", updatedInput: resp.updatedInput ?? input };
|
|
40
107
|
}
|
|
@@ -63,10 +130,13 @@ export class ClaudeCodeEngine {
|
|
|
63
130
|
...sessionOpts,
|
|
64
131
|
cwd,
|
|
65
132
|
permissionMode,
|
|
133
|
+
...(model ? { model } : {}),
|
|
134
|
+
...(this.executablePath ? { pathToClaudeCodeExecutable: this.executablePath } : {}),
|
|
66
135
|
allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : undefined,
|
|
67
136
|
canUseTool: permissionMode === "bypassPermissions" ? undefined : canUseTool,
|
|
68
137
|
abortController,
|
|
69
138
|
settingSources: ["user", "project", "local"],
|
|
139
|
+
...(this.configPath ? { settings: this.configPath } : {}),
|
|
70
140
|
...(mcpServers ? { mcpServers } : {}),
|
|
71
141
|
...(appendSystemPrompt
|
|
72
142
|
? {
|
|
@@ -82,6 +152,7 @@ export class ClaudeCodeEngine {
|
|
|
82
152
|
let resultSessionId;
|
|
83
153
|
let turnStats;
|
|
84
154
|
let lastInputTokens = 0;
|
|
155
|
+
let lastModel;
|
|
85
156
|
const toolUseIdToName = new Map();
|
|
86
157
|
try {
|
|
87
158
|
for await (const msg of q) {
|
|
@@ -89,12 +160,22 @@ export class ClaudeCodeEngine {
|
|
|
89
160
|
const sub = msg.type === "result" ? ` (${msg.subtype})` : "";
|
|
90
161
|
log.info(`[sdk] ${msg.type}${sub}`);
|
|
91
162
|
}
|
|
163
|
+
// Report session metadata immediately so a cancelled turn keeps its ID.
|
|
164
|
+
// The observed model is metadata, not a user-selected override.
|
|
165
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
166
|
+
const init = msg;
|
|
167
|
+
yield { type: "session", sessionId: init.session_id, model: init.model };
|
|
168
|
+
}
|
|
92
169
|
// Extract text and tool_use from assistant messages
|
|
93
170
|
if (msg.type === "assistant") {
|
|
94
171
|
// BetaMessage type isn't directly importable (@anthropic-ai/sdk not installed);
|
|
95
172
|
// .message is typed as `any` so property access works without casting.
|
|
96
173
|
const betaMsg = msg.message;
|
|
97
|
-
|
|
174
|
+
// Only the main thread describes the session; subagent turns carry a parent.
|
|
175
|
+
const mainThread = msg.parent_tool_use_id === null;
|
|
176
|
+
if (mainThread && betaMsg.model)
|
|
177
|
+
lastModel = betaMsg.model;
|
|
178
|
+
if (mainThread && betaMsg.usage) {
|
|
98
179
|
const u = betaMsg.usage;
|
|
99
180
|
lastInputTokens = (u.input_tokens ?? 0)
|
|
100
181
|
+ (u.cache_read_input_tokens ?? 0)
|
|
@@ -110,9 +191,7 @@ export class ClaudeCodeEngine {
|
|
|
110
191
|
toolUseIdToName.set(toolUseId, block.name);
|
|
111
192
|
yield {
|
|
112
193
|
type: "tool_use",
|
|
113
|
-
|
|
114
|
-
input: block.input,
|
|
115
|
-
toolUseId,
|
|
194
|
+
tool: toToolCall(block.name, block.input, toolUseId),
|
|
116
195
|
};
|
|
117
196
|
}
|
|
118
197
|
}
|
|
@@ -135,13 +214,7 @@ export class ClaudeCodeEngine {
|
|
|
135
214
|
// Relay rate limit events
|
|
136
215
|
if (msg.type === "rate_limit_event") {
|
|
137
216
|
const { rate_limit_info } = msg;
|
|
138
|
-
|
|
139
|
-
yield {
|
|
140
|
-
type: "rate_limit",
|
|
141
|
-
status: rate_limit_info.status,
|
|
142
|
-
resetsAt: rate_limit_info.resetsAt,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
217
|
+
yield claudePlanUsage(rate_limit_info);
|
|
145
218
|
}
|
|
146
219
|
// Relay local command output (e.g. /compact, /cost, /model)
|
|
147
220
|
if (msg.type === "system" && msg.subtype === "local_command_output") {
|
|
@@ -154,9 +227,10 @@ export class ClaudeCodeEngine {
|
|
|
154
227
|
if (msg.type === "result") {
|
|
155
228
|
const result = msg;
|
|
156
229
|
resultSessionId = result.session_id;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
230
|
+
// modelUsage also counts background calls (e.g. Haiku), so its first key
|
|
231
|
+
// is not necessarily the model that answered.
|
|
232
|
+
const model = lastModel ?? Object.keys(result.modelUsage)[0];
|
|
233
|
+
if (model) {
|
|
160
234
|
const mu = result.modelUsage[model];
|
|
161
235
|
// Build per-tool call counts from accumulated map
|
|
162
236
|
const toolCalls = {};
|
|
@@ -165,8 +239,9 @@ export class ClaudeCodeEngine {
|
|
|
165
239
|
}
|
|
166
240
|
turnStats = {
|
|
167
241
|
model,
|
|
242
|
+
modelLabel: model.replace(/^claude-/, "").replace(/-\d{8}$/, ""),
|
|
168
243
|
contextUsed: lastInputTokens,
|
|
169
|
-
contextWindow: mu
|
|
244
|
+
contextWindow: mu?.contextWindow ?? 0,
|
|
170
245
|
toolCalls,
|
|
171
246
|
};
|
|
172
247
|
}
|
|
@@ -192,12 +267,15 @@ export class ClaudeCodeEngine {
|
|
|
192
267
|
}
|
|
193
268
|
}
|
|
194
269
|
}
|
|
270
|
+
function isRecord(value) {
|
|
271
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
272
|
+
}
|
|
195
273
|
/**
|
|
196
274
|
* Build an AsyncIterable<SDKUserMessage> that yields a single user message
|
|
197
275
|
* with content blocks for attachments + text.
|
|
198
276
|
*/
|
|
199
277
|
function buildAttachmentPrompt(text, attachments) {
|
|
200
|
-
const contentBlocks = attachments.
|
|
278
|
+
const contentBlocks = attachments.flatMap(encodeAttachment);
|
|
201
279
|
// Text prompt always comes last
|
|
202
280
|
if (text) {
|
|
203
281
|
contentBlocks.push({ type: "text", text });
|
|
@@ -220,34 +298,42 @@ function buildAttachmentPrompt(text, attachments) {
|
|
|
220
298
|
const SUPPORTED_IMAGE_TYPES = new Set([
|
|
221
299
|
"image/jpeg", "image/png", "image/gif", "image/webp",
|
|
222
300
|
]);
|
|
223
|
-
/**
|
|
301
|
+
/** Build a metadata label for an attachment. */
|
|
302
|
+
function attachmentLabel(att) {
|
|
303
|
+
const name = att.filename ?? att.mimeType;
|
|
304
|
+
if (att.savedAs) {
|
|
305
|
+
return { type: "text", text: `[Attachment: ${name} — saved to ${att.savedAs}. Content already included; do not re-read from disk.]` };
|
|
306
|
+
}
|
|
307
|
+
return { type: "text", text: `[Attachment: ${name}]` };
|
|
308
|
+
}
|
|
309
|
+
/** Encode an attachment as SDK content blocks using its in-memory buffer. */
|
|
224
310
|
function encodeAttachment(att) {
|
|
225
311
|
const data = att.buffer.toString("base64");
|
|
312
|
+
const label = attachmentLabel(att);
|
|
226
313
|
if (SUPPORTED_IMAGE_TYPES.has(att.mimeType)) {
|
|
227
|
-
return {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
314
|
+
return [{
|
|
315
|
+
type: "image",
|
|
316
|
+
source: {
|
|
317
|
+
type: "base64",
|
|
318
|
+
media_type: att.mimeType,
|
|
319
|
+
data,
|
|
320
|
+
},
|
|
321
|
+
}, label];
|
|
235
322
|
}
|
|
236
323
|
if (att.mimeType === "application/pdf") {
|
|
237
|
-
return {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
324
|
+
return [{
|
|
325
|
+
type: "document",
|
|
326
|
+
source: { type: "base64", media_type: "application/pdf", data },
|
|
327
|
+
}, label];
|
|
241
328
|
}
|
|
242
329
|
if (att.mimeType.startsWith("text/")) {
|
|
243
330
|
const content = att.buffer.toString("utf-8");
|
|
244
|
-
|
|
245
|
-
return { type: "text", text: `${header}${content}` };
|
|
331
|
+
return [{ type: "text", text: content }, label];
|
|
246
332
|
}
|
|
247
333
|
// Unsupported (includes non-standard image types like svg, bmp, tiff)
|
|
248
334
|
const name = att.filename ?? att.mimeType;
|
|
249
335
|
log.warn("[engine] unsupported attachment type: %s", att.mimeType);
|
|
250
|
-
return { type: "text", text: `[Unsupported file: ${name}]` };
|
|
336
|
+
return [{ type: "text", text: `[Unsupported file type: ${name}]` }, label];
|
|
251
337
|
}
|
|
252
338
|
/** Pull plain text out of a tool_result content block. */
|
|
253
339
|
function extractToolResultText(content) {
|
|
@@ -261,4 +347,48 @@ function extractToolResultText(content) {
|
|
|
261
347
|
}
|
|
262
348
|
return "";
|
|
263
349
|
}
|
|
350
|
+
const CLAUDE_LABELS = {
|
|
351
|
+
five_hour: "5h", seven_day: "7d", seven_day_opus: "Opus 7d",
|
|
352
|
+
seven_day_sonnet: "Sonnet 7d", overage: "extra",
|
|
353
|
+
seven_day_overage_included: "Fable 7d",
|
|
354
|
+
};
|
|
355
|
+
function claudeWindowLabel(id) {
|
|
356
|
+
return CLAUDE_LABELS[id] ?? id.replace(/[_\r\n|]/g, " ").slice(0, 32);
|
|
357
|
+
}
|
|
358
|
+
function object(value) {
|
|
359
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
360
|
+
? value : undefined;
|
|
361
|
+
}
|
|
362
|
+
function finite(value) {
|
|
363
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
364
|
+
}
|
|
365
|
+
export const CLAUDE_PLAN_WINDOWS = [
|
|
366
|
+
{ id: "five_hour", label: "5h" },
|
|
367
|
+
{ id: "seven_day", label: "7d" },
|
|
368
|
+
{ id: "seven_day_overage_included", label: "Fable 7d" },
|
|
369
|
+
];
|
|
370
|
+
export function claudePlanUsage(info) {
|
|
371
|
+
const windows = [];
|
|
372
|
+
const utilization = finite(info.utilization);
|
|
373
|
+
if (info.rateLimitType)
|
|
374
|
+
windows.push({
|
|
375
|
+
id: info.rateLimitType, label: claudeWindowLabel(info.rateLimitType),
|
|
376
|
+
limited: info.status === "rejected",
|
|
377
|
+
usedPercent: utilization !== undefined && utilization >= 0
|
|
378
|
+
? utilization * 100 : undefined,
|
|
379
|
+
resetsAt: info.resetsAt,
|
|
380
|
+
});
|
|
381
|
+
// Claude Code 2.1.251 supplies all observed windows, even while allowed.
|
|
382
|
+
// Prefer these percentages over the selected top-level warning window.
|
|
383
|
+
for (const [id, raw] of Object.entries(object(info.unifiedWindows) ?? {})) {
|
|
384
|
+
const window = object(raw);
|
|
385
|
+
const utilization = finite(window?.utilization);
|
|
386
|
+
const resetsAt = finite(window?.resetsAt);
|
|
387
|
+
if (utilization === undefined || utilization < 0 || resetsAt === undefined || resetsAt <= 0)
|
|
388
|
+
continue;
|
|
389
|
+
windows.push({ id, label: claudeWindowLabel(id), usedPercent: utilization * 100,
|
|
390
|
+
resetsAt, limited: id === info.rateLimitType && info.status === "rejected" });
|
|
391
|
+
}
|
|
392
|
+
return { type: "plan_usage", windows, isUsingOverage: info.isUsingOverage };
|
|
393
|
+
}
|
|
264
394
|
//# sourceMappingURL=claude-code.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
/** A turn-scoped HTTP endpoint in front of one in-process MCP server. */
|
|
4
|
+
export interface McpBridge {
|
|
5
|
+
config: McpServer;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Expose the turn's existing handlers to a subprocess, without another tool process. */
|
|
9
|
+
export declare function serveMcpOverHttp(name: string, server: McpSdkServerConfigWithInstance): Promise<McpBridge>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import log from "../logger.js";
|
|
5
|
+
/** Expose the turn's existing handlers to a subprocess, without another tool process. */
|
|
6
|
+
export async function serveMcpOverHttp(name, server) {
|
|
7
|
+
const authorization = `Bearer ${randomBytes(32).toString("hex")}`;
|
|
8
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID });
|
|
9
|
+
const http = createServer((req, res) => {
|
|
10
|
+
if (req.headers.authorization !== authorization) {
|
|
11
|
+
res.writeHead(401).end();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (req.url !== "/mcp") {
|
|
15
|
+
res.writeHead(404).end();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
transport.handleRequest(req, res).catch((err) => {
|
|
19
|
+
log.error({ err }, "[mcp] HTTP transport failed");
|
|
20
|
+
if (!res.headersSent)
|
|
21
|
+
res.writeHead(500);
|
|
22
|
+
res.end();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
const close = async () => {
|
|
26
|
+
// Stop accepting requests before closing MCP streams and keep-alive sockets.
|
|
27
|
+
const closed = new Promise((resolve) => http.close(() => resolve()));
|
|
28
|
+
http.closeAllConnections();
|
|
29
|
+
await server.instance.close();
|
|
30
|
+
await closed;
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
await server.instance.connect(transport);
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
http.once("error", reject);
|
|
36
|
+
http.listen(0, "127.0.0.1", () => {
|
|
37
|
+
http.off("error", reject);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
const address = http.address();
|
|
42
|
+
if (!address || typeof address === "string")
|
|
43
|
+
throw new Error("MCP HTTP listener has no address");
|
|
44
|
+
return {
|
|
45
|
+
config: {
|
|
46
|
+
type: "http",
|
|
47
|
+
name,
|
|
48
|
+
url: `http://127.0.0.1:${address.port}/mcp`,
|
|
49
|
+
headers: [{ name: "Authorization", value: authorization }],
|
|
50
|
+
},
|
|
51
|
+
close,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
await close();
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=mcp-http.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EngineEntry } from "../config.js";
|
|
2
|
+
import type { Engine, RunTurnOpts } from "../types.js";
|
|
3
|
+
export interface AcpEngineDefinition {
|
|
4
|
+
command: [string, ...string[]];
|
|
5
|
+
env?: Record<string, string> | ((opts: RunTurnOpts) => Record<string, string> | undefined);
|
|
6
|
+
}
|
|
7
|
+
/** All known engine names (for validation / setup prompts). */
|
|
8
|
+
export declare const ENGINE_NAMES: readonly ["claude-code", ...string[]];
|
|
9
|
+
/** Return the CLI command an engine needs on PATH. */
|
|
10
|
+
export declare function engineCommand(name: string): string | undefined;
|
|
11
|
+
/** Prefer an installed CLI; bundled engines can omit the override. */
|
|
12
|
+
export declare function resolveEnginePath(name: string): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Build the engine map: claude-code (Agent SDK) + known ACP engines.
|
|
15
|
+
* Engines are lightweight — they store config, not running processes.
|
|
16
|
+
*
|
|
17
|
+
* @param engineConfigs - executable and settings-file overrides from config
|
|
18
|
+
*/
|
|
19
|
+
export declare function createEngineMap(engineConfigs?: Record<string, Pick<EngineEntry, "path" | "configPath">>): Map<string, Engine>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { ClaudeCodeEngine } from "./claude-code.js";
|
|
6
|
+
import { AcpEngine } from "./acp.js";
|
|
7
|
+
/** Known ACP engines: CLI for setup, optional adapter for launch. */
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const KNOWN_ACP_ENGINES = {
|
|
10
|
+
kiro: { cliCommand: "kiro-cli", command: (cliPath) => [cliPath, "acp"] },
|
|
11
|
+
codex: {
|
|
12
|
+
cliCommand: "codex",
|
|
13
|
+
bundled: true,
|
|
14
|
+
supportsConfigPath: true,
|
|
15
|
+
// Resolve only when starting Codex, and always use our pinned, patched dependency.
|
|
16
|
+
get command() { return [process.execPath, require.resolve("@agentclientprotocol/codex-acp")]; },
|
|
17
|
+
env: codexEnv,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* ACP has no system-prompt channel, so ClearClaw's assembled framework prompt
|
|
22
|
+
* reaches Codex through its config: codex-acp parses CODEX_CONFIG as a JSON
|
|
23
|
+
* config object and forwards it to codex core, which honors
|
|
24
|
+
* `developer_instructions`. An explicit JSON file supplies personal settings;
|
|
25
|
+
* without one, inherited CODEX_CONFIG remains the source.
|
|
26
|
+
* CODEX_PATH selects the underlying CLI while the pinned adapter stays fixed.
|
|
27
|
+
*/
|
|
28
|
+
function codexEnv(opts, executablePath, configPath) {
|
|
29
|
+
const developerInstructions = opts.appendSystemPrompt?.trim();
|
|
30
|
+
if (!developerInstructions && !executablePath && !configPath)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
...(executablePath ? { CODEX_PATH: executablePath } : {}),
|
|
34
|
+
...(developerInstructions || configPath ? {
|
|
35
|
+
CODEX_CONFIG: JSON.stringify({
|
|
36
|
+
...readCodexConfig(configPath),
|
|
37
|
+
...(developerInstructions ? { developer_instructions: developerInstructions } : {}),
|
|
38
|
+
}),
|
|
39
|
+
} : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Installation state the Codex CLI owns. codex-acp applies our config when it
|
|
44
|
+
* starts a thread, long after the CLI would have fetched a marketplace and
|
|
45
|
+
* installed a plugin, so declaring these here installs nothing: an enabled
|
|
46
|
+
* plugin stays missing, and a marketplace with no local snapshot fails every
|
|
47
|
+
* thread with an opaque config-load error. Reject them at load with the fix.
|
|
48
|
+
*/
|
|
49
|
+
const CLI_OWNED_CONFIG_KEYS = ["marketplaces", "plugins"];
|
|
50
|
+
function readCodexConfig(configPath) {
|
|
51
|
+
const raw = configPath ? readFileSync(configPath, "utf8") : process.env.CODEX_CONFIG?.trim() || "{}";
|
|
52
|
+
const source = configPath ?? "CODEX_CONFIG";
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
55
|
+
throw new Error(`${source} must be a JSON object`);
|
|
56
|
+
}
|
|
57
|
+
const config = parsed;
|
|
58
|
+
const cliOwned = CLI_OWNED_CONFIG_KEYS.filter((key) => key in config);
|
|
59
|
+
if (cliOwned.length > 0) {
|
|
60
|
+
throw new Error(`${source} sets ${cliOwned.join(" and ")}, which ClearClaw cannot apply: plugin installation is Codex CLI state, ` +
|
|
61
|
+
`not thread settings. Install with "codex plugin marketplace add <source>" and "codex plugin add <plugin>@<marketplace>", ` +
|
|
62
|
+
`then remove ${cliOwned.length > 1 ? "these keys" : "the key"}.`);
|
|
63
|
+
}
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
/** All known engine names (for validation / setup prompts). */
|
|
67
|
+
export const ENGINE_NAMES = ["claude-code", ...Object.keys(KNOWN_ACP_ENGINES)];
|
|
68
|
+
/** Return the CLI command an engine needs on PATH. */
|
|
69
|
+
export function engineCommand(name) {
|
|
70
|
+
return name === "claude-code" ? "claude" : KNOWN_ACP_ENGINES[name]?.cliCommand;
|
|
71
|
+
}
|
|
72
|
+
/** Prefer an installed CLI; bundled engines can omit the override. */
|
|
73
|
+
export function resolveEnginePath(name) {
|
|
74
|
+
const command = engineCommand(name);
|
|
75
|
+
if (!command)
|
|
76
|
+
throw new Error(`Unknown engine: ${name}`);
|
|
77
|
+
try {
|
|
78
|
+
return execFileSync("which", [command], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
if (KNOWN_ACP_ENGINES[name]?.bundled)
|
|
82
|
+
return undefined;
|
|
83
|
+
throw new Error(`"${command}" not found on PATH. Install it first or choose another engine.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the engine map: claude-code (Agent SDK) + known ACP engines.
|
|
88
|
+
* Engines are lightweight — they store config, not running processes.
|
|
89
|
+
*
|
|
90
|
+
* @param engineConfigs - executable and settings-file overrides from config
|
|
91
|
+
*/
|
|
92
|
+
export function createEngineMap(engineConfigs = {}) {
|
|
93
|
+
for (const [name, { configPath }] of Object.entries(engineConfigs)) {
|
|
94
|
+
if (configPath && !isAbsolute(configPath))
|
|
95
|
+
throw new Error(`${name} configPath must be absolute`);
|
|
96
|
+
if (configPath && name !== "claude-code" && !KNOWN_ACP_ENGINES[name]?.supportsConfigPath) {
|
|
97
|
+
throw new Error(`${name} does not support configPath`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const engines = new Map();
|
|
101
|
+
const claude = engineConfigs["claude-code"];
|
|
102
|
+
engines.set("claude-code", new ClaudeCodeEngine(claude?.path, claude?.configPath));
|
|
103
|
+
for (const [name, config] of Object.entries(KNOWN_ACP_ENGINES)) {
|
|
104
|
+
const { path, configPath } = engineConfigs[name] ?? {};
|
|
105
|
+
engines.set(name, new AcpEngine(name, {
|
|
106
|
+
get command() {
|
|
107
|
+
const command = config.command;
|
|
108
|
+
return typeof command === "function" ? command(path ?? config.cliCommand) : command;
|
|
109
|
+
},
|
|
110
|
+
env: (opts) => config.env?.(opts, path, configPath),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
return engines;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionMessage } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Inverse of the orchestrator's prompt framing, for showing a stored prompt back
|
|
4
|
+
* to a human (session summaries, catch-up blocks): drop the leading bracketed
|
|
5
|
+
* tags and the sender label.
|
|
6
|
+
* "[2026-09-10 …] [msg:10107] [user] Paddy (@handle): hi" → "hi".
|
|
7
|
+
* Text with no bracketed tag is left alone, so a summary that merely contains a
|
|
8
|
+
* colon keeps its first clause.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripPromptPrefix(text: string): string;
|
|
11
|
+
/** Format all engine-normalized conversation text for a handoff prompt. */
|
|
12
|
+
export declare function formatSessionTranscript(messages: SessionMessage[]): string;
|
|
13
|
+
/** Replay the last three user prompts and their replies, including a pending reply. */
|
|
14
|
+
export declare function formatSessionRecap(messages: SessionMessage[]): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inverse of the orchestrator's prompt framing, for showing a stored prompt back
|
|
3
|
+
* to a human (session summaries, catch-up blocks): drop the leading bracketed
|
|
4
|
+
* tags and the sender label.
|
|
5
|
+
* "[2026-09-10 …] [msg:10107] [user] Paddy (@handle): hi" → "hi".
|
|
6
|
+
* Text with no bracketed tag is left alone, so a summary that merely contains a
|
|
7
|
+
* colon keeps its first clause.
|
|
8
|
+
*/
|
|
9
|
+
export function stripPromptPrefix(text) {
|
|
10
|
+
return text.replace(/^(?:\[[^\]]*\]\s*)+(?:[^:\n]{0,60}:\s*)?/, "");
|
|
11
|
+
}
|
|
12
|
+
/** Format all engine-normalized conversation text for a handoff prompt. */
|
|
13
|
+
export function formatSessionTranscript(messages) {
|
|
14
|
+
let text = "";
|
|
15
|
+
for (const message of messages) {
|
|
16
|
+
if (!message.text.trim())
|
|
17
|
+
continue;
|
|
18
|
+
text += `${text ? "\n\n" : ""}${message.role === "user" ? "User" : "Assistant"}:\n${message.text}`;
|
|
19
|
+
}
|
|
20
|
+
return text || "[No conversation text was returned for the previous session.]";
|
|
21
|
+
}
|
|
22
|
+
/** Replay the last three user prompts and their replies, including a pending reply. */
|
|
23
|
+
export function formatSessionRecap(messages) {
|
|
24
|
+
const visible = messages.flatMap((message) => {
|
|
25
|
+
const text = (message.role === "user" ? stripPromptPrefix(message.text) : message.text).trim();
|
|
26
|
+
return text ? [{ ...message, text }] : [];
|
|
27
|
+
});
|
|
28
|
+
let start = 0;
|
|
29
|
+
let prompts = 0;
|
|
30
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
31
|
+
if (visible[i].role === "user" && ++prompts === 3) {
|
|
32
|
+
start = i;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return visible.slice(start).map((message) => `**${message.role === "user" ? "Prompt" : "Assistant"}**\n${message.text}`).join("\n\n") || "No conversation text to recap yet.";
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=session-transcript.js.map
|
package/dist/format.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Formatting utilities for Telegram display.
|
|
3
3
|
*
|
|
4
|
-
* Uses the `diff` library for
|
|
4
|
+
* Uses the `diff` library for interleaved diffs in Edit permission prompts.
|
|
5
5
|
*/
|
|
6
|
+
import { type ToolCall } from "./types.js";
|
|
6
7
|
/**
|
|
7
8
|
* Summary line for the rolling tool message after a turn completes.
|
|
8
9
|
* e.g. "🔧 3× Read, 2× Grep, 1× Bash"
|
|
@@ -12,12 +13,13 @@ export declare function formatToolCallSummary(toolCalls: Record<string, number>)
|
|
|
12
13
|
* Short one-liner for the rolling tool message. Includes key detail
|
|
13
14
|
* but truncated to stay compact.
|
|
14
15
|
*/
|
|
15
|
-
export declare function formatToolStatusLine(
|
|
16
|
+
export declare function formatToolStatusLine(tool: ToolCall): string;
|
|
16
17
|
/**
|
|
17
|
-
* Format a permission prompt for display.
|
|
18
|
-
*
|
|
18
|
+
* Format a permission prompt for display.
|
|
19
|
+
* Switches on action type: edit shows diff, write shows preview,
|
|
20
|
+
* execute shows command, others show available detail.
|
|
19
21
|
*/
|
|
20
|
-
export declare function formatPermissionPrompt(
|
|
22
|
+
export declare function formatPermissionPrompt(tool: ToolCall): string;
|
|
21
23
|
/**
|
|
22
24
|
* Relative time string: "just now", "5m ago", "2h ago", "3d ago".
|
|
23
25
|
*/
|
|
@@ -42,13 +44,9 @@ export declare function truncateButtonLabel(index: number, label: string): strin
|
|
|
42
44
|
* Format an ExitPlanMode permission prompt.
|
|
43
45
|
* Renders plan text from the description or input fields.
|
|
44
46
|
*/
|
|
45
|
-
export declare function formatExitPlanMode(input: Record<string, unknown
|
|
47
|
+
export declare function formatExitPlanMode(input: Record<string, unknown>): string;
|
|
46
48
|
/**
|
|
47
49
|
* Format a TodoWrite task list for display.
|
|
48
50
|
*/
|
|
49
51
|
export declare function formatTodoList(input: Record<string, unknown>): string;
|
|
50
|
-
/**
|
|
51
|
-
* Human-readable one-liner for a tool invocation (used in engine's canUseTool).
|
|
52
|
-
*/
|
|
53
|
-
export declare function formatToolDescription(toolName: string, input: Record<string, unknown>): string;
|
|
54
52
|
export {};
|