snow-ai 0.3.31 → 0.3.33

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.
@@ -0,0 +1,333 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { logger } from './logger.js';
5
+ /**
6
+ * Codebase SQLite database manager
7
+ * Handles embedding storage with vector support
8
+ */
9
+ export class CodebaseDatabase {
10
+ constructor(projectRoot) {
11
+ Object.defineProperty(this, "db", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: null
16
+ });
17
+ Object.defineProperty(this, "dbPath", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: void 0
22
+ });
23
+ Object.defineProperty(this, "initialized", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: false
28
+ });
29
+ // Store database in .snow/codebase directory
30
+ const snowDir = path.join(projectRoot, '.snow', 'codebase');
31
+ if (!fs.existsSync(snowDir)) {
32
+ fs.mkdirSync(snowDir, { recursive: true });
33
+ }
34
+ this.dbPath = path.join(snowDir, 'embeddings.db');
35
+ }
36
+ /**
37
+ * Initialize database and create tables
38
+ */
39
+ initialize() {
40
+ if (this.initialized)
41
+ return;
42
+ try {
43
+ // Open database with better-sqlite3
44
+ this.db = new Database(this.dbPath);
45
+ // Enable WAL mode for better concurrency
46
+ this.db.pragma('journal_mode = WAL');
47
+ // Create tables
48
+ this.createTables();
49
+ this.initialized = true;
50
+ logger.info('Codebase database initialized', { path: this.dbPath });
51
+ }
52
+ catch (error) {
53
+ logger.error('Failed to initialize codebase database', error);
54
+ throw error;
55
+ }
56
+ }
57
+ /**
58
+ * Create database tables
59
+ */
60
+ createTables() {
61
+ if (!this.db)
62
+ throw new Error('Database not initialized');
63
+ // Code chunks table with embeddings
64
+ this.db.exec(`
65
+ CREATE TABLE IF NOT EXISTS code_chunks (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ file_path TEXT NOT NULL,
68
+ content TEXT NOT NULL,
69
+ start_line INTEGER NOT NULL,
70
+ end_line INTEGER NOT NULL,
71
+ embedding BLOB NOT NULL,
72
+ file_hash TEXT NOT NULL,
73
+ created_at INTEGER NOT NULL,
74
+ updated_at INTEGER NOT NULL
75
+ );
76
+
77
+ CREATE INDEX IF NOT EXISTS idx_file_path ON code_chunks(file_path);
78
+ CREATE INDEX IF NOT EXISTS idx_file_hash ON code_chunks(file_hash);
79
+ `);
80
+ // Indexing progress table
81
+ this.db.exec(`
82
+ CREATE TABLE IF NOT EXISTS index_progress (
83
+ id INTEGER PRIMARY KEY CHECK (id = 1),
84
+ total_files INTEGER NOT NULL DEFAULT 0,
85
+ processed_files INTEGER NOT NULL DEFAULT 0,
86
+ total_chunks INTEGER NOT NULL DEFAULT 0,
87
+ status TEXT NOT NULL DEFAULT 'idle',
88
+ last_error TEXT,
89
+ last_processed_file TEXT,
90
+ started_at INTEGER,
91
+ completed_at INTEGER,
92
+ updated_at INTEGER NOT NULL,
93
+ watcher_enabled INTEGER NOT NULL DEFAULT 0
94
+ );
95
+
96
+ -- Initialize progress record if not exists
97
+ INSERT OR IGNORE INTO index_progress (id, updated_at) VALUES (1, ${Date.now()});
98
+ `);
99
+ }
100
+ /**
101
+ * Insert or update code chunks (batch operation)
102
+ */
103
+ insertChunks(chunks) {
104
+ if (!this.db)
105
+ throw new Error('Database not initialized');
106
+ const insert = this.db.prepare(`
107
+ INSERT INTO code_chunks (
108
+ file_path, content, start_line, end_line,
109
+ embedding, file_hash, created_at, updated_at
110
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
111
+ `);
112
+ const transaction = this.db.transaction((chunks) => {
113
+ for (const chunk of chunks) {
114
+ // Convert embedding array to Buffer for storage
115
+ const embeddingBuffer = Buffer.from(new Float32Array(chunk.embedding).buffer);
116
+ insert.run(chunk.filePath, chunk.content, chunk.startLine, chunk.endLine, embeddingBuffer, chunk.fileHash, chunk.createdAt, chunk.updatedAt);
117
+ }
118
+ });
119
+ transaction(chunks);
120
+ }
121
+ /**
122
+ * Delete chunks by file path
123
+ */
124
+ deleteChunksByFile(filePath) {
125
+ if (!this.db)
126
+ throw new Error('Database not initialized');
127
+ const stmt = this.db.prepare('DELETE FROM code_chunks WHERE file_path = ?');
128
+ stmt.run(filePath);
129
+ }
130
+ /**
131
+ * Get chunks by file path
132
+ */
133
+ getChunksByFile(filePath) {
134
+ if (!this.db)
135
+ throw new Error('Database not initialized');
136
+ const stmt = this.db.prepare('SELECT * FROM code_chunks WHERE file_path = ?');
137
+ const rows = stmt.all(filePath);
138
+ return rows.map(row => this.rowToChunk(row));
139
+ }
140
+ /**
141
+ * Check if file has been indexed by hash
142
+ */
143
+ hasFileHash(fileHash) {
144
+ if (!this.db)
145
+ throw new Error('Database not initialized');
146
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM code_chunks WHERE file_hash = ?');
147
+ const result = stmt.get(fileHash);
148
+ return result.count > 0;
149
+ }
150
+ /**
151
+ * Get total chunks count
152
+ */
153
+ getTotalChunks() {
154
+ if (!this.db)
155
+ throw new Error('Database not initialized');
156
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM code_chunks');
157
+ const result = stmt.get();
158
+ return result.count;
159
+ }
160
+ /**
161
+ * Search similar code chunks by embedding
162
+ * Uses cosine similarity
163
+ */
164
+ searchSimilar(queryEmbedding, limit = 10) {
165
+ if (!this.db)
166
+ throw new Error('Database not initialized');
167
+ // Get all chunks (in production, use approximate nearest neighbor)
168
+ const stmt = this.db.prepare('SELECT * FROM code_chunks');
169
+ const rows = stmt.all();
170
+ // Calculate cosine similarity for each chunk
171
+ const results = rows.map(row => {
172
+ const chunk = this.rowToChunk(row);
173
+ const similarity = this.cosineSimilarity(queryEmbedding, chunk.embedding);
174
+ return { chunk, similarity };
175
+ });
176
+ // Sort by similarity and return top N
177
+ results.sort((a, b) => b.similarity - a.similarity);
178
+ return results.slice(0, limit).map(r => r.chunk);
179
+ }
180
+ /**
181
+ * Update indexing progress
182
+ */
183
+ updateProgress(progress) {
184
+ if (!this.db || !this.initialized) {
185
+ // Silently ignore if database is not initialized
186
+ return;
187
+ }
188
+ const fields = [];
189
+ const values = [];
190
+ if (progress.totalFiles !== undefined) {
191
+ fields.push('total_files = ?');
192
+ values.push(progress.totalFiles);
193
+ }
194
+ if (progress.processedFiles !== undefined) {
195
+ fields.push('processed_files = ?');
196
+ values.push(progress.processedFiles);
197
+ }
198
+ if (progress.totalChunks !== undefined) {
199
+ fields.push('total_chunks = ?');
200
+ values.push(progress.totalChunks);
201
+ }
202
+ if (progress.status !== undefined) {
203
+ fields.push('status = ?');
204
+ values.push(progress.status);
205
+ }
206
+ if (progress.lastError !== undefined) {
207
+ fields.push('last_error = ?');
208
+ values.push(progress.lastError);
209
+ }
210
+ if (progress.lastProcessedFile !== undefined) {
211
+ fields.push('last_processed_file = ?');
212
+ values.push(progress.lastProcessedFile);
213
+ }
214
+ if (progress.startedAt !== undefined) {
215
+ fields.push('started_at = ?');
216
+ values.push(progress.startedAt);
217
+ }
218
+ if (progress.completedAt !== undefined) {
219
+ fields.push('completed_at = ?');
220
+ values.push(progress.completedAt);
221
+ }
222
+ fields.push('updated_at = ?');
223
+ values.push(Date.now());
224
+ const sql = `UPDATE index_progress SET ${fields.join(', ')} WHERE id = 1`;
225
+ this.db.prepare(sql).run(...values);
226
+ }
227
+ /**
228
+ * Get current indexing progress
229
+ */
230
+ getProgress() {
231
+ if (!this.db)
232
+ throw new Error('Database not initialized');
233
+ const stmt = this.db.prepare('SELECT * FROM index_progress WHERE id = 1');
234
+ const row = stmt.get();
235
+ return {
236
+ totalFiles: row.total_files,
237
+ processedFiles: row.processed_files,
238
+ totalChunks: row.total_chunks,
239
+ status: row.status,
240
+ lastError: row.last_error,
241
+ lastProcessedFile: row.last_processed_file,
242
+ startedAt: row.started_at,
243
+ completedAt: row.completed_at,
244
+ };
245
+ }
246
+ /**
247
+ * Set watcher enabled status
248
+ */
249
+ setWatcherEnabled(enabled) {
250
+ if (!this.db)
251
+ throw new Error('Database not initialized');
252
+ this.db
253
+ .prepare('UPDATE index_progress SET watcher_enabled = ? WHERE id = 1')
254
+ .run(enabled ? 1 : 0);
255
+ }
256
+ /**
257
+ * Get watcher enabled status
258
+ */
259
+ isWatcherEnabled() {
260
+ if (!this.db)
261
+ throw new Error('Database not initialized');
262
+ const stmt = this.db.prepare('SELECT watcher_enabled FROM index_progress WHERE id = 1');
263
+ const result = stmt.get();
264
+ return result.watcher_enabled === 1;
265
+ }
266
+ /**
267
+ * Clear all chunks and reset progress
268
+ */
269
+ clear() {
270
+ if (!this.db)
271
+ throw new Error('Database not initialized');
272
+ this.db.exec('DELETE FROM code_chunks');
273
+ this.db.exec(`
274
+ UPDATE index_progress
275
+ SET total_files = 0,
276
+ processed_files = 0,
277
+ total_chunks = 0,
278
+ status = 'idle',
279
+ last_error = NULL,
280
+ last_processed_file = NULL,
281
+ started_at = NULL,
282
+ completed_at = NULL,
283
+ updated_at = ${Date.now()}
284
+ WHERE id = 1
285
+ `);
286
+ }
287
+ /**
288
+ * Close database connection
289
+ */
290
+ close() {
291
+ if (this.db) {
292
+ this.db.close();
293
+ this.db = null;
294
+ this.initialized = false;
295
+ }
296
+ }
297
+ /**
298
+ * Convert database row to CodeChunk
299
+ */
300
+ rowToChunk(row) {
301
+ // Convert Buffer back to number array
302
+ const embeddingBuffer = row.embedding;
303
+ const embedding = Array.from(new Float32Array(embeddingBuffer.buffer, embeddingBuffer.byteOffset, embeddingBuffer.byteLength / 4));
304
+ return {
305
+ id: row.id,
306
+ filePath: row.file_path,
307
+ content: row.content,
308
+ startLine: row.start_line,
309
+ endLine: row.end_line,
310
+ embedding,
311
+ fileHash: row.file_hash,
312
+ createdAt: row.created_at,
313
+ updatedAt: row.updated_at,
314
+ };
315
+ }
316
+ /**
317
+ * Calculate cosine similarity between two vectors
318
+ */
319
+ cosineSimilarity(a, b) {
320
+ if (a.length !== b.length) {
321
+ throw new Error('Vectors must have same length');
322
+ }
323
+ let dotProduct = 0;
324
+ let normA = 0;
325
+ let normB = 0;
326
+ for (let i = 0; i < a.length; i++) {
327
+ dotProduct += a[i] * b[i];
328
+ normA += a[i] * a[i];
329
+ normB += b[i] * b[i];
330
+ }
331
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
332
+ }
333
+ }
@@ -5,7 +5,20 @@ import { resetOpenAIClient as resetChatClient } from '../../api/chat.js';
5
5
  import { resetOpenAIClient as resetResponseClient } from '../../api/responses.js';
