@jussmor/commit-memory-mcp 0.3.6 → 0.3.7

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
@@ -1,60 +1,225 @@
1
1
  # @jussmor/commit-memory-mcp
2
2
 
3
- Local commit-aware RAG package powered by sqlite-vec with MCP tool endpoints.
3
+ A local MCP server for PR intelligence, author tracing, and worktree-aware async planning.
4
4
 
5
- ## Features
5
+ ## Purpose
6
6
 
7
- - Indexes git commits across branches into commit-file chunks
8
- - Embeds chunks using a local embedding source (Ollama when configured)
9
- - Stores vectors in sqlite-vec (SQLite)
10
- - Exposes MCP tools for agent workflows
7
+ This package helps agents answer:
11
8
 
12
- ## MCP tools
9
+ - who changed a file or area
10
+ - why a change was made
11
+ - what landed on main recently
12
+ - what context should be reviewed before planning
13
+
14
+ ## Current MCP tools
15
+
16
+ - `sync_pr_context`
17
+ - `build_context_pack`
18
+ - `promote_context_facts`
19
+ - `archive_feature_context`
20
+ - `list_active_worktrees`
21
+ - `who_changed_this`
22
+ - `why_was_this_changed`
23
+ - `get_main_branch_overnight_brief`
24
+ - `resume_feature_session_brief`
25
+ - `pre_plan_sync_brief`
26
+
27
+ ## Removed tools
28
+
29
+ The following legacy tools were removed:
13
30
 
14
31
  - `search_related_commits`
15
32
  - `explain_commit_match`
16
33
  - `get_commit_diff`
34
+ - `reindex_commits`
17
35
 
18
36
  ## Quick start
19
37
 
20
38
  ```bash
21
39
  npm install @jussmor/commit-memory-mcp
22
- npx commit-memory-index --repo /path/to/repo --db /path/to/repo/.commit-rag.db --limit 400
23
40
  npx commit-memory-mcp
24
41
  ```
25
42
 
26
- ## Publish
43
+ For local development in this repository:
27
44
 
