structx 1.0.0 → 2.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.
Files changed (62) hide show
  1. package/README.md +72 -0
  2. package/dist/cli.js +260 -211
  3. package/dist/cli.js.map +1 -1
  4. package/dist/db/connection.js +28 -1
  5. package/dist/db/connection.js.map +1 -1
  6. package/dist/db/queries.d.ts +144 -0
  7. package/dist/db/queries.d.ts.map +1 -1
  8. package/dist/db/queries.js +282 -6
  9. package/dist/db/queries.js.map +1 -1
  10. package/dist/db/schema.sql +59 -0
  11. package/dist/ingest/constant-extractor.d.ts +11 -0
  12. package/dist/ingest/constant-extractor.d.ts.map +1 -0
  13. package/dist/ingest/constant-extractor.js +38 -0
  14. package/dist/ingest/constant-extractor.js.map +1 -0
  15. package/dist/ingest/file-metadata.d.ts +13 -0
  16. package/dist/ingest/file-metadata.d.ts.map +1 -0
  17. package/dist/ingest/file-metadata.js +55 -0
  18. package/dist/ingest/file-metadata.js.map +1 -0
  19. package/dist/ingest/ingester.d.ts +15 -0
  20. package/dist/ingest/ingester.d.ts.map +1 -0
  21. package/dist/ingest/ingester.js +217 -0
  22. package/dist/ingest/ingester.js.map +1 -0
  23. package/dist/ingest/parser.d.ts +12 -0
  24. package/dist/ingest/parser.d.ts.map +1 -1
  25. package/dist/ingest/parser.js +48 -0
  26. package/dist/ingest/parser.js.map +1 -1
  27. package/dist/ingest/route-extractor.d.ts +12 -0
  28. package/dist/ingest/route-extractor.d.ts.map +1 -0
  29. package/dist/ingest/route-extractor.js +64 -0
  30. package/dist/ingest/route-extractor.js.map +1 -0
  31. package/dist/ingest/type-extractor.d.ts +11 -0
  32. package/dist/ingest/type-extractor.d.ts.map +1 -0
  33. package/dist/ingest/type-extractor.js +47 -0
  34. package/dist/ingest/type-extractor.js.map +1 -0
  35. package/dist/instructions/claude.md +54 -25
  36. package/dist/instructions/codex.md +70 -0
  37. package/dist/instructions/copilot.md +57 -26
  38. package/dist/instructions/cursor.md +54 -25
  39. package/dist/instructions/generic.md +54 -25
  40. package/dist/query/answerer.d.ts.map +1 -1
  41. package/dist/query/answerer.js +6 -2
  42. package/dist/query/answerer.js.map +1 -1
  43. package/dist/query/classifier.d.ts +6 -1
  44. package/dist/query/classifier.d.ts.map +1 -1
  45. package/dist/query/classifier.js +22 -2
  46. package/dist/query/classifier.js.map +1 -1
  47. package/dist/query/context-builder.d.ts.map +1 -1
  48. package/dist/query/context-builder.js +152 -6
  49. package/dist/query/context-builder.js.map +1 -1
  50. package/dist/query/retriever.d.ts +44 -0
  51. package/dist/query/retriever.d.ts.map +1 -1
  52. package/dist/query/retriever.js +211 -14
  53. package/dist/query/retriever.js.map +1 -1
  54. package/dist/semantic/analyzer.d.ts +10 -0
  55. package/dist/semantic/analyzer.d.ts.map +1 -1
  56. package/dist/semantic/analyzer.js +147 -1
  57. package/dist/semantic/analyzer.js.map +1 -1
  58. package/dist/semantic/prompt.d.ts +21 -0
  59. package/dist/semantic/prompt.d.ts.map +1 -1
  60. package/dist/semantic/prompt.js +63 -0
  61. package/dist/semantic/prompt.js.map +1 -1
  62. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # StructX
