agentleFS
jo-incjo-inc/camofox-browser/AGENTS.md

camofox-browser

AGENTS.md12k starsChanged 5 days agoMITView on GitHub

Headless browser automation server for AI agents. Run locally or deploy to any cloud provider. Returns: {"tabId": "abc123", "url": "...", "title": "..."} Returns accessibility tree with refs: Use these instead of constructing URLs: Refs like e1, e2 are stable identifiers for page elements: Firefox-based with anti-detection. Bypasses Google captcha.

  • Reads credentials
  • Installs packages

Trending

More talk lately.about the whole repository

Dec 2025: 0 mentionsJan 2026: 0 mentionsFeb 2026: 3 mentionsMar 2026: 0 mentionsApr 2026: 7 mentionsMay 2026: 3 mentionsJun 2026: 3 mentionsJul 2026: 7 mentionsAug 2026: 12 mentionsSep 2026: 29 mentionsOct 2026: 11 mentionsNov 2026: 3 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the jo-inc/camofox-browser repository that our weekly crawl found. There is no adoption data for this file yet.

Who's talking about jo-inc/camofox-browser

The biggest 5 of 78 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 78

Talked about on GitHub, YouTube, Bluesky, Hacker News and the web (78 times).

On GitHub · 50

45 more on GitHub

On YouTube · 17

12 more on YouTube

On Bluesky · 8

3 more on Bluesky

On the web · 1

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

The file

Show its rules and text

We split this AGENTS.md into the rules it gives an agent

15 rules. About 0 are common, 2 are distinctive, and 13 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use these instead of constructing URLs:

    line 89
  2. Very specific:

    Call /snapshot to get current refs

    line 106
  3. Very specific:

    Use ref in /click or /type

    line 107
  4. Very specific:

    Delete all user data: DELETE /sessions/:userId

    line 115
  5. Very specific:

    Every route handler in server.js has a JSDoc comment block directly above it like:

    line 177
  6. Very specific:

    Every operation must have tags, summary, and responses

    line 225
  7. Very specific:

    Run npx jest tests/unit/openapi.test.js to verify coverage -- the test fails if any route is missing from the spec, if a stale route …

    line 231
  8. Very specific:

    Add or update the tool in mcp/lib/tool-contracts.mjs (schema + buildRequest case).

    line 240
  9. Very specific:

    Add the tool name to openclaw.plugin.json's contracts.tools.

    line 241
  10. Very specific:

    Add contract cases in tests/unit/mcp-contracts.test.js and bump the tool count.

    line 242
  11. Very specific:

    Update the tool list in scripts/test-mcp.mjs (smoke test) and README.md.

    line 243
  12. Very specific:

    After changes, run: node scripts/test-mcp.mjs (stdio smoke test) and NODE_OPTIONS='--experimental-vm-modules' npx jest …

    line 246
  13. Very specific:

    When adding features that need env vars or subprocesses, put that code in a lib/ module and import the result into server.js.

    line 270
  14. Very specific:

    If the file is missing or has no plugins key, all plugins in plugins/ are loaded (backward-compatible).

    line 462
  15. Distinctive:

    Use the plugin manager to install third-party plugins from git or local paths:

    line 467

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

camofox-browser Agent Guide

Headless browser automation server for AI agents. Run locally or deploy to any cloud provider.

Quick Start for Agents

# Install and start
npm install && npm start
# Server runs on http://localhost:9377

Core Workflow

  1. Create a tab -> Get tabId
  2. Navigate -> Go to URL or use search macro
  3. Get snapshot -> Receive page content with element refs (e1, e2, etc.)
  4. Interact -> Click/type using refs
  5. Repeat steps 3-4 as needed

API Reference

Create Tab

POST /tabs
{"userId": "agent1", "sessionKey": "task1", "url": "https://example.com"}

Returns: {"tabId": "abc123", "url": "...", "title": "..."}

Navigate

POST /tabs/:tabId/navigate
{"userId": "agent1", "url": "https://google.com"}
# Or use macro:
{"userId": "agent1", "macro": "@google_search", "query": "weather today"}

Get Snapshot

GET /tabs/:tabId/snapshot?userId=agent1

Returns accessibility tree with refs:

[heading] Example Domain
[paragraph] This domain is for use in examples.
[link e1] More information...

