sapper-iq 1.1.18 → 1.1.20
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.mjs +162 -46
package/package.json
CHANGED
package/sapper.mjs
CHANGED
|
@@ -62,6 +62,79 @@ try {
|
|
|
62
62
|
|
|
63
63
|
const spinner = ora();
|
|
64
64
|
const CONTEXT_FILE = '.sapper_context.json';
|
|
65
|
+
const EMBEDDINGS_FILE = '.sapper_embeddings.json';
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════
|
|
68
|
+
// EMBEDDINGS & SEMANTIC SEARCH
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
// Load or create embeddings store
|
|
72
|
+
function loadEmbeddings() {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(EMBEDDINGS_FILE)) {
|
|
75
|
+
return JSON.parse(fs.readFileSync(EMBEDDINGS_FILE, 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
return { chunks: [] }; // { chunks: [{ text, embedding, timestamp }] }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function saveEmbeddings(embeddings) {
|
|
82
|
+
fs.writeFileSync(EMBEDDINGS_FILE, JSON.stringify(embeddings, null, 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get embedding from Ollama (returns null silently if model not available)
|
|
86
|
+
async function getEmbedding(text, model = 'nomic-embed-text') {
|
|
87
|
+
try {
|
|
88
|
+
const response = await ollama.embeddings({ model, prompt: text });
|
|
89
|
+
return response.embedding;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Silently return null - caller handles missing embeddings
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cosine similarity between two vectors
|
|
97
|
+
function cosineSimilarity(a, b) {
|
|
98
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
99
|
+
let dotProduct = 0, normA = 0, normB = 0;
|
|
100
|
+
for (let i = 0; i < a.length; i++) {
|
|
101
|
+
dotProduct += a[i] * b[i];
|
|
102
|
+
normA += a[i] * a[i];
|
|
103
|
+
normB += b[i] * b[i];
|
|
104
|
+
}
|
|
105
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Find most relevant chunks for a query
|
|
109
|
+
async function findRelevantContext(query, embeddings, topK = 3) {
|
|
110
|
+
const queryEmbedding = await getEmbedding(query);
|
|
111
|
+
if (!queryEmbedding || embeddings.chunks.length === 0) return [];
|
|
112
|
+
|
|
113
|
+
const scored = embeddings.chunks.map(chunk => ({
|
|
114
|
+
...chunk,
|
|
115
|
+
score: cosineSimilarity(queryEmbedding, chunk.embedding)
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
scored.sort((a, b) => b.score - a.score);
|
|
119
|
+
return scored.slice(0, topK).filter(c => c.score > 0.5); // Only return if similarity > 0.5
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add text to embeddings store
|
|
123
|
+
async function addToEmbeddings(text, embeddings) {
|
|
124
|
+
const embedding = await getEmbedding(text);
|
|
125
|
+
if (embedding) {
|
|
126
|
+
embeddings.chunks.push({
|
|
127
|
+
text: text.substring(0, 2000), // Limit stored text
|
|
128
|
+
embedding,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
});
|
|
131
|
+
// Keep only last 100 chunks
|
|
132
|
+
if (embeddings.chunks.length > 100) {
|
|
133
|
+
embeddings.chunks = embeddings.chunks.slice(-100);
|
|
134
|
+
}
|
|
135
|
+
saveEmbeddings(embeddings);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
65
138
|
|
|
66
139
|
// ═══════════════════════════════════════════════════════════════
|
|
67
140
|
// FANCY UI HELPERS
|
|
@@ -430,44 +503,13 @@ async function runSapper() {
|
|
|
430
503
|
if (messages.length === 0) {
|
|
431
504
|
messages = [{
|
|
432
505
|
role: 'system',
|
|
433
|
-
content: `You are Sapper,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
-
|
|
437
|
-
-
|
|
438
|
-
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
[TOOL:LIST]path[/TOOL]
|
|
442
|
-
→ List files in a directory
|
|
443
|
-
→ Example: [TOOL:LIST].[/TOOL]
|
|
444
|
-
|
|
445
|
-
[TOOL:READ]path[/TOOL]
|
|
446
|
-
→ Read a file's contents
|
|
447
|
-
→ Example: [TOOL:READ]./package.json[/TOOL]
|
|
448
|
-
|
|
449
|
-
[TOOL:WRITE]path]content[/TOOL]
|
|
450
|
-
→ Create or overwrite a file (needs user confirmation)
|
|
451
|
-
→ Example: [TOOL:WRITE]./index.js]console.log("hello")[/TOOL]
|
|
452
|
-
|
|
453
|
-
[TOOL:PATCH]path]old_text|||new_text[/TOOL]
|
|
454
|
-
→ Replace specific text in a file (needs user confirmation)
|
|
455
|
-
→ Example: [TOOL:PATCH]./app.js]old code|||new code[/TOOL]
|
|
456
|
-
|
|
457
|
-
[TOOL:SEARCH]pattern[/TOOL]
|
|
458
|
-
→ Search for text across all files
|
|
459
|
-
→ Example: [TOOL:SEARCH]function login[/TOOL]
|
|
460
|
-
|
|
461
|
-
[TOOL:SHELL]command[/TOOL]
|
|
462
|
-
→ Run a terminal command (needs user confirmation)
|
|
463
|
-
→ Example: [TOOL:SHELL]npm install express[/TOOL]
|
|
464
|
-
|
|
465
|
-
PATH RULES:
|
|
466
|
-
- Always use relative paths: ./file.js, ./src/app.js
|
|
467
|
-
- NEVER use absolute paths like /file.js
|
|
468
|
-
- Use . for current directory
|
|
469
|
-
|
|
470
|
-
`
|
|
506
|
+
content: `You are Sapper, an AGENT, You can use tools to take action:
|
|
507
|
+
- [TOOL:LIST]path[/TOOL] - List directory
|
|
508
|
+
- [TOOL:READ]path[/TOOL] - Read file
|
|
509
|
+
- [TOOL:SEARCH]pattern[/TOOL] - Search codebase
|
|
510
|
+
- [TOOL:WRITE]path]content[/TOOL] - Create/overwrite file
|
|
511
|
+
- [TOOL:PATCH]path]old|||new[/TOOL] - Edit file
|
|
512
|
+
- [TOOL:SHELL]command[/TOOL] - Run terminal command`
|
|
471
513
|
}];
|
|
472
514
|
}
|
|
473
515
|
|
|
@@ -502,33 +544,61 @@ PATH RULES:
|
|
|
502
544
|
continue;
|
|
503
545
|
}
|
|
504
546
|
|
|
505
|
-
// Handle prune command -
|
|
547
|
+
// Handle prune command - AUTO-EMBED then clear old context
|
|
506
548
|
if (input.toLowerCase() === '/prune') {
|
|
507
549
|
if (messages.length <= 5) {
|
|
508
550
|
console.log(chalk.yellow('Context is already small, nothing to prune.'));
|
|
509
551
|
continue;
|
|
510
552
|
}
|
|
511
553
|
|
|
512
|
-
// 1.
|
|
554
|
+
// 1. AUTO-EMBED: Save conversation to memory BEFORE pruning (silently skip if no model)
|
|
555
|
+
const embeddings = loadEmbeddings();
|
|
556
|
+
|
|
557
|
+
// Get messages that will be pruned (all except system and last 4)
|
|
558
|
+
const messagesToEmbed = messages.slice(1, -4)
|
|
559
|
+
.filter(m => m.role !== 'system')
|
|
560
|
+
.map(m => m.content.substring(0, 500))
|
|
561
|
+
.join('\n---\n');
|
|
562
|
+
|
|
563
|
+
if (messagesToEmbed.length > 50) {
|
|
564
|
+
try {
|
|
565
|
+
const embedding = await getEmbedding(messagesToEmbed);
|
|
566
|
+
if (embedding) {
|
|
567
|
+
embeddings.chunks.push({
|
|
568
|
+
text: messagesToEmbed.substring(0, 2000),
|
|
569
|
+
embedding,
|
|
570
|
+
timestamp: Date.now()
|
|
571
|
+
});
|
|
572
|
+
if (embeddings.chunks.length > 100) {
|
|
573
|
+
embeddings.chunks = embeddings.chunks.slice(-100);
|
|
574
|
+
}
|
|
575
|
+
saveEmbeddings(embeddings);
|
|
576
|
+
console.log(chalk.green(`🧠 Saved to memory! (${embeddings.chunks.length} memories)`));
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {
|
|
579
|
+
// Silently skip embedding if model not available - prune still works
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 2. Capture the ORIGINAL detailed system prompt from the very first message
|
|
513
584
|
const originalSystemPrompt = messages[0];
|
|
514
585
|
|
|
515
|
-
//
|
|
586
|
+
// 3. Capture the last 4 messages (the most recent conversation)
|
|
516
587
|
const recentMessages = messages.slice(-4);
|
|
517
588
|
|
|
518
|
-
//
|
|
589
|
+
// 4. Rebuild the messages array starting with the ORIGINAL prompt
|
|
519
590
|
messages = [originalSystemPrompt, ...recentMessages];
|
|
520
591
|
|
|
521
592
|
// 4. Add reminder to stay in Agent Mode (not chatbot mode)
|
|
522
593
|
messages.push({
|
|
523
594
|
role: 'system',
|
|
524
|
-
content: `CONTEXT PRUNED. REMINDER: You are an AGENT,
|
|
595
|
+
content: `CONTEXT PRUNED. REMINDER: You are an AGENT, You can use tools to take action:
|
|
525
596
|
- [TOOL:LIST]path[/TOOL] - List directory
|
|
526
597
|
- [TOOL:READ]path[/TOOL] - Read file
|
|
527
598
|
- [TOOL:SEARCH]pattern[/TOOL] - Search codebase
|
|
528
599
|
- [TOOL:WRITE]path]content[/TOOL] - Create/overwrite file
|
|
529
600
|
- [TOOL:PATCH]path]old|||new[/TOOL] - Edit file
|
|
530
|
-
- [TOOL:SHELL]command[/TOOL] - Run terminal command
|
|
531
|
-
Do NOT just display content. Actually WRITE files using the tool.`
|
|
601
|
+
- [TOOL:SHELL]command[/TOOL] - Run terminal command.`
|
|
532
602
|
});
|
|
533
603
|
|
|
534
604
|
// 5. Save to context file so it persists
|
|
@@ -544,8 +614,9 @@ Do NOT just display content. Actually WRITE files using the tool.`
|
|
|
544
614
|
console.log();
|
|
545
615
|
const helpContent =
|
|
546
616
|
`${chalk.cyan('/scan')} ${chalk.gray('│')} Scan codebase into context\n` +
|
|
617
|
+
`${chalk.cyan('/recall')} ${chalk.gray('│')} Search memory for relevant context\n` +
|
|
547
618
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('│')} Clear all context\n` +
|
|
548
|
-
`${chalk.cyan('/prune')} ${chalk.gray('│')}
|
|
619
|
+
`${chalk.cyan('/prune')} ${chalk.gray('│')} Save to memory + keep last 4 msgs\n` +
|
|
549
620
|
`${chalk.cyan('/context')} ${chalk.gray('│')} Show context size\n` +
|
|
550
621
|
`${chalk.cyan('/debug')} ${chalk.gray('│')} Toggle debug mode\n` +
|
|
551
622
|
`${chalk.cyan('/help')} ${chalk.gray('│')} Show this help\n` +
|
|
@@ -575,6 +646,51 @@ Do NOT just display content. Actually WRITE files using the tool.`
|
|
|
575
646
|
continue;
|
|
576
647
|
}
|
|
577
648
|
|
|
649
|
+
// Handle recall command - search embeddings
|
|
650
|
+
if (input.toLowerCase().startsWith('/recall')) {
|
|
651
|
+
const query = input.slice(7).trim();
|
|
652
|
+
if (!query) {
|
|
653
|
+
console.log(chalk.yellow('Usage: /recall <search query>'));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const embeddings = loadEmbeddings();
|
|
658
|
+
if (embeddings.chunks.length === 0) {
|
|
659
|
+
console.log(chalk.yellow('No memories yet. Use /prune to auto-save conversations.'));
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
console.log(chalk.cyan(`\n🔍 Searching memory for: "${query}"...`));
|
|
664
|
+
const relevant = await findRelevantContext(query, embeddings, 3);
|
|
665
|
+
|
|
666
|
+
if (relevant.length === 0) {
|
|
667
|
+
console.log(chalk.yellow('No relevant memories found (or embedding model not available).'));
|
|
668
|
+
console.log(chalk.gray('Tip: Run "ollama pull nomic-embed-text" for semantic search.'));
|
|
669
|
+
} else {
|
|
670
|
+
console.log(chalk.green(`Found ${relevant.length} relevant memories:\n`));
|
|
671
|
+
relevant.forEach((chunk, i) => {
|
|
672
|
+
console.log(box(
|
|
673
|
+
chalk.gray(chunk.text.substring(0, 300) + '...') + '\n' +
|
|
674
|
+
chalk.cyan(`Similarity: ${(chunk.score * 100).toFixed(1)}%`),
|
|
675
|
+
`Memory ${i + 1}`, 'magenta'
|
|
676
|
+
));
|
|
677
|
+
console.log();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Optionally add to context
|
|
681
|
+
const addToContext = await safeQuestion(chalk.yellow('Add to current context? ') + chalk.gray('(y/n): '));
|
|
682
|
+
if (addToContext.toLowerCase() === 'y') {
|
|
683
|
+
const contextAddition = relevant.map(c => c.text).join('\n---\n');
|
|
684
|
+
messages.push({
|
|
685
|
+
role: 'user',
|
|
686
|
+
content: `Here is relevant context from memory:\n${contextAddition}\n\nUse this information to help me.`
|
|
687
|
+
});
|
|
688
|
+
console.log(chalk.green('✅ Added to context!'));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
578
694
|
// Handle codebase scan command
|
|
579
695
|
if (input.toLowerCase() === '/scan') {
|
|
580
696
|
console.log(chalk.cyan('\n🔍 Scanning codebase...'));
|