2
+
3
+ Graph-powered code intelligence for TypeScript. Drop into any project and let AI agents (Claude Code, Cursor, Copilot) use a function-level knowledge graph instead of reading raw files.
4
+
5
+ ## Quick Start
6
+
7
+ Two commands to set up any TypeScript project:
8
+
9
+ ```bash
10
+ # 1. Install AI agent instruction files into your project
11
+ npx structx install .
12
+
13
+ # 2. Bootstrap the function graph (init + ingest + analyze)
14
+ ANTHROPIC_API_KEY=your-key npx structx setup .
15
+ ```
16
+
17
+ That's it. Your AI agent will now automatically use StructX when it reads the instruction files.
18
+
19
+ ## What Happens
20
+
21
+ **Step 1 — `npx structx install .`** creates these files in your project:
22
+
23
+ | File | For |
24
+ |------|-----|
25
+ | `CLAUDE.md` | Claude Code |
26
+ | `.cursorrules` | Cursor |
27
+ | `.github/copilot-instructions.md` | GitHub Copilot |
28
+
29
+ If any of these files already exist, StructX appends its section instead of overwriting.
30
+
31
+ **Step 2 — `npx structx setup .`** does three things in one shot:
32
+
33
+ 1. **Init** — creates `.structx/` directory with a SQLite database
34
+ 2. **Ingest** — parses all TypeScript files into a function graph (signatures, call relationships, exports)
35
+ 3. **Analyze** — enriches each function with semantic metadata via LLM (purpose, behavior, tags)
36
+
37
+ ## Requirements
38
+
39
+ - Node.js >= 18
40
+ - An Anthropic API key (set as `ANTHROPIC_API_KEY` environment variable)
41
+
42
+ ## How AI Agents Use It
43
+
44
+ Once installed, the instruction files tell your AI agent to:
45
+
46
+ 1. Run `npx structx status` on session start to check the graph
47
+ 2. Run `npx structx ask "question" --repo .` before answering code questions
48
+ 3. Run `npx structx ingest .` after making code changes
49
+ 4. Run `npx structx analyze . --yes` after ingestion queues new functions
50
+ 5. Run `npx structx ask "what breaks if I change X" --repo .` for impact analysis
51
+
52
+ ## All Commands
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `npx structx install .` | Drop instruction files into your project |
57
+ | `npx structx setup .` | One-step bootstrap (init + ingest + analyze) |
58
+ | `npx structx status` | Show graph stats |
59
+ | `npx structx ingest .` | Re-parse codebase after changes |
60
+ | `npx structx analyze . --yes` | Run semantic analysis on new/changed functions |
61
+ | `npx structx ask "question" --repo .` | Query the function graph |
62
+ | `npx structx doctor` | Validate environment and configuration |
63
+
64
+ ## .gitignore
65
+
66
+ Add this to your `.gitignore`:
67
+
68
+ ```
69
+ .structx/
70
+ ```
71
+
72
+ The `.structx/` directory contains the SQLite database and is local to each developer.
package/dist/cli.js CHANGED
@@ -42,10 +42,6 @@ const config_1 = require("./config");
42
42
  const connection_1 = require("./db/connection");
43
43
  const queries_1 = require("./db/queries");
44
44
  const logger_1 = require("./utils/logger");
45
- const scanner_1 = require("./ingest/scanner");
46
- const parser_1 = require("./ingest/parser");
47
- const relationships_1 = require("./ingest/relationships");
48
- const differ_1 = require("./ingest/differ");
49
45
  const analyzer_1 = require("./semantic/analyzer");
50
46
  const cost_1 = require("./semantic/cost");
51
47
  const queries_2 = require("./db/queries");
@@ -55,11 +51,12 @@ const context_builder_1 = require("./query/context-builder");
55
51
  const answerer_1 = require("./query/answerer");
56
52
  const runner_1 = require("./benchmark/runner");
57
53
  const reporter_1 = require("./benchmark/reporter");
54
+ const ingester_1 = require("./ingest/ingester");
58
55
  const program = new commander_1.Command();
59
56
  program
60
57
  .name('structx')
61
58
  .description('Graph-powered code intelligence CLI for TypeScript')
62
- .version('1.0.0')
59
+ .version('2.1.0')
63
60
  .option('--verbose', 'Enable verbose logging')
