claude-mem-lite 2.71.4 → 2.73.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +22 -1
- package/cli/activity.mjs +6 -2
- package/cli/fts-check.mjs +7 -1
- package/hook-optimize.mjs +74 -38
- package/install.mjs +98 -36
- package/mem-cli.mjs +202 -38
- package/package.json +1 -1
- package/schema.mjs +4 -1
- package/server.mjs +21 -2
- package/tool-schemas.mjs +2 -0
package/adopt-cli.mjs
CHANGED
|
@@ -192,9 +192,22 @@ function statusAll() {
|
|
|
192
192
|
/**
|
|
193
193
|
* cmdUnadopt — precise removal of sentinel section + plugin doc.
|
|
194
194
|
* Exit code stays 0: unadopt is idempotent; "absent" isn't an error.
|
|
195
|
+
*
|
|
196
|
+
* Flags:
|
|
197
|
+
* --all Operate on every memdir under ~/.claude/projects/*\/memory/
|
|
198
|
+
* --status Read-only: list currently-adopted memdirs (mirrors `adopt --status`).
|
|
199
|
+
* --dry-run Preview what would be removed; no filesystem writes.
|
|
200
|
+
*
|
|
201
|
+
* Pre-fix history: unrecognized flags (e.g. `--status` extrapolated from `adopt --status`,
|
|
202
|
+
* or `--dry-run` extrapolated from `adopt --dry-run`) were silently ignored and the
|
|
203
|
+
* destructive default ran anyway, removing the sentinel block when the user expected
|
|
204
|
+
* a read-only probe.
|
|
195
205
|
*/
|
|
196
206
|
export function cmdUnadopt(args = []) {
|
|
207
|
+
if (hasFlag(args, '--status')) return statusAll();
|
|
208
|
+
|
|
197
209
|
const all = hasFlag(args, '--all');
|
|
210
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
198
211
|
const targets = all
|
|
199
212
|
? listAllMemdirs().map((m) => m.memdir)
|
|
200
213
|
: [memdirPath(detectCwd())];
|
|
@@ -206,6 +219,13 @@ export function cmdUnadopt(args = []) {
|
|
|
206
219
|
|
|
207
220
|
let removed = 0, absent = 0;
|
|
208
221
|
for (const memdir of targets) {
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
const adopted = isAdopted(memdir, PLUGIN_SLUG);
|
|
224
|
+
const action = adopted ? 'would-remove' : 'absent';
|
|
225
|
+
log(`[unadopt --dry-run] ${memdir} → ${action}`);
|
|
226
|
+
if (adopted) removed++; else absent++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
209
229
|
const r = removePluginSection(memdir, PLUGIN_SLUG);
|
|
210
230
|
removePluginDoc(memdir, PLUGIN_SLUG);
|
|
211
231
|
if (r.action === 'removed') removed++;
|
|
@@ -214,5 +234,6 @@ export function cmdUnadopt(args = []) {
|
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
log('');
|
|
217
|
-
|
|
237
|
+
const verb = dryRun ? 'would remove' : 'removed';
|
|
238
|
+
log(`[unadopt${dryRun ? ' --dry-run' : ''}] ${targets.length} target(s): ${removed} ${verb}, ${absent} absent`);
|
|
218
239
|
}
|
package/cli/activity.mjs
CHANGED
|
@@ -20,7 +20,7 @@ function formatActivityResults(rows) {
|
|
|
20
20
|
export async function cmdActivity(db, args) {
|
|
21
21
|
const sub = args[0];
|
|
22
22
|
if (!sub) {
|
|
23
|
-
fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show> ...');
|
|
23
|
+
fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show|delete> ...');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -105,7 +105,11 @@ export async function cmdActivity(db, args) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
const row = getEvent(db, id);
|
|
108
|
-
|
|
108
|
+
if (row) {
|
|
109
|
+
out(JSON.stringify(row, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
out(`[mem] activity show: event #${id} Not found`);
|
|
112
|
+
}
|
|
109
113
|
return;
|
|
110
114
|
}
|
|
111
115
|
|
package/cli/fts-check.mjs
CHANGED
|
@@ -7,10 +7,16 @@ import { parseArgs, out, fail } from './common.mjs';
|
|
|
7
7
|
export function cmdFtsCheck(db, args) {
|
|
8
8
|
const { positional } = parseArgs(args);
|
|
9
9
|
const action = positional[0];
|
|
10
|
-
if (!action
|
|
10
|
+
if (!action) {
|
|
11
11
|
fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
+
if (!['check', 'rebuild'].includes(action)) {
|
|
15
|
+
// Tell the user what was wrong rather than dumping the usage — they passed
|
|
16
|
+
// something concrete, the error should name the invalid token.
|
|
17
|
+
fail(`[mem] Invalid action "${action}". Use: check, rebuild`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
if (action === 'check') {
|
|
16
22
|
const result = checkFTSIntegrity(db);
|
package/hook-optimize.mjs
CHANGED
|
@@ -74,12 +74,13 @@ export function rebuildVector(db, obsId, textParts) {
|
|
|
74
74
|
*
|
|
75
75
|
* @param {object} db better-sqlite3 database handle
|
|
76
76
|
* @param {number} limit max candidates to return
|
|
77
|
-
* @param {{ scope?: 'narrow' | 'wide' }} [opts]
|
|
77
|
+
* @param {{ scope?: 'narrow' | 'wide', project?: string }} [opts] Optional project filter (e.g. inferProject()-resolved name) narrows candidates to a single project — opt-in to preserve prior cross-project default.
|
|
78
78
|
*/
|
|
79
|
-
export function findReenrichCandidates(db, limit = 10, { scope = 'narrow' } = {}) {
|
|
79
|
+
export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', project } = {}) {
|
|
80
|
+
const projectClause = project ? 'AND project = ?' : '';
|
|
80
81
|
if (scope === 'wide') {
|
|
81
|
-
|
|
82
|
-
SELECT id, title, narrative, type, subtitle, concepts, facts
|
|
82
|
+
const stmt = db.prepare(`
|
|
83
|
+
SELECT id, title, narrative, type, subtitle, concepts, facts, project
|
|
83
84
|
FROM observations
|
|
84
85
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
85
86
|
AND superseded_at IS NULL
|
|
@@ -88,14 +89,16 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow' } = {}
|
|
|
88
89
|
AND (lesson_learned IS NULL OR lesson_learned = '')
|
|
89
90
|
AND LENGTH(COALESCE(narrative, '')) > 100
|
|
90
91
|
AND ${notLowSignalTitleClause('')}
|
|
92
|
+
${projectClause}
|
|
91
93
|
ORDER BY
|
|
92
94
|
CASE type WHEN 'decision' THEN 0 WHEN 'bugfix' THEN 1 WHEN 'refactor' THEN 2 ELSE 3 END,
|
|
93
95
|
created_at_epoch DESC
|
|
94
96
|
LIMIT ?
|
|
95
|
-
`)
|
|
97
|
+
`);
|
|
98
|
+
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
96
99
|
}
|
|
97
|
-
|
|
98
|
-
SELECT id, title, narrative, type, subtitle
|
|
100
|
+
const stmt = db.prepare(`
|
|
101
|
+
SELECT id, title, narrative, type, subtitle, project
|
|
99
102
|
FROM observations
|
|
100
103
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
101
104
|
AND (concepts IS NULL OR concepts = '')
|
|
@@ -103,13 +106,15 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow' } = {}
|
|
|
103
106
|
AND lesson_learned IS NULL
|
|
104
107
|
AND search_aliases IS NULL
|
|
105
108
|
AND optimized_at IS NULL
|
|
109
|
+
${projectClause}
|
|
106
110
|
ORDER BY created_at_epoch DESC
|
|
107
111
|
LIMIT ?
|
|
108
|
-
`)
|
|
112
|
+
`);
|
|
113
|
+
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
export async function executeReenrich(db, limit = 10, { scope = 'narrow' } = {}) {
|
|
112
|
-
const candidates = findReenrichCandidates(db, limit, { scope });
|
|
116
|
+
export async function executeReenrich(db, limit = 10, { scope = 'narrow', project } = {}) {
|
|
117
|
+
const candidates = findReenrichCandidates(db, limit, { scope, project });
|
|
113
118
|
if (candidates.length === 0) return { processed: 0, skipped: 0 };
|
|
114
119
|
|
|
115
120
|
let processed = 0, skipped = 0;
|
|
@@ -206,14 +211,17 @@ export function shouldRunNormalize() {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
export function extractUniqueConcepts(db, limit = 500) {
|
|
210
|
-
const
|
|
214
|
+
export function extractUniqueConcepts(db, limit = 500, { project } = {}) {
|
|
215
|
+
const projectClause = project ? 'AND project = ?' : '';
|
|
216
|
+
const stmt = db.prepare(`
|
|
211
217
|
SELECT concepts FROM observations
|
|
212
218
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
213
219
|
AND concepts IS NOT NULL AND concepts != ''
|
|
220
|
+
${projectClause}
|
|
214
221
|
ORDER BY created_at_epoch DESC
|
|
215
222
|
LIMIT 2000
|
|
216
|
-
`)
|
|
223
|
+
`);
|
|
224
|
+
const rows = project ? stmt.all(project) : stmt.all();
|
|
217
225
|
|
|
218
226
|
const conceptSet = new Set();
|
|
219
227
|
for (const row of rows) {
|
|
@@ -304,10 +312,10 @@ export function applyNormalization(db, groups) {
|
|
|
304
312
|
return { updated };
|
|
305
313
|
}
|
|
306
314
|
|
|
307
|
-
export async function executeNormalize(db, force = false) {
|
|
315
|
+
export async function executeNormalize(db, force = false, { project } = {}) {
|
|
308
316
|
if (!force && !shouldRunNormalize()) return { skipped: true, reason: 'gate' };
|
|
309
317
|
|
|
310
|
-
const concepts = extractUniqueConcepts(db);
|
|
318
|
+
const concepts = extractUniqueConcepts(db, 500, { project });
|
|
311
319
|
if (concepts.length < 5) return { skipped: true, reason: 'too few concepts' };
|
|
312
320
|
|
|
313
321
|
const groups = await identifySynonymGroups(concepts);
|
|
@@ -326,18 +334,21 @@ const MERGE_TIME_WINDOW_MS = 30 * 86400000;
|
|
|
326
334
|
const MERGE_JACCARD_LOW = 0.4;
|
|
327
335
|
const MERGE_JACCARD_HIGH = 0.85;
|
|
328
336
|
|
|
329
|
-
export function findMergeCandidates(db, maxClusters = 5) {
|
|
337
|
+
export function findMergeCandidates(db, maxClusters = 5, { project } = {}) {
|
|
330
338
|
const cutoff = Date.now() - MERGE_TIME_WINDOW_MS;
|
|
331
|
-
const
|
|
332
|
-
|
|
339
|
+
const projectClause = project ? 'AND project = ?' : '';
|
|
340
|
+
const stmt = db.prepare(`
|
|
341
|
+
SELECT id, title, narrative, project, type, access_count, created_at_epoch, minhash_sig
|
|
333
342
|
FROM observations
|
|
334
343
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
335
344
|
AND optimized_at IS NULL
|
|
336
345
|
AND title IS NOT NULL AND title != ''
|
|
337
346
|
AND created_at_epoch > ?
|
|
347
|
+
${projectClause}
|
|
338
348
|
ORDER BY created_at_epoch DESC
|
|
339
349
|
LIMIT 200
|
|
340
|
-
`)
|
|
350
|
+
`);
|
|
351
|
+
const rows = project ? stmt.all(cutoff, project) : stmt.all(cutoff);
|
|
341
352
|
|
|
342
353
|
const used = new Set();
|
|
343
354
|
const clusters = [];
|
|
@@ -450,8 +461,8 @@ Return ONLY valid JSON:
|
|
|
450
461
|
}
|
|
451
462
|
}
|
|
452
463
|
|
|
453
|
-
export async function executeClusterMerge(db, maxClusters = 5) {
|
|
454
|
-
const clusters = findMergeCandidates(db, maxClusters);
|
|
464
|
+
export async function executeClusterMerge(db, maxClusters = 5, { project } = {}) {
|
|
465
|
+
const clusters = findMergeCandidates(db, maxClusters, { project });
|
|
455
466
|
if (clusters.length === 0) return { processed: 0, merged: 0 };
|
|
456
467
|
|
|
457
468
|
let merged = 0;
|
|
@@ -468,17 +479,20 @@ export async function executeClusterMerge(db, maxClusters = 5) {
|
|
|
468
479
|
const COMPRESS_TIME_SPLIT_MS = 14 * 86400000;
|
|
469
480
|
const COMPRESS_COSINE_THRESHOLD = 0.3;
|
|
470
481
|
|
|
471
|
-
export function findSmartCompressCandidates(db, ageDays = 30) {
|
|
482
|
+
export function findSmartCompressCandidates(db, ageDays = 30, { project } = {}) {
|
|
472
483
|
const cutoff = Date.now() - ageDays * 86400000;
|
|
473
|
-
|
|
484
|
+
const projectClause = project ? 'AND project = ?' : '';
|
|
485
|
+
const stmt = db.prepare(`
|
|
474
486
|
SELECT id, title, narrative, lesson_learned, project, type, created_at_epoch
|
|
475
487
|
FROM observations
|
|
476
488
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
477
489
|
AND COALESCE(importance, 1) = 1
|
|
478
490
|
AND COALESCE(access_count, 0) = 0
|
|
479
491
|
AND created_at_epoch < ?
|
|
492
|
+
${projectClause}
|
|
480
493
|
ORDER BY project, created_at_epoch
|
|
481
|
-
`)
|
|
494
|
+
`);
|
|
495
|
+
return project ? stmt.all(cutoff, project) : stmt.all(cutoff);
|
|
482
496
|
}
|
|
483
497
|
|
|
484
498
|
export function clusterForCompression(candidates, db) {
|
|
@@ -642,8 +656,8 @@ JSON: {"title":"descriptive summary ≤120 chars","narrative":"comprehensive sum
|
|
|
642
656
|
}
|
|
643
657
|
}
|
|
644
658
|
|
|
645
|
-
export async function executeSmartCompress(db, maxClusters = 5) {
|
|
646
|
-
const candidates = findSmartCompressCandidates(db);
|
|
659
|
+
export async function executeSmartCompress(db, maxClusters = 5, { project } = {}) {
|
|
660
|
+
const candidates = findSmartCompressCandidates(db, 30, { project });
|
|
647
661
|
if (candidates.length < 3) return { processed: 0, compressed: 0 };
|
|
648
662
|
|
|
649
663
|
const clusters = clusterForCompression(candidates, db);
|
|
@@ -661,23 +675,34 @@ export async function executeSmartCompress(db, maxClusters = 5) {
|
|
|
661
675
|
|
|
662
676
|
// ─── Pipeline Orchestrator ──────────────────────────────────────────────────
|
|
663
677
|
|
|
664
|
-
|
|
665
|
-
|
|
678
|
+
/**
|
|
679
|
+
* @param {object} db better-sqlite3 database handle
|
|
680
|
+
* @param {{ project?: string, detail?: boolean }} [opts]
|
|
681
|
+
* project: scope all candidate finders to a single project (opt-in; default scans all).
|
|
682
|
+
* detail: when true, also return `mergeClusters` / `reenrichSamples` / `compressSamples`
|
|
683
|
+
* arrays alongside the aggregate counts so callers (CLI --verbose, MCP detail mode)
|
|
684
|
+
* can render auditable previews without re-running the finders. The candidate-count
|
|
685
|
+
* arms still call the finders with high limits — detail mode does NOT widen scope,
|
|
686
|
+
* it surfaces the same rows the counts already crossed.
|
|
687
|
+
*/
|
|
688
|
+
export function optimizePreview(db, { project, detail = false } = {}) {
|
|
689
|
+
const reenrichCandidates = findReenrichCandidates(db, 1000, { project });
|
|
690
|
+
const reenrich = reenrichCandidates.length;
|
|
666
691
|
// R-7: also report the widened-scope candidate count so users can see how many
|
|
667
692
|
// bugfix/refactor/feature/decision observations are eligible for lesson backfill.
|
|
668
|
-
const reenrichWide = findReenrichCandidates(db, 5000, { scope: 'wide' }).length;
|
|
693
|
+
const reenrichWide = findReenrichCandidates(db, 5000, { scope: 'wide', project }).length;
|
|
669
694
|
|
|
670
|
-
const concepts = extractUniqueConcepts(db);
|
|
695
|
+
const concepts = extractUniqueConcepts(db, 500, { project });
|
|
671
696
|
const normalizeReady = shouldRunNormalize() && concepts.length >= 5;
|
|
672
697
|
|
|
673
|
-
const mergeClusters = findMergeCandidates(db, 50);
|
|
698
|
+
const mergeClusters = findMergeCandidates(db, 50, { project });
|
|
674
699
|
const clusterMerge = mergeClusters.length;
|
|
675
700
|
|
|
676
|
-
const compressCandidates = findSmartCompressCandidates(db);
|
|
701
|
+
const compressCandidates = findSmartCompressCandidates(db, 30, { project });
|
|
677
702
|
const compressClusters = clusterForCompression(compressCandidates, db);
|
|
678
703
|
const smartCompress = compressClusters.length;
|
|
679
704
|
|
|
680
|
-
|
|
705
|
+
const result = {
|
|
681
706
|
reenrich,
|
|
682
707
|
reenrichWide,
|
|
683
708
|
normalize: normalizeReady ? concepts.length : 0,
|
|
@@ -686,6 +711,15 @@ export function optimizePreview(db) {
|
|
|
686
711
|
smartCompress,
|
|
687
712
|
total: reenrich + (normalizeReady ? 1 : 0) + clusterMerge + smartCompress,
|
|
688
713
|
};
|
|
714
|
+
if (detail) {
|
|
715
|
+
// Caps avoid dumping arbitrarily large arrays into CLI/MCP output — 20 picks
|
|
716
|
+
// a sample size big enough to be auditable but small enough to fit a terminal
|
|
717
|
+
// page. Callers that need more can drop --verbose and run the finders directly.
|
|
718
|
+
result.mergeClusters = mergeClusters;
|
|
719
|
+
result.reenrichSamples = reenrichCandidates.slice(0, 20);
|
|
720
|
+
result.compressSamples = compressClusters.slice(0, 5);
|
|
721
|
+
}
|
|
722
|
+
return result;
|
|
689
723
|
}
|
|
690
724
|
|
|
691
725
|
/**
|
|
@@ -701,8 +735,10 @@ export function optimizePreview(db) {
|
|
|
701
735
|
* @param {boolean} [opts.force=false] Bypass time-based gates (e.g. normalize interval).
|
|
702
736
|
* @param {'narrow'|'wide'} [opts.reenrichScope='narrow'] Scope for the re-enrich task.
|
|
703
737
|
* 'wide' targets bugfix/refactor/feature/decision with narrative but no lesson (R-7).
|
|
738
|
+
* @param {string} [opts.project] Filter all tasks to a single project. Opt-in;
|
|
739
|
+
* absence preserves the prior all-projects default.
|
|
704
740
|
*/
|
|
705
|
-
export async function optimizeRun(db, { tasks, maxItems = 15, force = false, reenrichScope = 'narrow' } = {}) {
|
|
741
|
+
export async function optimizeRun(db, { tasks, maxItems = 15, force = false, reenrichScope = 'narrow', project } = {}) {
|
|
706
742
|
const allTasks = ['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'];
|
|
707
743
|
const selectedTasks = tasks && tasks.length > 0 ? tasks : allTasks;
|
|
708
744
|
// Single-task mode: give that task the full budget. Distribution only makes sense
|
|
@@ -716,16 +752,16 @@ export async function optimizeRun(db, { tasks, maxItems = 15, force = false, ree
|
|
|
716
752
|
try {
|
|
717
753
|
switch (task) {
|
|
718
754
|
case 're-enrich':
|
|
719
|
-
results.reenrich = await executeReenrich(db, budget.reenrich, { scope: reenrichScope });
|
|
755
|
+
results.reenrich = await executeReenrich(db, budget.reenrich, { scope: reenrichScope, project });
|
|
720
756
|
break;
|
|
721
757
|
case 'normalize':
|
|
722
|
-
results.normalize = await executeNormalize(db, force);
|
|
758
|
+
results.normalize = await executeNormalize(db, force, { project });
|
|
723
759
|
break;
|
|
724
760
|
case 'cluster-merge':
|
|
725
|
-
results.clusterMerge = await executeClusterMerge(db, budget.clusterMerge);
|
|
761
|
+
results.clusterMerge = await executeClusterMerge(db, budget.clusterMerge, { project });
|
|
726
762
|
break;
|
|
727
763
|
case 'smart-compress':
|
|
728
|
-
results.smartCompress = await executeSmartCompress(db, budget.smartCompress);
|
|
764
|
+
results.smartCompress = await executeSmartCompress(db, budget.smartCompress, { project });
|
|
729
765
|
break;
|
|
730
766
|
}
|
|
731
767
|
} catch (e) {
|
package/install.mjs
CHANGED
|
@@ -1057,18 +1057,21 @@ async function cleanupHooks() {
|
|
|
1057
1057
|
// ─── Status ─────────────────────────────────────────────────────────────────
|
|
1058
1058
|
|
|
1059
1059
|
async function status() {
|
|
1060
|
-
|
|
1060
|
+
// Dogfood-8: support --json so CI / setup scripts can probe install state
|
|
1061
|
+
// without scraping text. Collect each check as a structured record first,
|
|
1062
|
+
// then print text OR JSON. Text path keeps identical wording so existing
|
|
1063
|
+
// users / docs / screenshots stay correct.
|
|
1064
|
+
const json = flags.has('--json');
|
|
1065
|
+
const checks = [];
|
|
1066
|
+
const push = (level, key, message, extra = {}) => checks.push({ level, key, message, ...extra });
|
|
1061
1067
|
|
|
1062
1068
|
// MCP
|
|
1063
1069
|
try {
|
|
1064
1070
|
const list = execFileSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
} else {
|
|
1068
|
-
fail('MCP server: not registered');
|
|
1069
|
-
}
|
|
1071
|
+
const registered = list.includes('mem:') || list.includes('mem ');
|
|
1072
|
+
push(registered ? 'ok' : 'fail', 'mcp', registered ? 'MCP server: registered' : 'MCP server: not registered', { registered });
|
|
1070
1073
|
} catch {
|
|
1071
|
-
|
|
1074
|
+
push('warn', 'mcp', 'Could not check MCP status', { registered: null });
|
|
1072
1075
|
}
|
|
1073
1076
|
|
|
1074
1077
|
// Hooks
|
|
@@ -1077,34 +1080,29 @@ async function status() {
|
|
|
1077
1080
|
const pluginDisabled = isPluginExplicitlyDisabled(settings);
|
|
1078
1081
|
const pluginEnabled = settings.enabledPlugins?.[PLUGIN_KEY] === true;
|
|
1079
1082
|
|
|
1080
|
-
if (pluginEnabled) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
warn('Plugin: disabled in settings');
|
|
1084
|
-
} else {
|
|
1085
|
-
warn('Plugin: not present in enabledPlugins');
|
|
1086
|
-
}
|
|
1083
|
+
if (pluginEnabled) push('ok', 'plugin', 'Plugin: enabled in settings', { enabled: true, disabled: false });
|
|
1084
|
+
else if (pluginDisabled) push('warn', 'plugin', 'Plugin: disabled in settings', { enabled: false, disabled: true });
|
|
1085
|
+
else push('warn', 'plugin', 'Plugin: not present in enabledPlugins', { enabled: false, disabled: false });
|
|
1087
1086
|
|
|
1088
1087
|
if (hasHooks && pluginDisabled) {
|
|
1089
|
-
|
|
1088
|
+
push('warn', 'hooks', 'Hooks: still configured in settings.json while plugin is disabled (runtime ignores them; run cleanup-hooks or uninstall to clean up)', { configured: true });
|
|
1090
1089
|
} else if (hasHooks) {
|
|
1091
|
-
|
|
1090
|
+
push('ok', 'hooks', 'Hooks: configured', { configured: true });
|
|
1092
1091
|
} else if (pluginDisabled) {
|
|
1093
|
-
|
|
1092
|
+
push('ok', 'hooks', 'Hooks: not configured', { configured: false });
|
|
1094
1093
|
} else {
|
|
1095
|
-
|
|
1094
|
+
push('fail', 'hooks', 'Hooks: not configured', { configured: false });
|
|
1096
1095
|
}
|
|
1097
1096
|
|
|
1098
1097
|
// Plugin cache pollution: populated hooks.json in cache AND install.mjs-managed
|
|
1099
1098
|
// settings.json hooks → runtime registers both → duplicate firing.
|
|
1100
1099
|
const polluted = scanPluginCacheHookPollution();
|
|
1101
1100
|
if (polluted.length > 0 && hasHooks) {
|
|
1102
|
-
fail
|
|
1101
|
+
push('fail', 'plugin_cache', `Plugin cache: stale hooks.json in version(s) ${polluted.join(', ')} — duplicate firing alongside settings.json (run 'install' to auto-clear)`, { polluted_versions: polluted });
|
|
1103
1102
|
} else if (polluted.length > 0) {
|
|
1104
|
-
|
|
1105
|
-
ok(`Plugin cache: ${polluted.length} version(s) with hooks.json (plugin-only mode)`);
|
|
1103
|
+
push('ok', 'plugin_cache', `Plugin cache: ${polluted.length} version(s) with hooks.json (plugin-only mode)`, { polluted_versions: polluted });
|
|
1106
1104
|
} else if (pluginEnabled || hasHooks) {
|
|
1107
|
-
|
|
1105
|
+
push('ok', 'plugin_cache', 'Plugin cache: no stale hooks.json (no duplicate firing)', { polluted_versions: [] });
|
|
1108
1106
|
}
|
|
1109
1107
|
|
|
1110
1108
|
// Database
|
|
@@ -1115,35 +1113,67 @@ async function status() {
|
|
|
1115
1113
|
const obs = db.prepare('SELECT COUNT(*) as c FROM observations').get();
|
|
1116
1114
|
const sess = db.prepare('SELECT COUNT(*) as c FROM session_summaries').get();
|
|
1117
1115
|
db.close();
|
|
1118
|
-
ok
|
|
1116
|
+
push('ok', 'database', `Database: ${obs.c} observations, ${sess.c} sessions`, { exists: true, observations: obs.c, sessions: sess.c });
|
|
1119
1117
|
} catch (e) {
|
|
1120
|
-
|
|
1118
|
+
push('warn', 'database', 'Database: exists but check failed — ' + e.message, { exists: true, error: e.message });
|
|
1121
1119
|
}
|
|
1122
1120
|
} else {
|
|
1123
|
-
|
|
1121
|
+
push('warn', 'database', 'Database: not found', { exists: false });
|
|
1124
1122
|
}
|
|
1125
1123
|
|
|
1126
1124
|
// CLI
|
|
1127
1125
|
try {
|
|
1128
1126
|
execFileSync('claude-mem-lite', ['--help'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
1129
|
-
|
|
1127
|
+
push('ok', 'cli', 'CLI: claude-mem-lite command available', { available: true });
|
|
1130
1128
|
} catch {
|
|
1131
|
-
|
|
1129
|
+
push('warn', 'cli', 'CLI: command not on PATH — run install again to create symlink', { available: false });
|
|
1132
1130
|
}
|
|
1133
1131
|
|
|
1134
1132
|
// Old system
|
|
1135
1133
|
const vectorDb = join(OLD_DATA_DIR, 'vector-db');
|
|
1136
1134
|
if (existsSync(vectorDb)) {
|
|
1137
|
-
|
|
1135
|
+
push('warn', 'old_data', 'Old vector-db still exists (can be removed)', { vector_db_exists: true });
|
|
1138
1136
|
}
|
|
1139
1137
|
|
|
1138
|
+
if (json) {
|
|
1139
|
+
const out = {};
|
|
1140
|
+
for (const c of checks) {
|
|
1141
|
+
const { level, key, message, ...extra } = c;
|
|
1142
|
+
out[key] = { level, message, ...extra };
|
|
1143
|
+
}
|
|
1144
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
console.log('\nclaude-mem-lite status\n');
|
|
1149
|
+
for (const c of checks) {
|
|
1150
|
+
if (c.level === 'ok') ok(c.message);
|
|
1151
|
+
else if (c.level === 'warn') warn(c.message);
|
|
1152
|
+
else fail(c.message);
|
|
1153
|
+
}
|
|
1140
1154
|
console.log('');
|
|
1141
1155
|
}
|
|
1142
1156
|
|
|
1143
1157
|
// ─── Doctor ─────────────────────────────────────────────────────────────────
|
|
1144
1158
|
|
|
1145
1159
|
async function doctor() {
|
|
1146
|
-
|
|
1160
|
+
// Dogfood-9: structured --json output for CI / wrapper scripts that want to
|
|
1161
|
+
// act on individual checks (e.g. "fail my deploy if FTS5 integrity not ok").
|
|
1162
|
+
// Implementation strategy: shadow ok/warn/fail/log inside doctor() so every
|
|
1163
|
+
// existing call site automatically captures into `checks`, and route final
|
|
1164
|
+
// output to JSON or text. Mirror install.mjs::status() shape — { key: {...} }
|
|
1165
|
+
// would lose ordering, so use a flat array of { level, message } objects
|
|
1166
|
+
// (doctor checks are ordered by significance: deps → server → DB → drift).
|
|
1167
|
+
const json = flags.has('--json');
|
|
1168
|
+
const checks = [];
|
|
1169
|
+
if (!json) console.log('\nclaude-mem-lite doctor\n');
|
|
1170
|
+
|
|
1171
|
+
// Shadow file-level helpers so every call site auto-records.
|
|
1172
|
+
const ok = (msg) => { checks.push({ level: 'ok', message: msg }); if (!json) console.log(` ✓ ${msg}`); };
|
|
1173
|
+
const warn = (msg) => { checks.push({ level: 'warn', message: msg }); if (!json) console.log(` ⚠ ${msg}`); };
|
|
1174
|
+
const fail = (msg) => { checks.push({ level: 'fail', message: msg }); if (!json) console.log(` ✗ ${msg}`); };
|
|
1175
|
+
const log = (msg) => { if (!json) console.log(` ${msg}`); };
|
|
1176
|
+
|
|
1147
1177
|
let issues = 0;
|
|
1148
1178
|
let warnings = 0;
|
|
1149
1179
|
// Doctor-local ⚠ helper: visually identical to the file-level `warn`, but
|
|
@@ -1151,7 +1181,7 @@ async function doctor() {
|
|
|
1151
1181
|
// "warnings present". Used for informational ⚠ checks; the two ⚠ paths
|
|
1152
1182
|
// that ALSO bump `issues` (stale procs, dev drift) keep using the file-level
|
|
1153
1183
|
// `warn` directly to avoid double-counting.
|
|
1154
|
-
const dwarn = (msg) => { warnings++;
|
|
1184
|
+
const dwarn = (msg) => { warnings++; warn(msg); };
|
|
1155
1185
|
|
|
1156
1186
|
// Node version
|
|
1157
1187
|
const nodeVer = process.version;
|
|
@@ -1398,7 +1428,16 @@ async function doctor() {
|
|
|
1398
1428
|
} catch {}
|
|
1399
1429
|
}
|
|
1400
1430
|
|
|
1401
|
-
|
|
1431
|
+
if (json) {
|
|
1432
|
+
console.log(JSON.stringify({
|
|
1433
|
+
issues,
|
|
1434
|
+
warnings,
|
|
1435
|
+
summary: buildDoctorSummary(issues, warnings),
|
|
1436
|
+
checks,
|
|
1437
|
+
}, null, 2));
|
|
1438
|
+
} else {
|
|
1439
|
+
console.log(`\n ${buildDoctorSummary(issues, warnings)}\n`);
|
|
1440
|
+
}
|
|
1402
1441
|
// Diagnostic-tool exit-code contract: any ✗-level finding must propagate non-zero
|
|
1403
1442
|
// so CI / wrapper scripts (`claude-mem-lite doctor || alert`) actually trip. Keeps
|
|
1404
1443
|
// ⚠-only states at exit 0 (#8268 already established the visual ⚠ vs counted-issue
|
|
@@ -1532,7 +1571,12 @@ function writeSettings(settings) {
|
|
|
1532
1571
|
// ─── Cleanup Stale Files ─────────────────────────────────────────────────────
|
|
1533
1572
|
|
|
1534
1573
|
function cleanup() {
|
|
1535
|
-
|
|
1574
|
+
// Dogfood-7 addition: --dry-run lists which files would be removed without
|
|
1575
|
+
// touching disk. Useful before running cleanup on a remote/CI machine where
|
|
1576
|
+
// accidentally pruning the wrong file would be costly. Doctor reports stale
|
|
1577
|
+
// file counts and points users here; --dry-run lets them confirm the list.
|
|
1578
|
+
const dryRun = flags.has('--dry-run');
|
|
1579
|
+
console.log(`\nclaude-mem-lite cleanup${dryRun ? ' (--dry-run)' : ''}\n`);
|
|
1536
1580
|
let removed = 0;
|
|
1537
1581
|
|
|
1538
1582
|
// Clean .update-staging-* / .update-backup-* in INSTALL_DIR
|
|
@@ -1540,6 +1584,11 @@ function cleanup() {
|
|
|
1540
1584
|
if (existsSync(INSTALL_DIR)) {
|
|
1541
1585
|
for (const f of readdirSync(INSTALL_DIR)) {
|
|
1542
1586
|
if (stalePatterns.some(p => f.startsWith(p))) {
|
|
1587
|
+
if (dryRun) {
|
|
1588
|
+
ok(`Would remove: ${f}`);
|
|
1589
|
+
removed++;
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1543
1592
|
try {
|
|
1544
1593
|
rmSync(join(INSTALL_DIR, f), { recursive: true, force: true });
|
|
1545
1594
|
ok(`Removed: ${f}`);
|
|
@@ -1556,6 +1605,11 @@ function cleanup() {
|
|
|
1556
1605
|
if (existsSync(runtimeDir)) {
|
|
1557
1606
|
for (const f of readdirSync(runtimeDir)) {
|
|
1558
1607
|
if (f.startsWith('pending-') || f.startsWith('ep-flush-')) {
|
|
1608
|
+
if (dryRun) {
|
|
1609
|
+
ok(`Would remove: runtime/${f}`);
|
|
1610
|
+
removed++;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1559
1613
|
try {
|
|
1560
1614
|
rmSync(join(runtimeDir, f), { force: true });
|
|
1561
1615
|
ok(`Removed: runtime/${f}`);
|
|
@@ -1567,7 +1621,8 @@ function cleanup() {
|
|
|
1567
1621
|
}
|
|
1568
1622
|
}
|
|
1569
1623
|
|
|
1570
|
-
|
|
1624
|
+
const verb = dryRun ? 'would be removed' : 'removed';
|
|
1625
|
+
console.log(`\n ${removed === 0 ? 'No stale files found.' : `${removed} stale file(s) ${verb}.`}\n`);
|
|
1571
1626
|
}
|
|
1572
1627
|
|
|
1573
1628
|
// ─── Manual Update ───────────────────────────────────────────────────────────
|
|
@@ -1707,6 +1762,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1707
1762
|
// npx claude-mem-lite (no args) → auto install
|
|
1708
1763
|
await install();
|
|
1709
1764
|
} else {
|
|
1765
|
+
// Name the unknown token before the usage block. Pre-fix `install frobnicate`
|
|
1766
|
+
// dumped usage silently, which read like the user had typed nothing — they had
|
|
1767
|
+
// no idea their command was rejected.
|
|
1768
|
+
if (cmd) {
|
|
1769
|
+
console.error(`[install] Unknown command: "${cmd}"`);
|
|
1770
|
+
process.exitCode = 1;
|
|
1771
|
+
}
|
|
1710
1772
|
console.log(`
|
|
1711
1773
|
claude-mem-lite — Lightweight memory system for Claude Code
|
|
1712
1774
|
|
|
@@ -1715,9 +1777,9 @@ Usage:
|
|
|
1715
1777
|
node install.mjs install --dev Install dev mode (symlinks to dev dir)
|
|
1716
1778
|
node install.mjs uninstall Remove (keep data)
|
|
1717
1779
|
node install.mjs uninstall --purge Remove and delete all data
|
|
1718
|
-
node install.mjs status Show current status
|
|
1719
|
-
node install.mjs doctor Diagnose issues
|
|
1720
|
-
node install.mjs cleanup Remove stale temp/staging files
|
|
1780
|
+
node install.mjs status Show current status (use --json for structured output)
|
|
1781
|
+
node install.mjs doctor Diagnose issues (use --json for structured output)
|
|
1782
|
+
node install.mjs cleanup Remove stale temp/staging files (use --dry-run to preview)
|
|
1721
1783
|
node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
|
|
1722
1784
|
node install.mjs self-update Check for and install updates
|
|
1723
1785
|
node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
|
package/mem-cli.mjs
CHANGED
|
@@ -104,13 +104,16 @@ function cmdSearch(db, args) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Warn if obs-only filters used with non-observation source
|
|
107
|
-
if (source && source !== 'observations' && (type || tier || minImportance)) {
|
|
108
|
-
const ignored = [type && '--type', tier && '--tier', minImportance && '--importance'].filter(Boolean);
|
|
107
|
+
if (source && source !== 'observations' && (type || tier || minImportance || branch)) {
|
|
108
|
+
const ignored = [type && '--type', tier && '--tier', minImportance && '--importance', branch && '--branch'].filter(Boolean);
|
|
109
109
|
process.stderr.write(`[mem] Note: ${ignored.join(', ')} only apply to observations, ignored for --source ${source}\n`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// When --type/--tier/--importance (obs-only fields) is specified, implicitly restrict to observations
|
|
113
|
-
|
|
112
|
+
// When --type/--tier/--importance/--branch (obs-only fields) is specified, implicitly restrict to observations.
|
|
113
|
+
// --branch was previously cross-source: sessions/prompts have no branch column, so a query like
|
|
114
|
+
// `search "cache" --branch main` would include unrelated session/prompt rows, surprising users
|
|
115
|
+
// who passed --branch expecting a branch-scoped result.
|
|
116
|
+
const effectiveSource = source || ((type || tier || minImportance || branch) ? 'observations' : null);
|
|
114
117
|
|
|
115
118
|
// Cross-source mode: each source needs more candidates than the final limit
|
|
116
119
|
// so the post-merge sort has room to pick the best from each (paired-path with
|
|
@@ -392,9 +395,21 @@ function cmdRecent(db, args) {
|
|
|
392
395
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
393
396
|
const jsonOutput = flags.json === true || flags.json === 'true';
|
|
394
397
|
|
|
398
|
+
// `recent --type bugfix` previously parsed as a silent no-op — users naturally
|
|
399
|
+
// try this for "show recent bugfixes". Mirror cmdSearch's enum validation.
|
|
400
|
+
const type = flags.type || null;
|
|
401
|
+
if (type) {
|
|
402
|
+
const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
403
|
+
if (!validObsTypes.has(type)) {
|
|
404
|
+
fail(`[mem] Invalid --type "${type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
395
409
|
const params = [];
|
|
396
410
|
const wheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL'];
|
|
397
411
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
412
|
+
if (type) { wheres.push('type = ?'); params.push(type); }
|
|
398
413
|
params.push(limit);
|
|
399
414
|
|
|
400
415
|
const rows = db.prepare(`
|
|
@@ -409,6 +424,7 @@ function cmdRecent(db, args) {
|
|
|
409
424
|
out(JSON.stringify({
|
|
410
425
|
project: project || null,
|
|
411
426
|
limit,
|
|
427
|
+
type: type || null,
|
|
412
428
|
total: rows.length,
|
|
413
429
|
results: rows.map(r => ({
|
|
414
430
|
id: r.id,
|
|
@@ -1010,6 +1026,13 @@ function cmdDeferAdd(db, args) {
|
|
|
1010
1026
|
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1011
1027
|
return;
|
|
1012
1028
|
}
|
|
1029
|
+
// Mirror MCP memDeferSchema.title (z.string().min(1).max(200)). CLI used to
|
|
1030
|
+
// accept multi-line / 1000-char titles, then `defer list` would render them
|
|
1031
|
+
// as one wrapped row that pushed every other item off-screen.
|
|
1032
|
+
if (title.length > 200) {
|
|
1033
|
+
fail(`[mem] defer add: title too long (${title.length} chars, max 200). Move detail to --detail "<text>".`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1013
1036
|
const priority = flags.priority !== undefined ? parseInt(flags.priority, 10) : 2;
|
|
1014
1037
|
if (![1, 2, 3].includes(priority)) {
|
|
1015
1038
|
fail(`[mem] Invalid --priority "${flags.priority}". Must be 1 (low), 2 (normal), or 3 (urgent).`);
|
|
@@ -1054,7 +1077,7 @@ function cmdDeferList(db, args) {
|
|
|
1054
1077
|
function cmdDeferDrop(db, args) {
|
|
1055
1078
|
const { positional, flags } = parseArgs(args);
|
|
1056
1079
|
if (positional.length === 0) {
|
|
1057
|
-
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1080
|
+
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N>[,id2,...] --reason "<reason>" [--project P]');
|
|
1058
1081
|
return;
|
|
1059
1082
|
}
|
|
1060
1083
|
const reason = flags.reason;
|
|
@@ -1062,26 +1085,34 @@ function cmdDeferDrop(db, args) {
|
|
|
1062
1085
|
fail('[mem] defer drop requires --reason "<non-empty string>"');
|
|
1063
1086
|
return;
|
|
1064
1087
|
}
|
|
1065
|
-
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
const
|
|
1088
|
+
// Accept either a single token or a comma-separated batch. `save --closes-deferred`
|
|
1089
|
+
// already accepts the batch form (cmdSave uses resolveDeferredIds on a split list);
|
|
1090
|
+
// drop now mirrors that ergonomic so users can prune multiple items in one call
|
|
1091
|
+
// without N shell invocations.
|
|
1092
|
+
const rawTokens = positional.join(' ').split(',').map(s => s.trim()).filter(Boolean);
|
|
1093
|
+
const tokens = rawTokens.map(t => /^\d+$/.test(t) ? parseInt(t, 10) : t);
|
|
1070
1094
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1071
1095
|
|
|
1072
|
-
let
|
|
1096
|
+
let realIds;
|
|
1073
1097
|
try {
|
|
1074
|
-
|
|
1098
|
+
realIds = resolveDeferredIds(db, project, tokens);
|
|
1075
1099
|
} catch (e) {
|
|
1076
1100
|
fail(`[mem] defer drop: ${e.message}`);
|
|
1077
1101
|
return;
|
|
1078
1102
|
}
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1103
|
+
const dropped = [];
|
|
1104
|
+
const noop = [];
|
|
1105
|
+
for (const realId of realIds) {
|
|
1106
|
+
const r = dropDeferred(db, realId, reason);
|
|
1107
|
+
if (r.changed === 0) noop.push(realId);
|
|
1108
|
+
else dropped.push(realId);
|
|
1109
|
+
}
|
|
1110
|
+
if (dropped.length > 0) {
|
|
1111
|
+
out(`[mem] Dropped ${dropped.map(id => `D#${id}`).join(', ')} in project "${project}". Reason: ${reason.trim()}`);
|
|
1112
|
+
}
|
|
1113
|
+
if (noop.length > 0) {
|
|
1114
|
+
out(`[mem] No-op (not in 'open' status): ${noop.map(id => `D#${id}`).join(', ')}`);
|
|
1083
1115
|
}
|
|
1084
|
-
out(`[mem] Dropped D#${realId} in project "${project}". Reason: ${reason.trim()}`);
|
|
1085
1116
|
}
|
|
1086
1117
|
|
|
1087
1118
|
// N-1: Quality-focused stats for R-2 A/B baseline.
|
|
@@ -1563,7 +1594,15 @@ function cmdUpdate(db, args) {
|
|
|
1563
1594
|
|
|
1564
1595
|
const updates = [];
|
|
1565
1596
|
const params = [];
|
|
1566
|
-
if (flags.title !== undefined) {
|
|
1597
|
+
if (flags.title !== undefined) {
|
|
1598
|
+
// Reject empty title — clears the observation's identifier and would render it
|
|
1599
|
+
// as `(untitled)` in every listing. Almost always an accidental shell-stripped arg.
|
|
1600
|
+
if (typeof flags.title === 'string' && flags.title.trim() === '') {
|
|
1601
|
+
fail('[mem] --title cannot be empty. Pass a non-empty string or omit the flag to leave the title unchanged.');
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
updates.push('title = ?'); params.push(scrubSecrets(flags.title));
|
|
1605
|
+
}
|
|
1567
1606
|
if (flags.narrative !== undefined) { updates.push('narrative = ?'); params.push(scrubSecrets(flags.narrative)); }
|
|
1568
1607
|
if (flags.type) {
|
|
1569
1608
|
const validTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
@@ -1581,7 +1620,18 @@ function cmdUpdate(db, args) {
|
|
|
1581
1620
|
}
|
|
1582
1621
|
updates.push('importance = ?'); params.push(imp);
|
|
1583
1622
|
}
|
|
1584
|
-
if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) {
|
|
1623
|
+
if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) {
|
|
1624
|
+
const rawLesson = flags.lesson ?? flags['lesson-learned'] ?? '';
|
|
1625
|
+
// Mirror cmdSave's 500-char cap — pre-fix `update --lesson <501-char>` was silently
|
|
1626
|
+
// accepted, letting overlong lessons leak into the DB through the update path
|
|
1627
|
+
// even though save's path rejected them. MCP memSaveSchema also caps at 500.
|
|
1628
|
+
if (typeof rawLesson === 'string' && rawLesson.length > 500) {
|
|
1629
|
+
fail(`[mem] --lesson too long (${rawLesson.length} chars, max 500).`);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
updates.push('lesson_learned = ?');
|
|
1633
|
+
params.push(scrubSecrets(rawLesson));
|
|
1634
|
+
}
|
|
1585
1635
|
if (flags.concepts !== undefined) { updates.push('concepts = ?'); params.push(flags.concepts); }
|
|
1586
1636
|
|
|
1587
1637
|
if (updates.length === 0) {
|
|
@@ -1632,7 +1682,16 @@ function cmdExport(db, args) {
|
|
|
1632
1682
|
|
|
1633
1683
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1634
1684
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
1635
|
-
if (flags.type) {
|
|
1685
|
+
if (flags.type) {
|
|
1686
|
+
// Reject unknown types — silently returning [] for `--type bogus` looked like a
|
|
1687
|
+
// legitimate empty filter result, hiding the typo. Mirrors cmdSearch / cmdSave / cmdUpdate.
|
|
1688
|
+
const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
1689
|
+
if (!validObsTypes.has(flags.type)) {
|
|
1690
|
+
fail(`[mem] Invalid --type "${flags.type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
wheres.push('type = ?'); params.push(flags.type);
|
|
1694
|
+
}
|
|
1636
1695
|
let exportFromEpoch = null;
|
|
1637
1696
|
let exportToEpoch = null;
|
|
1638
1697
|
if (flags.from) {
|
|
@@ -1690,8 +1749,18 @@ function cmdExport(db, args) {
|
|
|
1690
1749
|
function cmdCompress(db, args) {
|
|
1691
1750
|
const { flags } = parseArgs(args);
|
|
1692
1751
|
const preview = flags.execute !== true && flags.execute !== 'true';
|
|
1693
|
-
|
|
1694
|
-
|
|
1752
|
+
// Reject malformed --age-days explicitly. The prior fallback (`|| 30`) silently used
|
|
1753
|
+
// the default whenever the value parsed as NaN or <1, so users typing `--age-days abc`
|
|
1754
|
+
// got the 30-day cutoff without knowing their input was discarded.
|
|
1755
|
+
let ageDays = 30;
|
|
1756
|
+
if (flags['age-days'] !== undefined) {
|
|
1757
|
+
const parsed = parseInt(flags['age-days'], 10);
|
|
1758
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1759
|
+
fail(`[mem] Invalid --age-days "${flags['age-days']}". Must be a positive integer.`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
ageDays = parsed;
|
|
1763
|
+
}
|
|
1695
1764
|
const cutoff = Date.now() - ageDays * 86400000;
|
|
1696
1765
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1697
1766
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
@@ -2084,6 +2153,20 @@ function cmdRegistry(_memDb, args) {
|
|
|
2084
2153
|
return;
|
|
2085
2154
|
}
|
|
2086
2155
|
|
|
2156
|
+
// `--type` and `--resource-type` are both constrained to skill|agent across
|
|
2157
|
+
// registry sub-actions. Validating once here means search/list/import/remove
|
|
2158
|
+
// all reject typos like `--type sklil` instead of silently returning
|
|
2159
|
+
// "No resources found." (which looked like the registry was empty for that
|
|
2160
|
+
// type, not like a typo).
|
|
2161
|
+
if (flags.type !== undefined && flags.type !== 'skill' && flags.type !== 'agent') {
|
|
2162
|
+
fail(`[mem] Invalid --type "${flags.type}". Use: skill, agent`);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (flags['resource-type'] !== undefined && flags['resource-type'] !== 'skill' && flags['resource-type'] !== 'agent') {
|
|
2166
|
+
fail(`[mem] Invalid --resource-type "${flags['resource-type']}". Use: skill, agent`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2087
2170
|
try {
|
|
2088
2171
|
if (action === 'search') {
|
|
2089
2172
|
const query = flags.query || positional.slice(1).join(' ');
|
|
@@ -2199,7 +2282,9 @@ function cmdRegistry(_memDb, args) {
|
|
|
2199
2282
|
const resourceType = flags['resource-type'];
|
|
2200
2283
|
if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry remove --name N --resource-type skill|agent'); return; }
|
|
2201
2284
|
const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
|
|
2202
|
-
out(result.changes > 0
|
|
2285
|
+
out(result.changes > 0
|
|
2286
|
+
? `[mem] Removed: ${resourceType}:${name}`
|
|
2287
|
+
: `[mem] Not found: ${resourceType}:${name}`);
|
|
2203
2288
|
return;
|
|
2204
2289
|
}
|
|
2205
2290
|
|
|
@@ -2286,7 +2371,7 @@ Commands:
|
|
|
2286
2371
|
--project P Filter by project
|
|
2287
2372
|
--from DATE Start date (YYYY-MM-DD or ISO 8601)
|
|
2288
2373
|
--to DATE End date (YYYY-MM-DD or ISO 8601)
|
|
2289
|
-
--importance N Minimum importance (1
|
|
2374
|
+
--importance N Minimum importance (1=routine, 2=notable, 3=critical)
|
|
2290
2375
|
--branch B Filter by git branch
|
|
2291
2376
|
--offset N Skip first N results (pagination)
|
|
2292
2377
|
--tier T Filter by tier (working|active|archive, observations only)
|
|
@@ -2298,7 +2383,8 @@ Commands:
|
|
|
2298
2383
|
recent [N] Show N most recent observations (default 10)
|
|
2299
2384
|
--limit N Sibling-parity alias for [N] (max 1000)
|
|
2300
2385
|
--project P Filter by project
|
|
2301
|
-
--
|
|
2386
|
+
--type T Filter obs type (bugfix|decision|discovery|feature|refactor|change)
|
|
2387
|
+
--json Output as JSON: {project,limit,type,total,results:[…]}
|
|
2302
2388
|
|
|
2303
2389
|
recall <file> Show observations related to a file
|
|
2304
2390
|
--limit N Max results (default 10)
|
|
@@ -2328,22 +2414,22 @@ Commands:
|
|
|
2328
2414
|
save "<text>" Save a new observation
|
|
2329
2415
|
--type T Observation type (default: discovery)
|
|
2330
2416
|
--title T Title (auto-generated if omitted)
|
|
2331
|
-
--importance N 1
|
|
2417
|
+
--importance N 1=routine, 2=notable, 3=critical (default: 2)
|
|
2332
2418
|
--project P Project name
|
|
2333
2419
|
--files f1,f2 Comma-separated file paths
|
|
2334
2420
|
--lesson T Lesson learned (≤500 chars; alias: --lesson-learned)
|
|
2335
2421
|
--closes-deferred 1,D#42 Close deferred items in same transaction
|
|
2336
2422
|
|
|
2337
2423
|
defer <action> First-class deferred work (v2.70+)
|
|
2338
|
-
add "<title>" Mark deferred work for next session
|
|
2339
|
-
--priority N 1
|
|
2424
|
+
add "<title>" Mark deferred work for next session (≤200 chars)
|
|
2425
|
+
--priority N 1=low, 2=normal, 3=urgent (default: 2)
|
|
2340
2426
|
--detail T Constraint + why deferred
|
|
2341
2427
|
--files f1,f2 Comma-separated file paths
|
|
2342
2428
|
--project P Project name
|
|
2343
2429
|
list List open deferred items
|
|
2344
2430
|
--limit N Max results (default 10)
|
|
2345
2431
|
--project P Filter by project
|
|
2346
|
-
drop <D#N|ordinal> Drop
|
|
2432
|
+
drop <D#N|ordinal>[,...] Drop one or more deferred items (no fix needed)
|
|
2347
2433
|
--reason "..." Required audit trail
|
|
2348
2434
|
|
|
2349
2435
|
delete <id1,id2,...> Delete observations by ID
|
|
@@ -2352,7 +2438,7 @@ Commands:
|
|
|
2352
2438
|
update <id> Update an existing observation
|
|
2353
2439
|
--title T New title
|
|
2354
2440
|
--type T New type
|
|
2355
|
-
--importance N New importance (1
|
|
2441
|
+
--importance N New importance (1=routine, 2=notable, 3=critical)
|
|
2356
2442
|
--lesson T Add/update lesson learned (alias: --lesson-learned)
|
|
2357
2443
|
--narrative T New narrative
|
|
2358
2444
|
--concepts T Space-separated concept tags
|
|
@@ -2383,6 +2469,8 @@ Commands:
|
|
|
2383
2469
|
--task T Comma-separated: re-enrich,normalize,cluster-merge,smart-compress
|
|
2384
2470
|
--max N Max items per task (1-100, default 15)
|
|
2385
2471
|
--scope S re-enrich scope: narrow (default) or wide
|
|
2472
|
+
--project P Limit to a single project (.|current = inferProject())
|
|
2473
|
+
--verbose / -v Preview also dumps cluster contents + re-enrich samples
|
|
2386
2474
|
|
|
2387
2475
|
doctor Environment diagnostics and benchmarks
|
|
2388
2476
|
--benchmark Run perf benchmark and emit JSON
|
|
@@ -2449,6 +2537,8 @@ Commands:
|
|
|
2449
2537
|
|
|
2450
2538
|
unadopt Precise removal of the sentinel block + plugin_claude_mem_lite.md.
|
|
2451
2539
|
--all Unadopt every project
|
|
2540
|
+
--status Read-only: list adopted projects (same as adopt --status)
|
|
2541
|
+
--dry-run Preview what would be removed; no filesystem writes
|
|
2452
2542
|
|
|
2453
2543
|
memdir-audit Audit memdir feedback_*.md / project_*.md for the
|
|
2454
2544
|
body-structure contract (**Why:** + **How to apply:**).
|
|
@@ -2549,16 +2639,30 @@ async function cmdImportJsonl(db, argv) {
|
|
|
2549
2639
|
if (files.length === 0) { out('[mem] No .jsonl files found.'); return; }
|
|
2550
2640
|
|
|
2551
2641
|
const { importJsonl } = await import('./lib/import-jsonl.mjs');
|
|
2552
|
-
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0;
|
|
2642
|
+
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0, errorCount = 0;
|
|
2553
2643
|
for (const f of files) {
|
|
2554
|
-
|
|
2644
|
+
// Per-file isolation: one unreadable file (EACCES, EBUSY, mid-batch IO error)
|
|
2645
|
+
// shouldn't crash the whole import — readFileSync inside importJsonl would
|
|
2646
|
+
// otherwise throw an unhandled exception with a node stack trace, leaving
|
|
2647
|
+
// earlier successes uncommitted-looking from the user's perspective.
|
|
2648
|
+
let r;
|
|
2649
|
+
try {
|
|
2650
|
+
r = await importJsonl(db, f, { project });
|
|
2651
|
+
} catch (e) {
|
|
2652
|
+
errorCount++;
|
|
2653
|
+
// e.message for node fs errors already begins with the code (e.g. "EACCES: permission denied, ...");
|
|
2654
|
+
// don't double-prefix.
|
|
2655
|
+
process.stderr.write(`[mem] ${f}: import failed — ${e.message}\n`);
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2555
2658
|
totalPrompts += r.prompts;
|
|
2556
2659
|
totalObs += r.observations;
|
|
2557
2660
|
totalSkip += r.skipped;
|
|
2558
2661
|
totalOrphans += r.orphans || 0;
|
|
2559
2662
|
out(`[mem] ${f}: +${r.prompts} prompts, +${r.observations} observations, ${r.orphans || 0} orphan tool_use, ${r.skipped} skipped`);
|
|
2560
2663
|
}
|
|
2561
|
-
|
|
2664
|
+
const errorTail = errorCount > 0 ? `, ${errorCount} file(s) errored` : '';
|
|
2665
|
+
out(`[mem] Total: ${totalPrompts} prompts, ${totalObs} observations, ${totalOrphans} orphan tool_use, ${totalSkip} skipped from ${files.length} file(s)${errorTail}.`);
|
|
2562
2666
|
if (totalPrompts > 0 || totalObs > 0) {
|
|
2563
2667
|
out(`[mem] Try: claude-mem-lite recent 5 --project ${project}`);
|
|
2564
2668
|
}
|
|
@@ -2617,6 +2721,7 @@ async function cmdEnrich(argv) {
|
|
|
2617
2721
|
async function cmdOptimize(db, args) {
|
|
2618
2722
|
const run = args.includes('--run');
|
|
2619
2723
|
const runAll = args.includes('--run-all');
|
|
2724
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
2620
2725
|
// T2-P1-D: --task accepts a single task or a comma-separated list, parity with MCP memOptimizeSchema.tasks.
|
|
2621
2726
|
const VALID_TASKS = ['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'];
|
|
2622
2727
|
const taskIdx = args.indexOf('--task');
|
|
@@ -2645,25 +2750,67 @@ async function cmdOptimize(db, args) {
|
|
|
2645
2750
|
}
|
|
2646
2751
|
// R-7 micro: --scope wide targets bugfix/refactor/feature/decision with narrative but no
|
|
2647
2752
|
// lesson_learned (the "Haiku judged 'none'" cases). Default 'narrow' preserves old behavior.
|
|
2753
|
+
// Validate explicitly so `--scope wlde` (typo) doesn't silently become narrow and waste an LLM run.
|
|
2648
2754
|
const scopeIdx = args.indexOf('--scope');
|
|
2649
|
-
|
|
2755
|
+
let reenrichScope = 'narrow';
|
|
2756
|
+
if (scopeIdx >= 0 && args[scopeIdx + 1] !== undefined) {
|
|
2757
|
+
const raw = args[scopeIdx + 1];
|
|
2758
|
+
if (raw !== 'narrow' && raw !== 'wide') {
|
|
2759
|
+
fail(`[mem] Invalid --scope "${raw}". Use: narrow, wide`);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
reenrichScope = raw;
|
|
2763
|
+
}
|
|
2764
|
+
// --project <name> filters all 4 tasks to one project. Opt-in; absence
|
|
2765
|
+
// preserves prior cross-project default. `.` or `current` auto-resolve via
|
|
2766
|
+
// inferProject() so users don't need to remember the exact name.
|
|
2767
|
+
const projectIdx = args.indexOf('--project');
|
|
2768
|
+
let project;
|
|
2769
|
+
if (projectIdx >= 0 && args[projectIdx + 1]) {
|
|
2770
|
+
const raw = args[projectIdx + 1];
|
|
2771
|
+
project = (raw === '.' || raw === 'current') ? inferProject() : raw;
|
|
2772
|
+
}
|
|
2650
2773
|
|
|
2651
2774
|
if (!run && !runAll) {
|
|
2652
|
-
const preview = optimizePreview(db);
|
|
2775
|
+
const preview = optimizePreview(db, { project, detail: verbose });
|
|
2653
2776
|
out('[mem] 🔍 LLM Optimization Preview:');
|
|
2777
|
+
if (project) out(` Project filter: ${project}`);
|
|
2654
2778
|
out(` Re-enrich candidates: ${preview.reenrich}${preview.reenrichWide !== undefined && preview.reenrichWide !== null ? ` (wide scope: ${preview.reenrichWide})` : ''}`);
|
|
2655
2779
|
out(` Normalize: ${preview.normalizeGateOpen ? `${preview.normalize} unique concepts` : 'gate closed (7-day interval)'}`);
|
|
2656
2780
|
out(` Cluster-merge: ${preview.clusterMerge} clusters`);
|
|
2657
2781
|
out(` Smart-compress: ${preview.smartCompress} clusters`);
|
|
2658
2782
|
out(` Total: ${preview.total} items`);
|
|
2783
|
+
if (verbose) {
|
|
2784
|
+
out('');
|
|
2785
|
+
if (preview.mergeClusters && preview.mergeClusters.length > 0) {
|
|
2786
|
+
out('─── Cluster-merge details ───');
|
|
2787
|
+
for (const [i, cluster] of preview.mergeClusters.entries()) {
|
|
2788
|
+
out(` Cluster ${i + 1} (${cluster.length} obs, project=${cluster[0]?.project || '?'}):`);
|
|
2789
|
+
for (const obs of cluster) out(` #${obs.id} [${obs.type || 'change'}] ${truncate(obs.title || '(untitled)', 100)}`);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (preview.reenrichSamples && preview.reenrichSamples.length > 0) {
|
|
2793
|
+
out('─── Re-enrich sample (first 20) ───');
|
|
2794
|
+
for (const obs of preview.reenrichSamples) {
|
|
2795
|
+
out(` #${obs.id} [${obs.type || 'change'}] (project=${obs.project || '?'}) ${truncate(obs.title || '(untitled)', 100)}`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
if (preview.compressSamples && preview.compressSamples.length > 0) {
|
|
2799
|
+
out('─── Smart-compress sample (first 5 clusters) ───');
|
|
2800
|
+
for (const [i, cluster] of preview.compressSamples.entries()) {
|
|
2801
|
+
out(` Cluster ${i + 1} (${cluster.observations?.length || 0} obs, project=${cluster.project || '?'})`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2659
2805
|
out('');
|
|
2660
2806
|
out('Run with --run to execute, --run-all to bypass gates.');
|
|
2661
2807
|
out('For R-7 backfill: --run --task re-enrich --scope wide --max N');
|
|
2808
|
+
out('Scope: --project <name|.|current> to limit; --verbose for cluster details.');
|
|
2662
2809
|
return;
|
|
2663
2810
|
}
|
|
2664
2811
|
|
|
2665
|
-
out(`[mem] Running LLM optimization${reenrichScope === 'wide' ? ' (scope: wide)' : ''}...`);
|
|
2666
|
-
const results = await optimizeRun(db, { tasks, maxItems, force: runAll, reenrichScope });
|
|
2812
|
+
out(`[mem] Running LLM optimization${reenrichScope === 'wide' ? ' (scope: wide)' : ''}${project ? ` (project: ${project})` : ''}...`);
|
|
2813
|
+
const results = await optimizeRun(db, { tasks, maxItems, force: runAll, reenrichScope, project });
|
|
2667
2814
|
|
|
2668
2815
|
if (results.reenrich) out(` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`);
|
|
2669
2816
|
if (results.normalize) {
|
|
@@ -2722,7 +2869,11 @@ export async function run(argv) {
|
|
|
2722
2869
|
const JSON_SUPPORTED_CMDS = new Set([
|
|
2723
2870
|
'search', 'context', 'recent', 'recall', 'timeline', 'stats', 'browse', 'export',
|
|
2724
2871
|
]);
|
|
2725
|
-
|
|
2872
|
+
// `doctor --benchmark` already emits JSON on its own — don't print the misleading
|
|
2873
|
+
// "doctor outputs text" note for that subpath. Without --benchmark, doctor is text
|
|
2874
|
+
// and the note is still useful.
|
|
2875
|
+
const doctorBenchmark = cmd === 'doctor' && cmdArgs.includes('--benchmark');
|
|
2876
|
+
if (cmdArgs.includes('--json') && !JSON_SUPPORTED_CMDS.has(cmd) && !doctorBenchmark) {
|
|
2726
2877
|
process.stderr.write(`[mem] Note: --json is supported only on: ${[...JSON_SUPPORTED_CMDS].join(', ')}. "${cmd}" outputs text.\n`);
|
|
2727
2878
|
}
|
|
2728
2879
|
|
|
@@ -2756,6 +2907,19 @@ export async function run(argv) {
|
|
|
2756
2907
|
out('[mem] Run "claude-mem-lite help" for usage');
|
|
2757
2908
|
process.exitCode = 1;
|
|
2758
2909
|
}
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
// SQLITE_BUSY / SQLITE_LOCKED + extended variants (SQLITE_BUSY_SNAPSHOT,
|
|
2912
|
+
// SQLITE_BUSY_RECOVERY, SQLITE_LOCKED_SHAREDCACHE…). All mean the same thing
|
|
2913
|
+
// to the user: writer contention past the 5s busy_timeout. Pre-fix this
|
|
2914
|
+
// raised an unhandled SqliteError with a node stack trace.
|
|
2915
|
+
const code = e && typeof e.code === 'string' ? e.code : '';
|
|
2916
|
+
if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED' ||
|
|
2917
|
+
code.startsWith('SQLITE_BUSY_') || code.startsWith('SQLITE_LOCKED_')) {
|
|
2918
|
+
process.stderr.write(`[mem] Database busy — another process held the writer past the 5s timeout. Retry shortly.\n`);
|
|
2919
|
+
process.exitCode = 1;
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
throw e;
|
|
2759
2923
|
} finally {
|
|
2760
2924
|
try { db.close(); } catch {}
|
|
2761
2925
|
}
|
package/package.json
CHANGED
package/schema.mjs
CHANGED
|
@@ -696,7 +696,10 @@ export function ensureDb() {
|
|
|
696
696
|
const db = new Database(DB_PATH);
|
|
697
697
|
try { chmodSync(DB_PATH, 0o600); } catch {}
|
|
698
698
|
db.pragma('journal_mode = WAL');
|
|
699
|
-
|
|
699
|
+
// 5000ms matches the MCP server (server.mjs) — 3000ms wasn't enough under realistic
|
|
700
|
+
// concurrency (parallel CLI saves + a long-running FTS rebuild can push individual
|
|
701
|
+
// transactions past 3s, triggering SQLITE_BUSY on the third caller).
|
|
702
|
+
db.pragma('busy_timeout = 5000');
|
|
700
703
|
db.pragma('synchronous = NORMAL');
|
|
701
704
|
db.pragma('foreign_keys = OFF'); // Enabled after dedup migration
|
|
702
705
|
|
package/server.mjs
CHANGED
|
@@ -1576,15 +1576,33 @@ server.registerTool(
|
|
|
1576
1576
|
const action = args.action || 'preview';
|
|
1577
1577
|
|
|
1578
1578
|
if (action === 'preview') {
|
|
1579
|
-
const preview = optimizePreview(db);
|
|
1579
|
+
const preview = optimizePreview(db, { project: args.project, detail: args.detail === true });
|
|
1580
1580
|
const lines = [
|
|
1581
1581
|
`🔍 LLM Optimization Preview:`,
|
|
1582
|
+
];
|
|
1583
|
+
if (args.project) lines.push(` Project filter: ${args.project}`);
|
|
1584
|
+
lines.push(
|
|
1582
1585
|
` Re-enrich candidates: ${preview.reenrich}`,
|
|
1583
1586
|
` Normalize: ${preview.normalizeGateOpen ? `${preview.normalize} unique concepts` : 'gate closed (7-day interval)'}`,
|
|
1584
1587
|
` Cluster-merge candidates: ${preview.clusterMerge} clusters`,
|
|
1585
1588
|
` Smart-compress candidates: ${preview.smartCompress} clusters`,
|
|
1586
1589
|
` Total: ${preview.total} items`,
|
|
1587
|
-
|
|
1590
|
+
);
|
|
1591
|
+
if (args.detail === true) {
|
|
1592
|
+
if (preview.mergeClusters && preview.mergeClusters.length > 0) {
|
|
1593
|
+
lines.push('', '─── Cluster-merge details ───');
|
|
1594
|
+
for (const [i, cluster] of preview.mergeClusters.entries()) {
|
|
1595
|
+
lines.push(` Cluster ${i + 1} (${cluster.length} obs, project=${cluster[0]?.project || '?'}):`);
|
|
1596
|
+
for (const obs of cluster) lines.push(` #${obs.id} [${obs.type || 'change'}] ${truncate(obs.title || '(untitled)', 100)}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (preview.reenrichSamples && preview.reenrichSamples.length > 0) {
|
|
1600
|
+
lines.push('', '─── Re-enrich sample (first 20) ───');
|
|
1601
|
+
for (const obs of preview.reenrichSamples) {
|
|
1602
|
+
lines.push(` #${obs.id} [${obs.type || 'change'}] (project=${obs.project || '?'}) ${truncate(obs.title || '(untitled)', 100)}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1588
1606
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1589
1607
|
}
|
|
1590
1608
|
|
|
@@ -1596,6 +1614,7 @@ server.registerTool(
|
|
|
1596
1614
|
// T2-P0-B: scope parity with CLI (--scope wide). When omitted, optimizeRun defaults
|
|
1597
1615
|
// to narrow via its own code; passing through keeps that fallback intact.
|
|
1598
1616
|
reenrichScope: args.scope,
|
|
1617
|
+
project: args.project,
|
|
1599
1618
|
});
|
|
1600
1619
|
|
|
1601
1620
|
const lines = ['🔧 LLM Optimization Results:'];
|
package/tool-schemas.mjs
CHANGED
|
@@ -197,6 +197,8 @@ export const memOptimizeSchema = {
|
|
|
197
197
|
.describe('Maximum LLM calls across all tasks (default: 15)'),
|
|
198
198
|
scope: z.enum(['narrow', 'wide']).optional().default('narrow')
|
|
199
199
|
.describe("Re-enrich scope: narrow=narrative-only candidates (default); wide=R-7 backfill (bugfix/refactor/feature/decision with narrative but lesson_learned='none'). CLI parity: --scope wide."),
|
|
200
|
+
project: z.string().optional().describe('Filter all 4 tasks to a single project. Default: scan all projects.'),
|
|
201
|
+
detail: coerceBool.optional().describe('preview action only — include cluster contents + re-enrich/compress sample arrays alongside aggregate counts.'),
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
export const memMaintainSchema = {
|