sapper-iq 1.0.25 → 1.1.0

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 +110 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.0.25",
3
+ "version": "1.1.0",
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
@@ -64,16 +64,52 @@ function recreateReadline() {
64
64
  });
65
65
  }
66
66
 
67
+ // Directories to ignore when listing files
68
+ const IGNORE_DIRS = new Set([
69
+ 'node_modules', '.git', '.svn', '.hg', 'dist', 'build',
70
+ '.next', '.nuxt', '__pycache__', '.cache', 'coverage',
71
+ '.idea', '.vscode', 'vendor', 'target', '.gradle'
72
+ ]);
73
+
67
74
  const tools = {
68
75
  read: (path) => {
69
76
  try { return fs.readFileSync(path.trim(), 'utf8'); }
70
77
  catch (error) { return `Error reading file: ${error.message}`; }
71
78
  },
72
- write: (path, content) => {
79
+ patch: async (path, oldText, newText) => {
80
+ const trimmedPath = path.trim();
73
81
  try {
74
- fs.writeFileSync(path.trim(), content);
75
- return `Successfully saved changes to ${path}`;
76
- } catch (error) { return `Error writing file: ${error.message}`; }
82
+ const content = fs.readFileSync(trimmedPath, 'utf8');
83
+ if (!content.includes(oldText)) {
84
+ return `Error: Could not find the text to replace in ${trimmedPath}. Make sure oldText matches exactly (including whitespace).`;
85
+ }
86
+ const newContent = content.replace(oldText, newText);
87
+
88
+ // Show diff preview
89
+ console.log(chalk.yellow.bold(`\n[PATCH] ${trimmedPath}`));
90
+ console.log(chalk.red('- ' + oldText.split('\n').join('\n- ')));
91
+ console.log(chalk.green('+ ' + newText.split('\n').join('\n+ ')));
92
+
93
+ const confirm = await safeQuestion(chalk.yellow('Apply this patch? (y/n): '));
94
+ if (confirm.toLowerCase() === 'y') {
95
+ fs.writeFileSync(trimmedPath, newContent);
96
+ return `Successfully patched ${trimmedPath}`;
97
+ }
98
+ return 'Patch rejected by user.';
99
+ } catch (error) { return `Error patching file: ${error.message}`; }
100
+ },
101
+ write: async (path, content) => {
102
+ const trimmedPath = path.trim();
103
+ console.log(chalk.yellow.bold(`\n[WRITE] Sapper wants to write to: `) + chalk.white(trimmedPath));
104
+ console.log(chalk.gray(`Content preview (first 200 chars):\n${content?.substring(0, 200)}${content?.length > 200 ? '...' : ''}`));
105
+ const confirm = await safeQuestion(chalk.yellow('Allow write? (y/n): '));
106
+ if (confirm.toLowerCase() === 'y') {
107
+ try {
108
+ fs.writeFileSync(trimmedPath, content);
109
+ return `Successfully saved changes to ${trimmedPath}`;
110
+ } catch (error) { return `Error writing file: ${error.message}`; }
111
+ }
112
+ return "Write blocked by user.";
77
113
  },
78
114
  mkdir: (path) => {
79
115
  try {
@@ -100,8 +136,18 @@ const tools = {
100
136
  return "Command blocked by user.";
101
137
  },
102
138
  list: (path) => {
103
- try { return fs.readdirSync(path.trim() || '.').join('\n'); }
104
- catch (e) { return `Error: ${e.message}`; }
139
+ try {
140
+ const dir = path.trim() || '.';
141
+ const entries = fs.readdirSync(dir);
142
+ // Filter out ignored directories
143
+ const filtered = entries.filter(entry => {
144
+ if (IGNORE_DIRS.has(entry)) return false;
145
+ // Also skip hidden files/folders (starting with .) except current dir
146
+ if (entry.startsWith('.') && entry !== '.') return false;
147
+ return true;
148
+ });
149
+ return filtered.length > 0 ? filtered.join('\n') : '(empty or all files filtered)';
150
+ } catch (e) { return `Error: ${e.message}`; }
105
151
  }
106
152
  };
107
153
 
@@ -175,9 +221,13 @@ READING GUIDELINES:
175
221
  TOOL FORMAT (CRITICAL - FOLLOW EXACTLY):
176
222
  ✅ CORRECT: [TOOL:LIST].[/TOOL]
177
223
  ✅ CORRECT: [TOOL:READ]./file.js[/TOOL]
178
- ✅ CORRECT: [TOOL:LIST]./src[/TOOL] then read all files found
224
+ ✅ CORRECT: [TOOL:WRITE]./file.js]full content here[/TOOL]
225
+ ✅ CORRECT: [TOOL:PATCH]./file.js]old code|||new code[/TOOL]
179
226
  ❌ WRONG: [TOOL:LIST].[/] - missing TOOL at end!
