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
|
@@ -2,12 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import sys
|
|
6
7
|
import urllib.parse
|
|
7
|
-
from
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
8
10
|
from urllib.parse import urlparse
|
|
9
11
|
|
|
10
|
-
from . import dates, env, http, web_search_keyless
|
|
12
|
+
from . import dates, env, http, parallel_mcp, schema, web_search_keyless
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class GroundedClaimText:
|
|
17
|
+
"""Candidate text with its exact primary evidence item."""
|
|
18
|
+
|
|
19
|
+
candidate_id: str
|
|
20
|
+
title: str
|
|
21
|
+
summary: str
|
|
22
|
+
item: schema.SourceItem
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def claim_source_map(report: schema.Report) -> dict[str, GroundedClaimText]:
|
|
26
|
+
"""Expose only candidate claims that have a clean primary-item trace.
|
|
27
|
+
|
|
28
|
+
Freshness verification deliberately starts here instead of scanning all
|
|
29
|
+
report prose. A candidate without a primary ``SourceItem`` cannot produce
|
|
30
|
+
an auditable per-claim verdict.
|
|
31
|
+
"""
|
|
32
|
+
grounded: dict[str, GroundedClaimText] = {}
|
|
33
|
+
for candidate in report.ranked_candidates:
|
|
34
|
+
item = schema.candidate_primary_item(candidate)
|
|
35
|
+
if item is None:
|
|
36
|
+
continue
|
|
37
|
+
grounded[candidate.candidate_id] = GroundedClaimText(
|
|
38
|
+
candidate_id=candidate.candidate_id,
|
|
39
|
+
title=candidate.title,
|
|
40
|
+
summary=candidate.snippet or item.snippet or item.body,
|
|
41
|
+
item=item,
|
|
42
|
+
)
|
|
43
|
+
return grounded
|
|
11
44
|
|
|
12
45
|
|
|
13
46
|
# ---------------------------------------------------------------------------
|
|
@@ -32,7 +65,7 @@ def brave_search(
|
|
|
32
65
|
for i, r in enumerate((data.get("web", {}).get("results", []))[:count]):
|
|
33
66
|
raw_date = r.get("page_age") or ""
|
|
34
67
|
pub_date = _normalize_date(raw_date[:10]) if raw_date else None
|
|
35
|
-
if
|
|
68
|
+
if _known_date_out_of_range(pub_date, date_range):
|
|
36
69
|
continue
|
|
37
70
|
items.append({
|
|
38
71
|
"id": f"WB{i + 1}",
|
|
@@ -43,6 +76,7 @@ def brave_search(
|
|
|
43
76
|
"date": pub_date,
|
|
44
77
|
"relevance": 0.8,
|
|
45
78
|
"why_relevant": "Brave web search",
|
|
79
|
+
"metadata": {"date_window_basis": "server_bounds"},
|
|
46
80
|
})
|
|
47
81
|
artifact = {"label": "brave", "webSearchQueries": [query], "resultCount": len(items)}
|
|
48
82
|
return items, artifact
|
|
@@ -77,7 +111,7 @@ def exa_search(
|
|
|
77
111
|
continue
|
|
78
112
|
raw_date = r.get("publishedDate") or ""
|
|
79
113
|
pub_date = _normalize_date(raw_date.split("T")[0] if "T" in raw_date else raw_date[:10]) if raw_date else None
|
|
80
|
-
if
|
|
114
|
+
if _known_date_out_of_range(pub_date, date_range):
|
|
81
115
|
continue
|
|
82
116
|
items.append({
|
|
83
117
|
"id": f"WE{i + 1}",
|
|
@@ -88,6 +122,7 @@ def exa_search(
|
|
|
88
122
|
"date": pub_date,
|
|
89
123
|
"relevance": 0.8,
|
|
90
124
|
"why_relevant": "Exa web search",
|
|
125
|
+
"metadata": {"date_window_basis": "server_bounds"},
|
|
91
126
|
})
|
|
92
127
|
artifact = {"label": "exa", "webSearchQueries": [query], "resultCount": len(items)}
|
|
93
128
|
return items, artifact
|
|
@@ -114,7 +149,9 @@ def serper_search(
|
|
|
114
149
|
for i, r in enumerate((data.get("organic", []))[:count]):
|
|
115
150
|
raw_date = r.get("date") or ""
|
|
116
151
|
pub_date = _parse_serper_date(raw_date)
|
|
117
|
-
if
|
|
152
|
+
if pub_date is None and _SERPER_RELATIVE_RE.match(raw_date.strip()):
|
|
153
|
+
continue
|
|
154
|
+
if _known_date_out_of_range(pub_date, date_range):
|
|
118
155
|
continue
|
|
119
156
|
items.append({
|
|
120
157
|
"id": f"WS{i + 1}",
|
|
@@ -125,6 +162,7 @@ def serper_search(
|
|
|
125
162
|
"date": pub_date,
|
|
126
163
|
"relevance": 0.8,
|
|
127
164
|
"why_relevant": "Serper web search",
|
|
165
|
+
"metadata": {"date_window_basis": "server_bounds"},
|
|
128
166
|
})
|
|
129
167
|
artifact = {"label": "serper", "webSearchQueries": [query], "resultCount": len(items)}
|
|
130
168
|
return items, artifact
|
|
@@ -137,12 +175,16 @@ def serper_search(
|
|
|
137
175
|
def parallel_search(
|
|
138
176
|
query: str, date_range: tuple[str, str], api_key: str, count: int = 5,
|
|
139
177
|
) -> tuple[list[dict], dict]:
|
|
178
|
+
current_window = date_range[1] >= datetime.now(timezone.utc).date().isoformat()
|
|
140
179
|
data = http.request(
|
|
141
180
|
"POST", "https://api.parallel.ai/v1/search",
|
|
142
|
-
headers={"
|
|
181
|
+
headers={"x-api-key": api_key, "Content-Type": "application/json"},
|
|
143
182
|
json_data={
|
|
144
183
|
"search_queries": [query],
|
|
145
|
-
"advanced_settings": {
|
|
184
|
+
"advanced_settings": {
|
|
185
|
+
"max_results": count,
|
|
186
|
+
"source_policy": {"after_date": date_range[0]},
|
|
187
|
+
},
|
|
146
188
|
},
|
|
147
189
|
timeout=15,
|
|
148
190
|
)
|
|
@@ -155,7 +197,9 @@ def parallel_search(
|
|
|
155
197
|
continue
|
|
156
198
|
raw_date = r.get("publish_date") or ""
|
|
157
199
|
pub_date = _normalize_date(raw_date[:10]) if raw_date else None
|
|
158
|
-
if
|
|
200
|
+
if _known_date_out_of_range(pub_date, date_range):
|
|
201
|
+
continue
|
|
202
|
+
if pub_date is None and not current_window:
|
|
159
203
|
continue
|
|
160
204
|
items.append({
|
|
161
205
|
"id": f"WP{i + 1}",
|
|
@@ -166,12 +210,71 @@ def parallel_search(
|
|
|
166
210
|
"date": pub_date,
|
|
167
211
|
"relevance": 0.8,
|
|
168
212
|
"why_relevant": "Parallel AI web search",
|
|
213
|
+
"metadata": {
|
|
214
|
+
"date_window_basis": "server_start_current" if current_window else "server_start"
|
|
215
|
+
},
|
|
169
216
|
})
|
|
170
217
|
artifact = {"label": "parallel", "webSearchQueries": [query], "resultCount": len(items)}
|
|
171
218
|
return items, artifact
|
|
172
219
|
|
|
173
220
|
|
|
174
|
-
|
|
221
|
+
_SERPER_RELATIVE_RE = re.compile(
|
|
222
|
+
r"^(?:about\s+)?(\d+)\s+(minute|min|hour|hr|day|week|month|year)s?\s+ago$", re.I
|
|
223
|
+
)
|
|
224
|
+
_SERPER_RELATIVE_DAYS = {"day": 1, "week": 7, "month": 30, "year": 365}
|
|
225
|
+
_SERPER_RELATIVE_WORDS = {"just now": 0, "today": 0, "yesterday": 1}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _now() -> datetime:
|
|
229
|
+
"""Query-time clock. Isolated so tests can pin it."""
|
|
230
|
+
return datetime.now()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_serper_relative(raw: str, now: datetime | None = None) -> str | None:
|
|
234
|
+
"""Resolve Serper's relative dates ("2 hours ago") to an ISO date.
|
|
235
|
+
|
|
236
|
+
Google renders a relative date for recent results, so the fresher a result
|
|
237
|
+
is the more likely it arrives in this form. Left unparsed it becomes None
|
|
238
|
+
and `_in_date_range` drops the item, which silently empties the web lane on
|
|
239
|
+
short research windows.
|
|
240
|
+
|
|
241
|
+
The label states the result's age **at query time**, so it resolves against
|
|
242
|
+
the clock rather than against the end of the research window. The two differ
|
|
243
|
+
whenever `--as-of` names a past window: a result inside a window ending two
|
|
244
|
+
weeks ago is still labelled with its age as of today, and anchoring to the
|
|
245
|
+
window end would place it a further two weeks back and discard it.
|
|
246
|
+
|
|
247
|
+
Sub-day units ("N minutes/hours ago") are subtracted from the clock rather
|
|
248
|
+
than rounded up to the current date, which would name the wrong day across
|
|
249
|
+
midnight: at 00:30 "23 hours ago" is yesterday. Residual slack comes only
|
|
250
|
+
from the timezone `_now()` reads against Google's.
|
|
251
|
+
|
|
252
|
+
An age large enough to overflow `timedelta` or run past the end of `date`
|
|
253
|
+
reads as unparseable, like any other junk label, so one malformed value
|
|
254
|
+
cannot abort the whole result loop in `serper_search`.
|
|
255
|
+
"""
|
|
256
|
+
anchor = now or _now()
|
|
257
|
+
text = raw.strip().lower()
|
|
258
|
+
if text in _SERPER_RELATIVE_WORDS:
|
|
259
|
+
return (anchor.date() - timedelta(days=_SERPER_RELATIVE_WORDS[text])).isoformat()
|
|
260
|
+
m = _SERPER_RELATIVE_RE.match(text)
|
|
261
|
+
if not m:
|
|
262
|
+
return None
|
|
263
|
+
unit = {"min": "minute", "hr": "hour"}.get(m.group(2), m.group(2))
|
|
264
|
+
try:
|
|
265
|
+
amount = int(m.group(1).lstrip("0") or "0")
|
|
266
|
+
if unit == "minute":
|
|
267
|
+
return (anchor - timedelta(minutes=amount)).date().isoformat()
|
|
268
|
+
if unit == "hour":
|
|
269
|
+
return (anchor - timedelta(hours=amount)).date().isoformat()
|
|
270
|
+
return (
|
|
271
|
+
anchor.date() - timedelta(days=amount * _SERPER_RELATIVE_DAYS[unit])
|
|
272
|
+
).isoformat()
|
|
273
|
+
except (OverflowError, ValueError):
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_serper_date(raw: str, now: datetime | None = None) -> str | None:
|
|
175
278
|
if not raw:
|
|
176
279
|
return None
|
|
177
280
|
normalized = _normalize_date(raw)
|
|
@@ -182,7 +285,7 @@ def _parse_serper_date(raw: str) -> str | None:
|
|
|
182
285
|
return datetime.strptime(raw.strip(), fmt).date().isoformat()
|
|
183
286
|
except ValueError:
|
|
184
287
|
continue
|
|
185
|
-
return
|
|
288
|
+
return _parse_serper_relative(raw, now)
|
|
186
289
|
|
|
187
290
|
|
|
188
291
|
|
|
@@ -236,6 +339,10 @@ def web_search(
|
|
|
236
339
|
if not key:
|
|
237
340
|
raise RuntimeError("PARALLEL_API_KEY is required when web_backend='parallel'")
|
|
238
341
|
items, artifact = parallel_search(query, date_range, key)
|
|
342
|
+
elif backend == "parallel-mcp":
|
|
343
|
+
items, artifact = parallel_mcp.search(
|
|
344
|
+
query, date_range, config.get("PARALLEL_API_KEY")
|
|
345
|
+
)
|
|
239
346
|
elif backend == "keyless":
|
|
240
347
|
items, artifact = web_search_keyless.keyless_search(query, date_range, config)
|
|
241
348
|
elif backend != "none":
|
|
@@ -243,7 +350,13 @@ def web_search(
|
|
|
243
350
|
else:
|
|
244
351
|
return [], {}
|
|
245
352
|
if items and not _reddit_excluded(config):
|
|
246
|
-
|
|
353
|
+
# Reddit enrichment is a best-effort secondary fetch on already-retrieved
|
|
354
|
+
# web results. Isolate its HTTP failures in a throwaway capture sink so a
|
|
355
|
+
# reddit.com fetch failure (e.g. a 403 on a datacenter IP) is not
|
|
356
|
+
# attributed to the web/grounding source itself — which would otherwise
|
|
357
|
+
# discard the successfully retrieved results and report the source failed.
|
|
358
|
+
with http.capture_failures():
|
|
359
|
+
items = _enrich_reddit_items(items)
|
|
247
360
|
return items, artifact
|
|
248
361
|
|
|
249
362
|
|
|
@@ -327,5 +440,10 @@ def _in_date_range(pub_date: str | None, date_range: tuple[str, str]) -> bool:
|
|
|
327
440
|
return date_range[0] <= pub_date <= date_range[1]
|
|
328
441
|
|
|
329
442
|
|
|
443
|
+
def _known_date_out_of_range(pub_date: str | None, date_range: tuple[str, str]) -> bool:
|
|
444
|
+
"""True when a parseable date falls outside the window. Unknown dates stay."""
|
|
445
|
+
return pub_date is not None and not _in_date_range(pub_date, date_range)
|
|
446
|
+
|
|
447
|
+
|
|
330
448
|
def _domain(url: str) -> str:
|
|
331
449
|
return urlparse(url).netloc.strip().lower()
|
|
@@ -40,6 +40,8 @@ ENRICH_LIMITS = {
|
|
|
40
40
|
"deep": 10,
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
DISCOVERY_LIMITS = {"quick": 20, "default": 40, "deep": 60}
|
|
44
|
+
|
|
43
45
|
|
|
44
46
|
def _log(msg: str):
|
|
45
47
|
log.source_log("HN", msg, tty_only=False)
|
|
@@ -145,6 +147,47 @@ def search_hackernews(
|
|
|
145
147
|
return response
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
def fetch_discovery_listings(
|
|
151
|
+
from_date: str,
|
|
152
|
+
to_date: str,
|
|
153
|
+
depth: str = "default",
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""Fetch topic-less HN front-page and best-in-window story listings."""
|
|
156
|
+
limit = DISCOVERY_LIMITS.get(depth, DISCOVERY_LIMITS["default"])
|
|
157
|
+
from_ts = _date_to_unix(from_date)
|
|
158
|
+
to_ts = _date_to_unix(to_date) + 86400
|
|
159
|
+
from urllib.parse import urlencode
|
|
160
|
+
|
|
161
|
+
urls = [
|
|
162
|
+
f"{ALGOLIA_SEARCH_URL}?{urlencode({'tags': 'front_page', 'hitsPerPage': str(limit)})}",
|
|
163
|
+
f"{ALGOLIA_SEARCH_URL}?{urlencode({
|
|
164
|
+
'tags': 'story',
|
|
165
|
+
'numericFilters': f'created_at_i>{from_ts},created_at_i<{to_ts}',
|
|
166
|
+
'hitsPerPage': str(limit),
|
|
167
|
+
})}",
|
|
168
|
+
]
|
|
169
|
+
hits: list[dict[str, Any]] = []
|
|
170
|
+
errors: list[str] = []
|
|
171
|
+
for url in urls:
|
|
172
|
+
try:
|
|
173
|
+
response = http.request("GET", url, timeout=30)
|
|
174
|
+
hits.extend(response.get("hits") or [])
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
errors.append(str(exc))
|
|
177
|
+
|
|
178
|
+
seen: set[str] = set()
|
|
179
|
+
unique_hits: list[dict[str, Any]] = []
|
|
180
|
+
for hit in hits:
|
|
181
|
+
object_id = str(hit.get("objectID") or "")
|
|
182
|
+
if not object_id or object_id in seen:
|
|
183
|
+
continue
|
|
184
|
+
seen.add(object_id)
|
|
185
|
+
unique_hits.append(hit)
|
|
186
|
+
|
|
187
|
+
items = parse_hackernews_response({"hits": unique_hits}, query="")
|
|
188
|
+
return {"items": items, "errors": errors}
|
|
189
|
+
|
|
190
|
+
|
|
148
191
|
_WORD_BOUNDARY_RE_CACHE: Dict[str, "re.Pattern[str]"] = {}
|
|
149
192
|
|
|
150
193
|
|
|
@@ -232,7 +275,7 @@ def parse_hackernews_response(response: Dict[str, Any], query: str = "") -> List
|
|
|
232
275
|
|
|
233
276
|
# Article URL vs HN discussion URL
|
|
234
277
|
article_url = hit.get("url") or ""
|
|
235
|
-
hn_url =
|
|
278
|
+
hn_url = _hn_item_url(object_id)
|
|
236
279
|
|
|
237
280
|
# Relevance: blend Algolia rank with token-overlap content matching
|
|
238
281
|
rank_score = max(0.3, 1.0 - (i * 0.02)) # 1.0 -> 0.3 over 35 items
|
|
@@ -261,6 +304,11 @@ def parse_hackernews_response(response: Dict[str, Any], query: str = "") -> List
|
|
|
261
304
|
return items
|
|
262
305
|
|
|
263
306
|
|
|
307
|
+
def _hn_item_url(item_id: Any) -> str:
|
|
308
|
+
"""HN discussion/comment permalink for an item id."""
|
|
309
|
+
return f"https://news.ycombinator.com/item?id={item_id}"
|
|
310
|
+
|
|
311
|
+
|
|
264
312
|
def _fetch_item_comments(object_id: str, max_comments: int = 5) -> Dict[str, Any]:
|
|
265
313
|
"""Fetch top-level comments for a story from Algolia items endpoint.
|
|
266
314
|
|
|
@@ -290,13 +338,18 @@ def _fetch_item_comments(object_id: str, max_comments: int = 5) -> Dict[str, Any
|
|
|
290
338
|
|
|
291
339
|
comments = []
|
|
292
340
|
insights = []
|
|
341
|
+
# A quote links to where it was said: the comment's own permalink, else the
|
|
342
|
+
# story's discussion page -- never the article URL.
|
|
343
|
+
discussion_url = _hn_item_url(object_id)
|
|
293
344
|
for c in real_comments[:max_comments]:
|
|
294
345
|
text = _strip_html(c.get("text", ""))
|
|
295
346
|
excerpt = text[:300] + "..." if len(text) > 300 else text
|
|
347
|
+
comment_id = c.get("id")
|
|
296
348
|
comments.append({
|
|
297
349
|
"author": c.get("author", ""),
|
|
298
350
|
"text": excerpt,
|
|
299
|
-
"points": c.get("points")
|
|
351
|
+
"points": c.get("points"),
|
|
352
|
+
"url": _hn_item_url(comment_id) if comment_id else discussion_url,
|
|
300
353
|
})
|
|
301
354
|
# First sentence as insight
|
|
302
355
|
first_sentence = text.split(". ")[0].split("\n")[0][:200]
|
|
@@ -327,7 +380,7 @@ def enrich_top_stories(
|
|
|
327
380
|
# Sort by points to enrich the most popular stories
|
|
328
381
|
by_points = sorted(
|
|
329
382
|
range(len(items)),
|
|
330
|
-
key=lambda i: items[i].get("engagement", {}).get("points"
|
|
383
|
+
key=lambda i: items[i].get("engagement", {}).get("points") or 0,
|
|
331
384
|
reverse=True,
|
|
332
385
|
)
|
|
333
386
|
to_enrich = by_points[:limit]
|
|
@@ -335,8 +388,12 @@ def enrich_top_stories(
|
|
|
335
388
|
_log(f"Enriching top {len(to_enrich)} stories with comments")
|
|
336
389
|
|
|
337
390
|
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
391
|
+
# submit_with_context, not executor.submit: a plain submit drops the
|
|
392
|
+
# pipeline's capture_failures() sink, so failed or rate-limited item
|
|
393
|
+
# fetches would vanish and the HN source would look fully enriched.
|
|
338
394
|
futures = {
|
|
339
|
-
|
|
395
|
+
http.submit_with_context(
|
|
396
|
+
executor,
|
|
340
397
|
_fetch_item_comments,
|
|
341
398
|
items[idx]["id"],
|
|
342
399
|
): idx
|