@jussmor/commit-memory-mcp 0.4.2 → 0.5.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.
@@ -25,8 +25,46 @@ export declare function buildContextPack(db: RagDatabase, options: {
25
25
  taskType?: string;
26
26
  includeDraft?: boolean;
27
27
  limit: number;
28
- }): ContextPackRecord[];
28
+ forceRefresh?: boolean;
29
+ summarizePR?: boolean;
30
+ }): {
31
+ learnedFeature: ContextPackRecord[];
32
+ branchContext: ContextPackRecord[];
33
+ prMetadata: ContextPackRecord[];
34
+ allContext: ContextPackRecord[];
35
+ };
29
36
  export declare function archiveFeatureContext(db: RagDatabase, options: {
30
37
  domain: string;
31
38
  feature: string;
32
39
  }): number;
40
+ export interface FeatureResumeOptions {
41
+ feature: string;
42
+ domain?: string;
43
+ limit?: number;
44
+ }
45
+ export declare function getFeatureResume(db: RagDatabase, options: FeatureResumeOptions): string;
46
+ export declare function listLearnedFeatures(db: RagDatabase, options?: {
47
+ domain?: string;
48
+ status?: string;
49
+ }): Array<{
50
+ feature: string;
51
+ domain: string;
52
+ branch: string;
53
+ confidence: number;
54
+ status: string;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ title: string;
58
+ contentLength: number;
59
+ }>;
60
+ export declare function listAvailableBranches(db: RagDatabase, options?: {
61
+ domain?: string;
62
+ feature?: string;
63
+ }): Array<{
64
+ branch: string;
65
+ feature: string;
66
+ domain: string;
67
+ factCount: number;
68
+ lastUpdated: string;
69
+ topConfidence: number;
70
+ }>;
package/dist/db/client.js CHANGED
@@ -7,6 +7,10 @@ export function openDatabase(dbPath) {
7
7
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
8
8
  const db = new Database(resolved);
9
9
  load(db);
10
+ // Enable WAL mode and ensure data is persisted to disk
11
+ db.pragma("journal_mode = WAL");
12
+ db.pragma("synchronous = NORMAL");
13
+ db.pragma("foreign_keys = ON");
10
14
  db.exec(`
11
15
  CREATE TABLE IF NOT EXISTS commits (
12
16
  sha TEXT PRIMARY KEY,
@@ -370,6 +374,14 @@ export function promoteContextFacts(db, options) {
370
374
  const result = db.prepare(sql).run(now, ...params);
371
375
  return Number(result.changes ?? 0);
372
376
  }
377
+ function summarizePRMetadata(fact) {
378
+ // Extract key info from PR metadata to keep it concise
379
+ if (fact.sourceType.startsWith("pr_")) {
380
+ const lines = fact.content.split("\n").slice(0, 2).join(" ");
381
+ return `[${fact.sourceRef}] ${fact.title} — ${lines.substring(0, 100)}`;
382
+ }
383
+ return fact.content;
384
+ }
373
385
  export function buildContextPack(db, options) {
374
386
  const taskType = options.taskType ?? "general";
375
387
  const GLOBAL_BRANCH = "main";
@@ -463,6 +475,75 @@ export function buildContextPack(db, options) {
463
475
  pack.push(row);
464
476
  }
465
477
  };
478
+ // PRIORITY 0) Learned feature knowledge is always included first when available.
479
+ // This ensures feature knowledge isn't lost behind PR metadata.
480
+ // Always include learned features regardless of parameter filters.
481
+ let learnedFacts = [];
482
+ if (options.feature) {
483
+ // Search for learned knowledge about the specific feature
484
+ learnedFacts = db.prepare(`
485
+ SELECT
486
+ id,
487
+ source_type,
488
+ source_ref,
489
+ title,
490
+ content,
491
+ scope_domain,
492
+ scope_feature,
493
+ scope_branch,
494
+ scope_task_type,
495
+ priority,
496
+ confidence,
497
+ status,
498
+ updated_at,
499
+ ((priority * 0.40) + (confidence * 0.30) + 0.25) AS score
500
+ FROM context_facts
501
+ WHERE source_type = 'feature-agent' AND scope_feature = ? AND status = 'promoted'
502
+ ORDER BY updated_at DESC, priority DESC
503
+ LIMIT ?
504
+ `).all(options.feature, Math.max(3, Math.floor(options.limit * 0.2)));
505
+ }
506
+ else {
507
+ // Auto-discover recently learned features when no specific feature is requested
508
+ learnedFacts = db.prepare(`
509
+ SELECT
510
+ id,
511
+ source_type,
512
+ source_ref,
513
+ title,
514
+ content,
515
+ scope_domain,
516
+ scope_feature,
517
+ scope_branch,
518
+ scope_task_type,
519
+ priority,
520
+ confidence,
521
+ status,
522
+ updated_at,
523
+ ((priority * 0.40) + (confidence * 0.30) + 0.25) AS score
524
+ FROM context_facts
525
+ WHERE source_type = 'feature-agent' AND status = 'promoted'
526
+ ORDER BY updated_at DESC, priority DESC
527
+ LIMIT ?
528
+ `).all(Math.max(3, Math.floor(options.limit * 0.2)));
529
+ }
530
+ const learnedRows = learnedFacts.map((row) => ({
531
+ id: String(row.id ?? ""),
532
+ sourceType: String(row.source_type ?? ""),
533
+ sourceRef: String(row.source_ref ?? ""),
534
+ title: String(row.title ?? ""),
535
+ content: String(row.content ?? ""),
536
+ domain: String(row.scope_domain ?? ""),
537
+ feature: String(row.scope_feature ?? ""),
538
+ branch: String(row.scope_branch ?? ""),
539
+ taskType: String(row.scope_task_type ?? ""),
540
+ priority: Number(row.priority ?? 0),
541
+ confidence: Number(row.confidence ?? 0),
542
+ score: Number(row.score ?? 0),
543
+ status: String(row.status ?? "promoted"),
544
+ updatedAt: String(row.updated_at ?? ""),
545
+ }));
546
+ addRows(learnedRows);
466
547
  // 1) Main branch domain context is the durable source-of-truth baseline.
467
548
  if (pack.length < options.limit) {
468
549
  addRows(runQuery({
@@ -503,7 +584,47 @@ export function buildContextPack(db, options) {
503
584
  includeBranch: false,
504
585
  }));
505
586
  }
506
- return pack;
587
+ // Categorize results and apply summarization if requested
588
+ const learnedFeature = [];
589
+ const branchContext = [];
590
+ const prMetadata = [];
591
+ for (const item of pack) {
592
+ if (item.sourceType === "feature-agent") {
593
+ learnedFeature.push(item);
594
+ }
595
+ else if (item.sourceType.startsWith("pr_")) {
596
+ if (options.summarizePR) {
597
+ prMetadata.push({
598
+ ...item,
599
+ content: summarizePRMetadata(item),
600
+ });
601
+ }
602
+ else {
603
+ prMetadata.push(item);
604
+ }
605
+ }
606
+ else if (item.branch && item.branch !== "main") {
607
+ branchContext.push(item);
608
+ }
609
+ else {
610
+ // Fallback for other context types
611
+ if (options.summarizePR && item.sourceType.startsWith("pr_")) {
612
+ branchContext.push({
613
+ ...item,
614
+ content: summarizePRMetadata(item),
615
+ });
616
+ }
617
+ else {
618
+ branchContext.push(item);
619
+ }
620
+ }
621
+ }
622
+ return {
623
+ learnedFeature,
624
+ branchContext,
625
+ prMetadata,
626
+ allContext: pack,
627
+ };
507
628
  }
508
629
  export function archiveFeatureContext(db, options) {
509
630
  const now = new Date().toISOString();
@@ -555,3 +676,163 @@ export function archiveFeatureContext(db, options) {
555
676
  const result = tx();
556
677
  return Number(result.changes ?? 0);
557
678
  }
679
+ export function getFeatureResume(db, options) {
680
+ const feature = String(options.feature ?? "").trim();
681
+ const domain = String(options.domain ?? "").trim();
682
+ const limit = Number(options.limit ?? 20);
683
+ if (!feature) {
684
+ return "# Feature Resume\n\nError: feature parameter is required.";
685
+ }
686
+ // Query learned feature facts
687
+ const learnedFacts = db
688
+ .prepare(`
689
+ SELECT
690
+ title,
691
+ content,
692
+ confidence,
693
+ updated_at
694
+ FROM context_facts
695
+ WHERE source_type = 'feature-agent'
696
+ AND scope_feature = ?
697
+ AND status = 'promoted'
698
+ ORDER BY updated_at DESC
699
+ LIMIT 1
700
+ `)
701
+ .all(feature);
702
+ // Query PR metadata related to the feature
703
+ const prFacts = db
704
+ .prepare(`
705
+ SELECT
706
+ title,
707
+ content,
708
+ source_ref,
709
+ confidence,
710
+ updated_at
711
+ FROM context_facts
712
+ WHERE source_type LIKE 'pr_%'
713
+ AND (scope_feature = ? OR scope_branch LIKE ?)
714
+ AND status = 'promoted'
715
+ ORDER BY updated_at DESC
716
+ LIMIT ?
717
+ `)
718
+ .all(feature, `%${feature}%`, limit);
719
+ // Build markdown document
720
+ const markdown = [];
721
+ markdown.push(`# Feature Resume: ${feature}`);
722
+ markdown.push("");
723
+ // Add learned knowledge section
724
+ if (learnedFacts.length > 0) {
725
+ const learned = learnedFacts[0];
726
+ markdown.push("## 📚 Feature Knowledge");
727
+ markdown.push(`**Confidence:** ${(learned.confidence * 100).toFixed(0)}%`);
728
+ markdown.push(`**Last Updated:** ${new Date(learned.updated_at).toLocaleString()}`);
729
+ markdown.push("");
730
+ markdown.push(learned.content);
731
+ markdown.push("");
732
+ }
733
+ else {
734
+ markdown.push("## 📚 Feature Knowledge");
735
+ markdown.push("*(No learned knowledge yet. Run `learn_feature` to seed context.)*");
736
+ markdown.push("");
737
+ }
738
+ // Add PR metadata section
739
+ if (prFacts.length > 0) {
740
+ markdown.push("## 🔗 Related Pull Requests");
741
+ markdown.push("");
742
+ for (const pr of prFacts) {
743
+ const confidence = (pr.confidence * 100).toFixed(0);
744
+ const ref = pr.source_ref || "unknown";
745
+ markdown.push(`- **${pr.title}** (${ref}) — ${confidence}% confidence`);
746
+ if (pr.content && pr.content.length < 200) {
747
+ markdown.push(` > ${pr.content}`);
748
+ }
749
+ }
750
+ markdown.push("");
751
+ }
752
+ // Add recommendations
753
+ markdown.push("## 💡 Recommendations");
754
+ if (learnedFacts.length === 0) {
755
+ markdown.push('- Run `learn_feature({ featureBranch: "${feature}", agentContent: "..." })` to seed knowledge');
756
+ }
757
+ if (prFacts.length === 0) {
758
+ markdown.push("- Sync PR context with `pre_plan_sync_brief` to see related PRs");
759
+ }
760
+ markdown.push("- Use this resume alongside your active worktree for complete context");
761
+ return markdown.join("\n");
762
+ }
763
+ export function listLearnedFeatures(db, options) {
764
+ const clauses = ["source_type = 'feature-agent'"];
765
+ const values = [];
766
+ if (options?.domain) {
767
+ clauses.push("scope_domain = ?");
768
+ values.push(options.domain);
769
+ }
770
+ if (options?.status) {
771
+ clauses.push("status = ?");
772
+ values.push(options.status);
773
+ }
774
+ else {
775
+ clauses.push("status = 'promoted'");
776
+ }
777
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
778
+ const rows = db.prepare(`
779
+ SELECT DISTINCT
780
+ scope_feature as feature,
781
+ scope_domain as domain,
782
+ scope_branch as branch,
783
+ confidence,
784
+ status,
785
+ created_at,
786
+ updated_at,
787
+ title,
788
+ LENGTH(content) as contentLength
789
+ FROM context_facts
790
+ ${where}
791
+ ORDER BY updated_at DESC
792
+ `).all(...values);
793
+ return rows.map((row) => ({
794
+ feature: String(row.feature ?? ""),
795
+ domain: String(row.domain ?? ""),
796
+ branch: String(row.branch ?? ""),
797
+ confidence: Number(row.confidence ?? 0),
798
+ status: String(row.status ?? ""),
799
+ createdAt: String(row.created_at ?? ""),
800
+ updatedAt: String(row.updated_at ?? ""),
801
+ title: String(row.title ?? ""),
802
+ contentLength: Number(row.contentLength ?? 0),
803
+ }));
804
+ }
805
+ export function listAvailableBranches(db, options) {
806
+ const clauses = ["status = 'promoted'"];
807
+ const values = [];
808
+ if (options?.domain) {
809
+ clauses.push("scope_domain = ?");
810
+ values.push(options.domain);
811
+ }
812
+ if (options?.feature) {
813
+ clauses.push("scope_feature = ?");
814
+ values.push(options.feature);
815
+ }
816
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
817
+ const rows = db.prepare(`
818
+ SELECT
819
+ scope_branch as branch,
820
+ scope_feature as feature,
821
+ scope_domain as domain,
822
+ COUNT(*) as factCount,
823
+ MAX(updated_at) as lastUpdated,
824
+ MAX(confidence) as topConfidence
825
+ FROM context_facts
826
+ ${where}
827
+ GROUP BY scope_branch, scope_feature, scope_domain
828
+ ORDER BY updated_at DESC
829
+ `).all(...values);
830
+ return rows.map((row) => ({
831
+ branch: String(row.branch ?? ""),
832
+ feature: String(row.feature ?? ""),
833
+ domain: String(row.domain ?? ""),
834
+ factCount: Number(row.factCount ?? 0),
835
+ lastUpdated: String(row.lastUpdated ?? ""),
836
+ topConfidence: Number(row.topConfidence ?? 0),
837
+ }));
838
+ }
@@ -6,7 +6,7 @@ import { execFileSync } from "node:child_process";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
- import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertContextFact, upsertWorktreeSession, } from "../db/client.js";
9
+ import { archiveFeatureContext, buildContextPack, getFeatureResume, listLearnedFeatures, listAvailableBranches, openDatabase, promoteContextFacts, upsertContextFact, upsertWorktreeSession, } from "../db/client.js";
10
10
  import { commitDetails, explainPathActivity, extractFeatureBranchCommits, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
11
11
  import { listActiveWorktrees } from "../git/worktree.js";
12
12
  import { syncPullRequestContext } from "../pr/sync.js";
@@ -175,7 +175,7 @@ export async function startMcpServer() {
175
175
  },
176
176
  {
177
177
  name: "build_context_pack",
178
- description: "Build a scoped context pack for a task/domain/feature/branch to keep agent prompts small.",
178
+ description: "Build a scoped context pack for a task/domain/feature/branch. Returns learned feature knowledge, branch context, and PR metadata separately.",
179
179
  inputSchema: {
180
180
  type: "object",
181
181
  properties: {
@@ -185,6 +185,14 @@ export async function startMcpServer() {
185
185
  taskType: { type: "string" },
186
186
  includeDraft: { type: "boolean" },
187
187
  limit: { type: "number" },
188
+ forceRefresh: {
189
+ type: "boolean",
190
+ description: "Re-run learn_feature to update feature knowledge",
191
+ },
192
+ summarizePR: {
193
+ type: "boolean",
194
+ description: "Return PR metadata as summaries instead of full content",
195
+ },
188
196
  },
189
197
  required: [],
190
198
  },
@@ -326,6 +334,49 @@ export async function startMcpServer() {
326
334
  required: ["featureBranch"],
327
335
  },
328
336
  },
337
+ {
338
+ name: "get_feature_resume",
339
+ description: "Combine learned feature knowledge with PR metadata and return as markdown. Does NOT require a git worktree. Use this to review a feature's complete context: what we learned about it + related PR activity.",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ feature: {
344
+ type: "string",
345
+ description: "e.g. messaging",
346
+ },
347
+ domain: { type: "string" },
348
+ limit: { type: "number" },
349
+ },
350
+ required: ["feature"],
351
+ },
352
+ },
353
+ {
354
+ name: "list_learned_features",
355
+ description: "List all learned features stored in the knowledge database with confidence levels and timestamps.",
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ domain: { type: "string" },
360
+ status: {
361
+ type: "string",
362
+ description: "Filter by status (promoted, draft, etc). Defaults to promoted.",
363
+ },
364
+ },
365
+ required: [],
366
+ },
367
+ },
368
+ {
369
+ name: "list_available_branches",
370
+ description: "List all available branches and features with knowledge context available for each.",
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ domain: { type: "string" },
375
+ feature: { type: "string" },
376
+ },
377
+ required: [],
378
+ },
379
+ },
329
380
  {
330
381
  name: "pre_plan_sync_brief",
331
382
  description: "Run sync + overnight + feature resume analysis before planning work.",
@@ -390,6 +441,8 @@ export async function startMcpServer() {
390
441
  const branch = String(request.params.arguments?.branch ?? "").trim();
391
442
  const taskType = String(request.params.arguments?.taskType ?? "").trim() || "general";
392
443
  const includeDraft = Boolean(request.params.arguments?.includeDraft);
444
+ const forceRefresh = Boolean(request.params.arguments?.forceRefresh);
445
+ const summarizePR = Boolean(request.params.arguments?.summarizePR);
393
446
  const db = openDatabase(dbPath);
394
447
  try {
395
448
  const pack = buildContextPack(db, {
@@ -399,6 +452,8 @@ export async function startMcpServer() {
399
452
  taskType,
400
453
  includeDraft,
401
454
  limit: Number.isFinite(limit) && limit > 0 ? limit : 20,
455
+ forceRefresh,
456
+ summarizePR,
402
457
  });
403
458
  return {
404
459
  content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
@@ -626,6 +681,125 @@ export async function startMcpServer() {
626
681
  content: [{ type: "text", text: JSON.stringify(brief, null, 2) }],
627
682
  };
628
683
  }
684
+ if (request.params.name === "get_feature_resume") {
685
+ const feature = String(request.params.arguments?.feature ?? "").trim();
686
+ const domain = String(request.params.arguments?.domain ?? "").trim();
687
+ const limit = Number(request.params.arguments?.limit ?? 20);
688
+ if (!feature) {
689
+ return {
690
+ content: [{ type: "text", text: "feature parameter is required" }],
691
+ isError: true,
692
+ };
693
+ }
694
+ const db = openDatabase(dbPath);
695
+ try {
696
+ const resume = getFeatureResume(db, {
697
+ feature,
698
+ domain: domain || undefined,
699
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 20,
700
+ });
701
+ return {
702
+ content: [{ type: "text", text: resume }],
703
+ };
704
+ }
705
+ finally {
706
+ db.close();
707
+ }
708
+ }
709
+ if (request.params.name === "list_learned_features") {
710
+ const domain = String(request.params.arguments?.domain ?? "").trim();
711
+ const status = String(request.params.arguments?.status ?? "").trim();
712
+ const db = openDatabase(dbPath);
713
+ try {
714
+ const features = listLearnedFeatures(db, {
715
+ domain: domain || undefined,
716
+ status: status || undefined,
717
+ });
718
+ const markdown = [
719
+ "# Learned Features",
720
+ "",
721
+ features.length === 0
722
+ ? "*(No learned features yet)*"
723
+ : `${features.length} feature(s) available:`,
724
+ "",
725
+ ];
726
+ if (features.length > 0) {
727
+ markdown.push("| Feature | Domain | Branch | Confidence | Status | Last Updated |");
728
+ markdown.push("|---------|--------|--------|------------|--------|--------------|");
729
+ for (const feat of features) {
730
+ const confidence = (feat.confidence * 100).toFixed(0);
731
+ const lastUpdated = new Date(feat.updatedAt).toLocaleDateString();
732
+ markdown.push(`| **${feat.feature}** | ${feat.domain} | ${feat.branch} | ${confidence}% | ${feat.status} | ${lastUpdated} |`);
733
+ }
734
+ markdown.push("");
735
+ markdown.push("## Feature Details");
736
+ markdown.push("");
737
+ for (const feat of features) {
738
+ markdown.push(`### ${feat.feature}`);
739
+ markdown.push(`- **Confidence:** ${(feat.confidence * 100).toFixed(0)}%`);
740
+ markdown.push(`- **Domain:** ${feat.domain}`);
741
+ markdown.push(`- **Branch:** ${feat.branch}`);
742
+ markdown.push(`- **Status:** ${feat.status}`);
743
+ markdown.push(`- **Created:** ${new Date(feat.createdAt).toLocaleString()}`);
744
+ markdown.push(`- **Updated:** ${new Date(feat.updatedAt).toLocaleString()}`);
745
+ markdown.push(`- **Title:** ${feat.title}`);
746
+ markdown.push(`- **Content size:** ${feat.contentLength} bytes`);
747
+ markdown.push("");
748
+ }
749
+ }
750
+ return {
751
+ content: [{ type: "text", text: markdown.join("\n") }],
752
+ };
753
+ }
754
+ finally {
755
+ db.close();
756
+ }
757
+ }
758
+ if (request.params.name === "list_available_branches") {
759
+ const domain = String(request.params.arguments?.domain ?? "").trim();
760
+ const feature = String(request.params.arguments?.feature ?? "").trim();
761
+ const db = openDatabase(dbPath);
762
+ try {
763
+ const branches = listAvailableBranches(db, {
764
+ domain: domain || undefined,
765
+ feature: feature || undefined,
766
+ });
767
+ const markdown = [
768
+ "# Available Branches and Features",
769
+ "",
770
+ branches.length === 0
771
+ ? "*(No branches with knowledge available)*"
772
+ : `${branches.length} branch/feature combination(s):`,
773
+ "",
774
+ ];
775
+ if (branches.length > 0) {
776
+ markdown.push("| Branch | Feature | Domain | Facts | Confidence | Last Updated |");
777
+ markdown.push("|--------|---------|--------|-------|------------|--------------|");
778
+ for (const branch of branches) {
779
+ const confidence = (branch.topConfidence * 100).toFixed(0);
780
+ const lastUpdated = new Date(branch.lastUpdated).toLocaleDateString();
781
+ markdown.push(`| **${branch.branch}** | ${branch.feature} | ${branch.domain} | ${branch.factCount} | ${confidence}% | ${lastUpdated} |`);
782
+ }
783
+ markdown.push("");
784
+ markdown.push("## Branch Details");
785
+ markdown.push("");
786
+ for (const branch of branches) {
787
+ markdown.push(`### ${branch.branch} (${branch.feature})`);
788
+ markdown.push(`- **Domain:** ${branch.domain}`);
789
+ markdown.push(`- **Facts stored:** ${branch.factCount}`);
790
+ markdown.push(`- **Top confidence:** ${(branch.topConfidence * 100).toFixed(0)}%`);
791
+ markdown.push(`- **Last updated:** ${new Date(branch.lastUpdated).toLocaleString()}`);
792
+ markdown.push("");
793
+ }
794
+ }
795
+ return {
796
+ content: [{ type: "text", text: markdown.join("\n") }],
797
+ };
798
+ }
799
+ finally {
800
+ db.close();
801
+ }
802
+ }
629
803
  if (request.params.name === "pre_plan_sync_brief") {
630
804
  const owner = String(request.params.arguments?.owner ?? "").trim();
631
805
  const repo = String(request.params.arguments?.repo ?? "").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jussmor/commit-memory-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "mcpName": "io.github.jussmor/commit-memory",
5
5
  "description": "Commit-aware RAG with sqlite-vec and MCP tools for local agent workflows",
6
6
  "license": "MIT",