luckiest-co 1.0.52 → 1.0.53
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 +4 -0
- package/skills/ab-testing/SKILL.md +3 -3
- package/skills/ab-testing/references/sample-size-guide.md +15 -2
- package/skills/ad-creative/CHANGELOG.md +8 -0
- package/skills/ad-creative/SKILL.md +23 -13
- package/skills/ads/CHANGELOG.md +6 -0
- package/skills/ads/SKILL.md +14 -10
- package/skills/ads/evals/evals.json +13 -0
- package/skills/ads/references/operating-check.md +75 -0
- package/skills/ai-seo/CHANGELOG.md +11 -0
- package/skills/ai-seo/SKILL.md +28 -27
- package/skills/ai-seo/references/agent-defaults.md +79 -0
- package/skills/ai-seo/references/ai-attribution.md +54 -0
- package/skills/ai-seo/references/answer-accuracy.md +84 -0
- package/skills/ai-seo/references/content-patterns.md +24 -0
- package/skills/ai-seo/references/multilingual-and-commerce.md +55 -0
- package/skills/ai-seo/references/platform-ranking-factors.md +29 -11
- package/skills/ai-seo/references/prompt-portfolio.md +83 -0
- package/skills/analytics/CHANGELOG.md +8 -0
- package/skills/analytics/SKILL.md +74 -18
- package/skills/analytics/references/event-library.md +2 -2
- package/skills/churn-prevention/CHANGELOG.md +7 -0
- package/skills/churn-prevention/SKILL.md +16 -8
- package/skills/churn-prevention/references/dunning-playbook.md +10 -7
- package/skills/co-marketing/CHANGELOG.md +3 -0
- package/skills/co-marketing/SKILL.md +9 -9
- package/skills/coder/CHANGELOG.md +19 -0
- package/skills/coder/SKILL.md +4 -3
- package/skills/coder-writing-plans/CHANGELOG.md +5 -0
- package/skills/coder-writing-plans/SKILL.md +4 -3
- package/skills/content-strategy/CHANGELOG.md +3 -0
- package/skills/content-strategy/SKILL.md +3 -3
- package/skills/content-strategy/references/headless-cms.md +4 -4
- package/skills/copy-editing/CHANGELOG.md +4 -0
- package/skills/copy-editing/SKILL.md +5 -3
- package/skills/copy-editing/evals/evals.json +13 -0
- package/skills/copy-editing/references/checklist.md +24 -0
- package/skills/copywriting/CHANGELOG.md +6 -0
- package/skills/copywriting/SKILL.md +57 -4
- package/skills/copywriting/evals/evals.json +14 -0
- package/skills/emails/CHANGELOG.md +4 -0
- package/skills/emails/SKILL.md +5 -3
- package/skills/marketing-plan/CHANGELOG.md +4 -0
- package/skills/marketing-plan/SKILL.md +8 -3
- package/skills/offers/CHANGELOG.md +4 -0
- package/skills/offers/SKILL.md +4 -4
- package/skills/onboarding/CHANGELOG.md +4 -0
- package/skills/onboarding/SKILL.md +13 -7
- package/skills/pricing/CHANGELOG.md +4 -0
- package/skills/pricing/SKILL.md +3 -3
- package/skills/pricing/references/research-methods.md +12 -2
- package/skills/product-marketing/CHANGELOG.md +5 -0
- package/skills/product-marketing/SKILL.md +76 -11
- package/skills/product-marketing/references/brand-voice.md +103 -0
- package/skills/prospecting/CHANGELOG.md +3 -0
- package/skills/prospecting/SKILL.md +22 -22
- package/skills/prospecting/references/data-sources.md +0 -18
- package/skills/prospecting/references/saas-prospecting.md +2 -2
- package/skills/referrals/CHANGELOG.md +4 -0
- package/skills/referrals/SKILL.md +3 -3
- package/skills/referrals/references/program-examples.md +9 -2
- package/skills/research/CHANGELOG.md +5 -0
- package/skills/research/SKILL.md +5 -5
- package/skills/research/references/visual-read.md +10 -4
- package/skills/revops/CHANGELOG.md +4 -0
- package/skills/revops/SKILL.md +6 -6
- package/skills/revops/references/scoring-models.md +14 -13
- package/skills/seo-audit/CHANGELOG.md +4 -0
- package/skills/seo-audit/SKILL.md +7 -7
- package/skills/seo-audit/references/international-seo.md +3 -1
- package/skills/social/CHANGELOG.md +4 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/social/references/listening.md +13 -0
- package/skills/social/references/platforms.md +35 -0
- package/skills/social/references/short-form-video.md +10 -0
- package/skills/trends/CHANGELOG.md +7 -0
- package/skills/trends/SKILL.md +5 -5
- package/skills/trends/agents/openai.yaml +8 -0
- package/skills/trends/references/comparison.md +5 -2
- package/skills/trends/references/engine-contract.md +2 -2
- package/skills/trends/references/synthesis.md +2 -2
- package/skills/trends/scripts/build-skill.sh +39 -0
- package/skills/trends/scripts/compare.sh +61 -0
- package/skills/trends/scripts/evaluate_search_quality.py +584 -0
- package/skills/trends/scripts/lib/arxiv.py +10 -1
- package/skills/trends/scripts/lib/bluesky.py +22 -2
- package/skills/trends/scripts/lib/devto.py +108 -18
- package/skills/trends/scripts/lib/digg.py +79 -17
- package/skills/trends/scripts/lib/github.py +9 -4
- package/skills/trends/scripts/lib/hackernews.py +74 -2
- package/skills/trends/scripts/lib/html_render.py +15 -1
- package/skills/trends/scripts/lib/pipeline.py +1 -1
- package/skills/trends/scripts/lib/planner.py +5 -0
- package/skills/trends/scripts/lib/providers.py +1 -1
- package/skills/trends/scripts/lib/reddit_shreddit.py +4 -1
- package/skills/trends/scripts/lib/render.py +49 -45
- package/skills/trends/scripts/lib/youtube_yt.py +10 -7
- package/skills/trends/scripts/test-v1-vs-v2.sh +220 -0
- package/skills/trends/scripts/test_device_auth.py +128 -0
- package/skills/trends/scripts/verify_v3.py +200 -0
- package/skills/video/CHANGELOG.md +1 -1
|
@@ -598,17 +598,20 @@ def _defang_corpus_sentinels(value: str) -> str:
|
|
|
598
598
|
def _defang_engine_sentinels(value: str) -> str:
|
|
599
599
|
"""Source content must not be able to forge the engine's own block markers.
|
|
600
600
|
|
|
601
|
-
The EVIDENCE FOR SYNTHESIS
|
|
602
|
-
|
|
603
|
-
verbatim. Scraped text carrying those markers could therefore close
|
|
604
|
-
evidence envelope early or open a footer the model relays to the user
|
|
605
|
-
unmodified. Breaking the comment delimiters
|
|
606
|
-
|
|
607
|
-
|
|
601
|
+
The EVIDENCE FOR SYNTHESIS envelope is an HTML comment pair and the engine
|
|
602
|
+
footer is a fenced ``text`` code block, which LAW 5 tells the host model to
|
|
603
|
+
emit verbatim. Scraped text carrying those markers could therefore close
|
|
604
|
+
the evidence envelope early or open a footer the model relays to the user
|
|
605
|
+
unmodified. Breaking the comment delimiters and the code-fence runs
|
|
606
|
+
(backtick and tilde, the two CommonMark fence characters) is what actually
|
|
607
|
+
neutralizes them; the phrase substitutions are defense in depth for a model
|
|
608
|
+
that pattern-matches on the wording rather than the syntax.
|
|
608
609
|
"""
|
|
609
610
|
return (
|
|
610
611
|
value.replace("<!--", "<!- -")
|
|
611
612
|
.replace("-->", "- ->")
|
|
613
|
+
.replace("```", "'''")
|
|
614
|
+
.replace("~~~", "~ ~ ~")
|
|
612
615
|
.replace("PASS-THROUGH FOOTER", "PASS-THROUGH-FOOTER")
|
|
613
616
|
.replace("EVIDENCE FOR SYNTHESIS", "EVIDENCE-FOR-SYNTHESIS")
|
|
614
617
|
)
|
|
@@ -845,8 +848,8 @@ def render_compact(
|
|
|
845
848
|
# evidence for the model to READ, not output to emit. LAW 6 in SKILL.md
|
|
846
849
|
# names the failure mode: 2026-04-19 Hermes Agent runs dumped this block
|
|
847
850
|
# verbatim as user output. The envelope comments give the model an
|
|
848
|
-
# unambiguous scope for "pass through verbatim" (the
|
|
849
|
-
#
|
|
851
|
+
# unambiguous scope for "pass through verbatim" (the fenced footer block
|
|
852
|
+
# below) vs "synthesize from" (this block).
|
|
850
853
|
lines.append(
|
|
851
854
|
"<!-- EVIDENCE FOR SYNTHESIS: read this, do not emit verbatim. Transform into `What I learned:` prose per LAW 2. -->"
|
|
852
855
|
)
|
|
@@ -936,13 +939,7 @@ def render_compact(
|
|
|
936
939
|
lines.extend(comparison_scaffold)
|
|
937
940
|
|
|
938
941
|
footer = _render_emoji_footer(report, save_path)
|
|
939
|
-
|
|
940
|
-
lines.append("")
|
|
941
|
-
lines.append(
|
|
942
|
-
"<!-- PASS-THROUGH FOOTER: emit verbatim in the model response per LAW 5. -->"
|
|
943
|
-
)
|
|
944
|
-
lines.extend(footer)
|
|
945
|
-
lines.append("<!-- END PASS-THROUGH FOOTER -->")
|
|
942
|
+
_append_pass_through_footer(lines, footer)
|
|
946
943
|
|
|
947
944
|
lines.extend(_render_canonical_boundary())
|
|
948
945
|
|
|
@@ -1168,12 +1165,24 @@ def _append_html_footer(
|
|
|
1168
1165
|
lines: list[str], report: schema.Report, save_path: str | None
|
|
1169
1166
|
) -> None:
|
|
1170
1167
|
footer = _render_emoji_footer(report, save_path)
|
|
1168
|
+
_append_pass_through_footer(lines, footer)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _append_pass_through_footer(lines: list[str], footer: list[str]) -> None:
|
|
1172
|
+
"""Emit the emoji footer inside the fenced ``text`` block LAW 5 relays.
|
|
1173
|
+
|
|
1174
|
+
Footer rows interpolate source-controlled values (handles, subreddits,
|
|
1175
|
+
market questions, advertiser names). A value carrying a newline plus a
|
|
1176
|
+
fence run would close the trusted block early and leave the rest of the
|
|
1177
|
+
row outside it, so every row is flattened to one line and defanged here,
|
|
1178
|
+
at the one place all footer paths share.
|
|
1179
|
+
"""
|
|
1180
|
+
if not footer:
|
|
1181
|
+
return
|
|
1171
1182
|
lines.append("")
|
|
1172
|
-
lines.append(
|
|
1173
|
-
|
|
1174
|
-
)
|
|
1175
|
-
lines.extend(footer)
|
|
1176
|
-
lines.append("<!-- END PASS-THROUGH FOOTER -->")
|
|
1183
|
+
lines.append("```text")
|
|
1184
|
+
lines.extend(_defang_engine_sentinels(" ".join(row.splitlines())) for row in footer)
|
|
1185
|
+
lines.append("```")
|
|
1177
1186
|
|
|
1178
1187
|
|
|
1179
1188
|
def _render_synthesis_directive() -> list[str]:
|
|
@@ -1199,7 +1208,7 @@ def _render_synthesis_directive() -> list[str]:
|
|
|
1199
1208
|
"> READ, not text to emit. Transform it into `What I learned:` prose paragraphs",
|
|
1200
1209
|
"> per LAW 2. Do NOT pass the `### N.` evidence clusters or the stats and",
|
|
1201
1210
|
"> source-coverage blocks through verbatim. The ONLY block you emit verbatim is",
|
|
1202
|
-
"> the
|
|
1211
|
+
"> the fenced `text` footer block (the emoji tree) lower down. The full contract repeats",
|
|
1203
1212
|
"> at the end-of-output boundary near the bottom; if your captured output was",
|
|
1204
1213
|
"> truncated and never reached it, this contract still binds.",
|
|
1205
1214
|
"",
|
|
@@ -1219,7 +1228,7 @@ def _render_canonical_boundary() -> list[str]:
|
|
|
1219
1228
|
"Pass through the lines ABOVE this boundary verbatim" phrasing was
|
|
1220
1229
|
ambiguous about scope and led two consecutive runs to dump the
|
|
1221
1230
|
`## Ranked Evidence Clusters` scratchpad as user output. The current
|
|
1222
|
-
phrasing scopes pass-through to the
|
|
1231
|
+
phrasing scopes pass-through to the fenced footer block only and
|
|
1223
1232
|
gives the model a concrete self-check string (`### 1.` + score tuple).
|
|
1224
1233
|
"""
|
|
1225
1234
|
return [
|
|
@@ -1227,7 +1236,9 @@ def _render_canonical_boundary() -> list[str]:
|
|
|
1227
1236
|
"---",
|
|
1228
1237
|
"# END OF last30days CANONICAL OUTPUT",
|
|
1229
1238
|
"",
|
|
1230
|
-
"Pass through ONLY the
|
|
1239
|
+
"Pass through ONLY the fenced `text` footer block above (emoji-tree stats).",
|
|
1240
|
+
"Keep the code fence, so Markdown does not turn `---` into horizontal",
|
|
1241
|
+
"rules or distort the tree.",
|
|
1231
1242
|
"The EVIDENCE FOR SYNTHESIS block above it is raw evidence for your synthesis,",
|
|
1232
1243
|
"not output. Transform it into `What I learned:` prose paragraphs per LAW 2.",
|
|
1233
1244
|
"",
|
|
@@ -1525,13 +1536,7 @@ def render_comparison_multi(
|
|
|
1525
1536
|
lines.extend(scaffold)
|
|
1526
1537
|
|
|
1527
1538
|
footer = _render_emoji_footer(main_report, save_path)
|
|
1528
|
-
|
|
1529
|
-
lines.append("")
|
|
1530
|
-
lines.append(
|
|
1531
|
-
"<!-- PASS-THROUGH FOOTER: emit verbatim in the model response per LAW 5. -->"
|
|
1532
|
-
)
|
|
1533
|
-
lines.extend(footer)
|
|
1534
|
-
lines.append("<!-- END PASS-THROUGH FOOTER -->")
|
|
1539
|
+
_append_pass_through_footer(lines, footer)
|
|
1535
1540
|
|
|
1536
1541
|
lines.extend(_render_canonical_boundary())
|
|
1537
1542
|
|
|
@@ -2007,9 +2012,7 @@ def render_full(report: schema.Report, save_path: str | None = None) -> str:
|
|
|
2007
2012
|
lines.extend(_render_stats(evidence_report))
|
|
2008
2013
|
lines.extend(_render_source_coverage(evidence_report))
|
|
2009
2014
|
if save_path:
|
|
2010
|
-
|
|
2011
|
-
if footer_lines:
|
|
2012
|
-
lines.extend(["", *footer_lines])
|
|
2015
|
+
_append_pass_through_footer(lines, _render_emoji_footer(evidence_report, save_path))
|
|
2013
2016
|
return "\n".join(lines).strip() + "\n"
|
|
2014
2017
|
|
|
2015
2018
|
|
|
@@ -3424,17 +3427,7 @@ def _format_date(item: schema.SourceItem | None) -> str:
|
|
|
3424
3427
|
def _format_actor(item: schema.SourceItem | None) -> str | None:
|
|
3425
3428
|
if not item:
|
|
3426
3429
|
return None
|
|
3427
|
-
|
|
3428
|
-
return f"r/{item.container}"
|
|
3429
|
-
if item.source in {"x", "bluesky", "truthsocial"} and item.author:
|
|
3430
|
-
return f"@{item.author.lstrip('@')}"
|
|
3431
|
-
if item.source == "youtube" and item.author:
|
|
3432
|
-
return item.author
|
|
3433
|
-
if item.container and item.container != "Polymarket":
|
|
3434
|
-
return item.container
|
|
3435
|
-
if item.author:
|
|
3436
|
-
return item.author
|
|
3437
|
-
return None
|
|
3430
|
+
return _stats_actor(item)
|
|
3438
3431
|
|
|
3439
3432
|
|
|
3440
3433
|
# Per-source engagement display fields: list of (field_name, label) tuples.
|
|
@@ -3556,6 +3549,17 @@ def _top_voices_overall(
|
|
|
3556
3549
|
|
|
3557
3550
|
|
|
3558
3551
|
def _stats_actor(item: schema.SourceItem) -> str | None:
|
|
3552
|
+
"""The handle, subreddit or channel an item is attributed to, on one line.
|
|
3553
|
+
|
|
3554
|
+
Author and container come from the source, and the result is interpolated
|
|
3555
|
+
into evidence rows, ``## Stats`` and the pass-through footer, so it gets the
|
|
3556
|
+
same flattening and sentinel defanging as a scraped title.
|
|
3557
|
+
"""
|
|
3558
|
+
actor = _raw_actor(item)
|
|
3559
|
+
return _safe_title(actor) if actor else None
|
|
3560
|
+
|
|
3561
|
+
|
|
3562
|
+
def _raw_actor(item: schema.SourceItem) -> str | None:
|
|
3559
3563
|
if item.source == "reddit" and item.container:
|
|
3560
3564
|
return f"r/{item.container}"
|
|
3561
3565
|
if item.source in {"x", "bluesky", "truthsocial"} and item.author:
|
|
@@ -82,8 +82,8 @@ _ytdlp_slots = threading.Semaphore(_YTDLP_MAX_CONCURRENT)
|
|
|
82
82
|
# every entity sub-run; cache hits avoid the redundant expensive --dump-json work.
|
|
83
83
|
# Inflight coalescing prevents N concurrent identical searches from all missing
|
|
84
84
|
# the cache and stampeding YouTube together.
|
|
85
|
-
_search_cache: Dict[Tuple[str, int, str], Dict[str, Any]] = {}
|
|
86
|
-
_search_inflight: Dict[Tuple[str, int, str], tuple[threading.Event, list]] = {}
|
|
85
|
+
_search_cache: Dict[Tuple[str, int, str, str, Optional[str]], Dict[str, Any]] = {}
|
|
86
|
+
_search_inflight: Dict[Tuple[str, int, str, str, Optional[str]], tuple[threading.Event, list]] = {}
|
|
87
87
|
_search_cache_lock = threading.Lock()
|
|
88
88
|
# Comments are enrichment, not core evidence: keep the budget tight so a slow
|
|
89
89
|
# comment API can never dominate a run's wall clock (bounded to 3 videos).
|
|
@@ -198,7 +198,7 @@ def _run_ytdlp(cmd: List[str], *, timeout: float) -> subproc.SubprocResult:
|
|
|
198
198
|
|
|
199
199
|
|
|
200
200
|
def _claim_search_slot(
|
|
201
|
-
cache_key: Tuple[str, int, str],
|
|
201
|
+
cache_key: Tuple[str, int, str, str, Optional[str]],
|
|
202
202
|
) -> tuple[Optional[Dict[str, Any]], Optional[threading.Event], Optional[list], bool]:
|
|
203
203
|
"""Return ``(cached, event, slot, is_leader)`` for search coalesce.
|
|
204
204
|
|
|
@@ -220,7 +220,7 @@ def _claim_search_slot(
|
|
|
220
220
|
|
|
221
221
|
|
|
222
222
|
def _finish_search_slot(
|
|
223
|
-
cache_key: Tuple[str, int, str],
|
|
223
|
+
cache_key: Tuple[str, int, str, str, Optional[str]],
|
|
224
224
|
payload: Dict[str, Any],
|
|
225
225
|
*,
|
|
226
226
|
event: threading.Event,
|
|
@@ -489,14 +489,17 @@ _RECENT_FRESHNESS_MODES = frozenset({"strict_recent", "balanced_recent"})
|
|
|
489
489
|
def _apply_date_window(
|
|
490
490
|
items: List[Dict[str, Any]],
|
|
491
491
|
from_date: str,
|
|
492
|
+
to_date: str,
|
|
492
493
|
freshness_mode: Optional[str],
|
|
493
494
|
) -> List[Dict[str, Any]]:
|
|
494
495
|
"""Prefer in-window videos; scope the keep-all fallback by freshness mode.
|
|
495
496
|
|
|
497
|
+
Videos published after ``to_date`` (an --as-of window) are never kept.
|
|
496
498
|
3+ in-window videos: keep only those (every mode). Fewer: recent modes
|
|
497
499
|
keep only the in-window ones; evergreen_ok and unknown/None keep all so
|
|
498
500
|
evergreen topics can still fall back to older videos.
|
|
499
501
|
"""
|
|
502
|
+
items = [i for i in items if not (i["date"] and i["date"] > to_date)]
|
|
500
503
|
recent = [i for i in items if i["date"] and i["date"] >= from_date]
|
|
501
504
|
if len(recent) >= 3:
|
|
502
505
|
_log(f"Found {len(recent)} videos within date range")
|
|
@@ -536,7 +539,7 @@ def search_youtube(
|
|
|
536
539
|
|
|
537
540
|
count = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"])
|
|
538
541
|
core_topic = _extract_core_subject(topic)
|
|
539
|
-
cache_key = (core_topic, count, from_date, freshness_mode)
|
|
542
|
+
cache_key = (core_topic, count, from_date, to_date, freshness_mode)
|
|
540
543
|
timeout = _search_timeout()
|
|
541
544
|
|
|
542
545
|
cached, event, slot, is_leader = _claim_search_slot(cache_key)
|
|
@@ -645,7 +648,7 @@ def search_youtube(
|
|
|
645
648
|
|
|
646
649
|
# Date filter: prefer recent items; the keep-all fallback is scoped
|
|
647
650
|
# by freshness mode (evergreen only), see _apply_date_window.
|
|
648
|
-
items = _apply_date_window(items, from_date, freshness_mode)
|
|
651
|
+
items = _apply_date_window(items, from_date, to_date, freshness_mode)
|
|
649
652
|
|
|
650
653
|
# Sort by views descending
|
|
651
654
|
items.sort(key=lambda x: x["engagement"]["views"], reverse=True)
|
|
@@ -1667,7 +1670,7 @@ def search_youtube_sc(
|
|
|
1667
1670
|
})
|
|
1668
1671
|
|
|
1669
1672
|
# Date filter (keep-all fallback scoped by freshness mode)
|
|
1670
|
-
items = _apply_date_window(items, from_date, freshness_mode)
|
|
1673
|
+
items = _apply_date_window(items, from_date, to_date, freshness_mode)
|
|
1671
1674
|
|
|
1672
1675
|
# Sort by views
|
|
1673
1676
|
items.sort(key=lambda x: x["engagement"]["views"], reverse=True)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# === V1 vs V2 Skill Test Harness ===
|
|
5
|
+
# Runs all 17 test queries through both v1 and v2 SKILL.md
|
|
6
|
+
# using `claude --print` to capture real end-to-end output.
|
|
7
|
+
|
|
8
|
+
SKILL_DIR="$HOME/.claude/skills/last30days"
|
|
9
|
+
REPO_DIR="${REPO_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
10
|
+
CLAUDE="${CLAUDE:-$(command -v claude || echo claude)}"
|
|
11
|
+
|
|
12
|
+
# Safety: always restore V2 SKILL.md on exit/crash
|
|
13
|
+
cleanup() {
|
|
14
|
+
if [ -f "$SKILL_DIR/SKILL.md.v2.bak" ]; then
|
|
15
|
+
echo ""
|
|
16
|
+
echo "⚠️ Restoring V2 SKILL.md from backup (script interrupted)..."
|
|
17
|
+
cp "$SKILL_DIR/SKILL.md.v2.bak" "$SKILL_DIR/SKILL.md"
|
|
18
|
+
rm -f "$SKILL_DIR/SKILL.md.v2.bak"
|
|
19
|
+
echo " ✅ V2 restored"
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
trap cleanup EXIT
|
|
23
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
24
|
+
OUT_DIR="$REPO_DIR/docs/test-results/v1-vs-v2-${TIMESTAMP}"
|
|
25
|
+
V1_DIR="$OUT_DIR/v1"
|
|
26
|
+
V2_DIR="$OUT_DIR/v2"
|
|
27
|
+
|
|
28
|
+
mkdir -p "$V1_DIR" "$V2_DIR"
|
|
29
|
+
|
|
30
|
+
echo "📁 Output directory: $OUT_DIR"
|
|
31
|
+
echo ""
|
|
32
|
+
|
|
33
|
+
# All 17 test queries
|
|
34
|
+
QUERIES=(
|
|
35
|
+
"prompting techniques for chatgpt for legal questions"
|
|
36
|
+
"best clawdbot use cases"
|
|
37
|
+
"how to best setup clawdbot"
|
|
38
|
+
"prompting tips for nano banana pro for ios designs"
|
|
39
|
+
"top claude code skills"
|
|
40
|
+
"using ChatGPT to make images of dogs"
|
|
41
|
+
"research best practices for beautiful remotion animation videos in claude code"
|
|
42
|
+
"photorealistic people in nano banana pro"
|
|
43
|
+
"What are the best rap songs lately"
|
|
44
|
+
"what are people saying about DeepSeek R1"
|
|
45
|
+
"best practices for cursor rules files for Cursor"
|
|
46
|
+
"prompt advice for using suno to make killer songs in simple mode"
|
|
47
|
+
"how do I use Codex with Claude Code on same app to make it better"
|
|
48
|
+
"kanye west"
|
|
49
|
+
"howie.ai"
|
|
50
|
+
"open claw"
|
|
51
|
+
"nano banana pro prompting"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
TYPES=(
|
|
55
|
+
"PROMPTING+TOOL"
|
|
56
|
+
"RECOMMENDATIONS"
|
|
57
|
+
"HOW-TO"
|
|
58
|
+
"PROMPTING+TOOL"
|
|
59
|
+
"RECOMMENDATIONS"
|
|
60
|
+
"GENERAL"
|
|
61
|
+
"PROMPTING"
|
|
62
|
+
"PROMPTING"
|
|
63
|
+
"RECOMMENDATIONS"
|
|
64
|
+
"NEWS"
|
|
65
|
+
"PROMPTING"
|
|
66
|
+
"PROMPTING"
|
|
67
|
+
"HOW-TO"
|
|
68
|
+
"NEWS"
|
|
69
|
+
"GENERAL"
|
|
70
|
+
"GENERAL"
|
|
71
|
+
"PROMPTING"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
slugify() {
|
|
75
|
+
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | cut -c1-50
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
run_version() {
|
|
79
|
+
local version="$1"
|
|
80
|
+
local outdir="$2"
|
|
81
|
+
local total=${#QUERIES[@]}
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "=========================================="
|
|
85
|
+
echo " Running $version — $total queries"
|
|
86
|
+
echo "=========================================="
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
for i in "${!QUERIES[@]}"; do
|
|
90
|
+
local query="${QUERIES[$i]}"
|
|
91
|
+
local type="${TYPES[$i]}"
|
|
92
|
+
local slug
|
|
93
|
+
slug=$(slugify "$query")
|
|
94
|
+
local num=$((i + 1))
|
|
95
|
+
local outfile="$outdir/${num}-${slug}.txt"
|
|
96
|
+
local errfile="$outdir/${num}-${slug}.stderr.txt"
|
|
97
|
+
|
|
98
|
+
echo "[$version] ($num/$total) $query [$type]"
|
|
99
|
+
|
|
100
|
+
local start_time
|
|
101
|
+
start_time=$(date +%s)
|
|
102
|
+
|
|
103
|
+
# Run claude --print with the skill invocation
|
|
104
|
+
# No timeout — claude --print exits on its own; kill manually if stuck
|
|
105
|
+
if "$CLAUDE" --print \
|
|
106
|
+
"/last30days $query" \
|
|
107
|
+
> "$outfile" 2>"$errfile"; then
|
|
108
|
+
local end_time
|
|
109
|
+
end_time=$(date +%s)
|
|
110
|
+
local duration=$((end_time - start_time))
|
|
111
|
+
local lines
|
|
112
|
+
lines=$(wc -l < "$outfile")
|
|
113
|
+
echo " ✅ Done — ${lines} lines, ${duration}s"
|
|
114
|
+
else
|
|
115
|
+
local exit_code=$?
|
|
116
|
+
echo " ❌ Failed (exit $exit_code)" | tee -a "$outfile"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Brief pause between queries to avoid rate limits
|
|
120
|
+
sleep 3
|
|
121
|
+
done
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# === Phase 1: Test V1 ===
|
|
125
|
+
echo "📦 Backing up current V2 SKILL.md..."
|
|
126
|
+
cp "$SKILL_DIR/SKILL.md" "$SKILL_DIR/SKILL.md.v2.bak"
|
|
127
|
+
|
|
128
|
+
echo "📥 Installing V1 SKILL.md from upstream..."
|
|
129
|
+
cd "$REPO_DIR"
|
|
130
|
+
git show upstream/main:SKILL.md | sed '/^context: fork$/d; /^agent: Explore$/d; /^disable-model-invocation: true$/d' > "$SKILL_DIR/SKILL.md"
|
|
131
|
+
cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v1-SKILL.md"
|
|
132
|
+
echo " ✅ V1 installed (stripped: context:fork, agent:Explore, disable-model-invocation)"
|
|
133
|
+
|
|
134
|
+
run_version "V1" "$V1_DIR"
|
|
135
|
+
|
|
136
|
+
# === Phase 2: Test V2 ===
|
|
137
|
+
echo ""
|
|
138
|
+
echo "📥 Restoring V2 SKILL.md..."
|
|
139
|
+
cp "$SKILL_DIR/SKILL.md.v2.bak" "$SKILL_DIR/SKILL.md"
|
|
140
|
+
cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v2-SKILL.md"
|
|
141
|
+
echo " ✅ V2 restored"
|
|
142
|
+
|
|
143
|
+
run_version "V2" "$V2_DIR"
|
|
144
|
+
|
|
145
|
+
# === Phase 3: Generate summary ===
|
|
146
|
+
echo ""
|
|
147
|
+
echo "=========================================="
|
|
148
|
+
echo " Generating comparison summary"
|
|
149
|
+
echo "=========================================="
|
|
150
|
+
|
|
151
|
+
SUMMARY="$OUT_DIR/comparison-summary.md"
|
|
152
|
+
|
|
153
|
+
cat > "$SUMMARY" << EOF
|
|
154
|
+
# V1 vs V2 Comparison Results
|
|
155
|
+
|
|
156
|
+
Generated: $(date)
|
|
157
|
+
Output directory: $OUT_DIR
|
|
158
|
+
|
|
159
|
+
## Output Files
|
|
160
|
+
|
|
161
|
+
| # | Query | Type | V1 Lines | V2 Lines | V1 Time | V2 Time |
|
|
162
|
+
|---|-------|------|----------|----------|---------|---------|
|
|
163
|
+
EOF
|
|
164
|
+
|
|
165
|
+
for i in "${!QUERIES[@]}"; do
|
|
166
|
+
query="${QUERIES[$i]}"
|
|
167
|
+
type="${TYPES[$i]}"
|
|
168
|
+
slug=$(slugify "$query")
|
|
169
|
+
num=$((i + 1))
|
|
170
|
+
|
|
171
|
+
v1file="$V1_DIR/${num}-${slug}.txt"
|
|
172
|
+
v2file="$V2_DIR/${num}-${slug}.txt"
|
|
173
|
+
|
|
174
|
+
v1lines=$(wc -l < "$v1file" 2>/dev/null || echo "ERR")
|
|
175
|
+
v2lines=$(wc -l < "$v2file" 2>/dev/null || echo "ERR")
|
|
176
|
+
|
|
177
|
+
echo "| $num | \`$query\` | $type | $v1lines | $v2lines | — | — |" >> "$SUMMARY"
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
cat >> "$SUMMARY" << 'EOF'
|
|
181
|
+
|
|
182
|
+
## Quick Check: Key Features
|
|
183
|
+
|
|
184
|
+
For each query, check these v2 improvements:
|
|
185
|
+
|
|
186
|
+
- [ ] Query parsing display (`🔍 **{TOPIC}** · {QUERY_TYPE}`)
|
|
187
|
+
- [ ] Sparse citations (not every sentence)
|
|
188
|
+
- [ ] Bold topic headers in summary
|
|
189
|
+
- [ ] Emoji stats tree (`├─ 🟠 Reddit:`)
|
|
190
|
+
- [ ] Quality checklist applied to prompts
|
|
191
|
+
- [ ] Self-check (research grounding, not generic)
|
|
192
|
+
|
|
193
|
+
## Scoring Guide
|
|
194
|
+
|
|
195
|
+
Use the full scoring rubric from:
|
|
196
|
+
`docs/plans/2026-02-06-test-v1-vs-v2-comparison-plan.md`
|
|
197
|
+
|
|
198
|
+
## Next Step
|
|
199
|
+
|
|
200
|
+
Have Claude read all 34 output files and generate scored comparison:
|
|
201
|
+
```
|
|
202
|
+
Read all files in docs/test-results/v1-vs-v2-*/v1/ and v2/
|
|
203
|
+
Score each on the 7 dimensions from the test plan
|
|
204
|
+
Write the final analysis to docs/test-results/v1-vs-v2-*/analysis.md
|
|
205
|
+
```
|
|
206
|
+
EOF
|
|
207
|
+
|
|
208
|
+
# Cleanup backup
|
|
209
|
+
rm -f "$SKILL_DIR/SKILL.md.v2.bak"
|
|
210
|
+
|
|
211
|
+
echo ""
|
|
212
|
+
echo "✅ All done!"
|
|
213
|
+
echo ""
|
|
214
|
+
echo "📁 Results: $OUT_DIR"
|
|
215
|
+
echo "📊 Summary: $SUMMARY"
|
|
216
|
+
echo "📄 V1 files: $V1_DIR/"
|
|
217
|
+
echo "📄 V2 files: $V2_DIR/"
|
|
218
|
+
echo ""
|
|
219
|
+
echo "To review:"
|
|
220
|
+
echo " open $OUT_DIR"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test ScrapeCreators GitHub device auth flow from the CLI.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 scripts/test_device_auth.py
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. Starts device code request
|
|
9
|
+
2. Shows user code + opens GitHub auth URL in browser
|
|
10
|
+
3. Polls for token until you complete auth
|
|
11
|
+
4. Fetches your profile and prints your API key
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import webbrowser
|
|
18
|
+
from urllib.request import Request, urlopen
|
|
19
|
+
from urllib.error import HTTPError, URLError
|
|
20
|
+
|
|
21
|
+
BASE = "https://api.scrapecreators.com/v1/github/device"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _post(url, data=None):
|
|
25
|
+
body = json.dumps(data).encode() if data else None
|
|
26
|
+
req = Request(url, data=body, method="POST")
|
|
27
|
+
req.add_header("Content-Type", "application/json")
|
|
28
|
+
with urlopen(req, timeout=15) as resp:
|
|
29
|
+
return json.loads(resp.read())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get(url, token):
|
|
33
|
+
req = Request(url)
|
|
34
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
35
|
+
with urlopen(req, timeout=15) as resp:
|
|
36
|
+
return json.loads(resp.read())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
# Step 1: Start device flow
|
|
41
|
+
print("Starting ScrapeCreators GitHub device auth...\n")
|
|
42
|
+
try:
|
|
43
|
+
code_resp = _post(f"{BASE}/code")
|
|
44
|
+
except (HTTPError, URLError) as e:
|
|
45
|
+
print(f"Failed to start device flow: {e}")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
device_code = code_resp.get("device_code")
|
|
49
|
+
user_code = code_resp.get("user_code")
|
|
50
|
+
verification_uri = code_resp.get("verification_uri")
|
|
51
|
+
interval = code_resp.get("interval", 5)
|
|
52
|
+
expires_in = code_resp.get("expires_in", 900)
|
|
53
|
+
|
|
54
|
+
if not device_code or not user_code:
|
|
55
|
+
print(f"Unexpected response: {json.dumps(code_resp, indent=2)}")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
print(f"Your code: {user_code}")
|
|
59
|
+
print(f"Open: {verification_uri}")
|
|
60
|
+
print(f"Expires in: {expires_in}s\n")
|
|
61
|
+
|
|
62
|
+
# Open browser
|
|
63
|
+
if verification_uri:
|
|
64
|
+
webbrowser.open(verification_uri)
|
|
65
|
+
print("Opened browser. Enter the code above, then authorize.\n")
|
|
66
|
+
|
|
67
|
+
# Step 2: Poll for token
|
|
68
|
+
print("Waiting for authorization", end="", flush=True)
|
|
69
|
+
deadline = time.time() + expires_in
|
|
70
|
+
access_token = None
|
|
71
|
+
|
|
72
|
+
while time.time() < deadline:
|
|
73
|
+
time.sleep(interval)
|
|
74
|
+
print(".", end="", flush=True)
|
|
75
|
+
try:
|
|
76
|
+
token_resp = _post(f"{BASE}/token", {"device_code": device_code})
|
|
77
|
+
except HTTPError as e:
|
|
78
|
+
# Some APIs return 4xx while pending
|
|
79
|
+
if e.code in (400, 403, 428):
|
|
80
|
+
continue
|
|
81
|
+
print(f"\nPoll error: {e}")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
except URLError:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if token_resp.get("access_token"):
|
|
87
|
+
access_token = token_resp["access_token"]
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
# Check for explicit error states
|
|
91
|
+
error = token_resp.get("error")
|
|
92
|
+
if error == "authorization_pending" or error == "slow_down":
|
|
93
|
+
if error == "slow_down":
|
|
94
|
+
interval = min(interval + 2, 30)
|
|
95
|
+
continue
|
|
96
|
+
if error in ("expired_token", "access_denied"):
|
|
97
|
+
print(f"\n\nAuth failed: {error}")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
if not access_token:
|
|
101
|
+
print("\n\nTimed out waiting for authorization.")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
print(f"\n\nAuthorized! Access token: {access_token[:12]}...\n")
|
|
105
|
+
|
|
106
|
+
# Step 3: Fetch profile
|
|
107
|
+
print("Fetching profile...")
|
|
108
|
+
try:
|
|
109
|
+
profile = _get(f"{BASE}/profile", access_token)
|
|
110
|
+
except (HTTPError, URLError) as e:
|
|
111
|
+
print(f"Failed to fetch profile: {e}")
|
|
112
|
+
print(f"(access_token was: {access_token})")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
print(f"\nProfile response:\n{json.dumps(profile, indent=2)}\n")
|
|
116
|
+
|
|
117
|
+
api_key = profile.get("api_key")
|
|
118
|
+
if api_key:
|
|
119
|
+
print("=" * 50)
|
|
120
|
+
print(f"Your ScrapeCreators API key: {api_key}")
|
|
121
|
+
print("=" * 50)
|
|
122
|
+
print(f"\nTo use it: echo 'SCRAPECREATORS_API_KEY={api_key}' >> ~/.config/last30days/.env")
|
|
123
|
+
else:
|
|
124
|
+
print("No api_key in profile response. Full response printed above.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|