sapper-iq 1.1.28 → 1.1.30
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 +1 -1
- package/sapper copy 3.mjs +1215 -0
- package/sapper.mjs +207 -2
package/sapper.mjs
CHANGED
|
@@ -316,6 +316,160 @@ function scanCodebase(dir = '.', depth = 0, maxDepth = 5) {
|
|
|
316
316
|
return { files, totalSize };
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// Scan directory for files (for @ file picker)
|
|
320
|
+
function getFilesForPicker(dir = '.', prefix = '', maxFiles = 50) {
|
|
321
|
+
let files = [];
|
|
322
|
+
try {
|
|
323
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
if (files.length >= maxFiles) break;
|
|
326
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
327
|
+
|
|
328
|
+
const fullPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
329
|
+
|
|
330
|
+
if (entry.isDirectory()) {
|
|
331
|
+
files.push({ path: fullPath + '/', isDir: true });
|
|
332
|
+
// Recurse one level for common structures
|
|
333
|
+
const subFiles = getFilesForPicker(`${dir}/${entry.name}`, fullPath, 20);
|
|
334
|
+
files = files.concat(subFiles.slice(0, 15)); // Limit subdirectory files
|
|
335
|
+
} else {
|
|
336
|
+
const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
|
|
337
|
+
if (CODE_EXTENSIONS.has(ext.toLowerCase()) || CODE_EXTENSIONS.has(entry.name)) {
|
|
338
|
+
try {
|
|
339
|
+
const stats = fs.statSync(`${dir}/${entry.name}`);
|
|
340
|
+
files.push({ path: fullPath, isDir: false, size: stats.size });
|
|
341
|
+
} catch (e) {
|
|
342
|
+
files.push({ path: fullPath, isDir: false, size: 0 });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {}
|
|
348
|
+
return files.slice(0, maxFiles);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Interactive file picker with arrow keys
|
|
352
|
+
async function pickFiles() {
|
|
353
|
+
const files = getFilesForPicker('.', '', 50).filter(f => !f.isDir);
|
|
354
|
+
|
|
355
|
+
if (files.length === 0) {
|
|
356
|
+
console.log(chalk.yellow('No code files found in current directory.'));
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const selected = new Set();
|
|
361
|
+
let cursor = 0;
|
|
362
|
+
const pageSize = Math.min(15, process.stdout.rows - 10 || 15);
|
|
363
|
+
|
|
364
|
+
// Enable raw mode for key capture
|
|
365
|
+
if (process.stdin.isTTY) {
|
|
366
|
+
process.stdin.setRawMode(true);
|
|
367
|
+
}
|
|
368
|
+
process.stdin.resume();
|
|
369
|
+
|
|
370
|
+
const renderList = () => {
|
|
371
|
+
// Clear screen and move cursor to top
|
|
372
|
+
console.clear();
|
|
373
|
+
console.log(box(
|
|
374
|
+
`${chalk.cyan('↑↓')} Navigate ${chalk.cyan('Space')} Toggle ${chalk.cyan('a')} All ${chalk.cyan('Enter')} Confirm ${chalk.cyan('q/Esc')} Cancel`,
|
|
375
|
+
'📎 Select Files', 'cyan'
|
|
376
|
+
));
|
|
377
|
+
console.log();
|
|
378
|
+
|
|
379
|
+
// Calculate visible range (pagination)
|
|
380
|
+
const startIdx = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), files.length - pageSize));
|
|
381
|
+
const endIdx = Math.min(startIdx + pageSize, files.length);
|
|
382
|
+
|
|
383
|
+
// Show scroll indicator if needed
|
|
384
|
+
if (startIdx > 0) {
|
|
385
|
+
console.log(chalk.gray(' ↑ more files above...'));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
389
|
+
const file = files[i];
|
|
390
|
+
const isSelected = selected.has(i);
|
|
391
|
+
const isCursor = i === cursor;
|
|
392
|
+
|
|
393
|
+
const checkbox = isSelected ? chalk.green('◉') : chalk.gray('○');
|
|
394
|
+
const prefix = isCursor ? chalk.cyan('▸ ') : ' ';
|
|
395
|
+
const name = isCursor ? chalk.cyan.bold(file.path) : chalk.white(file.path);
|
|
396
|
+
const size = file.size ? chalk.gray(` (${Math.round(file.size/1024)}KB)`) : '';
|
|
397
|
+
|
|
398
|
+
console.log(`${prefix}${checkbox} ${name}${size}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (endIdx < files.length) {
|
|
402
|
+
console.log(chalk.gray(' ↓ more files below...'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(chalk.gray(` Selected: ${selected.size} file${selected.size !== 1 ? 's' : ''}`));
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
renderList();
|
|
411
|
+
|
|
412
|
+
const onKeypress = (chunk, key) => {
|
|
413
|
+
if (!key) {
|
|
414
|
+
// Handle raw chunk for arrow keys
|
|
415
|
+
const str = chunk.toString();
|
|
416
|
+
if (str === '\x1b[A') key = { name: 'up' };
|
|
417
|
+
else if (str === '\x1b[B') key = { name: 'down' };
|
|
418
|
+
else if (str === '\x1b[C') key = { name: 'right' };
|
|
419
|
+
else if (str === '\x1b[D') key = { name: 'left' };
|
|
420
|
+
else if (str === ' ') key = { name: 'space' };
|
|
421
|
+
else if (str === '\r' || str === '\n') key = { name: 'return' };
|
|
422
|
+
else if (str === '\x1b' || str === 'q') key = { name: 'escape' };
|
|
423
|
+
else if (str === 'a' || str === 'A') key = { name: 'a' };
|
|
424
|
+
else if (str === '\x03') key = { name: 'c', ctrl: true }; // Ctrl+C
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!key) return;
|
|
428
|
+
|
|
429
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
430
|
+
cursor = cursor > 0 ? cursor - 1 : files.length - 1;
|
|
431
|
+
renderList();
|
|
432
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
433
|
+
cursor = cursor < files.length - 1 ? cursor + 1 : 0;
|
|
434
|
+
renderList();
|
|
435
|
+
} else if (key.name === 'space' || key.name === 'right') {
|
|
436
|
+
if (selected.has(cursor)) {
|
|
437
|
+
selected.delete(cursor);
|
|
438
|
+
} else {
|
|
439
|
+
selected.add(cursor);
|
|
440
|
+
}
|
|
441
|
+
renderList();
|
|
442
|
+
} else if (key.name === 'a') {
|
|
443
|
+
// Toggle all
|
|
444
|
+
if (selected.size === files.length) {
|
|
445
|
+
selected.clear();
|
|
446
|
+
} else {
|
|
447
|
+
for (let i = 0; i < files.length; i++) selected.add(i);
|
|
448
|
+
}
|
|
449
|
+
renderList();
|
|
450
|
+
} else if (key.name === 'return') {
|
|
451
|
+
cleanup();
|
|
452
|
+
const selectedFiles = Array.from(selected).map(i => files[i].path);
|
|
453
|
+
console.log(chalk.green(`\n✓ Selected ${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}`));
|
|
454
|
+
resolve(selectedFiles);
|
|
455
|
+
} else if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
456
|
+
cleanup();
|
|
457
|
+
console.log(chalk.gray('\nCancelled.'));
|
|
458
|
+
resolve([]);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const cleanup = () => {
|
|
463
|
+
process.stdin.removeListener('data', onKeypress);
|
|
464
|
+
if (process.stdin.isTTY) {
|
|
465
|
+
process.stdin.setRawMode(false);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
process.stdin.on('data', onKeypress);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
319
473
|
// Format scan results for AI context
|
|
320
474
|
function formatScanResults(scanResult) {
|
|
321
475
|
let output = `\n══════════════════════════════════════\n`;
|
|
@@ -681,7 +835,8 @@ TOOL SYNTAX:
|
|
|
681
835
|
if (input.toLowerCase() === '/help') {
|
|
682
836
|
console.log();
|
|
683
837
|
const helpContent =
|
|
684
|
-
`${chalk.cyan('@
|
|
838
|
+
`${chalk.cyan('@')} or ${chalk.cyan('/attach')} ${chalk.gray('│')} Pick files to attach (interactive)\n` +
|
|
839
|
+
`${chalk.cyan('@file')} ${chalk.gray('│')} Attach file inline (e.g., @src/app.js)\n` +
|
|
685
840
|
`${chalk.cyan('/scan')} ${chalk.gray('│')} Scan codebase into context\n` +
|
|
686
841
|
`${chalk.cyan('/recall')} ${chalk.gray('│')} Search memory for relevant context\n` +
|
|
687
842
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('│')} Clear all context\n` +
|
|
@@ -790,7 +945,56 @@ TOOL SYNTAX:
|
|
|
790
945
|
continue;
|
|
791
946
|
}
|
|
792
947
|
|
|
793
|
-
//
|
|
948
|
+
// Handle @ alone or /attach command - interactive file picker
|
|
949
|
+
if (input.trim() === '@' || input.toLowerCase() === '/attach') {
|
|
950
|
+
const selectedFiles = await pickFiles();
|
|
951
|
+
|
|
952
|
+
if (selectedFiles.length === 0) continue;
|
|
953
|
+
|
|
954
|
+
// Read and attach selected files
|
|
955
|
+
const fileAttachments = [];
|
|
956
|
+
for (const filePath of selectedFiles) {
|
|
957
|
+
try {
|
|
958
|
+
const stats = fs.statSync(filePath);
|
|
959
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
960
|
+
console.log(chalk.yellow(`⚠️ ${filePath} is too large, skipping`));
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
964
|
+
fileAttachments.push({ path: filePath, content, size: stats.size });
|
|
965
|
+
console.log(chalk.green(`📎 Attached: ${filePath}`));
|
|
966
|
+
} catch (e) {
|
|
967
|
+
console.log(chalk.yellow(`⚠️ Could not read ${filePath}`));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (fileAttachments.length === 0) continue;
|
|
972
|
+
|
|
973
|
+
// Ask for the prompt to go with these files
|
|
974
|
+
console.log();
|
|
975
|
+
const prompt = await safeQuestion(chalk.cyan('Your prompt for these files: '));
|
|
976
|
+
|
|
977
|
+
if (!prompt.trim()) {
|
|
978
|
+
console.log(chalk.gray('Cancelled.'));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Build message with attachments
|
|
983
|
+
let attachedContent = '\n\n══════════════════════════════════════\n';
|
|
984
|
+
attachedContent += `📎 ATTACHED FILES (${fileAttachments.length})\n`;
|
|
985
|
+
attachedContent += '══════════════════════════════════════\n\n';
|
|
986
|
+
|
|
987
|
+
for (const file of fileAttachments) {
|
|
988
|
+
attachedContent += `┌─── ${file.path} ───\n`;
|
|
989
|
+
attachedContent += file.content;
|
|
990
|
+
if (!file.content.endsWith('\n')) attachedContent += '\n';
|
|
991
|
+
attachedContent += `└─── END ${file.path} ───\n\n`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
messages.push({ role: 'user', content: prompt + attachedContent });
|
|
995
|
+
// Continue to AI response (don't use 'continue' here)
|
|
996
|
+
} else {
|
|
997
|
+
// Process @file attachments in prompt (e.g., "analyze @package.json" or "fix @src/index.js")
|
|
794
998
|
let processedInput = input;
|
|
795
999
|
const fileAttachments = [];
|
|
796
1000
|
const attachRegex = /@([\w.\/\-_]+)/g;
|
|
@@ -835,6 +1039,7 @@ TOOL SYNTAX:
|
|
|
835
1039
|
}
|
|
836
1040
|
|
|
837
1041
|
messages.push({ role: 'user', content: processedInput });
|
|
1042
|
+
} // End of else block for non-@ input
|
|
838
1043
|
|
|
839
1044
|
let toolRounds = 0; // Prevent infinite loops
|
|
840
1045
|
const MAX_TOOL_ROUNDS = 20;
|