28
45
  ```bash
46
+ cd packages/commit-rag-mcp
47
+ npm install
29
48
  npm run build
30
- npm publish --access public
31
- mcp-publisher login github
32
- mcp-publisher publish
49
+ node dist/mcp/server.js
50
+ ```
51
+
52
+ ## Requirements
53
+
54
+ - Node.js 20+
55
+ - `gh` CLI authenticated for GitHub PR sync features
56
+ - a git repository available through `COMMIT_RAG_REPO`
57
+
58
+ ## Environment variables
59
+
60
+ - `COMMIT_RAG_REPO` repository path used by the MCP server
61
+ - `COMMIT_RAG_DB` SQLite database path
62
+ - `COMMIT_RAG_LIMIT` default sync/query limit
63
+ - `OLLAMA_BASE_URL` optional Ollama base URL
64
+ - `OLLAMA_EMBED_MODEL` optional embedding model
65
+ - `COPILOT_TOKEN` optional Copilot reranking token
66
+ - `COPILOT_MODEL` optional Copilot model override
67
+ - `COPILOT_BASE_URL` optional Copilot API base URL
68
+
69
+ ## Use cases
70
+
71
+ ### Sync GitHub PR context
72
+
73
+ ```text
74
+ sync_pr_context({
75
+ owner: "MaxwellClinic-Development",
76
+ repo: "EverBetter-Pro",
77
+ domain: "billing",
78
+ feature: "invoice-retry",
79
+ branch: "feat/invoice-retry",
80
+ taskType: "planning",
81
+ limit: 20
82
+ })
83
+ ```
84
+
85
+ Use this before planning when you need fresh PR descriptions, comments, and reviews.
86
+
87
+ ### List active worktrees
88
+
89
+ ```text
90
+ list_active_worktrees({
91
+ baseBranch: "main"
92
+ })
93
+ ```
94
+
95
+ Use this when your team works on multiple features in parallel and wants session-aware context.
96
+
97
+ ### Build a scoped context pack
98
+
99
+ ```text
100
+ build_context_pack({
101
+ domain: "billing",
102
+ feature: "invoice-retry",
103
+ branch: "feat/invoice-retry",
104
+ taskType: "coding",
105
+ includeDraft: false,
106
+ limit: 12
107
+ })
108
+ ```
109
+
110
+ Use this before invoking a coding subagent to keep prompts small and focused.
111
+
112
+ ### Promote draft facts after review
113
+
114
+ ```text
115
+ promote_context_facts({
116
+ domain: "billing",
117
+ feature: "invoice-retry",
118
+ branch: "feat/invoice-retry"
119
+ })
120
+ ```
121
+
122
+ Use this when discussion outcomes are approved and should become durable context.
123
+
124
+ ### Archive completed feature context
125
+
126
+ ```text
127
+ archive_feature_context({
128
+ domain: "billing",
129
+ feature: "invoice-retry"
130
+ })
131
+ ```
132
+
133
+ Use this after merge/closure to prevent active context bloat.
134
+
135
+ ### Find ownership for a file
136
+
137
+ ```text
138
+ who_changed_this({
139
+ filePath: "src/features/auth/session.ts",
140
+ limit: 15
141
+ })
142
+ ```
143
+
144
+ Use this to discover recent authors and commit history for a target file.
145
+
146
+ ### Explain intent for a change
147
+
148
+ ```text
149
+ why_was_this_changed({
150
+ filePath: "src/features/auth/session.ts",
151
+ owner: "MaxwellClinic-Development",
152
+ repo: "EverBetter-Pro"
153
+ })
154
+ ```
155
+
156
+ Use this to combine commit metadata with synced PR context.
157
+
158
+ ### Get an overnight main-branch brief
159
+
160
+ ```text
161
+ get_main_branch_overnight_brief({
162
+ baseBranch: "main",
163
+ sinceHours: 12,
164
+ limit: 25
165
+ })
166
+ ```
167
+
168
+ Use this at the start of the day to review what landed while you were offline.
169
+
170
+ ### Resume a feature worktree
171
+
172
+ ```text
173
+ resume_feature_session_brief({
174
+ worktreePath: "/path/to/worktree",
175
+ baseBranch: "main"
176
+ })
177
+ ```
178
+
179
+ Use this before resuming unfinished work in a separate worktree.
180
+
181
+ ### Run the full pre-plan sync flow
182
+
183
+ ```text
184
+ pre_plan_sync_brief({
185
+ owner: "MaxwellClinic-Development",
186
+ repo: "EverBetter-Pro",
187
+ baseBranch: "main",
188
+ worktreePath: "/path/to/worktree",
189
+ sinceHours: 12,
190
+ limit: 25
191
+ })
33
192
  ```
34
193
 
35
- ## VS Code MCP registration
194
+ Use this as the default entrypoint for async team planning.
195
+
196
+ ## Multi-session git worktree flow
36
197
 
37
- Copy `mcp.config.example.json` entries into your user MCP config and adjust paths/env values.
198
+ For parallel AI coding sessions:
38
199
 
39
- For MCP Registry publication, keep `package.json` `mcpName` and `server.json` `name` in sync.
200
+ 1. Create one git worktree per feature branch.
201
+ 2. Use `list_active_worktrees` to enumerate active sessions.
202
+ 3. Use `resume_feature_session_brief` per worktree to check divergence and overlap risks.
203
+ 4. Generate a worktree-specific `build_context_pack` and hand it to the target subagent.
40
204
 
41
- ## Environment
205
+ This pattern avoids one giant shared context and scales better as features grow.
42
206
 
43
- - `COMMIT_RAG_REPO` default repository path for MCP
44
- - `COMMIT_RAG_DB` sqlite db path
45
- - `COMMIT_RAG_LIMIT` max commits to index per run
46
- - `OLLAMA_BASE_URL` local ollama URL (default `http://127.0.0.1:11434`)
47
- - `OLLAMA_EMBED_MODEL` local embedding model name
207
+ ## Data model overview
48
208
 
49
- If `OLLAMA_EMBED_MODEL` is not set, the package uses deterministic local fallback embeddings.
209
+ The package stores local context for:
50
210
 
