sapper-iq 1.0.1 → 1.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env node
2
+ import ollama from 'ollama';
3
+ import fs from 'fs';
4
+ import { spawn } from 'child_process';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import readline from 'readline';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const packageJson = JSON.parse(fs.readFileSync(join(__dirname, 'package.json'), 'utf8'));
14
+ const CURRENT_VERSION = packageJson.version;
15
+
16
+ const spinner = ora();
17
+ const CONTEXT_FILE = '.sapper_context.json';
18
+
19
+ let stepMode = false;
20
+ let rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ terminal: true,
24
+ historySize: 100
25
+ });
26
+
27
+ // Helper function to safely prompt for input
28
+ async function safeQuestion(query) {
29
+ return new Promise((resolve) => {
30
+ process.stdout.write(query);
31
+ rl.once('line', (answer) => {
32
+ resolve(answer.trim());
33
+ });
34
+ });
35
+ }
36
+
37
+ // Helper function to check for updates
38
+ async function checkForUpdates() {
39
+ try {
40
+ const response = await fetch('https://registry.npmjs.org/sapper-iq/latest');
41
+ const data = await response.json();
42
+ const latestVersion = data.version;
43
+
44
+ if (latestVersion && latestVersion !== CURRENT_VERSION) {
45
+ console.log(chalk.yellow('šŸ”„ UPDATE AVAILABLE!'));
46
+ console.log(chalk.gray(` Current: v${CURRENT_VERSION}`));
47
+ console.log(chalk.green(` Latest: v${latestVersion}`));
48
+ console.log(chalk.cyan(' Run: npm update -g sapper-iq\n'));
49
+ }
50
+ } catch (error) {
51
+ // Silently fail if update check fails
52
+ }
53
+ }
54
+
55
+ // Helper function to update sapper
56
+ async function updateSapper() {
57
+ console.log(chalk.cyan('šŸ”„ Updating Sapper...'));
58
+ const confirm = await safeQuestion(chalk.yellow('Continue with update? (y/n): '));
59
+ if (confirm.toLowerCase() === 'y') {
60
+ return new Promise((resolve) => {
61
+ const proc = spawn('npm', ['update', '-g', 'sapper-iq'], {
62
+ stdio: 'inherit'
63
+ });
64
+
65
+ proc.on('close', (code) => {
66
+ recreateReadline();
67
+ if (code === 0) {
68
+ console.log(chalk.green('\nāœ… Sapper updated successfully!'));
69
+ console.log(chalk.gray('Please restart Sapper to use the new version.\n'));
70
+ } else {
71
+ console.log(chalk.red('\nāŒ Update failed. Try manually: npm update -g sapper-iq\n'));
72
+ }
73
+ resolve();
74
+ });
75
+
76
+ proc.on('error', (err) => {
77
+ recreateReadline();
78
+ console.log(chalk.red(`\nāŒ Update error: ${err.message}\n`));
79
+ resolve();
80
+ });
81
+ });
82
+ }
83
+ }
84
+
85
+ // Helper function to recreate readline after shell commands
86
+ function recreateReadline() {
87
+ rl.close();
88
+ rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ terminal: true,
92
+ historySize: 100
93
+ });
94
+ }
95
+
96
+ // --- Tool Logic ---
97
+ const tools = {
98
+ read: (path) => {
99
+ try {
100
+ return fs.readFileSync(path.trim(), 'utf8');
101
+ } catch (error) {
102
+ return `Error reading file: ${error.message}`;
103
+ }
104
+ },
105
+ write: (path, content) => {
106
+ try {
107
+ fs.writeFileSync(path.trim(), content);
108
+ return `Successfully saved changes to ${path}`;
109
+ } catch (error) {
110
+ return `Error writing file: ${error.message}`;
111
+ }
112
+ },
113
+ mkdir: (path) => {
114
+ try {
115
+ fs.mkdirSync(path.trim(), { recursive: true });
116
+ return `Directory created: ${path}`;
117
+ } catch (error) {
118
+ return `Error creating directory: ${error.message}`;
119
+ }
120
+ },
121
+ shell: async (cmd) => {
122
+ console.log(chalk.red.bold(`\n[SECURITY] Sapper wants to execute: `) + chalk.white(cmd));
123
+ const confirm = await safeQuestion(chalk.yellow('Allow? (y/n): '));
124
+ if (confirm.toLowerCase() === 'y') {
125
+ return new Promise((resolve) => {
126
+ // Use shell for complex commands with pipes, redirects, cd, &&, ||, etc
127
+ const useShell = cmd.includes('&&') || cmd.includes('||') || cmd.includes('|') || cmd.includes('cd ') || cmd.includes('>');
128
+
129
+ console.log(chalk.cyan(`\n[RUNNING] ${cmd}\n`));
130
+
131
+ let proc;
132
+ if (useShell) {
133
+ // For complex commands, use shell
134
+ proc = spawn('sh', ['-c', cmd], {
135
+ stdio: 'inherit',
136
+ shell: true
137
+ });
138
+ } else {
139
+ // For simple commands, parse and use direct execution
140
+ const parts = cmd.trim().split(/\s+/);
141
+ const executable = parts[0];
142
+ const args = parts.slice(1);
143
+ proc = spawn(executable, args, {
144
+ stdio: 'inherit',
145
+ shell: false
146
+ });
147
+ }
148
+
149
+ proc.on('close', (code) => {
150
+ // Recreate readline after shell command completes
151
+ recreateReadline();
152
+ console.log(chalk.green(`\n[āœ“] Command completed with exit code ${code}\n`));
153
+ resolve(`Command completed with exit code ${code}.`);
154
+ });
155
+
156
+ proc.on('error', (err) => {
157
+ recreateReadline();
158
+ console.log(chalk.red(`\n[āœ—] Command error: ${err.message}\n`));
159
+ resolve(`Execution Error: ${err.message}`);
160
+ });
161
+ });
162
+ }
163
+ return "Command blocked by user.";
164
+ },
165
+ list: (path) => {
166
+ try {
167
+ return fs.readdirSync(path || '.').join('\n');
168
+ } catch (error) {
169
+ return `Error listing directory: ${error.message}`;
170
+ }
171
+ },
172
+ search: (pattern) => {
173
+ try {
174
+ const { execSync } = require('child_process');
175
+ const cmd = `grep -rnEi "${pattern.trim()}" . --exclude-dir=node_modules --exclude-dir=.git`;
176
+ return execSync(cmd, { encoding: 'utf8' }) || "No matches found.";
177
+ } catch (e) {
178
+ return "No matches found.";
179
+ }
180
+ }
181
+ };
182
+
183
+ async function selectModel() {
184
+ try {
185
+ const localModels = await ollama.list();
186
+ if (localModels.models.length === 0) {
187
+ console.log(chalk.red('āŒ No Ollama models found!'));
188
+ console.log(chalk.yellow('Please install at least one model:'));
189
+ console.log(chalk.gray(' ollama pull llama2'));
190
+ console.log(chalk.gray(' ollama pull codellama'));
191
+ process.exit(1);
192
+ }
193
+ console.log(chalk.magenta.bold("\nAvailable Models:"));
194
+ localModels.models.forEach((m, i) => console.log(`${i + 1}. ${chalk.white(m.name)}`));
195
+ const choice = await safeQuestion(chalk.yellow('\nChoose model: '));
196
+ const index = parseInt(choice) - 1;
197
+ return localModels.models[index]?.name || localModels.models[0].name;
198
+ } catch (error) {
199
+ console.log(chalk.red('āŒ Failed to connect to Ollama!'));
200
+ console.log(chalk.yellow('Please make sure Ollama is running:'));
201
+ console.log(chalk.gray(' 1. Install Ollama: https://ollama.ai'));
202
+ console.log(chalk.gray(' 2. Start Ollama: ollama serve'));
203
+ console.log(chalk.gray(' 3. Install a model: ollama pull llama2'));
204
+ console.log(chalk.red(`\nError details: ${error.message}`));
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ async function runSapper() {
210
+ console.clear();
211
+ console.log(chalk.cyan.bold(` SAPPER v${CURRENT_VERSION} | Multi-Tool Execution Mode`));
212
+ console.log(chalk.gray("Commands: /reset, /session-info, /step, /version, /update, /help, exit\n"));
213
+
214
+ // Check for updates on startup
215
+ await checkForUpdates();
216
+
217
+ // Early Ollama connectivity check
218
+ console.log(chalk.gray('šŸ” Checking Ollama connection...'));
219
+
220
+ let messages = [];
221
+ if (fs.existsSync(CONTEXT_FILE)) {
222
+ const resume = await safeQuestion(chalk.green('Resume previous session? (y/n): '));
223
+ if (resume.toLowerCase() === 'y') {
224
+ messages = JSON.parse(fs.readFileSync(CONTEXT_FILE, 'utf8'));
225
+ }
226
+ }
227
+
228
+ const selectedModel = await selectModel();
229
+
230
+ if (messages.length === 0) {
231
+ messages = [{
232
+ role: 'system',
233
+ content: `You are Sapper, a senior software engineer AI assistant.
234
+
235
+ **CRITICAL - Tool Format Rules:**
236
+ - NEVER use JSON format
237
+ - ONLY use this EXACT format for tools: [TOOL:TYPE:path]content[/TOOL]
238
+ - For single-line content: [TOOL:TYPE:path:content] (legacy format still supported)
239
+ - Types: SHELL, READ, WRITE, MKDIR, LIST, SEARCH
240
+
241
+ **Examples:**
242
+ [TOOL:SHELL:npm install][/TOOL]
243
+ [TOOL:READ:./package.json][/TOOL]
244
+ [TOOL:WRITE:./app.js]console.log('hello')[/TOOL]
245
+ [TOOL:MKDIR:./src/components][/TOOL]
246
+ [TOOL:LIST:./src][/TOOL]
247
+ [TOOL:SEARCH:function myFunction][/TOOL]
248
+
249
+ **For multi-line content (like markdown files):**
250
+ [TOOL:WRITE:./file.md]
251
+ Multi-line
252
+ content here
253
+ with - [ ] checkboxes
254
+ [/TOOL]
255
+
256
+ **Shell Command Rules:**
257
+ - For operations in a specific directory, chain with cd: cd /path/to/project && npm install
258
+ - Use && to chain commands that depend on each other
259
+ - Use | for pipes and > for redirects
260
+ - Use relative paths after cd into a directory
261
+ - Chain multiple commands: cd /path && npm install && npm run dev
262
+ - User will specify which directory to work in - always use that path
263
+
264
+ **Critical for npm/npx commands:**
265
+ - ALWAYS use non-interactive flags (--typescript, --tailwind, --eslint, --no-git, etc)
266
+ - Create projects with non-interactive flags
267
+ - Install dependencies with: cd /path && npm install
268
+ - Run apps with: cd /path && npm run dev
269
+
270
+ **Workflow:**
271
+ 1. For complex tasks, start with [PLAN:step1,step2,step3]
272
+ 2. Execute tools immediately using the exact format above
273
+ 3. You can provide MULTIPLE tools in one message
274
+ 4. Always end with [SUMMARY:description of what was completed]
275
+
276
+ **Important:**
277
+ - No JSON responses
278
+ - No markdown code blocks for tools
279
+ - Only the exact bracket format: [TOOL:TYPE:path:content]
280
+ - User will see live command output in terminal
281
+ - Execute all tools needed to complete the task
282
+ - Work flexibly with ANY directory the user specifies
283
+ - Always chain cd with your command when working in a specific directory`
284
+ }];
285
+ }
286
+
287
+ // Display working directory awareness
288
+ console.log(chalk.yellow(`Working Directory: ${process.cwd()}\n`));
289
+
290
+ const ask = () => {
291
+ safeQuestion(chalk.blue.bold('\nIbrahim āž” ')).then(async (input) => {
292
+ if (input.toLowerCase() === 'exit') process.exit();
293
+ if (input.toLowerCase() === '/reset' || input.toLowerCase() === '/clear-session') {
294
+ if (fs.existsSync(CONTEXT_FILE)) {
295
+ const fileSize = fs.statSync(CONTEXT_FILE).size;
296
+ console.log(chalk.yellow(`\nšŸ—‘ļø Clearing session (${(fileSize / 1024).toFixed(2)}KB)...`));
297
+ fs.unlinkSync(CONTEXT_FILE);
298
+ console.log(chalk.green('āœ… Session cleared! Starting fresh...\n'));
299
+ } else {
300
+ console.log(chalk.yellow('\nā„¹ļø No session to clear.\n'));
301
+ }
302
+ return runSapper();
303
+ }
304
+ if (input.toLowerCase() === '/session-info') {
305
+ if (fs.existsSync(CONTEXT_FILE)) {
306
+ const data = JSON.parse(fs.readFileSync(CONTEXT_FILE, 'utf8'));
307
+ const fileSize = fs.statSync(CONTEXT_FILE).size;
308
+ console.log(chalk.cyan(`\nšŸ“Š Session Info:`));
309
+ console.log(chalk.gray(` Messages: ${data.length}`));
310
+ console.log(chalk.gray(` File Size: ${(fileSize / 1024).toFixed(2)}KB`));
311
+ console.log(chalk.gray(` Last Message: ${data[data.length - 1]?.role || 'N/A'}`));
312
+ } else {
313
+ console.log(chalk.yellow('\nā„¹ļø No active session.\n'));
314
+ }
315
+ return ask();
316
+ }
317
+ if (input.toLowerCase() === '/version') {
318
+ console.log(chalk.cyan(`\nšŸ“¦ Sapper Version: v${CURRENT_VERSION}`));
319
+ console.log(chalk.gray(` Node.js: ${process.version}`));
320
+ console.log(chalk.gray(` Platform: ${process.platform}\n`));
321
+ // Check for updates
322
+ await checkForUpdates();
323
+ return ask();
324
+ }
325
+ if (input.toLowerCase() === '/update') {
326
+ await updateSapper();
327
+ return ask();
328
+ }
329
+ if (input.toLowerCase() === '/step') {
330
+ stepMode = !stepMode;
331
+ console.log(chalk.yellow(`Step Mode is ${stepMode ? 'ON' : 'OFF'}`));
332
+ return ask();
333
+ }
334
+ if (input.toLowerCase() === '/help') {
335
+ console.log(chalk.cyan(`\nšŸ“š Sapper Commands:`));
336
+ console.log(chalk.gray(` /reset or /clear-session - Start a new session`));
337
+ console.log(chalk.gray(` /session-info - Show current session details`));
338
+ console.log(chalk.gray(` /version - Show version and check for updates`));
339
+ console.log(chalk.gray(` /update - Update Sapper to latest version`));
340
+ console.log(chalk.gray(` /step - Toggle step-by-step mode`));
341
+ console.log(chalk.gray(` /help - Show this help menu`));
342
+ console.log(chalk.gray(` exit - Exit Sapper\n`));
343
+ return ask();
344
+ }
345
+
346
+ // Check if user mentioned a directory and provide context
347
+ const dirMatch = input.match(/\/Users\/[^\s]+|\/[a-zA-Z0-9_\/-]+/g);
348
+ let contextMsg = input;
349
+
350
+ if (dirMatch && dirMatch[0]) {
351
+ const mentionedDir = dirMatch[0];
352
+ try {
353
+ if (fs.existsSync(mentionedDir) && fs.statSync(mentionedDir).isDirectory()) {
354
+ const files = fs.readdirSync(mentionedDir).slice(0, 10).join(', ');
355
+ contextMsg = `${input}\n\n[CONTEXT: Directory "${mentionedDir}" contains: ${files}${fs.readdirSync(mentionedDir).length > 10 ? '...' : ''}]`;
356
+ }
357
+ } catch (e) {
358
+ // Silently ignore if directory doesn't exist
359
+ }
360
+ }
361
+
362
+ messages.push({ role: 'user', content: contextMsg });
363
+
364
+ let active = true;
365
+ let iterations = 0;
366
+ while (active && iterations < 30) {
367
+ iterations++;
368
+
369
+ if (stepMode) {
370
+ const proceed = await safeQuestion(chalk.gray('\n[STEP-MODE] Press Enter to continue (or type "/stop"): '));
371
+ if (proceed.toLowerCase() === '/stop') break;
372
+ }
373
+
374
+ spinner.stop();
375
+ console.log(chalk.blue(`\n${selectedModel} is thinking...`));
376
+
377
+ let response;
378
+ try {
379
+ response = await ollama.chat({
380
+ model: selectedModel,
381
+ messages,
382
+ stream: true,
383
+ options: { num_ctx: 16384 }
384
+ });
385
+ } catch (error) {
386
+ console.log(chalk.red('\nāŒ Failed to communicate with Ollama!'));
387
+ console.log(chalk.yellow('Possible issues:'));
388
+ console.log(chalk.gray(' - Ollama service stopped'));
389
+ console.log(chalk.gray(' - Model was removed'));
390
+ console.log(chalk.gray(' - Network connection issue'));
391
+ console.log(chalk.red(`Error: ${error.message}`));
392
+ console.log(chalk.cyan('\nšŸ’” Try restarting Sapper or check Ollama status'));
393
+ active = false;
394
+ ask();
395
+ return;
396
+ }
397
+
398
+ let msg = '';
399
+ process.stdout.write(chalk.white('Sapper: '));
400
+
401
+ try {
402
+ for await (const chunk of response) {
403
+ if (chunk.message && chunk.message.content) {
404
+ process.stdout.write(chunk.message.content);
405
+ msg += chunk.message.content;
406
+ }
407
+ }
408
+ } catch (error) {
409
+ console.log(chalk.red('\n\nāŒ Connection interrupted while streaming response!'));
410
+ console.log(chalk.yellow(`Error: ${error.message}`));
411
+ console.log(chalk.cyan('šŸ’” The conversation will continue, but you may want to restart Sapper'));
412
+ msg += `\n[ERROR: Response interrupted - ${error.message}]`;
413
+ }
414
+ console.log();
415
+
416
+ messages.push({ role: 'assistant', content: msg });
417
+
418
+ const summaryMatch = msg.match(/\[SUMMARY:(.*?)\]/s);
419
+
420
+ // Support both formats:
421
+ // New: [TOOL:TYPE:path]content[/TOOL] (handles multi-line content with brackets)
422
+ // Old: [TOOL:TYPE:path:content] (for backward compatibility)
423
+ const newFormatMatches = [...msg.matchAll(/\[TOOL:(\w+):([^\]]+)\]([\s\S]*?)\[\/TOOL\]/g)];
424
+ const oldFormatMatches = [...msg.matchAll(/\[TOOL:(\w+):([^:\]]+):([^\]]+)\]/g)];
425
+
426
+ // Normalize to unified format: [fullMatch, type, path, content]
427
+ const toolMatches = [
428
+ ...newFormatMatches.map(m => [m[0], m[1], m[2], m[3]]),
429
+ ...oldFormatMatches.map(m => [m[0], m[1], m[2], m[3]])
430
+ ];
431
+
432
+ if (summaryMatch) {
433
+ console.log(chalk.green.bold("\nāœ… MISSION COMPLETE:"));
434
+ console.log(chalk.white(summaryMatch[1].trim()));
435
+ active = false;
436
+ continue;
437
+ }
438
+
439
+ if (toolMatches.length > 0) {
440
+ for (const match of toolMatches) {
441
+ const [_, name, path, content] = match;
442
+ const toolName = name.toLowerCase();
443
+ console.log(chalk.cyan(`\n[ACTION] Executing ${toolName} on: ${path}`));
444
+
445
+ let result;
446
+ try {
447
+ if (toolName === 'shell') result = await tools.shell(path);
448
+ else if (toolName === 'write') result = tools.write(path, content);
449
+ else if (toolName === 'mkdir') result = tools.mkdir(path);
450
+ else if (toolName === 'read') result = tools.read(path);
451
+ else if (toolName === 'list') result = tools.list(path);
452
+ else if (toolName === 'search') result = tools.search(path);
453
+ else result = `Unknown tool: ${name}`;
454
+ } catch (e) {
455
+ result = `Error: ${e.message}`;
456
+ }
457
+
458
+ console.log(chalk.gray(`> Result: ${result.substring(0, 60)}...`));
459
+ messages.push({ role: 'user', content: `TOOL_RESULT for ${path}: ${result}` });
460
+ }
461
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages));
462
+
463
+ // Add interrupt check after tool execution
464
+ console.log(chalk.gray('\n[Press Enter to continue or type "/stop" to halt execution]'));
465
+ const userChoice = await safeQuestion('');
466
+ if (userChoice.toLowerCase() === '/stop') {
467
+ console.log(chalk.yellow('\nā¹ļø Execution halted by user'));
468
+ active = false;
469
+ break;
470
+ }
471
+ } else {
472
+ const planMatch = msg.match(/\[PLAN:([\s\S]*?)\]/) || msg.match(/\[PLAN\]([\s\S]*?)\[\/PLAN\]/);
473
+ if (planMatch) {
474
+ const feedback = await safeQuestion(chalk.yellow('\nModify plan or type "go": '));
475
+ if (feedback.toLowerCase() === '/stop') { active = false; break; }
476
+ messages.push({ role: 'user', content: feedback.toLowerCase() === 'go' ? "Plan approved. Proceed with all steps." : feedback });
477
+ } else {
478
+ active = false;
479
+ }
480
+ }
481
+
482
+ // Safety check: if model is repeating itself, break the loop
483
+ if (iterations > 5) {
484
+ const recentMessages = messages.slice(-4);
485
+ const isRepeating = recentMessages.every(m =>
486
+ m.role === 'assistant' &&
487
+ recentMessages[0].content &&
488
+ m.content === recentMessages[0].content
489
+ );
490
+ if (isRepeating) {
491
+ console.log(chalk.yellow('\nāš ļø Detected repetitive behavior, stopping execution'));
492
+ active = false;
493
+ }
494
+ }
495
+ }
496
+ ask();
497
+ });
498
+ };
499
+ ask();
500
+ }
501
+
502
+ runSapper();
package/sapper.mjs CHANGED
@@ -95,14 +95,28 @@ function recreateReadline() {
95
95
 
96
96
  // --- Tool Logic ---
97
97
  const tools = {
98
- read: (path) => fs.readFileSync(path.trim(), 'utf8'),
98
+ read: (path) => {
99
+ try {
100
+ return fs.readFileSync(path.trim(), 'utf8');
101
+ } catch (error) {
102
+ return `Error reading file: ${error.message}`;
103
+ }
104
+ },
99
105
  write: (path, content) => {
100
- fs.writeFileSync(path.trim(), content);
101
- return `Successfully saved changes to ${path}`;
106
+ try {
107
+ fs.writeFileSync(path.trim(), content);
108
+ return `Successfully saved changes to ${path}`;
109
+ } catch (error) {
110
+ return `Error writing file: ${error.message}`;
111
+ }
102
112
  },
103
113
  mkdir: (path) => {
104
- fs.mkdirSync(path.trim(), { recursive: true });
105
- return `Directory created: ${path}`;
114
+ try {
115
+ fs.mkdirSync(path.trim(), { recursive: true });
116
+ return `Directory created: ${path}`;
117
+ } catch (error) {
118
+ return `Error creating directory: ${error.message}`;
119
+ }
106
120
  },
107
121
  shell: async (cmd) => {
108
122
  console.log(chalk.red.bold(`\n[SECURITY] Sapper wants to execute: `) + chalk.white(cmd));
@@ -148,24 +162,48 @@ const tools = {
148
162
  }
149
163
  return "Command blocked by user.";
150
164
  },
151
- list: (path) => fs.readdirSync(path || '.').join('\n'),
165
+ list: (path) => {
166
+ try {
167
+ return fs.readdirSync(path || '.').join('\n');
168
+ } catch (error) {
169
+ return `Error listing directory: ${error.message}`;
170
+ }
171
+ },
152
172
  search: (pattern) => {
153
173
  try {
154
174
  const { execSync } = require('child_process');
155
175
  const cmd = `grep -rnEi "${pattern.trim()}" . --exclude-dir=node_modules --exclude-dir=.git`;
156
176
  return execSync(cmd, { encoding: 'utf8' }) || "No matches found.";
157
- } catch (e) { return "No matches found."; }
177
+ } catch (e) {
178
+ return "No matches found.";
179
+ }
158
180
  }
159
181
  };
