@mefistofelix/cdp.js 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,7 @@ In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above
37
37
  - Browser proxies and named direct-mode contexts with independent proxies/storage
38
38
  and automatic cleanup after their last target closes.
39
39
  - Incognito targets in direct and reverse modes.
40
+ - Named, colored tab groups through the reverse Chrome extension.
40
41
  - Attachment through WebSocket, HTTP discovery or a debugging port.
41
42
  - Optional Chrome extension reverse connections through one native Deno/Bun server,
42
43
  without browser remote-debugging arguments.
@@ -1273,6 +1274,7 @@ await client.call({
1273
1274
  | `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
1274
1275
  | `context` | unset | Direct-mode context name or first-use configuration object; see named contexts above. Reverse mode rejects this option. |
1275
1276
  | `incognito` | `false` | Create/select a private target; direct mode uses the shared `'incognito'` context by default, reverse uses a native incognito window with the user's extension grant. |
1277
+ | `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
1276
1278
  | `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
1277
1279
  | `initialize` | `true` | Run the convenience setup sequence; false skips it. |
1278
1280
  | `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
@@ -1283,6 +1285,78 @@ await client.call({
1283
1285
  | `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
1284
1286
  | `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
1285
1287
 
1288
+ ### Named tab groups
1289
+
1290
+ With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
1291
+ configuring a target:
1292
+
1293
+ ```js
1294
+ await client.call({
1295
+ method: 'Page.navigate',
1296
+ params: {
1297
+ browser: {
1298
+ name: 'work',
1299
+ cdp_ext: true,
1300
+ },
1301
+ target: {
1302
+ name: 'search',
1303
+ group: {
1304
+ name: 'Research',
1305
+ color: 'blue',
1306
+ },
1307
+ },
1308
+ url: 'https://example.com/',
1309
+ },
1310
+ })
1311
+ await client.call({
1312
+ method: 'Page.navigate',
1313
+ params: {
1314
+ browser: 'work',
1315
+ target: {
1316
+ name: 'notes',
1317
+ group: 'Research',
1318
+ },
1319
+ url: 'https://example.com/notes',
1320
+ },
1321
+ })
1322
+ ```
1323
+
1324
+ | Group option | Default | Meaning |
1325
+ | --- | --- | --- |
1326
+ | `name` | required in an object | Logical name in `record.groups`; also the visible group title. A string reference creates a default group if this name is new. |
1327
+ | `color` | unset | Chrome's native color selection for new groups; otherwise retain the current color. Explicit values: `grey`, `blue`, `red`, `yellow`, `green`, `pink`, `purple`, `cyan`, `orange`. |
1328
+
1329
+ The first use of a group name records its configuration. Concurrent target
1330
+ creation shares that definition and serializes group operations. Names are scoped
1331
+ to the logical browser. Each window has its own native group for that name:
1332
+ tabs stay in their current window, including separate regular/incognito windows.
1333
+ Groups are created by the library, not adopted from unrelated groups that happen
1334
+ to have the same visible title. Incognito still requires the bridge's user grant.
1335
+
1336
+ Grouping happens before optional CDP setup, including with `initialize: false`.
1337
+ It also works with `targetId` selecting an existing tab. As with other target
1338
+ options, passing a new group on a target label that was already configured does
1339
+ not reconfigure that label. Use another label with its physical `targetId`, or
1340
+ edit the public record and reinitialize the target.
1341
+
1342
+ Reconnection reapplies the name/color and rejoins a surviving group. If its last
1343
+ tab was closed or ungrouped, Chrome removes the native group; later target setup
1344
+ creates a new one. `record.groups[name].window_groups[windowId]` stores the last
1345
+ known native group ID. These IDs are checked against Chrome during setup and can
1346
+ remain stale between operations; no extra event subscriptions or persistence are
1347
+ added. In reverse target info, `groupId` is the current native ID, or `-1` when
1348
+ ungrouped. Generic `chrome.tabs` / `chrome.tabGroups` calls remain available for
1349
+ immediate native updates. Native errors, including invalid colors, reject setup
1350
+ with the usual RPC error context and can be retried after correcting the record.
1351
+
1352
+ Direct CDP does not expose browser tab-group commands. A non-false `group` option
1353
+ therefore rejects before connecting or launching in direct mode. The bridge's
1354
+ existing `tabGroups` permission is sufficient; no extra permission is added.
1355
+ See [Chrome tab groups](https://developer.chrome.com/docs/extensions/reference/api/tabGroups)
1356
+ and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/tabs#method-group).
1357
+
1358
+ ### Target initialization
1359
+
1286
1360
  Domain options set to false skip setup commands; they do not actively disable a
1287
1361
  domain already enabled by another caller. Likewise, `binding: false` does not
1288
1362
  remove a previously installed binding. Use native CDP commands for immediate
@@ -1554,6 +1628,7 @@ dictionary containers have a null prototype.
1554
1628
  | `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
1555
1629
  | `record.contexts[name]` | Recorded context options, native `browserContextId` or null, and pending `connecting`/`closing` promises. Direct mode only. |
1556
1630
  | `record.context_checking` | Pending automatic empty-context cleanup, otherwise unset/null. |
1631
+ | `record.groups[name]` | Recorded tab-group options, per-window native IDs in `window_groups`, and a pending `connecting` operation. Reverse only. |
1557
1632
  | `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
1558
1633
  | `record.session_targets[sessionId]` | Physical `targetId` for a session. |
1559
1634
  | `socket.pending[id]` | Response-wait promise, removed on completion. |
@@ -1641,6 +1716,7 @@ labels, target labels, custom methods or reconnection policy in this layer.
1641
1716
  | Failed discovery or local launch | Reject the call establishing the connection. |
1642
1717
  | Named context in reverse mode | Reject explicitly; no separate process emulation. |
1643
1718
  | Reverse incognito without the extension's user grant | Reject with an instruction to enable Allow in incognito. |
1719
+ | Tab group in direct mode | Reject before connecting; tab groups require the reverse extension APIs. |
1644
1720
  | Automatic context cleanup fails | Emit the manager's `error` event with browser label and original error. |
1645
1721
 
1646
1722
  ```js
@@ -1688,6 +1764,8 @@ The shared Deno/Bun reverse suite loads the real extension into two headless Chr
1688
1764
  server port without remote-debugging arguments. It checks native/custom calls,
1689
1765
  Network events, webRequest notifications without debugger attachment, bindings,
1690
1766
  child sessions, reconnect, detach/recreate, incognito and isolation.
1767
+ Tab-group checks cover concurrent creation, existing tabs, title/color, per-window
1768
+ incognito separation, reconnection, recreation and native validation/retry.
1691
1769
  Node coverage also checks actual browser/context proxy routing, shared private
1692
1770
  storage, concurrent context creation, automatic disposal, unselected targets and
1693
1771
  context ownership in externally attached browsers.
package/cdp.js CHANGED
@@ -684,9 +684,58 @@ export class cdp extends EventTarget {
684
684
  }
685
685
  }
686
686
 
687
+ async _group(browser, target) {
688
+ const socket = browser.socket
689
+ const group = browser.groups[target.group]
690
+ while (group.connecting) await group.connecting
691
+ return group.connecting = (async () => {
692
+ const tab = await socket.req({
693
+ method: 'chrome.tabs.get',
694
+ params: { args: [Number(target.targetId)] },
695
+ })
696
+ const groups = await socket.req({
697
+ method: 'chrome.tabGroups.query',
698
+ params: { args: [{ windowId: tab.windowId }] },
699
+ })
700
+ const groupId = group.window_groups[tab.windowId]
701
+ const existing = groups.some(value => value.id === groupId)
702
+ const id = await socket.req({
703
+ method: 'chrome.tabs.group',
704
+ params: {
705
+ args: [{
706
+ tabIds: tab.id,
707
+ groupId: existing ? groupId : undefined,
708
+ createProperties: existing ? undefined : { windowId: tab.windowId },
709
+ }],
710
+ },
711
+ })
712
+ group.window_groups[tab.windowId] = id
713
+ await socket.req({
714
+ method: 'chrome.tabGroups.update',
715
+ params: {
716
+ args: [id, {
717
+ title: group.name,
718
+ color: group.color,
719
+ }],
720
+ },
721
+ })
722
+ return id
723
+ })().finally(() => {
724
+ group.connecting = null
725
+ })
726
+ }
727
+
687
728
  async _target(browser, target) {
688
729
  const socket = browser.socket
689
730
  const reverse = browser.reverse || browser.cdp_ext
731
+ if (target.group != null && target.group !== false) {
732
+ const spec = typeof target.group === 'string' ? { name: target.group } : target.group
733
+ browser.groups[spec.name] ??= {
734
+ ...spec,
735
+ window_groups: Object.create(null),
736
+ }
737
+ target.group = spec.name
738
+ }
690
739
  if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
691
740
  target.context = 'incognito'
692
741
  }
@@ -720,6 +769,7 @@ export class cdp extends EventTarget {
720
769
  Object.assign(info, targetInfo)
721
770
  if (!info.incognito) throw new Error('Existing target is not incognito')
722
771
  }
772
+ if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
723
773
  if (!info.sessionId) {
724
774
  info.sessionId = (await socket.req({
725
775
  method: 'Target.attachToTarget',
@@ -796,10 +846,14 @@ export class cdp extends EventTarget {
796
846
  },
797
847
  targets: Object.create(null),
798
848
  contexts: Object.create(null),
849
+ groups: Object.create(null),
799
850
  }
800
851
  if (target?.context != null && (record.reverse || record.cdp_ext)) {
801
852
  throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
802
853
  }
854
+ if (target?.group != null && target.group !== false && !record.reverse && !record.cdp_ext) {
855
+ throw new Error('Tab groups require the reverse Chrome extension (cdp_ext or reverse)')
856
+ }
803
857
  if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
804
858
  record.connecting = this._connect(record).catch(error => {
805
859
  record.socket?.close()
@@ -19,6 +19,7 @@ function target_info(tab) {
19
19
  url: tab.url ?? tab.pendingUrl ?? '',
20
20
  attached: !!attached[tab.id],
21
21
  incognito: tab.incognito,
22
+ groupId: tab.groupId,
22
23
  }
23
24
  }
24
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mefistofelix/cdp.js",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Low-level Chromium DevTools Protocol client for Node.js, Deno and Bun, with no runtime dependencies.",
5
5
  "type": "module",
6
6
  "exports": "./cdp.js",