@canonry/canonry 4.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +117 -0
- package/assets/agent-workspace/AGENTS.md +88 -0
- package/assets/agent-workspace/USER.md +23 -0
- package/assets/agent-workspace/skills/aero/SKILL.md +72 -0
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +161 -0
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +66 -0
- package/assets/agent-workspace/skills/aero/references/orchestration.md +67 -0
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +58 -0
- package/assets/agent-workspace/skills/aero/references/reporting.md +98 -0
- package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +223 -0
- package/assets/agent-workspace/skills/aero/soul.md +30 -0
- package/assets/agent-workspace/skills/canonry/SKILL.md +152 -0
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +200 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +714 -0
- package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +300 -0
- package/assets/agent-workspace/skills/canonry/references/indexing.md +163 -0
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +372 -0
- package/assets/agent-workspace/skills/canonry/references/wordpress-integration.md +61 -0
- package/assets/apple-touch-icon.png +0 -0
- package/assets/assets/BacklinksPage-CixBvw--.js +1 -0
- package/assets/assets/ChartPrimitives-CjX4mQCy.js +1 -0
- package/assets/assets/ProjectPage-C09hcd6v.js +7 -0
- package/assets/assets/RunRow-Bel7mWgu.js +1 -0
- package/assets/assets/RunsPage-wqKWA2Ei.js +1 -0
- package/assets/assets/SettingsPage-esOqUkW5.js +1 -0
- package/assets/assets/TrafficPage-V0iCV3l4.js +1 -0
- package/assets/assets/TrafficSourceDetailPage-DUeuOh2v.js +1 -0
- package/assets/assets/arrow-left-CVZPxpW6.js +1 -0
- package/assets/assets/extract-error-message-BWAlA7ea.js +1 -0
- package/assets/assets/index-LsyRFLMG.css +1 -0
- package/assets/assets/index-avJiga1P.js +210 -0
- package/assets/assets/trash-2-CA_L6uGC.js +1 -0
- package/assets/assets/vendor-markdown-6dwjPOKK.js +14 -0
- package/assets/assets/vendor-radix-CQ77EO2y.js +45 -0
- package/assets/assets/vendor-recharts-C9EZkgbP.js +36 -0
- package/assets/assets/vendor-tanstack-Be8JdV5M.js +1 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon.svg +24 -0
- package/assets/index.html +25 -0
- package/bin/canonry-mcp.mjs +2 -0
- package/bin/canonry.mjs +2 -0
- package/dist/chunk-23DCY7DL.js +7030 -0
- package/dist/chunk-GIKNEXFE.js +12414 -0
- package/dist/chunk-GNMZTR6Y.js +35673 -0
- package/dist/chunk-Q7FFDQU4.js +5743 -0
- package/dist/cli.js +13344 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +12 -0
- package/dist/intelligence-service-GM3RXE2F.js +7 -0
- package/dist/mcp.js +442 -0
- package/package.json +97 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# Canonry CLI Reference
|
|
2
|
+
|
|
3
|
+
The CLI is invoked as `cnry` (short form) or `canonry` — both ship with the `@canonry/canonry` npm package and behave identically. This reference uses `cnry`.
|
|
4
|
+
|
|
5
|
+
## Server Management
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cnry init # interactive setup
|
|
9
|
+
cnry bootstrap # non-interactive setup from env vars
|
|
10
|
+
cnry start # start daemon
|
|
11
|
+
cnry stop # stop daemon
|
|
12
|
+
cnry serve # foreground mode
|
|
13
|
+
cnry serve --host 0.0.0.0 --port 4100
|
|
14
|
+
cnry serve --embed --embed-allow-origin https://app.example.com # read-only embed mode (#716)
|
|
15
|
+
cnry --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Read-only embed mode (#716)
|
|
19
|
+
|
|
20
|
+
Opt-in, OFF by default. Renders the dashboard "chromeless" (no nav/topbar/settings) so it can be iframed read-only, and emits a fail-closed framing contract. With `--embed` absent, served HTML + headers are byte-for-byte unchanged.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cnry serve --embed --embed-allow-origin https://app.example.com [--embed-allow-origin https://b.example] [--embed-view overview]
|
|
24
|
+
cnry start --embed --embed-allow-origin https://app.example.com # daemon form (forwards flags to serve)
|
|
25
|
+
# Env equivalents (env overrides config.yaml `embed:`):
|
|
26
|
+
# CANONRY_EMBED=1 CANONRY_EMBED_ORIGINS=https://a.com,https://b.com CANONRY_EMBED_VIEWS=overview,project
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **Framing:** emits `Content-Security-Policy: frame-ancestors <origins>` on the SPA document, **failing CLOSED to `frame-ancestors 'none'`** when no valid origins are configured (so a misconfigured embed is un-framable, not open to everyone). Origins must be bare `scheme://host[:port]` — paths, wildcards (`*.host`), and non-http(s) schemes are rejected. No `X-Frame-Options` is emitted (CSP is the single source of truth).
|
|
30
|
+
- **Read-only is server-enforced:** embed mode adds NO write surface; the read-only API-key gate (403 on POST/PUT/PATCH/DELETE) is unchanged.
|
|
31
|
+
- **Cross-origin auth caveat:** the `SameSite=Lax` session cookie is NOT sent in a cross-site iframe and the shipped bundle has no API key. v1 works for a **same-origin** embed (cookie flows) OR a self-hosted build with a read-only `VITE_API_KEY` baked in (then client-visible). Do not loosen the cookie to `SameSite=None`.
|
|
32
|
+
|
|
33
|
+
Production managed by PM2:
|
|
34
|
+
```bash
|
|
35
|
+
pm2 status
|
|
36
|
+
pm2 logs canonry
|
|
37
|
+
pm2 restart canonry
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Project Management
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cnry project list # list all projects
|
|
44
|
+
cnry project create <name> --domain <url> --country US --language en
|
|
45
|
+
cnry project show <name> # project detail
|
|
46
|
+
cnry project update <name> # update project settings
|
|
47
|
+
cnry project delete <name> # delete a project
|
|
48
|
+
cnry project delete <name> --dry-run # preview cascade impact (GET /delete-preview) without writing
|
|
49
|
+
cnry status <project> # mention + citation summary + domain info
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Brand aliases
|
|
53
|
+
|
|
54
|
+
`spec.brandAliases: string[]` on the project (set via `cnry apply` or the dashboard) widens the mention detector. Use it when the answer text says "Meta" but the canonical brand is "Facebook", or for product variants ("AcmeCloud", "Acme Cloud", "AcmeCloud Pro"). Aliases are case-insensitive and match the same answer-text scan that powers `answerMentioned`.
|
|
55
|
+
|
|
56
|
+
## Surgical Reads — `cnry get`
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cnry get <project> scores.mentionShare.value
|
|
60
|
+
cnry get <project> scores.mentionCoverage.value
|
|
61
|
+
cnry get <project> scores.citationCoverage.value
|
|
62
|
+
cnry get <project> insights[0].severity
|
|
63
|
+
cnry get <project> latestRun.status
|
|
64
|
+
cnry get <project> --from report scores.citationCoverage.value # pick a registered source
|
|
65
|
+
cnry get <project> <path> --format json # raw JSON output
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Resolves a dot/bracket path against the project's overview (default `--from overview`) or any registered source — `report`, `traffic`, `discovery`, etc. Returns the scalar (or sub-tree) at the path so an agent can lift a single number without pulling a 30 KB JSON payload. Use `--from <source> .` to see the available top-level keys for that source.
|
|
69
|
+
|
|
70
|
+
### Locations
|
|
71
|
+
|
|
72
|
+
Projects support multi-region location context for geographically-aware sweeps:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cnry project add-location <name> --label "NYC" --city "New York" --region NY --country US
|
|
76
|
+
cnry project locations <name> # list configured locations
|
|
77
|
+
cnry project set-default-location <name> <label>
|
|
78
|
+
cnry project remove-location <name> <label>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Sweeps
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cnry snapshot "Acme Corp" --domain acme.example.com # one-shot sales snapshot
|
|
85
|
+
cnry snapshot "Acme Corp" --domain acme.example.com --md # save markdown report
|
|
86
|
+
cnry snapshot "Acme Corp" --domain acme.example.com --output report.md # custom path
|
|
87
|
+
cnry snapshot "Acme Corp" --domain acme.example.com --pdf # save PDF report
|
|
88
|
+
cnry snapshot "Acme Corp" --domain acme.example.com --format json
|
|
89
|
+
|
|
90
|
+
cnry run <project> # sweep all configured providers
|
|
91
|
+
cnry run <project> --provider gemini # single provider only
|
|
92
|
+
cnry run <project> --query "alpha" --query "beta" # scope sweep to a subset of tracked queries (repeatable)
|
|
93
|
+
cnry run <project> --wait # block until complete
|
|
94
|
+
cnry run <project> --location <label> # run with specific location context
|
|
95
|
+
cnry run <project> --all-locations # run for every configured location
|
|
96
|
+
cnry run <project> --no-location # explicitly skip location context
|
|
97
|
+
cnry run <project> --probe --provider openai --query "..." # operator/agent test run — snapshot is inspectable but EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use for verification / "did this fix work?" / regression hypothesis testing.
|
|
98
|
+
cnry run --all --wait # all projects
|
|
99
|
+
cnry run cancel <project> [run-id] # force-cancel stuck runs
|
|
100
|
+
cnry runs <project> --limit 10 # list recent runs (includes both real and probe runs; filter on `trigger` if you only want one)
|
|
101
|
+
cnry run show <id> # show run details
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run statuses: `queued` → `running` → `completed` / `failed` / `partial`
|
|
105
|
+
|
|
106
|
+
`partial` = some providers failed (usually rate limits) — successful snapshots are still saved.
|
|
107
|
+
|
|
108
|
+
### Probe vs real runs
|
|
109
|
+
|
|
110
|
+
| Trigger | Source | Feeds dashboard/analytics | Runs intelligence | Fires notifications | Wakes Aero |
|
|
111
|
+
|---|---|---|---|---|---|
|
|
112
|
+
| `manual` | `cnry run <project>` | ✅ | ✅ | ✅ | ✅ |
|
|
113
|
+
| `scheduled` | cron schedule | ✅ | ✅ | ✅ | ✅ |
|
|
114
|
+
| `config-apply` | `cnry apply` after queries change | ✅ | ✅ | ✅ | ✅ |
|
|
115
|
+
| `backfill` | `cnry backfill ...` | partial (historical) | ✅ | — | — |
|
|
116
|
+
| **`probe`** | `cnry run --probe ...` | ❌ | ❌ | ❌ | ❌ |
|
|
117
|
+
|
|
118
|
+
Use `--probe` whenever you're testing on your own initiative — verifying a fix landed, reproducing a regression, sanity-checking a query — rather than producing data the user/dashboard will consume.
|
|
119
|
+
|
|
120
|
+
`snapshot` does not create a project or write to the DB. It generates category queries, runs providers, and produces a report for prospecting.
|
|
121
|
+
|
|
122
|
+
## Mention + Citation Data
|
|
123
|
+
|
|
124
|
+
Two independent signals per (query × provider): **mention** (`answerMentioned` — brand named in the answer text; the **primary** read) and **citation** (`cited`/`citedDomains` — domain in the grounding sources; **secondary**). Read mention first. Never compute one from the other; never coerce `answerMentioned` null → false (null = "not checked").
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cnry evidence <project> # per-query [C/c][M/m] cell + Mentioned: X / Y / Cited: X / Y
|
|
128
|
+
cnry evidence <project> --format json # JSON output
|
|
129
|
+
cnry history <project> # audit trail
|
|
130
|
+
cnry export <project> --include-results # export as YAML
|
|
131
|
+
cnry backfill answer-mentions # recompute answerMentioned (primary) from stored answers (honors brandAliases)
|
|
132
|
+
cnry backfill answer-mentions --dry-run
|
|
133
|
+
cnry backfill answer-visibility # recompute citationState (secondary) from stored answers
|
|
134
|
+
cnry backfill answer-visibility --dry-run # preview which snapshots would change
|
|
135
|
+
cnry backfill insights <project> # recompute insights for completed runs
|
|
136
|
+
cnry backfill insights <project> --since 2026-04-01 --dry-run
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Output uses a two-glyph cell per (query × provider): `[C/c][M/m]` — uppercase = present, lowercase = absent, `–` = no snapshot. **C/c = cited** (secondary), **M/m = mentioned** (primary). Always print the legend before the table; never collapse the two signals into one cell.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Legend: [C/c][M/m] C=cited c=not-cited M=mentioned m=not-mentioned –=no snapshot
|
|
143
|
+
|
|
144
|
+
[C][M] acme corp ny ← mentioned AND cited
|
|
145
|
+
[c][M] best crm for smb ← mentioned, not cited (mention win, citation gap)
|
|
146
|
+
[C][m] crm pricing ← cited, not mentioned (citation without share of voice)
|
|
147
|
+
[c][m] free crm tools ← neither
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Summary: `Mentioned: X / Y` (primary) and `Cited: X / Y` (secondary) are reported independently — a query can be one, both, or neither.
|
|
151
|
+
|
|
152
|
+
## Reports
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cnry report <project> # write canonry-report-<project>-YYYY-MM-DD.html
|
|
156
|
+
cnry report <project> --period 14 # time window: 7|14|30|90 days (default 30) — scopes GSC/GA/server-activity + period-over-period deltas
|
|
157
|
+
cnry report <project> --output dist/aeo.html # custom path
|
|
158
|
+
cnry report <project> --format json # raw report payload to stdout
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
One-command client-facing AEO report. Bundles the latest visibility sweep, competitor landscape, AI citation sources, GSC + GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps into a self-contained HTML file (inline CSS + SVG charts, no network dependencies). Backed by `GET /api/v1/projects/<name>/report` and the `canonry_report` MCP tool.
|
|
162
|
+
|
|
163
|
+
Behavior to know when narrating numbers from the report:
|
|
164
|
+
- `executiveSummary.citationRate` is **per-query** — `citedQueryCount / totalQueryCount`, with a query counted as cited if any provider in the run cited it. The rate is invariant to provider count, so a gemini-only run and a 4-provider run can be compared honestly. The same definition powers `citationsTrend[].citationRate` so trend deltas track real movement, not provider-mix variance.
|
|
165
|
+
- `citationsTrend` excludes partial runs to avoid skew. A project with only one completed run gets `trend: "unknown"` and the finding "No prior run to compare against." — not "Flat compared to the previous run."
|
|
166
|
+
- Project ownership uses subdomain-aware matching against `project.canonicalDomain` plus any configured `ownedDomains`. `blog.example.com` and `brand.io` count as the project, not as external sources, when those rules apply.
|
|
167
|
+
- Competitor tagging in `aiSourceOrigin.topDomains` uses the same subdomain-aware match — `blog.rival.com` is `isCompetitor: true` when `rival.com` is tracked.
|
|
168
|
+
- AI referral totals dedupe overlapping GA4 attribution dimensions (`session` / `first_user` / `manual_utm`) by picking the largest dimension per `(date, source, medium)`. Two 10-session rows for the same tuple report 10 sessions, not 20.
|
|
169
|
+
- GSC top-query CTR and avgPosition are impression-weighted, matching GSC's own metric semantics across multi-row queries.
|
|
170
|
+
|
|
171
|
+
## Analytics
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
cnry analytics <project> # default analytics view
|
|
175
|
+
cnry analytics <project> --feature metrics # mention + citation rate trends (BrandMetricsDto: mentionTrend primary, trend secondary)
|
|
176
|
+
cnry analytics <project> --feature gaps # brand gap analysis — mention buckets (mentionedQueries[]/mentionGap[]/notMentioned[]) primary, cited buckets (cited[]/gap[]/uncited[]) secondary
|
|
177
|
+
cnry analytics <project> --feature sources # source breakdown by category
|
|
178
|
+
cnry analytics <project> --window 7d # time window: 7d, 30d, 90d, all
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Cited-source rankings (`cnry sources`)
|
|
182
|
+
|
|
183
|
+
"Where do AI engines get the facts they cite?" — the full, per-provider, classified ranking of cited domains. Backed by `GET /api/v1/projects/<name>/analytics/sources` and the `canonry_analytics_sources` MCP tool. All counts/shares/classification are computed server-side; the CLI only renders.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cnry sources <project> # surface-class roll-up (own / direct-competitor / ota-aggregator / editorial-media / other)
|
|
187
|
+
cnry sources <project> --rank # full ranked cited-domain list, each tagged with category + surface class
|
|
188
|
+
cnry sources <project> --rank --limit 20 # top 20 domains; an explicit long-tail rollup preserves the totals
|
|
189
|
+
cnry sources <project> --by-provider # per-provider cited-domain mix + each provider's total cited slots
|
|
190
|
+
cnry sources <project> --window 30d --format json # window-filterable; --format json emits the SourceBreakdownDto directly
|
|
191
|
+
cnry sources <project> --rank --format jsonl # stream the ranked domains, one self-contained record per line
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- **Surface class** is deterministic (no LLM): `own` = the project's `canonicalDomain`/`ownedDomains`; `direct-competitor` = a tracked competitor; `ota-aggregator` = directories/marketplaces (Yelp, Booking.com, Tripadvisor, Amazon…); `editorial-media` = news/blogs/reference; `other` = everything else. When discovery has run, its stored per-domain classifications (`domain_classifications`) enrich recall for niche OTAs/regional media the static allow-list misses — `own` and tracked competitors always stay authoritative. Running `cnry discover run` improves coverage.
|
|
195
|
+
- The ranked list is **not truncated** by default (the old top-5-per-category cap is gone). Pass `--limit N` to cap each list; the response carries `truncatedDomainCount` / `truncatedCitedSlots` so totals always reconcile.
|
|
196
|
+
- Counts are **cited slots** (grounding citations), so a domain cited 3× in one answer counts 3. Probe runs are excluded.
|
|
197
|
+
|
|
198
|
+
### Aggregated visibility stats (`cnry visibility-stats`)
|
|
199
|
+
|
|
200
|
+
Per-query mention (answer-text) and citation (source-list) **counts with a sample size**, pooled across many answer-visibility runs — the data to compute a confidence-aware proportion (e.g. Wilson) or detect drift without fetching every run. Backed by `GET /api/v1/projects/<name>/visibility-stats` and the `canonry_visibility_stats` MCP tool. Probe runs and non-`answer-visibility` runs are excluded; only completed/partial runs count. `--share-of-voice` adds a pooled head-to-head vs tracked competitors (`project / (project + competitor)` brand mentions) — the primitive for a month-over-month client report: call it with `--month <this>` and `--month <last>` and compare.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
cnry visibility-stats <project> # all runs; per-query cited/total + mentioned/checked + pooled TOTAL
|
|
204
|
+
cnry visibility-stats <project> --last-runs 10 # most recent 10 runs (mutually exclusive with --since/--until/--month)
|
|
205
|
+
cnry visibility-stats <project> --since 2026-06-01 --until 2026-06-30 # ISO date/time window on run createdAt
|
|
206
|
+
cnry visibility-stats <project> --month 2026-06 # a whole calendar month (YYYY-MM), expanded to that month's UTC bounds
|
|
207
|
+
cnry visibility-stats <project> --by-provider # per-provider breakdown (counts sum to the pooled counts)
|
|
208
|
+
cnry visibility-stats <project> --month 2026-06 --share-of-voice # pooled share of voice vs competitors for the month (the m/m report primitive)
|
|
209
|
+
cnry visibility-stats <project> --format jsonl # stream one record per query, stamped with project + runCount
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- **Tri-state aware:** `checked` counts only snapshots where `answerMentioned` was recorded — `null` ("not checked") is **excluded**, never counted as not-mentioned. So `checked` is the correct `n` for a mention proportion. `mentionRate = mentioned/checked`; `citedRate = cited/total` (citation_state is always populated, so the citation `n` is `total`). Both rates are `null` when their denominator is 0 (undefined over no samples).
|
|
213
|
+
- **Date-only window:** `--since`/`--until` accept a full ISO instant or a bare `YYYY-MM-DD`. A date-only `--until 2026-06-30` covers the **whole** UTC day (through 23:59:59.999), so same-day runs are included; a date-only `--since` is that day's start.
|
|
214
|
+
- **Unbounded by default:** with no `--since`/`--until`/`--last-runs`, every completed/partial run is pooled (`window.runCount` reports how many). For a recent sample, bound it with `--last-runs N`.
|
|
215
|
+
- **`groupBy` in the payload:** present (`"provider"`) only with `--by-provider`; omitted otherwise (absent = no breakdown) — the generated SDK types it `groupBy?: 'provider'`.
|
|
216
|
+
- **mention vs cited stay independent** — a model can do either, both, or neither. Don't read one from the other.
|
|
217
|
+
|
|
218
|
+
## Technical AEO (site audit)
|
|
219
|
+
|
|
220
|
+
Site-wide technical audit (structured data, AI-readable content, AI-crawler access, content depth/freshness/extractability, …) powered by `@ainyc/aeo-audit`'s `runSitemapAudit`. Runs as the `site-audit` run kind — crawls the project's `sitemap.xml` and scores every reachable page, then rolls up into one 0–100 site score. Pure HTTP, no LLM cost; a large site can take minutes, so it runs in the background.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
cnry technical-aeo run <project> --wait # crawl + audit; --wait polls to terminal. Idempotent: returns the in-flight run if one is active.
|
|
224
|
+
cnry technical-aeo run <project> --sitemap-url <url> --limit 200 # override the sitemap / cap pages (highest <priority> first; default 500, max 2000)
|
|
225
|
+
cnry technical-aeo score <project> [--format json] # site score + per-factor scorecard (avg + pass/partial/fail per page) + delta vs the previous audit
|
|
226
|
+
cnry technical-aeo pages <project> [--status error] [--sort score-asc|score-desc|url] [--format json|jsonl] # per-page breakdown of the latest run (worst-first by default)
|
|
227
|
+
cnry technical-aeo trend <project> [--format json|jsonl] # aggregate-score history across past audits
|
|
228
|
+
cnry schedule set <project> --kind site-audit --preset weekly # keep it fresh
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- The score is only available after at least one audit runs — `score` returns `hasData: false` until then.
|
|
232
|
+
- Reads reflect the latest **completed/partial** site-audit run; probe runs are excluded.
|
|
233
|
+
|
|
234
|
+
## Intelligence
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
cnry insights <project> # list active insights (regressions, gains, opportunities)
|
|
238
|
+
cnry insights <project> --type gbp-* # filter by insight type; trailing * = prefix (e.g. only GBP insights)
|
|
239
|
+
cnry insights <project> --type gbp-description-missing # exact type match
|
|
240
|
+
cnry insights <project> --severity high # minimum severity (high returns high + critical)
|
|
241
|
+
cnry insights <project> --limit 10 # cap to the newest N
|
|
242
|
+
cnry insights <project> --dismissed # include dismissed insights
|
|
243
|
+
cnry insights <project> --format json # JSON output
|
|
244
|
+
cnry insights dismiss <project> <id> # dismiss an insight
|
|
245
|
+
cnry health <project> # latest citation health snapshot (citation-only — see known gap below)
|
|
246
|
+
cnry health <project> --history # health trend over time
|
|
247
|
+
cnry health <project> --history --limit 10 # limit history entries
|
|
248
|
+
cnry health <project> --format json # JSON output
|
|
249
|
+
cnry backfill insights <project> # backfill insights for all completed runs
|
|
250
|
+
cnry backfill insights <project> --from-run <id> --to-run <id> # backfill a range
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> **Known gap (mention-first read):** `cnry health` is **citation-only** today — it has no mention dimension. For the primary mention-first read, use `cnry overview` and `cnry get <project> scores.mentionCoverage.value` / `cnry get <project> scores.mentionShare.value` until health is extended.
|
|
254
|
+
|
|
255
|
+
## Queries & Competitors
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
cnry query add <project> "phrase one" "phrase two"
|
|
259
|
+
cnry query replace <project> "phrase one" "phrase two" # set the basket to exactly this list
|
|
260
|
+
cnry query replace <project> "..." --dry-run # preview adds/removes via /queries/replace-preview
|
|
261
|
+
cnry query remove <project> "phrase"
|
|
262
|
+
cnry query list <project>
|
|
263
|
+
cnry query import <project> queries.txt
|
|
264
|
+
cnry query generate <project> --provider gemini --count 10 --save
|
|
265
|
+
|
|
266
|
+
cnry competitor add <project> competitor1.com competitor2.com
|
|
267
|
+
cnry competitor list <project>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Scheduling & Notifications
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
cnry schedule set <project> --preset daily # or: weekly, twice-daily, daily@09
|
|
274
|
+
cnry schedule set <project> --cron "0 9 * * *" --timezone America/New_York
|
|
275
|
+
cnry schedule set <project> --kind data-refresh --preset daily # refresh all connected GSC/Bing/GA/GBP integrations (no --source)
|
|
276
|
+
cnry schedule set <project> --kind backlinks-sync --preset weekly # re-probe Common Crawl; sync only when a newer rolling window is published (no --source/--provider)
|
|
277
|
+
cnry schedule set <project> --kind site-audit --preset weekly # Technical AEO: crawl the sitemap + audit every page (no --source/--provider)
|
|
278
|
+
cnry schedule show <project>
|
|
279
|
+
cnry schedule enable <project>
|
|
280
|
+
cnry schedule disable <project>
|
|
281
|
+
cnry schedule remove <project>
|
|
282
|
+
|
|
283
|
+
cnry notify add <project> --webhook <url> --events citation.lost,citation.gained
|
|
284
|
+
cnry notify events # list all available event types
|
|
285
|
+
cnry notify list <project>
|
|
286
|
+
cnry notify remove <project> <id>
|
|
287
|
+
cnry notify test <project> <id>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Available events: `citation.lost`, `citation.gained`, `run.completed`, `run.failed`, `insight.critical`, `insight.high`
|
|
291
|
+
|
|
292
|
+
`insight.critical` and `insight.high` fire when the intelligence engine generates critical- or high-severity insights after a sweep completes.
|
|
293
|
+
|
|
294
|
+
> **No mention events yet.** Notification events cover the citation signal only — there are **no** `mention.lost` / `mention.gained` events today. For mention-first monitoring, read `scores.mentionCoverage` / `scores.mentionShare` via `cnry overview` (or the `insight.*` events, which can be driven by mention-side insights); do not wire automation to mention events that aren't emitted.
|
|
295
|
+
|
|
296
|
+
## Provider Settings & Quotas
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
cnry settings # show config: providers, apiUrl, db path
|
|
300
|
+
cnry settings --format json
|
|
301
|
+
cnry settings provider gemini --api-key <KEY> --model gemini-2.5-flash
|
|
302
|
+
cnry settings provider openai --max-per-day 1000 --max-per-minute 20
|
|
303
|
+
cnry settings provider perplexity --api-key <KEY>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Quota flags: `--max-concurrent`, `--max-per-minute`, `--max-per-day`
|
|
307
|
+
|
|
308
|
+
Available providers: `gemini`, `openai`, `claude`, `perplexity`, `local`, `cdp`
|
|
309
|
+
|
|
310
|
+
If a provider hits rate limits (429 errors), the run completes as `partial`. Reduce concurrency or increase time between sweeps.
|
|
311
|
+
|
|
312
|
+
### Gemini Vertex AI
|
|
313
|
+
|
|
314
|
+
Gemini supports Vertex AI as an alternative to API key authentication. Use GCP Application Default Credentials (ADC) or a service account JSON key file:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Via env vars (recommended for servers)
|
|
318
|
+
export GEMINI_VERTEX_PROJECT=my-gcp-project
|
|
319
|
+
export GEMINI_VERTEX_REGION=us-central1 # optional, defaults to us-central1
|
|
320
|
+
export GEMINI_VERTEX_CREDENTIALS=/path/to/sa.json # optional, falls back to ADC
|
|
321
|
+
|
|
322
|
+
# Or in canonry.yaml config
|
|
323
|
+
# vertexProject, vertexRegion, vertexCredentials fields under provider config
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
When Vertex AI is configured, no `GEMINI_API_KEY` is required. The provider uses the `@google-cloud/vertexai` SDK with `googleAuthOptions` for credential handling.
|
|
327
|
+
|
|
328
|
+
## API Keys
|
|
329
|
+
|
|
330
|
+
Mint, list, and revoke the `cnry_…` bearer tokens stored in the `api_keys` table. Keys are stored as a sha256 hash, never in plaintext.
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
cnry key list # table: NAME / PREFIX / SCOPES / CREATED / LAST USED / STATUS
|
|
334
|
+
cnry key list --format json|jsonl # jsonl streams one key per line
|
|
335
|
+
cnry key create --name ci-bot # mint a full-access key (scopes default to *)
|
|
336
|
+
cnry key create --name reader --read-only # read-only key: scopes=['read'], denied every write HTTP method
|
|
337
|
+
cnry key create --name reader --scope read # narrower key; repeat --scope or comma-separate (--scope a,b)
|
|
338
|
+
cnry key create --name ci-bot --format json # JSON output includes the plaintext key
|
|
339
|
+
cnry key revoke <id> # revoke (does not delete); effective on the next request
|
|
340
|
+
cnry key whoami [--format json] # introspect the CURRENT key (name, scopes, readOnly, status)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- **`--read-only` keys can read everything but write nothing.** The server denies every mutating method (POST/PUT/PATCH/DELETE) for a read-only key (`403 FORBIDDEN`); GET/HEAD pass. `--read-only` is sugar for `--scope read` and cannot be combined with `--scope`. The key DTO carries a derived `readOnly: boolean`. Point `canonry-mcp` at a read-only key and it auto-restricts to read tools.
|
|
344
|
+
|
|
345
|
+
- **Create returns the plaintext key exactly once.** It is shown with a "Save this now — it will not be shown again." warning (and is included in the JSON under `key`). It cannot be recovered later, so persist it on receipt.
|
|
346
|
+
- **List never exposes the hash or plaintext** — only safe metadata (id, name, prefix, scopes, created / last-used / revoked timestamps).
|
|
347
|
+
- **Mutations are gated by the `keys.write` scope.** The default key from `cnry init` carries `*`, which satisfies it. A narrower key needs `keys.write` to mint or revoke.
|
|
348
|
+
- **Revoke is not delete.** It sets `revokedAt`; the auth layer rejects the key on the next request. Revoking an already-revoked key is a no-op. You cannot revoke the key you are currently authenticating with (use a different key).
|
|
349
|
+
|
|
350
|
+
## Google Search Console
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
cnry google connect <project> # initiate OAuth flow
|
|
354
|
+
cnry google disconnect <project> # disconnect GSC
|
|
355
|
+
cnry google status <project> # connection status
|
|
356
|
+
cnry google properties <project> # list available properties
|
|
357
|
+
cnry google set-property <project> <url> # set GSC property URL
|
|
358
|
+
cnry google set-sitemap <project> <url> # set sitemap URL
|
|
359
|
+
cnry google list-sitemaps <project> # list submitted sitemaps
|
|
360
|
+
cnry google discover-sitemaps <project> --wait # auto-discover and inspect
|
|
361
|
+
|
|
362
|
+
cnry google sync <project> # sync GSC data
|
|
363
|
+
cnry google sync <project> --days 30 --full --wait # full sync with wait
|
|
364
|
+
|
|
365
|
+
cnry google coverage <project> # index coverage summary
|
|
366
|
+
cnry google refresh <project> # force-fetch fresh GSC coverage data
|
|
367
|
+
cnry google performance <project> # search performance data
|
|
368
|
+
cnry google performance <project> --days 30 --keyword "term" --page "/url"
|
|
369
|
+
|
|
370
|
+
cnry google inspect <project> <url> # inspect specific URL
|
|
371
|
+
cnry google inspect-sitemap <project> --wait # bulk inspect all sitemap URLs
|
|
372
|
+
cnry google inspections <project> # inspection history
|
|
373
|
+
cnry google inspections <project> --url <url> # filter by URL
|
|
374
|
+
cnry google deindexed <project> # pages that lost indexing
|
|
375
|
+
|
|
376
|
+
cnry google request-indexing <project> <url> # push URL to Google
|
|
377
|
+
cnry google request-indexing <project> --all-unindexed # push all unknown pages
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Discovery (Tracked-Basket Expansion)
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
cnry discover run <project> --icp "..." --wait --format json # full pipeline: seed → embed → cluster → probe → bucket
|
|
384
|
+
cnry discover run <project> --icp "..." --dedup-threshold 0.95 # tune cosine threshold (default 0.95)
|
|
385
|
+
cnry discover run <project> --icp "..." --max-probes 100 # per-session probe budget (default 100, hard cap 500)
|
|
386
|
+
cnry discover run <project> --icp-angle "angle 1" --icp-angle "angle 2" --wait # multi-angle: one session per ICP angle, useful for hyperlocal/niche businesses
|
|
387
|
+
cnry discover run <project> --icp "..." --locations michigan,florida # geo-constrain seed generation to a subset of project locations (omit = all project locations)
|
|
388
|
+
|
|
389
|
+
cnry discover list <project> # newest-first session list
|
|
390
|
+
cnry discover show <project> <session-id> # per-query probe rows + buckets + classified competitor domains
|
|
391
|
+
cnry discover harvest <project> <session-id> # gated candidate seeds from the model's issued search-query fan-out (read-only; nothing is probed/tracked/promoted)
|
|
392
|
+
cnry discover harvest <project> <session-id> --min-probe-hits 2 --no-anchor # raise the recurrence floor / disable the subject anchor
|
|
393
|
+
cnry discover promote preview <project> <session-id> # preview bucketed candidates + recurring suggested competitors of every classified type (read-only)
|
|
394
|
+
cnry discover promote <project> <session-id> # adopt cited + aspirational queries + direct-competitor domains
|
|
395
|
+
cnry discover promote <project> <session-id> --competitor-types direct-competitor,editorial-media # widen the competitor merge to other classified types
|
|
396
|
+
cnry discover promote <project> <session-id> --bucket aspirational --no-competitors # scope to a bucket subset / skip competitor merge
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Discovery requires Gemini configured (API key today; Vertex-mode embeddings are deferred). The pipeline writes a `discovery_sessions` row, a `runs` row (kind `aeo-discover-probe`), and one `discovery.basket-divergence` insight when the session completes. `discover harvest` is a separate read-only view over a completed session: it reads the search queries the answer engine actually issued to answer each probe (Gemini's `groundingMetadata.webSearchQueries` fan-out, already stored in the probe's `raw_response`) back out — no new model call — runs a mandatory quality gate (drops navigational/phone lookups, over-specific outliers, off-subject acronym collisions, exact already-tracked matches, and — via an embedding cosine pass over your tracked queries — semantic duplicates like paraphrases/synonyms that exact match can't see) and returns the survivors ranked by how many distinct probes issued each one. The output's `semanticNoveltyApplied` reports whether the embedding pass ran (it falls back to exact-match when no Gemini key is configured). These are a third signal — *issued retrieval queries*, neither "mention" nor "cited" — and carry no demand of their own; they are candidate seeds, so review them and add the good ones with `cnry query add`. Use `--min-probe-hits` to require recurrence and `--no-anchor` to allow new-subject discovery on a well-scoped project. Seed generation is location-aware: a project with locations configured (or a `--locations` label subset) geo-constrains the seed prompt so generated queries stay inside the service area, and a multi-location project gets a per-area seed quota so one area cannot dominate — `--locations` labels must match the project's configured locations or the run is rejected; projects with no locations are unaffected. After probing, one Gemini call classifies every recurring cited domain as `direct-competitor`, `ota-aggregator`, `editorial-media`, or `other` (a failed/legacy classification leaves domains `unknown`). Aero wakes unprompted with the bucket-count payload so the operator can act without polling. `discover promote` defaults to cited + aspirational queries and `direct-competitor` domains only — aggregators and editorial media are suppressed; pass `--competitor-types` to widen the merge (or to recover legacy `unknown` entries) and `--bucket wasted-surface` for off-ICP competitor gaps. Promotion is add-only and idempotent — queries/domains already tracked are reported as skipped, never inserted twice — and only works on `completed` sessions; promoted rows carry `provenance="discovery:<sessionId>"`.
|
|
400
|
+
|
|
401
|
+
## Bing Webmaster Tools
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
cnry bing connect <project> --api-key <key> # connect Bing WMT
|
|
405
|
+
cnry bing disconnect <project> # disconnect
|
|
406
|
+
cnry bing status <project> # connection status
|
|
407
|
+
cnry bing sites <project> # list verified sites
|
|
408
|
+
cnry bing set-site <project> <url> # set active site URL
|
|
409
|
+
cnry bing coverage <project> # URL coverage data
|
|
410
|
+
cnry bing refresh <project> # force-fetch fresh Bing coverage data
|
|
411
|
+
cnry bing inspect <project> <url> # inspect specific URL
|
|
412
|
+
cnry bing inspect-sitemap <project> # discover sitemap URLs and inspect each via Bing
|
|
413
|
+
cnry bing inspect-sitemap <project> --sitemap-url <url> --wait # explicit sitemap, wait for run
|
|
414
|
+
cnry bing inspections <project> # inspection history
|
|
415
|
+
cnry bing request-indexing <project> <url> # submit URL for indexing
|
|
416
|
+
cnry bing request-indexing <project> --all-unindexed # submit all unindexed
|
|
417
|
+
cnry bing performance <project> # search performance data
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## WordPress Integration
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
cnry wordpress connect <project> --url <url> --user <user> # connect (prompts for app password)
|
|
424
|
+
cnry wordpress disconnect <project> # disconnect
|
|
425
|
+
cnry wordpress status <project> # connection status
|
|
426
|
+
cnry wordpress pages <project> [--live|--staging] # list pages
|
|
427
|
+
cnry wordpress page <project> <slug> # show page detail
|
|
428
|
+
cnry wordpress create-page <project> --title <t> --slug <s> --content <c> # create page
|
|
429
|
+
cnry wordpress update-page <project> <slug> --content <c> # update page
|
|
430
|
+
cnry wordpress set-meta <project> <slug> --title <t> # set SEO meta (single page)
|
|
431
|
+
cnry wordpress set-meta <project> --from <file> # bulk set SEO meta from JSON
|
|
432
|
+
cnry wordpress schema <project> <slug> # read page JSON-LD
|
|
433
|
+
cnry wordpress schema deploy <project> --profile <file> # deploy schema from profile
|
|
434
|
+
cnry wordpress schema status <project> # schema status per page
|
|
435
|
+
cnry wordpress set-schema <project> <slug> # manual schema handoff
|
|
436
|
+
cnry wordpress audit <project> # audit pages for SEO issues
|
|
437
|
+
cnry wordpress diff <project> <slug> # compare live vs staging
|
|
438
|
+
cnry wordpress staging status <project> # staging config status
|
|
439
|
+
cnry wordpress staging push <project> # manual staging push handoff
|
|
440
|
+
cnry wordpress llms-txt <project> # read /llms.txt
|
|
441
|
+
cnry wordpress set-llms-txt <project> # manual llms.txt handoff
|
|
442
|
+
cnry wordpress onboard <project> --url <url> --user <user> # full onboarding workflow
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Onboard** runs: connect → audit → set-meta → schema deploy → Google submit → Bing submit. Use `--skip-schema` or `--skip-submit` to skip steps. `--profile <file>` provides business data and page-to-schema mapping for schema deployment.
|
|
446
|
+
|
|
447
|
+
## Google Analytics 4
|
|
448
|
+
|
|
449
|
+
GA4 integration uses service account authentication (no OAuth). The service account must have Viewer access on the GA4 property. `ga sync` writes to four DB tables (`gaTrafficSnapshots`, `gaAiReferrals`, `gaSocialReferrals`, `gaTrafficSummaries`); every subsequent read command queries the local store rather than re-fetching from GA4, so reads are fast and quotaless. AI-referral rows are tracked across 10 known providers (chatgpt, perplexity, claude, gemini, openai, anthropic, copilot, phind, you.com, meta.ai), three GA4 attribution dimensions (`session` / `first_user` / `manual_utm`), and joined to landing pages. Social referrals are split Organic vs Paid via GA4's `sessionDefaultChannelGroup`. All commands support `--format json`.
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
cnry ga connect <project> --property-id <id> --key-file ./sa-key.json
|
|
453
|
+
# connect via service account (auth method = service_account)
|
|
454
|
+
cnry ga disconnect <project> # disconnect; deletes all synced rows for the project
|
|
455
|
+
cnry ga status <project> # connected, propertyId, authMethod, lastSyncedAt
|
|
456
|
+
cnry ga sync <project> [--days 30] [--only traffic|ai|social]
|
|
457
|
+
# refresh from GA4 → DB; --only restricts which slice is replaced
|
|
458
|
+
# returns: synced, rowCount, aiReferralCount, socialReferralCount,
|
|
459
|
+
# syncedComponents, syncedAt
|
|
460
|
+
cnry ga traffic <project> # current-period rollup; returns: totalSessions,
|
|
461
|
+
# totalOrganicSessions/totalDirectSessions/totalUsers,
|
|
462
|
+
# organicSharePct/aiSharePct/socialSharePct/directSharePct,
|
|
463
|
+
# topPages[], aiReferrals[], aiReferralLandingPages[],
|
|
464
|
+
# aiSessionsDeduped, aiUsersBySession, socialReferrals[]
|
|
465
|
+
cnry ga attribution <project> [--trend] # unified channel breakdown (organic / ai / social / direct
|
|
466
|
+
# sessions + raw and display share %s); --trend adds 7d/30d
|
|
467
|
+
# direction per channel + biggest mover
|
|
468
|
+
cnry ga ai-referral-history <project> # daily array of {date, source, medium, attribution,
|
|
469
|
+
# sessions, users}; one row per (day × source × dimension)
|
|
470
|
+
cnry ga social-referral-history <project> # daily array of {date, source, medium, channel,
|
|
471
|
+
# sessions, users}; channel ∈ {Organic Social, Paid Social}
|
|
472
|
+
cnry ga social-referral-summary <project> [--trend]
|
|
473
|
+
# one-line social rollup: socialSessions, socialUsers,
|
|
474
|
+
# socialSharePct, topSources[]; --trend adds 7d/30d direction
|
|
475
|
+
cnry ga session-history <project> # daily totals: {date, sessions, organicSessions, users}
|
|
476
|
+
cnry ga coverage <project> # per-page overlay: {landingPage, sessions,
|
|
477
|
+
# organicSessions, users}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Every read command queries persisted DB rows, so a stale `lastSyncedAt` means the response is stale — always check `ga status` before drawing conclusions, and re-`ga sync` if the data is older than the analysis window. Use `--only ai` or `--only social` to refresh just one slice when iterating.
|
|
481
|
+
|
|
482
|
+
## Google Business Profile (Local AEO)
|
|
483
|
+
|
|
484
|
+
GBP integration tracks how AI engines see a business's local presence — search-keyword impressions, daily performance metrics, hotel lodging attributes, and booking CTAs. It reuses the **Google OAuth client** (same `google.clientId`/`clientSecret` as GSC; the connection is stored under the `gbp` connection type). **Hard prerequisite:** the Google Cloud project must be approved through Google's Business Profile API Basic Access form, or every call returns HTTP 403 at 0 QPM. See `references/google-business-profile.md` for the full GCP-setup + access-request playbook, the reviews/Q&A gating, and real-world data-shape quirks.
|
|
485
|
+
|
|
486
|
+
Like GA4, `gbp sync` writes to local DB tables and every read command queries the local store — reads are fast and quotaless; a stale sync means stale reads. All commands support `--format json`.
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
cnry gbp connect <project> [--public-url <url>] # OAuth connect (reuses the Google client)
|
|
490
|
+
cnry gbp disconnect <project> # remove the GBP connection + ALL synced GBP data
|
|
491
|
+
cnry gbp accounts <project> # list GBP accounts this connection can access
|
|
492
|
+
# (account selection is per project — pick one below)
|
|
493
|
+
cnry gbp locations discover <project> [--account accounts/{n}] [--switch-account] [--no-select-new]
|
|
494
|
+
# discover a chosen account's locations; --account targets a
|
|
495
|
+
# specific account (omit = the account the project already tracks,
|
|
496
|
+
# else the first visible one); --switch-account opts into the
|
|
497
|
+
# destructive re-point to a different account; selects all new by default
|
|
498
|
+
cnry gbp locations <project> [--selected-only] # list discovered locations + selection state
|
|
499
|
+
cnry gbp locations select <project> --location locations/{n}
|
|
500
|
+
cnry gbp locations deselect <project> --location locations/{n}
|
|
501
|
+
# only SELECTED locations are synced
|
|
502
|
+
cnry gbp sync <project> [--location locations/{n}] [--days N] [--months N] [--wait]
|
|
503
|
+
# fires the gbp-sync run: daily metrics + keyword impressions
|
|
504
|
+
# + place-action links + lodging snapshot per selected location;
|
|
505
|
+
# --wait polls to a terminal run status
|
|
506
|
+
cnry gbp metrics <project> [--location locations/{n}] [--metric <DailyMetric>]
|
|
507
|
+
# stored daily metrics + totals-by-metric
|
|
508
|
+
cnry gbp keywords <project> [--location locations/{n}]
|
|
509
|
+
# stored search-keyword impressions over the synced
|
|
510
|
+
# periodStart..periodEnd window; renders exact counts and
|
|
511
|
+
# <N thresholded floors + a thresholdedPct fidelity stat
|
|
512
|
+
cnry gbp place-actions <project> [--location locations/{n}]
|
|
513
|
+
# booking / reservation / order CTAs per location, with
|
|
514
|
+
# placeActionType, providerType (MERCHANT vs AGGREGATOR), isPreferred, uri
|
|
515
|
+
cnry gbp lodging <project> [--location locations/{n}]
|
|
516
|
+
# latest hotel-attribute snapshot per location (snapshot-on-change):
|
|
517
|
+
# populatedGroupCount + syncedAt; populatedGroupCount=0 means the Lodging
|
|
518
|
+
# API returns no structured attributes (common even for complete hotels;
|
|
519
|
+
# verify the "Hotel details" panel), a verify signal, not a confirmed gap
|
|
520
|
+
cnry gbp places <project> [--location locations/{n}]
|
|
521
|
+
# latest Places-API rendered-listing snapshot per location: the
|
|
522
|
+
# server-derived `amenities` the public listing advertises (#648 cross-reference).
|
|
523
|
+
# Needs a Places API key (places.apiKey / GOOGLE_PLACES_API_KEY)
|
|
524
|
+
cnry gbp summary <project> [--location locations/{n}]
|
|
525
|
+
# composite scorecard: performance totals + recent-vs-prior 7d
|
|
526
|
+
# deltas (deltaPct null when prior=0), keyword coverage,
|
|
527
|
+
# place-action CTA presence flags, Lodging API readable-group counts,
|
|
528
|
+
# owner-content profile completeness (secondary categories /
|
|
529
|
+
# description / service area / hours / phone + closed-status counts)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`gbp sync` produces a run with the standard statuses (`completed` / `partial` / `failed`); `partial` means some selected locations synced and others errored (the per-location errors are on the run). Non-lodging locations are skipped cleanly (Google answers the lodging call with HTTP 400, not 404). Reviews are **not** synced — the v4 Reviews API is producer-restricted by Google and unavailable on most projects; the Q&A API was retired (2025-11-03).
|
|
533
|
+
|
|
534
|
+
## OpenAI ads (ChatGPT ads)
|
|
535
|
+
|
|
536
|
+
Paid-surface data for the project's connected OpenAI ad account. Ads render only in the ChatGPT consumer UI (never in API answers), so the Advertiser API is the only window into the paid layer. Money is integer micros in all stored/JSON data; insights `ctr`/`cpcMicros` are derived server-side and `null` on zero denominators. Paid metrics are "paid"/"sponsored" — never conflate with organic `cited`/`mentioned`.
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
cnry ads connect <project> --api-key <sdk-key> # mint the key in OpenAI Ads Manager; validated upstream, stored in ~/.canonry/config.yaml
|
|
540
|
+
cnry ads status <project>
|
|
541
|
+
cnry ads sync <project> # ads-sync run: entity snapshots + daily rollups
|
|
542
|
+
cnry ads campaigns <project> --format jsonl # snapshots incl. context hints (newline-separated example queries)
|
|
543
|
+
cnry ads insights <project> --level campaign --from 2026-06-01 --format jsonl
|
|
544
|
+
cnry ads summary <project> # campaign-level totals only (no double counting)
|
|
545
|
+
cnry ads disconnect <project>
|
|
546
|
+
cnry schedule set <project> --kind ads-sync --preset daily
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`ads sync` runs report `completed` / `partial` (some campaigns failed; per-campaign errors on the run) / `failed`. Doctor checks: `ads.auth.connection`, `ads.data.recent-sync` (both skipped when not connected).
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
## Backlinks (source-aware: Common Crawl + Bing Webmaster)
|
|
553
|
+
|
|
554
|
+
Backlinks are **source-aware** — every read takes a `--source commoncrawl|bing-webmaster` filter (default `commoncrawl`) and rows are tagged with their source:
|
|
555
|
+
|
|
556
|
+
- **Common Crawl** — free public hyperlink graph, ~monthly. Workspace-level release sync + per-project extract. Requires DuckDB (install once with `cnry backlinks install`). Releases download once per workspace and reuse across projects.
|
|
557
|
+
- **Bing Webmaster** — live first-party inbound links from a connected Bing account (`cnry bing connect <project> --api-key <key>`). Per-project; pull with `cnry backlinks bing-sync`.
|
|
558
|
+
|
|
559
|
+
Common Crawl publishes the hyperlink graph as **rolling, monthly-stepped, overlapping 3-month windows** named by the window's first month's year: `cc-main-YYYY-<mon>-<mon>-<mon>` (e.g. `cc-main-2026-mar-apr-may`). Omit `--release` to auto-discover the newest published window. Bing windows are synthetic per-UTC-day snapshots (`bing-YYYY-MM-DD`).
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
cnry backlinks install # install bundled DuckDB binary (Common Crawl only)
|
|
563
|
+
cnry backlinks doctor # show install + plugin status
|
|
564
|
+
cnry backlinks status # latest workspace Common Crawl release sync
|
|
565
|
+
cnry backlinks releases # list cached releases on disk
|
|
566
|
+
cnry backlinks releases latest # probe Common Crawl for the newest published rolling window
|
|
567
|
+
cnry backlinks sync # Common Crawl: auto-discover + download + query the newest release (workspace-wide)
|
|
568
|
+
cnry backlinks sync --release cc-main-2026-mar-apr-may --wait # pin a window, block until ready/failed
|
|
569
|
+
cnry backlinks sources <project> # per-source availability (connected / has-data / latest window / freshness)
|
|
570
|
+
cnry backlinks sources <project> --exclude-crawlers # availability counts drop crawler/proxy hosts (matches the dashboard)
|
|
571
|
+
cnry backlinks bing-sync <project> # Bing: pull live inbound links for the project (needs Bing connected)
|
|
572
|
+
cnry backlinks bing-sync <project> --wait # block until the sync run completes
|
|
573
|
+
cnry backlinks list <project> # top linking domains (Common Crawl, default)
|
|
574
|
+
cnry backlinks list <project> --source bing-webmaster # top linking domains from Bing inbound links
|
|
575
|
+
cnry backlinks list <project> --limit 100 --release <id>
|
|
576
|
+
cnry backlinks extract <project> --release <id> --wait # Common Crawl: re-extract against a ready release
|
|
577
|
+
cnry backlinks cache prune --release <id> # delete cached release files from disk
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
All commands support `--format json`; collection commands (`list`, `sources`, `releases`) also support `--format jsonl`. A Common Crawl release sync has statuses `queued` → `downloading` → `querying` → `ready` / `failed`. Per-project extract / Bing-sync runs use the standard run statuses (`queued` → `running` → `completed` / `failed`). Projects with `autoExtractBacklinks` enabled get a Common Crawl extract enqueued automatically when a release sync transitions to `ready`; Bing-connected projects with a `backlinks-sync` schedule get a Bing pull each tick.
|
|
581
|
+
|
|
582
|
+
**`jsonl` output schema:** `backlinks list` streams `rows` (each `{ project, release, targetDomain, linkingDomain, numHosts, source }`); `backlinks sources` streams the per-source availability list (`{ project, targetDomain, source, connected, hasData, latestRelease, totalLinkingDomains, lastSyncedAt }`); `backlinks releases` streams cached-release rows bare.
|
|
583
|
+
|
|
584
|
+
To keep backlinks fresh automatically, schedule a `backlinks-sync` kind (`cnry schedule set <project> --kind backlinks-sync --preset weekly`): each tick re-probes Common Crawl and runs the workspace release sync **only when a newer rolling window is published** (it skips when the newest `ready` sync already covers the latest release, so it never re-downloads a near-identical window).
|
|
585
|
+
|
|
586
|
+
## CDP / Browser Provider
|
|
587
|
+
|
|
588
|
+
The CDP (Chrome DevTools Protocol) provider enables browser-based queries against AI chat interfaces (e.g., ChatGPT). This gives more accurate results than API-based providers for some use cases.
|
|
589
|
+
|
|
590
|
+
```bash
|
|
591
|
+
cnry cdp connect --host localhost --port 9222 # connect to Chrome CDP
|
|
592
|
+
cnry cdp status # show connection status
|
|
593
|
+
cnry cdp targets # list available targets (ChatGPT, etc.)
|
|
594
|
+
cnry cdp screenshot <query> --targets chatgpt # screenshot a query result
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Requires:** Chrome running with `--remote-debugging-port=9222`
|
|
598
|
+
|
|
599
|
+
## Telemetry
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
cnry telemetry status # show telemetry status
|
|
603
|
+
cnry telemetry enable # enable anonymous telemetry
|
|
604
|
+
cnry telemetry disable # disable telemetry
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Config as Code
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
cnry apply project.yaml # apply declarative config
|
|
611
|
+
cnry apply file1.yaml file2.yaml # multiple files
|
|
612
|
+
cnry export <project> --include-results > project.yaml
|
|
613
|
+
cnry sitemap inspect <project>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## Agent
|
|
617
|
+
|
|
618
|
+
Canonry ships the built-in **Aero** agent (backed by pi-agent-core) for users
|
|
619
|
+
who don't already have one, plus a webhook integration for users who want to
|
|
620
|
+
drive Canonry from Claude Code / Codex / a custom agent.
|
|
621
|
+
|
|
622
|
+
### Built-in Aero (one-shot CLI)
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
# One-shot turn — Aero picks its own tools, streams events to stdout.
|
|
626
|
+
cnry agent ask <project> "<prompt>"
|
|
627
|
+
cnry agent ask <project> "<prompt>" --format json # JSON event stream
|
|
628
|
+
cnry agent ask --all "<prompt>" # fan out the same prompt across every project
|
|
629
|
+
cnry agent ask <project> "<prompt>" --trace # emit tool-execution detail for debugging
|
|
630
|
+
|
|
631
|
+
# Select a specific provider / model (otherwise auto-detected from config).
|
|
632
|
+
cnry agent ask <project> "<prompt>" --provider anthropic --model claude-opus-4-7
|
|
633
|
+
cnry agent ask <project> "<prompt>" --provider zai --model glm-5.1
|
|
634
|
+
cnry agent ask <project> "<prompt>" --provider openai
|
|
635
|
+
cnry agent ask <project> "<prompt>" --provider google
|
|
636
|
+
cnry agent ask <project> "<prompt>" --provider deepinfra --model zai-org/GLM-5.2 # Western-hosted GLM/DeepSeek (key: DEEPINFRA_TOKEN)
|
|
637
|
+
|
|
638
|
+
# Restrict the tool surface. Default is --scope all (full read+write surface).
|
|
639
|
+
# --scope read-only matches the dashboard bar default so pasted "Copy as CLI"
|
|
640
|
+
# commands can't enable writes the UI turn couldn't perform.
|
|
641
|
+
cnry agent ask <project> "<prompt>" --scope read-only
|
|
642
|
+
cnry agent ask <project> "<prompt>" --scope all
|
|
643
|
+
|
|
644
|
+
# Session + provider introspection
|
|
645
|
+
cnry agent providers <project> # list provider keys Aero will pick from + the resolved default
|
|
646
|
+
cnry agent transcript <project> # dump the rolling transcript for the current session
|
|
647
|
+
cnry agent reset <project> # start a fresh session (drops in-memory state, keeps memory)
|
|
648
|
+
cnry agent clear <project> # delete the transcript row from the DB
|
|
649
|
+
|
|
650
|
+
# Durable project notes (the <memory> hydrate block on every new session)
|
|
651
|
+
cnry agent memory list <project>
|
|
652
|
+
cnry agent memory set <project> --key <k> --value <v> # 2 KB cap per value
|
|
653
|
+
cnry agent memory forget <project> --key <k>
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Provider detection order** when `--provider` is omitted: `anthropic` →
|
|
657
|
+
`openai` → `google` → `zai` → `deepinfra`, whichever has an API key present
|
|
658
|
+
first (from `~/.canonry/config.yaml` providers block, or the matching env var
|
|
659
|
+
`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / `ZAI_API_KEY` /
|
|
660
|
+
`DEEPINFRA_TOKEN`). `deepinfra` (GLM-5.2) is a
|
|
661
|
+
Western-hosted OpenAI-compatible host — useful when the agent / analyze /
|
|
662
|
+
classify tiers must avoid PRC-hosted GLM (`zai`).
|
|
663
|
+
|
|
664
|
+
Conversations **persist per project** — `cnry agent ask` continues the
|
|
665
|
+
same rolling thread each invocation. Reset with `cnry agent reset <project>`
|
|
666
|
+
or via the dashboard bar's reset button.
|
|
667
|
+
|
|
668
|
+
### External agents (webhook)
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
# Wire an external agent webhook to a project
|
|
672
|
+
cnry agent attach <project> --url <webhook-url> # register webhook subscription
|
|
673
|
+
cnry agent attach <project> --url <url> --format json # JSON output
|
|
674
|
+
cnry agent detach <project> # remove the agent webhook
|
|
675
|
+
cnry agent detach <project> --format json # JSON output
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Agent webhooks** fire on `run.completed`, `insight.critical`, `insight.high`, and `citation.gained`. The attach/detach pair is idempotent per project (one agent webhook per project, matched by source tag).
|
|
679
|
+
|
|
680
|
+
## Output Formats
|
|
681
|
+
|
|
682
|
+
Every command takes `--format`:
|
|
683
|
+
|
|
684
|
+
- **`text`** (default) — human-readable, decorated. Not a stable parse target.
|
|
685
|
+
- **`json`** — one pretty-printed JSON document (the full envelope). Stable contract.
|
|
686
|
+
- **`jsonl`** — newline-delimited JSON: the command's **primary collection**, one self-contained record per line. The agent-friendly machine format — no envelope key to guess (`.checks` vs `.results` vs `.rows`), no `jq` flattening, greppable line by line.
|
|
687
|
+
|
|
688
|
+
`jsonl` is supported by every **collection** command — one whose primary output is a list: `insights`, `runs`, `evidence`, `history`, `query/keyword/competitor list`, `notify list/events`, `google` reads (`performance`, `performance-daily`, `inspections`, `coverage-history`, `deindexed`, `status`, `properties`, `list-sitemaps`), `bing` reads (`coverage-history`, `inspections`, `performance`, `sites`), `ga` reads (`ai-referral-history`, `social-referral-history`, `session-history`, `coverage`), `traffic events/sources/status`, `discover list/show`, `content targets/sources/gaps/map`, `backlinks list/releases`, `project list/locations`, `key list`, `agent memory list`, `agent providers`, `sources` (streams the ranked cited-domain list), and `doctor`. (`content brief` is an object command — `jsonl` degrades to its JSON document.)
|
|
689
|
+
|
|
690
|
+
Each `jsonl` line re-injects the envelope context it would otherwise lose, so a line lifted out still self-describes:
|
|
691
|
+
|
|
692
|
+
- project-scoped lists stamp `{ "project": "<name>", …row }`;
|
|
693
|
+
- `ga *-history` also stamps `window`; `traffic events` stamps `windowStart`/`windowEnd`; `backlinks list` stamps `release`/`targetDomain`; `discover show` stamps `sessionId`; `content targets` stamps `latestRunId`; `project locations` stamps `isDefault`;
|
|
694
|
+
- global lists whose rows already self-identify (`project list`, `notify events`, `backlinks releases`) emit bare rows.
|
|
695
|
+
|
|
696
|
+
Empty collection → **no output** (the exit code still conveys success, so "no records" stays distinct from "failure"). On failure a command prints its records (if any), then exits non-zero — branch on the **exit code**, never on parsing stderr. JSON field names and the `{ "error": { "code", "message" } }` envelope are a public contract.
|
|
697
|
+
|
|
698
|
+
**Composite** commands return a single aggregate object (not a list), so there is nothing to stream — on them `--format jsonl` **degrades to the same JSON document** as `--format json`; it never falls through to decorated human text. So `--format jsonl` is safe to pass to *any* command: collection commands stream their records, every other command emits its JSON document. Composite shapes are below.
|
|
699
|
+
|
|
700
|
+
## Output schema per command
|
|
701
|
+
|
|
702
|
+
Compact reference for the composite / keyed commands agents read most (shapes can drift — the linked DTO source file is the source of truth; collection commands simply emit their primary array, see each command's own section above).
|
|
703
|
+
|
|
704
|
+
| Command | JSON output shape (top-level keys → DTO) | `jsonl` |
|
|
705
|
+
|---|---|---|
|
|
706
|
+
| `cnry doctor [--project p] [--all]` | `{ scope, project, generatedAt, durationMs, summary{total,ok,warn,fail,skipped}, checks[] }` — `DoctorReportDto` @ `contracts/doctor.ts`. `checks[]` = `CheckResultDto{ id, category, scope, title, status(ok\|warn\|fail\|skipped), code, summary, remediation?, details?, durationMs }`. With `--all`: an object keyed by `__global__` + each project name, each value a full report. | ✅ one check / line as `{project, …check}`; still exits non-zero if any `fail` |
|
|
707
|
+
| `cnry analytics <p> [--feature metrics\|gaps\|sources] [--window 7d\|30d\|90d\|all]` | Object **keyed by feature**: `{ metrics?, gaps?, sources? }` (all three present with no `--feature`; one with `--feature X`). `metrics`=`BrandMetricsDto{ window, buckets[], overall, byProvider, trend, mentionTrend, queryChanges[] }`; `gaps`=`GapAnalysisDto{ cited[], gap[], uncited[], mentionedQueries[], mentionGap[], notMentioned[], runId, window }` (each `[]`=`GapQuery`); `sources`=`SourceBreakdownDto` (same shape as `cnry sources`, below). @ `contracts/analytics.ts` | → degrades to the `json` document |
|
|
708
|
+
| `cnry sources <p> [--rank] [--limit N] [--by-provider] [--window …]` | `SourceBreakdownDto{ overall[], byQuery, ranked, byProvider, runId, window, limit }` @ `contracts/analytics.ts`. `ranked`/each `byProvider[name]` = `RankedSourceList{ totalCitedSlots, domainTotal, entries[], truncatedDomainCount, truncatedCitedSlots, bySurfaceClass[] }`; `entries[]`=`SourceRankEntry{ domain, count, percentage, category, label, surfaceClass }`; `bySurfaceClass[]`=`SurfaceClassCount{ surfaceClass, label, count, percentage, domainCount }`. `surfaceClass` ∈ own \| direct-competitor \| ota-aggregator \| editorial-media \| other. | ✅ streams `ranked.entries` one / line as `{project, …entry}` |
|
|
709
|
+
| `cnry visibility-stats <p> [--since <iso>] [--until <iso>] [--month <YYYY-MM>] [--last-runs N] [--by-provider] [--share-of-voice]` | `VisibilityStatsDto{ project, groupBy, window{since,until,lastRuns,runCount}, totals, byProvider?[], queries[], shareOfVoice? }` @ `contracts/visibility-stats.ts`. Each query / provider / totals entry = `{ total, checked, mentioned, cited, mentionRate, citedRate }` (+ `query`/`queryId`/`firstObserved`/`lastObserved` on queries, + `provider`/observed on provider entries). `checked`=snapshots with non-null `answerMentioned` (tri-state n for mention); `mentionRate=mentioned/checked`, `citedRate=cited/total`, both `null` on a 0 denominator. `byProvider`/per-query `providers` present only with `--by-provider`; counts sum to pooled. `--month YYYY-MM` echoes the resolved `window.since`/`until`. `shareOfVoice` present only with `--share-of-voice` = `{ percent, projectMentions, competitorMentions, snapshotsWithAnswerText, perCompetitor[{domain,mentions}] }`; `percent` (0-100) = `projectMentions/(projectMentions+competitorMentions)`, `null` when no competitors configured. | ✅ streams `queries` one / line as `{project, runCount, …query}` (envelope-only `shareOfVoice` not in the jsonl rows) |
|
|
710
|
+
| `cnry google coverage <p>` (index coverage) | `{ summary{total,indexed,notIndexed,deindexed,percentage}, lastInspectedAt, lastSyncedAt, indexed[], notIndexed[], deindexed[], reasonGroups[] }` — `GscCoverageSummaryDto` @ `contracts/google.ts`. `indexed[]`/`notIndexed[]`=`GscUrlInspectionDto`, `deindexed[]`=`GscDeindexedRowDto`. | → degrades to the `json` document. The single-array reads `google inspections` / `coverage-history` / `deindexed` **stream** `jsonl`. |
|
|
711
|
+
| `cnry ga traffic <p> [--window …]` | Object summary — `GA4TrafficSummaryDto` / `GaTrafficResponse` @ `contracts/ga.ts`: `{ totalSessions, totalOrganicSessions, totalDirectSessions, totalUsers, aiSessionsDeduped, aiUsersDeduped, aiSessionsBySession, aiUsersBySession, socialSessions, socialUsers, channelBreakdown{organic,social,direct,ai,other→{sessions,sharePct,sharePctDisplay}}, *SharePct (+ `*Display`), topPages[], aiReferrals[], aiReferralLandingPages[], socialReferrals[], lastSyncedAt, periodStart, periodEnd }`. | → degrades to the `json` document |
|
|
712
|
+
| `cnry ga attribution <p> [--trend]` | Object — a **renamed projection** of `GaTrafficResponse` (⚠️ field names differ from the DTO): `aiSessions`(←`aiSessionsDeduped`), `organicSessions`(←`totalOrganicSessions`), `directSessions`(←`totalDirectSessions`), plus `totalSessions, totalUsers, aiUsers, aiSessionsBySession, aiUsersBySession, socialSessions, socialUsers, {ai,social,organic,direct}SharePct (+ `*Display`), otherSessions, otherSharePct, channelBreakdown, aiReferrals[], aiReferralLandingPages[], socialReferrals[], periodStart, periodEnd`. With `--trend`: drops `periodStart/End`, adds `trend` (`GaAttributionTrendResponse`). Assembled inline in `commands/ga.ts`. | → degrades to the `json` document |
|
|
713
|
+
| `cnry key list` / `key create` / `key revoke <id>` | `list`: `{ keys[] }` — each `ApiKeyDto{ id, name, keyPrefix, scopes[], createdAt, lastUsedAt, revokedAt }` (SAFE metadata, never the hash or plaintext). `create`: `CreatedApiKeyDto` = `ApiKeyDto` **plus a one-time `key`** (the plaintext `cnry_…` token, shown once). `revoke`: the `ApiKeyDto` with `revokedAt` set. @ `contracts/api-keys.ts` | `key list` streams one key / line; `create` / `revoke` degrade to the `json` document |
|
|
714
|
+
| `cnry gbp summary <p> [--location …]` | `{ scope{locationName,locationCount}, performance{totals,recent7d,prior7d,deltaPct} (metric-keyed maps; keys are raw `BUSINESS_*` / `WEBSITE_CLICKS` tokens — label via `formatGbpMetricLabel`), freshness{dataThroughDate,latestStoredDate,pendingDays}, timeseries[], keywords{total,thresholdedCount,thresholdedPct}, placeActions{total,hasReservationCta,hasBookingCta,hasDirectMerchantCta}, lodging{lodgingLocationCount,populatedLodgingCount,emptyLodgingCount}, profileCompleteness{locationCount,withSecondaryCategories,secondaryCategoryTotal,withDescription,withServiceArea,withHours,withPrimaryPhone,permanentlyClosed,temporarilyClosed} }` — `GbpSummaryDto` @ `contracts/gbp.ts`; `emptyLodgingCount` means 0 readable Lodging API groups, a verify signal rather than proof the Hotel details panel is empty. `timeseries[]`=`{date,pending,metrics}`. | → degrades to the `json` document |
|