CLAUDE.md · git:20261027.a7c14db · 2026-10-27 · sha256 c91fde6e6ff2daf5

CLAUDE.md git:20261027.a7c14dbA

Immutable. This exact content never changes and is served at /api/v1/blob/c91fde6e6ff2daf5.

# CLAUDE.md

## Project Overview

@canonry/aeo-audit — an open-source AEO (Answer Engine Optimization) audit engine and single umbrella Claude Code / ClawHub skill. Scores websites across 16 ranking factors that determine AI citation.

Website: https://canonry.ai

## Tech Stack

- **Language:** TypeScript (ESM)
- **Runtime:** Node.js >= 20
- **Package manager:** pnpm
- **HTML parsing:** Cheerio
- **Build:** TypeScript compiler to `dist/`
- **Typecheck:** `tsc --noEmit`
- **Test runner:** `tsx --test`
- **Linter:** ESLint v9 flat config

## Commands

```bash
pnpm install    # Install dependencies
pnpm run typecheck  # Static typecheck
pnpm run build      # Compile src/*.ts to dist/
pnpm test           # Run all tests
pnpm lint           # Run linter
```

## Key Files

```
src/
  index.ts           # Main entry: runAeoAudit(url, options)
  scoring.ts         # Factor definitions, weights, score calculation
  fetch-page.ts      # URL fetching with SSRF protection
  errors.ts          # AeoAuditError class
  cli.ts             # CLI argument parsing
  formatters/        # json, markdown, text output formatters
  analyzers/         # Per-factor analyzer modules (16 core + 2 optional) plus shared helpers.ts
  types.ts           # Shared audit/report TypeScript types
dist/                # Compiled publishable ESM output
bin/
  aeo-audit.js       # CLI entry point -> dist/cli.js
skills/aeo/          # Single umbrella Claude Code / ClawHub skill
test/                # Unit and integration tests
```

## Architecture

- Each analyzer receives a context object `{ $, html, url, headers, auxiliary, structuredData, textContent, pageTitle }` and returns `{ score, findings, recommendations }`
- Scores are weighted and normalized in `scoring.ts`. Weights are RELATIVE and do NOT sum to 100 (the core set sums to 111); the score divides by the actual total of the factors it scored. Each factor carries `sharePct`, its real share of the score. Never present `weight` as a percentage
- A factor an analyzer reports `applicable: false` for is excluded from the score entirely, weight and all (`factorApplies` in `scoring.ts`), so a page is not marked down for lacking something it has no reason to have
- Geographic signals is optional (excluded by default); when included, weights renormalize
- The `--factors` flag allows running a subset of analyzers
- SSRF protection blocks private IPs and hostnames in `fetch-page.ts`
- Published entrypoints resolve to compiled `dist/` output; run `pnpm run build` before local CLI smoke tests

## Code Conventions

- Functional style, no classes except AeoAuditError
- Always use `clampScore()` for score calculations
- Findings types: `found`, `missing`, `info`, `timeout`, `unreachable`
- Unused vars starting with `_` are ignored by ESLint

## Documentation

Any change to user-visible CLI surface must update **all three** of these in the same change:

1. `printHelp()` in `src/cli.ts` — Options list **and** Examples block
2. `docs/cli.md` — the relevant mode section, the **Flag reference** table, and the **Exit codes** section (flags, examples, and any defaults). The root `README.md` only carries a handful of headline examples; the full CLI surface lives in `docs/cli.md`.
3. `skills/aeo/SKILL.md` — Examples and the relevant mode section (e.g. `### Sitemap Mode`)

This applies to:

- Adding, renaming, or removing a flag
- Changing a default value (e.g. sitemap `--limit` default of 200) — the default must be stated in the help string, `docs/cli.md`, and SKILL.md
- Adding a new CLI mode (e.g. `--sitemap`) — document flags, defaults, exit-code behavior, and an example invocation in all three places
- Changing exit-code semantics or output format options

Before opening a PR that touches `src/cli.ts`, grep `docs/cli.md` and SKILL.md for the affected flag name to confirm everything still matches.

## Versioning

Follows semver. Bump `version` in `package.json` on every change that ships:

- **Patch** (`1.3.x`) — bug fixes, scoring corrections, internal refactors with no API change
- **Minor** (`1.x.0`) — new analyzers, new CLI flags, new exported helpers (backwards-compatible)
- **Major** (`x.0.0`) — breaking changes to the public API or scoring weights that would change existing audit results

The ClawHub skill version must match `package.json`.

## ClawHub Publishing

CI publishes the skill; there is no manual step. When a push to `main` changes the `version` in `package.json`, `.github/workflows/publish.yml` publishes the npm package and then runs `clawhub publish skills/aeo` with that version and the commit subject as the changelog. A `skills/aeo/SKILL.md` change therefore ships with the next version bump.

### Publishing by hand

Only needed if the workflow failed, and rerunning it (`workflow_dispatch` on `main`) is usually the better fix. To publish by hand anyway, from the repo root:

```bash
clawhub whoami   # should print your handle (e.g. arberx); if not, run `clawhub login`
clawhub publish skills/aeo --version <semver> --changelog "<description of changes>"
```

`--version` must be valid semver and **must match `package.json`**.

### ClawHub Security Guidelines

ClawHub flags skills as suspicious when they request excessive capabilities. Follow these rules to stay under the threshold:

- **Pin npx versions** — use the current major (`@7`) instead of `@latest`. The `@latest` tag is a supply chain risk because a compromised publish can hijack all users immediately. Never pin an exact version (`@7.4.0`): the major pin already gives users every patch and minor release without a skill update. Bump it only in the release that changes the major.
- **Minimize Bash patterns** — only declare the single npx command end users need. Do not include local dev commands (`pnpm run build`, `node bin/...`) in the published skill; those are for contributors, not consumers.
- **Avoid generic Bash patterns** — `Bash(aeo-audit *)` is too broad and could match other binaries. Always use the fully qualified `npx @canonry/aeo-audit@7 *` form.
- **Scope file permissions narrowly** — only request Edit/Write for file types the skill actually modifies. Use `Write(filename)` for specific files (e.g., `llms.txt`, `robots.txt`) instead of broad `Edit(*.txt)` patterns.
- **Keep shell injection guards** — the Argument Safety section in SKILL.md is required. Never remove it.

## GitHub Actions Conventions

- **Single trigger path per release flow.** If the workflow auto-creates a tag, do not also trigger on that tag pattern — the self-pushed tag will re-fire the workflow, causing a duplicate publish that fails with 403 on npm.
- **Never interpolate step outputs directly into `run:` blocks.** Use an `env:` block to pass values into shell scripts: `env: { VERSION: "${{ steps.x.outputs.version }}" }` then reference `$VERSION` in the script. Direct `${{ }}` interpolation in `run:` is a script-injection vector.
- **Scope permissions to the minimum required per job.** If only one step needs `contents: write` (e.g., pushing a tag), prefer splitting it into a separate job with its own permissions block rather than elevating the entire job.
- **Declare explicit `permissions` on every job.** Omitting the `permissions` block inherits the repository default, which may be `write-all`. Always declare at minimum `contents: read`.