sapper-iq 1.1.34 → 1.1.35

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 (2) hide show
  1. package/package.json +2 -1
  2. package/sapper.mjs +330 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.1.34",
3
+ "version": "1.1.35",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "start": "node sapper.mjs"
12
12
  },
13
13
  "dependencies": {
14
+ "acorn": "^8.15.0",
14
15
  "chalk": "^5.3.0",
15
16
  "marked": "^15.0.12",
16
17
  "marked-terminal": "^7.3.0",
package/sapper.mjs CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  import { dirname, join } from 'path';
10
10
  import { marked } from 'marked';
11
11
  import TerminalRenderer from 'marked-terminal';
12
+ import * as acorn from 'acorn';
12
13
 
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = dirname(__filename);
@@ -232,6 +233,7 @@ async function buildWorkspaceGraph(showProgress = true) {
232
233
  modified: stats.mtime.toISOString(),
233
234
  imports: deps,
234
235
  exports: exports,
236
+ symbols: parseFileSymbols(content, fullPath), // AST-extracted symbols
235
237
  summary: summary || '(no summary)'
236
238
  };
237
239
 
@@ -310,6 +312,248 @@ function formatWorkspaceSummary(workspace) {
310
312
  return output;
311
313
  }
312
314
 
315
+ // ═══════════════════════════════════════════════════════════════
316
+ // AST PARSING - Extract symbols (functions, classes, variables)
317
+ // ═══════════════════════════════════════════════════════════════
318
+
319
+ // Parse JavaScript/TypeScript file and extract symbols
320
+ function parseFileSymbols(content, filePath) {
321
+ const symbols = [];
322
+ const ext = filePath.split('.').pop()?.toLowerCase();
323
+
324
+ // Only parse JS/TS files with acorn
325
+ if (!['js', 'jsx', 'ts', 'tsx', 'mjs'].includes(ext)) {
326
+ // For other languages, use regex-based extraction
327
+ return extractSymbolsWithRegex(content, filePath);
328
+ }
329
+
330
+ try {
331
+ // Parse with acorn (use loose parsing to handle more syntax)
332
+ const ast = acorn.parse(content, {
333
+ ecmaVersion: 'latest',
334
+ sourceType: 'module',
335
+ locations: true,
336
+ allowHashBang: true,
337
+ allowAwaitOutsideFunction: true,
338
+ allowImportExportEverywhere: true,
339
+ // Be lenient with errors
340
+ onComment: () => {},
341
+ });
342
+
343
+ // Walk the AST to extract symbols
344
+ function walk(node, parentName = null) {
345
+ if (!node || typeof node !== 'object') return;
346
+
347
+ switch (node.type) {
348
+ case 'FunctionDeclaration':
349
+ if (node.id?.name) {
350
+ symbols.push({
351
+ type: 'function',
352
+ name: node.id.name,
353
+ line: node.loc?.start?.line || 0,
354
+ params: node.params?.map(p => p.name || p.left?.name || '?').join(', ') || '',
355
+ async: node.async || false,
356
+ });
357
+ }
358
+ break;
359
+
360
+ case 'ClassDeclaration':
361
+ if (node.id?.name) {
362
+ symbols.push({
363
+ type: 'class',
364
+ name: node.id.name,
365
+ line: node.loc?.start?.line || 0,
366
+ extends: node.superClass?.name || null,
367
+ });
368
+ // Extract methods
369
+ if (node.body?.body) {
370
+ for (const member of node.body.body) {
371
+ if (member.type === 'MethodDefinition' && member.key?.name) {
372
+ symbols.push({
373
+ type: 'method',
374
+ name: `${node.id.name}.${member.key.name}`,
375
+ line: member.loc?.start?.line || 0,
376
+ kind: member.kind, // 'constructor', 'method', 'get', 'set'
377
+ });
378
+ }
379
+ }
380
+ }
381
+ }
382
+ break;
383
+
384
+ case 'VariableDeclaration':
385
+ for (const decl of node.declarations || []) {
386
+ if (decl.id?.name) {
387
+ // Check if it's a function expression or arrow function
388
+ const init = decl.init;
389
+ if (init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression') {
390
+ symbols.push({
391
+ type: 'function',
392
+ name: decl.id.name,
393
+ line: node.loc?.start?.line || 0,
394
+ params: init.params?.map(p => p.name || p.left?.name || '?').join(', ') || '',
395
+ async: init.async || false,
396
+ arrow: init.type === 'ArrowFunctionExpression',
397
+ });
398
+ } else {
399
+ symbols.push({
400
+ type: 'variable',
401
+ name: decl.id.name,
402
+ line: node.loc?.start?.line || 0,
403
+ kind: node.kind, // 'const', 'let', 'var'
404
+ });
405
+ }
406
+ }
407
+ }
408
+ break;
409
+
410
+ case 'ExportNamedDeclaration':
411
+ if (node.declaration) {
412
+ walk(node.declaration, parentName);
413
+ }
414
+ break;
415
+
416
+ case 'ExportDefaultDeclaration':
417
+ if (node.declaration) {
418
+ if (node.declaration.id?.name) {
419
+ symbols.push({
420
+ type: node.declaration.type === 'ClassDeclaration' ? 'class' : 'function',
421
+ name: node.declaration.id.name,
422
+ line: node.loc?.start?.line || 0,
423
+ exported: 'default',
424
+ });
425
+ }
426
+ }
427
+ break;
428
+ }
429
+
430
+ // Recursively walk children
431
+ for (const key in node) {
432
+ if (key === 'loc' || key === 'range') continue;
433
+ const child = node[key];
434
+ if (Array.isArray(child)) {
435
+ child.forEach(c => walk(c, parentName));
436
+ } else if (child && typeof child === 'object') {
437
+ walk(child, parentName);
438
+ }
439
+ }
440
+ }
441
+
442
+ walk(ast);
443
+
444
+ } catch (e) {
445
+ // If AST parsing fails, fall back to regex
446
+ return extractSymbolsWithRegex(content, filePath);
447
+ }
448
+
449
+ return symbols;
450
+ }
451
+
452
+ // Fallback: extract symbols using regex (for non-JS or when AST fails)
453
+ function extractSymbolsWithRegex(content, filePath) {
454
+ const symbols = [];
455
+ const lines = content.split('\n');
456
+ const ext = filePath.split('.').pop()?.toLowerCase();
457
+
458
+ // JavaScript/TypeScript patterns
459
+ if (['js', 'jsx', 'ts', 'tsx', 'mjs'].includes(ext)) {
460
+ const funcPattern = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
461
+ const classPattern = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
462
+ const arrowPattern = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g;
463
+ const methodPattern = /^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/gm;
464
+
465
+ let match;
466
+ while ((match = funcPattern.exec(content))) {
467
+ const line = content.substring(0, match.index).split('\n').length;
468
+ symbols.push({ type: 'function', name: match[1], line });
469
+ }
470
+ while ((match = classPattern.exec(content))) {
471
+ const line = content.substring(0, match.index).split('\n').length;
472
+ symbols.push({ type: 'class', name: match[1], line, extends: match[2] });
473
+ }
474
+ while ((match = arrowPattern.exec(content))) {
475
+ const line = content.substring(0, match.index).split('\n').length;
476
+ symbols.push({ type: 'function', name: match[1], line, arrow: true });
477
+ }
478
+ }
479
+
480
+ // Python patterns
481
+ if (ext === 'py') {
482
+ const funcPattern = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
483
+ const classPattern = /^class\s+(\w+)(?:\s*\([^)]*\))?:/gm;
484
+
485
+ let match;
486
+ while ((match = funcPattern.exec(content))) {
487
+ const line = content.substring(0, match.index).split('\n').length;
488
+ symbols.push({ type: 'function', name: match[1], line });
489
+ }
490
+ while ((match = classPattern.exec(content))) {
491
+ const line = content.substring(0, match.index).split('\n').length;
492
+ symbols.push({ type: 'class', name: match[1], line });
493
+ }
494
+ }
495
+
496
+ // Java/C#/Go patterns
497
+ if (['java', 'cs', 'go'].includes(ext)) {
498
+ const funcPattern = /(?:public|private|protected|static|func)?\s*(?:\w+\s+)?(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{/g;
499
+ const classPattern = /(?:public\s+)?(?:class|struct|interface)\s+(\w+)/g;
500
+
501
+ let match;
502
+ while ((match = funcPattern.exec(content))) {
503
+ const line = content.substring(0, match.index).split('\n').length;
504
+ if (!['if', 'for', 'while', 'switch', 'catch'].includes(match[1])) {
505
+ symbols.push({ type: 'function', name: match[1], line });
506
+ }
507
+ }
508
+ while ((match = classPattern.exec(content))) {
509
+ const line = content.substring(0, match.index).split('\n').length;
510
+ symbols.push({ type: 'class', name: match[1], line });
511
+ }
512
+ }
513
+
514
+ return symbols;
515
+ }
516
+
517
+ // Search for symbol across workspace
518
+ function searchSymbol(query, workspace) {
519
+ const results = [];
520
+ const queryLower = query.toLowerCase();
521
+
522
+ for (const [filePath, fileInfo] of Object.entries(workspace.files)) {
523
+ if (!fileInfo.symbols) continue;
524
+
525
+ for (const symbol of fileInfo.symbols) {
526
+ if (symbol.name.toLowerCase().includes(queryLower)) {
527
+ results.push({
528
+ ...symbol,
529
+ file: filePath,
530
+ score: symbol.name.toLowerCase() === queryLower ? 100 :
531
+ symbol.name.toLowerCase().startsWith(queryLower) ? 80 : 50
532
+ });
533
+ }
534
+ }
535
+ }
536
+
537
+ // Sort by score (exact match first) then by name
538
+ results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
539
+ return results;
540
+ }
541
+
542
+ // Format symbol for display
543
+ function formatSymbol(symbol) {
544
+ const icon = symbol.type === 'function' ? '𝑓' :
545
+ symbol.type === 'class' ? '◆' :
546
+ symbol.type === 'method' ? '○' :
547
+ symbol.type === 'variable' ? '◇' : '•';
548
+
549
+ let desc = `${icon} ${symbol.name}`;
550
+ if (symbol.params !== undefined) desc += `(${symbol.params})`;
551
+ if (symbol.async) desc = 'async ' + desc;
552
+ if (symbol.extends) desc += ` extends ${symbol.extends}`;
553
+
554
+ return desc;
555
+ }
556
+
313
557
  // ═══════════════════════════════════════════════════════════════
314
558
  // EMBEDDINGS & SEMANTIC SEARCH
315
559
  // ═══════════════════════════════════════════════════════════════
@@ -911,13 +1155,15 @@ async function runSapper() {
911
1155
  // Auto-load or build workspace graph
912
1156
  let workspace = loadWorkspaceGraph();
913
1157
  if (!workspace.indexed) {
914
- console.log(chalk.cyan('📊 Building workspace index...'));
1158
+ console.log(chalk.cyan('📊 Building workspace index with AST parsing...'));
915
1159
  workspace = await buildWorkspaceGraph();
916
- console.log(chalk.green(`✅ Indexed ${Object.keys(workspace.files).length} files\n`));
1160
+ const totalSymbols = Object.values(workspace.files).reduce((sum, f) => sum + (f.symbols?.length || 0), 0);
1161
+ console.log(chalk.green(`✅ Indexed ${Object.keys(workspace.files).length} files, ${totalSymbols} symbols\n`));
917
1162
  } else {
918
1163
  const fileCount = Object.keys(workspace.files).length;
1164
+ const symbolCount = Object.values(workspace.files).reduce((sum, f) => sum + (f.symbols?.length || 0), 0);
919
1165
  const indexAge = Math.round((Date.now() - new Date(workspace.indexed).getTime()) / 1000 / 60);
920
- console.log(chalk.gray(`📊 Workspace: ${fileCount} files indexed (${indexAge}m ago)`));
1166
+ console.log(chalk.gray(`📊 Workspace: ${fileCount} files, ${symbolCount} symbols (${indexAge}m ago)`));
921
1167
  if (indexAge > 60) {
922
1168
  console.log(chalk.yellow(` Tip: Run /index to refresh`));
923
1169
  }
@@ -1121,6 +1367,7 @@ TOOL SYNTAX:
1121
1367
  `${chalk.cyan('/scan')} ${chalk.gray('│')} Scan codebase into context\n` +
1122
1368
  `${chalk.cyan('/index')} ${chalk.gray('│')} Rebuild workspace graph\n` +
1123
1369
  `${chalk.cyan('/graph file')} ${chalk.gray('│')} Show related files\n` +
1370
+ `${chalk.cyan('/symbol name')} ${chalk.gray('│')} Search functions/classes\n` +
1124
1371
  `${chalk.cyan('/auto')} ${chalk.gray('│')} Toggle auto-attach related files\n` +
1125
1372
  `${chalk.cyan('/recall')} ${chalk.gray('│')} Search memory for relevant context\n` +
1126
1373
  `${chalk.cyan('/reset /clear')} ${chalk.gray('│')} Clear all context\n` +
@@ -1136,10 +1383,88 @@ TOOL SYNTAX:
1136
1383
 
1137
1384
  // Handle index command - rebuild workspace graph
1138
1385
  if (input.toLowerCase() === '/index') {
1139
- console.log(chalk.cyan('\n📊 Rebuilding workspace index...'));
1386
+ console.log(chalk.cyan('\n📊 Rebuilding workspace index with AST parsing...'));
1140
1387
  workspace = await buildWorkspaceGraph();
1388
+ const totalSymbols = Object.values(workspace.files).reduce((sum, f) => sum + (f.symbols?.length || 0), 0);
1141
1389
  console.log(chalk.green(`✅ Indexed ${Object.keys(workspace.files).length} files`));
1142
- console.log(chalk.gray(` Graph: ${Object.values(workspace.graph).flat().length} dependencies tracked\n`));
1390
+ console.log(chalk.gray(` 📦 ${totalSymbols} symbols (functions, classes, variables)`));
1391
+ console.log(chalk.gray(` 🔗 ${Object.values(workspace.graph).flat().length} dependencies tracked\n`));
1392
+ continue;
1393
+ }
1394
+
1395
+ // Handle symbol search command
1396
+ if (input.toLowerCase().startsWith('/symbol')) {
1397
+ const query = input.slice(7).trim();
1398
+ if (!query) {
1399
+ // Show all symbols summary
1400
+ const allSymbols = [];
1401
+ for (const [file, info] of Object.entries(workspace.files)) {
1402
+ for (const sym of info.symbols || []) {
1403
+ allSymbols.push({ ...sym, file });
1404
+ }
1405
+ }
1406
+
1407
+ // Group by type
1408
+ const functions = allSymbols.filter(s => s.type === 'function');
1409
+ const classes = allSymbols.filter(s => s.type === 'class');
1410
+ const methods = allSymbols.filter(s => s.type === 'method');
1411
+
1412
+ console.log();
1413
+ console.log(box(
1414
+ `${chalk.cyan('Functions:')} ${functions.length}\n` +
1415
+ `${chalk.cyan('Classes:')} ${classes.length}\n` +
1416
+ `${chalk.cyan('Methods:')} ${methods.length}\n` +
1417
+ chalk.gray('─'.repeat(30)) + '\n' +
1418
+ chalk.gray('Usage: /symbol <name> to search'),
1419
+ '📦 Symbol Index', 'cyan'
1420
+ ));
1421
+ continue;
1422
+ }
1423
+
1424
+ console.log(chalk.cyan(`\n🔍 Searching for: "${query}"...\n`));
1425
+ const results = searchSymbol(query, workspace);
1426
+
1427
+ if (results.length === 0) {
1428
+ console.log(chalk.yellow(`No symbols found matching "${query}"`));
1429
+ console.log(chalk.gray('Tip: Run /index to refresh symbol index'));
1430
+ continue;
1431
+ }
1432
+
1433
+ console.log(chalk.green(`Found ${results.length} symbol${results.length !== 1 ? 's' : ''}:\n`));
1434
+
1435
+ for (const sym of results.slice(0, 15)) {
1436
+ const typeIcon = sym.type === 'function' ? chalk.yellow('𝑓') :
1437
+ sym.type === 'class' ? chalk.blue('◆') :
1438
+ sym.type === 'method' ? chalk.cyan('○') : chalk.gray('◇');
1439
+ const asyncTag = sym.async ? chalk.magenta('async ') : '';
1440
+ const params = sym.params !== undefined ? chalk.gray(`(${sym.params})`) : '';
1441
+
1442
+ console.log(` ${typeIcon} ${asyncTag}${chalk.white.bold(sym.name)}${params}`);
1443
+ console.log(` ${chalk.gray(sym.file)}:${chalk.cyan(sym.line)}`);
1444
+ }
1445
+
1446
+ if (results.length > 15) {
1447
+ console.log(chalk.gray(`\n ... and ${results.length - 15} more`));
1448
+ }
1449
+
1450
+ // Offer to add file to context
1451
+ if (results.length > 0) {
1452
+ console.log();
1453
+ const addToCtx = await safeQuestion(chalk.yellow('Add first match file to context? ') + chalk.gray('(y/n): '));
1454
+ if (addToCtx.toLowerCase() === 'y') {
1455
+ const targetFile = results[0].file;
1456
+ try {
1457
+ const content = fs.readFileSync(targetFile, 'utf8');
1458
+ messages.push({
1459
+ role: 'user',
1460
+ content: `Here is ${targetFile} (contains ${results[0].type} "${results[0].name}" at line ${results[0].line}):\n\n${content}`
1461
+ });
1462
+ console.log(chalk.green(`✅ Added ${targetFile} to context`));
1463
+ } catch (e) {
1464
+ console.log(chalk.red(`Could not read ${targetFile}`));
1465
+ }
1466
+ }
1467
+ }
1143
1468
  continue;
1144
1469
  }
1145
1470