snow-ai 0.3.29 → 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.
- package/dist/api/systemPrompt.js +11 -0
- package/dist/hooks/useCommandHandler.js +3 -8
- package/dist/hooks/useVSCodeState.js +4 -3
- package/dist/mcp/filesystem.d.ts +6 -0
- package/dist/mcp/filesystem.js +43 -0
- package/dist/mcp/notebook.d.ts +10 -0
- package/dist/mcp/notebook.js +161 -0
- package/dist/utils/commands/ide.js +2 -0
- package/dist/utils/mcpToolsManager.js +31 -0
- package/dist/utils/notebookManager.d.ts +52 -0
- package/dist/utils/notebookManager.js +181 -0
- package/dist/utils/vscodeConnection.js +38 -18
- package/package.json +1 -1
package/dist/api/systemPrompt.js
CHANGED
|
@@ -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
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
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 {
|
|
@@ -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 (
|
|
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
|
-
},
|
|
70
|
+
}, 15000); // 15 seconds: 10s for connection timeout + 5s buffer
|
|
70
71
|
return () => {
|
|
71
72
|
clearTimeout(connectingTimeout);
|
|
72
73
|
};
|
package/dist/mcp/filesystem.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export declare class FilesystemMCPService {
|
|
|
20
20
|
* @returns Formatted string with relevant symbol information
|
|
21
21
|
*/
|
|
22
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;
|
|
23
29
|
/**
|
|
24
30
|
* Get the content of a file with optional line range
|
|
25
31
|
* Enhanced with symbol information for better AI context
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -13,6 +13,8 @@ import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/mat
|
|
|
13
13
|
import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
|
|
14
14
|
// ACE Code Search utilities for symbol parsing
|
|
15
15
|
import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
|
|
16
|
+
// Notebook utilities for automatic note retrieval
|
|
17
|
+
import { queryNotebook } from '../utils/notebookManager.js';
|
|
16
18
|
const { resolve, dirname, isAbsolute } = path;
|
|
17
19
|
const execAsync = promisify(exec);
|
|
18
20
|
/**
|
|
@@ -121,6 +123,37 @@ export class FilesystemMCPService {
|
|
|
121
123
|
'\n' +
|
|
122
124
|
parts.join('\n\n'));
|
|
123
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
|
+
}
|
|
124
157
|
/**
|
|
125
158
|
* Get the content of a file with optional line range
|
|
126
159
|
* Enhanced with symbol information for better AI context
|
|
@@ -209,6 +242,11 @@ export class FilesystemMCPService {
|
|
|
209
242
|
catch {
|
|
210
243
|
// Silently fail symbol parsing
|
|
211
244
|
}
|
|
245
|
+
// Append notebook entries
|
|
246
|
+
const notebookInfo = this.getNotebookEntries(file);
|
|
247
|
+
if (notebookInfo) {
|
|
248
|
+
fileContent += notebookInfo;
|
|
249
|
+
}
|
|
212
250
|
allContents.push(fileContent);
|
|
213
251
|
filesData.push({
|
|
214
252
|
path: file,
|
|
@@ -291,6 +329,11 @@ export class FilesystemMCPService {
|
|
|
291
329
|
// Silently fail symbol parsing - don't block file reading
|
|
292
330
|
// This is optional context enhancement, not critical
|
|
293
331
|
}
|
|
332
|
+
// Append notebook entries
|
|
333
|
+
const notebookInfo = this.getNotebookEntries(filePath);
|
|
334
|
+
if (notebookInfo) {
|
|
335
|
+
partialContent += notebookInfo;
|
|
336
|
+
}
|
|
294
337
|
return {
|
|
295
338
|
content: partialContent,
|
|
296
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
|
+
}
|
|
@@ -7,6 +7,8 @@ registerCommand('ide', {
|
|
|
7
7
|
if (vscodeConnection.isConnected()) {
|
|
8
8
|
vscodeConnection.stop();
|
|
9
9
|
vscodeConnection.resetReconnectAttempts();
|
|
10
|
+
// Wait a bit for cleanup to complete
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
10
12
|
}
|
|
11
13
|
// Try to connect to IDE plugin server
|
|
12
14
|
try {
|
|
@@ -9,6 +9,7 @@ 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
11
|
import { TodoService } from '../mcp/todo.js';
|
|
12
|
+
import { mcpTools as notebookTools, executeNotebookTool, } from '../mcp/notebook.js';
|
|
12
13
|
import { getMCPTools as getSubAgentTools, subAgentService, } from '../mcp/subagent.js';
|
|
13
14
|
import { sessionManager } from './sessionManager.js';
|
|
14
15
|
import { logger } from './logger.js';
|
|
@@ -139,6 +140,28 @@ async function refreshToolsCache() {
|
|
|
139
140
|
},
|
|
140
141
|
});
|
|
141
142
|
}
|
|
143
|
+
// Add built-in Notebook tools (always available)
|
|
144
|
+
const notebookServiceTools = notebookTools.map(tool => ({
|
|
145
|
+
name: tool.name.replace('notebook-', ''),
|
|
146
|
+
description: tool.description || '',
|
|
147
|
+
inputSchema: tool.inputSchema,
|
|
148
|
+
}));
|
|
149
|
+
servicesInfo.push({
|
|
150
|
+
serviceName: 'notebook',
|
|
151
|
+
tools: notebookServiceTools,
|
|
152
|
+
isBuiltIn: true,
|
|
153
|
+
connected: true,
|
|
154
|
+
});
|
|
155
|
+
for (const tool of notebookTools) {
|
|
156
|
+
allTools.push({
|
|
157
|
+
type: 'function',
|
|
158
|
+
function: {
|
|
159
|
+
name: tool.name,
|
|
160
|
+
description: tool.description || '',
|
|
161
|
+
parameters: tool.inputSchema,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
142
165
|
// Add built-in ACE Code Search tools (always available)
|
|
143
166
|
const aceServiceTools = aceCodeSearchTools.map(tool => ({
|
|
144
167
|
name: tool.name.replace('ace-', ''),
|
|
@@ -556,6 +579,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
|
|
|
556
579
|
serviceName = 'todo';
|
|
557
580
|
actualToolName = toolName.substring('todo-'.length);
|
|
558
581
|
}
|
|
582
|
+
else if (toolName.startsWith('notebook-')) {
|
|
583
|
+
serviceName = 'notebook';
|
|
584
|
+
actualToolName = toolName.substring('notebook-'.length);
|
|
585
|
+
}
|
|
559
586
|
else if (toolName.startsWith('filesystem-')) {
|
|
560
587
|
serviceName = 'filesystem';
|
|
561
588
|
actualToolName = toolName.substring('filesystem-'.length);
|
|
@@ -604,6 +631,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
|
|
|
604
631
|
// Handle built-in TODO tools (no connection needed)
|
|
605
632
|
return await todoService.executeTool(actualToolName, args);
|
|
606
633
|
}
|
|
634
|
+
else if (serviceName === 'notebook') {
|
|
635
|
+
// Handle built-in Notebook tools (no connection needed)
|
|
636
|
+
return await executeNotebookTool(toolName, args);
|
|
637
|
+
}
|
|
607
638
|
else if (serviceName === 'filesystem') {
|
|
608
639
|
// Handle built-in filesystem tools (no connection needed)
|
|
609
640
|
const { filesystemService } = await import('../mcp/filesystem.js');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 备忘录条目接口
|
|
3
|
+
*/
|
|
4
|
+
export interface NotebookEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
note: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 添加备忘录
|
|
13
|
+
* @param filePath 文件路径
|
|
14
|
+
* @param note 备忘说明
|
|
15
|
+
* @returns 添加的备忘录条目
|
|
16
|
+
*/
|
|
17
|
+
export declare function addNotebook(filePath: string, note: string): NotebookEntry;
|
|
18
|
+
/**
|
|
19
|
+
* 查询备忘录
|
|
20
|
+
* @param filePathPattern 文件路径(支持模糊匹配)
|
|
21
|
+
* @param topN 返回最新的N条记录(默认10)
|
|
22
|
+
* @returns 匹配的备忘录条目列表
|
|
23
|
+
*/
|
|
24
|
+
export declare function queryNotebook(filePathPattern?: string, topN?: number): NotebookEntry[];
|
|
25
|
+
/**
|
|
26
|
+
* 获取指定文件的所有备忘录
|
|
27
|
+
* @param filePath 文件路径
|
|
28
|
+
* @returns 该文件的所有备忘录
|
|
29
|
+
*/
|
|
30
|
+
export declare function getNotebooksByFile(filePath: string): NotebookEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* 删除备忘录
|
|
33
|
+
* @param notebookId 备忘录ID
|
|
34
|
+
* @returns 是否删除成功
|
|
35
|
+
*/
|
|
36
|
+
export declare function deleteNotebook(notebookId: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 清空指定文件的所有备忘录
|
|
39
|
+
* @param filePath 文件路径
|
|
40
|
+
*/
|
|
41
|
+
export declare function clearNotebooksByFile(filePath: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* 获取所有备忘录统计信息
|
|
44
|
+
*/
|
|
45
|
+
export declare function getNotebookStats(): {
|
|
46
|
+
totalFiles: number;
|
|
47
|
+
totalEntries: number;
|
|
48
|
+
files: Array<{
|
|
49
|
+
path: string;
|
|
50
|
+
count: number;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const MAX_ENTRIES_PER_FILE = 50;
|
|
4
|
+
/**
|
|
5
|
+
* 获取备忘录存储目录
|
|
6
|
+
*/
|
|
7
|
+
function getNotebookDir() {
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const notebookDir = path.join(projectRoot, '.snow', 'notebook');
|
|
10
|
+
if (!fs.existsSync(notebookDir)) {
|
|
11
|
+
fs.mkdirSync(notebookDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return notebookDir;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 获取当前项目的备忘录文件路径
|
|
17
|
+
*/
|
|
18
|
+
function getNotebookFilePath() {
|
|
19
|
+
const projectRoot = process.cwd();
|
|
20
|
+
const projectName = path.basename(projectRoot);
|
|
21
|
+
const notebookDir = getNotebookDir();
|
|
22
|
+
return path.join(notebookDir, `${projectName}.json`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 读取备忘录数据
|
|
26
|
+
*/
|
|
27
|
+
function readNotebookData() {
|
|
28
|
+
const filePath = getNotebookFilePath();
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error('Failed to read notebook data:', error);
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 保存备忘录数据
|
|
43
|
+
*/
|
|
44
|
+
function saveNotebookData(data) {
|
|
45
|
+
const filePath = getNotebookFilePath();
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('Failed to save notebook data:', error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 规范化文件路径(转换为相对于项目根目录的路径)
|
|
56
|
+
*/
|
|
57
|
+
function normalizePath(filePath) {
|
|
58
|
+
const projectRoot = process.cwd();
|
|
59
|
+
// 如果是绝对路径,转换为相对路径
|
|
60
|
+
if (path.isAbsolute(filePath)) {
|
|
61
|
+
return path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
62
|
+
}
|
|
63
|
+
// 已经是相对路径,规范化斜杠
|
|
64
|
+
return filePath.replace(/\\/g, '/');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 添加备忘录
|
|
68
|
+
* @param filePath 文件路径
|
|
69
|
+
* @param note 备忘说明
|
|
70
|
+
* @returns 添加的备忘录条目
|
|
71
|
+
*/
|
|
72
|
+
export function addNotebook(filePath, note) {
|
|
73
|
+
const normalizedPath = normalizePath(filePath);
|
|
74
|
+
const data = readNotebookData();
|
|
75
|
+
if (!data[normalizedPath]) {
|
|
76
|
+
data[normalizedPath] = [];
|
|
77
|
+
}
|
|
78
|
+
// 创建新的备忘录条目(使用本地时间)
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const localTimeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}T${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
|
|
81
|
+
const entry = {
|
|
82
|
+
id: `notebook-${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
83
|
+
filePath: normalizedPath,
|
|
84
|
+
note,
|
|
85
|
+
createdAt: localTimeStr,
|
|
86
|
+
updatedAt: localTimeStr,
|
|
87
|
+
};
|
|
88
|
+
// 添加到数组开头(最新的在前面)
|
|
89
|
+
data[normalizedPath].unshift(entry);
|
|
90
|
+
// 限制每个文件最多50条备忘录
|
|
91
|
+
if (data[normalizedPath].length > MAX_ENTRIES_PER_FILE) {
|
|
92
|
+
data[normalizedPath] = data[normalizedPath].slice(0, MAX_ENTRIES_PER_FILE);
|
|
93
|
+
}
|
|
94
|
+
saveNotebookData(data);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 查询备忘录
|
|
99
|
+
* @param filePathPattern 文件路径(支持模糊匹配)
|
|
100
|
+
* @param topN 返回最新的N条记录(默认10)
|
|
101
|
+
* @returns 匹配的备忘录条目列表
|
|
102
|
+
*/
|
|
103
|
+
export function queryNotebook(filePathPattern = '', topN = 10) {
|
|
104
|
+
const data = readNotebookData();
|
|
105
|
+
const results = [];
|
|
106
|
+
// 规范化搜索模式
|
|
107
|
+
const normalizedPattern = filePathPattern.toLowerCase().replace(/\\/g, '/');
|
|
108
|
+
// 遍历所有文件路径
|
|
109
|
+
for (const [filePath, entries] of Object.entries(data)) {
|
|
110
|
+
// 如果没有指定模式,或者文件路径包含模式
|
|
111
|
+
if (!normalizedPattern ||
|
|
112
|
+
filePath.toLowerCase().includes(normalizedPattern)) {
|
|
113
|
+
results.push(...entries);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 按创建时间倒序排序(最新的在前)
|
|
117
|
+
results.sort((a, b) => {
|
|
118
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
119
|
+
});
|
|
120
|
+
// 返回 TopN 条记录
|
|
121
|
+
return results.slice(0, topN);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 获取指定文件的所有备忘录
|
|
125
|
+
* @param filePath 文件路径
|
|
126
|
+
* @returns 该文件的所有备忘录
|
|
127
|
+
*/
|
|
128
|
+
export function getNotebooksByFile(filePath) {
|
|
129
|
+
const normalizedPath = normalizePath(filePath);
|
|
130
|
+
const data = readNotebookData();
|
|
131
|
+
return data[normalizedPath] || [];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 删除备忘录
|
|
135
|
+
* @param notebookId 备忘录ID
|
|
136
|
+
* @returns 是否删除成功
|
|
137
|
+
*/
|
|
138
|
+
export function deleteNotebook(notebookId) {
|
|
139
|
+
const data = readNotebookData();
|
|
140
|
+
let found = false;
|
|
141
|
+
for (const [, entries] of Object.entries(data)) {
|
|
142
|
+
const index = entries.findIndex(entry => entry.id === notebookId);
|
|
143
|
+
if (index !== -1) {
|
|
144
|
+
entries.splice(index, 1);
|
|
145
|
+
found = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (found) {
|
|
150
|
+
saveNotebookData(data);
|
|
151
|
+
}
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 清空指定文件的所有备忘录
|
|
156
|
+
* @param filePath 文件路径
|
|
157
|
+
*/
|
|
158
|
+
export function clearNotebooksByFile(filePath) {
|
|
159
|
+
const normalizedPath = normalizePath(filePath);
|
|
160
|
+
const data = readNotebookData();
|
|
161
|
+
if (data[normalizedPath]) {
|
|
162
|
+
delete data[normalizedPath];
|
|
163
|
+
saveNotebookData(data);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 获取所有备忘录统计信息
|
|
168
|
+
*/
|
|
169
|
+
export function getNotebookStats() {
|
|
170
|
+
const data = readNotebookData();
|
|
171
|
+
const files = Object.entries(data).map(([path, entries]) => ({
|
|
172
|
+
path,
|
|
173
|
+
count: entries.length,
|
|
174
|
+
}));
|
|
175
|
+
const totalEntries = files.reduce((sum, file) => sum + file.count, 0);
|
|
176
|
+
return {
|
|
177
|
+
totalFiles: files.length,
|
|
178
|
+
totalEntries,
|
|
179
|
+
files: files.sort((a, b) => b.count - a.count),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -122,12 +122,20 @@ class VSCodeConnectionManager {
|
|
|
122
122
|
const targetPort = this.findPortForWorkspace();
|
|
123
123
|
// Create a new connection promise and store it
|
|
124
124
|
this.connectingPromise = new Promise((resolve, reject) => {
|
|
125
|
+
let isSettled = false; // Prevent double resolve/reject
|
|
125
126
|
// Set connection timeout
|
|
126
127
|
this.connectionTimeout = setTimeout(() => {
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
if (!isSettled) {
|
|
129
|
+
isSettled = true;
|
|
130
|
+
this.cleanupConnection();
|
|
131
|
+
reject(new Error('Connection timeout after 10 seconds'));
|
|
132
|
+
}
|
|
129
133
|
}, this.CONNECTION_TIMEOUT);
|
|
130
134
|
const tryConnect = (port) => {
|
|
135
|
+
// If already settled (resolved or rejected), stop trying
|
|
136
|
+
if (isSettled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
131
139
|
// Check both VSCode and JetBrains port ranges
|
|
132
140
|
if (port > this.VSCODE_MAX_PORT && port < this.JETBRAINS_BASE_PORT) {
|
|
133
141
|
// Jump from VSCode range to JetBrains range
|
|
@@ -135,23 +143,29 @@ class VSCodeConnectionManager {
|
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
145
|
if (port > this.JETBRAINS_MAX_PORT) {
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
if (!isSettled) {
|
|
147
|
+
isSettled = true;
|
|
148
|
+
this.cleanupConnection();
|
|
149
|
+
reject(new Error(`Failed to connect: no IDE server found on ports ${this.VSCODE_BASE_PORT}-${this.VSCODE_MAX_PORT} or ${this.JETBRAINS_BASE_PORT}-${this.JETBRAINS_MAX_PORT}`));
|
|
150
|
+
}
|
|
140
151
|
return;
|
|
141
152
|
}
|
|
142
153
|
try {
|
|
143
154
|
this.client = new WebSocket(`ws://localhost:${port}`);
|
|
144
155
|
this.client.on('open', () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.connectionTimeout
|
|
156
|
+
if (!isSettled) {
|
|
157
|
+
isSettled = true;
|
|
158
|
+
// Reset reconnect attempts on successful connection
|
|
159
|
+
this.reconnectAttempts = 0;
|
|
160
|
+
this.port = port;
|
|
161
|
+
// Clear connection state
|
|
162
|
+
if (this.connectionTimeout) {
|
|
163
|
+
clearTimeout(this.connectionTimeout);
|
|
164
|
+
this.connectionTimeout = null;
|
|
165
|
+
}
|
|
166
|
+
this.connectingPromise = null;
|
|
167
|
+
resolve();
|
|
152
168
|
}
|
|
153
|
-
this.connectingPromise = null;
|
|
154
|
-
resolve();
|
|
155
169
|
});
|
|
156
170
|
this.client.on('message', message => {
|
|
157
171
|
try {
|
|
@@ -167,19 +181,25 @@ class VSCodeConnectionManager {
|
|
|
167
181
|
});
|
|
168
182
|
this.client.on('close', () => {
|
|
169
183
|
this.client = null;
|
|
170
|
-
this
|
|
184
|
+
// Only schedule reconnect if this was an established connection (not initial scan)
|
|
185
|
+
if (this.reconnectAttempts > 0 || isSettled) {
|
|
186
|
+
this.scheduleReconnect();
|
|
187
|
+
}
|
|
171
188
|
});
|
|
172
189
|
this.client.on('error', _error => {
|
|
173
190
|
// On initial connection, try next port
|
|
174
|
-
if (this.reconnectAttempts === 0) {
|
|
191
|
+
if (this.reconnectAttempts === 0 && !isSettled) {
|
|
175
192
|
this.client = null;
|
|
176
|
-
|
|
193
|
+
// Small delay before trying next port to avoid rapid fire
|
|
194
|
+
setTimeout(() => tryConnect(port + 1), 50);
|
|
177
195
|
}
|
|
178
196
|
// For reconnections, silently handle and let close event trigger reconnect
|
|
179
197
|
});
|
|
180
198
|
}
|
|
181
199
|
catch (error) {
|
|
182
|
-
|
|
200
|
+
if (!isSettled) {
|
|
201
|
+
setTimeout(() => tryConnect(port + 1), 50);
|
|
202
|
+
}
|
|
183
203
|
}
|
|
184
204
|
};
|
|
185
205
|
tryConnect(targetPort);
|
|
@@ -297,7 +317,7 @@ class VSCodeConnectionManager {
|
|
|
297
317
|
clearTimeout(this.connectionTimeout);
|
|
298
318
|
this.connectionTimeout = null;
|
|
299
319
|
}
|
|
300
|
-
// Clear connecting promise
|
|
320
|
+
// Clear connecting promise - this is critical for restart
|
|
301
321
|
this.connectingPromise = null;
|
|
302
322
|
if (this.client) {
|
|
303
323
|
try {
|