claude-ide-bridge 1.1.0

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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/activityLog.d.ts +26 -0
  4. package/dist/activityLog.js +76 -0
  5. package/dist/activityLog.js.map +1 -0
  6. package/dist/bridge.d.ts +19 -0
  7. package/dist/bridge.js +277 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +221 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/errors.d.ts +16 -0
  13. package/dist/errors.js +20 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/extensionClient.d.ts +193 -0
  16. package/dist/extensionClient.js +698 -0
  17. package/dist/extensionClient.js.map +1 -0
  18. package/dist/fileLock.d.ts +12 -0
  19. package/dist/fileLock.js +30 -0
  20. package/dist/fileLock.js.map +1 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +38 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/lockfile.d.ts +12 -0
  25. package/dist/lockfile.js +127 -0
  26. package/dist/lockfile.js.map +1 -0
  27. package/dist/logger.d.ts +16 -0
  28. package/dist/logger.js +68 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/probe.d.ts +22 -0
  31. package/dist/probe.js +45 -0
  32. package/dist/probe.js.map +1 -0
  33. package/dist/server.d.ts +25 -0
  34. package/dist/server.js +265 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/tools/activityLog.d.ts +39 -0
  37. package/dist/tools/activityLog.js +49 -0
  38. package/dist/tools/activityLog.js.map +1 -0
  39. package/dist/tools/aiComments.d.ts +26 -0
  40. package/dist/tools/aiComments.js +196 -0
  41. package/dist/tools/aiComments.js.map +1 -0
  42. package/dist/tools/bridgeStatus.d.ts +21 -0
  43. package/dist/tools/bridgeStatus.js +41 -0
  44. package/dist/tools/bridgeStatus.js.map +1 -0
  45. package/dist/tools/checkDocumentDirty.d.ts +28 -0
  46. package/dist/tools/checkDocumentDirty.js +61 -0
  47. package/dist/tools/checkDocumentDirty.js.map +1 -0
  48. package/dist/tools/clipboard.d.ts +50 -0
  49. package/dist/tools/clipboard.js +82 -0
  50. package/dist/tools/clipboard.js.map +1 -0
  51. package/dist/tools/closeTabs.d.ts +49 -0
  52. package/dist/tools/closeTabs.js +77 -0
  53. package/dist/tools/closeTabs.js.map +1 -0
  54. package/dist/tools/debug.d.ts +154 -0
  55. package/dist/tools/debug.js +248 -0
  56. package/dist/tools/debug.js.map +1 -0
  57. package/dist/tools/decorations.d.ts +92 -0
  58. package/dist/tools/decorations.js +150 -0
  59. package/dist/tools/decorations.js.map +1 -0
  60. package/dist/tools/diffDebugger.d.ts +62 -0
  61. package/dist/tools/diffDebugger.js +245 -0
  62. package/dist/tools/diffDebugger.js.map +1 -0
  63. package/dist/tools/editText.d.ts +80 -0
  64. package/dist/tools/editText.js +274 -0
  65. package/dist/tools/editText.js.map +1 -0
  66. package/dist/tools/fileOperations.d.ts +111 -0
  67. package/dist/tools/fileOperations.js +280 -0
  68. package/dist/tools/fileOperations.js.map +1 -0
  69. package/dist/tools/fileWatcher.d.ts +54 -0
  70. package/dist/tools/fileWatcher.js +100 -0
  71. package/dist/tools/fileWatcher.js.map +1 -0
  72. package/dist/tools/findFiles.d.ts +31 -0
  73. package/dist/tools/findFiles.js +119 -0
  74. package/dist/tools/findFiles.js.map +1 -0
  75. package/dist/tools/fixAllLintErrors.d.ts +29 -0
  76. package/dist/tools/fixAllLintErrors.js +114 -0
  77. package/dist/tools/fixAllLintErrors.js.map +1 -0
  78. package/dist/tools/flowGuardian.d.ts +61 -0
  79. package/dist/tools/flowGuardian.js +311 -0
  80. package/dist/tools/flowGuardian.js.map +1 -0
  81. package/dist/tools/formatDocument.d.ts +30 -0
  82. package/dist/tools/formatDocument.js +132 -0
  83. package/dist/tools/formatDocument.js.map +1 -0
  84. package/dist/tools/formatFile.d.ts +28 -0
  85. package/dist/tools/formatFile.js +110 -0
  86. package/dist/tools/formatFile.js.map +1 -0
  87. package/dist/tools/getBufferContent.d.ts +38 -0
  88. package/dist/tools/getBufferContent.js +100 -0
  89. package/dist/tools/getBufferContent.js.map +1 -0
  90. package/dist/tools/getCurrentSelection.d.ts +43 -0
  91. package/dist/tools/getCurrentSelection.js +75 -0
  92. package/dist/tools/getCurrentSelection.js.map +1 -0
  93. package/dist/tools/getDiagnostics.d.ts +38 -0
  94. package/dist/tools/getDiagnostics.js +204 -0
  95. package/dist/tools/getDiagnostics.js.map +1 -0
  96. package/dist/tools/getDocumentSymbols.d.ts +27 -0
  97. package/dist/tools/getDocumentSymbols.js +133 -0
  98. package/dist/tools/getDocumentSymbols.js.map +1 -0
  99. package/dist/tools/getFileTree.d.ts +36 -0
  100. package/dist/tools/getFileTree.js +111 -0
  101. package/dist/tools/getFileTree.js.map +1 -0
  102. package/dist/tools/getGitDiff.d.ts +34 -0
  103. package/dist/tools/getGitDiff.js +57 -0
  104. package/dist/tools/getGitDiff.js.map +1 -0
  105. package/dist/tools/getGitLog.d.ts +30 -0
  106. package/dist/tools/getGitLog.js +65 -0
  107. package/dist/tools/getGitLog.js.map +1 -0
  108. package/dist/tools/getGitStatus.d.ts +26 -0
  109. package/dist/tools/getGitStatus.js +95 -0
  110. package/dist/tools/getGitStatus.js.map +1 -0
  111. package/dist/tools/getOpenEditors.d.ts +21 -0
  112. package/dist/tools/getOpenEditors.js +84 -0
  113. package/dist/tools/getOpenEditors.js.map +1 -0
  114. package/dist/tools/getProjectInfo.d.ts +20 -0
  115. package/dist/tools/getProjectInfo.js +315 -0
  116. package/dist/tools/getProjectInfo.js.map +1 -0
  117. package/dist/tools/getToolCapabilities.d.ts +23 -0
  118. package/dist/tools/getToolCapabilities.js +249 -0
  119. package/dist/tools/getToolCapabilities.js.map +1 -0
  120. package/dist/tools/getWorkspaceFolders.d.ts +21 -0
  121. package/dist/tools/getWorkspaceFolders.js +47 -0
  122. package/dist/tools/getWorkspaceFolders.js.map +1 -0
  123. package/dist/tools/gitHistory.d.ts +78 -0
  124. package/dist/tools/gitHistory.js +151 -0
  125. package/dist/tools/gitHistory.js.map +1 -0
  126. package/dist/tools/gitWrite.d.ts +335 -0
  127. package/dist/tools/gitWrite.js +859 -0
  128. package/dist/tools/gitWrite.js.map +1 -0
  129. package/dist/tools/github/actions.d.ts +67 -0
  130. package/dist/tools/github/actions.js +155 -0
  131. package/dist/tools/github/actions.js.map +1 -0
  132. package/dist/tools/github/index.d.ts +4 -0
  133. package/dist/tools/github/index.js +5 -0
  134. package/dist/tools/github/index.js.map +1 -0
  135. package/dist/tools/github/issues.d.ts +140 -0
  136. package/dist/tools/github/issues.js +279 -0
  137. package/dist/tools/github/issues.js.map +1 -0
  138. package/dist/tools/github/pr.d.ts +101 -0
  139. package/dist/tools/github/pr.js +215 -0
  140. package/dist/tools/github/pr.js.map +1 -0
  141. package/dist/tools/github/review.d.ts +101 -0
  142. package/dist/tools/github/review.js +292 -0
  143. package/dist/tools/github/review.js.map +1 -0
  144. package/dist/tools/github/shared.d.ts +4 -0
  145. package/dist/tools/github/shared.js +12 -0
  146. package/dist/tools/github/shared.js.map +1 -0
  147. package/dist/tools/github.d.ts +308 -0
  148. package/dist/tools/github.js +656 -0
  149. package/dist/tools/github.js.map +1 -0
  150. package/dist/tools/hoverAtCursor.d.ts +22 -0
  151. package/dist/tools/hoverAtCursor.js +51 -0
  152. package/dist/tools/hoverAtCursor.js.map +1 -0
  153. package/dist/tools/httpClient.d.ts +83 -0
  154. package/dist/tools/httpClient.js +335 -0
  155. package/dist/tools/httpClient.js.map +1 -0
  156. package/dist/tools/index.d.ts +7 -0
  157. package/dist/tools/index.js +246 -0
  158. package/dist/tools/index.js.map +1 -0
  159. package/dist/tools/inlayHints.d.ts +38 -0
  160. package/dist/tools/inlayHints.js +56 -0
  161. package/dist/tools/inlayHints.js.map +1 -0
  162. package/dist/tools/linters/biome.d.ts +2 -0
  163. package/dist/tools/linters/biome.js +44 -0
  164. package/dist/tools/linters/biome.js.map +1 -0
  165. package/dist/tools/linters/cargo.d.ts +2 -0
  166. package/dist/tools/linters/cargo.js +45 -0
  167. package/dist/tools/linters/cargo.js.map +1 -0
  168. package/dist/tools/linters/eslint.d.ts +2 -0
  169. package/dist/tools/linters/eslint.js +59 -0
  170. package/dist/tools/linters/eslint.js.map +1 -0
  171. package/dist/tools/linters/govet.d.ts +2 -0
  172. package/dist/tools/linters/govet.js +37 -0
  173. package/dist/tools/linters/govet.js.map +1 -0
  174. package/dist/tools/linters/pyright.d.ts +2 -0
  175. package/dist/tools/linters/pyright.js +34 -0
  176. package/dist/tools/linters/pyright.js.map +1 -0
  177. package/dist/tools/linters/ruff.d.ts +2 -0
  178. package/dist/tools/linters/ruff.js +30 -0
  179. package/dist/tools/linters/ruff.js.map +1 -0
  180. package/dist/tools/linters/types.d.ts +16 -0
  181. package/dist/tools/linters/types.js +2 -0
  182. package/dist/tools/linters/types.js.map +1 -0
  183. package/dist/tools/linters/typescript.d.ts +2 -0
  184. package/dist/tools/linters/typescript.js +38 -0
  185. package/dist/tools/linters/typescript.js.map +1 -0
  186. package/dist/tools/lsp.d.ts +310 -0
  187. package/dist/tools/lsp.js +684 -0
  188. package/dist/tools/lsp.js.map +1 -0
  189. package/dist/tools/notebook.d.ts +95 -0
  190. package/dist/tools/notebook.js +144 -0
  191. package/dist/tools/notebook.js.map +1 -0
  192. package/dist/tools/openDiff.d.ts +41 -0
  193. package/dist/tools/openDiff.js +116 -0
  194. package/dist/tools/openDiff.js.map +1 -0
  195. package/dist/tools/openFile.d.ts +34 -0
  196. package/dist/tools/openFile.js +102 -0
  197. package/dist/tools/openFile.js.map +1 -0
  198. package/dist/tools/organizeImports.d.ts +29 -0
  199. package/dist/tools/organizeImports.js +64 -0
  200. package/dist/tools/organizeImports.js.map +1 -0
  201. package/dist/tools/planPersistence.d.ts +196 -0
  202. package/dist/tools/planPersistence.js +437 -0
  203. package/dist/tools/planPersistence.js.map +1 -0
  204. package/dist/tools/replaceBlock.d.ts +40 -0
  205. package/dist/tools/replaceBlock.js +105 -0
  206. package/dist/tools/replaceBlock.js.map +1 -0
  207. package/dist/tools/runCommand.d.ts +43 -0
  208. package/dist/tools/runCommand.js +141 -0
  209. package/dist/tools/runCommand.js.map +1 -0
  210. package/dist/tools/runTests.d.ts +32 -0
  211. package/dist/tools/runTests.js +160 -0
  212. package/dist/tools/runTests.js.map +1 -0
  213. package/dist/tools/saveDocument.d.ts +28 -0
  214. package/dist/tools/saveDocument.js +58 -0
  215. package/dist/tools/saveDocument.js.map +1 -0
  216. package/dist/tools/searchAndReplace.d.ts +50 -0
  217. package/dist/tools/searchAndReplace.js +203 -0
  218. package/dist/tools/searchAndReplace.js.map +1 -0
  219. package/dist/tools/searchWorkspace.d.ts +52 -0
  220. package/dist/tools/searchWorkspace.js +159 -0
  221. package/dist/tools/searchWorkspace.js.map +1 -0
  222. package/dist/tools/setActiveWorkspaceFolder.d.ts +28 -0
  223. package/dist/tools/setActiveWorkspaceFolder.js +32 -0
  224. package/dist/tools/setActiveWorkspaceFolder.js.map +1 -0
  225. package/dist/tools/tasks.d.ts +56 -0
  226. package/dist/tools/tasks.js +89 -0
  227. package/dist/tools/tasks.js.map +1 -0
  228. package/dist/tools/terminal.d.ts +241 -0
  229. package/dist/tools/terminal.js +539 -0
  230. package/dist/tools/terminal.js.map +1 -0
  231. package/dist/tools/testRunners/cargoTest.d.ts +2 -0
  232. package/dist/tools/testRunners/cargoTest.js +123 -0
  233. package/dist/tools/testRunners/cargoTest.js.map +1 -0
  234. package/dist/tools/testRunners/goTest.d.ts +2 -0
  235. package/dist/tools/testRunners/goTest.js +106 -0
  236. package/dist/tools/testRunners/goTest.js.map +1 -0
  237. package/dist/tools/testRunners/pytest.d.ts +2 -0
  238. package/dist/tools/testRunners/pytest.js +133 -0
  239. package/dist/tools/testRunners/pytest.js.map +1 -0
  240. package/dist/tools/testRunners/types.d.ts +18 -0
  241. package/dist/tools/testRunners/types.js +2 -0
  242. package/dist/tools/testRunners/types.js.map +1 -0
  243. package/dist/tools/testRunners/vitestJest.d.ts +3 -0
  244. package/dist/tools/testRunners/vitestJest.js +178 -0
  245. package/dist/tools/testRunners/vitestJest.js.map +1 -0
  246. package/dist/tools/typeHierarchy.d.ts +45 -0
  247. package/dist/tools/typeHierarchy.js +65 -0
  248. package/dist/tools/typeHierarchy.js.map +1 -0
  249. package/dist/tools/utils.d.ts +54 -0
  250. package/dist/tools/utils.js +267 -0
  251. package/dist/tools/utils.js.map +1 -0
  252. package/dist/tools/vscodeCommands.d.ts +59 -0
  253. package/dist/tools/vscodeCommands.js +108 -0
  254. package/dist/tools/vscodeCommands.js.map +1 -0
  255. package/dist/tools/watchDiagnostics.d.ts +32 -0
  256. package/dist/tools/watchDiagnostics.js +87 -0
  257. package/dist/tools/watchDiagnostics.js.map +1 -0
  258. package/dist/tools/workspaceSettings.d.ts +67 -0
  259. package/dist/tools/workspaceSettings.js +102 -0
  260. package/dist/tools/workspaceSettings.js.map +1 -0
  261. package/dist/tools/workspaceSnapshots.d.ts +174 -0
  262. package/dist/tools/workspaceSnapshots.js +474 -0
  263. package/dist/tools/workspaceSnapshots.js.map +1 -0
  264. package/dist/transport.d.ts +57 -0
  265. package/dist/transport.js +417 -0
  266. package/dist/transport.js.map +1 -0
  267. package/dist/version.d.ts +2 -0
  268. package/dist/version.js +3 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/wsUtils.d.ts +9 -0
  271. package/dist/wsUtils.js +54 -0
  272. package/dist/wsUtils.js.map +1 -0
  273. package/package.json +69 -0
