sapper-iq 1.1.27 ā 1.1.29
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.mjs +192 -3
package/package.json
CHANGED
package/sapper.mjs
CHANGED
|
@@ -316,6 +316,99 @@ 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
|
|
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
|
+
console.log();
|
|
361
|
+
console.log(box(
|
|
362
|
+
`Select files by number (e.g., ${chalk.cyan('1 3 5')} or ${chalk.cyan('1-5')} or ${chalk.cyan('all')})\n` +
|
|
363
|
+
`Press ${chalk.cyan('Enter')} with no input to cancel`,
|
|
364
|
+
'š File Picker', 'cyan'
|
|
365
|
+
));
|
|
366
|
+
console.log();
|
|
367
|
+
|
|
368
|
+
// Display files in columns
|
|
369
|
+
files.forEach((file, i) => {
|
|
370
|
+
const num = chalk.cyan.bold(`[${(i + 1).toString().padStart(2)}]`);
|
|
371
|
+
const size = file.size ? chalk.gray(`(${Math.round(file.size/1024)}KB)`) : '';
|
|
372
|
+
console.log(` ${num} ${chalk.white(file.path)} ${size}`);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
console.log();
|
|
376
|
+
const selection = await safeQuestion(chalk.cyan('Select files: '));
|
|
377
|
+
|
|
378
|
+
if (!selection.trim()) {
|
|
379
|
+
console.log(chalk.gray('Cancelled.'));
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Parse selection
|
|
384
|
+
const selectedFiles = [];
|
|
385
|
+
const parts = selection.toLowerCase().split(/[\s,]+/);
|
|
386
|
+
|
|
387
|
+
for (const part of parts) {
|
|
388
|
+
if (part === 'all') {
|
|
389
|
+
return files.map(f => f.path);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Handle ranges like "1-5"
|
|
393
|
+
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
|
|
394
|
+
if (rangeMatch) {
|
|
395
|
+
const start = parseInt(rangeMatch[1]);
|
|
396
|
+
const end = parseInt(rangeMatch[2]);
|
|
397
|
+
for (let i = start; i <= end && i <= files.length; i++) {
|
|
398
|
+
if (i >= 1) selectedFiles.push(files[i - 1].path);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
// Single number
|
|
402
|
+
const num = parseInt(part);
|
|
403
|
+
if (num >= 1 && num <= files.length) {
|
|
404
|
+
selectedFiles.push(files[num - 1].path);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return [...new Set(selectedFiles)]; // Remove duplicates
|
|
410
|
+
}
|
|
411
|
+
|
|
319
412
|
// Format scan results for AI context
|
|
320
413
|
function formatScanResults(scanResult) {
|
|
321
414
|
let output = `\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n`;
|
|
@@ -498,9 +591,9 @@ async function runSapper() {
|
|
|
498
591
|
|
|
499
592
|
// Quick tips box
|
|
500
593
|
console.log(box(
|
|
501
|
-
`${chalk.yellow('š”')}
|
|
594
|
+
`${chalk.yellow('š”')} Use ${chalk.cyan('@file')} to attach files (e.g., "fix @app.js")\n` +
|
|
502
595
|
`${chalk.yellow('š”')} Type ${chalk.cyan('/scan')} to load entire codebase\n` +
|
|
503
|
-
`${chalk.yellow('š”')} Type ${chalk.cyan('
|
|
596
|
+
`${chalk.yellow('š”')} Type ${chalk.cyan('/help')} for all commands`,
|
|
504
597
|
'Quick Tips', 'gray'
|
|
505
598
|
));
|
|
506
599
|
console.log();
|
|
@@ -681,6 +774,8 @@ TOOL SYNTAX:
|
|
|
681
774
|
if (input.toLowerCase() === '/help') {
|
|
682
775
|
console.log();
|
|
683
776
|
const helpContent =
|
|
777
|
+
`${chalk.cyan('@')} or ${chalk.cyan('/attach')} ${chalk.gray('ā')} Pick files to attach (interactive)\n` +
|
|
778
|
+
`${chalk.cyan('@file')} ${chalk.gray('ā')} Attach file inline (e.g., @src/app.js)\n` +
|
|
684
779
|
`${chalk.cyan('/scan')} ${chalk.gray('ā')} Scan codebase into context\n` +
|
|
685
780
|
`${chalk.cyan('/recall')} ${chalk.gray('ā')} Search memory for relevant context\n` +
|
|
686
781
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('ā')} Clear all context\n` +
|
|
@@ -789,7 +884,101 @@ TOOL SYNTAX:
|
|
|
789
884
|
continue;
|
|
790
885
|
}
|
|
791
886
|
|
|
792
|
-
|
|
887
|
+
// Handle @ alone or /attach command - interactive file picker
|
|
888
|
+
if (input.trim() === '@' || input.toLowerCase() === '/attach') {
|
|
889
|
+
const selectedFiles = await pickFiles();
|
|
890
|
+
|
|
891
|
+
if (selectedFiles.length === 0) continue;
|
|
892
|
+
|
|
893
|
+
// Read and attach selected files
|
|
894
|
+
const fileAttachments = [];
|
|
895
|
+
for (const filePath of selectedFiles) {
|
|
896
|
+
try {
|
|
897
|
+
const stats = fs.statSync(filePath);
|
|
898
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
899
|
+
console.log(chalk.yellow(`ā ļø ${filePath} is too large, skipping`));
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
903
|
+
fileAttachments.push({ path: filePath, content, size: stats.size });
|
|
904
|
+
console.log(chalk.green(`š Attached: ${filePath}`));
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.log(chalk.yellow(`ā ļø Could not read ${filePath}`));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (fileAttachments.length === 0) continue;
|
|
911
|
+
|
|
912
|
+
// Ask for the prompt to go with these files
|
|
913
|
+
console.log();
|
|
914
|
+
const prompt = await safeQuestion(chalk.cyan('Your prompt for these files: '));
|
|
915
|
+
|
|
916
|
+
if (!prompt.trim()) {
|
|
917
|
+
console.log(chalk.gray('Cancelled.'));
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Build message with attachments
|
|
922
|
+
let attachedContent = '\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n';
|
|
923
|
+
attachedContent += `š ATTACHED FILES (${fileAttachments.length})\n`;
|
|
924
|
+
attachedContent += 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n\n';
|
|
925
|
+
|
|
926
|
+
for (const file of fileAttachments) {
|
|
927
|
+
attachedContent += `āāāā ${file.path} āāā\n`;
|
|
928
|
+
attachedContent += file.content;
|
|
929
|
+
if (!file.content.endsWith('\n')) attachedContent += '\n';
|
|
930
|
+
attachedContent += `āāāā END ${file.path} āāā\n\n`;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
messages.push({ role: 'user', content: prompt + attachedContent });
|
|
934
|
+
// Continue to AI response (don't use 'continue' here)
|
|
935
|
+
} else {
|
|
936
|
+
// Process @file attachments in prompt (e.g., "analyze @package.json" or "fix @src/index.js")
|
|
937
|
+
let processedInput = input;
|
|
938
|
+
const fileAttachments = [];
|
|
939
|
+
const attachRegex = /@([\w.\/\-_]+)/g;
|
|
940
|
+
let attachMatch;
|
|
941
|
+
|
|
942
|
+
while ((attachMatch = attachRegex.exec(input)) !== null) {
|
|
943
|
+
const filePath = attachMatch[1];
|
|
944
|
+
try {
|
|
945
|
+
if (fs.existsSync(filePath)) {
|
|
946
|
+
const stats = fs.statSync(filePath);
|
|
947
|
+
if (stats.isFile()) {
|
|
948
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
949
|
+
console.log(chalk.yellow(`ā ļø @${filePath} is too large (${Math.round(stats.size/1024)}KB), skipping`));
|
|
950
|
+
} else {
|
|
951
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
952
|
+
fileAttachments.push({ path: filePath, content, size: stats.size });
|
|
953
|
+
console.log(chalk.green(`š Attached: ${filePath} (${Math.round(stats.size/1024)}KB)`));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
// Not a file - might be an @mention for something else, ignore
|
|
958
|
+
}
|
|
959
|
+
} catch (e) {
|
|
960
|
+
console.log(chalk.yellow(`ā ļø Could not read @${filePath}: ${e.message}`));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Build the final message with attachments
|
|
965
|
+
if (fileAttachments.length > 0) {
|
|
966
|
+
let attachedContent = '\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n';
|
|
967
|
+
attachedContent += `š ATTACHED FILES (${fileAttachments.length})\n`;
|
|
968
|
+
attachedContent += 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n\n';
|
|
969
|
+
|
|
970
|
+
for (const file of fileAttachments) {
|
|
971
|
+
attachedContent += `āāāā ${file.path} āāā\n`;
|
|
972
|
+
attachedContent += file.content;
|
|
973
|
+
if (!file.content.endsWith('\n')) attachedContent += '\n';
|
|
974
|
+
attachedContent += `āāāā END ${file.path} āāā\n\n`;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
processedInput = input + attachedContent;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
messages.push({ role: 'user', content: processedInput });
|
|
981
|
+
} // End of else block for non-@ input
|
|
793
982
|
|
|
794
983
|
let toolRounds = 0; // Prevent infinite loops
|
|
795
984
|
const MAX_TOOL_ROUNDS = 20;
|