[object Object]

← back to Gurisingh Scout

initial scaffold: gurisingh-scout skill

a8a04b89774676ed8f3ba093165ccddcf3015b7b · 2026-07-16 20:51:56 -0700 · Steve Abrams

Scouts newly open-sourced Claude Code skills / agent tools surfaced by Guri
Singh (@heygurisingh / gurisaroy) and evaluates each for adoption into the
stack. Retrieval fans out across mirrors to survive X's auth wall and the
account's intermittent suspension. Discovery/scoring free; install is gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit a8a04b89774676ed8f3ba093165ccddcf3015b7b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 20:51:56 2026 -0700

    initial scaffold: gurisingh-scout skill
    
    Scouts newly open-sourced Claude Code skills / agent tools surfaced by Guri
    Singh (@heygurisingh / gurisaroy) and evaluates each for adoption into the
    stack. Retrieval fans out across mirrors to survive X's auth wall and the
    account's intermittent suspension. Discovery/scoring free; install is gated.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .gitignore                       |   9 +++
 README.md                        |  46 ++++++++++++++
 SKILL.md                         |  95 ++++++++++++++++++++++++++++
 references/adoption-rubric.md    |  74 ++++++++++++++++++++++
 references/retrieval-playbook.md |  60 ++++++++++++++++++
 references/sources.md            |  61 ++++++++++++++++++
 scripts/gh-repo-stats.js         | 123 ++++++++++++++++++++++++++++++++++++
 scripts/hydrate-tweet.js         | 132 +++++++++++++++++++++++++++++++++++++++
 8 files changed, 600 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4479347
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.zip
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..266e471
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# gurisingh-scout
+
+A Claude Code **skill** (its own git repo, symlinked into `~/.claude/skills/`)
+that scouts newly open-sourced Claude Code skills / subagents / MCP servers /
+AI-agent tools surfaced by **Guri Singh** (`@heygurisingh` on X, `gurisaroy` on
+LinkedIn) and similar educators — then evaluates each for adoption into Steve's
+stack: dedupe against existing skills, score fit/leverage/trust, draft **gated**
+install memos. Discovery and scoring run freely; installing anything is Steve's
+call.
+
+## Why it exists
+
+Guri Singh's whole genre is *"someone just open-sourced X for Claude Code — it's
+called Y, here's how it works ↓"* (past picks: `codeburn`, `career-ops`,
+`gstack`, `claude-code-best-practice`, "Understand Anything", `Graphify`,
+`Caveman`). That's a high-signal adoption feed — this skill harvests it into
+adopt/watch/skip decisions instead of hype.
+
+The hard part is **retrieval**: `x.com` 402s unauthenticated reads and the
+account is intermittently suspended, so the skill fans out across mirrors
+(Exa-indexed Thread Reader + LinkedIn `gurisaroy`, WebSearch) and only uses
+direct-X hydration when the account is live.
+
+## Layout
+
+- `SKILL.md` — the procedure Claude runs (retrieve → extract → enrich → dedupe →
+  score → deliver gated shortlist).
+- `scripts/hydrate-tweet.js` — one X post → verbatim text + GitHub links, no
+  auth, `$0` (computes the syndication token locally). Exit `3` on tombstone.
+- `scripts/gh-repo-stats.js` — GitHub repo → stars/license/freshness +
+  Claude-skill detection. Honors `GITHUB_TOKEN`.
+- `references/sources.md` — handles, mirrors, similar sources, suspension note.
+- `references/retrieval-playbook.md` — auth-wall-resistant retrieval order.
+- `references/adoption-rubric.md` — six-dimension scoring, dedupe, gating.
+
+## Quick checks
+
+```bash
+node scripts/hydrate-tweet.js https://x.com/heygurisingh/status/<id>
+node scripts/gh-repo-stats.js owner/repo
+```
+
+## Invoke
+
+`/gurisingh-scout` — or "gurisingh scout", "what's Guri Singh posting", "any new
+Claude Code skills worth adding".
diff --git a/SKILL.md b/SKILL.md
new file mode 100644
index 0000000..28b3266
--- /dev/null
+++ b/SKILL.md
@@ -0,0 +1,95 @@
+---
+name: gurisingh-scout
+description: Scout newly open-sourced Claude Code skills, subagents, MCP servers, and AI-agent tools that Guri Singh (@heygurisingh / LinkedIn gurisaroy) and similar educators surface, then evaluate each for adoption into Steve's stack — dedupe against existing skills, score fit/leverage/trust, and draft gated install memos. Use when Steve says 'gurisingh scout', '/gurisingh-scout', 'what is Guri Singh posting', 'any new Claude Code skills or tools worth adding', 'scout new agent tooling', 'find tools to adopt', or points at an @heygurisingh / gurisaroy post (even a tombstoned or suspended X link) and asks what it is and whether to adopt it. Retrieval survives X auth walls and intermittent account suspension by fanning out across mirrors.
+---
+
+# Gurisingh Scout
+
+## Overview
+
+Guri Singh's feed is a high-signal firehose of "someone just open-sourced X for
+Claude Code / AI agents". This skill harvests that signal, resolves the real
+GitHub repo behind each pick, and turns it into an **adopt / watch / skip**
+decision for Steve's specific stack — never a hype relay, and never an
+auto-install. Discovery and scoring run freely; installing anything is gated.
+
+## When to use
+
+- A periodic sweep: "what's worth adopting that Guri Singh posted lately?"
+- A one-off: Steve drops an `@heygurisingh` or `gurisaroy` link (often a
+  suspended/tombstoned X URL) and asks "what is this and should I use it?"
+- A wider sweep of newly open-sourced Claude Code / agent tooling, using Guri
+  Singh as the anchor plus the similar sources in `references/sources.md`.
+
+## Workflow
+
+### 1. Retrieve (fan out — never trust one source)
+X blocks unauthenticated reads and the account is **intermittently suspended**,
+so an X failure ≠ "no new picks". Follow `references/retrieval-playbook.md`:
+discover with Exa (`mcp__exa__web_search_exa`) + `WebSearch`, pull full bodies
+from the **LinkedIn `gurisaroy` mirror** or Thread Reader, and only use direct-X
+hydration when the account is live. Read `references/sources.md` for the exact
+handles, mirrors, and query strings.
+
+For a specific tweet id/URL, get the verbatim text + repo links:
+```bash
+node scripts/hydrate-tweet.js <tweet-id-or-url>   # exit 3 = tombstone → use mirrors
+```
+
+### 2. Extract candidates
+For each post naming an open-source tool, capture: **tool name**, **GitHub
+repo**, **one-line what-it-does**, **category** (claude-skill | claude-plugin |
+CLI/TUI | MCP server | agent framework | prompt-pack). Resolve `lnkd.in`/`t.co`
+shortlinks to the real repo.
+
+### 3. Enrich each repo (hard signals)
+```bash
+node scripts/gh-repo-stats.js <github-url-or-owner/repo>
+```
+Returns stars, license, `days_since_push`, archived, `is_claude_skill`
+(detects `SKILL.md` / `.claude/` / plugin manifest), and quick `flags`. Set
+`GITHUB_TOKEN` in env to avoid the 60/hr unauthenticated rate limit.
+
+### 4. Dedupe against what Steve already has
+Before crediting novelty, check for an existing equivalent (procedure in
+`references/adoption-rubric.md`):
+```bash
+ls ~/.claude/skills/ | grep -i "<keyword>"
+grep -ril "<capability>" ~/.claude/skills/*/SKILL.md 2>/dev/null
+```
+If he already has it, the useful output is a **diff**, not a redundant recommend.
+
+### 5. Score & decide
+Apply the six-dimension rubric in `references/adoption-rubric.md` (stack fit,
+novelty, leverage, install cost, trust/license, safety). Verdict bands:
+**≥14 ADOPT · 9–13 WATCH · ≤8 SKIP.**
+
+### 6. Deliver — shortlist + gated memos
+Output a ranked table:
+`Tool | Repo (stars, license, last-push) | What it does | Category | Dedupe | Score | Verdict | 1-line reason`.
+For each **ADOPT**, draft a short memo to
+`~/.claude/yolo-queue/pending-approval/` with what it is, why it fits, exact
+install steps, **cost** (always show it — most scans are `$0 local`; Exa is
+metered), risks, and an APPROVE/REVISE/BLOCK line.
+
+## Hard rules
+
+- **Never auto-install.** `git clone`, `/plugin install`, `npx` on an unfamiliar
+  tool, pasting into `~/.claude`, or adding a paid key are all gated →
+  pending-approval. The deliverable is a decision-ready shortlist; adoption is
+  Steve's call.
+- **An X/tombstone failure is not "nothing new"** — fall back to mirrors.
+- **Show costs** on every run (discovery via Exa is metered; scripts are `$0`).
+- **Log real wins only** — a pick that actually gets adopted, not the scan.
+
+## Resources
+
+- `scripts/hydrate-tweet.js` — resolve one X post → verbatim text + GitHub links,
+  no auth, `$0` (computes the syndication token locally). Exit 3 on tombstone.
+- `scripts/gh-repo-stats.js` — GitHub repo → stars/license/freshness + Claude-skill
+  detection, feeds the rubric. Honors `GITHUB_TOKEN`.
+- `references/sources.md` — handles, mirrors, similar sources, suspension note.
+- `references/retrieval-playbook.md` — the ordered, auth-wall-resistant retrieval
+  procedure and failure-mode reads.
+- `references/adoption-rubric.md` — the six-dimension scoring model, dedupe
+  procedure, gating rules, and output shape.
diff --git a/references/adoption-rubric.md b/references/adoption-rubric.md
new file mode 100644
index 0000000..139033c
--- /dev/null
+++ b/references/adoption-rubric.md
@@ -0,0 +1,74 @@
+# Adoption rubric — should Steve actually add this?
+
+Guri Singh's feed is a firehose of "someone open-sourced X". Most picks are
+noise for *this* stack. The job isn't to relay hype — it's to answer, per
+candidate: **adopt, watch, or skip**, with a reason. Score each candidate on the
+six dimensions below (0–3 each, 18 max).
+
+## Dimensions
+
+1. **Stack fit (0–3)** — does it serve something Steve actually runs? High:
+   Claude Code skills/subagents/hooks, MCP servers, catalog/scraper/pricing
+   tooling, cost/observability for Claude, marketing/social automation, DW
+   commerce, directory builders. Low: generic web-dev boilerplate, crypto,
+   things with no path into his workflow.
+2. **Novelty vs. what he already has (0–3)** — 3 = fills a real gap; 0 = he
+   already has an equal-or-better skill/agent. **Always run the dedupe check
+   below before scoring this.**
+3. **Leverage (0–3)** — does it save real hours or unlock revenue, or is it a
+   toy? Favor "measurable time back" over "feels productive" (Steve's own
+   theater-vs-real test).
+4. **Install/run cost (0–3, higher = cheaper)** — 3 = one paste / `npx`, local,
+   no new paid API; 0 = heavy infra, new metered API, or a rewrite to fit.
+   Note any per-call cost explicitly (always-show-costs rule).
+5. **Trust & license (0–3)** — stars/traction, **license present** (MIT/Apache
+   good; `NONE`/`NOASSERTION` is a flag), recently maintained (`days_since_push`
+   low, not archived), no obvious security smell. Unlicensed + unmaintained caps
+   this at 1.
+6. **Safety/gating exposure (0–3, higher = safer)** — 3 = read-only/local, no
+   secrets, no outbound writes; 0 = wants prod creds, sends email, touches DNS,
+   writes to customer-facing systems, or auto-executes on a schedule. Low scores
+   here don't kill a pick — they route it to pending-approval (see Gating).
+
+**Verdict bands:** ≥14 → **ADOPT** (draft the install memo). 9–13 → **WATCH**
+(log it, revisit if it gains traction). ≤8 → **SKIP** (one-line why).
+
+## Dedupe against Steve's existing kit (do this before scoring novelty)
+
+Steve already has 200+ skills and dozens of agents. Before recommending
+anything, check for an existing equivalent:
+
+```bash
+ls ~/.claude/skills/ | grep -i "<keyword>"
+grep -ril "<capability keyword>" ~/.claude/skills/*/SKILL.md 2>/dev/null
+```
+
+Also sanity-check the cabinet/agents and CNCP. Examples of "he already has it":
+cost tracking → `cost-tracker`; token-spend analysis → overlaps `token-use`;
+multi-agent orchestration → the cabinet + `Workflow`; research → `last30`,
+`deep-research`, `web-extract`; code review → `claude-codex`, `code-review`.
+If an equivalent exists, the useful output is a **diff** ("codeburn breaks spend
+down by tool+MCP, which `cost-tracker` doesn't"), not a redundant recommend.
+
+## Gating — what the scout may and may NOT do
+
+**Discovery, retrieval, enrichment, scoring, and writing the shortlist are
+reversible and run freely.** Everything that changes Steve's machine or spends is
+gated — draft it to `~/.claude/yolo-queue/pending-approval/` with an
+APPROVE/REVISE/BLOCK block; never auto-run:
+
+- Installing a skill/plugin (`git clone`, `/plugin install`, paste-into-`.claude`).
+- `npx`/running an unfamiliar tool that executes code or reads `~/.claude`.
+- Anything needing a new paid API key or that spends.
+- Anything the tool itself does that is customer-facing / destructive / outbound.
+
+The scout's deliverable is a **decision-ready shortlist**, not an installed tool.
+Adoption is Steve's call.
+
+## Output shape (per run)
+
+Produce a ranked table: `Tool | Repo (stars, license, last-push) | What it does |
+Category | Dedupe (have/gap) | Score | Verdict | 1-line reason`. Then, for each
+ADOPT, a short pending-approval memo: what it is, why it fits, exact install
+steps, cost, risks, and the APPROVE/REVISE/BLOCK line. Log genuine wins (a pick
+that gets adopted) — not the scan itself.
diff --git a/references/retrieval-playbook.md b/references/retrieval-playbook.md
new file mode 100644
index 0000000..0c4f0c6
--- /dev/null
+++ b/references/retrieval-playbook.md
@@ -0,0 +1,60 @@
+# Retrieval playbook — getting the picks past X's auth wall
+
+The whole difficulty of this skill is retrieval: `x.com` returns HTTP 402 to
+unauthenticated readers, the target account is intermittently suspended, and
+Thread Reader gates some threads behind login. The answer is the same as
+`feed-first-scraper`: **never depend on one source; fan out and take the first
+that yields the post text + repo link.**
+
+## Step order (stop as soon as you have text + repo)
+
+1. **Discover with Exa.** Run `mcp__exa__web_search_exa` with 2–3 query
+   variants, e.g.:
+   - `heygurisingh open sourced Claude Code skill "it's called"`
+   - `Guri Singh gurisaroy Claude Code tool GitHub`
+   - `heygurisingh someone built agent skill terminal`
+   Exa returns Thread Reader unrolls **and** LinkedIn (`gurisaroy`) mirrors that
+   already contain the full caption + repo link. This alone usually answers the
+   whole scan. (Metered — show/log the cost per your always-show-costs rule.)
+
+2. **Confirm/expand with `WebSearch`** (free). Same intent, different index —
+   good for catching tweet URLs/IDs and the tool name when Exa misses.
+
+3. **Pull the full body from a mirror** if the search highlight was truncated:
+   `mcp__exa__web_fetch_exa` on the LinkedIn `gurisaroy_...` URL or the
+   `threadreaderapp.com/thread/<id>.html` URL. Prefer the LinkedIn mirror — it
+   rarely login-walls and usually has the `lnkd.in` repo shortlink.
+
+4. **Hydrate a specific tweet** only if you have an id/URL and want the verbatim
+   X text: `node scripts/hydrate-tweet.js <id-or-url>`. Exit `3` = tombstone
+   (suspended/deleted) → the account is likely suspended; go back to mirrors,
+   don't report "nothing new".
+
+5. **Resolve `lnkd.in` / `t.co` shortlinks** to the real GitHub URL by fetching
+   the shortlink (WebFetch/Exa follows the redirect) or by reading the repo name
+   out of the post body and searching GitHub for it.
+
+## Turning a post into a candidate
+
+For each post that names an open-source tool:
+
+- Extract: **tool name**, **GitHub repo URL**, **one-line what-it-does**,
+  **category** (claude-skill | claude-plugin | CLI/TUI | MCP server | agent
+  framework | prompt-pack).
+- Enrich the repo: `node scripts/gh-repo-stats.js <repo-url-or-owner/repo>`.
+  This returns stars, license, `days_since_push`, archived flag, and
+  `is_claude_skill` (detects `SKILL.md` / `.claude/` / plugin manifest) plus
+  quick `flags` (unlicensed / stale / archived / low_traction). Set
+  `GITHUB_TOKEN` in env to avoid the 60/hr unauthenticated rate limit.
+
+## Failure modes to expect (and how to read them)
+
+- **All tweets tombstone** → account suspended, not "no content". Use mirrors.
+- **Exa `SOURCE_NOT_AVAILABLE` on an x.com URL** → expected; never fetch x.com
+  directly, go through a mirror.
+- **Thread Reader login wall on one thread** → try the LinkedIn mirror or the
+  Thread Reader *user* page instead of that specific thread.
+- **`lnkd.in` won't resolve** → grep the repo name from the post and hit
+  `api.github.com/search/repositories?q=<name>` (or just `gh-repo-stats.js
+  owner/repo` once you know the slug).
+- **GitHub 403** → unauthenticated rate limit; set `GITHUB_TOKEN` and retry.
diff --git a/references/sources.md b/references/sources.md
new file mode 100644
index 0000000..fe737d1
--- /dev/null
+++ b/references/sources.md
@@ -0,0 +1,61 @@
+# Sources — where to find Guri Singh's picks
+
+## Primary subject
+
+- **Guri Singh** — X handle `@heygurisingh`; real name **Gurinderjeet Singh**,
+  Calgary AB. Self-described "AI Educator & Writer". Cross-posts the same
+  content to LinkedIn as **`gurisaroy`**.
+- **What he posts (the signal this skill harvests):** a recurring format —
+  *"Holy shit… someone just open-sourced a [Claude Code skill / AI-agent tool]
+  that does X. It's called Y. Here's how it works ↓"* — followed by a GitHub
+  repo link. He also posts prompt packs and tool roundups; those are lower
+  priority for adoption but still worth logging.
+- **Confirmed past picks (for calibration):** `codeburn` (Claude Code
+  token-spend TUI), `career-ops` (AI job-search pipeline), Garry Tan's
+  `gstack`, `claude-code-best-practice`, "Understand Anything" (codebase
+  knowledge-graph plugin), `Graphify`, `Caveman` (output-filler-stripping
+  skill), `GEO-SEO`, `The Agency` (147-agent org), `AnySearch`.
+
+## ⚠️ The account gets suspended intermittently
+
+As of the last scout run, `@heygurisingh` was **suspended** — every live tweet
+returns a "suspended account" tombstone through X's syndication endpoint, and
+`x.com` itself 402s unauthenticated. **Do not conclude "no new picks" from an X
+failure.** The mirrors below persist through suspensions and are the *primary*
+retrieval path; treat direct X as a bonus for when the account is live.
+
+## Retrieval sources, in priority order
+
+1. **Exa search** (`mcp__exa__web_search_exa`) with queries like
+   `heygurisingh open sourced Claude Code skill` — returns Thread Reader unrolls
+   AND LinkedIn mirrors that contain the **full post text + repo link**. This is
+   the most reliable discovery path and survives X suspensions. It's metered —
+   show/log the cost.
+2. **LinkedIn mirror** — the `gurisaroy` posts (e.g.
+   `linkedin.com/posts/gurisaroy_...`). Exa surfaces these with the complete
+   caption and often the `lnkd.in/...` repo shortlink. Fetch via Exa, not direct
+   (LinkedIn walls direct reads).
+3. **Thread Reader App** — `threadreaderapp.com/user/heygurisingh` lists his
+   recent threads; individual unrolled threads at
+   `threadreaderapp.com/thread/<id>.html`. Public unrolls are Exa-fetchable;
+   note that *some* specific threads hit a login wall, so fall back to source 1.
+4. **`WebSearch`** (built-in) — `heygurisingh "Claude Code" open sourced` — good
+   for surfacing tweet URLs/IDs and the tool name even when the body is walled.
+5. **Syndication hydration** (`scripts/hydrate-tweet.js`) — ONLY works when the
+   account is live and the specific tweet exists. Given a tweet id/URL it prints
+   the verbatim text + any GitHub links, $0, no auth. Exits `3` on a tombstone.
+
+## Similar accounts worth folding into a wider sweep
+
+The skill's value is "surface newly open-sourced Claude Code / agent tooling
+worth adopting." Guri Singh is the anchor, but the same signal shows up from:
+
+- **GitHub trending** — `github.com/trending?since=weekly` filtered to repos
+  whose description mentions `claude code`, `skill`, `subagent`, `MCP`, `agent`.
+- **Curated lists** — `GetBindu/awesome-claude-code-and-skills`,
+  `ykdojo/claude-code-tips` / `Opfour/claude-code-tips` (the `dx` plugin).
+- Other X educators posting the same "someone open-sourced X" format (e.g.
+  Garry Tan's `gstack`). Add handles here as they prove useful.
+
+Keep this file current: when a new mirror/source proves reliable (or a handle
+stops being worth watching), edit it here so the next run inherits the lesson.
diff --git a/scripts/gh-repo-stats.js b/scripts/gh-repo-stats.js
new file mode 100755
index 0000000..3ca5006
--- /dev/null
+++ b/scripts/gh-repo-stats.js
@@ -0,0 +1,123 @@
+#!/usr/bin/env node
+/**
+ * gh-repo-stats.js — turn a GitHub repo link into hard adoption signals.
+ *
+ * Guri Singh's posts almost always point at an open-source repo. Before scoring
+ * whether it's worth adopting, resolve the objective facts: popularity, license,
+ * how recently it was touched, primary language, archived/stale status, and —
+ * critically for us — whether it is literally a Claude Code skill/plugin
+ * (SKILL.md, .claude/, plugin manifest) versus a general CLI/tool.
+ *
+ * Usage:
+ *   node gh-repo-stats.js <github-url-or-owner/repo>
+ *
+ * Auth: unauthenticated works (60 req/hr). Set GITHUB_TOKEN in env to raise the
+ * limit to 5000/hr — the script reads it automatically if present.
+ *
+ * Exit codes: 0 ok, 1 error (bad input / not found / network).
+ * Output: JSON summary on stdout.
+ */
+
+'use strict';
+
+function parseRepo(arg) {
+  if (!arg) return null;
+  const m = String(arg).match(/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)/);
+  if (m) return { owner: m[1], repo: m[2].replace(/\.git$/, '') };
+  const slug = String(arg).match(/^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/);
+  if (slug) return { owner: slug[1], repo: slug[2] };
+  return null;
+}
+
+function ghHeaders() {
+  const h = {
+    'User-Agent': 'gurisingh-scout',
+    Accept: 'application/vnd.github+json',
+  };
+  const tok = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
+  if (tok) h.Authorization = `Bearer ${tok}`;
+  return h;
+}
+
+async function gh(url) {
+  const res = await fetch(url, { headers: ghHeaders() });
+  if (res.status === 404) return { __status: 404 };
+  if (res.status === 403) return { __status: 403, __rate: res.headers.get('x-ratelimit-remaining') };
+  if (!res.ok) throw new Error(`http ${res.status} ${url}`);
+  return res.json();
+}
+
+function daysSince(iso) {
+  if (!iso) return null;
+  return Math.round((Date.now() - new Date(iso).getTime()) / 86400000);
+}
+
+async function main() {
+  const parsed = parseRepo(process.argv[2]);
+  if (!parsed) {
+    console.error('usage: node gh-repo-stats.js <github-url-or-owner/repo>');
+    process.exit(1);
+  }
+  const { owner, repo } = parsed;
+
+  let meta;
+  try {
+    meta = await gh(`https://api.github.com/repos/${owner}/${repo}`);
+  } catch (e) {
+    console.error(`error: ${e.message}`);
+    process.exit(1);
+  }
+  if (meta.__status === 404) {
+    console.error(`not found: ${owner}/${repo}`);
+    process.exit(1);
+  }
+  if (meta.__status === 403) {
+    console.error(`rate-limited (remaining=${meta.__rate}). Set GITHUB_TOKEN to raise the limit.`);
+    process.exit(1);
+  }
+
+  // Root listing → detect "is this a Claude skill/plugin?" (one extra call).
+  let isClaudeSkill = false;
+  let claudeMarkers = [];
+  try {
+    const contents = await gh(`https://api.github.com/repos/${owner}/${repo}/contents`);
+    if (Array.isArray(contents)) {
+      const names = contents.map((c) => c.name.toLowerCase());
+      const markers = ['skill.md', '.claude', 'skills', 'plugin.json', '.claude-plugin', 'commands', 'agents'];
+      claudeMarkers = markers.filter((m) => names.includes(m));
+      isClaudeSkill = names.includes('skill.md') || names.includes('.claude') ||
+        names.includes('.claude-plugin') ||
+        (names.includes('skills') && (names.includes('plugin.json') || names.includes('.claude-plugin')));
+    }
+  } catch {
+    /* non-fatal — leave detection empty */
+  }
+
+  const daysStale = daysSince(meta.pushed_at);
+  const out = {
+    repo: `${owner}/${repo}`,
+    url: meta.html_url,
+    description: meta.description,
+    stars: meta.stargazers_count,
+    forks: meta.forks_count,
+    open_issues: meta.open_issues_count,
+    language: meta.language,
+    license: meta.license ? meta.license.spdx_id : 'NONE',
+    pushed_at: meta.pushed_at,
+    days_since_push: daysStale,
+    archived: meta.archived,
+    is_claude_skill: isClaudeSkill,
+    claude_markers: claudeMarkers,
+    // quick, opinionated flags to feed the adoption rubric
+    flags: {
+      unlicensed: !meta.license,
+      stale: daysStale != null && daysStale > 120,
+      archived: !!meta.archived,
+      low_traction: meta.stargazers_count < 50,
+    },
+  };
+  console.log(JSON.stringify(out, null, 2));
+  process.exit(0);
+}
+
+main();
diff --git a/scripts/hydrate-tweet.js b/scripts/hydrate-tweet.js
new file mode 100755
index 0000000..f119c95
--- /dev/null
+++ b/scripts/hydrate-tweet.js
@@ -0,0 +1,132 @@
+#!/usr/bin/env node
+/**
+ * hydrate-tweet.js — resolve a single X/Twitter post to plain text WITHOUT auth.
+ *
+ * X blocks unauthenticated page reads (WebFetch → HTTP 402), so this uses the
+ * public syndication endpoint that powers embedded tweets. The endpoint needs a
+ * `token` that is a deterministic function of the tweet id — we compute it
+ * locally ($0), so no API key and no login are ever required.
+ *
+ * Usage:
+ *   node hydrate-tweet.js <tweet-id-or-url>
+ *
+ * Examples:
+ *   node hydrate-tweet.js 2055557527153332556
+ *   node hydrate-tweet.js https://x.com/heygurisingh/status/2055557527153332556
+ *
+ * Exit codes:
+ *   0  ok — printed tweet JSON summary
+ *   3  tombstone — tweet/account is suspended, deleted, or age-gated (unreadable)
+ *   1  error — bad input or network/parse failure
+ *
+ * Output: a JSON object on stdout:
+ *   { id, url, author, created_at, text, urls[], github[] }
+ * On tombstone: { id, tombstone: "<reason text>" } and exit 3.
+ */
+
+'use strict';
+
+function extractId(arg) {
+  if (!arg) return null;
+  // trailing run of digits handles both bare ids and .../status/<id>[?query]
+  const m = String(arg).match(/(\d{5,25})/g);
+  return m ? m[m.length - 1] : null;
+}
+
+// react-tweet's documented token derivation: (id / 1e15 * PI) in base-36,
+// with zeros and the dot stripped. Verified against live tweets 2026-07.
+function syndicationToken(id) {
+  return ((Number(id) / 1e15) * Math.PI)
+    .toString(36)
+    .replace(/(0+|\.)/g, '');
+}
+
+function findGithub(text, urls) {
+  const found = new Set();
+  const scan = (s) => {
+    if (!s) return;
+    const re = /https?:\/\/(?:www\.)?github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+/g;
+    let m;
+    while ((m = re.exec(s))) found.add(m[0].replace(/[.,)]+$/, ''));
+  };
+  scan(text);
+  (urls || []).forEach(scan);
+  return [...found];
+}
+
+async function main() {
+  const id = extractId(process.argv[2]);
+  if (!id) {
+    console.error('usage: node hydrate-tweet.js <tweet-id-or-url>');
+    process.exit(1);
+  }
+
+  const token = syndicationToken(id);
+  const endpoint =
+    `https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&token=${token}`;
+
+  let res;
+  try {
+    res = await fetch(endpoint, {
+      headers: {
+        // a browser-ish UA avoids the occasional bot 403 on this endpoint
+        'User-Agent':
+          'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
+          '(KHTML, like Gecko) Chrome/125.0 Safari/537.36',
+        Accept: 'application/json',
+      },
+    });
+  } catch (e) {
+    console.error(`network error: ${e.message}`);
+    process.exit(1);
+  }
+
+  if (res.status === 404) {
+    console.log(JSON.stringify({ id, tombstone: 'not found (404)' }, null, 2));
+    process.exit(3);
+  }
+  if (!res.ok) {
+    console.error(`http ${res.status} from syndication endpoint`);
+    process.exit(1);
+  }
+
+  let data;
+  try {
+    data = await res.json();
+  } catch (e) {
+    console.error(`parse error: ${e.message}`);
+    process.exit(1);
+  }
+
+  // Tombstone shapes: suspended/deleted/age-gated tweets come back as a
+  // __typename of TweetTombstone (or an object with a `tombstone` field).
+  const tomb =
+    data.__typename === 'TweetTombstone' ||
+    data.tombstone ||
+    (data.text && /suspended account|Post is from a suspended|isn.?t available/i.test(data.text) && !data.user);
+  if (tomb) {
+    const reason =
+      (data.tombstone && (data.tombstone.text?.text || data.tombstone.text)) ||
+      data.text ||
+      'unavailable (suspended / deleted / age-gated)';
+    console.log(JSON.stringify({ id, tombstone: reason }, null, 2));
+    process.exit(3);
+  }
+
+  const urls = (data.entities?.urls || []).map((u) => u.expanded_url || u.url);
+  const out = {
+    id,
+    url: `https://x.com/${data.user?.screen_name || 'i'}/status/${id}`,
+    author: data.user
+      ? `@${data.user.screen_name} (${data.user.name})`
+      : null,
+    created_at: data.created_at || null,
+    text: data.text || '',
+    urls,
+    github: findGithub(data.text || '', urls),
+  };
+  console.log(JSON.stringify(out, null, 2));
+  process.exit(0);
+}
+
+main();

(oldest)  ·  back to Gurisingh Scout  ·  (newest)