160
182
 
161
183
  async function selectModel() {
162
- const localModels = await ollama.list();
163
- if (localModels.models.length === 0) process.exit(1);
164
- console.log(chalk.magenta.bold("\nAvailable Models:"));
165
- localModels.models.forEach((m, i) => console.log(`${i + 1}. ${chalk.white(m.name)}`));
166
- const choice = await safeQuestion(chalk.yellow('\nChoose model: '));
167
- const index = parseInt(choice) - 1;
168
- return localModels.models[index]?.name || localModels.models[0].name;
184
+ try {
185
+ const localModels = await ollama.list();
186
+ if (localModels.models.length === 0) {
187
+ console.log(chalk.red('āŒ No Ollama models found!'));
188
+ console.log(chalk.yellow('Please install at least one model:'));
189
+ console.log(chalk.gray(' ollama pull llama2'));
190
+ console.log(chalk.gray(' ollama pull codellama'));
191
+ process.exit(1);
192
+ }
193
+ console.log(chalk.magenta.bold("\nAvailable Models:"));
194
+ localModels.models.forEach((m, i) => console.log(`${i + 1}. ${chalk.white(m.name)}`));
195
+ const choice = await safeQuestion(chalk.yellow('\nChoose model: '));
196
+ const index = parseInt(choice) - 1;
197
+ return localModels.models[index]?.name || localModels.models[0].name;
198
+ } catch (error) {
199
+ console.log(chalk.red('āŒ Failed to connect to Ollama!'));
200
+ console.log(chalk.yellow('Please make sure Ollama is running:'));
201
+ console.log(chalk.gray(' 1. Install Ollama: https://ollama.ai'));
202
+ console.log(chalk.gray(' 2. Start Ollama: ollama serve'));
203
+ console.log(chalk.gray(' 3. Install a model: ollama pull llama2'));
204
+ console.log(chalk.red(`\nError details: ${error.message}`));
205
+ process.exit(1);
206
+ }
169
207
  }
