sapper-iq 1.1.18 → 1.1.19
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 +153 -5
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
|
|
@@ -502,20 +575,49 @@ PATH RULES:
|
|
|
502
575
|
continue;
|
|
503
576
|
}
|
|
504
577
|
|
|
505
|
-
// Handle prune command -
|
|
578
|
+
// Handle prune command - AUTO-EMBED then clear old context
|
|
506
579
|
if (input.toLowerCase() === '/prune') {
|
|
507
580
|
if (messages.length <= 5) {
|
|
508
581
|
console.log(chalk.yellow('Context is already small, nothing to prune.'));
|
|
509
582
|
continue;
|
|
510
583
|
}
|
|
511
584
|
|
|
512
|
-
// 1.
|
|
585
|
+
// 1. AUTO-EMBED: Save conversation to memory BEFORE pruning (silently skip if no model)
|
|
586
|
+
const embeddings = loadEmbeddings();
|
|
587
|
+
|
|
588
|
+
// Get messages that will be pruned (all except system and last 4)
|
|
589
|
+
const messagesToEmbed = messages.slice(1, -4)
|
|
590
|
+
.filter(m => m.role !== 'system')
|
|
591
|
+
.map(m => m.content.substring(0, 500))
|
|
592
|
+
.join('\n---\n');
|
|
593
|
+
|
|
594
|
+
if (messagesToEmbed.length > 50) {
|
|
595
|
+
try {
|
|
596
|
+
const embedding = await getEmbedding(messagesToEmbed);
|
|
597
|
+
if (embedding) {
|
|
598
|
+
embeddings.chunks.push({
|
|
599
|
+
text: messagesToEmbed.substring(0, 2000),
|
|
600
|
+
embedding,
|
|
601
|
+
timestamp: Date.now()
|
|
602
|
+
});
|
|
603
|
+
if (embeddings.chunks.length > 100) {
|
|
604
|
+
embeddings.chunks = embeddings.chunks.slice(-100);
|
|
605
|
+
}
|
|
606
|
+
saveEmbeddings(embeddings);
|
|
607
|
+
console.log(chalk.green(`🧠 Saved to memory! (${embeddings.chunks.length} memories)`));
|
|
608
|
+
}
|
|
609
|
+
} catch (e) {
|
|
610
|
+
// Silently skip embedding if model not available - prune still works
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 2. Capture the ORIGINAL detailed system prompt from the very first message
|
|
513
615
|
const originalSystemPrompt = messages[0];
|
|
514
616
|
|
|
515
|
-
//
|
|
617
|
+
// 3. Capture the last 4 messages (the most recent conversation)
|
|
516
618
|
const recentMessages = messages.slice(-4);
|
|
517
619
|
|
|
518
|
-
//
|
|
620
|
+
// 4. Rebuild the messages array starting with the ORIGINAL prompt
|
|
519
621
|
messages = [originalSystemPrompt, ...recentMessages];
|
|
520
622
|
|
|
521
623
|
// 4. Add reminder to stay in Agent Mode (not chatbot mode)
|
|
@@ -544,8 +646,9 @@ Do NOT just display content. Actually WRITE files using the tool.`
|
|
|
544
646
|
console.log();
|
|
545
647
|
const helpContent =
|
|
546
648
|
`${chalk.cyan('/scan')} ${chalk.gray('│')} Scan codebase into context\n` +
|
|
649
|
+
`${chalk.cyan('/recall')} ${chalk.gray('│')} Search memory for relevant context\n` +
|
|
547
650
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('│')} Clear all context\n` +
|
|
548
|
-
`${chalk.cyan('/prune')} ${chalk.gray('│')}
|
|
651
|
+
`${chalk.cyan('/prune')} ${chalk.gray('│')} Save to memory + keep last 4 msgs\n` +
|
|
549
652
|
`${chalk.cyan('/context')} ${chalk.gray('│')} Show context size\n` +
|
|
550
653
|
`${chalk.cyan('/debug')} ${chalk.gray('│')} Toggle debug mode\n` +
|
|
551
654
|
`${chalk.cyan('/help')} ${chalk.gray('│')} Show this help\n` +
|
|
@@ -575,6 +678,51 @@ Do NOT just display content. Actually WRITE files using the tool.`
|
|
|
575
678
|
continue;
|
|
576
679
|
}
|
|
577
680
|
|
|
681
|
+
// Handle recall command - search embeddings
|
|
682
|
+
if (input.toLowerCase().startsWith('/recall')) {
|
|
683
|
+
const query = input.slice(7).trim();
|
|
684
|
+
if (!query) {
|
|
685
|
+
console.log(chalk.yellow('Usage: /recall <search query>'));
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const embeddings = loadEmbeddings();
|
|
690
|
+
if (embeddings.chunks.length === 0) {
|
|
691
|
+
console.log(chalk.yellow('No memories yet. Use /prune to auto-save conversations.'));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
console.log(chalk.cyan(`\n🔍 Searching memory for: "${query}"...`));
|
|
696
|
+
const relevant = await findRelevantContext(query, embeddings, 3);
|
|
697
|
+
|
|
698
|
+
if (relevant.length === 0) {
|
|
699
|
+
console.log(chalk.yellow('No relevant memories found (or embedding model not available).'));
|
|
700
|
+
console.log(chalk.gray('Tip: Run "ollama pull nomic-embed-text" for semantic search.'));
|
|
701
|
+
} else {
|
|
702
|
+
console.log(chalk.green(`Found ${relevant.length} relevant memories:\n`));
|
|
703
|
+
relevant.forEach((chunk, i) => {
|
|
704
|
+
console.log(box(
|
|
705
|
+
chalk.gray(chunk.text.substring(0, 300) + '...') + '\n' +
|
|
706
|
+
chalk.cyan(`Similarity: ${(chunk.score * 100).toFixed(1)}%`),
|
|
707
|
+
`Memory ${i + 1}`, 'magenta'
|
|
708
|
+
));
|
|
709
|
+
console.log();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Optionally add to context
|
|
713
|
+
const addToContext = await safeQuestion(chalk.yellow('Add to current context? ') + chalk.gray('(y/n): '));
|
|
714
|
+
if (addToContext.toLowerCase() === 'y') {
|
|
715
|
+
const contextAddition = relevant.map(c => c.text).join('\n---\n');
|
|
716
|
+
messages.push({
|
|
717
|
+
role: 'user',
|
|
718
|
+
content: `Here is relevant context from memory:\n${contextAddition}\n\nUse this information to help me.`
|
|
719
|
+
});
|
|
720
|
+
console.log(chalk.green('✅ Added to context!'));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
|
|
578
726
|
// Handle codebase scan command
|
|
579
727
|
if (input.toLowerCase() === '/scan') {
|
|
580
728
|
console.log(chalk.cyan('\n🔍 Scanning codebase...'));
|