@jussmor/commit-memory-mcp 0.3.6 → 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 +211 -29
- package/dist/db/client.d.ts +20 -1
- package/dist/db/client.js +246 -0
- package/dist/git/insights.d.ts +21 -0
- package/dist/git/insights.js +60 -0
- package/dist/mcp/server.js +267 -2
- package/dist/pr/sync.d.ts +4 -0
- package/dist/pr/sync.js +40 -1
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,60 +1,242 @@
|
|
|
1
1
|
# @jussmor/commit-memory-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A local MCP server for PR intelligence, author tracing, and worktree-aware async planning.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Purpose
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
})
|
|
33
83
|
```
|
|
34
84
|
|
|
35
|
-
|
|
85
|
+
Use this before planning when you need fresh PR descriptions, comments, and reviews.
|
|
36
86
|
|
|
37
|
-
|
|
87
|
+
### List active worktrees
|
|
38
88
|
|
|
39
|
-
|
|
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.
|
|
40
96
|
|
|
41
|
-
|
|
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
|
+
```
|
|
42
109
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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.
|
|
48
112
|
|
|
49
|
-
|
|
113
|
+
Important: if you provide `domain`/`feature`/`branch` tags in `build_context_pack`, use the same tags during `sync_pr_context` for best precision.
|
|
114
|
+
|
|
115
|
+
### Promote draft facts after review
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
promote_context_facts({
|
|
119
|
+
domain: "billing",
|
|
120
|
+
feature: "invoice-retry",
|
|
121
|
+
branch: "feat/invoice-retry"
|
|
122
|
+
})
|
|
123
|
+
```
|
|
50
124
|
|
|
51
|
-
|
|
125
|
+
Use this when discussion outcomes are approved and should become durable context.
|
|
52
126
|
|
|
53
|
-
|
|
54
|
-
After initial vector/keyword retrieval, results are sent to Copilot for semantic scoring and re-sorted.
|
|
127
|
+
### Archive completed feature context
|
|
55
128
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
129
|
+
```text
|
|
130
|
+
archive_feature_context({
|
|
131
|
+
domain: "billing",
|
|
132
|
+
feature: "invoice-retry"
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use this after merge/closure to prevent active context bloat.
|
|
137
|
+
|
|
138
|
+
### Find ownership for a file
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
who_changed_this({
|
|
142
|
+
filePath: "src/features/auth/session.ts",
|
|
143
|
+
limit: 15
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use this to discover recent authors and commit history for a target file.
|
|
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
|
+
|
|
163
|
+
### Explain intent for a change
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
why_was_this_changed({
|
|
167
|
+
filePath: "src/features/auth/session.ts",
|
|
168
|
+
owner: "MaxwellClinic-Development",
|
|
169
|
+
repo: "EverBetter-Pro"
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use this to combine commit metadata with synced PR context.
|
|
174
|
+
|
|
175
|
+
### Get an overnight main-branch brief
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
get_main_branch_overnight_brief({
|
|
179
|
+
baseBranch: "main",
|
|
180
|
+
sinceHours: 12,
|
|
181
|
+
limit: 25
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Use this at the start of the day to review what landed while you were offline.
|
|
186
|
+
|
|
187
|
+
### Resume a feature worktree
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
resume_feature_session_brief({
|
|
191
|
+
worktreePath: "/path/to/worktree",
|
|
192
|
+
baseBranch: "main"
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Use this before resuming unfinished work in a separate worktree.
|
|
197
|
+
|
|
198
|
+
### Run the full pre-plan sync flow
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
pre_plan_sync_brief({
|
|
202
|
+
owner: "MaxwellClinic-Development",
|
|
203
|
+
repo: "EverBetter-Pro",
|
|
204
|
+
baseBranch: "main",
|
|
205
|
+
worktreePath: "/path/to/worktree",
|
|
206
|
+
sinceHours: 12,
|
|
207
|
+
limit: 25
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Use this as the default entrypoint for async team planning.
|
|
212
|
+
|
|
213
|
+
## Multi-session git worktree flow
|
|
214
|
+
|
|
215
|
+
For parallel AI coding sessions:
|
|
216
|
+
|
|
217
|
+
1. Create one git worktree per feature branch.
|
|
218
|
+
2. Use `list_active_worktrees` to enumerate active sessions.
|
|
219
|
+
3. Use `resume_feature_session_brief` per worktree to check divergence and overlap risks.
|
|
220
|
+
4. Generate a worktree-specific `build_context_pack` and hand it to the target subagent.
|
|
221
|
+
|
|
222
|
+
This pattern avoids one giant shared context and scales better as features grow.
|
|
223
|
+
|
|
224
|
+
## Data model overview
|
|
225
|
+
|
|
226
|
+
The package stores local context for:
|
|
227
|
+
|
|
228
|
+
- commits
|
|
229
|
+
- pull requests
|
|
230
|
+
- PR comments
|
|
231
|
+
- PR reviews
|
|
232
|
+
- promoted decision/blocker summaries
|
|
233
|
+
- worktree session checkpoints
|
|
234
|
+
|
|
235
|
+
## Publishing
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run build
|
|
239
|
+
npm publish --access public
|
|
240
|
+
```
|
|
59
241
|
|
|
60
|
-
|
|
242
|
+
For MCP Registry publication, keep `package.json` `mcpName` and `server.json` `name` aligned.
|
package/dist/db/client.d.ts
CHANGED
|
@@ -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,211 @@ 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 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 = `
|
|
398
|
+
SELECT
|
|
399
|
+
id,
|
|
400
|
+
source_type,
|
|
401
|
+
source_ref,
|
|
402
|
+
title,
|
|
403
|
+
content,
|
|
404
|
+
scope_domain,
|
|
405
|
+
scope_feature,
|
|
406
|
+
scope_branch,
|
|
407
|
+
scope_task_type,
|
|
408
|
+
priority,
|
|
409
|
+
confidence,
|
|
410
|
+
status,
|
|
411
|
+
updated_at,
|
|
412
|
+
((priority * 0.40) + (confidence * 0.30) +
|
|
413
|
+
CASE
|
|
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
|
|
421
|
+
ELSE 0.0
|
|
422
|
+
END) AS score
|
|
423
|
+
FROM context_facts
|
|
424
|
+
${where}
|
|
425
|
+
ORDER BY score DESC, updated_at DESC
|
|
426
|
+
LIMIT ?
|
|
427
|
+
`;
|
|
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
|
+
});
|
|
468
|
+
}
|
|
469
|
+
export function archiveFeatureContext(db, options) {
|
|
470
|
+
const now = new Date().toISOString();
|
|
471
|
+
const tx = db.transaction(() => {
|
|
472
|
+
db.prepare(`
|
|
473
|
+
INSERT OR REPLACE INTO context_fact_archive (
|
|
474
|
+
id,
|
|
475
|
+
source_type,
|
|
476
|
+
source_ref,
|
|
477
|
+
scope_domain,
|
|
478
|
+
scope_feature,
|
|
479
|
+
scope_branch,
|
|
480
|
+
scope_task_type,
|
|
481
|
+
title,
|
|
482
|
+
content,
|
|
483
|
+
priority,
|
|
484
|
+
confidence,
|
|
485
|
+
status,
|
|
486
|
+
created_at,
|
|
487
|
+
updated_at,
|
|
488
|
+
archived_at
|
|
489
|
+
)
|
|
490
|
+
SELECT
|
|
491
|
+
id,
|
|
492
|
+
source_type,
|
|
493
|
+
source_ref,
|
|
494
|
+
scope_domain,
|
|
495
|
+
scope_feature,
|
|
496
|
+
scope_branch,
|
|
497
|
+
scope_task_type,
|
|
498
|
+
title,
|
|
499
|
+
content,
|
|
500
|
+
priority,
|
|
501
|
+
confidence,
|
|
502
|
+
'archived',
|
|
503
|
+
created_at,
|
|
504
|
+
updated_at,
|
|
505
|
+
?
|
|
506
|
+
FROM context_facts
|
|
507
|
+
WHERE scope_domain = ? AND scope_feature = ?
|
|
508
|
+
`).run(now, options.domain, options.feature);
|
|
509
|
+
return db
|
|
510
|
+
.prepare(`
|
|
511
|
+
DELETE FROM context_facts
|
|
512
|
+
WHERE scope_domain = ? AND scope_feature = ?
|
|
513
|
+
`)
|
|
514
|
+
.run(options.domain, options.feature);
|
|
515
|
+
});
|
|
516
|
+
const result = tx();
|
|
517
|
+
return Number(result.changes ?? 0);
|
|
518
|
+
}
|
package/dist/git/insights.d.ts
CHANGED
|
@@ -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;
|
package/dist/git/insights.js
CHANGED
|
@@ -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",
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,8 +6,8 @@ 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";
|
|
10
|
-
import { commitDetails, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
9
|
+
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.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
|
|
@@ -88,11 +160,57 @@ export async function startMcpServer() {
|
|
|
88
160
|
type: "array",
|
|
89
161
|
items: { type: "number" },
|
|
90
162
|
},
|
|
163
|
+
domain: { type: "string" },
|
|
164
|
+
feature: { type: "string" },
|
|
165
|
+
branch: { type: "string" },
|
|
166
|
+
taskType: { type: "string" },
|
|
91
167
|
limit: { type: "number" },
|
|
92
168
|
},
|
|
93
169
|
required: ["owner", "repo"],
|
|
94
170
|
},
|
|
95
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: "build_context_pack",
|
|
174
|
+
description: "Build a scoped context pack for a task/domain/feature/branch to keep agent prompts small.",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
domain: { type: "string" },
|
|
179
|
+
feature: { type: "string" },
|
|
180
|
+
branch: { type: "string" },
|
|
181
|
+
taskType: { type: "string" },
|
|
182
|
+
includeDraft: { type: "boolean" },
|
|
183
|
+
limit: { type: "number" },
|
|
184
|
+
},
|
|
185
|
+
required: [],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "promote_context_facts",
|
|
190
|
+
description: "Promote scoped draft facts into durable promoted context.",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
domain: { type: "string" },
|
|
195
|
+
feature: { type: "string" },
|
|
196
|
+
branch: { type: "string" },
|
|
197
|
+
sourceType: { type: "string" },
|
|
198
|
+
},
|
|
199
|
+
required: [],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "archive_feature_context",
|
|
204
|
+
description: "Archive all active facts for a domain/feature once work is complete.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
domain: { type: "string" },
|
|
209
|
+
feature: { type: "string" },
|
|
210
|
+
},
|
|
211
|
+
required: ["domain", "feature"],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
96
214
|
{
|
|
97
215
|
name: "list_active_worktrees",
|
|
98
216
|
description: "List active git worktrees for multi-session feature work.",
|
|
@@ -116,6 +234,20 @@ export async function startMcpServer() {
|
|
|
116
234
|
required: ["filePath"],
|
|
117
235
|
},
|
|
118
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
|
+
},
|
|
119
251
|
{
|
|
120
252
|
name: "why_was_this_changed",
|
|
121
253
|
description: "Explain intent for a commit or file using git history and synced PR decisions.",
|
|
@@ -192,6 +324,10 @@ export async function startMcpServer() {
|
|
|
192
324
|
.filter((value) => Number.isFinite(value) && value > 0)
|
|
193
325
|
: undefined;
|
|
194
326
|
const limit = Number(request.params.arguments?.limit ?? 25);
|
|
327
|
+
const domain = String(request.params.arguments?.domain ?? "").trim();
|
|
328
|
+
const feature = String(request.params.arguments?.feature ?? "").trim();
|
|
329
|
+
const branch = String(request.params.arguments?.branch ?? "").trim();
|
|
330
|
+
const taskType = String(request.params.arguments?.taskType ?? "").trim();
|
|
195
331
|
const summary = await syncPullRequestContext({
|
|
196
332
|
repoPath,
|
|
197
333
|
dbPath,
|
|
@@ -199,11 +335,94 @@ export async function startMcpServer() {
|
|
|
199
335
|
repoName: repo,
|
|
200
336
|
prNumbers,
|
|
201
337
|
limit,
|
|
338
|
+
domain: domain || undefined,
|
|
339
|
+
feature: feature || undefined,
|
|
340
|
+
branch: branch || undefined,
|
|
341
|
+
taskType: taskType || undefined,
|
|
202
342
|
});
|
|
203
343
|
return {
|
|
204
344
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
205
345
|
};
|
|
206
346
|
}
|
|
347
|
+
if (request.params.name === "build_context_pack") {
|
|
348
|
+
const limit = Number(request.params.arguments?.limit ?? 20);
|
|
349
|
+
const domain = String(request.params.arguments?.domain ?? "").trim();
|
|
350
|
+
const feature = String(request.params.arguments?.feature ?? "").trim();
|
|
351
|
+
const branch = String(request.params.arguments?.branch ?? "").trim();
|
|
352
|
+
const taskType = String(request.params.arguments?.taskType ?? "").trim() || "general";
|
|
353
|
+
const includeDraft = Boolean(request.params.arguments?.includeDraft);
|
|
354
|
+
const db = openDatabase(dbPath);
|
|
355
|
+
try {
|
|
356
|
+
const pack = buildContextPack(db, {
|
|
357
|
+
domain: domain || undefined,
|
|
358
|
+
feature: feature || undefined,
|
|
359
|
+
branch: branch || undefined,
|
|
360
|
+
taskType,
|
|
361
|
+
includeDraft,
|
|
362
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 20,
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
db.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (request.params.name === "promote_context_facts") {
|
|
373
|
+
const domain = String(request.params.arguments?.domain ?? "").trim();
|
|
374
|
+
const feature = String(request.params.arguments?.feature ?? "").trim();
|
|
375
|
+
const branch = String(request.params.arguments?.branch ?? "").trim();
|
|
376
|
+
const sourceType = String(request.params.arguments?.sourceType ?? "").trim();
|
|
377
|
+
const db = openDatabase(dbPath);
|
|
378
|
+
try {
|
|
379
|
+
const promotedCount = promoteContextFacts(db, {
|
|
380
|
+
domain: domain || undefined,
|
|
381
|
+
feature: feature || undefined,
|
|
382
|
+
branch: branch || undefined,
|
|
383
|
+
sourceType: sourceType || undefined,
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
content: [
|
|
387
|
+
{
|
|
388
|
+
type: "text",
|
|
389
|
+
text: JSON.stringify({ promotedCount }, null, 2),
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
db.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (request.params.name === "archive_feature_context") {
|
|
399
|
+
const domain = String(request.params.arguments?.domain ?? "").trim();
|
|
400
|
+
const feature = String(request.params.arguments?.feature ?? "").trim();
|
|
401
|
+
if (!domain || !feature) {
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "text", text: "domain and feature are required" }],
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const db = openDatabase(dbPath);
|
|
408
|
+
try {
|
|
409
|
+
const archivedCount = archiveFeatureContext(db, {
|
|
410
|
+
domain,
|
|
411
|
+
feature,
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: JSON.stringify({ archivedCount }, null, 2),
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
db.close();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
207
426
|
if (request.params.name === "list_active_worktrees") {
|
|
208
427
|
const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
|
|
209
428
|
const worktrees = listActiveWorktrees(repoPath);
|
|
@@ -243,6 +462,52 @@ export async function startMcpServer() {
|
|
|
243
462
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
244
463
|
};
|
|
245
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
|
+
}
|
|
246
511
|
if (request.params.name === "why_was_this_changed") {
|
|
247
512
|
const owner = String(request.params.arguments?.owner ?? "").trim();
|
|
248
513
|
const repo = String(request.params.arguments?.repo ?? "").trim();
|
package/dist/pr/sync.d.ts
CHANGED
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