64
61
  .hook('preAction', (thisCommand) => {
65
62
  if (thisCommand.opts().verbose) {
@@ -71,7 +68,8 @@ program
71
68
  .command('setup')
72
69
  .description('One-step bootstrap: init + ingest + analyze')
73
70
  .argument('[repo-path]', 'Path to TypeScript repository', '.')
74
- .action(async (repoPath) => {
71
+ .option('--api-key <key>', 'Anthropic API key (overrides ANTHROPIC_API_KEY env var)')
72
+ .action(async (repoPath, opts) => {
75
73
  const resolved = path.resolve(repoPath);
76
74
  const structxDir = (0, config_1.getStructXDir)(resolved);
77
75
  // Step 1: Init
@@ -87,102 +85,14 @@ program
87
85
  }
88
86
  // Step 2: Ingest
89
87
  const config = (0, config_1.loadConfig)(structxDir);
88
+ if (opts.apiKey)
89
+ config.anthropicApiKey = opts.apiKey;
90
90
  const db = (0, connection_1.openDatabase)(dbPath);
91
- const project = (0, parser_1.createProject)(resolved);
92
91
  console.log(`\nScanning ${resolved} for TypeScript files...`);
93
- const files = (0, scanner_1.scanDirectory)(resolved);
94
- console.log(`Found ${files.length} TypeScript files.`);
95
- let newFiles = 0;
96
- let changedFiles = 0;
97
- let unchangedFiles = 0;
98
- let totalFunctions = 0;
99
- let totalRelationships = 0;
100
- let queued = 0;
101
- for (const filePath of files) {
102
- const relativePath = path.relative(resolved, filePath);
103
- const content = fs.readFileSync(filePath, 'utf-8');
104
- const contentHash = (0, parser_1.hashFileContent)(content);
105
- const existingFile = (0, queries_1.getFileByPath)(db, relativePath);
106
- if (existingFile && existingFile.content_hash === contentHash) {
107
- unchangedFiles++;
108
- continue;
109
- }
110
- const isNew = !existingFile;
111
- if (isNew)
112
- newFiles++;
113
- else
114
- changedFiles++;
115
- const fileId = (0, queries_1.upsertFile)(db, relativePath, contentHash);
116
- const oldFunctions = isNew ? [] : (0, queries_1.getFunctionsByFileId)(db, fileId);
117
- const oldFunctionMap = new Map(oldFunctions.map(f => [f.name, f]));
118
- if (!isNew) {
119
- for (const oldFn of oldFunctions) {
120
- (0, queries_1.deleteRelationshipsByCallerFunctionId)(db, oldFn.id);
121
- }
122
- (0, queries_1.deleteFunctionsByFileId)(db, fileId);
123
- }
124
- let functions;
125
- try {
126
- functions = (0, parser_1.parseFile)(project, filePath);
127
- }
128
- catch (err) {
129
- logger_1.logger.warn(`Failed to parse ${relativePath}: ${err.message}`);
130
- continue;
131
- }
132
- const functionIdMap = new Map();
133
- for (const fn of functions) {
134
- const fnId = (0, queries_1.insertFunction)(db, {
135
- file_id: fileId,
136
- name: fn.name,
137
- signature: fn.signature,
138
- body: fn.body,
139
- code_hash: fn.codeHash,
140
- start_line: fn.startLine,
141
- end_line: fn.endLine,
142
- is_exported: fn.isExported,
143
- is_async: fn.isAsync,
144
- });
145
- functionIdMap.set(fn.name, fnId);
146
- totalFunctions++;
147
- const oldFn = oldFunctionMap.get(fn.name);
148
- if (!oldFn) {
149
- (0, queries_1.enqueueForAnalysis)(db, fnId, 'new', (0, differ_1.getPriority)('new', fn.isExported));
150
- queued++;
151
- }
152
- else {
153
- const { reanalyze, reason } = (0, differ_1.shouldReanalyze)(oldFn, fn.signature, fn.codeHash, fn.body, config.diffThreshold);
154
- if (reanalyze) {
155
- (0, queries_1.enqueueForAnalysis)(db, fnId, reason, (0, differ_1.getPriority)(reason, fn.isExported));
156
- queued++;
157
- }
158
- }
159
- }
160
- try {
161
- const calls = (0, relationships_1.extractCallsFromFile)(project, filePath);
162
- for (const call of calls) {
163
- if (call.callerName === '__file__')
164
- continue;
165
- const callerId = functionIdMap.get(call.callerName);
166
- if (!callerId)
167
- continue;
168
- const callee = (0, queries_1.getFunctionByName)(db, call.calleeName);
169
- (0, queries_1.insertRelationship)(db, callerId, call.calleeName, call.relationType, callee?.id);
170
- totalRelationships++;
171
- }
172
- }
173
- catch (err) {
174
- logger_1.logger.warn(`Failed to extract calls from ${relativePath}: ${err.message}`);
175
- }
176
- }
177
- console.log(`\nIngestion complete:`);
178
- console.log(` New files: ${newFiles}`);
179
- console.log(` Changed files: ${changedFiles}`);
180
- console.log(` Unchanged: ${unchangedFiles}`);
181
- console.log(` Functions: ${totalFunctions}`);
182
- console.log(` Relationships: ${totalRelationships}`);
183
- console.log(` Queued: ${queued} functions for semantic analysis`);
92
+ const ingestResult = (0, ingester_1.ingestDirectory)(db, resolved, config.diffThreshold);
93
+ (0, ingester_1.printIngestResult)(ingestResult);
184
94
  // Step 3: Analyze (auto-confirm)
185
- if (queued > 0 && config.anthropicApiKey) {
95
+ if (ingestResult.queued > 0 && config.anthropicApiKey) {
186
96
  const pendingCount = (0, queries_2.getPendingAnalysisCount)(db);
187
97
  const estimate = (0, cost_1.estimateAnalysisCost)(pendingCount, config.batchSize, config.analysisModel);
188
98
  console.log('\n' + (0, cost_1.formatCostEstimate)(estimate));
@@ -209,19 +119,47 @@ program
209
119
  totalOutputTokens += batchResult.totalOutputTokens;
210
120
  totalCost += batchResult.totalCost;
211
121
  }
122
+ // Analyze types, routes, and file summaries
123
+ console.log('\n Analyzing types, routes, and file summaries...');
124
+ const typeResult = await (0, analyzer_1.analyzeTypes)(db, config.analysisModel, config.anthropicApiKey);
125
+ const routeResult = await (0, analyzer_1.analyzeRoutes)(db, config.analysisModel, config.anthropicApiKey);
126
+ const fileResult = await (0, analyzer_1.analyzeFileSummaries)(db, config.analysisModel, config.anthropicApiKey);
127
+ totalAnalyzed += typeResult.analyzed + routeResult.analyzed + fileResult.analyzed;
128
+ totalFailed += typeResult.failed + routeResult.failed + fileResult.failed;
129
+ totalInputTokens += typeResult.totalInputTokens + routeResult.totalInputTokens + fileResult.totalInputTokens;
130
+ totalOutputTokens += typeResult.totalOutputTokens + routeResult.totalOutputTokens + fileResult.totalOutputTokens;
131
+ totalCost += typeResult.totalCost + routeResult.totalCost + fileResult.totalCost;
212
132
  (0, analyzer_1.rebuildSearchIndex)(db);
213
133
  console.log(`\nAnalysis complete:`);
214
- console.log(` Analyzed: ${totalAnalyzed}`);
134
+ console.log(` Functions: ${totalAnalyzed - typeResult.analyzed - routeResult.analyzed - fileResult.analyzed}`);
135
+ console.log(` Types: ${typeResult.analyzed}`);
136
+ console.log(` Routes: ${routeResult.analyzed}`);
137
+ console.log(` Files: ${fileResult.analyzed}`);
215
138
  console.log(` From cache: ${totalCached}`);
216
139
  console.log(` Failed: ${totalFailed}`);
217
140
  console.log(` Input tokens: ${totalInputTokens.toLocaleString()}`);
218
141
  console.log(` Output tokens: ${totalOutputTokens.toLocaleString()}`);
219
142
  console.log(` Total cost: $${totalCost.toFixed(4)}`);
220
143
  }
221
- else if (queued > 0) {
144
+ else if (ingestResult.queued > 0) {
222
145
  console.log('\nSkipping analysis: ANTHROPIC_API_KEY not set.');
223
146
  console.log('Set the key and run "structx analyze . --yes" to enrich functions.');
224
147
  }
148
+ else if (config.anthropicApiKey) {
149
+ // Still analyze types/routes/files even if no new functions
150
+ console.log('\nAnalyzing types, routes, and file summaries...');
151
+ const typeResult = await (0, analyzer_1.analyzeTypes)(db, config.analysisModel, config.anthropicApiKey);
152
+ const routeResult = await (0, analyzer_1.analyzeRoutes)(db, config.analysisModel, config.anthropicApiKey);
153
+ const fileResult = await (0, analyzer_1.analyzeFileSummaries)(db, config.analysisModel, config.anthropicApiKey);
154
+ const entityCount = typeResult.analyzed + routeResult.analyzed + fileResult.analyzed;
155
+ if (entityCount > 0) {
156
+ (0, analyzer_1.rebuildSearchIndex)(db);
157
+ console.log(` Analyzed ${entityCount} entities (${typeResult.analyzed} types, ${routeResult.analyzed} routes, ${fileResult.analyzed} files)`);
158
+ }
159
+ else {
160
+ console.log('No entities to analyze.');
161
+ }
162
+ }
225
163
  else {
226
164
  console.log('\nNo functions to analyze.');
227
165
  }
@@ -303,6 +241,25 @@ program
303
241
  console.log(' .github/copilot-instructions.md — created.');
304
242
  installed++;
305
243
  }
244
+ // AGENTS.md (OpenAI Codex)
245
+ const agentsMdPath = path.join(resolved, 'AGENTS.md');
246
+ const codexContent = fs.readFileSync(path.join(instructionsDir, 'codex.md'), 'utf-8');
247
+ if (fs.existsSync(agentsMdPath)) {
248
+ const existing = fs.readFileSync(agentsMdPath, 'utf-8');
249
+ if (existing.includes('StructX')) {
250
+ console.log(' AGENTS.md — already contains StructX section, skipping.');
251
+ }
252
+ else {
253
+ fs.appendFileSync(agentsMdPath, '\n\n' + codexContent, 'utf-8');
254
+ console.log(' AGENTS.md — appended StructX section.');
255
+ installed++;
256
+ }
257
+ }
258
+ else {
259
+ fs.writeFileSync(agentsMdPath, codexContent, 'utf-8');
260
+ console.log(' AGENTS.md — created.');
261
+ installed++;
262
+ }
306
263
  console.log(`\nInstalled ${installed} instruction file(s) into ${resolved}`);
307
264
  });
308
265
  // ── init ──
@@ -350,11 +307,105 @@ program
350
307
  console.log('──────────────────────────');
351
308
  console.log(` Files: ${stats.totalFiles}`);
352
309
  console.log(` Functions: ${stats.totalFunctions}`);
310
+ console.log(` Types: ${stats.totalTypes}`);
311
+ console.log(` Routes: ${stats.totalRoutes}`);
312
+ console.log(` Constants: ${stats.totalConstants}`);
353
313
  console.log(` Relationships: ${stats.totalRelationships}`);
354
314
  console.log(` Analyzed: ${stats.analyzedFunctions} / ${stats.totalFunctions}`);
355
315
  console.log(` Pending: ${stats.pendingAnalysis}`);
356
316
  console.log(` QA Runs: ${stats.totalQaRuns}`);
357
317
  });
318
+ // ── overview ──
319
+ program
320
+ .command('overview')
321
+ .description('Full codebase summary in one shot — shows all files, functions, types, routes, and constants')
322
+ .option('--repo <path>', 'Path to TypeScript repository', '.')
323
+ .action((opts) => {
324
+ const resolved = path.resolve(opts.repo);
325
+ const structxDir = (0, config_1.getStructXDir)(resolved);
326
+ const dbPath = (0, connection_1.getDbPath)(structxDir);
327
+ if (!fs.existsSync(dbPath)) {
328
+ console.log('StructX not initialized. Run "structx setup ." first.');
329
+ return;
330
+ }
331
+ const db = (0, connection_1.openDatabase)(dbPath);
332
+ const overview = (0, queries_1.getFullOverview)(db);
333
+ db.close();
334
+ const { stats, files, functions, types, routes, constants } = overview;
335
+ // Header
336
+ console.log('StructX Codebase Overview');
337
+ console.log('═'.repeat(60));
338
+ console.log(` Files: ${stats.totalFiles} | Functions: ${stats.totalFunctions} | Types: ${stats.totalTypes} | Routes: ${stats.totalRoutes} | Constants: ${stats.totalConstants}`);
339
+ console.log(` Relationships: ${stats.totalRelationships} | Analyzed: ${stats.analyzedFunctions}/${stats.totalFunctions}`);
340
+ console.log('');
341
+ // Files section
342
+ if (files.length > 0) {
343
+ console.log('── Files ──');
344
+ for (const f of files) {
345
+ const purpose = f.summary?.purpose ? ` — ${f.summary.purpose}` : '';
346
+ const counts = [];
347
+ if (f.summary) {
348
+ if (f.summary.function_count > 0)
349
+ counts.push(`${f.summary.function_count} fns`);
350
+ if (f.summary.type_count > 0)
351
+ counts.push(`${f.summary.type_count} types`);
352
+ if (f.summary.route_count > 0)
353
+ counts.push(`${f.summary.route_count} routes`);
354
+ counts.push(`${f.summary.loc} LOC`);
355
+ }
356
+ const countsStr = counts.length > 0 ? ` (${counts.join(', ')})` : '';
357
+ console.log(` ${f.path}${countsStr}${purpose}`);
358
+ }
359
+ console.log('');
360
+ }
361
+ // Routes section
362
+ if (routes.length > 0) {
363
+ console.log('── Routes / Endpoints ──');
364
+ for (const r of routes) {
365
+ const purpose = r.purpose ? ` — ${r.purpose}` : '';
366
+ const file = r.filePath.split(/[/\\]/).slice(-1)[0];
367
+ console.log(` ${r.method.toUpperCase().padEnd(7)} ${r.path} [${file}:${r.start_line}]${purpose}`);
368
+ }
369
+ console.log('');
370
+ }
371
+ // Types section
372
+ if (types.length > 0) {
373
+ console.log('── Types & Interfaces ──');
374
+ for (const t of types) {
375
+ const purpose = t.purpose ? ` — ${t.purpose}` : '';
376
+ const exported = t.is_exported ? '(exported) ' : '';
377
+ const file = t.filePath.split(/[/\\]/).slice(-1)[0];
378
+ console.log(` ${t.kind.padEnd(12)} ${t.name} ${exported}[${file}:${t.start_line}]${purpose}`);
379
+ }
380
+ console.log('');
381
+ }
382
+ // Functions section
383
+ if (functions.length > 0) {
384
+ console.log('── Functions ──');
385
+ for (const fn of functions) {
386
+ const purpose = fn.purpose ? ` — ${fn.purpose}` : '';
387
+ const exported = fn.is_exported ? '(exported) ' : '';
388
+ const asyncStr = fn.is_async ? 'async ' : '';
389
+ const file = fn.filePath.split(/[/\\]/).slice(-1)[0];
390
+ console.log(` ${asyncStr}${fn.name} ${exported}[${file}:${fn.start_line}]${purpose}`);
391
+ }
392
+ console.log('');
393
+ }
394
+ // Exported constants section
395
+ if (constants.length > 0) {
396
+ console.log('── Exported Constants ──');
397
+ for (const c of constants) {
398
+ const typeStr = c.type_annotation ? `: ${c.type_annotation}` : '';
399
+ const valStr = c.value_text ? ` = ${c.value_text.substring(0, 60)}${c.value_text.length > 60 ? '...' : ''}` : '';
400
+ const file = c.filePath.split(/[/\\]/).slice(-1)[0];
401
+ console.log(` ${c.name}${typeStr}${valStr} [${file}:${c.start_line}]`);
402
+ }
403
+ console.log('');
404
+ }
405
+ if (stats.totalFunctions === 0 && stats.totalTypes === 0 && stats.totalRoutes === 0) {
406
+ console.log('Knowledge graph is empty. Run "structx setup ." to populate it.');
407
+ }
408
+ });
358
409
  // ── doctor ──
359
410
  program
360
411
  .command('doctor')
@@ -430,112 +481,11 @@ program
430
481
  }
431
482
  const config = (0, config_1.loadConfig)(structxDir);
432
483
  const db = (0, connection_1.openDatabase)(dbPath);
433
- const project = (0, parser_1.createProject)(resolved);
434
484
  console.log(`Scanning ${resolved} for TypeScript files...`);
435
- const files = (0, scanner_1.scanDirectory)(resolved);
436
- console.log(`Found ${files.length} TypeScript files.`);
437
- let newFiles = 0;
438
- let changedFiles = 0;
439
- let unchangedFiles = 0;
440
- let totalFunctions = 0;
441
- let totalRelationships = 0;
442
- let queued = 0;
443
- for (const filePath of files) {
444
- const relativePath = path.relative(resolved, filePath);
445
- const content = fs.readFileSync(filePath, 'utf-8');
446
- const contentHash = (0, parser_1.hashFileContent)(content);
447
- // Check if file changed
448
- const existingFile = (0, queries_1.getFileByPath)(db, relativePath);
449
- if (existingFile && existingFile.content_hash === contentHash) {
450
- unchangedFiles++;
451
- continue;
452
- }
453
- const isNew = !existingFile;
454
- if (isNew)
455
- newFiles++;
456
- else
457
- changedFiles++;
458
- // Upsert file record
459
- const fileId = (0, queries_1.upsertFile)(db, relativePath, contentHash);
460
- // Get old functions for diff comparison
461
- const oldFunctions = isNew ? [] : (0, queries_1.getFunctionsByFileId)(db, fileId);
462
- const oldFunctionMap = new Map(oldFunctions.map(f => [f.name, f]));
463
- // Clear old data for this file
464
- if (!isNew) {
465
- // Delete relationships for all old functions in this file
466
- for (const oldFn of oldFunctions) {
467
- (0, queries_1.deleteRelationshipsByCallerFunctionId)(db, oldFn.id);
468
- }
469
- (0, queries_1.deleteFunctionsByFileId)(db, fileId);
470
- }
471
- // Parse functions
472
- let functions;
473
- try {
474
- functions = (0, parser_1.parseFile)(project, filePath);
475
- }
476
- catch (err) {
477
- logger_1.logger.warn(`Failed to parse ${relativePath}: ${err.message}`);
478
- continue;
479
- }
480
- // Insert functions and check for re-analysis needs
481
- const functionIdMap = new Map();
482
- for (const fn of functions) {
483
- const fnId = (0, queries_1.insertFunction)(db, {
484
- file_id: fileId,
485
- name: fn.name,
486
- signature: fn.signature,
487
- body: fn.body,
488
- code_hash: fn.codeHash,
489
- start_line: fn.startLine,
490
- end_line: fn.endLine,
491
- is_exported: fn.isExported,
492
- is_async: fn.isAsync,
493
- });
494
- functionIdMap.set(fn.name, fnId);
495
- totalFunctions++;
496
- // Determine if we need semantic re-analysis
497
- const oldFn = oldFunctionMap.get(fn.name);
498
- if (!oldFn) {
499
- // New function — queue for analysis
500
- (0, queries_1.enqueueForAnalysis)(db, fnId, 'new', (0, differ_1.getPriority)('new', fn.isExported));
501
- queued++;
502
- }
503
- else {
504
- const { reanalyze, reason } = (0, differ_1.shouldReanalyze)(oldFn, fn.signature, fn.codeHash, fn.body, config.diffThreshold);
505
- if (reanalyze) {
506
- (0, queries_1.enqueueForAnalysis)(db, fnId, reason, (0, differ_1.getPriority)(reason, fn.isExported));
507
- queued++;
508
- }
509
- }
510
- }
511
- // Extract and insert relationships
512
- try {
513
- const calls = (0, relationships_1.extractCallsFromFile)(project, filePath);
514
- for (const call of calls) {
515
- if (call.callerName === '__file__')
516
- continue; // Skip file-level imports for now
517
- const callerId = functionIdMap.get(call.callerName);
518
- if (!callerId)
519
- continue;
520
- // Try to resolve callee to a function ID
521
- const callee = (0, queries_1.getFunctionByName)(db, call.calleeName);
522
- (0, queries_1.insertRelationship)(db, callerId, call.calleeName, call.relationType, callee?.id);
523
- totalRelationships++;
524
- }
525
- }
526
- catch (err) {
527
- logger_1.logger.warn(`Failed to extract calls from ${relativePath}: ${err.message}`);
528
- }
529
- }
485
+ const ingestResult = (0, ingester_1.ingestDirectory)(db, resolved, config.diffThreshold);
486
+ (0, ingester_1.printIngestResult)(ingestResult);
530
487
  db.close();
531
- console.log(`\nIngestion complete:`);
532
- console.log(` New files: ${newFiles}`);
533
- console.log(` Changed files: ${changedFiles}`);
534
- console.log(` Unchanged: ${unchangedFiles}`);
535
- console.log(` Functions: ${totalFunctions}`);
536
- console.log(` Relationships: ${totalRelationships}`);
537
- console.log(` Queued: ${queued} functions for semantic analysis`);
538
- if (queued > 0) {
488
+ if (ingestResult.queued > 0) {
539
489
  console.log(`\nNext: run 'structx analyze' to enrich functions with semantic metadata.`);
540
490
  }
541
491
  });
