aixis a Manifest V3 Chrome extension that injects a fixed export button into Claude and ChatGPT conversation pages, extracts visible turns from live DOM trees, converts them to Markdown, and downloads a local.mdfile without a background worker, storage layer, or remote service.
- Primary references: Chrome Extensions MV3, Bun docs, Biome docs, tsgo
- Local source-of-truth files:
manifest.json,package.json,src/index.ts,src/platforms/claude.ts,src/platforms/chatgpt.ts,src/parsers/markdown.ts - Regression surface:
tests/integration/,tests/parsers/,tests/fixtures/ - Mirror contract:
AGENTS.mdis canonical;CLAUDE.mdandREADME.mdsymlink to it
.
├── src/
│ ├── index.ts content-script entrypoint and export flow
│ ├── platforms/ Claude / ChatGPT detection, selectors, extraction
│ ├── parsers/ DOM -> Markdown conversion and sanitization
│ ├── ui/ injected button, toast, and styles
│ └── utils/ download, filename, markdown, SPA navigation
├── tests/
│ ├── integration/ fixture-backed export flow coverage
│ ├── fixtures/ synthetic Claude and ChatGPT DOM fixtures
│ └── helpers/ happy-dom factories and fixture loaders
├── assets/ extension icons copied into dist/
├── dist/ generated unpacked-extension payload
└── AGENTS.md canonical repo instructions
- Start in
src/platforms/for site-compatibility work and insrc/parsers/for markdown fidelity changes - Treat
tests/fixtures/as the DOM contract anddist/as generated output
| Layer | Choice | Notes |
|---|---|---|
| Extension | Chrome Extension MV3 | one content script, no popup, no service worker |
| Runtime | TypeScript + browser DOM | Bun bundles src/index.ts for --target browser |
| Targets | Claude + ChatGPT | supports claude.ai, chatgpt.com, and legacy chat.openai.com |
| Testing | Bun Test + happy-dom | bunfig.toml preloads tests/setup.ts |
| Tooling | Biome + tsgo + Husky | lint, typecheck, commitlint, and lint-staged are local only |
| Release build | Bun + terser | minifies dist/contentScript.js and strips console / debugger |
bun install- install dependencies and Husky hooksbun run dev- watch-build onlydist/contentScript.js; it does not recopymanifest.jsonorassets/bun run build- produce the full unpacked extension indist/with JS, manifest, and iconsbun run util:lint- read-only Biome lint passbun run util:types- read-onlytsgo --noEmitbun test --concurrent- run unit and integration tests against fixture DOMbun run util:check- write-enabled full gate; runs format, lint, types, and tests
src/index.ts: detects the host, injects shared styles, retries button insertion, hooks SPA navigation by patchinghistory.pushState/history.replaceState, then executes click -> extract -> compose -> downloadsrc/platforms/types.ts:PlatformConfigis the only adapter contract; adapters return orderedMessage[]plus a derived titlesrc/platforms/claude.ts: only activates on/chat/<uuid>routes, prefers.standard-markdown*blocks, extracts artifact cards, reads action-bar timestamps, and backfills missing timestamps across adjacent turnssrc/platforms/chatgpt.ts: accepts full conversation turns or/c/and/g/routes, deduplicates nested.markdown/.whitespace-pre-wrapblocks, and turns attached files into Markdown attachment sectionssrc/platforms/selectors.ts: centralizes primary and fallback selectors; keep DOM drift isolated here before widening parser logicsrc/parsers/markdown.ts: central DOM -> Markdown dispatcher for paragraphs, headings, lists, tables, blockquotes, code fences, inline code, images, task lists, and separator-based message compositionsrc/parsers/sanitizer.tsandsrc/utils/dom.ts: clone-and-prune hidden or non-content nodes before conversion; ChatGPT intentionally uses a custom selector set that keeps checkbox inputs and role-based imagessrc/ui/andsrc/utils/download.ts: render a fixed overlay button plus toast, then download viaBloband object URL with no background messaging
manifest.jsoninjects the content script atdocument_idleonhttps://claude.ai/*,https://chatgpt.com/*, andhttps://chat.openai.com/*- The extension is stateless at runtime: no Chrome storage, no service worker, no popup UI, and no network fetches in the shipped code
- Markdown metadata uses
new Date().toISOString()while filenames use local time inplatform-chat-YYYYMMDD-HHMMSS-<slug>.md - Generated artifacts live under
dist/:dist/contentScript.js,dist/manifest.json, anddist/assets/; regenerate them, do not edit by hand - Ignored scratch space exists under
.tmp/,tmp/,temp/, andcoverage/;.tmp/currently contains ad hoc HTML captures and screenshots that are not canonical test inputs - The existing symlink mirror is intentional:
CLAUDE.mdandREADME.mdshould keep pointing atAGENTS.md
- Use
@/for source imports and@tests/for test helpers; avoid relative TypeScript imports across the repo - Keep selector drift in
src/platforms/selectors.tsor adapter-local extraction helpers; when a platform DOM changes, add or update fixture coverage first - Preserve message order and duplicates; Claude timestamp backfill and ChatGPT chunk deduplication are intentional export behaviors
- Avoid depending on console output for debugging outcomes; existing debug logs are tolerated in source but removed from release builds by terser
- Commit messages must satisfy
commitlint.config.js: typesfeat|fix|refactor|docs|style|chore|test, scopesextension|parser|ui|platform|build|deps|docs
- Never hand-edit
dist/;bun run devonly refreshes JS andbun run buildregenerates the full unpacked extension - Keep
tests/fixtures/synthetic and reviewable; do not commit full live page captures, auth bootstrap blobs, sidebar history, or other account-scoped DOM dumps - Do not relax sanitize rules or hidden-node checks without proving exports stay clean on both platforms; they intentionally strip copy controls, system messages, and presentation-only DOM
- Be careful changing
src/utils/navigation.ts; it monkey-patches the history API globally and has no teardown path in production code - Do not rename files in
assets/without updatingmanifest.jsonand rebuildingdist/ - Automated tests do not exercise live Chrome injection, CSP behavior, or actual browser downloads; selector and UI work still needs a manual browser smoke test
- Read-only completion gate:
bun run util:lint,bun run util:types,bun test --concurrent - Build gate when changing shipped code, icons, or manifest:
bun run build - Use
bun run util:checkonly when you want the repo reformatted as part of the change; it runsutil:formatbefore lint, types, and tests - If you change
src/platforms/or parser behavior, update fixture-backed coverage intests/integration/ortests/parsers/and keeptests/fixtures/aligned with the new DOM assumptions - Manual smoke for platform changes: load
dist/as an unpacked extension, verify the button appears exactly once on one Claude/chat/<uuid>page and one ChatGPT/c/...or/g/...page, then confirm the exported markdown preserves order, code blocks, lists, tables, and attachment or artifact sections - No CI or GitHub workflow is present in the repository; local validation is the completion bar