170
208
 
171
209
  async function runSapper() {
@@ -176,6 +214,9 @@ async function runSapper() {
176
214
  // Check for updates on startup
177
215
  await checkForUpdates();
178
216
 
217
+ // Early Ollama connectivity check
218
+ console.log(chalk.gray('šŸ” Checking Ollama connection...'));
219
+
179
220
  let messages = [];
180
221
  if (fs.existsSync(CONTEXT_FILE)) {
181
222
  const resume = await safeQuestion(chalk.green('Resume previous session? (y/n): '));
@@ -193,16 +234,24 @@ async function runSapper() {
193
234
 
194
235
  **CRITICAL - Tool Format Rules:**
195
236
  - NEVER use JSON format
196
- - ONLY use this EXACT format for tools: [TOOL:TYPE:path:content]
237
+ - ONLY use this EXACT format for tools: [TOOL:TYPE:path]content[/TOOL]
238
+ - For single-line content: [TOOL:TYPE:path:content] (legacy format still supported)
197
239
  - Types: SHELL, READ, WRITE, MKDIR, LIST, SEARCH
198
240
 
199
241
  **Examples:**
200
- [TOOL:SHELL:npm install]
201
- [TOOL:READ:./package.json]
202
- [TOOL:WRITE:./app.js:console.log('hello')]
203
- [TOOL:MKDIR:./src/components]
204
- [TOOL:LIST:./src]
205
- [TOOL:SEARCH:function myFunction]
242
+ [TOOL:SHELL:npm install][/TOOL]
243
+ [TOOL:READ:./package.json][/TOOL]
244
+ [TOOL:WRITE:./app.js]console.log('hello')[/TOOL]
245
+ [TOOL:MKDIR:./src/components][/TOOL]
246
+ [TOOL:LIST:./src][/TOOL]
247
+ [TOOL:SEARCH:function myFunction][/TOOL]
248
+
249
+ **For multi-line content (like markdown files):**
250
+ [TOOL:WRITE:./file.md]
251
+ Multi-line
252
+ content here
253
+ with - [ ] checkboxes
254
+ [/TOOL]
206
255
 
207
256
  **Shell Command Rules:**
208
257
  - For operations in a specific directory, chain with cd: cd /path/to/project && npm install
@@ -325,28 +374,60 @@ async function runSapper() {
325
374
  spinner.stop();
326
375
  console.log(chalk.blue(`\n${selectedModel} is thinking...`));
327
376
 
328
- const response = await ollama.chat({
329
- model: selectedModel,
330
- messages,
331
- stream: true,
332
- options: { num_ctx: 16384 }
333
- });
377
+ let response;
378
+ try {
379
+ response = await ollama.chat({
380
+ model: selectedModel,
381
+ messages,
382
+ stream: true,
383
+ options: { num_ctx: 16384 }
384
+ });
385
+ } catch (error) {
386
+ console.log(chalk.red('\nāŒ Failed to communicate with Ollama!'));
387
+ console.log(chalk.yellow('Possible issues:'));
388
+ console.log(chalk.gray(' - Ollama service stopped'));
389
+ console.log(chalk.gray(' - Model was removed'));
390
+ console.log(chalk.gray(' - Network connection issue'));
391
+ console.log(chalk.red(`Error: ${error.message}`));
392
+ console.log(chalk.cyan('\nšŸ’” Try restarting Sapper or check Ollama status'));
393
+ active = false;
394
+ ask();
395
+ return;
396
+ }
334
397
 
335
398
  let msg = '';
336
399
  process.stdout.write(chalk.white('Sapper: '));
337
400
 
338
- for await (const chunk of response) {
339
- if (chunk.message && chunk.message.content) {
340
- process.stdout.write(chunk.message.content);
341
- msg += chunk.message.content;
401
+ try {
402
+ for await (const chunk of response) {
403
+ if (chunk.message && chunk.message.content) {
404
+ process.stdout.write(chunk.message.content);
405
+ msg += chunk.message.content;
406
+ }
342
407
  }
408
+ } catch (error) {
409
+ console.log(chalk.red('\n\nāŒ Connection interrupted while streaming response!'));
410
+ console.log(chalk.yellow(`Error: ${error.message}`));
411
+ console.log(chalk.cyan('šŸ’” The conversation will continue, but you may want to restart Sapper'));
412
+ msg += `\n[ERROR: Response interrupted - ${error.message}]`;
343
413
  }
344
414
  console.log();
345
415
 
346
416
  messages.push({ role: 'assistant', content: msg });
347
417
 
348
418
  const summaryMatch = msg.match(/\[SUMMARY:(.*?)\]/s);
349
- const toolMatches = [...msg.matchAll(/\[TOOL:(\w+):([^:\]]+):?([\s\S]*?)\]/g)];
419
+
420
+ // Support both formats:
421
+ // New: [TOOL:TYPE:path]content[/TOOL] (handles multi-line content with brackets)
422
+ // Old: [TOOL:TYPE:path:content] (for backward compatibility)
423
+ const newFormatMatches = [...msg.matchAll(/\[TOOL:(\w+):([^\]]+)\]([\s\S]*?)\[\/TOOL\]/g)];
424
+ const oldFormatMatches = [...msg.matchAll(/\[TOOL:(\w+):([^:\]]+):([^\]]+)\]/g)];
425
+
426
+ // Normalize to unified format: [fullMatch, type, path, content]
427
+ const toolMatches = [
428
+ ...newFormatMatches.map(m => [m[0], m[1], m[2], m[3]]),
429
+ ...oldFormatMatches.map(m => [m[0], m[1], m[2], m[3]])
430
+ ];
350
431
 
351
432
  if (summaryMatch) {
352
433
  console.log(chalk.green.bold("\nāœ… MISSION COMPLETE:"));
@@ -388,7 +469,7 @@ async function runSapper() {
388
469
  break;
389
470
  }
390
471
  } else {
391
- const planMatch = msg.match(/\[PLAN:(.*?)\]/);
472
+ const planMatch = msg.match(/\[PLAN:([\s\S]*?)\]/) || msg.match(/\[PLAN\]([\s\S]*?)\[\/PLAN\]/);
392
473
  if (planMatch) {
393
474
  const feedback = await safeQuestion(chalk.yellow('\nModify plan or type "go": '));
394
475
  if (feedback.toLowerCase() === '/stop') { active = false; break; }