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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper.mjs +162 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
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
@@ -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, a coding assistant.
434
-
435
- GOLDEN RULE:
436
- - NEVER add features the user didn't ask for.
437
- - ALWAYS confirm with the user before writing/patching files or running shell commands.
438
- - KEEP responses concise and to the point.
439
- TOOLS (use these to interact with files):
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 - summarize and clear old context
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. Capture the ORIGINAL detailed system prompt from the very first message
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
- // 2. Capture the last 4 messages (the most recent conversation)
586
+ // 3. Capture the last 4 messages (the most recent conversation)
516
587
  const recentMessages = messages.slice(-4);
517
588
 
518
- // 3. Rebuild the messages array starting with the ORIGINAL prompt
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, not a chatbot. You MUST use tools to take action:
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('│')} Keep only last 4 messages\n` +
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...'));