luckiest-co 1.0.47 → 1.0.49
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ab-testing/CHANGELOG.md +5 -0
- package/skills/ab-testing/SKILL.md +12 -9
- package/skills/ab-testing/evals/evals.json +18 -7
- package/skills/ab-testing/references/sample-size-guide.md +2 -2
- package/skills/ads/CHANGELOG.md +5 -0
- package/skills/ads/SKILL.md +4 -4
- package/skills/ai-seo/CHANGELOG.md +7 -0
- package/skills/ai-seo/SKILL.md +27 -20
- package/skills/analytics/CHANGELOG.md +6 -0
- package/skills/analytics/SKILL.md +4 -3
- package/skills/analytics/evals/evals.json +27 -0
- package/skills/analytics/references/ga4-implementation.md +7 -3
- package/skills/analytics/references/gtm-implementation.md +8 -2
- package/skills/aso/CHANGELOG.md +5 -0
- package/skills/aso/SKILL.md +4 -4
- package/skills/aso/evals/evals.json +15 -2
- package/skills/aso/references/benchmarks.md +4 -2
- package/skills/aso/references/google-play-specs.md +5 -1
- package/skills/brainstorm/SKILL.md +59 -16
- package/skills/brainstorm/builder-check-prompt.md +23 -0
- package/skills/churn-prevention/CHANGELOG.md +5 -0
- package/skills/churn-prevention/SKILL.md +3 -3
- package/skills/churn-prevention/evals/evals.json +14 -0
- package/skills/churn-prevention/references/dunning-playbook.md +6 -1
- package/skills/coder-brainstorming/CHANGELOG.md +10 -0
- package/skills/coder-brainstorming/SKILL.md +62 -20
- package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
- package/skills/coder-brainstorming/scripts/server.cjs +1 -1
- package/skills/coder-debugging/CHANGELOG.md +4 -0
- package/skills/coder-debugging/SKILL.md +5 -5
- package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
- package/skills/coder-requesting-code-review/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
- package/skills/coder-subagent-driven-development/SKILL.md +3 -3
- package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +27 -7
- package/skills/cold-email/evals/evals.json +30 -0
- package/skills/cold-email/references/benchmarks.md +15 -11
- package/skills/cold-email/references/deliverability.md +110 -0
- package/skills/cold-email/references/linkedin-outreach.md +91 -0
- package/skills/cold-email/references/multichannel-cadence.md +79 -0
- package/skills/cold-email/references/outbound-plays.md +88 -0
- package/skills/cold-email/references/personalization.md +11 -0
- package/skills/cold-email/references/reply-handling.md +65 -0
- package/skills/competitor-profiling/CHANGELOG.md +5 -0
- package/skills/competitor-profiling/SKILL.md +4 -4
- package/skills/content-strategy/CHANGELOG.md +5 -0
- package/skills/content-strategy/SKILL.md +5 -3
- package/skills/copy-editing/CHANGELOG.md +5 -0
- package/skills/copy-editing/SKILL.md +5 -5
- package/skills/copy-editing/references/content-refresh.md +33 -0
- package/skills/cro/CHANGELOG.md +5 -0
- package/skills/cro/SKILL.md +14 -3
- package/skills/cro/references/form.md +1 -1
- package/skills/directory-submissions/CHANGELOG.md +5 -0
- package/skills/directory-submissions/SKILL.md +13 -4
- package/skills/directory-submissions/evals/evals.json +15 -1
- package/skills/directory-submissions/references/directory-list.md +2 -4
- package/skills/emails/CHANGELOG.md +5 -0
- package/skills/emails/SKILL.md +14 -4
- package/skills/launch/CHANGELOG.md +7 -0
- package/skills/launch/SKILL.md +8 -4
- package/skills/launch/evals/evals.json +29 -0
- package/skills/launch/references/shipped-changes.md +84 -0
- package/skills/launch/references/site-launch-qa.md +55 -0
- package/skills/lead-magnets/CHANGELOG.md +5 -0
- package/skills/lead-magnets/SKILL.md +6 -4
- package/skills/lead-magnets/evals/evals.json +18 -0
- package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
- package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
- package/skills/lead-magnets/references/content-to-skill.md +108 -0
- package/skills/marketing-plan/CHANGELOG.md +5 -0
- package/skills/marketing-plan/SKILL.md +3 -3
- package/skills/marketing-plan/evals/evals.json +13 -0
- package/skills/marketing-plan/references/budget-planning.md +26 -17
- package/skills/model-router/CHANGELOG.md +6 -0
- package/skills/model-router/SKILL.md +3 -3
- package/skills/model-router/scripts/route.py +1 -1
- package/skills/model-router/scripts/test_route.py +5 -0
- package/skills/pricing/CHANGELOG.md +5 -0
- package/skills/pricing/SKILL.md +4 -4
- package/skills/pricing/evals/evals.json +12 -0
- package/skills/pricing/references/research-methods.md +8 -4
- package/skills/programmatic-seo/CHANGELOG.md +5 -0
- package/skills/programmatic-seo/SKILL.md +8 -4
- package/skills/programmatic-seo/evals/evals.json +13 -0
- package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +27 -13
- package/skills/prospecting/evals/evals.json +41 -0
- package/skills/prospecting/references/account-research.md +59 -0
- package/skills/prospecting/references/b2b-prospecting.md +1 -1
- package/skills/prospecting/references/compliance.md +44 -6
- package/skills/prospecting/references/data-sources.md +13 -15
- package/skills/prospecting/references/enrichment-playbook.md +61 -0
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/prospecting/references/signal-plays.md +99 -0
- package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
- package/skills/referrals/CHANGELOG.md +5 -0
- package/skills/referrals/SKILL.md +10 -4
- package/skills/referrals/evals/evals.json +99 -0
- package/skills/referrals/references/affiliate-operations.md +64 -0
- package/skills/referrals/references/affiliate-programs.md +2 -2
- package/skills/referrals/references/program-examples.md +14 -6
- package/skills/referrals/references/review-platform-rules.md +36 -0
- package/skills/referrals/references/review-program-operations.md +62 -0
- package/skills/referrals/references/review-programs.md +96 -0
- package/skills/revops/CHANGELOG.md +5 -0
- package/skills/revops/SKILL.md +10 -6
- package/skills/revops/evals/evals.json +13 -0
- package/skills/revops/references/automation-playbooks.md +1 -1
- package/skills/revops/references/inbound-routing.md +43 -0
- package/skills/revops/references/lifecycle-definitions.md +31 -0
- package/skills/sales-enablement/CHANGELOG.md +5 -0
- package/skills/sales-enablement/SKILL.md +6 -4
- package/skills/sales-enablement/evals/evals.json +14 -0
- package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
- package/skills/schema/CHANGELOG.md +5 -0
- package/skills/schema/SKILL.md +20 -8
- package/skills/schema/evals/evals.json +40 -0
- package/skills/schema/references/schema-examples.md +9 -15
- package/skills/seo-audit/CHANGELOG.md +5 -0
- package/skills/seo-audit/SKILL.md +10 -16
- package/skills/seo-audit/evals/evals.json +68 -0
- package/skills/seo-audit/references/international-seo.md +9 -11
- package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
- package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
- package/skills/seo-audit/references/local-seo.md +86 -0
- package/skills/seo-audit/references/rankings-push.md +62 -0
- package/skills/seo-audit/references/title-tags.md +89 -0
- package/skills/signup/CHANGELOG.md +5 -0
- package/skills/signup/SKILL.md +7 -3
- package/skills/site-architecture/CHANGELOG.md +5 -0
- package/skills/site-architecture/SKILL.md +5 -4
- package/skills/site-architecture/evals/evals.json +25 -0
- package/skills/site-architecture/references/platforms-and-migration.md +84 -0
- package/skills/social/CHANGELOG.md +5 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/trends/.skillignore +3 -0
- package/skills/trends/CHANGELOG.md +20 -0
- package/skills/trends/SKILL.md +12 -8
- package/skills/trends/references/agent-mode.md +36 -0
- package/skills/trends/references/category-peers.md +38 -0
- package/skills/trends/references/comparison.md +227 -0
- package/skills/trends/references/competitors.md +29 -0
- package/skills/trends/references/discovery.md +90 -0
- package/skills/trends/references/engine-contract.md +111 -1889
- package/skills/trends/references/followups.md +112 -0
- package/skills/trends/references/grok-bot-x.md +55 -0
- package/skills/trends/references/hiring-signals.md +19 -0
- package/skills/trends/references/library-queue.md +33 -0
- package/skills/trends/references/recommendations.md +86 -0
- package/skills/trends/references/research-runbook.md +781 -0
- package/skills/trends/references/runtime.md +165 -0
- package/skills/trends/references/save-html-brief.md +39 -20
- package/skills/trends/references/setup-wizard.md +253 -0
- package/skills/trends/references/synthesis.md +289 -0
- package/skills/trends/scripts/box_chrome_login.py +209 -0
- package/skills/trends/scripts/briefing.py +9 -17
- package/skills/trends/scripts/last30days.py +3247 -280
- package/skills/trends/scripts/lib/agentcookie.py +196 -0
- package/skills/trends/scripts/lib/amazon.py +877 -0
- package/skills/trends/scripts/lib/arxiv.py +42 -8
- package/skills/trends/scripts/lib/backends.py +769 -0
- package/skills/trends/scripts/lib/bird_x.py +340 -57
- package/skills/trends/scripts/lib/bluesky.py +84 -10
- package/skills/trends/scripts/lib/bounded_get.py +156 -0
- package/skills/trends/scripts/lib/brightdata.py +291 -0
- package/skills/trends/scripts/lib/categories.py +7 -1
- package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
- package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
- package/skills/trends/scripts/lib/cluster.py +31 -54
- package/skills/trends/scripts/lib/competitors.py +10 -1
- package/skills/trends/scripts/lib/cookie_extract.py +221 -44
- package/skills/trends/scripts/lib/cookie_paths.py +27 -0
- package/skills/trends/scripts/lib/corpus.py +439 -0
- package/skills/trends/scripts/lib/devto.py +313 -0
- package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
- package/skills/trends/scripts/lib/doctor.py +2166 -0
- package/skills/trends/scripts/lib/dripstack.py +203 -0
- package/skills/trends/scripts/lib/entity_extract.py +37 -0
- package/skills/trends/scripts/lib/env.py +1092 -138
- package/skills/trends/scripts/lib/fanout.py +11 -1
- package/skills/trends/scripts/lib/feed.py +61 -0
- package/skills/trends/scripts/lib/fetch_memo.py +135 -0
- package/skills/trends/scripts/lib/freshness.py +566 -0
- package/skills/trends/scripts/lib/fusion.py +314 -28
- package/skills/trends/scripts/lib/github.py +438 -24
- package/skills/trends/scripts/lib/grok_x.py +1237 -0
- package/skills/trends/scripts/lib/grounding.py +129 -11
- package/skills/trends/scripts/lib/hackernews.py +61 -4
- package/skills/trends/scripts/lib/health.py +373 -0
- package/skills/trends/scripts/lib/hosted.py +333 -0
- package/skills/trends/scripts/lib/html_publish.py +34 -0
- package/skills/trends/scripts/lib/html_render.py +169 -3
- package/skills/trends/scripts/lib/http.py +1215 -70
- package/skills/trends/scripts/lib/instagram.py +132 -9
- package/skills/trends/scripts/lib/jobs.py +4 -2
- package/skills/trends/scripts/lib/library.py +308 -0
- package/skills/trends/scripts/lib/library_index.py +477 -0
- package/skills/trends/scripts/lib/linkedin.py +271 -24
- package/skills/trends/scripts/lib/log.py +5 -2
- package/skills/trends/scripts/lib/meta_ads.py +982 -0
- package/skills/trends/scripts/lib/normalize.py +382 -28
- package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
- package/skills/trends/scripts/lib/permission_preflight.py +42 -3
- package/skills/trends/scripts/lib/perplexity.py +1080 -468
- package/skills/trends/scripts/lib/pipeline.py +5079 -211
- package/skills/trends/scripts/lib/planner.py +396 -53
- package/skills/trends/scripts/lib/polymarket.py +285 -23
- package/skills/trends/scripts/lib/prescriptions.py +432 -0
- package/skills/trends/scripts/lib/private_files.py +21 -0
- package/skills/trends/scripts/lib/providers.py +231 -10
- package/skills/trends/scripts/lib/quality_nudge.py +122 -37
- package/skills/trends/scripts/lib/reddit.py +177 -12
- package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
- package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
- package/skills/trends/scripts/lib/reddit_listing.py +215 -14
- package/skills/trends/scripts/lib/reddit_public.py +12 -13
- package/skills/trends/scripts/lib/reddit_search.py +304 -0
- package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
- package/skills/trends/scripts/lib/registers.py +134 -0
- package/skills/trends/scripts/lib/relevance.py +17 -0
- package/skills/trends/scripts/lib/render.py +2161 -271
- package/skills/trends/scripts/lib/rerank.py +623 -55
- package/skills/trends/scripts/lib/resolve.py +50 -1
- package/skills/trends/scripts/lib/safari_cookies.py +42 -15
- package/skills/trends/scripts/lib/schema.py +782 -1
- package/skills/trends/scripts/lib/setup_wizard.py +779 -120
- package/skills/trends/scripts/lib/signals.py +206 -7
- package/skills/trends/scripts/lib/stocktwits.py +374 -0
- package/skills/trends/scripts/lib/subproc.py +307 -53
- package/skills/trends/scripts/lib/techmeme.py +89 -64
- package/skills/trends/scripts/lib/telegram.py +317 -0
- package/skills/trends/scripts/lib/tiktok.py +13 -9
- package/skills/trends/scripts/lib/topic_shape.py +340 -0
- package/skills/trends/scripts/lib/transcribe.py +12 -3
- package/skills/trends/scripts/lib/trustpilot.py +256 -5
- package/skills/trends/scripts/lib/truthsocial.py +9 -1
- package/skills/trends/scripts/lib/ui.py +6 -0
- package/skills/trends/scripts/lib/usage.py +132 -0
- package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
- package/skills/trends/scripts/lib/windows_job.py +138 -0
- package/skills/trends/scripts/lib/x_api.py +824 -0
- package/skills/trends/scripts/lib/x_envelope.py +733 -0
- package/skills/trends/scripts/lib/x_judge.py +292 -0
- package/skills/trends/scripts/lib/xai_x.py +26 -3
- package/skills/trends/scripts/lib/xquik.py +89 -15
- package/skills/trends/scripts/lib/xurl_x.py +284 -91
- package/skills/trends/scripts/lib/youtube_yt.py +570 -119
- package/skills/trends/scripts/setup-keychain.sh +12 -4
- package/skills/trends/scripts/setup-pass.sh +3 -0
- package/skills/trends/scripts/store.py +444 -50
- package/skills/trends/scripts/watchlist.py +38 -11
- package/skills/trends/scripts/x_lane_parity.py +107 -0
- package/skills/video-studio/CHANGELOG.md +4 -0
- package/skills/video-studio/SKILL.md +4 -4
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
- package/skills/video-studio-motion/CHANGELOG.md +4 -0
- package/skills/video-studio-motion/SKILL.md +8 -5
- package/skills/youtube-planner/ATTRIBUTION.md +25 -0
- package/skills/youtube-planner/CHANGELOG.md +12 -0
- package/skills/youtube-planner/SKILL.md +191 -0
- package/skills/youtube-planner/references/plan-template.md +57 -0
- package/skills/youtube-planner/references/stages.md +119 -0
- package/skills/youtube-planner/scripts/floor_ceiling.py +132 -0
- package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/skills/trends/scripts/lib/reddit_rss.py +0 -224
|
@@ -21,6 +21,8 @@ import tempfile
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Optional
|
|
23
23
|
|
|
24
|
+
from .cookie_paths import path_exists, path_is_dir
|
|
25
|
+
|
|
24
26
|
logger = logging.getLogger(__name__)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -204,6 +206,7 @@ def _extract_chromium_cookies_macos(
|
|
|
204
206
|
keychain_service: str,
|
|
205
207
|
domain: str,
|
|
206
208
|
cookie_names: list[str],
|
|
209
|
+
key_cache: Optional[dict[str, Optional[bytes]]] = None,
|
|
207
210
|
) -> Optional[dict[str, str]]:
|
|
208
211
|
"""Extract cookies from any Chromium-based browser on macOS.
|
|
209
212
|
|
|
@@ -220,7 +223,11 @@ def _extract_chromium_cookies_macos(
|
|
|
220
223
|
Dict mapping cookie name to decrypted value, or None on failure.
|
|
221
224
|
Only includes cookies that were successfully found and decrypted.
|
|
222
225
|
"""
|
|
223
|
-
|
|
226
|
+
try:
|
|
227
|
+
db_path.stat()
|
|
228
|
+
except PermissionError:
|
|
229
|
+
raise
|
|
230
|
+
except OSError:
|
|
224
231
|
logger.info("%s cookies database not found at %s", keychain_service, db_path)
|
|
225
232
|
return None
|
|
226
233
|
|
|
@@ -229,15 +236,24 @@ def _extract_chromium_cookies_macos(
|
|
|
229
236
|
tmp_path = None
|
|
230
237
|
try:
|
|
231
238
|
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".sqlite")
|
|
232
|
-
|
|
239
|
+
# mkstemp creates the file 0600. copy2 would copy the source DB's
|
|
240
|
+
# permission bits onto the temp file before the chmod below runs,
|
|
241
|
+
# briefly exposing live cookies when the source DB is looser.
|
|
242
|
+
shutil.copyfile(str(db_path), tmp_path)
|
|
233
243
|
_lock_temp_cookie_copy(tmp_path)
|
|
234
244
|
except Exception as e:
|
|
235
|
-
|
|
245
|
+
browser_read_denied = isinstance(e, PermissionError) and e.filename == str(db_path)
|
|
246
|
+
if browser_read_denied:
|
|
247
|
+
logger.info("Permission denied reading %s cookies database", keychain_service)
|
|
248
|
+
else:
|
|
249
|
+
logger.info("Failed to copy %s cookies database: %s", keychain_service, e)
|
|
236
250
|
if tmp_path:
|
|
237
251
|
try:
|
|
238
252
|
Path(tmp_path).unlink(missing_ok=True)
|
|
239
253
|
except Exception:
|
|
240
254
|
pass
|
|
255
|
+
if browser_read_denied:
|
|
256
|
+
raise
|
|
241
257
|
return None
|
|
242
258
|
finally:
|
|
243
259
|
if tmp_fd is not None:
|
|
@@ -274,8 +290,13 @@ def _extract_chromium_cookies_macos(
|
|
|
274
290
|
# Keychain prompt for browsers that don't hold the requested
|
|
275
291
|
# cookie, which matters for FROM_BROWSER=auto across several
|
|
276
292
|
# installed Chromium browsers.
|
|
277
|
-
|
|
278
|
-
|
|
293
|
+
if key_cache is not None and keychain_service in key_cache:
|
|
294
|
+
aes_key = key_cache[keychain_service]
|
|
295
|
+
else:
|
|
296
|
+
passphrase = _get_chromium_encryption_key(keychain_service)
|
|
297
|
+
aes_key = _derive_aes_key(passphrase) if passphrase else None
|
|
298
|
+
if key_cache is not None:
|
|
299
|
+
key_cache[keychain_service] = aes_key
|
|
279
300
|
key_fetched = True
|
|
280
301
|
if aes_key is None:
|
|
281
302
|
logger.debug("Skipping encrypted cookie %s — no Keychain access", name)
|
|
@@ -316,16 +337,14 @@ def extract_chrome_cookies_macos(domain: str, cookie_names: list[str]) -> Option
|
|
|
316
337
|
same modern ``Default/Network/Cookies`` (Chromium >= 96) and legacy
|
|
317
338
|
``Default/Cookies`` probing as the rest of the Chromium family.
|
|
318
339
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
logger.info("Chrome cookies database not found under %s", CHROME_BASE_DIR)
|
|
322
|
-
return None
|
|
323
|
-
return _extract_chromium_cookies_macos(
|
|
324
|
-
db_path, "Chrome Safe Storage", domain, cookie_names
|
|
340
|
+
return _extract_chromium_cookies_any_profile(
|
|
341
|
+
CHROME_BASE_DIR, "Chrome Safe Storage", domain, cookie_names
|
|
325
342
|
)
|
|
326
343
|
|
|
327
344
|
|
|
328
|
-
def _profile_cookie_db(
|
|
345
|
+
def _profile_cookie_db(
|
|
346
|
+
profile_dir: Path, denials: Optional[list[PermissionError]] = None
|
|
347
|
+
) -> Optional[Path]:
|
|
329
348
|
"""Return the Cookies DB inside a profile dir, or None.
|
|
330
349
|
|
|
331
350
|
Prefers the modern ``Network/Cookies`` location (Chromium >= 96 moved the
|
|
@@ -333,10 +352,20 @@ def _profile_cookie_db(profile_dir: Path) -> Optional[Path]:
|
|
|
333
352
|
to the legacy flat ``Cookies`` file. Different browsers and versions use
|
|
334
353
|
different layouts, so both are probed.
|
|
335
354
|
"""
|
|
355
|
+
denied: list[PermissionError] = []
|
|
336
356
|
for rel in ("Network/Cookies", "Cookies"):
|
|
337
357
|
candidate = profile_dir / rel
|
|
338
|
-
|
|
339
|
-
|
|
358
|
+
try:
|
|
359
|
+
if path_exists(candidate):
|
|
360
|
+
if denials is not None:
|
|
361
|
+
denials.extend(denied)
|
|
362
|
+
return candidate
|
|
363
|
+
except PermissionError as exc:
|
|
364
|
+
denied.append(exc)
|
|
365
|
+
if denials is not None:
|
|
366
|
+
denials.extend(denied)
|
|
367
|
+
elif denied:
|
|
368
|
+
raise denied[0]
|
|
340
369
|
return None
|
|
341
370
|
|
|
342
371
|
|
|
@@ -351,28 +380,96 @@ def _find_chromium_cookies_db(base_dir: Path) -> Optional[Path]:
|
|
|
351
380
|
recently used one is the likeliest to hold current cookies. Lexicographic
|
|
352
381
|
sort would visit "Profile 10" before "Profile 2", which can return the
|
|
353
382
|
wrong profile, so we sort by mtime.
|
|
383
|
+
|
|
384
|
+
Kept for backward compatibility; new code should use
|
|
385
|
+
_find_all_chromium_cookies_dbs() to search across all profiles.
|
|
354
386
|
"""
|
|
355
|
-
|
|
356
|
-
if
|
|
357
|
-
|
|
387
|
+
dbs = _find_all_chromium_cookies_dbs(base_dir)
|
|
388
|
+
return dbs[0] if dbs else None
|
|
389
|
+
|
|
358
390
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
391
|
+
def _find_all_chromium_cookies_dbs(
|
|
392
|
+
base_dir: Path, denials: Optional[list[PermissionError]] = None
|
|
393
|
+
) -> list[Path]:
|
|
394
|
+
"""Return ALL candidate Cookies DBs under base_dir, best-guess order first.
|
|
362
395
|
|
|
396
|
+
Order: Default, the base dir itself (Opera's flat layout), then numbered
|
|
397
|
+
"Profile N" dirs by most-recently-modified. Unlike _find_chromium_cookies_db
|
|
398
|
+
(which returns the first DB that merely EXISTS), this returns every profile
|
|
399
|
+
so the caller can pick the one that actually holds the target domain's
|
|
400
|
+
cookies. Needed because a logged-in session often lives in a non-Default
|
|
401
|
+
profile while Default still has a (guest-only) cookie DB.
|
|
402
|
+
"""
|
|
403
|
+
paths: list[Path] = []
|
|
404
|
+
seen: set[Path] = set()
|
|
405
|
+
denied: list[PermissionError] = []
|
|
406
|
+
|
|
407
|
+
def add(p: Optional[Path]) -> None:
|
|
408
|
+
if p is not None and p not in seen:
|
|
409
|
+
seen.add(p)
|
|
410
|
+
paths.append(p)
|
|
411
|
+
|
|
412
|
+
add(_profile_cookie_db(base_dir / "Default", denied))
|
|
413
|
+
add(_profile_cookie_db(base_dir, denied))
|
|
363
414
|
try:
|
|
364
|
-
candidates = [
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
415
|
+
candidates = []
|
|
416
|
+
for child in base_dir.iterdir():
|
|
417
|
+
if not child.name.startswith("Profile "):
|
|
418
|
+
continue
|
|
419
|
+
try:
|
|
420
|
+
if path_is_dir(child):
|
|
421
|
+
candidates.append((child.stat().st_mtime, child))
|
|
422
|
+
except PermissionError as exc:
|
|
423
|
+
denied.append(exc)
|
|
424
|
+
for _, child in sorted(candidates, reverse=True):
|
|
425
|
+
add(_profile_cookie_db(child, denied))
|
|
426
|
+
except PermissionError as exc:
|
|
427
|
+
denied.append(exc)
|
|
372
428
|
except OSError:
|
|
373
429
|
pass
|
|
430
|
+
if denials is not None:
|
|
431
|
+
denials.extend(denied)
|
|
432
|
+
elif denied and not paths:
|
|
433
|
+
raise denied[0]
|
|
434
|
+
return paths
|
|
374
435
|
|
|
375
|
-
|
|
436
|
+
|
|
437
|
+
def _extract_chromium_cookies_any_profile(
|
|
438
|
+
base_dir: Path, keychain_service: str, domain: str, cookie_names: list[str]
|
|
439
|
+
) -> Optional[dict[str, str]]:
|
|
440
|
+
"""Try every profile under base_dir and return the best cookie match.
|
|
441
|
+
|
|
442
|
+
Returns the first profile that yields ALL requested cookie_names. If no
|
|
443
|
+
profile has the complete set, returns the first partial match found, or
|
|
444
|
+
None if no profile yielded any. This fixes the single-profile limitation
|
|
445
|
+
where a guest-only Default profile shadowed a logged-in "Profile N".
|
|
446
|
+
"""
|
|
447
|
+
denials: list[PermissionError] = []
|
|
448
|
+
db_paths = _find_all_chromium_cookies_dbs(base_dir, denials)
|
|
449
|
+
if not db_paths:
|
|
450
|
+
if denials:
|
|
451
|
+
raise denials[0]
|
|
452
|
+
logger.info("%s cookies database not found under %s", keychain_service, base_dir)
|
|
453
|
+
return None
|
|
454
|
+
best: Optional[dict[str, str]] = None
|
|
455
|
+
key_cache: dict[str, Optional[bytes]] = {}
|
|
456
|
+
for db_path in db_paths:
|
|
457
|
+
try:
|
|
458
|
+
got = _extract_chromium_cookies_macos(
|
|
459
|
+
db_path, keychain_service, domain, cookie_names, key_cache=key_cache
|
|
460
|
+
)
|
|
461
|
+
except PermissionError as exc:
|
|
462
|
+
denials.append(exc)
|
|
463
|
+
continue
|
|
464
|
+
if got:
|
|
465
|
+
if all(name in got for name in cookie_names):
|
|
466
|
+
logger.debug("Found complete cookie set for %s in %s", domain, db_path)
|
|
467
|
+
return got
|
|
468
|
+
if best is None:
|
|
469
|
+
best = got
|
|
470
|
+
if denials:
|
|
471
|
+
raise denials[0]
|
|
472
|
+
return best
|
|
376
473
|
|
|
377
474
|
|
|
378
475
|
def _find_brave_cookies_db() -> Optional[Path]:
|
|
@@ -386,11 +483,9 @@ def extract_brave_cookies_macos(domain: str, cookie_names: list[str]) -> Optiona
|
|
|
386
483
|
Brave uses the same v10 AES-128-CBC encryption as Chrome; only the DB
|
|
387
484
|
path and Keychain service name differ.
|
|
388
485
|
"""
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return None
|
|
393
|
-
return _extract_chromium_cookies_macos(db_path, "Brave Safe Storage", domain, cookie_names)
|
|
486
|
+
return _extract_chromium_cookies_any_profile(
|
|
487
|
+
BRAVE_BASE_DIR, "Brave Safe Storage", domain, cookie_names
|
|
488
|
+
)
|
|
394
489
|
|
|
395
490
|
|
|
396
491
|
def extract_chromium_browser_cookies_macos(
|
|
@@ -407,8 +502,6 @@ def extract_chromium_browser_cookies_macos(
|
|
|
407
502
|
logger.debug("Unknown Chromium browser: %s", browser)
|
|
408
503
|
return None
|
|
409
504
|
base_dir, keychain_service = spec
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return None
|
|
414
|
-
return _extract_chromium_cookies_macos(db_path, keychain_service, domain, cookie_names)
|
|
505
|
+
return _extract_chromium_cookies_any_profile(
|
|
506
|
+
base_dir, keychain_service, domain, cookie_names
|
|
507
|
+
)
|
|
@@ -2,59 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
from . import dedupe, entity_extract, schema
|
|
6
6
|
|
|
7
|
-
from . import dedupe, schema
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
def _cluster_sort_key(candidate: schema.Candidate) -> tuple:
|
|
9
|
+
"""Sort key that partitions stale candidates below fresh ones.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"make", "being", "making", "over", "into", "than", "they", "their",
|
|
19
|
-
"would", "could", "get", "got", "some", "like", "back", "going",
|
|
20
|
-
"breaking", "https", "http", "www", "com",
|
|
21
|
-
})
|
|
11
|
+
Stale items (all dated source_items outside the window) must never lead
|
|
12
|
+
cluster representatives or render as the cluster title.
|
|
13
|
+
"""
|
|
14
|
+
return (
|
|
15
|
+
1 if schema.candidate_out_of_window(candidate) else 0,
|
|
16
|
+
-candidate.final_score,
|
|
17
|
+
)
|
|
22
18
|
|
|
19
|
+
CLUSTERABLE_INTENTS = {"breaking_news", "opinion", "comparison", "prediction"}
|
|
23
20
|
|
|
24
21
|
def _candidate_text(candidate: schema.Candidate) -> str:
|
|
25
22
|
return " ".join(part for part in [candidate.title, candidate.snippet] if part).strip()
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
def _extract_entities(text: str) -> set[str]:
|
|
29
|
-
"""Extract significant words (proper nouns, numbers, capitalized words) from text.
|
|
30
|
-
|
|
31
|
-
Used for cross-source cluster merging where phrasing differs but entities overlap.
|
|
32
|
-
"""
|
|
33
|
-
# Normalize but preserve word boundaries
|
|
34
|
-
words = re.sub(r"[^\w\s]", " ", text).split()
|
|
35
|
-
entities = set()
|
|
36
|
-
for word in words:
|
|
37
|
-
lower = word.lower()
|
|
38
|
-
if lower in _ENTITY_STOPWORDS or len(word) <= 2:
|
|
39
|
-
continue
|
|
40
|
-
# Keep words that are: capitalized, ALL CAPS, contain digits, or 4+ chars
|
|
41
|
-
if word[0].isupper() or word.isupper() or any(c.isdigit() for c in word) or len(word) >= 4:
|
|
42
|
-
entities.add(lower)
|
|
43
|
-
return entities
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _entity_overlap(entities_a: set[str], entities_b: set[str]) -> float:
|
|
47
|
-
"""Jaccard-style overlap on extracted entities."""
|
|
48
|
-
if not entities_a or not entities_b:
|
|
49
|
-
return 0.0
|
|
50
|
-
intersection = entities_a & entities_b
|
|
51
|
-
smaller = min(len(entities_a), len(entities_b))
|
|
52
|
-
# Use overlap coefficient (intersection / min) instead of Jaccard,
|
|
53
|
-
# because a short tweet about the same event as a long Reddit post
|
|
54
|
-
# will have fewer total entities but high overlap with the larger set.
|
|
55
|
-
return len(intersection) / smaller if smaller > 0 else 0.0
|
|
56
|
-
|
|
57
|
-
|
|
58
24
|
def _mmr_representatives(
|
|
59
25
|
candidates: list[schema.Candidate],
|
|
60
26
|
text_cache: dict[str, dedupe._PreparedText],
|
|
@@ -66,7 +32,7 @@ def _mmr_representatives(
|
|
|
66
32
|
remaining = list(candidates)
|
|
67
33
|
while remaining and len(selected) < limit:
|
|
68
34
|
if not selected:
|
|
69
|
-
best =
|
|
35
|
+
best = min(remaining, key=_cluster_sort_key)
|
|
70
36
|
selected.append(best)
|
|
71
37
|
remaining_set.discard(best.candidate_id)
|
|
72
38
|
remaining = [c for c in remaining if c.candidate_id in remaining_set]
|
|
@@ -74,12 +40,16 @@ def _mmr_representatives(
|
|
|
74
40
|
|
|
75
41
|
selected_preps = [text_cache[c.candidate_id] for c in selected]
|
|
76
42
|
|
|
77
|
-
def score(candidate: schema.Candidate) ->
|
|
43
|
+
def score(candidate: schema.Candidate) -> tuple:
|
|
78
44
|
prep = text_cache[candidate.candidate_id]
|
|
79
45
|
diversity_penalty = max(
|
|
80
46
|
dedupe.prepared_similarity(prep, sp) for sp in selected_preps
|
|
81
47
|
)
|
|
82
|
-
|
|
48
|
+
base_score = (diversity_lambda * candidate.final_score) - ((1 - diversity_lambda) * diversity_penalty * 100)
|
|
49
|
+
return (
|
|
50
|
+
0 if schema.candidate_out_of_window(candidate) else 1,
|
|
51
|
+
base_score,
|
|
52
|
+
)
|
|
83
53
|
|
|
84
54
|
best = max(remaining, key=score)
|
|
85
55
|
selected.append(best)
|
|
@@ -135,7 +105,7 @@ def cluster_candidates(
|
|
|
135
105
|
|
|
136
106
|
clusters: list[schema.Cluster] = []
|
|
137
107
|
for index, group in enumerate(groups, start=1):
|
|
138
|
-
group.sort(key=
|
|
108
|
+
group.sort(key=_cluster_sort_key)
|
|
139
109
|
cluster_id = f"cluster-{index}"
|
|
140
110
|
representatives = _mmr_representatives(group, text_cache)
|
|
141
111
|
for candidate in group:
|
|
@@ -153,7 +123,11 @@ def cluster_candidates(
|
|
|
153
123
|
)
|
|
154
124
|
|
|
155
125
|
# Second pass: merge small clusters that share entities across sources.
|
|
156
|
-
clusters = _merge_entity_clusters(
|
|
126
|
+
clusters = _merge_entity_clusters(
|
|
127
|
+
clusters,
|
|
128
|
+
candidates,
|
|
129
|
+
min_shared_entities=2 if "discover-mode" in plan.notes else 1,
|
|
130
|
+
)
|
|
157
131
|
|
|
158
132
|
return sorted(clusters, key=lambda cluster: cluster.score, reverse=True)
|
|
159
133
|
|
|
@@ -161,6 +135,8 @@ def cluster_candidates(
|
|
|
161
135
|
def _merge_entity_clusters(
|
|
162
136
|
clusters: list[schema.Cluster],
|
|
163
137
|
all_candidates: list[schema.Candidate],
|
|
138
|
+
*,
|
|
139
|
+
min_shared_entities: int = 1,
|
|
164
140
|
) -> list[schema.Cluster]:
|
|
165
141
|
"""Merge small clusters that cover the same story across different sources.
|
|
166
142
|
|
|
@@ -182,7 +158,7 @@ def _merge_entity_clusters(
|
|
|
182
158
|
for cid in cl.candidate_ids:
|
|
183
159
|
cand = candidate_map.get(cid)
|
|
184
160
|
if cand:
|
|
185
|
-
entities |=
|
|
161
|
+
entities |= entity_extract.extract_text_entities(_candidate_text(cand))
|
|
186
162
|
cluster_entities.append(entities)
|
|
187
163
|
|
|
188
164
|
# Only merge clusters with <= 3 items (don't merge already-large clusters)
|
|
@@ -207,8 +183,9 @@ def _merge_entity_clusters(
|
|
|
207
183
|
if poly_i != poly_j:
|
|
208
184
|
continue
|
|
209
185
|
|
|
210
|
-
|
|
211
|
-
|
|
186
|
+
shared_entities = cluster_entities[i] & cluster_entities[j]
|
|
187
|
+
overlap = entity_extract.entity_overlap(cluster_entities[i], cluster_entities[j])
|
|
188
|
+
if len(shared_entities) >= min_shared_entities and overlap >= 0.45:
|
|
212
189
|
merged_into[j] = i
|
|
213
190
|
|
|
214
191
|
if not merged_into:
|
|
@@ -236,7 +213,7 @@ def _merge_entity_clusters(
|
|
|
236
213
|
|
|
237
214
|
# Pick representatives from combined pool
|
|
238
215
|
combined_candidates = [candidate_map[cid] for cid in combined_cids if cid in candidate_map]
|
|
239
|
-
combined_candidates.sort(key=
|
|
216
|
+
combined_candidates.sort(key=_cluster_sort_key)
|
|
240
217
|
merge_text_cache = {
|
|
241
218
|
c.candidate_id: dedupe._PreparedText(_candidate_text(c))
|
|
242
219
|
for c in combined_candidates
|
|
@@ -19,6 +19,15 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
19
19
|
from . import dates, grounding, log
|
|
20
20
|
from .resolve import _has_backend
|
|
21
21
|
|
|
22
|
+
# Peer cap vs total vs-entity cap (main + peers).
|
|
23
|
+
COMPETITORS_MIN = 1
|
|
24
|
+
COMPETITORS_MAX = 6
|
|
25
|
+
COMPETITORS_DEFAULT = 2
|
|
26
|
+
COMPARISON_ENTITY_MAX = COMPETITORS_MAX + 1
|
|
27
|
+
# Discovery SERP fan-out is small (3 queries today) but still needs a ceiling
|
|
28
|
+
# so a future query expansion cannot open one worker per query unbounded.
|
|
29
|
+
MAX_DISCOVERY_WORKERS = 3
|
|
30
|
+
|
|
22
31
|
# A "brand-shaped" token starts with uppercase OR is camelCase with an
|
|
23
32
|
# uppercase letter later. Catches "Anthropic", "OpenAI", "xAI", "iPhone",
|
|
24
33
|
# "eBay", "Hugging", "Face".
|
|
@@ -172,7 +181,7 @@ def discover_competitors(
|
|
|
172
181
|
items, _artifact = grounding.web_search(query, date_range, config)
|
|
173
182
|
return label, items
|
|
174
183
|
|
|
175
|
-
with ThreadPoolExecutor(max_workers=len(queries)) as executor:
|
|
184
|
+
with ThreadPoolExecutor(max_workers=min(len(queries), MAX_DISCOVERY_WORKERS)) as executor:
|
|
176
185
|
futures = {
|
|
177
186
|
executor.submit(_search, label, q): label
|
|
178
187
|
for label, q in queries.items()
|