@aabadin/project-memory-context 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/context.mjs +291 -15
- package/cli/install-pmc.mjs +20 -0
- package/cli/query.mjs +136 -0
- package/cli/status.mjs +56 -2
- package/mcp/pmc-query-server.mjs +90 -0
- package/package.json +3 -2
- package/src/command-dispatch.mjs +1 -0
- package/src/plugin-config.mjs +8 -0
- package/src/query/load-artifacts.mjs +96 -0
- package/src/query/orchestrator.mjs +175 -0
- package/src/retrieval/context-renderer-v1.mjs +53 -0
- package/src/retrieval/target-resolver.mjs +57 -0
- package/src/setup-bootstrap.mjs +1 -0
- package/src/template-installer.mjs +56 -4
- package/templates/claude-code/CLAUDE.md.snippet +10 -2
- package/templates/cursor/.cursorrules.snippet +10 -2
- package/templates/opencode/commands/get-context.md +22 -5
- package/templates/pmc-skill/SKILL.md +34 -0
package/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/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
|
+
}
|
package/cli/status.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
|
|
6
6
|
import { detectAgentType, resolveConfigDirs } from '../src/platform.mjs';
|
|
7
7
|
|
|
8
|
+
const DEFAULT_STALE_AFTER_SECONDS = 90;
|
|
9
|
+
|
|
8
10
|
export function summarizeWorklist(worklist) {
|
|
9
11
|
return {
|
|
10
12
|
pending: worklist.filter((e) => e.status === 'pending' || e.status === 'stale').length,
|
|
@@ -21,6 +23,52 @@ async function readJsonSafe(filePath) {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function toIsoString(value) {
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const date = new Date(value);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function heartbeatIsFresh(heartbeatAt, now, staleAfterSeconds = DEFAULT_STALE_AFTER_SECONDS) {
|
|
33
|
+
if (!heartbeatAt) return false;
|
|
34
|
+
const heartbeat = new Date(heartbeatAt).getTime();
|
|
35
|
+
const current = new Date(now).getTime();
|
|
36
|
+
if (!Number.isFinite(heartbeat) || !Number.isFinite(current)) return false;
|
|
37
|
+
return current - heartbeat <= staleAfterSeconds * 1000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deriveRuntimeState(queueState, now, staleAfterSeconds = DEFAULT_STALE_AFTER_SECONDS) {
|
|
41
|
+
if (!queueState || typeof queueState !== 'object') {
|
|
42
|
+
return { state: 'idle', runtime: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const runtime = {
|
|
46
|
+
pid: Number.isInteger(queueState.pid) ? queueState.pid : null,
|
|
47
|
+
startedAt: toIsoString(queueState.startedAt),
|
|
48
|
+
heartbeatAt: toIsoString(queueState.heartbeatAt),
|
|
49
|
+
finishedAt: toIsoString(queueState.finishedAt),
|
|
50
|
+
staleAfterSeconds,
|
|
51
|
+
lastError: queueState.lastError ?? null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (queueState.status === 'finished') {
|
|
55
|
+
return { state: 'finished', runtime };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (queueState.status === 'failed') {
|
|
59
|
+
return { state: 'failed', runtime };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (queueState.status === 'running') {
|
|
63
|
+
return {
|
|
64
|
+
state: heartbeatIsFresh(runtime.heartbeatAt, now, staleAfterSeconds) ? 'running' : 'stalled',
|
|
65
|
+
runtime,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { state: 'idle', runtime: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
24
72
|
async function getLastSyncTimestamp(enrichmentDir) {
|
|
25
73
|
const syncManifest = join(enrichmentDir, 'sync-manifest.json');
|
|
26
74
|
try {
|
|
@@ -31,16 +79,20 @@ async function getLastSyncTimestamp(enrichmentDir) {
|
|
|
31
79
|
}
|
|
32
80
|
}
|
|
33
81
|
|
|
34
|
-
export async function buildStatusReport({ projectRoot = process.cwd() } = {}) {
|
|
82
|
+
export async function buildStatusReport({ projectRoot = process.cwd(), now = new Date().toISOString() } = {}) {
|
|
35
83
|
const dirs = resolveConfigDirs(projectRoot);
|
|
36
84
|
const planningDir = join(projectRoot, '.planning', 'project-memory-context');
|
|
37
85
|
const enrichmentDir = join(planningDir, 'enrichment');
|
|
38
86
|
const worklistPath = join(enrichmentDir, 'worklist.json');
|
|
39
87
|
const installStatePath = join(planningDir, 'install.json');
|
|
88
|
+
const queueStatePath = join(enrichmentDir, 'queue-state.json');
|
|
40
89
|
|
|
41
90
|
const worklist = await readJsonSafe(worklistPath);
|
|
42
91
|
const installState = await readJsonSafe(installStatePath);
|
|
92
|
+
const queueState = await readJsonSafe(queueStatePath);
|
|
43
93
|
const lastSync = await getLastSyncTimestamp(enrichmentDir);
|
|
94
|
+
const { state, runtime } = deriveRuntimeState(queueState, now);
|
|
95
|
+
const worklistSummary = Array.isArray(worklist) ? summarizeWorklist(worklist) : null;
|
|
44
96
|
|
|
45
97
|
return {
|
|
46
98
|
ok: true,
|
|
@@ -49,7 +101,9 @@ export async function buildStatusReport({ projectRoot = process.cwd() } = {}) {
|
|
|
49
101
|
configLocation: dirs.projectConfig,
|
|
50
102
|
agentType: detectAgentType(projectRoot),
|
|
51
103
|
installState: installState ? { installedAt: installState.installedAt, version: installState.version } : null,
|
|
52
|
-
|
|
104
|
+
state,
|
|
105
|
+
runtime,
|
|
106
|
+
worklist: worklistSummary,
|
|
53
107
|
lastSync,
|
|
54
108
|
};
|
|
55
109
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { createQueryOrchestrator } from '../src/query/orchestrator.mjs';
|
|
7
|
+
|
|
8
|
+
const projectRoot = process.env.PMC_PROJECT_ROOT || process.cwd();
|
|
9
|
+
const orchestrator = createQueryOrchestrator({ projectRoot });
|
|
10
|
+
|
|
11
|
+
function textResult(value) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function errorResult(toolName, error) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: 'text', text: `${toolName} failed: ${String(error)}` }],
|
|
20
|
+
isError: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: 'pmc-query',
|
|
26
|
+
version: '0.1.5',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
server.tool(
|
|
30
|
+
'pmc_query_project',
|
|
31
|
+
'Query PMC project context and symbol artifacts using a natural-language question.',
|
|
32
|
+
{
|
|
33
|
+
question: z.string().describe('Natural-language project question'),
|
|
34
|
+
},
|
|
35
|
+
async ({ question }) => {
|
|
36
|
+
try {
|
|
37
|
+
return textResult(await orchestrator.query(question));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return errorResult('pmc_query_project', error);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
server.tool(
|
|
45
|
+
'pmc_search_symbols',
|
|
46
|
+
'Search PMC symbols by semantic summary and optional file path filter.',
|
|
47
|
+
{
|
|
48
|
+
query: z.string().describe('Search query for symbols'),
|
|
49
|
+
file: z.string().optional().describe('Optional normalized file path filter'),
|
|
50
|
+
},
|
|
51
|
+
async ({ query, file }) => {
|
|
52
|
+
try {
|
|
53
|
+
return textResult(await orchestrator.searchSymbols(query, file));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return errorResult('pmc_search_symbols', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
server.tool(
|
|
61
|
+
'pmc_get_dependents',
|
|
62
|
+
'List symbols that depend on the given symbol key.',
|
|
63
|
+
{
|
|
64
|
+
symbol: z.string().describe('Normalized PMC symbol key'),
|
|
65
|
+
},
|
|
66
|
+
async ({ symbol }) => {
|
|
67
|
+
try {
|
|
68
|
+
return textResult(await orchestrator.getDependents(symbol));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return errorResult('pmc_get_dependents', error);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
server.tool(
|
|
76
|
+
'pmc_get_dependencies',
|
|
77
|
+
'List symbols the given symbol key depends on.',
|
|
78
|
+
{
|
|
79
|
+
symbol: z.string().describe('Normalized PMC symbol key'),
|
|
80
|
+
},
|
|
81
|
+
async ({ symbol }) => {
|
|
82
|
+
try {
|
|
83
|
+
return textResult(await orchestrator.getDependencies(symbol));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return errorResult('pmc_get_dependencies', error);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await server.connect(new StdioServerTransport());
|