snow-ai 0.2.19 → 0.2.21

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.
@@ -16,34 +16,48 @@ interface Diagnostic {
16
16
  code?: string | number;
17
17
  }
18
18
  declare class VSCodeConnectionManager {
19
- private server;
20
19
  private client;
20
+ private reconnectTimer;
21
+ private reconnectAttempts;
22
+ private readonly MAX_RECONNECT_ATTEMPTS;
23
+ private readonly BASE_RECONNECT_DELAY;
24
+ private readonly MAX_RECONNECT_DELAY;
25
+ private readonly VSCODE_BASE_PORT;
26
+ private readonly VSCODE_MAX_PORT;
27
+ private readonly JETBRAINS_BASE_PORT;
28
+ private readonly JETBRAINS_MAX_PORT;
21
29
  private port;
22
30
  private editorContext;
23
31
  private listeners;
32
+ private currentWorkingDirectory;
24
33
  start(): Promise<void>;
34
+ /**
35
+ * Find the correct port for the current workspace
36
+ */
37
+ private findPortForWorkspace;
38
+ /**
39
+ * Check if we should handle this message based on workspace folder
40
+ */
41
+ private shouldHandleMessage;
42
+ private scheduleReconnect;
25
43
  stop(): void;
26
44
  isConnected(): boolean;
27
- isServerRunning(): boolean;
45
+ isClientRunning(): boolean;
28
46
  getContext(): EditorContext;
29
47
  onContextUpdate(listener: (context: EditorContext) => void): () => void;
30
48
  private handleMessage;
31
49
  private notifyListeners;
32
50
  getPort(): number;
33
51
  /**
34
- * Request diagnostics for a specific file from VS Code
52
+ * Request diagnostics for a specific file from IDE
35
53
  * @param filePath - The file path to get diagnostics for
36
54
  * @returns Promise that resolves with diagnostics array
37
55
  */
38
56
  requestDiagnostics(filePath: string): Promise<Diagnostic[]>;
39
57
  /**
40
- * Request DIFF+APPLY view for file changes in VS Code
41
- * @param filePath - The file path for the diff
42
- * @param oldContent - Original content with line numbers
43
- * @param newContent - Modified content with line numbers
44
- * @returns Promise that resolves with user's approval response
58
+ * Reset reconnection attempts (e.g., when user manually triggers reconnect)
45
59
  */
46
- requestDiffApply(filePath: string, oldContent: string, newContent: string): Promise<'approve' | 'approve_always' | 'reject'>;
60
+ resetReconnectAttempts(): void;
47
61
  }
48
62
  export declare const vscodeConnection: VSCodeConnectionManager;
49
63
  export type { EditorContext, Diagnostic };
@@ -1,18 +1,70 @@
1
- import { WebSocketServer, WebSocket } from 'ws';
1
+ import { WebSocket } from 'ws';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
2
5
  class VSCodeConnectionManager {
3
6
  constructor() {
4
- Object.defineProperty(this, "server", {
7
+ Object.defineProperty(this, "client", {
5
8
  enumerable: true,
6
9
  configurable: true,
7
10
  writable: true,
8
11
  value: null
9
12
  });
10
- Object.defineProperty(this, "client", {
13
+ Object.defineProperty(this, "reconnectTimer", {
11
14
  enumerable: true,
12
15
  configurable: true,
13
16
  writable: true,
14
17
  value: null
15
18
  });
19
+ Object.defineProperty(this, "reconnectAttempts", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: 0
24
+ });
25
+ Object.defineProperty(this, "MAX_RECONNECT_ATTEMPTS", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: 10
30
+ });
31
+ Object.defineProperty(this, "BASE_RECONNECT_DELAY", {
32
+ enumerable: true,
33
+ configurable: true,
34
+ writable: true,
35
+ value: 2000
36
+ }); // 2 seconds
37
+ Object.defineProperty(this, "MAX_RECONNECT_DELAY", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: 30000
42
+ }); // 30 seconds
43
+ // Port ranges: VSCode uses 9527-9537, JetBrains uses 9538-9548
44
+ Object.defineProperty(this, "VSCODE_BASE_PORT", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: 9527
49
+ });
50
+ Object.defineProperty(this, "VSCODE_MAX_PORT", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: 9537
55
+ });
56
+ Object.defineProperty(this, "JETBRAINS_BASE_PORT", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: 9538
61
+ });
62
+ Object.defineProperty(this, "JETBRAINS_MAX_PORT", {
63
+ enumerable: true,
64
+ configurable: true,
65
+ writable: true,
66
+ value: 9548
67
+ });
16
68
  Object.defineProperty(this, "port", {
17
69
  enumerable: true,
18
70
  configurable: true,
@@ -31,66 +83,147 @@ class VSCodeConnectionManager {
31
83
  writable: true,
32
84
  value: []
33
85
  });
86
+ Object.defineProperty(this, "currentWorkingDirectory", {
87
+ enumerable: true,
88
+ configurable: true,
89
+ writable: true,
90
+ value: process.cwd()
91
+ });
34
92
  }
