context-mcp-server 1.0.8 → 1.1.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.
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/query.py +43 -0
- package/codegraph/server.py +20 -17
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.js +94 -183
- package/src/db.js +79 -102
- package/src/search.js +73 -9
- package/src/templates/AGENTS.md +56 -46
- package/src/templates/CLAUDE.md +89 -55
- package/src/templates/GEMINI.md +56 -46
- package/src/templates/commands/save-context.md +6 -3
- package/src/templates/skills/SKILL.md +83 -50
- package/src/templates/windsurf-rules.md +69 -20
- package/src/tools/codegraph.js +46 -43
- package/src/tools/context.js +42 -24
- package/src/tools/plan.js +14 -11
- package/uv.lock +1 -1
- package/src/migrator.js +0 -124
package/src/tools/codegraph.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { saveGraph, saveContext, updateContext, getContext, flushToDisk } from '../db.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const REPO_ROOT = join(__dirname, '..', '..');
|
|
@@ -82,16 +83,18 @@ export const definitions = [
|
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
85
|
{
|
|
85
|
-
name: '
|
|
86
|
-
description:
|
|
86
|
+
name: 'codegraph_arch',
|
|
87
|
+
description:
|
|
88
|
+
'Return a module map of the project — every file with its exported functions/classes and its imports. ' +
|
|
89
|
+
'Use this to understand project structure without reading any files. ' +
|
|
90
|
+
'Call after codegraph_build. Much faster than reading each file individually.',
|
|
87
91
|
inputSchema: {
|
|
88
92
|
type: 'object',
|
|
89
93
|
properties: {
|
|
90
|
-
path:
|
|
91
|
-
|
|
92
|
-
to: { type: 'string' },
|
|
94
|
+
path: { type: 'string', description: 'Project root' },
|
|
95
|
+
limit: { type: 'integer', description: 'Max files in output (default 100)' },
|
|
93
96
|
},
|
|
94
|
-
required: ['path'
|
|
97
|
+
required: ['path'],
|
|
95
98
|
},
|
|
96
99
|
},
|
|
97
100
|
];
|
|
@@ -103,46 +106,46 @@ export function handle(name, args, state) {
|
|
|
103
106
|
|
|
104
107
|
// Persist graph metadata + save/update a context entry as a visible build record
|
|
105
108
|
if (name === 'codegraph_build' && result.success) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
saveGraph({
|
|
110
|
+
path: args.path,
|
|
111
|
+
nodes: result.nodes,
|
|
112
|
+
edges: result.edges,
|
|
113
|
+
communities: result.communities,
|
|
114
|
+
cached: result.cached,
|
|
115
|
+
changed: result.changed,
|
|
116
|
+
time_ms: result.time_ms,
|
|
117
|
+
summary: result.summary || '',
|
|
118
|
+
});
|
|
119
|
+
flushToDisk(); // write graph.json to disk immediately so ctx list sees it
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const inferredProject = args.path
|
|
122
|
+
? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
|
|
123
|
+
: null;
|
|
124
|
+
const project = state?.sessionProject || inferredProject || null;
|
|
125
|
+
const title = `ContextGraph built — ${args.path}`;
|
|
126
|
+
const content = [
|
|
127
|
+
`nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
|
|
128
|
+
`cached: ${result.cached} | changed: ${result.changed} | time: ${result.time_ms}ms`,
|
|
129
|
+
result.summary || '',
|
|
130
|
+
].filter(Boolean).join('\n');
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
// Search all projects — same path always produces same title regardless of session
|
|
133
|
+
const existing = getContext({ tags: ['codegraph'], limit: 100 })
|
|
134
|
+
.find(e => e.title === title);
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}).catch(() => {});
|
|
136
|
+
if (existing) {
|
|
137
|
+
updateContext({ id: existing.id, content, status: 'active' });
|
|
138
|
+
} else {
|
|
139
|
+
saveContext({
|
|
140
|
+
project,
|
|
141
|
+
sessionId: state?.sessionId || null,
|
|
142
|
+
title,
|
|
143
|
+
content,
|
|
144
|
+
type: 'note',
|
|
145
|
+
source: 'auto',
|
|
146
|
+
tags: ['codegraph', 'graph-build'],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
return result;
|
package/src/tools/context.js
CHANGED
|
@@ -25,22 +25,24 @@ function autoDigest(entries, project) {
|
|
|
25
25
|
export const definition = {
|
|
26
26
|
name: 'context',
|
|
27
27
|
description:
|
|
28
|
-
`Factual memory —
|
|
29
|
-
`• "resume" —
|
|
30
|
-
`• "save" —
|
|
31
|
-
`• "get" —
|
|
32
|
-
`• "update" —
|
|
33
|
-
`• "delete" —
|
|
34
|
-
`• "list_projects"—
|
|
28
|
+
`Factual memory — decisions, bugs, notes, discoveries.\n` +
|
|
29
|
+
`• "resume" — call first every session. Returns recent entries, active plans, graph status.\n` +
|
|
30
|
+
`• "save" — store an entry. Auto-deduplicates by content similarity.\n` +
|
|
31
|
+
`• "get" — fetch by id/ids, or filter by project/tags/limit.\n` +
|
|
32
|
+
`• "update" — edit an entry by id.\n` +
|
|
33
|
+
`• "delete" — remove one entry (id) or several (ids: [...]).\n` +
|
|
34
|
+
`• "list_projects"— list all projects and entry counts.`,
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
properties: {
|
|
38
38
|
action: { type: 'string', enum: ['resume', 'save', 'get', 'update', 'delete', 'list_projects'] },
|
|
39
39
|
content: { type: 'string' },
|
|
40
|
-
title: { type: 'string' },
|
|
40
|
+
title: { type: 'string', description: 'Up to 120 chars' },
|
|
41
|
+
why: { type: 'string', description: 'Why it mattered' },
|
|
42
|
+
outcome: { type: 'string', description: 'What the result was' },
|
|
41
43
|
project: { type: 'string' },
|
|
42
44
|
rootPath: { type: 'string', description: 'Absolute path to the project root directory. Stored on first call and used to sandbox file/git tool access.' },
|
|
43
|
-
type: { type: 'string', enum: ['
|
|
45
|
+
type: { type: 'string', enum: ['note', 'compaction'] },
|
|
44
46
|
status: { type: 'string', enum: ['active', 'archived'] },
|
|
45
47
|
tags: { type: 'array', items: { type: 'string' } },
|
|
46
48
|
source: { type: 'string', enum: ['user', 'ai-summary', 'file', 'web', 'cli', 'auto'] },
|
|
@@ -49,8 +51,8 @@ export const definition = {
|
|
|
49
51
|
expiresAt: { type: 'string' },
|
|
50
52
|
limit: { type: 'number' },
|
|
51
53
|
includeArchived: { type: 'boolean' },
|
|
52
|
-
id: { type: 'string', description: 'Single entry ID
|
|
53
|
-
ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs
|
|
54
|
+
id: { type: 'string', description: 'Single entry ID' },
|
|
55
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs' },
|
|
54
56
|
},
|
|
55
57
|
required: ['action'],
|
|
56
58
|
},
|
|
@@ -78,18 +80,27 @@ export async function handle(args, state) {
|
|
|
78
80
|
const proj = args.project || null;
|
|
79
81
|
archiveExpired(proj);
|
|
80
82
|
|
|
81
|
-
// Set project on state so autoLink works for subsequent saves
|
|
82
83
|
if (proj) state.sessionProject = proj;
|
|
83
84
|
|
|
84
|
-
// Store rootPath with project (first time only) and load it onto session state.
|
|
85
|
-
// Auto-detect from git if neither provided nor previously stored.
|
|
86
85
|
const storedRoot = proj ? getProjectRoot(proj) : null;
|
|
87
86
|
const resolvedRoot = args.rootPath || storedRoot || detectGitRoot() || null;
|
|
88
87
|
if (proj) ensureProject(proj, resolvedRoot || undefined);
|
|
89
88
|
state.projectRootPath = resolvedRoot;
|
|
90
89
|
|
|
91
|
-
const
|
|
90
|
+
const rawEntries = getContext({ project: proj, limit: 15, compact: false })
|
|
92
91
|
.filter(e => e.status !== 'archived');
|
|
92
|
+
// Newest 5 get full content; older entries get a lightweight preview
|
|
93
|
+
const entries = rawEntries.map((e, i) => {
|
|
94
|
+
if (i < 5) return e;
|
|
95
|
+
return {
|
|
96
|
+
id: e.id, project: e.project, title: e.title, type: e.type,
|
|
97
|
+
status: e.status, tags: e.tags, source: e.source,
|
|
98
|
+
createdAt: e.createdAt, updatedAt: e.updatedAt,
|
|
99
|
+
...(e.why ? { why: e.why } : {}),
|
|
100
|
+
...(e.outcome ? { outcome: e.outcome } : {}),
|
|
101
|
+
preview: (e.content || '').slice(0, 200),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
93
104
|
const discussions = listDiscussions({ project: proj, status: 'active' });
|
|
94
105
|
const allGraphs = listGraphs();
|
|
95
106
|
const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
|
|
@@ -103,7 +114,6 @@ export async function handle(args, state) {
|
|
|
103
114
|
: null;
|
|
104
115
|
const totalEntries = countContext(proj);
|
|
105
116
|
|
|
106
|
-
// Auto-restore single active discussion
|
|
107
117
|
if (discussions.length === 1) state.discussionId = discussions[0].id;
|
|
108
118
|
|
|
109
119
|
const digest = totalEntries > 10
|
|
@@ -127,7 +137,7 @@ export async function handle(args, state) {
|
|
|
127
137
|
? `All file and git operations are sandboxed to: ${state.projectRootPath} — do not use paths outside this root.`
|
|
128
138
|
: 'No project root configured — pass rootPath to restrict file/git access to a directory.',
|
|
129
139
|
hint: graphStatus.built
|
|
130
|
-
? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_query for
|
|
140
|
+
? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_arch for module map, codegraph_query for specific symbol lookups.`
|
|
131
141
|
: 'No graph built yet. Call codegraph_build on the project root to enable graph queries.',
|
|
132
142
|
};
|
|
133
143
|
}
|
|
@@ -135,7 +145,6 @@ export async function handle(args, state) {
|
|
|
135
145
|
case 'save': {
|
|
136
146
|
if (!args.content) throw new Error('content is required for save');
|
|
137
147
|
if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
|
|
138
|
-
// Auto-detect and store project root if not yet configured
|
|
139
148
|
if (args.project) {
|
|
140
149
|
const existing = getProjectRoot(args.project);
|
|
141
150
|
if (!existing) {
|
|
@@ -146,7 +155,6 @@ export async function handle(args, state) {
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
|
-
// Validate file paths in files[] and codeRefs[] stay within project root
|
|
150
158
|
if (state.projectRootPath) {
|
|
151
159
|
if (Array.isArray(args.files)) {
|
|
152
160
|
args.files.forEach(f => { if (f.path) guardPath(f.path, state.projectRootPath); });
|
|
@@ -162,6 +170,8 @@ export async function handle(args, state) {
|
|
|
162
170
|
id: dupe.id, content: args.content,
|
|
163
171
|
title: args.title || dupe.title, tags: args.tags || dupe.tags,
|
|
164
172
|
type: args.type || dupe.type, status: args.status || dupe.status,
|
|
173
|
+
why: args.why !== undefined ? args.why : dupe.why,
|
|
174
|
+
outcome: args.outcome !== undefined ? args.outcome : dupe.outcome,
|
|
165
175
|
expiresAt: args.expiresAt !== undefined ? args.expiresAt : dupe.expiresAt,
|
|
166
176
|
files: args.files || dupe.files, codeRefs: args.codeRefs || dupe.codeRefs,
|
|
167
177
|
});
|
|
@@ -172,13 +182,21 @@ export async function handle(args, state) {
|
|
|
172
182
|
const entry = saveContext({ ...args, rootPath: state.projectRootPath || undefined });
|
|
173
183
|
fireAutoLink(entry.id, state);
|
|
174
184
|
|
|
175
|
-
// Auto-compact when too many entries accumulate
|
|
185
|
+
// Auto-compact when too many entries accumulate.
|
|
186
|
+
// If the AI just saved a compaction entry, use that content as the summary
|
|
187
|
+
// instead of running TF-IDF on top of it.
|
|
176
188
|
let compaction = null;
|
|
177
189
|
if (shouldCompact(entry.project)) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
if (entry.type === 'compaction') {
|
|
191
|
+
// AI wrote a proper summary — compact old entries without creating a duplicate summary
|
|
192
|
+
compaction = compactProject(entry.project, entry.content, { skipSummaryEntry: true });
|
|
193
|
+
} else {
|
|
194
|
+
// AI didn't write a summary — fall back to TF-IDF extractive summarization
|
|
195
|
+
const old = getContext({ project: entry.project, limit: 500 });
|
|
196
|
+
const { summarizeEntries: summarize } = await import('../summarizer.js');
|
|
197
|
+
const summaryContent = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
|
|
198
|
+
compaction = compactProject(entry.project, summaryContent);
|
|
199
|
+
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
return { success: true, id: entry.id, deduplicated: false,
|
package/src/tools/plan.js
CHANGED
|
@@ -19,13 +19,12 @@ function writePlanFile(planDir, name, content, title) {
|
|
|
19
19
|
export const definition = {
|
|
20
20
|
name: 'plan',
|
|
21
21
|
description:
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
`• "
|
|
25
|
-
`• "
|
|
26
|
-
`• "
|
|
27
|
-
`• "
|
|
28
|
-
`• "delete" — Remove a plan by name or id.`,
|
|
22
|
+
`Plan storage — saves to project store, optionally writes a .md file to planDir.\n` +
|
|
23
|
+
`• "save" — store a new plan or overwrite by name.\n` +
|
|
24
|
+
`• "update" — patch title/content/status.\n` +
|
|
25
|
+
`• "get" — retrieve a plan by name or id.\n` +
|
|
26
|
+
`• "list" — list all plans for the project.\n` +
|
|
27
|
+
`• "delete" — remove a plan by name or id.`,
|
|
29
28
|
inputSchema: {
|
|
30
29
|
type: 'object',
|
|
31
30
|
properties: {
|
|
@@ -33,7 +32,7 @@ export const definition = {
|
|
|
33
32
|
name: { type: 'string', description: 'Short slug-style identifier for the plan, e.g. "auth-refactor"' },
|
|
34
33
|
id: { type: 'string' },
|
|
35
34
|
project: { type: 'string' },
|
|
36
|
-
title: { type: 'string', description: '
|
|
35
|
+
title: { type: 'string', description: 'Plan title' },
|
|
37
36
|
content: { type: 'string', description: 'Full plan summary in markdown' },
|
|
38
37
|
status: { type: 'string', enum: ['active', 'done'] },
|
|
39
38
|
tags: { type: 'array', items: { type: 'string' } },
|
|
@@ -83,6 +82,11 @@ export async function handle(args, state) {
|
|
|
83
82
|
|
|
84
83
|
case 'update': {
|
|
85
84
|
if (!args.name && !args.id) throw new Error('name or id is required for update');
|
|
85
|
+
if (args.status === 'done') {
|
|
86
|
+
const result = deleteDiscussion({ name: args.name, id: args.id });
|
|
87
|
+
state.discussionId = null;
|
|
88
|
+
return { success: true, message: `Plan "${args.name || args.id}" completed and removed.` };
|
|
89
|
+
}
|
|
86
90
|
const updated = updateDiscussion({
|
|
87
91
|
name: args.name,
|
|
88
92
|
id: args.id,
|
|
@@ -92,13 +96,12 @@ export async function handle(args, state) {
|
|
|
92
96
|
tags: args.tags,
|
|
93
97
|
});
|
|
94
98
|
if (!updated) throw new Error(`No plan found for "${args.name || args.id}".`);
|
|
95
|
-
if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
|
|
96
99
|
if (updated.status === 'active') state.discussionId = updated.id;
|
|
97
100
|
const filePath = writePlanFile(args.planDir, updated.name, updated.content, updated.title);
|
|
98
101
|
return {
|
|
99
|
-
success: true, id: updated.id, name: updated.name,
|
|
102
|
+
success: true, id: updated.id, name: updated.name,
|
|
100
103
|
filePath: filePath || undefined,
|
|
101
|
-
message: `Plan "${updated.name}" updated
|
|
104
|
+
message: `Plan "${updated.name}" updated.`,
|
|
102
105
|
};
|
|
103
106
|
}
|
|
104
107
|
|
package/uv.lock
CHANGED
package/src/migrator.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* migrator.js — one-time migration from flat JSON files to per-project directory structure.
|
|
3
|
-
*
|
|
4
|
-
* Old layout:
|
|
5
|
-
* ~/.context-mcp/contexts.json all entries flat
|
|
6
|
-
* ~/.context-mcp/discussions.json all discussions flat
|
|
7
|
-
* ~/.context-mcp/graphs.json all graph build records flat
|
|
8
|
-
*
|
|
9
|
-
* New layout:
|
|
10
|
-
* ~/.context-mcp/projects/<slug>/context.json
|
|
11
|
-
* ~/.context-mcp/projects/<slug>/graph.json { build, entries[] }
|
|
12
|
-
* ~/.context-mcp/projects/<slug>/summary.json
|
|
13
|
-
* ~/.context-mcp/projects/<slug>/discussions.json
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
|
|
19
|
-
function normPath(p) {
|
|
20
|
-
return p ? p.toLowerCase().replace(/\\/g, '/').replace(/\/$/, '') : '';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function treeFor(entry) {
|
|
24
|
-
if (entry.type === 'compaction') return 'summary';
|
|
25
|
-
return 'context';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readArr(filePath, key) {
|
|
29
|
-
if (!existsSync(filePath)) return [];
|
|
30
|
-
try {
|
|
31
|
-
const d = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
32
|
-
return Array.isArray(d[key]) ? d[key] : (Array.isArray(d) ? d : []);
|
|
33
|
-
} catch { return []; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Run migration if legacy flat files are present.
|
|
38
|
-
* @param {object} opts
|
|
39
|
-
* @param {string} opts.dataDir - base data dir (~/.context-mcp)
|
|
40
|
-
* @param {string} opts.projectsDir - projects sub-dir
|
|
41
|
-
* @param {string} opts.projectsPath - path to projects.json
|
|
42
|
-
* @param {Function} opts.slugify - name → filesystem slug
|
|
43
|
-
* @param {Function} opts.flushFile - (filePath, content) atomic write
|
|
44
|
-
* @param {Array} opts.projectsIndex - current projects.json array (mutated in place)
|
|
45
|
-
*/
|
|
46
|
-
export function runMigration({ dataDir, projectsDir, projectsPath, slugify, flushFile, projectsIndex }) {
|
|
47
|
-
const legacyContexts = join(dataDir, 'contexts.json');
|
|
48
|
-
const legacyDiscussions = join(dataDir, 'discussions.json');
|
|
49
|
-
const legacyGraphs = join(dataDir, 'graphs.json');
|
|
50
|
-
|
|
51
|
-
const hasLegacy = existsSync(legacyContexts)
|
|
52
|
-
|| existsSync(legacyDiscussions)
|
|
53
|
-
|| existsSync(legacyGraphs);
|
|
54
|
-
|
|
55
|
-
if (!hasLegacy) return false;
|
|
56
|
-
|
|
57
|
-
const oldContexts = readArr(legacyContexts, 'contexts');
|
|
58
|
-
const oldDiscussions = readArr(legacyDiscussions, 'discussions');
|
|
59
|
-
const oldGraphs = readArr(legacyGraphs, 'graphs');
|
|
60
|
-
|
|
61
|
-
// Remap legacy type names to current 4-type schema
|
|
62
|
-
const TYPE_MAP = { architecture: 'note', code: 'note', error: 'bug', summary: 'compaction' };
|
|
63
|
-
const remapType = entry => {
|
|
64
|
-
if (TYPE_MAP[entry.type]) entry.type = TYPE_MAP[entry.type];
|
|
65
|
-
return entry;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Group everything by project name
|
|
69
|
-
const byProject = {};
|
|
70
|
-
const ensure = name => {
|
|
71
|
-
if (!byProject[name]) byProject[name] = {
|
|
72
|
-
context: [], graph: { build: null }, summary: [], discussions: [],
|
|
73
|
-
};
|
|
74
|
-
return byProject[name];
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
for (const entry of oldContexts) {
|
|
78
|
-
remapType(entry);
|
|
79
|
-
const p = entry.project || 'global';
|
|
80
|
-
const d = ensure(p);
|
|
81
|
-
if (treeFor(entry) === 'summary') d.summary.push(entry);
|
|
82
|
-
else d.context.push(entry);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const disc of oldDiscussions) {
|
|
86
|
-
ensure(disc.project || 'global').discussions.push(disc);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Match graph build records to projects via rootPath
|
|
90
|
-
for (const graph of oldGraphs) {
|
|
91
|
-
const proj = projectsIndex.find(p => normPath(p.rootPath) === normPath(graph.path));
|
|
92
|
-
const name = proj ? proj.name : 'global';
|
|
93
|
-
const d = ensure(name);
|
|
94
|
-
d.graph.build = {
|
|
95
|
-
path: graph.path, nodes: graph.nodes, edges: graph.edges,
|
|
96
|
-
communities: graph.communities, cached: graph.cached || 0,
|
|
97
|
-
changed: graph.changed || 0, time_ms: graph.time_ms || 0,
|
|
98
|
-
summary: graph.summary || '', builtAt: graph.builtAt,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Write per-project files
|
|
103
|
-
for (const [name, data] of Object.entries(byProject)) {
|
|
104
|
-
const dir = join(projectsDir, slugify(name));
|
|
105
|
-
mkdirSync(dir, { recursive: true });
|
|
106
|
-
flushFile(join(dir, 'context.json'), { entries: data.context });
|
|
107
|
-
flushFile(join(dir, 'graph.json'), data.graph);
|
|
108
|
-
flushFile(join(dir, 'summary.json'), { entries: data.summary });
|
|
109
|
-
flushFile(join(dir, 'discussions.json'), { discussions: data.discussions });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Stamp dataDir onto each project record
|
|
113
|
-
for (const proj of projectsIndex) {
|
|
114
|
-
if (!proj.dataDir) proj.dataDir = `projects/${slugify(proj.name)}`;
|
|
115
|
-
}
|
|
116
|
-
flushFile(projectsPath, { projects: projectsIndex });
|
|
117
|
-
|
|
118
|
-
// Remove legacy flat files
|
|
119
|
-
try { unlinkSync(legacyContexts); } catch {}
|
|
120
|
-
try { unlinkSync(legacyDiscussions); } catch {}
|
|
121
|
-
try { unlinkSync(legacyGraphs); } catch {}
|
|
122
|
-
|
|
123
|
-
return true;
|
|
124
|
-
}
|