sapper-iq 1.0.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.
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [16.x, 18.x, 20.x]
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Use Node.js ${{ matrix.node-version }}
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+ cache: 'npm'
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Run tests
30
+ run: npm test --if-present
31
+
32
+ - name: Check if sapper runs
33
+ run: |
34
+ chmod +x sapper.mjs
35
+ timeout 5s node sapper.mjs --help || true
@@ -0,0 +1,46 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ push:
7
+ tags:
8
+ - 'v*'
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '18'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Run tests (if any)
27
+ run: npm test --if-present
28
+
29
+ - name: Publish to NPM
30
+ run: npm publish
31
+ env:
32
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33
+
34
+ - name: Create GitHub Release
35
+ if: startsWith(github.ref, 'refs/tags/v')
36
+ uses: actions/create-release@v1
37
+ env:
38
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
+ with:
40
+ tag_name: ${{ github.ref }}
41
+ release_name: Release ${{ github.ref }}
42
+ body: |
43
+ ## Changes in this release
44
+ - Auto-generated release from tag ${{ github.ref }}
45
+ draft: false
46
+ prerelease: false
package/PUBLISHING.md ADDED
@@ -0,0 +1,143 @@
1
+ # Publishing Sapper to NPM
2
+
3
+ This guide explains how to publish Sapper to npm registry manually.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **npm account** - Create at [npmjs.com](https://npmjs.com)
8
+ 2. **npm login** - Run `npm login` in terminal
9
+ 3. **Package name available** - Check if "sapper" is available on npm
10
+
11
+ ## Step-by-Step Publishing Process
12
+
13
+ ### 1. Verify Package Configuration
14
+
15
+ Check `package.json` has correct information:
16
+ ```json
17
+ {
18
+ "name": "sapper",
19
+ "version": "1.0.0",
20
+ "main": "sapper.mjs",
21
+ "bin": {
22
+ "sapper": "./sapper.mjs"
23
+ },
24
+ "type": "module"
25
+ }
26
+ ```
27
+
28
+ ### 2. Make Executable
29
+
30
+ Ensure the main file is executable:
31
+ ```bash
32
+ chmod +x sapper.mjs
33
+ ```
34
+
35
+ ### 3. Test Locally
36
+
37
+ Test the package locally before publishing:
38
+ ```bash
39
+ npm link
40
+ sapper --version # Test if it works
41
+ npm unlink -g sapper # Clean up
42
+ ```
43
+
44
+ ### 4. Check Package Contents
45
+
46
+ See what files will be published:
47
+ ```bash
48
+ npm pack --dry-run
49
+ ```
50
+
51
+ ### 5. Login to NPM
52
+
53
+ ```bash
54
+ npm login
55
+ # Enter username, password, email, and 2FA code
56
+ ```
57
+
58
+ ### 6. Publish
59
+
60
+ ```bash
61
+ npm publish
62
+ ```
63
+
64
+ If successful, you'll see:
65
+ ```
66
+ + sapper@1.0.0
67
+ ```
68
+
69
+ ### 7. Verify Installation
70
+
71
+ Test global installation:
72
+ ```bash
73
+ npm install -g sapper
74
+ sapper --version
75
+ ```
76
+
77
+ ## Publishing Updates
78
+
79
+ ### For patch updates (1.0.0 → 1.0.1):
80
+ ```bash
81
+ npm version patch
82
+ git push --follow-tags
83
+ npm publish
84
+ ```
85
+
86
+ ### For minor updates (1.0.0 → 1.1.0):
87
+ ```bash
88
+ npm version minor
89
+ git push --follow-tags
90
+ npm publish
91
+ ```
92
+
93
+ ### For major updates (1.0.0 → 2.0.0):
94
+ ```bash
95
+ npm version major
96
+ git push --follow-tags
97
+ npm publish
98
+ ```
99
+
100
+ ## Troubleshooting
101
+
102
+ ### Package name taken
103
+ If "sapper" is taken, try alternatives:
104
+ - `sapper-ai`
105
+ - `sapper-cli`
106
+ - `ai-sapper`
107
+
108
+ Update `package.json` name field accordingly.
109
+
110
+ ### Authentication errors
111
+ ```bash
112
+ npm logout
113
+ npm login
114
+ ```
115
+
116
+ ### Permission errors
117
+ Check if you have publish rights to the package name.
118
+
119
+ ### Version conflicts
120
+ Each publish must have a unique version number. Increment version in `package.json`.
121
+
122
+ ## Post-Publish Steps
123
+
124
+ 1. **Verify on npmjs.com** - Visit `https://npmjs.com/package/sapper`
125
+ 2. **Test installation** - `npm install -g sapper`
126
+ 3. **Update README** - Add npm installation instructions
127
+ 4. **Tag release on GitHub** - Create release tag matching npm version
128
+
129
+ ## NPM Commands Reference
130
+
131
+ - `npm whoami` - Check logged in user
132
+ - `npm view sapper` - View package info
133
+ - `npm unpublish sapper@1.0.0` - Remove specific version (within 24hrs)
134
+ - `npm deprecate sapper@1.0.0 "reason"` - Mark version as deprecated
135
+ - `npm owner ls sapper` - List package owners
136
+
137
+ ## GitHub Integration
138
+
139
+ After publishing, users can install via:
140
+ - `npm install -g sapper` (from npm registry)
141
+ - `npm install -g git+https://github.com/aledanee/sapper.git` (from GitHub)
142
+
143
+ Keep both npm and GitHub versions synchronized.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Sapper
2
+
3
+ 🚀 **AI-powered development assistant that executes commands and builds projects**
4
+
5
+ Sapper is a command-line interface that connects to Ollama models to help you build, manage, and execute development tasks through natural language conversations.
6
+
7
+ ## Features
8
+
9
+ - 🤖 **AI-powered assistance** - Chat with local Ollama models
10
+ - 🛠️ **Multi-tool execution** - File operations, shell commands, directory management
11
+ - 💬 **Conversational interface** - Natural language project management
12
+ - 🔄 **Session persistence** - Resume previous conversations
13
+ - 🎯 **Context-aware** - Automatically detects directory contents
14
+ - ⚡ **Live streaming** - See AI responses in real-time
15
+ - 🔒 **Security prompts** - Review commands before execution
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g sapper
21
+ ```
22
+
23
+ ## Prerequisites
24
+
25
+ - Node.js 16+
26
+ - [Ollama](https://ollama.ai/) installed and running
27
+ - At least one Ollama model downloaded
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ sapper
33
+ ```
34
+
35
+ ### Commands
36
+
37
+ - `/reset` or `/clear-session` - Start a new session
38
+ - `/session-info` - Show current session details
39
+ - `/step` - Toggle step-by-step mode
40
+ - `/help` - Show command help
41
+ - `exit` - Exit Sapper
42
+
43
+ ### Example Interactions
44
+
45
+ ```
46
+ > set up a React project in ./my-app
47
+ > run the development server
48
+ > create a login component with TypeScript
49
+ > add Tailwind CSS styling
50
+ ```
51
+
52
+ ## How It Works
53
+
54
+ 1. **Connect to Ollama** - Choose from your available local models
55
+ 2. **Natural conversation** - Describe what you want to build or do
56
+ 3. **AI executes tools** - Creates files, runs commands, manages projects
57
+ 4. **Review & approve** - Security prompts for shell commands
58
+ 5. **Context awareness** - Sapper understands your project structure
59
+
60
+ ## Supported Tools
61
+
62
+ - `SHELL` - Execute terminal commands
63
+ - `READ` - Read file contents
64
+ - `WRITE` - Create/edit files
65
+ - `MKDIR` - Create directories
66
+ - `LIST` - List directory contents
67
+ - `SEARCH` - Search for text in files
68
+
69
+ ## Examples
70
+
71
+ **Create a Next.js project:**
72
+ ```
73
+ > create a Next.js app with TypeScript and Tailwind in ./my-nextjs-app
74
+ ```
75
+
76
+ **Add features to existing project:**
77
+ ```
78
+ > analyze the codebase in ./my-project
79
+ > add a user authentication system
80
+ > create API endpoints for user management
81
+ ```
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ git clone https://github.com/yourusername/sapper
87
+ cd sapper
88
+ npm install
89
+ chmod +x sapper.mjs
90
+ ./sapper.mjs
91
+ ```
92
+
93
+ ## License
94
+
95
+ MIT
96
+
97
+ ## Author
98
+
99
+ Ibrahim Ihsan
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "sapper-iq",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered development assistant that executes commands and builds projects",
5
+ "main": "sapper.mjs",
6
+ "bin": {
7
+ "sapper": "sapper.mjs"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node sapper.mjs"
12
+ },
13
+ "dependencies": {
14
+ "ollama": "^0.5.0",
15
+ "chalk": "^5.3.0",
16
+ "ora": "^8.0.1"
17
+ },
18
+ "engines": {
19
+ "node": ">=16.0.0"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "ai",
24
+ "development",
25
+ "assistant",
26
+ "ollama",
27
+ "automation",
28
+ "productivity",
29
+ "developer-tools"
30
+ ],
31
+ "author": "Ibrahim Ihsan",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/aledanee/sapper.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/aledanee/sapper/issues"
39
+ },
40
+ "homepage": "https://github.com/aledanee/sapper#readme"
41
+ }
package/sapper.mjs ADDED
@@ -0,0 +1,421 @@
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/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\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'], {
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\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) => fs.readFileSync(path.trim(), 'utf8'),
99
+ write: (path, content) => {
100
+ fs.writeFileSync(path.trim(), content);
101
+ return `Successfully saved changes to ${path}`;
102
+ },
103
+ mkdir: (path) => {
104
+ fs.mkdirSync(path.trim(), { recursive: true });
105
+ return `Directory created: ${path}`;
106
+ },
107
+ shell: async (cmd) => {
108
+ console.log(chalk.red.bold(`\n[SECURITY] Sapper wants to execute: `) + chalk.white(cmd));
109
+ const confirm = await safeQuestion(chalk.yellow('Allow? (y/n): '));
110
+ if (confirm.toLowerCase() === 'y') {
111
+ return new Promise((resolve) => {
112
+ // Use shell for complex commands with pipes, redirects, cd, &&, ||, etc
113
+ const useShell = cmd.includes('&&') || cmd.includes('||') || cmd.includes('|') || cmd.includes('cd ') || cmd.includes('>');
114
+
115
+ console.log(chalk.cyan(`\n[RUNNING] ${cmd}\n`));
116
+
117
+ let proc;
118
+ if (useShell) {
119
+ // For complex commands, use shell
120
+ proc = spawn('sh', ['-c', cmd], {
121
+ stdio: 'inherit',
122
+ shell: true
123
+ });
124
+ } else {
125
+ // For simple commands, parse and use direct execution
126
+ const parts = cmd.trim().split(/\s+/);
127
+ const executable = parts[0];
128
+ const args = parts.slice(1);
129
+ proc = spawn(executable, args, {
130
+ stdio: 'inherit',
131
+ shell: false
132
+ });
133
+ }
134
+
135
+ proc.on('close', (code) => {
136
+ // Recreate readline after shell command completes
137
+ recreateReadline();
138
+ console.log(chalk.green(`\n[✓] Command completed with exit code ${code}\n`));
139
+ resolve(`Command completed with exit code ${code}.`);
140
+ });
141
+
142
+ proc.on('error', (err) => {
143
+ recreateReadline();
144
+ console.log(chalk.red(`\n[✗] Command error: ${err.message}\n`));
145
+ resolve(`Execution Error: ${err.message}`);
146
+ });
147
+ });
148
+ }
149
+ return "Command blocked by user.";
150
+ },
151
+ list: (path) => fs.readdirSync(path || '.').join('\n'),
152
+ search: (pattern) => {
153
+ try {
154
+ const { execSync } = require('child_process');
155
+ const cmd = `grep -rnEi "${pattern.trim()}" . --exclude-dir=node_modules --exclude-dir=.git`;
156
+ return execSync(cmd, { encoding: 'utf8' }) || "No matches found.";
157
+ } catch (e) { return "No matches found."; }
158
+ }
159
+ };
160
+
161
+ 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;
169
+ }
170
+
171
+ async function runSapper() {
172
+ console.clear();
173
+ console.log(chalk.cyan.bold(` SAPPER v${CURRENT_VERSION} | Multi-Tool Execution Mode`));
174
+ console.log(chalk.gray("Commands: /reset, /session-info, /step, /version, /update, /help, exit\n"));
175
+
176
+ // Check for updates on startup
177
+ await checkForUpdates();
178
+
179
+ let messages = [];
180
+ if (fs.existsSync(CONTEXT_FILE)) {
181
+ const resume = await safeQuestion(chalk.green('Resume previous session? (y/n): '));
182
+ if (resume.toLowerCase() === 'y') {
183
+ messages = JSON.parse(fs.readFileSync(CONTEXT_FILE, 'utf8'));
184
+ }
185
+ }
186
+
187
+ const selectedModel = await selectModel();
188
+
189
+ if (messages.length === 0) {
190
+ messages = [{
191
+ role: 'system',
192
+ content: `You are Sapper, a senior software engineer AI assistant.
193
+
194
+ **CRITICAL - Tool Format Rules:**
195
+ - NEVER use JSON format
196
+ - ONLY use this EXACT format for tools: [TOOL:TYPE:path:content]
197
+ - Types: SHELL, READ, WRITE, MKDIR, LIST, SEARCH
198
+
199
+ **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]
206
+
207
+ **Shell Command Rules:**
208
+ - For operations in a specific directory, chain with cd: cd /path/to/project && npm install
209
+ - Use && to chain commands that depend on each other
210
+ - Use | for pipes and > for redirects
211
+ - Use relative paths after cd into a directory
212
+ - Chain multiple commands: cd /path && npm install && npm run dev
213
+ - User will specify which directory to work in - always use that path
214
+
215
+ **Critical for npm/npx commands:**
216
+ - ALWAYS use non-interactive flags (--typescript, --tailwind, --eslint, --no-git, etc)
217
+ - Create projects with non-interactive flags
218
+ - Install dependencies with: cd /path && npm install
219
+ - Run apps with: cd /path && npm run dev
220
+
221
+ **Workflow:**
222
+ 1. For complex tasks, start with [PLAN:step1,step2,step3]
223
+ 2. Execute tools immediately using the exact format above
224
+ 3. You can provide MULTIPLE tools in one message
225
+ 4. Always end with [SUMMARY:description of what was completed]
226
+
227
+ **Important:**
228
+ - No JSON responses
229
+ - No markdown code blocks for tools
230
+ - Only the exact bracket format: [TOOL:TYPE:path:content]
231
+ - User will see live command output in terminal
232
+ - Execute all tools needed to complete the task
233
+ - Work flexibly with ANY directory the user specifies
234
+ - Always chain cd with your command when working in a specific directory`
235
+ }];
236
+ }
237
+
238
+ // Display working directory awareness
239
+ console.log(chalk.yellow(`Working Directory: ${process.cwd()}\n`));
240
+
241
+ const ask = () => {
242
+ safeQuestion(chalk.blue.bold('\nIbrahim ➔ ')).then(async (input) => {
243
+ if (input.toLowerCase() === 'exit') process.exit();
244
+ if (input.toLowerCase() === '/reset' || input.toLowerCase() === '/clear-session') {
245
+ if (fs.existsSync(CONTEXT_FILE)) {
246
+ const fileSize = fs.statSync(CONTEXT_FILE).size;
247
+ console.log(chalk.yellow(`\n🗑️ Clearing session (${(fileSize / 1024).toFixed(2)}KB)...`));
248
+ fs.unlinkSync(CONTEXT_FILE);
249
+ console.log(chalk.green('✅ Session cleared! Starting fresh...\n'));
250
+ } else {
251
+ console.log(chalk.yellow('\nℹ️ No session to clear.\n'));
252
+ }
253
+ return runSapper();
254
+ }
255
+ if (input.toLowerCase() === '/session-info') {
256
+ if (fs.existsSync(CONTEXT_FILE)) {
257
+ const data = JSON.parse(fs.readFileSync(CONTEXT_FILE, 'utf8'));
258
+ const fileSize = fs.statSync(CONTEXT_FILE).size;
259
+ console.log(chalk.cyan(`\n📊 Session Info:`));
260
+ console.log(chalk.gray(` Messages: ${data.length}`));
261
+ console.log(chalk.gray(` File Size: ${(fileSize / 1024).toFixed(2)}KB`));
262
+ console.log(chalk.gray(` Last Message: ${data[data.length - 1]?.role || 'N/A'}`));
263
+ } else {
264
+ console.log(chalk.yellow('\nℹ️ No active session.\n'));
265
+ }
266
+ return ask();
267
+ }
268
+ if (input.toLowerCase() === '/version') {
269
+ console.log(chalk.cyan(`\n📦 Sapper Version: v${CURRENT_VERSION}`));
270
+ console.log(chalk.gray(` Node.js: ${process.version}`));
271
+ console.log(chalk.gray(` Platform: ${process.platform}\n`));
272
+ // Check for updates
273
+ await checkForUpdates();
274
+ return ask();
275
+ }
276
+ if (input.toLowerCase() === '/update') {
277
+ await updateSapper();
278
+ return ask();
279
+ }
280
+ if (input.toLowerCase() === '/step') {
281
+ stepMode = !stepMode;
282
+ console.log(chalk.yellow(`Step Mode is ${stepMode ? 'ON' : 'OFF'}`));
283
+ return ask();
284
+ }
285
+ if (input.toLowerCase() === '/help') {
286
+ console.log(chalk.cyan(`\n📚 Sapper Commands:`));
287
+ console.log(chalk.gray(` /reset or /clear-session - Start a new session`));
288
+ console.log(chalk.gray(` /session-info - Show current session details`));
289
+ console.log(chalk.gray(` /version - Show version and check for updates`));
290
+ console.log(chalk.gray(` /update - Update Sapper to latest version`));
291
+ console.log(chalk.gray(` /step - Toggle step-by-step mode`));
292
+ console.log(chalk.gray(` /help - Show this help menu`));
293
+ console.log(chalk.gray(` exit - Exit Sapper\n`));
294
+ return ask();
295
+ }
296
+
297
+ // Check if user mentioned a directory and provide context
298
+ const dirMatch = input.match(/\/Users\/[^\s]+|\/[a-zA-Z0-9_\/-]+/g);
299
+ let contextMsg = input;
300
+
301
+ if (dirMatch && dirMatch[0]) {
302
+ const mentionedDir = dirMatch[0];
303
+ try {
304
+ if (fs.existsSync(mentionedDir) && fs.statSync(mentionedDir).isDirectory()) {
305
+ const files = fs.readdirSync(mentionedDir).slice(0, 10).join(', ');
306
+ contextMsg = `${input}\n\n[CONTEXT: Directory "${mentionedDir}" contains: ${files}${fs.readdirSync(mentionedDir).length > 10 ? '...' : ''}]`;
307
+ }
308
+ } catch (e) {
309
+ // Silently ignore if directory doesn't exist
310
+ }
311
+ }
312
+
313
+ messages.push({ role: 'user', content: contextMsg });
314
+
315
+ let active = true;
316
+ let iterations = 0;
317
+ while (active && iterations < 30) {
318
+ iterations++;
319
+
320
+ if (stepMode) {
321
+ const proceed = await safeQuestion(chalk.gray('\n[STEP-MODE] Press Enter to continue (or type "/stop"): '));
322
+ if (proceed.toLowerCase() === '/stop') break;
323
+ }
324
+
325
+ spinner.stop();
326
+ console.log(chalk.blue(`\n${selectedModel} is thinking...`));
327
+
328
+ const response = await ollama.chat({
329
+ model: selectedModel,
330
+ messages,
331
+ stream: true,
332
+ options: { num_ctx: 16384 }
333
+ });
334
+
335
+ let msg = '';
336
+ process.stdout.write(chalk.white('Sapper: '));
337
+
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;
342
+ }
343
+ }
344
+ console.log();
345
+
346
+ messages.push({ role: 'assistant', content: msg });
347
+
348
+ const summaryMatch = msg.match(/\[SUMMARY:(.*?)\]/s);
349
+ const toolMatches = [...msg.matchAll(/\[TOOL:(\w+):([^:\]]+):?([\s\S]*?)\]/g)];
350
+
351
+ if (summaryMatch) {
352
+ console.log(chalk.green.bold("\n✅ MISSION COMPLETE:"));
353
+ console.log(chalk.white(summaryMatch[1].trim()));
354
+ active = false;
355
+ continue;
356
+ }
357
+
358
+ if (toolMatches.length > 0) {
359
+ for (const match of toolMatches) {
360
+ const [_, name, path, content] = match;
361
+ const toolName = name.toLowerCase();
362
+ console.log(chalk.cyan(`\n[ACTION] Executing ${toolName} on: ${path}`));
363
+
364
+ let result;
365
+ try {
366
+ if (toolName === 'shell') result = await tools.shell(path);
367
+ else if (toolName === 'write') result = tools.write(path, content);
368
+ else if (toolName === 'mkdir') result = tools.mkdir(path);
369
+ else if (toolName === 'read') result = tools.read(path);
370
+ else if (toolName === 'list') result = tools.list(path);
371
+ else if (toolName === 'search') result = tools.search(path);
372
+ else result = `Unknown tool: ${name}`;
373
+ } catch (e) {
374
+ result = `Error: ${e.message}`;
375
+ }
376
+
377
+ console.log(chalk.gray(`> Result: ${result.substring(0, 60)}...`));
378
+ messages.push({ role: 'user', content: `TOOL_RESULT for ${path}: ${result}` });
379
+ }
380
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages));
381
+
382
+ // Add interrupt check after tool execution
383
+ console.log(chalk.gray('\n[Press Enter to continue or type "/stop" to halt execution]'));
384
+ const userChoice = await safeQuestion('');
385
+ if (userChoice.toLowerCase() === '/stop') {
386
+ console.log(chalk.yellow('\n⏹️ Execution halted by user'));
387
+ active = false;
388
+ break;
389
+ }
390
+ } else {
391
+ const planMatch = msg.match(/\[PLAN:(.*?)\]/);
392
+ if (planMatch) {
393
+ const feedback = await safeQuestion(chalk.yellow('\nModify plan or type "go": '));
394
+ if (feedback.toLowerCase() === '/stop') { active = false; break; }
395
+ messages.push({ role: 'user', content: feedback.toLowerCase() === 'go' ? "Plan approved. Proceed with all steps." : feedback });
396
+ } else {
397
+ active = false;
398
+ }
399
+ }
400
+
401
+ // Safety check: if model is repeating itself, break the loop
402
+ if (iterations > 5) {
403
+ const recentMessages = messages.slice(-4);
404
+ const isRepeating = recentMessages.every(m =>
405
+ m.role === 'assistant' &&
406
+ recentMessages[0].content &&
407
+ m.content === recentMessages[0].content
408
+ );
409
+ if (isRepeating) {
410
+ console.log(chalk.yellow('\n⚠️ Detected repetitive behavior, stopping execution'));
411
+ active = false;
412
+ }
413
+ }
414
+ }
415
+ ask();
416
+ });
417
+ };
418
+ ask();
419
+ }
420
+
421
+ runSapper();