@emqo/claudebridge 0.3.0 → 0.4.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.
@@ -8,5 +8,5 @@ export interface Adapter {
8
8
  start(): Promise<void>;
9
9
  stop(): void;
10
10
  }
11
- /** Split long text into chunks respecting newlines */
11
+ /** Split long text into chunks respecting newlines, with code-block-aware balancing */
12
12
  export declare function chunkText(text: string, maxLen: number): string[];
@@ -1,7 +1,8 @@
1
- /** Split long text into chunks respecting newlines */
1
+ /** Split long text into chunks respecting newlines, with code-block-aware balancing */
2
2
  export function chunkText(text, maxLen) {
3
3
  if (text.length <= maxLen)
4
4
  return [text];
5
+ // Phase 1: split by newlines (existing logic)
5
6
  const chunks = [];
6
7
  let remaining = text;
7
8
  while (remaining.length > 0) {
@@ -15,5 +16,35 @@ export function chunkText(text, maxLen) {
15
16
  chunks.push(remaining.slice(0, cut));
16
17
  remaining = remaining.slice(cut).replace(/^\n/, "");
17
18
  }
19
+ // Phase 2: balance code fences across chunks
20
+ // Ensures each chunk is self-contained valid MarkdownV2
21
+ let inCode = false;
22
+ let lang = "";
23
+ for (let i = 0; i < chunks.length; i++) {
24
+ const chunk = chunks[i];
25
+ const startsInCode = inCode;
26
+ // Scan fences in original chunk to determine end state and track language
27
+ const fenceRegex = /```(\w*)/g;
28
+ let toggleState = startsInCode;
29
+ let m;
30
+ while ((m = fenceRegex.exec(chunk)) !== null) {
31
+ // Opening fence (not in code) → track language
32
+ if (!toggleState && m[1]) {
33
+ lang = m[1];
34
+ }
35
+ toggleState = !toggleState;
36
+ }
37
+ inCode = toggleState;
38
+ // Apply fixes: reopen/close code blocks as needed
39
+ let fixed = chunk;
40
+ if (startsInCode) {
41
+ fixed = "```" + lang + "\n" + fixed;
42
+ }
43
+ if (inCode) {
44
+ fixed = fixed + "\n```";
45
+ // inCode stays true — next chunk's content is still logically inside code
46
+ }
47
+ chunks[i] = fixed;
48
+ }
18
49
  return chunks;
19
50
  }
@@ -9,10 +9,13 @@ export declare class DiscordAdapter implements Adapter {
9
9
  private locale;
10
10
  private client;
11
11
  private reminderTimer?;
12
+ private autoTimer?;
13
+ private autoRunning;
12
14
  constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
13
15
  private setup;
14
16
  private handlePrompt;
15
17
  start(): Promise<void>;
16
18
  stop(): void;
17
19
  private checkReminders;
20
+ private processAutoTasks;
18
21
  }
@@ -12,6 +12,8 @@ export class DiscordAdapter {
12
12
  locale;
13
13
  client;
14
14
  reminderTimer;
15
+ autoTimer;
16
+ autoRunning = false;
15
17
  constructor(engine, store, config, locale = "en") {
16
18
  this.engine = engine;
17
19
  this.store = store;
@@ -164,10 +166,13 @@ export class DiscordAdapter {
164
166
  await this.client.login(this.config.token);
165
167
  console.log(`[discord] logged in as ${this.client.user?.tag}`);
166
168
  this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
169
+ this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
167
170
  }
168
171
  stop() {
169
172
  if (this.reminderTimer)
170
173
  clearInterval(this.reminderTimer);
174
+ if (this.autoTimer)
175
+ clearInterval(this.autoTimer);
171
176
  this.client.destroy();
172
177
  }
173
178
  async checkReminders() {
@@ -184,4 +189,42 @@ export class DiscordAdapter {
184
189
  console.error("[discord] reminder error:", e);
185
190
  }
186
191
  }
192
+ async processAutoTasks() {
193
+ if (this.autoRunning)
194
+ return;
195
+ const task = this.store.getNextAutoTask("discord");
196
+ if (!task)
197
+ return;
198
+ this.autoRunning = true;
199
+ this.store.markTaskRunning(task.id);
200
+ try {
201
+ const ch = await this.client.channels.fetch(task.chat_id);
202
+ if (!ch?.isTextBased() || !("send" in ch))
203
+ throw new Error("channel not found");
204
+ const channel = ch;
205
+ await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
206
+ console.log(`[discord] auto-task #${task.id} for ${task.user_id}`);
207
+ const res = await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id);
208
+ this.store.markTaskResult(task.id, "done");
209
+ const maxLen = this.config.chunk_size || 1900;
210
+ const chunks = chunkText(res.text || "(no output)", maxLen);
211
+ await channel.send(t(this.locale, "auto_done", { id: task.id, cost: (res.cost || 0).toFixed(4) }));
212
+ for (const c of chunks)
213
+ await channel.send(c);
214
+ }
215
+ catch (err) {
216
+ this.store.markTaskResult(task.id, "failed");
217
+ console.error(`[discord] auto-task #${task.id} failed:`, err);
218
+ try {
219
+ const ch = await this.client.channels.fetch(task.chat_id);
220
+ if (ch?.isTextBased() && "send" in ch) {
221
+ await ch.send(t(this.locale, "auto_failed", { id: task.id, err: err.message || "unknown" }));
222
+ }
223
+ }
224
+ catch { }
225
+ }
226
+ finally {
227
+ this.autoRunning = false;
228
+ }
229
+ }
187
230
  }
