canonry is an open-source agent-first AEO monitoring platform that tracks how AI answer engines cite a domain for tracked keywords. Published as @ainyc/canonry on npm. The CLI and API are the primary interfaces — the web dashboard is supplementary.
apps/api/ Cloud API entry point (imports packages/api-routes)
apps/worker/ Cloud worker entry point
apps/web/ Vite SPA source (bundled into packages/canonry/assets/)
packages/canonry/ Publishable npm package (CLI + server + bundled SPA)
packages/api-routes/ Shared Fastify route plugins
packages/contracts/ DTOs, enums, config-schema, error codes
packages/config/ Typed environment parsing
packages/db/ Drizzle ORM schema, migrations, client (SQLite/Postgres)
packages/provider-gemini/ Gemini adapter
packages/provider-openai/ OpenAI adapter
packages/provider-claude/ Claude/Anthropic adapter
packages/provider-local/ Local LLM adapter (OpenAI-compatible API)
docs/ Architecture, roadmap, testing, ADRs
Start with docs/README.md when you need the current doc map, active plans, ADR index, or canonical roadmap.
# One-command dev setup: install deps, build all packages, install canonry globally
./canonry-install.sh
pnpm install
pnpm run typecheck
pnpm run test
pnpm run lint
pnpm run dev:web
# CLI (after Phase 2 implementation)
canonry init
canonry serve
canonry project create <name> --domain <domain> --country US --language en
canonry keyword add <project> <keyword>...
canonry run <project>
canonry run <project> --provider gemini # single-provider run
canonry status <project>
canonry apply <file...> # multi-doc YAML + multiple files
canonry export <project>
packages/api-routes/ must not import from apps/*.packages/canonry/ is the only publishable artifact. Internal packages are bundled via tsup.@ainyc/canonry-* naming convention.THIS IS AN AGENT-FIRST PLATFORM. The CLI and API are the primary interfaces. The web UI is a nice-to-have — it must never block or delay CLI/API work.
packages/api-routes/.packages/canonry/src/commands/.apps/web/ — aim to include it, but never block a release waiting for UI work.--format json on all commands that produce output).canonry apply) over interactive wizards.packages/contracts.packages/config.packages/provider-*/.packages/api-routes (no app-level concerns).cited/not-cited); transitions computed at query time.Every new sqliteTable(...) in packages/db/src/schema.ts MUST have a corresponding migration in packages/db/src/migrate.ts.
This is not optional. If you add a table to the schema but omit the migration, the table will never be created in any existing or new database, and every query against it will throw no such table at runtime.
CREATE TABLE IF NOT EXISTS ... to the MIGRATIONS array in migrate.ts. Include all indexes from the schema definition.ALTER TABLE ... ADD COLUMN ... to MIGRATIONS. SQLite ignores duplicate ADD COLUMN attempts, so these are safe to re-run.MIGRATIONS array only.// In packages/db/src/migrate.ts — MIGRATIONS array:
// v12: My new feature — my_new_table
`CREATE TABLE IF NOT EXISTS my_new_table (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
value TEXT NOT NULL,
created_at TEXT NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_my_new_table_project ON my_new_table(project_id)`,
schema.tsMIGRATIONS in migrate.tspnpm typecheck && pnpm lint && pnpm test all pass before committing~/.canonry/config.yaml is the source of truth for authentication credentials.Projects are managed via canonry.yaml files with Kubernetes-style structure:
apiVersion: canonry/v1
kind: Project
metadata:
name: my-project
spec:
displayName: My Project
canonicalDomain: example.com
country: US
language: en
keywords:
- keyword one
competitors:
- competitor.com
providers:
- gemini
- openai
Locations are project-scoped via spec.locations and spec.defaultLocation. Runs choose the default location, an explicit location, all configured locations, or no location. Do not model locations as keyword-owned state.
Multiple projects can be defined in one file using --- document separators. Apply with canonry apply <file...> (accepts multiple files) or POST /api/v1/apply. Applied project YAML is declarative input; runtime project/run data lives in the DB, while local authentication credentials live in ~/.canonry/config.yaml.
All endpoints under /api/v1/. Auth via Authorization: Bearer cnry_.... Key endpoints:
PUT /api/v1/projects/{name} — create/update projectPOST /api/v1/projects/{name}/runs — trigger visibility sweepGET /api/v1/projects/{name}/timeline — per-keyword citation historyGET /api/v1/projects/{name}/snapshots/diff — compare two runsPOST /api/v1/apply — config-as-code applyGET /api/v1/openapi.json — OpenAPI spec (no auth)See OpenAPI spec at /api/v1/openapi.json for the complete API surface.
The web dashboard follows a dark, professional analytics aesthetic inspired by Vercel’s design system — clean, minimal, high-contrast, and information-dense. Rival tools like Semrush, Ahrefs, and Profound for data richness, but match Vercel for polish: generous whitespace, sharp typography, subtle borders, no visual noise. Follow these conventions for all UI work:
w-56, hidden on mobile with full-screen overlay fallback).max-w-6xl, centered) for all page content.page-header (title + subtitle + optional actions) followed by sections separated by page-section-divider.bg-zinc-950. Cards/surfaces: bg-zinc-900/30 with border-zinc-800/60.text-zinc-50 primary, text-zinc-400 secondary, text-zinc-500/text-zinc-600 for labels.ScoreGauge): SVG radial progress rings for numeric and text metrics. Use on project pages instead of flat metric cards.insight-card-positive, insight-card-caution, insight-card-negative).toneFromRunStatus, toneFromCitationState, etc.).rounded-full pill style.rounded-full with tone-colored borders.LayoutDashboard, Globe, Play, Settings).Rocket icon for Setup.text-[10px], uppercase, tracking-wide) for section context.aria-current="page" on active nav items.aria-label on nav landmarks..sr-only) where needed.See docs/roadmap.md for the full feature roadmap including competitive analysis, priority matrix, and phased implementation order.
Never change existing API endpoint paths or HTTP methods during revisions. The CLI, UI, and any external integrations are hard-coded to the published routes. Changing a path or method is a breaking change regardless of the reason.
Every non-documentation change must include a version bump. The root package.json and packages/canonry/package.json versions must always be kept in sync with each other and with the latest published version on npm (@ainyc/canonry).
package.json files.Every non-trivial change must include tests. If you are adding a feature, fixing a bug, or refactoring logic, ship tests alongside the code. Trivial changes (typo fixes, comment updates, config-only changes) are exempt.
vitest.workspace.ts at the root with per-package vitest.config.ts files.vitest: import { test, expect, describe, it, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'.expect() for assertions (e.g. expect(value).toBe(expected), expect(obj).toEqual(expected), expect(fn).toThrow()).test/ directories colocated with the package (e.g. packages/canonry/test/).os.tmpdir()) for file-system tests; clean up in afterEach.pnpm run test to verify before committing.The skills/canonry-setup/ directory contains an OpenClaw/Claude skill that documents how to install, configure, and operate canonry. Keep this skill in sync with the codebase.
skills/canonry-setup/references/canonry-cli.mdSKILL.md and canonry-cli.mdskills/canonry-setup/references/SKILL.mdreferences/aeo-analysis.mdtypecheck, test, lint across the full workspace on PRs.packages/canonry/ is ready for npm.