@aabadin/project-memory-context 0.1.5 → 0.2.1
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/enrich-queue.mjs +83 -2
- package/cli/install-pmc.mjs +20 -0
- package/cli/query.mjs +136 -0
- package/cli/retry-errors.mjs +359 -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 +2 -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 +18 -3
- package/templates/cursor/.cursorrules.snippet +18 -3
- package/templates/opencode/commands/get-context.md +22 -5
- package/templates/pmc-skill/SKILL.md +34 -0
package/cli/context.mjs
CHANGED
|
@@ -1,12 +1,211 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { readJsonArtifact } from '../src/artifacts.mjs';
|
|
7
|
+
import { createQueryEngine, focusToEdgeTypes } from '../src/retrieval/query-engine.mjs';
|
|
8
|
+
import { resolveTarget } from '../src/retrieval/target-resolver.mjs';
|
|
9
|
+
import { renderTargetContext } from '../src/retrieval/context-renderer-v1.mjs';
|
|
7
10
|
|
|
8
|
-
function
|
|
9
|
-
|
|
11
|
+
async function fileExists(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
await access(filePath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function findProjectRoot(startDir = process.cwd()) {
|
|
21
|
+
let currentDir = resolve(startDir);
|
|
22
|
+
|
|
23
|
+
while (true) {
|
|
24
|
+
const installPath = join(currentDir, '.planning', 'project-memory-context', 'install.json');
|
|
25
|
+
if (await fileExists(installPath)) {
|
|
26
|
+
return currentDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parentDir = dirname(currentDir);
|
|
30
|
+
if (parentDir === currentDir) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
currentDir = parentDir;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseArgs(args) {
|
|
39
|
+
const DEPTH_VALUES = ['compact', 'extended', 'deep', 'disk'];
|
|
40
|
+
const FOCUS_VALUES = ['dependencies', 'callers', 'containment', 'impact', 'all'];
|
|
41
|
+
const EXPLICIT_MODES = ['symbol', 'file', 'query'];
|
|
42
|
+
|
|
43
|
+
let explicitMode = null;
|
|
44
|
+
let target = undefined;
|
|
45
|
+
let depth = 'compact';
|
|
46
|
+
let focus = 'all';
|
|
47
|
+
let refresh = false;
|
|
48
|
+
let help = false;
|
|
49
|
+
const positional = [];
|
|
50
|
+
|
|
51
|
+
for (const arg of args) {
|
|
52
|
+
if (arg === '--help' || arg === '-h') {
|
|
53
|
+
help = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (arg === '--refresh') {
|
|
58
|
+
refresh = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (EXPLICIT_MODES.includes(arg) && explicitMode === null && positional.length === 0) {
|
|
63
|
+
explicitMode = arg;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (DEPTH_VALUES.includes(arg)) {
|
|
68
|
+
depth = arg;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (FOCUS_VALUES.includes(arg)) {
|
|
73
|
+
focus = arg;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
positional.push(arg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (positional.length > 0) {
|
|
81
|
+
target = positional[0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { explicitMode, target, depth, focus, refresh, help };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function loadArtifacts(projectRoot) {
|
|
88
|
+
const pmcRoot = join(projectRoot, '.planning', 'project-memory-context');
|
|
89
|
+
try {
|
|
90
|
+
const [graph, symbolIndex, worklist] = await Promise.all([
|
|
91
|
+
readJsonArtifact(join(pmcRoot, 'graph', 'graph.json'), { nodes: [], links: [] }),
|
|
92
|
+
readJsonArtifact(join(pmcRoot, 'enrichment', 'symbol-index.json'), {}),
|
|
93
|
+
readJsonArtifact(join(pmcRoot, 'enrichment', 'worklist.json'), []),
|
|
94
|
+
]);
|
|
95
|
+
return { graph, symbolIndex, worklist };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(`Failed to load PMC artifacts from ${pmcRoot}: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function groupEdges(edges, edgeTypes) {
|
|
102
|
+
const byKind = new Map();
|
|
103
|
+
for (const edge of edges) {
|
|
104
|
+
if (!edgeTypes.includes(edge.relation)) continue;
|
|
105
|
+
const kind = edge.relation;
|
|
106
|
+
const arr = byKind.get(kind);
|
|
107
|
+
if (arr) {
|
|
108
|
+
arr.push(edge.target);
|
|
109
|
+
} else {
|
|
110
|
+
byKind.set(kind, [edge.target]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const [kind, items] of byKind) {
|
|
116
|
+
result.push({ kind, items });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildRenderInput(engine, resolved, { depth, focus }) {
|
|
123
|
+
const edgeTypes = focusToEdgeTypes(focus);
|
|
124
|
+
let summary = [];
|
|
125
|
+
let target = {};
|
|
126
|
+
let relevant = [];
|
|
127
|
+
let relations = [];
|
|
128
|
+
let nextReads = [];
|
|
129
|
+
|
|
130
|
+
switch (resolved.mode) {
|
|
131
|
+
case 'symbol': {
|
|
132
|
+
const ctx = engine.querySymbolContext({ symbolKey: resolved.symbolKey, depth });
|
|
133
|
+
target = { mode: 'symbol', name: ctx.target?.name, filePath: ctx.target?.filePath };
|
|
134
|
+
summary = [`Symbol: ${ctx.target?.name || resolved.target} (${ctx.target?.kind || 'unknown'})`];
|
|
135
|
+
relevant = (ctx.neighbors || []).map((n) => ({
|
|
136
|
+
label: n.name || n.label || 'unknown',
|
|
137
|
+
filePath: n.filePath || undefined,
|
|
138
|
+
}));
|
|
139
|
+
relations = groupEdges(ctx.edges || [], edgeTypes);
|
|
140
|
+
nextReads = relevant.map((r) => r.filePath).filter(Boolean);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'symbol-ambiguous': {
|
|
144
|
+
target = { mode: 'symbol-ambiguous', name: resolved.target };
|
|
145
|
+
summary = [`Multiple symbols match "${resolved.target}".`];
|
|
146
|
+
relevant = (resolved.symbolKeys || []).map((sk) => {
|
|
147
|
+
const parts = sk.split('|');
|
|
148
|
+
return {
|
|
149
|
+
label: parts.length >= 6 ? parts[parts.length - 2] : sk,
|
|
150
|
+
filePath: parts.length >= 2 ? parts[1] : undefined,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'file': {
|
|
156
|
+
const ctx = engine.queryFileContext({ filePath: resolved.target, depth });
|
|
157
|
+
target = { mode: 'file', filePath: resolved.target };
|
|
158
|
+
summary = ctx.symbols && ctx.symbols.length > 0
|
|
159
|
+
? ctx.symbols.map((s) => `${s.name || 'unknown'} (${s.kind || 'symbol'})`)
|
|
160
|
+
: [`File: ${resolved.target}`];
|
|
161
|
+
relevant = (ctx.neighbors || []).map((n) => ({
|
|
162
|
+
label: n.name || n.label || 'unknown',
|
|
163
|
+
filePath: n.filePath || undefined,
|
|
164
|
+
}));
|
|
165
|
+
relations = groupEdges(ctx.edges || [], edgeTypes);
|
|
166
|
+
nextReads = relevant.map((r) => r.filePath).filter(Boolean);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'query':
|
|
170
|
+
target = { mode: 'query', value: resolved.target };
|
|
171
|
+
summary = [`Query: ${resolved.target} (no structural context available)`];
|
|
172
|
+
break;
|
|
173
|
+
case 'symbol-missing':
|
|
174
|
+
target = { mode: 'symbol-missing', name: resolved.target };
|
|
175
|
+
summary = [`Symbol "${resolved.target}" not found in project index.`];
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
target = { mode: resolved.mode || 'unknown', value: resolved.target };
|
|
179
|
+
summary = [`Unrecognized target mode: ${resolved.mode}`];
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
summary,
|
|
185
|
+
target,
|
|
186
|
+
relevant,
|
|
187
|
+
relations,
|
|
188
|
+
nextReads,
|
|
189
|
+
metadata: { depth, focus },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function runTargetContext({ projectRoot, target, explicitMode, depth, focus }) {
|
|
194
|
+
const artifacts = await loadArtifacts(projectRoot);
|
|
195
|
+
const engine = createQueryEngine({
|
|
196
|
+
graph: artifacts.graph,
|
|
197
|
+
symbolIndex: artifacts.symbolIndex,
|
|
198
|
+
worklist: artifacts.worklist,
|
|
199
|
+
enrichmentDir: join(projectRoot, '.planning', 'project-memory-context', 'enrichment'),
|
|
200
|
+
projectSlug: 'project',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const resolved = resolveTarget({ engine, explicitMode, target });
|
|
204
|
+
|
|
205
|
+
const input = buildRenderInput(engine, resolved, { depth, focus });
|
|
206
|
+
const output = renderTargetContext(input);
|
|
207
|
+
|
|
208
|
+
return { output, resolved, input };
|
|
10
209
|
}
|
|
11
210
|
|
|
12
211
|
export async function runProjectContext(projectRoot = process.cwd(), refresh = false) {
|
|
@@ -14,27 +213,104 @@ export async function runProjectContext(projectRoot = process.cwd(), refresh = f
|
|
|
14
213
|
if (typeof mod.runProjectContextCli === 'function') {
|
|
15
214
|
return mod.runProjectContextCli(projectRoot, { refresh });
|
|
16
215
|
}
|
|
216
|
+
|
|
17
217
|
return null;
|
|
18
218
|
}
|
|
19
219
|
|
|
220
|
+
function printHelp() {
|
|
221
|
+
console.log('Usage: pmc context [options] [<target>]');
|
|
222
|
+
console.log(' pmc context {symbol|file|query} <target> [depth] [focus]');
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log('Get structural context about symbols, files, or free-text queries');
|
|
225
|
+
console.log('from the PMC project graph.');
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log('Modes:');
|
|
228
|
+
console.log(' symbol Resolve a symbol by name');
|
|
229
|
+
console.log(' file Query context for a file path');
|
|
230
|
+
console.log(' query Free-text query (structural only)');
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log('Options:');
|
|
233
|
+
console.log(' depth compact (default), extended, deep, disk');
|
|
234
|
+
console.log(' focus all (default), dependencies, callers, containment, impact');
|
|
235
|
+
console.log(' --refresh Run project-context detection and materialization');
|
|
236
|
+
console.log(' --help, -h Show this help');
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Examples:');
|
|
239
|
+
console.log(' pmc context createQueryEngine');
|
|
240
|
+
console.log(' pmc context symbol MyFunc extended dependencies');
|
|
241
|
+
console.log(' pmc context file src/auth.ts deep callers');
|
|
242
|
+
console.log(' pmc context query "how auth works"');
|
|
243
|
+
console.log(' pmc context . --refresh');
|
|
244
|
+
}
|
|
245
|
+
|
|
20
246
|
export async function main(args = process.argv.slice(2)) {
|
|
21
|
-
|
|
247
|
+
const parsed = parseArgs(args);
|
|
248
|
+
|
|
249
|
+
if (parsed.help) {
|
|
22
250
|
printHelp();
|
|
23
251
|
return 0;
|
|
24
252
|
}
|
|
25
253
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
254
|
+
if (parsed.refresh) {
|
|
255
|
+
let projectRoot;
|
|
256
|
+
|
|
257
|
+
if (parsed.target && parsed.target !== '.') {
|
|
258
|
+
const looksValid = /^[A-Za-z]:[\\/]/.test(parsed.target)
|
|
259
|
+
|| /^[\\/]/.test(parsed.target)
|
|
260
|
+
|| /^\.{2}[\\/]?/.test(parsed.target);
|
|
261
|
+
|
|
262
|
+
if (looksValid) {
|
|
263
|
+
projectRoot = resolve(parsed.target);
|
|
264
|
+
} else {
|
|
265
|
+
console.error(
|
|
266
|
+
`[context] --refresh does not accept a non-path target "${parsed.target}".`,
|
|
267
|
+
'Use . --refresh to refresh from cwd, or omit the target entirely.',
|
|
268
|
+
);
|
|
269
|
+
return 1;
|
|
33
270
|
}
|
|
271
|
+
} else {
|
|
272
|
+
projectRoot = await findProjectRoot();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!projectRoot) {
|
|
276
|
+
console.error('[context] Not inside a PMC-enabled project.');
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
34
279
|
|
|
35
|
-
|
|
36
|
-
|
|
280
|
+
try {
|
|
281
|
+
return await runProjectContext(projectRoot, true);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error(`[context] Refresh failed: ${error.message}`);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!parsed.target) {
|
|
289
|
+
if (parsed.explicitMode) {
|
|
290
|
+
console.error(`[context] Missing target for ${parsed.explicitMode} mode.`);
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
printHelp();
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const projectRoot = await findProjectRoot();
|
|
299
|
+
if (!projectRoot) {
|
|
300
|
+
console.error('[context] Not inside a PMC-enabled project (no install.json found).');
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { output } = await runTargetContext({
|
|
305
|
+
projectRoot,
|
|
306
|
+
target: parsed.target,
|
|
307
|
+
explicitMode: parsed.explicitMode,
|
|
308
|
+
depth: parsed.depth,
|
|
309
|
+
focus: parsed.focus,
|
|
37
310
|
});
|
|
311
|
+
|
|
312
|
+
console.log(output);
|
|
313
|
+
return 0;
|
|
38
314
|
}
|
|
39
315
|
|
|
40
316
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
package/cli/enrich-queue.mjs
CHANGED
|
@@ -29,6 +29,8 @@ let _symbolIndex = {};
|
|
|
29
29
|
let _worklistFile = '';
|
|
30
30
|
let _symbolIndexFile = '';
|
|
31
31
|
let _enrichmentDir = '';
|
|
32
|
+
let _queueStateFile = '';
|
|
33
|
+
let _startedAt = '';
|
|
32
34
|
|
|
33
35
|
async function loadJson(path) {
|
|
34
36
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -320,6 +322,30 @@ export async function runQueueSymbolEnrichment({
|
|
|
320
322
|
throw createQueueEnrichmentError(failure);
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
export function buildQueueState({ status, pid, startedAt, heartbeatAt, finishedAt = null, lastError = null, summary }) {
|
|
326
|
+
return {
|
|
327
|
+
status,
|
|
328
|
+
pid,
|
|
329
|
+
startedAt,
|
|
330
|
+
heartbeatAt,
|
|
331
|
+
finishedAt,
|
|
332
|
+
lastError,
|
|
333
|
+
summary: {
|
|
334
|
+
pending: summary?.pending ?? 0,
|
|
335
|
+
enriched: summary?.enriched ?? 0,
|
|
336
|
+
errors: summary?.errors ?? 0,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function writeQueueState(input) {
|
|
342
|
+
await saveJson(input.queueStateFile, buildQueueState(input));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function finalizeQueueState(input) {
|
|
346
|
+
await writeQueueState(input);
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
async function checkpointSave() {
|
|
324
350
|
if (!_worklistFile) return;
|
|
325
351
|
console.error('\n[checkpoint] Saving progress...');
|
|
@@ -371,6 +397,9 @@ async function main() {
|
|
|
371
397
|
_worklist = worklist;
|
|
372
398
|
_symbolIndex = symbolIndex;
|
|
373
399
|
|
|
400
|
+
const queueStateFile = resolve(enrichmentDir, 'queue-state.json');
|
|
401
|
+
_queueStateFile = queueStateFile;
|
|
402
|
+
|
|
374
403
|
for (const entry of worklist) {
|
|
375
404
|
if (entry.status === 'enriched' && entry.memoryId) {
|
|
376
405
|
entry.status = 'already_enriched';
|
|
@@ -385,10 +414,29 @@ async function main() {
|
|
|
385
414
|
|
|
386
415
|
if (pending.length === 0) {
|
|
387
416
|
const summary = buildQueueSummary(worklist);
|
|
417
|
+
await finalizeQueueState({
|
|
418
|
+
queueStateFile,
|
|
419
|
+
status: 'finished',
|
|
420
|
+
pid: process.pid,
|
|
421
|
+
startedAt: new Date().toISOString(),
|
|
422
|
+
heartbeatAt: new Date().toISOString(),
|
|
423
|
+
finishedAt: new Date().toISOString(),
|
|
424
|
+
summary,
|
|
425
|
+
});
|
|
388
426
|
console.log(JSON.stringify({ complete: true, total, enriched: summary.enriched, errors: summary.errors }));
|
|
389
427
|
return;
|
|
390
428
|
}
|
|
391
429
|
|
|
430
|
+
_startedAt = new Date().toISOString();
|
|
431
|
+
await writeQueueState({
|
|
432
|
+
queueStateFile,
|
|
433
|
+
status: 'running',
|
|
434
|
+
pid: process.pid,
|
|
435
|
+
startedAt: _startedAt,
|
|
436
|
+
heartbeatAt: _startedAt,
|
|
437
|
+
summary: buildQueueSummary(worklist),
|
|
438
|
+
});
|
|
439
|
+
|
|
392
440
|
console.error(`[queue] Starting continuous enrichment: ${pending.length} pending (${staleCount} stale), ${alreadyEnriched} already enriched, ${PMC_CONCURRENCY} parallel slots`);
|
|
393
441
|
console.error(`[queue] Modes: ${enrichmentConfig.preferredModes.join(', ')} | Local: ${enrichmentConfig.localModel.baseUrl} | Model: ${enrichmentConfig.localModel.model} | Timeout: ${TIMEOUT_MS}ms per symbol\n`);
|
|
394
442
|
|
|
@@ -458,7 +506,7 @@ async function main() {
|
|
|
458
506
|
await Promise.all(initPromises);
|
|
459
507
|
|
|
460
508
|
await new Promise((resolve) => {
|
|
461
|
-
const checkInterval = setInterval(() => {
|
|
509
|
+
const checkInterval = setInterval(async () => {
|
|
462
510
|
if (!tracker.hasActiveSlots() && queue.length === 0) {
|
|
463
511
|
clearInterval(checkInterval);
|
|
464
512
|
resolve();
|
|
@@ -468,6 +516,14 @@ async function main() {
|
|
|
468
516
|
const active = tracker.activeCount();
|
|
469
517
|
const done = results.length + errors.length;
|
|
470
518
|
console.error(`[queue status] done=${done} active=${active} queued=${remaining} elapsed=${Math.round(elapsedTotal / 1000)}s`);
|
|
519
|
+
await writeQueueState({
|
|
520
|
+
queueStateFile,
|
|
521
|
+
status: 'running',
|
|
522
|
+
pid: process.pid,
|
|
523
|
+
startedAt: _startedAt,
|
|
524
|
+
heartbeatAt: new Date().toISOString(),
|
|
525
|
+
summary: buildQueueSummary(worklist),
|
|
526
|
+
}).catch(() => {});
|
|
471
527
|
}
|
|
472
528
|
}, REPORT_INTERVAL_MS);
|
|
473
529
|
});
|
|
@@ -504,6 +560,17 @@ async function main() {
|
|
|
504
560
|
const summary = buildQueueSummary(worklist);
|
|
505
561
|
const avgTime = results.length > 0 ? results.reduce((a, r) => a + r.elapsed, 0) / results.length : 0;
|
|
506
562
|
|
|
563
|
+
const finishedAt = new Date().toISOString();
|
|
564
|
+
await finalizeQueueState({
|
|
565
|
+
queueStateFile,
|
|
566
|
+
status: 'finished',
|
|
567
|
+
pid: process.pid,
|
|
568
|
+
startedAt: _startedAt,
|
|
569
|
+
heartbeatAt: finishedAt,
|
|
570
|
+
finishedAt,
|
|
571
|
+
summary,
|
|
572
|
+
});
|
|
573
|
+
|
|
507
574
|
console.log(JSON.stringify({
|
|
508
575
|
complete: summary.pending === 0,
|
|
509
576
|
total: worklist.length,
|
|
@@ -518,7 +585,21 @@ async function main() {
|
|
|
518
585
|
}
|
|
519
586
|
|
|
520
587
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
521
|
-
main().catch(err => {
|
|
588
|
+
main().catch(async (err) => {
|
|
589
|
+
try {
|
|
590
|
+
const summary = Array.isArray(_worklist) ? buildQueueSummary(_worklist) : { pending: 0, enriched: 0, errors: 0 };
|
|
591
|
+
const enrichmentDir = _enrichmentDir || resolve(PROJECT_ROOT, '.planning/project-memory-context/enrichment');
|
|
592
|
+
await finalizeQueueState({
|
|
593
|
+
queueStateFile: resolve(enrichmentDir, 'queue-state.json'),
|
|
594
|
+
status: 'failed',
|
|
595
|
+
pid: process.pid,
|
|
596
|
+
startedAt: _startedAt || new Date().toISOString(),
|
|
597
|
+
heartbeatAt: new Date().toISOString(),
|
|
598
|
+
finishedAt: new Date().toISOString(),
|
|
599
|
+
lastError: err.message,
|
|
600
|
+
summary,
|
|
601
|
+
});
|
|
602
|
+
} catch {}
|
|
522
603
|
console.error('[fatal]', err.message);
|
|
523
604
|
process.exit(1);
|
|
524
605
|
});
|
package/cli/install-pmc.mjs
CHANGED
|
@@ -59,11 +59,16 @@ function copyTemplatesDir(srcTemplates, dstTemplates) {
|
|
|
59
59
|
export function installPmcTools({ sourceRoot, targetRoot }) {
|
|
60
60
|
const srcCli = resolve(sourceRoot, 'cli');
|
|
61
61
|
const srcSrc = resolve(sourceRoot, 'src');
|
|
62
|
+
const srcMcp = resolve(sourceRoot, 'mcp');
|
|
63
|
+
const srcPlugin = resolve(sourceRoot, 'plugin');
|
|
62
64
|
const srcTemplates = resolve(sourceRoot, 'templates');
|
|
65
|
+
const srcPackageJson = resolve(sourceRoot, 'package.json');
|
|
63
66
|
|
|
64
67
|
const dstBase = resolve(targetRoot, 'tools', 'project-memory-context');
|
|
65
68
|
const dstCli = resolve(dstBase, 'cli');
|
|
66
69
|
const dstSrc = resolve(dstBase, 'src');
|
|
70
|
+
const dstMcp = resolve(dstBase, 'mcp');
|
|
71
|
+
const dstPlugin = resolve(dstBase, 'plugin');
|
|
67
72
|
const dstTemplates = resolve(dstBase, 'templates');
|
|
68
73
|
|
|
69
74
|
mkdirSync(dstBase, { recursive: true });
|
|
@@ -85,15 +90,30 @@ export function installPmcTools({ sourceRoot, targetRoot }) {
|
|
|
85
90
|
srcFiles = copyMjsTree(srcSrc, dstSrc);
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
if (existsSync(srcMcp)) {
|
|
94
|
+
copyMjsTree(srcMcp, dstMcp);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (existsSync(srcPlugin)) {
|
|
98
|
+
copyMjsTree(srcPlugin, dstPlugin);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (existsSync(srcPackageJson)) {
|
|
102
|
+
copyFileSync(srcPackageJson, resolve(dstBase, 'package.json'));
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
templateFiles = copyTemplatesDir(srcTemplates, dstTemplates);
|
|
89
106
|
|
|
90
107
|
const planningBase = resolve(targetRoot, '.planning', 'project-memory-context');
|
|
108
|
+
const memoryDbPath = resolve(planningBase, 'memory-db');
|
|
91
109
|
for (const sub of ['intake', 'graph', 'enrichment', 'memory-db', 'db']) {
|
|
92
110
|
mkdirSync(resolve(planningBase, sub), { recursive: true });
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
const installState = {
|
|
96
114
|
installedAt: new Date().toISOString(),
|
|
115
|
+
memoryDbPath,
|
|
116
|
+
projectRoot: resolve(targetRoot),
|
|
97
117
|
sourceRoot: resolve(sourceRoot),
|
|
98
118
|
version: '0.1.0',
|
|
99
119
|
};
|
package/cli/query.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { createQueryOrchestrator } from '../src/query/orchestrator.mjs';
|
|
7
|
+
|
|
8
|
+
function printHelp() {
|
|
9
|
+
console.log('Usage: pmc query <question> [--format text|json]');
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log('Queries PMC project-context and symbol artifacts from the current project.');
|
|
12
|
+
console.log('Use --format json for machine-readable output.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fileExists(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
await access(filePath);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function findProjectRoot(startDir = process.cwd()) {
|
|
25
|
+
let currentDir = resolve(startDir);
|
|
26
|
+
|
|
27
|
+
while (true) {
|
|
28
|
+
const installPath = join(currentDir, '.planning', 'project-memory-context', 'install.json');
|
|
29
|
+
if (await fileExists(installPath)) {
|
|
30
|
+
return currentDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parentDir = dirname(currentDir);
|
|
34
|
+
if (parentDir === currentDir) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
currentDir = parentDir;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(args) {
|
|
43
|
+
let format = 'text';
|
|
44
|
+
const questionParts = [];
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47
|
+
const arg = args[index];
|
|
48
|
+
if (arg === '--format') {
|
|
49
|
+
const value = args[index + 1];
|
|
50
|
+
if (!value || value.startsWith('-')) {
|
|
51
|
+
throw new Error('Missing value for --format. Expected text or json.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
format = value;
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (arg.startsWith('--')) {
|
|
60
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
questionParts.push(arg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
format,
|
|
68
|
+
question: questionParts.join(' ').trim(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatSource(source) {
|
|
73
|
+
if (source.type === 'project-context') {
|
|
74
|
+
return `- [project-context] ${source.title} (${source.path})`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `- [symbol] ${source.symbolKey} (${source.filePath})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printTextResult(result) {
|
|
81
|
+
console.log(result.answer);
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log('Sources:');
|
|
84
|
+
if (result.sources.length === 0) {
|
|
85
|
+
console.log('- none');
|
|
86
|
+
} else {
|
|
87
|
+
for (const source of result.sources) {
|
|
88
|
+
console.log(formatSource(source));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log(`tokens_saved: ${result.tokens_saved}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function main(args = process.argv.slice(2)) {
|
|
95
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
96
|
+
printHelp();
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { format, question } = parseArgs(args);
|
|
101
|
+
if (format !== 'text' && format !== 'json') {
|
|
102
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!question) {
|
|
106
|
+
printHelp();
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const projectRoot = await findProjectRoot(process.cwd());
|
|
111
|
+
if (!projectRoot) {
|
|
112
|
+
throw new Error('pmc query must be run inside a PMC-enabled project (missing .planning/project-memory-context/install.json).');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const orchestrator = createQueryOrchestrator({ projectRoot });
|
|
116
|
+
const result = await orchestrator.query(question);
|
|
117
|
+
|
|
118
|
+
if (format === 'json') {
|
|
119
|
+
console.log(JSON.stringify(result, null, 2));
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
printTextResult(result);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
128
|
+
const exitCode = await main().catch((error) => {
|
|
129
|
+
console.error('[query] FATAL:', error.message);
|
|
130
|
+
return 1;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (exitCode !== 0) {
|
|
134
|
+
process.exit(exitCode);
|
|
135
|
+
}
|
|
136
|
+
}
|