claude-mem-lite 2.71.4 → 2.72.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/hook-optimize.mjs +74 -38
- package/mem-cli.mjs +39 -3
- package/package.json +1 -1
- package/server.mjs +21 -2
- package/tool-schemas.mjs +2 -0
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/mem-cli.mjs
CHANGED
|
@@ -2383,6 +2383,8 @@ Commands:
|
|
|
2383
2383
|
--task T Comma-separated: re-enrich,normalize,cluster-merge,smart-compress
|
|
2384
2384
|
--max N Max items per task (1-100, default 15)
|
|
2385
2385
|
--scope S re-enrich scope: narrow (default) or wide
|
|
2386
|
+
--project P Limit to a single project (.|current = inferProject())
|
|
2387
|
+
--verbose / -v Preview also dumps cluster contents + re-enrich samples
|
|
2386
2388
|
|
|
2387
2389
|
doctor Environment diagnostics and benchmarks
|
|
2388
2390
|
--benchmark Run perf benchmark and emit JSON
|
|
@@ -2617,6 +2619,7 @@ async function cmdEnrich(argv) {
|
|
|
2617
2619
|
async function cmdOptimize(db, args) {
|
|
2618
2620
|
const run = args.includes('--run');
|
|
2619
2621
|
const runAll = args.includes('--run-all');
|
|
2622
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
2620
2623
|
// T2-P1-D: --task accepts a single task or a comma-separated list, parity with MCP memOptimizeSchema.tasks.
|
|
2621
2624
|
const VALID_TASKS = ['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'];
|
|
2622
2625
|
const taskIdx = args.indexOf('--task');
|
|
@@ -2647,23 +2650,56 @@ async function cmdOptimize(db, args) {
|
|
|
2647
2650
|
// lesson_learned (the "Haiku judged 'none'" cases). Default 'narrow' preserves old behavior.
|
|
2648
2651
|
const scopeIdx = args.indexOf('--scope');
|
|
2649
2652
|
const reenrichScope = scopeIdx >= 0 && args[scopeIdx + 1] === 'wide' ? 'wide' : 'narrow';
|
|
2653
|
+
// --project <name> filters all 4 tasks to one project. Opt-in; absence
|
|
2654
|
+
// preserves prior cross-project default. `.` or `current` auto-resolve via
|
|
2655
|
+
// inferProject() so users don't need to remember the exact name.
|
|
2656
|
+
const projectIdx = args.indexOf('--project');
|
|
2657
|
+
let project;
|
|
2658
|
+
if (projectIdx >= 0 && args[projectIdx + 1]) {
|
|
2659
|
+
const raw = args[projectIdx + 1];
|
|
2660
|
+
project = (raw === '.' || raw === 'current') ? inferProject() : raw;
|
|
2661
|
+
}
|
|
2650
2662
|
|
|
2651
2663
|
if (!run && !runAll) {
|
|
2652
|
-
const preview = optimizePreview(db);
|
|
2664
|
+
const preview = optimizePreview(db, { project, detail: verbose });
|
|
2653
2665
|
out('[mem] 🔍 LLM Optimization Preview:');
|
|
2666
|
+
if (project) out(` Project filter: ${project}`);
|
|
2654
2667
|
out(` Re-enrich candidates: ${preview.reenrich}${preview.reenrichWide !== undefined && preview.reenrichWide !== null ? ` (wide scope: ${preview.reenrichWide})` : ''}`);
|
|
2655
2668
|
out(` Normalize: ${preview.normalizeGateOpen ? `${preview.normalize} unique concepts` : 'gate closed (7-day interval)'}`);
|
|
2656
2669
|
out(` Cluster-merge: ${preview.clusterMerge} clusters`);
|
|
2657
2670
|
out(` Smart-compress: ${preview.smartCompress} clusters`);
|
|
2658
2671
|
out(` Total: ${preview.total} items`);
|
|
2672
|
+
if (verbose) {
|
|
2673
|
+
out('');
|
|
2674
|
+
if (preview.mergeClusters && preview.mergeClusters.length > 0) {
|
|
2675
|
+
out('─── Cluster-merge details ───');
|
|
2676
|
+
for (const [i, cluster] of preview.mergeClusters.entries()) {
|
|
2677
|
+
out(` Cluster ${i + 1} (${cluster.length} obs, project=${cluster[0]?.project || '?'}):`);
|
|
2678
|
+
for (const obs of cluster) out(` #${obs.id} [${obs.type || 'change'}] ${truncate(obs.title || '(untitled)', 100)}`);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (preview.reenrichSamples && preview.reenrichSamples.length > 0) {
|
|
2682
|
+
out('─── Re-enrich sample (first 20) ───');
|
|
2683
|
+
for (const obs of preview.reenrichSamples) {
|
|
2684
|
+
out(` #${obs.id} [${obs.type || 'change'}] (project=${obs.project || '?'}) ${truncate(obs.title || '(untitled)', 100)}`);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
if (preview.compressSamples && preview.compressSamples.length > 0) {
|
|
2688
|
+
out('─── Smart-compress sample (first 5 clusters) ───');
|
|
2689
|
+
for (const [i, cluster] of preview.compressSamples.entries()) {
|
|
2690
|
+
out(` Cluster ${i + 1} (${cluster.observations?.length || 0} obs, project=${cluster.project || '?'})`);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2659
2694
|
out('');
|
|
2660
2695
|
out('Run with --run to execute, --run-all to bypass gates.');
|
|
2661
2696
|
out('For R-7 backfill: --run --task re-enrich --scope wide --max N');
|
|
2697
|
+
out('Scope: --project <name|.|current> to limit; --verbose for cluster details.');
|
|
2662
2698
|
return;
|
|
2663
2699
|
}
|
|
2664
2700
|
|
|
2665
|
-
out(`[mem] Running LLM optimization${reenrichScope === 'wide' ? ' (scope: wide)' : ''}...`);
|
|
2666
|
-
const results = await optimizeRun(db, { tasks, maxItems, force: runAll, reenrichScope });
|
|
2701
|
+
out(`[mem] Running LLM optimization${reenrichScope === 'wide' ? ' (scope: wide)' : ''}${project ? ` (project: ${project})` : ''}...`);
|
|
2702
|
+
const results = await optimizeRun(db, { tasks, maxItems, force: runAll, reenrichScope, project });
|
|
2667
2703
|
|
|
2668
2704
|
if (results.reenrich) out(` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`);
|
|
2669
2705
|
if (results.normalize) {
|
package/package.json
CHANGED
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 = {
|