@ainyc/canonry 1.48.0 → 1.48.2

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
@@ -1,6 +1,6 @@
1
1
  # Canonry <img src="apps/web/public/favicon-32.png" alt="Canonry canary icon" width="24" />
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@ainyc/canonry)](https://www.npmjs.com/package/@ainyc/canonry) [![License: FSL-1.1-ALv2](https://img.shields.io/badge/License-FSL--1.1--ALv2-blue.svg)](https://fsl.software/) [![Node.js >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
3
+ [![npm version](https://img.shields.io/npm/v/@ainyc/canonry)](https://www.npmjs.com/package/@ainyc/canonry) [![License: FSL-1.1-ALv2](https://img.shields.io/badge/License-FSL--1.1--ALv2-blue.svg)](https://fsl.software/) [![Node.js >= 22.14](https://img.shields.io/badge/node-%3E%3D22.14-brightgreen)](https://nodejs.org)
4
4
 
5
5
  Canonry is an agent-first AEO platform powered by [OpenClaw](https://openclaw.ai). It tracks how ChatGPT, Gemini, Claude, and Perplexity cite your site, detects regressions, diagnoses causes, coordinates fixes, and reports results.
6
6
 
@@ -177,7 +177,7 @@ Create a Web Service with runtime Docker, attach a disk at `/data`. Health check
177
177
 
178
178
  ## Requirements
179
179
 
180
- - Node.js >= 20
180
+ - Node.js >= 22.14.0
181
181
  - At least one provider API key (configurable after startup)
182
182
 
183
183
  If `npm install` fails with `node-gyp` errors, install build tools for `better-sqlite3`: `xcode-select --install` (macOS), `apt-get install python3 make g++` (Debian), or see the [troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md).
@@ -40,3 +40,4 @@ You coordinate across three tools to deliver comprehensive AEO monitoring:
40
40
  - [memory-patterns.md](references/memory-patterns.md) — What to persist per client
41
41
  - [regression-playbook.md](references/regression-playbook.md) — Detection through response
42
42
  - [reporting.md](references/reporting.md) — Report generation templates
43
+ - [wordpress-elementor-mcp.md](references/wordpress-elementor-mcp.md) — Elementor MCP tools for page management
@@ -0,0 +1,218 @@
1
+ # Elementor + WordPress MCP Development Guide
2
+
3
+ ## Overview
4
+
5
+ This guide covers programmatic management of WordPress + Elementor sites using the Elementor MCP plugin. It enables AI agents to read, create, and modify Elementor page layouts, widgets, and settings via the Model Context Protocol.
6
+
7
+ ## Prerequisites
8
+
9
+ ### WordPress Side
10
+ - WordPress >= 6.8
11
+ - Elementor >= 3.20 (container-based layouts)
12
+ - Elementor Pro (for custom CSS, forms, nav menus, etc.)
13
+ - **WordPress MCP Adapter** plugin ([GitHub](https://github.com/WordPress/mcp-adapter))
14
+ - **Elementor MCP** plugin ([GitHub](https://github.com/msrbuilds/elementor-mcp))
15
+ - Application Password created for API auth (Users > Profile > Application Passwords)
16
+ - Permalinks set to "Post name" (required for `/wp-json/` REST API routing)
17
+
18
+ ### Client Side
19
+ - `.mcp.json` in project root with HTTP MCP server config
20
+ - Base64-encoded credentials: `echo -n "username:app-password" | base64`
21
+
22
+ ## MCP Configuration
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "elementor-staging": {
28
+ "type": "http",
29
+ "url": "https://your-staging-site.com/wp-json/mcp/elementor-mcp-server",
30
+ "headers": {
31
+ "Authorization": "Basic BASE64_CREDENTIALS"
32
+ }
33
+ },
34
+ "elementor-production": {
35
+ "type": "http",
36
+ "url": "https://your-production-site.com/wp-json/mcp/elementor-mcp-server",
37
+ "headers": {
38
+ "Authorization": "Basic BASE64_CREDENTIALS"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### Troubleshooting Connection Issues
46
+ - If `/wp-json/` returns 404: go to Settings > Permalinks, set to "Post name", click Save
47
+ - If auth fails (401): verify application password is correct and user has admin role
48
+ - If MCP endpoint 404 but `/wp-json/` works: re-activate both MCP plugins
49
+ - After staging re-clone: re-install plugins, re-create app password, re-save permalinks
50
+
51
+ ## Elementor Architecture
52
+
53
+ ### Element Hierarchy
54
+ ```
55
+ Page
56
+ └── Container (root section)
57
+ └── Container (hero/section)
58
+ └── Container (row/column)
59
+ └── Widget (heading, text-editor, button, etc.)
60
+ ```
61
+
62
+ ### Key Concepts
63
+ - **Containers** are flex/grid layouts that hold other containers or widgets
64
+ - **Widgets** are the actual content elements (headings, text, images, buttons, forms)
65
+ - **Element IDs** are 7-char hex strings, unique within a page
66
+ - **Settings** control all visual properties (typography, colors, spacing, responsive)
67
+ - **Responsive suffixes**: `_tablet` and `_mobile` variants (e.g., `typography_font_size_tablet`)
68
+
69
+ ### Common Widget Types
70
+ - `heading` — H1-H6 titles
71
+ - `text-editor` — Rich text content
72
+ - `button` — CTA buttons with links
73
+ - `image` / `image-box` — Images with optional text
74
+ - `form` — Contact/lead forms (Pro)
75
+ - `google_maps` — Embedded maps
76
+ - `html` — Custom HTML (used for JSON-LD schema injection)
77
+ - `reviews` — Testimonials
78
+ - `nested-tabs` / `nested-accordion` — Tabbed/accordion content
79
+
80
+ ## Core MCP Workflow
81
+
82
+ ### 1. Discovery
83
+
84
+ ```
85
+ list-pages → Get all Elementor pages with IDs
86
+ get-page-structure → See element tree (containers + widgets)
87
+ get-element-settings → Inspect any element's full settings
88
+ find-element → Search by text content, widget type, or setting
89
+ list-widgets → See all available widget types
90
+ get-widget-schema → Get available settings for a widget type
91
+ ```
92
+
93
+ ### 2. Content Updates
94
+
95
+ ```
96
+ update-widget → Change text, links, colors, typography (partial merge)
97
+ update-container → Change layout, spacing, alignment, background
98
+ add-heading → Add a heading widget
99
+ add-text-editor → Add a text block
100
+ add-button → Add a CTA button
101
+ add-html → Add custom HTML (JSON-LD schema, tracking scripts)
102
+ add-container → Add a layout container
103
+ ```
104
+
105
+ ### 3. Structure Changes
106
+
107
+ ```
108
+ move-element → Reorder or re-parent elements
109
+ remove-element → Delete an element
110
+ duplicate-element → Clone an element
111
+ reorder-elements → Change sibling order
112
+ ```
113
+
114
+ ### 4. Styling
115
+
116
+ ```
117
+ add-custom-css → Page-level or element-level CSS (use media queries for responsive)
118
+ update-global-colors → Site-wide color palette
119
+ update-global-typography → Site-wide font presets
120
+ ```
121
+
122
+ ### 5. Page Management
123
+
124
+ ```
125
+ create-page → New WordPress page with Elementor
126
+ build-page → Create complete page from declarative JSON
127
+ export-page → Get full page data
128
+ delete-page-content → Clear page content
129
+ ```
130
+
131
+ ## Best Practices
132
+
133
+ ### Staging-First Workflow
134
+ 1. Make all changes on staging via MCP
135
+ 2. Verify visually across viewports (use Chrome browser tools)
136
+ 3. Get human approval
137
+ 4. Replay changes on production via MCP
138
+ 5. Re-clone staging from production for next iteration
139
+
140
+ ### Responsive Design
141
+ - Always check 3 breakpoints: desktop (1440+), tablet (768-1024), mobile (375-390)
142
+ - Use `_tablet` and `_mobile` setting suffixes for responsive overrides
143
+ - For large desktop fixes (1800px+), use `add-custom-css` with media queries
144
+ - Elementor's breakpoints: desktop (default), tablet (1024px), mobile (767px)
145
+
146
+ ### Settings Merge Behavior
147
+ - `update-widget` and `update-container` do **partial merges** — only specified settings change
148
+ - Nested objects (like `margin`, `padding`, `typography_font_size`) must include all subkeys
149
+ - Example margin: `{"unit": "px", "top": "0", "right": "0", "bottom": "0", "left": "20", "isLinked": false}`
150
+
151
+ ### CSS Custom Overrides
152
+ - Use `add-custom-css` with `replace: true` to avoid CSS accumulation
153
+ - Use `selector` keyword for element-level CSS: `selector .heading { color: red; }`
154
+ - Wrap responsive fixes in `@media` queries to avoid affecting other breakpoints
155
+ - Always verify CSS doesn't break other viewports after applying
156
+
157
+ ### JSON-LD Schema Injection
158
+ - Use `add-html` widget with `<script type="application/ld+json">` content
159
+ - Place in root container (append position -1) — script tags produce no visible output
160
+ - Use distinct `@id` values that don't conflict with Yoast's auto-generated schema
161
+ - Yoast generates: WebPage, BreadcrumbList, WebSite, Organization
162
+ - Custom schema should use: Service, RoofingContractor, DefinedTerm, LocalBusiness, etc.
163
+
164
+ ### Common Gotchas
165
+ - **Elementor CSS cache**: changes may not appear until CSS is regenerated. Use Elementor > Tools > Regenerate Files & Data, or the cache flush endpoint
166
+ - **Shared templates**: headers/footers are Elementor Library templates (different post type). Find their post ID via browser inspection (`data-elementor-id` attribute)
167
+ - **Widget IDs shared across pages**: pages cloned from templates share element IDs. Changes to one page don't affect others
168
+ - **Yoast SEO meta**: not writable via REST API. Must be set manually in wp-admin
169
+ - **WP Staging re-clone**: wipes plugins, app passwords, and permalink settings. Must reconfigure after each clone
170
+ - **Background images**: `background-position` and `background-size: cover` interact differently across viewport sizes. Use browser tools to measure actual rendered positions
171
+
172
+ ## Quick Reference: Common Operations
173
+
174
+ ### Change heading text
175
+ ```
176
+ update-widget(post_id, element_id, {"title": "New Heading"})
177
+ ```
178
+
179
+ ### Change text block content
180
+ ```
181
+ update-widget(post_id, element_id, {"editor": "<p>New content</p>"})
182
+ ```
183
+
184
+ ### Change font size (responsive)
185
+ ```
186
+ update-widget(post_id, element_id, {
187
+ "typography_font_size": {"unit": "px", "size": 80, "sizes": []},
188
+ "typography_font_size_tablet": {"unit": "px", "size": 65, "sizes": []},
189
+ "typography_font_size_mobile": {"unit": "px", "size": 40, "sizes": []}
190
+ })
191
+ ```
192
+
193
+ ### Change container margin
194
+ ```
195
+ update-container(post_id, element_id, {
196
+ "margin": {"unit": "px", "top": "0", "right": "0", "bottom": "0", "left": "250", "isLinked": false}
197
+ })
198
+ ```
199
+
200
+ ### Add JSON-LD schema to a page
201
+ ```
202
+ add-html(post_id, parent_id, '<script type="application/ld+json">{"@context":"https://schema.org",...}</script>')
203
+ ```
204
+
205
+ ### Add responsive CSS fix
206
+ ```
207
+ add-custom-css(post_id, '@media (min-width: 1800px) { .elementor-element-XXXXX { margin-top: -200px !important; } }', replace=true)
208
+ ```
209
+
210
+ ### Find text on a page
211
+ ```
212
+ find-element(post_id, search_text="lorem ipsum")
213
+ ```
214
+
215
+ ### Update Google Maps widget
216
+ ```
217
+ update-widget(post_id, element_id, {"address": "Southeast Michigan, USA", "zoom": {"unit": "px", "size": 8, "sizes": []}})
218
+ ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canonry
3
- description: "AEO (Answer Engine Optimization) monitoring and analysis using canonry CLI and aeo-audit tool. Use when: (1) running citation sweeps across AI providers (Gemini, ChatGPT, Claude, Perplexity); (2) auditing technical SEO with structured data validation; (3) implementing schema markup, sitemaps, llms.txt; (4) diagnosing indexing issues via Google Search Console and Bing Webmaster Tools; (5) optimizing content for AI readability and entity consistency. NOT for: general web development, content writing, PPC campaigns, or social media management."
3
+ description: "Agent-first AEO monitoring and operating platform."
4
4
  metadata:
5
5
  {
6
6
  "openclaw":
@@ -32,38 +32,25 @@ metadata:
32
32
 
33
33
  # Canonry
34
34
 
35
- Monitor and optimize site visibility across AI answer engines (Gemini, ChatGPT, Claude, Perplexity) and traditional search engines using the `canonry` CLI for AEO monitoring and `aeo-audit` for technical SEO analysis.
35
+ Open-source AEO (Answer Engine Optimization) monitoring platform. Track how AI answer engines cite your domain across Gemini, ChatGPT, Claude, and Perplexity.
36
36
 
37
- ## When to Use
37
+ **Website:** [ainyc.ai](https://ainyc.ai) | **Docs:** [github.com/AINYC/canonry](https://github.com/AINYC/canonry)
38
38
 
39
- **USE this skill when:**
39
+ ## When to Use
40
40
 
41
- - Tracking which keyphrases earn citations (or lose them) across AI providers
42
- - Running technical SEO audits with 14‑factor scoring
43
- - Implementing structured data (JSON‑LD: LocalBusiness, FAQPage, Service)
44
- - Diagnosing indexing gaps in Google Search Console / Bing Webmaster Tools
45
- - Optimizing `llms.txt`, `llms‑full.txt`, sitemaps, robots.txt for AI crawlers
46
- - Patching missing H1 tags, meta descriptions, image alt text
41
+ - Tracking keyphrase citations across AI providers
42
+ - Running technical SEO audits (14‑factor scoring)
43
+ - Implementing structured data (JSON‑LD)
44
+ - Diagnosing indexing gaps via Google Search Console / Bing Webmaster Tools
45
+ - Optimizing `llms.txt`, sitemaps, robots.txt for AI crawlers
47
46
  - Submitting URLs to Google Indexing API and Bing IndexNow
48
- - Analyzing competitor citation patterns in AI answers
49
-
50
- ## When NOT to Use
51
-
52
- ❌ **DON'T use this skill when:**
53
-
54
- - General WordPress development (use `wordpress` skill if available)
55
- - Content writing or copy creation (human‑led task)
56
- - Paid search/SEM campaigns (different specialty)
57
- - Social media management or outreach
58
- - Local business listing management (e.g., GBP, Yelp)
59
- - Backlink building or outreach campaigns
47
+ - Analyzing competitor citation patterns
60
48
 
61
49
  ## Core Philosophy
62
50
 
63
- - **AI models are black boxes** Measure citation outcomes, not assume causality
64
- - **Position, then wait** — Site changes take weeks/months to reflect in AI indexes; canonry tells us *when* it happens, not *if*
65
- - **Signalover‑noise** — Trim keyphrase lists to high‑intent queries; avoid granular targeting until base visibility exists
66
- - **CLI‑native, UI‑optional** — Prefer API‑driven changes over manual CMS clicks; faster, repeatable, auditable
51
+ - **Measure outcomes** — AI models are black boxes; track citations, don't assume causality
52
+ - **Signal over noise** — Focus on high‑intent queries; avoid granular targeting until base visibility exists
53
+ - **CLInative** — API‑driven changes over manual CMS clicks; faster, repeatable, auditable
67
54
 
68
55
  ## Toolchain
69
56
 
@@ -221,54 +208,7 @@ cat audit.json | jq -r '.factors[] | select(.score < 70) | "- \(.name): \(.score
221
208
  - **Client data stays private** — canonry repo is public; no real domains in issues
222
209
  - **Respect API rate limits** — batch operations, avoid tight loops
223
210
 
224
- ## Output Templates
225
-
226
- ### Audit Summary
227
- ```
228
- ## AEO/SEO Audit — https://client.com
229
-
230
- **Overall:** 66/100 (D)
231
-
232
- **Top strengths (A/A+):**
233
- - AI‑Readable Content (100) — llms.txt, llms‑full.txt present
234
- - FAQ Content (100) — FAQPage schema detected
235
- - AI Crawler Access (100) — robots.txt allows all bots
236
-
237
- **Critical gaps (F):**
238
- - Definition Blocks (0) — no "What is…" sections
239
- - E‑E‑A‑T Signals (45) — missing Person schema, author tags
240
- - Citations & Authority (44) — no external references to industry sources
241
-
242
- **Immediate actions:**
243
- 1. Add H1 tag to homepage (Technical SEO: 60/100)
244
- 2. Create "What is polyurea?" section on /services/ (Definition Blocks: 0/100)
245
- 3. Submit all 5 URLs to Bing IndexNow (indexing: 2/5)
246
- ```
247
-
248
- ### Citation Report
249
- ```
250
- ## canonry sweep — client-project
251
-
252
- **Run:** 2026‑04‑03T13:44Z (ID: 4a45ebfc...)
253
-
254
- **Keyphrase visibility (12 tracked):**
255
- ✅ polyurea roof coating — 3/3 providers
256
- ✅ commercial roof coating — 2/3 providers
257
- ❌ polyurea roof coating Michigan — 0/3 (geo gap)
258
- ❌ commercial roofing contractor Michigan — 0/3 (geo gap)
259
-
260
- **Changes since last sweep (2026‑03‑27):**
261
- - Lost `flat roof coating Michigan` on Gemini (−1)
262
- - Gained `industrial roof coating` on Claude (+1)
263
- - No change on ChatGPT (stable)
264
-
265
- **Next steps:**
266
- - Build Michigan location page (/michigan/)
267
- - Add county‑level references to llms.txt
268
- - Re‑sweep in 7 days
269
- ```
270
-
271
211
  ---
272
212
 
273
213
  **Tools:** canonry v1.37+, @ainyc/aeo‑audit v1.3+
274
- **Reference:** [AINYC AEO Methodology](https://ainyc.ai/aeo-methodology)
214
+ **Website:** [ainyc.ai](https://ainyc.ai) | **Reference:** [AINYC AEO Methodology](https://ainyc.ai/aeo-methodology)
@@ -55,3 +55,7 @@ canonry wordpress staging push mysite
55
55
  - If SEO meta is not writable through REST, canonry returns an actionable error instead of guessing
56
56
  - Duplicate slug matches are returned as explicit ambiguity errors with candidate page IDs/titles
57
57
  - Authentication is verified on connect by calling `/wp/v2/users/me` — if that fails, canonry returns an actionable error message
58
+
59
+ ## Related: Elementor MCP
60
+
61
+ For programmatic management of Elementor page layouts, widgets, and styling via MCP tools, see the aero skill reference: [`skills/aero/references/wordpress-elementor-mcp.md`](../../aero/references/wordpress-elementor-mcp.md).
@@ -115,7 +115,8 @@ var querySnapshots = sqliteTable("query_snapshots", {
115
115
  index("idx_snapshots_keyword").on(table.keywordId),
116
116
  index("idx_snapshots_citation_state").on(table.citationState),
117
117
  index("idx_snapshots_provider_model").on(table.provider, table.model),
118
- index("idx_snapshots_location").on(table.location)
118
+ index("idx_snapshots_location").on(table.location),
119
+ index("idx_snapshots_created_at").on(table.createdAt)
119
120
  ]);
120
121
  var auditLog = sqliteTable("audit_log", {
121
122
  id: text("id").primaryKey(),
@@ -848,7 +849,11 @@ var MIGRATIONS = [
848
849
  `CREATE INDEX IF NOT EXISTS idx_bing_coverage_snap_run ON bing_coverage_snapshots(sync_run_id)`,
849
850
  // v34: Rename unique index for bing_coverage_snapshots to follow convention
850
851
  `DROP INDEX IF EXISTS idx_bing_coverage_snap_project_date`,
851
- `CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_coverage_snap_project_date_unique ON bing_coverage_snapshots(project_id, date)`
852
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_coverage_snap_project_date_unique ON bing_coverage_snapshots(project_id, date)`,
853
+ // v35: Add missing index for query_snapshots createdAt for time-series filtering
854
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON query_snapshots(created_at)`
855
+ // v36: Transaction handling and SQL injection review: verified all strings use SQLite ? binding via Drizzle.
856
+ // No changes required for parameterization.
852
857
  ];
853
858
  function isDuplicateColumnError(err) {
854
859
  if (!(err instanceof Error)) return false;
@@ -23,7 +23,7 @@ import {
23
23
  runs,
24
24
  schedules,
25
25
  usageCounters
26
- } from "./chunk-HO22LHTY.js";
26
+ } from "./chunk-JTKHPNGL.js";
27
27
 
28
28
  // src/config.ts
29
29
  import fs from "fs";
@@ -6071,9 +6071,11 @@ var GSC_MAX_PAGES = 40;
6071
6071
 
6072
6072
  // ../integration-google/src/types.ts
6073
6073
  var GoogleAuthError = class extends Error {
6074
- constructor(message) {
6074
+ statusCode;
6075
+ constructor(message, statusCode) {
6075
6076
  super(message);
6076
6077
  this.name = "GoogleAuthError";
6078
+ this.statusCode = statusCode;
6077
6079
  }
6078
6080
  };
6079
6081
  var GoogleApiError = class extends Error {
@@ -6164,13 +6166,19 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6164
6166
  }),
6165
6167
  signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6166
6168
  });
6169
+ if (res.status === 429) {
6170
+ throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
6171
+ }
6167
6172
  if (!res.ok) {
6168
6173
  const body = await res.text();
6169
6174
  let detail = "";
6170
6175
  try {
6171
6176
  const parsed = JSON.parse(body);
6172
6177
  if (parsed.error) detail = parsed.error;
6173
- if (parsed.error_description) detail += detail ? `: ${parsed.error_description}` : parsed.error_description;
6178
+ if (parsed.error_description) {
6179
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
6180
+ detail += detail ? `: ${sanitized}` : sanitized;
6181
+ }
6174
6182
  } catch {
6175
6183
  detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
6176
6184
  }
@@ -6178,6 +6186,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6178
6186
  }
6179
6187
  return await res.json();
6180
6188
  }
6189
+ function escapeRegExp2(str) {
6190
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6191
+ }
6181
6192
  async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6182
6193
  validateClientId(clientId);
6183
6194
  validateClientSecret(clientSecret);
@@ -6193,13 +6204,19 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6193
6204
  }),
6194
6205
  signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6195
6206
  });
6207
+ if (res.status === 429) {
6208
+ throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
6209
+ }
6196
6210
  if (!res.ok) {
6197
6211
  const body = await res.text();
6198
6212
  let detail = "";
6199
6213
  try {
6200
6214
  const parsed = JSON.parse(body);
6201
6215
  if (parsed.error) detail = parsed.error;
6202
- if (parsed.error_description) detail += detail ? `: ${parsed.error_description}` : parsed.error_description;
6216
+ if (parsed.error_description) {
6217
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
6218
+ detail += detail ? `: ${sanitized}` : sanitized;
6219
+ }
6203
6220
  } catch {
6204
6221
  detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
6205
6222
  }
@@ -6271,6 +6288,20 @@ function gscClientLog(level, action, ctx) {
6271
6288
  ...ctx
6272
6289
  };
6273
6290
  if (entry.accessToken) entry.accessToken = "***";
6291
+ if (typeof entry.siteUrl === "string") {
6292
+ try {
6293
+ const url = new URL(entry.siteUrl);
6294
+ if (url.search) {
6295
+ url.searchParams.forEach((_, key) => {
6296
+ if (key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("auth")) {
6297
+ url.searchParams.set(key, "***");
6298
+ }
6299
+ });
6300
+ entry.siteUrl = url.toString();
6301
+ }
6302
+ } catch {
6303
+ }
6304
+ }
6274
6305
  const stream = level === "error" ? process.stderr : process.stdout;
6275
6306
  stream.write(JSON.stringify(entry) + "\n");
6276
6307
  }
@@ -6497,11 +6528,15 @@ async function getAccessToken(clientEmail, privateKey) {
6497
6528
  const body = await res.text().catch(() => "");
6498
6529
  ga4Log("error", "token.failed", { httpStatus: res.status });
6499
6530
  const detail = body.length <= 200 ? body : `${body.slice(0, 200)}... [truncated]`;
6500
- throw new GA4ApiError(`Failed to get access token: ${detail}`, res.status);
6531
+ const sanitizedDetail = detail.replace(new RegExp(escapeRegExp3(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp3(privateKey.slice(0, 32)), "g"), "***");
6532
+ throw new GA4ApiError(`Failed to get access token: ${sanitizedDetail}`, res.status);
6501
6533
  }
6502
6534
  const data = await res.json();
6503
6535
  return data.access_token;
6504
6536
  }
6537
+ function escapeRegExp3(str) {
6538
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6539
+ }
6505
6540
  async function runReport(accessToken, propertyId, request) {
6506
6541
  const url = `${GA4_DATA_API_BASE}/properties/${propertyId}:runReport`;
6507
6542
  const res = await fetch(url, {
@@ -6619,6 +6654,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6619
6654
  offset
6620
6655
  };
6621
6656
  const response = await runReport(accessToken, propertyId, request);
6657
+ if (!response) break;
6622
6658
  const pageRows = (response.rows ?? []).map((row) => ({
6623
6659
  date: row.dimensionValues[0].value,
6624
6660
  landingPage: row.dimensionValues[1].value,
@@ -6651,6 +6687,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6651
6687
  offset: organicOffset
6652
6688
  };
6653
6689
  const organicResponse = await runReport(accessToken, propertyId, organicRequest);
6690
+ if (!organicResponse) break;
6654
6691
  for (const row of organicResponse.rows ?? []) {
6655
6692
  const key = `${row.dimensionValues[0].value}::${row.dimensionValues[1].value}`;
6656
6693
  organicMap.set(key, parseInt(row.metricValues[0].value, 10) || 0);
@@ -6778,6 +6815,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
6778
6815
  offset
6779
6816
  };
6780
6817
  const response = await runReport(accessToken, propertyId, request);
6818
+ if (!response) break;
6781
6819
  const pageRows = (response.rows ?? []).map((row) => ({
6782
6820
  date: row.dimensionValues[0].value,
6783
6821
  source: row.dimensionValues[1].value,
@@ -6854,6 +6892,7 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
6854
6892
  offset
6855
6893
  };
6856
6894
  const response = await runReport(accessToken, propertyId, request);
6895
+ if (!response) break;
6857
6896
  const pageRows = (response.rows ?? []).map((row) => ({
6858
6897
  date: row.dimensionValues[0].value,
6859
6898
  source: row.dimensionValues[1].value,
@@ -7663,6 +7702,9 @@ function bingClientLog(level, action, ctx) {
7663
7702
  const stream = level === "error" ? process.stderr : process.stdout;
7664
7703
  stream.write(JSON.stringify(entry) + "\n");
7665
7704
  }
7705
+ function escapeRegExp4(str) {
7706
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7707
+ }
7666
7708
  async function bingFetch(apiKey, endpoint, opts) {
7667
7709
  const method = opts?.method ?? "GET";
7668
7710
  const separator = endpoint.includes("?") ? "&" : "?";
@@ -7687,7 +7729,8 @@ async function bingFetch(apiKey, endpoint, opts) {
7687
7729
  if (!res.ok) {
7688
7730
  const body = await res.text();
7689
7731
  bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status });
7690
- const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
7732
+ let detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
7733
+ detail = detail.replace(new RegExp(escapeRegExp4(apiKey), "g"), "***");
7691
7734
  throw new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status);
7692
7735
  }
7693
7736
  const text = await res.text();
@@ -8183,7 +8226,7 @@ async function bingRoutes(app, opts) {
8183
8226
  query: s.Query,
8184
8227
  impressions: s.Impressions,
8185
8228
  clicks: s.Clicks,
8186
- ctr: Number.isFinite(s.Ctr) ? s.Ctr : 0,
8229
+ ctr: s.Impressions > 0 ? s.Clicks / s.Impressions : 0,
8187
8230
  averagePosition: s.AverageClickPosition ?? s.AverageImpressionPosition ?? 0
8188
8231
  }));
8189
8232
  });
@@ -9221,6 +9264,8 @@ function buildAuthErrorMessage(res, responseText) {
9221
9264
  return "WordPress credentials are invalid or lack permission for this action";
9222
9265
  }
9223
9266
  async function fetchJson(connection, siteUrl, path9, init) {
9267
+ if (siteUrl.startsWith("http:")) {
9268
+ }
9224
9269
  const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
9225
9270
  ...init,
9226
9271
  headers: {
@@ -9235,6 +9280,9 @@ async function fetchJson(connection, siteUrl, path9, init) {
9235
9280
  const errorMessage = buildAuthErrorMessage(res, text);
9236
9281
  throw new WordpressApiError("AUTH_INVALID", errorMessage, res.status);
9237
9282
  }
9283
+ if (res.status === 429) {
9284
+ throw new WordpressApiError("UPSTREAM_ERROR", "WordPress API rate limit exceeded", 429);
9285
+ }
9238
9286
  if (res.status === 404) {
9239
9287
  throw new WordpressApiError("NOT_FOUND", "WordPress endpoint not found", 404);
9240
9288
  }
@@ -9733,12 +9781,12 @@ var CANONRY_SCHEMA_START = "<!-- canonry:schema:start -->";
9733
9781
  var CANONRY_SCHEMA_END = "<!-- canonry:schema:end -->";
9734
9782
  function stripCanonrySchema(content) {
9735
9783
  const regex = new RegExp(
9736
- `${escapeRegExp2(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp2(CANONRY_SCHEMA_END)}`,
9784
+ `${escapeRegExp5(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp5(CANONRY_SCHEMA_END)}`,
9737
9785
  "g"
9738
9786
  );
9739
9787
  return content.replace(regex, "").replace(/\n{3,}/g, "\n\n").trim();
9740
9788
  }
9741
- function escapeRegExp2(str) {
9789
+ function escapeRegExp5(str) {
9742
9790
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9743
9791
  }
9744
9792
  function injectCanonrySchema(content, schemas) {
@@ -9845,7 +9893,7 @@ async function getSchemaStatus(connection, env) {
9845
9893
  const thirdPartySchemas = [];
9846
9894
  if (hasCanonryMarker) {
9847
9895
  const markerRegex = new RegExp(
9848
- `${escapeRegExp2(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp2(CANONRY_SCHEMA_END)}`
9896
+ `${escapeRegExp5(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp5(CANONRY_SCHEMA_END)}`
9849
9897
  );
9850
9898
  const match = markerRegex.exec(rawContent);
9851
9899
  if (match?.[1]) {
@@ -10855,6 +10903,12 @@ function isRetryableError(err) {
10855
10903
  return status >= 500 || status === 429;
10856
10904
  }
10857
10905
  }
10906
+ if (err instanceof Error) {
10907
+ const msg = err.message.toLowerCase();
10908
+ if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
10909
+ return true;
10910
+ }
10911
+ }
10858
10912
  return true;
10859
10913
  }
10860
10914
  async function withRetry(fn, options = {}) {
@@ -11074,7 +11128,10 @@ function extractCitedDomainsFromSources(groundingSources) {
11074
11128
  }
11075
11129
  if (source.title) {
11076
11130
  const titleDomain = extractDomainFromTitle(source.title);
11077
- if (titleDomain) domains.add(titleDomain);
11131
+ if (titleDomain) {
11132
+ domains.add(titleDomain);
11133
+ continue;
11134
+ }
11078
11135
  }
11079
11136
  }
11080
11137
  return [...domains];
@@ -11089,7 +11146,10 @@ function extractDomainFromTitle(title) {
11089
11146
  function extractDomainFromUri(uri) {
11090
11147
  try {
11091
11148
  const url = new URL(uri);
11092
- const hostname = url.hostname.replace(/^www\./, "");
11149
+ const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
11150
+ if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
11151
+ return null;
11152
+ }
11093
11153
  if (hostname === "vertexaisearch.cloud.google.com") {
11094
11154
  const redirectPath = url.pathname.replace(/^\/grounding-api-redirect\//, "");
11095
11155
  if (redirectPath && redirectPath !== url.pathname) {
@@ -11239,6 +11299,12 @@ function isRetryableError2(err) {
11239
11299
  return status >= 500 || status === 429;
11240
11300
  }
11241
11301
  }
11302
+ if (err instanceof Error) {
11303
+ const msg = err.message.toLowerCase();
11304
+ if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
11305
+ return true;
11306
+ }
11307
+ }
11242
11308
  return true;
11243
11309
  }
11244
11310
  async function withRetry2(fn, options = {}) {
@@ -11465,7 +11531,11 @@ function extractCitedDomainsFromSources2(groundingSources) {
11465
11531
  function extractDomainFromUri2(uri) {
11466
11532
  try {
11467
11533
  const url = new URL(uri);
11468
- return url.hostname.replace(/^www\./, "").toLowerCase();
11534
+ const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
11535
+ if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
11536
+ return null;
11537
+ }
11538
+ return hostname;
11469
11539
  } catch {
11470
11540
  return null;
11471
11541
  }
@@ -11583,6 +11653,12 @@ function isRetryableError3(err) {
11583
11653
  return status >= 500 || status === 429;
11584
11654
  }
11585
11655
  }
11656
+ if (err instanceof Error) {
11657
+ const msg = err.message.toLowerCase();
11658
+ if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
11659
+ return true;
11660
+ }
11661
+ }
11586
11662
  return true;
11587
11663
  }
11588
11664
  async function withRetry3(fn, options = {}) {
@@ -11834,7 +11910,11 @@ function extractCitedDomainsFromSources3(groundingSources) {
11834
11910
  function extractDomainFromUri3(uri) {
11835
11911
  try {
11836
11912
  const url = new URL(uri);
11837
- return url.hostname.replace(/^www\./, "").toLowerCase();
11913
+ const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
11914
+ if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
11915
+ return null;
11916
+ }
11917
+ return hostname;
11838
11918
  } catch {
11839
11919
  return null;
11840
11920
  }
@@ -11950,6 +12030,12 @@ function isRetryableError4(err) {
11950
12030
  return status >= 500 || status === 429;
11951
12031
  }
11952
12032
  }
12033
+ if (err instanceof Error) {
12034
+ const msg = err.message.toLowerCase();
12035
+ if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
12036
+ return true;
12037
+ }
12038
+ }
11953
12039
  return true;
11954
12040
  }
11955
12041
  async function withRetry4(fn, options = {}) {
@@ -12054,11 +12140,15 @@ async function executeTrackedQuery4(input) {
12054
12140
  function normalizeResult4(raw) {
12055
12141
  const answerText = extractAnswerText2(raw.rawResponse);
12056
12142
  const citedDomains = extractDomainMentions(answerText);
12143
+ const groundingSources = citedDomains.map((domain) => ({
12144
+ uri: `http://${domain}`,
12145
+ title: domain
12146
+ }));
12057
12147
  return {
12058
12148
  provider: "local",
12059
12149
  answerText,
12060
12150
  citedDomains,
12061
- groundingSources: raw.groundingSources,
12151
+ groundingSources,
12062
12152
  searchQueries: raw.searchQueries
12063
12153
  };
12064
12154
  }
@@ -12764,6 +12854,12 @@ function isRetryableError5(err) {
12764
12854
  return status >= 500 || status === 429;
12765
12855
  }
12766
12856
  }
12857
+ if (err instanceof Error) {
12858
+ const msg = err.message.toLowerCase();
12859
+ if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
12860
+ return true;
12861
+ }
12862
+ }
12767
12863
  return true;
12768
12864
  }
12769
12865
  async function withRetry5(fn, options = {}) {
@@ -12981,7 +13077,11 @@ function extractCitedDomains2(groundingSources) {
12981
13077
  function extractDomainFromUri4(uri) {
12982
13078
  try {
12983
13079
  const url = new URL(uri);
12984
- return url.hostname.replace(/^www\./, "").toLowerCase();
13080
+ const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
13081
+ if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
13082
+ return null;
13083
+ }
13084
+ return hostname;
12985
13085
  } catch {
12986
13086
  return null;
12987
13087
  }
@@ -13607,7 +13707,7 @@ var JobRunner = class {
13607
13707
  normalized.answerText,
13608
13708
  allDomains,
13609
13709
  normalized.citedDomains,
13610
- overlap
13710
+ competitorDomains
13611
13711
  );
13612
13712
  let screenshotRelPath = null;
13613
13713
  if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
@@ -15481,7 +15581,8 @@ function isProcessAlive(pid) {
15481
15581
  try {
15482
15582
  process.kill(pid, 0);
15483
15583
  return true;
15484
- } catch {
15584
+ } catch (err) {
15585
+ if (err.code === "EPERM") return true;
15485
15586
  return false;
15486
15587
  }
15487
15588
  }
@@ -15509,6 +15610,9 @@ import os5 from "os";
15509
15610
  import path7 from "path";
15510
15611
  import { fileURLToPath } from "url";
15511
15612
  var CACHE_TTL_MS = 5 * 60 * 1e3;
15613
+ var OPENCLAW_VERSION = "2026.4.14";
15614
+ var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
15615
+ var MIN_NODE_VERSION = "22.14.0";
15512
15616
  var cachedResult = null;
15513
15617
  var cachedAt = 0;
15514
15618
  function getAeroStateDir(profile = "aero") {
@@ -15572,8 +15676,15 @@ function findInPath() {
15572
15676
  }
15573
15677
  }
15574
15678
  async function installOpenClaw(opts) {
15679
+ const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
15680
+ if (unsupportedNodeError) {
15681
+ return {
15682
+ success: false,
15683
+ error: unsupportedNodeError
15684
+ };
15685
+ }
15575
15686
  try {
15576
- execSync("npm install -g openclaw", {
15687
+ execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
15577
15688
  timeout: 12e4,
15578
15689
  stdio: opts?.silent ? "pipe" : "inherit"
15579
15690
  });
@@ -15588,11 +15699,57 @@ async function installOpenClaw(opts) {
15588
15699
  if (!detection.found) {
15589
15700
  return {
15590
15701
  success: false,
15591
- error: "npm install succeeded but openclaw binary was not found in PATH"
15702
+ error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
15592
15703
  };
15593
15704
  }
15705
+ if (detection.version) {
15706
+ const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
15707
+ const detectedVersion = parseVersionTuple(detection.version);
15708
+ if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
15709
+ return {
15710
+ success: false,
15711
+ error: `Installed OpenClaw binary reports version ${detection.version}, but Canonry pinned ${OPENCLAW_VERSION}. A different openclaw binary may be shadowing the npm-installed package in PATH.`
15712
+ };
15713
+ }
15714
+ }
15594
15715
  return { success: true, detection };
15595
15716
  }
15717
+ function getUnsupportedNodeError(currentNodeVersionOverride) {
15718
+ const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
15719
+ const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
15720
+ const currentTuple = parseVersionTuple(currentNodeVersion);
15721
+ if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
15722
+ return null;
15723
+ }
15724
+ return `Canonry requires Node.js >=${MIN_NODE_VERSION} and installs pinned OpenClaw ${OPENCLAW_VERSION}, but the current runtime is ${currentNodeVersion}. Upgrade Node.js before running "canonry agent setup".`;
15725
+ }
15726
+ function normalizeVersion(version) {
15727
+ const tuple = parseVersionTuple(version);
15728
+ if (!tuple) {
15729
+ return version.trim().replace(/^v/i, "");
15730
+ }
15731
+ return tuple.join(".");
15732
+ }
15733
+ function parseVersionTuple(version) {
15734
+ const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
15735
+ if (!match) {
15736
+ return null;
15737
+ }
15738
+ return [
15739
+ Number(match[1]),
15740
+ Number(match[2] ?? 0),
15741
+ Number(match[3] ?? 0)
15742
+ ];
15743
+ }
15744
+ function compareVersionTuples(left, right) {
15745
+ for (let index = 0; index < left.length; index++) {
15746
+ const delta = left[index] - right[index];
15747
+ if (delta !== 0) {
15748
+ return delta;
15749
+ }
15750
+ }
15751
+ return 0;
15752
+ }
15596
15753
  function seedWorkspace(stateDir) {
15597
15754
  const workspaceDir = path7.join(stateDir, "workspace");
15598
15755
  fs6.mkdirSync(workspaceDir, { recursive: true });
@@ -16483,7 +16640,7 @@ async function createServer(opts) {
16483
16640
  await app.register(fastifyStatic.default, {
16484
16641
  root: assetsDir,
16485
16642
  prefix: basePath ?? "/",
16486
- wildcard: false,
16643
+ wildcard: true,
16487
16644
  // Don't serve index.html automatically — we handle it with config injection
16488
16645
  serve: true,
16489
16646
  index: false
package/dist/cli.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  trackEvent,
48
48
  usageError,
49
49
  writeAgentEnv
50
- } from "./chunk-25QLMK4F.js";
50
+ } from "./chunk-YPTVJRJY.js";
51
51
  import {
52
52
  apiKeys,
53
53
  competitors,
@@ -57,7 +57,7 @@ import {
57
57
  projects,
58
58
  querySnapshots,
59
59
  runs
60
- } from "./chunk-HO22LHTY.js";
60
+ } from "./chunk-JTKHPNGL.js";
61
61
 
62
62
  // src/cli.ts
63
63
  import { pathToFileURL } from "url";
@@ -304,7 +304,7 @@ async function backfillAnswerVisibilityCommand(opts) {
304
304
  console.log(` Errors: ${providerErrors}`);
305
305
  }
306
306
  async function backfillInsightsCommand(project, opts) {
307
- const { IntelligenceService } = await import("./intelligence-service-ZISLIU4S.js");
307
+ const { IntelligenceService } = await import("./intelligence-service-Q4WX46MJ.js");
308
308
  const config = loadConfig();
309
309
  const db = createClient(config.database);
310
310
  migrate(db);
@@ -7920,7 +7920,7 @@ async function agentDetach(opts) {
7920
7920
  }
7921
7921
  async function autoInstallOrFail(format) {
7922
7922
  if (format !== "json") {
7923
- console.log("OpenClaw not found, installing via npm...");
7923
+ console.log("OpenClaw not found, installing pinned openclaw@2026.4.14 via npm...");
7924
7924
  }
7925
7925
  const install = await installOpenClaw({ silent: format === "json" });
7926
7926
  if (!install.success) {
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-25QLMK4F.js";
5
- import "./chunk-HO22LHTY.js";
4
+ } from "./chunk-YPTVJRJY.js";
5
+ import "./chunk-JTKHPNGL.js";
6
6
  export {
7
7
  createServer,
8
8
  loadConfig
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-HO22LHTY.js";
3
+ } from "./chunk-JTKHPNGL.js";
4
4
  export {
5
5
  IntelligenceService
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.48.0",
3
+ "version": "1.48.2",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "engines": {
34
- "node": ">=20"
34
+ "node": ">=22.14.0"
35
35
  },
36
36
  "dependencies": {
37
37
  "@ainyc/aeo-audit": "1.3.2",
@@ -54,20 +54,20 @@
54
54
  "@types/node-cron": "^3.0.11",
55
55
  "tsup": "^8.5.1",
56
56
  "tsx": "^4.19.0",
57
- "@ainyc/canonry-config": "0.0.0",
58
- "@ainyc/canonry-contracts": "0.0.0",
59
57
  "@ainyc/canonry-api-routes": "0.0.0",
58
+ "@ainyc/canonry-contracts": "0.0.0",
59
+ "@ainyc/canonry-config": "0.0.0",
60
60
  "@ainyc/canonry-db": "0.0.0",
61
+ "@ainyc/canonry-intelligence": "0.0.0",
61
62
  "@ainyc/canonry-integration-bing": "0.0.0",
63
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
62
64
  "@ainyc/canonry-integration-google": "0.0.0",
63
- "@ainyc/canonry-intelligence": "0.0.0",
64
65
  "@ainyc/canonry-provider-cdp": "0.0.0",
65
- "@ainyc/canonry-integration-wordpress": "0.0.0",
66
66
  "@ainyc/canonry-provider-claude": "0.0.0",
67
67
  "@ainyc/canonry-provider-gemini": "0.0.0",
68
+ "@ainyc/canonry-provider-local": "0.0.0",
68
69
  "@ainyc/canonry-provider-openai": "0.0.0",
69
- "@ainyc/canonry-provider-perplexity": "0.0.0",
70
- "@ainyc/canonry-provider-local": "0.0.0"
70
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",