@emqo/claudebridge 0.1.8 → 0.2.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.
@@ -27,9 +27,8 @@ agent:
27
27
  enabled: true
28
28
  auto_summary: true
29
29
  max_memories: 50
30
- intent:
30
+ skill:
31
31
  enabled: true
32
- use_claude_fallback: true
33
32
 
34
33
  workspace:
35
34
  base_dir: "./workspaces"
@@ -4,7 +4,6 @@ import { join } from "path";
4
4
  import { chunkText } from "./base.js";
5
5
  import { reloadConfig } from "../core/config.js";
6
6
  import { t } from "../core/i18n.js";
7
- import { detectIntent } from "../core/intent.js";
8
7
  const EDIT_INTERVAL = 1500;
9
8
  export class DiscordAdapter {
10
9
  engine;
@@ -40,7 +39,7 @@ export class DiscordAdapter {
40
39
  if (!this.engine.access.isAllowed(msg.author.id, groupId))
41
40
  return;
42
41
  const text = msg.content.replace(/<@!?\d+>/g, "").trim();
43
- // Commands
42
+ // Management commands
44
43
  if (text === "!help") {
45
44
  await msg.reply(t(this.locale, "help").replaceAll("/", "!"));
46
45
  return;
@@ -97,101 +96,6 @@ export class DiscordAdapter {
97
96
  }
98
97
  return;
99
98
  }
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
99
  // File upload handling
196
100
  if (msg.attachments.size > 0) {
197
101
  const ws = join("workspaces", msg.author.id);
@@ -209,36 +113,7 @@ export class DiscordAdapter {
209
113
  await this.handlePrompt(msg, prompt);
210
114
  return;
211
115
  }
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
- }
116
+ // Text message send to Claude (skill system handles intents)
242
117
  if (!text)
243
118
  return;
244
119
  await this.handlePrompt(msg, text);
@@ -253,7 +128,7 @@ export class DiscordAdapter {
253
128
  let lastEdit = 0;
254
129
  let lastText = "";
255
130
  try {
256
- const res = await this.engine.runStream(msg.author.id, text, "discord", async (_chunk, full) => {
131
+ const res = await this.engine.runStream(msg.author.id, text, "discord", String(msg.channelId), async (_chunk, full) => {
257
132
  const now = Date.now();
258
133
  if (now - lastEdit < EDIT_INTERVAL)
259
134
  return;
@@ -2,7 +2,6 @@ import { chunkText } from "./base.js";
2
2
  import { reloadConfig } from "../core/config.js";
3
3
  import { toTelegramMarkdown } from "../core/markdown.js";
4
4
  import { t, getCommandDescriptions } from "../core/i18n.js";
5
- import { detectIntent } from "../core/intent.js";
6
5
  const EDIT_INTERVAL = 1500;
7
6
  export class TelegramAdapter {
8
7
  engine;
@@ -78,7 +77,7 @@ export class TelegramAdapter {
78
77
  }
79
78
  const text = (msg.text || "").trim();
80
79
  console.log(`[telegram] ${uid}: ${text.slice(0, 50)}`);
81
- // Commands
80
+ // Management commands
82
81
  if (text === "/start" || text === "/help") {
83
82
  await this.reply(chatId, t(this.locale, "help"));
84
83
  return;
@@ -129,101 +128,6 @@ export class TelegramAdapter {
129
128
  }
130
129
  return;
131
130
  }
132
- if (text.startsWith("/remember ")) {
133
- const content = text.slice(10).trim();
134
- if (!content) {
135
- await this.reply(chatId, t(this.locale, "usage_remember"));
136
- return;
137
- }
138
- this.store.addMemory(String(uid), content);
139
- await this.reply(chatId, t(this.locale, "memory_saved"));
140
- return;
141
- }
142
- if (text === "/memories") {
143
- const mems = this.store.getMemories(String(uid));
144
- if (!mems.length) {
145
- await this.reply(chatId, t(this.locale, "no_memories"));
146
- return;
147
- }
148
- await this.reply(chatId, mems.map(m => `[${m.source}] ${m.content}`).join("\n\n"));
149
- return;
150
- }
151
- if (text === "/forget") {
152
- this.store.clearMemories(String(uid));
153
- await this.reply(chatId, t(this.locale, "memories_cleared"));
154
- return;
155
- }
156
- if (text.startsWith("/task ")) {
157
- const desc = text.slice(6).trim();
158
- if (!desc) {
159
- await this.reply(chatId, t(this.locale, "usage_task"));
160
- return;
161
- }
162
- const id = this.store.addTask(String(uid), "telegram", String(chatId), desc);
163
- await this.reply(chatId, t(this.locale, "task_added", { id }));
164
- return;
165
- }
166
- if (text === "/tasks") {
167
- const tasks = this.store.getTasks(String(uid));
168
- if (!tasks.length) {
169
- await this.reply(chatId, t(this.locale, "no_tasks"));
170
- return;
171
- }
172
- await this.reply(chatId, tasks.map(t => `#${t.id} ${t.description}${t.remind_at ? ` ⏰${new Date(t.remind_at).toLocaleString()}` : ""}`).join("\n"));
173
- return;
174
- }
175
- if (text.startsWith("/done ")) {
176
- const id = parseInt(text.slice(6).trim());
177
- if (isNaN(id)) {
178
- await this.reply(chatId, t(this.locale, "usage_done"));
179
- return;
180
- }
181
- const ok = this.store.completeTask(id, String(uid));
182
- await this.reply(chatId, ok ? t(this.locale, "task_done", { id }) : t(this.locale, "task_not_found", { id }));
183
- return;
184
- }
185
- if (text.startsWith("/remind ")) {
186
- const match = text.match(/^\/remind\s+(\d+)m\s+(.+)$/);
187
- if (!match) {
188
- await this.reply(chatId, t(this.locale, "usage_remind"));
189
- return;
190
- }
191
- const mins = parseInt(match[1]);
192
- const desc = match[2].trim();
193
- const remindAt = Date.now() + mins * 60000;
194
- const id = this.store.addTask(String(uid), "telegram", String(chatId), desc, remindAt);
195
- await this.reply(chatId, t(this.locale, "reminder_set", { id, mins }));
196
- return;
197
- }
198
- if (text.startsWith("/auto ")) {
199
- const desc = text.slice(6).trim();
200
- if (!desc) {
201
- await this.reply(chatId, t(this.locale, "usage_auto"));
202
- return;
203
- }
204
- const id = this.store.addTask(String(uid), "telegram", String(chatId), desc, undefined, true);
205
- await this.reply(chatId, t(this.locale, "auto_queued", { id }));
206
- return;
207
- }
208
- if (text === "/autotasks") {
209
- const all = this.store.getAutoTasks(String(uid));
210
- if (!all.length) {
211
- await this.reply(chatId, t(this.locale, "no_auto_tasks"));
212
- return;
213
- }
214
- await this.reply(chatId, all.map(t => `#${t.id} [${t.status}] ${t.description}`).join("\n"));
215
- return;
216
- }
217
- if (text.startsWith("/cancelauto ")) {
218
- const id = parseInt(text.slice(12).trim());
219
- if (isNaN(id)) {
220
- await this.reply(chatId, t(this.locale, "usage_cancelauto"));
221
- return;
222
- }
223
- this.store.markTaskResult(id, "cancelled");
224
- await this.reply(chatId, t(this.locale, "auto_cancelled", { id }));
225
- return;
226
- }
227
131
  // File upload
228
132
  if (msg.document || msg.photo) {
229
133
  let fileId;
@@ -255,37 +159,7 @@ export class TelegramAdapter {
255
159
  }
256
160
  return;
257
161
  }
258
- // Intent detection (before sending to Claude)
259
- if (text && !text.startsWith("/") && this.engine.getIntentConfig()?.enabled !== false) {
260
- const intent = await detectIntent(text, this.engine.getRotator(), this.engine.getIntentConfig());
261
- if (intent.type === "reminder" && intent.minutes && intent.description) {
262
- const remindAt = Date.now() + intent.minutes * 60000;
263
- const id = this.store.addTask(String(uid), "telegram", String(chatId), intent.description, remindAt);
264
- await this.reply(chatId, t(this.locale, "intent_reminder_set", { mins: intent.minutes, desc: intent.description, id }));
265
- return;
266
- }
267
- if (intent.type === "task" && intent.description) {
268
- const id = this.store.addTask(String(uid), "telegram", String(chatId), intent.description);
269
- await this.reply(chatId, t(this.locale, "intent_task_added", { id, desc: intent.description }));
270
- return;
271
- }
272
- if (intent.type === "memory" && intent.description) {
273
- this.store.addMemory(String(uid), intent.description, "nlp");
274
- await this.reply(chatId, t(this.locale, "intent_memory_saved", { desc: intent.description }));
275
- return;
276
- }
277
- if (intent.type === "forget") {
278
- this.store.clearMemories(String(uid));
279
- await this.reply(chatId, t(this.locale, "memories_cleared"));
280
- return;
281
- }
282
- if (intent.type === "clear_session") {
283
- this.store.clearSession(String(uid));
284
- await this.reply(chatId, t(this.locale, "session_cleared"));
285
- return;
286
- }
287
- }
288
- // Text message
162
+ // Text message send to Claude (skill system handles intents)
289
163
  if (text)
290
164
  await this.handlePrompt(chatId, String(uid), text);
291
165
  }
@@ -299,7 +173,7 @@ export class TelegramAdapter {
299
173
  let lastEdit = 0;
300
174
  try {
301
175
  console.log(`[telegram] running claude for ${uid}...`);
302
- const res = await this.engine.runStream(uid, text, "telegram", async (_chunk, full) => {
176
+ const res = await this.engine.runStream(uid, text, "telegram", String(chatId), async (_chunk, full) => {
303
177
  const now = Date.now();
304
178
  if (now - lastEdit < EDIT_INTERVAL)
305
179
  return;
@@ -399,7 +273,7 @@ export class TelegramAdapter {
399
273
  await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
400
274
  try {
401
275
  console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
402
- const res = await this.engine.runStream(task.user_id, task.description, "telegram");
276
+ const res = await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id);
403
277
  this.store.markTaskResult(task.id, "done");
404
278
  const maxLen = this.config.chunk_size || 4000;
405
279
  const chunks = chunkText(res.text || "(no output)", maxLen);
@@ -21,11 +21,10 @@ export declare class AgentEngine {
21
21
  model: string;
22
22
  }[];
23
23
  getRotator(): EndpointRotator;
24
- getIntentConfig(): import("./config.js").IntentConfig;
25
24
  getEndpointCount(): number;
26
25
  private getWorkDir;
27
26
  isLocked(userId: string): boolean;
28
- runStream(userId: string, prompt: string, platform: string, onChunk?: StreamCallback): Promise<AgentResponse>;
27
+ runStream(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback): Promise<AgentResponse>;
29
28
  private _executeWithRetry;
30
29
  private _execute;
31
30
  private _autoSummarize;
@@ -1,9 +1,10 @@
1
1
  import { spawn } from "child_process";
2
2
  import { mkdirSync } from "fs";
3
- import { join } from "path";
3
+ import { join, resolve as pathResolve } from "path";
4
4
  import { UserLock } from "./lock.js";
5
5
  import { AccessControl } from "./permissions.js";
6
6
  import { EndpointRotator } from "./keys.js";
7
+ import { generateSkillDoc } from "../skills/bridge.js";
7
8
  export class AgentEngine {
8
9
  config;
9
10
  store;
@@ -28,9 +29,6 @@ export class AgentEngine {
28
29
  getRotator() {
29
30
  return this.rotator;
30
31
  }
31
- getIntentConfig() {
32
- return this.config.agent.intent;
33
- }
34
32
  getEndpointCount() {
35
33
  return this.rotator.count;
36
34
  }
@@ -45,13 +43,13 @@ export class AgentEngine {
45
43
  isLocked(userId) {
46
44
  return this.lock.isLocked(userId);
47
45
  }
48
- async runStream(userId, prompt, platform, onChunk) {
46
+ async runStream(userId, prompt, platform, chatId, onChunk) {
49
47
  const release = await this.lock.acquire(userId);
50
48
  try {
51
49
  this.store.addHistory(userId, platform, "user", prompt);
52
50
  const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
53
51
  const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
54
- const res = await this._executeWithRetry(userId, prompt, platform, onChunk, memoryPrompt);
52
+ const res = await this._executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt);
55
53
  this.store.addHistory(userId, platform, "assistant", res.text);
56
54
  this.store.recordUsage(userId, platform, res.cost || 0);
57
55
  if (this.config.agent.memory?.auto_summary)
@@ -62,7 +60,7 @@ export class AgentEngine {
62
60
  release();
63
61
  }
64
62
  }
65
- async _executeWithRetry(userId, prompt, platform, onChunk, memoryPrompt) {
63
+ async _executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt) {
66
64
  const maxRetries = Math.max(Math.min(this.rotator.count, 3), 1);
67
65
  let lastErr;
68
66
  for (let i = 0; i < maxRetries; i++) {
@@ -70,7 +68,7 @@ export class AgentEngine {
70
68
  ? this.rotator.next()
71
69
  : { name: "cli-default", api_key: "", base_url: "", model: "" };
72
70
  try {
73
- return await this._execute(userId, prompt, platform, ep, onChunk, memoryPrompt);
71
+ return await this._execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt);
74
72
  }
75
73
  catch (err) {
76
74
  lastErr = err;
@@ -85,7 +83,7 @@ export class AgentEngine {
85
83
  }
86
84
  throw lastErr;
87
85
  }
88
- _execute(userId, prompt, platform, ep, onChunk, memoryPrompt) {
86
+ _execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt) {
89
87
  return new Promise((resolve, reject) => {
90
88
  const sessionId = this.store.getSession(userId) || "";
91
89
  const cwd = this.getWorkDir(userId);
@@ -96,8 +94,15 @@ export class AgentEngine {
96
94
  args.push("-r", sessionId);
97
95
  if (this.config.agent.system_prompt)
98
96
  args.push("--system-prompt", this.config.agent.system_prompt);
97
+ // Build combined append prompt: memories + skill doc
98
+ let appendPrompt = "";
99
99
  if (memoryPrompt)
100
- args.push("--append-system-prompt", `User memories:\n${memoryPrompt}`);
100
+ appendPrompt += `User memories:\n${memoryPrompt}\n\n`;
101
+ if (this.config.agent.skill?.enabled !== false) {
102
+ appendPrompt += generateSkillDoc({ userId, chatId, platform, locale: this.config.locale || "en" });
103
+ }
104
+ if (appendPrompt)
105
+ args.push("--append-system-prompt", appendPrompt.trim());
101
106
  if (this.config.agent.allowed_tools?.length)
102
107
  args.push("--allowed-tools", this.config.agent.allowed_tools.join(","));
103
108
  if (this.config.agent.max_turns)
@@ -109,6 +114,7 @@ export class AgentEngine {
109
114
  env.ANTHROPIC_API_KEY = ep.api_key;
110
115
  if (ep.base_url)
111
116
  env.ANTHROPIC_BASE_URL = ep.base_url;
117
+ env.CLAUDEBRIDGE_DB = pathResolve("./data/claudebridge.db");
112
118
  const child = spawn("claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
113
119
  child.stdin.end();
114
120
  console.log(`[agent] spawned claude pid=${child.pid} cwd=${cwd} args=${args.join(" ")}`);
@@ -5,9 +5,8 @@ export interface MemoryConfig {
5
5
  auto_summary: boolean;
6
6
  max_memories: number;
7
7
  }
8
- export interface IntentConfig {
8
+ export interface SkillConfig {
9
9
  enabled: boolean;
10
- use_claude_fallback: boolean;
11
10
  }
12
11
  export interface AgentConfig {
13
12
  allowed_tools: string[];
@@ -18,7 +17,7 @@ export interface AgentConfig {
18
17
  cwd: string;
19
18
  timeout_seconds: number;
20
19
  memory: MemoryConfig;
21
- intent: IntentConfig;
20
+ skill: SkillConfig;
22
21
  }
23
22
  export interface WorkspaceConfig {
24
23
  base_dir: string;
@@ -12,7 +12,7 @@ export function loadConfig(path) {
12
12
  ...raw.agent,
13
13
  timeout_seconds: raw.agent?.timeout_seconds ?? 300,
14
14
  memory: { enabled: true, auto_summary: true, max_memories: 50, ...raw.agent?.memory },
15
- intent: { enabled: true, use_claude_fallback: true, ...raw.agent?.intent },
15
+ skill: { enabled: true, ...raw.agent?.skill },
16
16
  },
17
17
  workspace: raw.workspace,
18
18
  access: raw.access || { allowed_users: [], allowed_groups: [] },
package/dist/core/i18n.js CHANGED
@@ -1,91 +1,45 @@
1
1
  const messages = {
2
2
  en: {
3
- help: "ClaudeBridge ready.\n\nCommands:\n/new - clear session\n/usage - your stats\n/allusage - all stats\n/history - recent chats\n/model - endpoints info\n/reload - reload config\n/remember <text> - save a memory\n/memories - list memories\n/forget - clear all memories\n/task <desc> - add a task\n/tasks - list pending tasks\n/done <id> - complete a task\n/remind <minutes>m <desc> - set reminder\n/auto <desc> - queue auto task\n/autotasks - list auto tasks\n/cancelauto <id> - cancel auto task\n\nSend text or files to chat. Unknown /commands are forwarded to Claude.",
3
+ help: "ClaudeBridge ready.\n\nManagement commands:\n/new - clear session\n/usage - your stats\n/allusage - all stats\n/history - recent chats\n/model - endpoints info\n/reload - reload config\n/help - show this help\n\nJust chat naturally to manage memories, tasks, reminders, and more Claude handles it all.",
4
4
  session_cleared: "Session cleared.",
5
5
  no_usage: "No usage data.",
6
6
  no_history: "No history.",
7
7
  config_reloaded: "Config reloaded.",
8
8
  reload_failed: "Reload failed: ",
9
- memory_saved: "✅ Memory saved.",
10
- no_memories: "No memories.",
11
- memories_cleared: "✅ All memories cleared.",
12
- task_added: "✅ Task #{id} added.",
13
- no_tasks: "No pending tasks.",
14
- task_done: "✅ Task #{id} done.",
15
- task_not_found: "Task #{id} not found.",
16
- reminder_set: "✅ Reminder #{id} set for {mins}m.",
17
- auto_queued: "🤖 Auto task #{id} queued. Will execute when idle.",
18
- no_auto_tasks: "No auto tasks.",
19
- auto_cancelled: "✅ Auto task #{id} cancelled.",
20
- auto_starting: "🤖 Auto task #{id} starting:\n{desc}",
21
- auto_done: "✅ Auto task #{id} done (cost: ${cost}):",
22
- auto_failed: "❌ Auto task #{id} failed: {err}",
23
9
  thinking: "⏳ Thinking...",
24
10
  still_processing: "⏳ Still processing...",
25
11
  upload_failed: "Upload failed: ",
26
12
  reminder_notify: "⏰ Reminder: {desc}",
27
- usage_remember: "Usage: /remember <text>",
28
- usage_task: "Usage: /task <description>",
29
- usage_done: "Usage: /done <task_id>",
30
- usage_remind: "Usage: /remind <minutes>m <description>",
31
- usage_auto: "Usage: /auto <task description>",
32
- usage_cancelauto: "Usage: /cancelauto <task_id>",
33
- intent_reminder_set: "✅ Reminder detected: in {mins}m — {desc} (#{id})",
34
- intent_task_added: "✅ Task detected #{id}: {desc}",
35
- intent_memory_saved: "✅ Remembered: {desc}",
13
+ auto_starting: "🤖 Auto task #{id} starting:\n{desc}",
14
+ auto_done: " Auto task #{id} done (cost: ${cost}):",
15
+ auto_failed: "❌ Auto task #{id} failed: {err}",
36
16
  },
37
17
  zh: {
38
- help: "ClaudeBridge 就绪。\n\n命令:\n/new - 清除会话\n/usage - 你的用量\n/allusage - 所有用量\n/history - 最近对话\n/model - 端点信息\n/reload - 重载配置\n/remember <文本> - 保存记忆\n/memories - 查看记忆\n/forget - 清除所有记忆\n/task <描述> - 添加任务\n/tasks - 查看待办\n/done <id> - 完成任务\n/remind <分钟>m <描述> - 设置提醒\n/auto <描述> - 排队自动任务\n/autotasks - 查看自动任务\n/cancelauto <id> - 取消自动任务\n\n发送文字或文件即可对话。未知 / 命令会转发给 Claude。",
18
+ help: "ClaudeBridge 就绪。\n\n管理命令:\n/new - 清除会话\n/usage - 你的用量\n/allusage - 所有用量\n/history - 最近对话\n/model - 端点信息\n/reload - 重载配置\n/help - 显示帮助\n\n直接对话即可管理记忆、任务、提醒等 Claude 会自动处理。",
39
19
  session_cleared: "会话已清除。",
40
20
  no_usage: "暂无用量数据。",
41
21
  no_history: "暂无历史记录。",
42
22
  config_reloaded: "配置已重载。",
43
23
  reload_failed: "重载失败:",
44
- memory_saved: "✅ 记忆已保存。",
45
- no_memories: "暂无记忆。",
46
- memories_cleared: "✅ 所有记忆已清除。",
47
- task_added: "✅ 任务 #{id} 已添加。",
48
- no_tasks: "暂无待办任务。",
49
- task_done: "✅ 任务 #{id} 已完成。",
50
- task_not_found: "任务 #{id} 未找到。",
51
- reminder_set: "✅ 提醒 #{id} 已设置,{mins}分钟后触发。",
52
- auto_queued: "🤖 自动任务 #{id} 已排队,空闲时执行。",
53
- no_auto_tasks: "暂无自动任务。",
54
- auto_cancelled: "✅ 自动任务 #{id} 已取消。",
55
- auto_starting: "🤖 自动任务 #{id} 开始执行:\n{desc}",
56
- auto_done: "✅ 自动任务 #{id} 完成(花费:${cost}):",
57
- auto_failed: "❌ 自动任务 #{id} 失败:{err}",
58
24
  thinking: "⏳ 思考中...",
59
25
  still_processing: "⏳ 仍在处理...",
60
26
  upload_failed: "上传失败:",
61
27
  reminder_notify: "⏰ 提醒:{desc}",
62
- usage_remember: "用法:/remember <文本>",
63
- usage_task: "用法:/task <描述>",
64
- usage_done: "用法:/done <任务ID>",
65
- usage_remind: "用法:/remind <分钟>m <描述>",
66
- usage_auto: "用法:/auto <任务描述>",
67
- usage_cancelauto: "用法:/cancelauto <任务ID>",
68
- intent_reminder_set: "✅ 已识别提醒:{mins}分钟后 — {desc} (#{id})",
69
- intent_task_added: "✅ 已识别任务 #{id}:{desc}",
70
- intent_memory_saved: "✅ 已识别并记住:{desc}",
28
+ auto_starting: "🤖 自动任务 #{id} 开始执行:\n{desc}",
29
+ auto_done: " 自动任务 #{id} 完成(花费:${cost}):",
30
+ auto_failed: " 自动任务 #{id} 失败:{err}",
71
31
  },
72
32
  };
73
33
  const commandDescriptions = {
74
34
  en: {
75
35
  new: "Clear session", usage: "Your usage stats", allusage: "All users usage",
76
36
  history: "Recent conversations", model: "Current model/endpoints", reload: "Reload config",
77
- remember: "Save a memory", memories: "List your memories", forget: "Clear all memories",
78
- task: "Add a task", tasks: "List pending tasks", done: "Complete a task",
79
- remind: "Set a timed reminder", auto: "Queue auto task (runs when idle)",
80
- autotasks: "List auto tasks", cancelauto: "Cancel an auto task", help: "Show all commands",
37
+ help: "Show all commands",
81
38
  },
82
39
  zh: {
83
40
  new: "清除会话", usage: "你的用量", allusage: "所有用量",
84
41
  history: "最近对话", model: "端点信息", reload: "重载配置",
85
- remember: "保存记忆", memories: "查看记忆", forget: "清除所有记忆",
86
- task: "添加任务", tasks: "查看待办", done: "完成任务",
87
- remind: "设置提醒", auto: "排队自动任务(空闲执行)",
88
- autotasks: "查看自动任务", cancelauto: "取消自动任务", help: "显示所有命令",
42
+ help: "显示帮助",
89
43
  },
90
44
  };
91
45
  export function t(locale, key, vars) {
package/dist/ctl.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/ctl.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ import Database from "better-sqlite3";
3
+ const DB_PATH = process.env.CLAUDEBRIDGE_DB;
4
+ if (!DB_PATH) {
5
+ console.error("Error: CLAUDEBRIDGE_DB environment variable is required");
6
+ process.exit(1);
7
+ }
8
+ const db = new Database(DB_PATH);
9
+ db.pragma("journal_mode = WAL");
10
+ db.pragma("busy_timeout = 5000");
11
+ const [, , category, action, ...rest] = process.argv;
12
+ function output(data) {
13
+ console.log(JSON.stringify(data, null, 2));
14
+ }
15
+ function fail(msg) {
16
+ console.error(msg);
17
+ process.exit(1);
18
+ }
19
+ if (category === "memory") {
20
+ if (action === "add") {
21
+ const [userId, ...contentParts] = rest;
22
+ if (!userId || !contentParts.length)
23
+ fail("Usage: memory add <user_id> <content>");
24
+ const content = contentParts.join(" ");
25
+ const existing = db.prepare("SELECT id FROM memories WHERE user_id = ? AND content = ? LIMIT 1").get(userId, content);
26
+ if (existing) {
27
+ output({ ok: true, message: "Memory already exists", duplicate: true });
28
+ }
29
+ else {
30
+ const r = db.prepare("INSERT INTO memories (user_id, content, source, created_at) VALUES (?, ?, 'manual', ?)").run(userId, content, Date.now());
31
+ output({ ok: true, id: Number(r.lastInsertRowid), message: "Memory saved" });
32
+ }
33
+ }
34
+ else if (action === "list") {
35
+ const [userId] = rest;
36
+ if (!userId)
37
+ fail("Usage: memory list <user_id>");
38
+ const rows = db.prepare("SELECT id, content, source, created_at FROM memories WHERE user_id = ? ORDER BY created_at DESC").all(userId);
39
+ output({ ok: true, memories: rows });
40
+ }
41
+ else if (action === "clear") {
42
+ const [userId] = rest;
43
+ if (!userId)
44
+ fail("Usage: memory clear <user_id>");
45
+ db.prepare("DELETE FROM memories WHERE user_id = ?").run(userId);
46
+ output({ ok: true, message: "All memories cleared" });
47
+ }
48
+ else {
49
+ fail("Usage: memory <add|list|clear> ...");
50
+ }
51
+ }
52
+ else if (category === "task") {
53
+ if (action === "add") {
54
+ const [userId, platform, chatId, ...descParts] = rest;
55
+ if (!userId || !platform || !chatId || !descParts.length)
56
+ fail("Usage: task add <user_id> <platform> <chat_id> <description>");
57
+ const desc = descParts.join(" ");
58
+ const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)").run(userId, platform, chatId, desc, Date.now());
59
+ output({ ok: true, id: Number(r.lastInsertRowid), message: "Task added" });
60
+ }
61
+ else if (action === "list") {
62
+ const [userId] = rest;
63
+ if (!userId)
64
+ fail("Usage: task list <user_id>");
65
+ const rows = db.prepare("SELECT id, description, status, remind_at, created_at FROM tasks WHERE user_id = ? AND status = 'pending' ORDER BY created_at DESC").all(userId);
66
+ output({ ok: true, tasks: rows });
67
+ }
68
+ else if (action === "done") {
69
+ const [taskId, userId] = rest;
70
+ if (!taskId || !userId)
71
+ fail("Usage: task done <task_id> <user_id>");
72
+ const r = db.prepare("UPDATE tasks SET status = 'done' WHERE id = ? AND user_id = ? AND status = 'pending'").run(parseInt(taskId), userId);
73
+ output({ ok: true, updated: r.changes > 0 });
74
+ }
75
+ else {
76
+ fail("Usage: task <add|list|done> ...");
77
+ }
78
+ }
79
+ else if (category === "reminder") {
80
+ if (action === "add") {
81
+ const [userId, platform, chatId, minutes, ...descParts] = rest;
82
+ if (!userId || !platform || !chatId || !minutes || !descParts.length)
83
+ fail("Usage: reminder add <user_id> <platform> <chat_id> <minutes> <description>");
84
+ const remindAt = Date.now() + parseInt(minutes) * 60000;
85
+ const desc = descParts.join(" ");
86
+ const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, remind_at, created_at) VALUES (?, ?, ?, ?, 'pending', ?, ?)").run(userId, platform, chatId, desc, remindAt, Date.now());
87
+ output({ ok: true, id: Number(r.lastInsertRowid), remind_at: remindAt, message: `Reminder set for ${minutes} minutes` });
88
+ }
89
+ else if (action === "list") {
90
+ const [userId] = rest;
91
+ if (!userId)
92
+ fail("Usage: reminder list <user_id>");
93
+ const rows = db.prepare("SELECT id, description, remind_at, reminder_sent, created_at FROM tasks WHERE user_id = ? AND remind_at IS NOT NULL ORDER BY created_at DESC").all(userId);
94
+ output({ ok: true, reminders: rows });
95
+ }
96
+ else {
97
+ fail("Usage: reminder <add|list> ...");
98
+ }
99
+ }
100
+ else if (category === "auto") {
101
+ if (action === "add") {
102
+ const [userId, platform, chatId, ...descParts] = rest;
103
+ if (!userId || !platform || !chatId || !descParts.length)
104
+ fail("Usage: auto add <user_id> <platform> <chat_id> <description>");
105
+ const desc = descParts.join(" ");
106
+ const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, created_at) VALUES (?, ?, ?, ?, 'auto', ?)").run(userId, platform, chatId, desc, Date.now());
107
+ output({ ok: true, id: Number(r.lastInsertRowid), message: "Auto task queued" });
108
+ }
109
+ else if (action === "list") {
110
+ const [userId] = rest;
111
+ if (!userId)
112
+ fail("Usage: auto list <user_id>");
113
+ const rows = db.prepare("SELECT id, description, status, created_at FROM tasks WHERE user_id = ? AND status IN ('auto','running') ORDER BY created_at DESC").all(userId);
114
+ output({ ok: true, tasks: rows });
115
+ }
116
+ else if (action === "cancel") {
117
+ const [taskId] = rest;
118
+ if (!taskId)
119
+ fail("Usage: auto cancel <task_id>");
120
+ db.prepare("UPDATE tasks SET status = 'cancelled' WHERE id = ?").run(parseInt(taskId));
121
+ output({ ok: true, message: "Auto task cancelled" });
122
+ }
123
+ else {
124
+ fail("Usage: auto <add|list|cancel> ...");
125
+ }
126
+ }
127
+ else {
128
+ fail("Usage: claudebridge-ctl <memory|task|reminder|auto> <action> [args...]");
129
+ }
130
+ db.close();
@@ -0,0 +1,7 @@
1
+ export interface SkillContext {
2
+ userId: string;
3
+ chatId: string;
4
+ platform: string;
5
+ locale: string;
6
+ }
7
+ export declare function generateSkillDoc(ctx: SkillContext): string;
@@ -0,0 +1,80 @@
1
+ import { resolve, dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = dirname(__filename);
5
+ export function generateSkillDoc(ctx) {
6
+ const ctlPath = resolve(__dirname, "../ctl.js");
7
+ const ctl = `node ${ctlPath}`;
8
+ if (ctx.locale === "zh") {
9
+ return [
10
+ `## ClaudeBridge 内置能力`,
11
+ ``,
12
+ `你正在 ClaudeBridge 中运行,连接着 ${ctx.platform} 平台。`,
13
+ `当前用户 ID: ${ctx.userId} | 聊天 ID: ${ctx.chatId} | 平台: ${ctx.platform}`,
14
+ ``,
15
+ `你可以通过 Bash 工具调用以下命令来管理用户的记忆、任务、提醒和自动任务:`,
16
+ ``,
17
+ `### 记忆管理`,
18
+ `- 保存记忆: \`${ctl} memory add ${ctx.userId} "内容"\``,
19
+ `- 查看记忆: \`${ctl} memory list ${ctx.userId}\``,
20
+ `- 清除记忆: \`${ctl} memory clear ${ctx.userId}\``,
21
+ ``,
22
+ `### 任务管理`,
23
+ `- 添加任务: \`${ctl} task add ${ctx.userId} ${ctx.platform} ${ctx.chatId} "任务描述"\``,
24
+ `- 查看任务: \`${ctl} task list ${ctx.userId}\``,
25
+ `- 完成任务: \`${ctl} task done <任务ID> ${ctx.userId}\``,
26
+ ``,
27
+ `### 提醒`,
28
+ `- 设置提醒: \`${ctl} reminder add ${ctx.userId} ${ctx.platform} ${ctx.chatId} <分钟数> "提醒内容"\``,
29
+ `- 查看提醒: \`${ctl} reminder list ${ctx.userId}\``,
30
+ ``,
31
+ `### 自动任务`,
32
+ `- 创建自动任务: \`${ctl} auto add ${ctx.userId} ${ctx.platform} ${ctx.chatId} "任务描述"\``,
33
+ `- 查看自动任务: \`${ctl} auto list ${ctx.userId}\``,
34
+ `- 取消自动任务: \`${ctl} auto cancel <任务ID>\``,
35
+ ``,
36
+ `### 使用指南`,
37
+ `- 用户要你记住某事 → 使用 memory add`,
38
+ `- 用户问你记住了什么 → 使用 memory list`,
39
+ `- 用户要设置提醒 → 使用 reminder add(计算分钟数)`,
40
+ `- 用户要添加任务/待办 → 使用 task add`,
41
+ `- 命令输出 JSON,请用自然语言向用户回复结果,不要直接展示 JSON`,
42
+ `- 提醒会由 Bridge 定时器自动推送,你只需创建即可`,
43
+ ].join("\n");
44
+ }
45
+ return [
46
+ `## ClaudeBridge Built-in Skills`,
47
+ ``,
48
+ `You are running inside ClaudeBridge, connected to the ${ctx.platform} platform.`,
49
+ `Current user ID: ${ctx.userId} | Chat ID: ${ctx.chatId} | Platform: ${ctx.platform}`,
50
+ ``,
51
+ `You can use the Bash tool to call these commands to manage the user's memories, tasks, reminders, and auto-tasks:`,
52
+ ``,
53
+ `### Memory Management`,
54
+ `- Save a memory: \`${ctl} memory add ${ctx.userId} "content"\``,
55
+ `- List memories: \`${ctl} memory list ${ctx.userId}\``,
56
+ `- Clear memories: \`${ctl} memory clear ${ctx.userId}\``,
57
+ ``,
58
+ `### Task Management`,
59
+ `- Add a task: \`${ctl} task add ${ctx.userId} ${ctx.platform} ${ctx.chatId} "task description"\``,
60
+ `- List tasks: \`${ctl} task list ${ctx.userId}\``,
61
+ `- Complete a task: \`${ctl} task done <task_id> ${ctx.userId}\``,
62
+ ``,
63
+ `### Reminders`,
64
+ `- Set a reminder: \`${ctl} reminder add ${ctx.userId} ${ctx.platform} ${ctx.chatId} <minutes> "description"\``,
65
+ `- List reminders: \`${ctl} reminder list ${ctx.userId}\``,
66
+ ``,
67
+ `### Auto Tasks`,
68
+ `- Queue an auto task: \`${ctl} auto add ${ctx.userId} ${ctx.platform} ${ctx.chatId} "description"\``,
69
+ `- List auto tasks: \`${ctl} auto list ${ctx.userId}\``,
70
+ `- Cancel an auto task: \`${ctl} auto cancel <task_id>\``,
71
+ ``,
72
+ `### Guidelines`,
73
+ `- User wants you to remember something → use memory add`,
74
+ `- User asks what you remember → use memory list`,
75
+ `- User wants a reminder → use reminder add (calculate minutes)`,
76
+ `- User wants to add a task/todo → use task add`,
77
+ `- Commands output JSON. Respond to the user in natural language, do not dump raw JSON.`,
78
+ `- Reminders are automatically pushed by Bridge timers — you only need to create them.`,
79
+ ].join("\n");
80
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Bridge Claude Code Agent SDK to chat platforms (Telegram, Discord, etc.)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "claudebridge": "dist/cli.js"
7
+ "claudebridge": "dist/cli.js",
8
+ "claudebridge-ctl": "dist/ctl.js"
8
9
  },
9
10
  "type": "module",
10
11
  "scripts": {
@@ -1,10 +0,0 @@
1
- import { IntentConfig } from "./config.js";
2
- import { EndpointRotator } from "./keys.js";
3
- export interface IntentResult {
4
- type: "reminder" | "task" | "memory" | "forget" | "clear_session" | "none";
5
- description?: string;
6
- minutes?: number;
7
- }
8
- export declare function regexDetect(text: string): IntentResult;
9
- export declare function claudeDetect(text: string, rotator: EndpointRotator): Promise<IntentResult>;
10
- export declare function detectIntent(text: string, rotator: EndpointRotator, config?: IntentConfig): Promise<IntentResult>;
@@ -1,108 +0,0 @@
1
- import { spawn } from "child_process";
2
- const patterns = [
3
- [/(?:提醒我?|remind\s*me)\s*(\d+)\s*(?:分钟|min(?:ute)?s?|m)\s*(?:后|later)?\s*(.+)/i,
4
- m => ({ type: "reminder", minutes: +m[1], description: m[2].trim() })],
5
- [/^(?:添加|加个?|创建|add|create)\s*(?:一个)?(?:任务|task)[::\s]*(.+)/i,
6
- m => ({ type: "task", description: m[1].trim() })],
7
- [/^(?:记住|记下|记忆|帮我记|remember)\s*(?:that|this)?[::\s]*(.+)/i,
8
- m => ({ type: "memory", description: m[1].trim() })],
9
- [/^(?:忘记所有|清除记忆|forget\s*all|clear\s*memo)/i,
10
- () => ({ type: "forget" })],
11
- [/^(?:新会话|新对话|new\s*session|clear\s*session)/i,
12
- () => ({ type: "clear_session" })],
13
- ];
14
- /** Loose keywords that hint the message *might* be an intent — worth a Claude check */
15
- const hintPatterns = [
16
- /提醒|醒我|remind/i,
17
- /任务|待办|todo|task/i,
18
- /记住|记下|记得|记忆|帮我记|remember/i,
19
- /忘记|忘掉|forget/i,
20
- /新会话|新对话|new\s*session|clear\s*session/i,
21
- /\d+\s*(?:分钟|min|m|小时|hour|h)\s*(?:后|later)/i,
22
- ];
23
- function mightBeIntent(text) {
24
- return hintPatterns.some(re => re.test(text));
25
- }
26
- export function regexDetect(text) {
27
- for (const [re, fn] of patterns) {
28
- const m = text.match(re);
29
- if (m)
30
- return fn(m);
31
- }
32
- return { type: "none" };
33
- }
34
- export function claudeDetect(text, rotator) {
35
- return new Promise(resolve => {
36
- const timeout = setTimeout(() => resolve({ type: "none" }), 15000);
37
- const ep = rotator.count
38
- ? rotator.next()
39
- : { name: "cli-default", api_key: "", base_url: "", model: "" };
40
- const env = { ...process.env };
41
- if (ep.api_key)
42
- env.ANTHROPIC_API_KEY = ep.api_key;
43
- if (ep.base_url)
44
- env.ANTHROPIC_BASE_URL = ep.base_url;
45
- const prompt = `You are an intent classifier. Classify the user message into exactly ONE of these types. Output ONLY a single JSON object, nothing else.
46
-
47
- Types:
48
- - "reminder": user wants to be reminded later. Extract minutes and description. Example: {"type":"reminder","minutes":5,"description":"check server"}
49
- - "task": user wants to add a task/todo. Extract description. Example: {"type":"task","description":"buy milk"}
50
- - "memory": user wants you to remember something. Extract description. Example: {"type":"memory","description":"I like TypeScript"}
51
- - "none": normal conversation, not an intent. Output: {"type":"none"}
52
-
53
- Rules:
54
- - Output ONLY one JSON object on a single line
55
- - Do NOT output any explanation or extra text
56
- - If unsure, output {"type":"none"}
57
-
58
- User message: ${text.slice(0, 500)}`;
59
- const args = ["-p", prompt, "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.005"];
60
- if (ep.model)
61
- args.push("--model", ep.model);
62
- const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
63
- child.stdin.end();
64
- let result = "";
65
- let buffer = "";
66
- child.stdout.on("data", (d) => {
67
- buffer += d.toString();
68
- const lines = buffer.split("\n");
69
- buffer = lines.pop() || "";
70
- for (const line of lines) {
71
- if (!line.trim())
72
- continue;
73
- try {
74
- const msg = JSON.parse(line);
75
- if (msg.type === "result" && msg.result)
76
- result = msg.result;
77
- }
78
- catch { }
79
- }
80
- });
81
- child.on("close", () => {
82
- clearTimeout(timeout);
83
- try {
84
- const m = result.match(/\{[^}]+\}/);
85
- if (m) {
86
- const obj = JSON.parse(m[0]);
87
- if (obj.type && obj.type !== "none") {
88
- resolve(obj);
89
- return;
90
- }
91
- }
92
- }
93
- catch { }
94
- resolve({ type: "none" });
95
- });
96
- child.on("error", () => { clearTimeout(timeout); resolve({ type: "none" }); });
97
- });
98
- }
99
- export async function detectIntent(text, rotator, config) {
100
- const r = regexDetect(text);
101
- if (r.type !== "none")
102
- return r;
103
- // Only call Claude fallback if text contains hint keywords (saves cost)
104
- if (config?.use_claude_fallback !== false && mightBeIntent(text)) {
105
- return claudeDetect(text, rotator);
106
- }
107
- return { type: "none" };
108
- }