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.
- package/dist/attribution-settings-jqkXfeVr.js +2347 -0
- package/dist/attribution-settings-jqkXfeVr.js.map +1 -0
- package/dist/auth-CP4q2CjO.js +44 -0
- package/dist/auth-CP4q2CjO.js.map +1 -0
- package/dist/browser-ext/background.js +406 -30
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/check-usage-6PB5IUFb.js +43 -0
- package/dist/check-usage-6PB5IUFb.js.map +1 -0
- package/dist/claude-Cpy7b9gd.js +593 -0
- package/dist/claude-Cpy7b9gd.js.map +1 -0
- package/dist/client-CQMfroGV.js +413 -0
- package/dist/client-CQMfroGV.js.map +1 -0
- package/dist/codex-BJK_QVsI.js +85 -0
- package/dist/codex-BJK_QVsI.js.map +1 -0
- package/dist/debug-CjmE9sgG.js +81 -0
- package/dist/debug-CjmE9sgG.js.map +1 -0
- package/dist/engine-DXn5XmFp.js +2 -0
- package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
- package/dist/exec-DTO2oin0.js.map +1 -0
- package/dist/file-log-reporter-IFQcL5IK.js +158 -0
- package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
- package/dist/gate-discovery-Bm1nxHSU.js +635 -0
- package/dist/gate-discovery-Bm1nxHSU.js.map +1 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
- package/dist/hook-mcp-client-DBvm9608.js +136 -0
- package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
- package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
- package/dist/internal-plan-review-ipnT3bhj.js +114 -0
- package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
- package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
- package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
- package/dist/internal-session-bind-DwK2iGW0.js +133 -0
- package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
- package/dist/internal-stop-hook-BaBkqJyg.js +212 -0
- package/dist/internal-stop-hook-BaBkqJyg.js.map +1 -0
- package/dist/internal-stop-review-AUcFssrW.js +122 -0
- package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
- package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
- package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
- package/dist/lifecycle-Byw5s1G8.js +134 -0
- package/dist/lifecycle-Byw5s1G8.js.map +1 -0
- package/dist/lifecycle-D80CaWie.js +2 -0
- package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
- package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
- package/dist/lifecycle-DzqCqDlM.js +2 -0
- package/dist/main.js +43 -12904
- package/dist/main.js.map +1 -1
- package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
- package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
- package/dist/models-B6UnGyRo.js +158 -0
- package/dist/models-B6UnGyRo.js.map +1 -0
- package/dist/orchestration-hRU4TdWr.js +39 -0
- package/dist/orchestration-hRU4TdWr.js.map +1 -0
- package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
- package/dist/paths-C1cLZ7Ga.js.map +1 -0
- package/dist/paths-CwKwc2pi.js +2 -0
- package/dist/{peer-mcp-personas-BvommiSI.js → peer-mcp-personas-BPl6_V2A.js} +288 -2445
- package/dist/peer-mcp-personas-BPl6_V2A.js.map +1 -0
- package/dist/plan-review-hook-C8x0uKPf.js +159 -0
- package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
- package/dist/provision-DueN4M8G.js +656 -0
- package/dist/provision-DueN4M8G.js.map +1 -0
- package/dist/serve-GN-flJmF.js +1511 -0
- package/dist/serve-GN-flJmF.js.map +1 -0
- package/dist/server-setup-BJL-X7w6.js +4776 -0
- package/dist/server-setup-BJL-X7w6.js.map +1 -0
- package/dist/start-CE0k-weK.js +129 -0
- package/dist/start-CE0k-weK.js.map +1 -0
- package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
- package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
- package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
- package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
- package/dist/token-R2ocFC3M.js +1054 -0
- package/dist/token-R2ocFC3M.js.map +1 -0
- package/dist/version-_Q1WpsQp.js +32 -0
- package/dist/version-_Q1WpsQp.js.map +1 -0
- package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
- package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-dOYoMQis.js +0 -2
- package/dist/lifecycle-0rXrwjWk.js +0 -2
- package/dist/lifecycle-DHZsVN8j.js.map +0 -1
- package/dist/lifecycle-hzPwXc3I.js +0 -2
- package/dist/paths-BjTMI_xK.js.map +0 -1
- package/dist/paths-D3XgmTcQ.js +0 -2
- 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
|
|
378
|
+
const deadline = Date.now() + SHOT_DEADLINE_MS
|
|
164
379
|
const format = args.format === "jpeg" ? "jpeg" : "png"
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
await sleep(150)
|
|
449
|
+
await sleep(SHOT_PAINT_SETTLE_MS)
|
|
176
450
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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.
|
|
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"}
|