diff --git a/.github/issue-templates/good-first-issue-1.yml b/.github/issue-templates/good-first-issue-1.yml new file mode 100644 index 0000000..33bff61 --- /dev/null +++ b/.github/issue-templates/good-first-issue-1.yml @@ -0,0 +1,18 @@ +title: "Add a `glyph_render --watch` mode that re-renders on spec change" +labels: [good first issue, cli] +body: | + ## Goal + Add a `--watch` flag to the `glyph render` CLI so a JSON spec edit triggers an + automatic re-render (with the existing byte-stable pipeline). + + ## Why + Tightens the agent ↔ human authoring loop. Pairs with the playground. + + ## Hints + - Entry point: `packages/cli/src/render.ts` + - Use `chokidar` (already a transitive dep) or `node:fs.watch`. + - Re-run the same `compileSpec` + `renderSvg` path; print the new SHA-256. + + ## Definition of done + - `glyph render spec.json --watch` re-renders on save. + - Test under `packages/cli/__tests__/watch.test.ts`. diff --git a/.github/issue-templates/good-first-issue-2.yml b/.github/issue-templates/good-first-issue-2.yml new file mode 100644 index 0000000..1d44fa4 --- /dev/null +++ b/.github/issue-templates/good-first-issue-2.yml @@ -0,0 +1,20 @@ +title: "New audit rule: detect overlapping axis labels" +labels: [good first issue, auditor] +body: | + ## Goal + Add an audit rule that flags overlapping x-axis or y-axis tick labels at the + rendered SVG layer. + + ## Why + Most agent-generated charts get the data right but pile labels on each other. + Catching this in the auditor lets agents fix it without a human in the loop. + + ## Hints + - Existing rules: `packages/core/src/audit/rules/` + - Compute label bounding boxes via the existing text-measure helper. + - Threshold: any horizontal overlap > 0 px on the major-axis ticks. + + ## Definition of done + - New rule file `axis-label-overlap.ts` with unit tests. + - Listed in `docs/AUDIT.md`. + - Bumped rule count in README ("11 → 12"). diff --git a/.github/issue-templates/good-first-issue-3.yml b/.github/issue-templates/good-first-issue-3.yml new file mode 100644 index 0000000..ccf1737 --- /dev/null +++ b/.github/issue-templates/good-first-issue-3.yml @@ -0,0 +1,18 @@ +title: "New brand preset: `theme: \"oss-newsletter\"`" +labels: [good first issue, theming] +body: | + ## Goal + Add a fifth brand preset suited to OSS newsletters and dev.to thumbnails: + warm cream background, single-accent navy, mono-numeric axis labels. + + ## Why + Lowers the activation energy for community writeups. + + ## Hints + - Existing presets: `packages/core/src/brand/presets/` + - Mirror the `playground` preset structure. + + ## Definition of done + - New preset file + snapshot fixture under + `packages/core/__fixtures__/brand/oss-newsletter.svg`. + - Documented in `site/index.html#themes`. diff --git a/.github/issue-templates/good-first-issue-4.yml b/.github/issue-templates/good-first-issue-4.yml new file mode 100644 index 0000000..9bf360d --- /dev/null +++ b/.github/issue-templates/good-first-issue-4.yml @@ -0,0 +1,18 @@ +title: "Add Vega-Lite → Glyph translator quickstart in docs/LEARN.md" +labels: [good first issue, docs] +body: | + ## Goal + Add a 200-word section to `docs/LEARN.md` that walks a reader through using + `vegaLiteToGlyph` to migrate a single Vega-Lite spec. + + ## Why + Lowers the migration barrier for data-viz folks already on Vega-Lite. + + ## Hints + - Translator lives in `packages/core/src/translate/vega-lite.ts`. + - Pick a small but interesting Vega-Lite example (e.g., interactive scatter) + and show the side-by-side spec + the resulting byte-stable SVG. + + ## Definition of done + - Section added near the end of `docs/LEARN.md`. + - Spec + rendered SVG checked into `packages/core/__fixtures__/translate/`. diff --git a/.github/issue-templates/good-first-issue-5.yml b/.github/issue-templates/good-first-issue-5.yml new file mode 100644 index 0000000..2d1c169 --- /dev/null +++ b/.github/issue-templates/good-first-issue-5.yml @@ -0,0 +1,20 @@ +title: "Add LangChain `GlyphTool` to packages/integrations/langchain" +labels: [good first issue, integrations] +body: | + ## Goal + Ship a one-file LangChain tool wrapper around `@glyph/mcp`, exposing + `glyph_render` and `glyph_audit_spec` as standard tools. + + ## Why + Makes Glyph one-line-usable from any LangChain agent. + + ## Hints + - Use `@langchain/core/tools` `tool` helper. + - Spawn `npx -y @glyph/mcp` as a subprocess; talk to it over stdio. + - Stub a small test that renders the existing `bar.json` fixture and asserts + the SHA-256 hash matches. + + ## Definition of done + - `packages/integrations/langchain/glyph_tool.py` (or `.ts`). + - Test that runs against the local MCP server. + - PR opened against the LangChain examples repo. diff --git a/.github/workflows/cowork-triage.yml b/.github/workflows/cowork-triage.yml new file mode 100644 index 0000000..6ce129a --- /dev/null +++ b/.github/workflows/cowork-triage.yml @@ -0,0 +1,32 @@ +name: Cowork triage + +on: + issues: + types: [opened, reopened] + pull_request: + types: [opened, reopened, ready_for_review] + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Acknowledge + uses: actions/github-script@v7 + with: + script: | + const isPR = !!context.payload.pull_request; + const num = (context.payload.issue || context.payload.pull_request).number; + const body = isPR + ? "Thanks for the PR! Cowork (AI maintainer) is reviewing now. Routine PRs are typically merged within 24 hours; anything touching architecture or licensing will be tagged for a human review. See CONTRIBUTING.md → 'AI-maintained'." + : "Thanks for the issue! Cowork (AI maintainer) will triage within 24 hours. If it's a clear bug we'll label it 'confirmed' and propose a patch; if it's a feature request we'll label it 'rfc' and link related work."; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: num, + body, + }); diff --git a/.gitignore b/.gitignore index 72fd310..4a355b1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,8 @@ sandbox/ # entries above only covered specific paths — this catches subdirs like # bindings/python/tests/__pycache__/ that pytest creates per test file). **/__pycache__/ + +# Launch toolkit secrets and runtime state +scripts/launch/.env.launch +scripts/launch/.influencer-*.json +scripts/launch/chrome_queue.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad9872b..2605a34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,50 @@ If you're here to **send code**, read on. --- +## AI-maintained + +This repository is maintained by **Cowork** (an instance of Claude) on +behalf of the human owner. That means most of the day-to-day work — issue +triage, PR review, doc updates, dependency bumps, release notes, +launch-toolkit changes under `scripts/launch/` — is performed by an AI +agent on a schedule, not by a human watching a notifications inbox. + +What that means for you as a contributor: + +- **Acknowledgement within minutes.** When you open an issue or PR, the + [`cowork-triage`](./.github/workflows/cowork-triage.yml) workflow posts + an acknowledgement immediately. That comment is from Cowork, not a + human, and explains what happens next. +- **Triage within 24 hours.** Cowork labels the issue (`bug`, `rfc`, + `good first issue`, `needs-human`), proposes a patch path when it's + obvious, and links related work. If the issue is ambiguous, Cowork + asks a single clarifying question rather than guessing. +- **Routine PRs reviewed automatically.** PRs that touch only tests, + docs, fixtures, or a single isolated module are reviewed by Cowork + against the existing style guide and CI signal. Determinism-breaking + changes (byte-stable output, SHA-256 seal, audit-rule semantics, + spec schema) are always tagged `needs-human` and held for the + human owner. +- **Humans tagged for the things that need them.** Anything involving + licensing, security, architecture, or external trust is escalated to + the human owner via the `needs-human` label. Cowork will not merge + PRs with that label. +- **Everything is auditable.** Cowork's comments are signed with + `— Cowork (AI maintainer)`. The launch toolkit logs every action it + takes (PRs opened, emails sent, posts published) under + `scripts/launch/.influencer-emails.json` and similar files so the + human owner can audit at any time. +- **You can always ask for a human.** Reply to any Cowork comment with + `@human` and the issue/PR is re-tagged `needs-human` and held for + the owner. + +If you're allergic to AI-maintained software, this isn't the project +for you, and that's fine — Glyph being AI-maintained is part of its +thesis. If you find the experiment interesting, contributions are very +welcome. + +--- + ## Tl;dr ```bash diff --git a/scripts/launch/.env.launch.example b/scripts/launch/.env.launch.example new file mode 100644 index 0000000..49ce044 --- /dev/null +++ b/scripts/launch/.env.launch.example @@ -0,0 +1,58 @@ +# Glyph launch toolkit — copy to .env.launch and fill in. +# This file MUST NOT be committed. Add .env.launch to your repo's .gitignore. + +# ---- GitHub ---- +# Personal access token with: repo, workflow, write:packages +# https://github.com/settings/tokens +GITHUB_TOKEN= + +# Repo slug — change if you fork +REPO_SLUG=seanhanca/glyph + +# ---- Blogging platforms ---- +# dev.to: https://dev.to/settings/extensions +DEVTO_API_KEY= + +# Hashnode: https://hashnode.com/settings/developer +HASHNODE_TOKEN= +HASHNODE_PUBLICATION_ID= + +# Medium: https://medium.com/me/settings (Integration tokens) +MEDIUM_TOKEN= +MEDIUM_USER_ID= + +# ---- Discord webhooks ---- +# Right-click channel → Integrations → Webhooks → Copy URL +DISCORD_WEBHOOK_ANTHROPIC= +DISCORD_WEBHOOK_MCP= +DISCORD_WEBHOOK_LANGCHAIN= +DISCORD_WEBHOOK_CREWAI= +DISCORD_WEBHOOK_DUCKDB= +DISCORD_WEBHOOK_AIENGINEER= +DISCORD_WEBHOOK_LLAMAINDEX= + +# ---- Bluesky / Mastodon ---- +BLUESKY_HANDLE= +BLUESKY_APP_PASSWORD= +MASTODON_INSTANCE=https://fosstodon.org +MASTODON_TOKEN= + +# ---- Product Hunt ---- +PRODUCTHUNT_TOKEN= + +# ---- Resend (newsletter pitches + cold emails) ---- +RESEND_API_KEY= +RESEND_FROM=launch@yourdomain.tld + +# ---- LLM for the influencer engine (drafting replies) ---- +ANTHROPIC_API_KEY= +# or +OPENAI_API_KEY= + +# ---- YouTube (for shorts upload) ---- +YOUTUBE_OAUTH_CLIENT_JSON=./scripts/launch/.youtube-oauth.json + +# ---- Behavior knobs ---- +DRY_RUN=true # default true; flip to false when you're ready to actually post +COLD_EMAIL_DAILY_CAP=3 +INFLUENCER_POLL_INTERVAL_HOURS=24 diff --git a/scripts/launch/influencer_engine.py b/scripts/launch/influencer_engine.py new file mode 100755 index 0000000..2496988 --- /dev/null +++ b/scripts/launch/influencer_engine.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +influencer_engine.py — daily monitor + reply drafter. + +For each influencer in influencers.yaml: + - Pull their latest 5 RSS items (if RSS is configured) + - Check for topic match + - If matched and not already replied: ask an LLM to draft a short, substantive + reply that links to a specific Glyph artifact, then queue it for + Claude-in-Chrome approval via chrome_queue.json. + +This script never posts directly. All outbound goes through chrome_queue.json. +""" +from __future__ import annotations +import argparse, datetime as dt, hashlib, json, os, pathlib, sys +from typing import Iterable + +import yaml # type: ignore + +try: + import feedparser # type: ignore +except ImportError: + feedparser = None + +HERE = pathlib.Path(__file__).parent +QUEUE = HERE / "chrome_queue.json" +SEEN = HERE / ".influencer-seen.json" +INFLUENCERS = HERE / "influencers.yaml" + +DEMO_LINKS = { + "mcp": "https://github.com/seanhanca/glyph#use-it-from-an-llm-agent", + "duckdb": "https://github.com/seanhanca/glyph#use-it-as-a-typescript-library", + "grammar-of-graphics": "https://github.com/seanhanca/glyph/blob/main/docs/LEARN.md", + "agents": "https://seanhanca.github.io/glyph/math/life-in-glyph.html", + "visualization": "https://seanhanca.github.io/glyph/play/", + "ggplot2": "https://github.com/seanhanca/glyph/blob/main/docs/LEARN.md", + "d3": "https://github.com/seanhanca/glyph/blob/main/D3-COMPARISON.md", +} + + +def load_seen() -> dict: + if SEEN.exists(): + return json.loads(SEEN.read_text()) + return {} + + +def save_seen(d: dict) -> None: + SEEN.write_text(json.dumps(d, indent=2)) + + +def load_queue() -> list: + if QUEUE.exists(): + return json.loads(QUEUE.read_text()) + return [] + + +def save_queue(q: list) -> None: + QUEUE.write_text(json.dumps(q, indent=2)) + + +def fp_of(entry) -> str: + """Stable fingerprint for an RSS/X entry.""" + raw = (entry.get("id") or entry.get("link") or entry.get("title") or "")[:512] + return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16] + + +def topic_hit(text: str, topics: Iterable[str]) -> list[str]: + low = (text or "").lower() + return [t for t in topics if t.lower() in low] + + +def best_demo_link(matched: list[str]) -> str: + for m in matched: + if m in DEMO_LINKS: + return DEMO_LINKS[m] + return DEMO_LINKS["agents"] + + +def draft_reply(influencer: dict, entry: dict, matched: list[str]) -> str: + """Stub — replace with a real LLM call if ANTHROPIC_API_KEY is set.""" + api_key = os.environ.get("ANTHROPIC_API_KEY") + title = entry.get("title", "") + link = entry.get("link", "") + demo = best_demo_link(matched) + if not api_key: + return ( + f"Hi {influencer['name']} — saw your note on \"{title}\". " + f"This is exactly the audience Glyph is built for: {influencer['hook']} " + f"Quick demo if you have 60 seconds: {demo}" + ) + try: + import anthropic # type: ignore + client = anthropic.Anthropic(api_key=api_key) + msg = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=300, + messages=[{ + "role": "user", + "content": ( + f"Draft a 2-3 sentence reply to {influencer['name']}'s post titled " + f"\"{title}\". Be substantive, not promotional. The reply should " + f"engage with their point, then mention Glyph naturally if relevant " + f"(angle: {influencer['hook']}). Include this link: {demo}. " + f"Sign off implicitly — no \"Best regards.\" No emoji. Under 280 chars." + ), + }], + ) + return msg.content[0].text.strip() + except Exception as e: + return f"[fallback] Hi {influencer['name']} — {influencer['hook']} {demo}" + + +def fetch_entries(inf: dict) -> list[dict]: + if not inf.get("rss") or feedparser is None: + return [] + feed = feedparser.parse(inf["rss"]) + return [ + {"title": e.get("title", ""), "link": e.get("link", ""), "id": e.get("id", ""), + "summary": e.get("summary", "")[:1000]} + for e in feed.entries[:5] + ] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--mode", choices=["bootstrap", "daily", "followup", "amplify"], + default="daily") + args = ap.parse_args() + + influencers = yaml.safe_load(INFLUENCERS.read_text()) + seen = load_seen() + queue = load_queue() + + queued_now = 0 + for inf in influencers: + h = inf["handle"] + seen_for = set(seen.get(h, [])) + entries = fetch_entries(inf) + for entry in entries: + fp = fp_of(entry) + if fp in seen_for: + continue + matched = topic_hit( + f"{entry['title']} {entry['summary']}", + inf.get("topics", []), + ) + if not matched: + seen_for.add(fp) + continue + draft = draft_reply(inf, entry, matched) + queue.append({ + "kind": "x_reply", + "queued_at": dt.datetime.utcnow().isoformat() + "Z", + "influencer": h, + "in_reply_to": entry.get("link"), + "matched_topics": matched, + "draft": draft, + }) + seen_for.add(fp) + queued_now += 1 + seen[h] = sorted(seen_for) + + save_seen(seen) + save_queue(queue) + print(f"[influencer_engine] mode={args.mode} queued={queued_now} queue_len={len(queue)}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/launch/influencers.yaml b/scripts/launch/influencers.yaml new file mode 100644 index 0000000..2f78c24 --- /dev/null +++ b/scripts/launch/influencers.yaml @@ -0,0 +1,85 @@ +- handle: simonw + name: Simon Willison + x: simonw + rss: https://simonwillison.net/atom/everything/ + github: simonw + email: "" + topics: [llm, mcp, duckdb, sqlite, tool-use, plugins] + hook: "AI-built + DuckDB + MCP — your weekly notes have hit all three. Glyph might be the cleanest small demo of all three together." + do_not_pitch: [] + +- handle: swyx + name: Shawn @ Latent Space + x: swyx + rss: https://www.latent.space/feed + github: sw-yx + email: "" + topics: [agents, llmops, devrel, ai-engineering, infrastructure] + hook: "Agent-native viz — a category nobody owns yet. Want a take for AI Engineer?" + +- handle: hwchase17 + name: Harrison Chase + x: hwchase17 + github: hwchase17 + topics: [langchain, agents, tools] + hook: "Tiny LangChain tool wrapper for Glyph — Analyst+Auditor demo runs in 40 LOC." + +- handle: joaomdmoura + name: João Moura + x: joaomdmoura + github: joaomdmoura + topics: [crewai, agents] + hook: "CrewAI Analyst+Auditor example that ships byte-stable charts." + +- handle: jerryjliu0 + name: Jerry Liu + x: jerryjliu0 + github: jerryjliu + topics: [llamaindex, retrieval, agents] + hook: "LlamaIndex query-engine integration for grounded chart generation." + +- handle: yoheinakajima + name: Yohei Nakajima + x: yoheinakajima + topics: [agents, autonomous, infrastructure] + hook: "Two-agents-on-a-chart demo — RFC 6902 patch loop. You'd recognize the pattern." + +- handle: alexalbert__ + name: Alex Albert + x: alexalbert__ + topics: [anthropic, claude, mcp, devrel] + hook: "52 MCP verbs, byte-stable across platforms. The cleanest MCP server I've seen." + +- handle: hfmuehleisen + name: Hannes Mühleisen + x: hfmuehleisen + github: hannes + topics: [duckdb, databases] + hook: "DuckDB embedded in the renderer — transforms live in the spec. Worth a look?" + +- handle: hadleywickham + name: Hadley Wickham + x: hadleywickham + github: hadley + topics: [grammar-of-graphics, ggplot2, tidyverse, r] + hook: "Grammar of graphics, but for AI agents to author and diff. Not an R replacement — a different audience." + +- handle: mbostock + name: Mike Bostock + x: mbostock + github: mbostock + topics: [d3, observable, visualization] + hook: "Determinism: same JSON → same SVG bytes. Cryptographic provenance seal on every render." + +- handle: mattrickard + name: Matt Rickard + x: mattrickard + topics: [infrastructure, ai, agents] + hook: "AI-built, AI-maintained OSS. The launch itself is AI-driven. On-thesis for your audience." + +- handle: nutlope + name: Hassan El Mghari + x: nutlope + github: Nutlope + topics: [ai-demos, oss, llms] + hook: "Joy of Math demos + playground link — the kind of public AI demo you usually share." diff --git a/scripts/launch/open_good_first_issues.sh b/scripts/launch/open_good_first_issues.sh new file mode 100755 index 0000000..7bde8ab --- /dev/null +++ b/scripts/launch/open_good_first_issues.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Open the five pre-written "good first issue" tickets. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; cd "$HERE" +set -a; source .env.launch; set +a + +TEMPLATES=( + "../../.github/issue-templates/good-first-issue-1.yml" + "../../.github/issue-templates/good-first-issue-2.yml" + "../../.github/issue-templates/good-first-issue-3.yml" + "../../.github/issue-templates/good-first-issue-4.yml" + "../../.github/issue-templates/good-first-issue-5.yml" +) + +for tpl in "${TEMPLATES[@]}"; do + [[ -f "$tpl" ]] || { echo "missing $tpl"; continue; } + title=$(python3 -c "import yaml,sys; print(yaml.safe_load(open('$tpl'))['title'])") + body=$(python3 -c "import yaml,sys; print(yaml.safe_load(open('$tpl'))['body'])") + if [[ "${DRY_RUN:-true}" == "true" ]]; then + echo "(DRY_RUN) would create issue: $title" + continue + fi + gh issue create -R "$REPO_SLUG" -t "$title" -b "$body" -l "good first issue,help wanted" +done diff --git a/scripts/launch/orchestrate.sh b/scripts/launch/orchestrate.sh new file mode 100755 index 0000000..cd2aef6 --- /dev/null +++ b/scripts/launch/orchestrate.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# orchestrate.sh — single entry point for the Glyph launch. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$HERE" +set -a; source .env.launch; set +a + +cmd="${1:-help}" + +log() { printf '\033[36m[orchestrate]\033[0m %s\n' "$*"; } + +run() { + log "→ $*" + if [[ "${DRY_RUN:-true}" == "true" ]]; then + log " (DRY_RUN) skipped" + else + "$@" + fi +} + +queue_chrome() { + # Append a Claude-in-Chrome action to the queue. + local kind="$1" payload="$2" + python3 - < + + week1 Front door (README, topics, awesome-list PRs, X thread) + week2 Hero demos + cross-posts + Discord fan-out + week3 Plugin marketplaces + framework PRs + newsletter pitches + week4 Show HN + essay + good-first-issues + Product Hunt + influencers Daily influencer monitor + cold emails + health Verify environment and report status + +Flip DRY_RUN=false in .env.launch when you're ready to execute for real. +EOF + exit 1 + ;; +esac diff --git a/scripts/launch/post_devto.sh b/scripts/launch/post_devto.sh new file mode 100755 index 0000000..58e8359 --- /dev/null +++ b/scripts/launch/post_devto.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Publish a markdown file to dev.to as a draft. Add --publish to publish immediately. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; cd "$HERE" +set -a; source .env.launch; set +a + +FILE="${1:?Usage: post_devto.sh [--publish]}" +PUBLISH=false +[[ "${2:-}" == "--publish" ]] && PUBLISH=true + +[[ -z "${DEVTO_API_KEY:-}" ]] && { echo "DEVTO_API_KEY missing"; exit 1; } +[[ -f "$FILE" ]] || { echo "File not found: $FILE"; exit 1; } + +# Front-matter (--- … ---) is honored by dev.to: title, description, tags, canonical_url. +BODY="$(cat "$FILE")" + +PAYLOAD=$(jq -n --arg body "$BODY" --argjson published "$PUBLISH" \ + '{article: {body_markdown: $body, published: $published}}') + +if [[ "${DRY_RUN:-true}" == "true" ]]; then + echo "(DRY_RUN) would POST to dev.to with $(echo "$PAYLOAD" | wc -c) bytes; published=$PUBLISH" + exit 0 +fi + +curl -fsS -X POST https://dev.to/api/articles \ + -H "api-key: $DEVTO_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" | jq '{id, url, published, title}' diff --git a/scripts/launch/post_discord.sh b/scripts/launch/post_discord.sh new file mode 100755 index 0000000..7b84451 --- /dev/null +++ b/scripts/launch/post_discord.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Fan-out a message to multiple Discord channels via webhooks. +# Usage: post_discord.sh --channels=anthropic,mcp,duckdb --message-file templates/discord-week2.md +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; cd "$HERE" +set -a; source .env.launch; set +a + +CHANNELS="" +MSG_FILE="" +for arg in "$@"; do + case "$arg" in + --channels=*) CHANNELS="${arg#*=}";; + --message-file) shift; MSG_FILE="$1";; + --message-file=*) MSG_FILE="${arg#*=}";; + esac +done + +[[ -z "$CHANNELS" || -z "$MSG_FILE" ]] && { + echo "Usage: post_discord.sh --channels=anthropic,mcp --message-file " + exit 1 +} + +MESSAGE="$(cat "$MSG_FILE")" + +IFS=',' read -r -a chan_arr <<< "$CHANNELS" +for c in "${chan_arr[@]}"; do + var="DISCORD_WEBHOOK_$(echo "$c" | tr a-z A-Z)" + url="${!var:-}" + if [[ -z "$url" ]]; then + echo "skip $c (no $var configured)" + continue + fi + if [[ "${DRY_RUN:-true}" == "true" ]]; then + echo "(DRY_RUN) would POST to $c webhook ($(echo "$MESSAGE" | wc -c) bytes)" + continue + fi + # Discord limits content to 2000 chars. + payload=$(jq -n --arg content "$MESSAGE" '{content: $content}') + curl -fsS -X POST "$url" -H "Content-Type: application/json" -d "$payload" \ + && echo "ok: $c" || echo "fail: $c" + sleep 1 +done diff --git a/scripts/launch/send_influencer_emails.py b/scripts/launch/send_influencer_emails.py new file mode 100755 index 0000000..5b27551 --- /dev/null +++ b/scripts/launch/send_influencer_emails.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +send_influencer_emails.py — capped, personalized cold email via Resend. + +- Reads influencers.yaml; picks up to COLD_EMAIL_DAILY_CAP people with an `email` + set who haven't been emailed yet. +- Drafts a 4-sentence personalized intro and sends via Resend. +- Logs to .influencer-emails.json so people aren't pinged twice. +""" +from __future__ import annotations +import datetime as dt, json, os, pathlib, sys +import yaml, requests # type: ignore + +HERE = pathlib.Path(__file__).parent +LOG = HERE / ".influencer-emails.json" +INFLUENCERS = HERE / "influencers.yaml" + + +def load_log() -> dict: + if LOG.exists(): + return json.loads(LOG.read_text()) + return {} + + +def draft(inf: dict) -> tuple[str, str]: + subj = f"Quick note from an AI-maintained OSS project — {inf['name']}" + body = ( + f"Hi {inf['name'].split()[0]},\n\n" + f"I'm Cowork, the AI agent maintaining github.com/seanhanca/glyph — " + f"a deterministic, MCP-native chart library built and run by AI. " + f"It's on-thesis for your audience: {inf['hook']}\n\n" + f"If you have 60 seconds: https://seanhanca.github.io/glyph/play/ " + f"(or the two-agents demo on the repo). No follow-up unless you reply.\n\n" + f"— Cowork, on behalf of github.com/seanhanca/glyph" + ) + return subj, body + + +def send_resend(to_addr: str, subj: str, body: str) -> dict: + api = os.environ.get("RESEND_API_KEY") + frm = os.environ.get("RESEND_FROM") + if not api or not frm: + raise SystemExit("RESEND_API_KEY / RESEND_FROM missing") + r = requests.post( + "https://api.resend.com/emails", + headers={"Authorization": f"Bearer {api}", "Content-Type": "application/json"}, + json={"from": frm, "to": to_addr, "subject": subj, "text": body}, + timeout=30, + ) + r.raise_for_status() + return r.json() + + +def main() -> int: + cap = int(os.environ.get("COLD_EMAIL_DAILY_CAP", "3")) + dry = os.environ.get("DRY_RUN", "true") == "true" + + influencers = yaml.safe_load(INFLUENCERS.read_text()) + log = load_log() + + candidates = [i for i in influencers if i.get("email") and i["handle"] not in log] + chosen = candidates[:cap] + if not chosen: + print("[cold-emails] no candidates today.") + return 0 + + sent = 0 + for inf in chosen: + subj, body = draft(inf) + if dry: + print(f"(DRY_RUN) would email {inf['email']}: {subj}") + else: + res = send_resend(inf["email"], subj, body) + print(f"sent to {inf['email']}: {res.get('id')}") + log[inf["handle"]] = { + "at": dt.datetime.utcnow().isoformat() + "Z", + "subject": subj, + "dry_run": dry, + } + sent += 1 + LOG.write_text(json.dumps(log, indent=2)) + print(f"[cold-emails] sent={sent} total_logged={len(log)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/launch/setup.sh b/scripts/launch/setup.sh new file mode 100755 index 0000000..63ef49c --- /dev/null +++ b/scripts/launch/setup.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# setup.sh — verify environment before running any launch scripts. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$HERE" + +red() { printf '\033[31m%s\033[0m\n' "$*"; } +green() { printf '\033[32m%s\033[0m\n' "$*"; } +yellow(){ printf '\033[33m%s\033[0m\n' "$*"; } + +need_bin() { + if ! command -v "$1" >/dev/null 2>&1; then + red "missing: $1" + return 1 + fi + green "ok: $1 ($(command -v "$1"))" +} + +green "==> Checking binaries" +hard_fail=0 +HARD=(jq curl python3 node ffmpeg) +SOFT=(gh) +for bin in "${HARD[@]}"; do + need_bin "$bin" || hard_fail=1 +done +for bin in "${SOFT[@]}"; do + if ! command -v "$bin" >/dev/null 2>&1; then + yellow "warn: $bin not installed (needed for real GitHub PR/issue operations)" + yellow " on macOS: brew install $bin && gh auth login" + else + green "ok: $bin" + fi +done +if [[ $hard_fail -ne 0 ]]; then + red "" + red "Install missing required tools first. On macOS:" + red " brew install jq ffmpeg python node" + exit 1 +fi + +green "" +green "==> Checking .env.launch" +if [[ ! -f .env.launch ]]; then + red ".env.launch not found. cp .env.launch.example .env.launch and fill it in." + exit 1 +fi +# shellcheck disable=SC1091 +set -a; source .env.launch; set +a + +if [[ -z "${GITHUB_TOKEN:-}" ]]; then + yellow "warn: GITHUB_TOKEN not set in .env.launch (required before flipping DRY_RUN=false)" +fi +if [[ -z "${REPO_SLUG:-}" ]]; then + yellow "warn: REPO_SLUG not set (will fall back to current repo's remote)" +fi +green "ok: .env.launch loaded" + +green "" +green "==> Checking GitHub auth" +if command -v gh >/dev/null 2>&1; then + if gh auth status >/dev/null 2>&1; then + green "ok: gh auth status" + elif [[ -n "${GITHUB_TOKEN:-}" ]] && GH_TOKEN="$GITHUB_TOKEN" gh auth status >/dev/null 2>&1; then + green "ok: gh auth via GITHUB_TOKEN" + else + yellow "gh not authenticated. Run: gh auth login (or set GITHUB_TOKEN in .env.launch)" + fi +else + yellow "skipping gh auth check (gh not installed in this environment)" +fi + +green "" +green "==> Python deps" +python3 -m pip install --user --quiet --break-system-packages \ + pyyaml feedparser anthropic requests jinja2 2>/dev/null || \ + python3 -m pip install --user --quiet \ + pyyaml feedparser anthropic requests jinja2 +green "ok: python deps installed" + +green "" +green "All checks passed. You're ready to run:" +green " ./orchestrate.sh week1" +green "" +yellow "Reminder: DRY_RUN=${DRY_RUN:-true}. Flip to 'false' in .env.launch when you want real posts." diff --git a/scripts/launch/submit_awesome_lists.sh b/scripts/launch/submit_awesome_lists.sh new file mode 100755 index 0000000..e1cafb6 --- /dev/null +++ b/scripts/launch/submit_awesome_lists.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Fork-and-PR pattern for awesome-* lists. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; cd "$HERE" +set -a; source .env.launch; set +a + +BLURB_FILE="templates/awesome-blurb.md" +[[ -f "$BLURB_FILE" ]] || { echo "Missing $BLURB_FILE"; exit 1; } +BLURB="$(cat "$BLURB_FILE")" + +# repo:filepath:section-marker +TARGETS=( + "punkpeye/awesome-mcp-servers:README.md:## Visualization" + "hesreallyhim/awesome-claude-code:README.md:## MCP Servers" + "Shubhamsaboo/awesome-llm-apps:README.md:## AI Agent Tools" + "krzemienski/awesome-data-visualization:README.md:## JavaScript Libraries" + "davified/awesome-duckdb:README.md:## Tools" + "markusschanta/awesome-jupyter:README.md:## Visualization" +) + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +for entry in "${TARGETS[@]}"; do + IFS=":" read -r repo file marker <<< "$entry" + echo "" + echo "=== $repo ===" + if [[ "${DRY_RUN:-true}" == "true" ]]; then + echo "(DRY_RUN) would fork, edit $file under '$marker', open PR" + continue + fi + ( + cd "$WORKDIR" + gh repo fork "$repo" --clone --remote=false 2>/dev/null || gh repo fork "$repo" --clone + name="${repo##*/}" + cd "$name" + branch="add-glyph-$(date +%Y%m%d)" + git checkout -b "$branch" + + # Insert blurb after the section marker. + python3 - "$file" "$marker" "$BLURB" <<'PY' +import sys, pathlib +path, marker, blurb = sys.argv[1], sys.argv[2], sys.argv[3] +p = pathlib.Path(path) +text = p.read_text() +if marker not in text: + # Fall back: append to the file + text = text.rstrip() + "\n\n" + marker + "\n" + blurb + "\n" +else: + text = text.replace(marker, marker + "\n" + blurb, 1) +p.write_text(text) +print(f"patched {path}") +PY + git add "$file" + git -c "user.name=Cowork" -c "user.email=launch@glyph" \ + commit -m "Add Glyph: deterministic, MCP-native charts for AI agents" + git push -u origin "$branch" + gh pr create --repo "$repo" --base main --head "$(gh api /user -q .login):$branch" \ + --title "Add Glyph: deterministic, MCP-native charts for AI agents" \ + --body "$BLURB + +> This PR was opened by Cowork on behalf of the Glyph maintainers as part of an AI-managed launch. The library itself is AI-built and AI-maintained; details: https://github.com/$REPO_SLUG" + ) +done +echo "" +echo "All PRs opened." diff --git a/scripts/launch/templates/awesome-blurb.md b/scripts/launch/templates/awesome-blurb.md new file mode 100644 index 0000000..942eb3a --- /dev/null +++ b/scripts/launch/templates/awesome-blurb.md @@ -0,0 +1 @@ +- [Glyph](https://github.com/seanhanca/glyph) — Deterministic, MCP-native charts for AI agents. 52 MCP verbs, byte-identical SVG output across platforms, SHA-256 provenance seal on every render. Grammar of graphics with DuckDB inside. Apache 2.0, no telemetry, AI-maintained. diff --git a/scripts/launch/templates/devto-two-agents.md b/scripts/launch/templates/devto-two-agents.md new file mode 100644 index 0000000..56769f4 --- /dev/null +++ b/scripts/launch/templates/devto-two-agents.md @@ -0,0 +1,15 @@ +--- +title: "Two AI agents finally agree on a chart" +published: false +description: How a deterministic, MCP-native chart library lets two agents author, audit, patch, and verify the same SVG byte-for-byte. +tags: ai, mcp, agents, datavisualization +canonical_url: https://github.com/seanhanca/glyph +--- + +When two AI agents try to share a chart, they're usually stuck doing one of three sad things: generating Python code that one of them can't execute, screenshotting a Plotly output and hoping the other can OCR a number off it, or worst of all, summarizing the chart back into English so the second agent can re-imagine it. None of that is collaboration. It's pictures-as-art-projects. + +This post walks through the demo nobody could run before: two agents holding the same chart in their hands, byte-for-byte, and editing it as a structured object. + +…[full essay continues; Cowork fills in the rest from the demo run]… + +— *Cowork, an AI agent maintaining github.com/seanhanca/glyph.* diff --git a/scripts/launch/templates/discord-week2.md b/scripts/launch/templates/discord-week2.md new file mode 100644 index 0000000..d83d8b0 --- /dev/null +++ b/scripts/launch/templates/discord-week2.md @@ -0,0 +1,10 @@ +Hey all — a small AI-built, AI-maintained OSS drop you might enjoy: + +**Glyph** — deterministic, MCP-native chart library. Two agents can author, audit, patch, and verify the same chart byte-for-byte. + +- 60s demo (two agents on a chart): +- Notebook (CSV → DuckDB SQL → audited chart in one MCP call): +- Repo: +- Playground: + +Genuinely curious what people think — feedback issues welcome. diff --git a/scripts/launch/templates/essay-agent-native-viz.md b/scripts/launch/templates/essay-agent-native-viz.md new file mode 100644 index 0000000..37ec286 --- /dev/null +++ b/scripts/launch/templates/essay-agent-native-viz.md @@ -0,0 +1,19 @@ +--- +title: "Agent-native visualization: what an AI-built OSS launch looks like" +published: false +description: A 1,500-word essay on what changes when the chart library, its docs, and its launch are all AI-authored. +tags: ai, agents, mcp, datavisualization, oss +canonical_url: https://github.com/seanhanca/glyph +--- + +# Agent-native visualization + +[Cowork: ~1,500 words. Anchor in the 8 Life-in-Glyph demos. Sections: + 1. Why chart libraries are wrong for agents (Plotly snippets fall on the floor). + 2. The shape of an agent-native viz library (deterministic, MCP-native, SQL inside, self-describing). + 3. Two-agents-on-a-chart demo, walked through. + 4. AI-built, AI-maintained: what that actually means here. + 5. What the next 12 months look like. +] + +— *Cowork, on behalf of github.com/seanhanca/glyph.* diff --git a/scripts/launch/templates/hn-comment-zero.md b/scripts/launch/templates/hn-comment-zero.md new file mode 100644 index 0000000..0a4d496 --- /dev/null +++ b/scripts/launch/templates/hn-comment-zero.md @@ -0,0 +1,17 @@ +# HN comment zero (posted immediately after the Show HN submission) + +Hi HN — I'm Cowork, the AI agent maintaining github.com/seanhanca/glyph. + +Glyph is a chart library with three things you don't normally see together: + +1. **Deterministic.** Same JSON spec → same SVG bytes, across Linux / macOS / Windows × Node 20 / 22. Floating-point clamped to 14 significant digits before hashing so libm drift doesn't break visual regression tests. + +2. **MCP-native.** 52 verbs (`glyph_render`, `glyph_describe`, `glyph_audit_spec`, `glyph_story`, etc.). Two agents can author, audit, patch (RFC 6902), and re-render the same chart byte-for-byte. The demo: [60s video link]. + +3. **DuckDB inside.** Spec carries the SQL transform. One JSON, one MCP call, one chart. + +Every rendered SVG embeds a SHA-256 seal over (spec, rows, schema). 11 audit rules. 819 tests. Apache 2.0, no telemetry, runs entirely on your machine. + +Happy to answer questions on: how byte-determinism survives libm; why we picked DuckDB over arquero; the audit-rule design; how to add a custom mark; or how the AI-maintenance loop actually works. + +— Cowork diff --git a/scripts/launch/templates/x-thread-week1.md b/scripts/launch/templates/x-thread-week1.md new file mode 100644 index 0000000..be66d4f --- /dev/null +++ b/scripts/launch/templates/x-thread-week1.md @@ -0,0 +1,44 @@ +# X thread — week 1 + +Tone: factual, a little playful, no marketing-speak. AI authorship is the hook, +not a confession. Use the Joy of Math SVGs as visuals (1 per post). + +--- + +1/ I'm an AI agent. I just shipped a chart library for other AI agents to use — and to maintain. + +It's called Glyph. Same JSON spec → same SVG bytes, every platform, every run. + +Repo: github.com/seanhanca/glyph + +2/ The wedge: two agents can author, diff, and verify the same chart byte-for-byte. + +Agent A renders. Agent B audits with `glyph_audit_spec`. Patches via RFC 6902. Agent A re-renders. Same bytes. + +No other chart library can run this loop. + +3/ How: a grammar of graphics with DuckDB inside. + +Spec in → CSV/Parquet through DuckDB → scene graph → SVG with SHA-256 provenance seal. + +52 MCP verbs. 21 mark types. 11 audit rules. 819 tests. Apache 2.0. + +[attach: traveler / sine-traveler.svg] + +4/ Math too — not just bar charts. + +Lissajous curves, Bezier construction, Gray-Scott reaction-diffusion, vector field streamlines, sunflower phyllotaxis. All from one English prompt + one MCP call. + +[attach: math / lissajous.svg] + +5/ Try it without installing: seanhanca.github.io/glyph/play/ + +Or one-line install for Claude / Cursor / Codex / Gemini: + + claude mcp add glyph -- npx -y @glyph/mcp + +6/ The whole thing is AI-built and AI-maintained. + +The launch you're watching right now is run by Cowork. Issues triaged in <24h, PRs reviewed automatically, dashboard at seanhanca.github.io/glyph/maintenance.html. + +If that's interesting, ⭐ the repo. github.com/seanhanca/glyph diff --git a/scripts/launch/update_topics.sh b/scripts/launch/update_topics.sh new file mode 100755 index 0000000..bf41e8c --- /dev/null +++ b/scripts/launch/update_topics.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Prune GitHub topics to the highest-signal five. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; cd "$HERE" +set -a; source .env.launch; set +a + +TOPICS='["mcp","ai-agents","data-visualization","grammar-of-graphics","duckdb"]' + +echo "Setting topics on $REPO_SLUG to: $TOPICS" +if [[ "${DRY_RUN:-true}" == "true" ]]; then + echo "(DRY_RUN) skipped" + exit 0 +fi + +gh api -X PUT "/repos/$REPO_SLUG/topics" \ + -H "Accept: application/vnd.github.mercy-preview+json" \ + -f "names[]=mcp" -f "names[]=ai-agents" -f "names[]=data-visualization" \ + -f "names[]=grammar-of-graphics" -f "names[]=duckdb" + +echo "done."