metame-cli 1.4.15 → 1.4.18
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 +9 -6
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +441 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +71 -22
- package/scripts/daemon-command-router.js +242 -3
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +216 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +374 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +288 -38
- package/scripts/skill-evolution.test.js +107 -0
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
package/scripts/memory.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* DB: ~/.metame/memory.db
|
|
10
10
|
*
|
|
11
11
|
* API:
|
|
12
|
-
* saveSession({ sessionId, project, summary, keywords, mood })
|
|
13
|
-
* searchSessions(query, { limit, project })
|
|
14
|
-
* recentSessions({ limit, project })
|
|
12
|
+
* saveSession({ sessionId, project, scope, summary, keywords, mood })
|
|
13
|
+
* searchSessions(query, { limit, project, scope })
|
|
14
|
+
* recentSessions({ limit, project, scope })
|
|
15
15
|
* getSession(sessionId)
|
|
16
16
|
* stats()
|
|
17
17
|
* close()
|
|
@@ -45,6 +45,7 @@ function getDb() {
|
|
|
45
45
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
46
46
|
id TEXT PRIMARY KEY,
|
|
47
47
|
project TEXT NOT NULL,
|
|
48
|
+
scope TEXT DEFAULT NULL,
|
|
48
49
|
summary TEXT NOT NULL,
|
|
49
50
|
keywords TEXT DEFAULT '',
|
|
50
51
|
mood TEXT DEFAULT '',
|
|
@@ -88,6 +89,10 @@ function getDb() {
|
|
|
88
89
|
try { _db.exec(t); } catch { /* trigger may already exist */ }
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Backward-compatible migration for old DBs without `scope`
|
|
93
|
+
try { _db.exec('ALTER TABLE sessions ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
94
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope)'); } catch {}
|
|
95
|
+
|
|
91
96
|
|
|
92
97
|
// ── Facts table: atomic knowledge triples ──
|
|
93
98
|
_db.exec(`
|
|
@@ -100,6 +105,7 @@ function getDb() {
|
|
|
100
105
|
source_type TEXT NOT NULL DEFAULT 'session',
|
|
101
106
|
source_id TEXT,
|
|
102
107
|
project TEXT NOT NULL DEFAULT '*',
|
|
108
|
+
scope TEXT DEFAULT NULL,
|
|
103
109
|
tags TEXT DEFAULT '[]',
|
|
104
110
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
105
111
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -142,8 +148,13 @@ function getDb() {
|
|
|
142
148
|
|
|
143
149
|
// Indexes
|
|
144
150
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
|
|
151
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch {}
|
|
145
152
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
|
|
146
153
|
|
|
154
|
+
// Backward-compatible migration for old DBs without `scope`
|
|
155
|
+
try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
156
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch {}
|
|
157
|
+
|
|
147
158
|
// Search frequency tracking: counts how many times a fact appeared in search results.
|
|
148
159
|
// This is a RELEVANCE PROXY, not a usefulness score — "searched" ≠ "actually helpful".
|
|
149
160
|
// Renamed from recall_count (was ambiguous). Migration copies existing data forward.
|
|
@@ -162,27 +173,42 @@ function getDb() {
|
|
|
162
173
|
* @param {object} opts
|
|
163
174
|
* @param {string} opts.sessionId - Claude session ID (unique key)
|
|
164
175
|
* @param {string} opts.project - Project key (e.g. 'metame', 'desktop')
|
|
176
|
+
* @param {string|null} [opts.scope] - Stable workspace scope ID (e.g. proj_<hash>)
|
|
165
177
|
* @param {string} opts.summary - Distilled summary text
|
|
166
178
|
* @param {string} [opts.keywords] - Comma-separated keywords for search boost
|
|
167
179
|
* @param {string} [opts.mood] - User mood/sentiment detected
|
|
168
180
|
* @param {number} [opts.tokenCost] - Approximate token cost of the session
|
|
169
181
|
* @returns {{ ok: boolean, id: string }}
|
|
170
182
|
*/
|
|
171
|
-
function saveSession({ sessionId, project, summary, keywords = '', mood = '', tokenCost = 0 }) {
|
|
183
|
+
function saveSession({ sessionId, project, scope = null, summary, keywords = '', mood = '', tokenCost = 0 }) {
|
|
172
184
|
if (!sessionId || !project || !summary) {
|
|
173
185
|
throw new Error('saveSession requires sessionId, project, summary');
|
|
174
186
|
}
|
|
187
|
+
const normalizedProject = project === '*' ? '*' : String(project || 'unknown');
|
|
188
|
+
const normalizedScope = normalizedProject === '*'
|
|
189
|
+
? '*'
|
|
190
|
+
: (scope && typeof scope === 'string' ? scope : null);
|
|
175
191
|
const db = getDb();
|
|
176
192
|
const stmt = db.prepare(`
|
|
177
|
-
INSERT INTO sessions (id, project, summary, keywords, mood, token_cost)
|
|
178
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
193
|
+
INSERT INTO sessions (id, project, scope, summary, keywords, mood, token_cost)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
179
195
|
ON CONFLICT(id) DO UPDATE SET
|
|
196
|
+
project = excluded.project,
|
|
197
|
+
scope = excluded.scope,
|
|
180
198
|
summary = excluded.summary,
|
|
181
199
|
keywords = excluded.keywords,
|
|
182
200
|
mood = excluded.mood,
|
|
183
201
|
token_cost = excluded.token_cost
|
|
184
202
|
`);
|
|
185
|
-
stmt.run(
|
|
203
|
+
stmt.run(
|
|
204
|
+
sessionId,
|
|
205
|
+
normalizedProject,
|
|
206
|
+
normalizedScope,
|
|
207
|
+
summary.slice(0, 10000),
|
|
208
|
+
keywords.slice(0, 1000),
|
|
209
|
+
mood.slice(0, 100),
|
|
210
|
+
tokenCost
|
|
211
|
+
);
|
|
186
212
|
return { ok: true, id: sessionId };
|
|
187
213
|
}
|
|
188
214
|
|
|
@@ -196,32 +222,53 @@ const STATEFUL_RELATIONS = new Set(['user_pref', 'config_fact', 'config_change',
|
|
|
196
222
|
* @param {string} sessionId - Source session ID
|
|
197
223
|
* @param {string} project - Project key ('metame', 'desktop', '*' for global)
|
|
198
224
|
* @param {Array} facts - Array of { entity, relation, value, confidence, tags }
|
|
225
|
+
* @param {object} [opts]
|
|
226
|
+
* @param {string|null} [opts.scope] - Stable workspace scope ID (e.g. proj_<hash>)
|
|
199
227
|
* @returns {{ saved: number, skipped: number, superseded: number }}
|
|
200
228
|
*/
|
|
201
|
-
function saveFacts(sessionId, project, facts) {
|
|
229
|
+
function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
202
230
|
if (!Array.isArray(facts) || facts.length === 0) return { saved: 0, skipped: 0, superseded: 0 };
|
|
203
231
|
const db = getDb();
|
|
232
|
+
const normalizedProject = project === '*' ? '*' : String(project || 'unknown');
|
|
233
|
+
const fallbackSessionScope = (() => {
|
|
234
|
+
const sid = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24);
|
|
235
|
+
return sid ? `sess_${sid}` : null;
|
|
236
|
+
})();
|
|
237
|
+
const normalizedScope = normalizedProject === '*'
|
|
238
|
+
? '*'
|
|
239
|
+
: (scope && typeof scope === 'string' ? scope : (normalizedProject === 'unknown' ? fallbackSessionScope : null));
|
|
240
|
+
|
|
241
|
+
let dedupScopeSql = '';
|
|
242
|
+
let dedupScopeParams = [];
|
|
243
|
+
if (normalizedScope === '*') {
|
|
244
|
+
dedupScopeSql = `((scope = '*') OR (scope IS NULL AND project = '*'))`;
|
|
245
|
+
} else if (normalizedScope) {
|
|
246
|
+
dedupScopeSql = `((scope = ?) OR (scope = '*') OR (scope IS NULL AND project IN (?, '*')))`;
|
|
247
|
+
dedupScopeParams = [normalizedScope, normalizedProject];
|
|
248
|
+
} else {
|
|
249
|
+
dedupScopeSql = `(project IN (?, '*'))`;
|
|
250
|
+
dedupScopeParams = [normalizedProject];
|
|
251
|
+
}
|
|
204
252
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
const existsDup = db.prepare(`
|
|
254
|
+
SELECT 1 AS ok
|
|
255
|
+
FROM facts
|
|
256
|
+
WHERE entity = ? AND relation = ? AND substr(value, 1, 50) = ?
|
|
257
|
+
AND ${dedupScopeSql}
|
|
258
|
+
LIMIT 1
|
|
259
|
+
`);
|
|
209
260
|
|
|
210
261
|
const insert = db.prepare(`
|
|
211
|
-
INSERT INTO facts (id, entity, relation, value, confidence, source_type, source_id, project, tags, created_at, updated_at)
|
|
212
|
-
VALUES (?, ?, ?, ?, ?, 'session', ?, ?, ?, datetime('now'), datetime('now'))
|
|
262
|
+
INSERT INTO facts (id, entity, relation, value, confidence, source_type, source_id, project, scope, tags, created_at, updated_at)
|
|
263
|
+
VALUES (?, ?, ?, ?, ?, 'session', ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
213
264
|
ON CONFLICT(id) DO NOTHING
|
|
214
265
|
`);
|
|
215
266
|
|
|
216
|
-
const supersede = db.prepare(`
|
|
217
|
-
UPDATE facts SET superseded_by = ?, updated_at = datetime('now')
|
|
218
|
-
WHERE entity = ? AND relation = ? AND id != ? AND superseded_by IS NULL
|
|
219
|
-
`);
|
|
220
|
-
|
|
221
267
|
let saved = 0;
|
|
222
268
|
let skipped = 0;
|
|
223
269
|
let superseded = 0;
|
|
224
270
|
const savedFacts = [];
|
|
271
|
+
const batchDedup = new Set();
|
|
225
272
|
|
|
226
273
|
for (const f of facts) {
|
|
227
274
|
// Basic validation
|
|
@@ -231,29 +278,49 @@ function saveFacts(sessionId, project, facts) {
|
|
|
231
278
|
// Dedup: same entity+relation with similar value prefix
|
|
232
279
|
const dupKey = `${f.entity}::${f.relation}`;
|
|
233
280
|
const prefix = f.value.slice(0, 50);
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
);
|
|
281
|
+
const dedupKey = `${dupKey}::${prefix}`;
|
|
282
|
+
const isBatchDup = batchDedup.has(dedupKey);
|
|
283
|
+
const dbDup = existsDup.get(f.entity, f.relation, prefix, ...dedupScopeParams);
|
|
284
|
+
const isDup = isBatchDup || !!(dbDup && dbDup.ok === 1);
|
|
237
285
|
if (isDup) { skipped++; continue; }
|
|
238
286
|
|
|
239
287
|
const id = `f-${sessionId.slice(0, 8)}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
240
288
|
const tags = JSON.stringify(Array.isArray(f.tags) ? f.tags.slice(0, 3) : []);
|
|
241
289
|
try {
|
|
242
290
|
insert.run(id, f.entity, f.relation, f.value.slice(0, 300),
|
|
243
|
-
f.confidence || 'medium', sessionId,
|
|
291
|
+
f.confidence || 'medium', sessionId, normalizedProject, normalizedScope, tags);
|
|
292
|
+
batchDedup.add(dedupKey);
|
|
244
293
|
savedFacts.push({ id, entity: f.entity, relation: f.relation, value: f.value,
|
|
245
|
-
project:
|
|
294
|
+
project: normalizedProject, scope: normalizedScope, tags: f.tags || [], created_at: new Date().toISOString() });
|
|
246
295
|
saved++;
|
|
247
296
|
|
|
248
297
|
// For stateful relations, mark older active facts with same entity::relation as superseded
|
|
249
298
|
if (STATEFUL_RELATIONS.has(f.relation)) {
|
|
299
|
+
let whereSql = '';
|
|
300
|
+
let filterParams = [];
|
|
301
|
+
if (normalizedScope === '*') {
|
|
302
|
+
whereSql = `((scope = '*') OR (scope IS NULL AND project = '*'))`;
|
|
303
|
+
} else if (normalizedScope) {
|
|
304
|
+
whereSql = `((scope = ?) OR (scope IS NULL AND project = ?))`;
|
|
305
|
+
filterParams = [normalizedScope, normalizedProject];
|
|
306
|
+
} else {
|
|
307
|
+
whereSql = `(project IN (?, '*'))`;
|
|
308
|
+
filterParams = [normalizedProject];
|
|
309
|
+
}
|
|
310
|
+
|
|
250
311
|
// Fetch the IDs being superseded before running the update (for audit log)
|
|
251
312
|
const db2 = getDb();
|
|
252
313
|
const toSupersede = db2.prepare(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
314
|
+
`SELECT id, value FROM facts
|
|
315
|
+
WHERE entity = ? AND relation = ? AND id != ? AND superseded_by IS NULL
|
|
316
|
+
AND ${whereSql}`
|
|
317
|
+
).all(f.entity, f.relation, id, ...filterParams);
|
|
318
|
+
|
|
319
|
+
const result = db.prepare(
|
|
320
|
+
`UPDATE facts SET superseded_by = ?, updated_at = datetime('now')
|
|
321
|
+
WHERE entity = ? AND relation = ? AND id != ? AND superseded_by IS NULL
|
|
322
|
+
AND ${whereSql}`
|
|
323
|
+
).run(id, f.entity, f.relation, id, ...filterParams);
|
|
257
324
|
const changes = result.changes || 0;
|
|
258
325
|
superseded += changes;
|
|
259
326
|
|
|
@@ -316,6 +383,26 @@ function _logSupersede(oldFacts, newId, entity, relation, newValue, sessionId) {
|
|
|
316
383
|
} catch { /* non-fatal */ }
|
|
317
384
|
}
|
|
318
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Scope filter semantics (new + legacy):
|
|
388
|
+
* - New rows: prefer `scope` exact match or global scope '*'
|
|
389
|
+
* - Legacy rows (scope NULL): fallback to project match or project='*'
|
|
390
|
+
*/
|
|
391
|
+
function _matchesFactScope(row, project, scope) {
|
|
392
|
+
if (!row) return false;
|
|
393
|
+
const rowScope = row.scope === undefined ? null : row.scope;
|
|
394
|
+
if (scope) {
|
|
395
|
+
if (rowScope === scope || rowScope === '*') return true;
|
|
396
|
+
if (rowScope === null) {
|
|
397
|
+
if (!project) return false;
|
|
398
|
+
return row.project === project || row.project === '*';
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
if (project) return row.project === project || row.project === '*';
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
319
406
|
/**
|
|
320
407
|
* Search facts: QMD hybrid search (if available) → FTS5 → LIKE fallback.
|
|
321
408
|
*
|
|
@@ -323,9 +410,10 @@ function _logSupersede(oldFacts, newId, entity, relation, newValue, sessionId) {
|
|
|
323
410
|
* @param {object} [opts]
|
|
324
411
|
* @param {number} [opts.limit=5] - Max results
|
|
325
412
|
* @param {string} [opts.project] - Filter by project (also always includes '*')
|
|
413
|
+
* @param {string} [opts.scope] - Stable workspace scope (also includes global '*')
|
|
326
414
|
* @returns {Promise<Array>|Array} Fact objects
|
|
327
415
|
*/
|
|
328
|
-
async function searchFactsAsync(query, { limit = 5, project = null } = {}) {
|
|
416
|
+
async function searchFactsAsync(query, { limit = 5, project = null, scope = null } = {}) {
|
|
329
417
|
// Try QMD hybrid search first
|
|
330
418
|
let qmdClient = null;
|
|
331
419
|
try { qmdClient = require('./qmd-client'); } catch { /* not available */ }
|
|
@@ -337,13 +425,13 @@ async function searchFactsAsync(query, { limit = 5, project = null } = {}) {
|
|
|
337
425
|
const db = getDb();
|
|
338
426
|
const placeholders = ids.map(() => '?').join(',');
|
|
339
427
|
let rows = db.prepare(
|
|
340
|
-
`SELECT id, entity, relation, value, confidence, project, tags, created_at
|
|
428
|
+
`SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
341
429
|
FROM facts WHERE id IN (${placeholders}) AND superseded_by IS NULL`
|
|
342
430
|
).all(...ids);
|
|
343
431
|
|
|
344
|
-
// Apply project filter
|
|
345
|
-
if (project) {
|
|
346
|
-
rows = rows.filter(r => r
|
|
432
|
+
// Apply project/scope filter
|
|
433
|
+
if (project || scope) {
|
|
434
|
+
rows = rows.filter(r => _matchesFactScope(r, project, scope));
|
|
347
435
|
}
|
|
348
436
|
|
|
349
437
|
// Preserve QMD ranking order
|
|
@@ -358,7 +446,7 @@ async function searchFactsAsync(query, { limit = 5, project = null } = {}) {
|
|
|
358
446
|
} catch { /* QMD failed, fall through to FTS5 */ }
|
|
359
447
|
}
|
|
360
448
|
|
|
361
|
-
return searchFacts(query, { limit, project });
|
|
449
|
+
return searchFacts(query, { limit, project, scope });
|
|
362
450
|
}
|
|
363
451
|
|
|
364
452
|
/**
|
|
@@ -368,9 +456,10 @@ async function searchFactsAsync(query, { limit = 5, project = null } = {}) {
|
|
|
368
456
|
* @param {object} [opts]
|
|
369
457
|
* @param {number} [opts.limit=5] - Max results
|
|
370
458
|
* @param {string} [opts.project] - Filter by project (also always includes '*')
|
|
371
|
-
* @
|
|
459
|
+
* @param {string} [opts.scope] - Stable workspace scope (also includes global '*')
|
|
460
|
+
* @returns {Array<{ id, entity, relation, value, confidence, project, scope, tags, created_at }>}
|
|
372
461
|
*/
|
|
373
|
-
function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
462
|
+
function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
374
463
|
if (!query || !query.trim()) return [];
|
|
375
464
|
const db = getDb();
|
|
376
465
|
|
|
@@ -380,9 +469,27 @@ function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
|
380
469
|
// FTS5 path
|
|
381
470
|
try {
|
|
382
471
|
let sql, params;
|
|
383
|
-
if (project) {
|
|
472
|
+
if (scope && project) {
|
|
473
|
+
sql = `
|
|
474
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
475
|
+
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
476
|
+
WHERE facts_fts MATCH ?
|
|
477
|
+
AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))
|
|
478
|
+
AND f.superseded_by IS NULL
|
|
479
|
+
ORDER BY rank LIMIT ?
|
|
480
|
+
`;
|
|
481
|
+
params = [sanitized, scope, project, limit];
|
|
482
|
+
} else if (scope) {
|
|
483
|
+
sql = `
|
|
484
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
485
|
+
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
486
|
+
WHERE facts_fts MATCH ? AND (f.scope = ? OR f.scope = '*') AND f.superseded_by IS NULL
|
|
487
|
+
ORDER BY rank LIMIT ?
|
|
488
|
+
`;
|
|
489
|
+
params = [sanitized, scope, limit];
|
|
490
|
+
} else if (project) {
|
|
384
491
|
sql = `
|
|
385
|
-
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.tags, f.created_at, rank
|
|
492
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
386
493
|
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
387
494
|
WHERE facts_fts MATCH ? AND (f.project = ? OR f.project = '*') AND f.superseded_by IS NULL
|
|
388
495
|
ORDER BY rank LIMIT ?
|
|
@@ -390,7 +497,7 @@ function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
|
390
497
|
params = [sanitized, project, limit];
|
|
391
498
|
} else {
|
|
392
499
|
sql = `
|
|
393
|
-
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.tags, f.created_at, rank
|
|
500
|
+
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
394
501
|
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
395
502
|
WHERE facts_fts MATCH ? AND f.superseded_by IS NULL
|
|
396
503
|
ORDER BY rank LIMIT ?
|
|
@@ -406,18 +513,33 @@ function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
|
406
513
|
|
|
407
514
|
// LIKE fallback
|
|
408
515
|
const like = '%' + query.trim() + '%';
|
|
409
|
-
const likeSql = project
|
|
410
|
-
? `SELECT id, entity, relation, value, confidence, project, tags, created_at
|
|
516
|
+
const likeSql = scope && project
|
|
517
|
+
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
518
|
+
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
519
|
+
AND ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))
|
|
520
|
+
AND superseded_by IS NULL
|
|
521
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
522
|
+
: scope
|
|
523
|
+
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
524
|
+
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
525
|
+
AND (scope = ? OR scope = '*') AND superseded_by IS NULL
|
|
526
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
527
|
+
: project
|
|
528
|
+
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
411
529
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
412
530
|
AND (project = ? OR project = '*') AND superseded_by IS NULL
|
|
413
531
|
ORDER BY created_at DESC LIMIT ?`
|
|
414
|
-
|
|
532
|
+
: `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
415
533
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
416
534
|
AND superseded_by IS NULL
|
|
417
535
|
ORDER BY created_at DESC LIMIT ?`;
|
|
418
|
-
const likeResults = project
|
|
419
|
-
? db.prepare(likeSql).all(like, like, like, project, limit)
|
|
420
|
-
:
|
|
536
|
+
const likeResults = scope && project
|
|
537
|
+
? db.prepare(likeSql).all(like, like, like, scope, project, limit)
|
|
538
|
+
: scope
|
|
539
|
+
? db.prepare(likeSql).all(like, like, like, scope, limit)
|
|
540
|
+
: project
|
|
541
|
+
? db.prepare(likeSql).all(like, like, like, project, limit)
|
|
542
|
+
: db.prepare(likeSql).all(like, like, like, limit);
|
|
421
543
|
if (likeResults.length > 0) _trackSearch(likeResults.map(r => r.id));
|
|
422
544
|
return likeResults;
|
|
423
545
|
}
|
|
@@ -429,9 +551,10 @@ function searchFacts(query, { limit = 5, project = null } = {}) {
|
|
|
429
551
|
* @param {object} [opts]
|
|
430
552
|
* @param {number} [opts.limit=5] - Max results
|
|
431
553
|
* @param {string} [opts.project] - Filter by project
|
|
432
|
-
* @
|
|
554
|
+
* @param {string} [opts.scope] - Stable workspace scope (also includes global '*')
|
|
555
|
+
* @returns {Array<{ id, project, scope, summary, keywords, mood, created_at, rank }>}
|
|
433
556
|
*/
|
|
434
|
-
function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
557
|
+
function searchSessions(query, { limit = 5, project = null, scope = null } = {}) {
|
|
435
558
|
if (!query || !query.trim()) return [];
|
|
436
559
|
const db = getDb();
|
|
437
560
|
|
|
@@ -439,9 +562,26 @@ function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
|
439
562
|
const sanitized = query.trim().split(/\s+/).map(t => '"' + t.replace(/"/g, '') + '"').join(' ');
|
|
440
563
|
|
|
441
564
|
let sql, params;
|
|
442
|
-
if (project) {
|
|
565
|
+
if (scope && project) {
|
|
443
566
|
sql = `
|
|
444
|
-
SELECT s.id, s.project, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
567
|
+
SELECT s.id, s.project, s.scope, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
568
|
+
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
569
|
+
WHERE sessions_fts MATCH ?
|
|
570
|
+
AND ((s.scope = ? OR s.scope = '*') OR (s.scope IS NULL AND (s.project = ? OR s.project = '*')))
|
|
571
|
+
ORDER BY rank LIMIT ?
|
|
572
|
+
`;
|
|
573
|
+
params = [sanitized, scope, project, limit];
|
|
574
|
+
} else if (scope) {
|
|
575
|
+
sql = `
|
|
576
|
+
SELECT s.id, s.project, s.scope, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
577
|
+
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
578
|
+
WHERE sessions_fts MATCH ? AND (s.scope = ? OR s.scope = '*')
|
|
579
|
+
ORDER BY rank LIMIT ?
|
|
580
|
+
`;
|
|
581
|
+
params = [sanitized, scope, limit];
|
|
582
|
+
} else if (project) {
|
|
583
|
+
sql = `
|
|
584
|
+
SELECT s.id, s.project, s.scope, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
445
585
|
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
446
586
|
WHERE sessions_fts MATCH ? AND s.project = ?
|
|
447
587
|
ORDER BY rank LIMIT ?
|
|
@@ -449,7 +589,7 @@ function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
|
449
589
|
params = [sanitized, project, limit];
|
|
450
590
|
} else {
|
|
451
591
|
sql = `
|
|
452
|
-
SELECT s.id, s.project, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
592
|
+
SELECT s.id, s.project, s.scope, s.summary, s.keywords, s.mood, s.created_at, s.token_cost, rank
|
|
453
593
|
FROM sessions_fts f JOIN sessions s ON s.rowid = f.rowid
|
|
454
594
|
WHERE sessions_fts MATCH ?
|
|
455
595
|
ORDER BY rank LIMIT ?
|
|
@@ -464,12 +604,34 @@ function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
|
464
604
|
|
|
465
605
|
// LIKE fallback (handles short CJK terms like "飞书" that trigram can't match)
|
|
466
606
|
const likeParam = '%' + query.trim() + '%';
|
|
467
|
-
const likeSql = project
|
|
468
|
-
?
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
607
|
+
const likeSql = scope && project
|
|
608
|
+
? `SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
609
|
+
FROM sessions
|
|
610
|
+
WHERE (summary LIKE ? OR keywords LIKE ?)
|
|
611
|
+
AND ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))
|
|
612
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
613
|
+
: scope
|
|
614
|
+
? `SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
615
|
+
FROM sessions
|
|
616
|
+
WHERE (summary LIKE ? OR keywords LIKE ?)
|
|
617
|
+
AND (scope = ? OR scope = '*')
|
|
618
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
619
|
+
: project
|
|
620
|
+
? `SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
621
|
+
FROM sessions
|
|
622
|
+
WHERE (summary LIKE ? OR keywords LIKE ?) AND project = ?
|
|
623
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
624
|
+
: `SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
625
|
+
FROM sessions
|
|
626
|
+
WHERE (summary LIKE ? OR keywords LIKE ?)
|
|
627
|
+
ORDER BY created_at DESC LIMIT ?`;
|
|
628
|
+
return scope && project
|
|
629
|
+
? db.prepare(likeSql).all(likeParam, likeParam, scope, project, limit)
|
|
630
|
+
: scope
|
|
631
|
+
? db.prepare(likeSql).all(likeParam, likeParam, scope, limit)
|
|
632
|
+
: project
|
|
633
|
+
? db.prepare(likeSql).all(likeParam, likeParam, project, limit)
|
|
634
|
+
: db.prepare(likeSql).all(likeParam, likeParam, limit);
|
|
473
635
|
}
|
|
474
636
|
|
|
475
637
|
/**
|
|
@@ -478,17 +640,34 @@ function searchSessions(query, { limit = 5, project = null } = {}) {
|
|
|
478
640
|
* @param {object} [opts]
|
|
479
641
|
* @param {number} [opts.limit=3] - Max results
|
|
480
642
|
* @param {string} [opts.project] - Filter by project
|
|
481
|
-
* @
|
|
643
|
+
* @param {string} [opts.scope] - Stable workspace scope (also includes global '*')
|
|
644
|
+
* @returns {Array<{ id, project, scope, summary, keywords, mood, created_at }>}
|
|
482
645
|
*/
|
|
483
|
-
function recentSessions({ limit = 3, project = null } = {}) {
|
|
646
|
+
function recentSessions({ limit = 3, project = null, scope = null } = {}) {
|
|
484
647
|
const db = getDb();
|
|
648
|
+
if (scope && project) {
|
|
649
|
+
return db.prepare(
|
|
650
|
+
`SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
651
|
+
FROM sessions
|
|
652
|
+
WHERE ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))
|
|
653
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
654
|
+
).all(scope, project, limit);
|
|
655
|
+
}
|
|
656
|
+
if (scope) {
|
|
657
|
+
return db.prepare(
|
|
658
|
+
`SELECT id, project, scope, summary, keywords, mood, created_at, token_cost
|
|
659
|
+
FROM sessions
|
|
660
|
+
WHERE (scope = ? OR scope = '*')
|
|
661
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
662
|
+
).all(scope, limit);
|
|
663
|
+
}
|
|
485
664
|
if (project) {
|
|
486
665
|
return db.prepare(
|
|
487
|
-
'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions WHERE project = ? ORDER BY created_at DESC LIMIT ?'
|
|
666
|
+
'SELECT id, project, scope, summary, keywords, mood, created_at, token_cost FROM sessions WHERE project = ? ORDER BY created_at DESC LIMIT ?'
|
|
488
667
|
).all(project, limit);
|
|
489
668
|
}
|
|
490
669
|
return db.prepare(
|
|
491
|
-
'SELECT id, project, summary, keywords, mood, created_at, token_cost FROM sessions ORDER BY created_at DESC LIMIT ?'
|
|
670
|
+
'SELECT id, project, scope, summary, keywords, mood, created_at, token_cost FROM sessions ORDER BY created_at DESC LIMIT ?'
|
|
492
671
|
).all(limit);
|
|
493
672
|
}
|
|
494
673
|
|
package/scripts/providers.js
CHANGED
|
@@ -217,7 +217,7 @@ function listFormatted() {
|
|
|
217
217
|
*/
|
|
218
218
|
function callHaiku(input, extraEnv, timeout) {
|
|
219
219
|
const { execFile } = require('child_process');
|
|
220
|
-
const env = { ...process.env, ...extraEnv };
|
|
220
|
+
const env = { ...process.env, ...extraEnv, METAME_INTERNAL_PROMPT: '1' };
|
|
221
221
|
delete env.CLAUDECODE;
|
|
222
222
|
return new Promise((resolve, reject) => {
|
|
223
223
|
const proc = execFile(
|