## 1. Project Overview & Quickstart (steipete/summarize)

## File: README.md

# Summarize 📝 — Link in, signal out.

[](https://github.com/steipete/summarize/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@steipete/summarize)
[](https://nodejs.org/)
[](LICENSE)

Summarize extracts clean text and produces summaries from web pages, files, YouTube videos, podcasts, and other audio or video. Use it from the Node.js CLI or from the Chrome Side Panel and Firefox Sidebar.



```console
$ summarize "https://example.com" --extract --plain
This domain is for use in documentation examples without needing permission. Avoid use in operations.
[Learn more](https://iana.org/domains/example)
```

## Install

Run it once without installing:

```bash
npx -y @steipete/summarize --version
```

Or install the CLI with npm or Homebrew:

```bash
npm install @steipete/summarize
```

```bash
brew install summarize
```

The npm package requires Node.js 24 or newer. Homebrew/core and the [macOS archives on GitHub Releases](https://github.com/steipete/summarize/releases/latest) provide standalone builds. See the [installation guide](docs/install.md) for optional media tools and platform notes.

## Quick start

Extract readable content without an API key:

```bash
summarize "https://example.com" --extract --plain
```

For a model-generated summary, configure a [supported provider](docs/llm.md) or use an authenticated coding CLI:

```bash
summarize "https://example.com" --cli codex
```

The default `auto` model chooses among configured providers. The [five-minute quickstart](docs/quickstart.md) covers API keys, local models, files, YouTube, podcasts, and JSON output.

## What it handles

| Input                        | Processing path                                                        |
| ---------------------------- | ---------------------------------------------------------------------- |
| Web pages                    | Readability extraction, with optional Markdown and Firecrawl fallbacks |
| PDFs, images, and text files | File-aware extraction and model attachments where supported            |
| YouTube and podcasts         | Published transcripts first, then configured transcription fallbacks   |
| Audio and video              | Local or cloud transcription, with optional speaker labels             |
| Video slides                 | Scene-based frames, optional OCR, and timestamped output               |

Use `--extract` when you only need clean content, `--json` for a stable automation envelope, and `--slides` for video frames. The [command reference](docs/commands/index.md) documents every flag.



## Browser extension

Install [Summarize Side Panel from the Chrome Web Store](https://chromewebstore.google.com/detail/summarize/cejgnmmhbbpdmjnfppjdfkocebngehfg) to summarize the active tab, chat with its content, and work with video transcripts and slides.

Direct mode runs without a companion service. Browser media uses Chrome capabilities for local transcription and slide extraction; the optional daemon adds CLI model backends, native media tools, OCR, shared caches, and Firefox media support.

To pair the installed CLI, copy the token shown by the extension and run:

```bash
summarize daemon install --token <TOKEN>
```

See the [extension setup](apps/chrome-extension/README.md) and [architecture and troubleshooting guide](docs/chrome-extension.md).

## Media and slides

Summarize prefers existing captions and podcast transcripts before it transcribes media. Native `ffmpeg`, `yt-dlp`, and `tesseract` extend codec, YouTube slide, and OCR support; a bundled WebAssembly FFmpeg path covers common media without a native install.



The focused guides describe [YouTube extraction](docs/youtube.md), [media routing](docs/media.md), [slides](docs/slides.md), and [local ONNX transcription](docs/nvidia-onnx-transcription.md).

## Models and configuration

Model IDs use `provider/model` names. Summarize works with configured API providers, OpenAI-compatible endpoints, Ollama, OpenRouter free models, and authenticated coding CLIs such as Codex, Claude, Gemini, OpenClaw, and GitHub Copilot.

Configuration lives at `~/.summarize/config.json`; command-line flags take precedence. Inspect the effective setup without exposing keys:

```bash
summarize status
```

Read the [model guide](docs/llm.md), [automatic selection rules](docs/model-auto.md), [CLI provider guide](docs/cli.md), and [configuration reference](docs/config.md) for the full setup.

## Library

For programmatic extraction without the CLI dependency surface, install [`@steipete/summarize-core`](https://www.npmjs.com/package/@steipete/summarize-core):

```bash
npm install @steipete/summarize-core
```

The package exports focused content and prompt entry points. See the [core package README](packages/core/README.md) for usage.

## Documentation

Start with the [documentation index](docs/README.md), then follow the guide for your task:

- [Installation](docs/install.md) and [quickstart](docs/quickstart.md)
- [Commands](docs/commands/index.md) and [configuration](docs/config.md)
- [Website extraction](docs/website.md) and [Firecrawl fallback](docs/firecrawl.md)
- [YouTube](docs/youtube.md), [media](docs/media.md), and [slides](docs/slides.md)
- [Agent skill](.agents/skills/summarize/SKILL.md) for automated workflows

## Development

Requires Node.js 24 and pnpm 10.34.5.

```bash
pnpm install --frozen-lockfile
pnpm build
pnpm check
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for repository layout and extension workflows.

## License

MIT. See [LICENSE](LICENSE).

---

## File: apps/chrome-extension/docs/firefox.md

# Firefox Compatibility Guide

This document details Firefox-specific implementation notes, API compatibility findings, and known differences between the Chrome and Firefox versions of the Summarize extension.

## Chrome API Usage Investigation

### Standard WebExtensions APIs (Compatible)

The following Chrome APIs are used throughout the extension and have direct Firefox equivalents via the `browser.*` namespace. WXT automatically polyfills these:

#### Core Extension APIs

- **`chrome.runtime`**: Message passing, extension info, connections
  - `runtime.onMessage`, `runtime.sendMessage`
  - `runtime.onConnect`, `runtime.connect`, `runtime.Port`
  - `runtime.getURL`, `runtime.getManifest`
  - `runtime.openOptionsPage`
  - ✅ **Firefox compatible** (all methods supported)

- **`chrome.tabs`**: Tab management and queries
  - `tabs.query`, `tabs.get`, `tabs.create`, `tabs.update`
  - `tabs.sendMessage`
  - `tabs.onActivated`, `tabs.onUpdated`
  - ✅ **Firefox compatible**

- **`chrome.storage`**: Persistent and session storage
  - `storage.local.get`, `storage.local.set`
  - `storage.session` (used for ephemeral data)
  - `storage.onChanged`
  - ⚠️ **Mostly compatible** - `storage.session` requires Firefox 115+

- **`chrome.scripting`**: Dynamic content script injection
  - `scripting.executeScript`
  - ✅ **Firefox compatible** (MV3)

- **`chrome.windows`**: Window management
  - `windows.getCurrent`, `windows.create`, `windows.update`
  - ✅ **Firefox compatible**

- **`chrome.webNavigation`**: Navigation events
  - `webNavigation.onHistoryStateUpdated`
  - ✅ **Firefox compatible**

- **`chrome.permissions`**: Runtime permissions
  - `permissions.contains`, `permissions.request`
  - ✅ **Firefox compatible**

### Chrome-Specific APIs (Requires Attention)

#### 1. Side Panel API (Primary Incompatibility)

**Chrome usage** (`wxt.config.ts:56, 77-79`, `background.ts:1583`):

```typescript
// Manifest
side_panel: {
  default_path: 'sidepanel/index.html',
}

// Runtime API
chrome.sidePanel?.setPanelBehavior?.({ openPanelOnActionClick: true })
```

**Firefox equivalent**: `sidebar_action` API (Firefox 131+)

```typescript
// Manifest override needed
sidebar_action: {
  default_panel: 'sidepanel.html',
  default_title: 'Summarize',
  default_icon: 'assets/icon-128.png'
}
```

**Migration notes**:

- Firefox sidebar is always visible in the sidebar (not a side panel that slides in)
- No equivalent to `setPanelBehavior` - sidebar is opened manually
- Same HTML content can be reused (sidepanel.html)
- UI may need minor adjustments for Firefox sidebar dimensions

**Files affected**:

- `wxt.config.ts` - Needs Firefox manifest override
- `src/entrypoints/background.ts:1583` - setPanelBehavior call should be Chrome-only

#### 2. Debugger API (Advanced Features)

**Usage** (`background.ts:407-480`, `automation/tools.ts:336-366`):

```typescript
chrome.debugger.attach({ tabId }, "1.3");
chrome.debugger.sendCommand({ tabId }, method, params);
chrome.debugger.detach({ tabId });
```

**Firefox compatibility**: ✅ **Supported** but may have behavioral differences

- Firefox exposes a related debugger API, but the current Firefox build omits the permission.
- Debugger-backed native input and the explicit debugger tool are unavailable in this build.

#### 3. UserScripts API (Optional)

**Usage** (`automation/userscripts.ts:14-16`, `background.ts`, `automation/repl.ts:142-171`):

```typescript
chrome.userScripts;
chrome.permissions.contains({ permissions: ["userScripts"] });
```

**Firefox compatibility**: ⚠️ **Limited support**

- Firefox has experimental `browser.userScripts` support
- Available in Firefox 128+ behind `extensions.userScripts.enabled` pref
- Less mature than Chrome implementation
- **Implementation**: Feature-detect and gracefully degrade (see `automation/userscripts.ts`)
  - `getUserScriptsStatus()` checks API availability
  - `buildUserScriptsGuidance()` provides user-friendly error messages
  - Automation features show clear guidance if API unavailable

### Storage Quota Considerations

**Current usage pattern**: Caching summaries, settings, and session data

**Chrome quotas**:

- `storage.local`: ~10 MB (can request more with `unlimitedStorage`)
- `storage.session`: ~10 MB

**Firefox quotas**:

- `storage.local`: 10 MB default (same as Chrome)
- `storage.session`: 10 MB (Firefox 115+)

**Action required**:

- Monitor cache size in production
- Implement LRU eviction if approaching limits
- Consider adding warnings at 80% capacity

## Manifest Differences

### Required Changes for Firefox

**Chrome manifest** (current):

```json
{
  "permissions": ["sidePanel", ...],
  "side_panel": {
    "default_path": "sidepanel/index.html"
  }
}
```

**Firefox manifest override** (needed):

```json
{
  "permissions": ["tabs", "activeTab", "storage", ...],
  "sidebar_action": {
    "default_panel": "sidepanel/index.html",
    "default_title": "Summarize"
  }
}
```

**Permissions to verify**:

- Remove `sidePanel` (Chrome-only)
- Verify `debugger` permission works in Firefox
- Verify `userScripts` in `optional_permissions` is handled gracefully

## Service Worker vs Background Page

**Current**: Chrome MV3 service worker (`background.ts`)

**Firefox MV3**: Also uses service workers (Firefox 109+)

- Same lifecycle as Chrome
- Same event-driven model
- SSE connection handling should work identically

**Testing priorities**:

1. Verify service worker restarts properly
2. Test SSE streaming during worker lifecycle
3. Verify port-based communication (sidepanel ↔ background)

## Content Script Timing

**Current injection strategy**:

- `extract.content.ts`: Readability-based extraction
- `hover.content.ts`: Hover summaries
- `automation.content.ts`: Automation features

**Firefox compatibility**: ✅ **Should work identically**

- WXT handles content script registration
- Same `run_at` timing behavior
- Same message passing APIs

## SSE/EventSource Support

**Usage**: Streaming summaries from daemon via SSE (`src/lib/sse.ts`)

**Testing needed**:

- Verify EventSource works in Firefox background context
- Test reconnection logic on Firefox
- Verify CORS headers work with Firefox origin

## Known Behavioral Differences

### 1. Sidebar vs Side Panel UX

**Chrome Side Panel**:

- Slides in from the right
- Programmatically opened via `sidePanel.setPanelBehavior()`
- Toggles on toolbar icon click

**Firefox Sidebar**:

- Always visible in sidebar area (left side by default)
- Programmatically controlled via `sidebarAction.toggle()`, `open()`, `close()`
- Toggles on toolbar icon click (implemented)
- Keyboard shortcut: `Ctrl+Shift+U` (customizable by user)
- Different width constraints than Chrome

**Impact**: Minimal - both browsers now support programmatic control and icon-click toggling

### 2. Extension Context URLs

**Chrome**: `chrome-extension:///...`
**Firefox**: `moz-extension:///...`

**Impact**: Minimal - WXT handles this via `runtime.getURL()`

### 3. Developer Tools Integration

**Chrome**: DevTools open via `chrome://extensions`
**Firefox**: DevTools open via `about:debugging`

**Impact**: Documentation only

## Testing Strategy

### Browser Compatibility Tags

Add tags to Playwright tests:

```typescript
// @cross-browser - Runs on both Chrome and Firefox
test('@cross-browser should generate pairing token', ...)

// @firefox - Firefox-specific tests
test('@firefox should use sidebar API', ...)

// @chrome - Chrome-specific tests
test('@chrome should use Side Panel API', ...)
```

### Test Execution

```bash
# All tests (both browsers)
pnpm test

# Firefox only
pnpm test:firefox

# Chrome only
pnpm test:chrome
```

### Critical Test Scenarios

1. **Pairing flow**: Token generation and daemon connection
2. **Summary streaming**: SSE stream rendering in sidebar
3. **Content extraction**: Readability on various sites
4. **Auto-summarize**: Navigation triggers
5. **Settings persistence**: storage.local across restarts
6. **Permissions**: Optional userScripts access; debugger features unavailable

## Development Workflow

### Building for Firefox

```bash
# Development mode (watch)
pnpm dev:firefox

# Production build
pnpm build:firefox

# Output location
.output/firefox-mv3/
```

### Loading in Firefox

1. Open `about:debugging#/runtime/this-firefox`
2. Click "Load Temporary Add-on"
3. Select `apps/chrome-extension/.output/firefox-mv3/manifest.json`

**Note**: Temporary add-ons are removed on browser restart

### Debugging

**Console logs**:

- Background script: `about:debugging` → This Firefox → Inspect
- Content scripts: Regular DevTools Console (per-page)
- Sidebar: Right-click sidebar → Inspect

**Common issues**:

- **"Error: Extension is invalid"**: Check manifest syntax
- **"Loading failed"**: Check console for missing permissions
- **Sidebar not rendering**: Verify `sidebar_action` in manifest

## Distribution

### Temporary Installation (Current)

- Use `about:debugging` → Load Temporary Add-on
- Extension removed on Firefox restart
- Suitable for development and early beta testing

### Future: AMO (Add-ons.mozilla.org)

When ready for public distribution:

1. Submit to AMO for review
2. Code signing required (automatic via AMO)
3. Update mechanism via AMO (similar to Chrome Web Store)

## Implementation Checklist

- [x] Investigate Chrome API usage
- [x] Document Chrome-specific APIs
- [x] Create WXT Firefox target configuration
- [x] Add `sidebar_action` manifest override
- [x] Implement toolbar icon click handler with `sidebarAction.toggle()`
- [x] Add keyboard shortcut support (`Ctrl+Shift+U`)
- [x] Handle userScripts gracefully if unavailable (checks availability, shows guidance)
- [x] Add browser compatibility test tags
- [x] Configure Playwright for Firefox
- [x] Run full test suite on Firefox build
- [x] Verify Firefox build succeeds (.output/firefox-mv3/)
- [x] Verify manifest permissions are correct
- [x] Update user-facing documentation (README.md installation guide)
- [x] Manual testing in Firefox Developer Edition
- [x] Test sidebar rendering in Firefox
- [x] Verify SSE streaming works
- [x] Keep debugger-backed automation out of the Firefox manifest

## References

- [Firefox Extension Workshop](https://extensionworkshop.com/)
- [WebExtensions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)
- [Firefox Sidebar API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction)
- [WXT Framework - Multi-Browser Support](https://wxt.dev/guide/multi-browser.html)
- [Firefox Extension Debugging](https://extensionworkshop.com/documentation/develop/debugging/)

## Open Questions

1. **UserScripts API**: Automation features require userScripts - graceful degradation implemented with user guidance
2. **Sidebar width**: May need CSS adjustments for Firefox sidebar dimensions (requires manual testing)
3. **Testing coverage**: Aiming for 100% feature parity where browser APIs allow
4. **Distribution timeline**: When to submit to AMO?
5. **Keyboard shortcut conflicts**: Does `Ctrl+Shift+U` conflict with common Firefox shortcuts?

---

**Last updated**: 2026-01-02
**Status**: ✅ Implementation complete - Ready for manual testing in Firefox

## Recent Changes (2026-01-02)

### Sidebar Control Implementation

Firefox **DOES support** programmatic sidebar control via the `sidebarAction` API! The implementation includes:

- **Toolbar Icon Click** (`background.ts:1589-1599`):
  - Detects Firefox builds using `import.meta.env.BROWSER === 'firefox'`
  - Adds `action.onClicked` listener that calls `browser.sidebarAction.toggle()`
  - Toggles sidebar visibility just like Chrome's side panel behavior

- **Keyboard Shortcut** (`wxt.config.ts:105-115`):
  - Added `_execute_sidebar_action` command to manifest
  - Default: `Ctrl+Shift+U` (Windows/Linux)
  - Mac: `Command+Shift+U`
  - Users can customize this in Firefox settings: `about:addons` → Extensions → Manage Extension Shortcuts

- **Manifest Changes** (`wxt.config.ts:61`):
  - Firefox builds use standard WebExtensions permissions (no special permissions needed)
  - Chrome builds continue to use `sidePanel` permission

**User Experience**:

1. **Click toolbar icon**: Toggles sidebar open/close
2. **Keyboard shortcut**: `Ctrl+Shift+U` (or `Cmd+Shift+U` on Mac) toggles sidebar
3. **Customizable**: Users can change the shortcut in Firefox's extension settings

---

## File: apps/chrome-extension/README.md

# Summarize (Browser Extension)

Browser extension for Chrome and Firefox that streams AI-powered summaries directly into your browser's sidebar/side panel.

**Supported browsers**:

- Chrome 120+ (Side Panel) - Auto-opens on toolbar icon click
- Firefox 140+ (Sidebar) - Toggle with toolbar icon or `Ctrl+Shift+U`

Docs + setup: `https://summarize.sh`

## Build

- From repo root: `pnpm install`
- Chrome dev: `pnpm -C apps/chrome-extension dev`
- Firefox dev: `pnpm -C apps/chrome-extension dev:firefox`
- Prod build (Chrome): `pnpm -C apps/chrome-extension build`
- Debugger-enabled automation build (Chrome): `pnpm -C apps/chrome-extension build:automation`
- Prod build (Firefox): `pnpm -C apps/chrome-extension build:firefox`
- Build both: `pnpm -C apps/chrome-extension build:all`

## Install in Chrome (Unpacked)

Step-by-step:

1. Build the extension:
   - `pnpm -C apps/chrome-extension build`
2. Open Chrome → go to `chrome://extensions`
   - Or Chrome menu → Extensions → “Manage Extensions”
3. Turn on **Developer mode** (top-right toggle).
4. Click **Load unpacked**.
5. Select the folder: `apps/chrome-extension/.output/chrome-mv3`
6. You should now see “Summarize” in the extensions list.
7. (Optional) Pin the extension (puzzle icon → pin), then click it to open the Side Panel.

Developer mode is required for loading unpacked extensions.

## Install in Firefox (Temporary Add-on)

Step-by-step:

1. Build the Firefox extension:
   - `pnpm -C apps/chrome-extension build:firefox`
2. Open Firefox → go to `about:debugging#/runtime/this-firefox`
   - Or Firefox menu → More tools → "This Firefox" (under "Debugging")
3. Click **Load Temporary Add-on**
4. Navigate to and select: `apps/chrome-extension/.output/firefox-mv3/manifest.json`
5. You should now see "Summarize" in the extensions list
6. Open the sidebar using any of these methods:
   - **Click the Summarize toolbar icon** (toggles sidebar open/close)
   - **Keyboard shortcut**: `Ctrl+Shift+U` (Windows/Linux) or `Cmd+Shift+U` (Mac)
   - **Menu**: View → Sidebar → Summarize

**Customize keyboard shortcut** (optional):

- Go to `about:addons` → Extensions → ⚙️ (gear icon) → Manage Extension Shortcuts
- Find "Summarize" and click the current shortcut to change it

**Note**: Temporary add-ons are removed when Firefox restarts. For permanent installation, the extension needs to be signed via AMO (Firefox Add-ons).

## AI and Media Runtimes

The extension separates its AI connection from media/slide extraction:

- **Direct**: works immediately with Auto using Chrome's built-in Gemini Nano Summarizer API and extractive fallback. A dismissible hint offers the optional daemon for faster media, OCR, and broader capabilities. Auto calls a configured OpenAI, OpenRouter, Anthropic, Gemini, xAI, Z.AI, NVIDIA, MiniMax, GitHub Models, Ollama, or overridden compatible endpoint directly from Chrome. Gemini Nano can also be selected explicitly. Provider-backed chat, automation, and hover summaries work without the daemon. Keys stay in `chrome.storage.local` and are sent only to the selected provider.
- **Daemon**: uses the local Summarize daemon and its configured providers, CLI fallbacks, cache, and diagnostics. Explicitly selecting Gemini Nano still keeps summaries on-device while daemon-only capabilities remain available.

Media/slides can independently use **Browser** or **Daemon**. Browser media uses MediaBunny with native WebCodecs for fetchable video slides up to 128 MB, summarizes each slide with Gemini Nano, and transcribes captionless YouTube videos with local multilingual Whisper. The AI models download on first use and are cached by Chrome. Daemon media adds native tools, configurable transcription providers, OCR, broader media support, and Firefox media support.

## Optional Daemon (Pairing)

1. Install `summarize` (choose one):
   - `npm i @steipete/summarize` (requires Node.js 24+)
   - `brew install summarize` (macOS, Linux)
2. Under **Options → Runtime → Daemon**, click **Enable local companion** and approve Chrome's
   optional **Communicate with cooperating native applications** permission. Switching either
   runtime to **Daemon** also starts this explicit permission flow. The side panel's **Connect**
   daemon hint opens this Runtime setup view directly.
3. Switch the AI connection or media runtime to **Daemon**, then copy the pairing token and install command from the extension.
4. Open Terminal:
   - macOS: Applications → Utilities → Terminal
   - Windows: Start menu → Terminal (or PowerShell) — **right-click → Run as administrator**
   - Linux: your Terminal app
5. Paste the command from the Setup screen and press Enter.
   - Installed binary: `summarize daemon install --token <TOKEN> --port 8787`
   - Repo/dev checkout: `pnpm summarize daemon install --token <TOKEN> --port 8787 --dev --extension-id `
   - The install registers native host `com.steipete.summarize` for the exact Web Store extension ID.
   - Non-default port: replace `8787`, then enter the same port under **Options → Runtime → Daemon → Port**.
6. Back in your browser, the Daemon runtime setup screen should disappear once the daemon is running.
7. Verify / troubleshoot:
   - `summarize daemon status`
   - `summarize daemon restart`

Chrome communicates with the daemon only through the optional native host. The manifest retains
loopback access for configured Direct local providers, but those requests never enter the daemon
bridge. The npm-installed Windows CLI still needs a packaged native-host `.exe` before Daemon mode
can work there. Direct and Browser modes are unaffected.

Company administrators can keep Direct and Browser modes available while blocking all daemon
access with Chrome policy; see [`docs/chrome-enterprise.md`](../../docs/chrome-enterprise.md).

## Optional Website Automation

Summarization, chat, and browser media do not require Chrome's `userScripts` or `debugger`
permissions. Website automation is off by default. **Options → Enable automation permissions**
requests optional `userScripts` access from an explicit user click so user-requested `browserjs()` /
REPL code can run in the page's main world.

Chrome does not allow `debugger` to be declared optional. The standard Chrome build omits it and
hides the debugger tool; `pnpm -C apps/chrome-extension build:automation` creates the separate
debugger-enabled build for native click/type/key input and the explicit debugger tool. That build
declares `debugger` as required, attaches only while executing a debugger-backed command, and then
detaches.

## Length Presets

- Presets match CLI: `short|medium|long|xl|xxl` (or custom like `20k`).
- Tooltips show target + range + paragraph guidance.
- Source of truth: `packages/core/src/prompts/summary-lengths.ts`.

---

## File: docs/agent.md

---
title: "Agent / daemon"
kicker: "apps"
summary: "Automation agent for the Chrome side panel (daemon-backed)."
read_when:
  - "When working on automation tools, /v1/agent, or the side panel agent loop."
---

# Automation Agent (Side Panel + Daemon)

Summarize can run as a **website automation agent** inside the Chrome side panel. This is **optional** and **gated by a checkbox** in Options.

Scope:

- **Off (default):** chat is Q&A only (no tools).
- **On:** chat runs a tool-capable agent for web automation.

Explicit exclusions (per product direction): **no update checker**, **no tutorial/welcome flow**, **no proxy config**, **no API key dialog**. All model calls go through the **local daemon**.

## Architecture (High Level)

1. **Side panel** maintains the agent loop + chat UI.
2. **Background** handles tab data, extraction, and tool execution.
3. **Content scripts** handle element picking and native-input bridge.
4. **Daemon** provides `/v1/agent` (SSE stream of chunks + final assistant message).

### Data Flow (Agent Loop)

- User sends a message in the side panel.
- Panel compacts history and sends `panel:agent` to background with:
  - `messages` (pi-ai Message[])
  - `tools` (names)
  - `summary` (optional current summary markdown)
- Background builds `pageContent` using the latest extract (summary + transcript/text + metadata).
- Background calls daemon `POST /v1/agent` (SSE).
- Daemon streams `chunk` events, then an `assistant` event (may include tool calls).
- Panel executes tool calls locally, appends `toolResult` messages, and repeats `/v1/agent` until no tool calls remain.

The daemon **never** executes tools. It only returns the next assistant message.

## Settings + Permissions

### Settings

`automationEnabled` (boolean) lives in `apps/chrome-extension/src/lib/settings.ts`.

- Options UI provides the toggle.
- When disabled, the tool list is empty and the daemon uses the **chat-only** prompt.

### Automation Permissions

Defined in `apps/chrome-extension/wxt.config.ts`:

- `userScripts` – optional; requested via Options for `browserjs()` main-world script execution.
- `debugger` – required only in `build:automation` for **native input** and the **debugger** tool.

Neither permission is needed for summarization. The standard Chrome build omits `debugger` because
[Chrome does not allow it to be optional](https://developer.chrome.com/docs/extensions/reference/api/permissions#optional_declarations),
and it hides debugger-backed tools. The separate automation build declares it as required.

#### Chrome: enable User Scripts (if needed)

1. `chrome://extensions`
2. Open extension details
3. Enable **Allow User Scripts**
4. Reload the tab

## Daemon Endpoint

### `POST /v1/agent` (SSE)

**Headers**

- `Authorization: Bearer <token>`
- `Content-Type: application/json`

**Body**

```
{
  "url": "https://...",
  "title": "Page title",
  "pageContent": "",
  "cacheContent": "",
  "messages": [/* pi-ai Message[] */],
  "model": "auto" | "openai/..." | "anthropic/..." | ...,
  "length": "short" | "xl" | "20k" | ...,
  "language": "auto" | "en" | "de" | ...,
  "tools": ["navigate", "repl", "ask_user_which_element", "skill", "debugger"],
  "automationEnabled": true
}
```

**Response (SSE, default)**

```
event: chunk
data: { "text": "..." }

event: assistant
data: { /* AssistantMessage */ }

event: done
data: {}

event: error
data: { "message": "..." }
```

**Response (JSON)**
Use `Accept: application/json` or `?format=json`.

```
{ "ok": true, "assistant": { /* AssistantMessage */ } }
```

### `POST /v1/agent/history`

Returns cached chat history for the same cache key as `/v1/agent`.

**Body**

```
{
  "url": "https://...",
  "pageContent": "",
  "cacheContent": "",
  "model": "auto" | "openai/..." | "anthropic/..." | ...,
  "length": "short" | "xl" | "20k" | ...,
  "language": "auto" | "en" | "de" | ...,
  "automationEnabled": true
}
```

**Response**

```
{ "ok": true, "messages": [/* Message[] */] }
```

### Model Resolution (Daemon)

- **Fixed model** (explicit `model`): parsed as `/<model>`. Provider base URL overrides come from config/env (OpenAI, Anthropic, Google, xAI, ZAI). OpenRouter uses OpenAI-compatible completions.
- **Auto model**: uses existing auto-selection logic (`buildAutoModelAttempts`), preferring API-key transports and then CLI fallback when available.
- **Synthetic models**: created for OpenAI-compatible base URLs (local/openrouter).
- `maxOutputTokens` defaults to 2048 or `maxOutputTokens` override.
- CLI models are supported as auto fallback and via explicit `cli/` or `cli//<model>` overrides.
- If the daemon still says no model is available after key/install changes, restart or reinstall it so the saved environment snapshot refreshes.

## Page Content Payload

`pageContent` is built from the latest extract and includes:

- **Summary** (optional): current summary markdown (truncated to settings cap).
- **Transcript/text**: timed transcript when available, otherwise extracted text.
- **Metadata**: URL/title, source (`page` vs `url`), extraction strategy, markdown provider, Firecrawl usage, transcript provider + cache status, media duration, word counts, truncation flags.

See `buildChatPageContent()` usage in `apps/chrome-extension/src/entrypoints/background.ts`.

## Tools

### 1) `navigate`

Navigate the active tab.

Params:

```
{ "url": "https://...", "newTab": false }
```

Result:

```
{ "finalUrl": "https://...", "title": "...", "tabId": 123 }
```

Notes:

- Uses `chrome.tabs.update` or `chrome.tabs.create`.
- Waits for tab status `complete` (15s timeout).

### 2) `repl`

Execute JavaScript in a sandbox, with `browserjs()` to run in the page context.

Params:

```
{ "title": "...", "code": "..." }
```

Result:

```
{ "output": "console/return output", "files": [{ "fileName": "...", "mimeType": "...", "contentBase64": "..." }] }
```

REPL environment:

- Runs in a **sandboxed iframe** (no DOM access to the panel).
- `browserjs(fn, ...args)` runs the function **in the page context**.
  - Uses `chrome.userScripts.execute` in the main world.
- `navigate({ url })` available inside the REPL (always use for navigation).
- `sleep(ms)` helper.
- Console output is captured and returned; return values are appended as `=> value`.
- `returnFile(name, content, mimeType)` or `returnFile({ fileName, content, mimeType })` attaches files to the tool result.

Safety:

- Navigation inside REPL code (`window.location`, `history`, etc.) is rejected. Use `navigate()` instead.

Page-context helpers (via `browserjs()`):

- Skills libraries are auto-injected when domain patterns match the active URL.
- If `debugger` permission is granted, native helpers are exposed:
  - `nativeClick(selector)`
  - `nativeType(selector, text)`
  - `nativePress(key)`
  - `nativeKeyDown(key)` / `nativeKeyUp(key)`

### 3) `ask_user_which_element`

Shows a click-to-select overlay and returns element metadata.

Params:

```
{ "message": "Optional guidance" }
```

Result (example):

```
{
  "selector": "#submit",
  "xpath": "//button[1]",
  "text": "Send",
  "tagName": "button",
  "attributes": { "type": "submit" },
  "html": "",
  "boundingBox": { "x": 20, "y": 120, "width": 180, "height": 40 }
}
```

Overlay UX:

- Hover highlights element under cursor.
- Click selects.
- ↑ / ↓ moves up or down the DOM tree.
- Esc cancels.

### 4) `skill`

CRUD for domain-specific libraries (stored in `chrome.storage.local`).

Storage keys:

- `automation.skills` (map of name → skill)
- `automation.skillsSeeded` (one-time default seed)

Actions:

- `list` (optionally filtered by URL)
- `get` (optionally includes library code)
- `create` / `rewrite`
- `update` (string replacement)
- `delete`

Default skills seed from `apps/chrome-extension/src/automation/default-skills.json`.

Matching:

- Glob-like domain patterns (supports `*` and `**`).
- Match is done against hostname + path (e.g. `github.com/*/issues`).

Notes:

- `update` is intended for in-place string replacements; use `rewrite` to rename.
- Skills libraries run inside `browserjs()` and must avoid navigation.

### 5) `debugger` (optional)

Runs JavaScript in the **main world** via the Chrome debugger. **Last resort** (shows Chrome debug banner).

Params:

```
{ "action": "eval", "code": "..." }
```

Result:

```
{ "text": "...", "details": { ... } }
```

### 6) `summarize`

Run the Summarize pipeline against a URL (summary or extract-only).

Params:

```
{
  "url": "https://...",
  "extractOnly": false,
  "format": "text",
  "markdownMode": "readability",
  "model": "openai/gpt-5-mini",
  "length": "short",
  "language": "auto",
  "prompt": "Optional override",
  "timeout": "2m",
  "maxOutputTokens": "2k",
  "noCache": false,
  "firecrawl": "auto",
  "preprocess": "auto",
  "youtube": "auto",
  "videoMode": "auto",
  "timestamps": false,
  "maxCharacters": 120000
}
```

Result (summary):

```
{ "output": "..." }
```

Result (extract-only):

```
{ "output": "...", "details": { "content": "...", "title": "...", "wordCount": 123, ... } }
```

Notes:

- URL-only (uses daemon URL pipeline).
- Use `extractOnly: true` + `format: "markdown"` to return extracted Markdown.
- When `format: "markdown"`, `markdownMode` defaults to `readability`.

## Native Input Bridge (Debugger Permission)

Native input events use the Chrome debugger protocol:

- `browserjs()` posts a message to the content script.
- Content script forwards to background (`automation:native-input`).
- Background attaches debugger and dispatches:
  - `Input.dispatchMouseEvent` (click)
  - `Input.insertText` (type)
  - `Input.dispatchKeyEvent` (press/keydown/keyup)

If permission is missing, the call fails and the tool reports the error.

## UX Notes

- Automation is opt-in via Options checkbox.
- Regular summarize flows remain unchanged.
- Tool results are treated as **data**, not instructions. The system prompt still asks the assistant to repeat important tool-derived info in plain text.
- Tool results are currently rendered in the chat UI for debugging; they may be hidden in the future.
- When the active tab URL changes during a conversation, the panel appends a **navigation tool result** with the new URL, title, and matching skills. This keeps the agent aware of user-driven navigation.
- REPL executions that call `browserjs()` show a small overlay with an **Abort** action; aborting stops the current agent loop (best-effort).

## Where Things Live

- **Daemon**: `src/daemon/server.ts`, `src/daemon/agent.ts`
- **Automation tools**: `apps/chrome-extension/src/automation/`
- **Element picker + native input bridge**: `apps/chrome-extension/src/entrypoints/automation.content.ts`
- **Background agent proxy**: `apps/chrome-extension/src/entrypoints/background.ts`
- **Side panel agent loop**: `apps/chrome-extension/src/entrypoints/sidepanel/`
- **Options toggle**: `apps/chrome-extension/src/entrypoints/options/`

---

## File: docs/cache.md

---
title: "Cache"
kicker: "internals"
summary: "CLI cache design, keys, config, and eviction."
read_when:
  - "When changing cache behavior, keys, or defaults."
---

# Cache (design)

Lightweight, CLI-only SQLite cache. Single DB file.

## Goals

- Avoid repeated transcripts/extractions/summaries.
- No file sprawl; bounded disk usage.
- Safe defaults; easy opt-out.
- Native SQLite only (Node 24 + Bun).

## Storage

- Default path: `~/.summarize/cache.sqlite`
- Override: `cache.path` in config.
- SQLite pragmas: WAL, NORMAL sync, busy timeout, incremental vacuum.

## Media cache (downloads)

Separate file cache for downloaded media (yt-dlp, direct media URLs). This is **not** the SQLite DB.

- Default path: `~/.summarize/cache/media`
- TTL: 7 days
- Size cap: 2048 MB
- Config: `cache.media` (see below)
- CLI: `--no-media-cache` disables media caching only
- Note: `--no-cache` **does not** disable the media cache

## What we cache

- **Transcripts**
  - key: `sha256({url, namespace, fileMtime?, formatVersion})` (local file paths include `fileMtime` for invalidation)
- **Extracted content** (URL → text/markdown)
  - key: `sha256({url, extractSettings, formatVersion})`
- **Summaries**
  - key: `sha256({contentHash, promptHash, model, length, language, formatVersion})`
  - cache hit even if URL differs (content hash wins).
  - `contentHash` comes from the `<content>` block actually sent to the model (so slide timelines / transcript extras affect the key), or from attached binary bytes when the prompt content is empty.
- **Slides** (manifest + on-disk images in the slides output dir)
  - key: `sha256({url, slideSettings, formatVersion})`

## Keys / hashes

- `sha256` from Node `crypto` / Bun `crypto`.
- `contentHash` from normalized prompt `<content>` (fallback: normalized extracted content when no tagged block exists).
- `promptHash` from instruction block (custom prompt or default).
- `formatVersion`: hardcoded constant to invalidate on prompt format changes.

## Config

```json
{
  "cache": {
    "enabled": true,
    "maxMb": 512,
    "ttlDays": 30,
    "path": "~/.summarize/cache.sqlite",
    "media": {
      "enabled": true,
      "maxMb": 2048,
      "ttlDays": 7,
      "path": "~/.summarize/cache/media",
      "verify": "size"
    }
  }
}
```

Defaults: `enabled=true`, `maxMb=512`, `ttlDays=30`, `path` unset.

## CLI flags

- `--no-cache`: bypass summary cache reads + writes (LLM output). Extract/transcript caches still apply.
- `--cache-stats`: print cache stats and exit.
- `--clear-cache`: delete cache DB (and WAL/SHM); must be used alone.

## Eviction policy

- TTL sweep on read/write.
- Size cap: if DB > `maxMb`, delete oldest entries by `last_accessed_at` until under cap.
- Optional count cap if needed later.

Media cache eviction:

- TTL sweep on read/write.
- Size cap: evict least-recently-used files until under cap.
- `verify` controls integrity checks: `size` (default), `hash`, or `none`.

## Notes

- No extension cache (daemon uses CLI cache).
- No third-party SQLite deps.
- Transcript namespace currently includes the YouTube mode (e.g. `yt:auto`, `yt:web`).

---

## File: docs/chrome-extension.md

---
title: "Chrome extension"
kicker: "apps"
summary: "Chrome side panel extension + daemon architecture, setup, and troubleshooting."
read_when:
  - "When working on the extension, daemon, or side panel UX."
---

# Browser Side Panel (Chrome + Firefox Extension + Daemon)

Goal: Chrome **Side Panel** (“real sidebar”) summarizes **what you see** on the current tab. Panel open → navigation → auto summarize (optional) → **streaming** Markdown rendered in-panel.

Quickstart:

- Build/load extension: `apps/chrome-extension/README.md`
- Direct mode works immediately without a daemon. Auto uses a configured provider, otherwise Gemini Nano when available with extractive fallback.
- Configured direct providers support summaries, chat, automation, hover summaries, and public-URL extraction without a daemon. Provider keys stay in extension-local storage.
- Browser media provides daemonless transcription, slide extraction, and Gemini Nano per-slide summaries. OCR, process/log tools, CLI fallbacks, and broader native media support require the daemon.
- Optional: install summarize for daemon-backed media support:
  - `npm i @steipete/summarize`
  - `brew install summarize` (macOS, Linux)
- Firefox sidebar build: `pnpm -C apps/chrome-extension build:firefox` (load via `about:debugging` → temporary add-on)
- Open side panel → copy token install command → run for daemon mode:
  - `summarize daemon install --token <TOKEN> --port 8787` (macOS: LaunchAgent, Linux: systemd user, Windows: Scheduled Task)
  - Non-default port: replace `8787`, then set the same value in **Options → Runtime → Daemon → Port**.
- Chrome asks for optional Native Messaging access only when the user explicitly enables a Daemon
  runtime. Retained loopback host access serves configured Direct local providers only, not daemon
  traffic. Managed deployments can browser-disable both boundaries; see
  [Chrome enterprise policy](chrome-enterprise.md).
- The side panel's daemon **Connect** hint opens **Options → Runtime** so users can verify the
  permission, token, port, and native-host setup in one place.
- Verify:
  - `summarize daemon status`
  - Restart (if needed): `summarize daemon restart`

Firefox notes:

- Sidebar UX differs from Chrome’s side panel (persistent sidebar instead of slide-in panel).
- Firefox loading is smoke-tested with Mozilla `web-ext`; Playwright page-level Firefox tests remain diagnostic-only. See `apps/chrome-extension/tests/README-firefox.md`.
- Compatibility details: `apps/chrome-extension/docs/firefox.md`.

Dev (repo checkout):

- Use: `pnpm summarize daemon install --token <TOKEN> --dev --extension-id ` (copy the ID from `chrome://extensions`; autostart runs `src/cli.ts` with no `dist/` build required).
- E2E (Playwright): `pnpm -C apps/chrome-extension test:e2e`
  - First run: `pnpm -C apps/chrome-extension exec playwright install chromium`
  - Chromium runs headless by default.
  - Visible debugging: `SHOW_UI=1 pnpm -C apps/chrome-extension test:e2e` or `HEADLESS=0 pnpm -C apps/chrome-extension test:e2e`

## Troubleshooting

- “Daemon not reachable”:
  - `summarize daemon status`
  - Confirm the native host is reported as installed and Chrome granted the optional companion permission.
  - If the daemon uses a non-default port, confirm **Options → Runtime → Daemon → Port** matches the daemon configuration.
  - If the native host was just reinstalled for an unpacked extension ID, reload the extension and reopen the side panel.
  - Logs: `~/.summarize/logs/daemon.err.log`
- Windows install:
  - `summarize daemon install` registers a Scheduled Task via `schtasks /Create /XML`, which requires an **elevated** shell. Run it from an Administrator PowerShell/cmd; otherwise you'll see `schtasks create failed: ERROR: Access is denied.`
  - The task launches `wscript.exe //B //Nologo %USERPROFILE%\.summarize\daemon-launch.vbs`. If install completes but `/health` is unreachable, run `cscript //nologo %USERPROFILE%\.summarize\daemon-launch.vbs` to surface launcher errors, then `schtasks /Query /TN "Summarize Daemon" /V /FO LIST` to inspect the task state.
- macOS `launchctl bootstrap` errors (`Input/output error`, `Domain does not support specified action`):
  - `summarize daemon install` now tries both launchd domains (`gui/` then `user/`).
  - Install as your normal user (not root) so HOME + launchd domain match.
  - Re-run: `summarize daemon install --token <TOKEN>`.
- Windows containers:
  - `summarize daemon install --token <TOKEN>` starts the daemon for the current container session but does not create a Scheduled Task.
  - Chrome Daemon mode also needs the pending packaged Windows native-host executable. Do not expose the daemon port as a substitute; use Direct and Browser modes.
- “Need extension-side traces”:
  - Options → Logs → `extension.log` (panel/background events).
  - Enable “Extended logging” in Advanced settings for full pipeline traces.
- Captionless YouTube transcription is slow on first use:
  - Chrome downloads and caches the multilingual Whisper Tiny model on demand.
  - WebGPU is preferred; a slower WASM/CPU path is used when WebGPU is unavailable.
  - Offline model bundling and subscription/download workflows are not currently provided.
- “Stream ended unexpectedly” / empty chat response:
  - The daemon likely stopped mid-stream. Restart it, then click “Try again”.
  - `summarize daemon restart`
- Slide strip/gallery missing after a parallel slide run failure:
  - Click the slide notice “Try again” button. If the dedicated slide run never started, the extension now requests a fresh summarize+slides run instead of reusing the summary-only run.
- Tweet video not transcribing / no progress:
  - Ensure `yt-dlp` is available on your PATH (or set `YT_DLP_PATH`) and you have a transcription provider (`whisper.cpp` installed or `GROQ_API_KEY` / `ASSEMBLYAI_API_KEY` / `GEMINI_API_KEY` / `OPENAI_API_KEY` / `FAL_KEY` / `DEEPGRAM_API_KEY`).
  - Re-run `summarize daemon install --token <TOKEN>` to refresh the daemon env snapshot (launchd won’t inherit your shell PATH).
- “Could not establish connection / Receiving end does not exist”:
  - The content script wasn’t injected (yet), or Chrome blocked site access.
  - Chrome → extension details → “Site access” → “On all sites” (or allow the domain), then reload the tab.
- “Native messaging host not found”:
  - Re-run `summarize daemon install --token <TOKEN>` so the Chrome host manifest and launcher are refreshed.
  - Verify `summarize daemon status` reports `Chrome native messaging host: installed`.
  - The host is bound to Web Store ID `cejgnmmhbbpdmjnfppjdfkocebngehfg`; a normal unpacked build has a different ID unless a development host manifest explicitly allows it.

## Architecture

- **Extension (MV3, WXT)**
  - Side Panel UI: length + typography controls (font family + size), auto/manual toggle.
  - Background service worker: tab + navigation tracking, content extraction, starts summarize runs.
  - Chrome offscreen page: runs MediaBunny with native WebCodecs for ranged video slides and chunked local media transcription, plus an idle-evictable Whisper runtime.
  - Content script: extract readable article text from the **rendered DOM** via Readability; also detect SPA URL changes.
  - Direct provider adapters: OpenAI-compatible SSE, Anthropic Messages SSE, and Gemini SSE, normalized to the same summary/chat/tool-call contracts.
  - Panel page reconstructs daemon SSE streams carried through the service worker's native bridge.
- **Daemon (local, autostart service)**
  - HTTP server on `127.0.0.1` (default port `8787`).
  - Token-authenticated API.
  - Runs the existing summarize pipeline (env/config-based) and streams tokens to client via SSE.
  - Native host `com.steipete.summarize` proxies only `/health` and `/v1/*` to the configured daemon port and preserves SSE and binary responses.
  - Port is configurable: set **Options → Runtime → Daemon → Port** to match a daemon started with `summarize daemon install --port <n>`.

## Data Flow

1. User opens side panel (click extension icon).
2. Panel sends a “ready” message to the background (plus periodic “ping” heartbeats while open).
3. On nav/tab change (and auto enabled): background asks the content script to extract `{ url, title, text }` (best-effort).
4. In Chrome Browser mode, fetchable audio is decoded in bounded chunks through MediaBunny/WebCodecs and transcribed locally. YouTube prefers active-player/watch-page direct audio, then Android VR, buffered direct audio, and captured SABR.
5. In Direct mode, background uses Gemini Nano for Auto without provider credentials, or calls the selected provider and returns the normalized summary/chat result to the panel.
6. In Daemon mode, extension contexts send request metadata through the service worker, which checks managed policy and optional permission before connecting to the native host. The host proxies the authenticated `/v1/*` API and streams response chunks back to the caller.

## Auto Mode (URL + Page Text)

The extension always sends the same request shape:

- Always: `url`, `title`
- When available: extracted `text` + `truncated`
- `mode: "auto"`

The daemon decides the best pipeline:

- YouTube / video / podcast / direct media URLs → prefer **URL** pipeline (transcripts, yt-dlp, Whisper, readability, …).
- Normal articles with extracted text → prefer **page** pipeline (“what you see”).
- Fallback: if the preferred path fails before output starts, try the other input (when available).

## Video Selection (Page vs Video)

When the page contains embedded audio/video, the Summarize button gains a dropdown caret. Click the caret to pick Page vs Video/Audio. Selecting Video/Audio forces URL mode with transcript-first extraction (captions → yt-dlp/Whisper fallback). Selection is per-run (not persisted).

See `docs/media.md` for detection and transcript rules.

## Slides (Side Panel)

- The slides toggle lights up on media-friendly URLs (YouTube/watch|shorts, youtu.be, direct media) or when the page reports video/audio. Defaults to Video on those pages.
- Turning slides **on** refreshes the current summary and requests slide extraction. Chrome Browser mode uses MediaBunny with native WebCodecs and ranged reads for fetchable videos, then falls back to visible-tab capture. Daemon mode adds `yt-dlp`, native ffmpeg, and optional `tesseract` OCR.
- Active slide mode is slide-first:
  - vertical image/text cards
  - transcript-first text; OCR fallback
  - text can appear before slide images finish extracting
  - the large summary block is hidden while slide cards are active
- Slides stay off elsewhere and the toggle is disabled on non-media pages.

## SPA Navigation

- Background listens to `chrome.webNavigation.onHistoryStateUpdated` (SPA route changes) and `tabs.onUpdated` (page loads).
- Only triggers summarize when the side panel is open (and auto is enabled).

## Markdown Rendering

- Use `markdown-it` in the panel.
- Disable raw HTML: `html: false` (avoid sanitizing libraries).
- `linkify: true`.
- Render links with `target=_blank` + `rel=noopener noreferrer`.

## Timestamp Links (Chat)

- When timed transcripts are available, chat context includes `[mm:ss]` lines.
- Assistant is prompted to cite timestamps; clicking them seeks the current media (video/audio) while preserving play/pause state.

## Model Selection UX

- Settings:
  - AI connection (Options → Runtime): Direct | Daemon.
  - Direct provider and credential/base URL (Options → Runtime). `auto` uses the configured provider, otherwise Gemini Nano; an explicit provider prefix overrides the selection.
  - Media/slide runtime (Options → Runtime): Browser | Daemon.
  - Daemon port (Options → Runtime; default `8787`): must match the installed daemon port.
  - Model preset: `auto` | Gemini Nano | `free` | custom string (e.g. `openai/gpt-5-mini`, `openrouter/...`, `github-copilot/...`). Explicit Gemini Nano summaries stay on-device in either connection mode.
  - Length: `short|medium|long|xl|xxl` (or a character target like `20k`). Tooltips show target ranges + paragraph guidance (from `packages/core/src/prompts/summary-lengths.ts`).
  - Language: `auto` (match source) or a tag like `en`, `de`, `pt-BR` (or free-form like “German”).
  - Prompt override (advanced): custom instruction prefix (context + content still appended).
  - Auto summarize: on/off.
  - Hover summaries: on/off (side panel drawer, default off).
  - Typography: font family (dropdown + custom), font size (slider).
- Advanced overrides (Options → Advanced tab).
  - Leave blank to use daemon config/defaults; set a value to override.
  - Chat: enabled for a configured Direct provider or authenticated daemon.
  - Summary timestamps (advanced): include `[mm:ss]` links in summaries for media when available (default on).
  - Slides parallel (advanced): show summary first and extract slides in parallel (default on).
  - Slides OCR text (advanced): allow OCR text as a slide text source (default off).
  - Extended logging: send full input/output to daemon logs (requires daemon logging enabled).
  - Hover summary prompt: customize the prompt used for link hover summaries (prefilled; reset to default).
  - Pipeline mode: `page|url` (default auto).
  - Firecrawl: `off|auto|always`.
  - Markdown mode: `readability|llm|auto|off`.
- Preprocess: `off|auto|always`.
- YouTube mode: `no-auto|yt-dlp|web|apify` (default auto).
- Timeout (e.g. `90s`, `2m`), retries, max output tokens (e.g. `2k`).
- Process manager: live list of daemon-spawned tools (ffmpeg, yt-dlp, tesseract, etc.) with logs.
- Extension includes current settings in request; daemon treats them like CLI flags (`--model`, `--length`, `--language`, `--prompt`).

## Token Pairing / Setup Mode

Problem: daemon must be secured; extension must discover and pair with it.

- Side panel “Setup” state:
  - Generates token (random, 32+ bytes).
  - Shows:
    - `summarize daemon install --token <TOKEN> --port ` (macOS: LaunchAgent, Linux: systemd user, Windows: Scheduled Task)
    - `summarize daemon status`
  - “Copy command” button.
- Daemon stores paired tokens in `~/.summarize/daemon.json`.
- Extension stores token in `chrome.storage.local`.
- Chrome declares `nativeMessaging` as an optional named permission. It is requested only when the
  user enables the local companion. The registered host is bound to the exact Web Store extension
  ID, accepts only daemon API paths, and rejects a port that does not match `daemon.json`.
- If daemon unreachable or 401: show Setup state + troubleshooting.

## Daemon Endpoints

- `GET /health`
  - 200 JSON: `{ ok: true, pid }`
- `GET /v1/ping`
  - Requires auth; returns `{ ok: true }`
- `POST /v1/summarize`
  - Headers: `Authorization: Bearer <token>`
  - Body:
    - `url: string` (required)
    - `title: string | null`
    - `model?: string` (e.g. `auto`, `free`, `openai/gpt-5-mini`, ...)
    - `length?: string` (e.g. `short`, `xl`, `20k`)
    - `language?: string` (e.g. `auto`, `en`, `de`, `pt-BR`)
    - `prompt?: string` (custom instruction prefix)
    - `mode?: "auto" | "page" | "url"` (default: `"auto"`)
    - `maxCharacters?: number | null` (caps URL-mode extraction before summarization; ignored for extract-only unless explicitly provided)
    - `format?: "text" | "markdown"` (default: `"text"`)
    - `markdownMode?: "readability" | "auto" | "llm" | "off"` (only when `format: "markdown"`)
    - `preprocess?: "off" | "auto" | "always"` (markitdown/HTML preprocess)
    - `extractOnly?: boolean` (when `true`, returns extracted content without summarizing; requires `mode: "url"`)
    - `text?: string` (required for `mode: "page"`; optional for `auto`)
    - `truncated?: boolean` (optional; indicates extracted `text` was shortened)
  - 200 JSON: `{ ok: true, id }`
- `GET /v1/summarize//slides/events`
  - Headers: `Authorization: Bearer <token>`
  - SSE stream of slide updates (`slides`, `status`, `done`, `error`) independent of summary stream.
- `POST /v1/agent` (SSE by default; JSON via `Accept: application/json` or `?format=json`)
  - Headers: `Authorization: Bearer <token>`
  - Body:
    - `url: string` (required)
    - `title?: string | null`
    - `pageContent: string`
    - `cacheContent?: string` (used for cache key; defaults to `pageContent`)
    - `messages: Array<Message>` (pi-ai format)
    - `model?: string`
    - `length?: string` (e.g. `short`, `xl`, `20k`)
    - `language?: string` (e.g. `auto`, `en`, `de`)
    - `tools?: string[]`
    - `automationEnabled?: boolean`
  - SSE events:
    - `event: chunk` `data: { text }`
    - `event: assistant` `data: { ...assistant }`
    - `event: done` `data: {}`
    - `event: error` `data: { message }`
- `POST /v1/agent/history`
  - Headers: `Authorization: Bearer <token>`
  - Body:
    - `url: string` (required)
    - `pageContent: string`
    - `cacheContent?: string` (used for cache key; defaults to `pageContent`)
    - `model?: string`
    - `length?: string`
    - `language?: string`
    - `automationEnabled?: boolean`
  - 200 JSON: `{ ok: true, messages }`
- `GET /v1/summarize/:id/events` (SSE)
  - `event: chunk` `data: { text }`
  - `event: meta` `data: { model }`
  - `event: status` `data: { text }` (progress messages before output starts)
  - `event: metrics` `data: { elapsedMs, summary, details, summaryDetailed, detailsDetailed }`
  - `event: done` `data: {}`
  - `event: error` `data: { message }`

Notes:

- SSE keeps the extension simple + streaming-friendly.
- Requests keyed by `id`; daemon keeps a small in-memory map while streaming.

## Daemon Autostart

- CLI commands:
  - `summarize daemon install --token <token> [--port ]`
    - Writes `~/.summarize/daemon.json`
    - Installs platform autostart service; verifies `/health`
  - `summarize daemon uninstall`
  - `summarize daemon status`
  - `summarize daemon run` (foreground; used by autostart service)
- Ensure “single daemon”:
  - Stable service name + predictable unit/task path
  - `install` reuses the same daemon service and appends new tokens instead of invalidating older paired browsers

Platform details:

- macOS: LaunchAgent plist in `~/Library/LaunchAgents/<label>.plist`
- Linux: systemd user unit in `~/.config/systemd/user/summarize-daemon.service`
- Windows: Scheduled Task “Summarize Daemon” + `~/.summarize/daemon.cmd`

## Docs

- `docs/chrome-extension.md` (this file): architecture + setup + troubleshooting.
- Main `README.md`: link to extension doc and “Quickstart: 2 commands + load unpacked”.
- `apps/chrome-extension/README.md`: extension-specific dev/build/load-unpacked instructions.

## Status

- Implemented (daemon + CLI + Chrome extension).

---

## File: docs/cli.md

---
title: "CLI providers"
kicker: "models"
summary: "CLI model providers and config for Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, GitHub Copilot, Antigravity, and pi."
read_when:
  - "When changing CLI model integration."
---

# CLI models

Summarize can use installed CLIs (Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, GitHub Copilot, Antigravity, pi) as local model backends.

## Model ids

- `cli/claude/<model>` (e.g. `cli/claude/sonnet`)
- `cli/codex/<model>` (e.g. `cli/codex/gpt-5.5`)
- `cli/gemini/<model>` (e.g. `cli/gemini/flash`)
- `cli/agent/<model>` (e.g. `cli/agent/auto`)
- `cli/openclaw/<model>` (e.g. `cli/openclaw/main`)
- `openclaw/<model>` (alias for the same OpenClaw CLI path)
- `cli/opencode/<model>` (e.g. `cli/opencode/openai/gpt-5.4`)
- `cli/opencode` (use the OpenCode runtime default model)
- `cli/copilot/<model>` (e.g. `cli/copilot/gpt-5.2`)
- `cli/copilot` (use the Copilot CLI runtime default model)
- `cli/agy` (use the Antigravity CLI active session model)
- `cli/pi/<model>` (e.g. `cli/pi/sonnet`)
- `cli/pi` (use pi's configured default model)

Use `--cli [provider]` (case-insensitive) for the provider default, or `--model cli//<model>` to pin a model.
Bare `cli/codex` defaults to GPT-5.5; set `cli.codex.model` to override it.
Antigravity does not support per-call model selection in print mode, so use `cli/agy` without a model suffix. pi supports `cli/pi/<model>` or a configured `cli.pi.model`.
If `--cli` is provided without a provider, auto selection is used with CLI enabled.

Codex GPT Fast:

- `--model codex-fast` maps to `cli/codex/gpt-fast`.
- OpenAI fast mode is not a CLI model. Use `--model openai/gpt-5.5 --fast --thinking medium`; see `docs/openai.md`.

## Auto mode

Auto mode can prepend CLI attempts in two ways:

- `cli.enabled` set in config:
  - Auto always uses this list order.
  - Also acts as an allowlist for explicit `--cli` / `--model cli/...`.
- Auto CLI fallback (`cli.autoFallback`, default enabled):
  - Applies only to **implicit** auto (when no model is set via flag/env/config).
  - Default behavior: only when no API key is configured.
  - Default order: `claude, gemini, codex, agent, openclaw, opencode, copilot`.
  - Antigravity and pi are opt-in unless added to `cli.autoFallback.order`.
  - Remembers + prioritizes the last successful CLI provider (`~/.summarize/cli-state.json`).

Gemini CLI performance: summarize sets `GEMINI_CLI_NO_RELAUNCH=true` for Gemini CLI runs to avoid a costly self-relaunch (can be overridden by setting it yourself).

Set explicit CLI allowlist:

```json
{
  "cli": { "enabled": ["gemini"] }
}
```

Configure auto CLI fallback:

```json
{
  "cli": {
    "autoFallback": {
      "enabled": true,
      "onlyWhenNoApiKeys": true,
      "order": ["claude", "gemini", "codex", "agent", "openclaw", "opencode", "copilot"]
    }
  }
}
```

Disable auto CLI fallback:

```json
{
  "cli": { "autoFallback": { "enabled": false } }
}
```

Note: `--model auto` (explicit) does not trigger auto CLI fallback unless `cli.enabled` is set.

## CLI discovery

Binary lookup:

- `CLAUDE_PATH`, `CODEX_PATH`, `GEMINI_PATH` (optional overrides)
- `AGENT_PATH` (optional override)
- `OPENCLAW_PATH` (optional override)
- `OPENCODE_PATH` (optional override)
- `COPILOT_PATH` (optional override)
- `AGY_PATH` (optional override)
- `PI_PATH` (optional override)
- Otherwise uses `PATH`

Run `summarize status` to list enabled CLI providers whose executable is currently available.
`summarize status --verbose` also prints the resolved executable path and configured CLI model.
Missing or disabled providers are omitted.

## Attachments (images/files)

When a CLI attempt is used for an image or non-text file, Summarize switches to a
path-based prompt and enables the required tool flags:

- Claude: `--tools Read --dangerously-skip-permissions`
- Gemini: `--yolo` and `--include-directories <dir>`
- Codex: `codex exec --output-last-message ...` and `-i ` for images
- Agent: uses built-in file tools in `agent --print` mode (no extra flags)
- OpenCode: `opencode run --format json ... --file ` when a file/image path is required
- Copilot: `copilot -p `; passes `--model <model>` when one is configured
- Antigravity: `agy --print`; does not auto-approve tools for attachment prompts
- pi: `pi --print --mode json`; passes `--system-prompt`, sends prompt over stdin, and uses `--no-tools` for isolated summaries

Remote image and binary-file URLs are limited to native providers and CLI backends that can consume the staged attachment without broad tool approval. Local file inputs keep the existing path-based CLI behavior because they are operator-selected files rather than untrusted remote content.

## Config

```json
{
  "cli": {
    "enabled": [
      "claude",
      "gemini",
      "codex",
      "agent",
      "openclaw",
      "opencode",
      "copilot",
      "agy",
      "pi"
    ],
    "autoFallback": {
      "enabled": true,
      "onlyWhenNoApiKeys": true,
      "order": ["claude", "gemini", "codex", "agent", "openclaw", "opencode", "copilot"]
    },
    "codex": { "model": "gpt-5.5" },
    "gemini": { "model": "flash", "extraArgs": ["--verbose"] },
    "claude": {
      "model": "sonnet",
      "binary": "/usr/local/bin/claude",
      "extraArgs": ["--verbose"]
    },
    "agent": {
      "model": "auto",
      "binary": "/usr/local/bin/agent"
    },
    "openclaw": {
      "model": "main",
      "binary": "/usr/local/bin/openclaw"
    },
    "opencode": {
      "binary": "/usr/local/bin/opencode"
    },
    "copilot": {
      "binary": "/usr/local/bin/copilot"
    },
    "agy": {
      "binary": "/usr/local/bin/agy"
    },
    "pi": {
      "binary": "/usr/local/bin/pi"
    }
  }
}
```

Notes:

- CLI output is treated as text only (no token accounting).
- If a CLI call fails, auto mode falls back to the next candidate.
- Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`).
- Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection.
- Antigravity normal text summaries run `agy --print ` in a temporary cwd with `--sandbox`. Because current agy print mode requires the prompt in argv, large prompts are rejected before launch using platform-specific argv limits; use another CLI/provider for large or sensitive extracted content. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools.
- pi runs in JSON mode (`--print --mode json`) with `--no-tools`, `--no-context-files`, `--no-extensions`, `--no-skills`, `--no-session` for isolated summarization. It receives the summarize system prompt via `--system-prompt`, the summarize prompt over stdin, and reports usage/cost via JSONL events. Use `PI_PATH` to override the binary path.
- Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C <temp-dir> ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules.
- Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases.
- OpenClaw uses `openclaw agent --agent <model> --message  --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits.
- OpenCode uses `opencode run --format json`, streams prompt text over stdin, and uses the runtime default model when none is configured.
- Copilot uses `copilot -p`, treats stdout as plain text, and uses the runtime default model when none is configured.

## Quick smoke test (all CLI providers)

Use a tiny local text file and run each provider with a longer timeout (Gemini can be slower):

```bash
printf "Summarize CLI smoke input.\nOne short paragraph. Reply can be brief.\n" >/tmp/summarize-cli-smoke.txt

summarize --cli codex --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli claude --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli gemini --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli agent --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli openclaw --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli opencode --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli copilot --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli agy --plain --timeout 2m /tmp/summarize-cli-smoke.txt
summarize --cli pi --plain --timeout 2m /tmp/summarize-cli-smoke.txt
```

If Agent fails with auth, run `agent login` (interactive) or set `CURSOR_API_KEY`.

## Generate free preset (OpenRouter)

`summarize` ships with a built-in preset `free`, backed by OpenRouter `:free` models.
To regenerate the candidate list (and persist it in your config):

```bash
summarize refresh-free
```

Options:

- `--runs 2` (default): extra timing runs per selected model (total runs = 1 + runs)
- `--smart 3` (default): number of “smart-first” picks (rest filled by fastest)
- `--set-default`: also sets `"model": "free"` in `~/.summarize/config.json`

---

## File: docs/config.md

---
title: "Config"
kicker: "get started"
summary: "Config file location, precedence, and schema."
read_when:
  - "When adding config keys or defaults."
---

# Config

`summarize` supports an optional JSON config file for defaults.

## Location

Default path:

- `~/.summarize/config.json`

## Status

Inspect the effective model selection and positive provider setup:

```bash
summarize status
summarize status --verbose
summarize status --probe
summarize status --json
```

The command omits missing providers and never prints secret values. API providers appear as
`configured` when an effective key is present. Enabled CLI providers appear as `available` when
their executable can be resolved. `--probe` checks supported OpenAI-compatible/local model-list
endpoints without running inference and marks successful providers as `usable`.

## Precedence

For `model`:

1. CLI flag `--model`
2. Env `SUMMARIZE_MODEL`
3. Config file `model`
4. Built-in default (`auto`)

For output language:

1. CLI flag `--language` / `--lang`
2. Config file `output.language` (preferred) or `language` (legacy)
3. Built-in default (`auto` = match source content language)

For output length:

1. CLI flag `--length`
2. Config file `output.length`
3. Built-in default (`long`)

See `docs/language.md` for supported values.

For prompt:

1. CLI flag `--prompt` / `--prompt-file`
2. Config file `prompt`
3. Built-in default prompt

For environment variables:

1. Process environment variables
2. Config file `env`
3. Legacy config file `apiKeys` (mapped to env names)

For UI theme:

1. CLI flag `--theme`
2. Env `SUMMARIZE_THEME`
3. Config file `ui.theme`
4. Built-in default (`aurora`)

## Format

`~/.summarize/config.json`:

```json
{
  "model": { "id": "google/gemini-3-flash" },
  "env": { "OPENAI_API_KEY": "sk-..." },
  "output": { "language": "auto", "length": "long" },
  "prompt": "Explain like I am five.",
  "ui": { "theme": "ember" }
}
```

`output.length` accepts the same values as `--length`:

- Presets: `short`, `medium`, `long`, `xl`, `xxl`
- Shorthand: `s`, `m`, `l`
- Character targets: `1500`, `20k`, `20000`

Shorthand (equivalent):

```json
{
  "model": "google/gemini-3-flash"
}
```

`model` can also be auto:

```json
{
  "model": { "mode": "auto" }
}
```

Shorthand (equivalent):

```json
{
  "model": "auto"
}
```

## Prompt

`prompt` replaces the built-in summary instructions (same behavior as `--prompt`).

Example:

```json
{
  "prompt": "Explain for a kid. Short sentences. Simple words."
}
```

## Environment defaults

Set any env var in config (process env still wins):

```json
{
  "env": {
    "ASSEMBLYAI_API_KEY": "...",
    "OPENAI_API_KEY": "sk-...",
    "OPENROUTER_API_KEY": "sk-or-...",
    "FIRECRAWL_API_KEY": "...",
    "CUSTOM_FLAG": "1"
  }
}
```

Legacy shortcut (still supported):

```json
{
  "apiKeys": {
    "openai": "sk-...",
    "groq": "gsk-...",
    "assemblyai": "...",
    "elevenlabs": "...",
    "anthropic": "sk-ant-...",
    "google": "...",
    "openrouter": "sk-or-...",
    "xai": "...",
    "zai": "...",
    "apify": "...",
    "firecrawl": "...",
    "fal": "..."
  }
}
```

## Cache

Configure the on-disk SQLite cache (extracted content, transcripts, summaries).

```json
{
  "cache": {
    "enabled": true,
    "maxMb": 512,
    "ttlDays": 30,
    "path": "~/.summarize/cache.sqlite",
    "media": {
      "enabled": true,
      "maxMb": 2048,
      "ttlDays": 7,
      "path": "~/.summarize/cache/media",
      "verify": "size"
    }
  }
}
```

Notes:

- `cache.media` controls the **media file** cache (yt-dlp downloads).
- `--no-cache` bypasses summary caching only (LLM output); extract/transcript caches still apply. Use `--no-media-cache` for media.
- `verify`: `size` (default), `hash`, or `none`.

## UI theme

Set a default CLI theme:

```json
{
  "ui": { "theme": "moss" }
}
```

## Slides defaults

Enable slides by default and tune extraction parameters:

```json
{
  "slides": {
    "enabled": true,
    "ocr": false,
    "dir": "slides",
    "sceneThreshold": 0.3,
    "max": 20,
    "minDuration": 2
  }
}
```

## Speaker identification

Define reusable names and context for diarized YouTube transcripts:

```json
{
  "speakers": {
    "defaultProfile": "modern-wisdom",
    "autoIdentify": true,
    "model": "openai/gpt-5.5",
    "minimumConfidence": 0.85,
    "profiles": {
      "modern-wisdom": {
        "host": "Chris Williamson",
        "knownSpeakers": ["Chris Williamson"],
        "context": "Modern Wisdom podcast. Chris Williamson is the host."
      }
    }
  }
}
```

Run with `--diarize`, then use `--speaker-profile`, repeatable `--speaker-at <timestamp=name>`, and
`--remember-speakers` to verify and persist a video's label mapping. `autoIdentify` enables identity
resolution whenever diarization is requested. `--no-identify-speakers` disables it for one run.

Remembered mappings live under `speakers.sources["youtube:<video-id>"]`. They include a transcript hash
and are ignored if provider output changes. Profile-level `model`, `minimumConfidence`, and `autoIdentify`
override the corresponding top-level speaker settings.

## Logging (daemon)

Enable JSON log files for the daemon:

```json
{
  "logging": {
    "enabled": true,
    "level": "info",
    "format": "json",
    "file": "~/.summarize/logs/daemon.jsonl",
    "maxMb": 10,
    "maxFiles": 3
  }
}
```

Notes:

- Default: logging is off.
- `format`: `json` (default) or `pretty`.
- `maxMb` is per file; `maxFiles` controls rotation (ring).
- Extension “Extended logging” sends full input/output to daemon logs (large). Cache hits skip content logging.

## Presets

Define presets you can select via `--model `:

```json
{
  "models": {
    "small": { "id": "openai/gpt-5-mini" },
    "or-free": {
      "rules": [
        {
          "candidates": [
            "openrouter/google/gemini-2.0-flash-exp:free",
            "openrouter/meta-llama/llama-3.3-70b-instruct:free"
          ]
        }
      ]
    }
  }
}
```

Notes:

- `auto` is reserved and can’t be defined as a preset.
- `free` is built-in (OpenRouter `:free` candidates). Override it by defining `models.free` in your config, or regenerate it via `summarize refresh-free`.
- `gpt-fast` and `fast` remain compatibility aliases for OpenAI GPT-5.5 Fast mode. Prefer `model: "openai/gpt-5.5"` plus `openai.serviceTier` / `openai.thinking`. `codex-fast` is the explicit Codex CLI fast preset.

OpenAI request options can be set globally:

```json
{
  "openai": {
    "serviceTier": "fast",
    "thinking": "medium",
    "textVerbosity": "low"
  }
}
```

Or per preset:

```json
{
  "models": {
    "fast-mini": {
      "id": "openai/gpt-5.4-mini",
      "serviceTier": "fast",
      "thinking": "low"
    }
  }
}
```

`thinking` is an alias for `reasoningEffort`. Supported values: `none`, `low`, `medium`, `high`, `xhigh`; shorthand aliases include `off`, `min` (low), `mid`, `med`, `x-high`, and `extra-high`.

`serviceTier: "fast"` is the summarize/Codex-facing spelling. Direct OpenAI requests map it to `service_tier="priority"`. CLI `--service-tier default` clears a configured tier for one run.

Use a preset as your default `model`:

```json
{
  "model": "small"
}
```

Notes:

- For presets, `"mode": "auto"` is optional when `"rules"` is present.

For auto selection with rules:

```json
{
  "model": {
    "mode": "auto",
    "rules": [
      {
        "when": ["video"],
        "candidates": ["google/gemini-3-flash"]
      },
      {
        "when": ["website", "youtube"],
        "bands": [
          {
            "token": { "max": 8000 },
            "candidates": ["openai/gpt-5-mini"]
          },
          {
            "candidates": ["xai/grok-4-fast-non-reasoning"]
          }
        ]
      },
      {
        "candidates": ["openai/gpt-5-mini", "openrouter/openai/gpt-5-mini"]
      }
    ]
  },
  "media": {
    "videoMode": "auto",
    "embeddedVideo": "auto"
  }
}
```

`media.embeddedVideo` accepts `"auto"`, `"off"`, `"prefer"`, or `"both"`. Automatic mode uses only high-confidence primary YouTube embeds and free captions, combining the transcript with substantial article text.

Notes:

- Parsed leniently (JSON5), but **comments are not allowed**.
- Unknown keys are ignored.
- `model.rules` is optional. If omitted, built-in defaults apply.
- `model.rules[].when` (optional) must be an array (e.g. `["video","youtube"]`).
- `model.rules[]` must use either `candidates` or `bands`.

## Output language

Set a default output language for summaries:

```json
{
  "output": { "language": "auto" }
}
```

Examples:

- `"auto"` (default): match the source language.
- `"en"`, `"de"`: common shorthands.
- `"english"`, `"german"`: common names.
- `"en-US"`, `"pt-BR"`: BCP-47-ish tags.

## CLI config

```json
{
  "cli": {
    "enabled": ["gemini", "agent", "openclaw", "opencode", "copilot", "agy", "pi"],
    "autoFallback": {
      "enabled": true,
      "onlyWhenNoApiKeys": true,
      "order": ["claude", "gemini", "codex", "agent", "openclaw", "opencode", "copilot"]
    },
    "codex": { "model": "gpt-5.5" },
    "claude": { "binary": "/usr/local/bin/claude", "extraArgs": ["--verbose"] },
    "agent": { "binary": "/usr/local/bin/agent", "model": "gpt-5.2" },
    "openclaw": { "binary": "/usr/local/bin/openclaw", "model": "main" },
    "opencode": { "binary": "/usr/local/bin/opencode", "model": "openai/gpt-5.4" },
    "copilot": { "binary": "/usr/local/bin/copilot", "model": "gpt-5.2" },
    "agy": { "binary": "/usr/local/bin/agy" },
    "pi": { "binary": "/usr/local/bin/pi" }
  }
}
```

Notes:

- `cli.enabled` is an allowlist (and order) for auto + explicit CLI model ids.
- `cli.autoFallback` controls implicit-auto CLI fallback when `cli.enabled` is not set.
- Default auto fallback order: `claude, gemini, codex, agent, openclaw, opencode, copilot`.
- Antigravity and pi are opt-in unless added to `cli.autoFallback.order`.
- Auto fallback stores the last successful provider in `~/.summarize/cli-state.json` and prioritizes it on the next run.
- `cli..binary` overrides CLI binary discovery.
- `cli..extraArgs` appends extra CLI args.
- Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection.
- pi CLI runs `pi --print --mode json`, passes extracted content over stdin, and accepts configured or per-call models; use `PI_PATH` to override binary.
- `cli.codex.isolated` defaults to `true` for normal summaries, adding Codex ephemeral/no-user-config/no-rules flags, a temporary cwd, and a sanitized temporary `CODEX_HOME` that carries auth only. Set it to `false` only when local Codex config/rules are intentional.

## OpenAI config

```json
{
  "openai": {
    "baseUrl": "https://my-openai-proxy.example.com/v1",
    "useChatCompletions": true,
    "whisperUsdPerMinute": 0.006
  }
}
```

Notes:

- `openai.baseUrl` overrides the OpenAI-compatible API endpoint. Use this for proxies, gateways, or OpenAI-compatible APIs. Env `OPENAI_BASE_URL` takes precedence.
- `openai.whisperUsdPerMinute` is only used to estimate transcription cost in the finish-line metrics when Whisper transcription runs via OpenAI.

## Provider base URLs

Override API endpoints for any provider to use proxies, gateways, or compatible APIs:

```json
{
  "openai": { "baseUrl": "https://my-openai-proxy.example.com/v1" },
  "nvidia": { "baseUrl": "https://integrate.api.nvidia.com/v1" },
  "anthropic": { "baseUrl": "https://my-anthropic-proxy.example.com" },
  "google": { "baseUrl": "https://my-google-proxy.example.com" },
  "xai": { "baseUrl": "https://my-xai-proxy.example.com" },
  "zai": { "baseUrl": "https://api.zhipuai.cn/paas/v4" },
  "minimax": { "baseUrl": "https://api.minimax.io/v1" },
  "ollama": { "baseUrl": "http://localhost:11434/v1" }
}
```

Or via environment variables (which take precedence over config):

| Provider  | Environment Variable(s)                      |
| --------- | -------------------------------------------- |
| OpenAI    | `OPENAI_BASE_URL`                            |
| NVIDIA    | `NVIDIA_BASE_URL`                            |
| Anthropic | `ANTHROPIC_BASE_URL`                         |
| Google    | `GOOGLE_BASE_URL` (alias: `GEMINI_BASE_URL`) |
| xAI       | `XAI_BASE_URL`                               |
| Z.AI      | `Z_AI_BASE_URL` (alias: `ZAI_BASE_URL`)      |
| MiniMax   | `MINIMAX_BASE_URL`                           |
| Ollama    | `OLLAMA_BASE_URL`                            |

---

## File: docs/extract-only.md

---
title: "Extract"
kicker: "modes"
summary: "Extract mode behavior and markdown handling."
read_when:
  - "When changing extract pipeline or flags."
---

# Extract mode

`--extract` prints the extracted content and exits.

Deprecated alias: `--extract-only`.

## Notes

- No summarization LLM call happens in this mode.
- Supported inputs are URLs, YouTube/direct media, Loom share/embed recordings, local audio/video, and local PDF files (`-` stdin is not supported with `--extract`).
- Extraction can still use configured remote transcription, OCR, or Markdown providers for media/PDF inputs. For sensitive files, use local transcription/provider settings for media and `--preprocess off` when you need to block PDF/Markdown preprocessing.
- No extraction cap is applied. Use `--max-extract-characters <count>` to cap output if needed.
- `--format md` may still convert HTML to Markdown (depending on `--markdown-mode` and available tools).
- `--length` is intended for summarization guidance; extraction prints full content.
- `--timestamps` keeps the plain transcript text but also exposes `transcriptSegments` and `transcriptTimedText` (JSON) and prints a timed transcript block when available.
- YouTube transcript output includes the public view count when available. JSON exposes `extracted.sourceMetrics` with `platform`, `videoId`, `viewCount`, and `observedAt`; human output prints `YouTube views: <count>` before the transcript. Metrics refresh at least hourly even when the transcript remains cached.
- `--slides` runs slide detection (YouTube/direct video URLs/local video files). Slide metadata is included in JSON output and written to `slides.json` in the slide directory.
  - When combined with `--extract` for videos that have timed transcripts, the CLI interleaves slide images inline at matching timestamps.
  - Scene detection auto-tunes using sampled frame hashes.
- For non-YouTube URLs with `--format md`, the CLI uses Readability article HTML as the default Markdown input (`--markdown-mode readability`).
  - Use `--markdown-mode auto` to prefer LLM/markitdown conversion without Readability preprocessing.
  - Use `--markdown-mode llm` to force an LLM conversion.
  - Use `--firecrawl always` to try Firecrawl first for non-YouTube URLs.
- For non-YouTube URLs with `--format md`, `--markdown-mode auto` can convert HTML to Markdown via an LLM when configured.
  - Force it with `--markdown-mode llm`.
  - If no LLM is configured, `--markdown-mode auto` may fall back to `uvx markitdown` when available.
- `--markdown-mode readability` uses Readability to extract article HTML before Markdown conversion.

Daemon note:

- `/v1/summarize` supports `format: "markdown"` + `markdownMode` for extract-only output (use `extractOnly: true`).

---

## File: docs/firecrawl.md

---
title: "Firecrawl"
kicker: "modes"
summary: "Firecrawl fallback modes and API key usage."
read_when:
  - "When changing Firecrawl behavior."
---

# Firecrawl mode

Firecrawl is a fallback for sites that block direct HTML fetching or don’t render meaningful content without JS.

## `--firecrawl off|auto|always`

- `off`: never use Firecrawl.
- `auto` (default): use Firecrawl only when HTML extraction looks blocked/thin.
- `always`: try Firecrawl first for non-YouTube URLs (falls back to HTML if Firecrawl is unavailable/empty).
  - YouTube URLs reject `--firecrawl always`; use `--youtube auto|web|yt-dlp|apify` instead.

## Extract default

When `--extract --format md` is used for non-YouTube URLs and `FIRECRAWL_API_KEY` is configured, the CLI defaults to `--firecrawl always` to return Markdown.

## API key

- `FIRECRAWL_API_KEY` (required for Firecrawl requests)

## 2. Official Technical Reference & Guides (steipete/docs)

# Mintlify Starter Kit

Use the starter kit to get your docs deployed and ready to customize.

Click the green **Use this template** button at the top of this repo to copy the Mintlify starter kit. The starter kit contains examples with

- Guide pages
- Navigation
- Customizations
- API reference pages
- Use of popular components

**[Follow the full quickstart guide](https://starter.mintlify.com/quickstart)**

## Development

Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command:

```
npm i mint
```

Run the following command at the root of your documentation, where your `docs.json` is located:

```
mint dev
```

View your local preview at `http://localhost:3000`.

## Publishing changes

Install our GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app) to propagate changes from your repo to your deployment. Changes are deployed to production automatically after pushing to the default branch.

## Need help?

### Troubleshooting

- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI.
- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`.

### Resources
- [Mintlify documentation](https://mintlify.com/docs)
- [Mintlify support](https://mintlify.com/support)