@@ -364,7 +364,7 @@ export class TelegramAdapter {
364
364
  async processAutoTasks() {
365
365
  if (this.autoRunning)
366
366
  return;
367
- const task = this.store.getNextAutoTask();
367
+ const task = this.store.getNextAutoTask("telegram");
368
368
  if (!task)
369
369
  return;
370
370
  this.autoRunning = true;
@@ -2,6 +2,7 @@
2
2
  export declare function escapeMarkdownV2(text: string): string;
3
3
  /**
4
4
  * Convert standard markdown to Telegram MarkdownV2.
5
- * Handles code blocks (preserve as-is), inline code, bold, italic, links.
5
+ * Uses iterative scanning for code blocks instead of regex split,
6
+ * properly handling unclosed blocks and multiple consecutive blocks.
6
7
  */
7
8
  export declare function toTelegramMarkdown(text: string): string;
@@ -3,49 +3,84 @@ export function escapeMarkdownV2(text) {
3
3
  // Characters that must be escaped outside code blocks
4
4
  return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
5
5
  }
6
+ /**
7
+ * Process inline markdown: inline code, links, bold, italic, strikethrough.
8
+ * Order: extract protected elements → escape → restore formatted elements → restore placeholders.
9
+ */
10
+ function processInline(text) {
11
+ let s = text;
12
+ // 1. Extract inline code → placeholders (before escaping)
13
+ const inlineCodes = [];
14
+ s = s.replace(/`([^`]+)`/g, (_, code) => {
15
+ const escaped = code.replace(/([\\`])/g, "\\$1");
16
+ inlineCodes.push(`\`${escaped}\``);
17
+ return `\x00IC${inlineCodes.length - 1}\x00`;
18
+ });
19
+ // 2. Extract markdown links [text](url) → placeholders (before escaping)
20
+ // Handles one level of nested parens in URLs, e.g. wiki/Rust_(programming_language)
21
+ const links = [];
22
+ s = s.replace(/\[([^\]]+)\]\(([^()]*(?:\([^()]*\))*[^()]*)\)/g, (_, linkText, url) => {
23
+ const escapedText = escapeMarkdownV2(linkText);
24
+ // Inside MarkdownV2 link URL, only \ and ) need escaping
25
+ const escapedUrl = url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)");
26
+ links.push(`[${escapedText}](${escapedUrl})`);
27
+ return `\x00LK${links.length - 1}\x00`;
28
+ });
29
+ // 3. Escape all remaining special chars
30
+ s = escapeMarkdownV2(s);
31
+ // 4. Restore bold **text** → *text* (MarkdownV2 single asterisk)
32
+ s = s.replace(/\\\*\\\*([\s\S]+?)\\\*\\\*/g, "*$1*");
33
+ // 5. Restore italic _text_ → _text_
34
+ s = s.replace(/\\_(.+?)\\_/g, "_$1_");
35
+ // 6. Restore strikethrough ~~text~~ → ~text~
36
+ s = s.replace(/\\~\\~(.+?)\\~\\~/g, "~$1~");
37
+ // 7. Restore link placeholders
38
+ s = s.replace(/\x00LK(\d+)\x00/g, (_, i) => links[Number(i)]);
39
+ // 8. Restore inline code placeholders
40
+ s = s.replace(/\x00IC(\d+)\x00/g, (_, i) => inlineCodes[Number(i)]);
41
+ return s;
42
+ }
6
43
  /**
7
44
  * Convert standard markdown to Telegram MarkdownV2.
8
- * Handles code blocks (preserve as-is), inline code, bold, italic, links.
45
+ * Uses iterative scanning for code blocks instead of regex split,
46
+ * properly handling unclosed blocks and multiple consecutive blocks.
9
47
  */
