snow-ai 0.3.28 โ†’ 0.3.30

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.
@@ -72,6 +72,7 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
72
72
  2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking
73
73
  3. **Smart Context**: Read what's needed for correctness, skip excessive exploration
74
74
  4. **Quality Verification**: run build/test after changes
75
+ 5. **NO Documentation Files**: โŒ NEVER create summary .md files after tasks - use \`notebook-add\` for important notes instead
75
76
 
76
77
  ## ๐Ÿš€ Execution Strategy - BALANCE ACTION & ANALYSIS
77
78
 
@@ -166,6 +167,16 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
166
167
  - Requires IDE plugin installed and running
167
168
  - Use AFTER code changes to verify quality
168
169
 
170
+ **Notebook (Code Memory):**
171
+ - \`notebook-add\` - Record fragile code that new features might break during iteration
172
+ - ๐ŸŽฏ Core purpose: Prevent new functionality from breaking old functionality
173
+ - ๐Ÿ“ Record: Bugs that recurred, fragile dependencies, critical constraints
174
+ - โš ๏ธ Examples: "validateInput() must run first - broke twice", "null return required by X"
175
+ - ๐Ÿ“Œ **IMPORTANT**: Use notebook for documentation, NOT separate .md files
176
+ - \`notebook-query\` - Manual search (rarely needed, auto-shown when reading files)
177
+ - ๐Ÿ” Auto-attached: Last 10 notebooks appear when reading ANY file
178
+ - ๐Ÿ’ก Use before: Adding features that might affect existing behavior
179
+
169
180
  **Web Search:**
170
181
  - \`websearch-search\` - Search web for latest docs/solutions
171
182
  - \`websearch-fetch\` - Read web page content (always provide userQuery)
@@ -154,14 +154,9 @@ export function useCommandHandler(options) {
154
154
  // Handle /ide command
155
155
  if (commandName === 'ide') {
156
156
  if (result.success) {
157
- // If already connected, set status to connected immediately
158
- // Otherwise, set to connecting and wait for VSCode extension
159
- if (result.alreadyConnected) {
160
- options.setVscodeConnectionStatus('connected');
161
- }
162
- else {
163
- options.setVscodeConnectionStatus('connecting');
164
- }
157
+ // Connection successful, set status to connected immediately
158
+ // The่ฝฎ่ฏข mechanism will also update the status, but we do it here for immediate feedback
159
+ options.setVscodeConnectionStatus('connected');
165
160
  // Don't add command message to keep UI clean
166
161
  }
167
162
  else {
@@ -515,8 +515,10 @@ export async function handleConversationWithTools(options) {
515
515
  if (subAgentMessage.message.type === 'tool_calls') {
516
516
  const toolCalls = subAgentMessage.message.tool_calls;
517
517
  if (toolCalls && toolCalls.length > 0) {
518
- // Add tool call messages for each tool
519
- const toolMessages = toolCalls.map((toolCall) => {
518
+ // Add tool call messages for each tool (only for time-consuming tools)
519
+ const toolMessages = toolCalls
520
+ .filter((toolCall) => isToolNeedTwoStepDisplay(toolCall.function.name))
521
+ .map((toolCall) => {
520
522
  const toolDisplay = formatToolCallMessage(toolCall);
521
523
  let toolArgs;
522
524
  try {
@@ -543,16 +545,21 @@ export async function handleConversationWithTools(options) {
543
545
  },
544
546
  subAgentInternal: true, // Mark as internal sub-agent message
545
547
  };
546
- // Save to session as 'assistant' role for API compatibility
547
- const sessionMsg = {
548
- role: 'assistant',
549
- content: `โš‡โšก ${toolDisplay.toolName}`,
550
- subAgentInternal: true,
551
- tool_calls: [toolCall],
552
- };
553
- saveMessage(sessionMsg).catch(err => console.error('Failed to save sub-agent tool call:', err));
554
548
  return uiMsg;
555
549
  });
550
+ // Save all tool calls to session (regardless of display type)
551
+ const sessionMsg = {
552
+ role: 'assistant',
553
+ content: toolCalls
554
+ .map((tc) => {
555
+ const display = formatToolCallMessage(tc);
556
+ return `โš‡โšก ${display.toolName}`;
557
+ })
558
+ .join(', '),
559
+ subAgentInternal: true,
560
+ tool_calls: toolCalls,
561
+ };
562
+ saveMessage(sessionMsg).catch(err => console.error('Failed to save sub-agent tool call:', err));
556
563
  return [...prev, ...toolMessages];
557
564
  }
558
565
  }
@@ -23,7 +23,7 @@ export function useVSCodeState() {
23
23
  lastStatusRef.current = 'disconnected';
24
24
  setVscodeConnectionStatus('disconnected');
25
25
  }
26
- }, 1000);
26
+ }, 1000); // Check every second
27
27
  const unsubscribe = vscodeConnection.onContextUpdate(context => {
28
28
  // Only update state if context has actually changed
29
29
  const hasChanged = context.activeFile !== lastEditorContextRef.current.activeFile ||
@@ -51,7 +51,7 @@ export function useVSCodeState() {
51
51
  if (vscodeConnectionStatus !== 'connecting') {
52
52
  return;
53
53
  }
54
- // Set timeout for connecting state (30 seconds to allow for IDE plugin reconnection)
54
+ // Set timeout for connecting state (15 seconds to allow for port scanning and connection)
55
55
  const connectingTimeout = setTimeout(() => {
56
56
  const isConnected = vscodeConnection.isConnected();
57
57
  const isClientRunning = vscodeConnection.isClientRunning();
@@ -65,8 +65,9 @@ export function useVSCodeState() {
65
65
  // Client not running - go back to disconnected
66
66
  setVscodeConnectionStatus('disconnected');
67
67
  }
68
+ lastStatusRef.current = isClientRunning ? 'error' : 'disconnected';
68
69
  }
69
- }, 30000); // Increased to 30 seconds
70
+ }, 15000); // 15 seconds: 10s for connection timeout + 5s buffer
70
71
  return () => {
71
72
  clearTimeout(connectingTimeout);
72
73
  };
@@ -10,8 +10,25 @@ export declare class FilesystemMCPService {
10
10
  */
11
11
  private readonly prettierSupportedExtensions;
12
12
  constructor(basePath?: string);
13
+ /**
14
+ * Extract relevant symbol information for a specific line range
15
+ * This provides context that helps AI make more accurate modifications
16
+ * @param symbols - All symbols in the file
17
+ * @param startLine - Start line of the range
18
+ * @param endLine - End line of the range
19
+ * @param _totalLines - Total lines in the file (reserved for future use)
20
+ * @returns Formatted string with relevant symbol information
21
+ */
22
+ private extractRelevantSymbols;
23
+ /**
24
+ * Get notebook entries for a file
25
+ * @param filePath - Path to the file
26
+ * @returns Formatted notebook entries string, or empty if none found
27
+ */
28
+ private getNotebookEntries;
13
29
  /**
14
30
  * Get the content of a file with optional line range
31
+ * Enhanced with symbol information for better AI context
15
32
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
16
33
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
17
34
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
@@ -11,6 +11,10 @@ import { calculateSimilarity, normalizeForDisplay, } from './utils/filesystem/si
11
11
  import { analyzeCodeStructure, findSmartContextBoundaries, } from './utils/filesystem/code-analysis.utils.js';
12
12
  import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/match-finder.utils.js';
13
13
  import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
14
+ // ACE Code Search utilities for symbol parsing
15
+ import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
16
+ // Notebook utilities for automatic note retrieval
17
+ import { queryNotebook } from '../utils/notebookManager.js';
14
18
  const { resolve, dirname, isAbsolute } = path;
15
19
  const execAsync = promisify(exec);
16
20
  /**
@@ -52,8 +56,107 @@ export class FilesystemMCPService {
52
56
  });
53
57
  this.basePath = resolve(basePath);
54
58
  }
59
+ /**
60
+ * Extract relevant symbol information for a specific line range
61
+ * This provides context that helps AI make more accurate modifications
62
+ * @param symbols - All symbols in the file
63
+ * @param startLine - Start line of the range
64
+ * @param endLine - End line of the range
65
+ * @param _totalLines - Total lines in the file (reserved for future use)
66
+ * @returns Formatted string with relevant symbol information
67
+ */
68
+ extractRelevantSymbols(symbols, startLine, endLine, _totalLines) {
69
+ if (symbols.length === 0) {
70
+ return '';
71
+ }
72
+ // Categorize symbols
73
+ const imports = symbols.filter(s => s.type === 'import');
74
+ const exports = symbols.filter(s => s.type === 'export');
75
+ // Symbols within the requested range
76
+ const symbolsInRange = symbols.filter(s => s.line >= startLine && s.line <= endLine);
77
+ // Symbols defined before the range that might be referenced
78
+ const symbolsBeforeRange = symbols.filter(s => s.line < startLine);
79
+ // Build context information
80
+ const parts = [];
81
+ // Always include imports (crucial for understanding dependencies)
82
+ if (imports.length > 0) {
83
+ const importList = imports
84
+ .slice(0, 10) // Limit to avoid excessive tokens
85
+ .map(s => ` โ€ข ${s.name} (line ${s.line})`)
86
+ .join('\n');
87
+ parts.push(`๐Ÿ“ฆ Imports:\n${importList}`);
88
+ }
89
+ // Symbols defined in the current range
90
+ if (symbolsInRange.length > 0) {
91
+ const rangeSymbols = symbolsInRange
92
+ .slice(0, 15)
93
+ .map(s => ` โ€ข ${s.type}: ${s.name} (line ${s.line})${s.signature ? ` - ${s.signature.slice(0, 60)}` : ''}`)
94
+ .join('\n');
95
+ parts.push(`๐ŸŽฏ Symbols in this range:\n${rangeSymbols}`);
96
+ }
97
+ // Key definitions before this range (that might be referenced)
98
+ if (symbolsBeforeRange.length > 0 && startLine > 1) {
99
+ const relevantBefore = symbolsBeforeRange
100
+ .filter(s => s.type === 'function' || s.type === 'class')
101
+ .slice(-5) // Last 5 before the range
102
+ .map(s => ` โ€ข ${s.type}: ${s.name} (line ${s.line})`)
103
+ .join('\n');
104
+ if (relevantBefore) {
105
+ parts.push(`โฌ†๏ธ Key definitions above:\n${relevantBefore}`);
106
+ }
107
+ }
108
+ // Exports (important for understanding module interface)
109
+ if (exports.length > 0) {
110
+ const exportList = exports
111
+ .slice(0, 10)
112
+ .map(s => ` โ€ข ${s.name} (line ${s.line})`)
113
+ .join('\n');
114
+ parts.push(`๐Ÿ“ค Exports:\n${exportList}`);
115
+ }
116
+ if (parts.length === 0) {
117
+ return '';
118
+ }
119
+ return ('\n\n' +
120
+ '='.repeat(60) +
121
+ '\n๐Ÿ“š SYMBOL INDEX & DEFINITIONS:\n' +
122
+ '='.repeat(60) +
123
+ '\n' +
124
+ parts.join('\n\n'));
125
+ }
126
+ /**
127
+ * Get notebook entries for a file
128
+ * @param filePath - Path to the file
129
+ * @returns Formatted notebook entries string, or empty if none found
130
+ */
131
+ getNotebookEntries(filePath) {
132
+ try {
133
+ const entries = queryNotebook(filePath, 10);
134
+ if (entries.length === 0) {
135
+ return '';
136
+ }
137
+ const notesText = entries
138
+ .map((entry, index) => {
139
+ // createdAt ๅทฒ็ปๆ˜ฏๆœฌๅœฐๆ—ถ้—ดๆ ผๅผ: "YYYY-MM-DDTHH:mm:ss.SSS"
140
+ // ๆๅ–ๆ—ฅๆœŸๅ’Œๆ—ถ้—ด้ƒจๅˆ†: "YYYY-MM-DD HH:mm"
141
+ const dateStr = entry.createdAt.substring(0, 16).replace('T', ' ');
142
+ return ` ${index + 1}. [${dateStr}] ${entry.note}`;
143
+ })
144
+ .join('\n');
145
+ return ('\n\n' +
146
+ '='.repeat(60) +
147
+ '\n๐Ÿ“ CODE NOTEBOOKS (Latest 10):\n' +
148
+ '='.repeat(60) +
149
+ '\n' +
150
+ notesText);
151
+ }
152
+ catch {
153
+ // Silently fail notebook retrieval - don't block file reading
154
+ return '';
155
+ }
156
+ }
55
157
  /**
56
158
  * Get the content of a file with optional line range
159
+ * Enhanced with symbol information for better AI context
57
160
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
58
161
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
59
162
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
@@ -127,7 +230,23 @@ export class FilesystemMCPService {
127
230
  const lineNum = start + index;
128
231
  return `${lineNum}โ†’${line}`;
129
232
  });
130
- const fileContent = `๐Ÿ“„ ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
233
+ let fileContent = `๐Ÿ“„ ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
234
+ // Parse and append symbol information
235
+ try {
236
+ const symbols = await parseFileSymbols(fullPath, content, this.basePath);
237
+ const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
238
+ if (symbolInfo) {
239
+ fileContent += symbolInfo;
240
+ }
241
+ }
242
+ catch {
243
+ // Silently fail symbol parsing
244
+ }
245
+ // Append notebook entries
246
+ const notebookInfo = this.getNotebookEntries(file);
247
+ if (notebookInfo) {
248
+ fileContent += notebookInfo;
249
+ }
131
250
  allContents.push(fileContent);
132
251
  filesData.push({
133
252
  path: file,
@@ -197,7 +316,24 @@ export class FilesystemMCPService {
197
316
  const lineNum = start + index;
198
317
  return `${lineNum}โ†’${line}`;
199
318
  });
200
- const partialContent = numberedLines.join('\n');
319
+ let partialContent = numberedLines.join('\n');
320
+ // Parse and append symbol information to provide better context for AI
321
+ try {
322
+ const symbols = await parseFileSymbols(fullPath, content, this.basePath);
323
+ const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
324
+ if (symbolInfo) {
325
+ partialContent += symbolInfo;
326
+ }
327
+ }
328
+ catch (error) {
329
+ // Silently fail symbol parsing - don't block file reading
330
+ // This is optional context enhancement, not critical
331
+ }
332
+ // Append notebook entries
333
+ const notebookInfo = this.getNotebookEntries(filePath);
334
+ if (notebookInfo) {
335
+ partialContent += notebookInfo;
336
+ }
201
337
  return {
202
338
  content: partialContent,
203
339
  startLine: start,
@@ -0,0 +1,10 @@
1
+ import { Tool, type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ /**
3
+ * Notebook MCP ๅทฅๅ…ทๅฎšไน‰
4
+ * ็”จไบŽไปฃ็ ๅค‡ๅฟ˜ๅฝ•็ฎก็†๏ผŒๅธฎๅŠฉAI่ฎฐๅฝ•้‡่ฆ็š„ไปฃ็ ๆณจๆ„ไบ‹้กน
5
+ */
6
+ export declare const mcpTools: Tool[];
7
+ /**
8
+ * ๆ‰ง่กŒ Notebook ๅทฅๅ…ท
9
+ */
10
+ export declare function executeNotebookTool(toolName: string, args: any): Promise<CallToolResult>;
@@ -0,0 +1,161 @@
1
+ import { addNotebook, queryNotebook } from '../utils/notebookManager.js';
2
+ /**
3
+ * Notebook MCP ๅทฅๅ…ทๅฎšไน‰
4
+ * ็”จไบŽไปฃ็ ๅค‡ๅฟ˜ๅฝ•็ฎก็†๏ผŒๅธฎๅŠฉAI่ฎฐๅฝ•้‡่ฆ็š„ไปฃ็ ๆณจๆ„ไบ‹้กน
5
+ */
6
+ export const mcpTools = [
7
+ {
8
+ name: 'notebook-add',
9
+ description: `๐Ÿ“ Record code parts that are fragile and easily broken during iteration.
10
+
11
+ **Core Purpose:** Prevent new features from breaking existing functionality.
12
+
13
+ **When to record:**
14
+ - After fixing bugs that could easily reoccur
15
+ - Fragile code that new features might break
16
+ - Non-obvious dependencies between components
17
+ - Workarounds that shouldn't be "optimized away"
18
+
19
+ **Examples:**
20
+ - "โš ๏ธ validateInput() MUST be called first - new features broke this twice"
21
+ - "Component X depends on null return - DO NOT change to empty array"
22
+ - "setTimeout workaround for race condition - don't remove"
23
+ - "Parser expects exact format - adding fields breaks backward compat"`,
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ filePath: {
28
+ type: 'string',
29
+ description: 'File path (relative or absolute). Example: "src/utils/parser.ts"',
30
+ },
31
+ note: {
32
+ type: 'string',
33
+ description: 'Brief, specific note. Focus on risks/constraints, NOT what code does.',
34
+ },
35
+ },
36
+ required: ['filePath', 'note'],
37
+ },
38
+ },
39
+ {
40
+ name: 'notebook-query',
41
+ description: `๐Ÿ” Search notebook entries by file path pattern.
42
+
43
+ **Auto-triggered:** When reading files, last 10 notebooks are automatically shown.
44
+ **Manual use:** Query specific patterns or see more entries.`,
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ filePathPattern: {
49
+ type: 'string',
50
+ description: 'Fuzzy search pattern (e.g., "parser"). Empty = all entries.',
51
+ default: '',
52
+ },
53
+ topN: {
54
+ type: 'number',
55
+ description: 'Max results to return (default: 10, max: 50)',
56
+ default: 10,
57
+ minimum: 1,
58
+ maximum: 50,
59
+ },
60
+ },
61
+ },
62
+ },
63
+ ];
64
+ /**
65
+ * ๆ‰ง่กŒ Notebook ๅทฅๅ…ท
66
+ */
67
+ export async function executeNotebookTool(toolName, args) {
68
+ try {
69
+ switch (toolName) {
70
+ case 'notebook-add': {
71
+ const { filePath, note } = args;
72
+ if (!filePath || !note) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: 'Error: Both filePath and note are required',
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ const entry = addNotebook(filePath, note);
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: JSON.stringify({
89
+ success: true,
90
+ message: `Notebook entry added for: ${entry.filePath}`,
91
+ entry: {
92
+ id: entry.id,
93
+ filePath: entry.filePath,
94
+ note: entry.note,
95
+ createdAt: entry.createdAt,
96
+ },
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ case 'notebook-query': {
103
+ const { filePathPattern = '', topN = 10 } = args;
104
+ const results = queryNotebook(filePathPattern, topN);
105
+ if (results.length === 0) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: JSON.stringify({
111
+ message: 'No notebook entries found',
112
+ pattern: filePathPattern || '(all)',
113
+ totalResults: 0,
114
+ }, null, 2),
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: JSON.stringify({
124
+ message: `Found ${results.length} notebook entries`,
125
+ pattern: filePathPattern || '(all)',
126
+ totalResults: results.length,
127
+ entries: results.map(entry => ({
128
+ id: entry.id,
129
+ filePath: entry.filePath,
130
+ note: entry.note,
131
+ createdAt: entry.createdAt,
132
+ })),
133
+ }, null, 2),
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ default:
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Unknown notebook tool: ${toolName}`,
144
+ },
145
+ ],
146
+ isError: true,
147
+ };
148
+ }
149
+ }
150
+ catch (error) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: 'text',
155
+ text: `Error executing notebook tool: ${error instanceof Error ? error.message : String(error)}`,
156
+ },
157
+ ],
158
+ isError: true,
159
+ };
160
+ }
161
+ }