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.
@@ -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;
@@ -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 handleMediaMessage;
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;
@@ -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.handleMediaMessage(ctx, largest.file_id, "image/jpeg", undefined, ctx.message.caption);
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.handleMediaMessage(ctx, doc.file_id, doc.mime_type ?? "application/octet-stream", doc.file_name, ctx.message.caption);
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 handleMediaMessage(ctx, fileId, mimeType, filename, caption) {
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${MIME_TO_EXT[att.mimeType] ?? ""}`;
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
@@ -16,7 +16,7 @@ export function initLogger(logPath) {
16
16
  file: logPath,
17
17
  frequency: "daily",
18
18
  size: "10m",
19
- limit: { count: 5 },
19
+ limit: { count: 5, removeOtherLogFiles: true },
20
20
  },
21
21
  level: process.env.LOG_LEVEL ?? "info",
22
22
  },
@@ -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.4.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"