Click Element

POST /tabs/:tabId/click
{"userId": "agent1", "ref": "e1"}
# Or CSS selector:
{"userId": "agent1", "selector": "button.submit"}

Type Text

POST /tabs/:tabId/type
{"userId": "agent1", "ref": "e2", "text": "hello world"}
# Add enter: {"userId": "agent1", "ref": "e2", "text": "search query", "pressEnter": true}

Scroll

POST /tabs/:tabId/scroll
{"userId": "agent1", "direction": "down", "amount": 500}

Navigation

POST /tabs/:tabId/back     {"userId": "agent1"}
POST /tabs/:tabId/forward  {"userId": "agent1"}
POST /tabs/:tabId/refresh  {"userId": "agent1"}

Get Links

GET /tabs/:tabId/links?userId=agent1&limit=50

Close Tab

DELETE /tabs/:tabId?userId=agent1

Search Macros

Use these instead of constructing URLs:

MacroSite
@google_searchGoogle
@youtube_searchYouTube
@amazon_searchAmazon
@reddit_searchReddit
@wikipedia_searchWikipedia
@twitter_searchTwitter/X
@yelp_searchYelp
@linkedin_searchLinkedIn

Element Refs

Refs like e1, e2 are stable identifiers for page elements:

  1. Call /snapshot to get current refs
  2. Use ref in /click or /type
  3. Refs reset on navigation - get new snapshot after

Session Management

  • userId isolates cookies/storage between users
  • sessionKey groups tabs by conversation/task (legacy: listItemId also accepted)
  • Sessions timeout after 30 minutes of inactivity
  • Delete all user data: DELETE /sessions/:userId

Running Engines

Camoufox (Default)

npm start
# Or: ./run.sh

Firefox-based with anti-detection. Bypasses Google captcha.

Testing

npm test                          # All tests (unit + e2e + plugin)
npm run test:plugins              # All plugin tests
npm run test:e2e                  # E2E tests
npm run test:live                 # Live Google tests
npm run test:debug                # With server output
npx jest plugins/youtube          # Single plugin's tests

Docker

docker build -t camofox-browser .
docker run -p 9377:9377 camofox-browser

Key Files

  • server.js - Camoufox engine (routes + browser logic only -- NO process.env or child_process)
  • lib/openapi.js - OpenAPI spec generation via swagger-jsdoc + docs route setup
  • lib/config.js - All process.env reads centralized here
  • plugins/youtube/youtube.js - YouTube transcript extraction via yt-dlp (child_process isolated here)
  • lib/launcher.js - Subprocess spawning (child_process isolated here)
  • lib/cookies.js - Cookie file I/O
  • lib/metrics.js - Prometheus metrics (lazy-loaded, off by default -- set PROMETHEUS_ENABLED=1)
  • lib/request-utils.js - HTTP request classification helpers (actionFromReq, classifyError)
  • lib/snapshot.js - Accessibility tree snapshot
  • lib/macros.js - Search macro URL expansion
  • lib/plugins.js - Plugin loader and event bus
  • lib/auth.js - Shared auth middleware (API key / loopback)
  • camofox.config.json - Plugin configuration (which plugins to load)
  • plugins/ - Plugin directory (loaded per camofox.config.json)
  • plugins/youtube/ - Default plugin: YouTube transcript extraction
  • scripts/install-plugin-deps.sh - Installs plugin deps (apt.txt + post-install.sh)
  • plugins/vnc/index.js - VNC plugin routes (no child_process -- spawning isolated in vnc-launcher.js)
  • plugins/vnc/vnc-launcher.js - VNC process management (child_process isolated here)
  • plugins/persistence/index.js - Session persistence lifecycle hooks
  • lib/persistence.js - Atomic storage state read/write
  • lib/inflight.js - Inflight request coalescing
  • lib/tmp-cleanup.js - Orphaned temp file cleanup
  • lib/reporter.js - Crash/hang reporter with anonymization + GitHub App auth (see README "Crash Reporter" for setup)
  • Dockerfile - Production container with default plugin deps pre-installed

OpenAPI Spec (REQUIRED for route changes)