51
- ### Copilot LLM reranking (optional)
211
+ - commits
212
+ - pull requests
213
+ - PR comments
214
+ - PR reviews
215
+ - promoted decision/blocker summaries
216
+ - worktree session checkpoints
52
217
 
53
- Set `COPILOT_TOKEN` to a GitHub token with Copilot access to enable LLM-based reranking.
54
- After initial vector/keyword retrieval, results are sent to Copilot for semantic scoring and re-sorted.
218
+ ## Publishing
55
219
 
56
- - `COPILOT_TOKEN` GitHub PAT or token with Copilot access (enables reranking)
57
- - `COPILOT_MODEL` model slug (default: `gpt-4o-mini`, supports `claude-sonnet-4-5`, `gpt-4o`, etc.)
58
- - `COPILOT_BASE_URL` API base URL (default: `https://api.githubcopilot.com`)
220
+ ```bash
221
+ npm run build
222
+ npm publish --access public
223
+ ```
59
224
 
60
- Reranking works alongside or instead of Ollama no embedding model required.
225
+ For MCP Registry publication, keep `package.json` `mcpName` and `server.json` `name` aligned.
@@ -1,5 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
- import type { CommitChunk, PullRequestCommentRecord, PullRequestDecisionRecord, PullRequestRecord, PullRequestReviewRecord, WorktreeSessionRecord } from "../types.js";
2
+ import type { CommitChunk, ContextFactRecord, ContextPackRecord, PullRequestCommentRecord, PullRequestDecisionRecord, PullRequestRecord, PullRequestReviewRecord, WorktreeSessionRecord } from "../types.js";
3
3
  export type RagDatabase = Database.Database;
4
4
  export declare function openDatabase(dbPath: string): RagDatabase;
5
5
  export declare function hasChunk(db: RagDatabase, chunkId: string): boolean;
@@ -11,3 +11,22 @@ export declare function replacePullRequestReviews(db: RagDatabase, repoOwner: st
11
11
  export declare function replacePullRequestDecisions(db: RagDatabase, repoOwner: string, repoName: string, prNumber: number, decisions: PullRequestDecisionRecord[]): void;
12
12
  export declare function touchPullRequestSyncState(db: RagDatabase, repoOwner: string, repoName: string): void;
13
13
  export declare function upsertWorktreeSession(db: RagDatabase, session: WorktreeSessionRecord): void;
14
+ export declare function upsertContextFact(db: RagDatabase, fact: ContextFactRecord): void;
15
+ export declare function promoteContextFacts(db: RagDatabase, options: {
16
+ domain?: string;
17
+ feature?: string;
18
+ branch?: string;
19
+ sourceType?: string;
20
+ }): number;
21
+ export declare function buildContextPack(db: RagDatabase, options: {
22
+ domain?: string;
23
+ feature?: string;
24
+ branch?: string;
25
+ taskType?: string;
26
+ includeDraft?: boolean;
27
+ limit: number;
28
+ }): ContextPackRecord[];
29
+ export declare function archiveFeatureContext(db: RagDatabase, options: {
30
+ domain: string;
31
+ feature: string;
32
+ }): number;
package/dist/db/client.js CHANGED
@@ -109,6 +109,44 @@ export function openDatabase(dbPath) {
109
109
  base_branch TEXT NOT NULL,
110
110
  last_synced_at TEXT NOT NULL
111
111
  );
112
+
113
+ CREATE TABLE IF NOT EXISTS context_facts (
114
+ id TEXT PRIMARY KEY,
115
+ source_type TEXT NOT NULL,
116
+ source_ref TEXT NOT NULL,
117
+ scope_domain TEXT NOT NULL,
118
+ scope_feature TEXT NOT NULL,
119
+ scope_branch TEXT NOT NULL,
120
+ scope_task_type TEXT NOT NULL,
121
+ title TEXT NOT NULL,
122
+ content TEXT NOT NULL,
123
+ priority REAL NOT NULL,
124
+ confidence REAL NOT NULL,
125
+ status TEXT NOT NULL,
126
+ created_at TEXT NOT NULL,
127
+ updated_at TEXT NOT NULL
128
+ );
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_context_scope
131
+ ON context_facts(scope_domain, scope_feature, scope_branch, scope_task_type, status, updated_at);
132
+
133
+ CREATE TABLE IF NOT EXISTS context_fact_archive (
134
+ id TEXT PRIMARY KEY,
135
+ source_type TEXT NOT NULL,
136
+ source_ref TEXT NOT NULL,
137
+ scope_domain TEXT NOT NULL,
138
+ scope_feature TEXT NOT NULL,
139
+ scope_branch TEXT NOT NULL,
140
+ scope_task_type TEXT NOT NULL,
141
+ title TEXT NOT NULL,
142
+ content TEXT NOT NULL,
143
+ priority REAL NOT NULL,
144
+ confidence REAL NOT NULL,
145
+ status TEXT NOT NULL,
146
+ created_at TEXT NOT NULL,
147
+ updated_at TEXT NOT NULL,
148
+ archived_at TEXT NOT NULL
149
+ );
112
150
  `);
113
151
  return db;
114
152
  }
@@ -270,3 +308,186 @@ export function upsertWorktreeSession(db, session) {
270
308
  last_synced_at = excluded.last_synced_at
271
309
  `).run(session.path, session.branch, session.baseBranch, session.lastSyncedAt);
