@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aabadin/project-memory-context",
3
- "version": "0.1.4",
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",
@@ -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'],
@@ -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
+ }
@@ -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/new-project.md',
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, { force: true });
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, { force: true });
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
- await writeFile(join(projectRoot, 'README-SETUP.md'), readme, 'utf8');
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: globalConfigDir ?? join(projectRoot, '.pmc'),
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
- When the user types `/new-project`, run:
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 . --refresh
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
- When the user types "/new-project", run:
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 . --refresh
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>