diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d902880 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,124 @@ +# AGENTS.md — Recaps + +YouTube + podcast summarizer + library, served as a single-page app from a Node.js backend. Ships as a StartOS `.s9pk` (single-mode self-host) and as the public `recaps.cc` cloud (multi-mode tenants). + +## Stack + +- **Server**: Node.js (`type: module`, ES modules). The dev box currently runs `v25.6.1`; container runtime is whatever the `Dockerfile` pins — check before assuming. +- **Frontend**: One file, `public/index.html`, with vanilla JS embedded (no framework, no bundler). Render is a render-string-into-`innerHTML` loop driven by a module-scoped `state` object. +- **DB**: SQLite via `better-sqlite3`. Multi-mode only; single-mode keeps everything on the filesystem. +- **Packaging**: `@start9labs/start-sdk` under `startos/` — version graph at `startos/versions/index.ts`. +- **Deps of note**: `@anthropic-ai/sdk`, `@google/genai`, `openai`, `nodemailer`, `express`, `@keysat/licensing-client` (vendored at `vendor/keysat-licensing-client`). + +## Commands + +Run from repo root unless noted. + +| Action | Command | +|---|---| +| Dev server (single-mode default) | `cd server && npm run dev` | +| Prod server | `cd server && npm start` | +| Run all tests | `cd server && npm test` | +| Run one test file | `cd server && node --test --test-reporter=spec test/.test.js` | +| Run one test by name | `cd server && node --test --test-reporter=spec --test-name-pattern='' test/.test.js` | +| Build `.s9pk` (x86) | `make x86` | +| Bump version (interactive) | `make bump` | +| Install to local StartOS | `make install` *(see Always/Never — bump first; the binary is `start-cli` under the hood)* | +| Lint | TODO — no `lint` script in `server/package.json`. Add one if you want it. | +| Type-check | TODO — there's a top-level `tsconfig.json` but the server is all `.js`. Confirm intent before assuming. | + +Mode is selected at boot via the `RECAP_MODE` env var: `single` (default) or `multi`. + +## Directory layout + +``` +server/ + index.js main HTTP + SSE entry; mounts every route + providers/ relay.js, gemini.js, openai.js, anthropic.js, ollama.js, + openai-compatible.js, whisper.js — each implements the + provider interface in providers/index.js + anon-trial.js multi-mode trial-cookie minting + IP cap + tenant-credits.js multi-mode signed-in-tenant credit pool + history.js per-scope library save/load + REST handlers + config.js StartOS config snapshot + server-side API-key resolver + db.js SQLite schema apply + getDb() handle (multi-mode only) + billing-routes.js multi-mode self-serve purchase: /api/billing/{plans,buy,status}; + Bitcoin (BTCPay inline Lightning) + card (Zaprite) rails + subscription-reminders.js daily expiry-reminder scan → sendMail (multi-mode) + smtp.js StartOS System-SMTP transport (magic links + reminders) + test/ node --test files +public/ + index.html the whole single-page app, ~10k lines vanilla JS + auth.html standalone magic-link landing page (multi-mode) +startos/ + manifest/ StartOS package manifest + versions/.ts one file per shipped version + index.ts version graph + actions/ operator-facing StartOS Actions +docs/ design notes; treat as in-progress, not authoritative +bin/bump-version.sh used by `make bump` and `make deploy` +vendor/keysat-licensing-client/ local-link Keysat SDK +``` + +## Conventions + +- **Plain language over jargon**, especially for git / packaging / dev-tooling steps. +- **Don't be sycophantic.** Push back when something doesn't add up. +- **Honest reports.** A failing test/build is a failure, even if pre-existing or unrelated. Don't fold it into a "success" summary. +- **Diff size matches change scope.** Small reviewable diffs, not sweeping rewrites. +- **Comments explain WHY, not what.** No narrating self-evident code. No referencing tasks/PRs/callers in source — that rots. +- **Match the file's own style** over any default of your own. The frontend's vanilla-JS shape is intentional; don't reach for a framework. +- **Write the test alongside the change** when the area already has tests (`server/test/*.test.js`). The repo uses the built-in `node --test`. +- **Plans persist in `docs/`** when scoped + named (e.g., `docs/per-tenant-subscriptions-plan.md`); ephemeral planning lives in conversation/tasks, not Markdown files. + +### Conventions for this codebase specifically + +- **Relay is the modern default provider.** The legacy "must have a Gemini API key configured" gate is dead — server-side callers should pick `relay` when configured, fall back to `gemini` only if a local key exists, otherwise surface a clear user-facing error. The frontend stores the choice in localStorage; the server can't read it. +- **Sanitize operator-internal language at error boundaries.** Strings like "Spark Control", "parakeet", "vLLM", LAN IPs, `*.local` URLs come from the sibling relay and must not reach cloud users. +- **Multi-mode credit gates fire BEFORE the pipeline.** See `/api/process` for the order — admin → license → free tenant → trial → anonymous-mint. Don't reorder without reading the comment block. +- **Trial IP cap is per-IP for IPv4, per-/64 prefix for IPv6.** Dual-stack home networks would otherwise bypass it via privacy-extension address rotation. +- **`safeFilename()` already exists** for any user-content → on-disk path. Use it; don't roll your own. +- **The relay owns cloud Pro/Max tier + expiry** (core-decoupling; `docs/core-decoupling-plan.md`). In multi-mode, paid status is `users.tier` — cached from the relay, keyed by the Recaps user-id — NOT a per-user Keysat license. Don't gate cloud paid features by `keysat_license`; the license only matters for self-hosted "take it home" portability. Cloud requests carry `X-Recap-User-Id` + the operator key; server-to-server tier reads/writes go through `providers/relay.js`. +- **Self-serve purchase has two rails, both prepaid (no auto-renew yet).** Bitcoin = a BTCPay invoice rendered as an INLINE Lightning QR on-screen — the relay fetches the BOLT11 server-to-server, so the buyer never loads BTCPay (replicate the buy-credits inline flow; do NOT redirect to a hosted checkout). Card = a Zaprite one-time hosted order (Zaprite's API has no recurring — see ROADMAP). Both settle webhooks land at the relay's `extendUserTier`; the frontend just polls `/api/billing/status`. Expiry reminders go out via the existing System-SMTP transport (`smtp.js`): the relay enumerates who's expiring (`GET /relay/expiring-subscriptions`), Recaps maps user-id → email and sends. +- **Tier credit allotments are operator-config-driven, never hardcoded.** The cards' "N relay credits each period" comes from the relay's tier-quota config (`credits_per_period` on `/relay/tier-plans`); `null` → "Unlimited". Don't bake a number or "Unlimited" into the UI. + +## Always + +- **Bump the version before `make install`.** StartOS dedupes sideloads by version string — installing the same version twice silently no-ops. Use `make bump` or edit `startos/versions/index.ts` + add a `vN.ts` file. Applies to EVERY iteration, even a one-line edit. +- **Add new version files to BOTH the import block AND the `other:` list** in `startos/versions/index.ts`, and update `current:` to the new version constant. +- **Ask before `make deploy` / `make redeploy`.** These push to the Start9 community registry — public-facing, attribution-tracked, irreversible from your laptop. `make install` is the safe iteration loop. +- **Verify mDNS resolution before blaming it** when `make install` fails. Substitute the operator's actual StartOS hostname (the `host:` field in `~/.startos/config.yaml`) and run `curl -sk "https://${STARTOS_HOST}/rpc/v1" -X POST -d '{}' -H 'Content-Type: application/json'` — if that reaches the box but `start-cli` doesn't on the same target, it's almost certainly **macOS Local Network privacy** blocking the third-party `start-cli` binary (Apple's `curl`/`ping` are exempt, so the box looks reachable). Tell: `node -e` TCP-connect to `:443` also gives `EHOSTUNREACH` while `curl` gets 200. Fix: System Settings → Privacy & Security → Local Network → enable the Claude app (restart Claude Code if it doesn't take). This often flips off after a Claude Code update. Details: memory `feedback_macos_local_network_install`. +- **Reference env-var names, never values.** Secrets live in `.env` / `.deploy.env` (both gitignored). Examples for new vars belong in `.deploy.env.example`. + +## Never + +- **No "Co-Authored-By"** trailers on commits, no mention of "Claude" in source files, comments, or commit messages. Commits are authored by the user. +- **Never claim `make install` succeeded without verifying it.** Confirm the `make` exit code is 0 AND the new version actually shows on the box (`start-cli package list`) — not just that the command ran (a `tail`/pipe can mask a non-zero exit). Installs DO work from this agent's shell now; the old "`start-cli` is blocked by the sandbox" framing was a misdiagnosis — it was macOS Local Network privacy, which is fixable (see the Always "verify mDNS" rule). +- **Never `make deploy` to the registry** without explicit per-action approval, even if a prior session ran one. +- **Never edit `startos/versions/.ts` for a version that's already been built and is being tested.** Add a new version file instead — operators may already have the prior `.s9pk` cached. +- **Don't add the relay's `internal-meetings` feature here.** That lives in the sibling `../recap-relay` repo. This repo is the client/library; the relay does diarization + clustering + meeting analysis. +- **Don't push to GitHub by default.** The configured remote is self-hosted Gitea unless the user says otherwise. +- **Don't pull `cookies.txt` into commits** — it's an operational yt-dlp artifact, not source. +- **Never modify `~/.startos/config.yaml`** without authorization (contains host credentials). + +## Adjacent repo + +- `../recap-relay` — the operator-side credit-metered service this client talks to. Owns Gemini/Parakeet/Sortformer routing, diarization, internal-meeting analysis, the operator dashboard at `/admin/*`. Private; ships via `make install` only, never `make deploy`. Reference it but do not change it from inside this repo. + +## Current state + +**Live on the operator's StartOS box** (app **0.2.155** + relay **0.2.124**, installed 2026-06-09): + +- **Self-serve purchase COMPLETE — all 5 phases** (`docs/self-serve-purchase-plan.md`). Signed-in cloud users buy Pro/Max themselves: "Pay with Bitcoin" renders an inline Lightning QR on-screen (no redirect); "Pay by card" mints a Zaprite one-time order (the card link shows only when the operator has configured Zaprite). Prepaid 30-day periods; the relay owns tier + expiry; both settle webhooks land at `extendUserTier`. Expiry-reminder emails (7d / 1d / lapsed) ride the existing System SMTP; operator test trigger: `POST /api/admin/reminders/run` with `{test_email}`. Tier cards show the real per-period credit allotment from the relay quota config (this box: Max = 120, Pro = 50). +- **Core-decoupling live** (relay owns cloud tier; `docs/core-decoupling-plan.md`) and **per-tenant subscriptions live** (`docs/per-tenant-subscriptions-plan.md`). +- The Bitcoin pill matches the standard purple; the relay-side **internal-meeting re-polish fix** (re-attributes topic summaries to the operator's corrected speaker names) shipped this session. + +**Pending real-world tests (operator):** +1. First on-device Bitcoin purchase — sign in as a Core tenant → Upgrade → Pay with Bitcoin → pay the inline invoice → badge flips. +2. Enable cards — run the relay's "Set Zaprite Connection" action (API key) + register the Zaprite webhook at `https:///relay/zaprite/webhook`. +3. Eyeball a reminder email via the test trigger above. + +**Known issues / open decisions** (details + next actions in `ROADMAP.md`): +- **Zaprite recurring isn't built** — Zaprite's API only does one-time orders. Card = prepaid until that's confirmed with Zaprite. +- **"Take Recaps home" is likely broken** for relay-tier users (no `keysat_license` after decoupling). +- **Cloud still has a free signed-in tier** (the simplification plan's "cloud paid-only" is unbuilt). +- No CI lint / type-check (unchanged). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..a35569d --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,33 @@ +# ROADMAP + +Longer-term backlog for Recaps. Near-term in-flight work and known issues live in `AGENTS.md` under **Current state**. + +## Near-term backlog + +- **Persist provider preference server-side.** `processItemInternally` currently runtime-detects (relay-if-configured / gemini-fallback) because the user's choice lives only in the client's `localStorage`. Persist it so a fresh-container rebuild or any non-browser caller (cron, background processor) picks the right path. Probably a single key in the StartOS config blob + a small migration to seed it from the first authenticated client. +- **Apply Export ▾ menu to the clip-collection panel.** The main view and history rows already have it; the clip collection still has the single legacy "Export PDF" button. Reuse the existing menu component. +- **CI lint + type-check.** No `lint` script in `server/package.json`; top-level `tsconfig.json` exists but the server is pure `.js`. Decide: add ESLint, adopt JSDoc-driven TS checking, or remove the empty `tsconfig.json`. +- **Surface failed auto-queue items in the dashboard.** Currently hidden by default behind a "Show all" toggle. Worth a small banner / count chip when failures exist so operators notice without hunting. +- **Zaprite recurring card billing (BLOCKED on Zaprite).** Grant wants card payments to DEFAULT to recurring (buyer can opt out at checkout). Zaprite's public API (`api.zaprite.com/openapi.json`) only creates one-time `/v1/orders` — recurring is a hosted/dashboard feature with no per-buyer metadata, no renewal webhook, and no billing-portal URL via API. The shipped card rail is one-time prepaid. UNBLOCK by confirming with Zaprite support whether the account can: (a) attach a per-buyer reference/metadata to a recurring checkout (so a payment maps to a Recaps user), (b) fire a webhook on each renewal charge (so we extend the tier each period), (c) expose a customer/billing-portal URL (for the chosen "link to Zaprite portal" cancel path). Decisions already made: no reminder emails for auto-renewing cards; a failed charge = lapse at period end (the relay's expiry-enforcement already does this — a missed renewal just doesn't extend `expires_at`). +- **Close the architecture-simplification gaps** (`docs/architecture-simplification-plan.md`). After core-decoupling + self-serve, these steps remain OPEN: **(8) "Take Recaps home"** — mint a fresh Keysat token on demand at click time; likely BROKEN today because relay-tier cloud users have no `keysat_license` for `/api/account/license-key` to return. **(10) cloud paid-only** — the free signed-in tier + signup-grant credits are still live; the plan wanted cloud to be paid-only with self-hosted as the free path (product call — confirm intent before building). **(5, partial) anon signup→Pro** still routes through `/api/license/purchase` + `pending_signups` (Keysat license) instead of the relay tier like the signed-in flow does. **(6, partial) tokenized renew** — the reminder email's renew link is `?renew=1` (requires sign-in); the plan wanted a one-time-token `/renew?token=…` for friction-free renewal. NOTE: the doc's Zaprite-*recurring* / cancel-button / Recaps-DB-owns-expiry parts were intentionally SUPERSEDED by the prepaid + relay-owns-tier model — don't build those. +- **Decide the Max tier-quota default.** The relay code default is `max.monthly: null` (unlimited) → cards render "Unlimited" on a fresh install. The operator set `max.monthly: 120` on their box via the Adjust-Tier-Quotas action (so cards show 120 there). Decide whether a metered number (e.g. 120) should be the shipped default in `recap-relay/server/config.js` — note it also enforces the ceiling, not just the card label. + +## Larger plans (already drafted in `docs/`) + +- `docs/architecture-simplification-plan.md` — broader simplification arc +- `docs/core-decoupling-plan.md` — separating the core summarize pipeline from billing / multi-tenant concerns +- `docs/per-tenant-subscriptions-plan.md` — moving subscription state into the per-user scope +- `docs/self-serve-purchase-plan.md` — buyer flow for Pro/Max and a la carte credits +- `docs/path-2b-and-path-1-interweave.md` — sequencing for the multi-tenant cloud meetings work (depends on the relay's Path 2A) + +Treat the `docs/` plans as the source of truth for those items; cross-reference rather than restating here. + +## Adjacent (lives in `../recap-relay`) + +The relay now has its own `AGENTS.md` + `ROADMAP.md` — track relay work there; this is just what the client surfaces or waits on. + +- **Speaker MERGE + re-run detection + re-polish — SHIPPED relay-side** (operator dashboard, live on the box at relay 0.2.124, 2026-06-13). Merge folds two clusters into one; re-run re-clusters at a new strictness to split over-merged speakers; re-polish rewrites topic summaries to corrected names. App-side UI for these is now unblocked if wanted. *(The relay tree is at 0.2.124 but uncommitted to git — see `../recap-relay/ROADMAP.md`.)* +- Cross-call speaker fingerprint memory (recognize the same voice across meetings) — not yet shipped. +- Phase 3 of Path 2A: multiple operator-editable meeting prompt sets (1on1 / all-hands / customer-interview / standup) selectable per upload — not yet shipped. + +Avoid building app-side UI for the unshipped items until the relay-side pieces land.