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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper.mjs +153 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
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 - summarize and clear old context
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. Capture the ORIGINAL detailed system prompt from the very first message
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
- // 2. Capture the last 4 messages (the most recent conversation)
617
+ // 3. Capture the last 4 messages (the most recent conversation)
516
618
  const recentMessages = messages.slice(-4);
517
619
 
518
- // 3. Rebuild the messages array starting with the ORIGINAL prompt
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('│')} Keep only last 4 messages\n` +
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...'));