@@ -0,0 +1,698 @@
1
+ import { WebSocket } from "ws";
2
+ import { BRIDGE_PROTOCOL_VERSION } from "./version.js";
3
+ import { safeSend, waitForDrain } from "./wsUtils.js";
4
+ /** Thrown when an extension request times out — distinguishable from "no results" (null). */
5
+ export class ExtensionTimeoutError extends Error {
6
+ method;
7
+ constructor(method) {
8
+ super(`Extension request ${method} timed out`);
9
+ this.name = "ExtensionTimeoutError";
10
+ this.method = method;
11
+ }
12
+ }
13
+ const REQUEST_TIMEOUT = 10_000;
14
+ export class ExtensionClient {
15
+ logger;
16
+ ws = null;
17
+ connected = false;
18
+ pendingRequests = new Map();
19
+ nextId = 0;
20
+ // Exponential backoff — prevents cascading 10s timeouts when extension is unresponsive
21
+ extensionSuspendedUntil = 0;
22
+ extensionFailures = 0;
23
+ extensionHalfOpen = false;
24
+ // State pushed by extension via notifications
25
+ latestDiagnostics = new Map();
26
+ latestSelection = null;
27
+ latestActiveFile = null;
28
+ lastDiagnosticsUpdate = 0;
29
+ // AI comment cache (pushed by extension)
30
+ latestAIComments = new Map();
31
+ // Debug state (pushed by extension via notifications)
32
+ latestDebugState = null;
33
+ // Callbacks for forwarding notifications to Claude Code
34
+ onDiagnosticsChanged = null;
35
+ onAICommentsChanged = null;
36
+ onFileChanged = null;
37
+ onExtensionDisconnected = null;
38
+ onDebugSessionChanged = null;
39
+ // Listener set for diagnostics (used by watchDiagnostics long-poll)
40
+ diagnosticsListeners = new Set();
41
+ constructor(logger) {
42
+ this.logger = logger;
43
+ }
44
+ /** Invoke a callback safely — log and swallow errors to prevent bridge crash */
45
+ safeCallback(fn, ...args) {
46
+ if (!fn)
47
+ return;
48
+ try {
49
+ fn(...args);
50
+ }
51
+ catch (err) {
52
+ this.logger.error(`Callback error: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ }
55
+ handleExtensionConnection(ws) {
56
+ // Replace existing connection
57
+ if (this.ws) {
58
+ this.logger.info("Replacing existing extension connection");
59
+ this.rejectAllPending("Extension reconnected");
60
+ this.ws.removeAllListeners();
61
+ if (this.ws.readyState === WebSocket.OPEN) {
62
+ this.ws.terminate();
63
+ }
64
+ }
65
+ this.ws = ws;
66
+ this.connected = true;
67
+ // Don't clear cached diagnostics/selection/file state — extension pushes
68
+ // fresh data on connect, so stale values are harmless for a few seconds.
69
+ // Reset backoff — fresh connection deserves a clean slate
70
+ this.extensionSuspendedUntil = 0;
71
+ this.extensionFailures = 0;
72
+ this.extensionHalfOpen = false;
73
+ this.logger.info("Extension client connected");
74
+ ws.on("message", (data) => {
75
+ try {
76
+ const msg = JSON.parse(data.toString("utf-8"));
77
+ // Response to our request
78
+ if (msg.id !== undefined && msg.id !== null && !msg.method) {
79
+ const pending = this.pendingRequests.get(msg.id);
80
+ if (pending) {
81
+ this.pendingRequests.delete(msg.id);
82
+ clearTimeout(pending.timer);
83
+ if (msg.error) {
84
+ pending.reject(new Error(msg.error.message));
85
+ }
86
+ else {
87
+ pending.resolve(msg.result);
88
+ }
89
+ }
90
+ else {
91
+ this.logger.debug(`Orphaned extension response for id=${msg.id} (likely timed out)`);
92
+ }
93
+ return;
94
+ }
95
+ // Push notification from extension
96
+ if (msg.method) {
97
+ this.handleNotification(msg.method, msg.params);
98
+ }
99
+ }
100
+ catch (err) {
101
+ this.logger.error(`Extension message parse error: ${err}`);
102
+ }
103
+ });
104
+ // Guard against double close/error cleanup
105
+ let disconnected = false;
106
+ const handleDisconnect = (reason) => {
107
+ if (disconnected)
108
+ return;
109
+ disconnected = true;
110
+ this.connected = false;
111
+ this.ws = null;
112
+ this.rejectAllPending(reason);
113
+ // Clear diagnostics listeners — stale watchDiagnostics closures must not
114
+ // accumulate across reconnects (they hold references to old AbortSignals).
115
+ this.diagnosticsListeners.clear();
116
+ this.logger.info(`Extension disconnected: ${reason}`);
117
+ this.safeCallback(this.onExtensionDisconnected);
118
+ };
119
+ ws.on("close", () => handleDisconnect("Connection closed"));
120
+ ws.on("error", (err) => {
121
+ this.logger.error(`Extension WS error: ${err.message}`);
122
+ // Set disconnected flag before terminate() so any synchronous close event
123
+ // emitted by terminate() sees the guard as already set.
124
+ handleDisconnect(`Error: ${err.message}`);
125
+ ws.terminate();
126
+ });
127
+ }
128
+ handleNotification(method, params) {
129
+ if (typeof params !== "object" || params === null) {
130
+ this.logger.debug(`Ignoring notification ${method} with invalid params`);
131
+ return;
132
+ }
133
+ const p = params;
134
+ switch (method) {
135
+ case "extension/diagnosticsChanged": {
136
+ const file = p.file;
137
+ const diagnostics = p.diagnostics;
138
+ if (typeof file !== "string" || !Array.isArray(diagnostics)) {
139
+ this.logger.debug("Ignoring malformed diagnosticsChanged notification");
140
+ return;
141
+ }
142
+ if (diagnostics.length === 0) {
143
+ this.latestDiagnostics.delete(file);
144
+ }
145
+ else {
146
+ this.latestDiagnostics.set(file, diagnostics);
147
+ // Cap diagnostics cache at 500 entries
148
+ if (this.latestDiagnostics.size > 500) {
149
+ const firstKey = this.latestDiagnostics.keys().next().value;
150
+ if (firstKey !== undefined)
151
+ this.latestDiagnostics.delete(firstKey);
152
+ }
153
+ }
154
+ // Update timestamp and forward to Claude Code
155
+ this.lastDiagnosticsUpdate = Date.now();
156
+ this.safeCallback(this.onDiagnosticsChanged, file, diagnostics);
157
+ for (const fn of [...this.diagnosticsListeners])
158
+ this.safeCallback(fn, file, diagnostics);
159
+ break;
160
+ }
161
+ case "extension/selectionChanged": {
162
+ if (typeof p.file !== "string" ||
163
+ typeof p.startLine !== "number" ||
164
+ typeof p.endLine !== "number" ||
165
+ typeof p.startColumn !== "number" ||
166
+ typeof p.endColumn !== "number" ||
167
+ typeof p.selectedText !== "string") {
168
+ this.logger.debug("Ignoring malformed selectionChanged notification");
169
+ return;
170
+ }
171
+ this.latestSelection = {
172
+ file: p.file,
173
+ startLine: p.startLine,
174
+ endLine: p.endLine,
175
+ startColumn: p.startColumn,
176
+ endColumn: p.endColumn,
177
+ selectedText: p.selectedText,
178
+ };
179
+ break;
180
+ }
181
+ case "extension/activeFileChanged": {
182
+ if (typeof p.file !== "string") {
183
+ this.logger.debug("Ignoring malformed activeFileChanged notification");
184
+ return;
185
+ }
186
+ this.latestActiveFile = p.file;
187
+ break;
188
+ }
189
+ case "extension/aiCommentsChanged": {
190
+ const comments = p.comments;
191
+ if (!Array.isArray(comments)) {
192
+ this.logger.debug("Ignoring malformed aiCommentsChanged notification");
193
+ return;
194
+ }
195
+ this.latestAIComments.clear();
196
+ for (const c of comments) {
197
+ const entry = c;
198
+ if (typeof entry.file !== "string")
199
+ continue;
200
+ const existing = this.latestAIComments.get(entry.file) || [];
201
+ existing.push(entry);
202
+ this.latestAIComments.set(entry.file, existing);
203
+ }
204
+ // Cap at 200 files
205
+ if (this.latestAIComments.size > 200) {
206
+ const firstKey = this.latestAIComments.keys().next().value;
207
+ if (firstKey !== undefined)
208
+ this.latestAIComments.delete(firstKey);
209
+ }
210
+ this.safeCallback(this.onAICommentsChanged, comments);
211
+ break;
212
+ }
213
+ case "extension/fileChanged": {
214
+ const id = p.id;
215
+ const type = p.type;
216
+ const file = p.file;
217
+ if (typeof id === "string" &&
218
+ typeof type === "string" &&
219
+ typeof file === "string") {
220
+ this.safeCallback(this.onFileChanged, id, type, file);
221
+ }
222
+ break;
223
+ }
224
+ case "extension/hello": {
225
+ const extVer = typeof p.extensionVersion === "string"
226
+ ? p.extensionVersion
227
+ : "unknown";
228
+ this.logger.info(`Extension hello: version=${extVer}`);
229
+ const [extMajor] = extVer.split(".").map(Number);
230
+ const [bridgeMajor] = BRIDGE_PROTOCOL_VERSION.split(".").map(Number);
231
+ if (extMajor !== bridgeMajor) {
232
+ this.logger.warn(`Extension major version mismatch: bridge=${BRIDGE_PROTOCOL_VERSION}, extension=${extVer}. Consider updating.`);
233
+ }
234
+ break;
235
+ }
236
+ case "extension/fileSaved":
237
+ break;
238
+ case "extension/debugSessionChanged": {
239
+ if (typeof p.hasActiveSession !== "boolean") {
240
+ this.logger.debug("Ignoring malformed debugSessionChanged notification");
241
+ return;
242
+ }
243
+ const state = p;
244
+ this.latestDebugState = state;
245
+ this.safeCallback(this.onDebugSessionChanged, state);
246
+ break;
247
+ }
248
+ default:
249
+ this.logger.debug(`Unknown extension notification: ${method}`);
250
+ }
251
+ }
252
+ static MAX_PENDING_REQUESTS = 100;
253
+ async request(method, params, timeoutMs, signal) {
254
+ if (this.pendingRequests.size >= ExtensionClient.MAX_PENDING_REQUESTS) {
255
+ throw new Error(`Too many pending extension requests (${ExtensionClient.MAX_PENDING_REQUESTS})`);
256
+ }
257
+ // Exponential backoff — fast-fail if extension is repeatedly timing out
258
+ const now = Date.now();
259
+ if (now < this.extensionSuspendedUntil) {
260
+ throw new ExtensionTimeoutError(method);
261
+ }
262
+ // Half-open: backoff expired but failures recorded — allow one probe through
263
+ if (this.extensionFailures > 0 && !this.extensionHalfOpen) {
264
+ this.extensionHalfOpen = true;
265
+ this.logger.debug(`Extension circuit breaker half-open — probing with ${method}`);
266
+ }
267
+ if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
268
+ throw new Error("Extension not connected");
269
+ }
270
+ if (signal?.aborted) {
271
+ throw new Error("Request aborted");
272
+ }
273
+ // Wait for backpressure to clear before sending
274
+ await waitForDrain(this.ws, this.logger, "Extension backpressure");
275
+ // Re-check after drain wait — socket may have closed
276
+ if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
277
+ throw new Error("Extension disconnected during drain wait");
278
+ }
279
+ const timeout = timeoutMs ?? REQUEST_TIMEOUT;
280
+ const id = this.nextId++;
281
+ if (this.nextId >= Number.MAX_SAFE_INTEGER)
282
+ this.nextId = 0;
283
+ const inner = new Promise((resolve, reject) => {
284
+ let settled = false;
285
+ const settle = (fn) => {
286
+ if (settled)
287
+ return;
288
+ settled = true;
289
+ fn();
290
+ };
291
+ const timer = setTimeout(() => {
292
+ this.pendingRequests.delete(id);
293
+ signal?.removeEventListener("abort", onAbort);
294
+ this.logger.warn(`Extension request ${method} timed out after ${timeout}ms`);
295
+ settle(() => reject(new ExtensionTimeoutError(method)));
296
+ }, timeout);
297
+ // Re-check abort after the async drain wait — signal may have fired during that gap.
298
+ // If already aborted, addEventListener would never fire so we must check here.
299
+ if (signal?.aborted) {
300
+ clearTimeout(timer);
301
+ settle(() => reject(new Error("Request aborted")));
302
+ return;
303
+ }
304
+ // Wire AbortSignal to cancel the pending request
305
+ const onAbort = () => {
306
+ this.pendingRequests.delete(id);
307
+ clearTimeout(timer);
308
+ settle(() => reject(new Error("Request aborted")));
309
+ };
310
+ if (signal) {
311
+ signal.addEventListener("abort", onAbort, { once: true });
312
+ }
313
+ this.pendingRequests.set(id, {
314
+ resolve: (value) => {
315
+ signal?.removeEventListener("abort", onAbort);
316
+ settle(() => resolve(value));
317
+ },
318
+ reject: (reason) => {
319
+ signal?.removeEventListener("abort", onAbort);
320
+ settle(() => reject(reason));
321
+ },
322
+ timer,
323
+ });
324
+ const data = JSON.stringify({ jsonrpc: "2.0", id, method, params });
325
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
326
+ try {
327
+ this.ws.send(data);
328
+ }
329
+ catch (err) {
330
+ this.pendingRequests.delete(id);
331
+ clearTimeout(timer);
332
+ signal?.removeEventListener("abort", onAbort);
333
+ settle(() => reject(new Error(`Failed to send extension request: ${err}`)));
334
+ }
335
+ }
336
+ else {
337
+ this.pendingRequests.delete(id);
338
+ clearTimeout(timer);
339
+ signal?.removeEventListener("abort", onAbort);
340
+ settle(() => reject(new Error("Extension disconnected before send")));
341
+ }
342
+ });
343
+ try {
344
+ const result = await inner;
345
+ // Success — reset backoff and half-open state
346
+ if (this.extensionFailures > 0) {
347
+ this.logger.warn("Extension backoff reset — connection recovered");
348
+ }
349
+ this.extensionFailures = 0;
350
+ this.extensionSuspendedUntil = 0;
351
+ this.extensionHalfOpen = false;
352
+ return result;
353
+ }
354
+ catch (err) {
355
+ if (err instanceof ExtensionTimeoutError) {
356
+ this.extensionHalfOpen = false;
357
+ const failures = ++this.extensionFailures;
358
+ // Full jitter (AWS-recommended): random in [1, cap] — prevents rhythmic
359
+ // retry storms when bridge and extension restart simultaneously.
360
+ const capMs = Math.min(1_000 * 2 ** (failures - 1), 60_000);
361
+ const backoffMs = Math.floor(Math.random() * capMs) + 1;
362
+ this.extensionSuspendedUntil = Date.now() + backoffMs;
363
+ this.logger.warn(`Extension timed out (failure #${failures}) — suspending for ${Math.round(backoffMs / 100) / 10}s`);
364
+ }
365
+ throw err;
366
+ }
367
+ }
368
+ /** Like request() but returns null on disconnect instead of rejecting.
369
+ * ExtensionTimeoutError is NOT caught — callers must handle it to distinguish
370
+ * timeout from genuine "no results" (null). */
371
+ async requestOrNull(method, params, timeoutMs, signal) {
372
+ return this.request(method, params, timeoutMs, signal).catch((err) => {
373
+ if (err instanceof ExtensionTimeoutError)
374
+ throw err;
375
+ return null;
376
+ });
377
+ }
378
+ rejectAllPending(reason) {
379
+ for (const [_id, pending] of this.pendingRequests) {
380
+ clearTimeout(pending.timer);
381
+ pending.reject(new Error(reason));
382
+ }
383
+ this.pendingRequests.clear();
384
+ }
385
+ /** Typed shorthand for requestOrNull — returns T | null without an intermediate variable */
386
+ async proxy(method, params, timeout, signal) {
387
+ return this.requestOrNull(method, params, timeout, signal);
388
+ }
389
+ async getDiagnostics(file) {
390
+ return this.proxy("extension/getDiagnostics", { file });
391
+ }
392
+ async getSelection() {
393
+ return this.proxy("extension/getSelection");
394
+ }
395
+ async getOpenFiles() {
396
+ return this.proxy("extension/getOpenFiles");
397
+ }
398
+ async isDirty(file) {
399
+ return this.proxy("extension/isDirty", { file });
400
+ }
401
+ async getFileContent(file) {
402
+ return this.requestOrNull("extension/getFileContent", { file });
403
+ }
404
+ async openFile(file, line) {
405
+ const result = await this.requestOrNull("extension/openFile", {
406
+ file,
407
+ line,
408
+ });
409
+ return result === true;
410
+ }
411
+ async saveFile(file) {
412
+ const result = await this.requestOrNull("extension/saveFile", { file });
413
+ return result === true;
414
+ }
415
+ async closeTab(file) {
416
+ const result = await this.requestOrNull("extension/closeTab", { file });
417
+ return result === true;
418
+ }
419
+ async getAIComments() {
420
+ return this.proxy("extension/getAIComments");
421
+ }
422
+ // --- LSP Semantic Features ---
423
+ async goToDefinition(file, line, column, signal) {
424
+ return this.requestOrNull("extension/goToDefinition", { file, line, column }, undefined, signal);
425
+ }
426
+ async findReferences(file, line, column, signal) {
427
+ return this.requestOrNull("extension/findReferences", { file, line, column }, undefined, signal);
428
+ }
429
+ async getHover(file, line, column, signal) {
430
+ return this.requestOrNull("extension/getHover", { file, line, column }, undefined, signal);
431
+ }
432
+ async getCodeActions(file, startLine, startColumn, endLine, endColumn) {
433
+ return this.requestOrNull("extension/getCodeActions", {
434
+ file,
435
+ startLine,
436
+ startColumn,
437
+ endLine,
438
+ endColumn,
439
+ });
440
+ }
441
+ async applyCodeAction(file, startLine, startColumn, endLine, endColumn, actionTitle) {
442
+ return this.requestOrNull("extension/applyCodeAction", {
443
+ file,
444
+ startLine,
445
+ startColumn,
446
+ endLine,
447
+ endColumn,
448
+ actionTitle,
449
+ });
450
+ }
451
+ async renameSymbol(file, line, column, newName) {
452
+ // Rename can be slow on large projects
453
+ return this.requestOrNull("extension/renameSymbol", { file, line, column, newName }, 15_000);
454
+ }
455
+ async searchSymbols(query, maxResults) {
456
+ return this.requestOrNull("extension/searchSymbols", { query, maxResults });
457
+ }
458
+ async watchFiles(id, pattern) {
459
+ return this.requestOrNull("extension/watchFiles", { id, pattern });
460
+ }
461
+ async unwatchFiles(id) {
462
+ return this.requestOrNull("extension/unwatchFiles", { id });
463
+ }
464
+ // --- Terminal Features ---
465
+ async listTerminals() {
466
+ return this.requestOrNull("extension/listTerminals");
467
+ }
468
+ async getTerminalOutput(name, index, lines) {
469
+ return this.requestOrNull("extension/getTerminalOutput", {
470
+ name,
471
+ index,
472
+ lines,
473
+ });
474
+ }
475
+ async disposeTerminal(name, index) {
476
+ return this.requestOrNull("extension/disposeTerminal", { name, index });
477
+ }
478
+ // --- File Operations ---
479
+ async createFile(filePath, content, isDirectory, overwrite, openAfterCreate) {
480
+ return this.requestOrNull("extension/createFile", {
481
+ filePath,
482
+ content,
483
+ isDirectory,
484
+ overwrite,
485
+ openAfterCreate,
486
+ });
487
+ }
488
+ async deleteFile(filePath, recursive, useTrash) {
489
+ return this.requestOrNull("extension/deleteFile", {
490
+ filePath,
491
+ recursive,
492
+ useTrash,
493
+ });
494
+ }
495
+ async renameFile(oldPath, newPath, overwrite) {
496
+ return this.requestOrNull("extension/renameFile", {
497
+ oldPath,
498
+ newPath,
499
+ overwrite,
500
+ });
501
+ }
502
+ // --- Text Editing ---
503
+ async editText(filePath, edits, save) {
504
+ return this.requestOrNull("extension/editText", { filePath, edits, save });
505
+ }
506
+ async replaceBlock(filePath, oldContent, newContent, save) {
507
+ return this.requestOrNull("extension/replaceBlock", {
508
+ filePath,
509
+ oldContent,
510
+ newContent,
511
+ save,
512
+ });
513
+ }
514
+ async getDocumentSymbols(file) {
515
+ return this.requestOrNull("extension/getDocumentSymbols", { file });
516
+ }
517
+ async getCallHierarchy(file, line, column, direction, maxResults) {
518
+ return this.requestOrNull("extension/getCallHierarchy", { file, line, column, direction, maxResults }, 15_000);
519
+ }
520
+ // --- Code Actions (format, fix, organize) ---
521
+ async formatDocument(file) {
522
+ return this.requestOrNull("extension/formatDocument", { file }, 15_000);
523
+ }
524
+ async fixAllLintErrors(file) {
525
+ return this.requestOrNull("extension/fixAllLintErrors", { file }, 15_000);
526
+ }
527
+ async organizeImports(file) {
528
+ return this.requestOrNull("extension/organizeImports", { file }, 15_000);
529
+ }
530
+ // --- Terminal Control ---
531
+ async createTerminal(name, cwd, env, show) {
532
+ return this.requestOrNull("extension/createTerminal", {
533
+ name,
534
+ cwd,
535
+ env,
536
+ show,
537
+ });
538
+ }
539
+ async sendTerminalCommand(text, name, index, addNewline) {
540
+ return this.requestOrNull("extension/sendTerminalCommand", {
541
+ text,
542
+ name,
543
+ index,
544
+ addNewline,
545
+ });
546
+ }
547
+ async waitForTerminalOutput(pattern, name, index, timeoutMs) {
548
+ const requestTimeout = (timeoutMs ?? 30_000) + 5_000;
549
+ return this.requestOrNull("extension/waitForTerminalOutput", { pattern, name, index, timeoutMs }, requestTimeout);
550
+ }
551
+ async executeInTerminal(command, name, index, timeoutMs, show) {
552
+ // Add 5s overhead beyond the command timeout so the bridge doesn't cut the extension off early
553
+ const requestTimeout = (timeoutMs ?? 30_000) + 5_000;
554
+ return this.requestOrNull("extension/executeInTerminal", { command, name, index, timeoutMs, show }, requestTimeout);
555
+ }
556
+ // --- Debug ---
557
+ async getDebugState() {
558
+ return this.proxy("extension/getDebugState");
559
+ }
560
+ async evaluateInDebugger(expression, frameId, context) {
561
+ return this.requestOrNull("extension/evaluateInDebugger", {
562
+ expression,
563
+ frameId,
564
+ context,
565
+ });
566
+ }
567
+ async setDebugBreakpoints(file, breakpoints) {
568
+ return this.requestOrNull("extension/setDebugBreakpoints", {
569
+ file,
570
+ breakpoints,
571
+ });
572
+ }
573
+ async startDebugging(configName) {
574
+ return this.requestOrNull("extension/startDebugging", { configName }, 15_000);
575
+ }
576
+ async stopDebugging() {
577
+ return this.requestOrNull("extension/stopDebugging");
578
+ }
579
+ // --- Decorations ---
580
+ async setDecorations(id, file, decorations) {
581
+ return this.requestOrNull("extension/setDecorations", {
582
+ id,
583
+ file,
584
+ decorations,
585
+ });
586
+ }
587
+ async clearDecorations(id) {
588
+ return this.requestOrNull("extension/clearDecorations", { id });
589
+ }
590
+ // --- VS Code Commands ---
591
+ async executeVSCodeCommand(command, args) {
592
+ return this.requestOrNull("extension/executeVSCodeCommand", {
593
+ command,
594
+ args,
595
+ });
596
+ }
597
+ async listVSCodeCommands(filter) {
598
+ return this.proxy("extension/listVSCodeCommands", { filter });
599
+ }
600
+ // --- Workspace Settings ---
601
+ async getWorkspaceSettings(section, target) {
602
+ return this.requestOrNull("extension/getWorkspaceSettings", {
603
+ section,
604
+ target,
605
+ });
606
+ }
607
+ async setWorkspaceSetting(key, value, target) {
608
+ return this.requestOrNull("extension/setWorkspaceSetting", {
609
+ key,
610
+ value,
611
+ target,
612
+ });
613
+ }
614
+ // --- Clipboard ---
615
+ async readClipboard() {
616
+ return this.requestOrNull("extension/readClipboard");
617
+ }
618
+ async writeClipboard(text) {
619
+ return this.requestOrNull("extension/writeClipboard", { text });
620
+ }
621
+ // --- Inlay Hints ---
622
+ async getInlayHints(file, startLine, endLine) {
623
+ return this.requestOrNull("extension/getInlayHints", {
624
+ file,
625
+ startLine,
626
+ endLine,
627
+ });
628
+ }
629
+ // --- Type Hierarchy ---
630
+ async getTypeHierarchy(file, line, column, direction, maxResults) {
631
+ return this.requestOrNull("extension/getTypeHierarchy", { file, line, column, direction, maxResults }, 15_000);
632
+ }
633
+ // --- Tasks ---
634
+ async listTasks() {
635
+ return this.requestOrNull("extension/listTasks", undefined, 15_000);
636
+ }
637
+ async runTask(name, type, timeoutMs) {
638
+ const requestTimeout = (timeoutMs ?? 60_000) + 5_000;
639
+ return this.requestOrNull("extension/runTask", { name, type, timeoutMs }, requestTimeout);
640
+ }
641
+ // --- Workspace Folders ---
642
+ async getWorkspaceFolders() {
643
+ return this.proxy("extension/getWorkspaceFolders");
644
+ }
645
+ // --- Notebook ---
646
+ async getNotebookCells(file) {
647
+ return this.requestOrNull("extension/getNotebookCells", { file });
648
+ }
649
+ async runNotebookCell(file, cellIndex, timeoutMs) {
650
+ const requestTimeout = (timeoutMs ?? 30_000) + 5_000;
651
+ return this.requestOrNull("extension/runNotebookCell", { file, cellIndex, timeoutMs }, requestTimeout);
652
+ }
653
+ async getNotebookOutput(file, cellIndex) {
654
+ return this.requestOrNull("extension/getNotebookOutput", {
655
+ file,
656
+ cellIndex,
657
+ });
658
+ }
659
+ // --- Diagnostics Helpers ---
660
+ getCachedDiagnostics(file) {
661
+ if (file)
662
+ return this.latestDiagnostics.get(file) ?? [];
663
+ return Array.from(this.latestDiagnostics.values()).flat();
664
+ }
665
+ addDiagnosticsListener(listener) {
666
+ this.diagnosticsListeners.add(listener);
667
+ return () => this.diagnosticsListeners.delete(listener);
668
+ }
669
+ /** Notify the extension about Claude Code connection state changes */
670
+ notifyClaudeConnectionState(connected) {
671
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
672
+ return;
673
+ void safeSend(this.ws, JSON.stringify({
674
+ jsonrpc: "2.0",
675
+ method: "bridge/claudeConnectionChanged",
676
+ params: { connected },
677
+ }), this.logger);
678
+ }
679
+ getCircuitBreakerState() {
680
+ return {
681
+ suspended: Date.now() < this.extensionSuspendedUntil,
682
+ suspendedUntil: this.extensionSuspendedUntil,
683
+ failures: this.extensionFailures,
684
+ };
685
+ }
686
+ isConnected() {
687
+ return this.connected;
688
+ }
689
+ disconnect() {
690
+ if (this.ws) {
691
+ this.ws.close(1000, "Bridge shutting down");
692
+ this.ws = null;
693
+ }
694
+ this.connected = false;
695
+ this.rejectAllPending("Bridge shutting down");
696
+ }
697
+ }
698
+ //# sourceMappingURL=extensionClient.js.map