github-router 0.3.250 → 0.3.256

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 (95) hide show
  1. package/dist/attribution-settings-jqkXfeVr.js +2347 -0
  2. package/dist/attribution-settings-jqkXfeVr.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/background.js +406 -30
  6. package/dist/browser-ext/manifest.json +1 -1
  7. package/dist/check-usage-6PB5IUFb.js +43 -0
  8. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  9. package/dist/claude-Cpy7b9gd.js +593 -0
  10. package/dist/claude-Cpy7b9gd.js.map +1 -0
  11. package/dist/client-CQMfroGV.js +413 -0
  12. package/dist/client-CQMfroGV.js.map +1 -0
  13. package/dist/codex-BJK_QVsI.js +85 -0
  14. package/dist/codex-BJK_QVsI.js.map +1 -0
  15. package/dist/debug-CjmE9sgG.js +81 -0
  16. package/dist/debug-CjmE9sgG.js.map +1 -0
  17. package/dist/engine-DXn5XmFp.js +2 -0
  18. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  19. package/dist/exec-DTO2oin0.js.map +1 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  21. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  22. package/dist/gate-discovery-Bm1nxHSU.js +635 -0
  23. package/dist/gate-discovery-Bm1nxHSU.js.map +1 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  25. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  26. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  27. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  29. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  30. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  32. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  34. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  36. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  38. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  39. package/dist/internal-stop-hook-BaBkqJyg.js +212 -0
  40. package/dist/internal-stop-hook-BaBkqJyg.js.map +1 -0
  41. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  42. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  44. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  46. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  47. package/dist/lifecycle-Byw5s1G8.js +134 -0
  48. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  49. package/dist/lifecycle-D80CaWie.js +2 -0
  50. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  51. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  52. package/dist/lifecycle-DzqCqDlM.js +2 -0
  53. package/dist/main.js +43 -12904
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  56. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  57. package/dist/models-B6UnGyRo.js +158 -0
  58. package/dist/models-B6UnGyRo.js.map +1 -0
  59. package/dist/orchestration-hRU4TdWr.js +39 -0
  60. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  61. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  62. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  63. package/dist/paths-CwKwc2pi.js +2 -0
  64. package/dist/{peer-mcp-personas-BvommiSI.js → peer-mcp-personas-BPl6_V2A.js} +288 -2445
  65. package/dist/peer-mcp-personas-BPl6_V2A.js.map +1 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  67. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  69. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  70. package/dist/provision-DueN4M8G.js +656 -0
  71. package/dist/provision-DueN4M8G.js.map +1 -0
  72. package/dist/serve-GN-flJmF.js +1511 -0
  73. package/dist/serve-GN-flJmF.js.map +1 -0
  74. package/dist/server-setup-BJL-X7w6.js +4776 -0
  75. package/dist/server-setup-BJL-X7w6.js.map +1 -0
  76. package/dist/start-CE0k-weK.js +129 -0
  77. package/dist/start-CE0k-weK.js.map +1 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  79. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  81. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  82. package/dist/token-R2ocFC3M.js +1054 -0
  83. package/dist/token-R2ocFC3M.js.map +1 -0
  84. package/dist/version-_Q1WpsQp.js +32 -0
  85. package/dist/version-_Q1WpsQp.js.map +1 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  87. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  88. package/package.json +1 -1
  89. package/dist/engine-dOYoMQis.js +0 -2
  90. package/dist/lifecycle-0rXrwjWk.js +0 -2
  91. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  92. package/dist/lifecycle-hzPwXc3I.js +0 -2
  93. package/dist/paths-BjTMI_xK.js.map +0 -1
  94. package/dist/paths-D3XgmTcQ.js +0 -2
  95. package/dist/peer-mcp-personas-BvommiSI.js.map +0 -1
@@ -159,42 +159,407 @@ async function toolNavigate(args) {
159
159
  return { finalUrl: t.url, statusCode: t.status === "complete" ? 200 : 0 }
160
160
  }
161
161
 
