clearclaw 0.6.1 → 0.7.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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { Channel, Button, ButtonResponse, SendFileOpts, SendMessageOpts, UserInfo } from "../types.js";
2
+ import type { Channel, Button, ButtonResponse, SendFileOpts, MessageOpts, UserInfo } from "../types.js";
3
3
  export declare class SlackChannel extends EventEmitter implements Channel {
4
4
  name: string;
5
5
  private static readonly EMOJI_TO_SLACK;
@@ -16,9 +16,9 @@ export declare class SlackChannel extends EventEmitter implements Channel {
16
16
  connect(): Promise<void>;
17
17
  disconnect(): Promise<void>;
18
18
  ownsId(chatId: string): boolean;
19
- sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<string[]>;
19
+ sendMessage(chatId: string, text: string, opts?: MessageOpts): Promise<string[]>;
20
20
  sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
21
- editMessage(chatId: string, handle: string, text: string): Promise<void>;
21
+ editMessage(chatId: string, handle: string, text: string, opts?: MessageOpts): Promise<void>;
22
22
  deleteMessage(chatId: string, handle: string): Promise<void>;
23
23
  pinMessage(chatId: string, handle: string): Promise<void>;
24
24
  unpinAllMessages(chatId: string): Promise<void>;
@@ -168,8 +168,13 @@ export class SlackChannel extends EventEmitter {
168
168
  const chunks = splitMessage(text);
169
169
  const handles = [];
170
170
  const threadTs = opts?.replyToMessageId;
171
- /** Post a chunk with mrkdwn blocks, falling back to plain text on failure. */
171
+ const plain = opts?.format === "plain";
172
+ /** Post a chunk, with mrkdwn blocks unless plain format requested. */
172
173
  const postChunk = async (chunk, extra = {}) => {
174
+ if (plain) {
175
+ const r = await this.app.client.chat.postMessage({ channel, text: chunk, ...extra });
176
+ return r.ts;
177
+ }
173
178
  const blocks = mrkdwnBlocks(chunk);
174
179
  try {
175
180
  const r = await this.app.client.chat.postMessage({ channel, text: chunk, blocks, ...extra });
@@ -187,18 +192,30 @@ export class SlackChannel extends EventEmitter {
187
192
  if (typingTs && chunks.length > 0) {
188
193
  this.typingMessageTs.delete(chatId);
189
194
  const firstChunk = chunks.shift();
190
- const blocks = mrkdwnBlocks(firstChunk);
191
- try {
192
- await this.app.client.chat.update({
193
- channel, ts: typingTs, text: firstChunk, blocks,
194
- });
195
- handles.push(typingTs);
195
+ if (plain) {
196
+ try {
197
+ await this.app.client.chat.update({ channel, ts: typingTs, text: firstChunk });
198
+ handles.push(typingTs);
199
+ }
200
+ catch {
201
+ const ts = await postChunk(firstChunk);
202
+ if (ts)
203
+ handles.push(ts);
204
+ }
196
205
  }
197
- catch {
198
- // Placeholder was deleted externally or mrkdwn invalid — post instead
199
- const ts = await postChunk(firstChunk);
200
- if (ts)
201
- handles.push(ts);
206
+ else {
207
+ const blocks = mrkdwnBlocks(firstChunk);
208
+ try {
209
+ await this.app.client.chat.update({
210
+ channel, ts: typingTs, text: firstChunk, blocks,
211
+ });
212
+ handles.push(typingTs);
213
+ }
214
+ catch {
215
+ const ts = await postChunk(firstChunk);
216
+ if (ts)
217
+ handles.push(ts);
218
+ }
202
219
  }
203
220
  }
204
221
  for (const chunk of chunks) {
@@ -311,14 +328,23 @@ export class SlackChannel extends EventEmitter {
311
328
  }
312
329
  return { value: pressed.value };
313
330
  }
314
- async editMessage(chatId, handle, text) {
331
+ async editMessage(chatId, handle, text, opts) {
315
332
  const chunk = splitMessage(text)[0];
316
- await this.app.client.chat.update({
317
- channel: this.slackId(chatId),
318
- ts: handle,
319
- text: chunk,
320
- blocks: mrkdwnBlocks(chunk),
321
- });
333
+ if (opts?.format === "plain") {
334
+ await this.app.client.chat.update({
335
+ channel: this.slackId(chatId),
336
+ ts: handle,
337
+ text: chunk,
338
+ });
339
+ }
340
+ else {
341
+ await this.app.client.chat.update({
342
+ channel: this.slackId(chatId),
343
+ ts: handle,
344
+ text: chunk,
345
+ blocks: mrkdwnBlocks(chunk),
346
+ });
347
+ }
322
348
  }
323
349
  async deleteMessage(chatId, handle) {
324
350
  await this.app.client.chat.delete({
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { Channel, Button, ButtonResponse, SendFileOpts, SendMessageOpts, UserInfo } from "../types.js";
2
+ import type { Channel, Button, ButtonResponse, SendFileOpts, MessageOpts, UserInfo } from "../types.js";
3
3
  export declare class TelegramChannel extends EventEmitter implements Channel {
4
4
  name: string;
5
5
  private bot;
@@ -14,9 +14,9 @@ export declare class TelegramChannel extends EventEmitter implements Channel {
14
14
  connect(): Promise<void>;
15
15
  disconnect(): Promise<void>;
16
16
  ownsId(chatId: string): boolean;
17
- sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<string[]>;
17
+ sendMessage(chatId: string, text: string, opts?: MessageOpts): Promise<string[]>;
18
18
  sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
19
- editMessage(chatId: string, handle: string, text: string): Promise<void>;
19
+ editMessage(chatId: string, handle: string, text: string, opts?: MessageOpts): Promise<void>;
20
20
  deleteMessage(chatId: string, handle: string): Promise<void>;
21
21
  pinMessage(chatId: string, handle: string): Promise<void>;
22
22
  unpinAllMessages(chatId: string): Promise<void>;
@@ -110,24 +110,31 @@ export class TelegramChannel extends EventEmitter {
110
110
  const numId = this.numericId(chatId);
111
111
  const chunks = splitMessage(text);
112
112
  const handles = [];
113
+ const plain = opts?.format === "plain";
113
114
  let firstChunk = true;
114
115
  for (const chunk of chunks) {
115
116
  const replyParams = firstChunk && opts?.replyToMessageId
116
117
  ? { reply_parameters: { message_id: Number(opts.replyToMessageId) } }
117
118
  : {};
118
- const rendered = telegramifyMarkdown(chunk, "escape");
119
- try {
120
- const sent = await this.bot.api.sendMessage(numId, rendered, {
121
- parse_mode: "MarkdownV2",
122
- ...replyParams,
123
- });
124
- handles.push(String(sent.message_id));
125
- }
126
- catch (err) {
127
- log.warn("[channel] sendMessage failed with MarkdownV2, retrying as plain text");
119
+ if (plain) {
128
120
  const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
129
121
  handles.push(String(sent.message_id));
130
122
  }
123
+ else {
124
+ const rendered = telegramifyMarkdown(chunk, "escape");
125
+ try {
126
+ const sent = await this.bot.api.sendMessage(numId, rendered, {
127
+ parse_mode: "MarkdownV2",
128
+ ...replyParams,
129
+ });
130
+ handles.push(String(sent.message_id));
131
+ }
132
+ catch (err) {
133
+ log.warn("[channel] sendMessage failed with MarkdownV2, retrying as plain text");
134
+ const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
135
+ handles.push(String(sent.message_id));
136
+ }
137
+ }
131
138
  firstChunk = false;
132
139
  }
133
140
  return handles;
@@ -212,9 +219,20 @@ export class TelegramChannel extends EventEmitter {
212
219
  }
213
220
  return { value: pressed.value };
214
221
  }
215
- async editMessage(chatId, handle, text) {
222
+ async editMessage(chatId, handle, text, opts) {
216
223
  const numId = this.numericId(chatId);
217
- await this.bot.api.editMessageText(numId, Number(handle), text);
224
+ if (opts?.format === "plain") {
225
+ await this.bot.api.editMessageText(numId, Number(handle), text);
226
+ }
227
+ else {
228
+ const rendered = telegramifyMarkdown(text, "escape");
229
+ try {
230
+ await this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" });
231
+ }
232
+ catch {
233
+ await this.bot.api.editMessageText(numId, Number(handle), text);
234
+ }
235
+ }
218
236
  }
219
237
  async deleteMessage(chatId, handle) {
220
238
  const numId = this.numericId(chatId);
package/dist/config.d.ts CHANGED
@@ -21,6 +21,11 @@ export interface PendingPairing {
21
21
  chatId: string;
22
22
  expiresAt: number;
23
23
  }
24
+ export interface EngineEntry {
25
+ name: string;
26
+ default?: boolean;
27
+ path: string;
28
+ }
24
29
  export declare function resolveDataDir(): string;
25
30
  /**
26
31
  * Unified config: env var resolution + persistent store backed by config.json.
@@ -33,20 +38,26 @@ export declare function resolveDataDir(): string;
33
38
  */
34
39
  export declare class Config {
35
40
  channel?: ChannelConfig;
41
+ engines: Record<string, EngineEntry>;
42
+ defaultEngine: string;
36
43
  permissionMode: PermissionMode;
37
44
  private envUserIds;
38
- readonly defaultPromptPath: string;
45
+ readonly homeWorkspacePath: string;
39
46
  readonly filesPath: string;
40
47
  readonly logPath: string;
48
+ readonly instructionsDir: string;
49
+ readonly frameworkPromptDir: string;
41
50
  private readonly dataDir;
42
51
  private readonly filePath;
43
52
  constructor();
44
53
  /** Resolve channel + env vars. Throws if no channel config found. */
45
54
  resolve(): this;
55
+ private readRaw;
46
56
  private read;
47
57
  private write;
48
58
  getChannel(): ChannelConfig | undefined;
49
59
  setChannel(config: ChannelConfig): void;
60
+ setEngines(engines: EngineEntry[]): void;
50
61
  hasUser(userId: string): boolean;
51
62
  addUser(user: AuthorizedUser): void;
52
63
  listUsers(): AuthorizedUser[];
@@ -61,8 +72,5 @@ export declare class Config {
61
72
  setSession(name: string, sessionId: string): void;
62
73
  clearSession(name: string): void;
63
74
  setBehavior(name: string, behavior: "assistant" | "relay"): void;
64
- ensureDefaultWorkspace(chatId: string): void;
65
- /** Copy bundled skills from the repo's skills/ directory to the home workspace's .claude/skills/. */
66
- syncSkills(): void;
67
- approveUser(userId: string, userName: string, chatId: string): void;
75
+ approveUser(userId: string, userName: string): void;
68
76
  }
package/dist/config.js CHANGED
@@ -28,19 +28,25 @@ export function resolveDataDir() {
28
28
  export class Config {
29
29
  // Resolved settings — populated by resolve()
30
30
  channel;
31
+ engines;
32
+ defaultEngine; // derived: name of the engine with default: true
31
33
  permissionMode = "default";
32
34
  envUserIds = new Set();
33
35
  // Derived paths
34
- defaultPromptPath;
36
+ homeWorkspacePath;
35
37
  filesPath;
36
38
  logPath;
39
+ instructionsDir;
40
+ frameworkPromptDir;
37
41
  dataDir = resolveDataDir();
38
42
  filePath;
39
43
  constructor() {
40
44
  this.filePath = path.join(this.dataDir, "config.json");
41
- this.defaultPromptPath = path.join(this.dataDir, "workspace", "CLAUDE.md");
45
+ this.homeWorkspacePath = path.join(this.dataDir, "workspace");
42
46
  this.filesPath = path.join(this.dataDir, "files");
43
47
  this.logPath = path.join(this.dataDir, "clearclaw.log");
48
+ this.instructionsDir = path.join(this.dataDir, "workspace", "instructions");
49
+ this.frameworkPromptDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompts");
44
50
  }
45
51
  /** Resolve channel + env vars. Throws if no channel config found. */
46
52
  resolve() {
@@ -70,26 +76,40 @@ export class Config {
70
76
  throw new Error(`PERMISSION_MODE must be one of: ${valid.join(", ")}`);
71
77
  }
72
78
  this.permissionMode = pm;
79
+ const data = this.read();
80
+ if (!data.engines?.length) {
81
+ throw new Error("No engines configured. Run `clearclaw setup`.");
82
+ }
83
+ this.engines = Object.fromEntries(data.engines.map((e) => [e.name, e]));
84
+ const defaultEntry = data.engines.find((e) => e.default);
85
+ if (!defaultEntry) {
86
+ throw new Error("No default engine configured. Run `clearclaw setup`.");
87
+ }
88
+ this.defaultEngine = defaultEntry.name;
73
89
  return this;
74
90
  }
75
91
  // --- File I/O (internal) ---
76
- read() {
77
- if (!fs.existsSync(this.filePath)) {
78
- return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
79
- }
92
+ readRaw() {
93
+ if (!fs.existsSync(this.filePath))
94
+ return {};
80
95
  try {
81
- const raw = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
82
- return {
83
- channel: raw.channel,
84
- authorizedUsers: raw.authorizedUsers ?? [],
85
- pendingPairings: raw.pendingPairings ?? [],
86
- workspaces: raw.workspaces ?? [],
87
- };
96
+ return JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
88
97
  }
89
98
  catch {
90
- return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
99
+ return {};
91
100
  }
92
101
  }
102
+ read() {
103
+ const raw = this.readRaw();
104
+ return {
105
+ ...raw,
106
+ channel: raw.channel,
107
+ engines: raw.engines,
108
+ authorizedUsers: (raw.authorizedUsers ?? []),
109
+ pendingPairings: (raw.pendingPairings ?? []),
110
+ workspaces: (raw.workspaces ?? []),
111
+ };
112
+ }
93
113
  write(data) {
94
114
  fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
95
115
  }
@@ -102,6 +122,12 @@ export class Config {
102
122
  data.channel = config;
103
123
  this.write(data);
104
124
  }
125
+ // --- Engines ---
126
+ setEngines(engines) {
127
+ const data = this.read();
128
+ data.engines = engines;
129
+ this.write(data);
130
+ }
105
131
  // --- Authorized users ---
106
132
  hasUser(userId) {
107
133
  return this.read().authorizedUsers.some((u) => u.id === userId);
@@ -199,26 +225,8 @@ export class Config {
199
225
  }
200
226
  }
201
227
  // --- Approval ---
202
- ensureDefaultWorkspace(chatId) {
203
- if (this.workspaceByChat(chatId) || this.workspaceByName("default"))
204
- return;
205
- const defaultCwd = path.join(this.dataDir, "workspace");
206
- fs.mkdirSync(defaultCwd, { recursive: true });
207
- this.upsertWorkspace({ name: "default", cwd: defaultCwd, chat_id: chatId, current_session_id: null });
208
- this.syncSkills();
209
- }
210
- /** Copy bundled skills from the repo's skills/ directory to the home workspace's .claude/skills/. */
211
- syncSkills() {
212
- const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
213
- const srcSkills = path.join(pkgRoot, "skills");
214
- if (!fs.existsSync(srcSkills))
215
- return;
216
- const destSkills = path.join(this.dataDir, "workspace", ".claude", "skills");
217
- fs.cpSync(srcSkills, destSkills, { recursive: true, force: true });
218
- }
219
- approveUser(userId, userName, chatId) {
228
+ approveUser(userId, userName) {
220
229
  this.addUser({ id: userId, name: userName, approvedAt: Date.now() });
221
- this.ensureDefaultWorkspace(chatId);
222
230
  }
223
231
  }
224
232
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,9 @@
1
+ import type { SpawnConfig } from "./registry.js";
2
+ import type { Engine, EngineEvent, RunTurnOpts, SessionInfo } from "../types.js";
3
+ export declare class AcpEngine implements Engine {
4
+ readonly name: string;
5
+ private readonly spawnConfig;
6
+ constructor(name: string, spawnConfig: SpawnConfig);
7
+ runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
8
+ listSessions(_cwd: string): Promise<SessionInfo[]>;
9
+ }
@@ -0,0 +1,247 @@
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
+ export class AcpEngine {
7
+ name;
8
+ spawnConfig;
9
+ constructor(name, spawnConfig) {
10
+ this.name = name;
11
+ this.spawnConfig = spawnConfig;
12
+ }
13
+ async *runTurn(opts) {
14
+ const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, signal, } = opts;
15
+ let proc;
16
+ const queue = new AsyncQueue();
17
+ const toolCalls = {};
18
+ // tool_call events carry content that requestPermission lacks — cache by ID
19
+ const pendingTools = new Map();
20
+ try {
21
+ proc = spawnAgent(this.spawnConfig);
22
+ // Log stderr for debugging
23
+ proc.stderr?.on("data", (chunk) => {
24
+ log.debug("[acp:%s] %s", this.name, chunk.toString().trim());
25
+ });
26
+ // If the process dies unexpectedly, close the queue
27
+ proc.on("exit", (code) => {
28
+ log.info("[acp:%s] process exited with code %d", this.name, code ?? -1);
29
+ queue.close();
30
+ });
31
+ // Build Client — sessionUpdate pushes to queue after session setup
32
+ let live = false;
33
+ const client = {
34
+ requestPermission: async (params) => {
35
+ return handlePermission(params, permissionMode, onPermissionRequest, pendingTools);
36
+ },
37
+ sessionUpdate: async (notification) => {
38
+ if (!live)
39
+ return; // Suppress replay events from loadSession
40
+ const event = mapSessionUpdate(notification, toolCalls, pendingTools);
41
+ if (event)
42
+ queue.push(event);
43
+ },
44
+ };
45
+ // Create ACP connection over ndjson stdio
46
+ const stream = ndJsonStream(Writable.toWeb(proc.stdin), Readable.toWeb(proc.stdout));
47
+ const conn = new ClientSideConnection((_agent) => client, stream);
48
+ // Initialize and read agent capabilities
49
+ const initResponse = await conn.initialize({
50
+ protocolVersion: PROTOCOL_VERSION,
51
+ clientInfo: { name: "clearclaw", version: "0.4.0" },
52
+ clientCapabilities: {},
53
+ });
54
+ const supportsImages = initResponse.agentCapabilities?.promptCapabilities?.image ?? false;
55
+ // Create or resume session
56
+ let acpSessionId;
57
+ if (sessionId) {
58
+ await conn.loadSession({ sessionId, cwd, mcpServers: [] });
59
+ acpSessionId = sessionId;
60
+ }
61
+ else {
62
+ const newSession = await conn.newSession({ cwd, mcpServers: [] });
63
+ acpSessionId = newSession.sessionId;
64
+ }
65
+ // Now start accepting live events
66
+ live = true;
67
+ // Wire cancellation
68
+ const onAbort = () => {
69
+ conn.cancel({ sessionId: acpSessionId }).catch(() => { });
70
+ };
71
+ signal?.addEventListener("abort", onAbort, { once: true });
72
+ // Build prompt content blocks: text + any image attachments
73
+ const promptBlocks = [
74
+ { type: "text", text: textPrompt },
75
+ ];
76
+ const imageAttachments = attachments?.filter((a) => a.mimeType.startsWith("image/")) ?? [];
77
+ if (imageAttachments.length > 0) {
78
+ if (supportsImages) {
79
+ for (const att of imageAttachments) {
80
+ promptBlocks.push({ type: "image", data: att.buffer.toString("base64"), mimeType: att.mimeType });
81
+ }
82
+ }
83
+ else {
84
+ log.warn("[acp:%s] dropping %d image attachment(s) — agent does not support images", this.name, imageAttachments.length);
85
+ }
86
+ }
87
+ // Send prompt — resolves when turn completes
88
+ const promptResponse = conn.prompt({
89
+ sessionId: acpSessionId,
90
+ prompt: promptBlocks,
91
+ });
92
+ // When prompt completes, push done event and close queue
93
+ promptResponse
94
+ .then(() => {
95
+ queue.push({
96
+ type: "done",
97
+ sessionId: acpSessionId,
98
+ stats: { model: null, contextUsed: 0, contextWindow: 0, toolCalls },
99
+ });
100
+ queue.close();
101
+ })
102
+ .catch((err) => {
103
+ queue.push({
104
+ type: "error",
105
+ message: err instanceof Error ? err.message : String(err),
106
+ });
107
+ queue.close();
108
+ });
109
+ // Yield events as they arrive
110
+ yield* queue;
111
+ // Cleanup
112
+ signal?.removeEventListener("abort", onAbort);
113
+ }
114
+ catch (err) {
115
+ if (!(err instanceof Error && err.name === "AbortError")) {
116
+ yield {
117
+ type: "error",
118
+ message: err instanceof Error ? err.message : String(err),
119
+ };
120
+ }
121
+ queue.close();
122
+ }
123
+ finally {
124
+ if (proc && !proc.killed) {
125
+ proc.kill();
126
+ }
127
+ }
128
+ }
129
+ async listSessions(_cwd) {
130
+ // Fast follow: spawn agent process and call conn.listSessions()
131
+ return [];
132
+ }
133
+ }
134
+ // --- Helpers ---
135
+ function spawnAgent(config) {
136
+ const proc = spawn(config.command, config.args, {
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ });
139
+ proc.on("error", (err) => {
140
+ log.error({ err }, "[acp] failed to spawn %s", config.command);
141
+ });
142
+ if (!proc.stdin || !proc.stdout) {
143
+ throw new Error(`Failed to spawn ${config.command} — stdin/stdout not available`);
144
+ }
145
+ return proc;
146
+ }
147
+ function mapSessionUpdate(notification, toolCalls, pendingTools) {
148
+ const update = notification.update;
149
+ switch (update.sessionUpdate) {
150
+ case "agent_message_chunk": {
151
+ const content = update.content;
152
+ if (content.type === "text") {
153
+ return { type: "text_chunk", text: content.text };
154
+ }
155
+ return null;
156
+ }
157
+ case "tool_call": {
158
+ const kind = update.kind ?? "tool";
159
+ toolCalls[kind] = (toolCalls[kind] ?? 0) + 1;
160
+ const tool = acpToolCall(update.title, update.toolCallId, kind, update.locations, update.content);
161
+ pendingTools.set(update.toolCallId, tool);
162
+ return { type: "tool_use", tool };
163
+ }
164
+ case "tool_call_update":
165
+ return null;
166
+ default:
167
+ return null;
168
+ }
169
+ }
170
+ /** Map ACP tool call fields to ToolCall. */
171
+ function acpToolCall(title, toolCallId, kind, locations, content) {
172
+ const toolName = title;
173
+ const toolUseId = toolCallId;
174
+ const paths = locations?.map((l) => l.line ? `${l.path}:${l.line}` : l.path);
175
+ switch (kind) {
176
+ case "edit": {
177
+ const diff = findDiff(content);
178
+ return {
179
+ action: "edit", toolName, toolUseId,
180
+ path: diff?.path ?? paths?.[0] ?? "",
181
+ before: diff?.oldText ?? "",
182
+ after: diff?.newText ?? "",
183
+ };
184
+ }
185
+ case "write": {
186
+ const diff = findDiff(content);
187
+ return {
188
+ action: "write", toolName, toolUseId,
189
+ path: diff?.path ?? paths?.[0] ?? "",
190
+ content: diff?.newText ?? "",
191
+ };
192
+ }
193
+ case "read":
194
+ return { action: "read", toolName, toolUseId, paths: paths ?? [] };
195
+ case "execute":
196
+ return { action: "execute", toolName, toolUseId, command: title };
197
+ case "search":
198
+ return { action: "search", toolName, toolUseId, pattern: title, paths };
199
+ case "fetch":
200
+ return { action: "fetch", toolName, toolUseId, url: title };
201
+ default: {
202
+ const detail = findTextContent(content);
203
+ return { action: kind, toolName, toolUseId, paths, detail };
204
+ }
205
+ }
206
+ }
207
+ /** Find the first diff content block from ACP tool call content. */
208
+ function findDiff(content) {
209
+ const entry = content?.find((c) => c.type === "diff");
210
+ return entry?.type === "diff" ? entry : undefined;
211
+ }
212
+ /** Extract text from the first content block. */
213
+ function findTextContent(content) {
214
+ const entry = content?.find((c) => c.type === "content");
215
+ if (entry?.type === "content" && entry.content.type === "text")
216
+ return entry.content.text;
217
+ return undefined;
218
+ }
219
+ async function handlePermission(params, permissionMode, onPermissionRequest, pendingTools) {
220
+ const allowOption = params.options.find((o) => o.kind === "allow_once");
221
+ const rejectOption = params.options.find((o) => o.kind === "reject_once");
222
+ // Auto-approve in bypass mode
223
+ if (permissionMode === "bypassPermissions" || permissionMode === "dontAsk") {
224
+ return {
225
+ outcome: allowOption
226
+ ? { outcome: "selected", optionId: allowOption.optionId }
227
+ : { outcome: "cancelled" },
228
+ };
229
+ }
230
+ // Use cached tool_call data (has content/locations) over the sparse permission request
231
+ const toolCall = params.toolCall;
232
+ const tool = pendingTools.get(toolCall.toolCallId)
233
+ ?? acpToolCall(toolCall.title ?? "Unknown tool", toolCall.toolCallId, toolCall.kind ?? "other", toolCall.locations, toolCall.content);
234
+ const resp = await onPermissionRequest(tool);
235
+ if (resp.decision === "allow" && allowOption) {
236
+ return {
237
+ outcome: { outcome: "selected", optionId: allowOption.optionId },
238
+ };
239
+ }
240
+ if (rejectOption) {
241
+ return {
242
+ outcome: { outcome: "selected", optionId: rejectOption.optionId },
243
+ };
244
+ }
245
+ return { outcome: { outcome: "cancelled" } };
246
+ }
247
+ //# 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
+ }