@aabadin/project-memory-context 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +123 -0
- package/bin/pmc.mjs +11 -0
- package/cli/apply-enrichment-result.mjs +31 -0
- package/cli/batch-enrich.mjs +5 -0
- package/cli/bootstrap.mjs +357 -0
- package/cli/build-worklist.mjs +35 -0
- package/cli/context.mjs +49 -0
- package/cli/doctor.mjs +29 -0
- package/cli/enrich-batch.mjs +11 -0
- package/cli/enrich-loop.sh +117 -0
- package/cli/enrich-orchestrator.mjs +5 -0
- package/cli/enrich-queue.mjs +525 -0
- package/cli/enrich-sync.mjs +5 -0
- package/cli/enrich.mjs +51 -0
- package/cli/fail-enrichment.mjs +28 -0
- package/cli/finalize-enrichment.mjs +25 -0
- package/cli/init.mjs +66 -0
- package/cli/install-pmc.mjs +153 -0
- package/cli/materialize-enrichment-artifacts.mjs +38 -0
- package/cli/new-project.mjs +41 -0
- package/cli/prepare-semantic-jobs.mjs +8 -0
- package/cli/project-context.mjs +224 -0
- package/cli/sanitize.mjs +235 -0
- package/cli/save-intake-context.mjs +22 -0
- package/cli/setup.mjs +80 -0
- package/cli/status.mjs +81 -0
- package/mcp/local-model-server.mjs +74 -0
- package/package.json +60 -0
- package/plugin/index.mjs +27 -0
- package/src/artifacts.mjs +39 -0
- package/src/change-detector.mjs +10 -0
- package/src/command-dispatch.mjs +84 -0
- package/src/declared-intake.mjs +25 -0
- package/src/doctor.mjs +114 -0
- package/src/enrichment-artifacts.mjs +67 -0
- package/src/enrichment-attempts.mjs +17 -0
- package/src/enrichment-config.mjs +121 -0
- package/src/enrichment-driver.mjs +167 -0
- package/src/enrichment-errors.mjs +46 -0
- package/src/enrichment-linker.mjs +29 -0
- package/src/extractors/architecture-extractor.mjs +8 -0
- package/src/extractors/js-ts-extractor.mjs +118 -0
- package/src/extractors/regex-extractor.mjs +439 -0
- package/src/extractors/rules-extractor.mjs +9 -0
- package/src/extractors/stack-extractor.mjs +48 -0
- package/src/extractors/structure-extractor.mjs +31 -0
- package/src/fail-enrichment.mjs +33 -0
- package/src/finalize-enrichment.mjs +30 -0
- package/src/graph-backfill.mjs +35 -0
- package/src/graph-node-resolver.mjs +64 -0
- package/src/index.mjs +2 -0
- package/src/intake-context.mjs +16 -0
- package/src/invalidation-matrix.mjs +33 -0
- package/src/markdown-renderer.mjs +27 -0
- package/src/materializer.mjs +128 -0
- package/src/memory-payload.mjs +55 -0
- package/src/persist-enrichment-result.mjs +33 -0
- package/src/platform.mjs +111 -0
- package/src/plugin-config.mjs +17 -0
- package/src/prepare-semantic-jobs.mjs +33 -0
- package/src/project-context-schema.mjs +57 -0
- package/src/providers/cloud-api-provider.mjs +88 -0
- package/src/providers/local-model-provider.mjs +67 -0
- package/src/refresh-state.mjs +21 -0
- package/src/result-input.mjs +9 -0
- package/src/retrieval/context-renderer.mjs +97 -0
- package/src/retrieval/query-engine.mjs +230 -0
- package/src/semantic-report.mjs +26 -0
- package/src/semantic-unit.mjs +74 -0
- package/src/setup-bootstrap.mjs +131 -0
- package/src/symbol-extractor.mjs +29 -0
- package/src/symbol-index.mjs +30 -0
- package/src/symbol-keys.mjs +28 -0
- package/src/sync-manifest.mjs +119 -0
- package/src/template-installer.mjs +181 -0
- package/src/worklist-state.mjs +12 -0
- package/templates/claude-code/CLAUDE.md.snippet +36 -0
- package/templates/cursor/.cursorrules.snippet +36 -0
- package/templates/generic/README-SETUP.md +53 -0
- package/templates/opencode/agent/enrich.md +28 -0
- package/templates/opencode/autostart-snippet.md +13 -0
- package/templates/opencode/commands/get-context.md +22 -0
- package/templates/opencode/commands/new-project.md +32 -0
- package/templates/opencode/commands/sanitize.md +21 -0
- package/templates/opencode/commands/sync-context.md +22 -0
- package/templates/project-memory-context workflow.md +129 -0
- package/templates/project-memory-context.md +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @aabadin/project-memory-context
|
|
2
|
+
|
|
3
|
+
Portable project memory context CLI — bootstraps semantic enrichment workflows for any AI coding agent.
|
|
4
|
+
|
|
5
|
+
This package installs and wires together:
|
|
6
|
+
|
|
7
|
+
- a project bootstrapper (`pmc bootstrap` / `pmc setup`)
|
|
8
|
+
- a local semantic MCP backed by Ollama (`pmc-local-model`)
|
|
9
|
+
- an `agent-memory` MCP registration using `npx -y @aabadin/agent-memory-mcp`
|
|
10
|
+
- command/workflow templates for `project-memory-context`
|
|
11
|
+
- helper CLIs and artifact management for the semantic enrichment loop
|
|
12
|
+
|
|
13
|
+
## What it installs for you
|
|
14
|
+
|
|
15
|
+
The setup flow is designed to resolve everything automatically except:
|
|
16
|
+
|
|
17
|
+
- installing Ollama itself
|
|
18
|
+
- pulling/downloading your local model
|
|
19
|
+
|
|
20
|
+
The setup flow handles:
|
|
21
|
+
|
|
22
|
+
- asking for `OLLAMA_BASE_URL`
|
|
23
|
+
- asking for `OLLAMA_MODEL`
|
|
24
|
+
- installing `graphifyy` with Python/pip
|
|
25
|
+
- creating `.planning/project-memory-context/`
|
|
26
|
+
- writing project install state
|
|
27
|
+
- registering MCP servers in `.mcp.json`
|
|
28
|
+
- registering the plugin in `.opencode/opencode.json` when using OpenCode
|
|
29
|
+
- copying `project-memory-context.md` and `project-memory-context workflow.md` into the target project
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Install the package globally, or run it with `npx`, then run setup inside the target repository.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g @aabadin/project-memory-context
|
|
37
|
+
pmc setup
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Without a global install:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx @aabadin/project-memory-context setup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Setup prompts
|
|
47
|
+
|
|
48
|
+
`pmc setup` asks for:
|
|
49
|
+
|
|
50
|
+
- Ollama base URL, default `http://localhost:11434`
|
|
51
|
+
- Ollama model name, default `deepseek-coder-v2:16b-ctx32k`
|
|
52
|
+
|
|
53
|
+
It then attempts to install:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m pip install graphifyy
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or an equivalent Python launcher depending on platform.
|
|
60
|
+
|
|
61
|
+
## Resulting project files
|
|
62
|
+
|
|
63
|
+
After setup, the target project contains:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
.mcp.json
|
|
67
|
+
.planning/project-memory-context/install.json
|
|
68
|
+
project-memory-context.md
|
|
69
|
+
project-memory-context workflow.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Runtime layout
|
|
73
|
+
|
|
74
|
+
During workflow execution, artifacts accumulate under:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
.planning/project-memory-context/
|
|
78
|
+
intake/
|
|
79
|
+
graph/
|
|
80
|
+
enrichment/
|
|
81
|
+
runs/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## MCP servers
|
|
85
|
+
|
|
86
|
+
The bootstrap flow writes `.mcp.json` with an `agent-memory` server that runs:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx -y @aabadin/agent-memory-mcp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
OpenCode plugin integration may also inject local MCP entries at runtime using values from `.planning/project-memory-context/install.json`:
|
|
93
|
+
|
|
94
|
+
- `pmc-local-model`
|
|
95
|
+
- `pmc-agent-memory`
|
|
96
|
+
|
|
97
|
+
`pmc-local-model` exposes a semantic report tool backed by Ollama.
|
|
98
|
+
|
|
99
|
+
`pmc-agent-memory` points at the public `agent-memory-mcp` binary when plugin injection is used.
|
|
100
|
+
|
|
101
|
+
## Workflow order
|
|
102
|
+
|
|
103
|
+
1. `stage-a`
|
|
104
|
+
- intake
|
|
105
|
+
- brainstorming clarification
|
|
106
|
+
- graphify structural mapping
|
|
107
|
+
|
|
108
|
+
2. `stage-b`
|
|
109
|
+
- build worklist
|
|
110
|
+
- prepare semantic jobs
|
|
111
|
+
- call `pmc-local-model`
|
|
112
|
+
- materialize `*.memory.json`
|
|
113
|
+
- store/update in `pmc-agent-memory`
|
|
114
|
+
- materialize `*.result.json`
|
|
115
|
+
- finalize or fail each symbol
|
|
116
|
+
|
|
117
|
+
## Development verification
|
|
118
|
+
|
|
119
|
+
Run the local test suite:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node --test "tools/project-memory-context/tests/*.test.mjs"
|
|
123
|
+
```
|
package/bin/pmc.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCommand } from '../src/command-dispatch.mjs';
|
|
3
|
+
|
|
4
|
+
const exitCode = await runCommand(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
console.error('[pmc] FATAL:', error.message);
|
|
6
|
+
return 1;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (exitCode !== 0) {
|
|
10
|
+
process.exit(exitCode);
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { persistEnrichmentResult } from '../src/persist-enrichment-result.mjs';
|
|
5
|
+
import { loadResultInput } from '../src/result-input.mjs';
|
|
6
|
+
|
|
7
|
+
const [, , rawJson] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!rawJson) {
|
|
10
|
+
console.error('Usage: node apply-enrichment-result.mjs "{...json result...}"');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await loadResultInput(rawJson);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(`Invalid JSON payload: ${error.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const projectRoot = resolve(process.cwd());
|
|
23
|
+
const persisted = await persistEnrichmentResult({ projectRoot, result });
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
saved: true,
|
|
26
|
+
graphFile: persisted.graphFile,
|
|
27
|
+
indexFile: persisted.indexFile,
|
|
28
|
+
symbolKey: result.symbolKey,
|
|
29
|
+
memoryId: result.memoryId ?? null,
|
|
30
|
+
graphNodeId: result.graphNodeId ?? null,
|
|
31
|
+
}, null, 2));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
console.error('[batch-enrich] DEPRECATED: This script hardcodes Ollama. Use enrich-queue.mjs instead.');
|
|
3
|
+
console.error('[batch-enrich] The enrich-queue supports the shared fallback driver (local-model → cloud-api → agent-subagent).');
|
|
4
|
+
console.error('[batch-enrich] Run: node tools/project-memory-context/cli/enrich-queue.mjs');
|
|
5
|
+
process.exit(1);
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { access, constants, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
import { existsSync, readdirSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { bootstrapProjectInstall } from '../src/setup-bootstrap.mjs';
|
|
9
|
+
import { resolveGraphify, spawnBackground, resolvePythonBin } from '../src/platform.mjs';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PMC_CLI_ROOT = resolve(__dirname);
|
|
13
|
+
const PMC_ROOT = resolve(__dirname, '..', '..', '..');
|
|
14
|
+
const PMC_PACKAGE_ROOT = resolve(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
|
|
17
|
+
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'deepseek-coder-v2:16b-ctx32k';
|
|
18
|
+
const PMC_CONCURRENCY = parseInt(process.env.PMC_CONCURRENCY || '8', 10);
|
|
19
|
+
|
|
20
|
+
function log(msg) { console.error(`[bootstrap] ${msg}`); }
|
|
21
|
+
|
|
22
|
+
export function buildDefaultEnrichmentConfig() {
|
|
23
|
+
return {
|
|
24
|
+
preferredModes: ['local-model', 'cloud-api', 'agent-subagent'],
|
|
25
|
+
localModel: {
|
|
26
|
+
provider: 'ollama',
|
|
27
|
+
baseUrl: OLLAMA_URL,
|
|
28
|
+
model: OLLAMA_MODEL,
|
|
29
|
+
},
|
|
30
|
+
cloudApi: {
|
|
31
|
+
provider: 'openai-compatible',
|
|
32
|
+
baseUrl: '',
|
|
33
|
+
model: '',
|
|
34
|
+
apiKeyEnv: 'PMC_CLOUD_API_KEY',
|
|
35
|
+
},
|
|
36
|
+
agentSubagent: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
agentName: 'enrich',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergeEnrichmentConfig(current = {}) {
|
|
44
|
+
current = current ?? {};
|
|
45
|
+
const source = current.enrichment ?? current;
|
|
46
|
+
const defaults = buildDefaultEnrichmentConfig();
|
|
47
|
+
return {
|
|
48
|
+
...defaults,
|
|
49
|
+
...source,
|
|
50
|
+
preferredModes: source.preferredModes ?? defaults.preferredModes,
|
|
51
|
+
localModel: {
|
|
52
|
+
...defaults.localModel,
|
|
53
|
+
...source.localModel,
|
|
54
|
+
},
|
|
55
|
+
cloudApi: {
|
|
56
|
+
...defaults.cloudApi,
|
|
57
|
+
...source.cloudApi,
|
|
58
|
+
},
|
|
59
|
+
agentSubagent: {
|
|
60
|
+
...defaults.agentSubagent,
|
|
61
|
+
...source.agentSubagent,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getTargetDir(args) {
|
|
67
|
+
for (const arg of args) {
|
|
68
|
+
if (arg === '--help' || arg === '-h') return null;
|
|
69
|
+
if (!arg.startsWith('--')) return resolve(arg);
|
|
70
|
+
}
|
|
71
|
+
return process.cwd();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function ensureDir(dir) {
|
|
75
|
+
try { await access(dir, constants.F_OK); return true; }
|
|
76
|
+
catch { return false; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readJson(filePath) {
|
|
80
|
+
try { return JSON.parse(await readFile(filePath, 'utf8')); }
|
|
81
|
+
catch { return null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function installGraphify() {
|
|
85
|
+
log('Installing graphify (Python)...');
|
|
86
|
+
const pythonBin = resolvePythonBin();
|
|
87
|
+
const r = spawnSync(pythonBin, ['-m', 'pip', 'install', 'graphifyy'], { stdio: 'inherit' });
|
|
88
|
+
if (r.status === 0) {
|
|
89
|
+
log(`graphifyy installed via ${pythonBin}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
log('WARNING: graphifyy install failed. Graphify step may not work.');
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function syncToolsToTarget(projectRoot) {
|
|
97
|
+
log('Copying PMC tools to target repo...');
|
|
98
|
+
const srcCli = resolve(PMC_ROOT, 'tools', 'project-memory-context', 'cli');
|
|
99
|
+
const dstCli = resolve(projectRoot, 'tools', 'project-memory-context', 'cli');
|
|
100
|
+
|
|
101
|
+
mkdirSync(resolve(projectRoot, 'tools', 'project-memory-context'), { recursive: true });
|
|
102
|
+
mkdirSync(dstCli, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const files = ['new-project.mjs', 'bootstrap.mjs', 'enrich-queue.mjs', 'build-worklist.mjs', 'project-context.mjs', 'sanitize.mjs'];
|
|
105
|
+
for (const f of files) {
|
|
106
|
+
try {
|
|
107
|
+
copyFileSync(resolve(srcCli, f), resolve(dstCli, f));
|
|
108
|
+
log(` copied ${f}`);
|
|
109
|
+
} catch {
|
|
110
|
+
log(` skipped ${f} (not found in source)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const srcSrc = resolve(PMC_ROOT, 'tools', 'project-memory-context', 'src');
|
|
115
|
+
const dstSrc = resolve(projectRoot, 'tools', 'project-memory-context', 'src');
|
|
116
|
+
mkdirSync(dstSrc, { recursive: true });
|
|
117
|
+
|
|
118
|
+
function copyTree(srcDir, dstDir) {
|
|
119
|
+
mkdirSync(dstDir, { recursive: true });
|
|
120
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const srcPath = resolve(srcDir, entry.name);
|
|
123
|
+
const dstPath = resolve(dstDir, entry.name);
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
copyTree(srcPath, dstPath);
|
|
126
|
+
} else if (entry.isFile() && entry.name.endsWith('.mjs')) {
|
|
127
|
+
copyFileSync(srcPath, dstPath);
|
|
128
|
+
log(` copied ${dstPath.replace(`${projectRoot}\\`, '').replace(/\\/g, '/')}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
copyTree(srcSrc, dstSrc);
|
|
135
|
+
} catch { log(' src/ copy skipped (may not exist)'); }
|
|
136
|
+
|
|
137
|
+
log(' PMC tools synced to target repo.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runStageA(projectRoot) {
|
|
141
|
+
log('Running graphify update (structural AST analysis, no LLM)...');
|
|
142
|
+
|
|
143
|
+
let graphifyExe;
|
|
144
|
+
try {
|
|
145
|
+
graphifyExe = resolveGraphify();
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log(` ${e.message}`);
|
|
148
|
+
log(' Stage-a skipped. Install graphify with `pip install graphifyy` or set PMC_GRAPHIFY_PATH.');
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const graphOutDir = resolve(projectRoot, '.planning', 'project-memory-context', 'graph');
|
|
153
|
+
const graphifyOutDir = resolve(projectRoot, 'graphify-out');
|
|
154
|
+
|
|
155
|
+
const r = spawnSync(`"${graphifyExe}"`, ['update', projectRoot], {
|
|
156
|
+
cwd: projectRoot,
|
|
157
|
+
stdio: 'inherit',
|
|
158
|
+
shell: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (r.status === 0) {
|
|
162
|
+
log(' Graph extraction complete. Copying to .planning...');
|
|
163
|
+
try {
|
|
164
|
+
if (!existsSync(graphifyOutDir)) { log(' graphify-out not found'); return false; }
|
|
165
|
+
const files = readdirSync(graphifyOutDir);
|
|
166
|
+
for (const f of files) {
|
|
167
|
+
if (f === 'graph.json' || f === 'graph.metadata.json' || f === 'graph.html' || f === 'GRAPH_REPORT.md') {
|
|
168
|
+
copyFileSync(resolve(graphifyOutDir, f), resolve(graphOutDir, f));
|
|
169
|
+
log(` copied ${f}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { log(` Copy error: ${e.message}`); }
|
|
173
|
+
log(' Graphify update complete (AST only, no semantic LLM).');
|
|
174
|
+
log(' For full semantic enrichment, set ANTHROPIC_API_KEY and run graphify extract.');
|
|
175
|
+
} else {
|
|
176
|
+
log(` Graphify update failed with code ${r.status}. Stage-b and enrichment still work.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findFiles(dir, exts, ignore, projectRoot) {
|
|
183
|
+
const results = [];
|
|
184
|
+
try {
|
|
185
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
const full = resolve(dir, entry.name);
|
|
188
|
+
if (ignore.some(i => full.includes(i))) continue;
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
results.push(...findFiles(full, exts, ignore, projectRoot));
|
|
191
|
+
} else if (exts.some(e => entry.name.endsWith(e))) {
|
|
192
|
+
results.push(full.replace(`${projectRoot}\\`, '').replace(`${projectRoot}/`, '').replace(/\\/g, '/'));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch { /* skip inaccessible dirs */ }
|
|
196
|
+
return results;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function runStageB(projectRoot) {
|
|
200
|
+
log('Running stage-b (build-worklist)...');
|
|
201
|
+
|
|
202
|
+
const ignore = ['node_modules', 'dist', '.git', 'bin', 'obj', '.opencode', '.planning'];
|
|
203
|
+
const files = [
|
|
204
|
+
...findFiles(projectRoot, ['.ts', '.mjs', '.js'], ignore, projectRoot),
|
|
205
|
+
...findFiles(projectRoot, ['.cs'], ignore, projectRoot),
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
log(` Found ${files.filter(f => f.endsWith('.cs')).length} CS files, ${files.filter(f => !f.endsWith('.cs')).length} TS/JS files`);
|
|
209
|
+
|
|
210
|
+
if (files.length === 0) {
|
|
211
|
+
log('No files to process. Skipping stage-b.');
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const worklistScript = resolve(PMC_CLI_ROOT, 'build-worklist.mjs');
|
|
216
|
+
const r = spawnSync('node', [worklistScript, ...files], {
|
|
217
|
+
cwd: projectRoot,
|
|
218
|
+
stdio: 'inherit',
|
|
219
|
+
env: { ...process.env, PMC_CONCURRENCY: String(PMC_CONCURRENCY) },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (r.status !== 0) {
|
|
223
|
+
const altWorklistScript = resolve(projectRoot, 'tools', 'project-memory-context', 'cli', 'build-worklist.mjs');
|
|
224
|
+
log(` Primary worklist failed, trying local: ${altWorklistScript}`);
|
|
225
|
+
const r2 = spawnSync('node', [altWorklistScript, ...files], {
|
|
226
|
+
cwd: projectRoot,
|
|
227
|
+
stdio: 'inherit',
|
|
228
|
+
env: { ...process.env, PMC_CONCURRENCY: String(PMC_CONCURRENCY) },
|
|
229
|
+
});
|
|
230
|
+
if (r2.status !== 0) {
|
|
231
|
+
log(`Stage-b failed with code ${r2.status}`);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
log('Stage-b complete.');
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function printUsage() {
|
|
241
|
+
console.log(`
|
|
242
|
+
bootstrap - Portable PMC bootstrap for any repo
|
|
243
|
+
|
|
244
|
+
Usage:
|
|
245
|
+
node bootstrap.mjs <target-repo> [--stage-a] [--stage-b] [--all] [--enrich]
|
|
246
|
+
|
|
247
|
+
Arguments:
|
|
248
|
+
target-repo Path to the target repository (default: current dir)
|
|
249
|
+
|
|
250
|
+
Options:
|
|
251
|
+
--stage-a Run intake + graphify after setup
|
|
252
|
+
--stage-b Run symbol extraction + worklist after setup
|
|
253
|
+
--all Run both stages after setup (default: setup only)
|
|
254
|
+
--enrich Also start the enrichment queue in background after setup
|
|
255
|
+
--help, -h Show this help
|
|
256
|
+
|
|
257
|
+
Environment variables:
|
|
258
|
+
OLLAMA_URL Ollama URL (default: http://localhost:11434)
|
|
259
|
+
OLLAMA_MODEL Ollama model (default: deepseek-coder-v2:16b-ctx32k)
|
|
260
|
+
PMC_CONCURRENCY Parallel slots (default: 8)
|
|
261
|
+
PMC_GRAPHIFY_PATH Custom path to graphify executable
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
node bootstrap.mjs /path/to/my-repo --all
|
|
265
|
+
node bootstrap.mjs /path/to/my-repo --all --enrich
|
|
266
|
+
OLLAMA_MODEL=qwen3-coder:30b node bootstrap.mjs . --stage-b --enrich
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function main(args = process.argv.slice(2)) {
|
|
271
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
272
|
+
printUsage();
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const targetDir = getTargetDir(args);
|
|
277
|
+
if (!targetDir) { printUsage(); return 0; }
|
|
278
|
+
|
|
279
|
+
const runStageAFlag = args.includes('--stage-a') || args.includes('--all');
|
|
280
|
+
const runStageBFlag = args.includes('--stage-b') || args.includes('--all');
|
|
281
|
+
const runEnrichFlag = args.includes('--enrich');
|
|
282
|
+
|
|
283
|
+
log(`Target repo: ${targetDir}`);
|
|
284
|
+
log(`PMC package root: ${PMC_ROOT}`);
|
|
285
|
+
log(`Ollama: ${OLLAMA_URL} | Model: ${OLLAMA_MODEL}`);
|
|
286
|
+
|
|
287
|
+
if (!await ensureDir(targetDir)) {
|
|
288
|
+
console.error(`[bootstrap] ERROR: Directory not found: ${targetDir}`);
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
log('Installing graphify...');
|
|
293
|
+
await installGraphify();
|
|
294
|
+
|
|
295
|
+
log('Running portable project install...');
|
|
296
|
+
const { configPath } = await bootstrapProjectInstall({
|
|
297
|
+
projectRoot: targetDir,
|
|
298
|
+
packageRoot: PMC_PACKAGE_ROOT,
|
|
299
|
+
ollamaBaseUrl: OLLAMA_URL,
|
|
300
|
+
ollamaModel: OLLAMA_MODEL,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
log('Syncing PMC tools to target repo...');
|
|
304
|
+
await syncToolsToTarget(targetDir);
|
|
305
|
+
|
|
306
|
+
log('Generating project context artifacts...');
|
|
307
|
+
const projectContextScript = resolve(PMC_CLI_ROOT, 'project-context.mjs');
|
|
308
|
+
const contextResult = spawnSync('node', [projectContextScript, targetDir], {
|
|
309
|
+
cwd: targetDir,
|
|
310
|
+
stdio: 'inherit',
|
|
311
|
+
env: { ...process.env },
|
|
312
|
+
});
|
|
313
|
+
if (contextResult.status !== 0) {
|
|
314
|
+
log(`WARNING: project-context generation failed with code ${contextResult.status}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
log('');
|
|
318
|
+
log('========================================');
|
|
319
|
+
log('PMC installed successfully!');
|
|
320
|
+
log(` Target: ${targetDir}`);
|
|
321
|
+
log(` Config: ${configPath}`);
|
|
322
|
+
log('========================================');
|
|
323
|
+
log('');
|
|
324
|
+
|
|
325
|
+
if (runStageAFlag) {
|
|
326
|
+
log('Executing stage-a...');
|
|
327
|
+
await runStageA(targetDir);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (runStageBFlag) {
|
|
331
|
+
log('Executing stage-b...');
|
|
332
|
+
const ok = await runStageB(targetDir);
|
|
333
|
+
if (!ok) log('WARNING: stage-b had issues. Check output above.');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (runEnrichFlag && (runStageAFlag || runStageBFlag)) {
|
|
337
|
+
log('Starting background enrichment queue...');
|
|
338
|
+
const enrichScript = resolve(targetDir, 'tools', 'project-memory-context', 'cli', 'enrich-queue.mjs');
|
|
339
|
+
spawnBackground(process.execPath, [enrichScript], { cwd: targetDir });
|
|
340
|
+
log(' Enrichment running in background.');
|
|
341
|
+
} else if (!runStageAFlag && !runStageBFlag) {
|
|
342
|
+
log('Run enrichment with:');
|
|
343
|
+
log(` cd ${targetDir}`);
|
|
344
|
+
log(` node tools/project-memory-context/cli/enrich-queue.mjs`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
log('Done.');
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
352
|
+
const exitCode = await main().catch((err) => {
|
|
353
|
+
console.error('[bootstrap] FATAL:', err.message);
|
|
354
|
+
return 1;
|
|
355
|
+
});
|
|
356
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
357
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ensureProjectMemoryContextDirs,
|
|
7
|
+
readJsonArtifact,
|
|
8
|
+
writeJsonArtifact,
|
|
9
|
+
} from '../src/artifacts.mjs';
|
|
10
|
+
import { attachGraphNodeIds } from '../src/graph-node-resolver.mjs';
|
|
11
|
+
import { buildEnrichmentWorklist, extractTopLevelSymbols } from '../src/symbol-extractor.mjs';
|
|
12
|
+
|
|
13
|
+
const projectRoot = resolve(process.cwd());
|
|
14
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
15
|
+
const symbolIndex = await readJsonArtifact(join(dirs.enrichment, 'symbol-index.json'), {});
|
|
16
|
+
const graph = await readJsonArtifact(join(dirs.graph, 'graph.json'), { nodes: [], edges: [] });
|
|
17
|
+
const files = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
console.error('Usage: node build-worklist.mjs <file1> [file2 ...]');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const symbols = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const absolute = resolve(projectRoot, file);
|
|
27
|
+
const content = await readFile(absolute, 'utf8');
|
|
28
|
+
symbols.push(...extractTopLevelSymbols({ filePath: relative(projectRoot, absolute), content }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolvedSymbols = attachGraphNodeIds({ symbols, graph });
|
|
32
|
+
const worklist = buildEnrichmentWorklist({ symbols: resolvedSymbols, symbolIndex });
|
|
33
|
+
const outputFile = join(dirs.enrichment, 'worklist.json');
|
|
34
|
+
await writeJsonArtifact(outputFile, worklist);
|
|
35
|
+
console.log(JSON.stringify({ saved: true, file: outputFile, count: worklist.length }, null, 2));
|
package/cli/context.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const SCRIPT_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'project-context.mjs');
|
|
7
|
+
|
|
8
|
+
function printHelp() {
|
|
9
|
+
console.log('Usage: pmc context [project-root] [--refresh]');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runProjectContext(projectRoot = process.cwd(), refresh = false) {
|
|
13
|
+
const mod = await import('./project-context.mjs');
|
|
14
|
+
if (typeof mod.runProjectContextCli === 'function') {
|
|
15
|
+
return mod.runProjectContextCli(projectRoot, { refresh });
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function main(args = process.argv.slice(2)) {
|
|
21
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
22
|
+
printHelp();
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
27
|
+
const child = spawn(process.execPath, [SCRIPT_PATH, ...args], { stdio: 'inherit' });
|
|
28
|
+
child.once('error', rejectPromise);
|
|
29
|
+
child.once('exit', (code, signal) => {
|
|
30
|
+
if (signal) {
|
|
31
|
+
rejectPromise(new Error(`context exited from signal ${signal}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resolvePromise(code ?? 0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
41
|
+
const exitCode = await main().catch((error) => {
|
|
42
|
+
console.error('[context] FATAL:', error.message);
|
|
43
|
+
return 1;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (exitCode !== 0) {
|
|
47
|
+
process.exit(exitCode);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/cli/doctor.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { runDoctor } from '../src/doctor.mjs';
|
|
4
|
+
import { resolvePythonBin } from '../src/platform.mjs';
|
|
5
|
+
|
|
6
|
+
function spawnCheck(bin, args) {
|
|
7
|
+
const result = spawnSync(bin, args, { encoding: 'utf-8', timeout: 5000 });
|
|
8
|
+
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { checks } = await runDoctor({ resolvePythonBin, spawnCheck });
|
|
12
|
+
|
|
13
|
+
const icon = { ok: '✓', warn: '⚠', fail: '✗' };
|
|
14
|
+
const color = { ok: '\x1b[32m', warn: '\x1b[33m', fail: '\x1b[31m', reset: '\x1b[0m' };
|
|
15
|
+
|
|
16
|
+
console.log('\npmc doctor\n');
|
|
17
|
+
for (const c of checks) {
|
|
18
|
+
const col = color[c.status] ?? '';
|
|
19
|
+
console.log(` ${col}${icon[c.status]}${color.reset} ${c.name.padEnd(22)} ${c.message}`);
|
|
20
|
+
}
|
|
21
|
+
console.log('');
|
|
22
|
+
|
|
23
|
+
const hasFail = checks.some(c => c.status === 'fail');
|
|
24
|
+
if (hasFail) {
|
|
25
|
+
console.log('Some checks failed. Fix the issues above before running pmc setup.\n');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
} else {
|
|
28
|
+
console.log('All checks passed.\n');
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PROJECT_ROOT = resolve(__dirname, '..', '..', '..');
|
|
7
|
+
|
|
8
|
+
console.error('[enrich-batch] DEPRECATED: This script hardcodes Ollama. Use enrich-queue.mjs instead.');
|
|
9
|
+
console.error('[enrich-batch] The enrich-queue supports the shared fallback driver (local-model → cloud-api → agent-subagent).');
|
|
10
|
+
console.error('[enrich-batch] Run: node tools/project-memory-context/cli/enrich-queue.mjs');
|
|
11
|
+
process.exit(1);
|