@@ -545,6 +495,7 @@ program
545
495
  .description('Run LLM semantic analysis on extracted functions')
546
496
  .argument('[repo-path]', 'Path to TypeScript repository', '.')
547
497
  .option('--yes', 'Skip cost confirmation prompt')
498
+ .option('--api-key <key>', 'Anthropic API key (overrides ANTHROPIC_API_KEY env var)')
548
499
  .action(async (repoPath, opts) => {
549
500
  const resolved = path.resolve(repoPath);
550
501
  const structxDir = (0, config_1.getStructXDir)(resolved);
@@ -554,8 +505,11 @@ program
554
505
  return;
555
506
  }
556
507
  const config = (0, config_1.loadConfig)(structxDir);
508
+ if (opts.apiKey)
509
+ config.anthropicApiKey = opts.apiKey;
557
510
  if (!config.anthropicApiKey) {
558
- console.log('Anthropic API key not set. Set ANTHROPIC_API_KEY env var or add to .structx/config.json');
511
+ console.log('ERROR: Anthropic API key not set.');
512
+ console.log('Fix: Set ANTHROPIC_API_KEY env var, pass --api-key <key>, or add to .structx/config.json');
559
513
  return;
560
514
  }
561
515
  const db = (0, connection_1.openDatabase)(dbPath);
@@ -604,11 +558,21 @@ program
604
558
  totalOutputTokens += batchResult.totalOutputTokens;
605
559
  totalCost += batchResult.totalCost;
606
560
  }
