clearclaw 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/slack.d.ts +2 -1
- package/dist/channel/slack.js +9 -0
- package/dist/channel/telegram.d.ts +3 -2
- package/dist/channel/telegram.js +23 -4
- package/dist/engine/claude-code.js +2 -1
- package/dist/files.js +2 -11
- package/dist/logger.js +1 -1
- package/dist/orchestrator.js +28 -0
- package/dist/types.d.ts +7 -1
- package/package.json +2 -1
package/dist/channel/slack.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { Channel, Button, ButtonResponse, SendMessageOpts } from "../types.js";
|
|
2
|
+
import type { Channel, Button, ButtonResponse, SendFileOpts, SendMessageOpts } from "../types.js";
|
|
3
3
|
export declare class SlackChannel extends EventEmitter implements Channel {
|
|
4
4
|
name: string;
|
|
5
5
|
private app;
|
|
@@ -22,6 +22,7 @@ export declare class SlackChannel extends EventEmitter implements Channel {
|
|
|
22
22
|
unpinAllMessages(chatId: string): Promise<void>;
|
|
23
23
|
updateStatus(chatId: string, text: string): Promise<void>;
|
|
24
24
|
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
25
|
+
sendFile(chatId: string, buffer: Buffer, filename: string, opts?: SendFileOpts): Promise<void>;
|
|
25
26
|
private slackId;
|
|
26
27
|
/** Download a file from Slack using bot token auth. */
|
|
27
28
|
private downloadFile;
|
package/dist/channel/slack.js
CHANGED
|
@@ -344,6 +344,15 @@ export class SlackChannel extends EventEmitter {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
async sendFile(chatId, buffer, filename, opts) {
|
|
348
|
+
const channel = this.slackId(chatId);
|
|
349
|
+
await this.app.client.files.uploadV2({
|
|
350
|
+
channel_id: channel,
|
|
351
|
+
file: buffer,
|
|
352
|
+
filename,
|
|
353
|
+
initial_comment: opts?.caption,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
347
356
|
slackId(chatId) {
|
|
348
357
|
return chatId.replace(/^slack:/, "");
|
|
349
358
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { Channel, Button, ButtonResponse, SendMessageOpts } from "../types.js";
|
|
2
|
+
import type { Channel, Button, ButtonResponse, SendFileOpts, SendMessageOpts } from "../types.js";
|
|
3
3
|
export declare class TelegramChannel extends EventEmitter implements Channel {
|
|
4
4
|
name: string;
|
|
5
5
|
private bot;
|
|
@@ -24,7 +24,8 @@ export declare class TelegramChannel extends EventEmitter implements Channel {
|
|
|
24
24
|
/** Extract auth-checked sender info from a grammY context. Returns null if unauthorized. */
|
|
25
25
|
private extractSender;
|
|
26
26
|
/** Shared handler for photo/document messages: download, emit attachment or fallback. */
|
|
27
|
-
private
|
|
27
|
+
private handleFileMessage;
|
|
28
|
+
sendFile(chatId: string, buffer: Buffer, filename: string, opts?: SendFileOpts): Promise<void>;
|
|
28
29
|
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
29
30
|
private downloadFile;
|
|
30
31
|
private numericId;
|
package/dist/channel/telegram.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { Bot, InlineKeyboard } from "grammy";
|
|
2
|
+
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
3
|
+
import mime from "mime";
|
|
3
4
|
import log from "../logger.js";
|
|
4
5
|
export class TelegramChannel extends EventEmitter {
|
|
5
6
|
name = "telegram";
|
|
@@ -38,12 +39,12 @@ export class TelegramChannel extends EventEmitter {
|
|
|
38
39
|
this.bot.on("message:photo", async (ctx) => {
|
|
39
40
|
const photo = ctx.message.photo;
|
|
40
41
|
const largest = photo[photo.length - 1];
|
|
41
|
-
await this.
|
|
42
|
+
await this.handleFileMessage(ctx, largest.file_id, "image/jpeg", undefined, ctx.message.caption);
|
|
42
43
|
});
|
|
43
44
|
// Documents: download with original MIME + filename
|
|
44
45
|
this.bot.on("message:document", async (ctx) => {
|
|
45
46
|
const doc = ctx.message.document;
|
|
46
|
-
await this.
|
|
47
|
+
await this.handleFileMessage(ctx, doc.file_id, doc.mime_type ?? "application/octet-stream", doc.file_name, ctx.message.caption);
|
|
47
48
|
});
|
|
48
49
|
// Placeholder handlers for unsupported media types
|
|
49
50
|
for (const [filter, label] of [
|
|
@@ -283,7 +284,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
283
284
|
};
|
|
284
285
|
}
|
|
285
286
|
/** Shared handler for photo/document messages: download, emit attachment or fallback. */
|
|
286
|
-
async
|
|
287
|
+
async handleFileMessage(ctx, fileId, mimeType, filename, caption) {
|
|
287
288
|
const sender = this.extractSender(ctx);
|
|
288
289
|
if (!sender)
|
|
289
290
|
return;
|
|
@@ -305,6 +306,24 @@ export class TelegramChannel extends EventEmitter {
|
|
|
305
306
|
});
|
|
306
307
|
}
|
|
307
308
|
}
|
|
309
|
+
async sendFile(chatId, buffer, filename, opts) {
|
|
310
|
+
const id = this.numericId(chatId);
|
|
311
|
+
const mimeType = opts?.mimeType ?? mime.getType(filename) ?? "application/octet-stream";
|
|
312
|
+
const file = new InputFile(buffer, filename);
|
|
313
|
+
const caption = opts?.caption;
|
|
314
|
+
if (mimeType.startsWith("image/")) {
|
|
315
|
+
await this.bot.api.sendPhoto(id, file, { caption });
|
|
316
|
+
}
|
|
317
|
+
else if (mimeType.startsWith("video/")) {
|
|
318
|
+
await this.bot.api.sendVideo(id, file, { caption });
|
|
319
|
+
}
|
|
320
|
+
else if (mimeType.startsWith("audio/")) {
|
|
321
|
+
await this.bot.api.sendAudio(id, file, { caption });
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
await this.bot.api.sendDocument(id, file, { caption });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
308
327
|
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
309
328
|
async downloadFile(fileId) {
|
|
310
329
|
const file = await this.bot.api.getFile(fileId);
|
|
@@ -15,7 +15,7 @@ export class ClaudeCodeEngine {
|
|
|
15
15
|
}));
|
|
16
16
|
}
|
|
17
17
|
async *runTurn(opts) {
|
|
18
|
-
const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, appendSystemPrompt, signal, } = opts;
|
|
18
|
+
const { sessionId, cwd, prompt: textPrompt, attachments, permissionMode, onPermissionRequest, appendSystemPrompt, mcpServers, signal, } = opts;
|
|
19
19
|
const abortController = new AbortController();
|
|
20
20
|
// Wire external signal to our abort controller
|
|
21
21
|
if (signal) {
|
|
@@ -66,6 +66,7 @@ export class ClaudeCodeEngine {
|
|
|
66
66
|
canUseTool,
|
|
67
67
|
abortController,
|
|
68
68
|
settingSources: ["user", "project", "local"],
|
|
69
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
69
70
|
...(appendSystemPrompt
|
|
70
71
|
? {
|
|
71
72
|
systemPrompt: {
|
package/dist/files.js
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import mime from "mime";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
/** Map common MIME types to file extensions. */
|
|
5
|
-
const MIME_TO_EXT = {
|
|
6
|
-
"image/jpeg": ".jpg",
|
|
7
|
-
"image/png": ".png",
|
|
8
|
-
"image/gif": ".gif",
|
|
9
|
-
"image/webp": ".webp",
|
|
10
|
-
"application/pdf": ".pdf",
|
|
11
|
-
"text/plain": ".txt",
|
|
12
|
-
"text/csv": ".csv",
|
|
13
|
-
};
|
|
14
5
|
/** Save an attachment to disk for audit logging. Returns the saved file path. */
|
|
15
6
|
export async function saveFile(att, workspace, filesPath) {
|
|
16
7
|
const epoch = Math.floor(Date.now() / 1000);
|
|
17
8
|
const hex = crypto.randomBytes(2).toString("hex");
|
|
18
9
|
const name = att.filename
|
|
19
10
|
? sanitizeFilename(att.filename)
|
|
20
|
-
: `file${
|
|
11
|
+
: `file${att.mimeType ? `.${mime.getExtension(att.mimeType) ?? "bin"}` : ""}`;
|
|
21
12
|
const filename = `${workspace}-${epoch}-${hex}-${name}`;
|
|
22
13
|
const filePath = path.join(filesPath, filename);
|
|
23
14
|
await fs.writeFile(filePath, att.buffer);
|
package/dist/logger.js
CHANGED
package/dist/orchestrator.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { z } from "zod";
|
|
3
5
|
import log from "./logger.js";
|
|
4
6
|
import { saveFile } from "./files.js";
|
|
5
7
|
import { formatToolStatusLine, formatToolCallSummary, formatPermissionPrompt, timeAgo } from "./format.js";
|
|
@@ -185,6 +187,26 @@ export class Orchestrator {
|
|
|
185
187
|
if (saved > 0)
|
|
186
188
|
log.info("[turn] saved %d/%d attachment(s) for workspace %s", saved, msg.attachments.length, ws.name);
|
|
187
189
|
}
|
|
190
|
+
const mcpServer = createSdkMcpServer({
|
|
191
|
+
name: "clearclaw",
|
|
192
|
+
tools: [
|
|
193
|
+
tool("send_file", "Send a file or image to the current chat conversation", {
|
|
194
|
+
file_path: z.string().optional().describe("Absolute path to the file to send"),
|
|
195
|
+
data: z.string().optional().describe("Base64-encoded file data (alternative to file_path)"),
|
|
196
|
+
filename: z.string().optional().describe("Filename (required when using data, optional with file_path)"),
|
|
197
|
+
caption: z.string().optional().describe("Optional caption to accompany the file"),
|
|
198
|
+
}, async (args) => {
|
|
199
|
+
if (!args.file_path && !args.data)
|
|
200
|
+
throw new Error("Either file_path or data must be provided");
|
|
201
|
+
const buffer = args.file_path
|
|
202
|
+
? await fs.promises.readFile(args.file_path)
|
|
203
|
+
: Buffer.from(args.data, "base64");
|
|
204
|
+
const name = args.filename ?? path.basename(args.file_path ?? "file");
|
|
205
|
+
await this.channel.sendFile(msg.chatId, buffer, name, { caption: args.caption });
|
|
206
|
+
return { content: [{ type: "text", text: `Sent ${name} to chat` }] };
|
|
207
|
+
}),
|
|
208
|
+
],
|
|
209
|
+
});
|
|
188
210
|
try {
|
|
189
211
|
for await (const event of this.engine.runTurn({
|
|
190
212
|
sessionId: ws.current_session_id,
|
|
@@ -193,8 +215,14 @@ export class Orchestrator {
|
|
|
193
215
|
attachments: msg.attachments,
|
|
194
216
|
permissionMode: state.permissionMode ?? this.permissionMode,
|
|
195
217
|
appendSystemPrompt,
|
|
218
|
+
mcpServers: { clearclaw: mcpServer },
|
|
196
219
|
signal: abort.signal,
|
|
197
220
|
onPermissionRequest: async (req) => {
|
|
221
|
+
// Auto-allow ClearClaw's own MCP tools — no chat prompt needed
|
|
222
|
+
if (req.toolName.startsWith("mcp__clearclaw__")) {
|
|
223
|
+
log.info(`[perm] ${req.toolName} → auto-allow`);
|
|
224
|
+
return { decision: "allow" };
|
|
225
|
+
}
|
|
198
226
|
log.info(`[perm] ${req.toolName}`);
|
|
199
227
|
try {
|
|
200
228
|
const promptText = formatPermissionPrompt(req.toolName, req.input, req.description);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import type { McpServerConfig, PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
export interface ChannelEvents {
|
|
3
3
|
message: [msg: InboundMessage];
|
|
4
4
|
}
|
|
@@ -15,6 +15,7 @@ export interface Channel {
|
|
|
15
15
|
unpinAllMessages(chatId: string): Promise<void>;
|
|
16
16
|
updateStatus(chatId: string, text: string): Promise<void>;
|
|
17
17
|
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
18
|
+
sendFile(chatId: string, buffer: Buffer, filename: string, opts?: SendFileOpts): Promise<void>;
|
|
18
19
|
on<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
19
20
|
off<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
20
21
|
emit<K extends keyof ChannelEvents>(event: K, ...args: ChannelEvents[K]): boolean;
|
|
@@ -33,6 +34,10 @@ export interface SendMessageOpts {
|
|
|
33
34
|
/** When false, skip consuming the typing placeholder (e.g. for tool status messages). */
|
|
34
35
|
consumeTyping?: boolean;
|
|
35
36
|
}
|
|
37
|
+
export interface SendFileOpts {
|
|
38
|
+
caption?: string;
|
|
39
|
+
mimeType?: string;
|
|
40
|
+
}
|
|
36
41
|
export interface SessionInfo {
|
|
37
42
|
sessionId: string;
|
|
38
43
|
summary: string;
|
|
@@ -52,6 +57,7 @@ export interface RunTurnOpts {
|
|
|
52
57
|
permissionMode: PermissionMode;
|
|
53
58
|
onPermissionRequest: (req: PermissionRequest) => Promise<PermissionResponse>;
|
|
54
59
|
appendSystemPrompt?: string;
|
|
60
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
55
61
|
signal?: AbortSignal;
|
|
56
62
|
}
|
|
57
63
|
export interface PermissionRequest {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clearclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Your coding agent, from your phone — personal assistant + project workspaces on your machine, built on the CLI's permissions, config, and memory instead of replacing them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@slack/bolt": "^4.6.0",
|
|
41
41
|
"diff": "^8.0.3",
|
|
42
42
|
"grammy": "^1.30.0",
|
|
43
|
+
"mime": "^4.1.0",
|
|
43
44
|
"pino": "^10.3.1",
|
|
44
45
|
"pino-pretty": "^13.1.3",
|
|
45
46
|
"pino-roll": "^4.0.0"
|