metame-cli 1.3.22 → 1.4.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.
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * memory-extract.js — Independent Memory Extraction
5
+ *
6
+ * Scans unanalyzed Claude Code sessions and extracts atomic facts
7
+ * into memory.db. Runs independently of raw_signals.jsonl so that
8
+ * pure technical sessions (no preference signals) are still captured.
9
+ *
10
+ * Designed to run as a standalone heartbeat task every 30 minutes.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const { callHaiku, buildDistillEnv } = require('./providers');
19
+
20
+ const HOME = os.homedir();
21
+ const LOCK_FILE = path.join(HOME, '.metame', 'memory-extract.lock');
22
+
23
+ // Atomic fact extraction prompt (local copy — distill.js no longer exports this)
24
+ const FACT_EXTRACTION_PROMPT = `你是精准的知识提取引擎。从以下会话骨架中提取「值得长期记住的原子事实」。
25
+
26
+ 提取类型(必须是以下之一):
27
+ - tech_decision(技术决策:为什么选A不选B)
28
+ - bug_lesson(Bug根因:什么设计/假设导致了问题)
29
+ - arch_convention(架构约定:系统组件的行为边界)
30
+ - config_fact(配置事实:某个值的真实含义,尤其反直觉的)
31
+ - user_pref(用户明确表达的偏好/红线)
32
+ - workflow_rule(工作流戒律:如“不要在某情况下做某事”的反常识流)
33
+ - project_milestone(项目里程碑:主要架构重构、版本发布等跨会话级成果)
34
+
35
+ 绝对不提取:
36
+ - 过程性描述("用户问了X"、"我们讨论了Y")
37
+ - 临时状态("当前正在..."、"这次会话...")
38
+ - 未经验证的猜测("可能是因为..."、"也许...")
39
+ - 显而易见的常识
40
+
41
+ 输出 JSON 对象,包含会话名称和提取的事实:
42
+ {
43
+ "session_name": "用3-5个词极其精简地概括这起会话的主题(例如:优化微信登录架构、排查Redis连接泄漏、配置Nginx反向代理)",
44
+ "facts": [
45
+ {"entity":"主体(点号层级如MetaMe.daemon.askClaude)","relation":"类型","value":"脱离上下文可独立理解的一句话","confidence":"high或medium","tags":["最多3个标签"]}
46
+ ]
47
+ }
48
+
49
+ 规则:
50
+ - 宁缺毋滥:0条比10条废话好
51
+ - value必须包含足够上下文,不能写"这个问题"、"上面说的"
52
+ - value长度20-200字
53
+ - entity用英文点号路径,value可用中文
54
+ - medium confidence必须有非空tags
55
+ - 没有值得提取的事实时 facts 返回 []
56
+
57
+ 只输出JSON对象,不要解释。
58
+
59
+ 会话骨架:
60
+ {{SKELETON}}`.trim();
61
+
62
+ const SESSION_TAGS_FILE = path.join(os.homedir(), '.metame', 'session_tags.json');
63
+
64
+ /**
65
+ * Persist session name and derived tags to ~/.metame/session_tags.json.
66
+ * Merges into existing file — never overwrites existing entries.
67
+ */
68
+ function saveSessionTag(sessionId, sessionName, facts) {
69
+ // Derive tags from facts' tags arrays (deduplicated, max 8)
70
+ const tagSet = new Set();
71
+ for (const f of facts) {
72
+ if (Array.isArray(f.tags)) f.tags.forEach(t => tagSet.add(t));
73
+ }
74
+ const tags = [...tagSet].slice(0, 8);
75
+
76
+ let existing = {};
77
+ try {
78
+ if (fs.existsSync(SESSION_TAGS_FILE)) {
79
+ existing = JSON.parse(fs.readFileSync(SESSION_TAGS_FILE, 'utf8'));
80
+ }
81
+ } catch { existing = {}; }
82
+
83
+ // Only update if not already present (never overwrite)
84
+ if (!existing[sessionId]) {
85
+ existing[sessionId] = {
86
+ name: sessionName,
87
+ tags,
88
+ extracted_at: new Date().toISOString(),
89
+ };
90
+ try {
91
+ fs.mkdirSync(path.dirname(SESSION_TAGS_FILE), { recursive: true });
92
+ fs.writeFileSync(SESSION_TAGS_FILE, JSON.stringify(existing, null, 2), 'utf8');
93
+ } catch (e) {
94
+ console.log(`[memory-extract] Failed to save session tag: ${e.message}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ const VAGUE_PATTERNS = [
100
+ /^用户(问|提|说|提到)/, /^我们(讨论|分析|查看)/,
101
+ /这个问题/, /上面(提到|说的|的)/, /可能是因为/,
102
+ /也许|或许|大概/, /当前正在|目前在/, /这次会话/,
103
+ ];
104
+ const ALLOWED_FLAT = new Set(['王总', 'system', 'user']);
105
+
106
+ /**
107
+ * Extract atomic facts from a session skeleton via Haiku.
108
+ * Returns filtered fact array (may be empty).
109
+ */
110
+ async function extractFacts(skeleton, sessionSummary, distillEnv) {
111
+ const skeletonText = JSON.stringify({ skeleton, sessionSummary }, null, 2).slice(0, 3000);
112
+ const prompt = FACT_EXTRACTION_PROMPT.replace('{{SKELETON}}', skeletonText);
113
+
114
+ let raw;
115
+ try {
116
+ raw = await Promise.race([
117
+ callHaiku(prompt, distillEnv, 60000),
118
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
119
+ ]);
120
+ } catch (e) {
121
+ console.log(`[memory-extract] Haiku call failed: ${e.message} | code:${e.code} killed:${e.killed} stdout:${String(e.stdout || '').slice(0, 100)} stderr:${String(e.stderr || '').slice(0, 100)}`);
122
+ return [];
123
+ }
124
+
125
+ let parsed;
126
+ try {
127
+ const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
128
+ parsed = JSON.parse(cleaned);
129
+ } catch {
130
+ return { facts: [], session_name: "未命名会话" };
131
+ }
132
+
133
+ let facts = Array.isArray(parsed.facts) ? parsed.facts : [];
134
+ const session_name = parsed.session_name || "未命名会话";
135
+
136
+ const filteredFacts = facts.filter(f => {
137
+ if (!f.entity || !f.relation || !f.value) return false;
138
+ if (f.value.length < 20 || f.value.length > 300) return false;
139
+ if (VAGUE_PATTERNS.some(re => re.test(f.value))) return false;
140
+ if (!f.entity.includes('.') && !ALLOWED_FLAT.has(f.entity)) return false;
141
+ if (f.confidence === 'medium' && (!f.tags || f.tags.length === 0)) return false;
142
+ return true;
143
+ });
144
+
145
+ return { facts: filteredFacts, session_name };
146
+ }
147
+
148
+ /**
149
+ * Main entry: scan all unanalyzed sessions and extract facts.
150
+ * Returns { sessionsProcessed, factsSaved, factsSkipped }
151
+ */
152
+ async function run() {
153
+ // Atomic lock — prevent concurrent extraction (O_EXCL guarantees no race)
154
+ let lockFd;
155
+ try {
156
+ lockFd = fs.openSync(LOCK_FILE, 'wx');
157
+ fs.writeSync(lockFd, process.pid.toString());
158
+ fs.closeSync(lockFd);
159
+ } catch (e) {
160
+ if (e.code === 'EEXIST') {
161
+ try {
162
+ const lockAge = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
163
+ if (lockAge < 300000) { // 5 min timeout for extraction
164
+ console.log('[memory-extract] Already running, skipping.');
165
+ return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
166
+ }
167
+ fs.unlinkSync(LOCK_FILE);
168
+ lockFd = fs.openSync(LOCK_FILE, 'wx');
169
+ fs.writeSync(lockFd, process.pid.toString());
170
+ fs.closeSync(lockFd);
171
+ } catch {
172
+ console.log('[memory-extract] Already running, skipping.');
173
+ return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
174
+ }
175
+ } else {
176
+ throw e;
177
+ }
178
+ }
179
+
180
+ let sessionAnalytics;
181
+ try {
182
+ sessionAnalytics = require('./session-analytics');
183
+ } catch {
184
+ console.log('[memory-extract] session-analytics not available, exiting.');
185
+ return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
186
+ }
187
+
188
+ let memory;
189
+ try {
190
+ memory = require('./memory');
191
+ } catch {
192
+ console.log('[memory-extract] memory module not available, exiting.');
193
+ return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
194
+ }
195
+
196
+ try {
197
+ let distillEnv = {};
198
+ try { distillEnv = buildDistillEnv(); } catch { }
199
+
200
+ const sessions = sessionAnalytics.findAllUnextractedSessions(8);
201
+ if (sessions.length === 0) {
202
+ console.log('[memory-extract] No unanalyzed sessions found.');
203
+ memory.close();
204
+ return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
205
+ }
206
+
207
+ let totalSaved = 0;
208
+ let totalSkipped = 0;
209
+ let processed = 0;
210
+
211
+ for (const session of sessions) {
212
+ try {
213
+ const skeleton = sessionAnalytics.extractSkeleton(session.path);
214
+
215
+ // Skip trivial sessions
216
+ if (skeleton.message_count < 2 && skeleton.duration_min < 1) {
217
+ sessionAnalytics.markFactsExtracted(skeleton.session_id);
218
+ continue;
219
+ }
220
+
221
+ const { facts, session_name } = await extractFacts(skeleton, null, distillEnv);
222
+
223
+ if (facts.length > 0) {
224
+ const { saved, skipped } = memory.saveFacts(
225
+ skeleton.session_id,
226
+ skeleton.project || 'unknown',
227
+ facts
228
+ );
229
+ totalSaved += saved;
230
+ totalSkipped += skipped;
231
+ console.log(`[memory-extract] Session ${skeleton.session_id.slice(0, 8)}: ${saved} facts saved, ${skipped} skipped`);
232
+ } else {
233
+ console.log(`[memory-extract] Session ${skeleton.session_id.slice(0, 8)} (${session_name}): no facts extracted`);
234
+ }
235
+
236
+ sessionAnalytics.markFactsExtracted(skeleton.session_id);
237
+
238
+ // P2-A: persist session name + tags to session_tags.json
239
+ saveSessionTag(skeleton.session_id, session_name, facts);
240
+
241
+ processed++;
242
+ } catch (e) {
243
+ console.log(`[memory-extract] Session error: ${e.message}`);
244
+ }
245
+ }
246
+
247
+ memory.close();
248
+ return { sessionsProcessed: processed, factsSaved: totalSaved, factsSkipped: totalSkipped };
249
+ } finally {
250
+ try { fs.unlinkSync(LOCK_FILE); } catch { }
251
+ }
252
+ }
253
+
254
+ if (require.main === module) {
255
+ run().then(({ sessionsProcessed, factsSaved, factsSkipped }) => {
256
+ console.log(`✅ memory-extract: ${sessionsProcessed} session(s), ${factsSaved} facts saved, ${factsSkipped} skipped`);
257
+ }).catch(e => {
258
+ console.error(`[memory-extract] Fatal: ${e.message}`);
259
+ process.exit(1);
260
+ });
261
+ }
262
+
263
+ module.exports = { run, extractFacts };
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * memory-search.js — Cross-session memory recall CLI
4
+ *
5
+ * Usage:
6
+ * node memory-search.js "<query>" # search both sessions and facts
7
+ * node memory-search.js --facts "<query>" # search facts only
8
+ * node memory-search.js --sessions "<query>" # search sessions only
9
+ * node memory-search.js --recent # show recent sessions
10
+ *
11
+ * Called by Claude via Bash tool when it needs to recall past knowledge.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ // Support both local dev and installed (~/.metame/) paths
20
+ const memoryPath = [
21
+ path.join(os.homedir(), '.metame', 'memory.js'),
22
+ path.join(__dirname, 'memory.js'),
23
+ ].find(p => { try { require.resolve(p); return true; } catch { return false; } });
24
+
25
+ if (!memoryPath) {
26
+ console.log('[]');
27
+ process.exit(0);
28
+ }
29
+
30
+ const memory = require(memoryPath);
31
+
32
+ const args = process.argv.slice(2);
33
+ const mode = args[0] && args[0].startsWith('--') ? args[0] : null;
34
+ const query = mode ? args[1] : args[0];
35
+
36
+ try {
37
+ if (mode === '--recent') {
38
+ const rows = memory.recentSessions({ limit: 5 });
39
+ console.log(JSON.stringify(rows.map(r => ({
40
+ type: 'session',
41
+ project: r.project,
42
+ date: r.created_at,
43
+ summary: r.summary,
44
+ })), null, 2));
45
+
46
+ } else if (mode === '--facts') {
47
+ if (!query) { console.log('[]'); process.exit(0); }
48
+ const facts = memory.searchFacts(query, { limit: 5 });
49
+ console.log(JSON.stringify(facts.map(f => ({
50
+ type: 'fact',
51
+ entity: f.entity,
52
+ relation: f.relation,
53
+ value: f.value,
54
+ confidence: f.confidence,
55
+ date: f.created_at,
56
+ })), null, 2));
57
+
58
+ } else if (mode === '--sessions') {
59
+ if (!query) { console.log('[]'); process.exit(0); }
60
+ const sessions = memory.searchSessions(query, { limit: 5 });
61
+ console.log(JSON.stringify(sessions.map(s => ({
62
+ type: 'session',
63
+ project: s.project,
64
+ date: s.created_at,
65
+ summary: s.summary,
66
+ })), null, 2));
67
+
68
+ } else {
69
+ // Default: search both facts and sessions
70
+ if (!query) { console.log('[]'); process.exit(0); }
71
+ const facts = (typeof memory.searchFacts === 'function')
72
+ ? memory.searchFacts(query, { limit: 3 })
73
+ : [];
74
+ const sessions = memory.searchSessions(query, { limit: 3 });
75
+
76
+ const results = [
77
+ ...facts.map(f => ({
78
+ type: 'fact',
79
+ entity: f.entity,
80
+ relation: f.relation,
81
+ value: f.value,
82
+ confidence: f.confidence,
83
+ date: f.created_at,
84
+ })),
85
+ ...sessions.map(s => ({
86
+ type: 'session',
87
+ project: s.project,
88
+ date: s.created_at,
89
+ summary: s.summary,
90
+ })),
91
+ ];
92
+
93
+ console.log(JSON.stringify(results, null, 2));
94
+ }
95
+ } catch (e) {
96
+ console.log('[]');
97
+ } finally {
98
+ try { memory.close(); } catch {}
99
+ }