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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""DripStack source for last30days — premium financial newsletter search.
|
|
2
|
+
|
|
3
|
+
DripStack indexes paid Substack newsletters, analyst writeups, and financial
|
|
4
|
+
podcasts. The search endpoint is free and public (no API key); it returns
|
|
5
|
+
article metadata including title, publication, date, and a relevance-scored
|
|
6
|
+
snippet. Full article summaries and stock picks are behind a paid layer and
|
|
7
|
+
are out of scope for this source adapter.
|
|
8
|
+
|
|
9
|
+
The signal is complementary to the other financial sources: StockTwits gives
|
|
10
|
+
retail sentiment, Polymarket gives prediction-market odds, and DripStack gives
|
|
11
|
+
what professional analysts and paid newsletter authors are actually writing
|
|
12
|
+
about. The search results carry publication attribution (e.g. "SemiAnalysis",
|
|
13
|
+
"Bloomberg") which is high-credibility signal for synthesis.
|
|
14
|
+
|
|
15
|
+
GATING: DripStack search is most valuable for finance, markets, company
|
|
16
|
+
analysis, and industry research topics. Like arXiv (science) and Techmeme
|
|
17
|
+
(tech news), DripStack is relevance-gated — the search API itself filters
|
|
18
|
+
for topic match, so off-topic runs return thin results naturally and the
|
|
19
|
+
engine's thin-retry + relevance scoring handles the rest.
|
|
20
|
+
|
|
21
|
+
API: public, no auth. Search endpoint returns up to 30 items per query.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import datetime
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
import urllib.parse
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from . import http
|
|
34
|
+
|
|
35
|
+
_BASE_URL = "https://dripstack.xyz"
|
|
36
|
+
_SEARCH_URL = f"{_BASE_URL}/api/v1/search"
|
|
37
|
+
_UA = "Mozilla/5.0 (last30days dripstack source)"
|
|
38
|
+
|
|
39
|
+
# Depth controls how many results we request per subquery.
|
|
40
|
+
_DEPTH_LIMITS = {"quick": 5, "default": 10, "deep": 20}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _log(msg: str) -> None:
|
|
44
|
+
try:
|
|
45
|
+
from . import log as _enginelog
|
|
46
|
+
_enginelog.source_log("DripStack", msg, tty_only=False)
|
|
47
|
+
except Exception:
|
|
48
|
+
print(f"[DripStack] {msg}", file=sys.stderr)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_json(url: str, timeout: int = 20) -> dict[str, Any]:
|
|
52
|
+
# All engine traffic goes through the shared lib/http.py choke point so
|
|
53
|
+
# capture/replay, fixtures, and failure taxonomy apply to this source too.
|
|
54
|
+
return http.get(url, headers={"User-Agent": _UA}, timeout=timeout, retries=2)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def search_dripstack(
|
|
58
|
+
topic: str,
|
|
59
|
+
from_date: str | None = None,
|
|
60
|
+
to_date: str | None = None,
|
|
61
|
+
*,
|
|
62
|
+
depth: str = "default",
|
|
63
|
+
) -> list[dict[str, Any]]:
|
|
64
|
+
"""Search DripStack for articles matching the topic.
|
|
65
|
+
|
|
66
|
+
Returns a list of raw item dicts from the search API. The free endpoint
|
|
67
|
+
requires no authentication. Results are relevance-ranked by DripStack's
|
|
68
|
+
own scoring (hybrid RRF — blended semantic + keyword match).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
topic: The search query (e.g. "AI capex risk", "Tesla earnings").
|
|
72
|
+
from_date: ISO date string for start of window (YYYY-MM-DD). Not sent
|
|
73
|
+
to the API (DripStack search has its own time handling), but
|
|
74
|
+
available for post-filtering if needed.
|
|
75
|
+
to_date: ISO date string for end of window (YYYY-MM-DD).
|
|
76
|
+
depth: One of "quick", "default", "deep" — controls result count.
|
|
77
|
+
"""
|
|
78
|
+
limit = _DEPTH_LIMITS.get(depth, 10)
|
|
79
|
+
params = urllib.parse.urlencode({"q": topic, "limit": limit})
|
|
80
|
+
url = f"{_SEARCH_URL}?{params}"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = _get_json(url)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
_log(f"search failed for '{topic}': {e}")
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
items = data.get("items") or []
|
|
89
|
+
if from_date or to_date:
|
|
90
|
+
windowed = []
|
|
91
|
+
dropped = 0
|
|
92
|
+
for item in items:
|
|
93
|
+
published = str(item.get("publishedAt") or "")[:10]
|
|
94
|
+
if published and from_date and published < from_date:
|
|
95
|
+
dropped += 1
|
|
96
|
+
continue
|
|
97
|
+
if published and to_date and published > to_date:
|
|
98
|
+
dropped += 1
|
|
99
|
+
continue
|
|
100
|
+
windowed.append(item)
|
|
101
|
+
if dropped:
|
|
102
|
+
_log(f"dropped {dropped} result(s) outside the {from_date}..{to_date} window")
|
|
103
|
+
items = windowed
|
|
104
|
+
_log(f"search '{topic}': {len(items)} results (confidence: {data.get('matchConfidence', '?')})")
|
|
105
|
+
return items
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_dripstack_response(
|
|
109
|
+
items: list[dict[str, Any]],
|
|
110
|
+
query: str = "",
|
|
111
|
+
) -> list[dict[str, Any]]:
|
|
112
|
+
"""Normalize DripStack search results into engine-style item dicts.
|
|
113
|
+
|
|
114
|
+
Each item maps to the same shape as other sources (HN, Reddit, StockTwits):
|
|
115
|
+
id, title, url, author, date, engagement, relevance, why_relevant,
|
|
116
|
+
snippet, metadata.
|
|
117
|
+
|
|
118
|
+
DripStack has no engagement signal (upvotes, likes), so engagement is
|
|
119
|
+
empty. Ranking relies on DripStack's own relevanceScore (0-100) which we
|
|
120
|
+
normalize to 0-1, plus recency.
|
|
121
|
+
"""
|
|
122
|
+
parsed: list[dict[str, Any]] = []
|
|
123
|
+
for i, item in enumerate(items):
|
|
124
|
+
title = (item.get("title") or "").strip()
|
|
125
|
+
subtitle = (item.get("subtitle") or "").strip()
|
|
126
|
+
snippet_text = (item.get("snippet") or "").strip()
|
|
127
|
+
pub_slug = (item.get("publicationSlug") or "").strip()
|
|
128
|
+
post_slug = (item.get("slug") or "").strip()
|
|
129
|
+
published_at = (item.get("publishedAt") or "")[:10] or None
|
|
130
|
+
|
|
131
|
+
# Build the article URL. For Substack-hosted publications the slug is
|
|
132
|
+
# the full hostname (e.g. "newsletter.doomberg.com") and the post slug
|
|
133
|
+
# is the path segment. For other domains the same pattern applies.
|
|
134
|
+
if pub_slug and post_slug:
|
|
135
|
+
url = f"https://{pub_slug}/{post_slug}"
|
|
136
|
+
else:
|
|
137
|
+
url = ""
|
|
138
|
+
|
|
139
|
+
# Normalize DripStack's 0-100 relevanceScore to 0-1 for the engine.
|
|
140
|
+
raw_score = item.get("relevanceScore", 0)
|
|
141
|
+
try:
|
|
142
|
+
relevance = round(min(1.0, max(0.0, float(raw_score) / 100.0)), 2)
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
relevance = 0.5
|
|
145
|
+
|
|
146
|
+
# Build a human-readable why_relevant from the whyMatched array.
|
|
147
|
+
why_parts = item.get("whyMatched") or []
|
|
148
|
+
# Filter out internal RRF details; keep the useful match explanations.
|
|
149
|
+
why_clean = [
|
|
150
|
+
w for w in why_parts
|
|
151
|
+
if "RRF" not in w and "Hybrid" not in w
|
|
152
|
+
]
|
|
153
|
+
why_relevant = "; ".join(why_clean) if why_clean else f"DripStack newsletter match for: {query}"
|
|
154
|
+
|
|
155
|
+
# The body feeds rerank and synthesis. Use subtitle (the article
|
|
156
|
+
# summary/lede) as the primary content, falling back to snippet.
|
|
157
|
+
body = subtitle or snippet_text or title
|
|
158
|
+
|
|
159
|
+
# Publication name as author — gives attribution credit to the
|
|
160
|
+
# newsletter/analyst who wrote it (e.g. "SemiAnalysis", "Bloomberg").
|
|
161
|
+
# Use the slug as a readable fallback.
|
|
162
|
+
author = pub_slug.replace(".substack.com", "").replace(".com", "")
|
|
163
|
+
|
|
164
|
+
parsed.append({
|
|
165
|
+
"id": f"DS{i + 1}",
|
|
166
|
+
"title": title or f"DripStack result {i + 1}",
|
|
167
|
+
"url": url,
|
|
168
|
+
"author": author or None,
|
|
169
|
+
"date": published_at,
|
|
170
|
+
"engagement": {},
|
|
171
|
+
"relevance": relevance,
|
|
172
|
+
"why_relevant": why_relevant,
|
|
173
|
+
"body": body,
|
|
174
|
+
"snippet": snippet_text[:400],
|
|
175
|
+
"metadata": {
|
|
176
|
+
"publication_slug": pub_slug,
|
|
177
|
+
"post_slug": post_slug,
|
|
178
|
+
"relevance_score": raw_score,
|
|
179
|
+
"match_confidence": item.get("matchConfidence"),
|
|
180
|
+
"topic_coverage_ratio": item.get("topicCoverageRatio"),
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
return parsed
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --------------------------------------------------------------------------- #
|
|
187
|
+
# Standalone CLI #
|
|
188
|
+
# python3 dripstack.py "AI capex risk" #
|
|
189
|
+
# --------------------------------------------------------------------------- #
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
topic = " ".join(sys.argv[1:]) or "AI capex"
|
|
193
|
+
today = datetime.date.today()
|
|
194
|
+
since = (today - datetime.timedelta(days=30)).isoformat()
|
|
195
|
+
|
|
196
|
+
raw = search_dripstack(topic, from_date=since, depth="default")
|
|
197
|
+
items = parse_dripstack_response(raw, query=topic)
|
|
198
|
+
|
|
199
|
+
print(f"Query: {topic} | {len(items)} results")
|
|
200
|
+
for it in items[:10]:
|
|
201
|
+
print(f" [{it['relevance']:.0%}] {it['title']} ({it['author']}, {it['date'] or 'no date'})")
|
|
202
|
+
if it["snippet"]:
|
|
203
|
+
print(f" {it['snippet'][:120]}")
|
|
@@ -13,6 +13,43 @@ GENERIC_HANDLES = {
|
|
|
13
13
|
"verified", "jack", "sundarpichai",
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
ENTITY_STOPWORDS = frozenset({
|
|
17
|
+
"the", "a", "an", "to", "for", "how", "is", "in", "of", "on", "and",
|
|
18
|
+
"with", "from", "by", "at", "this", "that", "it", "what", "are", "do",
|
|
19
|
+
"can", "his", "her", "he", "she", "its", "was", "has", "new", "just",
|
|
20
|
+
"says", "said", "will", "about", "after", "now", "all", "been", "here",
|
|
21
|
+
"not", "out", "up", "more", "also", "but", "who", "year", "first",
|
|
22
|
+
"make", "being", "making", "over", "into", "than", "they", "their",
|
|
23
|
+
"would", "could", "get", "got", "some", "like", "back", "going",
|
|
24
|
+
"breaking", "https", "http", "www", "com",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def has_anchor_signal(word: str) -> bool:
|
|
29
|
+
"""True when a word carries an anchor signal: leading capital, all-caps,
|
|
30
|
+
or any digit (product/person/version anchors)."""
|
|
31
|
+
return word[0].isupper() or word.isupper() or any(char.isdigit() for char in word)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_text_entities(text: str) -> set[str]:
|
|
35
|
+
"""Extract significant words used by clustering and eval scoring."""
|
|
36
|
+
words = re.sub(r"[^\w\s]", " ", text).split()
|
|
37
|
+
entities = set()
|
|
38
|
+
for word in words:
|
|
39
|
+
lower = word.lower()
|
|
40
|
+
if lower in ENTITY_STOPWORDS or len(word) <= 2:
|
|
41
|
+
continue
|
|
42
|
+
if has_anchor_signal(word) or len(word) >= 4:
|
|
43
|
+
entities.add(lower)
|
|
44
|
+
return entities
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def entity_overlap(entities_a: set[str], entities_b: set[str]) -> float:
|
|
48
|
+
"""Return overlap coefficient for two extracted entity sets."""
|
|
49
|
+
if not entities_a or not entities_b:
|
|
50
|
+
return 0.0
|
|
51
|
+
return len(entities_a & entities_b) / min(len(entities_a), len(entities_b))
|
|
52
|
+
|
|
16
53
|
|
|
17
54
|
def extract_entities(
|
|
18
55
|
reddit_items: List[Dict[str, Any]],
|