sapper-iq 1.1.28 ā 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 +146 -2
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`;
|
|
@@ -681,7 +774,8 @@ TOOL SYNTAX:
|
|
|
681
774
|
if (input.toLowerCase() === '/help') {
|
|
682
775
|
console.log();
|
|
683
776
|
const helpContent =
|
|
684
|
-
`${chalk.cyan('@
|
|
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` +
|
|
685
779
|
`${chalk.cyan('/scan')} ${chalk.gray('ā')} Scan codebase into context\n` +
|
|
686
780
|
`${chalk.cyan('/recall')} ${chalk.gray('ā')} Search memory for relevant context\n` +
|
|
687
781
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('ā')} Clear all context\n` +
|
|
@@ -790,7 +884,56 @@ TOOL SYNTAX:
|
|
|
790
884
|
continue;
|
|
791
885
|
}
|
|
792
886
|
|
|
793
|
-
//
|
|
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")
|
|
794
937
|
let processedInput = input;
|
|
795
938
|
const fileAttachments = [];
|
|
796
939
|
const attachRegex = /@([\w.\/\-_]+)/g;
|
|
@@ -835,6 +978,7 @@ TOOL SYNTAX:
|
|
|
835
978
|
}
|
|
836
979
|
|
|
837
980
|
messages.push({ role: 'user', content: processedInput });
|
|
981
|
+
} // End of else block for non-@ input
|
|
838
982
|
|
|
839
983
|
let toolRounds = 0; // Prevent infinite loops
|
|
840
984
|
const MAX_TOOL_ROUNDS = 20;
|