561
+ // Analyze types, routes, and file summaries
562
+ console.log('\n Analyzing types, routes, and file summaries...');
563
+ const typeResult = await (0, analyzer_1.analyzeTypes)(db, config.analysisModel, config.anthropicApiKey);
564
+ const routeResult = await (0, analyzer_1.analyzeRoutes)(db, config.analysisModel, config.anthropicApiKey);
565
+ const fileResult = await (0, analyzer_1.analyzeFileSummaries)(db, config.analysisModel, config.anthropicApiKey);
566
+ totalAnalyzed += typeResult.analyzed + routeResult.analyzed + fileResult.analyzed;
567
+ totalFailed += typeResult.failed + routeResult.failed + fileResult.failed;
568
+ totalInputTokens += typeResult.totalInputTokens + routeResult.totalInputTokens + fileResult.totalInputTokens;
569
+ totalOutputTokens += typeResult.totalOutputTokens + routeResult.totalOutputTokens + fileResult.totalOutputTokens;
570
+ totalCost += typeResult.totalCost + routeResult.totalCost + fileResult.totalCost;
607
571
  // Rebuild FTS index
608
572
  (0, analyzer_1.rebuildSearchIndex)(db);
609
573
  db.close();
610
574
  console.log(`\nAnalysis complete:`);
