claude-memory-layer 1.0.11 → 1.0.13
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 +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -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 +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- 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 +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-layer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Claude Code plugin that learns from conversations to provide personalized assistance",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"commander": "^12.0.0",
|
|
43
43
|
"duckdb": "^0.10.0",
|
|
44
44
|
"hono": "^4.0.0",
|
|
45
|
+
"mongodb": "^6.14.0",
|
|
45
46
|
"zod": "^3.22.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
package/scripts/build.ts
CHANGED
|
@@ -8,6 +8,8 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
|
|
10
10
|
const outdir = 'dist';
|
|
11
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')) as { version?: string };
|
|
12
|
+
const appVersion = packageJson.version ?? '0.0.0';
|
|
11
13
|
|
|
12
14
|
// Clean output directory
|
|
13
15
|
if (fs.existsSync(outdir)) {
|
|
@@ -30,11 +32,15 @@ const commonOptions: esbuild.BuildOptions = {
|
|
|
30
32
|
'duckdb',
|
|
31
33
|
'better-sqlite3',
|
|
32
34
|
'commander',
|
|
35
|
+
'mongodb',
|
|
33
36
|
'zod',
|
|
34
37
|
'hono',
|
|
35
38
|
'hono/cors',
|
|
36
39
|
'hono/logger'
|
|
37
40
|
],
|
|
41
|
+
define: {
|
|
42
|
+
'process.env.CLAUDE_MEMORY_LAYER_VERSION': JSON.stringify(appVersion)
|
|
43
|
+
},
|
|
38
44
|
banner: {
|
|
39
45
|
js: `import { createRequire } from 'module';
|
|
40
46
|
import { fileURLToPath } from 'url';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
cd "${ROOT_DIR}"
|
|
8
|
+
|
|
9
|
+
if [[ ! -f package.json ]]; then
|
|
10
|
+
echo "Error: package.json not found in ${ROOT_DIR}" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
old_version="$(node -p "require('./package.json').version")"
|
|
15
|
+
npm version patch --no-git-tag-version >/dev/null
|
|
16
|
+
new_version="$(node -p "require('./package.json').version")"
|
|
17
|
+
|
|
18
|
+
echo "Version bumped: ${old_version} -> ${new_version}"
|
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
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,103 @@ 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
|
+
|
|
430
531
|
/**
|
|
431
532
|
* Render import progress to terminal
|
|
432
533
|
*/
|
|
@@ -502,6 +603,184 @@ function printImportSummary(result: import('../services/session-history-importer
|
|
|
502
603
|
}
|
|
503
604
|
}
|
|
504
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
|
+
|
|
505
784
|
/**
|
|
506
785
|
* Import command - import existing Claude Code sessions
|
|
507
786
|
*/
|
|
@@ -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
|
*/
|
|
@@ -8,7 +8,8 @@ import type {
|
|
|
8
8
|
EndlessModeConfig,
|
|
9
9
|
MemoryEvent,
|
|
10
10
|
EventGroup,
|
|
11
|
-
WorkingSet
|
|
11
|
+
WorkingSet,
|
|
12
|
+
ConsolidationCostQualityReport
|
|
12
13
|
} from './types.js';
|
|
13
14
|
import { WorkingSetStore } from './working-set-store.js';
|
|
14
15
|
import { ConsolidatedStore } from './consolidated-store.js';
|
|
@@ -62,7 +63,19 @@ export class ConsolidationWorker {
|
|
|
62
63
|
* Force a consolidation run (manual trigger)
|
|
63
64
|
*/
|
|
64
65
|
async forceRun(): Promise<number> {
|
|
65
|
-
|
|
66
|
+
const out = await this.consolidateWithReport();
|
|
67
|
+
return out.consolidatedCount;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Force a consolidation run and return metrics report
|
|
72
|
+
*/
|
|
73
|
+
async forceRunWithReport(): Promise<{
|
|
74
|
+
consolidatedCount: number;
|
|
75
|
+
promotedRuleCount: number;
|
|
76
|
+
report: ConsolidationCostQualityReport;
|
|
77
|
+
}> {
|
|
78
|
+
return this.consolidateWithReport();
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|
|
@@ -109,15 +122,29 @@ export class ConsolidationWorker {
|
|
|
109
122
|
* Perform consolidation
|
|
110
123
|
*/
|
|
111
124
|
private async consolidate(): Promise<number> {
|
|
125
|
+
const out = await this.consolidateWithReport();
|
|
126
|
+
return out.consolidatedCount;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async consolidateWithReport(): Promise<{
|
|
130
|
+
consolidatedCount: number;
|
|
131
|
+
promotedRuleCount: number;
|
|
132
|
+
report: ConsolidationCostQualityReport;
|
|
133
|
+
}> {
|
|
112
134
|
const workingSet = await this.workingSetStore.get();
|
|
113
135
|
|
|
114
136
|
if (workingSet.recentEvents.length < 3) {
|
|
115
|
-
return
|
|
137
|
+
return {
|
|
138
|
+
consolidatedCount: 0,
|
|
139
|
+
promotedRuleCount: 0,
|
|
140
|
+
report: this.buildCostQualityReport(workingSet.recentEvents, [], 0)
|
|
141
|
+
};
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
// Group events by topic
|
|
119
145
|
const groups = this.groupByTopic(workingSet.recentEvents);
|
|
120
146
|
let consolidatedCount = 0;
|
|
147
|
+
const createdMemoryIds: string[] = [];
|
|
121
148
|
|
|
122
149
|
for (const group of groups) {
|
|
123
150
|
// Require minimum 3 events per group
|
|
@@ -132,16 +159,18 @@ export class ConsolidationWorker {
|
|
|
132
159
|
const summary = await this.summarize(group);
|
|
133
160
|
|
|
134
161
|
// Create consolidated memory
|
|
135
|
-
await this.consolidatedStore.create({
|
|
162
|
+
const memoryId = await this.consolidatedStore.create({
|
|
136
163
|
summary,
|
|
137
164
|
topics: group.topics,
|
|
138
165
|
sourceEvents: eventIds,
|
|
139
166
|
confidence: this.calculateConfidence(group)
|
|
140
167
|
});
|
|
141
|
-
|
|
168
|
+
createdMemoryIds.push(memoryId);
|
|
142
169
|
consolidatedCount++;
|
|
143
170
|
}
|
|
144
171
|
|
|
172
|
+
const promotedRuleCount = await this.promoteStableSummariesToRules(createdMemoryIds);
|
|
173
|
+
|
|
145
174
|
// Prune consolidated events from working set
|
|
146
175
|
if (consolidatedCount > 0) {
|
|
147
176
|
const consolidatedEventIds = groups
|
|
@@ -161,7 +190,87 @@ export class ConsolidationWorker {
|
|
|
161
190
|
}
|
|
162
191
|
}
|
|
163
192
|
|
|
164
|
-
|
|
193
|
+
const report = this.buildCostQualityReport(workingSet.recentEvents, groups, consolidatedCount);
|
|
194
|
+
return { consolidatedCount, promotedRuleCount, report };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async promoteStableSummariesToRules(memoryIds: string[]): Promise<number> {
|
|
198
|
+
let promoted = 0;
|
|
199
|
+
|
|
200
|
+
for (const memoryId of memoryIds) {
|
|
201
|
+
const memory = await this.consolidatedStore.get(memoryId);
|
|
202
|
+
if (!memory) continue;
|
|
203
|
+
if (memory.confidence < 0.55) continue;
|
|
204
|
+
if (memory.sourceEvents.length < 4) continue;
|
|
205
|
+
|
|
206
|
+
const exists = await this.consolidatedStore.hasRuleForSourceMemory(memoryId);
|
|
207
|
+
if (exists) continue;
|
|
208
|
+
|
|
209
|
+
const rule = this.buildRuleFromSummary(memory.summary, memory.topics);
|
|
210
|
+
if (!rule) continue;
|
|
211
|
+
|
|
212
|
+
await this.consolidatedStore.createRule({
|
|
213
|
+
rule,
|
|
214
|
+
topics: memory.topics,
|
|
215
|
+
sourceMemoryIds: [memory.memoryId],
|
|
216
|
+
sourceEvents: memory.sourceEvents,
|
|
217
|
+
confidence: Math.min(1, memory.confidence + 0.08)
|
|
218
|
+
});
|
|
219
|
+
promoted++;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return promoted;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private buildRuleFromSummary(summary: string, topics: string[]): string | null {
|
|
226
|
+
const lines = summary
|
|
227
|
+
.split(/\r?\n/)
|
|
228
|
+
.map((l) => l.trim())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.filter((l) => !l.toLowerCase().startsWith('topics:'));
|
|
231
|
+
|
|
232
|
+
const bullet = lines.find((l) => l.startsWith('- '))?.replace(/^-\s*/, '');
|
|
233
|
+
const seed = bullet || lines[0];
|
|
234
|
+
if (!seed || seed.length < 8) return null;
|
|
235
|
+
|
|
236
|
+
const topicPrefix = topics.length > 0 ? `[${topics.slice(0, 2).join(', ')}] ` : '';
|
|
237
|
+
return `${topicPrefix}${seed}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private buildCostQualityReport(
|
|
241
|
+
events: MemoryEvent[],
|
|
242
|
+
groups: EventGroup[],
|
|
243
|
+
consolidatedCount: number
|
|
244
|
+
): ConsolidationCostQualityReport {
|
|
245
|
+
const beforeTokenEstimate = events.reduce((acc, e) => acc + this.estimateTokens(e.content), 0);
|
|
246
|
+
|
|
247
|
+
const afterSummaries = groups
|
|
248
|
+
.filter((g) => g.events.length >= 3)
|
|
249
|
+
.slice(0, Math.max(consolidatedCount, 1));
|
|
250
|
+
|
|
251
|
+
const afterTokenEstimate = afterSummaries.length > 0
|
|
252
|
+
? afterSummaries.reduce((acc, g) => acc + this.estimateTokens(this.ruleBasedSummary(g)), 0)
|
|
253
|
+
: beforeTokenEstimate;
|
|
254
|
+
|
|
255
|
+
const reductionRatio = beforeTokenEstimate > 0
|
|
256
|
+
? Math.max(0, (beforeTokenEstimate - afterTokenEstimate) / beforeTokenEstimate)
|
|
257
|
+
: 0;
|
|
258
|
+
|
|
259
|
+
const qualityGuardPassed = consolidatedCount === 0
|
|
260
|
+
? true
|
|
261
|
+
: groups.filter((g) => g.events.length >= 3).every((g) => this.calculateConfidence(g) >= 0.55);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
beforeTokenEstimate,
|
|
265
|
+
afterTokenEstimate,
|
|
266
|
+
reductionRatio,
|
|
267
|
+
qualityGuardPassed,
|
|
268
|
+
details: `groups=${groups.length}, consolidated=${consolidatedCount}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private estimateTokens(text: string): number {
|
|
273
|
+
return Math.ceil((text || '').length / 4);
|
|
165
274
|
}
|
|
166
275
|
|
|
167
276
|
/**
|
package/src/core/event-store.ts
CHANGED
|
@@ -280,6 +280,19 @@ export class EventStore {
|
|
|
280
280
|
)
|
|
281
281
|
`);
|
|
282
282
|
|
|
283
|
+
// Consolidated Rules table (long-term stable memory)
|
|
284
|
+
await dbRun(this.db, `
|
|
285
|
+
CREATE TABLE IF NOT EXISTS consolidated_rules (
|
|
286
|
+
rule_id VARCHAR PRIMARY KEY,
|
|
287
|
+
rule TEXT NOT NULL,
|
|
288
|
+
topics JSON,
|
|
289
|
+
source_memory_ids JSON,
|
|
290
|
+
source_events JSON,
|
|
291
|
+
confidence FLOAT DEFAULT 0.5,
|
|
292
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
293
|
+
)
|
|
294
|
+
`);
|
|
295
|
+
|
|
283
296
|
// Endless Mode Config table
|
|
284
297
|
await dbRun(this.db, `
|
|
285
298
|
CREATE TABLE IF NOT EXISTS endless_config (
|
|
@@ -314,6 +327,7 @@ export class EventStore {
|
|
|
314
327
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at)`);
|
|
315
328
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score DESC)`);
|
|
316
329
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence DESC)`);
|
|
330
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence DESC)`);
|
|
317
331
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at)`);
|
|
318
332
|
|
|
319
333
|
this.initialized = true;
|
package/src/core/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './event-store.js';
|
|
|
14
14
|
export * from './sqlite-wrapper.js';
|
|
15
15
|
export * from './sqlite-event-store.js';
|
|
16
16
|
export * from './sync-worker.js';
|
|
17
|
+
export * from './mongo-sync-worker.js';
|
|
17
18
|
export * from './entity-repo.js';
|
|
18
19
|
export * from './edge-repo.js';
|
|
19
20
|
|