Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog

All notable changes to TweAI are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project loosely follows [SemVer](https://semver.org/).

## [Unreleased]

### Tooling
- `package.json` + `eslint.config.js` + `.prettierrc.json` (PR #9)
- `tools/build.mjs` packages `dist/` and `tweai-v<version>.zip` without `tweai-mcp-server/` or `docs/`
- `.github/workflows/checks.yml` runs lint + format + build on every PR
- `.github/workflows/release.yml` builds and publishes a GitHub release with the zip when a `v*` tag is pushed

### Architecture
- `selectors.js` introduces `window.TTASelectors` with multi-strategy DOM lookups (testid → semantic → structural) and a ring-buffer health counter in `chrome.storage.local.tta_selector_health` (PR #7)
- Provider abstraction: `callOpenAI` / `callGrok` / `callGemini` merged into a `PROVIDERS` registry + `aiFetch()` with `AbortController` timeout and 429/5xx retry with exponential backoff (PR #6)
- Service worker keep-alive via `chrome.alarms` while AI requests are in-flight; MCP responses cached in-memory for 5 min (PR #8)
- Gemini API key is now sent via `x-goog-api-key` header instead of `?key=` URL param

### Internationalization
- `_locales/en/messages.json` and `_locales/ru/messages.json` cover ~110 UI keys; `manifest.default_locale: "en"` (PR #5)
- `options.html` strings bound via `data-i18n` / `data-i18n-placeholder` / `data-i18n-html` / `data-i18n-aria-label`
- Built-in personas now ship with English `label` / `hint` (fallback if `chrome.i18n` is unavailable)

### Security
- `chrome.runtime.onMessage` validates `sender`: only own extension pages and `*.x.com` / `*.twitter.com` frames are allowed
- `http://localhost/*` / `http://127.0.0.1/*` moved from `host_permissions` to `optional_host_permissions`
- Dev-logger no longer reads `?tta_debug=1` or `localStorage 'tta_debug'` (both forgeable from x.com)
- `boot()` runs only in the top frame
- `document.execCommand('insertText')` consolidated into a single helper with InputEvent → execCommand → direct-assignment fallback chain
- Persona hint renders via DOM API (`replaceChildren`, `textContent`) instead of `innerHTML`

## [1.8.1] - 2026-05-19

Security & CWS hygiene patch (see [release notes](https://github.com/froggychips/tweai/releases/tag/v1.8.1)).

## [1.8] - 2026-05-17

Open-source launch.
93 changes: 93 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Contributing to TweAI

Thanks for considering a contribution! TweAI is a single-developer side-project that turned out useful enough to open up. PRs are welcome but I review at a hobby cadence.

## Project layout

```
.
├── manifest.json # MV3 manifest
├── background.js # service worker — AI calls, MCP, message dispatch
├── content_script.js # UI injection on x.com / twitter.com
├── selectors.js # window.TTASelectors with fallback DOM strategies
├── dev-logger.js # opt-in floating log overlay (dev/debug)
├── ad-blocker.js # promoted-post hider (separate content_scripts entry)
├── profile-scraper.js # author profile sniffer
├── options.html / .js / .css
├── styles.css # shared CSS injected into x.com
├── _locales/{en,ru}/messages.json
├── tools/build.mjs # dist/ + zip packager
├── tweai-mcp-server/ # Node MCP gateway (separate package; not bundled)
└── docs/
├── ARCHITECTURE.md
├── TROUBLESHOOTING.md
├── CWS_REVIEW.md
└── ...
```

`tweai-mcp-server/` is a separate Node project for an optional local gateway. It has its own `package.json` and is excluded from the extension zip.

## Local development

```bash
git clone https://github.com/froggychips/tweai.git
cd tweai
npm install
npm run lint
npm run format
npm run package # produces dist/ + tweai-v<version>.zip
```

Load `dist/` (or the repo root) via `chrome://extensions` → Developer mode → Load unpacked.

After every file change in the extension, click the reload icon on the TweAI card in `chrome://extensions`. Content scripts also need a tab reload on x.com to re-inject.

## Style

- Prettier handles formatting (`npm run format:fix` to apply). 100-col, single quotes, trailing commas, LF.
- ESLint is intentionally light: it catches typos and unused vars but doesn't enforce style.
- Comments: explain *why* something is non-obvious, not *what* the code does. Skip comments for code that reads cleanly on its own.

## DOM selectors

If you touch DOM lookups on x.com, **add or update a strategy in `selectors.js`** rather than embedding `data-testid="…"` in `content_script.js` directly. The selector module:

1. tries `data-testid` (level 1),
2. falls back to ARIA/semantic attributes (level 2),
3. then to structural selectors (level 3),
4. records every call into a ring buffer in `chrome.storage.local.tta_selector_health`.

This lets us tell *which* lookup broke after an X redesign without staring at silent failures.

## i18n

Strings that the user sees go through `_locales/{en,ru}/messages.json`. In HTML, use `data-i18n="key"` (or `data-i18n-placeholder` / `data-i18n-html` / `data-i18n-aria-label`). In JS, use the `i18n('key')` helper in `options.js` or `tta_i18n('key')` in `content_script.js` — both have an English fallback if the message is missing.

New strings: add the key to both `en/messages.json` and `ru/messages.json`. Don't ship hardcoded Russian text in default-locale UI — that's a Chrome Web Store blocker.

## Provider additions

To add a new AI provider, register it in the `PROVIDERS` registry in `background.js` with `keyField`, `label`, `buildRequest(apiKey, model, messages, temperature)` and `parseResponse(j)`. The dispatcher `callAI()` picks it up automatically. Make sure the new key field is also in `baseDefaults` and surfaced in `options.html`.

## Commits & PRs

- One logical change per PR. If something feels like two PRs, it is.
- Title imperative: "Add X" / "Fix Y" / "Refactor Z". Body explains *why*, not *what*.
- For DOM changes, paste a screenshot of x.com before/after.
- For provider/API changes, describe how you verified the wire format (DevTools Network screenshot is fine).
- CI must be green (lint, format, build). Don't `--no-verify` past it; ask if the hook is wrong.

## Reporting bugs

[GitHub Issues](https://github.com/froggychips/tweai/issues). Include:

- Chrome version
- TweAI version (`manifest.json` or `chrome://extensions`)
- Reproduction steps
- DevTools console output if there's a JS error
- Output of "Run health check" in TweAI options
- For DOM-breakage reports: also dump `chrome.storage.local.get('tta_selector_health', console.log)` so we see which selector level fell over

## Security issues

See [SECURITY.md](SECURITY.md). Don't open public issues for vulnerabilities.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,22 @@ The extension wraps tweet text in delimiters and explicitly instructs the model

## Releasing

Build a Chrome Web Store zip:

```bash
zip -r tweai-v1.7.zip . \
-x '.git/*' '.gitignore' 'docs/*' 'env.json' \
'*.md' 'env.template.json' 'package.json' 'node_modules/*'
npm install
npm run lint
npm run format
npm run package # → dist/ + tweai-v<version>.zip
```

Launch-day copy (Chrome Store listing, Product Hunt, tech-twitter thread, HN, Reddit) lives in [`docs/LAUNCH.md`](docs/LAUNCH.md). Demo GIF instructions are in [`docs/RECORDING.md`](docs/RECORDING.md).
`tools/build.mjs` builds the CWS-ready zip with the right inclusions (no `tweai-mcp-server/`, no `docs/`). The same script runs in [`.github/workflows/release.yml`](.github/workflows/release.yml) — pushing a `v*` tag uploads a GitHub release with the zip attached automatically.

CWS submission checklist: [`docs/CWS_REVIEW.md`](docs/CWS_REVIEW.md). Launch-day copy: [`docs/LAUNCH.md`](docs/LAUNCH.md). Demo GIF: [`docs/RECORDING.md`](docs/RECORDING.md).

## Contributing

PRs welcome. Run `npx --yes web-ext lint --source-dir .` before opening one — the only acceptable lint output is the 2 Firefox-specific errors and 2 warnings (we target Chrome MV3 only).
See [CONTRIBUTING.md](CONTRIBUTING.md) for project layout, local dev, and PR conventions. Architecture overview lives in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). If something on x.com stops working, start with [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).

The project has zero runtime dependencies. Source is plain MV3 JavaScript, no bundler, no minifier.
The project has zero runtime dependencies. Source is plain MV3 JavaScript; only `eslint` and `prettier` ship as dev-deps.

## Contact

Expand Down
98 changes: 98 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Architecture

A bird's-eye view of how TweAI's moving parts fit together. The goal here is to give a maintainer enough mental model to find the right file when something breaks.

## Top-level flow

```
┌────────────────────┐
user on x.com│ content_script.js │ (per-tab, isolated world)
│ + selectors.js │
│ + dev-logger.js │
│ + ad-blocker.js │
│ + profile-scraper │
└────────┬───────────┘
│ chrome.runtime.sendMessage
┌────────────────────┐
│ background.js │ service worker
│ (MV3 SW) │
└────┬───────────┬───┘
│ │
AI providers │ │ MCP gateway (optional)
┌─────────────────┴┐ ┌┴──────────────────────┐
│ api.openai.com │ │ 127.0.0.1:<port>/... │
│ api.x.ai (Grok) │ │ tweai-mcp-server/ │
│ generativelang… │ │ │
│ (Gemini) │ └───────────────────────┘
└──────────────────┘
```

## Components

### Service worker (`background.js`)

- **Settings store** (`baseDefaults` + `chrome.storage.sync`)
- **Persona prompts** (`PERSONAS` constant)
- **Provider registry** — `PROVIDERS = { openai, grok, gemini }`. Each entry has `keyField`, `label`, `buildRequest`, `parseResponse`. The dispatcher `callAI(provider, model, messages, temperature)` picks the right entry by name.
- **`aiFetch()`** — fetch wrapper with `AbortController` timeout (30s default) + exponential backoff retry on 429/5xx.
- **Token budgeting** (`checkBudget` / `addUsage`) — daily quota stored under `usage:YYYY-MM-DD` in `chrome.storage.local`.
- **MCP client** — `mcpFetch` with 5-min in-memory cache; `mcpGetProfile`, `mcpGetRecentTweets`.
- **SW keep-alive** — `chrome.alarms` registered while `inflightCount > 0`; `chrome.storage.session.tta_inflight_requests` tracks pending calls for restart visibility.
- **Message handlers** (`handlers` object) — `TTA_GET_PREFS`, `TTA_TRANSLATE_TWEET`, `TTA_EXPLAIN_TWEET`, `TTA_GENERATE_REPLY`, `TTA_LIST_PERSONAS`, `TTA_TEST_KEY`, `TTA_HEALTH_CHECK`, `TTA_MCP_*`, etc. Each handler is `async msg => result`; the listener validates `sender` origin before dispatch.

### Content script (`content_script.js`)

Injected on `x.com` / `twitter.com` at `document_start`. Runs only in the top frame (nested iframes skip `boot()`).

- **`boot()`** wires `MutationObserver` for the timeline, plus `scroll` / `popstate` / `visibilitychange` listeners (all funnel into `scheduleScan`).
- **`scan()`** finds every `<article>` and DM composer and attaches UI (translate label, AI explain/reply submenu, compose box, etc).
- **DOM lookups go through `window.TTASelectors`** — see `selectors.js`. Never embed `data-testid="…"` directly in this file.
- **AI calls** go to `background.js` via `chrome.runtime.sendMessage({ type: 'TTA_…' })`. The content script never talks to AI providers directly.

### `selectors.js`

`window.TTASelectors` is the only place that knows X's DOM. Each function tries:

1. `data-testid` (level 1 — precise, fragile),
2. ARIA / `lang` / `dir` attributes (level 2 — semantic),
3. structural CSS path (level 3 — last resort).

Every call records success level into a local ring buffer, persisted to `chrome.storage.local.tta_selector_health` for diagnostics. See [TROUBLESHOOTING.md](TROUBLESHOOTING.md#dom-selectors-have-degraded) for how to read it.

### Options page (`options.html` + `options.js`)

Single page, no modules. Reads/writes `chrome.storage.sync` via `storageGet` / `storageSet`. UI text comes from `_locales/<lang>/messages.json` via `data-i18n*` attributes and `applyI18n()` on `DOMContentLoaded`.

Onboarding wizard (`#tta-onboarding`) overlays on first run if `onboardingDone` is unset.

### Dev tooling

- `dev-logger.js` — floating log overlay; opt-in via `chrome.storage.local.ttaDebugLogs` or unpacked dev build (no `update_url`).
- `profile-scraper.js` — listens for `history.pushState` to detect profile navigation, scrapes basic data, stores in `chrome.storage.local.profileData`.
- `ad-blocker.js` — separate content script entry; reads `chrome.storage.local.adBlockerEnabled` and removes promoted posts.

### MCP gateway (optional, separate repo folder)

`tweai-mcp-server/` is a tiny Node + Express service that wraps X's GraphQL endpoints. The extension talks to it via `mcpFetch` when `prefs.mcpUrl` is set. The fallback chain in `content_script.js` is **DOM scan → MCP (background-side cached) → local storage cache**.

The MCP server is excluded from the Chrome Web Store package — users who want it install it themselves.

## Data persistence

| Where | What |
|---|---|
| `chrome.storage.sync` | User settings: API keys (encrypted by Chrome), models, persona, custom personas, language, MCP URL |
| `chrome.storage.local` | Token usage per day (`usage:YYYY-MM-DD`), profile cache, ad-blocker stats, dev-logger toggle, selector-health ring buffer |
| `chrome.storage.session` | In-flight request tracker (cleared on SW restart) |
| In-memory (SW) | MCP response cache (5 min TTL), inflight counter |

## What lives off-disk

Nothing. There is no backend server, no telemetry, no analytics. The extension's only outbound calls are:

- The AI provider you chose (OpenAI / Grok / Gemini),
- Google Translate (only if you enabled that path with a key),
- Your own MCP server (only if you configured `mcpUrl`).

If you see TweAI calling anything else in DevTools Network, that's a bug — please [file an issue](https://github.com/froggychips/tweai/issues).
76 changes: 76 additions & 0 deletions docs/CWS_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Chrome Web Store submission checklist

Internal checklist before pushing a new version to the Chrome Web Store. Walk through this top to bottom; every item should be either ✅ or have a comment on why it's skipped.

## Pre-submission

### Manifest hygiene

- [ ] `manifest_version: 3`
- [ ] `version` bumped from previous release (numeric SemVer, e.g. `1.8.1`)
- [ ] `default_locale: "en"` set; `_locales/en/messages.json` exists
- [ ] `name` and `description` reference `__MSG_*__` not hardcoded text
- [ ] `host_permissions` contains only what the extension actually calls
- [ ] `optional_host_permissions` used for everything user-toggled (MCP, localhost)
- [ ] No `<all_urls>` in matches or host_permissions
- [ ] Icons present at 16/48/128 px in `icons/` and referenced from `action` + top-level `icons`

### Code

- [ ] `npm run lint` passes (no errors)
- [ ] `npm run format` passes
- [ ] `npm run package` succeeds; resulting zip is < 10 MB
- [ ] Test the zip in `chrome://extensions` → Load unpacked from `dist/`
- [ ] No `console.log` spam in production paths (`dev-logger` is opt-in only)
- [ ] No hardcoded API keys, tokens, or URLs to dev infrastructure

### Documentation

- [ ] `CHANGELOG.md` has an entry for the new version
- [ ] `README.md` install instructions still work
- [ ] `PRIVACY.md` accurately describes data flow for any new features
- [ ] `SECURITY.md` threat model matches reality

### Store listing assets

- [ ] **At least one screenshot**, 1280×800 or 640×400 PNG/JPEG, showing TweAI in action on x.com (CWS rejects without this)
- [ ] Promotional tile 440×280 (optional but recommended)
- [ ] Updated short description (132 chars max)
- [ ] Updated long description (up to 16,384 chars; usually mirrors README "Features" section)
- [ ] **Privacy practices form** filled out in the dev dashboard:
- Does not collect personally identifiable information ✅
- Does not collect health information ✅
- Does not collect financial info ✅
- Authentication info: API keys stored locally only (declare this)
- User activity: not collected
- Web content: tweet text is sent to user-configured AI provider only

### Permissions justification

For every permission in `manifest.json`, have a 1-sentence reason ready (CWS asks for this):

| Permission | Justification |
|---|---|
| `storage` | Persist user settings, API keys, custom personas, token usage |
| `scripting` | Required for `chrome.scripting` calls from background script |
| `activeTab` | Quick action on the current X tab without `<all_urls>` |
| `webNavigation` | Detect SPA navigation on x.com to refresh injected UI |
| `clipboardWrite` | Copy generated replies to clipboard on user action |
| `tabs` | Open options page; iterate X tabs to broadcast settings updates |
| `alarms` | Keep service worker alive during long AI requests (>30s) |
| host_permissions: `api.openai.com`, `api.x.ai`, `generativelanguage.googleapis.com` | The user's AI provider endpoints |
| host_permissions: `x.com`, `*.x.com`, `twitter.com`, `*.twitter.com` | Inject UI on the target sites |
| optional `127.0.0.1`, `localhost` | User-toggled MCP gateway; requested at runtime if enabled |

## Submission

- [ ] Upload the zip from `npm run package`
- [ ] Fill out all required listing fields
- [ ] Submit for review

## Post-submission

- [ ] Tag the release in git: `git tag v<version> && git push origin v<version>`
- [ ] CI release workflow attaches the zip to a GitHub release automatically
- [ ] Monitor [CWS dev dashboard](https://chrome.google.com/webstore/devconsole/) for review feedback (typically 1–3 business days)
- [ ] Once approved, update README install link to the CWS listing
Loading
Loading