162
+ // Window-remediation policy for pixel capture. Resolved proxy-side from
163
+ // GH_ROUTER_BROWSER_WINDOW_POLICY and passed down per call, because the
164
+ // extension has no access to the proxy's environment. Kept permissive by
165
+ // default so a capture against a minimized window succeeds rather than
166
+ // erroring; set the env var to "deny" to keep the proxy's hands off
167
+ // window state entirely.
168
+ const WINDOW_POLICIES = new Set(["deny", "restore", "foreground"])
169
+ const DEFAULT_WINDOW_POLICY = "foreground"
170
+
171
+ // Screenshot failure codes. These travel back to the caller so a model
172
+ // gets a stated cause and remedy instead of the dispatcher's blunt
173
+ // "timeout after 15000ms", and so it can tell a retryable failure from
174
+ // one that needs a state change and will never succeed on retry.
175
+ const SHOT_ERR = {
176
+ BAD_TAB_ID: "BAD_TAB_ID",
177
+ WINDOW_MINIMIZED: "WINDOW_MINIMIZED",
178
+ CAPTURE_STALLED: "CAPTURE_STALLED",
179
+ TARGET_CHANGED: "TARGET_CHANGED",
180
+ QUEUE_BUSY: "QUEUE_BUSY",
181
+ RATE_LIMITED: "RATE_LIMITED",
182
+ }
183
+
184
+ /**
185
+ * Chrome's own per-second cap on captureVisibleTab. Matched on the message
186
+ * because the API surfaces it as a plain Error with no code of its own.
187
+ */
188
+ function isCaptureQuotaError(err) {
189
+ return Boolean(
190
+ err
191
+ && typeof err.message === "string"
192
+ && /MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND|exceeds the .*quota/i.test(err.message),
193
+ )
194
+ }
195
+
196
+ /** Chrome's quota window is one second; back off just past it before retrying. */
197
+ const CAPTURE_QUOTA_BACKOFF_MS = 1_100
198
+
199
+ function shotError(code, message) {
200
+ const err = new Error(message)
201
+ err.code = code
202
+ return err
203
+ }
204
+
205
+ /**
206
+ * Internal deadline for the whole screenshot operation. The dispatcher
207
+ * gives us 15 s (PER_TOOL_TIMEOUTS.browser_screenshot); we must return a
208
+ * typed diagnostic comfortably before that, or the caller gets the bare
209
+ * timeout string and learns nothing.
210
+ */
211
+ const SHOT_DEADLINE_MS = 12_000
212
+ /**
213
+ * How long to wait on a capture that has already passed the paintability
214
+ * pre-check before declaring it stalled.
215
+ *
216
+ * Deliberately generous. A healthy capture returns in tens of
217
+ * milliseconds, but a 4K viewport encoding a heavy page to PNG can take
218
+ * a second or two on a loaded machine, and calling that "stalled" would
219
+ * turn a slow success into a hard failure. The point of this watchdog is
220
+ * to return a *typed, explanatory* error instead of the dispatcher's
221
+ * anonymous timeout — not to shave seconds — so it is set well above any
222
+ * realistic capture time while still leaving room under the 12 s
223
+ * internal deadline.
224
+ */
225
+ const SHOT_WATCHDOG_MS = 6_000
226
+ /** Paint settle after restoring or activating. Not a paint barrier — see note in toolScreenshot. */
227
+ const SHOT_PAINT_SETTLE_MS = 250
228
+
229
+ /**
230
+ * Make `windowId` paintable if policy allows; throw if policy forbids it.
231
+ *
232
+ * `captureVisibleTab` reads from the compositor. A minimized window has
233
+ * no compositing surface, and a tab that has NEVER been composited in
234
+ * one has no frame to hand back — so the call does not reject, it simply
235
+ * never settles. That much is measured. What is NOT established is
236
+ * whether a capture already in flight recovers once the window becomes
237
+ * paintable again, so we do not rely on either answer: we check BEFORE
238
+ * starting a capture, which is correct regardless, and it is also the
239
+ * only point at which we can still choose not to start one.
240
+ *
241
+ * `policy` is resolved proxy-side (the extension cannot read env) and is
242
+ * one of "deny" | "restore" | "foreground".
243
+ */
244
+ async function ensureWindowPaintable(windowId, policy) {
245
+ if (typeof windowId !== "number") return
246
+ let win
247
+ try {
248
+ win = await chrome.windows.get(windowId)
249
+ } catch {
250
+ win = undefined
251
+ }
252
+ if (!win) {
253
+ // State is unknown. Under `deny` the whole contract is "never touch
254
+ // the window, and don't start a capture that can't work", so an
255
+ // unknown state must fail closed rather than fall through to a
256
+ // capture that may hang. Under the permissive policies, proceed and
257
+ // let the capture path surface the real error.
258
+ if (policy === "deny") {
259
+ throw shotError(
260
+ SHOT_ERR.WINDOW_MINIMIZED,
261
+ "browser_screenshot: could not determine the target window's state, and window policy is 'deny' so it was left untouched. "
262
+ + "Make sure the browser window is open and visible, then retry.",
263
+ )
264
+ }
265
+ return
266
+ }
267
+ if (win.state !== "minimized") return
268
+
269
+ if (policy === "deny") {
270
+ throw shotError(
271
+ SHOT_ERR.WINDOW_MINIMIZED,
272
+ "browser_screenshot: the target window is minimized, so the browser is not painting it and a capture cannot succeed. "
273
+ + "Window policy is 'deny', so the window was left untouched. Restore the browser window and retry, "
274
+ + "or set GH_ROUTER_BROWSER_WINDOW_POLICY=restore to let the proxy un-minimize it automatically.",
275
+ )
276
+ }
277
+
278
+ // Prefer `{focused: true}` alone for the foreground policy: on Windows
279
+ // that un-minimizes to the window's PREVIOUS state, so a maximized
280
+ // window stays maximized. Forcing `state: "normal"` would silently
281
+ // demote it to a floating window and corrupt the user's layout.
282
+ if (policy === "foreground") {
283
+ try {
284
+ await chrome.windows.update(windowId, { focused: true })
285
+ await sleep(SHOT_PAINT_SETTLE_MS)
286
+ const after = await chrome.windows.get(windowId)
287
+ if (after && after.state !== "minimized") return
288
+ } catch {
289
+ // fall through to the explicit state set
290
+ }
291
+ }
292
+
293
+ // `restore` (and the foreground fallback): there is no API to
294
+ // un-minimize to the prior state without focusing, so this is the only
295
+ // lever available, at the cost of demoting a maximized window.
296
+ // Deliberately omit `focused` rather than passing `focused: false` —
297
+ // the explicit false has its own focus/z-order semantics, whereas
298
+ // omission genuinely leaves focus alone.
299
+ await chrome.windows.update(windowId, { state: "normal" })
300
+ await sleep(SHOT_PAINT_SETTLE_MS)
301
+ }
302
+
303
+ /**
304
+ * Serialize pixel capture per WINDOW.
305
+ *
306
+ * `captureVisibleTab` snapshots whichever tab is active in the window at
307
+ * capture time, not the tab we asked for. Two concurrent screenshots of
308
+ * different tabs in the same window would each activate their own tab
309
+ * and then race, so one could return the other's pixels — handing a
310
+ * model page content it never asked for. Note this is per-WINDOW and so
311
+ * cannot reuse `withTabInputLock`, which is keyed by tab: two different
312
+ * tabs of one window take different tab locks and would not exclude each
313
+ * other at all, which is exactly the race we need to prevent.
314
+ *
315
+ * The hold cap matters more here than for input: a capture with no
316
+ * rendering surface never settles, so without a cap one stuck call would
317
+ * wedge every future capture on that window for the life of the service
318
+ * worker.
319
+ */
320
+ /**
321
+ * Captures that were started but have not settled, per window.
322
+ *
323
+ * The per-window lock alone does NOT bound these. When the watchdog
324
+ * fires we throw and release the lock, but the underlying
325
+ * `captureVisibleTab` is not cancellable and stays pending — so without
326
+ * this, every retry would start another one and they would pile up.
327
+ *
328
+ * A pending entry clears itself if the capture ever settles, so this
329
+ * self-heals: if making the window visible lets a stuck capture finally
330
+ * complete, the count drops and captures resume. (Whether a stalled
331
+ * capture recovers once the window becomes paintable is not something
332
+ * we have measured, so this deliberately does not assume either way.)
333
+ */
334
+ const windowPendingCaptures = new Map()
335
+
336
+ const windowCaptureLockTails = new Map()
337
+ // Backstop only. `fn()` bounds itself with SHOT_DEADLINE_MS, so this cap
338
+ // should never fire in normal operation; it sits just above that
339
+ // deadline so a genuinely wedged capture releases the window shortly
340
+ // after its own call has died, rather than holding it for the life of
341
+ // the service worker.
342
+ const WINDOW_CAPTURE_LOCK_HOLD_CAP_MS = 14_000
343
+
344
+ async function withWindowCaptureLock(windowId, fn) {
345
+ const key = typeof windowId === "number" ? windowId : "current"
346
+ const previousTail = windowCaptureLockTails.get(key) || Promise.resolve()
347
+ let release
348
+ const myTurn = new Promise((r) => { release = r })
349
+ const newTail = previousTail.then(() => myTurn)
350
+ windowCaptureLockTails.set(key, newTail)
351
+ await previousTail
352
+ let timer
353
+ try {
354
+ return await Promise.race([
355
+ fn(),
356
+ new Promise((_, reject) => {
357
+ timer = setTimeout(() => {
358
+ reject(shotError(
359
+ SHOT_ERR.CAPTURE_STALLED,
360
+ `browser_screenshot: capture lock held > ${WINDOW_CAPTURE_LOCK_HOLD_CAP_MS}ms on windowId=${key}. `
361
+ + "A previous capture never completed, which means the window has no rendering surface. "
362
+ + "Make the browser window visible and retry.",
363
+ ))
364
+ }, WINDOW_CAPTURE_LOCK_HOLD_CAP_MS)
365
+ }),
366
+ ])
367
+ } finally {
368
+ if (timer !== undefined) clearTimeout(timer)
369
+ release()
370
+ // Drop the entry when nobody queued behind us, so a browser session
371
+ // that opens and closes many windows doesn't accumulate dead keys
372
+ // for the life of the service worker.
373
+ if (windowCaptureLockTails.get(key) === newTail) windowCaptureLockTails.delete(key)
374
+ }
375
+ }
376
+
162
377
  async function toolScreenshot(args) {
163
- const tabId = typeof args.tabId === "number" ? args.tabId : undefined
378
+ const deadline = Date.now() + SHOT_DEADLINE_MS
164
379
  const format = args.format === "jpeg" ? "jpeg" : "png"
165
- // captureVisibleTab needs the tab's windowId, not the tab id.
380
+ const policy = WINDOW_POLICIES.has(args.windowPolicy) ? args.windowPolicy : DEFAULT_WINDOW_POLICY
381
+
382
+ // `if (tabId)` was wrong: chrome.tabs.TAB_ID_NONE is -1, which is
383
+ // truthy, so a "no tab" sentinel sailed through and we captured
384
+ // whatever window happened to be current.
385
+ let tabId
386
+ if (args.tabId !== undefined && args.tabId !== null) {
387
+ if (!Number.isInteger(args.tabId) || args.tabId === chrome.tabs.TAB_ID_NONE || args.tabId < 0) {
388
+ throw shotError(
389
+ SHOT_ERR.BAD_TAB_ID,
390
+ `browser_screenshot: tabId must be a valid tab id; got ${JSON.stringify(args.tabId)}. Use browser_list_tabs or browser_open_tab to get one.`,
391
+ )
392
+ }
393
+ tabId = args.tabId
394
+ }
395
+
396
+ // Resolve the target window BEFORE taking the lock, since the lock is
397
+ // keyed by window.
166
398
  let windowId
167
- if (tabId) {
399
+ if (tabId === undefined) {
400
+ // Resolve "current" deterministically instead of letting
401
+ // captureVisibleTab pick for us — "the current window" from a
402
+ // service worker is whatever Chrome last considered focused, which
403
+ // is not stable across SW dormancy.
404
+ const [activeTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
405
+ if (!activeTab) {
406
+ throw shotError(SHOT_ERR.BAD_TAB_ID, "browser_screenshot: no tabId given and no active tab could be resolved.")
407
+ }
408
+ tabId = activeTab.id
409
+ windowId = activeTab.windowId
410
+ } else {
411
+ windowId = (await chrome.tabs.get(tabId)).windowId
412
+ }
413
+
414
+ // Everything from activation through capture must be atomic per
415
+ // window: captureVisibleTab grabs whichever tab is active at capture
416
+ // time, so an interleaved second screenshot could activate its own tab
417
+ // and make this one return the wrong page's pixels.
418
+ return withWindowCaptureLock(windowId, async () => {
419
+ // Waiting for the lock spends the same budget the capture needs, so
420
+ // report contention AS contention. Reusing CAPTURE_STALLED here
421
+ // would blame the browser for what is actually our own queue.
422
+ if (Date.now() >= deadline) {
423
+ throw shotError(
424
+ SHOT_ERR.QUEUE_BUSY,
425
+ `browser_screenshot: another capture on window ${windowId} held the queue for the whole time budget. Retry.`,
426
+ )
427
+ }
428
+
168
429
  const tab = await chrome.tabs.get(tabId)
169
- windowId = tab.windowId
430
+ // Do NOT re-key onto the new window: the lock is held on the window
431
+ // resolved before acquisition, so proceeding against a different one
432
+ // would mutate and capture a window we hold no lock for — exactly
433
+ // the interleaving this lock exists to prevent.
434
+ if (tab.windowId !== windowId) {
435
+ throw shotError(
436
+ SHOT_ERR.TARGET_CHANGED,
437
+ `browser_screenshot: tab ${tabId} moved to another window while the capture was being set up. Retry.`,
438
+ )
439
+ }
440
+
441
+ // Make the window paintable BEFORE selecting the tab. Restoring a
442
+ // window yields the event loop, and any yield AFTER confirming the
443
+ // active tab reopens the window for the user or another caller to
444
+ // switch tabs — which captureVisibleTab would silently honour.
445
+ await ensureWindowPaintable(windowId, policy)
446
+
170
447
  if (!tab.active) {
171
- // Must activate the tab to capture it (captureVisibleTab is
172
- // window-scoped, snapshots the active tab of the named window).
173
448
  await chrome.tabs.update(tabId, { active: true })
174
- // Tiny pause so the renderer has a chance to paint after activation.
175
- await sleep(150)
449
+ await sleep(SHOT_PAINT_SETTLE_MS)
176
450
  }
177
- }
178
- // Both this API and CDP Page.captureScreenshot require the browser
179
- // to have a real OS-level rendering surface. On Chrome-for-Testing
180
- // launched in plain headed mode without --headless=new, no such
181
- // surface exists and either path hangs indefinitely — the Playwright
182
- // E2E harness passes --headless=new in its args list for exactly this
183
- // reason. Real Chrome with a visible window has a surface and works
184
- // fine. If you're driving Chrome-for-Testing programmatically and
185
- // need screenshots, launch with `--headless=new`.
186
- // `quality` (1-100) applies to JPEG only; Chrome ignores it for PNG. It is the
187
- // model's lever for shrinking a capture when a full-size one is refused for
188
- // exceeding a model's image-size limit without it, "retake it smaller" is
189
- // advice the caller cannot act on. An older extension build that predates this
190
- // simply ignores the extra key and captures at default quality.
191
- const captureOpts = { format }
192
- if (typeof args.quality === "number") captureOpts.quality = args.quality
193
- const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOpts)
194
- // dataUrl: "data:image/png;base64,...."
195
- const m = /^data:([^;]+);base64,(.*)$/.exec(dataUrl)
196
- if (!m) throw new Error("browser_screenshot: captureVisibleTab returned unexpected shape")
197
- return { contentType: m[1], dataBase64: m[2] }
451
+
452
+ // Last check before the capture, with no awaits in between.
453
+ const confirmed = await chrome.tabs.get(tabId)
454
+ if (!confirmed.active || confirmed.windowId !== windowId) {
455
+ throw shotError(
456
+ SHOT_ERR.TARGET_CHANGED,
457
+ `browser_screenshot: tab ${tabId} is not the active tab of window ${windowId} at capture time, so a capture would return a different page. Retry, or activate the tab first.`,
458
+ )
459
+ }
460
+
461
+ // `quality` (1-100) applies to JPEG only; Chrome ignores it for PNG. It is the
462
+ // model's lever for shrinking a capture when a full-size one is refused for
463
+ // exceeding a model's image-size limit without it, "retake it smaller" is
464
+ // advice the caller cannot act on.
465
+ const captureOpts = { format }
466
+ if (Number.isFinite(args.quality)) {
467
+ captureOpts.quality = Math.max(1, Math.min(100, Math.round(args.quality)))
468
+ }
469
+
470
+ // The deadline is a hard contract: overrunning it hands the caller
471
+ // back the dispatcher's anonymous timeout, which is the failure this
472
+ // whole path exists to eliminate. So no minimum-budget floor — if
473
+ // there is no time left, say so in terms the caller can act on.
474
+ const remaining = deadline - Date.now()
475
+ if (remaining <= 0) {
476
+ throw shotError(
477
+ SHOT_ERR.CAPTURE_STALLED,
478
+ "browser_screenshot: preparing the window used the entire time budget before a capture could start. "
479
+ + "The window is most likely not visible. Make it visible and retry.",
480
+ )
481
+ }
482
+ const budget = Math.min(SHOT_WATCHDOG_MS, remaining)
483
+
484
+ // One capture attempt. Kept as a closure so the rate-limit path below
485
+ // can run it a second time WITHOUT ever having two in flight: it is
486
+ // only re-invoked after the first attempt has REJECTED, i.e. settled.
487
+ const attemptCapture = async (budgetMs) => {
488
+ // Refuse to stack captures on a window that already has one stuck.
489
+ // The lock does not cover this: a stalled capture stays pending
490
+ // after we release, so without this guard each retry would add
491
+ // another unkillable in-flight call.
492
+ if ((windowPendingCaptures.get(windowId) || 0) > 0) {
493
+ throw shotError(
494
+ SHOT_ERR.CAPTURE_STALLED,
495
+ `browser_screenshot: an earlier capture on window ${windowId} never completed and cannot be cancelled, so a new one is not being started. `
496
+ + "Make the browser window visible; if that lets the earlier capture finish, captures resume automatically.",
497
+ )
498
+ }
499
+
500
+ windowPendingCaptures.set(windowId, (windowPendingCaptures.get(windowId) || 0) + 1)
501
+ const capture = chrome.tabs.captureVisibleTab(windowId, captureOpts)
502
+ // Doubles as the unhandled-rejection guard: the losing promise is
503
+ // unobservable but must not surface as an unhandled rejection.
504
+ // Promise.race still sees the original settlement either way.
505
+ const settled = () => {
506
+ const next = (windowPendingCaptures.get(windowId) || 1) - 1
507
+ if (next <= 0) windowPendingCaptures.delete(windowId)
508
+ else windowPendingCaptures.set(windowId, next)
509
+ }
510
+ capture.then(settled, settled)
511
+
512
+ // Sentinel rather than a mutable flag: the raced value itself tells us
513
+ // who won, so there is no ordering assumption between the timer's
514
+ // continuation and this one.
515
+ const STALLED = Symbol("stalled")
516
+ const raced = await Promise.race([capture, sleep(budgetMs).then(() => STALLED)])
517
+ if (raced === STALLED) {
518
+ throw shotError(
519
+ SHOT_ERR.CAPTURE_STALLED,
520
+ `browser_screenshot: the browser accepted the capture but produced no frame within ${budgetMs}ms. `
521
+ + "This means the target has no rendering surface — typically the window is minimized, fully covered, "
522
+ + "on another virtual desktop, or the session is locked. Make the browser window visible and retry.",
523
+ )
524
+ }
525
+ return raced
526
+ }
527
+
528
+ // Chrome rate-limits captureVisibleTab to a couple of calls per second
529
+ // per window and REJECTS past that with a raw quota message. An agent
530
+ // loop (screenshot -> act -> screenshot) hits this routinely, and the
531
+ // bare message tells it nothing — it reads like a hard failure when the
532
+ // only thing wrong is that it asked too fast. The per-window lock
533
+ // serializes captures but does not PACE them, so serialization alone
534
+ // does not avoid it.
535
+ //
536
+ // Retrying here is safe specifically because the first attempt
537
+ // REJECTED: there is no in-flight capture to overlap with, which is the
538
+ // condition that makes retrying a stalled capture unsafe.
539
+ let dataUrl
540
+ try {
541
+ dataUrl = await attemptCapture(budget)
542
+ } catch (err) {
543
+ const left = deadline - Date.now()
544
+ if (!isCaptureQuotaError(err) || left <= CAPTURE_QUOTA_BACKOFF_MS) throw err
545
+ await sleep(CAPTURE_QUOTA_BACKOFF_MS)
546
+ try {
547
+ dataUrl = await attemptCapture(Math.min(SHOT_WATCHDOG_MS, deadline - Date.now()))
548
+ } catch (retryErr) {
549
+ if (!isCaptureQuotaError(retryErr)) throw retryErr
550
+ throw shotError(
551
+ SHOT_ERR.RATE_LIMITED,
552
+ "browser_screenshot: the browser is rate-limiting screen captures (it allows only a couple per second per window) "
553
+ + "and still refused after a backoff. Nothing is broken — space captures out, or take one screenshot and work "
554
+ + "from it rather than re-capturing after every action.",
555
+ )
556
+ }
557
+ }
558
+
559
+ const m = /^data:([^;]+);base64,(.*)$/.exec(dataUrl || "")
560
+ if (!m) throw new Error("browser_screenshot: captureVisibleTab returned unexpected shape")
561
+ return { contentType: m[1], dataBase64: m[2] }
562
+ })
198
563
  }
