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.
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +46 -0
- package/PUBLISHING.md +143 -0
- package/README.md +99 -0
- package/package.json +41 -0
- package/sapper.mjs +421 -0
|
@@ -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();
|