The API spec is auto-generated from @openapi JSDoc comments in server.js via swagger-jsdoc. It's served at GET /openapi.json (machine-readable) and GET /docs (swagger-stripey three-panel UI).

When adding, modifying, or removing a route, you MUST update the @openapi JSDoc block above it.

Every route handler in server.js has a JSDoc comment block directly above it like:

/**
 * @openapi
 * /tabs/{tabId}/click:
 *   post:
 *     tags: [Interaction]
 *     summary: Click an element
 *     parameters:
 *       - name: tabId
 *         in: path
 *         required: true
 *         schema:
 *           type: string
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required: [userId]
 *             properties:
 *               userId:
 *                 type: string
 *               ref:
 *                 type: string
 *     responses:
 *       200:
 *         description: Click result.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *       404:
 *         description: Tab not found.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 */
app.post('/tabs/:tabId/click', async (req, res) => {

Rules:

  • New routes: add a @openapi JSDoc block immediately above the app.get/post/delete(...) call
  • Path params use {tabId} syntax (not :tabId) in the JSDoc YAML
  • Tag must be one of: System, Tabs, Navigation, Interaction, Content, Sessions, Browser, Legacy
  • Every operation must have tags, summary, and responses
  • Include requestBody for POST/PUT/DELETE routes that accept JSON
  • Include parameters for path params and required query params
  • Mark backward-compat endpoints with deprecated: true
  • Removing a route: delete the @openapi block along with the handler
  • After any route change, run npm run generate-openapi to regenerate the committed openapi.json. The test suite will fail if it's stale.
  • Run npx jest tests/unit/openapi.test.js to verify coverage -- the test fails if any route is missing from the spec, if a stale route exists, or if openapi.json is out of date
  • Reusable schemas go in components.schemas in lib/openapi.js (the swaggerDefinition); reference them via $ref: '#/components/schemas/Name'

MCP / OpenClaw Tool Parity (REQUIRED for route changes)

Camofox has two front doors: the HTTP API and the agent tool interface (an MCP server, wrapped by the OpenClaw plugin). Every tool-like browser action must be reachable from both. This is enforced by a build-time gate -- you cannot add a browser action to HTTP and quietly leave it out of the tool set.

  • Single source of truth: mcp/lib/tool-contracts.mjs defines every tool's name, input schema, and how it maps to an HTTP request (buildRequest). lib/mcp-tool-contracts.mjs just re-exports it, and openclaw.plugin.json lists the same tool names. Edit the contract once; MCP and OpenClaw both update.
  • When you add or change an HTTP route that an agent would call (anything tool-like -- navigation, reading the page, interacting, scripting/state), you MUST also:
    1. Add or update the tool in mcp/lib/tool-contracts.mjs (schema + buildRequest case).
    2. Add the tool name to openclaw.plugin.json's contracts.tools.
    3. Add contract cases in tests/unit/mcp-contracts.test.js and bump the tool count.
    4. Update the tool list in scripts/test-mcp.mjs (smoke test) and README.md.
  • The parity gate: tests/unit/mcpHttpParity.test.js fails if any openapi.json route is not either (a) reachable by a tool, or (b) explicitly allowlisted. The allowlist has two buckets: INFRA_HTTP_ONLY (health, metrics, start/stop, session admin, traces, download streaming, deprecated aliases -- not agent tools) and TOOL_BACKLOG_HTTP_ONLY (browser actions still missing a tool). The backlog is currently empty and must stay that way -- a genuinely non-tool route goes in INFRA_HTTP_ONLY with a reason; a tool-like route gets a tool. Do not grow the backlog.
  • Keep tool schemas matching the real route, not the OpenAPI doc if the two disagree (e.g. /tabs/:tabId/wait accepts timeout/waitForNetwork/dismissConsent). Verify against the handler in server.js.
  • After changes, run: node scripts/test-mcp.mjs (stdio smoke test) and NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/mcp-contracts.test.js tests/unit/mcpHttpParity.test.js.

Telemetry

No credentials are embedded in this package. lib/reporter.js is a stateless HTTP client that sends anonymized crash/hang telemetry to a Cloudflare Worker endpoint (camofox-telemetry.askjo.workers.dev). The endpoint holds the GitHub App credentials as environment secrets -- see workers/crash-reporter/index.ts. The source is in-repo and auditable.

  • Architecture: lib/reporter.js (client, no secrets, no fs) -> POST -> Cloudflare Worker endpoint -> GitHub Issues
  • lib/reporter.js has ZERO credentials, ZERO private keys, ZERO fs imports. It only does fetch() to the telemetry endpoint.
  • lib/resources.js handles fs-based resource snapshots (reading /proc on Linux) -- separated from reporter.js so no file-read + network-send pattern exists in any single file. No child_process import.
  • Anonymization is in lib/reporter.js L28-290 -- text scrubbing (anonymize()), URL anonymization (createUrlAnonymizer()), and tab health tracking (createTabHealthTracker())
  • Public domain list (~120 entries) determines which domains are shown verbatim vs HMAC-hashed
  • Tests: tests/unit/crashRelay.test.js (telemetry client), tests/unit/crashRelayWorker.test.js (worker contract), tests/unit/noSecrets.test.js (asserts no key material in shipped files)
  • Self-hosted endpoint: see README "Self-hosted telemetry endpoint" section
  • Disable with CAMOFOX_CRASH_REPORT_ENABLED=false

Code Separation Conventions

The codebase separates concerns across files for clarity and auditability:

  • Configuration: process.env reads live in lib/config.js, which exports a plain config object. No other file reads environment variables directly.
  • Subprocess management: child_process usage lives in dedicated launcher modules (lib/launcher.js, plugins/youtube/youtube.js, plugins/vnc/vnc-launcher.js), not in route handlers.
  • Route handlers: server.js defines Express routes but delegates env/config reads and subprocess spawning to the modules above.
  • Metrics: lib/metrics.js lazy-loads prom-client. lib/request-utils.js handles HTTP method classification.

When adding features that need env vars or subprocesses, put that code in a lib/ module and import the result into server.js.

Plugin System

Plugins extend camofox-browser with new endpoints, background processes, and lifecycle hooks. The server auto-loads all plugins from plugins/<name>/index.js on startup.

Creating a Plugin

plugins/
  my-plugin/
    index.js        Required -- exports register(app, ctx)
    apt.txt         Optional -- system packages (one per line)
    post-install.sh Optional -- executable hook for binary downloads
    *.test.js       Optional -- Jest tests (auto-discovered)
// plugins/my-plugin/index.js

export function register(app, ctx) {
  const { sessions, config, log, events, auth, ensureBrowser, getSession, destroySession,
          withUserLimit, safePageClose, normalizeUserId, validateUrl, safeError,
          buildProxyUrl, proxyPool, failuresTotal } = ctx;

  // Register Express routes (auth() enforces API key or loopback)
  app.get('/my-endpoint', auth(), async (req, res) => {
    const session = sessions.get(req.params.userId);
    res.json({ ok: true });
  });

  // Listen to lifecycle events
  events.on('browser:launched', ({ browser, display }) => {
    log('info', 'browser is up', { display });
  });

  events.on('session:created', ({ userId, context }) => {
    log('info', 'new session', { userId });
  });

  events.on('tab:navigated', ({ userId, tabId, url }) => {
    log('info', 'navigation', { userId, tabId, url });
  });
}

Plugin Context (ctx)

PropertyTypeDescription
sessionsMapLive sessions: userId -> { context, tabGroups, lastAccess }
configobjectServer CONFIG (port, apiKey, nodeEnv, proxy, etc.)
logfunctionlog(level, msg, fields) -- structured JSON logging
eventsEventEmitterPlugin event bus (29 events -- see below)
authfunctionauth() returns Express middleware enforcing API key / loopback
ensureBrowserasync functionLaunch browser if not running, return browser instance
getSessionasync functiongetSession(userId) -- get or create a session
destroySessionasync functiondestroySession(userId, { reason }) -- tear down and await a session close
withUserLimitasync functionwithUserLimit(userId, fn) -- run fn within per-user concurrency limit
safePageCloseasync functionsafePageClose(page) -- close a page with timeout guard
normalizeUserIdfunctionnormalizeUserId(id) -- coerce to string for map keys
validateUrlfunctionvalidateUrl(url) -- returns error string or null
safeErrorfunctionsafeError(err) -- sanitize error for client response
buildProxyUrlfunctionbuildProxyUrl(pool, proxyConfig) -- get proxy URL for external requests
proxyPoolobject|nullProxy pool instance (null if no proxy configured)
failuresTotalCounterPrometheus counter: failuresTotal.labels(type, action).inc()
createMetricasync functionCreate a Prometheus metric registered to the shared registry (see below)
metricsRegistryfunctionmetricsRegistry() -- raw prom-client Registry or null

Events (29)

28 emitted by core, 1 (session:storage:export) emitted by plugins.

Browser Lifecycle

EventPayloadMutating?
browser:launching{ options }(ok) Modify launch options in-place
browser:launched{ browser, display }
browser:restart{ reason }
browser:closed{ reason }
browser:error{ error }

Session Lifecycle

EventPayloadMutating?
session:creating{ userId, contextOptions }(ok) Modify context options in-place
session:created{ userId, context }
session:destroyed{ userId, reason }
session:expired{ userId, idleMs }

Tab Lifecycle

EventPayload
tab:created{ userId, tabId, page, url }
tab:navigated{ userId, tabId, url, prevUrl }
tab:destroyed{ userId, tabId, reason }
tab:recycled{ userId, tabId }
tab:error{ userId, tabId, error }

Content

EventPayload
tab:snapshot{ userId, tabId, snapshot }
tab:screenshot{ userId, tabId, buffer }
tab:evaluate{ userId, tabId, expression }
tab:evaluated{ userId, tabId, result }

Input

EventPayload
tab:click{ userId, tabId, ref, selector }
tab:type{ userId, tabId, text, ref, mode }
tab:scroll{ userId, tabId, direction, amount }
tab:press{ userId, tabId, key }

Downloads

EventPayload
tab:download:start{ userId, tabId, filename, url }
tab:download:complete{ userId, tabId, filename, path, size }

Cookies / Auth

EventPayload
session:cookies:import{ userId, count }
session:storage:export{ userId, storageState }

Server

EventPayload
server:starting{ port }
server:started{ port, pid }
server:shutdown{ signal }

Mutating Hooks

browser:launching, session:creating, session:created, and session:destroyed are emitted via events.emitAsync() -- the server awaits all listeners (including async ones) before proceeding. This ensures async work like loading storage state from disk completes before the context is created.

Other events use regular events.emit() (fire-and-forget).

Modify payload objects in-place:

// Change Xvfb resolution (e.g., for VNC plugin)
events.on('browser:launching', ({ options }) => {
  options.virtual_display_resolution = '1920x1080x24';
});

// Inject saved auth state into new sessions
events.on('session:creating', ({ userId, contextOptions }) => {
  const saved = loadStorageState(userId);
  if (saved) contextOptions.storageState = saved;
});

System Packages (apt.txt) and Post-Install Hooks

Plugins that need system packages list them one per line in apt.txt:

# plugins/vnc/apt.txt
x11vnc
novnc
python3-websockify

For binary downloads or setup not available via apt, add an executable post-install.sh:

# plugins/youtube/post-install.sh
#!/bin/sh
set -e
curl -fL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
chmod +x /usr/local/bin/yt-dlp

Both are run by scripts/install-plugin-deps.sh during Docker build.

Configuration (camofox.config.json)

camofox.config.json controls which plugins are loaded at runtime and during Docker build:

{
  "id": "camofox-browser",
  "name": "Camofox Browser",
  "version": "1.5.2",
  "plugins": ["youtube"]
}
  • plugins -- array of plugin directory names to load. Only these are loaded at startup and have deps installed during build.
  • If the file is missing or has no plugins key, all plugins in plugins/ are loaded (backward-compatible).
  • This is camofox's own config. openclaw.plugin.json is separate -- it tells the OpenClaw Gateway how to configure camofox as an external service.

Installing Plugins

Use the plugin manager to install third-party plugins from git or local paths:

# Install from git
node scripts/plugin.js install https://github.com/user/camofox-screenshot-plugin
node scripts/plugin.js install git:github.com/user/my-plugin

# Install from local directory
node scripts/plugin.js install ./path/to/my-plugin

# List installed plugins
node scripts/plugin.js list

# Remove a plugin
node scripts/plugin.js remove my-plugin

The installer copies the plugin into plugins/, adds it to camofox.config.json, and runs npm install for any npm dependencies. System deps (apt.txt, post-install.sh) are flagged but must be installed manually or via Docker rebuild.

Plugin sources can be:

  • Git repos where the root has index.js with register() (installed as one plugin)
  • Git repos with a plugins/ subdirectory (each subdirectory installed as a separate plugin)
  • Local directories with index.js and register()

Default Plugins

Three plugins ship by default:

  • youtube -- YouTube transcript extraction (enabled by default)
  • persistence -- Per-user session state persistence to ~/.camofox/profiles/ (enabled by default)
  • vnc -- Interactive browser login via noVNC (disabled by default, requires ENABLE_VNC=1)

The youtube plugin ships as a default plugin -- it's listed in camofox.config.json and included in the base Docker image with its deps pre-installed. The base image runs scripts/install-plugin-deps.sh which reads the config and installs apt.txt packages + post-install.sh hooks for listed plugins.

The with-plugins Dockerfile stage is for rebuilding after adding third-party plugins:

docker build --target with-plugins -t camofox-browser .

The with-plugins stage re-runs install-plugin-deps.sh to pick up any new plugins added to plugins/.

Code Separation Rules

Plugins follow the same separation conventions as core (see "Code Separation Conventions" above):

  • No process.env in plugin files that also have route handlers -- read config from ctx.config
  • No child_process in plugin files that also have route handlers -- spawn from a separate lib/ module

Custom Metrics

Plugins create Prometheus metrics via ctx.createMetric(). Returns a no-op stub when Prometheus is disabled -- no null checks needed.

// In register(app, ctx):
const transcriptsTotal = await ctx.createMetric('counter', {
  name: 'camofox_youtube_transcripts_total',
  help: 'YouTube transcripts extracted',
  labelNames: ['method'],
});

// Use anywhere -- works whether Prometheus is enabled or not
transcriptsTotal.labels('yt-dlp').inc();

Supported types: 'counter', 'histogram', 'gauge'. Options are standard prom-client options (name, help, labelNames, buckets, etc.). Metrics auto-register to the shared registry and appear on /metrics.

For advanced use, ctx.metricsRegistry() returns the raw prom-client Registry (or null when disabled).

Example: YouTube Transcript Plugin

The YouTube plugin (plugins/youtube/) is the reference implementation. It extracts transcripts via yt-dlp with browser fallback, using ctx helpers for auth, logging, browser access, and concurrency control.

plugins/
  youtube/
    index.js        # register(app, ctx) -- route handler + browser fallback
    youtube.js      # yt-dlp process management + transcript parsing
    youtube.test.js # parser unit tests
    apt.txt         # python3-minimal (yt-dlp runtime dep)
    post-install.sh # downloads yt-dlp binary
// plugins/youtube/index.js (simplified)
import { detectYtDlp, hasYtDlp, ensureYtDlp, ytDlpTranscript } from './youtube.js';
import { classifyError } from '../../lib/request-utils.js';

export async function register(app, ctx) {
  const { log, config, sessions, ensureBrowser, getSession,
          withUserLimit, safePageClose, normalizeUserId,
          validateUrl, safeError, buildProxyUrl, proxyPool,
          failuresTotal } = ctx;

  await detectYtDlp(log);

  app.post('/youtube/transcript', ctx.auth(), async (req, res) => {
    // ... validate URL, extract videoId, try yt-dlp then browser fallback
  });

  async function browserTranscript(reqId, url, videoId, lang) {
    return await withUserLimit('__yt_transcript__', async () => {
      await ensureBrowser();
      const session = await getSession('__yt_transcript__');
      const page = await session.context.newPage();
      // ... intercept captions, parse transcript
      await safePageClose(page);
    });
  }
}

Key patterns:

  • Auth: ctx.auth() middleware on the route
  • Logging: ctx.log('info', ...) -- never console.log
  • Browser access: ctx.ensureBrowser() + ctx.getSession() for browser-backed features
  • Concurrency: ctx.withUserLimit() to respect per-user limits
  • Metrics: ctx.failuresTotal.labels(...) for core counters, ctx.createMetric() for custom
  • Code separation: child_process in youtube.js, route handler in index.js -- separate files
  • System deps: apt.txt lists packages installed via scripts/install-plugin-deps.sh

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.