@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 +75 -10
- package/assets/agent-workspace/skills/aero/SKILL.md +15 -7
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -1
- package/assets/agent-workspace/skills/aero/references/orchestration.md +5 -3
- package/assets/agent-workspace/skills/aero/soul.md +1 -1
- package/assets/agent-workspace/skills/canonry/SKILL.md +50 -13
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -1
- package/assets/agent-workspace/skills/canonry/references/indexing.md +4 -2
- package/dist/{chunk-HQRUFTLN.js → chunk-EF67Y6CI.js} +165 -19
- package/dist/{chunk-A4YBV4Y4.js → chunk-ZHWWSU7H.js} +23 -3
- package/dist/cli.js +210 -34
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-2WKS3EWJ.js → intelligence-service-NTIKN7YG.js} +1 -1
- package/package.json +7 -7
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 [
|
|
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
|
-
##
|
|
58
|
+
## Manage ChatGPT ads with human control
|
|
58
59
|
|
|
59
|
-
|
|
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.
|
|
66
|
-
3.
|
|
67
|
-
4. Read
|
|
68
|
-
5.
|
|
69
|
-
6.
|
|
70
|
-
7.
|
|
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
|
-
| **
|
|
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
|
-
|
|
6
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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?**
|
|
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
|
|
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.
|
|
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: "
|
|
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** —
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
24148
|
-
|
|
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(
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
|
145
|
-
if (args.length <
|
|
146
|
-
return
|
|
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
|
|
3509
|
+
const fs16 = await import("fs");
|
|
3510
3510
|
try {
|
|
3511
|
-
const content =
|
|
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,
|
|
4770
|
-
if (!
|
|
4771
|
-
const segments =
|
|
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
|
|
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:
|
|
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
|
|
4864
|
+
const fs16 = await import("fs");
|
|
4865
4865
|
try {
|
|
4866
|
-
applicationPassword =
|
|
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
|
|
4922
|
+
const fs16 = await import("fs");
|
|
4923
4923
|
let keyJson;
|
|
4924
4924
|
try {
|
|
4925
|
-
keyJson =
|
|
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
|
|
4985
|
+
const fs16 = await import("fs");
|
|
4986
4986
|
try {
|
|
4987
|
-
token =
|
|
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
|
|
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
|
|
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
|
|
13098
|
-
const
|
|
13099
|
-
const filePath =
|
|
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
|
|
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
|
|
13200
|
-
const
|
|
13375
|
+
const fs16 = await import("fs/promises");
|
|
13376
|
+
const path12 = await import("path");
|
|
13201
13377
|
const yaml = await loadYamlModule();
|
|
13202
|
-
const filePath =
|
|
13378
|
+
const filePath = path12.resolve(opts.profile);
|
|
13203
13379
|
let raw;
|
|
13204
13380
|
try {
|
|
13205
|
-
raw = await
|
|
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
|
|
13311
|
-
const
|
|
13486
|
+
const fs16 = await import("fs/promises");
|
|
13487
|
+
const path12 = await import("path");
|
|
13312
13488
|
const yaml = await loadYamlModule();
|
|
13313
|
-
const filePath =
|
|
13489
|
+
const filePath = path12.resolve(opts.profile);
|
|
13314
13490
|
let raw;
|
|
13315
13491
|
try {
|
|
13316
|
-
raw = await
|
|
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
|
|
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-
|
|
3
|
+
} from "./chunk-ZHWWSU7H.js";
|
|
4
4
|
import {
|
|
5
5
|
loadConfig
|
|
6
6
|
} from "./chunk-IHRW6WWV.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-EF67Y6CI.js";
|
|
8
8
|
import "./chunk-BLSHHRRU.js";
|
|
9
9
|
export {
|
|
10
10
|
createServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonry/canonry",
|
|
3
|
-
"version": "4.
|
|
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-
|
|
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-
|
|
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",
|