@ainyc/canonry 1.48.0 → 1.48.4
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 +8 -8
- package/assets/agent-workspace/skills/aero/SKILL.md +1 -0
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +1 -1
- package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +218 -0
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +15 -75
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +5 -5
- package/assets/agent-workspace/skills/canonry-setup/references/wordpress-integration.md +4 -0
- package/dist/{chunk-25QLMK4F.js → chunk-IPOVH342.js} +230 -85
- package/dist/{chunk-HO22LHTY.js → chunk-ZZ57GRV6.js} +92 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-ZISLIU4S.js → intelligence-service-MZ7SXEGE.js} +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
Canonry is an agent-first AEO platform
|
|
5
|
+
Canonry is an agent-first AEO platform — CLI- and API-native, with a bundled AI agent. It tracks how ChatGPT, Gemini, Claude, and Perplexity cite your site, detects regressions, diagnoses causes, coordinates fixes, and reports results.
|
|
6
6
|
|
|
7
7
|
AEO (Answer Engine Optimization) is about making sure your content shows up accurately in AI-generated answers. As search shifts from links to synthesized responses, you need something that can monitor, analyze, and act across these engines continuously.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ npm install -g @ainyc/canonry
|
|
|
15
15
|
canonry agent setup
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
One command. It installs
|
|
18
|
+
One command. It installs the agent runtime, configures the agent's LLM, sets up monitoring providers, and seeds the workspace. Interactive prompts guide you through everything, or pass flags for fully automated setup:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
canonry agent setup --gemini-key <key> --agent-key <key> --format json
|
|
@@ -42,7 +42,7 @@ canonry serve
|
|
|
42
42
|
|
|
43
43
|
## What the Agent Does
|
|
44
44
|
|
|
45
|
-
The Canonry agent ("Aero") is an
|
|
45
|
+
The Canonry agent ("Aero") is an autonomous operator:
|
|
46
46
|
|
|
47
47
|
- **Monitors** visibility sweeps across providers on schedule, tracking citation changes over time
|
|
48
48
|
- **Analyzes** regressions, emerging opportunities, and correlates visibility shifts with site changes
|
|
@@ -53,7 +53,7 @@ Every action the agent takes goes through the same CLI and API available to ever
|
|
|
53
53
|
|
|
54
54
|
## Features
|
|
55
55
|
|
|
56
|
-
- **Agent-operated.** The
|
|
56
|
+
- **Agent-operated.** The bundled agent monitors, analyzes, and acts autonomously. Humans supervise via the dashboard.
|
|
57
57
|
- **Multi-provider.** Query Gemini, OpenAI, Claude, Perplexity, and local LLMs from a single platform.
|
|
58
58
|
- **Config-as-code.** Kubernetes-style YAML files. Version control your monitoring, let agents apply changes declaratively.
|
|
59
59
|
- **Self-hosted.** Runs locally with SQLite. No cloud account required.
|
|
@@ -148,9 +148,9 @@ Integration setup guides: [Google Search Console](docs/google-search-console-set
|
|
|
148
148
|
|
|
149
149
|
## Skills
|
|
150
150
|
|
|
151
|
-
The agent learns how to operate canonry through bundled
|
|
151
|
+
The agent learns how to operate canonry through bundled skills that cover CLI commands, provider setup, analysis workflows, and troubleshooting. Skills are seeded into the agent workspace during `canonry agent setup`.
|
|
152
152
|
|
|
153
|
-
**Claude Code** also picks up the skill automatically from `.claude/skills/canonry-setup/` when you open this repo.
|
|
153
|
+
**Claude Code** also picks up the skill automatically from `.claude/skills/canonry-setup/` when you open this repo.
|
|
154
154
|
|
|
155
155
|
## Deployment
|
|
156
156
|
|
|
@@ -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,9 +1,9 @@
|
|
|
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
|
+
"agent":
|
|
7
7
|
{
|
|
8
8
|
"emoji": "📡",
|
|
9
9
|
"requires": { "bins": ["canonry"] },
|
|
@@ -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)
|
|
@@ -296,10 +296,10 @@ canonry export <project> --include-results > project.yaml
|
|
|
296
296
|
canonry sitemap inspect <project>
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
## Agent
|
|
299
|
+
## Agent
|
|
300
300
|
|
|
301
301
|
`canonry agent setup` is the single entry point for configuring the agent. It handles everything:
|
|
302
|
-
canonry initialization,
|
|
302
|
+
canonry initialization, agent runtime installation, profile setup, LLM credential configuration,
|
|
303
303
|
and workspace seeding. If canonry is not yet configured, it runs the interactive init flow first
|
|
304
304
|
(prompting for monitoring provider keys and agent LLM credentials).
|
|
305
305
|
|
|
@@ -313,7 +313,7 @@ canonry agent setup --agent-provider openrouter --agent-key <key> --agent-model
|
|
|
313
313
|
GEMINI_API_KEY=<key> canonry agent setup --agent-key <key> --format json
|
|
314
314
|
|
|
315
315
|
# Lifecycle
|
|
316
|
-
canonry agent start # start
|
|
316
|
+
canonry agent start # start agent gateway as background process
|
|
317
317
|
canonry agent stop # stop the gateway process
|
|
318
318
|
canonry agent status # check if gateway is running
|
|
319
319
|
canonry agent status --format json # JSON output
|
|
@@ -336,9 +336,9 @@ canonry agent detach <project> # remove agent webhook from pro
|
|
|
336
336
|
canonry agent detach <project> --format json # JSON output
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
**Setup flow:** init canonry (if needed) → install
|
|
339
|
+
**Setup flow:** init canonry (if needed) → install agent runtime (if needed) → configure profile → configure gateway → set agent LLM credentials → seed workspace with skills.
|
|
340
340
|
|
|
341
|
-
**Agent LLM credentials** are stored in
|
|
341
|
+
**Agent LLM credentials** are stored in the agent env file (e.g. `ANTHROPIC_API_KEY=...`) and loaded into the gateway process at start time. The model is set via the agent CLI.
|
|
342
342
|
|
|
343
343
|
**Re-running is safe:** setup is idempotent — it skips steps that are already configured.
|
|
344
344
|
|
|
@@ -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).
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
bingUrlInspections,
|
|
7
7
|
competitors,
|
|
8
8
|
createLogger,
|
|
9
|
+
dropLegacyCredentialColumns,
|
|
10
|
+
extractLegacyCredentials,
|
|
9
11
|
gaAiReferrals,
|
|
10
12
|
gaSocialReferrals,
|
|
11
13
|
gaTrafficSnapshots,
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
runs,
|
|
24
26
|
schedules,
|
|
25
27
|
usageCounters
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ZZ57GRV6.js";
|
|
27
29
|
|
|
28
30
|
// src/config.ts
|
|
29
31
|
import fs from "fs";
|
|
@@ -338,7 +340,7 @@ import crypto23 from "crypto";
|
|
|
338
340
|
import fs7 from "fs";
|
|
339
341
|
import path8 from "path";
|
|
340
342
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
341
|
-
import { eq as eq24
|
|
343
|
+
import { eq as eq24 } from "drizzle-orm";
|
|
342
344
|
import Fastify from "fastify";
|
|
343
345
|
|
|
344
346
|
// ../contracts/src/config-schema.ts
|
|
@@ -6071,9 +6073,11 @@ var GSC_MAX_PAGES = 40;
|
|
|
6071
6073
|
|
|
6072
6074
|
// ../integration-google/src/types.ts
|
|
6073
6075
|
var GoogleAuthError = class extends Error {
|
|
6074
|
-
|
|
6076
|
+
statusCode;
|
|
6077
|
+
constructor(message, statusCode) {
|
|
6075
6078
|
super(message);
|
|
6076
6079
|
this.name = "GoogleAuthError";
|
|
6080
|
+
this.statusCode = statusCode;
|
|
6077
6081
|
}
|
|
6078
6082
|
};
|
|
6079
6083
|
var GoogleApiError = class extends Error {
|
|
@@ -6164,13 +6168,19 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6164
6168
|
}),
|
|
6165
6169
|
signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
|
|
6166
6170
|
});
|
|
6171
|
+
if (res.status === 429) {
|
|
6172
|
+
throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
|
|
6173
|
+
}
|
|
6167
6174
|
if (!res.ok) {
|
|
6168
6175
|
const body = await res.text();
|
|
6169
6176
|
let detail = "";
|
|
6170
6177
|
try {
|
|
6171
6178
|
const parsed = JSON.parse(body);
|
|
6172
6179
|
if (parsed.error) detail = parsed.error;
|
|
6173
|
-
if (parsed.error_description)
|
|
6180
|
+
if (parsed.error_description) {
|
|
6181
|
+
const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
|
|
6182
|
+
detail += detail ? `: ${sanitized}` : sanitized;
|
|
6183
|
+
}
|
|
6174
6184
|
} catch {
|
|
6175
6185
|
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6176
6186
|
}
|
|
@@ -6178,6 +6188,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6178
6188
|
}
|
|
6179
6189
|
return await res.json();
|
|
6180
6190
|
}
|
|
6191
|
+
function escapeRegExp2(str) {
|
|
6192
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6193
|
+
}
|
|
6181
6194
|
async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
6182
6195
|
validateClientId(clientId);
|
|
6183
6196
|
validateClientSecret(clientSecret);
|
|
@@ -6193,13 +6206,19 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
6193
6206
|
}),
|
|
6194
6207
|
signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
|
|
6195
6208
|
});
|
|
6209
|
+
if (res.status === 429) {
|
|
6210
|
+
throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
|
|
6211
|
+
}
|
|
6196
6212
|
if (!res.ok) {
|
|
6197
6213
|
const body = await res.text();
|
|
6198
6214
|
let detail = "";
|
|
6199
6215
|
try {
|
|
6200
6216
|
const parsed = JSON.parse(body);
|
|
6201
6217
|
if (parsed.error) detail = parsed.error;
|
|
6202
|
-
if (parsed.error_description)
|
|
6218
|
+
if (parsed.error_description) {
|
|
6219
|
+
const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
|
|
6220
|
+
detail += detail ? `: ${sanitized}` : sanitized;
|
|
6221
|
+
}
|
|
6203
6222
|
} catch {
|
|
6204
6223
|
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6205
6224
|
}
|
|
@@ -6271,6 +6290,20 @@ function gscClientLog(level, action, ctx) {
|
|
|
6271
6290
|
...ctx
|
|
6272
6291
|
};
|
|
6273
6292
|
if (entry.accessToken) entry.accessToken = "***";
|
|
6293
|
+
if (typeof entry.siteUrl === "string") {
|
|
6294
|
+
try {
|
|
6295
|
+
const url = new URL(entry.siteUrl);
|
|
6296
|
+
if (url.search) {
|
|
6297
|
+
url.searchParams.forEach((_, key) => {
|
|
6298
|
+
if (key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("auth")) {
|
|
6299
|
+
url.searchParams.set(key, "***");
|
|
6300
|
+
}
|
|
6301
|
+
});
|
|
6302
|
+
entry.siteUrl = url.toString();
|
|
6303
|
+
}
|
|
6304
|
+
} catch {
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6274
6307
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
6275
6308
|
stream.write(JSON.stringify(entry) + "\n");
|
|
6276
6309
|
}
|
|
@@ -6497,11 +6530,15 @@ async function getAccessToken(clientEmail, privateKey) {
|
|
|
6497
6530
|
const body = await res.text().catch(() => "");
|
|
6498
6531
|
ga4Log("error", "token.failed", { httpStatus: res.status });
|
|
6499
6532
|
const detail = body.length <= 200 ? body : `${body.slice(0, 200)}... [truncated]`;
|
|
6500
|
-
|
|
6533
|
+
const sanitizedDetail = detail.replace(new RegExp(escapeRegExp3(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp3(privateKey.slice(0, 32)), "g"), "***");
|
|
6534
|
+
throw new GA4ApiError(`Failed to get access token: ${sanitizedDetail}`, res.status);
|
|
6501
6535
|
}
|
|
6502
6536
|
const data = await res.json();
|
|
6503
6537
|
return data.access_token;
|
|
6504
6538
|
}
|
|
6539
|
+
function escapeRegExp3(str) {
|
|
6540
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6541
|
+
}
|
|
6505
6542
|
async function runReport(accessToken, propertyId, request) {
|
|
6506
6543
|
const url = `${GA4_DATA_API_BASE}/properties/${propertyId}:runReport`;
|
|
6507
6544
|
const res = await fetch(url, {
|
|
@@ -6619,6 +6656,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6619
6656
|
offset
|
|
6620
6657
|
};
|
|
6621
6658
|
const response = await runReport(accessToken, propertyId, request);
|
|
6659
|
+
if (!response) break;
|
|
6622
6660
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6623
6661
|
date: row.dimensionValues[0].value,
|
|
6624
6662
|
landingPage: row.dimensionValues[1].value,
|
|
@@ -6651,6 +6689,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6651
6689
|
offset: organicOffset
|
|
6652
6690
|
};
|
|
6653
6691
|
const organicResponse = await runReport(accessToken, propertyId, organicRequest);
|
|
6692
|
+
if (!organicResponse) break;
|
|
6654
6693
|
for (const row of organicResponse.rows ?? []) {
|
|
6655
6694
|
const key = `${row.dimensionValues[0].value}::${row.dimensionValues[1].value}`;
|
|
6656
6695
|
organicMap.set(key, parseInt(row.metricValues[0].value, 10) || 0);
|
|
@@ -6778,6 +6817,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
6778
6817
|
offset
|
|
6779
6818
|
};
|
|
6780
6819
|
const response = await runReport(accessToken, propertyId, request);
|
|
6820
|
+
if (!response) break;
|
|
6781
6821
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6782
6822
|
date: row.dimensionValues[0].value,
|
|
6783
6823
|
source: row.dimensionValues[1].value,
|
|
@@ -6854,6 +6894,7 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
|
|
|
6854
6894
|
offset
|
|
6855
6895
|
};
|
|
6856
6896
|
const response = await runReport(accessToken, propertyId, request);
|
|
6897
|
+
if (!response) break;
|
|
6857
6898
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6858
6899
|
date: row.dimensionValues[0].value,
|
|
6859
6900
|
source: row.dimensionValues[1].value,
|
|
@@ -7663,6 +7704,9 @@ function bingClientLog(level, action, ctx) {
|
|
|
7663
7704
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
7664
7705
|
stream.write(JSON.stringify(entry) + "\n");
|
|
7665
7706
|
}
|
|
7707
|
+
function escapeRegExp4(str) {
|
|
7708
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7709
|
+
}
|
|
7666
7710
|
async function bingFetch(apiKey, endpoint, opts) {
|
|
7667
7711
|
const method = opts?.method ?? "GET";
|
|
7668
7712
|
const separator = endpoint.includes("?") ? "&" : "?";
|
|
@@ -7687,7 +7731,8 @@ async function bingFetch(apiKey, endpoint, opts) {
|
|
|
7687
7731
|
if (!res.ok) {
|
|
7688
7732
|
const body = await res.text();
|
|
7689
7733
|
bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status });
|
|
7690
|
-
|
|
7734
|
+
let detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
7735
|
+
detail = detail.replace(new RegExp(escapeRegExp4(apiKey), "g"), "***");
|
|
7691
7736
|
throw new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status);
|
|
7692
7737
|
}
|
|
7693
7738
|
const text = await res.text();
|
|
@@ -8183,7 +8228,7 @@ async function bingRoutes(app, opts) {
|
|
|
8183
8228
|
query: s.Query,
|
|
8184
8229
|
impressions: s.Impressions,
|
|
8185
8230
|
clicks: s.Clicks,
|
|
8186
|
-
ctr:
|
|
8231
|
+
ctr: s.Impressions > 0 ? s.Clicks / s.Impressions : 0,
|
|
8187
8232
|
averagePosition: s.AverageClickPosition ?? s.AverageImpressionPosition ?? 0
|
|
8188
8233
|
}));
|
|
8189
8234
|
});
|
|
@@ -9221,6 +9266,8 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
9221
9266
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
9222
9267
|
}
|
|
9223
9268
|
async function fetchJson(connection, siteUrl, path9, init) {
|
|
9269
|
+
if (siteUrl.startsWith("http:")) {
|
|
9270
|
+
}
|
|
9224
9271
|
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
|
|
9225
9272
|
...init,
|
|
9226
9273
|
headers: {
|
|
@@ -9235,6 +9282,9 @@ async function fetchJson(connection, siteUrl, path9, init) {
|
|
|
9235
9282
|
const errorMessage = buildAuthErrorMessage(res, text);
|
|
9236
9283
|
throw new WordpressApiError("AUTH_INVALID", errorMessage, res.status);
|
|
9237
9284
|
}
|
|
9285
|
+
if (res.status === 429) {
|
|
9286
|
+
throw new WordpressApiError("UPSTREAM_ERROR", "WordPress API rate limit exceeded", 429);
|
|
9287
|
+
}
|
|
9238
9288
|
if (res.status === 404) {
|
|
9239
9289
|
throw new WordpressApiError("NOT_FOUND", "WordPress endpoint not found", 404);
|
|
9240
9290
|
}
|
|
@@ -9733,12 +9783,12 @@ var CANONRY_SCHEMA_START = "<!-- canonry:schema:start -->";
|
|
|
9733
9783
|
var CANONRY_SCHEMA_END = "<!-- canonry:schema:end -->";
|
|
9734
9784
|
function stripCanonrySchema(content) {
|
|
9735
9785
|
const regex = new RegExp(
|
|
9736
|
-
`${
|
|
9786
|
+
`${escapeRegExp5(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp5(CANONRY_SCHEMA_END)}`,
|
|
9737
9787
|
"g"
|
|
9738
9788
|
);
|
|
9739
9789
|
return content.replace(regex, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
9740
9790
|
}
|
|
9741
|
-
function
|
|
9791
|
+
function escapeRegExp5(str) {
|
|
9742
9792
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9743
9793
|
}
|
|
9744
9794
|
function injectCanonrySchema(content, schemas) {
|
|
@@ -9845,7 +9895,7 @@ async function getSchemaStatus(connection, env) {
|
|
|
9845
9895
|
const thirdPartySchemas = [];
|
|
9846
9896
|
if (hasCanonryMarker) {
|
|
9847
9897
|
const markerRegex = new RegExp(
|
|
9848
|
-
`${
|
|
9898
|
+
`${escapeRegExp5(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp5(CANONRY_SCHEMA_END)}`
|
|
9849
9899
|
);
|
|
9850
9900
|
const match = markerRegex.exec(rawContent);
|
|
9851
9901
|
if (match?.[1]) {
|
|
@@ -10855,6 +10905,12 @@ function isRetryableError(err) {
|
|
|
10855
10905
|
return status >= 500 || status === 429;
|
|
10856
10906
|
}
|
|
10857
10907
|
}
|
|
10908
|
+
if (err instanceof Error) {
|
|
10909
|
+
const msg = err.message.toLowerCase();
|
|
10910
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
10911
|
+
return true;
|
|
10912
|
+
}
|
|
10913
|
+
}
|
|
10858
10914
|
return true;
|
|
10859
10915
|
}
|
|
10860
10916
|
async function withRetry(fn, options = {}) {
|
|
@@ -11074,7 +11130,10 @@ function extractCitedDomainsFromSources(groundingSources) {
|
|
|
11074
11130
|
}
|
|
11075
11131
|
if (source.title) {
|
|
11076
11132
|
const titleDomain = extractDomainFromTitle(source.title);
|
|
11077
|
-
if (titleDomain)
|
|
11133
|
+
if (titleDomain) {
|
|
11134
|
+
domains.add(titleDomain);
|
|
11135
|
+
continue;
|
|
11136
|
+
}
|
|
11078
11137
|
}
|
|
11079
11138
|
}
|
|
11080
11139
|
return [...domains];
|
|
@@ -11089,7 +11148,10 @@ function extractDomainFromTitle(title) {
|
|
|
11089
11148
|
function extractDomainFromUri(uri) {
|
|
11090
11149
|
try {
|
|
11091
11150
|
const url = new URL(uri);
|
|
11092
|
-
const hostname = url.hostname.replace(/^www\./, "");
|
|
11151
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11152
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11153
|
+
return null;
|
|
11154
|
+
}
|
|
11093
11155
|
if (hostname === "vertexaisearch.cloud.google.com") {
|
|
11094
11156
|
const redirectPath = url.pathname.replace(/^\/grounding-api-redirect\//, "");
|
|
11095
11157
|
if (redirectPath && redirectPath !== url.pathname) {
|
|
@@ -11239,6 +11301,12 @@ function isRetryableError2(err) {
|
|
|
11239
11301
|
return status >= 500 || status === 429;
|
|
11240
11302
|
}
|
|
11241
11303
|
}
|
|
11304
|
+
if (err instanceof Error) {
|
|
11305
|
+
const msg = err.message.toLowerCase();
|
|
11306
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
11307
|
+
return true;
|
|
11308
|
+
}
|
|
11309
|
+
}
|
|
11242
11310
|
return true;
|
|
11243
11311
|
}
|
|
11244
11312
|
async function withRetry2(fn, options = {}) {
|
|
@@ -11465,7 +11533,11 @@ function extractCitedDomainsFromSources2(groundingSources) {
|
|
|
11465
11533
|
function extractDomainFromUri2(uri) {
|
|
11466
11534
|
try {
|
|
11467
11535
|
const url = new URL(uri);
|
|
11468
|
-
|
|
11536
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11537
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11538
|
+
return null;
|
|
11539
|
+
}
|
|
11540
|
+
return hostname;
|
|
11469
11541
|
} catch {
|
|
11470
11542
|
return null;
|
|
11471
11543
|
}
|
|
@@ -11583,6 +11655,12 @@ function isRetryableError3(err) {
|
|
|
11583
11655
|
return status >= 500 || status === 429;
|
|
11584
11656
|
}
|
|
11585
11657
|
}
|
|
11658
|
+
if (err instanceof Error) {
|
|
11659
|
+
const msg = err.message.toLowerCase();
|
|
11660
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
11661
|
+
return true;
|
|
11662
|
+
}
|
|
11663
|
+
}
|
|
11586
11664
|
return true;
|
|
11587
11665
|
}
|
|
11588
11666
|
async function withRetry3(fn, options = {}) {
|
|
@@ -11834,7 +11912,11 @@ function extractCitedDomainsFromSources3(groundingSources) {
|
|
|
11834
11912
|
function extractDomainFromUri3(uri) {
|
|
11835
11913
|
try {
|
|
11836
11914
|
const url = new URL(uri);
|
|
11837
|
-
|
|
11915
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11916
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11917
|
+
return null;
|
|
11918
|
+
}
|
|
11919
|
+
return hostname;
|
|
11838
11920
|
} catch {
|
|
11839
11921
|
return null;
|
|
11840
11922
|
}
|
|
@@ -11950,6 +12032,12 @@ function isRetryableError4(err) {
|
|
|
11950
12032
|
return status >= 500 || status === 429;
|
|
11951
12033
|
}
|
|
11952
12034
|
}
|
|
12035
|
+
if (err instanceof Error) {
|
|
12036
|
+
const msg = err.message.toLowerCase();
|
|
12037
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
12038
|
+
return true;
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
11953
12041
|
return true;
|
|
11954
12042
|
}
|
|
11955
12043
|
async function withRetry4(fn, options = {}) {
|
|
@@ -12054,11 +12142,15 @@ async function executeTrackedQuery4(input) {
|
|
|
12054
12142
|
function normalizeResult4(raw) {
|
|
12055
12143
|
const answerText = extractAnswerText2(raw.rawResponse);
|
|
12056
12144
|
const citedDomains = extractDomainMentions(answerText);
|
|
12145
|
+
const groundingSources = citedDomains.map((domain) => ({
|
|
12146
|
+
uri: `http://${domain}`,
|
|
12147
|
+
title: domain
|
|
12148
|
+
}));
|
|
12057
12149
|
return {
|
|
12058
12150
|
provider: "local",
|
|
12059
12151
|
answerText,
|
|
12060
12152
|
citedDomains,
|
|
12061
|
-
groundingSources
|
|
12153
|
+
groundingSources,
|
|
12062
12154
|
searchQueries: raw.searchQueries
|
|
12063
12155
|
};
|
|
12064
12156
|
}
|
|
@@ -12764,6 +12856,12 @@ function isRetryableError5(err) {
|
|
|
12764
12856
|
return status >= 500 || status === 429;
|
|
12765
12857
|
}
|
|
12766
12858
|
}
|
|
12859
|
+
if (err instanceof Error) {
|
|
12860
|
+
const msg = err.message.toLowerCase();
|
|
12861
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
12862
|
+
return true;
|
|
12863
|
+
}
|
|
12864
|
+
}
|
|
12767
12865
|
return true;
|
|
12768
12866
|
}
|
|
12769
12867
|
async function withRetry5(fn, options = {}) {
|
|
@@ -12981,7 +13079,11 @@ function extractCitedDomains2(groundingSources) {
|
|
|
12981
13079
|
function extractDomainFromUri4(uri) {
|
|
12982
13080
|
try {
|
|
12983
13081
|
const url = new URL(uri);
|
|
12984
|
-
|
|
13082
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
13083
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
13084
|
+
return null;
|
|
13085
|
+
}
|
|
13086
|
+
return hostname;
|
|
12985
13087
|
} catch {
|
|
12986
13088
|
return null;
|
|
12987
13089
|
}
|
|
@@ -13607,7 +13709,7 @@ var JobRunner = class {
|
|
|
13607
13709
|
normalized.answerText,
|
|
13608
13710
|
allDomains,
|
|
13609
13711
|
normalized.citedDomains,
|
|
13610
|
-
|
|
13712
|
+
competitorDomains
|
|
13611
13713
|
);
|
|
13612
13714
|
let screenshotRelPath = null;
|
|
13613
13715
|
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
@@ -15481,7 +15583,8 @@ function isProcessAlive(pid) {
|
|
|
15481
15583
|
try {
|
|
15482
15584
|
process.kill(pid, 0);
|
|
15483
15585
|
return true;
|
|
15484
|
-
} catch {
|
|
15586
|
+
} catch (err) {
|
|
15587
|
+
if (err.code === "EPERM") return true;
|
|
15485
15588
|
return false;
|
|
15486
15589
|
}
|
|
15487
15590
|
}
|
|
@@ -15509,6 +15612,9 @@ import os5 from "os";
|
|
|
15509
15612
|
import path7 from "path";
|
|
15510
15613
|
import { fileURLToPath } from "url";
|
|
15511
15614
|
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
15615
|
+
var OPENCLAW_VERSION = "2026.4.14";
|
|
15616
|
+
var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
|
|
15617
|
+
var MIN_NODE_VERSION = "22.14.0";
|
|
15512
15618
|
var cachedResult = null;
|
|
15513
15619
|
var cachedAt = 0;
|
|
15514
15620
|
function getAeroStateDir(profile = "aero") {
|
|
@@ -15572,8 +15678,15 @@ function findInPath() {
|
|
|
15572
15678
|
}
|
|
15573
15679
|
}
|
|
15574
15680
|
async function installOpenClaw(opts) {
|
|
15681
|
+
const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
|
|
15682
|
+
if (unsupportedNodeError) {
|
|
15683
|
+
return {
|
|
15684
|
+
success: false,
|
|
15685
|
+
error: unsupportedNodeError
|
|
15686
|
+
};
|
|
15687
|
+
}
|
|
15575
15688
|
try {
|
|
15576
|
-
execSync(
|
|
15689
|
+
execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
|
|
15577
15690
|
timeout: 12e4,
|
|
15578
15691
|
stdio: opts?.silent ? "pipe" : "inherit"
|
|
15579
15692
|
});
|
|
@@ -15588,11 +15701,57 @@ async function installOpenClaw(opts) {
|
|
|
15588
15701
|
if (!detection.found) {
|
|
15589
15702
|
return {
|
|
15590
15703
|
success: false,
|
|
15591
|
-
error:
|
|
15704
|
+
error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
|
|
15592
15705
|
};
|
|
15593
15706
|
}
|
|
15707
|
+
if (detection.version) {
|
|
15708
|
+
const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
|
|
15709
|
+
const detectedVersion = parseVersionTuple(detection.version);
|
|
15710
|
+
if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
|
|
15711
|
+
return {
|
|
15712
|
+
success: false,
|
|
15713
|
+
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.`
|
|
15714
|
+
};
|
|
15715
|
+
}
|
|
15716
|
+
}
|
|
15594
15717
|
return { success: true, detection };
|
|
15595
15718
|
}
|
|
15719
|
+
function getUnsupportedNodeError(currentNodeVersionOverride) {
|
|
15720
|
+
const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
|
|
15721
|
+
const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
|
|
15722
|
+
const currentTuple = parseVersionTuple(currentNodeVersion);
|
|
15723
|
+
if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
|
|
15724
|
+
return null;
|
|
15725
|
+
}
|
|
15726
|
+
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".`;
|
|
15727
|
+
}
|
|
15728
|
+
function normalizeVersion(version) {
|
|
15729
|
+
const tuple = parseVersionTuple(version);
|
|
15730
|
+
if (!tuple) {
|
|
15731
|
+
return version.trim().replace(/^v/i, "");
|
|
15732
|
+
}
|
|
15733
|
+
return tuple.join(".");
|
|
15734
|
+
}
|
|
15735
|
+
function parseVersionTuple(version) {
|
|
15736
|
+
const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
15737
|
+
if (!match) {
|
|
15738
|
+
return null;
|
|
15739
|
+
}
|
|
15740
|
+
return [
|
|
15741
|
+
Number(match[1]),
|
|
15742
|
+
Number(match[2] ?? 0),
|
|
15743
|
+
Number(match[3] ?? 0)
|
|
15744
|
+
];
|
|
15745
|
+
}
|
|
15746
|
+
function compareVersionTuples(left, right) {
|
|
15747
|
+
for (let index = 0; index < left.length; index++) {
|
|
15748
|
+
const delta = left[index] - right[index];
|
|
15749
|
+
if (delta !== 0) {
|
|
15750
|
+
return delta;
|
|
15751
|
+
}
|
|
15752
|
+
}
|
|
15753
|
+
return 0;
|
|
15754
|
+
}
|
|
15596
15755
|
function seedWorkspace(stateDir) {
|
|
15597
15756
|
const workspaceDir = path7.join(stateDir, "workspace");
|
|
15598
15757
|
fs6.mkdirSync(workspaceDir, { recursive: true });
|
|
@@ -15802,68 +15961,46 @@ function serializeSessionCookie(opts) {
|
|
|
15802
15961
|
}
|
|
15803
15962
|
return parts.join("; ");
|
|
15804
15963
|
}
|
|
15805
|
-
function
|
|
15806
|
-
|
|
15807
|
-
|
|
15808
|
-
|
|
15809
|
-
)
|
|
15810
|
-
|
|
15811
|
-
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
let migrated = 0;
|
|
15846
|
-
for (const row of rows) {
|
|
15847
|
-
const project = db.select({ name: projects.name }).from(projects).where(eq24(projects.id, row.project_id)).get();
|
|
15848
|
-
if (!project) continue;
|
|
15849
|
-
const existing = getGa4Connection(config, project.name);
|
|
15850
|
-
if (existing?.privateKey) continue;
|
|
15851
|
-
upsertGa4Connection(config, {
|
|
15852
|
-
projectName: project.name,
|
|
15853
|
-
propertyId: row.property_id,
|
|
15854
|
-
clientEmail: row.client_email,
|
|
15855
|
-
privateKey: row.private_key,
|
|
15856
|
-
createdAt: row.created_at,
|
|
15857
|
-
updatedAt: row.updated_at
|
|
15858
|
-
});
|
|
15859
|
-
migrated++;
|
|
15860
|
-
}
|
|
15861
|
-
if (migrated > 0) {
|
|
15862
|
-
saveConfigPatch({ ga4: config.ga4 });
|
|
15863
|
-
log9.info("credentials.migrated", { type: "ga4", count: migrated });
|
|
15864
|
-
}
|
|
15865
|
-
}
|
|
15866
|
-
} catch {
|
|
15964
|
+
function applyLegacyCredentials(rows, config) {
|
|
15965
|
+
let migratedGoogle = 0;
|
|
15966
|
+
for (const row of rows.google) {
|
|
15967
|
+
const existing = getGoogleConnection(config, row.domain, row.connectionType);
|
|
15968
|
+
if (existing?.refreshToken) continue;
|
|
15969
|
+
upsertGoogleConnection(config, {
|
|
15970
|
+
domain: row.domain,
|
|
15971
|
+
connectionType: row.connectionType,
|
|
15972
|
+
propertyId: row.propertyId,
|
|
15973
|
+
sitemapUrl: row.sitemapUrl,
|
|
15974
|
+
accessToken: row.accessToken ?? void 0,
|
|
15975
|
+
refreshToken: row.refreshToken,
|
|
15976
|
+
tokenExpiresAt: row.tokenExpiresAt,
|
|
15977
|
+
scopes: row.scopes,
|
|
15978
|
+
createdAt: row.createdAt,
|
|
15979
|
+
updatedAt: row.updatedAt
|
|
15980
|
+
});
|
|
15981
|
+
migratedGoogle++;
|
|
15982
|
+
}
|
|
15983
|
+
if (migratedGoogle > 0) {
|
|
15984
|
+
saveConfigPatch({ google: config.google });
|
|
15985
|
+
log9.info("credentials.migrated", { type: "google", count: migratedGoogle });
|
|
15986
|
+
}
|
|
15987
|
+
let migratedGa4 = 0;
|
|
15988
|
+
for (const row of rows.ga4) {
|
|
15989
|
+
const existing = getGa4Connection(config, row.projectName);
|
|
15990
|
+
if (existing?.privateKey) continue;
|
|
15991
|
+
upsertGa4Connection(config, {
|
|
15992
|
+
projectName: row.projectName,
|
|
15993
|
+
propertyId: row.propertyId,
|
|
15994
|
+
clientEmail: row.clientEmail,
|
|
15995
|
+
privateKey: row.privateKey,
|
|
15996
|
+
createdAt: row.createdAt,
|
|
15997
|
+
updatedAt: row.updatedAt
|
|
15998
|
+
});
|
|
15999
|
+
migratedGa4++;
|
|
16000
|
+
}
|
|
16001
|
+
if (migratedGa4 > 0) {
|
|
16002
|
+
saveConfigPatch({ ga4: config.ga4 });
|
|
16003
|
+
log9.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
|
|
15867
16004
|
}
|
|
15868
16005
|
}
|
|
15869
16006
|
async function createServer(opts) {
|
|
@@ -15890,7 +16027,15 @@ async function createServer(opts) {
|
|
|
15890
16027
|
quota: opts.config.geminiQuota
|
|
15891
16028
|
};
|
|
15892
16029
|
}
|
|
15893
|
-
|
|
16030
|
+
try {
|
|
16031
|
+
const legacyRows = extractLegacyCredentials(opts.db);
|
|
16032
|
+
applyLegacyCredentials(legacyRows, opts.config);
|
|
16033
|
+
dropLegacyCredentialColumns(opts.db);
|
|
16034
|
+
} catch (err) {
|
|
16035
|
+
log9.warn("credentials.migration.failed", {
|
|
16036
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16037
|
+
});
|
|
16038
|
+
}
|
|
15894
16039
|
log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
15895
16040
|
const p = providers[k];
|
|
15896
16041
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
@@ -16483,7 +16628,7 @@ async function createServer(opts) {
|
|
|
16483
16628
|
await app.register(fastifyStatic.default, {
|
|
16484
16629
|
root: assetsDir,
|
|
16485
16630
|
prefix: basePath ?? "/",
|
|
16486
|
-
wildcard:
|
|
16631
|
+
wildcard: true,
|
|
16487
16632
|
// Don't serve index.html automatically — we handle it with config injection
|
|
16488
16633
|
serve: true,
|
|
16489
16634
|
index: false
|
|
@@ -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,17 @@ 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.
|
|
857
|
+
// v37: The legacy credential columns (private_key on ga_connections; access_token,
|
|
858
|
+
// refresh_token, token_expires_at on google_connections) are removed by the
|
|
859
|
+
// extractLegacyCredentials / dropLegacyCredentialColumns pair below. Callers
|
|
860
|
+
// read the rows, persist them to config.yaml, and only then drop the columns
|
|
861
|
+
// so a failed config write doesn't permanently lose credentials. Keeping the
|
|
862
|
+
// DROPs as raw SQL here would race with that read.
|
|
852
863
|
];
|
|
853
864
|
function isDuplicateColumnError(err) {
|
|
854
865
|
if (!(err instanceof Error)) return false;
|
|
@@ -856,6 +867,83 @@ function isDuplicateColumnError(err) {
|
|
|
856
867
|
if (err.cause instanceof Error && err.cause.message.includes("duplicate column name")) return true;
|
|
857
868
|
return false;
|
|
858
869
|
}
|
|
870
|
+
function columnExists(db, table, column) {
|
|
871
|
+
const rows = db.all(sql.raw(
|
|
872
|
+
`SELECT COUNT(*) as c FROM pragma_table_info('${table}') WHERE name = '${column}'`
|
|
873
|
+
));
|
|
874
|
+
return (rows[0]?.c ?? 0) > 0;
|
|
875
|
+
}
|
|
876
|
+
function dropColumnIfExists(db, table, column) {
|
|
877
|
+
try {
|
|
878
|
+
db.run(sql.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
|
|
879
|
+
} catch (err) {
|
|
880
|
+
if (!(err instanceof Error)) throw err;
|
|
881
|
+
const msg = err.message;
|
|
882
|
+
const causeMsg = err.cause instanceof Error ? err.cause.message : "";
|
|
883
|
+
const expected = `no such column: "${column}"`;
|
|
884
|
+
const expectedAlt = `no such column: ${column}`;
|
|
885
|
+
if (msg.includes(expected) || msg.includes(expectedAlt)) return;
|
|
886
|
+
if (causeMsg.includes(expected) || causeMsg.includes(expectedAlt)) return;
|
|
887
|
+
throw err;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function extractLegacyCredentials(db) {
|
|
891
|
+
const out = { google: [], ga4: [] };
|
|
892
|
+
if (columnExists(db, "google_connections", "access_token")) {
|
|
893
|
+
const rows = db.all(sql.raw(
|
|
894
|
+
`SELECT domain, connection_type, property_id, sitemap_url, access_token, refresh_token, token_expires_at, scopes, created_at, updated_at
|
|
895
|
+
FROM google_connections
|
|
896
|
+
WHERE refresh_token IS NOT NULL AND refresh_token != ''`
|
|
897
|
+
));
|
|
898
|
+
for (const row of rows) {
|
|
899
|
+
out.google.push({
|
|
900
|
+
domain: row.domain,
|
|
901
|
+
connectionType: row.connection_type,
|
|
902
|
+
propertyId: row.property_id,
|
|
903
|
+
sitemapUrl: row.sitemap_url,
|
|
904
|
+
accessToken: row.access_token,
|
|
905
|
+
refreshToken: row.refresh_token,
|
|
906
|
+
tokenExpiresAt: row.token_expires_at,
|
|
907
|
+
scopes: parseJsonColumn(row.scopes, []),
|
|
908
|
+
createdAt: row.created_at,
|
|
909
|
+
updatedAt: row.updated_at
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (columnExists(db, "ga_connections", "private_key")) {
|
|
914
|
+
const rows = db.all(sql.raw(
|
|
915
|
+
`SELECT p.name AS project_name, ga.property_id, ga.client_email, ga.private_key, ga.created_at, ga.updated_at
|
|
916
|
+
FROM ga_connections ga
|
|
917
|
+
INNER JOIN projects p ON p.id = ga.project_id
|
|
918
|
+
WHERE ga.private_key IS NOT NULL AND ga.private_key != ''`
|
|
919
|
+
));
|
|
920
|
+
for (const row of rows) {
|
|
921
|
+
out.ga4.push({
|
|
922
|
+
projectName: row.project_name,
|
|
923
|
+
propertyId: row.property_id,
|
|
924
|
+
clientEmail: row.client_email,
|
|
925
|
+
privateKey: row.private_key,
|
|
926
|
+
createdAt: row.created_at,
|
|
927
|
+
updatedAt: row.updated_at
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
}
|
|
933
|
+
function dropLegacyCredentialColumns(db) {
|
|
934
|
+
if (columnExists(db, "google_connections", "access_token")) {
|
|
935
|
+
dropColumnIfExists(db, "google_connections", "access_token");
|
|
936
|
+
}
|
|
937
|
+
if (columnExists(db, "google_connections", "refresh_token")) {
|
|
938
|
+
dropColumnIfExists(db, "google_connections", "refresh_token");
|
|
939
|
+
}
|
|
940
|
+
if (columnExists(db, "google_connections", "token_expires_at")) {
|
|
941
|
+
dropColumnIfExists(db, "google_connections", "token_expires_at");
|
|
942
|
+
}
|
|
943
|
+
if (columnExists(db, "ga_connections", "private_key")) {
|
|
944
|
+
dropColumnIfExists(db, "ga_connections", "private_key");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
859
947
|
function migrate(db) {
|
|
860
948
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
861
949
|
for (const statement of statements) {
|
|
@@ -1303,6 +1391,8 @@ export {
|
|
|
1303
1391
|
healthSnapshots,
|
|
1304
1392
|
createClient,
|
|
1305
1393
|
parseJsonColumn,
|
|
1394
|
+
extractLegacyCredentials,
|
|
1395
|
+
dropLegacyCredentialColumns,
|
|
1306
1396
|
migrate,
|
|
1307
1397
|
createLogger,
|
|
1308
1398
|
IntelligenceService
|
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-IPOVH342.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-ZZ57GRV6.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-MZ7SXEGE.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.4",
|
|
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
61
|
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
62
|
-
"@ainyc/canonry-integration-google": "0.0.0",
|
|
63
|
-
"@ainyc/canonry-intelligence": "0.0.0",
|
|
64
|
-
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
65
62
|
"@ainyc/canonry-integration-wordpress": "0.0.0",
|
|
63
|
+
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
64
|
+
"@ainyc/canonry-intelligence": "0.0.0",
|
|
66
65
|
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
67
|
-
"@ainyc/canonry-provider-
|
|
66
|
+
"@ainyc/canonry-provider-local": "0.0.0",
|
|
68
67
|
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
68
|
+
"@ainyc/canonry-integration-google": "0.0.0",
|
|
69
69
|
"@ainyc/canonry-provider-perplexity": "0.0.0",
|
|
70
|
-
"@ainyc/canonry-provider-
|
|
70
|
+
"@ainyc/canonry-provider-gemini": "0.0.0"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
|