| name | sageo |
| description | Run SEO, AEO, and GEO audits with the Sageo CLI. Use when auditing a website, analysing Core Web Vitals, tracking keyword rankings, checking AI Overview or brand-mention coverage across ChatGPT/Claude/Gemini/Perplexity, diffing two audit runs, or generating a client-ready HTML report. |
| allowed-tools | Bash, Read, Write, Edit, Grep, Glob |
Sageo CLI Playbook
Sageo is a single-binary Go CLI that audits a website across SEO, AEO, and GEO. Every command emits a JSON envelope. Paid commands are cost-estimated and budget-gated. State lives at .sageo/state.json; per-run history lives at .sageo/snapshots/<utc-ts>/.
Working directory: commands assume the repo root of the project being audited (where .sageo/ lives).
Before anything else
- Binary available:
sageo --help (or go run ./cmd/sageo --help from the sageo repo).
- Credentials:
sageo auth status. If anything is missing, walk the user through sageo login (interactive). Do not paste secrets into files.
- Project state:
sageo status. If absent, run sageo init --url <site> (optionally --brand "Name,alias").
Output contract
Every command returns this envelope on stdout. Rely on it, never scrape text:
{
"success": true,
"data": { ... },
"error": null,
"metadata": {
"estimated_cost": 0.0006, "currency": "USD",
"requires_approval": false, "cached": false,
"source": "dataforseo", "fetched_at": "2026-04-22T...",
"dry_run": false
}
}
Parse with jq. Default format is json. Use -o text / -o table only when the user wants human output. sageo compare defaults to text (override with --format json). sageo report html writes an .html file; stdout stays JSON.
Some commands log progress to stderr before emitting the JSON envelope on stdout (notably sageo recommendations forecast — one line per skipped row). Always redirect stderr (2>/dev/null) when piping to jq, or keep it separate (2> err.log | jq ...).
Honest framing rules
Read these before running any flow. Agents presenting Sageo output to humans MUST follow them — they are not disclaimers, they are how the tool behaves.
- Quote priority tiers, not specific click numbers. The primary signal is
priority_tier: high, medium, low, unknown. If a range is quoted, quote it as a range with the tier attached. "High tier, estimated 200 to 500 clicks/mo, calibrated against 42 prior outcomes on this site." Never "you will get 347 more clicks". On cold projects without calibration data, the tier may be derived from the rule-engine priority score (>=80 High, >=50 Medium, else Low) and is rendered with a (provisional) badge; treat these as directional until historical outcomes accumulate.
- Surface caveats every time. The forecaster emits
caveats[] like low_search_volume, short_history, insufficient_data, insufficient_calibration_data, uncalibrated. Pass these through to the user.
- Observational data is not causal.
compare output is correlational (see data.caveats[]). Algorithm updates, seasonality, and concurrent work are not controlled for. Say so.
- LLM drafts are a starting point.
recommended_value written by recommendations draft is pending_review. Never present it as publish-ready. Require sageo recommendations review before it hits a report.
- When calibration is thin, say so. If
calibration_samples is low or calibrated: false with confidence_label: "insufficient_data", explicitly state confidence is low. Do not fabricate certainty.
- Tier recommendations by evidence strength. Every ChangeType maps to a section in
docs/research/ai-citation-signals-2026.md. Some are confirmed (Google schema for AIO). Some are likely (direct-answer blocks, backlinks). Some are unclear (FAQPage, llms.txt). Never upgrade "likely" to "proven".
The recommended flow
For almost every "audit this site" request, use this exact sequence. Do not skip steps 2 and 3 — without an active GSC property, every forecast collapses to priority_tier: unknown and recommendations lose their search-volume signal. Run sageo doctor if any step is unclear; it will name every missing piece and the exact command to fix it.
sageo init --url https://example.com --brand "Example,example.com"
sageo auth login gsc
sageo gsc sites list
sageo gsc sites use https://example.com/
sageo run https://example.com --budget 10
sageo recommendations review
sageo report html --open
Do not run sageo audit run standalone before sageo run. sageo run already includes the audit stage. The standalone sageo audit run subcommand is for isolated re-audits only.
sageo run stages (run in order, per buildRunStages in internal/cli/commands/run.go): crawl → audit → gsc → psi → labs → serp → backlinks → aeo → aeo-mentions → merge → recommendations → draft → forecast, ending at a review_gate stage that is non-interactive unless --no-review / --auto-approve-all is passed. Every run writes a snapshot under .sageo/snapshots/<utc-ts>/ (disable with --no-snapshot) and mirrors the rendered report to .sageo/reports/latest.html. Note: the one-liner in sageo run --help currently lists a different order; trust the source and the snapshot's stages_run field.
sageo run now hard-aborts when no GSC property is selected. The pre-flight returns error code GSC_NOT_CONFIGURED with a hint naming the three commands to fix it (sageo auth login gsc, sageo gsc sites list, sageo gsc sites use <property>). Run those, then re-run. To deliberately skip GSC, pass --skip gsc.
Key run flags:
--budget <USD>: hard ceiling for total paid spend (0 = no cap)
--dry-run: estimate per stage, call no paid APIs
--skip a,b / --only a,b: stage filters
--resume: continue after the last completed stage (pipeline_cursor in state)
--approve: pre-approve cost gates (no mid-run prompts)
--no-review: leave drafts in pending_review (a later recommendations review is still required before shipping anything)
--prompts <file>: newline-delimited prompts for AEO (otherwise derived from GSC + Labs)
--retain N / --retain-within DURATION: snapshot retention after this run
Repeat-run flow (tracking whether recommendations worked):
sageo run https://example.com --budget 5 --approve
sageo compare
sageo compare --output-html ./diff-$(date +%F).html
compare infers which earlier recommendations were "addressed" via per-ChangeType detectors (cleared audit finding, PSI crossing the good-band threshold, schema appearing in the crawl, referring-domain growth). When both snapshots have paired GSC data for an addressed recommendation, an ObservedLift record is appended to .sageo/calibration.json. Subsequent forecasts use this history.
State, snapshots, and calibration
- State file
.sageo/state.json: single source of truth for the current run. Per-source keys: gsc, psi, serp, labs, backlinks, aeo, mentions, findings, merged_findings, recommendations, pipeline_cursor, pipeline_runs, history. Inspect with sageo status or jq. Never hand-edit.
- Snapshots
.sageo/snapshots/<utc-ts>/: a frozen copy of state + recommendations + HTML report + metadata, written atomically by sageo run. Previous runs are never overwritten. Manage with sageo snapshots list|show|path|prune.
- Calibration
.sageo/calibration.json: append-only ObservedLift records written by compare when a recommendation was addressed and both snapshots have paired GSC data. The forecaster reads this on every recommendations forecast call; thresholds: per-ChangeType calibration at 20 samples, overall at a conservative floor, otherwise confidence_label = insufficient_data.
- Scope: Sageo is single-site per working directory. A cross-site (global) registry is not yet shipped; do not reference
sageo sites or a portfolio command.
Forecast output shape
sageo recommendations forecast emits this envelope (confirmed against live data):
{
"data": {
"estimated_point": 1178,
"estimated_range_low": 469,
"estimated_range_high": 1887,
"tier_counts": { "high": 2, "medium": 0, "low": 0, "unknown": 49 },
"top": [
{
"id": "...", "target_url": "...", "change_type": "indexability_fix",
"priority_tier": "high",
"point_estimate": 477, "range_low": 191, "range_high": 763,
"raw_estimate": 477,
"calibrated": false,
"calibration_samples": 0,
"confidence_label": "insufficient_data",
"caveats": ["insufficient_calibration_data"]
}
]
}
}
priority_tier is the primary user-facing signal. calibrated_estimate, calibrated_confidence_low, calibrated_confidence_high, and method only appear once calibration data exists for a ChangeType; don't assume they will be present on cold projects. Forecast rows for URLs without GSC search-volume signal are logged to stderr (forecast: no search-volume signal for <url> (change_type) — skipping) and then dropped from data.top.
Cost tiers
| Tier | Cost | Commands |
|---|
| Free | $0 | crawl, audit, psi, report html, init, status, analyze, compare, snapshots *, recommendations list/forecast, aeo mentions scan |
| Free + OAuth | $0 | all gsc * |
| Paid (micro, ~$0.0006 to $0.02 per call) | paid | serp *, labs *, aeo keywords, geo *, aeo mentions search/top-*, opportunities --with-serp |
| Paid (LLM, provider-priced) | paid | aeo responses, recommendations draft |
| Paid deposit ($100/mo) | paid | backlinks * |
Rules for paid commands:
- Always run
--dry-run first. Show metadata.estimated_cost to the user before the real call.
- Never retry a failed paid call automatically. Surface the error and ask.
- Prefer
sageo run <url> --budget N over stacking individual paid commands.
Review gate (non-negotiable for client work)
LLM drafts never auto-apply to reports. The review workflow:
sageo recommendations review
sageo recommendations review --format json
sageo recommendations review --type title --url https://example.com/pricing
sageo recommendations review --auto-approve-under-priority 30
Only approved or edited recommendations render in sageo report html. pending_review shows a badge; rejected is excluded.
sageo run --auto-approve-all exists but is labelled UNSAFE. Do not use it for any output the user will share.
HTML report (primary output)
sageo report html --output ./report.html --open
sageo report html --logo ./logo.png --brand-color "#0A6AFF" --appendix --title "Acme Audit"
Self-contained .html with inlined CSS and minimal JS, works offline. For a PDF, open in any modern browser and press Cmd+P (macOS) or Ctrl+P (Linux/Windows), then "Save as PDF". sageo report pdf exists as a deprecated alias that prints a warning and runs html; prefer report html.
Full reference
- Command table with every flag: commands.md
- Recipes mapped to common intents (Core Web Vitals, easy wins, AI Overviews, backlink gap, repeat-run deltas): workflows.md
- Failure modes (AUTH, APPROVAL, empty GSC, thin calibration, stale cache, missing snapshots): troubleshooting.md
- Evidence base for every ChangeType:
docs/research/ai-citation-signals-2026.md in the repo
- Architecture, testing, configuration:
README.md, CLAUDE.md, ARCHITECTURE.md, TESTING.md
Defaults to remember
- Locale: Australia (DataForSEO
location_code: 2036, language en). Override per command for other markets.
- Config file:
~/.config/sageo/config.json (override with SAGEO_CONFIG).
- Secrets via env:
SAGEO_DATAFORSEO_LOGIN, SAGEO_DATAFORSEO_PASSWORD, SAGEO_PSI_API_KEY, SAGEO_ANTHROPIC_API_KEY, SAGEO_OPENAI_API_KEY, SAGEO_SERP_API_KEY, SAGEO_GSC_CLIENT_ID/SECRET. See CLAUDE.md for the complete list.
- Cost approval threshold:
SAGEO_APPROVAL_THRESHOLD_USD env or approval_threshold_usd config. Above threshold requires --approve or an interactive confirmation.
Validation
After any code change in this repo, run before committing:
go vet ./...
go test ./...
Full gate: make fmt && make vet && make test && make lint.
Don't
- Don't invent commands. If
sageo <group> --help does not list it, it does not exist.
- Don't run paid commands without
--dry-run first and surfacing the cost.
- Don't edit
.sageo/state.json, .sageo/calibration.json, or any snapshot by hand. The CLI owns the schema.
- Don't present a specific monthly click number as a prediction. Quote the tier and the range, with caveats.
- Don't ship LLM-drafted copy to a client report without
recommendations review.
- Don't embed Anthropic/OpenAI keys in config files. Env vars only.