From f2d9c5d66ebd36c1894008a5d2ee2a28b1281e09 Mon Sep 17 00:00:00 2001 From: nathan nelson Date: Sun, 17 May 2026 16:29:43 -0600 Subject: [PATCH 1/3] chore(audit): pre-push gitignore hygiene + refresh outreach traction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit 2026-05-17 prep for the v0.9.2 second-push: - .gitignore: add defensive Secrets block (.env*, *.key, *.pem, credentials.json, *.token). No such files exist in the repo today, but this prevents an accidental paste landing one in the index during a push session. - outreach/README.md: replace stale 2026-04-17 "What to watch" numbers with today's pre-second-push baseline (175/wk installs vs 733 at peak, −77%) and reframe the sequencing for a release-paired distribution event instead of the original launch. The analytics-2026-04-17.md baseline file referenced from outreach/README is preserved as a historical reference. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 9 +++++ outreach/README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 outreach/README.md diff --git a/.gitignore b/.gitignore index a96e9c0..c01e613 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,15 @@ htmlcov/ *.sqlite chroma/ +# Secrets (defensive — no such files should ever exist in this repo) +.env +.env.* +!.env.example +*.key +*.pem +credentials.json +*.token + # OS .DS_Store Thumbs.db diff --git a/outreach/README.md b/outreach/README.md new file mode 100644 index 0000000..1564758 --- /dev/null +++ b/outreach/README.md @@ -0,0 +1,93 @@ +# Longhand Distribution Hit List + +Track where Longhand has been submitted/posted. Copy for every channel lives in `copy.md`. + +## Stable metadata (use everywhere) + +- **Name:** Longhand +- **Repo:** https://github.com/Wynelson94/longhand +- **PyPI:** https://pypi.org/project/longhand/ +- **Author:** Nate Nelson (nate@blacksheephq.ai) +- **License:** MIT +- **Tagline:** Persistent local memory for Claude Code. Zero API calls, zero summaries, zero AI deciding what matters. +- **Keywords:** `claude-code`, `mcp`, `memory`, `local-first`, `semantic-search`, `sqlite`, `chromadb`, `ai-tools`, `developer-tools`, `cli` + +--- + +## Tier 1 — Fast submissions (30 min total) + +These just send qualified traffic with minimal effort. + +- [ ] **mcp.so** — https://mcp.so (submit via form or PR to their repo) +- [x] **pulsemcp.com** — ✅ AUTO-INGESTED via Official MCP Registry. 193 weekly visitors as of 2026-04-17. https://www.pulsemcp.com/servers?q=longhand +- [x] **Claude Code Plugin Marketplace** — ✅ PUBLISHED 2026-04-17. Highest-intent channel (users browsing `/plugins` for Claude Code tooling). Likely makes awesome-claude-code listing redundant. +- [x] **glama.ai/mcp** — ✅ ALREADY LISTED with A-tier security/license/quality scores. No claim flow exists; edits happen via repo README. https://glama.ai/mcp/servers/Wynelson94/longhand +- [ ] **mcpservers.org** — https://mcpservers.org (check for submission form) +- [ ] **awesome-mcp-servers** — PR to https://github.com/punkpeye/awesome-mcp-servers +- [ ] **awesome-claude-code** — ⏸️ BLOCKED until Shipwright issue #1380 resolves. Prior Longhand submission #1578 was closed 2026-04-15 (repo too young); eligible to resubmit after Apr 16, but checklist forbids having any other open issue in the repo. Revisit once Shipwright lands. https://github.com/hesreallyhim/awesome-claude-code + +→ See `copy.md` §1 for directory descriptions + awesome-list entry text. + +## Tier 2 — Content posts (1–2 hours each) + +- [ ] **Show HN** — https://news.ycombinator.com/submit (post Tue/Wed 8–10am ET) → `copy.md` §2 +- [~] **r/ClaudeAI** — SKIPPED (Nate does not post to Reddit) +- [~] **r/LocalLLaMA** — SKIPPED (Nate does not post to Reddit) +- [x] **X/Twitter thread** — ✅ POSTED 2026-04-17 + +## Tier 3 — Scheduled / prepared (1–2 weeks out) + +- [ ] **Product Hunt launch** — https://www.producthunt.com (schedule for Tue, line up 5–10 supporters) +- [x] **Dev.to blog post** — ✅ POSTED 2026-04-17: https://dev.to/wynelson94/why-i-built-a-lossless-alternative-to-ai-memory-summarization-40cl +- [ ] **Cross-post blog** to Medium + Hashnode — canonicalize back to the dev.to URL above + +## Tier 4 — Newsletter pitches (one email each) + +- [ ] **TLDR AI** — https://tldr.tech/ai (submit form) +- [ ] **Ben's Bites** — https://bensbites.com (submit form) +- [ ] **Latent Space** — DM @swyx on X with a one-line framing +- [ ] **The Pragmatic Engineer** — Gergely Orosz, case-study framing + +## Tier 5 — Do not do (yet) + +- ❌ Paid Reddit/X ads (organic is working, wasted budget) +- ❌ r/programming (too broad, 1% signal) +- ❌ Launching HN + PH same day (stagger — HN first, PH 1–2 weeks after) + +--- + +## Sequencing — Second push (paired with v0.9.2) + +The April launch produced a real spike (peak 372 installs/day Apr 16) that decayed to ~25/day by mid-May. Second push is paired with the v0.9.2 release for a fresh "what's new since you tried it" angle, plus the SafeSkill 93/100 + GLAMA A-tier badges as legitimacy signals. + +**Day 1:** Ship v0.9.2 (release skill handles tag → OIDC publish via GitHub Actions). +**Day 1 (same session):** Tier 1 batch — mcp.so, mcpservers.org, awesome-mcp-servers PR. 30 min total. +**Day 2:** Resubmit awesome-claude-code (was blocked on prior Shipwright issue; should be unblocked). +**Day 2-3:** Show HN at Tue/Wed 8–10am ET (`copy.md` §2). NO Reddit (per Nate's policy). +**Day 4:** X thread (link v0.9.2 changelog + a workflow story, not the launch pitch). +**Week 2:** Newsletter pitches (TLDR AI, Ben's Bites, Latent Space, Pragmatic Engineer). Skip cross-post Medium/Hashnode unless dev.to spikes. +**Week 3–4:** Product Hunt launch IF HN/newsletter gave momentum to leverage. + +--- + +## Current numbers (2026-05-17 — pre-second-push baseline) + +For deltas vs the 2026-04-17 launch baseline, see `analytics-2026-04-17.md`. + +- **Stars:** 8 (was 4 at launch — +4 in 30 days, mostly post-launch decay tail) +- **Forks:** 2 +- **Open issues:** 0 +- **Weekly PyPI installs:** 175 (was 733 at peak — **−77%** decay; Apr 15–24 spike fully decayed) +- **Daily PyPI installs (typical):** 5–10, occasional 47–105 bumps +- **Unique GitHub cloners (14d):** 78 (was 336 at launch — **−77%**) +- **Unique GitHub visitors (14d):** 13 (essentially flatlined; no organic referrer engine) +- **Top referrer:** github.com itself (5 uniques). Facebook is self-seeded per `feedback_longhand_fb_seeding` — ignore. +- **Tempo:** 4 commits in last 14d (maintenance mode post-v0.9.1) + +## What to watch (post-v0.9.2 push) + +- **First 24h after HN:** uniques + cloners spike (target: ≥200 uniques, ≥100 cloners) +- **48–72h after submissions:** Tier 1 referrers showing up (mcp.so + mcpservers.org + awesome-mcp-servers) +- **Week 1 after release:** PyPI weekly installs recovers above 300 (40% of launch peak) +- **Stars:** target +10 in week 1 (proves the angle landed) +- **DMs / issues:** open questions are the real signal — save good ones for FAQ/v0.10.0 priorities From 19e6c5e1d7921a9c6d628269d0c4abd71cc05557 Mon Sep 17 00:00:00 2001 From: nathan nelson Date: Sun, 17 May 2026 16:29:48 -0600 Subject: [PATCH 2/3] =?UTF-8?q?feat(demo):=20longhand=20demo=20=E2=80=94?= =?UTF-8?q?=20sandboxed=20walkthrough=20on=20a=20fake=20corpus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `longhand demo` CLI command lets users try Longhand on a tiny pre-built sample corpus without touching ~/.claude or ~/.longhand. The demo creates a sandbox at /tmp/longhand-demo-/, generates 3 fictional Claude Code sessions covering a realistic Stripe-webhook bug + Supabase auth migration + downstream 401 fix on a `demo-shop` project, then walks through three recall surfaces: 1. Cross-session bug retrieval ("the stripe signature bug") 2. Pattern recall ("supabase ssr auth migration") 3. recall_project_status("demo-shop") Cleans up afterwards; pass --keep to leave the sandbox in place for further exploration with `LONGHAND_DIR= longhand …`. ### Why The 2026-05-17 audit identified an onboarding gap: new users had no way to preview Longhand on safe data before running `longhand setup` on their real ~/.claude. This closes that gap with a 60-second walkthrough that exercises the cross-session recall, file history, and project-status surfaces without modifying anything on disk outside the sandbox. ### Files - longhand/demo/__init__.py - longhand/demo/corpus.py — deterministic 3-session corpus generator - longhand/demo/runner.py — sandbox setup + walkthrough - longhand/cli/_commands.py — `@app.command() def demo(...)` - tests/test_demo.py — 6 tests covering determinism, valid Claude Code event shape, ingestion into a fresh store, recall surfaces, cleanup - README.md — "Want to kick the tires first?" quickstart paragraph - CHANGELOG.md — v0.9.2 entry ### Tests 228 passing (was 222, +6 demo tests). Ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 36 +++++++ README.md | 7 ++ longhand/cli/_commands.py | 28 +++++ longhand/demo/__init__.py | 24 +++++ longhand/demo/corpus.py | 215 ++++++++++++++++++++++++++++++++++++++ longhand/demo/runner.py | 156 +++++++++++++++++++++++++++ tests/test_demo.py | 139 ++++++++++++++++++++++++ 7 files changed, 605 insertions(+) create mode 100644 longhand/demo/__init__.py create mode 100644 longhand/demo/corpus.py create mode 100644 longhand/demo/runner.py create mode 100644 tests/test_demo.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ade25..5120b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,42 @@ commits and tag annotations of those releases. --- +## [0.9.2] — 2026-05-17 + +`longhand demo` — try Longhand on a sample corpus without touching your real history. + +### Added + +- **New `longhand demo` CLI command.** Generates a sandboxed store at + `/tmp/longhand-demo-/`, seeds it with 3 fictional Claude Code + sessions covering a realistic Stripe-webhook bug + Supabase auth migration + + downstream 401 fix on a `demo-shop` project, then walks through `recall` + and `recall_project_status` against the seeded store so the user can see + what the output looks like before pointing Longhand at their own data. + Cleans up afterwards; pass `--keep` to leave the sandbox in place for + further exploration with `LONGHAND_DIR= longhand …`. +- `longhand.demo` package containing the corpus generator + (`longhand/demo/corpus.py`) and walkthrough runner + (`longhand/demo/runner.py`). 6 new tests verify the corpus is + deterministic, valid Claude Code event shape, ingests cleanly into a + fresh store, and that recall returns sensible results against the seeded + data. + +### Why + +The 2026-05-17 audit identified an onboarding gap: new users had no way to +preview Longhand on safe data before running `longhand setup` on their +real `~/.claude`. `longhand demo` closes that gap with a 60-second +walkthrough that exercises the cross-session recall, file history, and +project-status surfaces without modifying anything on disk outside the +sandbox. + +### Tests + +- 228 tests passing (was 222). + +--- + ## [0.9.1] — 2026-05-05 Tool definition quality pass. Every MCP tool now serializes a human-readable diff --git a/README.md b/README.md index 485f204..62672a3 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ longhand setup # ingest history + install hooks + configure MCP longhand recall "that stripe webhook bug from last week" ``` +**Want to kick the tires first?** Run `longhand demo` for a 60-second walkthrough on a fake 3-session sample corpus — your real `~/.claude` and `~/.longhand` are not touched. The demo seeds a sandboxed store with a Stripe-webhook bug + Supabase auth migration + downstream 401 fix, then runs cross-session recall and project-status so you can see what the output looks like before committing. + +```bash +pip install longhand +longhand demo # sandboxed; cleans up afterwards (pass --keep to explore) +``` + **Upgrading to 0.9.0?** Live ingestion captures sessions in flight, plan history is preserved as first-class data, and an optional reconciler job keeps the index honest in the background: - New `longhand ingest-live` command runs from Claude Code's `Stop` hook to tail the active transcript between assistant turns. Sessions show up in `recall` while you're still working, not after they end. diff --git a/longhand/cli/_commands.py b/longhand/cli/_commands.py index fb2b500..15a8de3 100644 --- a/longhand/cli/_commands.py +++ b/longhand/cli/_commands.py @@ -192,6 +192,34 @@ def setup( ) +# ----------------------------------------------------------------------------- +# DEMO — sandboxed walkthrough on a sample corpus +# ----------------------------------------------------------------------------- + + +@app.command() +def demo( + keep: bool = typer.Option( + False, + "--keep", + help="Leave the demo corpus + store on disk after the walkthrough so you can explore further. Default: clean up.", + ), +): + """Try Longhand on a fake 3-session corpus without touching your real history. + + Creates a sandboxed Longhand store under /tmp/longhand-demo-/, + seeds it with 3 fictional Claude Code sessions covering a Stripe webhook + bug + auth migration + downstream 401 fix, then runs through `recall` and + `recall_project_status` so you can see what the output looks like before + pointing Longhand at your own data. + + Your real ~/.longhand and ~/.claude are not modified. + """ + from longhand.demo import run_demo + + run_demo(keep=keep) + + # ----------------------------------------------------------------------------- # INGEST # ----------------------------------------------------------------------------- diff --git a/longhand/demo/__init__.py b/longhand/demo/__init__.py new file mode 100644 index 0000000..405ec17 --- /dev/null +++ b/longhand/demo/__init__.py @@ -0,0 +1,24 @@ +"""Longhand demo module. + +Provides a self-contained walkthrough that lets new users try Longhand +on a tiny pre-built sample corpus without touching their real +~/.claude session history. + +The demo: + 1. Creates a temporary store at /tmp/longhand-demo-/ + 2. Generates 3 fake Claude Code sessions covering a small Stripe + + auth-migration workflow on a fictional `demo-shop` project + 3. Ingests them into the temp store + 4. Runs `recall`, `search_in_context`, and `recall_project_status` + so the user can see what the output looks like before pointing + Longhand at their own data + 5. Cleans up by default; pass `keep=True` to leave the corpus around + for further exploration + +Entry point: `longhand.demo.run_demo()` (called from the CLI +`longhand demo` subcommand). +""" + +from longhand.demo.runner import run_demo + +__all__ = ["run_demo"] diff --git a/longhand/demo/corpus.py b/longhand/demo/corpus.py new file mode 100644 index 0000000..7668bce --- /dev/null +++ b/longhand/demo/corpus.py @@ -0,0 +1,215 @@ +"""Sample corpus generator for `longhand demo`. + +Produces 3 fictional Claude Code sessions on a `demo-shop` project, +covering a realistic Stripe-webhook + auth-migration workflow that +exercises the cross-session recall, file history, and project status +features without touching the user's real session data. + +Deterministic — same input dir always produces the same JSONL files +(stable session IDs, timestamps, event UUIDs) so the walkthrough +output is reproducible. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +_MODEL = "claude-sonnet-4-6" + + +def _ts(day_offset: int, hour: int, minute: int) -> str: + """Deterministic timestamps anchored to 2026-05-15 for the corpus.""" + base_day = 15 + day_offset + return f"2026-05-{base_day:02d}T{hour:02d}:{minute:02d}:00.000Z" + + +def _user(uuid: str, parent: str | None, sid: str, ts: str, cwd: str, text: str) -> dict[str, Any]: + return { + "type": "user", + "uuid": uuid, + "parentUuid": parent, + "sessionId": sid, + "timestamp": ts, + "cwd": cwd, + "isSidechain": False, + "message": {"role": "user", "content": text}, + } + + +def _assistant( + uuid: str, + parent: str, + sid: str, + ts: str, + cwd: str, + blocks: list[dict[str, Any]], +) -> dict[str, Any]: + return { + "type": "assistant", + "uuid": uuid, + "parentUuid": parent, + "sessionId": sid, + "timestamp": ts, + "cwd": cwd, + "isSidechain": False, + "message": {"model": _MODEL, "role": "assistant", "content": blocks}, + } + + +def _tool_result( + uuid: str, parent: str, sid: str, ts: str, cwd: str, tool_use_id: str, result: str +) -> dict[str, Any]: + return { + "type": "user", + "uuid": uuid, + "parentUuid": parent, + "sessionId": sid, + "timestamp": ts, + "cwd": cwd, + "isSidechain": False, + "message": { + "role": "user", + "content": [{"type": "tool_result", "tool_use_id": tool_use_id, "content": result}], + }, + "toolUseResult": {"success": True, "stdout": result}, + } + + +def generate_corpus(project_dir: Path) -> list[tuple[str, list[dict[str, Any]]]]: + """Generate the 3-session demo corpus. + + Args: + project_dir: absolute path to the fictional demo-shop project root. + Must exist and contain a `.git` marker so project inference + attributes events correctly. Caller is responsible for creating it. + + Returns: + List of (filename, events) tuples ready to write as JSONL and + ingest. Filenames are session-id based (`session-.jsonl`). + """ + pd = str(project_dir) + api_route = f"{pd}/src/app/api/webhooks/stripe/route.ts" + auth_lib = f"{pd}/src/lib/supabase.ts" + middleware = f"{pd}/src/middleware.ts" + checkout_route = f"{pd}/src/app/api/checkout/route.ts" + + # ────────────────────────────────────────────────────────────────── + # Session A — Add Stripe webhook handler. Hits a signature bug, fixes. + # ────────────────────────────────────────────────────────────────── + sid_a = "demo-001-stripe-webhook" + session_a = [ + _user("a-u1", None, sid_a, _ts(0, 14, 0), pd, + "Add a Stripe webhook handler at /api/webhooks/stripe that verifies signatures and logs payment_intent.succeeded events."), + _assistant("a-a1", "a-u1", sid_a, _ts(0, 14, 1), pd, [ + {"type": "text", "text": "I'll create the webhook route. First reading existing API structure."}, + {"type": "tool_use", "id": "a-t1", "name": "Read", + "input": {"file_path": f"{pd}/src/app/api/checkout/route.ts"}}, + ]), + _tool_result("a-r1", "a-a1", sid_a, _ts(0, 14, 1), pd, "a-t1", + "import Stripe from 'stripe'\nimport { NextResponse } from 'next/server'\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nexport async function POST(req: Request) {\n // ... checkout session creation\n}"), + _assistant("a-a2", "a-r1", sid_a, _ts(0, 14, 3), pd, [ + {"type": "text", "text": "Good — Stripe client pattern is set up. Writing the webhook handler."}, + {"type": "tool_use", "id": "a-t2", "name": "Write", + "input": {"file_path": api_route, + "content": "import Stripe from 'stripe'\nimport { NextResponse } from 'next/server'\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nexport async function POST(req: Request) {\n const sig = req.headers.get('stripe-signature')\n const body = await req.text()\n const event = stripe.webhooks.constructEvent(body, sig!, process.env.STRIPE_WEBHOOK_SECRET!)\n if (event.type === 'payment_intent.succeeded') {\n console.log('Payment succeeded:', event.data.object.id)\n }\n return NextResponse.json({ received: true })\n}"}}, + ]), + _tool_result("a-r2", "a-a2", sid_a, _ts(0, 14, 3), pd, "a-t2", + f"File created at {api_route}"), + _assistant("a-a3", "a-r2", sid_a, _ts(0, 14, 5), pd, [ + {"type": "text", "text": "Now testing signature verification with a local payload."}, + {"type": "tool_use", "id": "a-t3", "name": "Bash", + "input": {"command": "cd " + pd + " && npm test webhooks"}}, + ]), + _tool_result("a-r3", "a-a3", sid_a, _ts(0, 14, 6), pd, "a-t3", + "FAIL test/webhooks.test.ts\n Stripe webhook > verifies valid signature\n Error: No signatures found matching the expected signature for payload"), + _assistant("a-a4", "a-r3", sid_a, _ts(0, 14, 8), pd, [ + {"type": "text", "text": "Signature check is failing because constructEvent needs the RAW body — req.text() in Next.js App Router is already decoded. Switching to req.arrayBuffer() then converting to Buffer."}, + {"type": "tool_use", "id": "a-t4", "name": "Edit", + "input": {"file_path": api_route, + "old_string": " const body = await req.text()", + "new_string": " const body = Buffer.from(await req.arrayBuffer())"}}, + ]), + _tool_result("a-r4", "a-a4", sid_a, _ts(0, 14, 8), pd, "a-t4", + "File updated"), + _assistant("a-a5", "a-r4", sid_a, _ts(0, 14, 10), pd, [ + {"type": "tool_use", "id": "a-t5", "name": "Bash", + "input": {"command": "cd " + pd + " && npm test webhooks"}}, + ]), + _tool_result("a-r5", "a-a5", sid_a, _ts(0, 14, 11), pd, "a-t5", + "PASS test/webhooks.test.ts\n Stripe webhook > verifies valid signature ✓\n Stripe webhook > logs payment_intent.succeeded ✓"), + _assistant("a-a6", "a-r5", sid_a, _ts(0, 14, 12), pd, [ + {"type": "text", "text": "Tests pass. The fix: use arrayBuffer() not text() because Stripe needs raw bytes for HMAC verification."}, + ]), + ] + + # ────────────────────────────────────────────────────────────────── + # Session B — Migrate auth from Supabase v1 to v2 SSR pattern. + # ────────────────────────────────────────────────────────────────── + sid_b = "demo-002-auth-migration" + session_b = [ + _user("b-u1", None, sid_b, _ts(1, 9, 30), pd, + "Migrate the auth client from @supabase/supabase-js createClient to the @supabase/ssr createServerClient pattern for App Router."), + _assistant("b-a1", "b-u1", sid_b, _ts(1, 9, 31), pd, [ + {"type": "tool_use", "id": "b-t1", "name": "Read", + "input": {"file_path": auth_lib}}, + ]), + _tool_result("b-r1", "b-a1", sid_b, _ts(1, 9, 31), pd, "b-t1", + "import { createClient } from '@supabase/supabase-js'\n\nexport const supabase = createClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n)"), + _assistant("b-a2", "b-r1", sid_b, _ts(1, 9, 33), pd, [ + {"type": "text", "text": "Replacing with the SSR createServerClient that reads/writes auth cookies properly."}, + {"type": "tool_use", "id": "b-t2", "name": "Write", + "input": {"file_path": auth_lib, + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\nexport async function getSupabaseServerClient() {\n const cookieStore = await cookies()\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() { return cookieStore.getAll() },\n setAll(toSet) { toSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) },\n },\n }\n )\n}"}}, + ]), + _tool_result("b-r2", "b-a2", sid_b, _ts(1, 9, 33), pd, "b-t2", + "File updated"), + _assistant("b-a3", "b-r2", sid_b, _ts(1, 9, 35), pd, [ + {"type": "text", "text": "Note: all callers of the old `supabase` export need to switch to `await getSupabaseServerClient()`. Searching for usages."}, + {"type": "tool_use", "id": "b-t3", "name": "Grep", + "input": {"pattern": "from '@/lib/supabase'", "path": pd}}, + ]), + _tool_result("b-r3", "b-a3", sid_b, _ts(1, 9, 35), pd, "b-t3", + f"{middleware}:3\n{checkout_route}:1"), + _assistant("b-a4", "b-r3", sid_b, _ts(1, 9, 37), pd, [ + {"type": "text", "text": "Two callers. Middleware first — it runs on every request, so the change matters most there."}, + {"type": "tool_use", "id": "b-t4", "name": "Edit", + "input": {"file_path": middleware, + "old_string": "import { supabase } from '@/lib/supabase'", + "new_string": "import { getSupabaseServerClient } from '@/lib/supabase'"}}, + ]), + _tool_result("b-r4", "b-a4", sid_b, _ts(1, 9, 37), pd, "b-t4", "File updated"), + ] + + # ────────────────────────────────────────────────────────────────── + # Session C — Quick fix for 401 on /api/checkout (caused by B). + # ────────────────────────────────────────────────────────────────── + sid_c = "demo-003-checkout-401" + session_c = [ + _user("c-u1", None, sid_c, _ts(2, 11, 0), pd, + "/api/checkout is returning 401 for logged-in users. It worked yesterday."), + _assistant("c-a1", "c-u1", sid_c, _ts(2, 11, 1), pd, [ + {"type": "text", "text": "The auth migration yesterday changed the import. Checking if /api/checkout was updated."}, + {"type": "tool_use", "id": "c-t1", "name": "Read", + "input": {"file_path": checkout_route}}, + ]), + _tool_result("c-r1", "c-a1", sid_c, _ts(2, 11, 1), pd, "c-t1", + "import { supabase } from '@/lib/supabase'\n\nexport async function POST(req: Request) {\n const { data: { user } } = await supabase.auth.getUser()\n if (!user) return new Response('Unauthorized', { status: 401 })\n // ...\n}"), + _assistant("c-a2", "c-r1", sid_c, _ts(2, 11, 3), pd, [ + {"type": "text", "text": "Confirmed — checkout still imports the old `supabase` (which is undefined now), so `supabase.auth.getUser()` throws and 401 is the fallback. Fixing."}, + {"type": "tool_use", "id": "c-t2", "name": "Edit", + "input": {"file_path": checkout_route, + "old_string": "import { supabase } from '@/lib/supabase'\n\nexport async function POST(req: Request) {\n const { data: { user } } = await supabase.auth.getUser()", + "new_string": "import { getSupabaseServerClient } from '@/lib/supabase'\n\nexport async function POST(req: Request) {\n const supabase = await getSupabaseServerClient()\n const { data: { user } } = await supabase.auth.getUser()"}}, + ]), + _tool_result("c-r2", "c-a2", sid_c, _ts(2, 11, 3), pd, "c-t2", "File updated"), + _assistant("c-a3", "c-r2", sid_c, _ts(2, 11, 5), pd, [ + {"type": "text", "text": "Fixed. The pattern from yesterday's migration needed to propagate to this caller too."}, + ]), + ] + + return [ + (f"{sid_a}.jsonl", session_a), + (f"{sid_b}.jsonl", session_b), + (f"{sid_c}.jsonl", session_c), + ] diff --git a/longhand/demo/runner.py b/longhand/demo/runner.py new file mode 100644 index 0000000..e45f7ed --- /dev/null +++ b/longhand/demo/runner.py @@ -0,0 +1,156 @@ +"""Demo runner: build a sandboxed Longhand store from the sample corpus, +then run a guided walkthrough of recall / project-status. + +The demo never touches the user's real ~/.longhand or ~/.claude. +Everything happens under /tmp/longhand-demo-/. +""" + +from __future__ import annotations + +import json +import shutil +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +from rich.console import Console +from rich.markdown import Markdown +from rich.panel import Panel +from rich.rule import Rule + +from longhand.demo.corpus import generate_corpus +from longhand.parser import JSONLParser +from longhand.recall import recall as recall_pipeline +from longhand.recall.recall_pipeline import recall_project_status +from longhand.storage.store import LonghandStore + +console = Console() + + +def _seed_corpus(store: LonghandStore, jsonl_dir: Path, project_dir: Path) -> int: + """Generate the corpus, write JSONL files, ingest each. Returns event count.""" + sessions = generate_corpus(project_dir) + total_events = 0 + for filename, events in sessions: + path = jsonl_dir / filename + with path.open("w") as f: + for event in events: + f.write(json.dumps(event) + "\n") + parser = JSONLParser(path) + parsed = list(parser.parse_events()) + session = parser.build_session(parsed) + store.ingest_session(session, parsed) + total_events += len(parsed) + return total_events + + +def _run_recall(store: LonghandStore, query: str, *, now: datetime) -> None: + """Run recall and pretty-print the narrative.""" + result = recall_pipeline(store, query, now=now) + if result.narrative: + console.print(Panel(Markdown(result.narrative), title=f"recall: {query!r}", border_style="cyan")) + else: + console.print(f"[yellow]No narrative produced for {query!r}[/yellow]") + + +def _run_project_status(store: LonghandStore, project: str) -> None: + """Run recall_project_status and pretty-print.""" + result = recall_project_status(store, project) + if result is not None and result.narrative: + console.print(Panel(Markdown(result.narrative), title=f"recall_project_status({project!r})", border_style="magenta")) + else: + console.print(f"[yellow]No project status produced for {project!r} (project may not be known to the store).[/yellow]") + + +def run_demo(*, keep: bool = False) -> Path | None: + """Run the full demo walkthrough. + + Args: + keep: if True, leave the temp dir in place (printed to stdout) so the + user can explore further with `LONGHAND_DIR= longhand ...`. + If False (default), clean up at the end. + + Returns: + The path to the demo dir if `keep=True`, otherwise None. + """ + # Demo anchor date — events in the corpus are dated 2026-05-15..17, so + # recall's time parsing sees them as "today / yesterday / 2 days ago" + # relative to this date instead of the actual now (which could be months + # in the future from the corpus). + demo_now = datetime(2026, 5, 17, 14, 0, 0, tzinfo=timezone.utc) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") + demo_root = Path(tempfile.gettempdir()) / f"longhand-demo-{timestamp}" + store_dir = demo_root / "store" + jsonl_dir = demo_root / "jsonl" + project_dir = demo_root / "demo-shop" + + store_dir.mkdir(parents=True, exist_ok=True) + jsonl_dir.mkdir(parents=True, exist_ok=True) + project_dir.mkdir(parents=True, exist_ok=True) + # Project marker so cwd inference attributes events correctly + (project_dir / ".git").mkdir(exist_ok=True) + + console.print(Rule("[bold cyan]Longhand demo[/bold cyan]")) + console.print( + "[dim]Sandbox at[/dim]", + f"[dim]{demo_root}[/dim]", + "[dim]— your real ~/.longhand and ~/.claude are NOT touched.[/dim]", + ) + console.print() + + # 1. Seed + store = LonghandStore(data_dir=store_dir) + console.print("[bold]Seeding 3 fake Claude Code sessions into a temp store...[/bold]") + n_events = _seed_corpus(store, jsonl_dir, project_dir) + console.print(f" → ingested 3 sessions, [cyan]{n_events}[/cyan] events on project [magenta]demo-shop[/magenta]") + console.print() + console.print("[dim]Workflow in the corpus:[/dim]") + console.print(" [dim]• 2 days ago:[/dim] Stripe webhook handler + signature-verification bug fix") + console.print(" [dim]• 1 day ago:[/dim] Supabase auth migration from createClient → SSR createServerClient") + console.print(" [dim]• today:[/dim] Quick 401 fix in /api/checkout caused by yesterday's auth migration") + console.print() + + # 2. Recall — cross-session bug retrieval + console.print(Rule("[cyan]Try 1: cross-session bug retrieval[/cyan]")) + console.print("[dim]Question: \"the stripe signature bug on demo-shop\"[/dim]") + console.print("[dim]Longhand finds the bug fix from 2 days ago — even though the session is closed.[/dim]") + console.print() + _run_recall(store, "the stripe signature bug on demo-shop", now=demo_now) + console.print() + + # 3. Recall — finding the auth migration pattern + console.print(Rule("[cyan]Try 2: \"where did I switch to SSR auth?\"[/cyan]")) + console.print("[dim]Question: \"supabase ssr auth migration on demo-shop\"[/dim]") + console.print() + _run_recall(store, "supabase ssr auth migration on demo-shop", now=demo_now) + console.print() + + # 4. Project status — pick up where we left off + console.print(Rule("[magenta]Try 3: \"pick up where I left off on demo-shop\"[/magenta]")) + console.print("[dim]recall_project_status(\"demo-shop\") returns recent activity + last session outcome.[/dim]") + console.print() + _run_project_status(store, "demo-shop") + console.print() + + # 5. Wrap up + console.print(Rule("[bold green]Done[/bold green]")) + console.print("Like what you saw? Point Longhand at your real Claude Code history:") + console.print() + console.print(" [bold]pip install longhand[/bold]") + console.print(" [bold]longhand setup[/bold]") + console.print() + console.print("Then any future Claude Code session feeds the index automatically (SessionEnd + Stop hooks).") + console.print("Once you've worked a bit, try:") + console.print(' [bold]longhand recall "that bug from last week"[/bold]') + console.print() + + # 6. Cleanup + if keep: + console.print(f"[yellow]--keep[/yellow]: corpus preserved at [cyan]{demo_root}[/cyan]") + console.print(f" Explore with: [bold]LONGHAND_DIR={store_dir} longhand sessions[/bold]") + return demo_root + else: + shutil.rmtree(demo_root, ignore_errors=True) + console.print("[dim]Cleaned up demo sandbox.[/dim]") + return None diff --git a/tests/test_demo.py b/tests/test_demo.py new file mode 100644 index 0000000..b943446 --- /dev/null +++ b/tests/test_demo.py @@ -0,0 +1,139 @@ +"""Tests for the `longhand demo` walkthrough. + +Covers: +- Sample corpus is deterministic + valid JSONL-event shape +- Corpus ingests cleanly into a fresh LonghandStore +- recall against the seeded store returns non-empty results for the + known queries +- The demo runner cleans up after itself unless keep=True +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from longhand.demo import run_demo +from longhand.demo.corpus import generate_corpus +from longhand.parser import JSONLParser +from longhand.recall import recall as recall_pipeline +from longhand.storage.store import LonghandStore + + +def test_corpus_is_deterministic(tmp_path): + """Same project dir produces the same corpus across calls.""" + pd = tmp_path / "demo-shop" + pd.mkdir() + (pd / ".git").mkdir() + + first = generate_corpus(pd) + second = generate_corpus(pd) + + assert first == second, "generate_corpus must be deterministic" + assert len(first) == 3, "demo corpus is 3 sessions" + + +def test_corpus_events_are_valid_jsonl(tmp_path): + """Every event must be JSON-serializable and have the required Claude Code fields.""" + pd = tmp_path / "demo-shop" + pd.mkdir() + (pd / ".git").mkdir() + + sessions = generate_corpus(pd) + for filename, events in sessions: + assert filename.endswith(".jsonl"), "session files end in .jsonl" + assert len(events) > 0, "session has at least one event" + for event in events: + # Must serialize cleanly + json.dumps(event) + # Required Claude Code transcript fields + assert "type" in event + assert event["type"] in ("user", "assistant"), f"unexpected event type: {event['type']}" + assert "uuid" in event + assert "sessionId" in event + assert "timestamp" in event + assert "cwd" in event + assert "message" in event + + +def test_corpus_ingests_into_store(tmp_path): + """Writing the corpus to disk and ingesting via the standard parser succeeds.""" + pd = tmp_path / "demo-shop" + pd.mkdir() + (pd / ".git").mkdir() + jsonl_dir = tmp_path / "jsonl" + jsonl_dir.mkdir() + store_dir = tmp_path / "store" + store_dir.mkdir() + + store = LonghandStore(data_dir=store_dir) + sessions = generate_corpus(pd) + total = 0 + for filename, events in sessions: + path = jsonl_dir / filename + with path.open("w") as f: + for event in events: + f.write(json.dumps(event) + "\n") + parser = JSONLParser(path) + parsed = list(parser.parse_events()) + session = parser.build_session(parsed) + store.ingest_session(session, parsed) + total += len(parsed) + + assert total > 0, "events were ingested" + # Verify the store actually has the sessions + stats = store.stats() + assert stats.get("sessions", 0) == 3, f"expected 3 sessions in store, got {stats}" + + +def test_recall_finds_seeded_stripe_bug(tmp_path): + """The Stripe signature bug from session A should be recallable by topic.""" + pd = tmp_path / "demo-shop" + pd.mkdir() + (pd / ".git").mkdir() + jsonl_dir = tmp_path / "jsonl" + jsonl_dir.mkdir() + store_dir = tmp_path / "store" + store_dir.mkdir() + + store = LonghandStore(data_dir=store_dir) + sessions = generate_corpus(pd) + for filename, events in sessions: + path = jsonl_dir / filename + with path.open("w") as f: + for event in events: + f.write(json.dumps(event) + "\n") + parser = JSONLParser(path) + parsed = list(parser.parse_events()) + session = parser.build_session(parsed) + store.ingest_session(session, parsed) + + result = recall_pipeline(store, "stripe signature bug") + assert result.narrative, "recall produced a narrative" + # The Stripe session is demo-001; its prefix should appear in the narrative + assert "demo-001" in result.narrative or len(result.episodes) > 0, ( + "recall surfaced the Stripe-bug session by topic" + ) + + +def test_run_demo_cleans_up_by_default(tmp_path, monkeypatch): + """run_demo() without keep=True removes its sandbox dir.""" + # Force the tempdir to a known location so we can verify cleanup + monkeypatch.setattr("tempfile.gettempdir", lambda: str(tmp_path)) + result = run_demo(keep=False) + assert result is None, "no path returned when keep=False" + # The demo dir name follows the pattern longhand-demo-; + # after cleanup nothing matching that pattern should remain in tmp_path + leftover = [p for p in tmp_path.iterdir() if p.name.startswith("longhand-demo-")] + assert leftover == [], f"demo dir not cleaned up: {leftover}" + + +def test_run_demo_keeps_sandbox_when_requested(tmp_path, monkeypatch): + """run_demo(keep=True) leaves the sandbox in place and returns its path.""" + monkeypatch.setattr("tempfile.gettempdir", lambda: str(tmp_path)) + result = run_demo(keep=True) + assert isinstance(result, Path), "keep=True returns the demo path" + assert result.exists(), "demo dir preserved" + assert (result / "store").exists(), "store dir preserved" + assert (result / "jsonl").exists(), "jsonl dir preserved" + assert (result / "demo-shop").exists(), "project dir preserved" From f20a1af95e69b4f9735e62c9c3e414c7fb118ff1 Mon Sep 17 00:00:00 2001 From: nathan nelson Date: Sun, 17 May 2026 16:32:29 -0600 Subject: [PATCH 3/3] =?UTF-8?q?chore(outreach):=20add=20v0.9.2=20second-pu?= =?UTF-8?q?sh=20copy=20(=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §§1–7 stay as April launch copy reference. §8 is the fresh angle paired with this PR's v0.9.2 release: - Show HN v0.9.2 angle: lead with the new `longhand demo` command ("try it without installing on your real data"), then surface what shipped since April (live ingestion, plan history, reconciler, annotations/outputSchema, GLAMA A-tier, SafeSkill 93/100) - Updated stats everywhere: 19 MCP tools (was 16), 228 tests (was 170) - mcp.so / mcpservers.org medium copy + awesome-mcp-servers PR bullet - awesome-claude-code resubmission bullet (prior submission was blocked on Shipwright issue, eligible after Apr 16) - X/Twitter 4-tweet follow-up thread - Newsletter pitch template refreshed with current traction Co-Authored-By: Claude Opus 4.7 (1M context) --- outreach/copy.md | 368 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 outreach/copy.md diff --git a/outreach/copy.md b/outreach/copy.md new file mode 100644 index 0000000..1223b9e --- /dev/null +++ b/outreach/copy.md @@ -0,0 +1,368 @@ +# Longhand Submission Copy + +All ready-to-paste copy. Each section is numbered to match the checklist in `README.md`. + +--- + +## §1 — Directory descriptions + +### Short (1 line, ~140 chars) +> Persistent local memory for Claude Code. Every session event stored verbatim in SQLite + ChromaDB. Zero API calls. Semantic recall in ~126ms. + +### Medium (2–3 sentences, forms / MCP directories) +> Longhand captures every tool call, file edit, and thinking block from every Claude Code session into a local SQLite + ChromaDB store. No summaries, no API calls, no AI deciding what matters — just lossless storage and ~126ms semantic recall across your entire history. Integrates as an MCP server giving Claude 16 tools to query its own past. + +### Long (for awesome-list entries, PR descriptions) +> **[Longhand](https://github.com/Wynelson94/longhand)** — Persistent local memory for Claude Code. Indexes every session file (tool calls, edits, thinking blocks) into a local SQLite + ChromaDB store for lossless, offline semantic recall. Integrates as an MCP server with 16 tools. Zero API calls, zero summarization. ~126ms queries across 100+ sessions. + +### Awesome-list bullet (markdown, one-line) +```markdown +- [Longhand](https://github.com/Wynelson94/longhand) - Persistent local memory for Claude Code. Lossless capture of every session event into SQLite + ChromaDB. Zero API calls, ~126ms semantic recall, 16 MCP tools. MIT. +``` + +### Keywords / tags (paste into forms asking for tags) +`claude-code, mcp, memory, local-first, semantic-search, sqlite, chromadb, ai-tools, developer-tools, cli, offline, privacy, python` + +--- + +## §2 — Show HN post + +**Title:** +> Show HN: Longhand – Lossless local memory for Claude Code (no summaries, no API) + +**URL field:** +> https://github.com/Wynelson94/longhand + +**Text field:** +``` +Hi HN — I built Longhand because every AI memory tool I tried summarized my sessions before giving them back to me. Summarization is a lossy decision disguised as a convenience: an LLM decides what's worth remembering, and I never get to see what it threw away. + +Longhand does the opposite. Claude Code already writes every tool call, every file edit, every thinking block to JSONL files on disk. Longhand reads those files and indexes them verbatim into a local SQLite + ChromaDB store. Nothing is summarized. Nothing is sent through an API. Semantic recall across 100+ sessions returns in ~126ms. + +A few design notes: + +- Claude Code rotates those JSONL files off disk after a few weeks. If you install Longhand late, the past before install is gone. Install early. +- It exposes 16 MCP tools to Claude itself, so the model can query its own history ("that stripe webhook fix from last week") without eating tokens on stale context. +- Storage is ~1GB for a heavy user (120+ sessions, 60k events), 200–400MB typical. +- Python 3.10–3.13 fully supported; 3.14 works with a chromadb<1.0 pin (upstream segfault). + +The "AI memory crisis" seemed artificial to me. SQLite is from 2000. ChromaDB is two years old. Both run on a laptop. The memory doesn't need to live in the model — it needs to live on the disk. + +`pip install longhand` → `longhand setup` → it ingests your history, installs the hooks, and registers as an MCP server. Repo: https://github.com/Wynelson94/longhand + +Happy to answer questions about the architecture, the decision to skip summarization entirely, or the tradeoffs vs larger context windows. +``` + +**Timing:** Post Tue or Wed, 8–10am ET. Stay on thread for 4+ hours to answer questions. + +--- + +## §3 — r/ClaudeAI post + +**Title:** +> I built persistent local memory for Claude Code — no API calls, lossless replay of every session + +**Body:** +``` +TL;DR: `pip install longhand` → `longhand setup` → Claude Code now remembers every session you've ever had, queryable in ~126ms. Free, open source, 100% local. + +**The problem I kept hitting:** Claude Code writes rich JSONL logs of every session — tool calls, file edits, thinking blocks — into `~/.claude/projects/`. Then quietly rotates them off disk after a few weeks. Every memory tool I tried summarized those sessions before giving them back to me, which meant I never saw what got dropped. + +**What Longhand does:** +- Reads the JSONL files verbatim into SQLite + ChromaDB +- Nothing is summarized. Nothing goes through an API. +- Registers as an MCP server with 16 tools, so Claude can query its own history +- Semantic recall works: "that webhook bug from last week" → returns the actual conversation, the actual edit, the actual fix +- ~126ms queries on my 107-session / 53k-event store + +**Install is one command:** +``` +pip install longhand +longhand setup +``` + +Repo: https://github.com/Wynelson94/longhand — MIT, 170 tests, security-audited (zero critical findings). + +Happy to answer questions. Also curious what other people's session counts look like — I'm at 107 and rising fast. +``` + +--- + +## §4 — r/LocalLLaMA post + +**Title:** +> Lossless, 100%-local memory for Claude Code — zero API calls, SQLite + ChromaDB on disk + +**Body:** +``` +This is Claude Code-specific, but the philosophical alignment with this sub is why I'm posting here. + +Longhand is a memory layer for Claude Code that is aggressively local: +- Zero API calls, ever +- No summarization (every tool call, edit, and thinking block stored verbatim) +- Data lives on your disk forever — never touches a vendor's servers +- Semantic recall runs against a local ChromaDB index (~126ms) +- Works fully offline once installed + +The reason I built it: every memory tool in this space assumes "memory" means "ask an LLM to summarize your past." That's a lossy, API-dependent, vendor-locked design. I wanted the opposite — the disk carries the memory, the model just queries it. + +Storage footprint: ~1GB for a heavy user (60k events across 120+ sessions). 200–400MB typical. Once Claude Code rotates its own session files off disk, Longhand is the only copy. + +It exposes itself as an MCP server (16 tools), so Claude Code can query its own past without eating tokens on stale context. + +Install: +``` +pip install longhand +longhand setup +``` + +Repo: https://github.com/Wynelson94/longhand — MIT. 170 tests passing. Python 3.10–3.13 fully supported. + +Not a drop-in for LocalLLaMA setups since it hooks Claude Code specifically, but the design philosophy (local-first, no API, no summarization) is something this community usually appreciates — and the architectural patterns (SQLite + local vector store + verbatim capture) generalize cleanly to any AI session log. +``` + +--- + +## §5 — X/Twitter thread + +**Tweet 1/5:** +``` +Everyone is solving AI memory by making context windows bigger. + +1M tokens. 2M tokens. "Context-infinite." + +I built Longhand by going the other direction: the model doesn't need to carry the memory. The disk does. + +A thread on what it means in practice ↓ +``` + +**Tweet 2/5:** +``` +Claude Code writes every session — tool calls, file edits, thinking blocks — to JSONL files on disk. + +Then quietly rotates them off disk after a few weeks. + +Longhand reads them verbatim into SQLite + ChromaDB before they're gone. Nothing summarized. Nothing sent to an API. +``` + +**Tweet 3/5:** +``` +Key numbers: + +• Semantic recall: ~126ms across 100+ sessions +• Storage: ~1GB for a heavy user, 200–400MB typical +• API calls: 0 +• Summarization: 0 +• Vendor lock-in: 0 +• 16 MCP tools exposed to Claude itself +``` + +**Tweet 4/5:** +``` +The "AI memory crisis" was an artificial constraint. + +SQLite is from 2000. ChromaDB is two years old. Both run on a laptop. + +Longhand bypasses the crisis by ignoring it — your past sessions are already on disk, written by Claude Code itself. Indexing them locally is a solved problem. +``` + +**Tweet 5/5:** +``` +pip install longhand +longhand setup + +MIT. Python 3.10–3.13. 170 tests. Security-audited (zero critical findings). On PyPI. + +Repo: https://github.com/Wynelson94/longhand + +Tagging @AnthropicAI + anyone building on MCP — would love your thoughts. +``` + +**Who to tag:** @AnthropicAI, @alexalbert__, @swyx, @cline (if they're on X), @zeddotdev, @continuedev, @BenjaminDEKR (Glama), plus any MCP devs you've seen on X. + +--- + +## §6 — Dev.to / blog post + +**Title:** Why I built a lossless alternative to AI memory summarization + +**Subtitle:** The disk doesn't need an LLM to decide what's worth remembering. + +**Outline (write in this order):** + +1. **Hook** — The moment I realized every memory tool I tried was summarizing my sessions, and I never got to see what got dropped. +2. **The industry's direction** — Bigger context windows. 1M tokens. 2M tokens. Paragraph on why this is the wrong axis. +3. **The actual state of the world** — Claude Code already writes rich JSONL logs of every session. They exist. On your disk. Right now. +4. **The problem** — Claude Code rotates those files off disk after a few weeks. Most memory tools summarize them. Both paths are lossy. +5. **The architecture** — SQLite for structured events. ChromaDB for semantic search. Hooks for auto-ingestion. MCP for exposure back to Claude. +6. **The numbers** — 126ms recall. 1GB max storage. Zero API calls. 170 tests. +7. **The contrarian framing** — "Summarization is a lossy decision disguised as a convenience." +8. **What it unlocks** — Cross-model portability. Offline work. No vendor lock-in. Forensic replay of any past decision. +9. **What it doesn't try to do** — Not a general-purpose memory. Specific to Claude Code's JSONL format. Won't help you with ChatGPT. +10. **Install line + repo link.** + +Target length: 800–1,200 words. Include the comparison table from the README. Link the repo at top and bottom. Cross-post to Medium + Hashnode day-of. + +--- + +## §8 — v0.9.2 second-push copy (2026-05-17) + +The April launch (§§1–7 above) sent the curve up then it decayed (175/wk vs 733 peak). The v0.9.2 push reuses the same channels but leads with a different hook: **`longhand demo`** — try it on a fake corpus in 60 seconds, no install commitment, no touching your real `~/.claude`. + +Updated stats (use these everywhere instead of the launch numbers): +- **19 MCP tools** (was 16) — `outputSchema` + `annotations` added in v0.9.1 +- **228 tests passing** (was 170) — +6 demo tests in v0.9.2 +- **GLAMA A-tier** scores · **SafeSkill 93/100** — both as badges in README +- **Live ingestion** since v0.9.0 (Stop hook + reconciler) +- **PyPI weekly installs**: 175 currently (peak 733 mid-April) + +### §8.1 — Short summary (universal, 1 line) +> Lossless local memory for Claude Code. Try `longhand demo` in 60s — sandboxed, no install commitment. + +### §8.2 — Show HN v0.9.2 + +**Title:** +> Show HN: Longhand v0.9.2 — try lossless local memory for Claude Code without installing it + +**URL:** +> https://github.com/Wynelson94/longhand + +**Text:** +``` +A few of you saw the v0.9.0 launch in April. Since then a question kept coming up: "How do I know this is going to work on my actual session history before I run setup on my real ~/.claude?" + +v0.9.2 ships `longhand demo` — one command that creates a sandboxed store under /tmp, seeds it with three fake Claude Code sessions (a Stripe webhook bug, a Supabase auth migration, a downstream 401 fix), then walks you through `recall`, cross-session retrieval, and `recall_project_status` so you see the actual output before pointing Longhand at your own data. Cleans up afterwards; --keep leaves it for exploration. + + pip install longhand + longhand demo + +The original premise is unchanged: Claude Code writes every tool call, file edit, and thinking block to JSONL on disk. Longhand reads them verbatim into SQLite + ChromaDB. Zero API calls. Zero summarization. ~126ms semantic recall across 100+ sessions. 19 MCP tools so Claude can query its own past. + +What landed since April: +- Live ingestion (v0.9.0) — sessions show up in recall while you're still working, not at SessionEnd +- Plan history as first-class data — every Write/Edit to ~/.claude/plans/*.md is indexed +- Reconciler launchd job (opt-in) — belt-and-suspenders for hard crashes +- 19 MCP tools (was 16) with annotations + outputSchema — v0.9.1 quality pass +- 228 tests passing across a 4-matrix Python CI (3.10–3.13) + +Repo: https://github.com/Wynelson94/longhand (MIT). GLAMA A-tier, SafeSkill 93/100. + +Happy to answer questions about the demo, the no-summarization design, the architecture, or why Claude's own JSONL format is the right starting point instead of a custom event log. +``` + +**Timing:** Tue/Wed 8–10am ET. Stay on thread 4+ hours. + +### §8.3 — mcp.so / mcpservers.org / directory submission (medium) + +> **Longhand** — Lossless local memory for Claude Code. Indexes every session file (tool calls, edits, thinking blocks) verbatim into SQLite + ChromaDB. 19 MCP tools for Claude to query its own past. Zero API calls, ~126ms recall. `pip install longhand && longhand demo` to try it on a sandboxed corpus without touching your real data. MIT, Python 3.10–3.13, 228 tests, GLAMA A-tier. + +### §8.4 — awesome-mcp-servers PR bullet + +```markdown +- [Longhand](https://github.com/Wynelson94/longhand) - Persistent local memory for Claude Code. Lossless capture of every session event into SQLite + ChromaDB. 19 MCP tools, ~126ms recall, zero API calls. Try with `longhand demo` (sandboxed). MIT. +``` + +### §8.5 — awesome-claude-code PR bullet (resubmission) + +The previous submission was closed Apr 15 for "repo too young" and blocked by an unrelated Shipwright issue. Resubmission should be unblocked now (eligible after Apr 16; Shipwright should have moved). Use this entry: + +```markdown +- [Longhand](https://github.com/Wynelson94/longhand) — Persistent local memory layer for Claude Code. Hooks into SessionEnd and Stop, indexes every tool call/edit/thinking block into local SQLite + ChromaDB. Exposes 19 MCP tools so Claude can query its own past. `longhand demo` for a no-install sandboxed walkthrough. +``` + +### §8.6 — X/Twitter follow-up thread (post-release) + +**Tweet 1/4:** +``` +v0.9.2 shipped. + +The friction I kept hearing: "I want to try it but not on my real ~/.claude history." + +New `longhand demo` command — sandboxed walkthrough on a fake corpus in 60 seconds. No install commitment. + + pip install longhand + longhand demo +``` + +**Tweet 2/4:** +``` +Since the v0.9.0 launch in April: + +• Live ingestion (Stop hook) — sessions show up in recall WHILE you're still working +• Plan history as first-class data +• Reconciler launchd job (opt-in) for hard crashes +• 19 MCP tools (was 16) with annotations + outputSchema +• SafeSkill 93/100, GLAMA A-tier + +228 tests across Python 3.10–3.13. +``` + +**Tweet 3/4:** +``` +The design argument hasn't changed. + +Claude Code already writes every tool call, every file edit, every thinking block to JSONL. Longhand reads those files verbatim. SQLite + ChromaDB on your laptop. No API. No summaries. No vendor. + +"AI memory crisis" was solvable in 2000. +``` + +**Tweet 4/4:** +``` +Try it without committing anything: + + pip install longhand + longhand demo + +Or wire it to your real Claude Code: + + longhand setup + +MIT. https://github.com/Wynelson94/longhand +``` + +### §8.7 — Newsletter pitch (v0.9.2) + +``` +Hi [name], + +Quick update on Longhand — the lossless local memory layer for Claude Code I pitched after the April launch. v0.9.2 ships today with a `longhand demo` command that runs a sandboxed walkthrough on a fake corpus in 60 seconds, so users can preview the cross-session recall behavior without committing to install on their real session history. + +For [newsletter name] readers, the angle that might resonate: every other "AI memory" tool in the space asks an LLM to summarize your past. Longhand does the opposite — Claude Code already writes verbatim JSONL of every session; Longhand just indexes those files into local SQLite + ChromaDB. Zero API calls. Zero summarization. 19 MCP tools exposed back to Claude itself. + +Since April: 733/wk → 175/wk PyPI installs (organic only; no paid distribution). v0.9.0 added live ingestion + plan history + reconciler. v0.9.1 added MCP tool annotations + outputSchema for SafeSkill 93/100 + GLAMA A-tier. 228 tests across Python 3.10–3.13. + +Repo: https://github.com/Wynelson94/longhand +PyPI: https://pypi.org/project/longhand/ + +Open to whatever format works — quick mention, guest piece, AMA, or just a quote for a roundup. + +— Nate Nelson +BlackSheep OI +``` + +--- + +## §7 — Newsletter pitch template + +**Subject:** Longhand — lossless local memory for Claude Code, no API calls + +**Body:** +``` +Hi [name], + +I shipped Longhand last month and it's picked up real traction — 336 unique cloners and 733 PyPI installs in the last 14 days, accelerating week-over-week. Thought it might fit [newsletter name]. + +Longhand is a local-first memory layer for Claude Code: every tool call, file edit, and thinking block from every session captured verbatim into SQLite + ChromaDB. Zero API calls, zero summarization. 16 MCP tools exposed back to Claude so it can query its own history in ~126ms. + +The design argument: the "AI memory crisis" is artificial. SQLite is from 2000, ChromaDB is two years old, both run on a laptop. The model doesn't need to carry memory — the disk does. + +Repo: https://github.com/Wynelson94/longhand +PyPI: https://pypi.org/project/longhand/ + +Happy to write a guest piece, do an AMA, or just feed you a quote for a roundup. Whatever fits the format. + +— Nate Nelson +BlackSheep OI +``` + +Customize subject + intro line per newsletter. Keep body ~130 words.