@emqo/claudebridge 0.1.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/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # ClaudeBridge
2
+
3
+ <p align="center">
4
+ <a href="#english">English</a> | <a href="#中文">中文</a>
5
+ </p>
6
+
7
+ ---
8
+
9
+ <a name="english"></a>
10
+
11
+ Bridge the `claude` CLI to chat platforms (Telegram, Discord). Spawns `claude` as a subprocess with `--output-format stream-json` -- no SDK dependency, just raw CLI power.
12
+
13
+ ## Features
14
+
15
+ - **Multi-platform**: Telegram (raw Bot API long polling) + Discord (discord.js)
16
+ - **Streaming responses**: Real-time message editing as Claude thinks
17
+ - **Multi-endpoint rotation**: Round-robin with auto-cooldown on 429/401/529
18
+ - **Session persistence**: SQLite-backed session resume via `-r <session_id>`
19
+ - **Per-user workspace isolation**: Each user gets their own working directory
20
+ - **Memory system**: Manual + auto-summary memories per user
21
+ - **Task & reminder system**: Create tasks, set timed reminders
22
+ - **Natural language intent detection**: Say "remind me in 5 min to check server" — no commands needed
23
+ - Regex-first (zero cost, zero latency) + Claude fallback (~$0.005/call)
24
+ - **Auto tasks**: Queue background tasks that execute when idle
25
+ - **File uploads**: Send files to Claude for analysis
26
+ - **Access control**: User/group whitelist
27
+ - **Hot reload**: Edit `config.yaml`, changes apply instantly
28
+ - **i18n**: English + Chinese
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # Install
34
+ npm install
35
+
36
+ # Configure
37
+ cp config.yaml.example config.yaml # or edit config.yaml directly
38
+ # Set your endpoints, tokens, and access whitelist
39
+
40
+ # Build & run
41
+ npm run build
42
+ npm start
43
+
44
+ # Or dev mode (hot reload)
45
+ npm run dev
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ All config lives in `config.yaml`:
51
+
52
+ ```yaml
53
+ endpoints:
54
+ - name: "my-endpoint"
55
+ base_url: "" # optional, for proxies
56
+ api_key: "sk-..."
57
+ model: "claude-sonnet-4-20250514"
58
+
59
+ locale: en # "zh" for Chinese
60
+
61
+ agent:
62
+ allowed_tools: [Read, Edit, Bash, Grep, Glob, WebSearch, WebFetch]
63
+ permission_mode: "acceptEdits"
64
+ max_turns: 50
65
+ max_budget_usd: 2.0
66
+ system_prompt: ""
67
+ timeout_seconds: 300
68
+ memory:
69
+ enabled: true
70
+ auto_summary: true
71
+ max_memories: 50
72
+ intent:
73
+ enabled: true # natural language intent detection
74
+ use_claude_fallback: true # use Claude when regex doesn't match
75
+
76
+ workspace:
77
+ base_dir: "./workspaces"
78
+ isolation: true
79
+
80
+ access:
81
+ allowed_users: ["your_user_id"]
82
+ allowed_groups: []
83
+
84
+ platforms:
85
+ telegram:
86
+ enabled: true
87
+ token: "your-bot-token"
88
+ chunk_size: 4000
89
+ discord:
90
+ enabled: false
91
+ token: ""
92
+ chunk_size: 1900
93
+ ```
94
+
95
+ Environment variables (`.env`) work as fallback for single-endpoint setup.
96
+
97
+ ## Commands
98
+
99
+ | Telegram | Discord | Action |
100
+ |----------|---------|--------|
101
+ | /new | !new | Clear session |
102
+ | /usage | !usage | Your usage stats |
103
+ | /allusage | !allusage | All users stats |
104
+ | /history | !history | Recent conversations |
105
+ | /model | !model | Endpoint info |
106
+ | /reload | !reload | Hot reload config |
107
+ | /remember | !remember | Save a memory |
108
+ | /memories | !memories | List memories |
109
+ | /forget | !forget | Clear all memories |
110
+ | /task | !task | Add a task |
111
+ | /tasks | !tasks | List pending tasks |
112
+ | /done | !done | Complete a task |
113
+ | /remind | !remind | Set a timed reminder |
114
+ | /auto | !auto | Queue auto task |
115
+ | /autotasks | !autotasks | List auto tasks |
116
+ | /cancelauto | !cancelauto | Cancel auto task |
117
+
118
+ ## Natural Language Intent
119
+
120
+ No need to type commands — just talk naturally:
121
+
122
+ | Say this | Detected as |
123
+ |----------|-------------|
124
+ | "remind me 5 min later check server" | Reminder: 5min, check server |
125
+ | "remind me 10 minutes later to deploy" | Reminder: 10min, deploy |
126
+ | "add task buy groceries" | Task: buy groceries |
127
+ | "create task: fix login bug" | Task: fix login bug |
128
+ | "remember I prefer dark mode" | Memory: I prefer dark mode |
129
+ | "forget all" | Clear all memories |
130
+ | "new session" | Clear session |
131
+
132
+ Regex matches first (instant, free). If no match and `use_claude_fallback` is enabled, a low-budget Claude call classifies the intent.
133
+
134
+ ## Architecture
135
+
136
+ ```
137
+ src/index.ts Entry point, config loading, hot reload
138
+ src/core/
139
+ agent.ts Claude CLI subprocess spawner with retry & rotation
140
+ config.ts YAML config with env fallback
141
+ intent.ts Hybrid intent detection (regex + Claude)
142
+ keys.ts Endpoint round-robin with cooldown
143
+ lock.ts Per-user concurrency mutex
144
+ store.ts SQLite (WAL): sessions, usage, history, memories, tasks
145
+ permissions.ts Whitelist access control
146
+ markdown.ts Markdown → Telegram MarkdownV2
147
+ i18n.ts Internationalization (en/zh)
148
+ src/adapters/
149
+ base.ts Adapter interface
150
+ telegram.ts Telegram Bot API (raw fetch, long polling)
151
+ discord.ts Discord.js (@mentions + DMs)
152
+ ```
153
+
154
+ ## Prerequisites
155
+
156
+ - Node.js 18+
157
+ - `claude` CLI installed and authenticated
158
+ - Telegram bot token (from @BotFather) and/or Discord bot token
159
+
160
+ ## License
161
+
162
+ MIT
163
+
164
+ ---
165
+
166
+ <a name="中文"></a>
167
+
168
+ # ClaudeBridge
169
+
170
+ 将 `claude` CLI 桥接到聊天平台(Telegram、Discord)。通过子进程方式调用 `claude --output-format stream-json`,无需 SDK,直接使用 CLI。
171
+
172
+ ## 功能特性
173
+
174
+ - **多平台**:Telegram(原生 Bot API 长轮询)+ Discord(discord.js)
175
+ - **流式响应**:Claude 思考时实时编辑消息
176
+ - **多端点轮转**:Round-robin,429/401/529 自动冷却切换
177
+ - **会话持久化**:SQLite 存储,通过 `-r <session_id>` 恢复会话
178
+ - **用户工作区隔离**:每个用户独立工作目录
179
+ - **记忆系统**:手动 + 自动摘要记忆
180
+ - **任务与提醒**:创建任务、设置定时提醒
181
+ - **自然语言意图识别**:直接说"提醒我5分钟后检查服务器",无需打命令
182
+ - 正则优先(零成本零延迟)+ Claude 兜底(~$0.005/次)
183
+ - **自动任务**:排队后台任务,空闲时自动执行
184
+ - **文件上传**:发送文件给 Claude 分析
185
+ - **访问控制**:用户/群组白名单
186
+ - **热重载**:编辑 `config.yaml` 即时生效
187
+ - **国际化**:英文 + 中文
188
+
189
+ ## 快速开始
190
+
191
+ ```bash
192
+ npm install
193
+ # 编辑 config.yaml,配置端点、Token、白名单
194
+ npm run build && npm start
195
+ # 或开发模式
196
+ npm run dev
197
+ ```
198
+
199
+ ## 自然语言意图
200
+
201
+ 无需输入命令,直接自然对话:
202
+
203
+ | 你说 | 识别为 |
204
+ |------|--------|
205
+ | "提醒我5分钟后检查服务器" | 提醒:5分钟后,检查服务器 |
206
+ | "remind me 10 min later to deploy" | 提醒:10分钟后,部署 |
207
+ | "添加任务买牛奶" | 任务:买牛奶 |
208
+ | "记住我喜欢TypeScript" | 记忆:我喜欢TypeScript |
209
+ | "忘记所有" | 清除所有记忆 |
210
+ | "新会话" | 清除会话 |
211
+
212
+ 正则优先匹配(即时、免费)。未匹配且 `use_claude_fallback` 开启时,用低预算 Claude 调用分类意图。
213
+
214
+ ## 前置要求
215
+
216
+ - Node.js 18+
217
+ - `claude` CLI 已安装并认证
218
+ - Telegram bot token(从 @BotFather 获取)和/或 Discord bot token
219
+
220
+ ## 许可证
221
+
222
+ MIT
223
+
224
+ ---
225
+
226
+ ## Star History
227
+
228
+ [![Star History Chart](https://api.star-history.com/svg?repos=Emqo/ClaudeBridge&type=Date)](https://star-history.com/#Emqo/ClaudeBridge&Date)
@@ -0,0 +1,52 @@
1
+ endpoints:
2
+ - name: "default"
3
+ base_url: ""
4
+ api_key: "sk-your-api-key"
5
+ model: "claude-sonnet-4-20250514"
6
+
7
+ locale: en # "zh" for Chinese
8
+
9
+ agent:
10
+ allowed_tools:
11
+ - Read
12
+ - Edit
13
+ - Bash
14
+ - Grep
15
+ - Glob
16
+ - WebSearch
17
+ - WebFetch
18
+ permission_mode: "acceptEdits"
19
+ max_turns: 50
20
+ max_budget_usd: 2.0
21
+ system_prompt: ""
22
+ cwd: ""
23
+ timeout_seconds: 300
24
+ memory:
25
+ enabled: true
26
+ auto_summary: true
27
+ max_memories: 50
28
+ intent:
29
+ enabled: true
30
+ use_claude_fallback: true
31
+
32
+ workspace:
33
+ base_dir: "./workspaces"
34
+ isolation: true
35
+
36
+ access:
37
+ allowed_users: []
38
+ allowed_groups: []
39
+
40
+ redis:
41
+ enabled: false
42
+ url: ""
43
+
44
+ platforms:
45
+ telegram:
46
+ enabled: true
47
+ token: "your-telegram-bot-token"
48
+ chunk_size: 4000
49
+ discord:
50
+ enabled: false
51
+ token: ""
52
+ chunk_size: 1900
@@ -0,0 +1,12 @@
1
+ export interface MessageContext {
2
+ userId: string;
3
+ text: string;
4
+ platform: string;
5
+ reply: (text: string) => Promise<void>;
6
+ }
7
+ export interface Adapter {
8
+ start(): Promise<void>;
9
+ stop(): void;
10
+ }
11
+ /** Split long text into chunks respecting newlines */
12
+ export declare function chunkText(text: string, maxLen: number): string[];
@@ -0,0 +1,19 @@
1
+ /** Split long text into chunks respecting newlines */
2
+ export function chunkText(text, maxLen) {
3
+ if (text.length <= maxLen)
4
+ return [text];
5
+ const chunks = [];
6
+ let remaining = text;
7
+ while (remaining.length > 0) {
8
+ if (remaining.length <= maxLen) {
9
+ chunks.push(remaining);
10
+ break;
11
+ }
12
+ let cut = remaining.lastIndexOf("\n", maxLen);
13
+ if (cut <= 0)
14
+ cut = maxLen;
15
+ chunks.push(remaining.slice(0, cut));
16
+ remaining = remaining.slice(cut).replace(/^\n/, "");
17
+ }
18
+ return chunks;
19
+ }
@@ -0,0 +1,18 @@
1
+ import { Adapter } from "./base.js";
2
+ import { AgentEngine } from "../core/agent.js";
3
+ import { Store } from "../core/store.js";
4
+ import { DiscordConfig } from "../core/config.js";
5
+ export declare class DiscordAdapter implements Adapter {
6
+ private engine;
7
+ private store;
8
+ private config;
9
+ private locale;
10
+ private client;
11
+ private reminderTimer?;
12
+ constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
13
+ private setup;
14
+ private handlePrompt;
15
+ start(): Promise<void>;
16
+ stop(): void;
17
+ private checkReminders;
18
+ }
@@ -0,0 +1,313 @@
1
+ import { Client, GatewayIntentBits } from "discord.js";
2
+ import { writeFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { chunkText } from "./base.js";
5
+ import { reloadConfig } from "../core/config.js";
6
+ import { t } from "../core/i18n.js";
7
+ import { detectIntent } from "../core/intent.js";
8
+ const EDIT_INTERVAL = 1500;
9
+ export class DiscordAdapter {
10
+ engine;
11
+ store;
12
+ config;
13
+ locale;
14
+ client;
15
+ reminderTimer;
16
+ constructor(engine, store, config, locale = "en") {
17
+ this.engine = engine;
18
+ this.store = store;
19
+ this.config = config;
20
+ this.locale = locale;
21
+ this.client = new Client({
22
+ intents: [
23
+ GatewayIntentBits.Guilds,
24
+ GatewayIntentBits.GuildMessages,
25
+ GatewayIntentBits.MessageContent,
26
+ GatewayIntentBits.DirectMessages,
27
+ ],
28
+ });
29
+ this.setup();
30
+ }
31
+ setup() {
32
+ this.client.on("messageCreate", async (msg) => {
33
+ if (msg.author.bot)
34
+ return;
35
+ const isDM = !msg.guild;
36
+ const isMentioned = msg.mentions.has(this.client.user);
37
+ if (!isDM && !isMentioned)
38
+ return;
39
+ const groupId = msg.guild ? String(msg.guild.id) : undefined;
40
+ if (!this.engine.access.isAllowed(msg.author.id, groupId))
41
+ return;
42
+ const text = msg.content.replace(/<@!?\d+>/g, "").trim();
43
+ // Commands
44
+ if (text === "!help") {
45
+ await msg.reply(t(this.locale, "help").replaceAll("/", "!"));
46
+ return;
47
+ }
48
+ if (text === "!new") {
49
+ this.store.clearSession(msg.author.id);
50
+ await msg.reply(t(this.locale, "session_cleared"));
51
+ return;
52
+ }
53
+ if (text === "!usage") {
54
+ const u = this.store.getUsage(msg.author.id);
55
+ await msg.reply(`Requests: ${u.count}\nTotal cost: $${u.total_cost.toFixed(4)}`);
56
+ return;
57
+ }
58
+ if (text === "!allusage") {
59
+ const rows = this.store.getUsageAll();
60
+ if (!rows.length) {
61
+ await msg.reply(t(this.locale, "no_usage"));
62
+ return;
63
+ }
64
+ const out = rows.map((r) => `${r.user_id}: ${r.count} reqs, $${r.total_cost.toFixed(4)}`).join("\n");
65
+ await msg.reply(out);
66
+ return;
67
+ }
68
+ if (text === "!history") {
69
+ const rows = this.store.getHistory(msg.author.id, 5);
70
+ if (!rows.length) {
71
+ await msg.reply(t(this.locale, "no_history"));
72
+ return;
73
+ }
74
+ const out = rows.reverse().map((r) => {
75
+ const ts = new Date(r.created_at).toLocaleString();
76
+ return `[${ts}] ${r.role}: ${r.content.slice(0, 150)}`;
77
+ }).join("\n\n");
78
+ await msg.reply(out);
79
+ return;
80
+ }
81
+ if (text === "!model") {
82
+ const eps = this.engine.getEndpoints();
83
+ const out = `Endpoints (${eps.length}):\n` +
84
+ eps.map((e) => `• ${e.name}: ${e.model || "default"}`).join("\n");
85
+ await msg.reply(out);
86
+ return;
87
+ }
88
+ if (text === "!reload") {
89
+ try {
90
+ const c = reloadConfig();
91
+ this.engine.reloadConfig(c);
92
+ this.locale = c.locale;
93
+ await msg.reply(t(this.locale, "config_reloaded"));
94
+ }
95
+ catch (err) {
96
+ await msg.reply(t(this.locale, "reload_failed") + err.message);
97
+ }
98
+ return;
99
+ }
100
+ if (text.startsWith("!remember ")) {
101
+ const content = text.slice(10).trim();
102
+ if (!content) {
103
+ await msg.reply(t(this.locale, "usage_remember").replace("/", "!"));
104
+ return;
105
+ }
106
+ this.store.addMemory(msg.author.id, content);
107
+ await msg.reply(t(this.locale, "memory_saved"));
108
+ return;
109
+ }
110
+ if (text === "!memories") {
111
+ const mems = this.store.getMemories(msg.author.id);
112
+ if (!mems.length) {
113
+ await msg.reply(t(this.locale, "no_memories"));
114
+ return;
115
+ }
116
+ await msg.reply(mems.map(m => `[${m.source}] ${m.content}`).join("\n\n"));
117
+ return;
118
+ }
119
+ if (text === "!forget") {
120
+ this.store.clearMemories(msg.author.id);
121
+ await msg.reply(t(this.locale, "memories_cleared"));
122
+ return;
123
+ }
124
+ if (text.startsWith("!task ")) {
125
+ const desc = text.slice(6).trim();
126
+ if (!desc) {
127
+ await msg.reply(t(this.locale, "usage_task").replace("/", "!"));
128
+ return;
129
+ }
130
+ const id = this.store.addTask(msg.author.id, "discord", String(msg.channelId), desc);
131
+ await msg.reply(t(this.locale, "task_added", { id }));
132
+ return;
133
+ }
134
+ if (text === "!tasks") {
135
+ const tasks = this.store.getTasks(msg.author.id);
136
+ if (!tasks.length) {
137
+ await msg.reply(t(this.locale, "no_tasks"));
138
+ return;
139
+ }
140
+ await msg.reply(tasks.map(tk => `#${tk.id} ${tk.description}${tk.remind_at ? ` ⏰${new Date(tk.remind_at).toLocaleString()}` : ""}`).join("\n"));
141
+ return;
142
+ }
143
+ if (text.startsWith("!done ")) {
144
+ const id = parseInt(text.slice(6).trim());
145
+ if (isNaN(id)) {
146
+ await msg.reply(t(this.locale, "usage_done").replace("/", "!"));
147
+ return;
148
+ }
149
+ const ok = this.store.completeTask(id, msg.author.id);
150
+ await msg.reply(ok ? t(this.locale, "task_done", { id }) : t(this.locale, "task_not_found", { id }));
151
+ return;
152
+ }
153
+ if (text.startsWith("!remind ")) {
154
+ const match = text.match(/^!remind\s+(\d+)m\s+(.+)$/);
155
+ if (!match) {
156
+ await msg.reply(t(this.locale, "usage_remind").replace("/", "!"));
157
+ return;
158
+ }
159
+ const mins = parseInt(match[1]);
160
+ const desc = match[2].trim();
161
+ const remindAt = Date.now() + mins * 60000;
162
+ const id = this.store.addTask(msg.author.id, "discord", String(msg.channelId), desc, remindAt);
163
+ await msg.reply(t(this.locale, "reminder_set", { id, mins }));
164
+ return;
165
+ }
166
+ if (text.startsWith("!auto ")) {
167
+ const desc = text.slice(6).trim();
168
+ if (!desc) {
169
+ await msg.reply(t(this.locale, "usage_auto").replace("/", "!"));
170
+ return;
171
+ }
172
+ const id = this.store.addTask(msg.author.id, "discord", String(msg.channelId), desc, undefined, true);
173
+ await msg.reply(t(this.locale, "auto_queued", { id }));
174
+ return;
175
+ }
176
+ if (text === "!autotasks") {
177
+ const all = this.store.getAutoTasks(msg.author.id);
178
+ if (!all.length) {
179
+ await msg.reply(t(this.locale, "no_auto_tasks"));
180
+ return;
181
+ }
182
+ await msg.reply(all.map(tk => `#${tk.id} [${tk.status}] ${tk.description}`).join("\n"));
183
+ return;
184
+ }
185
+ if (text.startsWith("!cancelauto ")) {
186
+ const id = parseInt(text.slice(12).trim());
187
+ if (isNaN(id)) {
188
+ await msg.reply(t(this.locale, "usage_cancelauto").replace("/", "!"));
189
+ return;
190
+ }
191
+ this.store.markTaskResult(id, "cancelled");
192
+ await msg.reply(t(this.locale, "auto_cancelled", { id }));
193
+ return;
194
+ }
195
+ // File upload handling
196
+ if (msg.attachments.size > 0) {
197
+ const ws = join("workspaces", msg.author.id);
198
+ mkdirSync(ws, { recursive: true });
199
+ for (const [, att] of msg.attachments) {
200
+ try {
201
+ const resp = await fetch(att.url);
202
+ const buf = Buffer.from(await resp.arrayBuffer());
203
+ writeFileSync(join(ws, att.name || "upload"), buf);
204
+ }
205
+ catch { }
206
+ }
207
+ const names = [...msg.attachments.values()].map(a => a.name).join(", ");
208
+ const prompt = text || `Analyze the uploaded file(s): ${names}`;
209
+ await this.handlePrompt(msg, prompt);
210
+ return;
211
+ }
212
+ // Intent detection (before sending to Claude)
213
+ if (text && !text.startsWith("!") && this.engine.getIntentConfig()?.enabled !== false) {
214
+ const intent = await detectIntent(text, this.engine.getRotator(), this.engine.getIntentConfig());
215
+ if (intent.type === "reminder" && intent.minutes && intent.description) {
216
+ const remindAt = Date.now() + intent.minutes * 60000;
217
+ const id = this.store.addTask(msg.author.id, "discord", String(msg.channelId), intent.description, remindAt);
218
+ await msg.reply(t(this.locale, "intent_reminder_set", { mins: intent.minutes, desc: intent.description, id }));
219
+ return;
220
+ }
221
+ if (intent.type === "task" && intent.description) {
222
+ const id = this.store.addTask(msg.author.id, "discord", String(msg.channelId), intent.description);
223
+ await msg.reply(t(this.locale, "intent_task_added", { id, desc: intent.description }));
224
+ return;
225
+ }
226
+ if (intent.type === "memory" && intent.description) {
227
+ this.store.addMemory(msg.author.id, intent.description, "nlp");
228
+ await msg.reply(t(this.locale, "intent_memory_saved", { desc: intent.description }));
229
+ return;
230
+ }
231
+ if (intent.type === "forget") {
232
+ this.store.clearMemories(msg.author.id);
233
+ await msg.reply(t(this.locale, "memories_cleared"));
234
+ return;
235
+ }
236
+ if (intent.type === "clear_session") {
237
+ this.store.clearSession(msg.author.id);
238
+ await msg.reply(t(this.locale, "session_cleared"));
239
+ return;
240
+ }
241
+ }
242
+ if (!text)
243
+ return;
244
+ await this.handlePrompt(msg, text);
245
+ });
246
+ }
247
+ async handlePrompt(msg, text) {
248
+ if (this.engine.isLocked(msg.author.id)) {
249
+ await msg.reply(t(this.locale, "still_processing"));
250
+ return;
251
+ }
252
+ const placeholder = await msg.reply(t(this.locale, "thinking"));
253
+ let lastEdit = 0;
254
+ let lastText = "";
255
+ try {
256
+ const res = await this.engine.runStream(msg.author.id, text, "discord", async (_chunk, full) => {
257
+ const now = Date.now();
258
+ if (now - lastEdit < EDIT_INTERVAL)
259
+ return;
260
+ const preview = full.slice(-1900) + "\n\n⏳...";
261
+ if (preview === lastText)
262
+ return;
263
+ lastText = preview;
264
+ lastEdit = now;
265
+ try {
266
+ await placeholder.edit(preview);
267
+ }
268
+ catch { }
269
+ });
270
+ const maxLen = this.config.chunk_size || 1900;
271
+ const chunks = chunkText(res.text, maxLen);
272
+ try {
273
+ await placeholder.edit(chunks[0]);
274
+ }
275
+ catch { }
276
+ for (let i = 1; i < chunks.length; i++) {
277
+ await msg.reply(chunks[i]);
278
+ }
279
+ }
280
+ catch (err) {
281
+ console.error("[discord] error:", err);
282
+ try {
283
+ await placeholder.edit(`Error: ${err.message || "unknown"}`);
284
+ }
285
+ catch { }
286
+ }
287
+ }
288
+ async start() {
289
+ console.log("[discord] starting bot...");
290
+ await this.client.login(this.config.token);
291
+ console.log(`[discord] logged in as ${this.client.user?.tag}`);
292
+ this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
293
+ }
294
+ stop() {
295
+ if (this.reminderTimer)
296
+ clearInterval(this.reminderTimer);
297
+ this.client.destroy();
298
+ }
299
+ async checkReminders() {
300
+ try {
301
+ const due = this.store.getDueReminders().filter(r => r.platform === "discord");
302
+ for (const r of due) {
303
+ const ch = await this.client.channels.fetch(r.chat_id);
304
+ if (ch?.isTextBased() && "send" in ch)
305
+ await ch.send(t(this.locale, "reminder_notify", { desc: r.description }));
306
+ this.store.markReminderSent(r.id);
307
+ }
308
+ }
309
+ catch (e) {
310
+ console.error("[discord] reminder error:", e);
311
+ }
312
+ }
313
+ }
@@ -0,0 +1,27 @@
1
+ import { Adapter } from "./base.js";
2
+ import { AgentEngine } from "../core/agent.js";
3
+ import { Store } from "../core/store.js";
4
+ import { TelegramConfig } from "../core/config.js";
5
+ export declare class TelegramAdapter implements Adapter {
6
+ private engine;
7
+ private store;
8
+ private config;
9
+ private locale;
10
+ private running;
11
+ private offset;
12
+ private reminderTimer?;
13
+ private autoTimer?;
14
+ private autoRunning;
15
+ constructor(engine: AgentEngine, store: Store, config: TelegramConfig, locale?: string);
16
+ private get api();
17
+ private call;
18
+ private reply;
19
+ private editMsg;
20
+ private handleUpdate;
21
+ private handlePrompt;
22
+ start(): Promise<void>;
23
+ stop(): void;
24
+ private registerCommands;
25
+ private checkReminders;
26
+ private processAutoTasks;
27
+ }