@emqo/claudebridge 0.1.6 → 0.1.8

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.
@@ -196,8 +196,11 @@ export class AgentEngine {
196
196
  args.push("--model", ep.model);
197
197
  const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
198
198
  child.stdin.end();
199
+ console.log(`[agent] auto-summary spawned pid=${child.pid} for ${userId}`);
199
200
  let result = "";
201
+ let cost = 0;
200
202
  let buffer = "";
203
+ let stderr = "";
201
204
  child.stdout.on("data", (data) => {
202
205
  buffer += data.toString();
203
206
  const lines = buffer.split("\n");
@@ -207,18 +210,37 @@ export class AgentEngine {
207
210
  continue;
208
211
  try {
209
212
  const msg = JSON.parse(line);
210
- if (msg.type === "result" && msg.result)
211
- result = msg.result;
213
+ if (msg.type === "result") {
214
+ if (msg.result)
215
+ result = msg.result;
216
+ if (msg.total_cost_usd)
217
+ cost = msg.total_cost_usd;
218
+ }
212
219
  }
213
220
  catch { }
214
221
  }
215
222
  });
216
- child.on("close", () => {
223
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
224
+ child.on("close", (code) => {
225
+ if (code !== 0) {
226
+ console.warn(`[agent] auto-summary failed code=${code} stderr=${stderr.slice(0, 200)} for ${userId}`);
227
+ }
228
+ if (cost > 0) {
229
+ this.store.recordUsage(userId, "auto-summary", cost);
230
+ console.log(`[agent] auto-summary cost=$${cost.toFixed(4)} for ${userId}`);
231
+ }
217
232
  if (result && !result.includes("NONE")) {
218
- this.store.addMemory(userId, result.trim(), "auto");
233
+ const saved = this.store.addMemory(userId, result.trim(), "auto");
219
234
  this.store.trimMemories(userId, this.config.agent.memory?.max_memories || 50);
220
- console.log(`[agent] auto-summary saved for ${userId}`);
235
+ if (saved)
236
+ console.log(`[agent] auto-summary saved for ${userId}`);
237
+ else
238
+ console.log(`[agent] auto-summary skipped (duplicate) for ${userId}`);
239
+ }
240
+ else {
241
+ console.log(`[agent] auto-summary result=NONE for ${userId}`);
221
242
  }
222
243
  });
244
+ child.on("error", (err) => { console.warn(`[agent] auto-summary spawn error: ${err.message}`); });
223
245
  }
224
246
  }
@@ -4,13 +4,25 @@ const patterns = [
4
4
  m => ({ type: "reminder", minutes: +m[1], description: m[2].trim() })],
5
5
  [/^(?:添加|加个?|创建|add|create)\s*(?:一个)?(?:任务|task)[::\s]*(.+)/i,
6
6
  m => ({ type: "task", description: m[1].trim() })],
7
- [/^(?:记住|记下|remember)\s*(?:that|this)?[::\s]*(.+)/i,
7
+ [/^(?:记住|记下|记忆|帮我记|remember)\s*(?:that|this)?[::\s]*(.+)/i,
8
8
  m => ({ type: "memory", description: m[1].trim() })],
9
9
  [/^(?:忘记所有|清除记忆|forget\s*all|clear\s*memo)/i,
10
10
  () => ({ type: "forget" })],
11
11
  [/^(?:新会话|新对话|new\s*session|clear\s*session)/i,
12
12
  () => ({ type: "clear_session" })],
13
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
+ }
14
26
  export function regexDetect(text) {
15
27
  for (const [re, fn] of patterns) {
16
28
  const m = text.match(re);
@@ -30,13 +42,20 @@ export function claudeDetect(text, rotator) {
30
42
  env.ANTHROPIC_API_KEY = ep.api_key;
31
43
  if (ep.base_url)
32
44
  env.ANTHROPIC_BASE_URL = ep.base_url;
33
- const prompt = `Classify the user's intent. Output ONLY one JSON line, no other text:
34
- {"type":"reminder","minutes":5,"description":"check server"}
35
- {"type":"task","description":"buy milk"}
36
- {"type":"memory","description":"I like TypeScript"}
37
- {"type":"none"}
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"}
38
52
 
39
- User: ${text.slice(0, 500)}`;
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)}`;
40
59
  const args = ["-p", prompt, "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.005"];
41
60
  if (ep.model)
42
61
  args.push("--model", ep.model);
@@ -81,7 +100,9 @@ export async function detectIntent(text, rotator, config) {
81
100
  const r = regexDetect(text);
82
101
  if (r.type !== "none")
83
102
  return r;
84
- if (config?.use_claude_fallback !== false)
103
+ // Only call Claude fallback if text contains hint keywords (saves cost)
104
+ if (config?.use_claude_fallback !== false && mightBeIntent(text)) {
85
105
  return claudeDetect(text, rotator);
106
+ }
86
107
  return { type: "none" };
87
108
  }
@@ -20,7 +20,7 @@ export declare class Store {
20
20
  content: string;
21
21
  created_at: number;
22
22
  }[];
23
- addMemory(userId: string, content: string, source?: string): void;
23
+ addMemory(userId: string, content: string, source?: string): boolean;
24
24
  getMemories(userId: string): {
25
25
  id: number;
26
26
  content: string;
@@ -101,7 +101,12 @@ export class Store {
101
101
  }
102
102
  // --- memories ---
103
103
  addMemory(userId, content, source = "manual") {
104
+ // Dedup: skip if identical content already exists for this user
105
+ const existing = this.db.prepare("SELECT id FROM memories WHERE user_id = ? AND content = ? LIMIT 1").get(userId, content);
106
+ if (existing)
107
+ return false;
104
108
  this.db.prepare("INSERT INTO memories (user_id, content, source, created_at) VALUES (?, ?, ?, ?)").run(userId, content, source, Date.now());
109
+ return true;
105
110
  }
106
111
  getMemories(userId) {
107
112
  return this.db.prepare("SELECT id, content, source, created_at FROM memories WHERE user_id = ? ORDER BY created_at DESC").all(userId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Bridge Claude Code Agent SDK to chat platforms (Telegram, Discord, etc.)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {