metame-cli 1.3.23 → 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.
- package/README.md +185 -26
- package/index.js +187 -141
- package/package.json +3 -3
- package/scripts/daemon-default.yaml +39 -1
- package/scripts/daemon.js +456 -104
- package/scripts/distill.js +40 -90
- package/scripts/feishu-adapter.js +61 -148
- package/scripts/memory-extract.js +263 -0
- package/scripts/memory-search.js +99 -0
- package/scripts/memory.js +439 -0
- package/scripts/providers.js +32 -0
- package/scripts/qmd-client.js +276 -0
- package/scripts/schema.js +37 -40
- package/scripts/session-analytics.js +64 -7
- package/scripts/session-summarize.js +118 -0
- package/scripts/skill-evolution.js +19 -16
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* memory.js — MetaMe Lightweight Session Memory
|
|
5
|
+
*
|
|
6
|
+
* SQLite + FTS5 keyword search, Node.js native (node:sqlite), zero deps.
|
|
7
|
+
* Stores distilled session summaries for cross-session recall.
|
|
8
|
+
*
|
|
9
|
+
* DB: ~/.metame/memory.db
|
|
10
|
+
*
|
|
11
|
+
* API:
|
|
12
|
+
* saveSession({ sessionId, project, summary, keywords, mood })
|
|
13
|
+
* searchSessions(query, { limit, project })
|
|
14
|
+
* recentSessions({ limit, project })
|
|
15
|
+
* getSession(sessionId)
|
|
16
|
+
* stats()
|
|
17
|
+
* close()
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
|
|
26
|
+
const DB_PATH = path.join(os.homedir(), '.metame', 'memory.db');
|
|
27
|
+
|
|
28
|
+
// Lazy-init: only open DB when first called
|
|
29
|
+
let _db = null;
|
|
30
|
+
|
|
31
|
+
function getDb() {
|
|
32
|
+
if (_db) return _db;
|
|
33
|
+
|
|
34
|
+
const dir = path.dirname(DB_PATH);
|
|
35
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
38
|
+
_db = new DatabaseSync(DB_PATH);
|
|
39
|
+
|
|
40
|
+
_db.exec('PRAGMA journal_mode = WAL');
|
|
41
|
+
_db.exec('PRAGMA busy_timeout = 3000');
|
|
42
|
+
|
|
43
|
+
// Core table
|
|
44
|
+
_db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
project TEXT NOT NULL,
|
|
48
|
+
summary TEXT NOT NULL,
|
|
49
|
+
keywords TEXT DEFAULT '',
|
|
50
|
+
mood TEXT DEFAULT '',
|
|
51
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
+
token_cost INTEGER DEFAULT 0
|
|
53
|
+
)
|
|
54
|
+
`);
|
|
55
|
+
|
|
56
|
+
// FTS5 index for keyword search over summary + keywords
|
|
57
|
+
try {
|
|
58
|
+
_db.exec(`
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
60
|
+
summary, keywords, project,
|
|
61
|
+
content='sessions',
|
|
62
|
+
content_rowid='rowid',
|
|
63
|
+
tokenize='trigram'
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
} catch {
|
|
67
|
+
// FTS table may already exist with different schema on upgrade
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Triggers to keep FTS in sync
|
|
71
|
+
const triggers = [
|
|
72
|
+
`CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
|
|
73
|
+
INSERT INTO sessions_fts(rowid, summary, keywords, project)
|
|
74
|
+
VALUES (new.rowid, new.summary, new.keywords, new.project);
|
|
75
|
+
END`,
|
|
76
|
+
`CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions BEGIN
|
|
77
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords, project)
|
|
78
|
+
VALUES ('delete', old.rowid, old.summary, old.keywords, old.project);
|
|
79
|
+
END`,
|
|
80
|
+
`CREATE TRIGGER IF NOT EXISTS sessions_au AFTER UPDATE ON sessions BEGIN
|
|
81
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords, project)
|
|
82
|
+
VALUES ('delete', old.rowid, old.summary, old.keywords, old.project);
|
|
83
|
+
INSERT INTO sessions_fts(rowid, summary, keywords, project)
|
|
84
|
+
VALUES (new.rowid, new.summary, new.keywords, new.project);
|
|
85
|
+
END`,
|
|
86
|
+
];
|
|
87
|
+
for (const t of triggers) {
|
|
88
|
+
try { _db.exec(t); } catch { /* trigger may already exist */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// ── Facts table: atomic knowledge triples ──
|
|
93
|
+
_db.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
entity TEXT NOT NULL,
|
|
97
|
+
relation TEXT NOT NULL,
|
|
98
|
+
value TEXT NOT NULL,
|
|
99
|
+
confidence TEXT NOT NULL DEFAULT 'medium',
|
|
100
|
+
source_type TEXT NOT NULL DEFAULT 'session',
|
|
101
|
+
source_id TEXT,
|
|
102
|
+
project TEXT NOT NULL DEFAULT '*',
|
|
103
|
+
tags TEXT DEFAULT '[]',
|
|
104
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
105
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
106
|
+
superseded_by TEXT
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
// FTS5 index for facts (separate from sessions_fts, zero compatibility risk)
|
|
111
|
+
try {
|
|
112
|
+
_db.exec(`
|
|
113
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5(
|
|
114
|
+
entity, relation, value, tags,
|
|
115
|
+
content='facts',
|
|
116
|
+
content_rowid='rowid',
|
|
117
|
+
tokenize='trigram'
|
|
118
|
+
)
|
|
119
|
+
`);
|
|
120
|
+
} catch { /* already exists */ }
|
|
121
|
+
|
|
122
|
+
// Triggers to keep facts_fts in sync
|
|
123
|
+
const factTriggers = [
|
|
124
|
+
`CREATE TRIGGER IF NOT EXISTS facts_ai AFTER INSERT ON facts BEGIN
|
|
125
|
+
INSERT INTO facts_fts(rowid, entity, relation, value, tags)
|
|
126
|
+
VALUES (new.rowid, new.entity, new.relation, new.value, new.tags);
|
|
127
|
+
END`,
|
|
128
|
+
`CREATE TRIGGER IF NOT EXISTS facts_ad AFTER DELETE ON facts BEGIN
|
|
129
|
+
INSERT INTO facts_fts(facts_fts, rowid, entity, relation, value, tags)
|
|
130
|
+
VALUES ('delete', old.rowid, old.entity, old.relation, old.value, old.tags);
|
|
131
|
+
END`,
|
|
132
|
+
`CREATE TRIGGER IF NOT EXISTS facts_au AFTER UPDATE ON facts BEGIN
|
|
133
|
+
INSERT INTO facts_fts(facts_fts, rowid, entity, relation, value, tags)
|
|
134
|
+
VALUES ('delete', old.rowid, old.entity, old.relation, old.value, old.tags);
|
|
135
|
+
INSERT INTO facts_fts(rowid, entity, relation, value, tags)
|
|
136
|
+
VALUES (new.rowid, new.entity, new.relation, new.value, new.tags);
|
|
137
|
+
END`,
|
|
138
|
+
];
|
|
139
|
+
for (const t of factTriggers) {
|
|
140
|
+
try { _db.exec(t); } catch { /* trigger may already exist */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Indexes
|
|
144
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
|
|
145
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
|
|
146
|
+
|
|
147
|
+
return _db;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Save a distilled session summary.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} opts
|
|
154
|
+
* @param {string} opts.sessionId - Claude session ID (unique key)
|
|
155
|
+
* @param {string} opts.project - Project key (e.g. 'metame', 'desktop')
|
|
156
|
+
* @param {string} opts.summary - Distilled summary text
|
|
157
|
+
* @param {string} [opts.keywords] - Comma-separated keywords for search boost
|
|
158
|
+
* @param {string} [opts.mood] - User mood/sentiment detected
|
|
159
|
+
* @param {number} [opts.tokenCost] - Approximate token cost of the session
|
|
160
|
+
* @returns {{ ok: boolean, id: string }}
|
|
161
|
+
*/
|
|
162
|
+
function saveSession({ sessionId, project, summary, keywords = '', mood = '', tokenCost = 0 }) {
|
|
163
|
+
if (!sessionId || !project || !summary) {
|
|
164
|
+
throw new Error('saveSession requires sessionId, project, summary');
|
|
165
|
+
}
|
|
166
|
+
const db = getDb();
|
|
167
|
+
const stmt = db.prepare(`
|
|
168
|
+
INSERT INTO sessions (id, project, summary, keywords, mood, token_cost)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
171
|
+
summary = excluded.summary,
|
|
172
|
+
keywords = excluded.keywords,
|
|
173
|
+
mood = excluded.mood,
|
|
174
|
+
token_cost = excluded.token_cost
|
|
175
|
+
`);
|
|
176
|
+
stmt.run(sessionId, project, summary.slice(0, 10000), keywords.slice(0, 1000), mood.slice(0, 100), tokenCost);
|
|
177
|
+
return { ok: true, id: sessionId };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Save atomic facts extracted from a session.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} sessionId - Source session ID
|
|
184
|
+
* @param {string} project - Project key ('metame', 'desktop', '*' for global)
|
|
185
|
+
* @param {Array} facts - Array of { entity, relation, value, confidence, tags }
|
|
186
|
+
* @returns {{ saved: number, skipped: number }}
|
|
187
|
+
*/
|
|
188
|
+
function saveFacts(sessionId, project, facts) {
|
|
189
|
+
if (!Array.isArray(facts) || facts.length === 0) return { saved: 0, skipped: 0 };
|
|
190
|
+
const db = getDb();
|
|
191
|
+
|
|
192
|
+
// Load existing facts for dedup check
|
|
193
|
+
const existing = db.prepare(
|
|
194
|
+
"SELECT entity, relation, value FROM facts WHERE project IN (?, '*')"
|
|
195
|
+
).all(project);
|
|
196
|
+
|
|
197
|
+
const insert = db.prepare(`
|
|
198
|
+
INSERT INTO facts (id, entity, relation, value, confidence, source_type, source_id, project, tags, created_at, updated_at)
|
|
199
|
+
VALUES (?, ?, ?, ?, ?, 'session', ?, ?, ?, datetime('now'), datetime('now'))
|
|
200
|
+
ON CONFLICT(id) DO NOTHING
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
let saved = 0;
|
|
204
|
+
let skipped = 0;
|
|
205
|
+
const savedFacts = [];
|
|
206
|
+
|
|
207
|
+
for (const f of facts) {
|
|
208
|
+
// Basic validation
|
|
209
|
+
if (!f.entity || !f.relation || !f.value) { skipped++; continue; }
|
|
210
|
+
if (f.value.length < 20 || f.value.length > 300) { skipped++; continue; }
|
|
211
|
+
|
|
212
|
+
// Dedup: same entity+relation with similar value prefix
|
|
213
|
+
const dupKey = `${f.entity}::${f.relation}`;
|
|
214
|
+
const prefix = f.value.slice(0, 50);
|
|
215
|
+
const isDup = existing.some(e =>
|
|
216
|
+
`${e.entity}::${e.relation}` === dupKey && e.value.slice(0, 50) === prefix
|
|
217
|
+
);
|
|
218
|
+
if (isDup) { skipped++; continue; }
|
|
219
|
+
|
|
220
|
+
const id = `f-${sessionId.slice(0, 8)}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
221
|
+
const tags = JSON.stringify(Array.isArray(f.tags) ? f.tags.slice(0, 3) : []);
|
|
222
|
+
try {
|
|
223
|
+
insert.run(id, f.entity, f.relation, f.value.slice(0, 300),
|
|
224
|
+
f.confidence || 'medium', sessionId, project === '*' ? '*' : project, tags);
|
|
225
|
+
savedFacts.push({ id, entity: f.entity, relation: f.relation, value: f.value,
|
|
226
|
+
project: project === '*' ? '*' : project, tags: f.tags || [], created_at: new Date().toISOString() });
|
|
227
|
+
saved++;
|
|
228
|
+
} catch { skipped++; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Async sync to QMD (non-blocking, non-fatal)
|
|
232
|
+
if (savedFacts.length > 0) {
|
|
233
|
+
let qmdClient = null;
|
|
234
|
+
try { qmdClient = require('./qmd-client'); } catch { /* qmd-client not available */ }
|
|
235
|
+
if (qmdClient) qmdClient.upsertFacts(savedFacts);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { saved, skipped };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Search facts: QMD hybrid search (if available) → FTS5 → LIKE fallback.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} query - Search keywords / natural language
|
|
245
|
+
* @param {object} [opts]
|
|
246
|
+
* @param {number} [opts.limit=5] - Max results
|
|
247
|
+
* @param {string} [opts.project] - Filter by project (also always includes '*')
|
|
248
|
+
* @returns {Promise<Array>|Array} Fact objects
|
|
249
|
+
*/
|
|
250
|
+
async function searchFactsAsync(query, { limit = 5, project = null } = {}) {
|
|
251
|
+
// Try QMD hybrid search first
|
|
252
|
+
let qmdClient = null;
|
|
253
|
+
try { qmdClient = require('./qmd-client'); } catch { /* not available */ }
|
|
254
|
+
|
|
255
|
+
if (qmdClient && qmdClient.isAvailable()) {
|
|
256
|
+
try {
|
|
257
|
+
const ids = await qmdClient.search(query, limit * 2); // fetch extra for project filter
|
|
258
|
+
if (ids && ids.length > 0) {
|
|
259
|
+
const db = getDb();
|
|
260
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
261
|
+
let rows = db.prepare(
|
|
262
|
+
`SELECT id, entity, relation, value, confidence, project, tags, created_at
|
|
263
|
+
FROM facts WHERE id IN (${placeholders}) AND superseded_by IS NULL`
|
|
264
|
+
).all(...ids);
|
|
265
|
+
|
|
266
|
+
// Apply project filter
|
|
267
|
+
if (project) {
|
|
268
|
+
rows = rows.filter(r => r.project === project || r.project === '*');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Preserve QMD ranking order
|
|
272
|
+
const idOrder = new Map(ids.map((id, i) => [id, i]));
|
|
273
|
+
rows.sort((a, b) => (idOrder.get(a.id) ?? 999) - (idOrder.get(b.id) ?? 999));
|
|
274
|
+
|
|
275
|
+
if (rows.length > 0) return rows.slice(0, limit);
|
|
276
|
+
}
|
|
277
|
+
} catch { /* QMD failed, fall through to FTS5 */ }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return searchFacts(query, { limit, project });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Search facts by keyword (FTS5 + LIKE fallback). Synchronous.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} query - Search keywords
|
|
287
|
+
* @param {object} [opts]
|
|
288
|
+
* @param {number} [opts.limit=5] - Max results
|
|
289
|
+
* @param {string} [opts.project] - Filter by project (also always includes '*')
|
|
290
|
+
* @returns {Array<{ id, entity, relation, value, confidence, project, tags, created_at }>}
|
|
291
|
+
*/
|
|
292
|
+
function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
293
|
+
if (!query || !query.trim()) return [];
|
|
294
|
+
const db = getDb();
|
|
295
|
+
|
|
296
|
+
const sanitized = query.trim().split(/\s+/)
|
|
297
|
+
.map(t => '"' + t.replace(/"/g, '') + '"').join(' ');
|
|
298
|
+
|
|
299
|
+
// FTS5 path
|
|
300
|
+
try {
|
|
301
|
+
let sql, params;
|
|
302
|
+
if (project) {
|
|
303
|
+
sql = `
|
|
304
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.tags, f.created_at, rank
|
|
305
|
+
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
306
|
+
WHERE facts_fts MATCH ? AND (f.project = ? OR f.project = '*') AND f.superseded_by IS NULL
|
|
307
|
+
ORDER BY rank LIMIT ?
|
|
308
|
+
`;
|
|
309
|
+
params = [sanitized, project, limit];
|
|
310
|
+
} else {
|
|
311
|
+
sql = `
|
|
312
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.tags, f.created_at, rank
|
|
313
|
+
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
314
|
+
WHERE facts_fts MATCH ? AND f.superseded_by IS NULL
|
|
315
|
+
ORDER BY rank LIMIT ?
|
|
316
|
+
`;
|
|
317
|
+
params = [sanitized, limit];
|
|
318
|
+
}
|
|
319
|
+
const ftsResults = db.prepare(sql).all(...params);
|
|
320
|
+
if (ftsResults.length > 0) return ftsResults;
|
|
321
|
+
} catch { /* FTS error, fall through */ }
|
|
322
|
+
|
|
323
|
+
// LIKE fallback
|
|
324
|
+
const like = '%' + query.trim() + '%';
|
|
325
|
+
const likeSql = project
|
|
326
|
+
? `SELECT id, entity, relation, value, confidence, project, tags, created_at
|
|
327
|
+
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
328
|
+
AND (project = ? OR project = '*') AND superseded_by IS NULL
|
|
329
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
330
|
+
: `SELECT id, entity, relation, value, confidence, project, tags, created_at
|
|
331
|
+
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
332
|
+
AND superseded_by IS NULL
|
|
333
|
+
ORDER BY created_at DESC LIMIT ?`;
|
|
334
|
+
return project
|
|
335
|
+
? db.prepare(likeSql).all(like, like, like, project, limit)
|
|
336
|
+
: db.prepare(likeSql).all(like, like, like, limit);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Search sessions by keyword (FTS5 match).
|
|
341
|
+
*
|
|
342
|
+
* @param {string} query - Search query (FTS5 syntax supported)
|
|
343
|
+
* @param {object} [opts]
|
|
344
|
+
* @param {number} [opts.limit=5] - Max results
|
|
345
|
+
* @param {string} [opts.project] - Filter by project
|
|
346
|
+
* @returns {Array<{ id, project, summary, keywords, mood, created_at, rank }>}
|
|
347
|
+
*/
|
|
348
|
+
function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
349
|
+
if (!query || !query.trim()) return [];
|
|
350
|
+
const db = getDb();
|
|
351
|
+
|
|
352
|
+
// Sanitize: wrap each term in quotes to prevent FTS5 syntax errors
|
|
353
|
+
const sanitized = query.trim().split(/\s+/).map(t => '"' + t.replace(/"/g, '') + '"').join(' ');
|
|
354
|
+
|
|
355
|
+
let sql, params;
|
|
356
|
+
if (project) {
|
|
357
|
+
sql = `
|
|
358
|
+
SELECT s.id, s.project, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
359
|
+
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
360
|
+
WHERE sessions_fts MATCH ? AND s.project = ?
|
|
361
|
+
ORDER BY rank LIMIT ?
|
|
362
|
+
`;
|
|
363
|
+
params = [sanitized, project, limit];
|
|
364
|
+
} else {
|
|
365
|
+
sql = `
|
|
366
|
+
SELECT s.id, s.project, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
367
|
+
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
368
|
+
WHERE sessions_fts MATCH ?
|
|
369
|
+
ORDER BY rank LIMIT ?
|
|
370
|
+
`;
|
|
371
|
+
params = [sanitized, limit];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Try FTS first, fall back to LIKE if FTS errors OR returns 0 (e.g. short CJK queries < 3 chars)
|
|
375
|
+
let ftsResults = [];
|
|
376
|
+
try { ftsResults = db.prepare(sql).all(...params); } catch { /* FTS syntax error */ }
|
|
377
|
+
if (ftsResults.length > 0) return ftsResults;
|
|
378
|
+
|
|
379
|
+
// LIKE fallback (handles short CJK terms like "飞书" that trigram can't match)
|
|
380
|
+
const likeParam = '%' + query.trim() + '%';
|
|
381
|
+
const likeSql = project
|
|
382
|
+
? 'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions WHERE (summary LIKE ? OR keywords LIKE ?) AND project = ? ORDER BY created_at DESC LIMIT ?'
|
|
383
|
+
: 'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions WHERE (summary LIKE ? OR keywords LIKE ?) ORDER BY created_at DESC LIMIT ?';
|
|
384
|
+
return project
|
|
385
|
+
? db.prepare(likeSql).all(likeParam, likeParam, project, limit)
|
|
386
|
+
: db.prepare(likeSql).all(likeParam, likeParam, limit);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get most recent sessions.
|
|
391
|
+
*
|
|
392
|
+
* @param {object} [opts]
|
|
393
|
+
* @param {number} [opts.limit=3] - Max results
|
|
394
|
+
* @param {string} [opts.project] - Filter by project
|
|
395
|
+
* @returns {Array<{ id, project, summary, keywords, mood, created_at }>}
|
|
396
|
+
*/
|
|
397
|
+
function recentSessions({ limit = 3, project = null } = {}) {
|
|
398
|
+
const db = getDb();
|
|
399
|
+
if (project) {
|
|
400
|
+
return db.prepare(
|
|
401
|
+
'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions WHERE project = ? ORDER BY created_at DESC LIMIT ?'
|
|
402
|
+
).all(project, limit);
|
|
403
|
+
}
|
|
404
|
+
return db.prepare(
|
|
405
|
+
'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions ORDER BY created_at DESC LIMIT ?'
|
|
406
|
+
).all(limit);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get a single session by ID.
|
|
411
|
+
* @param {string} sessionId
|
|
412
|
+
* @returns {object|null}
|
|
413
|
+
*/
|
|
414
|
+
function getSession(sessionId) {
|
|
415
|
+
const db = getDb();
|
|
416
|
+
return db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) || null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get total memory stats.
|
|
421
|
+
* @returns {{ count, dbSizeKB, oldestDate, newestDate }}
|
|
422
|
+
*/
|
|
423
|
+
function stats() {
|
|
424
|
+
const db = getDb();
|
|
425
|
+
const row = db.prepare('SELECT COUNT(*) as count, MIN(created_at) as oldest, MAX(created_at) as newest FROM sessions').get();
|
|
426
|
+
const factsRow = db.prepare('SELECT COUNT(*) as count FROM facts WHERE superseded_by IS NULL').get();
|
|
427
|
+
let dbSizeKB = 0;
|
|
428
|
+
try { dbSizeKB = Math.round(fs.statSync(DB_PATH).size / 1024); } catch { /* */ }
|
|
429
|
+
return { count: row.count, facts: factsRow.count, dbSizeKB, oldestDate: row.oldest || null, newestDate: row.newest || null };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Close the database connection (for clean shutdown).
|
|
434
|
+
*/
|
|
435
|
+
function close() {
|
|
436
|
+
if (_db) { _db.close(); _db = null; }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
module.exports = { saveSession, saveFacts, searchFacts, searchFactsAsync, searchSessions, recentSessions, getSession, stats, close, DB_PATH };
|
package/scripts/providers.js
CHANGED
|
@@ -208,6 +208,37 @@ function listFormatted() {
|
|
|
208
208
|
return lines.join('\n');
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// ---------------------------------------------------------
|
|
212
|
+
// Claude subprocess helper (shared by distill.js + skill-evolution.js)
|
|
213
|
+
// ---------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* Call `claude -p --model haiku` as a subprocess with extra env vars.
|
|
216
|
+
* Deletes CLAUDECODE from env to prevent recursive session detection.
|
|
217
|
+
*/
|
|
218
|
+
function callHaiku(input, extraEnv, timeout) {
|
|
219
|
+
const { execFile } = require('child_process');
|
|
220
|
+
const env = { ...process.env, ...extraEnv };
|
|
221
|
+
delete env.CLAUDECODE;
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const proc = execFile(
|
|
224
|
+
'claude',
|
|
225
|
+
['-p', '--model', 'haiku', '--no-session-persistence'],
|
|
226
|
+
{ env, timeout, maxBuffer: 10 * 1024 * 1024 },
|
|
227
|
+
(err, stdout, stderr) => {
|
|
228
|
+
if (err) {
|
|
229
|
+
const detail = (stderr || stdout || '').trim().split('\n')[0];
|
|
230
|
+
err.message = detail || err.message;
|
|
231
|
+
err.stdout = stdout;
|
|
232
|
+
err.stderr = stderr;
|
|
233
|
+
reject(err);
|
|
234
|
+
} else resolve(stdout.trim());
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
proc.stdin.write(input);
|
|
238
|
+
proc.stdin.end();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
211
242
|
// ---------------------------------------------------------
|
|
212
243
|
// EXPORTS
|
|
213
244
|
// ---------------------------------------------------------
|
|
@@ -226,5 +257,6 @@ module.exports = {
|
|
|
226
257
|
removeProvider,
|
|
227
258
|
setRole,
|
|
228
259
|
listFormatted,
|
|
260
|
+
callHaiku,
|
|
229
261
|
PROVIDERS_FILE,
|
|
230
262
|
};
|