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
|
|
460
|
+
"""Inject IndexedDB records into a running Chrome via a single reused tab.
|
|
461
461
|
|
|
462
|
-
Returns (written, total).
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
"
|
|
536
|
-
"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
151
|
+
"""Inject localStorage into a running Chrome by reusing a single hidden tab.
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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:
|
|
162
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
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"
|