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
|
@@ -19,9 +19,28 @@ import tempfile
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Dict, List, Optional
|
|
21
21
|
|
|
22
|
+
from .cookie_paths import path_is_dir, path_is_file
|
|
23
|
+
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def has_complete_pair(result: Optional[Dict[str, str]], cookie_names) -> bool:
|
|
28
|
+
"""Return True when ``result`` holds every requested cookie with a value.
|
|
29
|
+
|
|
30
|
+
Shared by the Firefox profile scan, ``env.extract_browser_credentials``,
|
|
31
|
+
and the setup wizard so a partial pair (e.g. a lone ``ct0`` from a
|
|
32
|
+
logged-out session) never shadows a complete one. Empty values do not
|
|
33
|
+
count: ``{"auth_token": "", "ct0": ""}`` is incomplete. ``cookie_names``
|
|
34
|
+
is the spec's cookie list (a spec dict with a ``"cookies"`` key also
|
|
35
|
+
works).
|
|
36
|
+
"""
|
|
37
|
+
if not result:
|
|
38
|
+
return False
|
|
39
|
+
if isinstance(cookie_names, dict):
|
|
40
|
+
cookie_names = cookie_names.get("cookies", [])
|
|
41
|
+
return all(result.get(name) for name in cookie_names)
|
|
42
|
+
|
|
43
|
+
|
|
25
44
|
def _lock_temp_cookie_copy(path: str) -> None:
|
|
26
45
|
"""Restrict copied cookie DB temp files to the current user on POSIX."""
|
|
27
46
|
if os.name == "nt":
|
|
@@ -48,18 +67,30 @@ def _get_wsl_firefox_profiles_dir() -> Optional[Path]:
|
|
|
48
67
|
directories (skips Public, Default, etc.).
|
|
49
68
|
"""
|
|
50
69
|
mnt_users = Path("/mnt/c/Users")
|
|
51
|
-
if not mnt_users
|
|
70
|
+
if not path_is_dir(mnt_users):
|
|
52
71
|
return None
|
|
53
72
|
skip = {"Public", "Default", "Default User", "All Users"}
|
|
73
|
+
denied = None
|
|
54
74
|
try:
|
|
55
75
|
for user_dir in sorted(mnt_users.iterdir()):
|
|
56
|
-
if user_dir.name in skip
|
|
76
|
+
if user_dir.name in skip:
|
|
57
77
|
continue
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
try:
|
|
79
|
+
if not path_is_dir(user_dir):
|
|
80
|
+
continue
|
|
81
|
+
ff_dir = user_dir / "AppData" / "Roaming" / "Mozilla" / "Firefox"
|
|
82
|
+
if path_is_dir(ff_dir):
|
|
83
|
+
return ff_dir
|
|
84
|
+
except PermissionError as exc:
|
|
85
|
+
if denied is None:
|
|
86
|
+
denied = exc
|
|
87
|
+
except PermissionError as exc:
|
|
88
|
+
if denied is None:
|
|
89
|
+
denied = exc
|
|
61
90
|
except OSError:
|
|
62
91
|
pass
|
|
92
|
+
if denied is not None:
|
|
93
|
+
raise denied
|
|
63
94
|
return None
|
|
64
95
|
|
|
65
96
|
|
|
@@ -71,8 +102,12 @@ def _get_firefox_profiles_dir() -> Optional[Path]:
|
|
|
71
102
|
elif system == "Linux":
|
|
72
103
|
# Default location for most distros
|
|
73
104
|
path = Path.home() / ".mozilla" / "firefox"
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
denied = None
|
|
106
|
+
try:
|
|
107
|
+
if path_is_dir(path):
|
|
108
|
+
return path
|
|
109
|
+
except PermissionError as exc:
|
|
110
|
+
denied = exc
|
|
76
111
|
# Some distros (e.g. Fedora) honour $XDG_CONFIG_HOME
|
|
77
112
|
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
|
78
113
|
if xdg_config and os.path.isabs(xdg_config):
|
|
@@ -83,46 +118,95 @@ def _get_firefox_profiles_dir() -> Optional[Path]:
|
|
|
83
118
|
# Windows: %APPDATA%\Mozilla\Firefox — best-effort
|
|
84
119
|
appdata = Path.home() / "AppData" / "Roaming" / "Mozilla" / "Firefox"
|
|
85
120
|
path = appdata
|
|
86
|
-
|
|
121
|
+
if path_is_dir(path):
|
|
122
|
+
return path
|
|
123
|
+
if system == "Linux" and denied is not None:
|
|
124
|
+
raise denied
|
|
125
|
+
return None
|
|
87
126
|
|
|
88
127
|
|
|
89
|
-
def
|
|
128
|
+
def _load_profiles_ini(ini_path: Path) -> configparser.ConfigParser:
|
|
129
|
+
"""Parse Firefox profiles.ini, retrying UTF-16 LE used on Windows (#1067)."""
|
|
130
|
+
config = configparser.ConfigParser()
|
|
131
|
+
try:
|
|
132
|
+
config.read(str(ini_path), encoding="utf-8")
|
|
133
|
+
except UnicodeDecodeError:
|
|
134
|
+
config.read(str(ini_path), encoding="utf-16")
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _find_default_profile(
|
|
139
|
+
profiles_dir: Path, denials: Optional[list[PermissionError]] = None
|
|
140
|
+
) -> Optional[Path]:
|
|
90
141
|
"""Parse profiles.ini to find the default profile directory.
|
|
91
142
|
|
|
92
143
|
Looks for a section with Default=1. Falls back to the first profile
|
|
93
144
|
directory found on disk if profiles.ini is missing or malformed.
|
|
94
145
|
"""
|
|
95
146
|
ini_path = profiles_dir / "profiles.ini"
|
|
147
|
+
denied: list[PermissionError] = []
|
|
96
148
|
|
|
97
|
-
|
|
149
|
+
try:
|
|
150
|
+
has_ini = path_is_file(ini_path)
|
|
151
|
+
except PermissionError as exc:
|
|
152
|
+
denied.append(exc)
|
|
153
|
+
has_ini = False
|
|
154
|
+
if has_ini:
|
|
98
155
|
try:
|
|
99
|
-
config =
|
|
100
|
-
config.read(str(ini_path), encoding="utf-8")
|
|
156
|
+
config = _load_profiles_ini(ini_path)
|
|
101
157
|
|
|
102
158
|
# First pass: Install* section (Firefox >= 67 format, takes priority)
|
|
103
159
|
for section in config.sections():
|
|
104
160
|
if section.startswith("Install") and config.has_option(section, "Default"):
|
|
105
161
|
raw = config.get(section, "Default")
|
|
106
162
|
candidate = profiles_dir / raw
|
|
107
|
-
|
|
163
|
+
try:
|
|
164
|
+
found = path_is_dir(candidate)
|
|
165
|
+
except PermissionError as exc:
|
|
166
|
+
denied.append(exc)
|
|
167
|
+
continue
|
|
168
|
+
if found:
|
|
169
|
+
if denials is not None:
|
|
170
|
+
denials.extend(denied)
|
|
108
171
|
return candidate
|
|
109
172
|
|
|
110
173
|
# Second pass: Profile section with Default=1
|
|
111
174
|
for section in config.sections():
|
|
112
175
|
if section.startswith("Profile") and config.has_option(section, "Default") and config.get(section, "Default") == "1":
|
|
113
|
-
|
|
176
|
+
try:
|
|
177
|
+
resolved = _resolve_profile_path(profiles_dir, config, section)
|
|
178
|
+
except PermissionError as exc:
|
|
179
|
+
denied.append(exc)
|
|
180
|
+
continue
|
|
181
|
+
if resolved is not None:
|
|
182
|
+
if denials is not None:
|
|
183
|
+
denials.extend(denied)
|
|
184
|
+
return resolved
|
|
114
185
|
|
|
115
186
|
# Third pass: first Profile section that exists on disk
|
|
116
187
|
for section in config.sections():
|
|
117
188
|
if section.startswith("Profile"):
|
|
118
|
-
|
|
119
|
-
|
|
189
|
+
try:
|
|
190
|
+
resolved = _resolve_profile_path(profiles_dir, config, section)
|
|
191
|
+
except PermissionError as exc:
|
|
192
|
+
denied.append(exc)
|
|
193
|
+
continue
|
|
194
|
+
if resolved:
|
|
195
|
+
if denials is not None:
|
|
196
|
+
denials.extend(denied)
|
|
120
197
|
return resolved
|
|
121
|
-
except
|
|
198
|
+
except PermissionError as exc:
|
|
199
|
+
denied.append(exc)
|
|
200
|
+
except (configparser.Error, OSError, UnicodeDecodeError) as exc:
|
|
122
201
|
logger.debug("Failed to parse profiles.ini: %s", exc)
|
|
123
202
|
|
|
124
203
|
# Fallback: scan directory for anything that looks like a profile
|
|
125
|
-
|
|
204
|
+
profile = _fallback_find_profile(profiles_dir, denied)
|
|
205
|
+
if denials is not None:
|
|
206
|
+
denials.extend(denied)
|
|
207
|
+
elif denied and profile is None:
|
|
208
|
+
raise denied[0]
|
|
209
|
+
return profile
|
|
126
210
|
|
|
127
211
|
|
|
128
212
|
def _resolve_profile_path(
|
|
@@ -137,17 +221,31 @@ def _resolve_profile_path(
|
|
|
137
221
|
candidate = profiles_dir / raw_path
|
|
138
222
|
else:
|
|
139
223
|
candidate = Path(raw_path)
|
|
140
|
-
return candidate if candidate
|
|
224
|
+
return candidate if path_is_dir(candidate) else None
|
|
141
225
|
|
|
142
226
|
|
|
143
|
-
def _fallback_find_profile(
|
|
227
|
+
def _fallback_find_profile(
|
|
228
|
+
profiles_dir: Path, denials: Optional[list[PermissionError]] = None
|
|
229
|
+
) -> Optional[Path]:
|
|
144
230
|
"""Find the first directory that contains cookies.sqlite."""
|
|
231
|
+
denied: list[PermissionError] = []
|
|
145
232
|
try:
|
|
146
233
|
for child in sorted(profiles_dir.iterdir()):
|
|
147
|
-
|
|
148
|
-
|
|
234
|
+
try:
|
|
235
|
+
if path_is_dir(child) and path_is_file(child / "cookies.sqlite"):
|
|
236
|
+
if denials is not None:
|
|
237
|
+
denials.extend(denied)
|
|
238
|
+
return child
|
|
239
|
+
except PermissionError as exc:
|
|
240
|
+
denied.append(exc)
|
|
241
|
+
except PermissionError as exc:
|
|
242
|
+
denied.append(exc)
|
|
149
243
|
except OSError:
|
|
150
244
|
pass
|
|
245
|
+
if denials is not None:
|
|
246
|
+
denials.extend(denied)
|
|
247
|
+
elif denied:
|
|
248
|
+
raise denied[0]
|
|
151
249
|
return None
|
|
152
250
|
|
|
153
251
|
|
|
@@ -159,7 +257,7 @@ def _query_cookies_db(
|
|
|
159
257
|
Firefox locks cookies.sqlite while running, so we copy first.
|
|
160
258
|
Returns {name: value} dict or None if no matching cookies found.
|
|
161
259
|
"""
|
|
162
|
-
if not db_path
|
|
260
|
+
if not path_is_file(db_path):
|
|
163
261
|
return None
|
|
164
262
|
|
|
165
263
|
tmp_fd = None
|
|
@@ -196,6 +294,11 @@ def _query_cookies_db(
|
|
|
196
294
|
return None
|
|
197
295
|
return {name: value for name, value in rows}
|
|
198
296
|
|
|
297
|
+
except PermissionError as exc:
|
|
298
|
+
if exc.filename == str(db_path):
|
|
299
|
+
raise
|
|
300
|
+
logger.debug("Failed to query cookies database %s: %s", db_path, exc)
|
|
301
|
+
return None
|
|
199
302
|
except (sqlite3.Error, OSError) as exc:
|
|
200
303
|
logger.debug("Failed to query cookies database %s: %s", db_path, exc)
|
|
201
304
|
return None
|
|
@@ -219,29 +322,51 @@ def _try_firefox_dir(profiles_dir: Path, domain: str, cookie_names: List[str]) -
|
|
|
219
322
|
Tries the default profile first, then falls back to scanning all
|
|
220
323
|
profiles for matching cookies. This handles multi-profile setups
|
|
221
324
|
where the user is logged into x.com on a non-default profile.
|
|
325
|
+
A complete match (all ``cookie_names``) always wins: a partial
|
|
326
|
+
default-profile result is kept only as a fallback.
|
|
222
327
|
"""
|
|
223
|
-
|
|
328
|
+
denials: list[PermissionError] = []
|
|
329
|
+
default_profile = _find_default_profile(profiles_dir, denials)
|
|
224
330
|
profiles_tried = 0
|
|
331
|
+
fallback: Optional[Dict[str, str]] = None
|
|
225
332
|
if default_profile is not None:
|
|
226
|
-
|
|
333
|
+
try:
|
|
334
|
+
result = _query_cookies_db(default_profile / "cookies.sqlite", domain, cookie_names)
|
|
335
|
+
except PermissionError as exc:
|
|
336
|
+
denials.append(exc)
|
|
337
|
+
result = None
|
|
227
338
|
if result is not None:
|
|
228
|
-
|
|
339
|
+
if has_complete_pair(result, cookie_names):
|
|
340
|
+
return result
|
|
341
|
+
fallback = result
|
|
229
342
|
profiles_tried = 1
|
|
230
343
|
# Fallback: scan every profile directory for matching cookies
|
|
231
344
|
try:
|
|
232
345
|
for child in sorted(profiles_dir.iterdir()):
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
346
|
+
try:
|
|
347
|
+
if not path_is_dir(child):
|
|
348
|
+
continue
|
|
349
|
+
if default_profile is not None and child == default_profile:
|
|
350
|
+
continue
|
|
351
|
+
db = child / "cookies.sqlite"
|
|
352
|
+
if path_is_file(db):
|
|
353
|
+
result = _query_cookies_db(db, domain, cookie_names)
|
|
354
|
+
if result is not None:
|
|
355
|
+
if has_complete_pair(result, cookie_names):
|
|
356
|
+
return result
|
|
357
|
+
if fallback is None:
|
|
358
|
+
fallback = result
|
|
359
|
+
profiles_tried += 1
|
|
360
|
+
except PermissionError as exc:
|
|
361
|
+
denials.append(exc)
|
|
362
|
+
except PermissionError as exc:
|
|
363
|
+
denials.append(exc)
|
|
243
364
|
except OSError:
|
|
244
365
|
pass
|
|
366
|
+
if denials:
|
|
367
|
+
raise denials[0]
|
|
368
|
+
if fallback is not None:
|
|
369
|
+
return fallback
|
|
245
370
|
logger.debug("No matching cookies found in %d Firefox profile(s)", profiles_tried)
|
|
246
371
|
return None
|
|
247
372
|
|
|
@@ -265,18 +390,41 @@ def extract_firefox_cookies(
|
|
|
265
390
|
Returns:
|
|
266
391
|
Dict of {cookie_name: cookie_value} or None if extraction fails.
|
|
267
392
|
"""
|
|
268
|
-
|
|
393
|
+
denied = None
|
|
394
|
+
try:
|
|
395
|
+
profiles_dir = _get_firefox_profiles_dir()
|
|
396
|
+
except PermissionError as exc:
|
|
397
|
+
denied = exc
|
|
398
|
+
profiles_dir = None
|
|
269
399
|
if profiles_dir is not None:
|
|
270
|
-
|
|
400
|
+
try:
|
|
401
|
+
result = _try_firefox_dir(profiles_dir, domain, cookie_names)
|
|
402
|
+
except PermissionError as exc:
|
|
403
|
+
denied = exc
|
|
404
|
+
result = None
|
|
271
405
|
if result is not None:
|
|
272
406
|
return result
|
|
273
407
|
|
|
274
408
|
if platform.system() == "Linux" and _is_wsl():
|
|
275
|
-
|
|
409
|
+
try:
|
|
410
|
+
wsl_dir = _get_wsl_firefox_profiles_dir()
|
|
411
|
+
except PermissionError as exc:
|
|
412
|
+
if denied is None:
|
|
413
|
+
denied = exc
|
|
414
|
+
wsl_dir = None
|
|
276
415
|
if wsl_dir is not None:
|
|
277
416
|
logger.debug("Trying Windows Firefox via WSL: %s", wsl_dir)
|
|
278
|
-
|
|
417
|
+
try:
|
|
418
|
+
result = _try_firefox_dir(wsl_dir, domain, cookie_names)
|
|
419
|
+
except PermissionError as exc:
|
|
420
|
+
if denied is None:
|
|
421
|
+
denied = exc
|
|
422
|
+
result = None
|
|
423
|
+
if result is not None:
|
|
424
|
+
return result
|
|
279
425
|
|
|
426
|
+
if denied is not None:
|
|
427
|
+
raise denied
|
|
280
428
|
if profiles_dir is None:
|
|
281
429
|
logger.debug("Firefox profiles directory not found")
|
|
282
430
|
return None
|
|
@@ -299,6 +447,8 @@ def extract_chrome_cookies(
|
|
|
299
447
|
try:
|
|
300
448
|
from .chrome_cookies import extract_chrome_cookies_macos
|
|
301
449
|
return extract_chrome_cookies_macos(domain, cookie_names)
|
|
450
|
+
except PermissionError:
|
|
451
|
+
raise
|
|
302
452
|
except Exception as exc:
|
|
303
453
|
logger.debug("Chrome cookie extraction failed: %s", exc)
|
|
304
454
|
return None
|
|
@@ -322,6 +472,8 @@ def extract_brave_cookies(
|
|
|
322
472
|
try:
|
|
323
473
|
from .chrome_cookies import extract_brave_cookies_macos
|
|
324
474
|
return extract_brave_cookies_macos(domain, cookie_names)
|
|
475
|
+
except PermissionError:
|
|
476
|
+
raise
|
|
325
477
|
except Exception as exc:
|
|
326
478
|
logger.debug("Brave cookie extraction failed: %s", exc)
|
|
327
479
|
return None
|
|
@@ -342,6 +494,8 @@ def _extract_chromium_family_cookies(
|
|
|
342
494
|
try:
|
|
343
495
|
from .chrome_cookies import extract_chromium_browser_cookies_macos
|
|
344
496
|
return extract_chromium_browser_cookies_macos(browser, domain, cookie_names)
|
|
497
|
+
except PermissionError:
|
|
498
|
+
raise
|
|
345
499
|
except Exception as exc:
|
|
346
500
|
logger.debug("%s cookie extraction failed: %s", browser, exc)
|
|
347
501
|
return None
|
|
@@ -388,6 +542,8 @@ def extract_safari_cookies(
|
|
|
388
542
|
try:
|
|
389
543
|
from .safari_cookies import extract_safari_cookies_macos
|
|
390
544
|
return extract_safari_cookies_macos(domain, cookie_names)
|
|
545
|
+
except PermissionError:
|
|
546
|
+
raise
|
|
391
547
|
except Exception as exc:
|
|
392
548
|
logger.debug("Safari cookie extraction failed: %s", exc)
|
|
393
549
|
return None
|
|
@@ -425,20 +581,41 @@ def _extract_firefox_with_source(
|
|
|
425
581
|
Returns (cookies, "firefox") for native Linux/macOS Firefox, or
|
|
426
582
|
(cookies, "firefox-wsl") for Windows Firefox accessed via WSL2.
|
|
427
583
|
"""
|
|
428
|
-
|
|
584
|
+
denied = None
|
|
585
|
+
try:
|
|
586
|
+
profiles_dir = _get_firefox_profiles_dir()
|
|
587
|
+
except PermissionError as exc:
|
|
588
|
+
denied = exc
|
|
589
|
+
profiles_dir = None
|
|
429
590
|
if profiles_dir is not None:
|
|
430
|
-
|
|
591
|
+
try:
|
|
592
|
+
result = _try_firefox_dir(profiles_dir, domain, cookie_names)
|
|
593
|
+
except PermissionError as exc:
|
|
594
|
+
denied = exc
|
|
595
|
+
result = None
|
|
431
596
|
if result is not None:
|
|
432
597
|
return (result, "firefox")
|
|
433
598
|
|
|
434
599
|
if platform.system() == "Linux" and _is_wsl():
|
|
435
|
-
|
|
600
|
+
try:
|
|
601
|
+
wsl_dir = _get_wsl_firefox_profiles_dir()
|
|
602
|
+
except PermissionError as exc:
|
|
603
|
+
if denied is None:
|
|
604
|
+
denied = exc
|
|
605
|
+
wsl_dir = None
|
|
436
606
|
if wsl_dir is not None:
|
|
437
607
|
logger.debug("Trying Windows Firefox via WSL: %s", wsl_dir)
|
|
438
|
-
|
|
608
|
+
try:
|
|
609
|
+
result = _try_firefox_dir(wsl_dir, domain, cookie_names)
|
|
610
|
+
except PermissionError as exc:
|
|
611
|
+
if denied is None:
|
|
612
|
+
denied = exc
|
|
613
|
+
result = None
|
|
439
614
|
if result is not None:
|
|
440
615
|
return (result, "firefox-wsl")
|
|
441
616
|
|
|
617
|
+
if denied is not None:
|
|
618
|
+
raise denied
|
|
442
619
|
return None
|
|
443
620
|
|
|
444
621
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Path probes that preserve browser-data permission failures."""
|
|
2
|
+
|
|
3
|
+
import stat
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _mode(path: Path) -> int | None:
|
|
8
|
+
try:
|
|
9
|
+
return path.stat().st_mode
|
|
10
|
+
except PermissionError:
|
|
11
|
+
raise
|
|
12
|
+
except OSError:
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def path_exists(path: Path) -> bool:
|
|
17
|
+
return _mode(path) is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def path_is_file(path: Path) -> bool:
|
|
21
|
+
mode = _mode(path)
|
|
22
|
+
return mode is not None and stat.S_ISREG(mode)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def path_is_dir(path: Path) -> bool:
|
|
26
|
+
mode = _mode(path)
|
|
27
|
+
return mode is not None and stat.S_ISDIR(mode)
|