Files
antisocial/AGENTS.md
T
Kydoimos 045a312f14
CI / Typecheck, test, build (pull_request) Successful in 29s
Point agent docs at AGENTS.md as the source of truth
2026-09-16 11:42:40 -03:00

6.0 KiB

AGENTS.md

Self-hosted page that renders a social post (X, Threads, Instagram, Facebook, TikTok, Bluesky, Reddit) without the app, by driving real headless Chromium logged-out. No auth; private-network use only. Keep deployment specifics (hosts, clusters, registries, manifests) out of this repo.

AGENTS.md (this file) is the source of truth for agent instructions. CLAUDE.md is supplemental detail (adapter internals, per-platform quirks, publishing/signing). README.md is user-facing. On conflict, this file wins; executable sources win over all prose.

Commands

npm ci
npx playwright install chromium   # once
npm run dev                       # http://localhost:8080, runs src directly via type-stripping
npm run resolve -- '<post url>'   # ground truth: real browser, prints Post JSON or error
npm run probe -- '<adapter url>' [waitMs]  # what the page actually served (payload keys, DOM video/img, OG, challenge, filtered API traffic)
npm test                          # node:test vs test/fixtures/, no network
npm run typecheck                 # this IS the lint step — no ESLint
npm run build                     # tsc -> dist/

CI (.gitea/workflows/ci.yml) runs typecheck -> test -> build with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 (fixtures need no browser).

  1. npm run resolve -- '<url>' — ground truth.
  2. Classify before fixing: transient rate-limit (esp. TikTok — wait and retry) vs verification puzzle (expect redirect to /challenge/<id>) vs structured-payload miss (carousel count wrong / poster where video belongs = DOM fall-through) vs Facebook wrong-post (feed read instead of post — must become an error) vs Reddit post-ok-no-comments (.json refused, page fallback).
  3. npm run probe -- '<url the adapter loads>' — e.g. Instagram /embed/captioned/, not the post URL. Every adapter fix starts here.
  4. Fix adapter + capture a real-payload fixture in test/fixtures/ + assert in test/<platform>.test.ts. Fix without fixture regresses.
  5. npm test, then re-resolve end to end.

Architecture

Request → src/routes/post.tssrc/platforms/index.ts (prefix → adapter) → src/resolve.ts (cache/timeout/concurrency) → withPage (src/browser/pool.ts) → adapter → Post (src/types.ts) → src/render/post.ts.

Adding a platform: one file in src/platforms/ + one row in index.ts. Nothing downstream changes.

Rules agents get wrong

  • Extraction order is load-bearing: in-flight API response → inline payload → rendered DOM → OG tags. Keep it.
  • Post is Segment[], not one body. Bluesky/Threads emit author chains; isAnchor marks the linked post. Segment.quoted (X, Bluesky) keeps quoted author+text+media together — never lift quoted media into the quoter's media (false attribution).
  • Post.comments tree is Reddit-only. Renderer uses <details> per comment; nesting is what makes folding free.
  • Media is never linked to a CDN: register in src/media/registry.ts, serve /m/<id> with stored Referer/Cookie, forward Range. Exception: direct: true HLS (no playlist rewriting).
  • One Chromium, one persistent context — cookies/banners accumulate deliberately. Images/video/fonts are aborted at route layer (URLs/src attrs survive for DOM extraction).
  • src/platforms/scan.ts pulls balanced JSON by key, including escaped-inside-JS-string (Instagram). Payload present-but-empty usually means one more encoding level.
  • Facebook: narrow to the linked post via partsOfPost (id, or permalink_url/wwwURL for pfbid; unmatched id = failure, never read the shipped-along feed). Collect all claiming nodes (caption/author/files live in different blocks); post owner comes from actors/owner/video_owner/owner_as_page, never a bare author (that's a commenter).
  • X: decode pre-escaped text after slicing display_text_range (UTF-16 units into escaped text); expand t.co by matching shortlink text in entities.urls, not indices; rebuild quoted URL from handle + id_str.
  • Reddit: prefer .json (post + first comment page); cold profile gets a JS challenge — navigate once, retry, keep the cookie. replies is "" when empty. Video with sound (has_audio) must use hls_url direct; silent uses proxied MP4; poster is preview.images, never scrubber_media_url. Gallery order comes only from gallery_data. Comment images are tokens resolved via the comment's own media_metadata, with Giphy-id → i.giphy.com/media/<id>/giphy.gif fallback; render Markdown via src/render/markdown.ts (escape-first), never body_html.
  • TikTok: find whichever __UNIVERSAL_DATA_FOR_REHYDRATION__ scope holds itemStruct (key varies); CDN needs Referer + cookies; bare-path vm./vt. codes rebuilt in buildOriginalUrl.
  • Threads/Instagram share the media schema (meta-media.ts); Threads follow-up = author replying to self (vs stranger reply with same reply_to_author); Bluesky app calls getPostThreadV2, page fallback matches V1 only.
  • Never a bare error page: failed resolves render a card with platform + original URL + copy button.
  • Video sizing comes from the poster (empty SVG stand-in when missing); iOS audio needs the playback session in public/app.js.

Conventions

  • TS 7, ESM, .ts import specifiers (tsc rewrites to .js); node --experimental-strip-types runs src/test/bin directly. strict + noUnusedLocals + noUncheckedIndexedAccess enforced by typecheck.
  • public/ is served as-is: plain JS, not TS. public/browsers.js stays a separate module so tests can import it (allowJs).
  • Tests: node:test, fixture(payload) via test/helpers.ts, no network. Single-file run: node --test --experimental-strip-types test/reddit.test.ts.
  • All post text is untrusted: emit via html tagged template / linkify only, never string-concatenated markup. Comments explain why, not what the next line does.
  • Config is env-only with working defaults (src/config.ts); PROFILE_DIR persists browser state. Chromium needs >64Mi /dev/shm.