ai-browser-profile 1.0.10 → 1.0.11

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.
@@ -457,12 +457,16 @@ def inject_indexeddb_via_cdp(
457
457
  cdp_url: str = "http://127.0.0.1:9655",
458
458
  load_wait_sec: float = 4.0,
459
459
  ) -> tuple[int, int]:
460
- """Inject IndexedDB records into a running Chrome via per-origin tabs.
460
+ """Inject IndexedDB records into a running Chrome via a single reused tab.
461
461
 
462
- Returns (written, total). For each origin we open a new tab at that
463
- origin (so the JS context is same-origin), wait for initial load to let
464
- the destination site bootstrap its own IDB schema, then run a single
465
- Runtime.evaluate that replays all of our records.
462
+ Returns (written, total). Opens ONE tab at the start, hides it off-screen,
463
+ then navigates that same tab through each origin in sequence. For each
464
+ origin: navigate, wait for bootstrap, run a single Runtime.evaluate that
465
+ replays the IDB records via the standard JS API. Closes the tab at end.
466
+
467
+ This replaces the previous pattern of opening one visible tab per origin
468
+ (which produced a flood of tab open/close churn when many domains were in
469
+ the import list).
466
470
  """
467
471
  from websocket import create_connection
468
472
 
@@ -471,8 +475,46 @@ def inject_indexeddb_via_cdp(
471
475
  msg_id = 0
472
476
  total_records = 0
473
477
  total_written = 0
478
+ target_id: Optional[str] = None
479
+ session_id: Optional[str] = None
474
480
 
475
481
  try:
482
+ # Open ONE reusable tab. We start at about:blank and navigate it
483
+ # per origin below; reusing the tab is what eliminates the visible
484
+ # "open a tab per origin" UX issue when many domains are in scope.
485
+ msg_id += 1
486
+ r = _cdp_send(ws, msg_id, "Target.createTarget", {"url": "about:blank"})
487
+ target_id = r.get("result", {}).get("targetId")
488
+ if not target_id:
489
+ log.warning("createTarget(about:blank) failed: %s", r.get("error"))
490
+ return 0, 0
491
+
492
+ msg_id += 1
493
+ r = _cdp_send(ws, msg_id, "Target.attachToTarget",
494
+ {"targetId": target_id, "flatten": True})
495
+ session_id = r.get("result", {}).get("sessionId")
496
+ if not session_id:
497
+ log.warning("attachToTarget(about:blank) failed: %s", r.get("error"))
498
+ return 0, 0
499
+
500
+ # Hide the import window off-screen so the user doesn't see it bounce
501
+ # through every origin. Best-effort.
502
+ try:
503
+ msg_id += 1
504
+ w = _cdp_send(ws, msg_id, "Browser.getWindowForTarget",
505
+ {"targetId": target_id})
506
+ window_id = w.get("result", {}).get("windowId")
507
+ if window_id:
508
+ msg_id += 1
509
+ _cdp_send(ws, msg_id, "Browser.setWindowBounds", {
510
+ "windowId": window_id,
511
+ "bounds": {"left": -32000, "top": -32000,
512
+ "width": 800, "height": 600,
513
+ "windowState": "normal"},
514
+ })
515
+ except Exception as e:
516
+ log.debug("Could not hide import window: %s", e)
517
+
476
518
  for origin, dumps in data.items():
477
519
  if not dumps:
478
520
  continue
@@ -489,84 +531,75 @@ def inject_indexeddb_via_cdp(
489
531
  total_records += origin_total
490
532
 
491
533
  url = origin.rstrip("/") + "/"
492
- target_id = None
493
- session_id = None
494
- try:
495
- msg_id += 1
496
- r = _cdp_send(ws, msg_id, "Target.createTarget", {"url": url})
497
- target_id = r.get("result", {}).get("targetId")
498
- if not target_id:
499
- log.warning("Couldn't create tab for %s: %s", origin, r)
500
- continue
501
534
 
502
- msg_id += 1
503
- r = _cdp_send(ws, msg_id, "Target.attachToTarget",
504
- {"targetId": target_id, "flatten": True})
505
- session_id = r.get("result", {}).get("sessionId")
506
- if not session_id:
507
- log.warning("Couldn't attach to tab for %s: %s", origin, r)
508
- continue
535
+ # Navigate the SAME tab to this origin. No new tab is created.
536
+ msg_id += 1
537
+ nav = _cdp_send(ws, msg_id, "Page.navigate", {"url": url},
538
+ session_id=session_id)
539
+ err = nav.get("result", {}).get("errorText") or nav.get("error")
540
+ if err:
541
+ log.warning(" %s: navigate failed (%s)", origin, err)
542
+ continue
509
543
 
510
- # Let the destination site finish its initial bootstrap (it
511
- # may create its own IDB schema with the canonical keyPath /
512
- # version; we then add to it).
513
- time.sleep(load_wait_sec)
514
-
515
- # Serialize the data to JSON and inline into the JS expression.
516
- # Records can be large; CDP accepts multi-MB expressions.
517
- payload = {
518
- "dbs": [
519
- {
520
- "name": db.name,
521
- "stores": {
522
- sn: [{"key": r.key, "value": r.value} for r in recs]
523
- for sn, recs in db.stores.items()
524
- },
525
- }
526
- for db in dumps
527
- ]
528
- }
529
- expression = _INJECT_JS.replace("__PAYLOAD__", json.dumps(payload))
544
+ # Let the destination site finish its initial bootstrap (it may
545
+ # create its own IDB schema with the canonical keyPath/version;
546
+ # we then add to it).
547
+ time.sleep(load_wait_sec)
530
548
 
531
- msg_id += 1
532
- r = _cdp_send(
533
- ws, msg_id, "Runtime.evaluate",
549
+ # Serialize the data to JSON and inline into the JS expression.
550
+ # Records can be large; CDP accepts multi-MB expressions.
551
+ payload = {
552
+ "dbs": [
534
553
  {
535
- "expression": expression,
536
- "awaitPromise": True,
537
- "returnByValue": True,
538
- "timeout": 60000,
539
- },
540
- session_id=session_id,
541
- )
542
- result = r.get("result", {}).get("result", {})
543
- exc = r.get("result", {}).get("exceptionDetails")
544
- if exc:
545
- log.warning(" %s: JS error %s", origin, exc.get("text") or exc)
546
- continue
547
- value = result.get("value")
548
- try:
549
- summary = json.loads(value).get("summary", []) if isinstance(value, str) else []
550
- except Exception:
551
- summary = []
552
- origin_written = 0
553
- for s in summary:
554
- if s.get("opened"):
555
- origin_written += s.get("written", 0)
556
- if s.get("errored"):
557
- log.warning(" %s/%s: %d errored", origin, s.get("db"), s.get("errored"))
558
- else:
559
- log.warning(" %s/%s: open failed (%s)", origin, s.get("db"), s.get("error"))
560
- total_written += origin_written
561
- log.info(" %s: wrote %d/%d records", origin, origin_written, origin_total)
562
- finally:
563
- if target_id:
564
- try:
565
- msg_id += 1
566
- _cdp_send(ws, msg_id, "Target.closeTarget", {"targetId": target_id})
567
- except Exception:
568
- pass
554
+ "name": db.name,
555
+ "stores": {
556
+ sn: [{"key": r.key, "value": r.value} for r in recs]
557
+ for sn, recs in db.stores.items()
558
+ },
559
+ }
560
+ for db in dumps
561
+ ]
562
+ }
563
+ expression = _INJECT_JS.replace("__PAYLOAD__", json.dumps(payload))
564
+
565
+ msg_id += 1
566
+ r = _cdp_send(
567
+ ws, msg_id, "Runtime.evaluate",
568
+ {
569
+ "expression": expression,
570
+ "awaitPromise": True,
571
+ "returnByValue": True,
572
+ "timeout": 60000,
573
+ },
574
+ session_id=session_id,
575
+ )
576
+ result = r.get("result", {}).get("result", {})
577
+ exc = r.get("result", {}).get("exceptionDetails")
578
+ if exc:
579
+ log.warning(" %s: JS error %s", origin, exc.get("text") or exc)
580
+ continue
581
+ value = result.get("value")
582
+ try:
583
+ summary = json.loads(value).get("summary", []) if isinstance(value, str) else []
584
+ except Exception:
585
+ summary = []
586
+ origin_written = 0
587
+ for s in summary:
588
+ if s.get("opened"):
589
+ origin_written += s.get("written", 0)
590
+ if s.get("errored"):
591
+ log.warning(" %s/%s: %d errored", origin, s.get("db"), s.get("errored"))
592
+ else:
593
+ log.warning(" %s/%s: open failed (%s)", origin, s.get("db"), s.get("error"))
594
+ total_written += origin_written
595
+ log.info(" %s: wrote %d/%d records", origin, origin_written, origin_total)
569
596
  finally:
597
+ if target_id:
598
+ try:
599
+ msg_id += 1
600
+ _cdp_send(ws, msg_id, "Target.closeTarget", {"targetId": target_id})
601
+ except Exception:
602
+ pass
570
603
  ws.close()
571
604
 
572
605
  log.info("Injected %d/%d IndexedDB records total", total_written, total_records)
@@ -148,19 +148,23 @@ def inject_localstorage_via_cdp(
148
148
  cdp_url: str = "http://127.0.0.1:9222",
149
149
  load_wait_sec: float = 4.0,
150
150
  ) -> int:
151
- """Inject localStorage into a running Chrome via per-origin tabs.
151
+ """Inject localStorage into a running Chrome by reusing a single hidden tab.
152
152
 
153
- For each origin: opens a new tab to that origin (so the JS context is
154
- same-origin), waits for load, evaluates a localStorage.setItem batch via
155
- Runtime.evaluate, then closes the tab. Returns total items written.
153
+ Opens ONE tab at the start, hides it off-screen, then navigates that same
154
+ tab through each origin in sequence to run a localStorage.setItem batch in
155
+ the page's JS context. Closes the tab at the end. Returns total items
156
+ written.
157
+
158
+ This replaces the previous pattern of opening one visible tab per origin
159
+ (which produced a flood of tab open/close churn when many domains were in
160
+ the import list).
156
161
 
157
162
  Args:
158
163
  data: dict of {origin -> {key: value}}. Origin must be http(s)://...
159
164
  cdp_url: base http(s) URL of the Chrome DevTools endpoint or a
160
165
  cdp://host:port shorthand.
161
- load_wait_sec: how long to wait between tab open and the JS eval to
162
- let the page initialize (no Page.loadEventFired listener
163
- yet — keep simple, race-tolerant via the JS try/catch).
166
+ load_wait_sec: seconds to wait after navigating between origins before
167
+ injecting (lets the destination page initialize).
164
168
  """