611
- console.log(` Analyzed: ${totalAnalyzed}`);
575
+ console.log(` Analyzed: ${totalAnalyzed} (incl. ${typeResult.analyzed} types, ${routeResult.analyzed} routes, ${fileResult.analyzed} files)`);
612
576
  console.log(` From cache: ${totalCached}`);
613
577
  console.log(` Failed: ${totalFailed}`);
614
578
  console.log(` Input tokens: ${totalInputTokens.toLocaleString()}`);
@@ -621,20 +585,88 @@ program
621
585
  .description('Ask a question about the codebase')
622
586
  .argument('<question>', 'The question to ask')
623
587
  .option('--repo <path>', 'Path to TypeScript repository', '.')
588
+ .option('--api-key <key>', 'Anthropic API key (overrides ANTHROPIC_API_KEY env var)')
624
589
  .action(async (question, opts) => {
625
590
  const resolved = path.resolve(opts.repo);
626
591
  const structxDir = (0, config_1.getStructXDir)(resolved);
627
592
  const dbPath = (0, connection_1.getDbPath)(structxDir);
593
+ // Auto-setup: if DB doesn't exist, run full setup automatically
628
594
  if (!fs.existsSync(dbPath)) {
629
- console.log('StructX not initialized. Run "structx init" first.');
630
- return;
595
+ console.log('StructX not initialized. Running automatic setup...\n');
596
+ const db = (0, connection_1.initializeDatabase)(dbPath);
597
+ (0, config_1.saveConfig)(structxDir, { repoPath: resolved });
598
+ const config = (0, config_1.loadConfig)(structxDir);
599
+ if (opts.apiKey)
600
+ config.anthropicApiKey = opts.apiKey;
601
+ const result = (0, ingester_1.ingestDirectory)(db, resolved, config.diffThreshold);
602
+ (0, ingester_1.printIngestResult)(result);
603
+ // Run semantic analysis if API key available
604
+ if (config.anthropicApiKey) {
605
+ console.log('\nRunning semantic analysis...');
606
+ const pending = (0, queries_2.getPendingAnalysis)(db, config.batchSize);
607
+ if (pending.length > 0) {
608
+ let batchNum = 0;
609
+ while (true) {
610
+ const items = (0, queries_2.getPendingAnalysis)(db, config.batchSize);
611
+ if (items.length === 0)
612
+ break;
613
+ batchNum++;
614
+ console.log(` Batch ${batchNum}: ${items.length} functions...`);
615
+ await (0, analyzer_1.analyzeBatch)(db, items.map(p => ({ id: p.id, function_id: p.function_id })), config.analysisModel, config.anthropicApiKey);
616
+ }
617
+ await (0, analyzer_1.analyzeTypes)(db, config.analysisModel, config.anthropicApiKey);
618
+ await (0, analyzer_1.analyzeRoutes)(db, config.analysisModel, config.anthropicApiKey);
619
+ await (0, analyzer_1.analyzeFileSummaries)(db, config.analysisModel, config.anthropicApiKey);
620
+ (0, analyzer_1.rebuildSearchIndex)(db);
621
+ }
622
+ console.log('Setup complete. Now answering your question...\n');
623
+ }
624
+ else {
625
+ console.log('\nWARNING: ANTHROPIC_API_KEY not set. Semantic analysis skipped.');
626
+ console.log('Results will be limited. Set the key and run "structx analyze . --yes" for better answers.\n');
627
+ }
628
+ db.close();
631
629
  }
632
630
  const config = (0, config_1.loadConfig)(structxDir);
631
+ if (opts.apiKey)
632
+ config.anthropicApiKey = opts.apiKey;
633
633
  if (!config.anthropicApiKey) {
634
- console.log('Anthropic API key not set. Set ANTHROPIC_API_KEY env var or add to .structx/config.json');
634
+ console.log('ERROR: Anthropic API key not set.');
635
+ console.log('Fix one of:');
636
+ console.log(' 1. Set ANTHROPIC_API_KEY environment variable');
637
+ console.log(' 2. Pass --api-key <key> to this command');
638
+ console.log(' 3. Add "anthropicApiKey" to .structx/config.json');
639
+ console.log('\nNote: "structx overview --repo ." works without an API key to see the codebase structure.');
635
640
  return;
636
641
  }
637
642
  const db = (0, connection_1.openDatabase)(dbPath);
643
+ // Check if DB is empty — suggest re-ingesting
644
+ const stats = (0, queries_1.getStats)(db);
645
+ if (stats.totalFunctions === 0 && stats.totalTypes === 0 && stats.totalRoutes === 0) {
646
+ console.log('WARNING: The knowledge graph is empty (0 functions, 0 types, 0 routes).');
647
+ console.log('Running automatic re-ingestion...\n');
648
+ const result = (0, ingester_1.ingestDirectory)(db, resolved, config.diffThreshold);
649
+ (0, ingester_1.printIngestResult)(result);
650
+ if (result.queued > 0) {
651
+ console.log('\nRunning semantic analysis...');
652
+ while (true) {
653
+ const items = (0, queries_2.getPendingAnalysis)(db, config.batchSize);
654
+ if (items.length === 0)
655
+ break;
656
+ await (0, analyzer_1.analyzeBatch)(db, items.map(p => ({ id: p.id, function_id: p.function_id })), config.analysisModel, config.anthropicApiKey);
657
+ }
658
+ await (0, analyzer_1.analyzeTypes)(db, config.analysisModel, config.anthropicApiKey);
659
+ await (0, analyzer_1.analyzeRoutes)(db, config.analysisModel, config.anthropicApiKey);
660
+ await (0, analyzer_1.analyzeFileSummaries)(db, config.analysisModel, config.anthropicApiKey);
661
+ (0, analyzer_1.rebuildSearchIndex)(db);
662
+ }
663
+ console.log('');
664
+ }
665
+ // Warn if semantic analysis hasn't been done
666
+ if (stats.totalFunctions > 0 && stats.analyzedFunctions === 0) {
667
+ console.log('WARNING: No functions have been semantically analyzed. Results may be limited.');
668
+ console.log('Run "structx analyze . --yes" to enrich the knowledge graph.\n');
669
+ }
638
670
  const startTime = Date.now();
639
671
  // Step 1: Classify the question
640
672
  console.log('Classifying question...');
@@ -660,6 +692,21 @@ program
660
692
  case 'impact':
661
693
  retrieved = (0, retriever_1.impactAnalysis)(db, classification.functionName || '');
662
694
  break;
695
+ case 'route':
696
+ retrieved = (0, retriever_1.routeQuery)(db, classification.routePath, classification.routeMethod);
697
+ break;
698
+ case 'type':
699
+ retrieved = (0, retriever_1.typeQuery)(db, classification.typeName || classification.keywords.join(' '));
700
+ break;
701
+ case 'file':
702
+ retrieved = (0, retriever_1.fileQuery)(db, classification.filePath);
703
+ break;
704
+ case 'list':
705
+ retrieved = (0, retriever_1.listQuery)(db, classification.listEntity);
706
+ break;
707
+ case 'pattern':
708
+ retrieved = (0, retriever_1.patternQuery)(db, classification.keywords);
709
+ break;
663
710
  default:
664
711
  retrieved = (0, retriever_1.semanticSearch)(db, classification.keywords);
665
712
  }
@@ -670,10 +717,12 @@ program
670
717
  console.log('Generating answer...\n');
671
718
  const answerResult = await (0, answerer_1.generateAnswer)(question, context, config.answerModel, config.anthropicApiKey);
672
719
  // Display answer
720
+ const entityCount = retrieved.functions.length + retrieved.types.length +
721
+ retrieved.routes.length + retrieved.files.length + retrieved.constants.length;
673
722
  console.log('─'.repeat(60));
674
723
  console.log(answerResult.answer);
675
724
  console.log('─'.repeat(60));
676
- console.log(`\nStrategy: ${classification.strategy} | Functions: ${retrieved.functions.length} | Graph query: ${graphQueryTimeMs}ms`);
725
+ console.log(`\nStrategy: ${classification.strategy} | Entities: ${entityCount} | Graph query: ${graphQueryTimeMs}ms`);
677
726
  console.log(`Tokens: ${answerResult.inputTokens} in / ${answerResult.outputTokens} out | Cost: $${answerResult.cost.toFixed(4)} | Time: ${answerResult.responseTimeMs}ms`);
678
727
  // Save run to DB
679
728
  (0, queries_2.insertQaRun)(db, {