sapper-iq 1.0.2 ā 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 +1 -1
- package/sapper copy.mjs +502 -0
- package/sapper.mjs +28 -9
package/package.json
CHANGED
package/sapper copy.mjs
ADDED
|
@@ -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
|
@@ -234,16 +234,24 @@ async function runSapper() {
|
|
|
234
234
|
|
|
235
235
|
**CRITICAL - Tool Format Rules:**
|
|
236
236
|
- NEVER use JSON format
|
|
237
|
-
- ONLY use this EXACT format for tools: [TOOL:TYPE:path
|
|
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)
|
|
238
239
|
- Types: SHELL, READ, WRITE, MKDIR, LIST, SEARCH
|
|
239
240
|
|
|
240
241
|
**Examples:**
|
|
241
|
-
[TOOL:SHELL:npm install]
|
|
242
|
-
[TOOL:READ:./package.json]
|
|
243
|
-
[TOOL:WRITE:./app.js
|
|
244
|
-
[TOOL:MKDIR:./src/components]
|
|
245
|
-
[TOOL:LIST:./src]
|
|
246
|
-
[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]
|
|
247
255
|
|
|
248
256
|
**Shell Command Rules:**
|
|
249
257
|
- For operations in a specific directory, chain with cd: cd /path/to/project && npm install
|
|
@@ -408,7 +416,18 @@ async function runSapper() {
|
|
|
408
416
|
messages.push({ role: 'assistant', content: msg });
|
|
409
417
|
|
|
410
418
|
const summaryMatch = msg.match(/\[SUMMARY:(.*?)\]/s);
|
|
411
|
-
|
|
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
|
+
];
|
|
412
431
|
|
|
413
432
|
if (summaryMatch) {
|
|
414
433
|
console.log(chalk.green.bold("\nā
MISSION COMPLETE:"));
|
|
@@ -450,7 +469,7 @@ async function runSapper() {
|
|
|
450
469
|
break;
|
|
451
470
|
}
|
|
452
471
|
} else {
|
|
453
|
-
const planMatch = msg.match(/\[PLAN:(
|
|
472
|
+
const planMatch = msg.match(/\[PLAN:([\s\S]*?)\]/) || msg.match(/\[PLAN\]([\s\S]*?)\[\/PLAN\]/);
|
|
454
473
|
if (planMatch) {
|
|
455
474
|
const feedback = await safeQuestion(chalk.yellow('\nModify plan or type "go": '));
|
|
456
475
|
if (feedback.toLowerCase() === '/stop') { active = false; break; }
|