6
6
  // Home command handler - returns to welcome screen
7
7
  registerCommand('home', {
8
- execute: () => {
8
+ execute: async () => {
9
+ // Stop codebase indexing if running (to avoid database errors)
10
+ if (global.__stopCodebaseIndexing) {
11
+ try {
12
+ // Show stopping message
13
+ console.log('\n⏸ Pausing codebase indexing...');
14
+ await global.__stopCodebaseIndexing();
15
+ console.log('✓ Indexing paused, progress saved\n');
16
+ }
17
+ catch (error) {
18
+ // Ignore errors during stop
19
+ console.error('Failed to stop codebase indexing:', error);
20
+ }
21
+ }
9
22
  // Clear all API configuration caches
10
23
  resetAnthropicClient();
11
24
  resetGeminiClient();
@@ -8,6 +8,7 @@ import { mcpTools as terminalTools } from '../mcp/bash.js';
8
8
  import { mcpTools as aceCodeSearchTools } from '../mcp/aceCodeSearch.js';
9
9
  import { mcpTools as websearchTools } from '../mcp/websearch.js';
10
10
  import { mcpTools as ideDiagnosticsTools } from '../mcp/ideDiagnostics.js';
11
+ import { mcpTools as codebaseSearchTools } from '../mcp/codebaseSearch.js';
11
12
  import { TodoService } from '../mcp/todo.js';
12
13
  import { mcpTools as notebookTools, executeNotebookTool, } from '../mcp/notebook.js';
13
14
  import { getMCPTools as getSubAgentTools, subAgentService, } from '../mcp/subagent.js';
@@ -32,13 +33,17 @@ export function getTodoService() {
32
33
  /**
33
34
  * Generate a hash of the current MCP configuration and sub-agents
34
35
  */
35
- function generateConfigHash() {
36
+ async function generateConfigHash() {
36
37
  try {
37
38
  const mcpConfig = getMCPConfig();
38
39
  const subAgents = getSubAgentTools(); // Include sub-agents in hash
40
+ // 🔥 CRITICAL: Include codebase enabled status in hash
41
+ const { loadCodebaseConfig } = await import('./codebaseConfig.js');
42
+ const codebaseConfig = loadCodebaseConfig();
39
43
  return JSON.stringify({
40
44
  mcpServers: mcpConfig.mcpServers,
41
45
  subAgents: subAgents.map(t => t.name), // Only track agent names for hash
46
+ codebaseEnabled: codebaseConfig.enabled, // 🔥 Must include to invalidate cache on enable/disable
42
47
  });
43
48
  }
44
49
  catch {
@@ -48,19 +53,20 @@ function generateConfigHash() {
48
53
  /**
49
54
  * Check if the cache is valid and not expired
50
55
  */
51
- function isCacheValid() {
56
+ async function isCacheValid() {
52
57
  if (!toolsCache)
53
58
  return false;
54
59
  const now = Date.now();
55
60
  const isExpired = now - toolsCache.lastUpdate > CACHE_DURATION;
56
- const configChanged = toolsCache.configHash !== generateConfigHash();
61
+ const configHash = await generateConfigHash();
62
+ const configChanged = toolsCache.configHash !== configHash;
57
63
  return !isExpired && !configChanged;
58
64
  }
59
65
  /**
60
66
  * Get cached tools or build cache if needed
61
67
  */
62
68
  async function getCachedTools() {
63
- if (isCacheValid()) {
69
+ if (await isCacheValid()) {
64
70
  return toolsCache.tools;
65
71
  }
66
72
  await refreshToolsCache();
@@ -248,6 +254,54 @@ async function refreshToolsCache() {
248
254
  });
249
255
  }
250
256
  }
257
+ // Add built-in Codebase Search tools (conditionally loaded if enabled and index is available)
258
+ try {
259
+ // First check if codebase feature is enabled in config
260
+ const { loadCodebaseConfig } = await import('./codebaseConfig.js');
261
+ const codebaseConfig = loadCodebaseConfig();
262
+ // Only proceed if feature is enabled
263
+ if (codebaseConfig.enabled) {
264
+ const projectRoot = process.cwd();
265
+ const dbPath = path.join(projectRoot, '.snow', 'codebase', 'embeddings.db');
266
+ const fs = await import('node:fs');
267
+ // Only add if database file exists
268
+ if (fs.existsSync(dbPath)) {
269
+ // Check if database has data by importing CodebaseDatabase
270
+ const { CodebaseDatabase } = await import('./codebaseDatabase.js');
271
+ const db = new CodebaseDatabase(projectRoot);
272
+ db.initialize();
273
+ const totalChunks = db.getTotalChunks();
274
+ db.close();
275
+ if (totalChunks > 0) {
276
+ const codebaseSearchServiceTools = codebaseSearchTools.map(tool => ({
277
+ name: tool.name.replace('codebase-', ''),
278
+ description: tool.description,
279
+ inputSchema: tool.inputSchema,
280
+ }));
281
+ servicesInfo.push({
282
+ serviceName: 'codebase',
283
+ tools: codebaseSearchServiceTools,
284
+ isBuiltIn: true,
285
+ connected: true,
286
+ });
287
+ for (const tool of codebaseSearchTools) {
288
+ allTools.push({
289
+ type: 'function',
290
+ function: {
291
+ name: tool.name,
292
+ description: tool.description,
293
+ parameters: tool.inputSchema,
294
+ },
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ catch (error) {
302
+ // Silently ignore if codebase search tools are not available
303
+ logger.debug('Codebase search tools not available:', error);
304
+ }
251
305
  // Add user-configured MCP server tools (probe for availability but don't maintain connections)
252
306
  try {
253
307
  const mcpConfig = getMCPConfig();
@@ -290,7 +344,7 @@ async function refreshToolsCache() {
290
344
  tools: allTools,
291
345
  servicesInfo,
292
346
  lastUpdate: Date.now(),
293
- configHash: generateConfigHash(),
347
+ configHash: await generateConfigHash(),
294
348
  };
295
349
  }
296
350
  /**
@@ -316,6 +370,7 @@ export async function reconnectMCPService(serviceName) {
316
370
  serviceName === 'todo' ||
317
371
  serviceName === 'ace' ||
318
372
  serviceName === 'websearch' ||
373
+ serviceName === 'codebase' ||
319
374
  serviceName === 'subagent') {
320
375
  return;
321
376
  }
@@ -390,7 +445,8 @@ export async function getMCPServicesInfo() {
390
445
  if (!isCacheValid()) {
391
446
  await refreshToolsCache();
392
447
  }
393
- return toolsCache.servicesInfo;
448
+ // Ensure toolsCache is not null before accessing
449
+ return toolsCache?.servicesInfo || [];
394
450
  }
395
451
  /**
396
452
  * Quick probe of MCP service tools without maintaining connections
@@ -603,6 +659,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
603
659
  serviceName = 'ide';
604
660
  actualToolName = toolName.substring('ide-'.length);
605
661
  }
662
+ else if (toolName.startsWith('codebase-')) {
663
+ serviceName = 'codebase';
664
+ actualToolName = toolName.substring('codebase-'.length);
665
+ }
606
666
  else if (toolName.startsWith('subagent-')) {
607
667
  serviceName = 'subagent';
608
668
  actualToolName = toolName.substring('subagent-'.length);
@@ -690,11 +750,6 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
690
750
  return await aceCodeSearchService.getFileOutline(args.filePath);
691
751
  case 'text_search':
692
752
  return await aceCodeSearchService.textSearch(args.pattern, args.fileGlob, args.isRegex, args.maxResults);
693
- case 'index_stats':
694
- return aceCodeSearchService.getIndexStats();
695
- case 'clear_cache':
696
- aceCodeSearchService.clearCache();
697
- return { message: 'ACE Code Search cache cleared successfully' };
698
753
  default:
699
754
  throw new Error(`Unknown ACE tool: ${actualToolName}`);
700
755
  }
@@ -734,6 +789,16 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
734
789
  throw new Error(`Unknown IDE tool: ${actualToolName}`);
735
790
  }
736
791
  }
792
+ else if (serviceName === 'codebase') {
793
+ // Handle built-in Codebase Search tools (no connection needed)
794
+ const { codebaseSearchService } = await import('../mcp/codebaseSearch.js');
795
+ switch (actualToolName) {
796
+ case 'search':
797
+ return await codebaseSearchService.search(args.query, args.topN);
798
+ default:
799
+ throw new Error(`Unknown codebase tool: ${actualToolName}`);
800
+ }
801
+ }
737
802
  else if (serviceName === 'subagent') {
738
803
  // Handle sub-agent tools
739
804
  // actualToolName is the agent ID
@@ -13,6 +13,8 @@ const TWO_STEP_TOOLS = new Set([
13
13
  'filesystem-create',
14
14
  // 终端执行工具 - 执行时间不确定,需要显示进度
15
15
  'terminal-execute',
16
+ // 代码库搜索工具 - 需要生成 embedding 和搜索,耗时较长
17
+ 'codebase-search',
16
18
  // 子代理工具 - 执行复杂任务,需要显示进度
17
19
  // 所有以 'subagent-' 开头的工具都需要两步显示
18
20
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.31",
3
+ "version": "0.3.33",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -41,12 +41,15 @@
41
41
  "dependencies": {
42
42
  "@inkjs/ui": "^2.0.0",
43
43
  "@modelcontextprotocol/sdk": "^1.17.3",
44
+ "@types/better-sqlite3": "^7.6.13",
45
+ "better-sqlite3": "^12.4.1",
44
46
  "cli-highlight": "^2.1.11",
45
47
  "cli-markdown": "^3.5.1",
46
48
  "diff": "^8.0.2",
47
49
  "fzf": "^0.5.2",
48
50
  "http-proxy-agent": "^7.0.2",
49
51
  "https-proxy-agent": "^7.0.6",
52
+ "ignore": "^7.0.5",
50
53
  "ink": "^5.2.1",
51
54
  "ink-gradient": "^3.0.0",
52
55
  "ink-select-input": "^6.2.0",