35
93
  async start() {
36
- // If already running, just return success
37
- if (this.server) {
94
+ // If already connected, just return success
95
+ if (this.client?.readyState === WebSocket.OPEN) {
38
96
  return Promise.resolve();
39
97
  }
98
+ // Try to find the correct port for this workspace
99
+ const targetPort = this.findPortForWorkspace();
40
100
  return new Promise((resolve, reject) => {
41
- try {
42
- this.server = new WebSocketServer({ port: this.port });
43
- this.server.on('connection', (ws) => {
44
- // Close old client if exists
45
- if (this.client && this.client !== ws) {
46
- this.client.close();
47
- }
48
- this.client = ws;
49
- ws.on('message', (message) => {
101
+ const tryConnect = (port) => {
102
+ // Check both VSCode and JetBrains port ranges
103
+ if (port > this.VSCODE_MAX_PORT && port < this.JETBRAINS_BASE_PORT) {
104
+ // Jump from VSCode range to JetBrains range
105
+ tryConnect(this.JETBRAINS_BASE_PORT);
106
+ return;
107
+ }
108
+ if (port > this.JETBRAINS_MAX_PORT) {
109
+ 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}`));
110
+ return;
111
+ }
112
+ try {
113
+ this.client = new WebSocket(`ws://localhost:${port}`);
114
+ this.client.on('open', () => {
115
+ // Reset reconnect attempts on successful connection
116
+ this.reconnectAttempts = 0;
117
+ this.port = port;
118
+ resolve();
119
+ });
120
+ this.client.on('message', message => {
50
121
  try {
51
122
  const data = JSON.parse(message.toString());
52
- this.handleMessage(data);
123
+ // Filter messages by workspace folder
124
+ if (this.shouldHandleMessage(data)) {
125
+ this.handleMessage(data);
126
+ }
53
127
  }
54
128
  catch (error) {
55
129
  // Ignore invalid JSON
56
130
  }
57
131
  });
58
- ws.on('close', () => {
59
- if (this.client === ws) {
132
+ this.client.on('close', () => {
133
+ this.client = null;
134
+ this.scheduleReconnect();
135
+ });
136
+ this.client.on('error', _error => {
137
+ // On initial connection, try next port
138
+ if (this.reconnectAttempts === 0) {
60
139
  this.client = null;
140
+ tryConnect(port + 1);
61
141
  }
142
+ // For reconnections, silently handle and let close event trigger reconnect
62
143
  });
63
- ws.on('error', () => {
64
- // Silently handle errors
65
- });
66
- });
67
- this.server.on('listening', () => {
68
- resolve();
69
- });
70
- this.server.on('error', (error) => {
71
- reject(error);
72
- });
73
- }
74
- catch (error) {
75
- reject(error);
76
- }
144
+ }
145
+ catch (error) {
146
+ tryConnect(port + 1);
147
+ }
148
+ };
149
+ tryConnect(targetPort);
77
150
  });
78
151
  }
152
+ /**
153
+ * Find the correct port for the current workspace
154
+ */
155
+ findPortForWorkspace() {
156
+ try {
157
+ const portInfoPath = path.join(os.tmpdir(), 'snow-cli-ports.json');
158
+ if (fs.existsSync(portInfoPath)) {
159
+ const portInfo = JSON.parse(fs.readFileSync(portInfoPath, 'utf8'));
160
+ // Try to match current working directory
161
+ const cwd = this.currentWorkingDirectory;
162
+ // Direct match
163
+ if (portInfo[cwd]) {
164
+ return portInfo[cwd];
165
+ }
166
+ // Check if cwd is within any of the workspace folders
167
+ for (const [workspace, port] of Object.entries(portInfo)) {
168
+ if (cwd.startsWith(workspace)) {
169
+ return port;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ catch (error) {
175
+ // Ignore errors, will fall back to VSCODE_BASE_PORT
176
+ }
177
+ // Start with VSCode port range by default
178
+ return this.VSCODE_BASE_PORT;
179
+ }
180
+ /**
181
+ * Check if we should handle this message based on workspace folder
182
+ */
183
+ shouldHandleMessage(data) {
184
+ // If no workspace folder in message, accept it (backwards compatibility)
185
+ if (!data.workspaceFolder) {
186
+ return true;
187
+ }
188
+ // Check if message's workspace folder matches our current working directory
189
+ const cwd = this.currentWorkingDirectory;
190
+ // Bidirectional check: either cwd is within IDE workspace, or IDE workspace is within cwd
191
+ const cwdInWorkspace = cwd.startsWith(data.workspaceFolder);
192
+ const workspaceInCwd = data.workspaceFolder.startsWith(cwd);
193
+ const matches = cwdInWorkspace || workspaceInCwd;
194
+ return matches;
195
+ }
196
+ scheduleReconnect() {
197
+ if (this.reconnectTimer) {
198
+ clearTimeout(this.reconnectTimer);
199
+ }
200
+ this.reconnectAttempts++;
201
+ if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
202
+ return;
203
+ }
204
+ const delay = Math.min(this.BASE_RECONNECT_DELAY * Math.pow(1.5, this.reconnectAttempts - 1), this.MAX_RECONNECT_DELAY);
205
+ this.reconnectTimer = setTimeout(() => {
206
+ this.start().catch(() => {
207
+ // Silently handle reconnection failures
208
+ });
209
+ }, delay);
210
+ }
79
211
  stop() {
212
+ if (this.reconnectTimer) {
213
+ clearTimeout(this.reconnectTimer);
214
+ this.reconnectTimer = null;
215
+ }
80
216
  if (this.client) {
81
217
  this.client.close();
82
218
  this.client = null;
83
219
  }
84
- if (this.server) {
85
- this.server.close();
86
- this.server = null;
87
- }
220
+ this.reconnectAttempts = 0;
88
221
  }
89
222
  isConnected() {
90
- return this.client !== null && this.client.readyState === WebSocket.OPEN;
223
+ return this.client?.readyState === WebSocket.OPEN;
91
224
  }
92
- isServerRunning() {
93
- return this.server !== null;
225
+ isClientRunning() {
226
+ return this.client !== null;
94
227
  }
95
228
  getContext() {
96
229
  return { ...this.editorContext };
@@ -98,7 +231,7 @@ class VSCodeConnectionManager {
98
231
  onContextUpdate(listener) {
99
232
  this.listeners.push(listener);
100
233
  return () => {
101
- this.listeners = this.listeners.filter((l) => l !== listener);
234
+ this.listeners = this.listeners.filter(l => l !== listener);
102
235
  };
103
236
  }
104
237
  handleMessage(data) {
@@ -107,7 +240,7 @@ class VSCodeConnectionManager {
107
240
  activeFile: data.activeFile,
108
241
  selectedText: data.selectedText,
109
242
  cursorPosition: data.cursorPosition,
110
- workspaceFolder: data.workspaceFolder
243
+ workspaceFolder: data.workspaceFolder,
111
244
  };
112
245
  this.notifyListeners();
113
246
  }
@@ -121,12 +254,12 @@ class VSCodeConnectionManager {
121
254
  return this.port;
122
255
  }
123
256
  /**
124
- * Request diagnostics for a specific file from VS Code
257
+ * Request diagnostics for a specific file from IDE
125
258
  * @param filePath - The file path to get diagnostics for
126
259
  * @returns Promise that resolves with diagnostics array
127
260
  */
128
261
  async requestDiagnostics(filePath) {
129
- return new Promise((resolve) => {
262
+ return new Promise(resolve => {
130
263
  if (!this.client || this.client.readyState !== WebSocket.OPEN) {
131
264
  resolve([]); // Return empty array if not connected
132
265
  return;
@@ -156,53 +289,15 @@ class VSCodeConnectionManager {
156
289
  this.client.send(JSON.stringify({
157
290
  type: 'getDiagnostics',
158
291
  requestId,
159
- filePath
292
+ filePath,
160
293
  }));
161
294
  });
162
295
  }
163
296
  /**
164
- * Request DIFF+APPLY view for file changes in VS Code
165
- * @param filePath - The file path for the diff
166
- * @param oldContent - Original content with line numbers
167
- * @param newContent - Modified content with line numbers
168
- * @returns Promise that resolves with user's approval response
297
+ * Reset reconnection attempts (e.g., when user manually triggers reconnect)
169
298
  */
170
- async requestDiffApply(filePath, oldContent, newContent) {
171
- return new Promise((resolve) => {
172
- if (!this.client || this.client.readyState !== WebSocket.OPEN) {
173
- resolve('approve'); // If not connected, default to approve (fallback to CLI confirmation)
174
- return;
175
- }
176
- const requestId = Math.random().toString(36).substring(7);
177
- const timeout = setTimeout(() => {
178
- cleanup();
179
- resolve('approve'); // Timeout, default to approve
180
- }, 30000); // 30 second timeout for user decision
181
- const handler = (message) => {
182
- try {
183
- const data = JSON.parse(message.toString());
184
- if (data.type === 'diffApplyResult' && data.requestId === requestId) {
185
- cleanup();
186
- resolve(data.result || 'approve');
187
- }
188
- }
189
- catch (error) {
190
- // Ignore invalid JSON
191
- }
192
- };
193
- const cleanup = () => {
194
- clearTimeout(timeout);
195
- this.client?.removeListener('message', handler);
196
- };
197
- this.client.on('message', handler);
198
- this.client.send(JSON.stringify({
199
- type: 'diffApply',
200
- requestId,
201
- filePath,
202
- oldContent,
203
- newContent
204
- }));
205
- });
299
+ resetReconnectAttempts() {
300
+ this.reconnectAttempts = 0;
206
301
  }
207
302
  }
208
303
  export const vscodeConnection = new VSCodeConnectionManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {