@emqo/claudebridge 0.8.0 → 0.9.1

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/index.js CHANGED
@@ -8,10 +8,13 @@ import { AgentEngine } from "./core/agent.js";
8
8
  import { TelegramAdapter } from "./adapters/telegram.js";
9
9
  import { DiscordAdapter } from "./adapters/discord.js";
10
10
  import { WebhookServer } from "./webhook.js";
11
+ import { log, setLogLevel } from "./core/logger.js";
11
12
  async function main() {
12
13
  const _cfgIdx = process.argv.indexOf("--config");
13
14
  const _cfgPath = _cfgIdx !== -1 ? process.argv[_cfgIdx + 1] : undefined;
14
15
  let config = loadConfig(_cfgPath);
16
+ if (config.log_level)
17
+ setLogLevel(config.log_level);
15
18
  // Derive DB path from config file directory (not CWD)
16
19
  const configDir = _cfgPath ? dirname(resolve(_cfgPath)) : process.cwd();
17
20
  const dbPath = join(configDir, "data", "claudebridge.db");
@@ -21,20 +24,20 @@ async function main() {
21
24
  let webhookServer = null;
22
25
  if (config.platforms.telegram.enabled) {
23
26
  if (!config.platforms.telegram.token) {
24
- console.error("[fatal] TELEGRAM_BOT_TOKEN not set");
27
+ log.error("TELEGRAM_BOT_TOKEN not set");
25
28
  process.exit(1);
26
29
  }
27
30
  adapters.push(new TelegramAdapter(engine, store, config.platforms.telegram, config.locale));
28
31
  }
29
32
  if (config.platforms.discord.enabled) {
30
33
  if (!config.platforms.discord.token) {
31
- console.error("[fatal] DISCORD_BOT_TOKEN not set");
34
+ log.error("DISCORD_BOT_TOKEN not set");
32
35
  process.exit(1);
33
36
  }
34
37
  adapters.push(new DiscordAdapter(engine, store, config.platforms.discord, config.locale));
35
38
  }
36
39
  if (!adapters.length) {
37
- console.error("[fatal] no platform enabled");
40
+ log.error("no platform enabled");
38
41
  process.exit(1);
39
42
  }
40
43
  // Start webhook server if enabled
@@ -44,15 +47,24 @@ async function main() {
44
47
  }
45
48
  // --- Register signal handlers and hot-reload BEFORE starting adapters ---
46
49
  const shutdown = () => {
47
- console.log("[claudebridge] shutting down...");
50
+ log.info("shutting down...");
48
51
  for (const a of adapters)
49
52
  a.stop();
50
53
  if (webhookServer)
51
54
  webhookServer.stop();
55
+ store.close();
52
56
  setTimeout(() => process.exit(0), 1000);
53
57
  };
54
58
  process.on("SIGINT", shutdown);
55
59
  process.on("SIGTERM", shutdown);
60
+ process.on("uncaughtException", (err) => {
61
+ log.error("uncaught exception", { error: err.message, stack: err.stack });
62
+ shutdown();
63
+ });
64
+ process.on("unhandledRejection", (reason) => {
65
+ const msg = reason instanceof Error ? reason.message : String(reason);
66
+ log.error("unhandled rejection", { error: msg });
67
+ });
56
68
  process.on("SIGHUP", () => {
57
69
  try {
58
70
  config = reloadConfig();
@@ -63,10 +75,10 @@ async function main() {
63
75
  a.reloadConfig(plat, config.locale);
64
76
  }
65
77
  }
66
- console.log("[claudebridge] config reloaded (SIGHUP)");
78
+ log.info("config reloaded (SIGHUP)");
67
79
  }
68
80
  catch (err) {
69
- console.error("[claudebridge] config reload failed:", err);
81
+ log.error("config reload failed", { error: err?.message });
70
82
  }
71
83
  });
72
84
  // Hot reload config.yaml on file change
@@ -84,23 +96,28 @@ async function main() {
84
96
  a.reloadConfig(plat, config.locale);
85
97
  }
86
98
  }
87
- console.log("[claudebridge] config reloaded");
99
+ log.info("config reloaded");
88
100
  }
89
101
  catch (err) {
90
- console.error("[claudebridge] config reload failed:", err);
102
+ log.error("config reload failed", { error: err?.message });
91
103
  }
92
104
  }, 500); // debounce
93
105
  });
94
- // --- Start adapters (fire-and-forget, they run infinite polling loops) ---
106
+ // --- Start adapters with crash recovery ---
95
107
  for (const a of adapters) {
96
108
  a.start().catch(err => {
97
- console.error("[fatal] adapter crashed:", err);
98
- process.exit(1);
109
+ log.error("adapter crashed, retry in 10s", { adapter: a.constructor.name, error: err?.message });
110
+ setTimeout(() => {
111
+ a.start().catch(err2 => {
112
+ log.error("adapter restart failed, exiting", { error: err2?.message });
113
+ process.exit(1);
114
+ });
115
+ }, 10000);
99
116
  });
100
117
  }
101
- console.log(`[claudebridge] running with ${adapters.length} adapter(s)`);
118
+ log.info("running", { adapters: adapters.length });
102
119
  }
103
120
  main().catch((err) => {
104
- console.error("[fatal]", err);
121
+ log.error("fatal", { error: err?.message });
105
122
  process.exit(1);
106
123
  });
@@ -0,0 +1,26 @@
1
+ export interface ProviderStreamEvent {
2
+ type: "session_init" | "text_chunk" | "result" | "unknown";
3
+ sessionId?: string;
4
+ text?: string;
5
+ cost?: number;
6
+ isError?: boolean;
7
+ }
8
+ export interface ProviderExecOpts {
9
+ prompt: string;
10
+ model: string;
11
+ resumeSessionId?: string;
12
+ systemPrompt?: string;
13
+ appendSystemPrompt?: string;
14
+ allowedTools?: string[];
15
+ maxTurns?: number;
16
+ maxBudgetUsd?: number;
17
+ permissionMode?: string;
18
+ }
19
+ export interface Provider {
20
+ readonly binary: string;
21
+ readonly supportsSessionResume: boolean;
22
+ readonly supportsAppendSystemPrompt: boolean;
23
+ buildArgs(opts: ProviderExecOpts): string[];
24
+ buildEnv(extra: Record<string, string>): Record<string, string>;
25
+ parseLine(line: string): ProviderStreamEvent;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { Provider, ProviderExecOpts, ProviderStreamEvent } from "./base.js";
2
+ export declare class ClaudeProvider implements Provider {
3
+ readonly binary = "claude";
4
+ readonly supportsSessionResume = true;
5
+ readonly supportsAppendSystemPrompt = true;
6
+ buildArgs(opts: ProviderExecOpts): string[];
7
+ buildEnv(extra: Record<string, string>): Record<string, string>;
8
+ parseLine(line: string): ProviderStreamEvent;
9
+ }
@@ -0,0 +1,53 @@
1
+ export class ClaudeProvider {
2
+ binary = "claude";
3
+ supportsSessionResume = true;
4
+ supportsAppendSystemPrompt = true;
5
+ buildArgs(opts) {
6
+ const args = ["-p", opts.prompt, "--verbose", "--output-format", "stream-json"];
7
+ if (opts.permissionMode)
8
+ args.push("--permission-mode", opts.permissionMode);
9
+ if (opts.model)
10
+ args.push("--model", opts.model);
11
+ if (opts.resumeSessionId)
12
+ args.push("-r", opts.resumeSessionId);
13
+ if (opts.systemPrompt)
14
+ args.push("--system-prompt", opts.systemPrompt);
15
+ if (opts.appendSystemPrompt)
16
+ args.push("--append-system-prompt", opts.appendSystemPrompt);
17
+ if (opts.allowedTools?.length)
18
+ args.push("--allowed-tools", opts.allowedTools.join(","));
19
+ if (opts.maxTurns)
20
+ args.push("--max-turns", String(opts.maxTurns));
21
+ if (opts.maxBudgetUsd)
22
+ args.push("--max-budget-usd", String(opts.maxBudgetUsd));
23
+ return args;
24
+ }
25
+ buildEnv(extra) {
26
+ return { ...process.env, ...extra };
27
+ }
28
+ parseLine(line) {
29
+ try {
30
+ const msg = JSON.parse(line);
31
+ if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
32
+ return { type: "session_init", sessionId: msg.session_id };
33
+ }
34
+ if (msg.type === "assistant" && msg.message?.content) {
35
+ const texts = msg.message.content
36
+ .filter((b) => b.type === "text" && b.text)
37
+ .map((b) => b.text);
38
+ if (texts.length)
39
+ return { type: "text_chunk", text: texts.join("") };
40
+ }
41
+ if (msg.type === "result") {
42
+ return {
43
+ type: "result",
44
+ text: msg.result || undefined,
45
+ cost: msg.total_cost_usd || undefined,
46
+ isError: msg.is_error || false,
47
+ };
48
+ }
49
+ }
50
+ catch { }
51
+ return { type: "unknown" };
52
+ }
53
+ }
@@ -0,0 +1,9 @@
1
+ import type { Provider, ProviderExecOpts, ProviderStreamEvent } from "./base.js";
2
+ export declare class CodexProvider implements Provider {
3
+ readonly binary = "codex";
4
+ readonly supportsSessionResume = false;
5
+ readonly supportsAppendSystemPrompt = false;
6
+ buildArgs(opts: ProviderExecOpts): string[];
7
+ buildEnv(extra: Record<string, string>): Record<string, string>;
8
+ parseLine(line: string): ProviderStreamEvent;
9
+ }
@@ -0,0 +1,35 @@
1
+ export class CodexProvider {
2
+ binary = "codex";
3
+ supportsSessionResume = false;
4
+ supportsAppendSystemPrompt = false;
5
+ buildArgs(opts) {
6
+ const prompt = opts.appendSystemPrompt
7
+ ? `[System Context]\n${opts.appendSystemPrompt}\n\n[User Message]\n${opts.prompt}`
8
+ : opts.prompt;
9
+ const args = ["exec", prompt, "--json", "--dangerously-bypass-approvals-and-sandbox"];
10
+ if (opts.model)
11
+ args.push("-m", opts.model);
12
+ return args;
13
+ }
14
+ buildEnv(extra) {
15
+ return { ...process.env, ...extra };
16
+ }
17
+ parseLine(line) {
18
+ try {
19
+ const msg = JSON.parse(line);
20
+ if (msg.type === "thread.started" && msg.thread_id) {
21
+ return { type: "session_init", sessionId: msg.thread_id };
22
+ }
23
+ if (msg.type === "item.completed" && msg.item?.type === "agent_message" && msg.item.text) {
24
+ return { type: "text_chunk", text: msg.item.text };
25
+ }
26
+ if (msg.type === "turn.completed") {
27
+ const usage = msg.usage || {};
28
+ const cost = ((usage.input_tokens || 0) * 0.000003 + (usage.output_tokens || 0) * 0.000012);
29
+ return { type: "result", cost: cost || undefined };
30
+ }
31
+ }
32
+ catch { }
33
+ return { type: "unknown" };
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { Provider } from "./base.js";
2
+ export declare function getProvider(name: string): Provider;
@@ -0,0 +1,12 @@
1
+ import { ClaudeProvider } from "./claude.js";
2
+ import { CodexProvider } from "./codex.js";
3
+ const providers = new Map([
4
+ ["claude", new ClaudeProvider()],
5
+ ["codex", new CodexProvider()],
6
+ ]);
7
+ export function getProvider(name) {
8
+ const p = providers.get(name);
9
+ if (!p)
10
+ throw new Error(`Unknown provider: ${name}. Available: ${[...providers.keys()].join(", ")}`);
11
+ return p;
12
+ }
@@ -3,5 +3,7 @@ export interface SkillContext {
3
3
  chatId: string;
4
4
  platform: string;
5
5
  locale: string;
6
+ subSessionId?: string;
7
+ subSessionLabel?: string;
6
8
  }
7
9
  export declare function generateSkillDoc(ctx: SkillContext): string;
@@ -11,6 +11,7 @@ export function generateSkillDoc(ctx) {
11
11
  ``,
12
12
  `你正在 ClaudeBridge 中运行,连接着 ${ctx.platform} 平台。`,
13
13
  `当前用户 ID: ${ctx.userId} | 聊天 ID: ${ctx.chatId} | 平台: ${ctx.platform}`,
14
+ ...(ctx.subSessionId ? [`当前子会话: ${ctx.subSessionId.slice(0, 8)} (话题: "${ctx.subSessionLabel || ""}")`] : []),
14
15
  ``,
15
16
  `你可以通过 Bash 工具调用以下命令来管理用户的记忆、任务、提醒和自动任务:`,
16
17
  ``,
@@ -120,6 +121,7 @@ export function generateSkillDoc(ctx) {
120
121
  ``,
121
122
  `You are running inside ClaudeBridge, connected to the ${ctx.platform} platform.`,
122
123
  `Current user ID: ${ctx.userId} | Chat ID: ${ctx.chatId} | Platform: ${ctx.platform}`,
124
+ ...(ctx.subSessionId ? [`Current sub-session: ${ctx.subSessionId.slice(0, 8)} (topic: "${ctx.subSessionLabel || ""}")`] : []),
123
125
  ``,
124
126
  `You can use the Bash tool to call these commands to manage the user's memories, tasks, reminders, and auto-tasks:`,
125
127
  ``,
package/dist/webhook.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
+ import { log as rootLog } from "./core/logger.js";
4
+ const log = rootLog.child("webhook");
3
5
  export class WebhookServer {
4
6
  store;
5
7
  config;
@@ -14,7 +16,7 @@ export class WebhookServer {
14
16
  start() {
15
17
  this.server = createServer((req, res) => this.handleRequest(req, res));
16
18
  this.server.listen(this.config.port, () => {
17
- console.log(`[webhook] HTTP server listening on port ${this.config.port}`);
19
+ log.info("HTTP server listening", { port: this.config.port });
18
20
  });
19
21
  // Start cron schedulers
20
22
  for (const entry of this.cronEntries) {
@@ -22,14 +24,14 @@ export class WebhookServer {
22
24
  const timer = setInterval(() => {
23
25
  try {
24
26
  const id = this.store.addTask(entry.user_id, entry.platform, entry.chat_id, entry.description, undefined, true);
25
- console.log(`[cron] created auto-task #${id}: ${entry.description}`);
27
+ log.info("cron created auto-task", { id, description: entry.description });
26
28
  }
27
29
  catch (e) {
28
- console.error(`[cron] failed to create task:`, e);
30
+ log.error("cron failed to create task", { error: e?.message });
29
31
  }
30
32
  }, ms);
31
33
  this.cronTimers.push(timer);
32
- console.log(`[cron] scheduled every ${entry.schedule_minutes}min: ${entry.description}`);
34
+ log.info("cron scheduled", { minutes: entry.schedule_minutes, description: entry.description });
33
35
  }
34
36
  }
35
37
  stop() {
@@ -97,7 +99,7 @@ export class WebhookServer {
97
99
  const event = req.headers["x-github-event"] || "unknown";
98
100
  const description = this.buildGitHubDescription(event, payload);
99
101
  const id = this.store.addTask(userId, platform, chatId, description, undefined, true);
100
- console.log(`[webhook] github ${event} auto-task #${id}`);
102
+ log.info("github webhook", { event, taskId: id });
101
103
  this.json(res, 201, { ok: true, id, event });
102
104
  }
103
105
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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": {
@@ -11,7 +11,9 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "start": "node dist/index.js",
14
- "dev": "tsx src/index.ts"
14
+ "dev": "tsx src/index.ts",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
15
17
  },
16
18
  "keywords": [
17
19
  "claude",
@@ -40,14 +42,16 @@
40
42
  "discord.js": "^14.25.1",
41
43
  "dotenv": "^17.3.1",
42
44
  "ioredis": "^5.9.3",
43
- "yaml": "^2.8.2"
45
+ "yaml": "^2.8.2",
46
+ "zod": "^4.3.6"
44
47
  },
45
48
  "devDependencies": {
46
49
  "@types/better-sqlite3": "^7.6.13",
47
50
  "@types/ioredis": "^4.28.10",
48
51
  "@types/node": "^25.3.0",
49
52
  "tsx": "^4.21.0",
50
- "typescript": "^5.9.3"
53
+ "typescript": "^5.9.3",
54
+ "vitest": "^4.0.18"
51
55
  },
52
56
  "overrides": {
53
57
  "undici": ">=6.23.0"