@canonry/canonry 4.127.0 → 4.129.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/README.md CHANGED
@@ -8,12 +8,13 @@
8
8
  - Watch AI engines crawl and refer traffic via [server-log ingestion](skills/canonry/references/server-side-traffic.md) — Cloud Run, Vercel, and the WordPress Traffic Logger plugin today
9
9
  - Diagnose against real traffic with built-in [GSC](docs/google-search-console-setup.md), [GA4](docs/google-analytics-setup.md), and [Bing Webmaster](docs/bing-webmaster-setup.md)
10
10
  - Track local AEO via [Google Business Profile](skills/canonry/references/google-business-profile.md) — search-term impressions, performance metrics, and hotel lodging + booking-CTA gaps
11
+ - Manage [ChatGPT ads](docs/mcp.md#tool-surface) with OpenAI Ads Manager — connect an ad account, inspect conversion setup and performance, prepare paused campaigns, and launch only with an explicit human approval
11
12
  - Discover who links to you with [Common Crawl backlinks](skills/canonry/references/canonry-cli.md#backlinks-common-crawl) — follows Common Crawl's rolling monthly hyperlink graph, auto-syncing each new window on a schedule, queried locally with DuckDB
12
13
  - Execute fixes via [WordPress](docs/wordpress-setup.md), JSON-LD schema, and indexing submissions
13
14
  - Manage many clients declaratively — config-as-code YAML + `cnry apply`
14
15
  - Schedule recurring visibility checks, traffic syncs, and Business Profile syncs, with webhook alerts on regressions
15
16
  - Generate client-ready HTML reports — `cnry report <project>`
16
- - Drive from your own agent via the [67-tool MCP adapter](docs/mcp.md) or webhooks
17
+ - Drive from your own agent via the [MCP adapter](docs/mcp.md), a [native Codex or Claude Code plugin](docs/plugins.md), or webhooks
17
18
  - Or use **Aero** — Canonry's built-in agent that wakes up after every run
18
19
 
19
20
  Every dashboard view has a matching CLI command and API endpoint. The CLI is the surface; the UI consumes the same API your agent does.
@@ -54,20 +55,82 @@ cnry project update my-site --provider gemini --provider-model gemini=gemini-2.5
54
55
  cnry project update my-site --clear-provider-model gemini
55
56
  ```
56
57
 
57
- ## Or set it up with your AI coding agent
58
+ ## Manage ChatGPT ads with human control
58
59
 
59
- Drop this into Claude Code, Codex, or any shell-capable agent. It installs canonry, runs your first sweep, audits your site for AEO readiness, and stops for your sign-off before taking any action on your behalf:
60
+ Connect an OpenAI Ads Manager account to bring its account state, integrity review,
61
+ conversion pixels and event settings, campaign structure, and paid-performance
62
+ rollups into the same project as your organic AEO evidence.
63
+
64
+ ```bash
65
+ cnry ads connect my-site --api-key <ads-manager-sdk-key>
66
+ cnry ads sync my-site
67
+ cnry ads account my-site
68
+ cnry ads summary my-site
69
+ ```
70
+
71
+ Canonry and your agent can inspect the live account, look up geo targets, prepare
72
+ campaigns, ad groups, and ChatGPT chat-card ads in a paused state, and keep durable
73
+ operation receipts for reconciliation. Activation is deliberately separate: a human
74
+ approves one exact campaign tree, then a scoped executor may launch only that approved
75
+ tree. This keeps spend-bearing changes reviewable and recoverable instead of granting
76
+ an agent unrestricted access to your ad account. See the [MCP tool surface](docs/mcp.md#tool-surface)
77
+ for the complete agent workflow and safety model.
78
+
79
+ ## Use the native Codex or Claude Code plugin
80
+
81
+ The native plugin gives your agent Canonry's operator playbooks and starts the existing `canonry-mcp` adapter. Canonry itself remains the execution and data plane.
82
+
83
+ Install the runtime. If this is the first initialization, skip the legacy per-project skill and MCP copies:
84
+
85
+ ```bash
86
+ npm install -g @canonry/canonry
87
+ # First initialization only
88
+ cnry init --skip-skills --skip-mcp
89
+ ```
90
+
91
+ Run `cnry init` in a private terminal: it prompts for credentials and prints the
92
+ new full-access API key once. Never paste that output into an agent chat or
93
+ shared log.
94
+
95
+ Then install the plugin for your client:
96
+
97
+ ```bash
98
+ # Codex
99
+ codex plugin marketplace add Canonry/canonry
100
+ codex plugin add canonry@canonry
101
+
102
+ # Claude Code
103
+ claude plugin marketplace add Canonry/canonry
104
+ claude plugin install canonry@canonry
105
+ ```
106
+
107
+ Ensure Canonry's local daemon is running, then verify that its advisory doctor
108
+ check sees the enabled plugin:
109
+
110
+ ```bash
111
+ # Only when Canonry is not already running
112
+ cnry start
113
+ cnry doctor --check 'agent.skills.*' --format json
114
+ ```
115
+
116
+ The plugin contains no credentials and declares no hooks or automatic provider calls. It does not expand Canonry's server-enforced key scope, but fresh `cnry init` creates a full-instance `*` key and a write-capable key makes write tools available to the client by default; a read-only key restricts the catalog to reads. Treat the default as teammate-level access to every project and shared setting on that single-tenant instance, and get explicit approval before every mutation or quota-consuming sweep. Existing `cnry skills install` and standalone MCP configuration remain supported; use one integration path per client to avoid duplicate skills or MCP servers. See the [plugin setup and security guide](docs/plugins.md).
117
+
118
+ ## Or use any shell-capable coding agent
119
+
120
+ Without the native plugin, drop this into any shell-capable agent. It keeps
121
+ credential setup private and asks before each persisted or quota-consuming
122
+ operation:
60
123
 
61
124
  ```text
62
125
  Set up canonry for me. Canonry is an open-source platform that tracks how AI answer engines (Gemini, ChatGPT, Claude, Perplexity) cite my site.
63
126
 
64
127
  1. Ask me for: my domain, 3–5 queries I want to track, and which provider I want to start with (gemini / openai / claude / perplexity). Wait for my answers before proceeding.
65
- 2. Run `npm install -g @canonry/canonry`.
66
- 3. Run `cnry init` in this directory. This scaffolds config and installs the canonry skills into `.claude/skills/canonry/`, `.claude/skills/aero/`, `.codex/skills/canonry/`, and `.codex/skills/aero/`. If the skills aren't there afterwards, run `cnry skills install`.
67
- 4. Read the operator playbook at `.claude/skills/canonry/SKILL.md` and follow it end-to-end: create the project with my domain and queries, wire up the provider key I chose, and trigger the first sweep.
68
- 5. Open my browser to the dashboard so I can see the run results.
69
- 6. Switch to the analyst playbook at `.claude/skills/aero/SKILL.md` and run a baseline AEO audit on my behalf. Read citation evidence with `cnry evidence <project> --format json`, then run `cnry technical-aeo run <project> --wait` followed by `cnry technical-aeo score <project> --format json` for a site-readiness score. This crawls every page in my sitemap (not just the homepage), scores the whole site 0–100, and saves the result so it appears in the dashboard and can be re-audited on a schedule.
70
- 7. Summarize what you found: my mention and citation rates per provider, the top 3 queries I'm not yet cited on, and the highest-impact site issues from the audit. Ask me for permission before taking any further action, such as drafting content, submitting URLs for indexing, editing files, or anything else that changes my site.
128
+ 2. Ask for approval, then run `npm install -g @canonry/canonry`.
129
+ 3. Do not run `cnry init` yourself or ask me for credentials. Tell me to run `cnry init` in my own private terminal because it prompts for provider secrets and prints the new full-access API key once. Wait for me to confirm completion; never ask me to paste its output. This scaffolds config and installs the Canonry skills.
130
+ 4. Read `.claude/skills/canonry/SKILL.md`, run the read-only doctor checks, and show me the exact project/domain/query changes you propose. Ask for explicit approval before creating the project or changing queries.
131
+ 5. After project setup, show the provider and query count for the first sweep and ask for explicit approval for that quota-consuming run. Only then trigger it.
132
+ 6. Read `.claude/skills/aero/SKILL.md` and summarize the existing mention and citation evidence. Propose the technical audit, including its page limit, and ask for separate approval before running `cnry technical-aeo run <project> --wait`; after approval, read the result with `cnry technical-aeo score <project> --format json`.
133
+ 7. Open the dashboard only if I ask, then summarize what you found: mention and citation rates per provider, the top 3 gaps, and the highest-impact site issues. Ask again before drafting content, submitting URLs, editing files, publishing, or performing any other mutation or quota-consuming operation.
71
134
  ```
72
135
 
73
136
  One-click copy at [canonry.ai](https://canonry.ai).
@@ -100,11 +163,13 @@ Configure during `cnry init`, in the dashboard `/settings`, or as env vars.
100
163
  |---|---|
101
164
  | **Architecture & data model** | [docs/architecture.md](docs/architecture.md) · [docs/data-model.md](docs/data-model.md) |
102
165
  | **Aero — built-in agent** | [skills/aero/SKILL.md](skills/aero/SKILL.md) |
166
+ | **Native plugins — Codex / Claude Code** | [docs/plugins.md](docs/plugins.md) |
103
167
  | **MCP — Claude Desktop / Cursor / Codex** | [docs/mcp.md](docs/mcp.md) |
104
168
  | **Integrations** | [GSC](docs/google-search-console-setup.md) · [GA4](docs/google-analytics-setup.md) · [Bing](docs/bing-webmaster-setup.md) · [Google Business Profile](skills/canonry/references/google-business-profile.md) · [WordPress](docs/wordpress-setup.md) · [Server-side traffic (Cloud Run + Vercel + WordPress logs)](skills/canonry/references/server-side-traffic.md) |
105
169
  | **Deployment** — Docker, Railway, Render, systemd, Tailscale | [docs/deployment.md](docs/deployment.md) |
106
170
  | **API** — 118+ endpoints | `GET /api/v1/openapi.json` (no auth) |
107
- | **Skills bundle** for Claude Code / Codex | `cnry skills install` ([details](skills/canonry/SKILL.md)) |
171
+ | **Standalone skills bundle** for Claude Code / Codex | `cnry skills install` ([details](skills/canonry/SKILL.md)) |
172
+ | **Roadmap & ADRs** | [docs/roadmap.md](docs/roadmap.md) · [docs/adr/](docs/adr/) |
108
173
  | **All docs** | [docs/README.md](docs/README.md) |
109
174
 
110
175
  ## Requirements
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: aero
3
- slug: aero
4
3
  description: AEO analyst orchestration — coordinates canonry sweeps and aeo-audit analysis with persistent memory and proactive regression response.
5
- homepage: https://canonry.ai
6
- repository: https://github.com/AINYC/aero
4
+ metadata:
5
+ homepage: https://canonry.ai
6
+ repository: https://github.com/AINYC/aero
7
7
  ---
8
8
 
9
9
  # Aero Orchestration Skill
@@ -16,7 +16,7 @@ Persist only *user-scoped* context (operator preferences, communication style) i
16
16
 
17
17
  **Two signals, not one.** Every (query × provider) snapshot tracks **mentioned** (brand in answer text) and **cited** (domain in source links) independently. Lead with **Mention Coverage** when narrating health — it is the primary gauge — and report **Citation Coverage** as the secondary signal. Never compute one from the other, and never collapse them into a single "visibility" headline. The downloadable report (`cnry report`) and the dashboard hero both honor this split.
18
18
 
19
- When a project has GA4 connected, traffic is a first-class signal alongside mentions and citations. Use `cnry ga traffic` / `cnry ga attribution --trend` for the current snapshot, `cnry ga ai-referral-history` and `cnry ga social-referral-history` for daily series. Reads query a local DB synced by `cnry ga sync` — confirm `cnry ga status` shows a recent `lastSyncedAt` before quoting numbers; if stale, re-sync first. When the project has a server-side traffic source attached (Cloud Run / WordPress / Vercel), `cnry traffic status` and `cnry traffic events` surface crawler + AI-referral evidence the GA4 layer can miss. Full command reference and return shapes live in the co-installed `canonry/references/canonry-cli.md`.
19
+ When a project has GA4 connected, traffic is a first-class signal alongside mentions and citations. Use `cnry ga traffic` / `cnry ga attribution --trend` for the current snapshot, `cnry ga ai-referral-history` and `cnry ga social-referral-history` for daily series. Reads query a local DB synced by `cnry ga sync` — confirm `cnry ga status` shows a recent `lastSyncedAt` before quoting numbers; if stale, ask for explicit approval before re-syncing. When the project has a server-side traffic source attached (Cloud Run / WordPress / Vercel), `cnry traffic status` and `cnry traffic events` surface crawler + AI-referral evidence the GA4 layer can miss. Full command reference and return shapes live in the co-installed `canonry/references/canonry-cli.md`.
20
20
 
21
21
  **Diagnosing a stuck Vercel/Cloud Run source:** if `cnry traffic status` shows `status=error` with a recent `lastError` of `refusing to advance` or `ExceedsBillingLimitError`, the source's `lastSyncedAt` has aged past the upstream retention boundary and every sync now throws. Recovery: `cnry traffic reset <project> --source <id> --advance-to-now`. This advances `lastSyncedAt` to NOW and resumes going-forward syncs — historical events in the gap are unrecoverable from the sync path; run `cnry traffic backfill --days N` separately if any of that history is needed (capped at retention).
22
22
 
@@ -35,19 +35,27 @@ Mention is the primary gauge (see "Two signals, not one" above); citation is the
35
35
  ### What NOT to Do
36
36
  - Don't promise fixes will appear in the next sweep (AEO changes take weeks/months)
37
37
  - Don't give generic SEO advice — always ground recommendations in mention and citation data, leading with the mention signal
38
- - Don't run sweeps without user confirmation (they consume API quota)
38
+ - Don't run sweeps, probes, syncs, audits, discovery sessions, or any other write or quota-consuming operation without explicit user approval
39
39
  - Don't edit client's code without showing diffs and getting approval
40
40
  - Don't conflate "not mentioned" with "page doesn't exist" — and don't conflate "not cited" with "not mentioned" either; check first. The two signals are independent (see "Two signals, not one") and are never computed from each other.
41
41
  - Don't coerce `answerMentioned` null → false. Null means "not checked," not "not mentioned" — treat it as missing data, never as a negative.
42
42
 
43
43
  ### When to use `--probe` runs
44
- When you need to **verify** something on your own initiative — "did the OpenAI provider migration land cleanly?", "is the regression still reproducible after the WP fix?", "does this query actually surface us when I think it should?" — use `cnry run <project> --probe --provider <p> --query "..."`. Probe runs:
44
+ When a verification would help, propose the exact probe and get explicit
45
+ approval before running it. A probe is safer for metrics than a real sweep, but
46
+ it is still a paid/quota-consuming write. After approval, use `cnry run
47
+ <project> --probe --provider <p> --query "..."`. Probe runs:
45
48
  - Still cost provider API quota (same wire call)
46
49
  - Write a snapshot you can inspect via `cnry runs get <id>`
47
50
  - Are EXCLUDED from dashboard, analytics, intelligence, insights, and notifications
48
51
  - Won't wake you up again via the post-run hook (no recursive analysis loops)
49
52
 
50
- Use probes whenever the run is for **your** investigation, not for the user's metrics. The two May-17 ainyc probes that broke the dashboard before this convention existed are the canonical example of why this matters — a 1-snapshot test masqueraded as "the latest sweep" and zeroed the headline.
53
+ Use an approved probe when the run is for investigation rather than the user's
54
+ metrics. Approval for one probe does not authorize repeats; ask again unless
55
+ the operator approved a specific bounded batch. The two May-17 ainyc probes
56
+ that broke the dashboard before this convention existed are the canonical
57
+ example of why this matters — a 1-snapshot test masqueraded as "the latest
58
+ sweep" and zeroed the headline.
51
59
 
52
60
  A real (non-probe) sweep is appropriate when the user explicitly asks to refresh data ("run it again", "get the latest", "trigger a sweep").
53
61
 
@@ -31,7 +31,9 @@ After probing, one Gemini call classifies every recurring cited domain into a `c
31
31
 
32
32
  ## Triggering a session
33
33
 
34
- The operator runs:
34
+ Discovery is a paid, quota-consuming write (about $1 at the default 100-probe
35
+ budget). Show the proposed ICP, provider, and probe budget, then get explicit
36
+ operator approval before starting a session. After approval, the operator runs:
35
37
 
36
38
  ```bash
37
39
  cnry discover run <project> --icp "..." --wait
@@ -14,7 +14,7 @@ Trigger: First sweep completes for a new project
14
14
  Steps:
15
15
  1. `cnry overview <project> --format json` → mention read first: `queryCounts.mentionRate`, `scores.mention`, `scores.mentionShare`. Then `cnry analytics <project> --feature gaps --format json` for `mentionedQueries[]` / `mentionGap[]` / `notMentioned[]`, and `cnry evidence <project> --format json` for the per-query `[C/c][M/m]` drilldown and the secondary cited data.
16
16
  2. Compute baseline in this order: **mention rate, mention share**, then cited rate; provider breakdown; top/bottom queries by mention.
17
- 3. `cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json` → site readiness score across every page in the sitemap (auto-discovered; add `--limit <n>` to `run` to cap, default 500). Persists to the dashboard and is trendable via `cnry technical-aeo trend <project>`.
17
+ 3. With explicit operator approval for the crawl and persisted run, `cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json` → site readiness score across every page in the sitemap (auto-discovered; add `--limit <n>` to `run` to cap, default 500). Persists to the dashboard and is trendable via `cnry technical-aeo trend <project>`.
18
18
  4. Identify top 3 gaps — lead with `mentionGap[]` / `notMentioned[]` (where competitors are named and you aren't), then the cited gaps with fixable site issues.
19
19
  5. Generate onboarding report with baseline + action plan
20
20
  6. Store baseline metrics in memory (include mention rate + mention share, not just cited rate)
@@ -34,13 +34,15 @@ Steps:
34
34
  8. If content fix: generate diff (schema, llms.txt, or content changes)
35
35
  9. Update memory with regression event + diagnosis (record which signal regressed: mention, citation, or both)
36
36
 
37
- **Want to verify the regression is real / reproducible before reporting?** Use a probe run instead of a real sweep:
37
+ **Want to verify the regression is real / reproducible before reporting?**
38
+ Propose the exact provider/query and get explicit approval, then use a probe
39
+ run instead of a real sweep:
38
40
 
39
41
  ```
40
42
  cnry run <project> --probe --provider <p> --query "<regressed-query>"
41
43
  ```
42
44
 
43
- Then `cnry runs get <id>` to inspect the snapshot. The probe's snapshot won't displace the latest scheduled sweep on the dashboard, won't generate insights, and won't fire notifications so you can re-test as many times as needed without polluting metrics. Promote to a real sweep (drop `--probe`) only if the operator explicitly wants the data to feed the dashboard.
45
+ Then `cnry runs get <id>` to inspect the snapshot. The probe's snapshot won't displace the latest scheduled sweep on the dashboard, won't generate insights, and won't fire notifications. It still costs provider quota and writes a snapshot, so approval for one probe does not authorize repeats; ask again unless the operator approved a specific bounded batch. Promote to a real sweep (drop `--probe`) only if the operator explicitly wants the data to feed the dashboard.
44
46
 
45
47
  ## Workflow 3: Weekly Review
46
48
 
@@ -12,7 +12,7 @@ You are **Aero** — an AEO analyst. You help operators understand whether AI an
12
12
  - **Evidence over opinion.** Numbers before interpretation. "ChatGPT stopped mentioning you for 'roof repair phoenix' between March 28 and April 2, and your mention share fell from 50% to 0%" beats "your visibility decreased" — then note the lost citation second.
13
13
  - **Proactive, not passive.** Regressions don't wait to be asked about. Surface them when you spot them. Flag competitors the moment they take mention share in answers you used to own, and when they appear in citations you own.
14
14
  - **Honest about uncertainty.** When the data is ambiguous, say so. Don't manufacture confidence. Don't promise fixes will appear in the next sweep — AEO changes take weeks.
15
- - **Cautious with writes.** Sweeps cost quota. Schedules shape downstream notifications. Queries define what gets tracked. Confirm intent before mutating state the operator will notice. When *you* need to test something (verify a fix, reproduce a regression), use `cnry run --probe` — same wire call, no dashboard/analytics/notification pollution.
15
+ - **Cautious with writes.** Sweeps and probes cost quota. Schedules shape downstream notifications. Queries define what gets tracked. Get explicit approval before every write or quota-consuming operation, including probes whose snapshots stay out of dashboard metrics. When an approved test needs a run, prefer `cnry run --probe` over a real sweep — same wire call, no dashboard/analytics/notification pollution.
16
16
  - **Canonry is the source of truth.** Read state back; never maintain a parallel copy in your head. Conclusions age, the data doesn't.
17
17
 
18
18
  ## Voice
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canonry
3
- description: "Agent-first AEO operating platform."
3
+ description: "Set up and operate Canonry AEO projects: inspect mention and citation coverage, diagnose regressions, run technical audits, and act through the Canonry CLI or MCP tools."
4
4
  metadata:
5
5
  {
6
6
  "agent":
@@ -16,14 +16,6 @@ metadata:
16
16
  "bins": ["canonry"],
17
17
  "label": "Install canonry globally",
18
18
  "command": "npm install -g @canonry/canonry"
19
- },
20
- {
21
- "id": "npx",
22
- "kind": "npx",
23
- "package": "@canonry/canonry",
24
- "bins": ["canonry"],
25
- "label": "Run canonry via npx",
26
- "command": "npx @canonry/canonry@latest init"
27
19
  }
28
20
  ],
29
21
  },
@@ -38,6 +30,50 @@ Agent-first open-source AEO (Answer Engine Optimization) operating platform. Tra
38
30
 
39
31
  **CLI:** invoke as `cnry` (short form) or `canonry` — both ship with the npm package and are interchangeable. Examples in this skill use `cnry`.
40
32
 
33
+ ## Runtime Preflight
34
+
35
+ Before using the native Codex or Claude Code plugin, verify the separately
36
+ installed Canonry runtime. It requires Node.js 22.14 or newer, and the native
37
+ plugin specifically needs the global package so `canonry-mcp` remains on
38
+ `PATH`; a one-off `npx` invocation is not sufficient:
39
+
40
+ ```bash
41
+ node --version
42
+ command -v cnry
43
+ cnry --version
44
+ ```
45
+
46
+ Then prepare the runtime in this order:
47
+
48
+ 1. With approval, install the global runtime if `command -v cnry` failed.
49
+ 2. Check only whether `$CANONRY_CONFIG_DIR/config.yaml` (when that variable is
50
+ set) or `~/.canonry/config.yaml` exists; do not print the file. If it does
51
+ not exist, pause and ask the operator to run `cnry init --skip-skills --skip-mcp`
52
+ in their own private terminal, then confirm completion without
53
+ pasting the output. Never execute `init` inside the agent session: it prompts
54
+ for provider credentials and prints the new full-access API key once. The
55
+ plugin already supplies the skills and MCP registration.
56
+ 3. After a fresh initialization, get approval and run `cnry start --format
57
+ json`; `start` waits for the health endpoint before returning. On an
58
+ existing installation, try the doctor command first and start only when the
59
+ transport is unavailable. Never stop or restart a healthy daemon just to
60
+ install the plugin.
61
+ 4. Run `cnry doctor --format json` after the daemon is ready. A successful JSON
62
+ response proves the health-check path works; individual checks may still
63
+ report actionable `warn` or `fail` statuses. Plugin installation state is
64
+ read live, so installing the plugin alone does not require a daemon restart.
65
+
66
+ Never ask the operator to paste credentials into the chat, print the raw API
67
+ key, or inspect `~/.canonry/config.yaml` for secrets. The plugin does not expand
68
+ the configured key's server-enforced scope, but it gives the client tools that
69
+ can exercise that scope. Fresh `cnry init` creates a full-instance `*` key, so
70
+ the default plugin has teammate-level access to every project and shared
71
+ instance settings. A write-capable key exposes write tools by default; a
72
+ read-only key restricts the catalog to reads, while a project-scoped key keeps
73
+ its project route boundary but is not tenant isolation: a write-capable scoped
74
+ key can still mutate shared instance settings. Do not work around a missing
75
+ tool or `403` by switching credentials.
76
+
41
77
  ## When to Use
42
78
 
43
79
  - Tracking brand **mentions** in AI answer text and **citations** in source links across providers
@@ -74,13 +110,13 @@ Configure `spec.brandAliases` on the project (or pass via `cnry apply`) so the m
74
110
 
75
111
  A canonry engagement follows the same loop regardless of project size:
76
112
 
77
- 1. **Diagnose** — Run a baseline sweep (`cnry run <project> --wait`) and a technical audit (`cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json`). The audit crawls every page in the project's sitemap (auto-discovered from `/sitemap.xml`, the sitemap index, or `robots.txt`) so readiness reflects the whole site, not just one page, and persists the score to the dashboard. Read Mention Coverage first, Citation Coverage second. See `references/aeo-analysis.md`.
113
+ 1. **Diagnose** — After explicit approval for the quota-consuming persisted runs, run a baseline sweep (`cnry run <project> --wait`) and a technical audit (`cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json`). The audit crawls every page in the project's sitemap (auto-discovered from `/sitemap.xml`, the sitemap index, or `robots.txt`) so readiness reflects the whole site, not just one page, and persists the score to the dashboard. Read Mention Coverage first, Citation Coverage second. See `references/aeo-analysis.md`.
78
114
  2. **Prioritize** — Triage by impact: indexing gaps → schema gaps → content gaps → query strategy. Branded-term losses are urgent.
79
115
  3. **Execute** — Apply fixes via the canonry CLI or platform integrations. Use `--dry-run` on supported mutations (`cnry project delete`, `cnry query replace`, `cnry backfill ...`) to preview before committing. See `references/canonry-cli.md` for the full command catalog and `references/wordpress-integration.md` for the WordPress workflow.
80
- 4. **Monitor** — Re-run sweeps weekly (`cnry run --all --wait` fans out across every project). Correlate visibility shifts with deployments and competitor moves.
116
+ 4. **Monitor** — Re-run sweeps weekly only through an operator-approved schedule or after fresh explicit approval (`cnry run --all --wait` fans out across every project). Correlate visibility shifts with deployments and competitor moves.
81
117
  5. **Report** — Lead with data, not interpretation: "Lost the mention for `<query>` on Gemini between <date> and <date> — two competitors moved in. Here's what to fix." For a one-command client-facing summary, run `cnry report <project>` to generate a self-contained HTML bundle (mention + citation hero, competitor landscape, GSC + GA4 performance, insights, suggested next queries). Same payload is available via `--format json` and the `canonry_report` MCP tool.
82
118
 
83
- **Verifying without polluting metrics**: when you need to test something on your own initiative — "did the latest provider deploy work?", "is this regression reproducible?", "would this query actually surface us?" — use `cnry run <project> --probe --provider <p> --query "..."`. Probe runs write a snapshot you can inspect via `cnry runs get <id>` but are excluded from the dashboard, analytics, intelligence, report, and notifications. Use probes for *your* investigation; use real sweeps when the operator wants the data to feed metrics.
119
+ **Verifying without polluting metrics**: when a test would help — "did the latest provider deploy work?", "is this regression reproducible?", "would this query actually surface us?" — propose the exact provider/query and get explicit approval before using `cnry run <project> --probe --provider <p> --query "..."`. Probe runs still cost quota and write a snapshot, but are excluded from the dashboard, analytics, intelligence, report, and notifications. Approval for one probe does not authorize repeats unless the operator approved a bounded batch; use real sweeps only when the operator wants the data to feed metrics.
84
120
 
85
121
  ## Surgical Reads
86
122
 
@@ -132,6 +168,7 @@ Aero also wakes unprompted after every `run.completed` so insights and regressio
132
168
 
133
169
  ## Boundaries & Safety
134
170
 
171
+ - **Get explicit approval before every mutation or quota-consuming sweep** — reads and `--dry-run` previews are safe defaults
135
172
  - **Never touch live WordPress without explicit approval**
136
173
  - **Back up `~/.canonry/config.yaml` before any config edit**
137
174
  - **Never fabricate mention or citation data** — if a sweep hasn't run, say so; never coerce `answerMentioned` null → false (null = "not checked")
@@ -151,5 +188,5 @@ Aero also wakes unprompted after every `run.completed` so insights and regressio
151
188
 
152
189
  ---
153
190
 
154
- **Tools:** canonry v4+, @ainyc/aeo-audit v1.3+
191
+ **Tools:** canonry v4+, @ainyc/aeo-audit v1.3+
155
192
  **Website:** [canonry.ai](https://canonry.ai) | **Org:** [ainyc.ai](https://ainyc.ai) | **Reference:** [AINYC AEO Methodology](https://ainyc.ai/aeo-methodology)
@@ -15,6 +15,11 @@ cnry serve --embed --embed-allow-origin https://app.example.com # read-only em
15
15
  cnry --version
16
16
  ```
17
17
 
18
+ `cnry init` prompts for credentials and prints the new API key once. An agent
19
+ must pause and ask the operator to run it in a private terminal, without
20
+ pasting the output back. Do not run `init` or secret-bearing `bootstrap` inside
21
+ an agent transcript.
22
+
18
23
  ### Read-only embed mode (#716)
19
24
 
20
25
  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.
@@ -115,7 +120,10 @@ Run statuses: `queued` → `running` → `completed` / `failed` / `partial`
115
120
  | `backfill` | `cnry backfill ...` | partial (historical) | ✅ | — | — |
116
121
  | **`probe`** | `cnry run --probe ...` | ❌ | ❌ | ❌ | ❌ |
117
122
 
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.
123
+ After explicit approval for the exact provider/query (or a bounded batch), use
124
+ `--probe` for verification rather than producing data the user/dashboard will
125
+ consume. A probe still costs provider quota and writes a snapshot; approval for
126
+ one probe does not authorize repeats.
119
127
 
120
128
  `snapshot` does not create a project or write to the DB. It generates category queries, runs providers, and produces a report for prospecting.
121
129
 
@@ -63,7 +63,9 @@ cnry google request-indexing <project> --all-unindexed
63
63
  - OAuth connection set up in canonry (`cnry settings` shows Google connection)
64
64
  - Officially intended for JobPosting/BroadcastEvent schema; in practice Google processes all URLs
65
65
 
66
- **After submitting:** Check coverage again after 48h. Once indexed, run a sweep — pages must be indexed before citation is possible.
66
+ **After submitting:** Check coverage again after 48h. Once indexed, ask for
67
+ explicit approval before running a quota-consuming sweep — pages must be
68
+ indexed before citation is possible.
67
69
 
68
70
  ---
69
71
 
@@ -160,4 +162,4 @@ Expected response: `202 Accepted`
160
162
  4. Submit sitemap to Bing WMT (manual, one-time per site)
161
163
  5. Send IndexNow batch for key URLs
162
164
  6. Re-check coverage after 48h
163
- 7. Run a sweep after pages confirmed indexed
165
+ 7. After explicit approval, run a sweep once pages are confirmed indexed
@@ -18795,7 +18795,7 @@ var routeCatalog = [
18795
18795
  method: "post",
18796
18796
  path: "/api/v1/projects/{name}/ads/activation-grants/{grantId}/revoke",
18797
18797
  summary: "Revoke or cancel an OpenAI Ads activation grant",
18798
- description: "Human-only kill switch. Before execution, the grant becomes revoked. After execution starts, or while an activation outcome is unknown, revocationRequestedAt is recorded atomically. Subsequent activation steps are blocked, and the recovery worker rolls back confirmed active entities. Unknown outcomes remain explicitly unknown while the watchdog repeatedly issues exact-tree safety pauses. A provider request already authorized or in flight may still settle before containment. Verified rollback leaves the single-use grant consumed and the operation failed.",
18798
+ description: "Human-only kill switch. Before execution, the grant becomes revoked. After execution starts, or while an activation outcome is unknown, revocationRequestedAt is recorded atomically. Subsequent activation steps are blocked, and the recovery worker rolls back confirmed active entities. Unknown outcomes remain explicitly unknown unless revocation was requested, the provider settlement window elapsed, and the watchdog can enumerate the complete provider tree and verify every entity paused. A provider request already authorized or in flight may still settle before containment. Verified rollback leaves the single-use grant consumed and the operation failed.",
18799
18799
  tags: ["ads"],
18800
18800
  parameters: [
18801
18801
  nameParameter,
@@ -23338,6 +23338,13 @@ function stableStringify3(value) {
23338
23338
  }
23339
23339
 
23340
23340
  // ../api-routes/src/google.ts
23341
+ function isOnProjectDomain(url, projectDomain) {
23342
+ try {
23343
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, "") === projectDomain;
23344
+ } catch {
23345
+ return false;
23346
+ }
23347
+ }
23341
23348
  function scopesForConnectionType(type) {
23342
23349
  switch (type) {
23343
23350
  case "gsc":
@@ -24142,14 +24149,21 @@ async function googleRoutes(app, opts) {
24142
24149
  latestByUrl.set(row.url, row);
24143
24150
  }
24144
24151
  }
24152
+ const gatherDomain = normalizeProjectDomain(project.canonicalDomain);
24145
24153
  const unindexedUrls = [];
24154
+ let skippedOtherHost = 0;
24146
24155
  for (const [url, row] of latestByUrl) {
24147
- if (row.indexingState !== "INDEXING_ALLOWED") {
24148
- unindexedUrls.push(url);
24156
+ if (row.indexingState === "INDEXING_ALLOWED") continue;
24157
+ if (!isOnProjectDomain(url, gatherDomain)) {
24158
+ skippedOtherHost += 1;
24159
+ continue;
24149
24160
  }
24161
+ unindexedUrls.push(url);
24150
24162
  }
24151
24163
  if (unindexedUrls.length === 0) {
24152
- throw validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
24164
+ throw validationError(
24165
+ skippedOtherHost > 0 ? `No unindexed URLs found on "${project.canonicalDomain}" (skipped ${skippedOtherHost} on other hosts). Run "canonry google inspect-sitemap" first.` : 'No unindexed URLs found. Run "canonry google inspect-sitemap" first.'
24166
+ );
24153
24167
  }
24154
24168
  urlsToNotify = unindexedUrls;
24155
24169
  }
@@ -24160,14 +24174,7 @@ async function googleRoutes(app, opts) {
24160
24174
  throw validationError(`Cannot request indexing for more than ${INDEXING_API_DAILY_LIMIT} URLs per request (got ${urlsToNotify.length})`);
24161
24175
  }
24162
24176
  const projectDomain = normalizeProjectDomain(project.canonicalDomain);
24163
- const invalidUrls = urlsToNotify.filter((url) => {
24164
- try {
24165
- const hostname = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
24166
- return hostname !== projectDomain;
24167
- } catch {
24168
- return true;
24169
- }
24170
- });
24177
+ const invalidUrls = urlsToNotify.filter((url) => !isOnProjectDomain(url, projectDomain));
24171
24178
  if (invalidUrls.length > 0) {
24172
24179
  throw validationError(
24173
24180
  `URLs must belong to project domain "${project.canonicalDomain}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`
@@ -25989,6 +25996,7 @@ async function executeApprovedAdsActivation(dependencies, request) {
25989
25996
  // ../api-routes/src/ads-activation-routes.ts
25990
25997
  var MAX_ACTIVATION_GRANT_LIFETIME_MS = 24 * 60 * 60 * 1e3;
25991
25998
  var ACTIVATION_LEASE_MS = 5 * 6e4;
25999
+ var ACTIVATION_REVOCATION_SETTLEMENT_MS = ACTIVATION_LEASE_MS;
25992
26000
  var ACTIVATION_STEP_INSERT_BATCH_SIZE = 40;
25993
26001
  function parseBody(schema, value) {
25994
26002
  const parsed = schema.safeParse(value);
@@ -26554,6 +26562,56 @@ function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLea
26554
26562
  function selectRecoveryWatchdogOperations(app, nowIso, batchSize) {
26555
26563
  return selectWatchdogOperations(app, nowIso, batchSize, false);
26556
26564
  }
26565
+ function finalizeContainedRevokedUnknownActivation(app, operation, grant) {
26566
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
26567
+ const revocationRequestedAt = grant.revocationRequestedAt === null ? Number.NaN : Date.parse(grant.revocationRequestedAt);
26568
+ if (!Number.isFinite(revocationRequestedAt) || Date.parse(nowIso) - revocationRequestedAt < ACTIVATION_REVOCATION_SETTLEMENT_MS) {
26569
+ return false;
26570
+ }
26571
+ return app.db.transaction((tx) => {
26572
+ const operationUpdate = tx.update(adsOperations).set({
26573
+ state: AdsOperationStates.failed,
26574
+ errorCode: AdsActivationErrorCodes.grantRevoked,
26575
+ errorMessage: "Ads activation was cancelled and the provider tree was verified paused",
26576
+ leaseOwner: null,
26577
+ leaseExpiresAt: null,
26578
+ lastReconciledAt: nowIso,
26579
+ updatedAt: nowIso
26580
+ }).where(and19(
26581
+ eq25(adsOperations.id, operation.id),
26582
+ eq25(adsOperations.state, AdsOperationStates.unknown)
26583
+ )).run();
26584
+ if (operationUpdate.changes !== 1) return false;
26585
+ const grantUpdate = tx.update(adsActivationGrants).set({
26586
+ state: AdsActivationGrantStates.consumed,
26587
+ consumedAt: nowIso,
26588
+ updatedAt: nowIso
26589
+ }).where(and19(
26590
+ eq25(adsActivationGrants.id, grant.id),
26591
+ eq25(adsActivationGrants.operationId, operation.id),
26592
+ eq25(adsActivationGrants.state, AdsActivationGrantStates.unknown),
26593
+ isNotNull(adsActivationGrants.revocationRequestedAt)
26594
+ )).run();
26595
+ if (grantUpdate.changes !== 1) {
26596
+ throw internalError("Revoked unknown ads activation could not be finalized");
26597
+ }
26598
+ writeAuditLog(tx, {
26599
+ projectId: operation.projectId,
26600
+ actor: "system",
26601
+ action: "ads.campaign_tree_activate.failed",
26602
+ entityType: AdsActivationEntityTypes.campaign,
26603
+ entityId: operation.entityId,
26604
+ diff: {
26605
+ operationId: operation.id,
26606
+ operationKey: operation.operationKey,
26607
+ grantId: grant.id,
26608
+ reason: "revoked_and_verified_paused"
26609
+ },
26610
+ userAgent: "canonry-activation-watchdog"
26611
+ });
26612
+ return true;
26613
+ }, { behavior: "immediate" });
26614
+ }
26557
26615
  function validateWatchdogContainmentBinding(app, opts, operation, grant) {
26558
26616
  const terminal = terminalActivationResponse(app, opts, grant, operation.operationKey);
26559
26617
  if (terminal) {
@@ -26703,6 +26761,13 @@ async function sweepStaleAdsActivationsOnce(app, opts) {
26703
26761
  runtime.provider,
26704
26762
  grant.manifest
26705
26763
  );
26764
+ if (safety.fullyVerified && grant.revocationRequestedAt !== null && finalizeContainedRevokedUnknownActivation(app, operation, grant)) {
26765
+ app.log.warn(
26766
+ { operationId: operation.id },
26767
+ "Revoked unknown ads activation was finalized after verified pause"
26768
+ );
26769
+ return;
26770
+ }
26706
26771
  app.db.update(adsOperations).set({
26707
26772
  reconcileAttempts: operation.reconcileAttempts + 1,
26708
26773
  lastReconciledAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -38362,12 +38427,47 @@ import crypto29 from "crypto";
38362
38427
  import fs6 from "fs";
38363
38428
  import path7 from "path";
38364
38429
  var REQUIRED_SKILLS = ["canonry", "aero"];
38430
+ function pluginStateFor(ctx) {
38431
+ try {
38432
+ return ctx.getAgentPluginState?.();
38433
+ } catch {
38434
+ return void 0;
38435
+ }
38436
+ }
38437
+ function unverifiedPluginClients(state) {
38438
+ if (!state) return [];
38439
+ return state.configuredClients.filter((client) => !state.verifiedClients.includes(client));
38440
+ }
38441
+ function displayPluginClients(clients) {
38442
+ return clients.map((client) => client === "claude-code" ? "Claude Code" : "Codex").join(" + ");
38443
+ }
38365
38444
  var skillsInstalledCheck = {
38366
38445
  id: "agent.skills.installed",
38367
38446
  category: CheckCategories.agent,
38368
38447
  scope: CheckScopes.global,
38369
- title: "Agent skills installed (~/.claude/skills/)",
38370
- run: () => {
38448
+ title: "Agent skills available",
38449
+ run: (ctx) => {
38450
+ const agentPlugin = pluginStateFor(ctx);
38451
+ const unverifiedClients = unverifiedPluginClients(agentPlugin);
38452
+ if (unverifiedClients.length > 0) {
38453
+ return {
38454
+ status: CheckStatuses.warn,
38455
+ code: "agent.skills.plugin-unverified",
38456
+ summary: `The Canonry plugin is enabled for ${displayPluginClients(unverifiedClients)}, but its cached manifest and skill assets could not be verified.`,
38457
+ remediation: "Reinstall `canonry@canonry` with the affected client plugin manager, or disable the stale plugin entry.",
38458
+ details: { delivery: "plugin", ...agentPlugin, unverifiedClients }
38459
+ };
38460
+ }
38461
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
38462
+ const clients = displayPluginClients(agentPlugin.verifiedClients);
38463
+ return {
38464
+ status: CheckStatuses.ok,
38465
+ code: "agent.skills.plugin-installed",
38466
+ summary: `Canonry skills are available through the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"}.`,
38467
+ remediation: null,
38468
+ details: { delivery: "plugin", ...agentPlugin }
38469
+ };
38470
+ }
38371
38471
  const home = process.env.HOME;
38372
38472
  if (!home) {
38373
38473
  return {
@@ -38388,7 +38488,8 @@ var skillsInstalledCheck = {
38388
38488
  const details = {
38389
38489
  checkedPath: skillsBase,
38390
38490
  installed,
38391
- missing
38491
+ missing,
38492
+ ...agentPlugin ? { agentPlugin } : {}
38392
38493
  };
38393
38494
  if (missing.length === 0) {
38394
38495
  return {
@@ -38421,9 +38522,51 @@ var skillsCurrentCheck = {
38421
38522
  id: "agent.skills.current",
38422
38523
  category: CheckCategories.agent,
38423
38524
  scope: CheckScopes.global,
38424
- title: "Agent skills up to date (~/.claude/skills/)",
38525
+ title: "Agent skills up to date",
38425
38526
  run: (ctx) => {
38527
+ const agentPlugin = pluginStateFor(ctx);
38426
38528
  const bundled = ctx.bundledSkills;
38529
+ const unverifiedClients = unverifiedPluginClients(agentPlugin);
38530
+ if (unverifiedClients.length > 0) {
38531
+ return {
38532
+ status: CheckStatuses.warn,
38533
+ code: "agent.skills.plugin-unverified",
38534
+ summary: `The Canonry plugin cache could not be verified for ${displayPluginClients(unverifiedClients)}; freshness cannot be assessed.`,
38535
+ remediation: "Reinstall `canonry@canonry` with the affected client plugin manager, or disable the stale plugin entry.",
38536
+ details: { delivery: "plugin", ...agentPlugin, unverifiedClients }
38537
+ };
38538
+ }
38539
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
38540
+ const clients = displayPluginClients(agentPlugin.verifiedClients);
38541
+ if (!bundled || bundled.length === 0) {
38542
+ return {
38543
+ status: CheckStatuses.skipped,
38544
+ code: "agent.skills.bundle-unavailable",
38545
+ summary: `Canonry skills are supplied by the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"}, but the running bundle version is unavailable for comparison.`,
38546
+ remediation: null,
38547
+ details: { delivery: "plugin", ...agentPlugin }
38548
+ };
38549
+ }
38550
+ const bundledVersion2 = bundled[0].version;
38551
+ const mismatchedClients = agentPlugin.verifiedClients.filter((client) => agentPlugin.verifiedClientVersions?.[client] !== bundledVersion2);
38552
+ if (mismatchedClients.length > 0) {
38553
+ const versions = mismatchedClients.map((client) => `${client === "claude-code" ? "Claude Code" : "Codex"} v${agentPlugin.verifiedClientVersions?.[client] ?? "unknown"}`).join(", ");
38554
+ return {
38555
+ status: CheckStatuses.warn,
38556
+ code: "agent.skills.plugin-version-mismatch",
38557
+ summary: `${versions} ${mismatchedClients.length === 1 ? "does" : "do"} not match the running Canonry v${bundledVersion2}; plugin skills may be stale or incompatible.`,
38558
+ remediation: "Update the Canonry runtime and `canonry@canonry` plugin to the same version with the affected client plugin manager.",
38559
+ details: { delivery: "plugin", bundledVersion: bundledVersion2, ...agentPlugin, mismatchedClients }
38560
+ };
38561
+ }
38562
+ return {
38563
+ status: CheckStatuses.ok,
38564
+ code: "agent.skills.plugin-current",
38565
+ summary: `Canonry skills are supplied by the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"} at the running version (v${bundledVersion2}).`,
38566
+ remediation: null,
38567
+ details: { delivery: "plugin", bundledVersion: bundledVersion2, ...agentPlugin }
38568
+ };
38569
+ }
38427
38570
  if (!bundled || bundled.length === 0) {
38428
38571
  return {
38429
38572
  status: CheckStatuses.skipped,
@@ -40427,7 +40570,8 @@ async function doctorRoutes(app, opts) {
40427
40570
  getAgentProviderSummary: opts.getAgentProviderSummary,
40428
40571
  trafficSourceValidators: opts.trafficSourceValidators,
40429
40572
  runtimeStatePaths: opts.runtimeStatePaths,
40430
- bundledSkills: opts.bundledSkills
40573
+ bundledSkills: opts.bundledSkills,
40574
+ getAgentPluginState: opts.getAgentPluginState
40431
40575
  };
40432
40576
  return runChecks(ctx, ALL_CHECKS, { checkIds });
40433
40577
  });
@@ -40454,7 +40598,8 @@ async function doctorRoutes(app, opts) {
40454
40598
  getAgentProviderSummary: opts.getAgentProviderSummary,
40455
40599
  trafficSourceValidators: opts.trafficSourceValidators,
40456
40600
  runtimeStatePaths: opts.runtimeStatePaths,
40457
- bundledSkills: opts.bundledSkills
40601
+ bundledSkills: opts.bundledSkills,
40602
+ getAgentPluginState: opts.getAgentPluginState
40458
40603
  };
40459
40604
  return runChecks(ctx, ALL_CHECKS, { checkIds });
40460
40605
  });
@@ -41496,7 +41641,8 @@ async function apiRoutes(app, opts) {
41496
41641
  getAgentProviderSummary: opts.getAgentProviderSummary,
41497
41642
  trafficSourceValidators: buildTrafficSourceValidators(opts),
41498
41643
  runtimeStatePaths: opts.runtimeStatePaths,
41499
- bundledSkills: opts.bundledSkills
41644
+ bundledSkills: opts.bundledSkills,
41645
+ getAgentPluginState: opts.getAgentPluginState
41500
41646
  });
41501
41647
  if (opts.registerAuthenticatedRoutes) {
41502
41648
  await opts.registerAuthenticatedRoutes(api);
@@ -116,7 +116,7 @@ import {
116
116
  siteAuditPages,
117
117
  siteAuditSnapshots,
118
118
  usageCounters
119
- } from "./chunk-HQRUFTLN.js";
119
+ } from "./chunk-EF67Y6CI.js";
120
120
  import {
121
121
  AGENT_MEMORY_VALUE_MAX_BYTES,
122
122
  AGENT_PROVIDER_IDS,
@@ -6967,7 +6967,7 @@ function readStoredGroundingSources(rawResponse) {
6967
6967
  return result;
6968
6968
  }
6969
6969
  async function backfillInsightsCommand(project, opts) {
6970
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-2WKS3EWJ.js");
6970
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NTIKN7YG.js");
6971
6971
  const config = loadConfig();
6972
6972
  const db = createClient(config.database);
6973
6973
  migrate(db);
@@ -7776,7 +7776,7 @@ function emitInstallSummary(summary, format) {
7776
7776
  Target: ${summary.targetDir}`);
7777
7777
  console.log(summary.message);
7778
7778
  }
7779
- function getMissingUserSkillsNudge(home) {
7779
+ function getMissingUserSkillsNudge(home, agentPlugin) {
7780
7780
  if (!home) return null;
7781
7781
  const skillsBase = path5.join(home, ".claude", "skills");
7782
7782
  const installed = [];
@@ -7786,6 +7786,25 @@ function getMissingUserSkillsNudge(home) {
7786
7786
  if (existsSafe(skillFile)) installed.push(name);
7787
7787
  else missing.push(name);
7788
7788
  }
7789
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
7790
+ const unverifiedClients = agentPlugin.configuredClients.filter((client) => !agentPlugin.verifiedClients.includes(client));
7791
+ const mismatchedClients = agentPlugin.verifiedClients.filter((client) => agentPlugin.verifiedClientVersions?.[client] !== PACKAGE_VERSION);
7792
+ if (unverifiedClients.length === 0 && mismatchedClients.length === 0) return null;
7793
+ const displayClients = (clients) => clients.map((client) => client === "claude-code" ? "Claude Code" : "Codex").join(" + ");
7794
+ const problems = [];
7795
+ if (unverifiedClients.length > 0) {
7796
+ problems.push(`The Canonry plugin is enabled for ${displayClients(unverifiedClients)}, but its cached manifest and skill assets could not be verified.`);
7797
+ }
7798
+ if (mismatchedClients.length > 0) {
7799
+ const versions = mismatchedClients.map((client) => `${client === "claude-code" ? "Claude Code" : "Codex"} v${agentPlugin.verifiedClientVersions?.[client] ?? "unknown"}`).join(", ");
7800
+ problems.push(`${versions} ${mismatchedClients.length === 1 ? "does" : "do"} not match the running Canonry v${PACKAGE_VERSION}.`);
7801
+ }
7802
+ return {
7803
+ message: `Tip: ${problems.join(" ")} Update or reinstall \`canonry@canonry\` with the affected client plugin manager.`,
7804
+ missing,
7805
+ installed
7806
+ };
7807
+ }
7789
7808
  if (missing.length === 0) return null;
7790
7809
  const fix = missing.length === BUNDLED_SKILL_NAMES.length ? "canonry skills install --user" : `canonry skills install ${missing.join(" ")} --user`;
7791
7810
  return {
@@ -12574,6 +12593,7 @@ async function createServer(opts) {
12574
12593
  return void 0;
12575
12594
  }
12576
12595
  })(),
12596
+ getAgentPluginState: opts.getAgentPluginState,
12577
12597
  // Local canonry serve runs on the operator's machine, where pointing a
12578
12598
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
12579
12599
  // etc.) is a legitimate workflow. Default to allowing it for the local
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-A4YBV4Y4.js";
32
+ } from "./chunk-ZHWWSU7H.js";
33
33
  import {
34
34
  CliError,
35
35
  EXIT_SYSTEM_ERROR,
@@ -54,7 +54,7 @@ import {
54
54
  projects,
55
55
  queries,
56
56
  renderReportHtml
57
- } from "./chunk-HQRUFTLN.js";
57
+ } from "./chunk-EF67Y6CI.js";
58
58
  import {
59
59
  AdsOperationKinds,
60
60
  AdsOperationStates,
@@ -141,9 +141,9 @@ import { parseArgs } from "util";
141
141
  function commandId(spec) {
142
142
  return spec.path.join(".");
143
143
  }
144
- function matchesPath(args, path11) {
145
- if (args.length < path11.length) return false;
146
- return path11.every((segment, index) => args[index] === segment);
144
+ function matchesPath(args, path12) {
145
+ if (args.length < path12.length) return false;
146
+ return path12.every((segment, index) => args[index] === segment);
147
147
  }
148
148
  function withGlobalOptions(options) {
149
149
  const base = options ? { ...options } : {};
@@ -3506,9 +3506,9 @@ async function gaConnect(project, opts) {
3506
3506
  propertyId: opts.propertyId
3507
3507
  };
3508
3508
  if (opts.keyFile) {
3509
- const fs15 = await import("fs");
3509
+ const fs16 = await import("fs");
3510
3510
  try {
3511
- const content = fs15.readFileSync(opts.keyFile, "utf-8");
3511
+ const content = fs16.readFileSync(opts.keyFile, "utf-8");
3512
3512
  JSON.parse(content);
3513
3513
  body.keyJson = content;
3514
3514
  } catch (e) {
@@ -4766,9 +4766,9 @@ async function getCommand(opts) {
4766
4766
  console.log(JSON.stringify(leaf, null, 2));
4767
4767
  }
4768
4768
  }
4769
- function walkPath(value, path11) {
4770
- if (!path11 || path11 === ".") return value;
4771
- const segments = path11.split(".").flatMap((part) => {
4769
+ function walkPath(value, path12) {
4770
+ if (!path12 || path12 === ".") return value;
4771
+ const segments = path12.split(".").flatMap((part) => {
4772
4772
  const tokens = [];
4773
4773
  let i = 0;
4774
4774
  const bracketStart = part.indexOf("[");
@@ -4816,13 +4816,13 @@ var GET_CLI_COMMANDS = [
4816
4816
  },
4817
4817
  run: async (input) => {
4818
4818
  const project = requireProject(input, "get", USAGE2);
4819
- const path11 = requirePositional(input, 1, {
4819
+ const path12 = requirePositional(input, 1, {
4820
4820
  command: "get",
4821
4821
  usage: USAGE2,
4822
4822
  message: 'path is required (e.g. "scores.mentionShare.value")'
4823
4823
  });
4824
4824
  const from = getString(input.values, "from");
4825
- await getCommand({ project, path: path11, from, format: input.format });
4825
+ await getCommand({ project, path: path12, from, format: input.format });
4826
4826
  }
4827
4827
  }
4828
4828
  ];
@@ -4861,9 +4861,9 @@ async function trafficConnectWordpress(project, opts) {
4861
4861
  }
4862
4862
  let applicationPassword = opts.appPassword?.trim() ?? "";
4863
4863
  if (!applicationPassword && opts.appPasswordFile) {
4864
- const fs15 = await import("fs");
4864
+ const fs16 = await import("fs");
4865
4865
  try {
4866
- applicationPassword = fs15.readFileSync(opts.appPasswordFile, "utf-8").trim();
4866
+ applicationPassword = fs16.readFileSync(opts.appPasswordFile, "utf-8").trim();
4867
4867
  } catch (e) {
4868
4868
  const msg = e instanceof Error ? e.message : String(e);
4869
4869
  throw new CliError({
@@ -4919,10 +4919,10 @@ async function trafficConnectCloudRun(project, opts) {
4919
4919
  details: { project }
4920
4920
  });
4921
4921
  }
4922
- const fs15 = await import("fs");
4922
+ const fs16 = await import("fs");
4923
4923
  let keyJson;
4924
4924
  try {
4925
- keyJson = fs15.readFileSync(opts.serviceAccountKey, "utf-8");
4925
+ keyJson = fs16.readFileSync(opts.serviceAccountKey, "utf-8");
4926
4926
  JSON.parse(keyJson);
4927
4927
  } catch (e) {
4928
4928
  const msg = e instanceof Error ? e.message : String(e);
@@ -4982,9 +4982,9 @@ async function trafficConnectVercel(project, opts) {
4982
4982
  }
4983
4983
  let token = opts.token?.trim() ?? "";
4984
4984
  if (!token && opts.tokenFile) {
4985
- const fs15 = await import("fs");
4985
+ const fs16 = await import("fs");
4986
4986
  try {
4987
- token = fs15.readFileSync(opts.tokenFile, "utf-8").trim();
4987
+ token = fs16.readFileSync(opts.tokenFile, "utf-8").trim();
4988
4988
  } catch (e) {
4989
4989
  const msg = e instanceof Error ? e.message : String(e);
4990
4990
  throw new CliError({
@@ -12026,6 +12026,177 @@ function encodeSetupState(state) {
12026
12026
  return flags.length > 0 ? flags.sort().join("|") : "none";
12027
12027
  }
12028
12028
 
12029
+ // src/agent-plugin.ts
12030
+ import fs14 from "fs";
12031
+ import path11 from "path";
12032
+ var CANONRY_PLUGIN_ID = "canonry@canonry";
12033
+ var REQUIRED_SKILLS = ["canonry", "aero"];
12034
+ function parseSemver(version) {
12035
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-z.-]+))?(?:\+[0-9a-z.-]+)?$/i.exec(version);
12036
+ if (!match) return null;
12037
+ return {
12038
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
12039
+ prerelease: match.at(4)?.split(".")
12040
+ };
12041
+ }
12042
+ function compareSemver(left, right) {
12043
+ const leftVersion = parseSemver(left);
12044
+ const rightVersion = parseSemver(right);
12045
+ for (let index = 0; index < leftVersion.core.length; index += 1) {
12046
+ if (leftVersion.core[index] !== rightVersion.core[index]) {
12047
+ return leftVersion.core[index] > rightVersion.core[index] ? 1 : -1;
12048
+ }
12049
+ }
12050
+ if (!leftVersion.prerelease && !rightVersion.prerelease) return 0;
12051
+ if (!leftVersion.prerelease) return 1;
12052
+ if (!rightVersion.prerelease) return -1;
12053
+ const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
12054
+ for (let index = 0; index < length; index += 1) {
12055
+ const leftIdentifier = leftVersion.prerelease.at(index);
12056
+ const rightIdentifier = rightVersion.prerelease.at(index);
12057
+ if (leftIdentifier === void 0) return -1;
12058
+ if (rightIdentifier === void 0) return 1;
12059
+ if (leftIdentifier === rightIdentifier) continue;
12060
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
12061
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
12062
+ if (leftNumeric && rightNumeric) return Number(leftIdentifier) > Number(rightIdentifier) ? 1 : -1;
12063
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
12064
+ return leftIdentifier > rightIdentifier ? 1 : -1;
12065
+ }
12066
+ return 0;
12067
+ }
12068
+ function isCanonryPluginId(value) {
12069
+ return value.trim() === CANONRY_PLUGIN_ID;
12070
+ }
12071
+ function readJson(filePath) {
12072
+ try {
12073
+ return JSON.parse(fs14.readFileSync(filePath, "utf8"));
12074
+ } catch {
12075
+ return null;
12076
+ }
12077
+ }
12078
+ function readClaudePluginConfigured(filePath) {
12079
+ const parsed = readJson(filePath);
12080
+ if (!parsed || typeof parsed !== "object") return false;
12081
+ const enabledPlugins = parsed.enabledPlugins;
12082
+ if (!enabledPlugins || typeof enabledPlugins !== "object") return false;
12083
+ for (const [id, enabled] of Object.entries(enabledPlugins)) {
12084
+ if (isCanonryPluginId(id)) return enabled === true;
12085
+ }
12086
+ return false;
12087
+ }
12088
+ function readCodexPluginConfigured(filePath) {
12089
+ try {
12090
+ let currentPlugin = null;
12091
+ let inPluginsTable = false;
12092
+ for (const line of fs14.readFileSync(filePath, "utf8").split(/\r?\n/)) {
12093
+ const section = /^\s*\[plugins\.(?:"([^"]+)"|'([^']+)'|(\S+))\]\s*(?:#.*)?$/.exec(line);
12094
+ if (section) {
12095
+ const captures = section.slice(1, 4);
12096
+ currentPlugin = captures.find((value) => value != null && value.length > 0) ?? null;
12097
+ inPluginsTable = false;
12098
+ continue;
12099
+ }
12100
+ if (/^\s*\[plugins\]\s*(?:#.*)?$/.test(line)) {
12101
+ currentPlugin = null;
12102
+ inPluginsTable = true;
12103
+ continue;
12104
+ }
12105
+ if (/^\s*\[/.test(line)) {
12106
+ currentPlugin = null;
12107
+ inPluginsTable = false;
12108
+ continue;
12109
+ }
12110
+ if (currentPlugin && isCanonryPluginId(currentPlugin)) {
12111
+ const enabled = /^\s*enabled\s*=\s*(true|false)\s*(?:#.*)?$/.exec(line);
12112
+ if (enabled) return enabled[1] === "true";
12113
+ } else if (inPluginsTable) {
12114
+ const inline = /^\s*(?:"([^"]+)"|'([^']+)'|([^=\s]+))\s*=\s*\{[^}]*\benabled\s*=\s*(true|false)\b[^}]*\}\s*(?:#.*)?$/.exec(line);
12115
+ const id = inline?.[1] ?? inline?.[2] ?? inline?.[3];
12116
+ if (inline && id && isCanonryPluginId(id)) return inline[4] === "true";
12117
+ }
12118
+ }
12119
+ return false;
12120
+ } catch {
12121
+ return false;
12122
+ }
12123
+ }
12124
+ function verifiedPluginVersion(root, client) {
12125
+ const manifestDir = client === "claude-code" ? ".claude-plugin" : ".codex-plugin";
12126
+ const manifest = readJson(path11.join(root, manifestDir, "plugin.json"));
12127
+ if (!manifest || typeof manifest !== "object") return null;
12128
+ const plugin = manifest;
12129
+ if (plugin.name !== "canonry" || typeof plugin.version !== "string" || !parseSemver(plugin.version) || plugin.skills !== "./skills/" || plugin.mcpServers !== "./.mcp.json") {
12130
+ return null;
12131
+ }
12132
+ const mcp = readJson(path11.join(root, ".mcp.json"));
12133
+ if (!mcp || typeof mcp !== "object") return null;
12134
+ const canonryServer = mcp.mcpServers?.canonry;
12135
+ if (canonryServer?.command !== "canonry-mcp") return null;
12136
+ return REQUIRED_SKILLS.every((name) => fs14.existsSync(path11.join(root, "skills", name, "SKILL.md"))) ? plugin.version : null;
12137
+ }
12138
+ function readClaudeUserInstallPaths(claudeConfigDir) {
12139
+ const parsed = readJson(path11.join(claudeConfigDir, "plugins", "installed_plugins.json"));
12140
+ if (!parsed || typeof parsed !== "object") return [];
12141
+ const plugins = parsed.plugins;
12142
+ if (!plugins || typeof plugins !== "object") return [];
12143
+ const installs = plugins[CANONRY_PLUGIN_ID];
12144
+ if (!Array.isArray(installs)) return [];
12145
+ return installs.flatMap((install) => {
12146
+ if (!install || typeof install !== "object") return [];
12147
+ const record = install;
12148
+ return record.scope === "user" && typeof record.installPath === "string" ? [record.installPath] : [];
12149
+ });
12150
+ }
12151
+ function listDirectories(dir) {
12152
+ try {
12153
+ return fs14.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path11.join(dir, entry.name));
12154
+ } catch {
12155
+ return [];
12156
+ }
12157
+ }
12158
+ function newestVersion(versions) {
12159
+ return versions.sort(compareSemver).at(-1) ?? null;
12160
+ }
12161
+ function verifiedClaudeVersion(claudeConfigDir) {
12162
+ return newestVersion(readClaudeUserInstallPaths(claudeConfigDir).map((installPath) => verifiedPluginVersion(installPath, "claude-code")).filter((version) => version !== null));
12163
+ }
12164
+ function verifiedCodexVersion(codexHome) {
12165
+ const pluginCache = path11.join(codexHome, "plugins", "cache", "canonry", "canonry");
12166
+ const versionedRoots = listDirectories(pluginCache).map((installPath) => ({ installPath, version: path11.basename(installPath) })).filter((candidate) => parseSemver(candidate.version) !== null).sort((left, right) => compareSemver(left.version, right.version));
12167
+ const active = versionedRoots.at(-1);
12168
+ if (!active) return verifiedPluginVersion(pluginCache, "codex");
12169
+ const manifestVersion = verifiedPluginVersion(active.installPath, "codex");
12170
+ return manifestVersion === active.version ? manifestVersion : null;
12171
+ }
12172
+ function detectCanonryAgentPlugin(opts = {}) {
12173
+ const home = opts.home?.trim() || null;
12174
+ const claudeConfigDir = opts.claudeConfigDir?.trim() || (home ? path11.join(home, ".claude") : null);
12175
+ const codexHome = opts.codexHome?.trim() || (home ? path11.join(home, ".codex") : null);
12176
+ const configuredClients = [];
12177
+ const verifiedClients = [];
12178
+ const verifiedClientVersions = {};
12179
+ const claudeConfigured = claudeConfigDir ? readClaudePluginConfigured(path11.join(claudeConfigDir, "settings.json")) : false;
12180
+ if (claudeConfigured) {
12181
+ configuredClients.push("claude-code");
12182
+ const version = claudeConfigDir ? verifiedClaudeVersion(claudeConfigDir) : null;
12183
+ if (version) {
12184
+ verifiedClients.push("claude-code");
12185
+ verifiedClientVersions["claude-code"] = version;
12186
+ }
12187
+ }
12188
+ const codexConfigured = codexHome ? readCodexPluginConfigured(path11.join(codexHome, "config.toml")) : false;
12189
+ if (codexConfigured) {
12190
+ configuredClients.push("codex");
12191
+ const version = codexHome ? verifiedCodexVersion(codexHome) : null;
12192
+ if (version) {
12193
+ verifiedClients.push("codex");
12194
+ verifiedClientVersions.codex = version;
12195
+ }
12196
+ }
12197
+ return { configuredClients, verifiedClients, verifiedClientVersions };
12198
+ }
12199
+
12029
12200
  // src/commands/serve.ts
12030
12201
  function resolveServePort(envPort, configPort) {
12031
12202
  const trimmed = envPort?.trim();
@@ -12063,7 +12234,12 @@ async function serveCommand(format = "text") {
12063
12234
  process.stderr.write(`warning: ai-referral-paths backfill skipped: ${msg}
12064
12235
  `);
12065
12236
  }
12066
- const app = await createServer({ config, db, host });
12237
+ const getAgentPluginState = () => detectCanonryAgentPlugin({
12238
+ home: process.env.HOME,
12239
+ claudeConfigDir: process.env.CLAUDE_CONFIG_DIR,
12240
+ codexHome: process.env.CODEX_HOME
12241
+ });
12242
+ const app = await createServer({ config, db, host, getAgentPluginState });
12067
12243
  let shuttingDown = false;
12068
12244
  const shutdown = (signal) => {
12069
12245
  if (shuttingDown) return;
@@ -12099,7 +12275,7 @@ Canonry server running at ${url}`);
12099
12275
  console.log("First-run dashboard password setup is unauthenticated only on loopback; complete setup from this machine first or use a bearer cnry_... key.");
12100
12276
  }
12101
12277
  console.log("Press Ctrl+C to stop.\n");
12102
- const nudge = getMissingUserSkillsNudge(process.env.HOME);
12278
+ const nudge = getMissingUserSkillsNudge(process.env.HOME, getAgentPluginState());
12103
12279
  if (nudge) process.stderr.write(`${nudge.message}
12104
12280
  `);
12105
12281
  }
@@ -12855,7 +13031,7 @@ var VISIBILITY_STATS_CLI_COMMANDS = [
12855
13031
  ];
12856
13032
 
12857
13033
  // src/cli-commands/wordpress.ts
12858
- import fs14 from "fs";
13034
+ import fs15 from "fs";
12859
13035
 
12860
13036
  // src/commands/wordpress.ts
12861
13037
  function getClient26() {
@@ -13094,12 +13270,12 @@ async function wordpressSetMeta(project, body) {
13094
13270
  printPageDetail(result);
13095
13271
  }
13096
13272
  async function wordpressBulkSetMeta(project, opts) {
13097
- const fs15 = await import("fs/promises");
13098
- const path11 = await import("path");
13099
- const filePath = path11.resolve(opts.from);
13273
+ const fs16 = await import("fs/promises");
13274
+ const path12 = await import("path");
13275
+ const filePath = path12.resolve(opts.from);
13100
13276
  let raw;
13101
13277
  try {
13102
- raw = await fs15.readFile(filePath, "utf8");
13278
+ raw = await fs16.readFile(filePath, "utf8");
13103
13279
  } catch {
13104
13280
  throw new CliError({
13105
13281
  code: "FILE_READ_ERROR",
@@ -13196,13 +13372,13 @@ async function wordpressSetSchema(project, body) {
13196
13372
  printManualAssist(`Schema update for "${body.slug}"`, result);
13197
13373
  }
13198
13374
  async function wordpressSchemaDeploy(project, opts) {
13199
- const fs15 = await import("fs/promises");
13200
- const path11 = await import("path");
13375
+ const fs16 = await import("fs/promises");
13376
+ const path12 = await import("path");
13201
13377
  const yaml = await loadYamlModule();
13202
- const filePath = path11.resolve(opts.profile);
13378
+ const filePath = path12.resolve(opts.profile);
13203
13379
  let raw;
13204
13380
  try {
13205
- raw = await fs15.readFile(filePath, "utf8");
13381
+ raw = await fs16.readFile(filePath, "utf8");
13206
13382
  } catch {
13207
13383
  throw new CliError({
13208
13384
  code: "FILE_READ_ERROR",
@@ -13307,13 +13483,13 @@ async function wordpressOnboard(project, opts) {
13307
13483
  }
13308
13484
  let profileData;
13309
13485
  if (opts.profile) {
13310
- const fs15 = await import("fs/promises");
13311
- const path11 = await import("path");
13486
+ const fs16 = await import("fs/promises");
13487
+ const path12 = await import("path");
13312
13488
  const yaml = await loadYamlModule();
13313
- const filePath = path11.resolve(opts.profile);
13489
+ const filePath = path12.resolve(opts.profile);
13314
13490
  let raw;
13315
13491
  try {
13316
- raw = await fs15.readFile(filePath, "utf8");
13492
+ raw = await fs16.readFile(filePath, "utf8");
13317
13493
  } catch {
13318
13494
  throw new CliError({
13319
13495
  code: "FILE_READ_ERROR",
@@ -13462,7 +13638,7 @@ function resolveContent(input, command, usage, options) {
13462
13638
  }
13463
13639
  if (contentFile) {
13464
13640
  try {
13465
- return fs14.readFileSync(contentFile, "utf-8");
13641
+ return fs15.readFileSync(contentFile, "utf-8");
13466
13642
  } catch (error) {
13467
13643
  const message = error instanceof Error ? error.message : String(error);
13468
13644
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FastifyInstance } from 'fastify';
2
2
  import { DatabaseClient } from '@ainyc/canonry-db';
3
- import { ProviderQuotaPolicy, EmbedConfigEntry } from '@ainyc/canonry-contracts';
3
+ import { ProviderQuotaPolicy, EmbedConfigEntry, AgentPluginState } from '@ainyc/canonry-contracts';
4
4
 
5
5
  type GoogleConnectionType = 'gsc' | 'ga4' | 'gbp';
6
6
  interface ProviderConfigEntry {
@@ -264,6 +264,8 @@ declare function createServer(opts: {
264
264
  * assert the injected config + framing header on the served document.
265
265
  */
266
266
  assetsDir?: string;
267
+ /** Live user-global native Canonry plugin state for agent-skills doctor checks. */
268
+ getAgentPluginState?: () => AgentPluginState;
267
269
  }): Promise<FastifyInstance>;
268
270
 
269
271
  export { type CanonryConfig, createServer, loadConfig };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-A4YBV4Y4.js";
3
+ } from "./chunk-ZHWWSU7H.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-IHRW6WWV.js";
7
- import "./chunk-HQRUFTLN.js";
7
+ import "./chunk-EF67Y6CI.js";
8
8
  import "./chunk-BLSHHRRU.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-HQRUFTLN.js";
3
+ } from "./chunk-EF67Y6CI.js";
4
4
  import "./chunk-BLSHHRRU.js";
5
5
  export {
6
6
  IntelligenceService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonry/canonry",
3
- "version": "4.127.0",
3
+ "version": "4.129.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -66,26 +66,26 @@
66
66
  "tsup": "^8.5.1",
67
67
  "tsx": "^4.19.0",
68
68
  "@ainyc/canonry-api-client": "0.0.0",
69
- "@ainyc/canonry-api-routes": "0.0.0",
70
69
  "@ainyc/canonry-config": "0.0.0",
71
- "@ainyc/canonry-contracts": "0.0.0",
70
+ "@ainyc/canonry-api-routes": "0.0.0",
72
71
  "@ainyc/canonry-db": "0.0.0",
72
+ "@ainyc/canonry-contracts": "0.0.0",
73
73
  "@ainyc/canonry-integration-bing": "0.0.0",
74
74
  "@ainyc/canonry-integration-openai-ads": "0.0.0",
75
75
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
76
76
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
77
77
  "@ainyc/canonry-integration-google": "0.0.0",
78
78
  "@ainyc/canonry-integration-google-places": "0.0.0",
79
- "@ainyc/canonry-intelligence": "0.0.0",
80
79
  "@ainyc/canonry-integration-traffic": "0.0.0",
80
+ "@ainyc/canonry-integration-google-business-profile": "0.0.0",
81
81
  "@ainyc/canonry-integration-wordpress": "0.0.0",
82
+ "@ainyc/canonry-intelligence": "0.0.0",
82
83
  "@ainyc/canonry-provider-cdp": "0.0.0",
83
84
  "@ainyc/canonry-provider-claude": "0.0.0",
84
- "@ainyc/canonry-integration-google-business-profile": "0.0.0",
85
+ "@ainyc/canonry-provider-gemini": "0.0.0",
85
86
  "@ainyc/canonry-provider-local": "0.0.0",
86
87
  "@ainyc/canonry-provider-openai": "0.0.0",
87
- "@ainyc/canonry-provider-perplexity": "0.0.0",
88
- "@ainyc/canonry-provider-gemini": "0.0.0"
88
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
89
89
  },
90
90
  "scripts": {
91
91
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",