@jussmor/commit-memory-mcp 0.3.7 → 0.3.8

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/README.md CHANGED
@@ -108,6 +108,9 @@ build_context_pack({
108
108
  ```
109
109
 
110
110
  Use this before invoking a coding subagent to keep prompts small and focused.
111
+ If no rows are found in strict scope, the server now falls back automatically to broader scope levels.
112
+
113
+ Important: if you provide `domain`/`feature`/`branch` tags in `build_context_pack`, use the same tags during `sync_pr_context` for best precision.
111
114
 
112
115
  ### Promote draft facts after review
113
116
 
@@ -143,6 +146,20 @@ who_changed_this({
143
146
 
144
147
  Use this to discover recent authors and commit history for a target file.
145
148
 
149
+ ### Explain what is happening in a Next.js folder
150
+
151
+ ```text
152
+ explain_path_activity({
153
+ targetPath: "app/dashboard",
154
+ owner: "MaxwellClinic-Development",
155
+ repo: "EverBetter-Pro",
156
+ limit: 30
157
+ })
158
+ ```
159
+
160
+ Use this when you want a fast folder-level summary for areas like `app/dashboard`, `app/(authenticated)`, or `src/components`.
161
+ It returns recent commits, top authors, most touched files, and related PR context/decisions when available.
162
+
146
163
  ### Explain intent for a change
147
164
 
148
165
  ```text
package/dist/db/client.js CHANGED
@@ -371,32 +371,30 @@ export function promoteContextFacts(db, options) {
371
371
  return Number(result.changes ?? 0);
372
372
  }
373
373
  export function buildContextPack(db, options) {
374
- const clauses = [];
375
- const params = [];
376
- if (options.domain) {
377
- clauses.push("scope_domain = ?");
378
- params.push(options.domain);
379
- }
380
- if (options.feature) {
381
- clauses.push("scope_feature = ?");
382
- params.push(options.feature);
383
- }
384
- if (options.branch) {
385
- clauses.push("scope_branch = ?");
386
- params.push(options.branch);
387
- }
388
- if (options.taskType) {
389
- clauses.push("scope_task_type IN (?, 'general')");
390
- params.push(options.taskType);
391
- }
392
- if (options.includeDraft) {
393
- clauses.push("status IN ('promoted', 'draft')");
394
- }
395
- else {
396
- clauses.push("status = 'promoted'");
397
- }
398
- const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
399
- const sql = `
374
+ const taskType = options.taskType ?? "general";
375
+ function runQuery(params) {
376
+ const clauses = [];
377
+ const values = [taskType, taskType];
378
+ if (params.includeDomain && options.domain) {
379
+ clauses.push("scope_domain = ?");
380
+ values.push(options.domain);
381
+ }
382
+ if (params.includeFeature && options.feature) {
383
+ clauses.push("scope_feature = ?");
384
+ values.push(options.feature);
385
+ }
386
+ if (params.includeBranch && options.branch) {
387
+ clauses.push("scope_branch = ?");
388
+ values.push(options.branch);
389
+ }
390
+ if (options.includeDraft) {
391
+ clauses.push("status IN ('promoted', 'draft')");
392
+ }
393
+ else {
394
+ clauses.push("status = 'promoted'");
395
+ }
396
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
397
+ const sql = `
400
398
  SELECT
401
399
  id,
402
400
  source_type,
@@ -411,10 +409,15 @@ export function buildContextPack(db, options) {
411
409
  confidence,
412
410
  status,
413
411
  updated_at,
414
- ((priority * 0.45) + (confidence * 0.35) +
412
+ ((priority * 0.40) + (confidence * 0.30) +
415
413
  CASE
416
- WHEN scope_task_type = ? THEN 0.20
417
- WHEN scope_task_type = 'general' THEN 0.10
414
+ WHEN scope_task_type = ? THEN 0.30
415
+ WHEN scope_task_type = 'general' THEN 0.15
416
+ ELSE 0.0
417
+ END +
418
+ CASE
419
+ WHEN source_type = 'pr_description' THEN 0.15
420
+ WHEN source_type LIKE 'pr_%' THEN 0.08
418
421
  ELSE 0.0
419
422
  END) AS score
420
423
  FROM context_facts
@@ -422,24 +425,46 @@ export function buildContextPack(db, options) {
422
425
  ORDER BY score DESC, updated_at DESC
423
426
  LIMIT ?
424
427
  `;
425
- const taskType = options.taskType ?? "general";
426
- const rows = db.prepare(sql).all(taskType, ...params, options.limit);
427
- return rows.map((row) => ({
428
- id: String(row.id ?? ""),
429
- sourceType: String(row.source_type ?? ""),
430
- sourceRef: String(row.source_ref ?? ""),
431
- title: String(row.title ?? ""),
432
- content: String(row.content ?? ""),
433
- domain: String(row.scope_domain ?? ""),
434
- feature: String(row.scope_feature ?? ""),
435
- branch: String(row.scope_branch ?? ""),
436
- taskType: String(row.scope_task_type ?? ""),
437
- priority: Number(row.priority ?? 0),
438
- confidence: Number(row.confidence ?? 0),
439
- score: Number(row.score ?? 0),
440
- status: String(row.status ?? "promoted"),
441
- updatedAt: String(row.updated_at ?? ""),
442
- }));
428
+ values.push(options.limit);
429
+ const rows = db.prepare(sql).all(...values);
430
+ return rows.map((row) => ({
431
+ id: String(row.id ?? ""),
432
+ sourceType: String(row.source_type ?? ""),
433
+ sourceRef: String(row.source_ref ?? ""),
434
+ title: String(row.title ?? ""),
435
+ content: String(row.content ?? ""),
436
+ domain: String(row.scope_domain ?? ""),
437
+ feature: String(row.scope_feature ?? ""),
438
+ branch: String(row.scope_branch ?? ""),
439
+ taskType: String(row.scope_task_type ?? ""),
440
+ priority: Number(row.priority ?? 0),
441
+ confidence: Number(row.confidence ?? 0),
442
+ score: Number(row.score ?? 0),
443
+ status: String(row.status ?? "promoted"),
444
+ updatedAt: String(row.updated_at ?? ""),
445
+ }));
446
+ }
447
+ const strictRows = runQuery({
448
+ includeDomain: true,
449
+ includeFeature: true,
450
+ includeBranch: true,
451
+ });
452
+ if (strictRows.length > 0) {
453
+ return strictRows;
454
+ }
455
+ const broadRows = runQuery({
456
+ includeDomain: true,
457
+ includeFeature: false,
458
+ includeBranch: false,
459
+ });
460
+ if (broadRows.length > 0) {
461
+ return broadRows;
462
+ }
463
+ return runQuery({
464
+ includeDomain: false,
465
+ includeFeature: false,
466
+ includeBranch: false,
467
+ });
443
468
  }
444
469
  export function archiveFeatureContext(db, options) {
445
470
  const now = new Date().toISOString();
@@ -16,6 +16,27 @@ export declare function whoChangedFile(options: {
16
16
  lastCommitAt: string;
17
17
  }>;
18
18
  };
19
+ export declare function explainPathActivity(options: {
20
+ repoPath: string;
21
+ targetPath: string;
22
+ limit: number;
23
+ }): {
24
+ targetPath: string;
25
+ commits: Array<{
26
+ sha: string;
27
+ author: string;
28
+ date: string;
29
+ subject: string;
30
+ }>;
31
+ topAuthors: Array<{
32
+ author: string;
33
+ commitCount: number;
34
+ }>;
35
+ topFiles: Array<{
36
+ filePath: string;
37
+ touchCount: number;
38
+ }>;
39
+ };
19
40
  export declare function latestCommitForFile(repoPath: string, filePath: string): string | null;
20
41
  export declare function commitDetails(repoPath: string, sha: string): {
21
42
  sha: string;
@@ -46,6 +46,66 @@ export function whoChangedFile(options) {
46
46
  authors,
47
47
  };
48
48
  }
49
+ export function explainPathActivity(options) {
50
+ const repoPath = path.resolve(options.repoPath);
51
+ const limit = Math.max(1, options.limit);
52
+ const targetPath = options.targetPath.trim();
53
+ const commitsRaw = runGit(repoPath, [
54
+ "log",
55
+ `-n${limit}`,
56
+ "--format=%H%x1f%an%x1f%aI%x1f%s",
57
+ "--",
58
+ targetPath,
59
+ ]).trim();
60
+ if (!commitsRaw) {
61
+ return {
62
+ targetPath,
63
+ commits: [],
64
+ topAuthors: [],
65
+ topFiles: [],
66
+ };
67
+ }
68
+ const commits = commitsRaw.split("\n").map((line) => {
69
+ const [sha = "", author = "", date = "", subject = ""] = line.split("\x1f");
70
+ return { sha, author, date, subject };
71
+ });
72
+ const authorCounts = new Map();
73
+ const fileCounts = new Map();
74
+ for (const commit of commits) {
75
+ authorCounts.set(commit.author, (authorCounts.get(commit.author) ?? 0) + 1);
76
+ const filesRaw = runGit(repoPath, [
77
+ "show",
78
+ "--name-only",
79
+ "--pretty=format:",
80
+ commit.sha,
81
+ "--",
82
+ targetPath,
83
+ ]).trim();
84
+ if (!filesRaw) {
85
+ continue;
86
+ }
87
+ for (const filePath of filesRaw
88
+ .split("\n")
89
+ .map((line) => line.trim())
90
+ .filter(Boolean)) {
91
+ fileCounts.set(filePath, (fileCounts.get(filePath) ?? 0) + 1);
92
+ }
93
+ }
94
+ const topAuthors = Array.from(authorCounts.entries())
95
+ .map(([author, commitCount]) => ({ author, commitCount }))
96
+ .sort((a, b) => b.commitCount - a.commitCount)
97
+ .slice(0, 8);
98
+ const topFiles = Array.from(fileCounts.entries())
99
+ .map(([filePath, touchCount]) => ({ filePath, touchCount }))
100
+ .sort((a, b) => b.touchCount - a.touchCount)
101
+ .slice(0, 12);
102
+ return {
103
+ targetPath,
104
+ commits,
105
+ topAuthors,
106
+ topFiles,
107
+ };
108
+ }
49
109
  export function latestCommitForFile(repoPath, filePath) {
50
110
  const output = runGit(path.resolve(repoPath), [
51
111
  "log",
@@ -7,7 +7,7 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.js";
10
- import { commitDetails, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
10
+ import { commitDetails, explainPathActivity, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
11
11
  import { listActiveWorktrees } from "../git/worktree.js";
12
12
  import { syncPullRequestContext } from "../pr/sync.js";
13
13
  function fetchRemote(repoPath) {
@@ -26,6 +26,78 @@ function detectReferencedPrNumber(text) {
26
26
  }
27
27
  return value;
28
28
  }
29
+ function parsePrNumberFromSourceRef(sourceRef) {
30
+ const match = sourceRef.match(/#(\d{1,8})\b/);
31
+ if (!match) {
32
+ return null;
33
+ }
34
+ const value = Number.parseInt(match[1] ?? "", 10);
35
+ if (!Number.isFinite(value) || value <= 0) {
36
+ return null;
37
+ }
38
+ return value;
39
+ }
40
+ function loadPathPullRequestContext(db, options) {
41
+ const prNumbers = new Set(options.referencedPrNumbers);
42
+ const likePattern = `%${options.targetPath}%`;
43
+ const sourceRows = db
44
+ .prepare(`
45
+ SELECT source_ref
46
+ FROM context_facts
47
+ WHERE (title LIKE ? OR content LIKE ?)
48
+ AND source_ref LIKE '%#%'
49
+ ORDER BY updated_at DESC
50
+ LIMIT 80
51
+ `)
52
+ .all(likePattern, likePattern);
53
+ for (const row of sourceRows) {
54
+ const number = parsePrNumberFromSourceRef(String(row.source_ref ?? ""));
55
+ if (number) {
56
+ prNumbers.add(number);
57
+ }
58
+ }
59
+ if (prNumbers.size === 0) {
60
+ return [];
61
+ }
62
+ const results = [];
63
+ for (const prNumber of Array.from(prNumbers).slice(0, 20)) {
64
+ const pr = options.owner && options.repo
65
+ ? (db
66
+ .prepare(`
67
+ SELECT repo_owner, repo_name, pr_number, title, body, author, state, created_at, updated_at, merged_at, url
68
+ FROM prs
69
+ WHERE repo_owner = ? AND repo_name = ? AND pr_number = ?
70
+ LIMIT 1
71
+ `)
72
+ .get(options.owner, options.repo, prNumber) ?? null)
73
+ : (db
74
+ .prepare(`
75
+ SELECT repo_owner, repo_name, pr_number, title, body, author, state, created_at, updated_at, merged_at, url
76
+ FROM prs
77
+ WHERE pr_number = ?
78
+ ORDER BY updated_at DESC
79
+ LIMIT 1
80
+ `)
81
+ .get(prNumber) ?? null);
82
+ if (!pr) {
83
+ continue;
84
+ }
85
+ const decisions = db
86
+ .prepare(`
87
+ SELECT id, source, author, summary, severity, created_at
88
+ FROM pr_decisions
89
+ WHERE repo_owner = ? AND repo_name = ? AND pr_number = ?
90
+ ORDER BY created_at DESC
91
+ LIMIT 20
92
+ `)
93
+ .all(pr.repo_owner, pr.repo_name, pr.pr_number);
94
+ results.push({
95
+ pr,
96
+ decisions,
97
+ });
98
+ }
99
+ return results;
100
+ }
29
101
  function loadPullRequestContext(db, prNumber, repoOwner, repoName) {
30
102
  const pr = repoOwner && repoName
31
103
  ? (db
@@ -162,6 +234,20 @@ export async function startMcpServer() {
162
234
  required: ["filePath"],
163
235
  },
164
236
  },
237
+ {
238
+ name: "explain_path_activity",
239
+ description: "Given a file or folder path, summarize activity, top files/authors, and related PR context.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ targetPath: { type: "string" },
244
+ owner: { type: "string" },
245
+ repo: { type: "string" },
246
+ limit: { type: "number" },
247
+ },
248
+ required: ["targetPath"],
249
+ },
250
+ },
165
251
  {
166
252
  name: "why_was_this_changed",
167
253
  description: "Explain intent for a commit or file using git history and synced PR decisions.",
@@ -376,6 +462,52 @@ export async function startMcpServer() {
376
462
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
377
463
  };
378
464
  }
465
+ if (request.params.name === "explain_path_activity") {
466
+ const targetPath = String(request.params.arguments?.targetPath ?? "").trim();
467
+ const owner = String(request.params.arguments?.owner ?? "").trim();
468
+ const repo = String(request.params.arguments?.repo ?? "").trim();
469
+ const limit = Number(request.params.arguments?.limit ?? 25);
470
+ if (!targetPath) {
471
+ return {
472
+ content: [{ type: "text", text: "targetPath is required" }],
473
+ isError: true,
474
+ };
475
+ }
476
+ const output = explainPathActivity({
477
+ repoPath,
478
+ targetPath,
479
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 25,
480
+ });
481
+ const referencedPrNumbers = output.commits
482
+ .map((commit) => {
483
+ const details = commitDetails(repoPath, commit.sha);
484
+ return detectReferencedPrNumber(`${details.subject}\n${details.body}`);
485
+ })
486
+ .filter((value) => Number.isFinite(value));
487
+ const db = openDatabase(dbPath);
488
+ try {
489
+ const relatedPullRequests = loadPathPullRequestContext(db, {
490
+ targetPath,
491
+ owner: owner || undefined,
492
+ repo: repo || undefined,
493
+ referencedPrNumbers,
494
+ });
495
+ return {
496
+ content: [
497
+ {
498
+ type: "text",
499
+ text: JSON.stringify({
500
+ ...output,
501
+ relatedPullRequests,
502
+ }, null, 2),
503
+ },
504
+ ],
505
+ };
506
+ }
507
+ finally {
508
+ db.close();
509
+ }
510
+ }
379
511
  if (request.params.name === "why_was_this_changed") {
380
512
  const owner = String(request.params.arguments?.owner ?? "").trim();
381
513
  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.3.7",
3
+ "version": "0.3.8",
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",