@aabadin/project-memory-context 0.1.5 → 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/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 +56 -4
- package/templates/claude-code/CLAUDE.md.snippet +10 -2
- package/templates/cursor/.cursorrules.snippet +10 -2
- package/templates/opencode/commands/get-context.md +22 -5
- 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
|
|
|
@@ -103,14 +113,20 @@ async function installOpencode({ projectRoot, packageRoot, placeholders, globalC
|
|
|
103
113
|
for (const tpl of commandTemplates) {
|
|
104
114
|
const rendered = renderTemplate(await readTemplate(packageRoot, tpl), placeholders);
|
|
105
115
|
const fileName = tpl.split('/').at(-1);
|
|
106
|
-
await writeIfMissingOrForced(join(globalDir, 'commands', fileName), rendered
|
|
116
|
+
await writeIfMissingOrForced(join(globalDir, 'commands', fileName), rendered);
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
const enrichTemplate = renderTemplate(
|
|
110
120
|
await readTemplate(packageRoot, 'opencode/agent/enrich.md'),
|
|
111
121
|
placeholders,
|
|
112
122
|
);
|
|
113
|
-
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);
|
|
114
130
|
|
|
115
131
|
const agentsMdPath = join(projectRoot, 'AGENTS.md');
|
|
116
132
|
const autostartBlock = renderTemplate(
|
|
@@ -148,11 +164,43 @@ async function installCursor({ projectRoot, packageRoot, placeholders }) {
|
|
|
148
164
|
}
|
|
149
165
|
|
|
150
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');
|
|
151
170
|
const readme = renderTemplate(
|
|
152
171
|
await readTemplate(packageRoot, 'generic/README-SETUP.md'),
|
|
153
172
|
placeholders,
|
|
154
173
|
);
|
|
155
|
-
|
|
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');
|
|
156
204
|
}
|
|
157
205
|
|
|
158
206
|
const INSTALLERS = {
|
|
@@ -172,11 +220,15 @@ export async function installAgentTemplates({
|
|
|
172
220
|
throw new Error(`Unsupported agent type: ${agent}. Supported: ${[...SUPPORTED_AGENTS].join(', ')}`);
|
|
173
221
|
}
|
|
174
222
|
|
|
223
|
+
if (agent === 'opencode' && !globalConfigDir) {
|
|
224
|
+
throw new Error('globalConfigDir is required for agent: opencode');
|
|
225
|
+
}
|
|
226
|
+
|
|
175
227
|
const pkgRoot = packageRoot ?? resolvePackageRoot();
|
|
176
228
|
const placeholders = await buildPlaceholders(projectRoot, pkgRoot);
|
|
177
229
|
|
|
178
230
|
await INSTALLERS[agent]({
|
|
179
|
-
globalConfigDir
|
|
231
|
+
globalConfigDir,
|
|
180
232
|
packageRoot: pkgRoot,
|
|
181
233
|
placeholders,
|
|
182
234
|
projectRoot,
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- pmc:init -->
|
|
4
4
|
|
|
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
|
+
|
|
5
13
|
When the user types `/map-project`, run:
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -11,7 +19,7 @@ When the user types `/map-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]
|
|
15
23
|
```
|
|
16
24
|
|
|
17
25
|
When the user types `/enrich-status`, run:
|
|
@@ -48,7 +56,7 @@ When starting a session in a project with `.planning/project-memory-context/`:
|
|
|
48
56
|
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
49
57
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
50
58
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run `/sync-context` to apply them."
|
|
51
|
-
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.
|
|
52
60
|
6. Remind the user: "Use `/get-context <target>` for structural deep-dive before reading files."
|
|
53
61
|
|
|
54
62
|
<!-- /pmc:init -->
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- pmc:init -->
|
|
4
4
|
|
|
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
|
+
|
|
5
13
|
When the user types "/map-project", run:
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -11,7 +19,7 @@ When the user types "/map-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]
|
|
15
23
|
```
|
|
16
24
|
|
|
17
25
|
When the user types "/enrich-status", run:
|
|
@@ -48,7 +56,7 @@ When starting a session in a project with `.planning/project-memory-context/`:
|
|
|
48
56
|
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
49
57
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
50
58
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run /sync-context to apply them."
|
|
51
|
-
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.
|
|
52
60
|
6. Remind the user: "Use /get-context <target> for structural deep-dive before reading files."
|
|
53
61
|
|
|
54
62
|
<!-- /pmc:init -->
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: get-context
|
|
3
|
-
description: Retrieve structured project context
|
|
4
|
-
argument-hint: "<target>"
|
|
3
|
+
description: Retrieve structured project context for a target (symbol, file, or query).
|
|
4
|
+
argument-hint: "<target> [depth] [focus]"
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Bash
|
|
7
7
|
- Read
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
<objective>
|
|
11
|
-
Retrieve and display structured project context
|
|
11
|
+
Retrieve and display structured project context for the given target. Use before reading files to get a structural overview.
|
|
12
12
|
</objective>
|
|
13
13
|
|
|
14
14
|
<execution>
|
|
15
|
-
|
|
15
|
+
Auto mode (recommended):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
{{PMC_BIN}} context <target> [depth] [focus]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Explicit modes:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
{{PMC_BIN}} context symbol <target> [depth] [focus]
|
|
25
|
+
{{PMC_BIN}} context file <target> [depth] [focus]
|
|
26
|
+
{{PMC_BIN}} context query <target> [depth] [focus]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Depths: compact (default), extended, deep.
|
|
30
|
+
Focus modes: all (default), dependencies, callers, containment, impact.
|
|
31
|
+
|
|
32
|
+
Repository refresh (legacy):
|
|
16
33
|
|
|
17
34
|
```bash
|
|
18
35
|
{{PMC_BIN}} context . --refresh
|
|
19
36
|
```
|
|
20
37
|
|
|
21
|
-
This
|
|
38
|
+
This resets the 9 base project-context memories without a specific target.
|
|
22
39
|
</execution>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PMC-Aware Workflow
|
|
2
|
+
|
|
3
|
+
PMC first, files second.
|
|
4
|
+
|
|
5
|
+
Before reading more than 3 files, query PMC for structural context first. Use PMC to understand symbols, dependencies, and callers before falling back to raw file reads.
|
|
6
|
+
|
|
7
|
+
## Rule of thumb
|
|
8
|
+
|
|
9
|
+
- Query PMC before reading more than 3 files.
|
|
10
|
+
- Prefer PMC summaries for architecture, dependencies, and symbol lookup.
|
|
11
|
+
- Read source files after PMC when you need exact implementation details.
|
|
12
|
+
|
|
13
|
+
## Available commands
|
|
14
|
+
|
|
15
|
+
- `/map-project`
|
|
16
|
+
- `/get-context`
|
|
17
|
+
- `/enrich-status`
|
|
18
|
+
- `/doctor`
|
|
19
|
+
- `/init-project`
|
|
20
|
+
- `/sync-context`
|
|
21
|
+
- `/sanitize`
|
|
22
|
+
|
|
23
|
+
- `/get-context <target> [depth] [focus]`—resolve a symbol, file, or query and return structural context
|
|
24
|
+
|
|
25
|
+
## Available MCP tools
|
|
26
|
+
|
|
27
|
+
- `pmc_query_project`
|
|
28
|
+
- `pmc_search_symbols`
|
|
29
|
+
- `pmc_get_dependents`
|
|
30
|
+
- `pmc_get_dependencies`
|
|
31
|
+
|
|
32
|
+
## Why this saves tokens
|
|
33
|
+
|
|
34
|
+
PMC returns focused structural context, so the agent can avoid loading many full files into context. Querying graph-backed summaries first usually costs fewer tokens than broad file reads and helps reserve file inspection for the exact places that matter.
|