@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 CHANGED
@@ -1,12 +1,211 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
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
- const SCRIPT_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'project-context.mjs');
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 printHelp() {
9
- console.log('Usage: pmc context [project-root] [--refresh]');
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
- if (args.includes('--help') || args.includes('-h')) {
247
+ const parsed = parseArgs(args);
248
+
249
+ if (parsed.help) {
22
250
  printHelp();
23
251
  return 0;
24
252
  }
25
253
 
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;
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
- resolvePromise(code ?? 0);
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)) {
@@ -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
  });
@@ -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
+ }