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.
- package/package.json +2 -1
- package/sapper.mjs +330 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapper-iq",
|
|
3
|
-
"version": "1.1.
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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
|
|