@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.
- package/dist/adapters/base.d.ts +1 -1
- package/dist/adapters/base.js +32 -1
- package/dist/adapters/discord.d.ts +3 -0
- package/dist/adapters/discord.js +43 -0
- package/dist/adapters/telegram.js +1 -1
- package/dist/core/markdown.d.ts +2 -1
- package/dist/core/markdown.js +73 -38
- package/dist/core/store.d.ts +1 -1
- package/dist/core/store.js +4 -1
- package/dist/skills/bridge.js +38 -0
- package/package.json +1 -1
package/dist/adapters/base.d.ts
CHANGED
|
@@ -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[];
|
package/dist/adapters/base.js
CHANGED
|
@@ -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
|
}
|
package/dist/adapters/discord.js
CHANGED
|
@@ -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;
|
package/dist/core/markdown.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export declare function escapeMarkdownV2(text: string): string;
|
|
3
3
|
/**
|
|
4
4
|
* Convert standard markdown to Telegram MarkdownV2.
|
|
5
|
-
*
|
|
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;
|
package/dist/core/markdown.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
85
|
+
return result.join("");
|
|
51
86
|
}
|
package/dist/core/store.d.ts
CHANGED
package/dist/core/store.js
CHANGED
|
@@ -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) {
|
package/dist/skills/bridge.js
CHANGED
|
@@ -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
|
}
|