@aabadin/project-memory-context 0.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/LICENSE +674 -0
- package/README.md +123 -0
- package/bin/pmc.mjs +11 -0
- package/cli/apply-enrichment-result.mjs +31 -0
- package/cli/batch-enrich.mjs +5 -0
- package/cli/bootstrap.mjs +357 -0
- package/cli/build-worklist.mjs +35 -0
- package/cli/context.mjs +49 -0
- package/cli/doctor.mjs +29 -0
- package/cli/enrich-batch.mjs +11 -0
- package/cli/enrich-loop.sh +117 -0
- package/cli/enrich-orchestrator.mjs +5 -0
- package/cli/enrich-queue.mjs +525 -0
- package/cli/enrich-sync.mjs +5 -0
- package/cli/enrich.mjs +51 -0
- package/cli/fail-enrichment.mjs +28 -0
- package/cli/finalize-enrichment.mjs +25 -0
- package/cli/init.mjs +66 -0
- package/cli/install-pmc.mjs +153 -0
- package/cli/materialize-enrichment-artifacts.mjs +38 -0
- package/cli/new-project.mjs +41 -0
- package/cli/prepare-semantic-jobs.mjs +8 -0
- package/cli/project-context.mjs +224 -0
- package/cli/sanitize.mjs +235 -0
- package/cli/save-intake-context.mjs +22 -0
- package/cli/setup.mjs +80 -0
- package/cli/status.mjs +81 -0
- package/mcp/local-model-server.mjs +74 -0
- package/package.json +60 -0
- package/plugin/index.mjs +27 -0
- package/src/artifacts.mjs +39 -0
- package/src/change-detector.mjs +10 -0
- package/src/command-dispatch.mjs +84 -0
- package/src/declared-intake.mjs +25 -0
- package/src/doctor.mjs +114 -0
- package/src/enrichment-artifacts.mjs +67 -0
- package/src/enrichment-attempts.mjs +17 -0
- package/src/enrichment-config.mjs +121 -0
- package/src/enrichment-driver.mjs +167 -0
- package/src/enrichment-errors.mjs +46 -0
- package/src/enrichment-linker.mjs +29 -0
- package/src/extractors/architecture-extractor.mjs +8 -0
- package/src/extractors/js-ts-extractor.mjs +118 -0
- package/src/extractors/regex-extractor.mjs +439 -0
- package/src/extractors/rules-extractor.mjs +9 -0
- package/src/extractors/stack-extractor.mjs +48 -0
- package/src/extractors/structure-extractor.mjs +31 -0
- package/src/fail-enrichment.mjs +33 -0
- package/src/finalize-enrichment.mjs +30 -0
- package/src/graph-backfill.mjs +35 -0
- package/src/graph-node-resolver.mjs +64 -0
- package/src/index.mjs +2 -0
- package/src/intake-context.mjs +16 -0
- package/src/invalidation-matrix.mjs +33 -0
- package/src/markdown-renderer.mjs +27 -0
- package/src/materializer.mjs +128 -0
- package/src/memory-payload.mjs +55 -0
- package/src/persist-enrichment-result.mjs +33 -0
- package/src/platform.mjs +111 -0
- package/src/plugin-config.mjs +17 -0
- package/src/prepare-semantic-jobs.mjs +33 -0
- package/src/project-context-schema.mjs +57 -0
- package/src/providers/cloud-api-provider.mjs +88 -0
- package/src/providers/local-model-provider.mjs +67 -0
- package/src/refresh-state.mjs +21 -0
- package/src/result-input.mjs +9 -0
- package/src/retrieval/context-renderer.mjs +97 -0
- package/src/retrieval/query-engine.mjs +230 -0
- package/src/semantic-report.mjs +26 -0
- package/src/semantic-unit.mjs +74 -0
- package/src/setup-bootstrap.mjs +131 -0
- package/src/symbol-extractor.mjs +29 -0
- package/src/symbol-index.mjs +30 -0
- package/src/symbol-keys.mjs +28 -0
- package/src/sync-manifest.mjs +119 -0
- package/src/template-installer.mjs +181 -0
- package/src/worklist-state.mjs +12 -0
- package/templates/claude-code/CLAUDE.md.snippet +36 -0
- package/templates/cursor/.cursorrules.snippet +36 -0
- package/templates/generic/README-SETUP.md +53 -0
- package/templates/opencode/agent/enrich.md +28 -0
- package/templates/opencode/autostart-snippet.md +13 -0
- package/templates/opencode/commands/get-context.md +22 -0
- package/templates/opencode/commands/new-project.md +32 -0
- package/templates/opencode/commands/sanitize.md +21 -0
- package/templates/opencode/commands/sync-context.md +22 -0
- package/templates/project-memory-context workflow.md +129 -0
- package/templates/project-memory-context.md +42 -0
package/cli/sanitize.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname, basename, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { ensureProjectMemoryContextDirs, readJsonArtifact, writeJsonArtifact } from '../src/artifacts.mjs';
|
|
9
|
+
import { extractTopLevelSymbols } from '../src/symbol-extractor.mjs';
|
|
10
|
+
import { attachGraphNodeIds } from '../src/graph-node-resolver.mjs';
|
|
11
|
+
import { appendSyncEntries, createSyncEntry, clearManifest } from '../src/sync-manifest.mjs';
|
|
12
|
+
import { spawnBackground } from '../src/platform.mjs';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PROJECT_ROOT = resolve(process.argv[2] || process.cwd());
|
|
16
|
+
|
|
17
|
+
function log(msg) { console.error(`[sanitize] ${msg}`); }
|
|
18
|
+
|
|
19
|
+
function safeKey(key) {
|
|
20
|
+
return key.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function codeHash(content) {
|
|
24
|
+
return createHash('sha256').update(content).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getGraphifyExe() {
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
const localAppData = process.env.LOCALAPPDATA || resolve(process.env.APPDATA || '', '..', 'Local');
|
|
30
|
+
return resolve(localAppData, 'Programs', 'Python', 'Python313', 'Scripts', 'graphify.exe');
|
|
31
|
+
}
|
|
32
|
+
return 'graphify';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function findSourceFiles(projectRoot) {
|
|
36
|
+
const exts = new Set(['.ts', '.mjs', '.js', '.cs']);
|
|
37
|
+
const ignore = ['node_modules', 'dist', '.git', 'bin', 'obj', '.opencode', '.planning', 'graphify-out'];
|
|
38
|
+
const results = [];
|
|
39
|
+
|
|
40
|
+
async function walk(dir) {
|
|
41
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const full = resolve(dir, entry.name);
|
|
44
|
+
if (ignore.some(i => full.includes(i))) continue;
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await walk(full);
|
|
47
|
+
} else if (Array.from(exts).some(e => entry.name.endsWith(e))) {
|
|
48
|
+
results.push(relative(projectRoot, full).replace(/\\/g, '/'));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await walk(projectRoot);
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runGraphifyUpdate(projectRoot) {
|
|
58
|
+
const graphifyExe = getGraphifyExe();
|
|
59
|
+
const graphifyOutDir = resolve(projectRoot, 'graphify-out');
|
|
60
|
+
const graphOutDir = resolve(projectRoot, '.planning', 'project-memory-context', 'graph');
|
|
61
|
+
|
|
62
|
+
log('Running graphify update (structural AST)...');
|
|
63
|
+
const r = spawnSync(`"${graphifyExe}"`, ['update', `"${projectRoot}"`], {
|
|
64
|
+
cwd: projectRoot,
|
|
65
|
+
stdio: 'inherit',
|
|
66
|
+
shell: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (r.status !== 0) {
|
|
70
|
+
log(`WARNING: graphify update failed (code ${r.status}). Continuing with existing graph.`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const files = await readdir(graphifyOutDir);
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
if (f === 'graph.json' || f === 'graph.metadata.json' || f === 'graph.html' || f === 'GRAPH_REPORT.md') {
|
|
78
|
+
const { copyFileSync } = await import('node:fs');
|
|
79
|
+
copyFileSync(resolve(graphifyOutDir, f), resolve(graphOutDir, f));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch { /* graphify-out may not exist */ }
|
|
83
|
+
|
|
84
|
+
log('Graphify update complete.');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function extractCurrentSymbols(projectRoot, files) {
|
|
89
|
+
const allSymbols = [];
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
try {
|
|
92
|
+
const content = await readFile(resolve(projectRoot, file), 'utf8');
|
|
93
|
+
const symbols = extractTopLevelSymbols({ filePath: file, content });
|
|
94
|
+
for (const sym of symbols) {
|
|
95
|
+
sym.codeHash = codeHash(
|
|
96
|
+
content.split('\n').slice(sym.range.startLine - 1, sym.range.endLine).join('\n')
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
allSymbols.push(...symbols);
|
|
100
|
+
} catch { /* skip unreadable files */ }
|
|
101
|
+
}
|
|
102
|
+
return allSymbols;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function sanitize() {
|
|
106
|
+
log(`Target: ${PROJECT_ROOT}`);
|
|
107
|
+
const projectSlug = basename(PROJECT_ROOT).toLowerCase();
|
|
108
|
+
const dirs = await ensureProjectMemoryContextDirs(PROJECT_ROOT);
|
|
109
|
+
|
|
110
|
+
await runGraphifyUpdate(PROJECT_ROOT);
|
|
111
|
+
|
|
112
|
+
const graph = await readJsonArtifact(resolve(dirs.graph, 'graph.json'), { nodes: [], edges: [] });
|
|
113
|
+
|
|
114
|
+
log('Scanning source files...');
|
|
115
|
+
const files = await findSourceFiles(PROJECT_ROOT);
|
|
116
|
+
log(`Found ${files.length} source files.`);
|
|
117
|
+
|
|
118
|
+
log('Extracting symbols...');
|
|
119
|
+
const currentSymbols = await extractCurrentSymbols(PROJECT_ROOT, files);
|
|
120
|
+
const resolvedSymbols = attachGraphNodeIds({ symbols: currentSymbols, graph });
|
|
121
|
+
log(`Extracted ${resolvedSymbols.length} symbols.`);
|
|
122
|
+
|
|
123
|
+
const currentMap = new Map();
|
|
124
|
+
for (const sym of resolvedSymbols) {
|
|
125
|
+
currentMap.set(sym.symbolKey, sym);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const existingWorklist = await readJsonArtifact(resolve(dirs.enrichment, 'worklist.json'), []);
|
|
129
|
+
const existingMap = new Map();
|
|
130
|
+
for (const entry of existingWorklist) {
|
|
131
|
+
existingMap.set(entry.symbolKey, entry);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const newEntries = [];
|
|
135
|
+
const staleEntries = [];
|
|
136
|
+
const removedEntries = [];
|
|
137
|
+
const unchangedEntries = [];
|
|
138
|
+
const syncOps = [];
|
|
139
|
+
|
|
140
|
+
for (const [key, sym] of currentMap) {
|
|
141
|
+
const existing = existingMap.get(key);
|
|
142
|
+
if (!existing) {
|
|
143
|
+
newEntries.push({
|
|
144
|
+
...sym,
|
|
145
|
+
status: 'pending',
|
|
146
|
+
memoryId: null,
|
|
147
|
+
});
|
|
148
|
+
} else if (existing.codeHash !== sym.codeHash) {
|
|
149
|
+
staleEntries.push({
|
|
150
|
+
...sym,
|
|
151
|
+
status: 'stale',
|
|
152
|
+
staleReason: 'code-hash-changed',
|
|
153
|
+
staleAt: new Date().toISOString(),
|
|
154
|
+
memoryId: existing.memoryId || null,
|
|
155
|
+
});
|
|
156
|
+
syncOps.push(createSyncEntry({
|
|
157
|
+
action: 'delete',
|
|
158
|
+
keyTag: `key:symbol:${safeKey(key)}`,
|
|
159
|
+
tags: ['symbol', sym.language, sym.kind, `project:${projectSlug}`, `file:${sym.filePath}`],
|
|
160
|
+
source: 'sanitize',
|
|
161
|
+
symbolKey: key,
|
|
162
|
+
}));
|
|
163
|
+
} else {
|
|
164
|
+
unchangedEntries.push({
|
|
165
|
+
...existing,
|
|
166
|
+
verifiedAt: new Date().toISOString(),
|
|
167
|
+
status: existing.status === 'stale' ? 'stale' : existing.status,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const [key, existing] of existingMap) {
|
|
173
|
+
if (!currentMap.has(key)) {
|
|
174
|
+
removedEntries.push(existing);
|
|
175
|
+
syncOps.push(createSyncEntry({
|
|
176
|
+
action: 'delete',
|
|
177
|
+
keyTag: `key:symbol:${safeKey(key)}`,
|
|
178
|
+
tags: ['symbol', existing.language, existing.kind, `project:${projectSlug}`, `file:${existing.filePath}`],
|
|
179
|
+
source: 'sanitize',
|
|
180
|
+
symbolKey: key,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const newWorklist = [
|
|
186
|
+
...newEntries,
|
|
187
|
+
...staleEntries,
|
|
188
|
+
...unchangedEntries,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
await writeJsonArtifact(resolve(dirs.enrichment, 'worklist.json'), newWorklist);
|
|
192
|
+
|
|
193
|
+
if (syncOps.length > 0) {
|
|
194
|
+
await appendSyncEntries(dirs.enrichment, syncOps);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const pendingCount = newWorklist.filter(e => e.status === 'pending' || e.status === 'stale').length;
|
|
198
|
+
const enrichedCount = newWorklist.filter(e => e.status === 'enriched' || e.status === 'already_enriched').length;
|
|
199
|
+
|
|
200
|
+
log('');
|
|
201
|
+
log('=== Sanitize Report ===');
|
|
202
|
+
log(`Total symbols: ${newWorklist.length}`);
|
|
203
|
+
log(`New: ${newEntries.length}`);
|
|
204
|
+
log(`Stale (code changed): ${staleEntries.length}`);
|
|
205
|
+
log(`Removed: ${removedEntries.length}`);
|
|
206
|
+
log(`Unchanged/verified: ${unchangedEntries.length}`);
|
|
207
|
+
log(`Pending enrichment: ${pendingCount}`);
|
|
208
|
+
log(`Already enriched: ${enrichedCount}`);
|
|
209
|
+
log(`Sync-manifest operations: ${syncOps.length} (deletes for stale/removed)`);
|
|
210
|
+
log('');
|
|
211
|
+
|
|
212
|
+
if (pendingCount > 0) {
|
|
213
|
+
const enrichCli = resolve(__dirname, 'enrich.mjs');
|
|
214
|
+
const launchedPid = spawnBackground(process.execPath, [enrichCli, PROJECT_ROOT], { cwd: PROJECT_ROOT });
|
|
215
|
+
log(`Background enrichment launched via spawnBackground (pid=${launchedPid})`);
|
|
216
|
+
log(` ${process.execPath} ${enrichCli} ${PROJECT_ROOT}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = {
|
|
220
|
+
total: newWorklist.length,
|
|
221
|
+
new: newEntries.length,
|
|
222
|
+
stale: staleEntries.length,
|
|
223
|
+
removed: removedEntries.length,
|
|
224
|
+
unchanged: unchangedEntries.length,
|
|
225
|
+
pendingEnrichment: pendingCount,
|
|
226
|
+
enriched: enrichedCount,
|
|
227
|
+
syncOps: syncOps.length,
|
|
228
|
+
};
|
|
229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sanitize().catch(err => {
|
|
233
|
+
console.error('[sanitize] FATAL:', err.message);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ensureProjectMemoryContextDirs, writeJsonArtifact } from '../src/artifacts.mjs';
|
|
5
|
+
import { buildIntakeContext } from '../src/intake-context.mjs';
|
|
6
|
+
|
|
7
|
+
const [, , projectDescription = '', ...goalArgs] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!projectDescription.trim()) {
|
|
10
|
+
console.error('Usage: node save-intake-context.mjs "project description" goal-one goal-two');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const projectRoot = resolve(process.cwd());
|
|
15
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
16
|
+
const intake = buildIntakeContext({
|
|
17
|
+
projectDescription,
|
|
18
|
+
mappingGoals: goalArgs,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await writeJsonArtifact(join(dirs.intake, 'latest-context.json'), intake);
|
|
22
|
+
console.log(JSON.stringify({ saved: true, file: join(dirs.intake, 'latest-context.json'), intake }, null, 2));
|
package/cli/setup.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { bootstrapProjectInstall } from '../src/setup-bootstrap.mjs';
|
|
9
|
+
import { runDoctor } from '../src/doctor.mjs';
|
|
10
|
+
import { resolvePythonBin } from '../src/platform.mjs';
|
|
11
|
+
|
|
12
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
|
+
|
|
14
|
+
function installGraphify() {
|
|
15
|
+
const candidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
|
|
16
|
+
for (const command of candidates) {
|
|
17
|
+
const result = spawnSync(command, ['-m', 'pip', 'install', 'graphifyy'], { stdio: 'inherit' });
|
|
18
|
+
if (result.status === 0) return command;
|
|
19
|
+
}
|
|
20
|
+
console.warn('\n⚠ Could not install graphifyy automatically. Run: pip install graphifyy\n');
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function spawnCheck(bin, args) {
|
|
25
|
+
const result = spawnSync(bin, args, { encoding: 'utf-8', timeout: 5000 });
|
|
26
|
+
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rl = createInterface({ input, output });
|
|
30
|
+
const cwd = resolve(process.cwd());
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
console.log('\n─── pmc setup ───────────────────────────────────────\n');
|
|
34
|
+
|
|
35
|
+
const ollamaBaseUrl =
|
|
36
|
+
(await rl.question('Ollama base URL [http://localhost:11434]: ')).trim() ||
|
|
37
|
+
'http://localhost:11434';
|
|
38
|
+
|
|
39
|
+
const ollamaModel =
|
|
40
|
+
(await rl.question('Ollama model name [deepseek-coder-v2:16b-ctx32k]: ')).trim() ||
|
|
41
|
+
'deepseek-coder-v2:16b-ctx32k';
|
|
42
|
+
|
|
43
|
+
installGraphify();
|
|
44
|
+
|
|
45
|
+
const result = await bootstrapProjectInstall({
|
|
46
|
+
projectRoot: cwd,
|
|
47
|
+
packageRoot,
|
|
48
|
+
ollamaBaseUrl,
|
|
49
|
+
ollamaModel,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log('\n─── Installation complete ───────────────────────────\n');
|
|
53
|
+
console.log(` Memory DB path: ${result.installState.memoryDbPath}`);
|
|
54
|
+
console.log(` Embedding cache: ${result.installState.embeddingCachePath}`);
|
|
55
|
+
console.log(` MCP config: ${result.configPath}`);
|
|
56
|
+
console.log(` Command template: ${result.commandPath}`);
|
|
57
|
+
|
|
58
|
+
// Run doctor to surface any remaining issues
|
|
59
|
+
console.log('\n─── Environment check ───────────────────────────────\n');
|
|
60
|
+
const env = {
|
|
61
|
+
...process.env,
|
|
62
|
+
MEMORY_DB_PATH: result.installState.memoryDbPath,
|
|
63
|
+
EMBEDDING_CACHE_PATH: result.installState.embeddingCachePath,
|
|
64
|
+
};
|
|
65
|
+
const { checks } = await runDoctor({ env, resolvePythonBin, spawnCheck });
|
|
66
|
+
|
|
67
|
+
const icon = { ok: '✓', warn: '⚠', fail: '✗' };
|
|
68
|
+
for (const c of checks) {
|
|
69
|
+
console.log(` ${icon[c.status]} ${c.name.padEnd(22)} ${c.message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hasFail = checks.some(c => c.status === 'fail');
|
|
73
|
+
if (hasFail) {
|
|
74
|
+
console.log('\nFix the issues above and re-run `pmc setup` if needed.\n');
|
|
75
|
+
} else {
|
|
76
|
+
console.log('\nAll checks passed. Run `pmc enrich` to start enriching the project.\n');
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rl.close();
|
|
80
|
+
}
|
package/cli/status.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { detectAgentType, resolveConfigDirs } from '../src/platform.mjs';
|
|
7
|
+
|
|
8
|
+
export function summarizeWorklist(worklist) {
|
|
9
|
+
return {
|
|
10
|
+
pending: worklist.filter((e) => e.status === 'pending' || e.status === 'stale').length,
|
|
11
|
+
enriched: worklist.filter((e) => e.status === 'enriched' || e.status === 'already_enriched').length,
|
|
12
|
+
errors: worklist.filter((e) => e.status === 'error').length,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readJsonSafe(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getLastSyncTimestamp(enrichmentDir) {
|
|
25
|
+
const syncManifest = join(enrichmentDir, 'sync-manifest.json');
|
|
26
|
+
try {
|
|
27
|
+
const st = await stat(syncManifest);
|
|
28
|
+
return st.mtime.toISOString();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function buildStatusReport({ projectRoot = process.cwd() } = {}) {
|
|
35
|
+
const dirs = resolveConfigDirs(projectRoot);
|
|
36
|
+
const planningDir = join(projectRoot, '.planning', 'project-memory-context');
|
|
37
|
+
const enrichmentDir = join(planningDir, 'enrichment');
|
|
38
|
+
const worklistPath = join(enrichmentDir, 'worklist.json');
|
|
39
|
+
const installStatePath = join(planningDir, 'install.json');
|
|
40
|
+
|
|
41
|
+
const worklist = await readJsonSafe(worklistPath);
|
|
42
|
+
const installState = await readJsonSafe(installStatePath);
|
|
43
|
+
const lastSync = await getLastSyncTimestamp(enrichmentDir);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
command: 'status',
|
|
48
|
+
projectRoot: resolve(projectRoot),
|
|
49
|
+
configLocation: dirs.projectConfig,
|
|
50
|
+
agentType: detectAgentType(projectRoot),
|
|
51
|
+
installState: installState ? { installedAt: installState.installedAt, version: installState.version } : null,
|
|
52
|
+
worklist: worklist ? summarizeWorklist(worklist) : null,
|
|
53
|
+
lastSync,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function main(args = process.argv.slice(2)) {
|
|
58
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
59
|
+
console.log('Usage: pmc status [project-dir]');
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log('Shows enrichment queue state, worklist counts, config location,');
|
|
62
|
+
console.log('agent type, and last sync timestamp.');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const projectRoot = args.find((a) => !a.startsWith('-')) || process.cwd();
|
|
67
|
+
const report = await buildStatusReport({ projectRoot });
|
|
68
|
+
console.log(JSON.stringify(report, null, 2));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
73
|
+
const exitCode = await main().catch((error) => {
|
|
74
|
+
console.error('[status] FATAL:', error.message);
|
|
75
|
+
return 1;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (exitCode !== 0) {
|
|
79
|
+
process.exit(exitCode);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const OLLAMA_BASE_URL = (process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434').replace(/\/+$/, '');
|
|
7
|
+
const OLLAMA_MODEL = process.env.OLLAMA_MODEL ?? 'deepseek-coder-v2:16b-ctx32k';
|
|
8
|
+
|
|
9
|
+
async function generateSemanticReport(prompt) {
|
|
10
|
+
const system = [
|
|
11
|
+
'You analyze a single code symbol and must answer in compact structured text.',
|
|
12
|
+
'Always emit these lines exactly once:',
|
|
13
|
+
'responsibility: ...',
|
|
14
|
+
'inputs: item1, item2',
|
|
15
|
+
'output: ...',
|
|
16
|
+
'dependencies: dep1, dep2',
|
|
17
|
+
'role: ...',
|
|
18
|
+
].join('\n');
|
|
19
|
+
|
|
20
|
+
const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
model: OLLAMA_MODEL,
|
|
25
|
+
prompt,
|
|
26
|
+
system,
|
|
27
|
+
stream: false,
|
|
28
|
+
options: {
|
|
29
|
+
temperature: 0.1,
|
|
30
|
+
num_ctx: 8192,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Ollama API error (${response.status})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const raw = String(data.response ?? '').trim();
|
|
41
|
+
return {
|
|
42
|
+
summary: raw.split('\n').find((line) => line.toLowerCase().startsWith('responsibility:'))?.split(':').slice(1).join(':').trim() ?? raw,
|
|
43
|
+
findings: raw.split('\n').map((line) => line.trim()).filter(Boolean),
|
|
44
|
+
raw,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const server = new McpServer({
|
|
49
|
+
name: 'pmc-local-model',
|
|
50
|
+
version: '0.1.0',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
'semantic_report',
|
|
55
|
+
'Analyze a single symbol prompt with a local Ollama model and return a structured semantic report.',
|
|
56
|
+
{
|
|
57
|
+
prompt: z.string().describe('Prepared semantic prompt for a single symbol'),
|
|
58
|
+
},
|
|
59
|
+
async ({ prompt }) => {
|
|
60
|
+
try {
|
|
61
|
+
const report = await generateSemanticReport(prompt);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: JSON.stringify(report, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: `semantic_report failed: ${String(error)}` }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aabadin/project-memory-context",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portable project memory context CLI — bootstraps semantic enrichment workflows for any AI coding agent.",
|
|
5
|
+
"license": "GPL-3.0-or-later",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.mjs",
|
|
10
|
+
"./platform": "./src/platform.mjs",
|
|
11
|
+
"./retrieval": "./src/retrieval/query-engine.mjs"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"pmc": "bin/pmc.mjs"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test tests/*.test.mjs",
|
|
18
|
+
"test:watch": "node --test --watch tests/*.test.mjs",
|
|
19
|
+
"prepublishOnly": "npm test"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"cli/",
|
|
24
|
+
"mcp/",
|
|
25
|
+
"plugin/",
|
|
26
|
+
"src/",
|
|
27
|
+
"templates/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"memory",
|
|
34
|
+
"project-context",
|
|
35
|
+
"semantic-enrichment",
|
|
36
|
+
"code-intelligence",
|
|
37
|
+
"graphify",
|
|
38
|
+
"agent-memory",
|
|
39
|
+
"model-context-protocol"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/adamrdrew/agent-memory-mcp.git",
|
|
44
|
+
"directory": "tools/project-memory-context"
|
|
45
|
+
},
|
|
46
|
+
"author": "Adam Drew",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@babel/parser": "^7.26.0",
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
56
|
+
"acorn": "^8.16.0",
|
|
57
|
+
"acorn-walk": "^8.3.0",
|
|
58
|
+
"zod": "^3.24.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/plugin/index.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { buildInjectedPmcConfig } from '../src/plugin-config.mjs';
|
|
5
|
+
|
|
6
|
+
async function readInstallState(projectRoot) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(join(projectRoot, '.planning', 'project-memory-context', 'install.json'), 'utf8'));
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async ({ directory }) => {
|
|
15
|
+
return {
|
|
16
|
+
config: async (cfg) => {
|
|
17
|
+
const installState = await readInstallState(directory);
|
|
18
|
+
if (!installState) return;
|
|
19
|
+
|
|
20
|
+
const injected = buildInjectedPmcConfig({ installState });
|
|
21
|
+
cfg.mcp = {
|
|
22
|
+
...(cfg.mcp ?? {}),
|
|
23
|
+
...injected.mcp,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function ensureProjectMemoryContextDirs(projectRoot) {
|
|
5
|
+
const base = join(projectRoot, '.planning', 'project-memory-context');
|
|
6
|
+
const projectContext = join(base, 'project-context');
|
|
7
|
+
const dirs = {
|
|
8
|
+
base,
|
|
9
|
+
intake: join(base, 'intake'),
|
|
10
|
+
graph: join(base, 'graph'),
|
|
11
|
+
enrichment: join(base, 'enrichment'),
|
|
12
|
+
runs: join(base, 'runs'),
|
|
13
|
+
projectContext,
|
|
14
|
+
projectContextDetected: join(projectContext, 'detected'),
|
|
15
|
+
projectContextDeclared: join(projectContext, 'declared'),
|
|
16
|
+
projectContextMaterialized: join(projectContext, 'materialized'),
|
|
17
|
+
projectContextMarkdown: join(projectContext, 'markdown'),
|
|
18
|
+
projectContextState: join(projectContext, 'state'),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
await Promise.all(Object.values(dirs).map((dir) => mkdir(dir, { recursive: true })));
|
|
22
|
+
return dirs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function writeJsonArtifact(filePath, value) {
|
|
26
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
27
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function readJsonArtifact(filePath, fallback = null) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error && error.code === 'ENOENT') {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function detectChangedFilesFromHashes(previousHashes, nextHashes) {
|
|
2
|
+
const changed = [];
|
|
3
|
+
const files = new Set([...Object.keys(previousHashes), ...Object.keys(nextHashes)]);
|
|
4
|
+
for (const file of files) {
|
|
5
|
+
if (previousHashes[file] !== nextHashes[file]) {
|
|
6
|
+
changed.push(file);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return changed;
|
|
10
|
+
}
|