180
- ❌ WRONG: [TOOL:LIST]/[/TOOL] - wrong directory!
227
+
228
+ PATCH vs WRITE:
229
+ - Use PATCH for small changes (1-10 lines): [TOOL:PATCH]path]old|||new[/TOOL]
230
+ - Use WRITE only for new files or complete rewrites
181
231
 
182
232
  WORKFLOW:
183
233
  1. LIST directory → 2. READ files (as many as needed) → 3. ANALYZE and RESPOND`
@@ -204,6 +254,48 @@ WORKFLOW:
204
254
  continue;
205
255
  }
206
256
 
257
+ // Handle prune command - summarize and clear old context
258
+ if (input.toLowerCase() === '/prune') {
259
+ if (messages.length <= 5) {
260
+ console.log(chalk.yellow('Context is already small, nothing to prune.'));
261
+ continue;
262
+ }
263
+
264
+ // Keep system prompt + last 4 messages
265
+ const systemPrompt = messages[0];
266
+ const recentMessages = messages.slice(-4);
267
+
268
+ // Count what we're removing
269
+ const removedCount = messages.length - 5;
270
+
271
+ messages = [systemPrompt, ...recentMessages];
272
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages));
273
+ console.log(chalk.green(`✅ Pruned ${removedCount} old messages. Kept system prompt + last 4 messages.`));
274
+ console.log(chalk.gray(`Context size: ${messages.length} messages\n`));
275
+ continue;
276
+ }
277
+
278
+ // Handle help command
279
+ if (input.toLowerCase() === '/help') {
280
+ console.log(chalk.cyan('\n📚 SAPPER COMMANDS:'));
281
+ console.log(chalk.white(' /reset, /clear') + chalk.gray(' - Clear all context and start fresh'));
282
+ console.log(chalk.white(' /prune') + chalk.gray(' - Remove old messages, keep last 4'));
283
+ console.log(chalk.white(' /context') + chalk.gray(' - Show current context size'));
284
+ console.log(chalk.white(' /help') + chalk.gray(' - Show this help message'));
285
+ console.log(chalk.white(' exit') + chalk.gray(' - Exit Sapper\n'));
286
+ continue;
287
+ }
288
+
289
+ // Handle context size command
290
+ if (input.toLowerCase() === '/context') {
291
+ const contextSize = JSON.stringify(messages).length;
292
+ console.log(chalk.cyan(`\n📊 Context: ${messages.length} messages, ~${Math.round(contextSize/1024)}KB`));
293
+ if (contextSize > 50000) {
294
+ console.log(chalk.yellow('⚠️ Context is large! Consider using /prune'));
295
+ }
296
+ continue;
297
+ }
298
+
207
299
  messages.push({ role: 'user', content: input });
208
300
 
209
301
  let toolRounds = 0; // Prevent infinite loops
@@ -258,7 +350,16 @@ WORKFLOW:
258
350
  if (type.toLowerCase() === 'list') result = tools.list(path);
259
351
  else if (type.toLowerCase() === 'read') result = tools.read(path);
260
352
  else if (type.toLowerCase() === 'mkdir') result = tools.mkdir(path);
261
- else if (type.toLowerCase() === 'write') result = tools.write(path, content);
353
+ else if (type.toLowerCase() === 'write') result = await tools.write(path, content);
354
+ else if (type.toLowerCase() === 'patch') {
355
+ // PATCH format: [TOOL:PATCH]path]OLD_TEXT|||NEW_TEXT[/TOOL]
356
+ const parts = content?.split('|||');
357
+ if (parts && parts.length === 2) {
358
+ result = await tools.patch(path, parts[0], parts[1]);
359
+ } else {
360
+ result = 'Error: PATCH requires format [TOOL:PATCH]path]OLD_TEXT|||NEW_TEXT[/TOOL]';
361
+ }
362
+ }
262
363
  else if (type.toLowerCase() === 'shell') result = await tools.shell(path);
263
364
 
264
365
  messages.push({ role: 'user', content: `RESULT (${path}): ${result}` });