@ainyc/canonry 4.180.7 → 4.182.3
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 +80 -45
- package/assets/agent-workspace/skills/canonry/SKILL.md +13 -10
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -6
- package/assets/assets/{AuditHistoryPanel-B1lmI7wm.js → AuditHistoryPanel-1_FHpzj-.js} +1 -1
- package/assets/assets/{BacklinksPage-Bw61uQu8.js → BacklinksPage-045za10_.js} +1 -1
- package/assets/assets/{ChartPrimitives-DG35TyhH.js → ChartPrimitives-DLEICrrL.js} +1 -1
- package/assets/assets/{HistoryPage-D8z9-NkT.js → HistoryPage-36K_6asu.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-CXX9GFj8.js → MeasurementPropertyPage-B4DJGoY6.js} +1 -1
- package/assets/assets/ProjectPage-CshC68Ne.js +9 -0
- package/assets/assets/{RunRow-DXHy16T3.js → RunRow-U2CN755e.js} +1 -1
- package/assets/assets/{RunsPage-D2aE620i.js → RunsPage-D0CtU4d-.js} +1 -1
- package/assets/assets/SettingsPage-zMWieTPK.js +1 -0
- package/assets/assets/SiteHealthSection-TpQPu_Uj.js +4 -0
- package/assets/assets/{TrafficPage-DFHWgvkU.js → TrafficPage-DfLUt-64.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DxjyRL8Y.js → TrafficSourceDetailPage-DbjNNWSt.js} +1 -1
- package/assets/assets/extract-error-message-CWqz1QPC.js +1 -0
- package/assets/assets/index-Brb7M_6G.js +86 -0
- package/assets/assets/index-DwdK2e4e.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-DAcOZae7.js → react-sigma_core.esm.min-Drqprr9L.js} +1 -1
- package/assets/assets/{vendor-lucide-nYKv-BB7.js → vendor-lucide-XjCNFK0x.js} +1 -1
- package/assets/assets/{vendor-markdown-Bn7fRwrx.js → vendor-markdown-9tTeP8v7.js} +1 -1
- package/assets/assets/{vendor-radix-4UOaVWza.js → vendor-radix-BpmvLvpp.js} +1 -1
- package/assets/assets/{vendor-recharts-D8rt5lKn.js → vendor-recharts-FA0uySU7.js} +1 -1
- package/assets/assets/{vendor-tanstack-Dock0eH1.js → vendor-tanstack-CYBjbqVr.js} +1 -1
- package/assets/index.html +7 -7
- package/dist/{chunk-VZO6CLHY.js → chunk-BDJT7XZL.js} +169 -64
- package/dist/{chunk-RXXZ5BU5.js → chunk-DY5ZDKQW.js} +183 -1
- package/dist/{chunk-CWXPBQR5.js → chunk-FVC7PGYQ.js} +47 -5
- package/dist/{chunk-XYTSKRO3.js → chunk-I3KSOMJA.js} +2 -2
- package/dist/{chunk-WJFBNCVD.js → chunk-K45YKXE4.js} +4310 -3323
- package/dist/cli.js +323 -99
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-MEPYXRBS.js → intelligence-service-HGA553GT.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +7 -7
- package/assets/assets/ProjectPage-BaN_rvoV.js +0 -9
- package/assets/assets/SettingsPage-BiiFqarS.js +0 -1
- package/assets/assets/SiteHealthSection-DCH2Ejtc.js +0 -4
- package/assets/assets/extract-error-message-BdXUZQW9.js +0 -1
- package/assets/assets/index-BuEq5Rhg.css +0 -1
- package/assets/assets/index-CSictO9U.js +0 -83
package/dist/cli.js
CHANGED
|
@@ -19,18 +19,20 @@ import {
|
|
|
19
19
|
getOrCreateAnonymousId,
|
|
20
20
|
isAeroToolProfile,
|
|
21
21
|
isFirstRun,
|
|
22
|
+
isLoopbackBindHost,
|
|
22
23
|
isTelemetryEnabled,
|
|
23
24
|
listAgentProviders,
|
|
24
25
|
setGoogleAuthConfig,
|
|
25
26
|
setTelemetrySource,
|
|
26
27
|
showFirstRunNotice,
|
|
27
28
|
trackCliCommandFinished,
|
|
28
|
-
trackEvent
|
|
29
|
-
|
|
29
|
+
trackEvent,
|
|
30
|
+
waitForServerRuntimeStartup
|
|
31
|
+
} from "./chunk-BDJT7XZL.js";
|
|
30
32
|
import {
|
|
31
33
|
autoSyncSkills,
|
|
32
34
|
formatAutoSyncNotice
|
|
33
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-I3KSOMJA.js";
|
|
34
36
|
import {
|
|
35
37
|
CliError,
|
|
36
38
|
EXIT_SYSTEM_ERROR,
|
|
@@ -55,7 +57,7 @@ import {
|
|
|
55
57
|
saveConfigPatch,
|
|
56
58
|
systemError,
|
|
57
59
|
usageError
|
|
58
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-FVC7PGYQ.js";
|
|
59
61
|
import {
|
|
60
62
|
CLOUDFLARE_WORKER_BINDINGS,
|
|
61
63
|
CLOUDFLARE_WORKER_GENERATED_MARKER,
|
|
@@ -69,7 +71,7 @@ import {
|
|
|
69
71
|
projects,
|
|
70
72
|
queries,
|
|
71
73
|
renderReportHtml
|
|
72
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-K45YKXE4.js";
|
|
73
75
|
import {
|
|
74
76
|
AdsDeliverySnapshotStatuses,
|
|
75
77
|
AdsHistoricalCampaignRollupStatuses,
|
|
@@ -138,7 +140,7 @@ import {
|
|
|
138
140
|
providerQuotaPolicySchema,
|
|
139
141
|
resolveProviderInput,
|
|
140
142
|
winnabilityClassSchema
|
|
141
|
-
} from "./chunk-
|
|
143
|
+
} from "./chunk-DY5ZDKQW.js";
|
|
142
144
|
|
|
143
145
|
// src/cli.ts
|
|
144
146
|
import { pathToFileURL } from "url";
|
|
@@ -184,6 +186,13 @@ import { parseArgs } from "util";
|
|
|
184
186
|
function commandId(spec) {
|
|
185
187
|
return spec.path.join(".");
|
|
186
188
|
}
|
|
189
|
+
function printCommandHelp(spec) {
|
|
190
|
+
console.log(`
|
|
191
|
+
Usage: ${spec.usage}
|
|
192
|
+
`);
|
|
193
|
+
if (spec.help) console.log(`${spec.help}
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
187
196
|
function matchesPath(args, path13) {
|
|
188
197
|
if (args.length < path13.length) return false;
|
|
189
198
|
return path13.every((segment, index) => args[index] === segment);
|
|
@@ -222,9 +231,7 @@ async function dispatchRegisteredCommand(args, fallbackFormat, specs) {
|
|
|
222
231
|
const argsWithoutHelp = args.filter((a) => a !== "--help" && a !== "-h");
|
|
223
232
|
const exactMatch = [...specs].sort((a, b) => b.path.length - a.path.length).find((candidate) => matchesPath(argsWithoutHelp, candidate.path));
|
|
224
233
|
if (exactMatch && exactMatch.path.length > 1) {
|
|
225
|
-
|
|
226
|
-
Usage: ${exactMatch.usage}
|
|
227
|
-
`);
|
|
234
|
+
printCommandHelp(exactMatch);
|
|
228
235
|
return true;
|
|
229
236
|
}
|
|
230
237
|
const group = argsWithoutHelp[0];
|
|
@@ -236,9 +243,7 @@ Usage: ${exactMatch.usage}
|
|
|
236
243
|
}
|
|
237
244
|
const single = specs.find((s) => s.path.length === 1 && s.path[0] === group);
|
|
238
245
|
if (single) {
|
|
239
|
-
|
|
240
|
-
Usage: ${single.usage}
|
|
241
|
-
`);
|
|
246
|
+
printCommandHelp(single);
|
|
242
247
|
return true;
|
|
243
248
|
}
|
|
244
249
|
}
|
|
@@ -7564,8 +7569,95 @@ async function listCompetitors(project, format) {
|
|
|
7564
7569
|
console.log(` ${c.domain}`);
|
|
7565
7570
|
}
|
|
7566
7571
|
}
|
|
7572
|
+
async function showCompetitorLandscape(project, options) {
|
|
7573
|
+
const client = getClient10();
|
|
7574
|
+
const data = await client.getCompetitorLandscape(project, {
|
|
7575
|
+
window: options.window,
|
|
7576
|
+
scope: options.scope,
|
|
7577
|
+
groupKey: options.groupKey,
|
|
7578
|
+
provider: options.provider,
|
|
7579
|
+
...options.groupBy ? { groupBy: options.groupBy } : {},
|
|
7580
|
+
...options.model !== void 0 ? { model: options.model } : {},
|
|
7581
|
+
queryClass: options.queryClass,
|
|
7582
|
+
location: options.location,
|
|
7583
|
+
runId: options.runId
|
|
7584
|
+
});
|
|
7585
|
+
if (options.format === "json") {
|
|
7586
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7587
|
+
return;
|
|
7588
|
+
}
|
|
7589
|
+
if (options.format === "jsonl") {
|
|
7590
|
+
console.log(JSON.stringify(data));
|
|
7591
|
+
return;
|
|
7592
|
+
}
|
|
7593
|
+
printCompetitorLandscape(data);
|
|
7594
|
+
}
|
|
7595
|
+
function printCompetitorLandscape(data) {
|
|
7596
|
+
const scope = data.scope.kind === "group" ? `market ${data.scope.groupKey}` : data.scope.kind === "all-markets" ? "all markets" : "project";
|
|
7597
|
+
console.log(`Competitor landscape \xB7 ${scope} \xB7 ${data.window}`);
|
|
7598
|
+
if (data.filters.model !== void 0) {
|
|
7599
|
+
console.log(`Requested model filter: ${data.filters.provider} \xB7 ${data.filters.model}`);
|
|
7600
|
+
}
|
|
7601
|
+
console.log("Mention share is answer-text evidence; citations are independent source-list evidence.");
|
|
7602
|
+
console.log(
|
|
7603
|
+
data.filters.queryClass === "all" ? "Share of voice is blank: branded and non-brand queries are pooled here. Add --query-class non-brand for a ratio." : `Counting ${data.filters.queryClass} queries only.`
|
|
7604
|
+
);
|
|
7605
|
+
if (data.truncated) console.log("Top 100 observed competitors and top 100 other cited sources shown; pinned competitors are complete.");
|
|
7606
|
+
console.log("");
|
|
7607
|
+
printLandscapeRows("Your brand", [data.project]);
|
|
7608
|
+
printLandscapeRows("Pinned competitors", data.pinned);
|
|
7609
|
+
printLandscapeRows("Observed competitors", data.observed);
|
|
7610
|
+
printLandscapeRows("Other cited sources", data.otherSources);
|
|
7611
|
+
console.log("");
|
|
7612
|
+
console.log(
|
|
7613
|
+
`Evidence: ${data.evidence.answeredResults} answer-text result(s), ${data.evidence.sourceResults} source result(s); excluded: ${data.evidence.excludedProbeResults} probe, ${data.evidence.excludedNonCompletedResults} non-completed.`
|
|
7614
|
+
);
|
|
7615
|
+
if (data.modelComparison) printModelComparison(data.modelComparison);
|
|
7616
|
+
}
|
|
7617
|
+
function describeServedModels(evidence) {
|
|
7618
|
+
if (evidence.status === "unknown") return "Unknown (not disclosed)";
|
|
7619
|
+
if (evidence.status === "known") return evidence.model;
|
|
7620
|
+
return [...evidence.models, ...evidence.includesUnknown ? ["Unknown (not disclosed)"] : []].join(", ");
|
|
7621
|
+
}
|
|
7622
|
+
function printModelComparison(comparison) {
|
|
7623
|
+
console.log("");
|
|
7624
|
+
console.log(`Model comparison \xB7 requested-model basis \xB7 ${comparison.groups.length} of ${comparison.totalGroups} groups`);
|
|
7625
|
+
console.log("Groups use stored observations. They do not form a matched-query or equal-weight comparison.");
|
|
7626
|
+
if (comparison.truncated) console.log("The first 50 provider/model groups are shown. Additional groups are omitted.");
|
|
7627
|
+
for (const group of comparison.groups) {
|
|
7628
|
+
console.log("");
|
|
7629
|
+
console.log(`${group.provider} \xB7 requested model: ${group.model ?? "Unknown (not recorded)"}`);
|
|
7630
|
+
console.log(`Served model evidence: ${describeServedModels(group.servedModels)}`);
|
|
7631
|
+
console.log(`Samples: ${group.snapshotCount} snapshot(s), ${group.evidence.answeredResults} answer-text result(s), ${group.evidence.sourceResults} source result(s).`);
|
|
7632
|
+
printLandscapeRows("Your brand", [group.project], true);
|
|
7633
|
+
printLandscapeRows("Pinned competitors", group.pinned, true);
|
|
7634
|
+
printLandscapeRows("Observed competitors", group.observed, true);
|
|
7635
|
+
printLandscapeRows("Other cited sources", group.otherSources, true);
|
|
7636
|
+
if (group.truncated) console.log("Top 100 observed competitors and top 100 other sources shown. Pinned competitors are complete.");
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
function printLandscapeRows(heading, rows, showSampleCount = false) {
|
|
7640
|
+
console.log(`${heading}:`);
|
|
7641
|
+
if (rows.length === 0) {
|
|
7642
|
+
console.log(" \u2014");
|
|
7643
|
+
return;
|
|
7644
|
+
}
|
|
7645
|
+
for (const row of rows) {
|
|
7646
|
+
const sov = row.shareOfVoice === null ? "\u2014" : `${row.shareOfVoice.toFixed(1)}%`;
|
|
7647
|
+
console.log(` ${row.domain} mention ${row.mentionCount} \xB7 citation ${row.citationCount} \xB7 SOV ${sov}${showSampleCount ? ` \xB7 answers ${row.answeredResults}` : ""}`);
|
|
7648
|
+
}
|
|
7649
|
+
}
|
|
7567
7650
|
|
|
7568
7651
|
// src/cli-commands/competitor.ts
|
|
7652
|
+
var LANDSCAPE_USAGE = "canonry competitor landscape <project> [--window 7d|30d|90d|all] [--group-key <key>|--scope all-markets] [--by-model] [--provider <provider> [--model <id>]] [--query-class all|branded|non-brand] [--location <label>] [--run-id <id>] [--format json|jsonl]";
|
|
7653
|
+
function parseLandscapeScope(value) {
|
|
7654
|
+
if (value === void 0 || value === "") return void 0;
|
|
7655
|
+
if (value !== "all-markets") throw usageError(`--scope must be "all-markets" (got "${value}")`, {
|
|
7656
|
+
message: '--scope must be "all-markets"',
|
|
7657
|
+
details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
|
|
7658
|
+
});
|
|
7659
|
+
return value;
|
|
7660
|
+
}
|
|
7569
7661
|
var COMPETITOR_CLI_COMMANDS = [
|
|
7570
7662
|
{
|
|
7571
7663
|
path: ["competitor", "add"],
|
|
@@ -7629,14 +7721,60 @@ var COMPETITOR_CLI_COMMANDS = [
|
|
|
7629
7721
|
await listCompetitors(project, input.format);
|
|
7630
7722
|
}
|
|
7631
7723
|
},
|
|
7724
|
+
{
|
|
7725
|
+
path: ["competitor", "landscape"],
|
|
7726
|
+
usage: LANDSCAPE_USAGE,
|
|
7727
|
+
options: {
|
|
7728
|
+
window: stringOption(),
|
|
7729
|
+
"group-key": stringOption(),
|
|
7730
|
+
scope: stringOption(),
|
|
7731
|
+
provider: stringOption(),
|
|
7732
|
+
"by-model": { type: "boolean" },
|
|
7733
|
+
model: stringOption(),
|
|
7734
|
+
"query-class": stringOption(),
|
|
7735
|
+
location: stringOption(),
|
|
7736
|
+
"run-id": stringOption()
|
|
7737
|
+
},
|
|
7738
|
+
run: async (input) => {
|
|
7739
|
+
const project = requireProject(input, "competitor.landscape", LANDSCAPE_USAGE);
|
|
7740
|
+
const scope = parseLandscapeScope(getString(input.values, "scope"));
|
|
7741
|
+
const groupKey = getString(input.values, "group-key");
|
|
7742
|
+
const provider = getString(input.values, "provider");
|
|
7743
|
+
const model = getString(input.values, "model");
|
|
7744
|
+
if (model !== void 0 && !provider?.trim()) {
|
|
7745
|
+
throw usageError("--model requires --provider", {
|
|
7746
|
+
message: "--model requires --provider",
|
|
7747
|
+
details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
|
|
7748
|
+
});
|
|
7749
|
+
}
|
|
7750
|
+
if (scope === "all-markets" && groupKey) {
|
|
7751
|
+
throw usageError("--group-key cannot be combined with --scope all-markets", {
|
|
7752
|
+
message: "--group-key cannot be combined with --scope all-markets",
|
|
7753
|
+
details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
|
|
7754
|
+
});
|
|
7755
|
+
}
|
|
7756
|
+
await showCompetitorLandscape(project, {
|
|
7757
|
+
window: getString(input.values, "window"),
|
|
7758
|
+
groupKey,
|
|
7759
|
+
scope,
|
|
7760
|
+
provider,
|
|
7761
|
+
...getBoolean(input.values, "by-model") ? { groupBy: "model" } : {},
|
|
7762
|
+
...model !== void 0 ? { model } : {},
|
|
7763
|
+
queryClass: getString(input.values, "query-class"),
|
|
7764
|
+
location: getString(input.values, "location"),
|
|
7765
|
+
runId: getString(input.values, "run-id"),
|
|
7766
|
+
format: input.format
|
|
7767
|
+
});
|
|
7768
|
+
}
|
|
7769
|
+
},
|
|
7632
7770
|
{
|
|
7633
7771
|
path: ["competitor"],
|
|
7634
|
-
usage: "canonry competitor <add|remove|delete|list> <project> [args]",
|
|
7772
|
+
usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
|
|
7635
7773
|
run: async (input) => {
|
|
7636
7774
|
unknownSubcommand(input.positionals[0], {
|
|
7637
7775
|
command: "competitor",
|
|
7638
|
-
usage: "canonry competitor <add|remove|delete|list> <project> [args]",
|
|
7639
|
-
available: ["add", "remove", "delete", "list"]
|
|
7776
|
+
usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
|
|
7777
|
+
available: ["add", "remove", "delete", "list", "landscape"]
|
|
7640
7778
|
});
|
|
7641
7779
|
}
|
|
7642
7780
|
}
|
|
@@ -13866,6 +14004,7 @@ var CONTENT_CLI_COMMANDS = [
|
|
|
13866
14004
|
// src/commands/bootstrap.ts
|
|
13867
14005
|
import crypto from "crypto";
|
|
13868
14006
|
import path9 from "path";
|
|
14007
|
+
import { isDeepStrictEqual } from "util";
|
|
13869
14008
|
import { eq } from "drizzle-orm";
|
|
13870
14009
|
|
|
13871
14010
|
// ../config/src/index.ts
|
|
@@ -14011,36 +14150,40 @@ function getBootstrapEnv(source, overrides) {
|
|
|
14011
14150
|
}
|
|
14012
14151
|
|
|
14013
14152
|
// src/commands/bootstrap.ts
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
if (!hasProvider) {
|
|
14020
|
-
console.warn(
|
|
14021
|
-
"Warning: No provider env vars set (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, or LOCAL_BASE_URL). You can configure providers later via the dashboard."
|
|
14153
|
+
function persistedValue(value) {
|
|
14154
|
+
if (Array.isArray(value)) return value.map(persistedValue);
|
|
14155
|
+
if (value && typeof value === "object") {
|
|
14156
|
+
return Object.fromEntries(
|
|
14157
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, persistedValue(entry)])
|
|
14022
14158
|
);
|
|
14023
14159
|
}
|
|
14160
|
+
return value;
|
|
14161
|
+
}
|
|
14162
|
+
async function bootstrapCommand(opts) {
|
|
14163
|
+
const format = opts?.format ?? "text";
|
|
14164
|
+
const env = getBootstrapEnv(process.env);
|
|
14165
|
+
const providers = env.providers;
|
|
14024
14166
|
const configDir = getConfigDir();
|
|
14025
|
-
const databasePath = env.databasePath || path9.join(configDir, "data.db");
|
|
14026
14167
|
const existing = configExists();
|
|
14027
14168
|
const existingConfig = existing ? loadConfig() : void 0;
|
|
14169
|
+
const existingRaw = existing ? loadConfigRaw() : null;
|
|
14170
|
+
const databasePath = env.databasePath || existingRaw?.database || path9.join(configDir, "data.db");
|
|
14028
14171
|
let rawApiKey;
|
|
14029
14172
|
let generatedApiKey;
|
|
14030
14173
|
if (env.apiKey) {
|
|
14031
14174
|
rawApiKey = env.apiKey;
|
|
14032
|
-
} else if (
|
|
14033
|
-
rawApiKey =
|
|
14175
|
+
} else if (existingRaw) {
|
|
14176
|
+
rawApiKey = existingRaw.apiKey;
|
|
14034
14177
|
} else {
|
|
14035
14178
|
generatedApiKey = `cnry_${crypto.randomBytes(16).toString("hex")}`;
|
|
14036
14179
|
rawApiKey = generatedApiKey;
|
|
14037
14180
|
}
|
|
14038
14181
|
const mergedProviders = { ...existingConfig?.providers };
|
|
14039
|
-
if (providers
|
|
14040
|
-
if (providers
|
|
14041
|
-
if (providers
|
|
14042
|
-
if (providers
|
|
14043
|
-
if (providers
|
|
14182
|
+
if (providers.gemini) mergedProviders.gemini = providers.gemini;
|
|
14183
|
+
if (providers.openai) mergedProviders.openai = providers.openai;
|
|
14184
|
+
if (providers.claude) mergedProviders.claude = providers.claude;
|
|
14185
|
+
if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
|
|
14186
|
+
if (providers.local) mergedProviders.local = providers.local;
|
|
14044
14187
|
if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
|
|
14045
14188
|
console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
|
|
14046
14189
|
}
|
|
@@ -14051,25 +14194,42 @@ async function bootstrapCommand(_opts) {
|
|
|
14051
14194
|
} : existingConfig?.google;
|
|
14052
14195
|
const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
|
|
14053
14196
|
const keyPrefix = rawApiKey.slice(0, 9);
|
|
14054
|
-
const existingConfigKeyHash =
|
|
14197
|
+
const existingConfigKeyHash = existingRaw ? crypto.createHash("sha256").update(existingRaw.apiKey).digest("hex") : void 0;
|
|
14055
14198
|
const db = createClient(databasePath);
|
|
14056
14199
|
migrate(db);
|
|
14057
|
-
db.transaction((tx) => {
|
|
14200
|
+
const keyChanged = db.transaction((tx) => {
|
|
14201
|
+
let changed = false;
|
|
14058
14202
|
const rotatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14059
|
-
const existingDefaults = tx.select({
|
|
14203
|
+
const existingDefaults = tx.select({
|
|
14204
|
+
id: apiKeys.id,
|
|
14205
|
+
keyHash: apiKeys.keyHash,
|
|
14206
|
+
keyPrefix: apiKeys.keyPrefix,
|
|
14207
|
+
scopes: apiKeys.scopes,
|
|
14208
|
+
projectId: apiKeys.projectId,
|
|
14209
|
+
lastUsedAt: apiKeys.lastUsedAt,
|
|
14210
|
+
revokedAt: apiKeys.revokedAt
|
|
14211
|
+
}).from(apiKeys).where(eq(apiKeys.name, "default")).all();
|
|
14060
14212
|
const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
|
|
14061
14213
|
if (existingDefault) {
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14214
|
+
const rotating = existingDefault.keyHash !== keyHash;
|
|
14215
|
+
const needsUpdate = rotating || existingDefault.keyPrefix !== keyPrefix || !isDeepStrictEqual(existingDefault.scopes, ["*"]) || existingDefault.projectId !== null || existingDefault.revokedAt !== null;
|
|
14216
|
+
if (needsUpdate) {
|
|
14217
|
+
tx.update(apiKeys).set({
|
|
14218
|
+
keyHash,
|
|
14219
|
+
keyPrefix,
|
|
14220
|
+
scopes: ["*"],
|
|
14221
|
+
projectId: null,
|
|
14222
|
+
...rotating ? { lastUsedAt: null } : {},
|
|
14223
|
+
revokedAt: null
|
|
14224
|
+
}).where(eq(apiKeys.id, existingDefault.id)).run();
|
|
14225
|
+
changed = true;
|
|
14226
|
+
}
|
|
14070
14227
|
for (const duplicate of existingDefaults) {
|
|
14071
14228
|
if (duplicate.id === existingDefault.id) continue;
|
|
14072
|
-
|
|
14229
|
+
if (duplicate.revokedAt === null) {
|
|
14230
|
+
tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
|
|
14231
|
+
changed = true;
|
|
14232
|
+
}
|
|
14073
14233
|
}
|
|
14074
14234
|
} else {
|
|
14075
14235
|
tx.insert(apiKeys).values({
|
|
@@ -14080,29 +14240,41 @@ async function bootstrapCommand(_opts) {
|
|
|
14080
14240
|
scopes: ["*"],
|
|
14081
14241
|
createdAt: rotatedAt
|
|
14082
14242
|
}).run();
|
|
14243
|
+
changed = true;
|
|
14083
14244
|
}
|
|
14245
|
+
return changed;
|
|
14084
14246
|
});
|
|
14085
|
-
|
|
14086
|
-
|
|
14247
|
+
const apiUrl = env.apiUrl || existingRaw?.apiUrl || `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`;
|
|
14248
|
+
const nextConfig = {
|
|
14249
|
+
...existingRaw,
|
|
14250
|
+
apiUrl,
|
|
14087
14251
|
database: databasePath,
|
|
14088
14252
|
apiKey: rawApiKey,
|
|
14089
14253
|
providers: mergedProviders,
|
|
14090
14254
|
google: mergedGoogle
|
|
14091
|
-
}
|
|
14255
|
+
};
|
|
14256
|
+
const configChanged = !existingRaw || existingRaw.apiUrl !== apiUrl || existingRaw.database !== databasePath || existingRaw.apiKey !== rawApiKey || !isDeepStrictEqual(persistedValue(existingRaw.providers ?? {}), persistedValue(mergedProviders)) || !isDeepStrictEqual(persistedValue(existingRaw.google), persistedValue(mergedGoogle));
|
|
14257
|
+
if (configChanged) saveConfig(nextConfig);
|
|
14258
|
+
const status = !existing ? "created" : configChanged || keyChanged ? "updated" : "unchanged";
|
|
14092
14259
|
if (format === "json") {
|
|
14093
14260
|
console.log(JSON.stringify({
|
|
14094
14261
|
bootstrapped: true,
|
|
14262
|
+
status,
|
|
14263
|
+
changed: status !== "unchanged",
|
|
14095
14264
|
configPath: getConfigPath(),
|
|
14096
14265
|
databasePath,
|
|
14097
|
-
apiUrl
|
|
14098
|
-
providers: Object.keys(mergedProviders
|
|
14266
|
+
apiUrl,
|
|
14267
|
+
providers: Object.keys(mergedProviders),
|
|
14099
14268
|
googleConfigured: !!mergedGoogle,
|
|
14100
14269
|
generatedApiKey
|
|
14101
14270
|
}, null, 2));
|
|
14102
14271
|
return;
|
|
14103
14272
|
}
|
|
14104
|
-
console.log(`Bootstrap
|
|
14273
|
+
console.log(`Bootstrap ${status}. Config: ${getConfigPath()}`);
|
|
14105
14274
|
console.log(`SQLite database path: ${databasePath}`);
|
|
14275
|
+
if (Object.keys(mergedProviders).length === 0) {
|
|
14276
|
+
console.log("Providers: none (Page Health works now; add one later to enable AI Visibility).");
|
|
14277
|
+
}
|
|
14106
14278
|
if (generatedApiKey) {
|
|
14107
14279
|
console.log(`API key: ${generatedApiKey}`);
|
|
14108
14280
|
}
|
|
@@ -14112,6 +14284,27 @@ async function bootstrapCommand(_opts) {
|
|
|
14112
14284
|
import { spawn as spawn2 } from "child_process";
|
|
14113
14285
|
import fs15 from "fs";
|
|
14114
14286
|
import path10 from "path";
|
|
14287
|
+
|
|
14288
|
+
// src/operator-url.ts
|
|
14289
|
+
function operatorHost(bindHost) {
|
|
14290
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
14291
|
+
if (bindHost === "::" || bindHost === "[::]") return "::1";
|
|
14292
|
+
return bindHost;
|
|
14293
|
+
}
|
|
14294
|
+
function operatorHttpUrl(bindHost, port) {
|
|
14295
|
+
const host = operatorHost(bindHost);
|
|
14296
|
+
const bare = host.replace(/^\[|\]$/g, "");
|
|
14297
|
+
return `http://${bare.includes(":") ? `[${bare}]` : bare}:${port}`;
|
|
14298
|
+
}
|
|
14299
|
+
|
|
14300
|
+
// src/serve-endpoint.ts
|
|
14301
|
+
function resolveServePort(envPort, configPort) {
|
|
14302
|
+
const trimmed = envPort?.trim();
|
|
14303
|
+
if (trimmed) return parseInt(trimmed, 10);
|
|
14304
|
+
return configPort ?? 4100;
|
|
14305
|
+
}
|
|
14306
|
+
|
|
14307
|
+
// src/commands/daemon.ts
|
|
14115
14308
|
function getPidPath() {
|
|
14116
14309
|
return path10.join(getConfigDir(), "canonry.pid");
|
|
14117
14310
|
}
|
|
@@ -14125,7 +14318,7 @@ function isProcessAlive(pid) {
|
|
|
14125
14318
|
}
|
|
14126
14319
|
}
|
|
14127
14320
|
async function waitForReady(host, port, maxMs = 1e4) {
|
|
14128
|
-
const url =
|
|
14321
|
+
const url = `${operatorHttpUrl(host, port)}/health`;
|
|
14129
14322
|
const deadline = Date.now() + maxMs;
|
|
14130
14323
|
while (Date.now() < deadline) {
|
|
14131
14324
|
try {
|
|
@@ -14137,6 +14330,13 @@ async function waitForReady(host, port, maxMs = 1e4) {
|
|
|
14137
14330
|
}
|
|
14138
14331
|
return false;
|
|
14139
14332
|
}
|
|
14333
|
+
function resolveDaemonEndpoint(opts, env, configPort) {
|
|
14334
|
+
const portInput = opts.port || env.CANONRY_PORT;
|
|
14335
|
+
return {
|
|
14336
|
+
host: opts.host || env.CANONRY_HOST || "127.0.0.1",
|
|
14337
|
+
port: resolveServePort(portInput, configPort)
|
|
14338
|
+
};
|
|
14339
|
+
}
|
|
14140
14340
|
function buildServeForwardArgs(opts) {
|
|
14141
14341
|
const args = [];
|
|
14142
14342
|
if (opts.port) args.push("--port", opts.port);
|
|
@@ -14167,10 +14367,14 @@ async function startDaemon(opts) {
|
|
|
14167
14367
|
}
|
|
14168
14368
|
fs15.unlinkSync(pidPath);
|
|
14169
14369
|
}
|
|
14370
|
+
const config = loadConfig();
|
|
14371
|
+
const endpoint = resolveDaemonEndpoint(opts, process.env, config.port);
|
|
14372
|
+
const host = endpoint.host;
|
|
14373
|
+
const port = String(endpoint.port);
|
|
14170
14374
|
const cliPath = path10.resolve(new URL(import.meta.url).pathname);
|
|
14171
14375
|
const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
|
|
14172
14376
|
const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
|
|
14173
|
-
args.push(...buildServeForwardArgs(opts));
|
|
14377
|
+
args.push(...buildServeForwardArgs({ ...opts, host, port }));
|
|
14174
14378
|
const child = spawn2(process.execPath, args, {
|
|
14175
14379
|
detached: true,
|
|
14176
14380
|
stdio: "ignore"
|
|
@@ -14188,13 +14392,15 @@ async function startDaemon(opts) {
|
|
|
14188
14392
|
fs15.mkdirSync(configDir, { recursive: true });
|
|
14189
14393
|
}
|
|
14190
14394
|
fs15.writeFileSync(pidPath, String(child.pid), "utf-8");
|
|
14191
|
-
const port = opts.port ?? "4100";
|
|
14192
|
-
const host = opts.host ?? "127.0.0.1";
|
|
14193
14395
|
if (!isMachineFormat(format)) {
|
|
14194
14396
|
process.stderr.write("Waiting for server to start...");
|
|
14195
14397
|
}
|
|
14196
14398
|
const ready = await waitForReady(host, port);
|
|
14197
14399
|
if (!ready) {
|
|
14400
|
+
try {
|
|
14401
|
+
child.kill("SIGTERM");
|
|
14402
|
+
} catch {
|
|
14403
|
+
}
|
|
14198
14404
|
try {
|
|
14199
14405
|
fs15.unlinkSync(pidPath);
|
|
14200
14406
|
} catch {
|
|
@@ -14213,7 +14419,7 @@ async function startDaemon(opts) {
|
|
|
14213
14419
|
if (!isMachineFormat(format)) {
|
|
14214
14420
|
process.stderr.write("\n");
|
|
14215
14421
|
}
|
|
14216
|
-
const url =
|
|
14422
|
+
const url = operatorHttpUrl(host, port);
|
|
14217
14423
|
if (isMachineFormat(format)) {
|
|
14218
14424
|
console.log(JSON.stringify({
|
|
14219
14425
|
started: true,
|
|
@@ -14490,7 +14696,7 @@ async function initCommand(opts) {
|
|
|
14490
14696
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14491
14697
|
}).run();
|
|
14492
14698
|
saveConfig({
|
|
14493
|
-
apiUrl: `http://
|
|
14699
|
+
apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
|
|
14494
14700
|
database: databasePath,
|
|
14495
14701
|
apiKey: rawApiKey,
|
|
14496
14702
|
providers,
|
|
@@ -14537,7 +14743,7 @@ async function initCommand(opts) {
|
|
|
14537
14743
|
initialized: true,
|
|
14538
14744
|
configPath: getConfigPath(),
|
|
14539
14745
|
databasePath,
|
|
14540
|
-
apiUrl: `http://
|
|
14746
|
+
apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
|
|
14541
14747
|
apiKey: rawApiKey,
|
|
14542
14748
|
providers: providerNames,
|
|
14543
14749
|
googleConfigured: !!google,
|
|
@@ -14594,7 +14800,7 @@ ${mcpTip}`);
|
|
|
14594
14800
|
}
|
|
14595
14801
|
if (!isMachineFormat(format)) {
|
|
14596
14802
|
showFirstRunNotice();
|
|
14597
|
-
console.log("\nNext: canonry serve to
|
|
14803
|
+
console.log("\nNext: canonry serve to map your site and capture a Page Health baseline.");
|
|
14598
14804
|
console.log("\nNext steps:");
|
|
14599
14805
|
for (const line of nextSteps) {
|
|
14600
14806
|
console.log(` ${line}`);
|
|
@@ -14630,27 +14836,27 @@ ${mcpTip}`);
|
|
|
14630
14836
|
const answer = await prompt("\nStart the dashboard now? [Y/n]: ");
|
|
14631
14837
|
if (!/^n/i.test(answer.trim())) {
|
|
14632
14838
|
pendingServeHandoff = true;
|
|
14633
|
-
console.log("Handing off to the dashboard.
|
|
14839
|
+
console.log("Handing off to the dashboard. Map your site at /setup once it is up.");
|
|
14634
14840
|
}
|
|
14635
14841
|
}
|
|
14636
14842
|
return agentLLM;
|
|
14637
14843
|
}
|
|
14638
14844
|
function buildNextSteps() {
|
|
14639
14845
|
return [
|
|
14640
|
-
"1. Start the dashboard and open
|
|
14846
|
+
"1. Start the dashboard and open Page Health setup:",
|
|
14641
14847
|
" canonry serve",
|
|
14642
|
-
" \u2192 http://
|
|
14848
|
+
" \u2192 http://127.0.0.1:4100/setup",
|
|
14643
14849
|
"",
|
|
14644
|
-
"
|
|
14645
|
-
"
|
|
14850
|
+
" Map your public site and capture a persisted Page Health baseline.",
|
|
14851
|
+
" AI Visibility is optional and can be configured after Page Health.",
|
|
14646
14852
|
" For remote/exposed hosts, complete dashboard password setup from loopback first.",
|
|
14647
14853
|
"",
|
|
14648
14854
|
"Prefer the terminal? The same flow as CLI commands:",
|
|
14649
14855
|
"",
|
|
14650
14856
|
" a. canonry project create my-site --domain example.com --country US --language en",
|
|
14651
|
-
|
|
14652
|
-
" c. canonry
|
|
14653
|
-
" d. canonry
|
|
14857
|
+
" b. canonry technical-aeo run my-site --max-pages 100 --wait --format json",
|
|
14858
|
+
" c. canonry technical-aeo score my-site --format json",
|
|
14859
|
+
" d. canonry technical-aeo pages my-site --sort score-asc --limit 10 --format jsonl",
|
|
14654
14860
|
"",
|
|
14655
14861
|
'Tip: "canonry doctor" verifies your setup before you start.'
|
|
14656
14862
|
];
|
|
@@ -14869,10 +15075,8 @@ function detectCanonryAgentPlugin(opts = {}) {
|
|
|
14869
15075
|
}
|
|
14870
15076
|
|
|
14871
15077
|
// src/commands/serve.ts
|
|
14872
|
-
function
|
|
14873
|
-
|
|
14874
|
-
if (trimmed) return parseInt(trimmed, 10);
|
|
14875
|
-
return configPort ?? 4100;
|
|
15078
|
+
function shouldWarnAboutRemoteSetup(host) {
|
|
15079
|
+
return !isLoopbackBindHost(host);
|
|
14876
15080
|
}
|
|
14877
15081
|
async function serveCommand(format = "text") {
|
|
14878
15082
|
const config = loadConfig();
|
|
@@ -14911,26 +15115,29 @@ async function serveCommand(format = "text") {
|
|
|
14911
15115
|
codexHome: process.env.CODEX_HOME
|
|
14912
15116
|
});
|
|
14913
15117
|
const app = await createServer({ config, db, host, getAgentPluginState });
|
|
14914
|
-
let
|
|
14915
|
-
const shutdown = (signal) => {
|
|
14916
|
-
if (shuttingDown) return;
|
|
14917
|
-
shuttingDown = true;
|
|
14918
|
-
if (format === "text") {
|
|
14919
|
-
console.log(`
|
|
14920
|
-
Received ${signal}, stopping server...`);
|
|
14921
|
-
}
|
|
14922
|
-
closeWithIdleSweep(app).then(() => {
|
|
14923
|
-
process.exit(0);
|
|
14924
|
-
}).catch((err) => {
|
|
14925
|
-
console.error("Error during shutdown:", err);
|
|
14926
|
-
process.exit(1);
|
|
14927
|
-
});
|
|
14928
|
-
};
|
|
14929
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
14930
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
15118
|
+
let listening = false;
|
|
14931
15119
|
try {
|
|
14932
15120
|
await app.listen({ host, port });
|
|
14933
|
-
|
|
15121
|
+
await waitForServerRuntimeStartup(app);
|
|
15122
|
+
listening = true;
|
|
15123
|
+
let shuttingDown = false;
|
|
15124
|
+
const shutdown = (signal) => {
|
|
15125
|
+
if (shuttingDown) return;
|
|
15126
|
+
shuttingDown = true;
|
|
15127
|
+
if (format === "text") {
|
|
15128
|
+
console.log(`
|
|
15129
|
+
Received ${signal}, stopping server...`);
|
|
15130
|
+
}
|
|
15131
|
+
closeWithIdleSweep(app).then(() => {
|
|
15132
|
+
process.exit(0);
|
|
15133
|
+
}).catch((err) => {
|
|
15134
|
+
console.error("Error during shutdown:", err);
|
|
15135
|
+
process.exit(1);
|
|
15136
|
+
});
|
|
15137
|
+
};
|
|
15138
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
15139
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
15140
|
+
const url = operatorHttpUrl(host, port);
|
|
14934
15141
|
if (isMachineFormat(format)) {
|
|
14935
15142
|
console.log(JSON.stringify({
|
|
14936
15143
|
started: true,
|
|
@@ -14941,8 +15148,8 @@ Received ${signal}, stopping server...`);
|
|
|
14941
15148
|
} else {
|
|
14942
15149
|
console.log(`
|
|
14943
15150
|
Canonry server running at ${url}`);
|
|
14944
|
-
console.log(`Open ${url}/setup to
|
|
14945
|
-
if (host
|
|
15151
|
+
console.log(`Open ${url}/setup to map your site and run your first Page Health scan.`);
|
|
15152
|
+
if (shouldWarnAboutRemoteSetup(host)) {
|
|
14946
15153
|
console.log("First-run dashboard password setup is unauthenticated only on loopback; complete setup from this machine first or use a bearer cnry_... key.");
|
|
14947
15154
|
}
|
|
14948
15155
|
console.log("Press Ctrl+C to stop.\n");
|
|
@@ -14960,6 +15167,17 @@ Canonry server running at ${url}`);
|
|
|
14960
15167
|
});
|
|
14961
15168
|
} catch (err) {
|
|
14962
15169
|
const message = describeError(err);
|
|
15170
|
+
if (listening) {
|
|
15171
|
+
process.stderr.write(`warning: server started but post-startup reporting failed: ${message}
|
|
15172
|
+
`);
|
|
15173
|
+
return;
|
|
15174
|
+
}
|
|
15175
|
+
try {
|
|
15176
|
+
await closeWithIdleSweep(app);
|
|
15177
|
+
} catch (closeErr) {
|
|
15178
|
+
process.stderr.write(`warning: failed to close server after startup error: ${describeError(closeErr)}
|
|
15179
|
+
`);
|
|
15180
|
+
}
|
|
14963
15181
|
throw new CliError({
|
|
14964
15182
|
code: "SERVE_START_FAILED",
|
|
14965
15183
|
message: `Failed to start server: ${message}`,
|
|
@@ -15016,7 +15234,7 @@ function telemetryCommand(subcommand, format = "text") {
|
|
|
15016
15234
|
console.log(JSON.stringify(payload, null, 2));
|
|
15017
15235
|
return;
|
|
15018
15236
|
}
|
|
15019
|
-
console.log('Telemetry: enabled (no config yet \u2014 run "canonry
|
|
15237
|
+
console.log('Telemetry: enabled (no config yet \u2014 run "canonry bootstrap" first)');
|
|
15020
15238
|
return;
|
|
15021
15239
|
}
|
|
15022
15240
|
const config = loadConfig();
|
|
@@ -15041,8 +15259,8 @@ function telemetryCommand(subcommand, format = "text") {
|
|
|
15041
15259
|
if (!configExists()) {
|
|
15042
15260
|
throw new CliError({
|
|
15043
15261
|
code: "CONFIG_REQUIRED",
|
|
15044
|
-
message: 'No config found. Run "canonry
|
|
15045
|
-
displayMessage: 'No config found. Run "canonry
|
|
15262
|
+
message: 'No config found. Run "canonry bootstrap" first.',
|
|
15263
|
+
displayMessage: 'No config found. Run "canonry bootstrap" first.',
|
|
15046
15264
|
details: {
|
|
15047
15265
|
command: "telemetry.enable"
|
|
15048
15266
|
}
|
|
@@ -15065,8 +15283,8 @@ function telemetryCommand(subcommand, format = "text") {
|
|
|
15065
15283
|
if (!configExists()) {
|
|
15066
15284
|
throw new CliError({
|
|
15067
15285
|
code: "CONFIG_REQUIRED",
|
|
15068
|
-
message: 'No config found. Run "canonry
|
|
15069
|
-
displayMessage: 'No config found. Run "canonry
|
|
15286
|
+
message: 'No config found. Run "canonry bootstrap" first.',
|
|
15287
|
+
displayMessage: 'No config found. Run "canonry bootstrap" first.',
|
|
15070
15288
|
details: {
|
|
15071
15289
|
command: "telemetry.disable"
|
|
15072
15290
|
}
|
|
@@ -15156,14 +15374,16 @@ var SYSTEM_CLI_COMMANDS = [
|
|
|
15156
15374
|
},
|
|
15157
15375
|
{
|
|
15158
15376
|
path: ["bootstrap"],
|
|
15159
|
-
usage: "canonry bootstrap [--
|
|
15377
|
+
usage: "canonry bootstrap [--format json]",
|
|
15378
|
+
help: "Create or reconcile the local config, SQLite database, and default API key from supported environment variables. Provider credentials are optional. Safe to rerun: existing settings are preserved and the result reports created, updated, or unchanged. The legacy --force/-f flag is accepted but has no effect.",
|
|
15160
15379
|
options: {
|
|
15380
|
+
// Compatibility for scripts written before bootstrap became idempotent.
|
|
15381
|
+
// Keep this out of usage: the flag is unnecessary and has no behavior.
|
|
15161
15382
|
force: { type: "boolean", short: "f", default: false }
|
|
15162
15383
|
},
|
|
15163
15384
|
allowPositionals: false,
|
|
15164
15385
|
run: async (input) => {
|
|
15165
15386
|
await bootstrapCommand({
|
|
15166
|
-
force: getBoolean(input.values, "force"),
|
|
15167
15387
|
format: input.format
|
|
15168
15388
|
});
|
|
15169
15389
|
}
|
|
@@ -18784,17 +19004,21 @@ function buildSetupNudgeLine(input) {
|
|
|
18784
19004
|
// src/cli.ts
|
|
18785
19005
|
import { createRequire as createRequire2 } from "module";
|
|
18786
19006
|
var USAGE6 = `
|
|
18787
|
-
cnry \u2014
|
|
19007
|
+
cnry \u2014 Page Health and AI visibility CLI ('canonry' also works)
|
|
18788
19008
|
|
|
18789
19009
|
Usage: cnry <command> [options]
|
|
18790
19010
|
|
|
18791
19011
|
Setup:
|
|
18792
|
-
|
|
18793
|
-
bootstrap Bootstrap config/database from env vars
|
|
19012
|
+
bootstrap Create local config/database (no provider required)
|
|
18794
19013
|
serve Start the local server (foreground)
|
|
18795
19014
|
start / stop Start/stop as a background daemon
|
|
19015
|
+
init Optional interactive provider/OAuth provisioning
|
|
18796
19016
|
skills List or install bundled agent skills (claude/codex)
|
|
18797
19017
|
|
|
19018
|
+
Page Health:
|
|
19019
|
+
technical-aeo Crawl, score, and inspect site health evidence
|
|
19020
|
+
doctor Diagnose runtime and integration health
|
|
19021
|
+
|
|
18798
19022
|
Projects:
|
|
18799
19023
|
project Create, update, list, show, delete projects
|
|
18800
19024
|
query Add, replace, remove, list, import, generate queries
|