@jussmor/commit-memory-mcp 0.4.3 → 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.
@@ -37,3 +37,34 @@ export declare function archiveFeatureContext(db: RagDatabase, options: {
37
37
  domain: string;
38
38
  feature: string;
39
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
@@ -477,9 +477,35 @@ export function buildContextPack(db, options) {
477
477
  };
478
478
  // PRIORITY 0) Learned feature knowledge is always included first when available.
479
479
  // This ensures feature knowledge isn't lost behind PR metadata.
480
- if (!options.feature && !options.branch) {
481
- // Auto-discover recently learned features (source_type='feature-agent')
482
- const learnedFacts = db.prepare(`
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(`
483
509
  SELECT
484
510
  id,
485
511
  source_type,
@@ -500,24 +526,24 @@ export function buildContextPack(db, options) {
500
526
  ORDER BY updated_at DESC, priority DESC
501
527
  LIMIT ?
502
528
  `).all(Math.max(3, Math.floor(options.limit * 0.2)));
503
- const learnedRows = learnedFacts.map((row) => ({
504
- id: String(row.id ?? ""),
505
- sourceType: String(row.source_type ?? ""),
506
- sourceRef: String(row.source_ref ?? ""),
507
- title: String(row.title ?? ""),
508
- content: String(row.content ?? ""),
509
- domain: String(row.scope_domain ?? ""),
510
- feature: String(row.scope_feature ?? ""),
511
- branch: String(row.scope_branch ?? ""),
512
- taskType: String(row.scope_task_type ?? ""),
513
- priority: Number(row.priority ?? 0),
514
- confidence: Number(row.confidence ?? 0),
515
- score: Number(row.score ?? 0),
516
- status: String(row.status ?? "promoted"),
517
- updatedAt: String(row.updated_at ?? ""),
518
- }));
519
- addRows(learnedRows);
520
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);
521
547
  // 1) Main branch domain context is the durable source-of-truth baseline.
522
548
  if (pack.length < options.limit) {
523
549
  addRows(runQuery({
@@ -650,3 +676,163 @@ export function archiveFeatureContext(db, options) {
650
676
  const result = tx();
651
677
  return Number(result.changes ?? 0);
652
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";
@@ -334,6 +334,49 @@ export async function startMcpServer() {
334
334
  required: ["featureBranch"],
335
335
  },
336
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
+ },
337
380
  {
338
381
  name: "pre_plan_sync_brief",
339
382
  description: "Run sync + overnight + feature resume analysis before planning work.",
@@ -638,6 +681,125 @@ export async function startMcpServer() {
638
681
  content: [{ type: "text", text: JSON.stringify(brief, null, 2) }],
639
682
  };
640
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
+ }
641
803
  if (request.params.name === "pre_plan_sync_brief") {
642
804
  const owner = String(request.params.arguments?.owner ?? "").trim();
643
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.3",
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",