@aabadin/project-memory-context 0.1.4 → 0.2.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/README.md +514 -69
- package/cli/context.mjs +291 -15
- package/cli/install-pmc.mjs +20 -0
- package/cli/query.mjs +136 -0
- package/cli/status.mjs +56 -2
- package/mcp/pmc-query-server.mjs +90 -0
- package/package.json +3 -2
- package/src/command-dispatch.mjs +1 -0
- package/src/plugin-config.mjs +8 -0
- package/src/query/load-artifacts.mjs +96 -0
- package/src/query/orchestrator.mjs +175 -0
- package/src/retrieval/context-renderer-v1.mjs +53 -0
- package/src/retrieval/target-resolver.mjs +57 -0
- package/src/setup-bootstrap.mjs +1 -0
- package/src/template-installer.mjs +60 -5
- package/templates/claude-code/CLAUDE.md.snippet +29 -3
- package/templates/cursor/.cursorrules.snippet +29 -3
- package/templates/opencode/commands/doctor.md +21 -0
- package/templates/opencode/commands/enrich-status.md +21 -0
- package/templates/opencode/commands/get-context.md +22 -5
- package/templates/opencode/commands/init-project.md +21 -0
- package/templates/opencode/commands/{new-project.md → map-project.md} +1 -1
- package/templates/pmc-skill/SKILL.md +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aabadin/project-memory-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Portable project memory context CLI — bootstraps semantic enrichment workflows for any AI coding agent.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"./retrieval": "./src/retrieval/query-engine.mjs"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"pmc": "bin/pmc.mjs"
|
|
14
|
+
"pmc": "bin/pmc.mjs",
|
|
15
|
+
"pmc-query-server": "mcp/pmc-query-server.mjs"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"test": "node --test tests/*.test.mjs",
|
package/src/command-dispatch.mjs
CHANGED
|
@@ -14,6 +14,7 @@ const COMMANDS = new Map([
|
|
|
14
14
|
['install-pmc', 'cli/install-pmc.mjs'],
|
|
15
15
|
['new-project', 'cli/new-project.mjs'],
|
|
16
16
|
['project-context', 'cli/project-context.mjs'],
|
|
17
|
+
['query', 'cli/query.mjs'],
|
|
17
18
|
['sanitize', 'cli/sanitize.mjs'],
|
|
18
19
|
['setup', 'cli/setup.mjs'],
|
|
19
20
|
['status', 'cli/status.mjs'],
|
package/src/plugin-config.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export function buildInjectedPmcConfig({ installState }) {
|
|
2
2
|
return {
|
|
3
3
|
mcp: {
|
|
4
|
+
'pmc-query': {
|
|
5
|
+
type: 'local',
|
|
6
|
+
command: ['npx', '--yes', '--package', '@aabadin/project-memory-context', 'pmc-query-server'],
|
|
7
|
+
enabled: true,
|
|
8
|
+
environment: {
|
|
9
|
+
PMC_PROJECT_ROOT: installState.projectRoot,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
4
12
|
'pmc-agent-memory': {
|
|
5
13
|
type: 'local',
|
|
6
14
|
command: ['npx', '-y', '@aabadin/agent-memory-mcp'],
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function normalizePath(filePath) {
|
|
5
|
+
return String(filePath ?? '').replace(/\\/g, '/');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function readJson(filePath, fallback) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error && error.code === 'ENOENT') {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readJsonDirectory(directoryPath) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
22
|
+
return entries
|
|
23
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json'))
|
|
24
|
+
.map((entry) => entry.name)
|
|
25
|
+
.sort((a, b) => a.localeCompare(b));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error && error.code === 'ENOENT') {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadMemories(projectContextDir) {
|
|
35
|
+
const materializedDir = join(projectContextDir, 'materialized');
|
|
36
|
+
const materializedEntries = await readJsonDirectory(materializedDir);
|
|
37
|
+
const directory = materializedEntries.length > 0 ? materializedDir : projectContextDir;
|
|
38
|
+
const fileNames = materializedEntries.length > 0
|
|
39
|
+
? materializedEntries
|
|
40
|
+
: await readJsonDirectory(projectContextDir);
|
|
41
|
+
|
|
42
|
+
const memories = [];
|
|
43
|
+
for (const fileName of fileNames) {
|
|
44
|
+
const path = join(directory, fileName);
|
|
45
|
+
const memory = await readJson(path, null);
|
|
46
|
+
if (memory) {
|
|
47
|
+
memories.push({ ...memory, path });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return memories;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSymbol(symbolKey, entry, semanticSummary) {
|
|
55
|
+
const parts = String(symbolKey ?? '').split('|');
|
|
56
|
+
return {
|
|
57
|
+
symbolKey,
|
|
58
|
+
filePath: parts.length >= 2 ? normalizePath(parts[1]) : '',
|
|
59
|
+
name: parts.length >= 2 ? parts[parts.length - 2] : '',
|
|
60
|
+
graphNodeId: entry?.graphNodeId ?? null,
|
|
61
|
+
memoryId: entry?.memoryId ?? null,
|
|
62
|
+
status: entry?.status ?? null,
|
|
63
|
+
semanticSummary,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function loadQueryArtifacts(projectRoot) {
|
|
68
|
+
const pmcRoot = join(projectRoot, '.planning', 'project-memory-context');
|
|
69
|
+
const projectContextDir = join(pmcRoot, 'project-context');
|
|
70
|
+
const symbolIndexPath = join(pmcRoot, 'enrichment', 'symbol-index.json');
|
|
71
|
+
const graphPath = join(pmcRoot, 'graph', 'graph.json');
|
|
72
|
+
|
|
73
|
+
const memories = await loadMemories(projectContextDir);
|
|
74
|
+
const symbolIndex = await readJson(symbolIndexPath, {});
|
|
75
|
+
const graph = await readJson(graphPath, {});
|
|
76
|
+
const nodes = graph.nodes ?? [];
|
|
77
|
+
const edges = graph.edges ?? graph.links ?? [];
|
|
78
|
+
|
|
79
|
+
const nodeById = new Map(nodes.map((node) => [node?.id, node]));
|
|
80
|
+
const symbols = Object.entries(symbolIndex).map(([symbolKey, entry]) => {
|
|
81
|
+
const semanticSummary = String(
|
|
82
|
+
entry?.semanticSummary
|
|
83
|
+
?? nodeById.get(entry?.graphNodeId)?.metadata?.semanticSummary
|
|
84
|
+
?? '',
|
|
85
|
+
).trim();
|
|
86
|
+
|
|
87
|
+
return parseSymbol(symbolKey, entry, semanticSummary);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
memories,
|
|
92
|
+
symbols,
|
|
93
|
+
nodes,
|
|
94
|
+
edges,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { loadQueryArtifacts } from './load-artifacts.mjs';
|
|
2
|
+
|
|
3
|
+
function normalizePath(filePath) {
|
|
4
|
+
return String(filePath ?? '').replace(/\\/g, '/');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function tokenize(value) {
|
|
8
|
+
return String(value ?? '')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.split(/[^a-z0-9]+/i)
|
|
11
|
+
.map((token) => token.trim())
|
|
12
|
+
.filter((token) => token.length >= 2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function countMatches(tokens, fields) {
|
|
16
|
+
if (tokens.length === 0) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const haystack = fields.join(' ').toLowerCase();
|
|
21
|
+
let score = 0;
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
if (haystack.includes(token)) {
|
|
24
|
+
score += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return score;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function searchMemoryMatches(memories, tokens) {
|
|
31
|
+
return memories
|
|
32
|
+
.map((memory) => ({
|
|
33
|
+
...memory,
|
|
34
|
+
score: countMatches(tokens, [
|
|
35
|
+
memory.title,
|
|
36
|
+
memory.summary,
|
|
37
|
+
memory.body,
|
|
38
|
+
...(memory.tags ?? []),
|
|
39
|
+
]),
|
|
40
|
+
}))
|
|
41
|
+
.filter((memory) => memory.score > 0)
|
|
42
|
+
.sort((a, b) => b.score - a.score || String(a.path).localeCompare(String(b.path)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function searchSymbolMatches(symbols, tokens, fileFilter) {
|
|
46
|
+
const normalizedFilter = normalizePath(fileFilter);
|
|
47
|
+
|
|
48
|
+
return symbols
|
|
49
|
+
.map((symbol) => ({
|
|
50
|
+
...symbol,
|
|
51
|
+
score: countMatches(tokens, [symbol.name, symbol.filePath, symbol.semanticSummary]),
|
|
52
|
+
}))
|
|
53
|
+
.filter((symbol) => symbol.score > 0)
|
|
54
|
+
.filter((symbol) => !normalizedFilter || symbol.filePath === normalizedFilter)
|
|
55
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildRelatedSymbols(symbols) {
|
|
59
|
+
const symbolByKey = new Map(symbols.map((symbol) => [symbol.symbolKey, symbol]));
|
|
60
|
+
const symbolKeyByNodeId = new Map(
|
|
61
|
+
symbols
|
|
62
|
+
.filter((symbol) => symbol.graphNodeId)
|
|
63
|
+
.map((symbol) => [symbol.graphNodeId, symbol.symbolKey]),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return { symbolByKey, symbolKeyByNodeId };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createQueryOrchestrator({ projectRoot, loadArtifacts = loadQueryArtifacts }) {
|
|
70
|
+
async function searchSymbols(query, fileFilter) {
|
|
71
|
+
const tokens = tokenize(query);
|
|
72
|
+
if (tokens.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const artifacts = await loadArtifacts(projectRoot);
|
|
77
|
+
return searchSymbolMatches(artifacts.symbols, tokens, fileFilter);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function query(question) {
|
|
81
|
+
const tokens = tokenize(question);
|
|
82
|
+
if (tokens.length === 0) {
|
|
83
|
+
return { answer: '', sources: [], tokens_saved: 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const artifacts = await loadArtifacts(projectRoot);
|
|
87
|
+
const memoryMatches = searchMemoryMatches(artifacts.memories, tokens);
|
|
88
|
+
const symbolMatches = searchSymbolMatches(artifacts.symbols, tokens);
|
|
89
|
+
|
|
90
|
+
if (memoryMatches.length === 0 && symbolMatches.length === 0) {
|
|
91
|
+
return { answer: '', sources: [], tokens_saved: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const answerParts = [];
|
|
95
|
+
const sources = [];
|
|
96
|
+
let sourceCharacters = 0;
|
|
97
|
+
|
|
98
|
+
for (const memory of memoryMatches.slice(0, 3)) {
|
|
99
|
+
const body = String(memory.body ?? '').trim();
|
|
100
|
+
if (!body) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
answerParts.push(`${memory.title}: ${body}`);
|
|
105
|
+
sources.push({
|
|
106
|
+
type: 'project-context',
|
|
107
|
+
path: normalizePath(memory.path),
|
|
108
|
+
title: memory.title,
|
|
109
|
+
});
|
|
110
|
+
sourceCharacters += body.length + String(memory.summary ?? '').length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const symbol of symbolMatches.slice(0, 3)) {
|
|
114
|
+
const summary = String(symbol.semanticSummary ?? '').trim();
|
|
115
|
+
if (!summary) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
answerParts.push(`Symbol ${symbol.name} (${symbol.filePath}): ${summary}`);
|
|
120
|
+
sources.push({
|
|
121
|
+
type: 'symbol',
|
|
122
|
+
symbolKey: symbol.symbolKey,
|
|
123
|
+
filePath: symbol.filePath,
|
|
124
|
+
graphNodeId: symbol.graphNodeId,
|
|
125
|
+
});
|
|
126
|
+
sourceCharacters += summary.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const answer = answerParts.join('\n\n');
|
|
130
|
+
if (!answer) {
|
|
131
|
+
return { answer: '', sources: [], tokens_saved: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
answer,
|
|
136
|
+
sources,
|
|
137
|
+
tokens_saved: Math.max(0, Math.ceil(sourceCharacters / 4) - Math.ceil(answer.length / 4)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function getRelatedSymbols(symbolKey, direction) {
|
|
142
|
+
if (!symbolKey) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const artifacts = await loadArtifacts(projectRoot);
|
|
147
|
+
const { symbolByKey, symbolKeyByNodeId } = buildRelatedSymbols(artifacts.symbols);
|
|
148
|
+
const origin = symbolByKey.get(symbolKey);
|
|
149
|
+
if (!origin?.graphNodeId) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return artifacts.edges
|
|
154
|
+
.map((edge) => {
|
|
155
|
+
const relatedNodeId = direction === 'inbound'
|
|
156
|
+
? (edge.target === origin.graphNodeId ? edge.source : null)
|
|
157
|
+
: (edge.source === origin.graphNodeId ? edge.target : null);
|
|
158
|
+
|
|
159
|
+
return relatedNodeId ? symbolKeyByNodeId.get(relatedNodeId) : null;
|
|
160
|
+
})
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map((relatedKey) => symbolByKey.get(relatedKey));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
query,
|
|
167
|
+
searchSymbols,
|
|
168
|
+
getDependents(symbolKey) {
|
|
169
|
+
return getRelatedSymbols(symbolKey, 'inbound');
|
|
170
|
+
},
|
|
171
|
+
getDependencies(symbolKey) {
|
|
172
|
+
return getRelatedSymbols(symbolKey, 'outbound');
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function renderTargetContext({ summary = [], target = {}, relevant = [], relations = [], nextReads = [], metadata = {} } = {}) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
|
|
4
|
+
lines.push('Summary');
|
|
5
|
+
if (summary.length > 0) {
|
|
6
|
+
for (const s of summary) {
|
|
7
|
+
lines.push(`- ${s}`);
|
|
8
|
+
}
|
|
9
|
+
} else {
|
|
10
|
+
lines.push('- none');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
lines.push('Target');
|
|
14
|
+
if (target.mode != null) lines.push(` mode: ${target.mode}`);
|
|
15
|
+
if (target.name != null) lines.push(` name: ${target.name}`);
|
|
16
|
+
if (target.value != null) lines.push(` value: ${target.value}`);
|
|
17
|
+
if (target.filePath != null) lines.push(` filePath: ${target.filePath}`);
|
|
18
|
+
|
|
19
|
+
lines.push('Relevant');
|
|
20
|
+
if (relevant.length > 0) {
|
|
21
|
+
for (const r of relevant) {
|
|
22
|
+
const display = r.filePath ?? r.label ?? 'unknown';
|
|
23
|
+
lines.push(`- ${display}`);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
lines.push('- none');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lines.push('Relations');
|
|
30
|
+
if (relations.length > 0) {
|
|
31
|
+
for (const rel of relations) {
|
|
32
|
+
const itemsStr = (rel.items ?? []).join(', ');
|
|
33
|
+
lines.push(`- ${rel.kind}: ${itemsStr}`);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
lines.push('- none');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lines.push('Next Reads');
|
|
40
|
+
if (nextReads.length > 0) {
|
|
41
|
+
for (const nr of nextReads) {
|
|
42
|
+
lines.push(`- ${nr}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
lines.push('- none');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push('Metadata');
|
|
49
|
+
if (metadata.depth != null) lines.push(` depth: ${metadata.depth}`);
|
|
50
|
+
if (metadata.focus != null) lines.push(` focus: ${metadata.focus}`);
|
|
51
|
+
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function normalizeTargetPath(target) {
|
|
2
|
+
return String(target ?? '').replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function hasFileNameishLastSegment(target) {
|
|
6
|
+
const last = target.split(/[\\/]/).pop();
|
|
7
|
+
return looksLikeBareFilename(last) || /\.[a-zA-Z0-9]{2,6}$/.test(last);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function looksLikePath(target) {
|
|
11
|
+
return /^[A-Za-z]:[\\/]/.test(target)
|
|
12
|
+
|| /^[\\/]{2}/.test(target)
|
|
13
|
+
|| /^\.{1,2}$/.test(target)
|
|
14
|
+
|| /^\.{1,2}[\\/]/.test(target)
|
|
15
|
+
|| (/[\\/]/.test(target) && (!/\s/.test(target) || hasFileNameishLastSegment(target)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function looksLikeBareFilename(target) {
|
|
19
|
+
return /^(Dockerfile|Makefile|README\.(md|txt)|package\.json|tsconfig\.json|jsconfig\.json|\.gitignore|\.npmrc|\.env(\.[A-Za-z0-9_-]+)?)$/i.test(target);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveSymbolTarget(engine, target) {
|
|
23
|
+
const symbolKeys = engine.findSymbolKeyByName(target);
|
|
24
|
+
if (symbolKeys.length === 1) {
|
|
25
|
+
return { mode: 'symbol', target, symbolKey: symbolKeys[0] };
|
|
26
|
+
}
|
|
27
|
+
if (symbolKeys.length > 1) {
|
|
28
|
+
return { mode: 'symbol-ambiguous', target, symbolKeys };
|
|
29
|
+
}
|
|
30
|
+
return { mode: 'symbol-missing', target };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveTarget({ engine, explicitMode = null, target }) {
|
|
34
|
+
if (explicitMode === 'query') {
|
|
35
|
+
return { mode: 'query', target };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (explicitMode === 'file') {
|
|
39
|
+
return { mode: 'file', target: normalizeTargetPath(target) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (explicitMode === 'symbol') {
|
|
43
|
+
return resolveSymbolTarget(engine, target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalizedTarget = normalizeTargetPath(target);
|
|
47
|
+
if (looksLikePath(target) || looksLikeBareFilename(target) || engine.findSymbolKeysByFilePath(normalizedTarget).length > 0) {
|
|
48
|
+
return { mode: 'file', target: normalizedTarget };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const symbolResult = resolveSymbolTarget(engine, target);
|
|
52
|
+
if (symbolResult.mode !== 'symbol-missing') {
|
|
53
|
+
return symbolResult;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { mode: 'query', target };
|
|
57
|
+
}
|
package/src/setup-bootstrap.mjs
CHANGED
|
@@ -145,6 +145,7 @@ export async function bootstrapProjectInstall({
|
|
|
145
145
|
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
146
146
|
const memoryDbPath = join(dirs.base, 'memory-db');
|
|
147
147
|
const installState = {
|
|
148
|
+
projectRoot,
|
|
148
149
|
packageRoot,
|
|
149
150
|
ollamaBaseUrl,
|
|
150
151
|
ollamaModel,
|
|
@@ -69,6 +69,10 @@ function stripBlockMarkers(content, marker) {
|
|
|
69
69
|
.trim();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function wrapBlock(marker, block) {
|
|
73
|
+
return `<!-- pmc:${marker} -->\n${block}\n<!-- /pmc:${marker} -->\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
async function installWithBlockMarker({ projectRoot, packageRoot, placeholders, targetFile, templatePath, marker = 'init' }) {
|
|
73
77
|
const targetPath = join(projectRoot, targetFile);
|
|
74
78
|
const snippet = renderTemplate(await readTemplate(packageRoot, templatePath), placeholders);
|
|
@@ -84,6 +88,12 @@ async function installWithBlockMarker({ projectRoot, packageRoot, placeholders,
|
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
if (existing.trim()) {
|
|
92
|
+
const updated = replaceOrAppendBlock(existing, marker, stripBlockMarkers(snippet, marker));
|
|
93
|
+
await writeFile(targetPath, updated, 'utf8');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
await writeFile(targetPath, snippet, 'utf8');
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -91,23 +101,32 @@ async function installOpencode({ projectRoot, packageRoot, placeholders, globalC
|
|
|
91
101
|
const globalDir = globalConfigDir;
|
|
92
102
|
|
|
93
103
|
const commandTemplates = [
|
|
94
|
-
'opencode/commands/
|
|
104
|
+
'opencode/commands/map-project.md',
|
|
95
105
|
'opencode/commands/get-context.md',
|
|
96
106
|
'opencode/commands/sync-context.md',
|
|
97
107
|
'opencode/commands/sanitize.md',
|
|
108
|
+
'opencode/commands/enrich-status.md',
|
|
109
|
+
'opencode/commands/doctor.md',
|
|
110
|
+
'opencode/commands/init-project.md',
|
|
98
111
|
];
|
|
99
112
|
|
|
100
113
|
for (const tpl of commandTemplates) {
|
|
101
114
|
const rendered = renderTemplate(await readTemplate(packageRoot, tpl), placeholders);
|
|
102
115
|
const fileName = tpl.split('/').at(-1);
|
|
103
|
-
await writeIfMissingOrForced(join(globalDir, 'commands', fileName), rendered
|
|
116
|
+
await writeIfMissingOrForced(join(globalDir, 'commands', fileName), rendered);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
const enrichTemplate = renderTemplate(
|
|
107
120
|
await readTemplate(packageRoot, 'opencode/agent/enrich.md'),
|
|
108
121
|
placeholders,
|
|
109
122
|
);
|
|
110
|
-
await writeIfMissingOrForced(join(globalDir, 'agents', 'enrich.md'), enrichTemplate
|
|
123
|
+
await writeIfMissingOrForced(join(globalDir, 'agents', 'enrich.md'), enrichTemplate);
|
|
124
|
+
|
|
125
|
+
const pmcSkill = renderTemplate(
|
|
126
|
+
await readTemplate(packageRoot, 'pmc-skill/SKILL.md'),
|
|
127
|
+
placeholders,
|
|
128
|
+
);
|
|
129
|
+
await writeIfMissingOrForced(join(globalDir, 'skills', 'pmc-skill', 'SKILL.md'), pmcSkill);
|
|
111
130
|
|
|
112
131
|
const agentsMdPath = join(projectRoot, 'AGENTS.md');
|
|
113
132
|
const autostartBlock = renderTemplate(
|
|
@@ -145,11 +164,43 @@ async function installCursor({ projectRoot, packageRoot, placeholders }) {
|
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
async function installGeneric({ projectRoot, packageRoot, placeholders }) {
|
|
167
|
+
const marker = 'generic';
|
|
168
|
+
const readmePath = join(projectRoot, 'README-SETUP.md');
|
|
169
|
+
const statePath = join(projectRoot, '.pmc', 'generic-readme-installed');
|
|
148
170
|
const readme = renderTemplate(
|
|
149
171
|
await readTemplate(packageRoot, 'generic/README-SETUP.md'),
|
|
150
172
|
placeholders,
|
|
151
173
|
);
|
|
152
|
-
|
|
174
|
+
const block = stripBlockMarkers(readme, marker);
|
|
175
|
+
|
|
176
|
+
if (existsSync(statePath)) {
|
|
177
|
+
if (!existsSync(readmePath)) {
|
|
178
|
+
await writeFile(readmePath, wrapBlock(marker, block), 'utf8');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const existing = await readFile(readmePath, 'utf8');
|
|
183
|
+
if (hasBlockMarker(existing, marker)) {
|
|
184
|
+
await writeFile(readmePath, replaceOrAppendBlock(existing, marker, block), 'utf8');
|
|
185
|
+
} else {
|
|
186
|
+
await writeFile(readmePath, replaceOrAppendBlock(existing, marker, block), 'utf8');
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (existsSync(readmePath)) {
|
|
192
|
+
const existing = await readFile(readmePath, 'utf8');
|
|
193
|
+
if (existing.trim()) {
|
|
194
|
+
await writeFile(readmePath, replaceOrAppendBlock(existing, marker, block), 'utf8');
|
|
195
|
+
} else {
|
|
196
|
+
await writeFile(readmePath, wrapBlock(marker, block), 'utf8');
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
await writeFile(readmePath, wrapBlock(marker, block), 'utf8');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await mkdir(dirname(statePath), { recursive: true });
|
|
203
|
+
await writeFile(statePath, 'installed\n', 'utf8');
|
|
153
204
|
}
|
|
154
205
|
|
|
155
206
|
const INSTALLERS = {
|
|
@@ -169,11 +220,15 @@ export async function installAgentTemplates({
|
|
|
169
220
|
throw new Error(`Unsupported agent type: ${agent}. Supported: ${[...SUPPORTED_AGENTS].join(', ')}`);
|
|
170
221
|
}
|
|
171
222
|
|
|
223
|
+
if (agent === 'opencode' && !globalConfigDir) {
|
|
224
|
+
throw new Error('globalConfigDir is required for agent: opencode');
|
|
225
|
+
}
|
|
226
|
+
|
|
172
227
|
const pkgRoot = packageRoot ?? resolvePackageRoot();
|
|
173
228
|
const placeholders = await buildPlaceholders(projectRoot, pkgRoot);
|
|
174
229
|
|
|
175
230
|
await INSTALLERS[agent]({
|
|
176
|
-
globalConfigDir
|
|
231
|
+
globalConfigDir,
|
|
177
232
|
packageRoot: pkgRoot,
|
|
178
233
|
placeholders,
|
|
179
234
|
projectRoot,
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- pmc:init -->
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
PMC first, files second.
|
|
6
|
+
|
|
7
|
+
Query PMC before reading more than 3 files. Prefer PMC for architecture, symbol lookup, dependencies, and callers before reading raw source.
|
|
8
|
+
|
|
9
|
+
Available PMC MCP tools: `pmc_query_project`, `pmc_search_symbols`, `pmc_get_dependents`, `pmc_get_dependencies`.
|
|
10
|
+
|
|
11
|
+
`/get-context` resolves a target (symbol, file, or query) and returns structural context. Accepts optional depth (compact|extended|deep) and focus (all|dependencies|callers|containment|impact).
|
|
12
|
+
|
|
13
|
+
When the user types `/map-project`, run:
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
{{PMC_BIN}} bootstrap . --all --enrich
|
|
@@ -11,7 +19,25 @@ When the user types `/new-project`, run:
|
|
|
11
19
|
When the user types `/get-context`, run:
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
|
-
{{PMC_BIN}} context
|
|
22
|
+
{{PMC_BIN}} context <target> [depth] [focus]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When the user types `/enrich-status`, run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
{{PMC_BIN}} status .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When the user types `/doctor`, run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
{{PMC_BIN}} doctor
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When the user types `/init-project`, run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
{{PMC_BIN}} init .
|
|
15
41
|
```
|
|
16
42
|
|
|
17
43
|
When the user types `/sync-context`, process `.planning/project-memory-context/enrichment/sync-manifest.json` and upsert pending entries into `agent-memory`.
|
|
@@ -30,7 +56,7 @@ When starting a session in a project with `.planning/project-memory-context/`:
|
|
|
30
56
|
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
31
57
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
32
58
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run `/sync-context` to apply them."
|
|
33
|
-
5. Search agent-memory with `query: "project context overview"` to fetch base context.
|
|
59
|
+
5. Search agent-memory with `query: "project context overview"` and `tags: ["project-context"]` to fetch base context.
|
|
34
60
|
6. Remind the user: "Use `/get-context <target>` for structural deep-dive before reading files."
|
|
35
61
|
|
|
36
62
|
<!-- /pmc:init -->
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- pmc:init -->
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
PMC first, files second.
|
|
6
|
+
|
|
7
|
+
Query PMC before reading more than 3 files. Prefer PMC for architecture, symbol lookup, dependencies, and callers before reading raw source.
|
|
8
|
+
|
|
9
|
+
Available PMC MCP tools: `pmc_query_project`, `pmc_search_symbols`, `pmc_get_dependents`, `pmc_get_dependencies`.
|
|
10
|
+
|
|
11
|
+
`/get-context` resolves a target (symbol, file, or query) and returns structural context. Accepts optional depth (compact|extended|deep) and focus (all|dependencies|callers|containment|impact).
|
|
12
|
+
|
|
13
|
+
When the user types "/map-project", run:
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
{{PMC_BIN}} bootstrap . --all --enrich
|
|
@@ -11,7 +19,25 @@ When the user types "/new-project", run:
|
|
|
11
19
|
When the user types "/get-context", run:
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
|
-
{{PMC_BIN}} context
|
|
22
|
+
{{PMC_BIN}} context <target> [depth] [focus]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When the user types "/enrich-status", run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
{{PMC_BIN}} status .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When the user types "/doctor", run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
{{PMC_BIN}} doctor
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When the user types "/init-project", run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
{{PMC_BIN}} init .
|
|
15
41
|
```
|
|
16
42
|
|
|
17
43
|
When the user types "/sync-context", process `.planning/project-memory-context/enrichment/sync-manifest.json` and upsert pending entries into `agent-memory`.
|
|
@@ -30,7 +56,7 @@ When starting a session in a project with `.planning/project-memory-context/`:
|
|
|
30
56
|
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
31
57
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
32
58
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run /sync-context to apply them."
|
|
33
|
-
5. Search agent-memory with query "project context overview" to fetch base context.
|
|
59
|
+
5. Search agent-memory with query "project context overview" and tags ["project-context"] to fetch base context.
|
|
34
60
|
6. Remind the user: "Use /get-context <target> for structural deep-dive before reading files."
|
|
35
61
|
|
|
36
62
|
<!-- /pmc:init -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doctor
|
|
3
|
+
description: Run environment diagnostics — check Python, Ollama, Node, agent-memory, and graphify.
|
|
4
|
+
argument-hint: ""
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<objective>
|
|
10
|
+
Run the PMC environment doctor to verify all dependencies are correctly installed and configured.
|
|
11
|
+
</objective>
|
|
12
|
+
|
|
13
|
+
<execution>
|
|
14
|
+
Run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
{{PMC_BIN}} doctor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This checks: Node version, Python availability, graphifyy installation, Ollama connectivity, MEMORY_DB_PATH, and embedding cache path.
|
|
21
|
+
</execution>
|