272
310
  }
311
+ export function upsertContextFact(db, fact) {
312
+ db.prepare(`
313
+ INSERT INTO context_facts (
314
+ id,
315
+ source_type,
316
+ source_ref,
317
+ scope_domain,
318
+ scope_feature,
319
+ scope_branch,
320
+ scope_task_type,
321
+ title,
322
+ content,
323
+ priority,
324
+ confidence,
325
+ status,
326
+ created_at,
327
+ updated_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
329
+ ON CONFLICT(id) DO UPDATE SET
330
+ source_type = excluded.source_type,
331
+ source_ref = excluded.source_ref,
332
+ scope_domain = excluded.scope_domain,
333
+ scope_feature = excluded.scope_feature,
334
+ scope_branch = excluded.scope_branch,
335
+ scope_task_type = excluded.scope_task_type,
336
+ title = excluded.title,
337
+ content = excluded.content,
338
+ priority = excluded.priority,
339
+ confidence = excluded.confidence,
340
+ status = excluded.status,
341
+ created_at = excluded.created_at,
342
+ updated_at = excluded.updated_at
343
+ `).run(fact.id, fact.sourceType, fact.sourceRef, fact.domain, fact.feature, fact.branch, fact.taskType, fact.title, fact.content, fact.priority, fact.confidence, fact.status, fact.createdAt, fact.updatedAt);
344
+ }
345
+ export function promoteContextFacts(db, options) {
346
+ const clauses = ["status = 'draft'"];
347
+ const params = [];
348
+ if (options.domain) {
349
+ clauses.push("scope_domain = ?");
350
+ params.push(options.domain);
351
+ }
352
+ if (options.feature) {
353
+ clauses.push("scope_feature = ?");
354
+ params.push(options.feature);
355
+ }
356
+ if (options.branch) {
357
+ clauses.push("scope_branch = ?");
358
+ params.push(options.branch);
359
+ }
360
+ if (options.sourceType) {
361
+ clauses.push("source_type = ?");
362
+ params.push(options.sourceType);
363
+ }
364
+ const sql = `
365
+ UPDATE context_facts
366
+ SET status = 'promoted', updated_at = ?
367
+ WHERE ${clauses.join(" AND ")}
368
+ `;
369
+ const now = new Date().toISOString();
370
+ const result = db.prepare(sql).run(now, ...params);
371
+ return Number(result.changes ?? 0);
372
+ }
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 = `
400
+ SELECT
401
+ id,
402
+ source_type,
403
+ source_ref,
404
+ title,
405
+ content,
406
+ scope_domain,
407
+ scope_feature,
408
+ scope_branch,
409
+ scope_task_type,
410
+ priority,
411
+ confidence,
412
+ status,
413
+ updated_at,
414
+ ((priority * 0.45) + (confidence * 0.35) +
415
+ CASE
416
+ WHEN scope_task_type = ? THEN 0.20
417
+ WHEN scope_task_type = 'general' THEN 0.10
418
+ ELSE 0.0
419
+ END) AS score
420
+ FROM context_facts
421
+ ${where}
422
+ ORDER BY score DESC, updated_at DESC
423
+ LIMIT ?
424
+ `;
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
+ }));
443
+ }
444
+ export function archiveFeatureContext(db, options) {
445
+ const now = new Date().toISOString();
446
+ const tx = db.transaction(() => {
447
+ db.prepare(`
448
+ INSERT OR REPLACE INTO context_fact_archive (
449
+ id,
450
+ source_type,
451
+ source_ref,
452
+ scope_domain,
453
+ scope_feature,
454
+ scope_branch,
455
+ scope_task_type,
456
+ title,
457
+ content,
458
+ priority,
459
+ confidence,
460
+ status,
461
+ created_at,
462
+ updated_at,
463
+ archived_at
464
+ )
465
+ SELECT
466
+ id,
467
+ source_type,
468
+ source_ref,
469
+ scope_domain,
470
+ scope_feature,
471
+ scope_branch,
472
+ scope_task_type,
473
+ title,
474
+ content,
475
+ priority,
476
+ confidence,
477
+ 'archived',
478
+ created_at,
479
+ updated_at,
480
+ ?
481
+ FROM context_facts
482
+ WHERE scope_domain = ? AND scope_feature = ?
483
+ `).run(now, options.domain, options.feature);
484
+ return db
485
+ .prepare(`
486
+ DELETE FROM context_facts
487
+ WHERE scope_domain = ? AND scope_feature = ?
488
+ `)
489
+ .run(options.domain, options.feature);
490
+ });
491
+ const result = tx();
492
+ return Number(result.changes ?? 0);
493
+ }
@@ -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 { openDatabase, upsertWorktreeSession } from "../db/client.js";
9
+ import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.js";
10
10
  import { commitDetails, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
11
11
  import { listActiveWorktrees } from "../git/worktree.js";
12
12
  import { syncPullRequestContext } from "../pr/sync.js";
@@ -88,11 +88,57 @@ export async function startMcpServer() {
88
88
  type: "array",
89
89
  items: { type: "number" },
90
90
  },
91
+ domain: { type: "string" },
92
+ feature: { type: "string" },
93
+ branch: { type: "string" },
94
+ taskType: { type: "string" },
91
95
  limit: { type: "number" },
92
96
  },
