@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 +2 -2
- package/assets/agent-workspace/skills/aero/SKILL.md +1 -0
- package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +218 -0
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +14 -74
- package/assets/agent-workspace/skills/canonry-setup/references/wordpress-integration.md +4 -0
- package/dist/{chunk-HO22LHTY.js → chunk-JTKHPNGL.js} +7 -2
- package/dist/{chunk-25QLMK4F.js → chunk-YPTVJRJY.js} +178 -21
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-ZISLIU4S.js → intelligence-service-Q4WX46MJ.js} +1 -1
- package/package.json +8 -8
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
|
-
[](https://www.npmjs.com/package/@ainyc/canonry) [](https://fsl.software/) [](https://www.npmjs.com/package/@ainyc/canonry) [](https://fsl.software/) [](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 >=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
**Website:** [ainyc.ai](https://ainyc.ai) | **Docs:** [github.com/AINYC/canonry](https://github.com/AINYC/canonry)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
## When to Use
|
|
40
40
|
|
|
41
|
-
- Tracking
|
|
42
|
-
- Running technical SEO audits
|
|
43
|
-
- Implementing structured data (JSON‑LD
|
|
44
|
-
- Diagnosing indexing gaps
|
|
45
|
-
- Optimizing `llms.txt`,
|
|
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
|
|
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
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
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
|
+
- **CLI‑native** — 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-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
`${
|
|
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
|
|
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
|
-
`${
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "1.48.
|
|
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": ">=
|
|
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",
|