165
169
  from websocket import create_connection
166
170
 
@@ -168,8 +172,47 @@ def inject_localstorage_via_cdp(
168
172
  ws = create_connection(ws_url, timeout=15, suppress_origin=True)
169
173
  msg_id = 0
170
174
  total_set = 0
175
+ target_id: Optional[str] = None
176
+ session_id: Optional[str] = None
171
177
 
172
178
  try:
179
+ # Open ONE reusable tab. We start at about:blank and navigate it
180
+ # per origin below; reusing the tab is what eliminates the visible
181
+ # "29 tabs flashing open" UX issue.
182
+ msg_id += 1
183
+ r = _cdp_send(ws, msg_id, "Target.createTarget", {"url": "about:blank"})
184
+ target_id = r.get("result", {}).get("targetId")
185
+ if not target_id:
186
+ log.warning("createTarget(about:blank) failed: %s", r.get("error"))
187
+ return 0
188
+
189
+ msg_id += 1
190
+ r = _cdp_send(ws, msg_id, "Target.attachToTarget",
191
+ {"targetId": target_id, "flatten": True})
192
+ session_id = r.get("result", {}).get("sessionId")
193
+ if not session_id:
194
+ log.warning("attachToTarget(about:blank) failed: %s", r.get("error"))
195
+ return 0
196
+
197
+ # Move the tab's window way off-screen so the user doesn't see it
198
+ # bounce through every origin. Best-effort; some Chrome builds reject
199
+ # negative window bounds, in which case we just stay on-screen.
200
+ try:
201
+ msg_id += 1
202
+ w = _cdp_send(ws, msg_id, "Browser.getWindowForTarget",
203
+ {"targetId": target_id})
204
+ window_id = w.get("result", {}).get("windowId")
205
+ if window_id:
206
+ msg_id += 1
207
+ _cdp_send(ws, msg_id, "Browser.setWindowBounds", {
208
+ "windowId": window_id,
209
+ "bounds": {"left": -32000, "top": -32000,
210
+ "width": 800, "height": 600,
211
+ "windowState": "normal"},
212
+ })
213
+ except Exception as e:
214
+ log.debug("Could not hide import window: %s", e)
215
+
173
216
  for origin, items in data.items():
174
217
  if not items:
175
218
  continue
@@ -185,53 +228,44 @@ def inject_localstorage_via_cdp(
185
228
  continue
186
229
  url = origin.rstrip("/") + "/"
187
230
 
188
- target_id = None
189
- try:
190
- msg_id += 1
191
- r = _cdp_send(ws, msg_id, "Target.createTarget", {"url": url})
192
- target_id = r.get("result", {}).get("targetId")
193
- if not target_id:
194
- log.warning("createTarget failed for %s: %s", origin, r.get("error"))
195
- continue
196
-
197
- msg_id += 1
198
- r = _cdp_send(ws, msg_id, "Target.attachToTarget",
199
- {"targetId": target_id, "flatten": True})
200
- session_id = r.get("result", {}).get("sessionId")
201
- if not session_id:
202
- log.warning("attachToTarget failed for %s", origin)
203
- continue
204
-
205
- time.sleep(load_wait_sec)
231
+ # Navigate the SAME tab to this origin. No new tab is created.
232
+ msg_id += 1
233
+ nav = _cdp_send(ws, msg_id, "Page.navigate", {"url": url},
234
+ session_id=session_id)
235
+ err = nav.get("result", {}).get("errorText") or nav.get("error")
236
+ if err:
237
+ log.warning(" %s: navigate failed (%s)", origin, err)
238
+ continue
206
239
 
207
- # Inline the items as a JS object literal; localStorage rejects
208
- # non-string values implicitly by coercion (we already string-
209
- # coerced in read_localstorage).
210
- expr = (
211
- "(function(){try{var items=" + json.dumps(items) + ";"
212
- "var n=0;for(var k in items){try{localStorage.setItem(k,items[k]);n++;}catch(e){}}"
213
- "return n;}catch(e){return 'ERROR:'+e.toString();}})()"
214
- )
215
- msg_id += 1
216
- r = _cdp_send(
217
- ws, msg_id, "Runtime.evaluate",
218
- {"expression": expr, "returnByValue": True},
219
- session_id=session_id,
220
- )
221
- value = r.get("result", {}).get("result", {}).get("value")
222
- if isinstance(value, int):
223
- total_set += value
224
- log.info(" %s: set %d/%d items", origin, value, len(items))
225
- else:
226
- log.warning(" %s: %s", origin, value)
227
- finally:
228
- if target_id:
229
- try:
230
- msg_id += 1
231
- _cdp_send(ws, msg_id, "Target.closeTarget", {"targetId": target_id})
232
- except Exception:
233
- pass
240
+ time.sleep(load_wait_sec)
241
+
242
+ # Inline the items as a JS object literal; localStorage rejects
243
+ # non-string values implicitly by coercion (we already string-
244
+ # coerced in read_localstorage).
245
+ expr = (
246
+ "(function(){try{var items=" + json.dumps(items) + ";"
247
+ "var n=0;for(var k in items){try{localStorage.setItem(k,items[k]);n++;}catch(e){}}"
248
+ "return n;}catch(e){return 'ERROR:'+e.toString();}})()"
249
+ )
250
+ msg_id += 1
251
+ r = _cdp_send(
252
+ ws, msg_id, "Runtime.evaluate",
253
+ {"expression": expr, "returnByValue": True},
254
+ session_id=session_id,
255
+ )
256
+ value = r.get("result", {}).get("result", {}).get("value")
257
+ if isinstance(value, int):
258
+ total_set += value
259
+ log.info(" %s: set %d/%d items", origin, value, len(items))
260
+ else:
261
+ log.warning(" %s: %s", origin, value)
234
262
  finally:
263
+ if target_id:
264
+ try:
265
+ msg_id += 1
266
+ _cdp_send(ws, msg_id, "Target.closeTarget", {"targetId": target_id})
267
+ except Exception:
268
+ pass
235
269
  ws.close()
236
270
 
237
271
  log.info("Injected %d localStorage items total", total_set)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-browser-profile",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Extract user identity (name, emails, accounts, addresses, payments) from browser data into a self-ranking SQLite database. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "ai-browser-profile": "bin/cli.js"