@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ensureProjectMemoryContextDirs, writeJsonArtifact } from './artifacts.mjs';
|
|
5
|
+
import { PMC_ENRICHMENT_CONFIG_FILE } from './enrichment-config.mjs';
|
|
6
|
+
import { resolveConfigDirs } from './platform.mjs';
|
|
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') return fallback;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildMcpConfig(installState) {
|
|
18
|
+
return {
|
|
19
|
+
mcpServers: {
|
|
20
|
+
'agent-memory': {
|
|
21
|
+
command: 'npx',
|
|
22
|
+
args: ['-y', '@aabadin/agent-memory-mcp'],
|
|
23
|
+
env: {
|
|
24
|
+
MEMORY_DB_PATH: installState.memoryDbPath,
|
|
25
|
+
...(installState.embeddingCachePath
|
|
26
|
+
? { EMBEDDING_CACHE_PATH: installState.embeddingCachePath }
|
|
27
|
+
: {}),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function writeMcpJson(projectRoot, installState) {
|
|
35
|
+
const mcpPath = join(projectRoot, '.mcp.json');
|
|
36
|
+
const existing = await readJson(mcpPath, {});
|
|
37
|
+
const merged = {
|
|
38
|
+
...existing,
|
|
39
|
+
mcpServers: {
|
|
40
|
+
...(existing.mcpServers ?? {}),
|
|
41
|
+
...buildMcpConfig(installState).mcpServers,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
await writeFile(mcpPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
|
|
45
|
+
return mcpPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ensureAgentConfigRegistration(projectRoot, installState) {
|
|
49
|
+
const { projectConfig } = resolveConfigDirs(projectRoot);
|
|
50
|
+
const agentDir = basename(projectConfig);
|
|
51
|
+
|
|
52
|
+
if (agentDir === '.opencode') {
|
|
53
|
+
const configPath = join(projectConfig, 'opencode.json');
|
|
54
|
+
const config = await readJson(configPath, { $schema: 'https://opencode.ai/config.json' });
|
|
55
|
+
const existing = Array.isArray(config.plugin) ? config.plugin : [];
|
|
56
|
+
if (!existing.includes('@aabadin/project-memory-context')) {
|
|
57
|
+
config.plugin = [...existing, '@aabadin/project-memory-context'];
|
|
58
|
+
}
|
|
59
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
60
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
61
|
+
// Also write .mcp.json so agent-memory is available immediately
|
|
62
|
+
await writeMcpJson(projectRoot, installState);
|
|
63
|
+
return configPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (agentDir === '.claude' || agentDir === '.cursor' || agentDir === '.pmc') {
|
|
67
|
+
// For Claude Code, Cursor, and generic PMC setups: write .mcp.json directly
|
|
68
|
+
const mcpPath = await writeMcpJson(projectRoot, installState);
|
|
69
|
+
// Also persist enrichment config
|
|
70
|
+
const enrichPath = join(projectConfig, PMC_ENRICHMENT_CONFIG_FILE);
|
|
71
|
+
const existingConfig = await readJson(enrichPath, {});
|
|
72
|
+
const config = {
|
|
73
|
+
...existingConfig,
|
|
74
|
+
enrichment: {
|
|
75
|
+
...existingConfig.enrichment,
|
|
76
|
+
localModel: {
|
|
77
|
+
...existingConfig.enrichment?.localModel,
|
|
78
|
+
baseUrl: installState.ollamaBaseUrl,
|
|
79
|
+
model: installState.ollamaModel,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
await mkdir(dirname(enrichPath), { recursive: true });
|
|
84
|
+
await writeFile(enrichPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
85
|
+
return mcpPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fallback: no agent dir detected — write .mcp.json at project root
|
|
89
|
+
return writeMcpJson(projectRoot, installState);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function copyTemplate(packageRoot, templateName, projectRoot) {
|
|
93
|
+
const source = join(packageRoot, 'templates', templateName);
|
|
94
|
+
const target = join(projectRoot, templateName);
|
|
95
|
+
const content = await readFile(source, 'utf8');
|
|
96
|
+
const rendered = content.replaceAll('__PMC_PACKAGE_ROOT__', packageRoot);
|
|
97
|
+
await writeFile(target, rendered, 'utf8');
|
|
98
|
+
return target;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function bootstrapProjectInstall({
|
|
102
|
+
projectRoot,
|
|
103
|
+
packageRoot,
|
|
104
|
+
ollamaBaseUrl,
|
|
105
|
+
ollamaModel,
|
|
106
|
+
embeddingCachePath,
|
|
107
|
+
}) {
|
|
108
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
109
|
+
const memoryDbPath = join(dirs.base, 'memory-db');
|
|
110
|
+
const installState = {
|
|
111
|
+
packageRoot,
|
|
112
|
+
ollamaBaseUrl,
|
|
113
|
+
ollamaModel,
|
|
114
|
+
memoryDbPath,
|
|
115
|
+
embeddingCachePath: embeddingCachePath ?? join(dirs.base, 'embedding-cache'),
|
|
116
|
+
installedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await mkdir(memoryDbPath, { recursive: true });
|
|
120
|
+
await writeJsonArtifact(join(dirs.base, 'install.json'), installState);
|
|
121
|
+
const configPath = await ensureAgentConfigRegistration(projectRoot, installState);
|
|
122
|
+
const commandPath = await copyTemplate(packageRoot, 'project-memory-context.md', projectRoot);
|
|
123
|
+
const workflowPath = await copyTemplate(packageRoot, 'project-memory-context workflow.md', projectRoot);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
installState,
|
|
127
|
+
configPath,
|
|
128
|
+
commandPath,
|
|
129
|
+
workflowPath,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { extractJsTsSymbols } from './extractors/js-ts-extractor.mjs';
|
|
2
|
+
import { extractRegexSymbols, EXTENSION_TO_LANGUAGE } from './extractors/regex-extractor.mjs';
|
|
3
|
+
import { buildSymbolKey } from './symbol-keys.mjs';
|
|
4
|
+
|
|
5
|
+
const JS_TS_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
6
|
+
|
|
7
|
+
export function extractTopLevelSymbols({ filePath, content }) {
|
|
8
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
9
|
+
if (JS_TS_EXTENSIONS.has(ext)) {
|
|
10
|
+
return extractJsTsSymbols({ filePath, content });
|
|
11
|
+
}
|
|
12
|
+
if (EXTENSION_TO_LANGUAGE.has(ext)) {
|
|
13
|
+
return extractRegexSymbols({ filePath, content });
|
|
14
|
+
}
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildEnrichmentWorklist({ symbols, symbolIndex }) {
|
|
19
|
+
return symbols.map((symbol) => {
|
|
20
|
+
const prior = symbolIndex[symbol.symbolKey];
|
|
21
|
+
const status = prior && prior.codeHash === symbol.codeHash ? 'enriched' : 'pending';
|
|
22
|
+
return {
|
|
23
|
+
...symbol,
|
|
24
|
+
status,
|
|
25
|
+
memoryId: prior?.memoryId ?? null,
|
|
26
|
+
graphNodeId: symbol.graphNodeId ?? prior?.graphNodeId ?? null,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function upsertSymbolIndexEntry(index, entry) {
|
|
2
|
+
return {
|
|
3
|
+
...index,
|
|
4
|
+
[entry.symbolKey]: {
|
|
5
|
+
memoryId: entry.memoryId ?? null,
|
|
6
|
+
graphNodeId: entry.graphNodeId ?? null,
|
|
7
|
+
codeHash: entry.codeHash,
|
|
8
|
+
status: entry.status,
|
|
9
|
+
lastEnrichedAt: entry.lastEnrichedAt ?? null,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function findGraphNodeIdByMemoryId(index, memoryId) {
|
|
15
|
+
for (const entry of Object.values(index)) {
|
|
16
|
+
if (entry.memoryId === memoryId) {
|
|
17
|
+
return entry.graphNodeId ?? null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function findMemoryIdByGraphNodeId(index, graphNodeId) {
|
|
24
|
+
for (const entry of Object.values(index)) {
|
|
25
|
+
if (entry.graphNodeId === graphNodeId) {
|
|
26
|
+
return entry.memoryId ?? null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function normalizePath(filePath) {
|
|
2
|
+
return String(filePath).replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildSymbolKey(symbol) {
|
|
6
|
+
const filePath = normalizePath(symbol.filePath);
|
|
7
|
+
|
|
8
|
+
if (symbol.language === 'csharp') {
|
|
9
|
+
return [
|
|
10
|
+
'csharp',
|
|
11
|
+
filePath,
|
|
12
|
+
symbol.namespace ?? 'global',
|
|
13
|
+
symbol.containerName ?? 'none',
|
|
14
|
+
symbol.kind,
|
|
15
|
+
symbol.name,
|
|
16
|
+
symbol.signature ?? '()',
|
|
17
|
+
].join('|');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [
|
|
21
|
+
symbol.language,
|
|
22
|
+
filePath,
|
|
23
|
+
symbol.kind,
|
|
24
|
+
symbol.exportScope ?? 'local',
|
|
25
|
+
symbol.name,
|
|
26
|
+
String(symbol.arity ?? 0),
|
|
27
|
+
].join('|');
|
|
28
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const MANIFEST_FILE = 'sync-manifest.json';
|
|
6
|
+
const manifestWriteLocks = new Map();
|
|
7
|
+
|
|
8
|
+
function manifestPath(enrichmentDir) {
|
|
9
|
+
return join(enrichmentDir, MANIFEST_FILE);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readSyncManifest(enrichmentDir) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(await readFile(manifestPath(enrichmentDir), 'utf8'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error && error.code !== 'ENOENT') {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return { entries: [] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function writeSyncManifest(enrichmentDir, manifest) {
|
|
24
|
+
const path = manifestPath(enrichmentDir);
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function withManifestLock(enrichmentDir, operation) {
|
|
30
|
+
const path = manifestPath(enrichmentDir);
|
|
31
|
+
const previous = manifestWriteLocks.get(path) ?? Promise.resolve();
|
|
32
|
+
const next = previous
|
|
33
|
+
.catch(() => {})
|
|
34
|
+
.then(operation);
|
|
35
|
+
manifestWriteLocks.set(path, next);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return await next;
|
|
39
|
+
} finally {
|
|
40
|
+
if (manifestWriteLocks.get(path) === next) {
|
|
41
|
+
manifestWriteLocks.delete(path);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createSyncEntry({ action, keyTag, content, category, tags, source, symbolKey }) {
|
|
47
|
+
return {
|
|
48
|
+
id: randomUUID(),
|
|
49
|
+
action,
|
|
50
|
+
key_tag: keyTag,
|
|
51
|
+
content: content || null,
|
|
52
|
+
category: category || 'architecture',
|
|
53
|
+
tags: tags || [],
|
|
54
|
+
status: 'pending',
|
|
55
|
+
source: source || 'unknown',
|
|
56
|
+
symbolKey: symbolKey || null,
|
|
57
|
+
addedAt: new Date().toISOString(),
|
|
58
|
+
syncedAt: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function appendSyncEntry(enrichmentDir, entry) {
|
|
63
|
+
return withManifestLock(enrichmentDir, async () => {
|
|
64
|
+
const manifest = await readSyncManifest(enrichmentDir);
|
|
65
|
+
manifest.entries.push(entry);
|
|
66
|
+
await writeSyncManifest(enrichmentDir, manifest);
|
|
67
|
+
return entry;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function appendSyncEntries(enrichmentDir, entries) {
|
|
72
|
+
return withManifestLock(enrichmentDir, async () => {
|
|
73
|
+
const manifest = await readSyncManifest(enrichmentDir);
|
|
74
|
+
manifest.entries.push(...entries);
|
|
75
|
+
await writeSyncManifest(enrichmentDir, manifest);
|
|
76
|
+
return entries;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getPendingEntries(manifest) {
|
|
81
|
+
return manifest.entries.filter(e => e.status === 'pending');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getPendingUpserts(manifest) {
|
|
85
|
+
return manifest.entries.filter(e => e.status === 'pending' && e.action === 'upsert');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getPendingDeletes(manifest) {
|
|
89
|
+
return manifest.entries.filter(e => e.status === 'pending' && e.action === 'delete');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function markEntriesSynced(enrichmentDir, ids) {
|
|
93
|
+
await withManifestLock(enrichmentDir, async () => {
|
|
94
|
+
const manifest = await readSyncManifest(enrichmentDir);
|
|
95
|
+
const idSet = new Set(ids);
|
|
96
|
+
for (const entry of manifest.entries) {
|
|
97
|
+
if (idSet.has(entry.id)) {
|
|
98
|
+
entry.status = 'synced';
|
|
99
|
+
entry.syncedAt = new Date().toISOString();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await writeSyncManifest(enrichmentDir, manifest);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function removeSyncedEntries(enrichmentDir) {
|
|
107
|
+
return withManifestLock(enrichmentDir, async () => {
|
|
108
|
+
const manifest = await readSyncManifest(enrichmentDir);
|
|
109
|
+
manifest.entries = manifest.entries.filter(e => e.status !== 'synced');
|
|
110
|
+
await writeSyncManifest(enrichmentDir, manifest);
|
|
111
|
+
return manifest.entries.length;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function clearManifest(enrichmentDir) {
|
|
116
|
+
await withManifestLock(enrichmentDir, async () => {
|
|
117
|
+
await writeSyncManifest(enrichmentDir, { entries: [] });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export { detectAgentType } from './platform.mjs';
|
|
7
|
+
|
|
8
|
+
export function renderTemplate(content, placeholders) {
|
|
9
|
+
return Object.entries(placeholders).reduce(
|
|
10
|
+
(text, [key, value]) => text.replaceAll(`{{${key}}}`, value),
|
|
11
|
+
content,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SUPPORTED_AGENTS = new Set(['opencode', 'claude-code', 'cursor', 'generic']);
|
|
16
|
+
|
|
17
|
+
function resolvePackageRoot() {
|
|
18
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadBinNames(packageRoot) {
|
|
22
|
+
const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
|
|
23
|
+
return {
|
|
24
|
+
PMC_BIN: Object.keys(packageJson.bin ?? {})[0] ?? 'pmc',
|
|
25
|
+
AGENT_MEMORY_CMD: 'npx -y @aabadin/agent-memory-mcp',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildPlaceholders(projectRoot, packageRoot) {
|
|
30
|
+
const binNames = await loadBinNames(packageRoot);
|
|
31
|
+
return {
|
|
32
|
+
...binNames,
|
|
33
|
+
PROJECT_ROOT: projectRoot,
|
|
34
|
+
CONFIG_DIR: '.pmc',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readTemplate(packageRoot, templatePath) {
|
|
39
|
+
return readFile(join(packageRoot, 'templates', templatePath), 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeIfMissingOrForced(filePath, content, options = {}) {
|
|
43
|
+
const { force = false } = options;
|
|
44
|
+
if (!force && existsSync(filePath)) return false;
|
|
45
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
46
|
+
await writeFile(filePath, content, 'utf8');
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasBlockMarker(content, marker) {
|
|
51
|
+
return content.includes(`<!-- pmc:${marker} -->`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function replaceOrAppendBlock(content, marker, block) {
|
|
55
|
+
const open = `<!-- pmc:${marker} -->`;
|
|
56
|
+
const close = `<!-- /pmc:${marker} -->`;
|
|
57
|
+
const regex = new RegExp(`${open}[\\s\\S]*?${close}`, 'g');
|
|
58
|
+
|
|
59
|
+
if (regex.test(content)) {
|
|
60
|
+
return content.replace(regex, `${open}\n${block}\n${close}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `${content}\n\n${open}\n${block}\n${close}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stripBlockMarkers(content, marker) {
|
|
67
|
+
return content
|
|
68
|
+
.replace(new RegExp(`<!-- pmc:${marker} -->|<!-- /pmc:${marker} -->`, 'g'), '')
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function installWithBlockMarker({ projectRoot, packageRoot, placeholders, targetFile, templatePath, marker = 'init' }) {
|
|
73
|
+
const targetPath = join(projectRoot, targetFile);
|
|
74
|
+
const snippet = renderTemplate(await readTemplate(packageRoot, templatePath), placeholders);
|
|
75
|
+
|
|
76
|
+
let existing = '';
|
|
77
|
+
if (existsSync(targetPath)) {
|
|
78
|
+
existing = await readFile(targetPath, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasBlockMarker(existing, marker)) {
|
|
82
|
+
const updated = replaceOrAppendBlock(existing, marker, stripBlockMarkers(snippet, marker));
|
|
83
|
+
await writeFile(targetPath, updated, 'utf8');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await writeFile(targetPath, snippet, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function installOpencode({ projectRoot, packageRoot, placeholders, globalConfigDir }) {
|
|
91
|
+
const globalDir = globalConfigDir;
|
|
92
|
+
|
|
93
|
+
const commandTemplates = [
|
|
94
|
+
'opencode/commands/new-project.md',
|
|
95
|
+
'opencode/commands/get-context.md',
|
|
96
|
+
'opencode/commands/sync-context.md',
|
|
97
|
+
'opencode/commands/sanitize.md',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const tpl of commandTemplates) {
|
|
101
|
+
const rendered = renderTemplate(await readTemplate(packageRoot, tpl), placeholders);
|
|
102
|
+
const fileName = tpl.split('/').at(-1);
|
|
103
|
+
await writeIfMissingOrForced(join(globalDir, 'commands', fileName), rendered, { force: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const enrichTemplate = renderTemplate(
|
|
107
|
+
await readTemplate(packageRoot, 'opencode/agent/enrich.md'),
|
|
108
|
+
placeholders,
|
|
109
|
+
);
|
|
110
|
+
await writeIfMissingOrForced(join(globalDir, 'agent', 'enrich.md'), enrichTemplate, { force: true });
|
|
111
|
+
|
|
112
|
+
const agentsMdPath = join(projectRoot, 'AGENTS.md');
|
|
113
|
+
const autostartBlock = renderTemplate(
|
|
114
|
+
await readTemplate(packageRoot, 'opencode/autostart-snippet.md'),
|
|
115
|
+
placeholders,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
let existing = '';
|
|
119
|
+
if (existsSync(agentsMdPath)) {
|
|
120
|
+
existing = await readFile(agentsMdPath, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const updated = replaceOrAppendBlock(existing, 'autostart', autostartBlock.trim());
|
|
124
|
+
await writeFile(agentsMdPath, updated, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function installClaudeCode({ projectRoot, packageRoot, placeholders }) {
|
|
128
|
+
await installWithBlockMarker({
|
|
129
|
+
packageRoot,
|
|
130
|
+
placeholders,
|
|
131
|
+
projectRoot,
|
|
132
|
+
targetFile: 'CLAUDE.md',
|
|
133
|
+
templatePath: 'claude-code/CLAUDE.md.snippet',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function installCursor({ projectRoot, packageRoot, placeholders }) {
|
|
138
|
+
await installWithBlockMarker({
|
|
139
|
+
packageRoot,
|
|
140
|
+
placeholders,
|
|
141
|
+
projectRoot,
|
|
142
|
+
targetFile: '.cursorrules',
|
|
143
|
+
templatePath: 'cursor/.cursorrules.snippet',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function installGeneric({ projectRoot, packageRoot, placeholders }) {
|
|
148
|
+
const readme = renderTemplate(
|
|
149
|
+
await readTemplate(packageRoot, 'generic/README-SETUP.md'),
|
|
150
|
+
placeholders,
|
|
151
|
+
);
|
|
152
|
+
await writeFile(join(projectRoot, 'README-SETUP.md'), readme, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const INSTALLERS = {
|
|
156
|
+
'claude-code': installClaudeCode,
|
|
157
|
+
cursor: installCursor,
|
|
158
|
+
generic: installGeneric,
|
|
159
|
+
opencode: installOpencode,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export async function installAgentTemplates({
|
|
163
|
+
projectRoot,
|
|
164
|
+
agent,
|
|
165
|
+
packageRoot,
|
|
166
|
+
globalConfigDir,
|
|
167
|
+
}) {
|
|
168
|
+
if (!SUPPORTED_AGENTS.has(agent)) {
|
|
169
|
+
throw new Error(`Unsupported agent type: ${agent}. Supported: ${[...SUPPORTED_AGENTS].join(', ')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pkgRoot = packageRoot ?? resolvePackageRoot();
|
|
173
|
+
const placeholders = await buildPlaceholders(projectRoot, pkgRoot);
|
|
174
|
+
|
|
175
|
+
await INSTALLERS[agent]({
|
|
176
|
+
globalConfigDir: globalConfigDir ?? join(projectRoot, '.pmc'),
|
|
177
|
+
packageRoot: pkgRoot,
|
|
178
|
+
placeholders,
|
|
179
|
+
projectRoot,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# PMC Commands
|
|
2
|
+
|
|
3
|
+
<!-- pmc:init -->
|
|
4
|
+
|
|
5
|
+
When the user types `/new-project`, run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
{{PMC_BIN}} bootstrap . --all --enrich
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
When the user types `/get-context`, run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
{{PMC_BIN}} context . --refresh
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
When the user types `/sync-context`, process `.planning/project-memory-context/enrichment/sync-manifest.json` and upsert pending entries into `agent-memory`.
|
|
18
|
+
|
|
19
|
+
When the user types `/sanitize`, run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
{{PMC_BIN}} sanitize .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Session autostart
|
|
26
|
+
|
|
27
|
+
When starting a session in a project with `.planning/project-memory-context/`:
|
|
28
|
+
|
|
29
|
+
1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
|
|
30
|
+
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
31
|
+
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
32
|
+
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.
|
|
34
|
+
6. Remind the user: "Use `/get-context <target>` for structural deep-dive before reading files."
|
|
35
|
+
|
|
36
|
+
<!-- /pmc:init -->
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# PMC Commands
|
|
2
|
+
|
|
3
|
+
<!-- pmc:init -->
|
|
4
|
+
|
|
5
|
+
When the user types "/new-project", run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
{{PMC_BIN}} bootstrap . --all --enrich
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
When the user types "/get-context", run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
{{PMC_BIN}} context . --refresh
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
When the user types "/sync-context", process `.planning/project-memory-context/enrichment/sync-manifest.json` and upsert pending entries into `agent-memory`.
|
|
18
|
+
|
|
19
|
+
When the user types "/sanitize", run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
{{PMC_BIN}} sanitize .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Session autostart
|
|
26
|
+
|
|
27
|
+
When starting a session in a project with `.planning/project-memory-context/`:
|
|
28
|
+
|
|
29
|
+
1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
|
|
30
|
+
2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
|
|
31
|
+
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
32
|
+
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.
|
|
34
|
+
6. Remind the user: "Use /get-context <target> for structural deep-dive before reading files."
|
|
35
|
+
|
|
36
|
+
<!-- /pmc:init -->
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# PMC Setup Guide
|
|
2
|
+
|
|
3
|
+
This project uses PMC (Project Memory Context) for persistent structured memory.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Bootstrap the project (graphify + worklist + base memories)
|
|
9
|
+
{{PMC_BIN}} bootstrap . --all --enrich
|
|
10
|
+
|
|
11
|
+
# Check enrichment status
|
|
12
|
+
{{PMC_BIN}} status .
|
|
13
|
+
|
|
14
|
+
# Run semantic enrichment for pending symbols
|
|
15
|
+
{{PMC_BIN}} enrich .
|
|
16
|
+
|
|
17
|
+
# Refresh project context memories
|
|
18
|
+
{{PMC_BIN}} context . --refresh
|
|
19
|
+
|
|
20
|
+
# Sanitize (re-run graphify, mark stale entries)
|
|
21
|
+
{{PMC_BIN}} sanitize .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## How It Works
|
|
25
|
+
|
|
26
|
+
1. **Bootstrap** runs `graphify` to map your codebase structure, extracts symbols, and creates an enrichment worklist.
|
|
27
|
+
2. **Enrich** processes each symbol through a semantic enrichment pipeline (local model -> cloud API -> agent subagent fallback chain).
|
|
28
|
+
3. **Sync** pushes enriched memories to `agent-memory-mcp` for persistent retrieval.
|
|
29
|
+
4. **Context** materializes 9 base project-context memories (stack, architecture, dependencies, etc.).
|
|
30
|
+
|
|
31
|
+
## Files
|
|
32
|
+
|
|
33
|
+
- `.planning/project-memory-context/` — all PMC data lives here
|
|
34
|
+
- `.planning/project-memory-context/enrichment/worklist.json` — symbol enrichment queue
|
|
35
|
+
- `.planning/project-memory-context/enrichment/sync-manifest.json` — pending agent-memory upserts
|
|
36
|
+
- `.planning/project-memory-context/graph/` — graphify output
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Node.js >= 18
|
|
41
|
+
- Python + `graphifyy` (`pip install graphifyy`)
|
|
42
|
+
- `{{AGENT_MEMORY_CMD}}` (optional, for persistent memory)
|
|
43
|
+
|
|
44
|
+
## CLI Reference
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
{{PMC_BIN}} init [--agent opencode|claude-code|cursor|generic]
|
|
48
|
+
{{PMC_BIN}} bootstrap [dir] [--all] [--enrich]
|
|
49
|
+
{{PMC_BIN}} enrich [dir] [--concurrency N]
|
|
50
|
+
{{PMC_BIN}} context [dir] [--refresh]
|
|
51
|
+
{{PMC_BIN}} sanitize [dir]
|
|
52
|
+
{{PMC_BIN}} status [dir]
|
|
53
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: enrich
|
|
3
|
+
description: Run batch semantic enrichment for pending symbols using the fallback chain.
|
|
4
|
+
argument-hint: "[--concurrency N]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Bash
|
|
7
|
+
- Read
|
|
8
|
+
- Write
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<objective>
|
|
12
|
+
Run batch semantic enrichment for all pending symbols in the worklist. Uses the fallback chain: local-model -> cloud-api -> agent-subagent.
|
|
13
|
+
</objective>
|
|
14
|
+
|
|
15
|
+
<execution>
|
|
16
|
+
Run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
{{PMC_BIN}} enrich . --concurrency 3
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Each symbol gets enriched with a semantic report describing responsibility, inputs, outputs, dependencies, and role. Results are persisted and the sync-manifest is updated.
|
|
23
|
+
</execution>
|
|
24
|
+
|
|
25
|
+
<success_criteria>
|
|
26
|
+
- All pending symbols processed (enriched or marked as error)
|
|
27
|
+
- Sync-manifest updated with new entries
|
|
28
|
+
</success_criteria>
|