93
97
  required: ["owner", "repo"],
94
98
  },
95
99
  },
100
+ {
101
+ name: "build_context_pack",
102
+ description: "Build a scoped context pack for a task/domain/feature/branch to keep agent prompts small.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ domain: { type: "string" },
107
+ feature: { type: "string" },
108
+ branch: { type: "string" },
109
+ taskType: { type: "string" },
110
+ includeDraft: { type: "boolean" },
111
+ limit: { type: "number" },
112
+ },
113
+ required: [],
114
+ },
115
+ },
116
+ {
117
+ name: "promote_context_facts",
118
+ description: "Promote scoped draft facts into durable promoted context.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ domain: { type: "string" },
123
+ feature: { type: "string" },
124
+ branch: { type: "string" },
125
+ sourceType: { type: "string" },
126
+ },
127
+ required: [],
128
+ },
129
+ },
130
+ {
131
+ name: "archive_feature_context",
132
+ description: "Archive all active facts for a domain/feature once work is complete.",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ domain: { type: "string" },
137
+ feature: { type: "string" },
138
+ },
139
+ required: ["domain", "feature"],
140
+ },
141
+ },
96
142
  {
97
143
  name: "list_active_worktrees",
98
144
  description: "List active git worktrees for multi-session feature work.",
@@ -192,6 +238,10 @@ export async function startMcpServer() {
192
238
  .filter((value) => Number.isFinite(value) && value > 0)
193
239
  : undefined;
194
240
  const limit = Number(request.params.arguments?.limit ?? 25);
241
+ const domain = String(request.params.arguments?.domain ?? "").trim();
242
+ const feature = String(request.params.arguments?.feature ?? "").trim();
243
+ const branch = String(request.params.arguments?.branch ?? "").trim();
244
+ const taskType = String(request.params.arguments?.taskType ?? "").trim();
195
245
  const summary = await syncPullRequestContext({
196
246
  repoPath,
197
247
  dbPath,
@@ -199,11 +249,94 @@ export async function startMcpServer() {
199
249
  repoName: repo,
200
250
  prNumbers,
201
251
  limit,
252
+ domain: domain || undefined,
253
+ feature: feature || undefined,
254
+ branch: branch || undefined,
255
+ taskType: taskType || undefined,
202
256
  });
203
257
  return {
204
258
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
205
259
  };
206
260
  }
261
+ if (request.params.name === "build_context_pack") {
262
+ const limit = Number(request.params.arguments?.limit ?? 20);
263
+ const domain = String(request.params.arguments?.domain ?? "").trim();
264
+ const feature = String(request.params.arguments?.feature ?? "").trim();
265
+ const branch = String(request.params.arguments?.branch ?? "").trim();
266
+ const taskType = String(request.params.arguments?.taskType ?? "").trim() || "general";
267
+ const includeDraft = Boolean(request.params.arguments?.includeDraft);
268
+ const db = openDatabase(dbPath);
269
+ try {
270
+ const pack = buildContextPack(db, {
271
+ domain: domain || undefined,
272
+ feature: feature || undefined,
273
+ branch: branch || undefined,
274
+ taskType,
275
+ includeDraft,
276
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 20,
277
+ });
278
+ return {
279
+ content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
280
+ };
281
+ }
282
+ finally {
283
+ db.close();
284
+ }
285
+ }
286
+ if (request.params.name === "promote_context_facts") {
287
+ const domain = String(request.params.arguments?.domain ?? "").trim();
288
+ const feature = String(request.params.arguments?.feature ?? "").trim();
289
+ const branch = String(request.params.arguments?.branch ?? "").trim();
290
+ const sourceType = String(request.params.arguments?.sourceType ?? "").trim();
291
+ const db = openDatabase(dbPath);
292
+ try {
293
+ const promotedCount = promoteContextFacts(db, {
294
+ domain: domain || undefined,
295
+ feature: feature || undefined,
296
+ branch: branch || undefined,
297
+ sourceType: sourceType || undefined,
298
+ });
299
+ return {
300
+ content: [
301
+ {
302
+ type: "text",
303
+ text: JSON.stringify({ promotedCount }, null, 2),
304
+ },
305
+ ],
306
+ };
307
+ }
308
+ finally {
309
+ db.close();
310
+ }
311
+ }
312
+ if (request.params.name === "archive_feature_context") {
313
+ const domain = String(request.params.arguments?.domain ?? "").trim();
314
+ const feature = String(request.params.arguments?.feature ?? "").trim();
315
+ if (!domain || !feature) {
316
+ return {
317
+ content: [{ type: "text", text: "domain and feature are required" }],
318
+ isError: true,
319
+ };
320
+ }
321
+ const db = openDatabase(dbPath);
322
+ try {
323
+ const archivedCount = archiveFeatureContext(db, {
324
+ domain,
325
+ feature,
326
+ });
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: JSON.stringify({ archivedCount }, null, 2),
332
+ },
333
+ ],
334
+ };
335
+ }
336
+ finally {
337
+ db.close();
338
+ }
339
+ }
207
340
  if (request.params.name === "list_active_worktrees") {
208
341
  const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
209
342
  const worktrees = listActiveWorktrees(repoPath);
package/dist/pr/sync.d.ts CHANGED
@@ -6,4 +6,8 @@ export declare function syncPullRequestContext(options: {
6
6
  repoName: string;
7
7
  prNumbers?: number[];
8
8
  limit?: number;
9
+ domain?: string;
10
+ feature?: string;
11
+ branch?: string;
12
+ taskType?: string;
9
13
  }): Promise<PullRequestSyncSummary>;
package/dist/pr/sync.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import path from "node:path";
3
- import { openDatabase, replacePullRequestComments, replacePullRequestDecisions, replacePullRequestReviews, touchPullRequestSyncState, upsertPullRequest, } from "../db/client.js";
3
+ import { openDatabase, replacePullRequestComments, replacePullRequestDecisions, replacePullRequestReviews, touchPullRequestSyncState, upsertContextFact, upsertPullRequest, } from "../db/client.js";
4
4
  function runGh(repoPath, args) {
5
5
  return execFileSync("gh", args, {
6
6
  cwd: repoPath,
@@ -180,6 +180,45 @@ export async function syncPullRequestContext(options) {
180
180
  replacePullRequestComments(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.comments);
181
181
  replacePullRequestReviews(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.reviews);
182
182
  replacePullRequestDecisions(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.decisions);
183
+ const scopeDomain = (options.domain ?? options.repoName).trim();
184
+ const scopeFeature = (options.feature ?? `pr-${parsed.pr.number}`).trim() ||
185
+ `pr-${parsed.pr.number}`;
186
+ const scopeBranch = (options.branch ?? "main").trim() || "main";
187
+ const taskType = (options.taskType ?? "planning").trim() || "planning";
188
+ upsertContextFact(db, {
189
+ id: `pr:${options.repoOwner}/${options.repoName}#${parsed.pr.number}:description`,
190
+ sourceType: "pr_description",
191
+ sourceRef: `${options.repoOwner}/${options.repoName}#${parsed.pr.number}`,
192
+ domain: scopeDomain,
193
+ feature: scopeFeature,
194
+ branch: scopeBranch,
195
+ taskType,
196
+ title: parsed.pr.title,
197
+ content: parsed.pr.body,
198
+ priority: 0.85,
199
+ confidence: 0.9,
200
+ status: "promoted",
201
+ createdAt: parsed.pr.createdAt,
202
+ updatedAt: parsed.pr.updatedAt,
203
+ });
204
+ for (const decision of parsed.decisions) {
205
+ upsertContextFact(db, {
206
+ id: `decision:${options.repoOwner}/${options.repoName}#${parsed.pr.number}:${decision.id}`,
207
+ sourceType: `pr_${decision.source}`,
208
+ sourceRef: `${options.repoOwner}/${options.repoName}#${parsed.pr.number}`,
209
+ domain: scopeDomain,
210
+ feature: scopeFeature,
211
+ branch: scopeBranch,
212
+ taskType,
213
+ title: `Decision ${parsed.pr.number} (${decision.source})`,
214
+ content: decision.summary,
215
+ priority: decision.severity === "blocker" ? 1 : 0.75,
216
+ confidence: 0.8,
217
+ status: decision.source === "description" ? "promoted" : "draft",
218
+ createdAt: decision.createdAt,
219
+ updatedAt: decision.createdAt,
220
+ });
221
+ }
183
222
  syncedPrs += 1;
184
223
  syncedComments += parsed.comments.length;
185
224
  syncedReviews += parsed.reviews.length;
package/dist/types.d.ts CHANGED
@@ -84,3 +84,36 @@ export type WorktreeSessionRecord = {
84
84
  lastSyncedAt: string;
85
85
  baseBranch: string;
86
86
  };
87
+ export type ContextFactStatus = "draft" | "promoted" | "archived";
88
+ export type ContextFactRecord = {
89
+ id: string;
90
+ sourceType: string;
91
+ sourceRef: string;
92
+ domain: string;
93
+ feature: string;
94
+ branch: string;
95
+ taskType: string;
96
+ title: string;
97
+ content: string;
98
+ priority: number;
99
+ confidence: number;
100
+ status: ContextFactStatus;
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ };
104
+ export type ContextPackRecord = {
105
+ id: string;
106
+ sourceType: string;
107
+ sourceRef: string;
108
+ title: string;
109
+ content: string;
110
+ domain: string;
111
+ feature: string;
112
+ branch: string;
113
+ taskType: string;
114
+ priority: number;
115
+ confidence: number;
116
+ score: number;
117
+ status: ContextFactStatus;
118
+ updatedAt: string;
119
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jussmor/commit-memory-mcp",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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",