chrome-ai-bridge 2.3.9 → 2.4.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.
@@ -146,8 +146,15 @@ export class NetworkInterceptor {
146
146
  const isSSE = contentType.includes('text/event-stream');
147
147
  if (isResponseUrl(url) || isSSE) {
148
148
  this.pendingBodies.add(requestId);
149
- this.fetchResponseBody(requestId, url).catch(() => {
150
- // Best-effort; failures are expected for some responses
149
+ this.fetchResponseBody(requestId, url).catch((err) => {
150
+ console.error(`[NetworkInterceptor] fetchResponseBody error for ${url.slice(0, 80)}: ${err instanceof Error ? err.message : String(err)}`);
151
+ });
152
+ }
153
+ else if (!url) {
154
+ // Speculative capture: requestWillBeSent was missed (tab reuse scenario)
155
+ // Try fetching body and check if it looks like ChatGPT SSE or Gemini response
156
+ this.speculativeFetchBody(requestId).catch(() => {
157
+ // Best-effort; silent failure expected
151
158
  });
152
159
  }
153
160
  });
@@ -163,40 +170,87 @@ export class NetworkInterceptor {
163
170
  this.client.on(event, wrappedHandler);
164
171
  }
165
172
  async fetchResponseBody(requestId, url) {
166
- try {
167
- const result = await this.client.send('Network.getResponseBody', { requestId });
168
- if (result?.body) {
169
- let data;
170
- if (result.base64Encoded) {
171
- try {
172
- data = Buffer.from(result.body, 'base64').toString('utf-8');
173
+ const maxRetries = 2;
174
+ const retryDelayMs = 500;
175
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
176
+ try {
177
+ const result = await this.client.send('Network.getResponseBody', { requestId });
178
+ if (result?.body) {
179
+ let data;
180
+ if (result.base64Encoded) {
181
+ try {
182
+ data = Buffer.from(result.body, 'base64').toString('utf-8');
183
+ }
184
+ catch (decodeErr) {
185
+ console.error(`[NetworkInterceptor] Base64 decode failed for ${url.slice(0, 80)}: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
186
+ this.pendingBodies.delete(requestId);
187
+ return;
188
+ }
173
189
  }
174
- catch (decodeErr) {
175
- console.error(`[NetworkInterceptor] Base64 decode failed for ${url.slice(0, 80)}: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
176
- return;
190
+ else {
191
+ data = result.body;
177
192
  }
193
+ this.frames.push({
194
+ timestamp: Date.now() / 1000,
195
+ type: 'fetch-body',
196
+ requestId,
197
+ url,
198
+ data,
199
+ });
200
+ console.error(`[NetworkInterceptor] Body captured: ${url.slice(0, 80)} (${data.length} bytes)`);
201
+ }
202
+ this.pendingBodies.delete(requestId);
203
+ return;
204
+ }
205
+ catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ const isRetryable = msg.includes('No resource') || msg.includes('No data found');
208
+ if (isRetryable && attempt < maxRetries) {
209
+ console.error(`[NetworkInterceptor] getResponseBody retry ${attempt + 1}/${maxRetries} for ${url.slice(0, 80)}`);
210
+ await new Promise(r => setTimeout(r, retryDelayMs));
211
+ continue;
212
+ }
213
+ if (!isRetryable) {
214
+ console.error(`[NetworkInterceptor] getResponseBody failed for ${url.slice(0, 80)}: ${msg}`);
178
215
  }
179
216
  else {
180
- data = result.body;
217
+ console.error(`[NetworkInterceptor] getResponseBody failed after ${maxRetries} retries for ${url.slice(0, 80)}: ${msg}`);
181
218
  }
219
+ this.pendingBodies.delete(requestId);
220
+ return;
221
+ }
222
+ }
223
+ this.pendingBodies.delete(requestId);
224
+ }
225
+ /**
226
+ * Speculative body fetch for requests where requestWillBeSent was missed
227
+ * (e.g., tab reuse). Checks if the body looks like a ChatGPT or Gemini response.
228
+ */
229
+ async speculativeFetchBody(requestId) {
230
+ try {
231
+ const result = await this.client.send('Network.getResponseBody', { requestId });
232
+ if (!result?.body)
233
+ return;
234
+ const data = result.base64Encoded
235
+ ? Buffer.from(result.body, 'base64').toString('utf-8')
236
+ : result.body;
237
+ // Check if body looks like ChatGPT SSE (contains "data: " lines and [DONE])
238
+ const isChatGPTSSE = data.includes('data: ') && data.includes('[DONE]');
239
+ // Check if body looks like Gemini response (starts with )]}')
240
+ const isGemini = data.startsWith(")]}'");
241
+ if (isChatGPTSSE || isGemini) {
182
242
  this.frames.push({
183
243
  timestamp: Date.now() / 1000,
184
244
  type: 'fetch-body',
185
245
  requestId,
186
- url,
246
+ url: '<speculative>',
187
247
  data,
188
248
  });
249
+ console.error(`[NetworkInterceptor] Speculative capture hit: ${isChatGPTSSE ? 'ChatGPT SSE' : 'Gemini'} (${data.length} bytes)`);
189
250
  }
190
251
  }
191
- catch (err) {
192
- // Common: "No resource with given identifier" for redirects/cancelled requests
193
- const msg = err instanceof Error ? err.message : String(err);
194
- if (!msg.includes('No resource') && !msg.includes('No data found')) {
195
- console.error(`[NetworkInterceptor] getResponseBody failed for ${url.slice(0, 80)}: ${msg}`);
196
- }
197
- }
198
- finally {
199
- this.pendingBodies.delete(requestId);
252
+ catch {
253
+ // Silent: speculative capture is best-effort
200
254
  }
201
255
  }
202
256
  stopCapture() {
@@ -212,14 +266,30 @@ export class NetworkInterceptor {
212
266
  }
213
267
  /**
214
268
  * Wait for all pending response body fetches, then stop capture.
269
+ * IMPORTANT: capturing remains true during the wait so that late-arriving
270
+ * loadingFinished events are still processed (this was the root cause of
271
+ * textLength=0 — setting capturing=false first dropped those events).
215
272
  */
216
- async stopCaptureAndWait(timeoutMs = 3000) {
217
- this.capturing = false; // Stop receiving new events
218
- // Wait for pending body fetches
273
+ async stopCaptureAndWait(timeoutMs = 15000) {
274
+ // Phase 1: Wait for pending body fetches (capturing stays true)
219
275
  const deadline = Date.now() + timeoutMs;
220
276
  while (this.pendingBodies.size > 0 && Date.now() < deadline) {
221
277
  await new Promise(r => setTimeout(r, 100));
222
278
  }
279
+ // Phase 2: Grace period — if no frames captured yet, wait for late loadingFinished
280
+ if (this.frames.length === 0) {
281
+ const graceDeadline = Math.min(Date.now() + 3000, deadline);
282
+ console.error('[NetworkInterceptor] No frames yet, waiting grace period for late events...');
283
+ while (this.frames.length === 0 && Date.now() < graceDeadline) {
284
+ await new Promise(r => setTimeout(r, 100));
285
+ // Also wait for any new pending bodies that appeared during grace
286
+ while (this.pendingBodies.size > 0 && Date.now() < graceDeadline) {
287
+ await new Promise(r => setTimeout(r, 100));
288
+ }
289
+ }
290
+ }
291
+ // Phase 3: Now stop capturing
292
+ this.capturing = false;
223
293
  // Warn if pending bodies remain after timeout
224
294
  if (this.pendingBodies.size > 0) {
225
295
  console.warn(`[NetworkInterceptor] Timeout: ${this.pendingBodies.size} pending bodies abandoned (requestIds: ${[...this.pendingBodies].slice(0, 3).join(', ')}${this.pendingBodies.size > 3 ? '...' : ''})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "2.3.9",
3
+ "version": "2.4.0",
4
4
  "description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "files": [
48
48
  "build/src",
49
+ "build/extension",
49
50
  "scripts/cli.mjs",
50
51
  "scripts/browser-globals-mock.mjs",
51
52
  "README.md",