199
564
 
200
565
  async function toolReadPage(args) {
@@ -2102,7 +2467,18 @@ async function handleBridgeRequest(req, port) {
2102
2467
  }
2103
2468
  }
2104
2469
  } catch (err) {
2105
- port.postMessage({ id: req.id, ok: false, error: err && err.message ? err.message : String(err) })
2470
+ // Forward the typed `code` when a handler set one. Without this a
2471
+ // caller sees only prose and cannot distinguish a failure that a
2472
+ // retry might fix from one that needs a state change first — which
2473
+ // is how a stuck capture read as an anonymous timeout.
2474
+ const code = err && typeof err.code === "string" ? err.code : undefined
2475
+ if (code) console.warn(`[browser-bridge] ${req.tool} failed (${code}):`, err.message)
2476
+ port.postMessage({
2477
+ id: req.id,
2478
+ ok: false,
2479
+ error: err && err.message ? err.message : String(err),
2480
+ ...(code ? { code } : {}),
2481
+ })
2106
2482
  }
2107
2483
  }
2108
2484
 
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "github-router browser bridge",
4
4
  "short_name": "gh-router-browser",
5
- "version": "0.3.250",
5
+ "version": "0.3.256",
6
6
  "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
8
  "background": {
@@ -0,0 +1,43 @@
1
+ import { i as ensurePaths } from "./paths-C1cLZ7Ga.js";
2
+ import { r as setupGitHubToken } from "./token-R2ocFC3M.js";
3
+ import { t as getCopilotUsage } from "./get-copilot-usage-DD1N0Xlh.js";
4
+ import { defineCommand } from "citty";
5
+ import consola from "consola";
6
+ //#region src/check-usage.ts
7
+ const checkUsage = defineCommand({
8
+ meta: {
9
+ name: "check-usage",
10
+ description: "Show current GitHub Copilot usage/quota information"
11
+ },
12
+ async run() {
13
+ await ensurePaths();
14
+ await setupGitHubToken();
15
+ try {
16
+ const usage = await getCopilotUsage();
17
+ const premium = usage.quota_snapshots.premium_interactions;
18
+ const premiumTotal = premium.entitlement;
19
+ const premiumUsed = premiumTotal - premium.remaining;
20
+ const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
21
+ const premiumPercentRemaining = premium.percent_remaining;
22
+ function summarizeQuota(name, snap) {
23
+ if (!snap) return `${name}: N/A`;
24
+ const total = snap.entitlement;
25
+ const used = total - snap.remaining;
26
+ const percentUsed = total > 0 ? used / total * 100 : 0;
27
+ const percentRemaining = snap.percent_remaining;
28
+ return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
29
+ }
30
+ const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
31
+ const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
32
+ const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
33
+ consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
34
+ } catch (err) {
35
+ consola.error("Failed to fetch Copilot usage:", err);
36
+ process.exit(1);
37
+ }
38
+ }
39
+ });
40
+ //#endregion
41
+ export { checkUsage };
42
+
43
+ //# sourceMappingURL=check-usage-6PB5IUFb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-usage-6PB5IUFb.js","names":[],"sources":["../src/check-usage.ts"],"sourcesContent":["import { defineCommand } from \"citty\"\nimport consola from \"consola\"\n\nimport { ensurePaths } from \"./lib/paths\"\nimport { setupGitHubToken } from \"./lib/token\"\nimport {\n getCopilotUsage,\n type QuotaDetail,\n} from \"./services/github/get-copilot-usage\"\n\nexport const checkUsage = defineCommand({\n meta: {\n name: \"check-usage\",\n description: \"Show current GitHub Copilot usage/quota information\",\n },\n async run() {\n await ensurePaths()\n await setupGitHubToken()\n try {\n const usage = await getCopilotUsage()\n const premium = usage.quota_snapshots.premium_interactions\n const premiumTotal = premium.entitlement\n const premiumUsed = premiumTotal - premium.remaining\n const premiumPercentUsed =\n premiumTotal > 0 ? (premiumUsed / premiumTotal) * 100 : 0\n const premiumPercentRemaining = premium.percent_remaining\n\n // Helper to summarize a quota snapshot\n function summarizeQuota(name: string, snap: QuotaDetail | undefined) {\n if (!snap) return `${name}: N/A`\n const total = snap.entitlement\n const used = total - snap.remaining\n const percentUsed = total > 0 ? (used / total) * 100 : 0\n const percentRemaining = snap.percent_remaining\n return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`\n }\n\n const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`\n const chatLine = summarizeQuota(\"Chat\", usage.quota_snapshots.chat)\n const completionsLine = summarizeQuota(\n \"Completions\",\n usage.quota_snapshots.completions,\n )\n\n consola.box(\n `Copilot Usage (plan: ${usage.copilot_plan})\\n`\n + `Quota resets: ${usage.quota_reset_date}\\n`\n + `\\nQuotas:\\n`\n + ` ${premiumLine}\\n`\n + ` ${chatLine}\\n`\n + ` ${completionsLine}`,\n )\n } catch (err) {\n consola.error(\"Failed to fetch Copilot usage:\", err)\n process.exit(1)\n }\n },\n})\n"],"mappings":";;;;;;AAUA,MAAa,aAAa,cAAc;CACtC,MAAM;EACJ,MAAM;EACN,aAAa;CACf;CACA,MAAM,MAAM;EACV,MAAM,YAAY;EAClB,MAAM,iBAAiB;EACvB,IAAI;GACF,MAAM,QAAQ,MAAM,gBAAgB;GACpC,MAAM,UAAU,MAAM,gBAAgB;GACtC,MAAM,eAAe,QAAQ;GAC7B,MAAM,cAAc,eAAe,QAAQ;GAC3C,MAAM,qBACJ,eAAe,IAAK,cAAc,eAAgB,MAAM;GAC1D,MAAM,0BAA0B,QAAQ;GAGxC,SAAS,eAAe,MAAc,MAA+B;IACnE,IAAI,CAAC,MAAM,OAAO,GAAG,KAAK;IAC1B,MAAM,QAAQ,KAAK;IACnB,MAAM,OAAO,QAAQ,KAAK;IAC1B,MAAM,cAAc,QAAQ,IAAK,OAAO,QAAS,MAAM;IACvD,MAAM,mBAAmB,KAAK;IAC9B,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,MAAM,SAAS,YAAY,QAAQ,CAAC,EAAE,UAAU,iBAAiB,QAAQ,CAAC,EAAE;GACzG;GAEA,MAAM,cAAc,YAAY,YAAY,GAAG,aAAa,SAAS,mBAAmB,QAAQ,CAAC,EAAE,UAAU,wBAAwB,QAAQ,CAAC,EAAE;GAChJ,MAAM,WAAW,eAAe,QAAQ,MAAM,gBAAgB,IAAI;GAClE,MAAM,kBAAkB,eACtB,eACA,MAAM,gBAAgB,WACxB;GAEA,QAAQ,IACN,wBAAwB,MAAM,aAAa,mBACtB,MAAM,iBAAiB,iBAEnC,YAAY,MACZ,SAAS,MACT,iBACX;EACF,SAAS,KAAK;GACZ,QAAQ,MAAM,kCAAkC,GAAG;GACnD,QAAQ,KAAK,CAAC;EAChB;CACF;AACF,CAAC"}