@emqo/claudebridge 0.1.9 → 0.2.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/config.yaml.example +1 -2
- package/dist/adapters/discord.js +5 -136
- package/dist/adapters/telegram.js +6 -140
- package/dist/core/agent.d.ts +2 -3
- package/dist/core/agent.js +17 -11
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +1 -1
- package/dist/core/i18n.js +10 -56
- package/dist/ctl.d.ts +2 -0
- package/dist/ctl.js +130 -0
- package/dist/skills/bridge.d.ts +7 -0
- package/dist/skills/bridge.js +80 -0
- package/package.json +3 -2
- package/dist/core/intent.d.ts +0 -10
- package/dist/core/intent.js +0 -108
package/config.yaml.example
CHANGED
package/dist/adapters/discord.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Client, GatewayIntentBits } from "discord.js";
|
|
2
|
-
import { writeFileSync
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
3
|
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
|
-
//
|
|
42
|
+
// Management commands
|
|
44
43
|
if (text === "!help") {
|
|
45
44
|
await msg.reply(t(this.locale, "help").replaceAll("/", "!"));
|
|
46
45
|
return;
|
|
@@ -97,105 +96,9 @@ 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
|
-
const ws =
|
|
198
|
-
mkdirSync(ws, { recursive: true });
|
|
101
|
+
const ws = this.engine.getWorkDir(msg.author.id);
|
|
199
102
|
for (const [, att] of msg.attachments) {
|
|
200
103
|
try {
|
|
201
104
|
const resp = await fetch(att.url);
|
|
@@ -209,41 +112,7 @@ export class DiscordAdapter {
|
|
|
209
112
|
await this.handlePrompt(msg, prompt);
|
|
210
113
|
return;
|
|
211
114
|
}
|
|
212
|
-
//
|
|
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
|
-
if (/上面|之前|刚才|这个|那个|above|previous|earlier|this|that/.test(intent.description)) {
|
|
228
|
-
// Fall through to Claude conversation for context resolution
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
this.store.addMemory(msg.author.id, intent.description, "nlp");
|
|
232
|
-
await msg.reply(t(this.locale, "intent_memory_saved", { desc: intent.description }));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (intent.type === "forget") {
|
|
237
|
-
this.store.clearMemories(msg.author.id);
|
|
238
|
-
await msg.reply(t(this.locale, "memories_cleared"));
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (intent.type === "clear_session") {
|
|
242
|
-
this.store.clearSession(msg.author.id);
|
|
243
|
-
await msg.reply(t(this.locale, "session_cleared"));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
115
|
+
// Text message — send to Claude (skill system handles intents)
|
|
247
116
|
if (!text)
|
|
248
117
|
return;
|
|
249
118
|
await this.handlePrompt(msg, text);
|
|
@@ -258,7 +127,7 @@ export class DiscordAdapter {
|
|
|
258
127
|
let lastEdit = 0;
|
|
259
128
|
let lastText = "";
|
|
260
129
|
try {
|
|
261
|
-
const res = await this.engine.runStream(msg.author.id, text, "discord", async (_chunk, full) => {
|
|
130
|
+
const res = await this.engine.runStream(msg.author.id, text, "discord", String(msg.channelId), async (_chunk, full) => {
|
|
262
131
|
const now = Date.now();
|
|
263
132
|
if (now - lastEdit < EDIT_INTERVAL)
|
|
264
133
|
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
|
-
//
|
|
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;
|
|
@@ -242,10 +146,9 @@ export class TelegramAdapter {
|
|
|
242
146
|
const url = `https://api.telegram.org/file/bot${this.config.token}/${file.file_path}`;
|
|
243
147
|
const resp = await fetch(url);
|
|
244
148
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
245
|
-
const {
|
|
149
|
+
const { writeFileSync } = await import("fs");
|
|
246
150
|
const { join } = await import("path");
|
|
247
|
-
const ws =
|
|
248
|
-
mkdirSync(ws, { recursive: true });
|
|
151
|
+
const ws = this.engine.getWorkDir(String(uid));
|
|
249
152
|
writeFileSync(join(ws, fileName), buf);
|
|
250
153
|
const prompt = msg.caption || `Analyze the uploaded file: ${fileName}`;
|
|
251
154
|
await this.handlePrompt(chatId, String(uid), prompt);
|
|
@@ -255,44 +158,7 @@ export class TelegramAdapter {
|
|
|
255
158
|
}
|
|
256
159
|
return;
|
|
257
160
|
}
|
|
258
|
-
//
|
|
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
|
-
// If description contains references to context ("上面", "this", "that", etc.),
|
|
274
|
-
// fall through to Claude conversation so it can resolve from session history
|
|
275
|
-
if (/上面|之前|刚才|这个|那个|above|previous|earlier|this|that/.test(intent.description)) {
|
|
276
|
-
// Don't intercept — let Claude handle it with full context
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
this.store.addMemory(String(uid), intent.description, "nlp");
|
|
280
|
-
await this.reply(chatId, t(this.locale, "intent_memory_saved", { desc: intent.description }));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (intent.type === "forget") {
|
|
285
|
-
this.store.clearMemories(String(uid));
|
|
286
|
-
await this.reply(chatId, t(this.locale, "memories_cleared"));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (intent.type === "clear_session") {
|
|
290
|
-
this.store.clearSession(String(uid));
|
|
291
|
-
await this.reply(chatId, t(this.locale, "session_cleared"));
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Text message
|
|
161
|
+
// Text message — send to Claude (skill system handles intents)
|
|
296
162
|
if (text)
|
|
297
163
|
await this.handlePrompt(chatId, String(uid), text);
|
|
298
164
|
}
|
|
@@ -306,7 +172,7 @@ export class TelegramAdapter {
|
|
|
306
172
|
let lastEdit = 0;
|
|
307
173
|
try {
|
|
308
174
|
console.log(`[telegram] running claude for ${uid}...`);
|
|
309
|
-
const res = await this.engine.runStream(uid, text, "telegram", async (_chunk, full) => {
|
|
175
|
+
const res = await this.engine.runStream(uid, text, "telegram", String(chatId), async (_chunk, full) => {
|
|
310
176
|
const now = Date.now();
|
|
311
177
|
if (now - lastEdit < EDIT_INTERVAL)
|
|
312
178
|
return;
|
|
@@ -406,7 +272,7 @@ export class TelegramAdapter {
|
|
|
406
272
|
await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
407
273
|
try {
|
|
408
274
|
console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
|
|
409
|
-
const res = await this.engine.runStream(task.user_id, task.description, "telegram");
|
|
275
|
+
const res = await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id);
|
|
410
276
|
this.store.markTaskResult(task.id, "done");
|
|
411
277
|
const maxLen = this.config.chunk_size || 4000;
|
|
412
278
|
const chunks = chunkText(res.text || "(no output)", maxLen);
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -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
|
+
getWorkDir(userId: string): string;
|
|
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;
|
package/dist/core/agent.js
CHANGED
|
@@ -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
|
-
|
|
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(" ")}`);
|
|
@@ -191,7 +197,7 @@ export class AgentEngine {
|
|
|
191
197
|
if (ep.base_url)
|
|
192
198
|
env.ANTHROPIC_BASE_URL = ep.base_url;
|
|
193
199
|
const summaryPrompt = `Extract 1-3 key facts worth remembering about the user from this exchange. Output only bullet points, no preamble. If nothing worth remembering, output "NONE".\n\nUser: ${prompt.slice(0, 500)}\nAssistant: ${response.slice(0, 1000)}`;
|
|
194
|
-
const args = ["-p", summaryPrompt, "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.05"];
|
|
200
|
+
const args = ["-p", summaryPrompt, "--verbose", "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.05"];
|
|
195
201
|
if (ep.model)
|
|
196
202
|
args.push("--model", ep.model);
|
|
197
203
|
const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
|
package/dist/core/config.d.ts
CHANGED
|
@@ -5,9 +5,8 @@ export interface MemoryConfig {
|
|
|
5
5
|
auto_summary: boolean;
|
|
6
6
|
max_memories: number;
|
|
7
7
|
}
|
|
8
|
-
export interface
|
|
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
|
-
|
|
20
|
+
skill: SkillConfig;
|
|
22
21
|
}
|
|
23
22
|
export interface WorkspaceConfig {
|
|
24
23
|
base_dir: string;
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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\
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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,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
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
package/dist/core/intent.d.ts
DELETED
|
@@ -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>;
|
package/dist/core/intent.js
DELETED
|
@@ -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
|
-
}
|