@emqo/claudebridge 0.7.0 → 0.8.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 -0
- package/dist/adapters/discord.d.ts +1 -0
- package/dist/adapters/discord.js +7 -3
- package/dist/adapters/telegram.d.ts +1 -0
- package/dist/adapters/telegram.js +20 -7
- package/dist/core/agent.js +5 -0
- package/dist/core/config.js +1 -1
- package/dist/core/keys.js +2 -0
- package/dist/core/lock.js +4 -1
- package/dist/core/store.js +11 -3
- package/dist/ctl.js +19 -29
- package/dist/index.js +12 -0
- package/package.json +1 -1
package/dist/adapters/base.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface MessageContext {
|
|
|
7
7
|
export interface Adapter {
|
|
8
8
|
start(): Promise<void>;
|
|
9
9
|
stop(): void;
|
|
10
|
+
reloadConfig?(config: any, locale: string): void;
|
|
10
11
|
}
|
|
11
12
|
/** Split long text into chunks respecting newlines, with code-block-aware balancing */
|
|
12
13
|
export declare function chunkText(text: string, maxLen: number): string[];
|
|
@@ -14,6 +14,7 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
14
14
|
private activeAutoTasks;
|
|
15
15
|
private maxParallel;
|
|
16
16
|
constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
|
|
17
|
+
reloadConfig(config: DiscordConfig, locale: string): void;
|
|
17
18
|
private setup;
|
|
18
19
|
private handlePrompt;
|
|
19
20
|
start(): Promise<void>;
|
package/dist/adapters/discord.js
CHANGED
|
@@ -31,6 +31,11 @@ export class DiscordAdapter {
|
|
|
31
31
|
});
|
|
32
32
|
this.setup();
|
|
33
33
|
}
|
|
34
|
+
reloadConfig(config, locale) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.locale = locale;
|
|
37
|
+
this.maxParallel = this.engine.getMaxParallel();
|
|
38
|
+
}
|
|
34
39
|
setup() {
|
|
35
40
|
this.client.on("messageCreate", async (msg) => {
|
|
36
41
|
if (msg.author.bot)
|
|
@@ -239,9 +244,8 @@ export class DiscordAdapter {
|
|
|
239
244
|
const channel = ch;
|
|
240
245
|
await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
241
246
|
console.log(`[discord] auto-task #${task.id} for ${task.user_id}`);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
|
|
247
|
+
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
248
|
+
const res = await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
|
|
245
249
|
if (res.timedOut) {
|
|
246
250
|
this.store.markTaskResult(task.id, "failed");
|
|
247
251
|
if (res.text)
|
|
@@ -17,6 +17,7 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
17
17
|
private pages;
|
|
18
18
|
private static PAGE_TTL;
|
|
19
19
|
constructor(engine: AgentEngine, store: Store, config: TelegramConfig, locale?: string);
|
|
20
|
+
reloadConfig(config: TelegramConfig, locale: string): void;
|
|
20
21
|
private get api();
|
|
21
22
|
private call;
|
|
22
23
|
private reply;
|
|
@@ -23,6 +23,11 @@ export class TelegramAdapter {
|
|
|
23
23
|
this.config = config;
|
|
24
24
|
this.locale = locale;
|
|
25
25
|
}
|
|
26
|
+
reloadConfig(config, locale) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.locale = locale;
|
|
29
|
+
this.maxParallel = this.engine.getMaxParallel();
|
|
30
|
+
}
|
|
26
31
|
get api() {
|
|
27
32
|
return `https://api.telegram.org/bot${this.config.token}`;
|
|
28
33
|
}
|
|
@@ -286,6 +291,11 @@ export class TelegramAdapter {
|
|
|
286
291
|
else {
|
|
287
292
|
// Multi-page — store pages and show inline keyboard
|
|
288
293
|
const key = `${chatId}:${msgId}`;
|
|
294
|
+
// Evict oldest if too many pages cached
|
|
295
|
+
if (this.pages.size >= 50) {
|
|
296
|
+
const oldest = this.pages.keys().next().value;
|
|
297
|
+
this.pages.delete(oldest);
|
|
298
|
+
}
|
|
289
299
|
this.pages.set(key, { chunks: mdChunks, raw: rawChunks, ts: Date.now() });
|
|
290
300
|
setTimeout(() => this.pages.delete(key), TelegramAdapter.PAGE_TTL);
|
|
291
301
|
const keyboard = this.pageKeyboard(chatId, msgId, 0, mdChunks.length);
|
|
@@ -325,25 +335,29 @@ export class TelegramAdapter {
|
|
|
325
335
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
326
336
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
327
337
|
await this.registerCommands();
|
|
338
|
+
let pollBackoff = 0;
|
|
328
339
|
while (this.running) {
|
|
329
340
|
try {
|
|
330
341
|
const ctrl = new AbortController();
|
|
331
|
-
const timer = setTimeout(() => ctrl.abort(),
|
|
332
|
-
const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=
|
|
342
|
+
const timer = setTimeout(() => ctrl.abort(), 30000);
|
|
343
|
+
const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=10`, { signal: ctrl.signal });
|
|
333
344
|
clearTimeout(timer);
|
|
334
345
|
const json = await res.json();
|
|
335
346
|
if (!json.ok) {
|
|
336
347
|
console.error("[telegram] poll error:", json);
|
|
337
348
|
continue;
|
|
338
349
|
}
|
|
350
|
+
pollBackoff = 0; // reset on success
|
|
339
351
|
for (const update of json.result) {
|
|
340
352
|
this.offset = update.update_id + 1;
|
|
341
353
|
this.handleUpdate(update).catch(e => console.error("[telegram] handler error:", e));
|
|
342
354
|
}
|
|
343
355
|
}
|
|
344
356
|
catch (err) {
|
|
345
|
-
|
|
346
|
-
|
|
357
|
+
pollBackoff = Math.min(pollBackoff + 1, 6);
|
|
358
|
+
const delay = Math.min(3000 * Math.pow(2, pollBackoff), 120000);
|
|
359
|
+
console.warn(`[telegram] poll error (retry in ${delay / 1000}s): ${err.cause?.code || err.message || "unknown"}`);
|
|
360
|
+
await new Promise(r => setTimeout(r, delay));
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
363
|
}
|
|
@@ -393,9 +407,8 @@ export class TelegramAdapter {
|
|
|
393
407
|
await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
394
408
|
try {
|
|
395
409
|
console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
: await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
|
|
410
|
+
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
411
|
+
const res = await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
|
|
399
412
|
if (res.timedOut) {
|
|
400
413
|
this.store.markTaskResult(task.id, "failed");
|
|
401
414
|
if (res.text)
|
package/dist/core/agent.js
CHANGED
|
@@ -333,6 +333,10 @@ export class AgentEngine {
|
|
|
333
333
|
args.push("--model", ep.model);
|
|
334
334
|
const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
335
335
|
child.stdin.end();
|
|
336
|
+
const killTimer = setTimeout(() => { try {
|
|
337
|
+
child.kill("SIGTERM");
|
|
338
|
+
}
|
|
339
|
+
catch { } }, 60000);
|
|
336
340
|
console.log(`[agent] auto-summary spawned pid=${child.pid} for ${userId}`);
|
|
337
341
|
let result = "";
|
|
338
342
|
let cost = 0;
|
|
@@ -359,6 +363,7 @@ export class AgentEngine {
|
|
|
359
363
|
});
|
|
360
364
|
child.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
361
365
|
child.on("close", (code) => {
|
|
366
|
+
clearTimeout(killTimer);
|
|
362
367
|
if (code !== 0) {
|
|
363
368
|
console.warn(`[agent] auto-summary failed code=${code} stderr=${stderr.slice(0, 200)} for ${userId}`);
|
|
364
369
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function loadConfig(path) {
|
|
|
10
10
|
endpoints: raw.endpoints || [],
|
|
11
11
|
agent: {
|
|
12
12
|
...raw.agent,
|
|
13
|
-
timeout_seconds: raw.agent?.timeout_seconds ??
|
|
13
|
+
timeout_seconds: raw.agent?.timeout_seconds ?? 0,
|
|
14
14
|
max_parallel: raw.agent?.max_parallel ?? 1,
|
|
15
15
|
memory: { enabled: true, auto_summary: true, max_memories: 50, ...raw.agent?.memory },
|
|
16
16
|
skill: { enabled: true, ...raw.agent?.skill },
|
package/dist/core/keys.js
CHANGED
|
@@ -8,6 +8,8 @@ export class EndpointRotator {
|
|
|
8
8
|
this.endpoints = endpoints.filter(e => e.api_key);
|
|
9
9
|
}
|
|
10
10
|
next() {
|
|
11
|
+
if (!this.endpoints.length)
|
|
12
|
+
throw new Error("No endpoints configured");
|
|
11
13
|
const now = Date.now();
|
|
12
14
|
const len = this.endpoints.length;
|
|
13
15
|
for (let i = 0; i < len; i++) {
|
package/dist/core/lock.js
CHANGED
|
@@ -43,11 +43,14 @@ export class UserLock {
|
|
|
43
43
|
}
|
|
44
44
|
async _acquireRedis(userId) {
|
|
45
45
|
const key = this.prefix + userId;
|
|
46
|
-
//
|
|
46
|
+
const maxWait = this.ttl * 1000 + 5000; // TTL + 5s grace
|
|
47
|
+
const start = Date.now();
|
|
47
48
|
while (true) {
|
|
48
49
|
const ok = await this.redis.set(key, "1", "EX", this.ttl, "NX");
|
|
49
50
|
if (ok)
|
|
50
51
|
break;
|
|
52
|
+
if (Date.now() - start > maxWait)
|
|
53
|
+
throw new Error(`Lock timeout for user ${userId}`);
|
|
51
54
|
await new Promise((r) => setTimeout(r, 500));
|
|
52
55
|
}
|
|
53
56
|
return async () => {
|
package/dist/core/store.js
CHANGED
|
@@ -70,10 +70,18 @@ export class Store {
|
|
|
70
70
|
catch { }
|
|
71
71
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
|
|
72
72
|
// Startup recovery: reset orphaned 'running' tasks back to 'auto' so they get re-executed
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
const orphaned = this.db.prepare("SELECT id, description FROM tasks WHERE status = 'running'").all();
|
|
74
|
+
for (const t of orphaned) {
|
|
75
|
+
const desc = t.description.startsWith("[recovered]") ? t.description : `[recovered] Check current state before making changes — previous attempt was interrupted. Original task: ${t.description}`;
|
|
76
|
+
this.db.prepare("UPDATE tasks SET status = 'auto', description = ? WHERE id = ?").run(desc, t.id);
|
|
76
77
|
}
|
|
78
|
+
if (orphaned.length > 0) {
|
|
79
|
+
console.log(`[store] recovered ${orphaned.length} orphaned running task(s) back to auto queue`);
|
|
80
|
+
}
|
|
81
|
+
// Startup cleanup: prune history/usage older than 30 days
|
|
82
|
+
const cutoff = Date.now() - 30 * 86400000;
|
|
83
|
+
this.db.prepare("DELETE FROM history WHERE created_at < ?").run(cutoff);
|
|
84
|
+
this.db.prepare("DELETE FROM usage WHERE created_at < ?").run(cutoff);
|
|
77
85
|
}
|
|
78
86
|
// --- sessions ---
|
|
79
87
|
getSession(userId) {
|
package/dist/ctl.js
CHANGED
|
@@ -16,6 +16,17 @@ function fail(msg) {
|
|
|
16
16
|
console.error(msg);
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
|
+
function extractFlag(parts, flag) {
|
|
20
|
+
// Search from end to avoid matching flag text inside description
|
|
21
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
22
|
+
if (parts[i] === flag) {
|
|
23
|
+
const val = parts[i + 1];
|
|
24
|
+
parts.splice(i, 2);
|
|
25
|
+
return val;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
19
30
|
if (category === "memory") {
|
|
20
31
|
if (action === "add") {
|
|
21
32
|
const [userId, ...contentParts] = rest;
|
|
@@ -102,21 +113,10 @@ else if (category === "auto") {
|
|
|
102
113
|
const [userId, platform, chatId, ...descParts] = rest;
|
|
103
114
|
if (!userId || !platform || !chatId || !descParts.length)
|
|
104
115
|
fail("Usage: auto add <user_id> <platform> <chat_id> <description> [--parent <id>]");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
parentId = parseInt(descParts[parentIdx + 1]);
|
|
110
|
-
descParts.splice(parentIdx, 2);
|
|
111
|
-
}
|
|
112
|
-
// Parse optional --delay flag
|
|
113
|
-
let scheduledAt = null;
|
|
114
|
-
const delayIdx = descParts.indexOf("--delay");
|
|
115
|
-
if (delayIdx !== -1 && descParts[delayIdx + 1]) {
|
|
116
|
-
const delayMin = parseInt(descParts[delayIdx + 1]);
|
|
117
|
-
scheduledAt = Date.now() + delayMin * 60000;
|
|
118
|
-
descParts.splice(delayIdx, 2);
|
|
119
|
-
}
|
|
116
|
+
const parentRaw = extractFlag(descParts, "--parent");
|
|
117
|
+
const parentId = parentRaw ? parseInt(parentRaw) : null;
|
|
118
|
+
const delayRaw = extractFlag(descParts, "--delay");
|
|
119
|
+
const scheduledAt = delayRaw ? Date.now() + parseInt(delayRaw) * 60000 : null;
|
|
120
120
|
const desc = descParts.join(" ");
|
|
121
121
|
const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, parent_id, scheduled_at, created_at) VALUES (?, ?, ?, ?, 'auto', ?, ?, ?)").run(userId, platform, chatId, desc, parentId, scheduledAt, Date.now());
|
|
122
122
|
output({ ok: true, id: Number(r.lastInsertRowid), scheduled_at: scheduledAt, message: scheduledAt ? `Auto task scheduled (in ${Math.ceil((scheduledAt - Date.now()) / 60000)} min)` : "Auto task queued" });
|
|
@@ -125,20 +125,10 @@ else if (category === "auto") {
|
|
|
125
125
|
const [userId, platform, chatId, ...descParts] = rest;
|
|
126
126
|
if (!userId || !platform || !chatId || !descParts.length)
|
|
127
127
|
fail("Usage: auto add-approval <user_id> <platform> <chat_id> <description> [--parent <id>] [--delay <minutes>]");
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
descParts.splice(parentIdx, 2);
|
|
133
|
-
}
|
|
134
|
-
// Parse optional --delay flag
|
|
135
|
-
let scheduledAt = null;
|
|
136
|
-
const delayIdx = descParts.indexOf("--delay");
|
|
137
|
-
if (delayIdx !== -1 && descParts[delayIdx + 1]) {
|
|
138
|
-
const delayMin = parseInt(descParts[delayIdx + 1]);
|
|
139
|
-
scheduledAt = Date.now() + delayMin * 60000;
|
|
140
|
-
descParts.splice(delayIdx, 2);
|
|
141
|
-
}
|
|
128
|
+
const parentRaw = extractFlag(descParts, "--parent");
|
|
129
|
+
const parentId = parentRaw ? parseInt(parentRaw) : null;
|
|
130
|
+
const delayRaw = extractFlag(descParts, "--delay");
|
|
131
|
+
const scheduledAt = delayRaw ? Date.now() + parseInt(delayRaw) * 60000 : null;
|
|
142
132
|
const desc = descParts.join(" ");
|
|
143
133
|
const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, parent_id, scheduled_at, created_at) VALUES (?, ?, ?, ?, 'approval_pending', ?, ?, ?)").run(userId, platform, chatId, desc, parentId, scheduledAt, Date.now());
|
|
144
134
|
output({ ok: true, id: Number(r.lastInsertRowid), scheduled_at: scheduledAt, message: scheduledAt ? `Auto task queued for approval (scheduled in ${Math.ceil((scheduledAt - Date.now()) / 60000)} min)` : "Auto task queued for approval" });
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,12 @@ async function main() {
|
|
|
57
57
|
try {
|
|
58
58
|
config = reloadConfig();
|
|
59
59
|
engine.reloadConfig(config);
|
|
60
|
+
for (const a of adapters) {
|
|
61
|
+
if ('reloadConfig' in a && typeof a.reloadConfig === 'function') {
|
|
62
|
+
const plat = a.constructor.name === 'TelegramAdapter' ? config.platforms.telegram : config.platforms.discord;
|
|
63
|
+
a.reloadConfig(plat, config.locale);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
60
66
|
console.log("[claudebridge] config reloaded (SIGHUP)");
|
|
61
67
|
}
|
|
62
68
|
catch (err) {
|
|
@@ -72,6 +78,12 @@ async function main() {
|
|
|
72
78
|
try {
|
|
73
79
|
config = reloadConfig();
|
|
74
80
|
engine.reloadConfig(config);
|
|
81
|
+
for (const a of adapters) {
|
|
82
|
+
if ('reloadConfig' in a && typeof a.reloadConfig === 'function') {
|
|
83
|
+
const plat = a.constructor.name === 'TelegramAdapter' ? config.platforms.telegram : config.platforms.discord;
|
|
84
|
+
a.reloadConfig(plat, config.locale);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
75
87
|
console.log("[claudebridge] config reloaded");
|
|
76
88
|
}
|
|
77
89
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emqo/claudebridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|