10
48
  export function toTelegramMarkdown(text) {
11
- const parts = [];
12
- // Split by code blocks first (``` ... ```)
13
- const segments = text.split(/(```[\s\S]*?```)/g);
14
- for (const seg of segments) {
15
- if (seg.startsWith("```")) {
16
- // Code block: extract lang and content, only escape backslash and backtick inside
17
- const match = seg.match(/^```(\w*)\n?([\s\S]*?)```$/);
18
- if (match) {
19
- const lang = match[1];
20
- const code = match[2].replace(/([\\`])/g, "\\$1");
21
- parts.push(`\`\`\`${lang}\n${code}\`\`\``);
22
- }
23
- else {
24
- parts.push(seg);
25
- }
49
+ const result = [];
50
+ let pos = 0;
51
+ while (pos < text.length) {
52
+ // Find next code fence ```
53
+ const fenceStart = text.indexOf("```", pos);
54
+ if (fenceStart === -1) {
55
+ // No more code blocks — process rest as inline
56
+ result.push(processInline(text.slice(pos)));
57
+ break;
58
+ }
59
+ // Process text before code block as inline
60
+ if (fenceStart > pos) {
61
+ result.push(processInline(text.slice(pos, fenceStart)));
26
62
  }
27
- else {
28
- // Process inline elements
29
- let s = seg;
30
- // Protect inline code first
31
- const inlineCodes = [];
32
- s = s.replace(/`([^`]+)`/g, (_, code) => {
33
- const escaped = code.replace(/([\\`])/g, "\\$1");
34
- inlineCodes.push(`\`${escaped}\``);
35
- return `\x00IC${inlineCodes.length - 1}\x00`;
36
- });
37
- // Escape special chars in normal text
38
- s = escapeMarkdownV2(s);
39
- // Restore bold **text** → *text*
40
- s = s.replace(/\\\*\\\*(.+?)\\\*\\\*/g, "*$1*");
41
- // Restore italic _text_ (single underscore)
42
- s = s.replace(/\\_(.+?)\\_/g, "_$1_");
43
- // Restore links [text](url)
44
- s = s.replace(/\\\[(.+?)\\\]\\\((.+?)\\\)/g, "[$1]($2)");
45
- // Restore inline codes
46
- s = s.replace(/\x00IC(\d+)\x00/g, (_, i) => inlineCodes[Number(i)]);
47
- parts.push(s);
63
+ // Find closing ```
64
+ const afterOpen = fenceStart + 3;
65
+ const fenceEnd = text.indexOf("```", afterOpen);
66
+ if (fenceEnd === -1) {
67
+ // Unclosed code block — auto-close it
68
+ const raw = text.slice(afterOpen);
69
+ const langMatch = raw.match(/^(\w*)\n?/);
70
+ const lang = langMatch ? langMatch[1] : "";
71
+ const contentStart = langMatch ? langMatch[0].length : 0;
72
+ const code = raw.slice(contentStart).replace(/([\\`])/g, "\\$1");
73
+ result.push(`\`\`\`${lang}\n${code}\n\`\`\``);
74
+ break;
48
75
  }
76
+ // Complete code block — extract lang and content, escape only \ and `
77
+ const raw = text.slice(afterOpen, fenceEnd);
78
+ const langMatch = raw.match(/^(\w*)\n?/);
79
+ const lang = langMatch ? langMatch[1] : "";
80
+ const contentStart = langMatch ? langMatch[0].length : 0;
81
+ const code = raw.slice(contentStart).replace(/([\\`])/g, "\\$1");
82
+ result.push(`\`\`\`${lang}\n${code}\`\`\``);
83
+ pos = fenceEnd + 3;
49
84
  }
50
- return parts.join("");
85
+ return result.join("");
51
86
  }
@@ -46,7 +46,7 @@ export declare class Store {
46
46
  description: string;
47
47
  }[];
48
48
  markReminderSent(taskId: number): void;
49
- getNextAutoTask(): {
49
+ getNextAutoTask(platform?: string): {
50
50
  id: number;
51
51
  user_id: string;
52
52
  platform: string;
@@ -135,7 +135,10 @@ export class Store {
135
135
  markReminderSent(taskId) {
136
136
  this.db.prepare("UPDATE tasks SET reminder_sent = 1 WHERE id = ?").run(taskId);
137
137
  }
138
- getNextAutoTask() {
138
+ getNextAutoTask(platform) {
139
+ if (platform) {
140
+ return this.db.prepare("SELECT id, user_id, platform, chat_id, description FROM tasks WHERE status = 'auto' AND platform = ? ORDER BY created_at ASC LIMIT 1").get(platform) ?? null;
141
+ }
139
142
  return this.db.prepare("SELECT id, user_id, platform, chat_id, description FROM tasks WHERE status = 'auto' ORDER BY created_at ASC LIMIT 1").get() ?? null;
140
143
  }
141
144
  markTaskRunning(taskId) {
@@ -40,6 +40,25 @@ export function generateSkillDoc(ctx) {
40
40
  `- 用户要添加任务/待办 → 使用 task add`,
41
41
  `- 命令输出 JSON,请用自然语言向用户回复结果,不要直接展示 JSON`,
42
42
  `- 提醒会由 Bridge 定时器自动推送,你只需创建即可`,
43
+ ``,
44
+ `### 高级用法:任务自动分解`,
45
+ `- 遇到复杂/大型任务时,必须拆分为多个 auto-task 分步执行`,
46
+ `- 每个 auto-task 应该是独立可完成的小任务(预算内可完成)`,
47
+ `- auto-task 按创建顺序排队,每60秒执行一个`,
48
+ `- 在 auto-task 执行中可以创建新的 auto-task(链式执行)`,
49
+ `- 每个 auto-task 的描述要足够详细,因为它会在全新会话中执行`,
50
+ `- 示例:用户说"优化整个项目" → 创建多个 auto-task:`,
51
+ ` 1. "分析项目结构,列出所有需要优化的模块和具体改进点"`,
52
+ ` 2. "优化模块A:[具体描述,包含文件路径和修改内容]"`,
53
+ ` 3. "优化模块B:[具体描述]"`,
54
+ ` 4. "运行测试验证所有修改,提交代码,生成优化报告"`,
55
+ ``,
56
+ `### 跨任务记忆传递`,
57
+ `- 每个 auto-task 完成关键分析后,用 memory add 保存结论`,
58
+ `- 下一个 auto-task 会自动加载记忆,可以读取前序任务的成果`,
59
+ `- 示例:分析完成后调用 \`${ctl} memory add ${ctx.userId} "模块A优化点:1.重构API 2.添加缓存 3.修复N+1查询"\``,
60
+ `- 最后一个 auto-task 完成后,清理临时工作记忆(可选)`,
61
+ `- 重要:描述中包含"先用 memory list 查看前序任务的分析结果"可确保链式上下文不断`,
43
62
  ].join("\n");
44
63
  }
45
64
  return [
@@ -76,5 +95,24 @@ export function generateSkillDoc(ctx) {
76
95
  `- User wants to add a task/todo → use task add`,
77
96
  `- Commands output JSON. Respond to the user in natural language, do not dump raw JSON.`,
78
97
  `- Reminders are automatically pushed by Bridge timers — you only need to create them.`,
98
+ ``,
99
+ `### Advanced: Auto-Task Decomposition`,
100
+ `- For complex/large tasks, decompose into multiple auto-tasks`,
101
+ `- Each auto-task should be independently completable within budget`,
102
+ `- Auto-tasks execute in FIFO order, one every 60 seconds`,
103
+ `- An auto-task can create new auto-tasks (chaining)`,
104
+ `- Each description must be detailed enough for a fresh session`,
105
+ `- Example: user says "optimize the project" → create:`,
106
+ ` 1. "Analyze project structure, list modules needing optimization"`,
107
+ ` 2. "Optimize module A: [specific file paths and changes]"`,
108
+ ` 3. "Optimize module B: [specific description]"`,
109
+ ` 4. "Run tests, commit changes, generate optimization report"`,
110
+ ``,
111
+ `### Cross-Task Memory Bridging`,
112
+ `- After completing key analysis in an auto-task, save conclusions via memory add`,
113
+ `- The next auto-task automatically loads memories, accessing prior task findings`,
114
+ `- Example: after analysis, call \`${ctl} memory add ${ctx.userId} "Module A needs: 1.refactor API 2.add cache 3.fix N+1 queries"\``,
115
+ `- Optionally clean up temporary work memories after the final auto-task`,
116
+ `- Tip: include "first run memory list to review prior task findings" in descriptions to ensure chain continuity`,
79
117
  ].join("\n");
80
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Bridge Claude Code Agent SDK to chat platforms (Telegram, Discord, etc.)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {