gclm-code 1.0.0 → 1.0.1

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 (43) hide show
  1. package/README.md +1 -1
  2. package/bin/gc.js +53 -25
  3. package/bin/install-runtime.js +253 -0
  4. package/package.json +10 -5
  5. package/vendor/manifest.json +92 -0
  6. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
  7. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
  8. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
  9. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
  10. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
  11. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
  12. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
  13. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
  14. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
  15. package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
  16. package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
  17. package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
  18. package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
  19. package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
  20. package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
  21. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
  22. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
  23. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
  24. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
  25. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
  26. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
  27. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
  28. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
  29. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
  30. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
  31. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
  32. package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
  33. package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
  34. package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
  35. package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
  36. package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
  37. package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
  38. package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
  39. package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
  40. package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
  41. package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
  42. package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
  43. package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
@@ -0,0 +1,553 @@
1
+ /**
2
+ * App category lookup for tiered CU permissions. Three categories land at a
3
+ * restricted tier instead of `"full"`:
4
+ *
5
+ * - **browser** → `"read"` tier — visible in screenshots, NO interaction.
6
+ * The model can read an already-open page but must use the Claude-in-Chrome
7
+ * MCP for navigation/clicking/typing.
8
+ * - **terminal** → `"click"` tier — visible + clickable, NO typing. The
9
+ * model can click a Run button or scroll test output in an IDE, but can't
10
+ * type into the integrated terminal. Use the Bash tool for shell work.
11
+ * - **trading** → `"read"` tier — same restrictions as browsers, but no
12
+ * CiC-MCP alternative exists. For platforms where a stray click can
13
+ * execute a trade or send a message to a counterparty.
14
+ *
15
+ * Uncategorized apps default to `"full"`. See `getDefaultTierForApp`.
16
+ *
17
+ * Identification is two-layered:
18
+ * 1. Bundle ID match (macOS-only; `InstalledApp.bundleId` is a
19
+ * CFBundleIdentifier and meaningless on Windows). Fast, exact, the
20
+ * primary mechanism while CU is darwin-gated.
21
+ * 2. Display-name substring match (cross-platform fallback). Catches
22
+ * unresolved requests ("Chrome" when Chrome isn't installed) AND will
23
+ * be the primary mechanism on Windows/Linux where there's no bundle ID.
24
+ * Windows-relevant names (PowerShell, cmd, Windows Terminal) are
25
+ * included now so they activate the moment the darwin gate lifts.
26
+ *
27
+ * Keep this file **import-free** (like sentinelApps.ts) — the renderer may
28
+ * import it via a package.json subpath export, and pulling in
29
+ * `@modelcontextprotocol/sdk` (a devDep) through the index → mcpServer chain
30
+ * would fail module resolution in Next.js. The `CuAppPermTier` type is
31
+ * duplicated as a string literal below rather than imported.
32
+ */
33
+
34
+ export type DeniedCategory = "browser" | "terminal" | "trading";
35
+
36
+ /**
37
+ * Map a category to its hardcoded tier. Return-type is the string-literal
38
+ * union inline (this file is import-free; see header comment). The
39
+ * authoritative type is `CuAppPermTier` in types.ts — keep in sync.
40
+ *
41
+ * Not bijective — both `"browser"` and `"trading"` map to `"read"`. Copy
42
+ * that differs by category (the "use CiC" hint is browser-only) must check
43
+ * the category, not just the tier.
44
+ */
45
+ export function categoryToTier(
46
+ category: DeniedCategory | null,
47
+ ): "read" | "click" | "full" {
48
+ if (category === "browser" || category === "trading") return "read";
49
+ if (category === "terminal") return "click";
50
+ return "full";
51
+ }
52
+
53
+ // ─── Bundle-ID deny sets (macOS) ─────────────────────────────────────────
54
+
55
+ const BROWSER_BUNDLE_IDS: ReadonlySet<string> = new Set([
56
+ // Apple
57
+ "com.apple.Safari",
58
+ "com.apple.SafariTechnologyPreview",
59
+ // Google
60
+ "com.google.Chrome",
61
+ "com.google.Chrome.beta",
62
+ "com.google.Chrome.dev",
63
+ "com.google.Chrome.canary",
64
+ // Microsoft
65
+ "com.microsoft.edgemac",
66
+ "com.microsoft.edgemac.Beta",
67
+ "com.microsoft.edgemac.Dev",
68
+ "com.microsoft.edgemac.Canary",
69
+ // Mozilla
70
+ "org.mozilla.firefox",
71
+ "org.mozilla.firefoxdeveloperedition",
72
+ "org.mozilla.nightly",
73
+ // Chromium-based
74
+ "org.chromium.Chromium",
75
+ "com.brave.Browser",
76
+ "com.brave.Browser.beta",
77
+ "com.brave.Browser.nightly",
78
+ "com.operasoftware.Opera",
79
+ "com.operasoftware.OperaGX",
80
+ "com.operasoftware.OperaDeveloper",
81
+ "com.vivaldi.Vivaldi",
82
+ // The Browser Company
83
+ "company.thebrowser.Browser", // Arc
84
+ "company.thebrowser.dia", // Dia (agentic)
85
+ // Privacy-focused
86
+ "org.torproject.torbrowser",
87
+ "com.duckduckgo.macos.browser",
88
+ "ru.yandex.desktop.yandex-browser",
89
+ // Agentic / AI browsers — newer entrants with LLM integrations
90
+ "ai.perplexity.comet",
91
+ "com.sigmaos.sigmaos.macos", // SigmaOS
92
+ // Webkit-based misc
93
+ "com.kagi.kagimacOS", // Orion
94
+ ]);
95
+
96
+ /**
97
+ * Terminals + IDEs with integrated terminals. Supersets
98
+ * `SHELL_ACCESS_BUNDLE_IDS` from sentinelApps.ts — terminals proceed to the
99
+ * approval dialog at tier "click", and the sentinel warning renders
100
+ * alongside the tier badge.
101
+ */
102
+ const TERMINAL_BUNDLE_IDS: ReadonlySet<string> = new Set([
103
+ // Dedicated terminals
104
+ "com.apple.Terminal",
105
+ "com.googlecode.iterm2",
106
+ "dev.warp.Warp-Stable",
107
+ "dev.warp.Warp-Beta",
108
+ "com.github.wez.wezterm",
109
+ "org.alacritty",
110
+ "io.alacritty", // pre-v0.11.0 (renamed 2022-07) — kept for legacy installs
111
+ "net.kovidgoyal.kitty",
112
+ "co.zeit.hyper",
113
+ "com.mitchellh.ghostty",
114
+ "org.tabby",
115
+ "com.termius-dmg.mac", // Termius
116
+ // IDEs with integrated terminals — we can't distinguish "type in the
117
+ // editor" from "type in the integrated terminal" via screenshot+click.
118
+ // VS Code family
119
+ "com.microsoft.VSCode",
120
+ "com.microsoft.VSCodeInsiders",
121
+ "com.vscodium", // VSCodium
122
+ "com.todesktop.230313mzl4w4u92", // Cursor
123
+ "com.exafunction.windsurf", // Windsurf / Codeium
124
+ "dev.zed.Zed",
125
+ "dev.zed.Zed-Preview",
126
+ // JetBrains family (all have integrated terminals)
127
+ "com.jetbrains.intellij",
128
+ "com.jetbrains.intellij.ce",
129
+ "com.jetbrains.pycharm",
130
+ "com.jetbrains.pycharm.ce",
131
+ "com.jetbrains.WebStorm",
132
+ "com.jetbrains.CLion",
133
+ "com.jetbrains.goland",
134
+ "com.jetbrains.rubymine",
135
+ "com.jetbrains.PhpStorm",
136
+ "com.jetbrains.datagrip",
137
+ "com.jetbrains.rider",
138
+ "com.jetbrains.AppCode",
139
+ "com.jetbrains.rustrover",
140
+ "com.jetbrains.fleet",
141
+ "com.google.android.studio", // Android Studio (JetBrains-based)
142
+ // Other IDEs
143
+ "com.axosoft.gitkraken", // GitKraken has an integrated terminal panel. Also keeps the "kraken" trading-substring from miscategorizing it — bundle-ID wins.
144
+ "com.sublimetext.4",
145
+ "com.sublimetext.3",
146
+ "org.vim.MacVim",
147
+ "com.neovim.neovim",
148
+ "org.gnu.Emacs",
149
+ // Xcode's previous carve-out (full tier for Interface Builder / simulator)
150
+ // was reversed — at tier "click" IB and simulator taps still work (both are
151
+ // plain clicks) while the integrated terminal is blocked from keyboard input.
152
+ "com.apple.dt.Xcode",
153
+ "org.eclipse.platform.ide",
154
+ "org.netbeans.ide",
155
+ "com.microsoft.visual-studio", // Visual Studio for Mac
156
+ // AppleScript/automation execution surfaces — same threat as terminals:
157
+ // type(script) → key("cmd+r") runs arbitrary code. Added after #28011
158
+ // removed the osascript MCP server, making CU the only tool-call route
159
+ // to AppleScript.
160
+ "com.apple.ScriptEditor2",
161
+ "com.apple.Automator",
162
+ "com.apple.shortcuts",
163
+ ]);
164
+
165
+ /**
166
+ * Trading / crypto platforms — granted at tier `"read"` so the agent can see
167
+ * balances and prices but can't click into an order, transfer, or IB chat.
168
+ * Bundle IDs populated from Homebrew cask `uninstall.quit` stanzas as they're
169
+ * verified; the name-substring fallback below is the primary check. Bloomberg
170
+ * Terminal has no native macOS build per their FAQ (web/Citrix only).
171
+ *
172
+ * Budgeting/accounting apps (Quicken, YNAB, QuickBooks, etc.) are NOT listed
173
+ * here — they default to tier `"full"`. The risk model for brokerage/crypto
174
+ * (a stray click can execute a trade) doesn't apply to budgeting apps; the
175
+ * Cowork system prompt carries the soft instruction to never execute trades
176
+ * or transfer money on the user's behalf.
177
+ */
178
+ const TRADING_BUNDLE_IDS: ReadonlySet<string> = new Set([
179
+ // Verified via Homebrew quit/zap stanzas + mdls + electron-builder source.
180
+ // Trading
181
+ "com.webull.desktop.v1", // Webull (direct download, Qt)
182
+ "com.webull.trade.mac.v1", // Webull (Mac App Store)
183
+ "com.tastytrade.desktop",
184
+ "com.tradingview.tradingviewapp.desktop",
185
+ "com.fidelity.activetrader", // Fidelity Trader+ (new)
186
+ "com.fmr.activetrader", // Fidelity Active Trader Pro (legacy)
187
+ // Interactive Brokers TWS — install4j wrapper; Homebrew quit stanza is
188
+ // authoritative for this exact value but install4j IDs can drift across
189
+ // major versions — name-substring "trader workstation" is the fallback.
190
+ "com.install4j.5889-6375-8446-2021",
191
+ // Crypto
192
+ "com.binance.BinanceDesktop",
193
+ "com.electron.exodus",
194
+ // Electrum uses PyInstaller with bundle_identifier=None → defaults to
195
+ // org.pythonmac.unspecified.<AppName>. Confirmed in spesmilo/electrum
196
+ // source + Homebrew zap. IntuneBrew's "org.electrum.electrum" is a fork.
197
+ "org.pythonmac.unspecified.Electrum",
198
+ "com.ledger.live",
199
+ "io.trezor.TrezorSuite",
200
+ // No native macOS app (name-substring only): Schwab, E*TRADE, TradeStation,
201
+ // Robinhood, NinjaTrader, Coinbase, Kraken, Bloomberg. thinkorswim
202
+ // install4j ID drifts per-install — substring safer.
203
+ ]);
204
+
205
+ // ─── Policy-deny (not a tier — cannot be granted at all) ─────────────────
206
+ //
207
+ // Streaming / ebook / music apps and a handful of publisher apps. These
208
+ // are auto-denied before the approval dialog — no tier can be granted.
209
+ // Rationale is copyright / content-control (the agent has no legitimate
210
+ // need to screenshot Netflix or click Play on Spotify).
211
+ //
212
+ // Sourced from the ACP CU-apps blocklist xlsx ("Full block" tab). See
213
+ // /tmp/extract_cu_blocklist.py for the extraction script.
214
+
215
+ const POLICY_DENIED_BUNDLE_IDS: ReadonlySet<string> = new Set([
216
+ // Verified via Homebrew quit/zap + mdls /System/Applications + IntuneBrew.
217
+ // Apple built-ins
218
+ "com.apple.TV",
219
+ "com.apple.Music",
220
+ "com.apple.iBooksX",
221
+ "com.apple.podcasts",
222
+ // Music
223
+ "com.spotify.client",
224
+ "com.amazon.music",
225
+ "com.tidal.desktop",
226
+ "com.deezer.deezer-desktop",
227
+ "com.pandora.desktop",
228
+ "com.electron.pocket-casts", // direct-download Electron wrapper
229
+ "au.com.shiftyjelly.PocketCasts", // Mac App Store
230
+ // Video
231
+ "tv.plex.desktop",
232
+ "tv.plex.htpc",
233
+ "tv.plex.plexamp",
234
+ "com.amazon.aiv.AIVApp", // Prime Video (iOS-on-Apple-Silicon)
235
+ // Ebooks
236
+ "net.kovidgoyal.calibre",
237
+ "com.amazon.Kindle", // legacy desktop, discontinued
238
+ "com.amazon.Lassen", // current Mac App Store (iOS-on-Mac)
239
+ "com.kobo.desktop.Kobo",
240
+ // No native macOS app (name-substring only): Netflix, Disney+, Hulu,
241
+ // HBO Max, Peacock, Paramount+, YouTube, Crunchyroll, Tubi, Vudu,
242
+ // Audible, Reddit, NYTimes. Their iOS apps don't opt into iPad-on-Mac.
243
+ ]);
244
+
245
+ const POLICY_DENIED_NAME_SUBSTRINGS: readonly string[] = [
246
+ // Video streaming
247
+ "netflix",
248
+ "disney+",
249
+ "hulu",
250
+ "prime video",
251
+ "apple tv",
252
+ "peacock",
253
+ "paramount+",
254
+ // "plex" is too generic — would match "Perplexity". Covered by
255
+ // tv.plex.* bundle IDs on macOS.
256
+ "tubi",
257
+ "crunchyroll",
258
+ "vudu",
259
+ // E-readers / audiobooks
260
+ "kindle",
261
+ "apple books",
262
+ "kobo",
263
+ "play books",
264
+ "calibre",
265
+ "libby",
266
+ "readium",
267
+ "audible",
268
+ "libro.fm",
269
+ "speechify",
270
+ // Music
271
+ "spotify",
272
+ "apple music",
273
+ "amazon music",
274
+ "youtube music",
275
+ "tidal",
276
+ "deezer",
277
+ "pandora",
278
+ "pocket casts",
279
+ // Publisher / social apps (from the same blocklist tab)
280
+ "naver",
281
+ "reddit",
282
+ "sony music",
283
+ "vegas pro",
284
+ "pitchfork",
285
+ "economist",
286
+ "nytimes",
287
+ // Skipped (too generic for substring matching — need bundle ID):
288
+ // HBO Max / Max, YouTube (non-Music), Nook, Sony Catalyst, Wired
289
+ ];
290
+
291
+ /**
292
+ * Policy-level auto-deny. Unlike `userDeniedBundleIds` (per-user Settings
293
+ * page), this is baked into the build. `buildAccessRequest` strips these
294
+ * before the approval dialog with "blocked by policy" guidance; the agent
295
+ * is told to not retry.
296
+ */
297
+ export function isPolicyDenied(
298
+ bundleId: string | undefined,
299
+ displayName: string,
300
+ ): boolean {
301
+ if (bundleId && POLICY_DENIED_BUNDLE_IDS.has(bundleId)) return true;
302
+ const lower = displayName.toLowerCase();
303
+ for (const sub of POLICY_DENIED_NAME_SUBSTRINGS) {
304
+ if (lower.includes(sub)) return true;
305
+ }
306
+ return false;
307
+ }
308
+
309
+ export function getDeniedCategory(bundleId: string): DeniedCategory | null {
310
+ if (BROWSER_BUNDLE_IDS.has(bundleId)) return "browser";
311
+ if (TERMINAL_BUNDLE_IDS.has(bundleId)) return "terminal";
312
+ if (TRADING_BUNDLE_IDS.has(bundleId)) return "trading";
313
+ return null;
314
+ }
315
+
316
+ // ─── Display-name fallback (cross-platform) ──────────────────────────────
317
+
318
+ /**
319
+ * Lowercase substrings checked against the requested display name. Catches:
320
+ * - Unresolved requests (app not installed, Spotlight miss)
321
+ * - Future Windows/Linux support where bundleId is meaningless
322
+ *
323
+ * Matched via `.includes()` on `name.toLowerCase()`. Entries are ordered
324
+ * by specificity (more-specific first is irrelevant since we return on
325
+ * first match, but groupings are by category for readability).
326
+ */
327
+ const BROWSER_NAME_SUBSTRINGS: readonly string[] = [
328
+ "safari",
329
+ "chrome",
330
+ "firefox",
331
+ "microsoft edge",
332
+ "brave",
333
+ "opera",
334
+ "vivaldi",
335
+ "chromium",
336
+ // Arc/Dia: the canonical display name is just "Arc"/"Dia" — too short for
337
+ // substring matching (false-positives: "Arcade", "Diagram"). Covered by
338
+ // bundle ID on macOS. The "... browser" entries below catch natural-language
339
+ // phrasings ("the arc browser") but NOT the canonical short name.
340
+ "arc browser",
341
+ "tor browser",
342
+ "duckduckgo",
343
+ "yandex",
344
+ "orion browser",
345
+ // Agentic / AI browsers
346
+ "comet", // Perplexity's browser — "Comet" substring risks false positives
347
+ // but leaving for now; "comet" in an app name is rare
348
+ "sigmaos",
349
+ "dia browser",
350
+ ];
351
+
352
+ const TERMINAL_NAME_SUBSTRINGS: readonly string[] = [
353
+ // macOS / cross-platform terminals
354
+ "terminal", // catches Terminal, Windows Terminal (NOT iTerm — separate entry)
355
+ "iterm",
356
+ "wezterm",
357
+ "alacritty",
358
+ "kitty",
359
+ "ghostty",
360
+ "tabby",
361
+ "termius",
362
+ // AppleScript runners — see bundle-ID comment above. "shortcuts" is too
363
+ // generic for substring matching (many apps have "shortcuts" in the name);
364
+ // covered by bundle ID only, like warp/hyper.
365
+ "script editor",
366
+ "automator",
367
+ // NOTE: "warp" and "hyper" are too generic for substring matching —
368
+ // they'd false-positive on "Warpaint" or "Hyperion". Covered by bundle ID
369
+ // (dev.warp.Warp-Stable, co.zeit.hyper) for macOS; Windows exe-name
370
+ // matching can be added when Windows CU ships.
371
+ // Windows shells (activate when the darwin gate lifts)
372
+ "powershell",
373
+ "cmd.exe",
374
+ "command prompt",
375
+ "git bash",
376
+ "conemu",
377
+ "cmder",
378
+ // IDEs (VS Code family)
379
+ "visual studio code",
380
+ "visual studio", // catches VS for Mac + Windows
381
+ "vscode",
382
+ "vs code",
383
+ "vscodium",
384
+ "cursor", // Cursor IDE — "cursor" is generic but IDE is the only common app
385
+ "windsurf",
386
+ // Zed: display name is just "Zed" — too short for substring matching
387
+ // (false-positives). Covered by bundle ID (dev.zed.Zed) on macOS.
388
+ // IDEs (JetBrains family)
389
+ "intellij",
390
+ "pycharm",
391
+ "webstorm",
392
+ "clion",
393
+ "goland",
394
+ "rubymine",
395
+ "phpstorm",
396
+ "datagrip",
397
+ "rider",
398
+ "appcode",
399
+ "rustrover",
400
+ "fleet",
401
+ "android studio",
402
+ // Other IDEs
403
+ "sublime text",
404
+ "macvim",
405
+ "neovim",
406
+ "emacs",
407
+ "xcode",
408
+ "eclipse",
409
+ "netbeans",
410
+ ];
411
+
412
+ const TRADING_NAME_SUBSTRINGS: readonly string[] = [
413
+ // Trading — brokerage apps. Sourced from the ACP CU-apps blocklist xlsx
414
+ // ("Read Only" tab). Name-substring safe for proper nouns below; generic
415
+ // names (IG, Delta, HTX) are skipped and need bundle-ID matching once
416
+ // verified.
417
+ "bloomberg",
418
+ "ameritrade",
419
+ "thinkorswim",
420
+ "schwab",
421
+ "fidelity",
422
+ "e*trade",
423
+ "interactive brokers",
424
+ "trader workstation", // Interactive Brokers TWS
425
+ "tradestation",
426
+ "webull",
427
+ "robinhood",
428
+ "tastytrade",
429
+ "ninjatrader",
430
+ "tradingview",
431
+ "moomoo",
432
+ "tradezero",
433
+ "prorealtime",
434
+ "plus500",
435
+ "saxotrader",
436
+ "oanda",
437
+ "metatrader",
438
+ "forex.com",
439
+ "avaoptions",
440
+ "ctrader",
441
+ "jforex",
442
+ "iq option",
443
+ "olymp trade",
444
+ "binomo",
445
+ "pocket option",
446
+ "raceoption",
447
+ "expertoption",
448
+ "quotex",
449
+ "naga",
450
+ "morgan stanley",
451
+ "ubs neo",
452
+ "eikon", // Thomson Reuters / LSEG Workspace
453
+ // Crypto — exchanges, wallets, portfolio trackers
454
+ "coinbase",
455
+ "kraken",
456
+ "binance",
457
+ "okx",
458
+ "bybit",
459
+ // "gate.io" is too generic — the ".io" TLD suffix is common in app names
460
+ // (e.g., "Draw.io"). Needs bundle-ID matching once verified.
461
+ "phemex",
462
+ "stormgain",
463
+ "crypto.com",
464
+ // "exodus" is too generic — it's a common noun and would match unrelated
465
+ // apps/games. Needs bundle-ID matching once verified.
466
+ "electrum",
467
+ "ledger live",
468
+ "trezor",
469
+ "guarda",
470
+ "atomic wallet",
471
+ "bitpay",
472
+ "bisq",
473
+ "koinly",
474
+ "cointracker",
475
+ "blockfi",
476
+ "stripe cli",
477
+ // Crypto games / metaverse (same trade-execution risk model)
478
+ "decentraland",
479
+ "axie infinity",
480
+ "gods unchained",
481
+ ];
482
+
483
+ /**
484
+ * Display-name substring match. Called when bundle-ID resolution returned
485
+ * nothing (`resolved === undefined`) or when no bundle-ID deny-list entry
486
+ * matched. Returns the category for the first matching substring, or null.
487
+ *
488
+ * Case-insensitive, substring — so `"Google Chrome"`, `"chrome"`, and
489
+ * `"Chrome Canary"` all match the `"chrome"` entry.
490
+ */
491
+ export function getDeniedCategoryByDisplayName(
492
+ name: string,
493
+ ): DeniedCategory | null {
494
+ const lower = name.toLowerCase();
495
+ // Trading first — proper-noun-only set, most specific. "Bloomberg Terminal"
496
+ // contains "terminal" and would miscategorize if TERMINAL_NAME_SUBSTRINGS
497
+ // ran first.
498
+ for (const sub of TRADING_NAME_SUBSTRINGS) {
499
+ if (lower.includes(sub)) return "trading";
500
+ }
501
+ for (const sub of BROWSER_NAME_SUBSTRINGS) {
502
+ if (lower.includes(sub)) return "browser";
503
+ }
504
+ for (const sub of TERMINAL_NAME_SUBSTRINGS) {
505
+ if (lower.includes(sub)) return "terminal";
506
+ }
507
+ return null;
508
+ }
509
+
510
+ /**
511
+ * Combined check — bundle ID first (exact, fast), then display-name
512
+ * fallback. This is the function tool-call handlers should use.
513
+ *
514
+ * `bundleId` may be undefined (unresolved request — model asked for an app
515
+ * that isn't installed or Spotlight didn't find). In that case only the
516
+ * display-name check runs.
517
+ */
518
+ export function getDeniedCategoryForApp(
519
+ bundleId: string | undefined,
520
+ displayName: string,
521
+ ): DeniedCategory | null {
522
+ if (bundleId) {
523
+ const byId = getDeniedCategory(bundleId);
524
+ if (byId) return byId;
525
+ }
526
+ return getDeniedCategoryByDisplayName(displayName);
527
+ }
528
+
529
+ /**
530
+ * Default tier for an app at grant time. Wraps `getDeniedCategoryForApp` +
531
+ * `categoryToTier`. Browsers → `"read"`, terminals/IDEs → `"click"`,
532
+ * everything else → `"full"`.
533
+ *
534
+ * Called by `buildAccessRequest` to populate `ResolvedAppRequest.proposedTier`
535
+ * before the approval dialog shows.
536
+ */
537
+ export function getDefaultTierForApp(
538
+ bundleId: string | undefined,
539
+ displayName: string,
540
+ ): "read" | "click" | "full" {
541
+ return categoryToTier(getDeniedCategoryForApp(bundleId, displayName));
542
+ }
543
+
544
+ export const _test = {
545
+ BROWSER_BUNDLE_IDS,
546
+ TERMINAL_BUNDLE_IDS,
547
+ TRADING_BUNDLE_IDS,
548
+ POLICY_DENIED_BUNDLE_IDS,
549
+ BROWSER_NAME_SUBSTRINGS,
550
+ TERMINAL_NAME_SUBSTRINGS,
551
+ TRADING_NAME_SUBSTRINGS,
552
+ POLICY_DENIED_NAME_SUBSTRINGS,
553
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Port of the API's image transcoder target-size algorithm. Pre-sizing
3
+ * screenshots to this function's output means the API's early-return fires
4
+ * (tokens ≤ max) and the image is NOT resized server-side — so the model
5
+ * sees exactly the dimensions in `ScreenshotResult.width/height` and
6
+ * `scaleCoord` stays coherent.
7
+ *
8
+ * Rust reference: api/api/image_transcoder/rust_transcoder/src/utils/resize.rs
9
+ * Sibling TS port: apps/claude-browser-use/src/utils/imageResize.ts (identical
10
+ * algorithm, lives in the Chrome extension tree — not a shared package).
11
+ *
12
+ * See COORDINATES.md for why this matters for click accuracy.
13
+ */
14
+
15
+ export interface ResizeParams {
16
+ pxPerToken: number;
17
+ maxTargetPx: number;
18
+ maxTargetTokens: number;
19
+ }
20
+
21
+ /**
22
+ * Production defaults — match `resize.rs:160-164` and Chrome's
23
+ * `CDPService.ts:638-642`. Vision encoder uses 28px tiles; 1568 is both
24
+ * the long-edge cap (56 tiles) AND the token budget.
25
+ */
26
+ export const API_RESIZE_PARAMS: ResizeParams = {
27
+ pxPerToken: 28,
28
+ maxTargetPx: 1568,
29
+ maxTargetTokens: 1568,
30
+ };
31
+
32
+ /** ceil(px / pxPerToken). Matches resize.rs:74-76 (which uses integer ceil-div). */
33
+ export function nTokensForPx(px: number, pxPerToken: number): number {
34
+ return Math.floor((px - 1) / pxPerToken) + 1;
35
+ }
36
+
37
+ function nTokensForImg(
38
+ width: number,
39
+ height: number,
40
+ pxPerToken: number,
41
+ ): number {
42
+ return nTokensForPx(width, pxPerToken) * nTokensForPx(height, pxPerToken);
43
+ }
44
+
45
+ /**
46
+ * Binary-search along the width dimension for the largest image that:
47
+ * - preserves the input aspect ratio
48
+ * - has long edge ≤ maxTargetPx
49
+ * - has ceil(w/pxPerToken) × ceil(h/pxPerToken) ≤ maxTargetTokens
50
+ *
51
+ * Returns [width, height]. No-op if input already satisfies all three.
52
+ *
53
+ * The long-edge constraint alone (what we used to use) is insufficient on
54
+ * squarer-than-16:9 displays: 1568×1014 (MBP 16" AR) is 56×37 = 2072 tokens,
55
+ * over budget, and gets server-resized to 1372×887 — model then clicks in
56
+ * 1372-space but scaleCoord assumed 1568-space → ~14% coord error.
57
+ *
58
+ * Matches resize.rs:91-155 exactly (verified against its test vectors).
59
+ */
60
+ export function targetImageSize(
61
+ width: number,
62
+ height: number,
63
+ params: ResizeParams,
64
+ ): [number, number] {
65
+ const { pxPerToken, maxTargetPx, maxTargetTokens } = params;
66
+
67
+ if (
68
+ width <= maxTargetPx &&
69
+ height <= maxTargetPx &&
70
+ nTokensForImg(width, height, pxPerToken) <= maxTargetTokens
71
+ ) {
72
+ return [width, height];
73
+ }
74
+
75
+ // Normalize to landscape for the search; transpose result back.
76
+ if (height > width) {
77
+ const [w, h] = targetImageSize(height, width, params);
78
+ return [h, w];
79
+ }
80
+
81
+ const aspectRatio = width / height;
82
+
83
+ // Loop invariant: lowerBoundWidth is always valid, upperBoundWidth is
84
+ // always invalid. ~12 iterations for a 4000px image.
85
+ let upperBoundWidth = width;
86
+ let lowerBoundWidth = 1;
87
+
88
+ for (;;) {
89
+ if (lowerBoundWidth + 1 === upperBoundWidth) {
90
+ return [
91
+ lowerBoundWidth,
92
+ Math.max(Math.round(lowerBoundWidth / aspectRatio), 1),
93
+ ];
94
+ }
95
+
96
+ const middleWidth = Math.floor((lowerBoundWidth + upperBoundWidth) / 2);
97
+ const middleHeight = Math.max(Math.round(middleWidth / aspectRatio), 1);
98
+
99
+ if (
100
+ middleWidth <= maxTargetPx &&
101
+ nTokensForImg(middleWidth, middleHeight, pxPerToken) <= maxTargetTokens
102
+ ) {
103
+ lowerBoundWidth = middleWidth;
104
+ } else {
105
+ upperBoundWidth = middleWidth;
106
+ }
107
+ }
108
+ }