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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper.mjs +146 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.1.28",
3
+ "version": "1.1.29",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
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('@file')} ${chalk.gray('│')} Attach file to prompt (e.g., @src/app.js)\n` +
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
- // Process @file attachments in prompt (e.g., "analyze @package.json" or "fix @src/index.js")
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;