@emqo/claudebridge 0.10.0 → 0.10.2

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.
@@ -11,6 +11,7 @@ export declare class DiscordAdapter implements Adapter {
11
11
  private reminderTimer?;
12
12
  private autoTimer?;
13
13
  private approvalTimer?;
14
+ private fileSendTimer?;
14
15
  private activeAutoTasks;
15
16
  private maxParallel;
16
17
  constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
@@ -27,4 +28,5 @@ export declare class DiscordAdapter implements Adapter {
27
28
  private checkApprovals;
28
29
  private handleStatusCommand;
29
30
  private handleSessionsCommand;
31
+ private checkFileSends;
30
32
  }
@@ -1,5 +1,5 @@
1
1
  import { Client, GatewayIntentBits } from "discord.js";
2
- import { writeFileSync } from "fs";
2
+ import { writeFileSync, existsSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { chunkText } from "./base.js";
5
5
  import { reloadConfig } from "../core/config.js";
@@ -16,6 +16,7 @@ export class DiscordAdapter {
16
16
  reminderTimer;
17
17
  autoTimer;
18
18
  approvalTimer;
19
+ fileSendTimer;
19
20
  activeAutoTasks = 0;
20
21
  maxParallel = 1;
21
22
  constructor(engine, store, config, locale = "en") {
@@ -255,6 +256,7 @@ export class DiscordAdapter {
255
256
  this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
256
257
  this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
257
258
  this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
259
+ this.fileSendTimer = setInterval(() => this.checkFileSends(), 5000);
258
260
  }
259
261
  stop() {
260
262
  if (this.reminderTimer)
@@ -263,6 +265,8 @@ export class DiscordAdapter {
263
265
  clearInterval(this.autoTimer);
264
266
  if (this.approvalTimer)
265
267
  clearInterval(this.approvalTimer);
268
+ if (this.fileSendTimer)
269
+ clearInterval(this.fileSendTimer);
266
270
  this.client.destroy();
267
271
  }
268
272
  async checkReminders() {
@@ -409,4 +413,31 @@ export class DiscordAdapter {
409
413
  });
410
414
  await msg.reply(`${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
411
415
  }
416
+ async checkFileSends() {
417
+ try {
418
+ const pending = this.store.getPendingFileSends("discord");
419
+ for (const f of pending) {
420
+ if (!existsSync(f.file_path)) {
421
+ this.store.markFileFailed(f.id);
422
+ continue;
423
+ }
424
+ try {
425
+ const ch = await this.client.channels.fetch(f.chat_id);
426
+ if (!ch?.isTextBased() || !("send" in ch)) {
427
+ this.store.markFileFailed(f.id);
428
+ continue;
429
+ }
430
+ await ch.send({ content: f.caption || undefined, files: [f.file_path] });
431
+ this.store.markFileSent(f.id);
432
+ }
433
+ catch (err) {
434
+ log.error("file send error", { id: f.id, error: err?.message });
435
+ this.store.markFileFailed(f.id);
436
+ }
437
+ }
438
+ }
439
+ catch (e) {
440
+ log.error("checkFileSends error", { error: e?.message });
441
+ }
442
+ }
412
443
  }
@@ -12,6 +12,7 @@ export declare class TelegramAdapter implements Adapter {
12
12
  private reminderTimer?;
13
13
  private autoTimer?;
14
14
  private approvalTimer?;
15
+ private fileSendTimer?;
15
16
  private activeAutoTasks;
16
17
  private maxParallel;
17
18
  private pages;
@@ -38,4 +39,5 @@ export declare class TelegramAdapter implements Adapter {
38
39
  private handleApprovalCallback;
39
40
  private handleStatusCommand;
40
41
  private handleSessionsCommand;
42
+ private checkFileSends;
41
43
  }
@@ -1,4 +1,6 @@
1
1
  import { chunkText } from "./base.js";
2
+ import { existsSync } from "fs";
3
+ import { basename, extname } from "path";
2
4
  import { reloadConfig } from "../core/config.js";
3
5
  import { toTelegramMarkdown } from "../core/markdown.js";
4
6
  import { t, getCommandDescriptions } from "../core/i18n.js";
@@ -15,6 +17,7 @@ export class TelegramAdapter {
15
17
  reminderTimer;
16
18
  autoTimer;
17
19
  approvalTimer;
20
+ fileSendTimer;
18
21
  activeAutoTasks = 0;
19
22
  maxParallel = 1;
20
23
  pages = new Map();
@@ -78,7 +81,10 @@ export class TelegramAdapter {
78
81
  ...(parseMode ? { parse_mode: parseMode } : {}),
79
82
  });
80
83
  }
81
- catch { }
84
+ catch (e) {
85
+ if (parseMode)
86
+ log.warn("editMsg failed", { parseMode, error: e?.message?.slice(0, 200) });
87
+ }
82
88
  }
83
89
  async handleUpdate(update) {
84
90
  if (update.callback_query) {
@@ -337,9 +343,12 @@ export class TelegramAdapter {
337
343
  const rawChunks = chunkText(fullText, maxLen);
338
344
  if (mdChunks.length <= 1) {
339
345
  try {
340
- await this.editMsg(chatId, msgId, mdChunks[0], "MarkdownV2");
346
+ await this.call("editMessageText", {
347
+ chat_id: chatId, message_id: msgId, text: mdChunks[0], parse_mode: "MarkdownV2",
348
+ });
341
349
  }
342
- catch {
350
+ catch (e) {
351
+ log.warn("MarkdownV2 fallback", { error: e?.message?.slice(0, 200) });
343
352
  await this.editMsg(chatId, msgId, fullText);
344
353
  }
345
354
  }
@@ -381,6 +390,7 @@ export class TelegramAdapter {
381
390
  this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
382
391
  this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
383
392
  this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
393
+ this.fileSendTimer = setInterval(() => this.checkFileSends(), 5000);
384
394
  await this.registerCommands();
385
395
  let pollBackoff = 0;
386
396
  while (this.running) {
@@ -416,6 +426,8 @@ export class TelegramAdapter {
416
426
  clearInterval(this.autoTimer);
417
427
  if (this.approvalTimer)
418
428
  clearInterval(this.approvalTimer);
429
+ if (this.fileSendTimer)
430
+ clearInterval(this.fileSendTimer);
419
431
  }
420
432
  async registerCommands() {
421
433
  try {
@@ -625,4 +637,43 @@ export class TelegramAdapter {
625
637
  });
626
638
  await this.reply(chatId, `${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
627
639
  }
640
+ async checkFileSends() {
641
+ try {
642
+ const pending = this.store.getPendingFileSends("telegram");
643
+ for (const f of pending) {
644
+ if (!existsSync(f.file_path)) {
645
+ this.store.markFileFailed(f.id);
646
+ continue;
647
+ }
648
+ const chatId = Number(f.chat_id);
649
+ const ext = extname(f.file_path).toLowerCase();
650
+ const isPhoto = [".jpg", ".jpeg", ".png", ".gif"].includes(ext);
651
+ const method = isPhoto ? "sendPhoto" : "sendDocument";
652
+ const fieldName = isPhoto ? "photo" : "document";
653
+ try {
654
+ const form = new FormData();
655
+ form.append("chat_id", String(chatId));
656
+ const blob = new Blob([await import("fs").then(fs => fs.readFileSync(f.file_path))]);
657
+ form.append(fieldName, blob, basename(f.file_path));
658
+ if (f.caption)
659
+ form.append("caption", f.caption);
660
+ const res = await fetch(`${this.api}/${method}`, { method: "POST", body: form });
661
+ const json = await res.json();
662
+ if (json.ok)
663
+ this.store.markFileSent(f.id);
664
+ else {
665
+ log.error("file send API error", { id: f.id, desc: json.description });
666
+ this.store.markFileFailed(f.id);
667
+ }
668
+ }
669
+ catch (err) {
670
+ log.error("file send error", { id: f.id, error: err?.message });
671
+ this.store.markFileFailed(f.id);
672
+ }
673
+ }
674
+ }
675
+ catch (e) {
676
+ log.error("checkFileSends error", { error: e?.message });
677
+ }
678
+ }
628
679
  }
@@ -152,4 +152,15 @@ export declare class Store {
152
152
  summary: string;
153
153
  last_active_at: number;
154
154
  }[];
155
+ addFileSend(userId: string, platform: string, chatId: string, filePath: string, caption: string): number;
156
+ getPendingFileSends(platform: string): {
157
+ id: number;
158
+ user_id: string;
159
+ platform: string;
160
+ chat_id: string;
161
+ file_path: string;
162
+ caption: string;
163
+ }[];
164
+ markFileSent(id: number): void;
165
+ markFileFailed(id: number): void;
155
166
  }
@@ -72,6 +72,17 @@ export class Store {
72
72
  message_count INTEGER NOT NULL DEFAULT 0,
73
73
  total_cost REAL NOT NULL DEFAULT 0
74
74
  );
75
+ CREATE TABLE IF NOT EXISTS file_sends (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ user_id TEXT NOT NULL,
78
+ platform TEXT NOT NULL,
79
+ chat_id TEXT NOT NULL,
80
+ file_path TEXT NOT NULL,
81
+ caption TEXT NOT NULL DEFAULT '',
82
+ status TEXT NOT NULL DEFAULT 'pending',
83
+ created_at INTEGER NOT NULL
84
+ );
85
+ CREATE INDEX IF NOT EXISTS idx_file_sends_status ON file_sends(platform, status);
75
86
  CREATE INDEX IF NOT EXISTS idx_subsess_user ON sub_sessions(user_id, platform, status);
76
87
  CREATE TABLE IF NOT EXISTS sub_session_messages (
77
88
  platform_msg_id TEXT NOT NULL,
@@ -319,4 +330,18 @@ export class Store {
319
330
  getSubSessionSummaries(userId, platform) {
320
331
  return this.db.prepare("SELECT id, label, summary, last_active_at FROM sub_sessions WHERE user_id = ? AND platform = ? AND status IN ('active','idle') ORDER BY last_active_at DESC").all(userId, platform);
321
332
  }
333
+ // --- file_sends ---
334
+ addFileSend(userId, platform, chatId, filePath, caption) {
335
+ const r = this.db.prepare("INSERT INTO file_sends (user_id, platform, chat_id, file_path, caption, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?)").run(userId, platform, chatId, filePath, caption, Date.now());
336
+ return Number(r.lastInsertRowid);
337
+ }
338
+ getPendingFileSends(platform) {
339
+ return this.db.prepare("SELECT id, user_id, platform, chat_id, file_path, caption FROM file_sends WHERE platform = ? AND status = 'pending'").all(platform);
340
+ }
341
+ markFileSent(id) {
342
+ this.db.prepare("UPDATE file_sends SET status = 'sent' WHERE id = ?").run(id);
343
+ }
344
+ markFileFailed(id) {
345
+ this.db.prepare("UPDATE file_sends SET status = 'failed' WHERE id = ?").run(id);
346
+ }
322
347
  }
package/dist/ctl.js CHANGED
@@ -166,6 +166,24 @@ else if (category === "auto") {
166
166
  fail("Usage: auto <add|add-approval|result|list|cancel|clear> ...");
167
167
  }
168
168
  }
169
+ else if (category === "file") {
170
+ if (action === "send") {
171
+ const [userId, platform, chatId, filePath, ...captionParts] = rest;
172
+ if (!userId || !platform || !chatId || !filePath)
173
+ fail("Usage: file send <user_id> <platform> <chat_id> <file_path> [caption...]");
174
+ const { resolve } = await import("path");
175
+ const { existsSync } = await import("fs");
176
+ const resolved = resolve(filePath);
177
+ if (!existsSync(resolved))
178
+ fail(`File not found: ${resolved}`);
179
+ const caption = captionParts.join(" ");
180
+ const r = db.prepare("INSERT INTO file_sends (user_id, platform, chat_id, file_path, caption, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?)").run(userId, platform, chatId, resolved, caption, Date.now());
181
+ output({ ok: true, id: Number(r.lastInsertRowid), message: `File queued for sending: ${resolved}` });
182
+ }
183
+ else {
184
+ fail("Usage: file <send> ...");
185
+ }
186
+ }
169
187
  else if (category === "session") {
170
188
  if (action === "list") {
171
189
  const [userId] = rest;
@@ -179,6 +197,6 @@ else if (category === "session") {
179
197
  }
180
198
  }
181
199
  else {
182
- fail("Usage: claudebridge-ctl <memory|task|reminder|auto|session> <action> [args...]");
200
+ fail("Usage: claudebridge-ctl <memory|task|reminder|auto|file|session> <action> [args...]");
183
201
  }
184
202
  db.close();
@@ -36,6 +36,12 @@ export function generateSkillDoc(ctx) {
36
36
  `- 取消自动任务: \`${ctl} auto cancel <任务ID>\``,
37
37
  `- 清除已完成任务: \`${ctl} auto clear ${ctx.userId}\``,
38
38
  ``,
39
+ `### 文件发送`,
40
+ `- 发送文件: \`${ctl} file send ${ctx.userId} ${ctx.platform} ${ctx.chatId} <文件路径> "说明"\``,
41
+ `- filePath 为 workspace 中的文件路径(相对或绝对)`,
42
+ `- 支持图片(jpg/png/gif)和文档(pdf/csv/txt/zip 等)`,
43
+ `- 用户要求生成文件时:先创建文件,再调用此命令发送`,
44
+ ``,
39
45
  `### 使用指南`,
40
46
  `- 用户要你记住某事 → 使用 memory add`,
41
47
  `- 用户问你记住了什么 → 使用 memory list`,
@@ -146,6 +152,12 @@ export function generateSkillDoc(ctx) {
146
152
  `- Cancel an auto task: \`${ctl} auto cancel <task_id>\``,
147
153
  `- Clear completed tasks: \`${ctl} auto clear ${ctx.userId}\``,
148
154
  ``,
155
+ `### File Sending`,
156
+ `- Send a file: \`${ctl} file send ${ctx.userId} ${ctx.platform} ${ctx.chatId} <filePath> "caption"\``,
157
+ `- filePath can be relative (to workspace) or absolute`,
158
+ `- Supports images (jpg/png/gif) and documents (pdf/csv/txt/zip etc.)`,
159
+ `- When user asks to generate a file: create it first, then call this command to send`,
160
+ ``,
149
161
  `### Guidelines`,
150
162
  `- User wants you to remember something → use memory add`,
151
163
  `- User asks what you remember → use memory list`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Bridge claude CLI to chat platforms (Telegram, Discord) with scheduled auto-tasks, autonomous project management, HITL approval, conditional branching, webhook triggers, parallel execution, and observability",
5
5
  "main": "dist/index.js",
6
6
  "bin": {