claude-memory-layer 1.0.10 → 1.0.12
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +3577 -389
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1383 -138
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1917 -214
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1813 -231
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1802 -205
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1909 -248
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1861 -206
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +2341 -217
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +2350 -226
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1805 -206
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +1447 -55
- package/dist/ui/index.html +318 -147
- package/dist/ui/style.css +892 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/docs/OPERATIONS.md +18 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +9 -2
- package/scripts/build.ts +6 -0
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +391 -60
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +794 -7
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +51 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +9 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +89 -8
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +508 -71
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1447 -55
- package/src/ui/index.html +318 -147
- package/src/ui/style.css +892 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
package/src/cli/index.ts
CHANGED
|
@@ -11,10 +11,14 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import {
|
|
13
13
|
getDefaultMemoryService,
|
|
14
|
-
getMemoryServiceForProject
|
|
14
|
+
getMemoryServiceForProject,
|
|
15
|
+
getProjectStoragePath
|
|
15
16
|
} from '../services/memory-service.js';
|
|
16
|
-
import { createSessionHistoryImporter } from '../services/session-history-importer.js';
|
|
17
|
+
import { createSessionHistoryImporter, type ProgressEvent } from '../services/session-history-importer.js';
|
|
18
|
+
import { bootstrapKnowledgeBase } from '../services/bootstrap-organizer.js';
|
|
17
19
|
import { startServer, stopServer, isServerRunning } from '../server/index.js';
|
|
20
|
+
import { SQLiteEventStore } from '../core/sqlite-event-store.js';
|
|
21
|
+
import { MongoSyncWorker } from '../core/mongo-sync-worker.js';
|
|
18
22
|
|
|
19
23
|
// ============================================================
|
|
20
24
|
// Hook Installation Utilities
|
|
@@ -107,7 +111,7 @@ const program = new Command();
|
|
|
107
111
|
program
|
|
108
112
|
.name('claude-memory-layer')
|
|
109
113
|
.description('Claude Code Memory Plugin CLI')
|
|
110
|
-
.version('
|
|
114
|
+
.version(process.env.CLAUDE_MEMORY_LAYER_VERSION || '0.0.0');
|
|
111
115
|
|
|
112
116
|
// ============================================================
|
|
113
117
|
// Install / Uninstall Commands
|
|
@@ -427,6 +431,356 @@ program
|
|
|
427
431
|
}
|
|
428
432
|
});
|
|
429
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Mongo Sync command - sync local SQLite events with a shared MongoDB database (optional)
|
|
436
|
+
*/
|
|
437
|
+
program
|
|
438
|
+
.command('mongo-sync')
|
|
439
|
+
.description('Sync events with MongoDB for multi-server collaboration (optional)')
|
|
440
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
441
|
+
.option('--mongo-uri <uri>', 'MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)')
|
|
442
|
+
.option('--mongo-db <name>', 'MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)')
|
|
443
|
+
.option('--mongo-project <key>', 'Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))')
|
|
444
|
+
.option('--direction <dir>', 'push|pull|both', 'both')
|
|
445
|
+
.option('--batch-size <n>', 'Batch size', '500')
|
|
446
|
+
.option('--interval <ms>', 'Watch interval ms', '30000')
|
|
447
|
+
.option('--watch', 'Run continuously')
|
|
448
|
+
.action(async (options) => {
|
|
449
|
+
const projectPath = options.project || process.cwd();
|
|
450
|
+
const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
|
|
451
|
+
const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
|
|
452
|
+
const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path.basename(projectPath);
|
|
453
|
+
const direction = String(options.direction || 'both').toLowerCase();
|
|
454
|
+
|
|
455
|
+
if (!mongoUri || !mongoDb) {
|
|
456
|
+
console.error('\n❌ MongoDB sync is not configured.');
|
|
457
|
+
console.error(' Set --mongo-uri/--mongo-db or env CLAUDE_MEMORY_MONGO_URI/CLAUDE_MEMORY_MONGO_DB.\n');
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!['push', 'pull', 'both'].includes(direction)) {
|
|
462
|
+
console.error('\n❌ Invalid --direction. Use: push | pull | both\n');
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const storagePath = getProjectStoragePath(projectPath);
|
|
467
|
+
if (!fs.existsSync(storagePath)) {
|
|
468
|
+
fs.mkdirSync(storagePath, { recursive: true });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const batchSizeParsed = parseInt(options.batchSize, 10);
|
|
472
|
+
const intervalParsed = parseInt(options.interval, 10);
|
|
473
|
+
const batchSize = (Number.isFinite(batchSizeParsed) && batchSizeParsed > 0) ? batchSizeParsed : 500;
|
|
474
|
+
const intervalMs = (Number.isFinite(intervalParsed) && intervalParsed > 0) ? intervalParsed : 30000;
|
|
475
|
+
|
|
476
|
+
const sqliteStore = new SQLiteEventStore(path.join(storagePath, 'events.sqlite'));
|
|
477
|
+
const worker = new MongoSyncWorker(sqliteStore, {
|
|
478
|
+
uri: mongoUri,
|
|
479
|
+
dbName: mongoDb,
|
|
480
|
+
projectKey,
|
|
481
|
+
direction,
|
|
482
|
+
batchSize,
|
|
483
|
+
intervalMs
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const runOnce = async () => {
|
|
487
|
+
const { pushed, pulled } = await worker.syncNow();
|
|
488
|
+
const ts = new Date().toISOString();
|
|
489
|
+
process.stdout.write(`[mongo-sync] ${ts} project=${projectKey} pushed=${pushed} pulled=${pulled}\n`);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
if (!options.watch) {
|
|
494
|
+
await runOnce();
|
|
495
|
+
await worker.shutdown();
|
|
496
|
+
sqliteStore.close();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(`[mongo-sync] Watch mode started (interval=${intervalMs}ms, project=${projectKey})`);
|
|
501
|
+
|
|
502
|
+
const handle = setInterval(() => {
|
|
503
|
+
runOnce().catch((err) => {
|
|
504
|
+
console.error('[mongo-sync] Sync failed:', err);
|
|
505
|
+
});
|
|
506
|
+
}, intervalMs);
|
|
507
|
+
|
|
508
|
+
const shutdown = async () => {
|
|
509
|
+
clearInterval(handle);
|
|
510
|
+
console.log('\n[mongo-sync] Shutting down...');
|
|
511
|
+
try {
|
|
512
|
+
await worker.shutdown();
|
|
513
|
+
} finally {
|
|
514
|
+
sqliteStore.close();
|
|
515
|
+
}
|
|
516
|
+
process.exit(0);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
process.on('SIGINT', () => { void shutdown(); });
|
|
520
|
+
process.on('SIGTERM', () => { void shutdown(); });
|
|
521
|
+
|
|
522
|
+
// Run immediately, then keep alive
|
|
523
|
+
await runOnce();
|
|
524
|
+
await new Promise(() => {});
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('[mongo-sync] Failed:', error);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Render import progress to terminal
|
|
533
|
+
*/
|
|
534
|
+
function renderProgress(event: ProgressEvent): void {
|
|
535
|
+
switch (event.phase) {
|
|
536
|
+
case 'scan':
|
|
537
|
+
console.log(` 🔍 ${event.message}`);
|
|
538
|
+
break;
|
|
539
|
+
case 'session-start': {
|
|
540
|
+
const pct = Math.round(((event.sessionIndex) / event.totalSessions) * 100);
|
|
541
|
+
const sessionName = path.basename(event.filePath, '.jsonl').slice(0, 8);
|
|
542
|
+
process.stdout.write(
|
|
543
|
+
`\r 📄 [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
|
|
544
|
+
);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case 'session-progress': {
|
|
548
|
+
process.stdout.write(
|
|
549
|
+
`\r 📄 [${event.sessionIndex + 1}/...] ${event.messagesProcessed} msgs | +${event.imported} imported, ~${event.skipped} skipped `
|
|
550
|
+
);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'session-done': {
|
|
554
|
+
const imported = event.importedPrompts + event.importedResponses;
|
|
555
|
+
if (imported > 0) {
|
|
556
|
+
process.stdout.write(
|
|
557
|
+
`\r ✅ [${event.sessionIndex + 1}] +${event.importedPrompts} prompts, +${event.importedResponses} responses${event.skipped > 0 ? `, ~${event.skipped} skipped` : ''} \n`
|
|
558
|
+
);
|
|
559
|
+
} else if (event.skipped > 0) {
|
|
560
|
+
process.stdout.write(
|
|
561
|
+
`\r ⏭️ [${event.sessionIndex + 1}] All ${event.skipped} already imported \n`
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
process.stdout.write(
|
|
565
|
+
`\r ⏭️ [${event.sessionIndex + 1}] Empty session \n`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'embedding':
|
|
571
|
+
process.stdout.write(
|
|
572
|
+
`\r 🧠 Embeddings: ${event.processed}/${event.total} processed `
|
|
573
|
+
);
|
|
574
|
+
if (event.processed >= event.total) {
|
|
575
|
+
process.stdout.write('\n');
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
case 'done':
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function printImportSummary(result: import('../services/session-history-importer.js').ImportResult, embedCount: number): void {
|
|
584
|
+
console.log('\n┌─────────────────────────────────┐');
|
|
585
|
+
console.log('│ ✅ Import Complete │');
|
|
586
|
+
console.log('├─────────────────────────────────┤');
|
|
587
|
+
console.log(`│ Sessions processed: ${String(result.totalSessions).padStart(8)} │`);
|
|
588
|
+
console.log(`│ Total messages: ${String(result.totalMessages).padStart(8)} │`);
|
|
589
|
+
console.log(`│ Imported prompts: ${String(result.importedPrompts).padStart(8)} │`);
|
|
590
|
+
console.log(`│ Imported responses: ${String(result.importedResponses).padStart(8)} │`);
|
|
591
|
+
console.log(`│ Skipped duplicates: ${String(result.skippedDuplicates).padStart(8)} │`);
|
|
592
|
+
console.log(`│ Embeddings queued: ${String(embedCount).padStart(8)} │`);
|
|
593
|
+
console.log('└─────────────────────────────────┘');
|
|
594
|
+
|
|
595
|
+
if (result.errors.length > 0) {
|
|
596
|
+
console.log(`\n⚠️ Errors (${result.errors.length}):`);
|
|
597
|
+
for (const error of result.errors.slice(0, 5)) {
|
|
598
|
+
console.log(` - ${error}`);
|
|
599
|
+
}
|
|
600
|
+
if (result.errors.length > 5) {
|
|
601
|
+
console.log(` ... and ${result.errors.length - 5} more`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function sanitizeSegment(input: string | undefined, fallback: string): string {
|
|
607
|
+
const v = (input || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
608
|
+
return v || fallback;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function listMarkdownFiles(root: string): Promise<string[]> {
|
|
612
|
+
const out: string[] = [];
|
|
613
|
+
const stack = [root];
|
|
614
|
+
|
|
615
|
+
while (stack.length > 0) {
|
|
616
|
+
const dir = stack.pop()!;
|
|
617
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
618
|
+
for (const e of entries) {
|
|
619
|
+
const full = path.join(dir, e.name);
|
|
620
|
+
if (e.isDirectory()) stack.push(full);
|
|
621
|
+
else if (e.isFile() && e.name.endsWith('.md') && e.name !== '_index.md') out.push(full);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return out.sort();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function deriveNamespaceCategory(sourceRoot: string, filePath: string): { namespace: string; categoryPath: string[] } {
|
|
629
|
+
const rel = path.relative(sourceRoot, filePath);
|
|
630
|
+
const dirSeg = path.dirname(rel).split(path.sep).filter(Boolean);
|
|
631
|
+
|
|
632
|
+
if (dirSeg.length >= 2) {
|
|
633
|
+
const namespace = sanitizeSegment(dirSeg[0], 'default');
|
|
634
|
+
const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment(s, 'uncategorized'));
|
|
635
|
+
return { namespace, categoryPath: categoryPath.length > 0 ? categoryPath : ['uncategorized'] };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return { namespace: 'default', categoryPath: ['uncategorized'] };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function extractImportEvidence(markdown: string): { confidence?: string; sources: string[] } {
|
|
642
|
+
const confidenceMatch = markdown.match(/^-\s*confidence:\s*([^\n]+)/m);
|
|
643
|
+
const sources = markdown
|
|
644
|
+
.split(/\r?\n/)
|
|
645
|
+
.map((line) => line.trim())
|
|
646
|
+
.filter((line) => line.startsWith('- source:'))
|
|
647
|
+
.map((line) => line.replace(/^-\s*source:\s*/i, '').trim())
|
|
648
|
+
.filter(Boolean)
|
|
649
|
+
.slice(0, 30);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
confidence: confidenceMatch ? confidenceMatch[1].trim() : undefined,
|
|
653
|
+
sources
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Organize-import command - import legacy markdown memories into structured mirror
|
|
659
|
+
*/
|
|
660
|
+
program
|
|
661
|
+
.command('organize-import [sourceDir]')
|
|
662
|
+
.description('Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing')
|
|
663
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
664
|
+
.option('--session <id>', 'Session id for imported events (default: import:organized)')
|
|
665
|
+
.option('--limit <n>', 'Limit number of files to import')
|
|
666
|
+
.option('--dry-run', 'Preview mapping without writing')
|
|
667
|
+
.option('--bootstrap', 'Force-generate structured markdown from codebase + git history before import')
|
|
668
|
+
.option('--bootstrap-if-empty', 'Auto-bootstrap when source has no markdown files (default: true)', true)
|
|
669
|
+
.option('--no-bootstrap-if-empty', 'Disable auto-bootstrap when source has no markdown files')
|
|
670
|
+
.option('--force-bootstrap', 'Run bootstrap even when markdown files exist')
|
|
671
|
+
.option('--repo <path>', 'Repository root for bootstrap analysis (default: project path)')
|
|
672
|
+
.option('--out <path>', 'Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)')
|
|
673
|
+
.option('--since <range>', 'Git history range for bootstrap (default: "180 days ago")')
|
|
674
|
+
.option('--max-commits <n>', 'Max commits to analyze for bootstrap (default: 1000)')
|
|
675
|
+
.option('--incremental', 'Use previous bootstrap manifest as baseline for incremental updates (default: true)', true)
|
|
676
|
+
.option('--no-incremental', 'Disable incremental bootstrap; regenerate full snapshot')
|
|
677
|
+
.action(async (sourceDir: string | undefined, options) => {
|
|
678
|
+
const projectPath = options.project || process.cwd();
|
|
679
|
+
const sessionId = options.session || 'import:organized';
|
|
680
|
+
const sourceRoot = path.resolve(sourceDir || options.out || projectPath);
|
|
681
|
+
const repoPath = path.resolve(options.repo || projectPath);
|
|
682
|
+
|
|
683
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
684
|
+
fs.mkdirSync(sourceRoot, { recursive: true });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
let activeSourceRoot = sourceRoot;
|
|
691
|
+
let importRoot = sourceRoot;
|
|
692
|
+
let files = await listMarkdownFiles(importRoot);
|
|
693
|
+
const hasMarkdown = files.length > 0;
|
|
694
|
+
const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || (!hasMarkdown && options.bootstrapIfEmpty));
|
|
695
|
+
|
|
696
|
+
if (shouldBootstrap) {
|
|
697
|
+
const outDir = path.resolve(options.out || path.join(sourceRoot, 'bootstrap-kb'));
|
|
698
|
+
const since = options.since || '180 days ago';
|
|
699
|
+
const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1000;
|
|
700
|
+
|
|
701
|
+
console.log('\n🧠 Bootstrapping markdown knowledge base...');
|
|
702
|
+
const bootstrap = await bootstrapKnowledgeBase({
|
|
703
|
+
repoPath,
|
|
704
|
+
outDir,
|
|
705
|
+
since,
|
|
706
|
+
maxCommits,
|
|
707
|
+
incremental: options.incremental
|
|
708
|
+
});
|
|
709
|
+
console.log(` Repo: ${repoPath}`);
|
|
710
|
+
console.log(` Output: ${bootstrap.outDir}`);
|
|
711
|
+
console.log(` Files analyzed: ${bootstrap.fileCount}`);
|
|
712
|
+
console.log(` Commits analyzed: ${bootstrap.commitCount}`);
|
|
713
|
+
console.log(` Modules: ${bootstrap.moduleCount}`);
|
|
714
|
+
|
|
715
|
+
activeSourceRoot = outDir;
|
|
716
|
+
importRoot = outDir;
|
|
717
|
+
files = await listMarkdownFiles(importRoot);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (files.length === 0) {
|
|
721
|
+
console.error('\n❌ organize-import found no markdown files to import.\n');
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const limit = options.limit ? Math.max(1, parseInt(options.limit, 10)) : files.length;
|
|
726
|
+
const targets = files.slice(0, limit);
|
|
727
|
+
|
|
728
|
+
console.log(`\n📦 organize-import`);
|
|
729
|
+
console.log(` Source: ${activeSourceRoot}`);
|
|
730
|
+
console.log(` Project: ${projectPath}`);
|
|
731
|
+
console.log(` Files: ${targets.length}${targets.length < files.length ? `/${files.length}` : ''}`);
|
|
732
|
+
console.log(` Dry-run: ${options.dryRun ? 'yes' : 'no'}\n`);
|
|
733
|
+
|
|
734
|
+
if (!options.dryRun) {
|
|
735
|
+
await service.initialize();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let imported = 0;
|
|
739
|
+
let skipped = 0;
|
|
740
|
+
|
|
741
|
+
for (const file of targets) {
|
|
742
|
+
const text = await fs.promises.readFile(file, 'utf8');
|
|
743
|
+
if (!text.trim()) {
|
|
744
|
+
skipped += 1;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
|
|
749
|
+
const rel = path.relative(activeSourceRoot, file);
|
|
750
|
+
const evidence = extractImportEvidence(text);
|
|
751
|
+
|
|
752
|
+
if (options.dryRun) {
|
|
753
|
+
console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join('/')} confidence=${evidence.confidence || 'n/a'} sources=${evidence.sources.length}`);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
await service.storeSessionSummary(sessionId, text, {
|
|
758
|
+
namespace,
|
|
759
|
+
categoryPath,
|
|
760
|
+
confidence: evidence.confidence,
|
|
761
|
+
sources: evidence.sources,
|
|
762
|
+
import: {
|
|
763
|
+
sourceFile: rel,
|
|
764
|
+
importedAt: new Date().toISOString(),
|
|
765
|
+
bootstrap: shouldBootstrap === true
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
imported += 1;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (!options.dryRun) {
|
|
772
|
+
const embed = await service.processPendingEmbeddings();
|
|
773
|
+
await service.shutdown();
|
|
774
|
+
console.log(`\n✅ Imported: ${imported}, skipped-empty: ${skipped}, embeddings: ${embed}\n`);
|
|
775
|
+
} else {
|
|
776
|
+
console.log(`\n✅ Dry-run complete (planned imports: ${targets.length - skipped}, skipped-empty: ${skipped})\n`);
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error('\n❌ organize-import failed:', error);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
430
784
|
/**
|
|
431
785
|
* Import command - import existing Claude Code sessions
|
|
432
786
|
*/
|
|
@@ -437,8 +791,11 @@ program
|
|
|
437
791
|
.option('-s, --session <file>', 'Import specific session file (JSONL)')
|
|
438
792
|
.option('-a, --all', 'Import all sessions from all projects')
|
|
439
793
|
.option('-l, --limit <number>', 'Limit messages per session')
|
|
794
|
+
.option('-f, --force', 'Force reimport: delete existing events and reimport with turn_id grouping')
|
|
440
795
|
.option('-v, --verbose', 'Show detailed progress')
|
|
441
796
|
.action(async (options) => {
|
|
797
|
+
const startTime = Date.now();
|
|
798
|
+
|
|
442
799
|
// Determine target project path for storage
|
|
443
800
|
const targetProjectPath = options.project || process.cwd();
|
|
444
801
|
|
|
@@ -446,102 +803,76 @@ program
|
|
|
446
803
|
const service = getMemoryServiceForProject(targetProjectPath);
|
|
447
804
|
const importer = createSessionHistoryImporter(service);
|
|
448
805
|
|
|
806
|
+
const importOpts = {
|
|
807
|
+
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
808
|
+
force: options.force,
|
|
809
|
+
verbose: options.verbose,
|
|
810
|
+
onProgress: renderProgress
|
|
811
|
+
};
|
|
812
|
+
|
|
449
813
|
try {
|
|
814
|
+
console.log('\n⏳ Initializing memory service...');
|
|
450
815
|
await service.initialize();
|
|
816
|
+
console.log(' ✅ Ready\n');
|
|
817
|
+
|
|
818
|
+
if (options.force) {
|
|
819
|
+
console.log('🔄 Force mode: existing events will be deleted and reimported with turn_id grouping\n');
|
|
820
|
+
}
|
|
451
821
|
|
|
452
822
|
let result;
|
|
453
823
|
|
|
454
824
|
if (options.session) {
|
|
455
825
|
// Import specific session file
|
|
456
|
-
console.log(
|
|
457
|
-
console.log(` Target
|
|
826
|
+
console.log(`📥 Importing session: ${options.session}`);
|
|
827
|
+
console.log(` Target: ${targetProjectPath}\n`);
|
|
458
828
|
result = await importer.importSessionFile(options.session, {
|
|
829
|
+
...importOpts,
|
|
459
830
|
projectPath: targetProjectPath,
|
|
460
|
-
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
461
|
-
verbose: options.verbose
|
|
462
831
|
});
|
|
463
832
|
} else if (options.project) {
|
|
464
833
|
// Import all sessions from a project
|
|
465
|
-
console.log(
|
|
466
|
-
result = await importer.importProject(options.project,
|
|
467
|
-
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
468
|
-
verbose: options.verbose
|
|
469
|
-
});
|
|
834
|
+
console.log(`📥 Importing project: ${options.project}\n`);
|
|
835
|
+
result = await importer.importProject(options.project, importOpts);
|
|
470
836
|
} else if (options.all) {
|
|
471
837
|
// Import all sessions from all projects
|
|
472
|
-
|
|
473
|
-
console.log('\n📥 Importing all sessions from all projects');
|
|
838
|
+
console.log('📥 Importing all sessions from all projects');
|
|
474
839
|
console.log(' ⚠️ Using global storage (use -p for project-specific)\n');
|
|
475
840
|
const globalService = getDefaultMemoryService();
|
|
476
841
|
const globalImporter = createSessionHistoryImporter(globalService);
|
|
477
842
|
await globalService.initialize();
|
|
478
|
-
result = await globalImporter.importAll(
|
|
479
|
-
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
480
|
-
verbose: options.verbose
|
|
481
|
-
});
|
|
843
|
+
result = await globalImporter.importAll(importOpts);
|
|
482
844
|
|
|
483
845
|
// Process embeddings
|
|
484
|
-
console.log('\n
|
|
846
|
+
console.log('\n🧠 Processing embeddings...');
|
|
485
847
|
const embedCount = await globalService.processPendingEmbeddings();
|
|
486
848
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
console.log(
|
|
490
|
-
console.log(`Total messages: ${result.totalMessages}`);
|
|
491
|
-
console.log(`Imported prompts: ${result.importedPrompts}`);
|
|
492
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
493
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
494
|
-
console.log(`Embeddings processed: ${embedCount}`);
|
|
495
|
-
|
|
496
|
-
if (result.errors.length > 0) {
|
|
497
|
-
console.log(`\n⚠️ Errors (${result.errors.length}):`);
|
|
498
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
499
|
-
console.log(` - ${error}`);
|
|
500
|
-
}
|
|
501
|
-
if (result.errors.length > 5) {
|
|
502
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
849
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
850
|
+
printImportSummary(result, embedCount);
|
|
851
|
+
console.log(`\n⏱️ Completed in ${elapsed}s`);
|
|
505
852
|
|
|
506
853
|
await globalService.shutdown();
|
|
507
854
|
return;
|
|
508
855
|
} else {
|
|
509
856
|
// Default: import current project
|
|
510
857
|
const cwd = process.cwd();
|
|
511
|
-
console.log(
|
|
858
|
+
console.log(`📥 Importing sessions for: ${cwd}\n`);
|
|
512
859
|
result = await importer.importProject(cwd, {
|
|
860
|
+
...importOpts,
|
|
513
861
|
projectPath: cwd,
|
|
514
|
-
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
515
|
-
verbose: options.verbose
|
|
516
862
|
});
|
|
517
863
|
}
|
|
518
864
|
|
|
519
865
|
// Process embeddings
|
|
520
|
-
console.log('\n
|
|
866
|
+
console.log('\n🧠 Processing embeddings...');
|
|
521
867
|
const embedCount = await service.processPendingEmbeddings();
|
|
522
868
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
console.log(
|
|
526
|
-
console.log(`Total messages: ${result.totalMessages}`);
|
|
527
|
-
console.log(`Imported prompts: ${result.importedPrompts}`);
|
|
528
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
529
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
530
|
-
console.log(`Embeddings processed: ${embedCount}`);
|
|
531
|
-
|
|
532
|
-
if (result.errors.length > 0) {
|
|
533
|
-
console.log(`\n⚠️ Errors (${result.errors.length}):`);
|
|
534
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
535
|
-
console.log(` - ${error}`);
|
|
536
|
-
}
|
|
537
|
-
if (result.errors.length > 5) {
|
|
538
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
869
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
870
|
+
printImportSummary(result, embedCount);
|
|
871
|
+
console.log(`\n⏱️ Completed in ${elapsed}s`);
|
|
541
872
|
|
|
542
873
|
await service.shutdown();
|
|
543
874
|
} catch (error) {
|
|
544
|
-
console.error('Import failed:', error);
|
|
875
|
+
console.error('\n❌ Import failed:', error);
|
|
545
876
|
process.exit(1);
|
|
546
877
|
}
|
|
547
878
|
});
|
|
@@ -8,7 +8,9 @@ import { randomUUID } from 'crypto';
|
|
|
8
8
|
import { dbRun, dbAll, toDate, type Database } from './db-wrapper.js';
|
|
9
9
|
import type {
|
|
10
10
|
ConsolidatedMemory,
|
|
11
|
-
ConsolidatedMemoryInput
|
|
11
|
+
ConsolidatedMemoryInput,
|
|
12
|
+
ConsolidationRule,
|
|
13
|
+
ConsolidationRuleInput
|
|
12
14
|
} from './types.js';
|
|
13
15
|
import { EventStore } from './event-store.js';
|
|
14
16
|
|
|
@@ -170,6 +172,66 @@ export class ConsolidatedStore {
|
|
|
170
172
|
);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Create a long-term rule promoted from stable summaries
|
|
177
|
+
*/
|
|
178
|
+
async createRule(input: ConsolidationRuleInput): Promise<string> {
|
|
179
|
+
const ruleId = randomUUID();
|
|
180
|
+
|
|
181
|
+
await dbRun(
|
|
182
|
+
this.db,
|
|
183
|
+
`INSERT INTO consolidated_rules
|
|
184
|
+
(rule_id, rule, topics, source_memory_ids, source_events, confidence, created_at)
|
|
185
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
186
|
+
[
|
|
187
|
+
ruleId,
|
|
188
|
+
input.rule,
|
|
189
|
+
JSON.stringify(input.topics),
|
|
190
|
+
JSON.stringify(input.sourceMemoryIds),
|
|
191
|
+
JSON.stringify(input.sourceEvents),
|
|
192
|
+
input.confidence
|
|
193
|
+
]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return ruleId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getRules(options?: { limit?: number }): Promise<ConsolidationRule[]> {
|
|
200
|
+
const limit = options?.limit || 100;
|
|
201
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
202
|
+
this.db,
|
|
203
|
+
`SELECT * FROM consolidated_rules ORDER BY confidence DESC, created_at DESC LIMIT ?`,
|
|
204
|
+
[limit]
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return rows.map((row) => ({
|
|
208
|
+
ruleId: row.rule_id as string,
|
|
209
|
+
rule: row.rule as string,
|
|
210
|
+
topics: JSON.parse((row.topics as string) || '[]'),
|
|
211
|
+
sourceMemoryIds: JSON.parse((row.source_memory_ids as string) || '[]'),
|
|
212
|
+
sourceEvents: JSON.parse((row.source_events as string) || '[]'),
|
|
213
|
+
confidence: Number(row.confidence ?? 0.5),
|
|
214
|
+
createdAt: toDate(row.created_at) || new Date()
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async countRules(): Promise<number> {
|
|
219
|
+
const result = await dbAll<{ count: number }>(
|
|
220
|
+
this.db,
|
|
221
|
+
`SELECT COUNT(*) as count FROM consolidated_rules`
|
|
222
|
+
);
|
|
223
|
+
return result[0]?.count || 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async hasRuleForSourceMemory(memoryId: string): Promise<boolean> {
|
|
227
|
+
const rows = await dbAll<{ count: number }>(
|
|
228
|
+
this.db,
|
|
229
|
+
`SELECT COUNT(*) as count FROM consolidated_rules WHERE source_memory_ids LIKE ?`,
|
|
230
|
+
[`%"${memoryId}"%`]
|
|
231
|
+
);
|
|
232
|
+
return (rows[0]?.count || 0) > 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
173
235
|
/**
|
|
174
236
|
* Get count of consolidated memories
|
|
175
237
|
*/
|