@ainyc/canonry 1.28.1 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/assets/{index-1U4hNkT4.js → index-D1pCtUfW.js} +79 -79
- package/assets/index.html +1 -1
- package/dist/{chunk-2WEJYH27.js → chunk-IWUQVYU3.js} +828 -50
- package/dist/cli.js +512 -50
- package/dist/index.js +1 -1
- package/package.json +10 -8
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createClient,
|
|
6
6
|
createServer,
|
|
7
7
|
effectiveDomains,
|
|
8
|
+
formatAuditFactorScore,
|
|
8
9
|
getConfigDir,
|
|
9
10
|
getConfigPath,
|
|
10
11
|
getOrCreateAnonymousId,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
setGoogleAuthConfig,
|
|
21
22
|
showFirstRunNotice,
|
|
22
23
|
trackEvent
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-IWUQVYU3.js";
|
|
24
25
|
|
|
25
26
|
// src/cli.ts
|
|
26
27
|
import { pathToFileURL } from "url";
|
|
@@ -96,9 +97,9 @@ import { parseArgs } from "util";
|
|
|
96
97
|
function commandId(spec) {
|
|
97
98
|
return spec.path.join(".");
|
|
98
99
|
}
|
|
99
|
-
function matchesPath(args,
|
|
100
|
-
if (args.length <
|
|
101
|
-
return
|
|
100
|
+
function matchesPath(args, path5) {
|
|
101
|
+
if (args.length < path5.length) return false;
|
|
102
|
+
return path5.every((segment, index) => args[index] === segment);
|
|
102
103
|
}
|
|
103
104
|
function withFormatOption(options) {
|
|
104
105
|
if (!options) {
|
|
@@ -197,9 +198,9 @@ var ApiClient = class {
|
|
|
197
198
|
}
|
|
198
199
|
return this.probePromise;
|
|
199
200
|
}
|
|
200
|
-
async request(method,
|
|
201
|
+
async request(method, path5, body) {
|
|
201
202
|
await this.probeBasePath();
|
|
202
|
-
const url = `${this.baseUrl}${
|
|
203
|
+
const url = `${this.baseUrl}${path5}`;
|
|
203
204
|
const serializedBody = body != null ? JSON.stringify(body) : void 0;
|
|
204
205
|
const headers = {
|
|
205
206
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
@@ -297,6 +298,9 @@ var ApiClient = class {
|
|
|
297
298
|
async getSettings() {
|
|
298
299
|
return this.request("GET", "/settings");
|
|
299
300
|
}
|
|
301
|
+
async createSnapshot(body) {
|
|
302
|
+
return this.request("POST", "/snapshot", body);
|
|
303
|
+
}
|
|
300
304
|
async updateProvider(name, body) {
|
|
301
305
|
return this.request("PUT", `/settings/providers/${encodeURIComponent(name)}`, body);
|
|
302
306
|
}
|
|
@@ -1275,9 +1279,9 @@ async function gaConnect(project, opts) {
|
|
|
1275
1279
|
propertyId: opts.propertyId
|
|
1276
1280
|
};
|
|
1277
1281
|
if (opts.keyFile) {
|
|
1278
|
-
const
|
|
1282
|
+
const fs7 = await import("fs");
|
|
1279
1283
|
try {
|
|
1280
|
-
const content =
|
|
1284
|
+
const content = fs7.readFileSync(opts.keyFile, "utf-8");
|
|
1281
1285
|
JSON.parse(content);
|
|
1282
1286
|
body.keyJson = content;
|
|
1283
1287
|
} catch (e) {
|
|
@@ -4212,9 +4216,462 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
|
|
|
4212
4216
|
}
|
|
4213
4217
|
];
|
|
4214
4218
|
|
|
4219
|
+
// src/snapshot-pdf.ts
|
|
4220
|
+
import fs3 from "fs";
|
|
4221
|
+
import path from "path";
|
|
4222
|
+
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
|
|
4223
|
+
var PAGE_WIDTH = 612;
|
|
4224
|
+
var PAGE_HEIGHT = 792;
|
|
4225
|
+
var MARGIN = 48;
|
|
4226
|
+
var BRAND = rgb(0.58, 0, 0);
|
|
4227
|
+
var INK = rgb(0.1, 0.1, 0.1);
|
|
4228
|
+
var MUTED = rgb(0.38, 0.38, 0.38);
|
|
4229
|
+
var LINE = rgb(0.82, 0.8, 0.76);
|
|
4230
|
+
var PASS = rgb(0.18, 0.49, 0.31);
|
|
4231
|
+
var CAUTION = rgb(0.72, 0.45, 0.2);
|
|
4232
|
+
var FAIL = rgb(0.7, 0.15, 0.15);
|
|
4233
|
+
var PdfWriter = class {
|
|
4234
|
+
constructor(doc, regular, bold) {
|
|
4235
|
+
this.doc = doc;
|
|
4236
|
+
this.regular = regular;
|
|
4237
|
+
this.bold = bold;
|
|
4238
|
+
this.addPage();
|
|
4239
|
+
}
|
|
4240
|
+
usableWidth = PAGE_WIDTH - MARGIN * 2;
|
|
4241
|
+
page;
|
|
4242
|
+
y = 0;
|
|
4243
|
+
addPage() {
|
|
4244
|
+
this.page = this.doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
4245
|
+
this.y = PAGE_HEIGHT - MARGIN;
|
|
4246
|
+
}
|
|
4247
|
+
ensureSpace(height) {
|
|
4248
|
+
if (this.y - height < MARGIN) {
|
|
4249
|
+
this.addPage();
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
heading(text, size = 18) {
|
|
4253
|
+
this.ensureSpace(size + 12);
|
|
4254
|
+
this.page.drawText(text, {
|
|
4255
|
+
x: MARGIN,
|
|
4256
|
+
y: this.y,
|
|
4257
|
+
size,
|
|
4258
|
+
font: this.bold,
|
|
4259
|
+
color: BRAND
|
|
4260
|
+
});
|
|
4261
|
+
this.y -= size + 8;
|
|
4262
|
+
}
|
|
4263
|
+
subheading(text, size = 12) {
|
|
4264
|
+
this.ensureSpace(size + 8);
|
|
4265
|
+
this.page.drawText(text, {
|
|
4266
|
+
x: MARGIN,
|
|
4267
|
+
y: this.y,
|
|
4268
|
+
size,
|
|
4269
|
+
font: this.bold,
|
|
4270
|
+
color: INK
|
|
4271
|
+
});
|
|
4272
|
+
this.y -= size + 6;
|
|
4273
|
+
}
|
|
4274
|
+
paragraph(text, opts) {
|
|
4275
|
+
const size = opts?.size ?? 10;
|
|
4276
|
+
const color = opts?.color ?? INK;
|
|
4277
|
+
const lineHeight = opts?.lineHeight ?? size + 4;
|
|
4278
|
+
const lines = wrapText(this.regular, text, size, this.usableWidth);
|
|
4279
|
+
this.ensureSpace(lines.length * lineHeight + 4);
|
|
4280
|
+
for (const line of lines) {
|
|
4281
|
+
this.page.drawText(line, {
|
|
4282
|
+
x: MARGIN,
|
|
4283
|
+
y: this.y,
|
|
4284
|
+
size,
|
|
4285
|
+
font: this.regular,
|
|
4286
|
+
color
|
|
4287
|
+
});
|
|
4288
|
+
this.y -= lineHeight;
|
|
4289
|
+
}
|
|
4290
|
+
this.y -= 2;
|
|
4291
|
+
}
|
|
4292
|
+
bullet(text) {
|
|
4293
|
+
const lines = wrapText(this.regular, text, 10, this.usableWidth - 14);
|
|
4294
|
+
this.ensureSpace(lines.length * 14 + 2);
|
|
4295
|
+
this.page.drawText("-", {
|
|
4296
|
+
x: MARGIN,
|
|
4297
|
+
y: this.y,
|
|
4298
|
+
size: 10,
|
|
4299
|
+
font: this.bold,
|
|
4300
|
+
color: BRAND
|
|
4301
|
+
});
|
|
4302
|
+
let first = true;
|
|
4303
|
+
for (const line of lines) {
|
|
4304
|
+
this.page.drawText(line, {
|
|
4305
|
+
x: MARGIN + 14,
|
|
4306
|
+
y: this.y,
|
|
4307
|
+
size: 10,
|
|
4308
|
+
font: this.regular,
|
|
4309
|
+
color: INK
|
|
4310
|
+
});
|
|
4311
|
+
this.y -= 14;
|
|
4312
|
+
if (first) first = false;
|
|
4313
|
+
}
|
|
4314
|
+
this.y -= 2;
|
|
4315
|
+
}
|
|
4316
|
+
rule() {
|
|
4317
|
+
this.ensureSpace(8);
|
|
4318
|
+
this.page.drawLine({
|
|
4319
|
+
start: { x: MARGIN, y: this.y },
|
|
4320
|
+
end: { x: PAGE_WIDTH - MARGIN, y: this.y },
|
|
4321
|
+
thickness: 1,
|
|
4322
|
+
color: LINE
|
|
4323
|
+
});
|
|
4324
|
+
this.y -= 10;
|
|
4325
|
+
}
|
|
4326
|
+
keyValue(label, value) {
|
|
4327
|
+
const size = 10;
|
|
4328
|
+
const labelWidth = this.bold.widthOfTextAtSize(`${label}: `, size);
|
|
4329
|
+
this.ensureSpace(16);
|
|
4330
|
+
this.page.drawText(`${label}:`, {
|
|
4331
|
+
x: MARGIN,
|
|
4332
|
+
y: this.y,
|
|
4333
|
+
size,
|
|
4334
|
+
font: this.bold,
|
|
4335
|
+
color: INK
|
|
4336
|
+
});
|
|
4337
|
+
const lines = wrapText(this.regular, value, size, this.usableWidth - labelWidth - 4);
|
|
4338
|
+
let currentY = this.y;
|
|
4339
|
+
for (const line of lines) {
|
|
4340
|
+
this.page.drawText(line, {
|
|
4341
|
+
x: MARGIN + labelWidth + 4,
|
|
4342
|
+
y: currentY,
|
|
4343
|
+
size,
|
|
4344
|
+
font: this.regular,
|
|
4345
|
+
color: INK
|
|
4346
|
+
});
|
|
4347
|
+
currentY -= 14;
|
|
4348
|
+
}
|
|
4349
|
+
this.y = currentY - 2;
|
|
4350
|
+
}
|
|
4351
|
+
table(headers, rows, widths) {
|
|
4352
|
+
const columnWidths = widths ?? headers.map(() => this.usableWidth / headers.length);
|
|
4353
|
+
const headerHeight = 20;
|
|
4354
|
+
this.ensureSpace(headerHeight + 10);
|
|
4355
|
+
let x = MARGIN;
|
|
4356
|
+
for (let i = 0; i < headers.length; i++) {
|
|
4357
|
+
const width = columnWidths[i];
|
|
4358
|
+
this.page.drawRectangle({
|
|
4359
|
+
x,
|
|
4360
|
+
y: this.y - headerHeight + 4,
|
|
4361
|
+
width,
|
|
4362
|
+
height: headerHeight,
|
|
4363
|
+
color: BRAND
|
|
4364
|
+
});
|
|
4365
|
+
const lines = wrapText(this.bold, headers[i], 9, width - 8);
|
|
4366
|
+
let lineY = this.y - 10;
|
|
4367
|
+
for (const line of lines) {
|
|
4368
|
+
this.page.drawText(line, {
|
|
4369
|
+
x: x + 4,
|
|
4370
|
+
y: lineY,
|
|
4371
|
+
size: 9,
|
|
4372
|
+
font: this.bold,
|
|
4373
|
+
color: rgb(1, 0.98, 0.93)
|
|
4374
|
+
});
|
|
4375
|
+
lineY -= 10;
|
|
4376
|
+
}
|
|
4377
|
+
x += width;
|
|
4378
|
+
}
|
|
4379
|
+
this.y -= headerHeight + 4;
|
|
4380
|
+
for (const row of rows) {
|
|
4381
|
+
const lineCounts = row.map((cell, index) => wrapText(this.regular, cell, 9, columnWidths[index] - 8).length);
|
|
4382
|
+
const rowHeight = Math.max(18, Math.max(...lineCounts) * 11 + 6);
|
|
4383
|
+
this.ensureSpace(rowHeight + 4);
|
|
4384
|
+
let cellX = MARGIN;
|
|
4385
|
+
for (let i = 0; i < row.length; i++) {
|
|
4386
|
+
const width = columnWidths[i];
|
|
4387
|
+
this.page.drawRectangle({
|
|
4388
|
+
x: cellX,
|
|
4389
|
+
y: this.y - rowHeight + 4,
|
|
4390
|
+
width,
|
|
4391
|
+
height: rowHeight,
|
|
4392
|
+
borderColor: LINE,
|
|
4393
|
+
borderWidth: 0.5
|
|
4394
|
+
});
|
|
4395
|
+
const lines = wrapText(this.regular, row[i], 9, width - 8);
|
|
4396
|
+
let lineY = this.y - 10;
|
|
4397
|
+
for (const line of lines) {
|
|
4398
|
+
this.page.drawText(line, {
|
|
4399
|
+
x: cellX + 4,
|
|
4400
|
+
y: lineY,
|
|
4401
|
+
size: 9,
|
|
4402
|
+
font: this.regular,
|
|
4403
|
+
color: INK
|
|
4404
|
+
});
|
|
4405
|
+
lineY -= 11;
|
|
4406
|
+
}
|
|
4407
|
+
cellX += width;
|
|
4408
|
+
}
|
|
4409
|
+
this.y -= rowHeight + 2;
|
|
4410
|
+
}
|
|
4411
|
+
this.y -= 4;
|
|
4412
|
+
}
|
|
4413
|
+
};
|
|
4414
|
+
async function writeSnapshotPdf(report, outputPath) {
|
|
4415
|
+
const doc = await PDFDocument.create();
|
|
4416
|
+
doc.setTitle(`${report.companyName} AI Perception Snapshot`);
|
|
4417
|
+
doc.setAuthor("Canonry");
|
|
4418
|
+
doc.setSubject("AEO snapshot report");
|
|
4419
|
+
doc.setProducer("Canonry");
|
|
4420
|
+
doc.setCreator("Canonry");
|
|
4421
|
+
const regular = await doc.embedFont(StandardFonts.Helvetica);
|
|
4422
|
+
const bold = await doc.embedFont(StandardFonts.HelveticaBold);
|
|
4423
|
+
const pdf = new PdfWriter(doc, regular, bold);
|
|
4424
|
+
renderCover(pdf, report);
|
|
4425
|
+
renderSummary(pdf, report);
|
|
4426
|
+
renderAudit(pdf, report);
|
|
4427
|
+
renderCompetitors(pdf, report);
|
|
4428
|
+
renderQueries(pdf, report);
|
|
4429
|
+
const bytes = await doc.save();
|
|
4430
|
+
const resolvedPath = path.resolve(outputPath);
|
|
4431
|
+
fs3.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
4432
|
+
fs3.writeFileSync(resolvedPath, bytes);
|
|
4433
|
+
return resolvedPath;
|
|
4434
|
+
}
|
|
4435
|
+
function renderCover(pdf, report) {
|
|
4436
|
+
pdf.heading("AI Perception Snapshot", 24);
|
|
4437
|
+
pdf.paragraph(report.companyName, { size: 15, color: INK, lineHeight: 18 });
|
|
4438
|
+
pdf.paragraph(report.domain, { size: 11, color: MUTED, lineHeight: 14 });
|
|
4439
|
+
pdf.rule();
|
|
4440
|
+
pdf.keyValue("Generated", new Date(report.generatedAt).toLocaleString("en-US", {
|
|
4441
|
+
year: "numeric",
|
|
4442
|
+
month: "long",
|
|
4443
|
+
day: "numeric",
|
|
4444
|
+
hour: "numeric",
|
|
4445
|
+
minute: "2-digit"
|
|
4446
|
+
}));
|
|
4447
|
+
pdf.keyValue("AEO Audit", `${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
|
|
4448
|
+
pdf.keyValue("Visibility Gap", report.summary.visibilityGap);
|
|
4449
|
+
pdf.paragraph(report.profile.summary, { size: 11, color: INK, lineHeight: 16 });
|
|
4450
|
+
pdf.rule();
|
|
4451
|
+
}
|
|
4452
|
+
function renderSummary(pdf, report) {
|
|
4453
|
+
pdf.heading("What This Means");
|
|
4454
|
+
for (const line of report.summary.whatThisMeans) {
|
|
4455
|
+
pdf.bullet(line);
|
|
4456
|
+
}
|
|
4457
|
+
pdf.subheading("Recommended Actions");
|
|
4458
|
+
for (const action of report.summary.recommendedActions) {
|
|
4459
|
+
pdf.bullet(action);
|
|
4460
|
+
}
|
|
4461
|
+
pdf.rule();
|
|
4462
|
+
}
|
|
4463
|
+
function renderAudit(pdf, report) {
|
|
4464
|
+
pdf.heading("Audit Snapshot");
|
|
4465
|
+
pdf.paragraph(report.audit.summary, { size: 10, color: MUTED, lineHeight: 14 });
|
|
4466
|
+
const factorRows = [...report.audit.factors].sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)).slice(0, 5).map((factor) => [
|
|
4467
|
+
factor.name,
|
|
4468
|
+
formatAuditFactorScore(factor),
|
|
4469
|
+
factor.status
|
|
4470
|
+
]);
|
|
4471
|
+
if (factorRows.length > 0) {
|
|
4472
|
+
pdf.table(["Weakest factor", "Score / Weight", "Status"], factorRows, [270, 120, 126]);
|
|
4473
|
+
}
|
|
4474
|
+
pdf.rule();
|
|
4475
|
+
}
|
|
4476
|
+
function renderCompetitors(pdf, report) {
|
|
4477
|
+
pdf.heading("Recommended Instead");
|
|
4478
|
+
if (report.summary.topCompetitors.length === 0) {
|
|
4479
|
+
pdf.paragraph("No clear competitor cluster was extracted from the responses.", {
|
|
4480
|
+
size: 10,
|
|
4481
|
+
color: MUTED
|
|
4482
|
+
});
|
|
4483
|
+
pdf.rule();
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
pdf.table(
|
|
4487
|
+
["Competitor", "Mentions"],
|
|
4488
|
+
report.summary.topCompetitors.map((entry) => [entry.name, String(entry.count)]),
|
|
4489
|
+
[420, 96]
|
|
4490
|
+
);
|
|
4491
|
+
pdf.rule();
|
|
4492
|
+
}
|
|
4493
|
+
function renderQueries(pdf, report) {
|
|
4494
|
+
pdf.heading("Provider Comparison");
|
|
4495
|
+
for (const query of report.queryResults) {
|
|
4496
|
+
pdf.subheading(query.phrase, 11);
|
|
4497
|
+
for (const result of query.providerResults) {
|
|
4498
|
+
const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
|
|
4499
|
+
const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
|
|
4500
|
+
const competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
|
|
4501
|
+
const line = `${result.displayName}: ${status}${accuracy}${competitors}`;
|
|
4502
|
+
pdf.bullet(line);
|
|
4503
|
+
if (result.error) {
|
|
4504
|
+
pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
|
|
4505
|
+
} else if (result.accuracyNotes) {
|
|
4506
|
+
const color = result.describedAccurately === "yes" ? PASS : result.describedAccurately === "no" ? FAIL : CAUTION;
|
|
4507
|
+
pdf.paragraph(result.accuracyNotes, { size: 9, color, lineHeight: 12 });
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
pdf.rule();
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
function wrapText(font, text, size, maxWidth) {
|
|
4514
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
4515
|
+
if (!normalized) return [""];
|
|
4516
|
+
const words = normalized.split(" ");
|
|
4517
|
+
const lines = [];
|
|
4518
|
+
let current = "";
|
|
4519
|
+
for (const word of words) {
|
|
4520
|
+
const next = current ? `${current} ${word}` : word;
|
|
4521
|
+
if (font.widthOfTextAtSize(next, size) <= maxWidth) {
|
|
4522
|
+
current = next;
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if (current) {
|
|
4526
|
+
lines.push(current);
|
|
4527
|
+
current = word;
|
|
4528
|
+
continue;
|
|
4529
|
+
}
|
|
4530
|
+
let chunk = "";
|
|
4531
|
+
for (const char of word) {
|
|
4532
|
+
const candidate = `${chunk}${char}`;
|
|
4533
|
+
if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
|
|
4534
|
+
chunk = candidate;
|
|
4535
|
+
} else {
|
|
4536
|
+
if (chunk) lines.push(chunk);
|
|
4537
|
+
chunk = char;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
current = chunk;
|
|
4541
|
+
}
|
|
4542
|
+
if (current) lines.push(current);
|
|
4543
|
+
return lines;
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
// src/commands/snapshot.ts
|
|
4547
|
+
function getClient16() {
|
|
4548
|
+
return createApiClient();
|
|
4549
|
+
}
|
|
4550
|
+
async function createSnapshotReport(companyName, opts) {
|
|
4551
|
+
const client = getClient16();
|
|
4552
|
+
const report = await client.createSnapshot({
|
|
4553
|
+
companyName,
|
|
4554
|
+
domain: opts.domain,
|
|
4555
|
+
...opts.phrases && opts.phrases.length > 0 ? { phrases: opts.phrases } : {},
|
|
4556
|
+
...opts.competitors && opts.competitors.length > 0 ? { competitors: opts.competitors } : {}
|
|
4557
|
+
});
|
|
4558
|
+
let savedPdfPath;
|
|
4559
|
+
if (opts.pdf) {
|
|
4560
|
+
savedPdfPath = await writeSnapshotPdf(report, opts.pdf);
|
|
4561
|
+
}
|
|
4562
|
+
if (opts.format === "json") {
|
|
4563
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4564
|
+
if (savedPdfPath) {
|
|
4565
|
+
process.stderr.write(`Saved PDF: ${savedPdfPath}
|
|
4566
|
+
`);
|
|
4567
|
+
}
|
|
4568
|
+
return;
|
|
4569
|
+
}
|
|
4570
|
+
console.log(formatSnapshotText(report));
|
|
4571
|
+
if (savedPdfPath) {
|
|
4572
|
+
console.log(`
|
|
4573
|
+
PDF saved: ${savedPdfPath}`);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
function formatSnapshotText(report) {
|
|
4577
|
+
const lines = [];
|
|
4578
|
+
lines.push(`Snapshot: ${report.companyName} (${report.domain})`);
|
|
4579
|
+
lines.push(`AEO audit: ${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
|
|
4580
|
+
lines.push(report.summary.visibilityGap);
|
|
4581
|
+
lines.push("");
|
|
4582
|
+
if (report.summary.topCompetitors.length > 0) {
|
|
4583
|
+
lines.push(
|
|
4584
|
+
`Top competitors AI recommended instead: ${report.summary.topCompetitors.map((entry) => `${entry.name} (${entry.count})`).join(", ")}`
|
|
4585
|
+
);
|
|
4586
|
+
lines.push("");
|
|
4587
|
+
}
|
|
4588
|
+
if (report.summary.whatThisMeans.length > 0) {
|
|
4589
|
+
lines.push("What this means:");
|
|
4590
|
+
for (const item of report.summary.whatThisMeans) {
|
|
4591
|
+
lines.push(` - ${item}`);
|
|
4592
|
+
}
|
|
4593
|
+
lines.push("");
|
|
4594
|
+
}
|
|
4595
|
+
const providerWidth = Math.max(
|
|
4596
|
+
8,
|
|
4597
|
+
...report.queryResults.flatMap((query) => query.providerResults.map((result) => result.displayName.length))
|
|
4598
|
+
);
|
|
4599
|
+
for (const query of report.queryResults) {
|
|
4600
|
+
lines.push(`"${query.phrase}"`);
|
|
4601
|
+
for (const result of query.providerResults) {
|
|
4602
|
+
lines.push(` ${result.displayName.padEnd(providerWidth)} ${formatProviderLine(result)}`);
|
|
4603
|
+
}
|
|
4604
|
+
lines.push("");
|
|
4605
|
+
}
|
|
4606
|
+
if (report.summary.recommendedActions.length > 0) {
|
|
4607
|
+
lines.push("Recommended actions:");
|
|
4608
|
+
for (const action of report.summary.recommendedActions) {
|
|
4609
|
+
lines.push(` - ${action}`);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
return lines.join("\n").trimEnd();
|
|
4613
|
+
}
|
|
4614
|
+
function formatProviderLine(result) {
|
|
4615
|
+
if (result.error) {
|
|
4616
|
+
return `ERROR: ${result.error}`;
|
|
4617
|
+
}
|
|
4618
|
+
const bits = [];
|
|
4619
|
+
bits.push(result.mentioned ? "YES mentioned" : "NO mention");
|
|
4620
|
+
if (result.cited) bits.push("cited");
|
|
4621
|
+
if (result.describedAccurately !== "not-mentioned") {
|
|
4622
|
+
bits.push(`accuracy=${result.describedAccurately}`);
|
|
4623
|
+
}
|
|
4624
|
+
if (result.recommendedCompetitors.length > 0) {
|
|
4625
|
+
bits.push(`recommended instead: ${result.recommendedCompetitors.join(", ")}`);
|
|
4626
|
+
}
|
|
4627
|
+
if (result.incorrectClaims.length > 0) {
|
|
4628
|
+
bits.push(`incorrect: ${result.incorrectClaims.join("; ")}`);
|
|
4629
|
+
}
|
|
4630
|
+
return bits.join(" | ");
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
// src/cli-commands/snapshot.ts
|
|
4634
|
+
function parseCsvOption(value) {
|
|
4635
|
+
if (!value) return void 0;
|
|
4636
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
4637
|
+
return parts.length > 0 ? [...new Set(parts)] : void 0;
|
|
4638
|
+
}
|
|
4639
|
+
var SNAPSHOT_CLI_COMMANDS = [
|
|
4640
|
+
{
|
|
4641
|
+
path: ["snapshot"],
|
|
4642
|
+
usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]',
|
|
4643
|
+
options: {
|
|
4644
|
+
domain: stringOption(),
|
|
4645
|
+
phrases: stringOption(),
|
|
4646
|
+
competitors: stringOption(),
|
|
4647
|
+
pdf: stringOption()
|
|
4648
|
+
},
|
|
4649
|
+
run: async (input) => {
|
|
4650
|
+
const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]';
|
|
4651
|
+
const companyName = requirePositional(input, 0, {
|
|
4652
|
+
command: "snapshot",
|
|
4653
|
+
usage,
|
|
4654
|
+
message: "company name is required"
|
|
4655
|
+
});
|
|
4656
|
+
const domain = requireStringOption(input, "domain", {
|
|
4657
|
+
command: "snapshot",
|
|
4658
|
+
usage,
|
|
4659
|
+
message: "--domain is required"
|
|
4660
|
+
});
|
|
4661
|
+
await createSnapshotReport(companyName, {
|
|
4662
|
+
domain,
|
|
4663
|
+
phrases: parseCsvOption(getString(input.values, "phrases")),
|
|
4664
|
+
competitors: parseCsvOption(getString(input.values, "competitors")),
|
|
4665
|
+
pdf: getString(input.values, "pdf"),
|
|
4666
|
+
format: input.format
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
];
|
|
4671
|
+
|
|
4215
4672
|
// src/commands/bootstrap.ts
|
|
4216
4673
|
import crypto from "crypto";
|
|
4217
|
-
import
|
|
4674
|
+
import path2 from "path";
|
|
4218
4675
|
import { eq } from "drizzle-orm";
|
|
4219
4676
|
|
|
4220
4677
|
// ../config/src/index.ts
|
|
@@ -4360,7 +4817,7 @@ async function bootstrapCommand(_opts) {
|
|
|
4360
4817
|
);
|
|
4361
4818
|
}
|
|
4362
4819
|
const configDir = getConfigDir();
|
|
4363
|
-
const databasePath = env.databasePath ||
|
|
4820
|
+
const databasePath = env.databasePath || path2.join(configDir, "data.db");
|
|
4364
4821
|
const existing = configExists();
|
|
4365
4822
|
const existingConfig = existing ? loadConfig() : void 0;
|
|
4366
4823
|
let rawApiKey;
|
|
@@ -4428,10 +4885,10 @@ async function bootstrapCommand(_opts) {
|
|
|
4428
4885
|
|
|
4429
4886
|
// src/commands/daemon.ts
|
|
4430
4887
|
import { spawn } from "child_process";
|
|
4431
|
-
import
|
|
4432
|
-
import
|
|
4888
|
+
import fs4 from "fs";
|
|
4889
|
+
import path3 from "path";
|
|
4433
4890
|
function getPidPath() {
|
|
4434
|
-
return
|
|
4891
|
+
return path3.join(getConfigDir(), "canonry.pid");
|
|
4435
4892
|
}
|
|
4436
4893
|
function isProcessAlive(pid) {
|
|
4437
4894
|
try {
|
|
@@ -4458,8 +4915,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
|
|
|
4458
4915
|
async function startDaemon(opts) {
|
|
4459
4916
|
const pidPath = getPidPath();
|
|
4460
4917
|
const format = opts.format ?? "text";
|
|
4461
|
-
if (
|
|
4462
|
-
const existingPid = parseInt(
|
|
4918
|
+
if (fs4.existsSync(pidPath)) {
|
|
4919
|
+
const existingPid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
4463
4920
|
if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
|
|
4464
4921
|
throw new CliError({
|
|
4465
4922
|
code: "DAEMON_ALREADY_RUNNING",
|
|
@@ -4470,9 +4927,9 @@ async function startDaemon(opts) {
|
|
|
4470
4927
|
}
|
|
4471
4928
|
});
|
|
4472
4929
|
}
|
|
4473
|
-
|
|
4930
|
+
fs4.unlinkSync(pidPath);
|
|
4474
4931
|
}
|
|
4475
|
-
const cliPath =
|
|
4932
|
+
const cliPath = path3.resolve(new URL(import.meta.url).pathname);
|
|
4476
4933
|
const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
|
|
4477
4934
|
const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
|
|
4478
4935
|
if (opts.port) args.push("--port", opts.port);
|
|
@@ -4491,10 +4948,10 @@ async function startDaemon(opts) {
|
|
|
4491
4948
|
});
|
|
4492
4949
|
}
|
|
4493
4950
|
const configDir = getConfigDir();
|
|
4494
|
-
if (!
|
|
4495
|
-
|
|
4951
|
+
if (!fs4.existsSync(configDir)) {
|
|
4952
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
4496
4953
|
}
|
|
4497
|
-
|
|
4954
|
+
fs4.writeFileSync(pidPath, String(child.pid), "utf-8");
|
|
4498
4955
|
const port = opts.port ?? "4100";
|
|
4499
4956
|
const host = opts.host ?? "127.0.0.1";
|
|
4500
4957
|
if (format !== "json") {
|
|
@@ -4503,7 +4960,7 @@ async function startDaemon(opts) {
|
|
|
4503
4960
|
const ready = await waitForReady(host, port);
|
|
4504
4961
|
if (!ready) {
|
|
4505
4962
|
try {
|
|
4506
|
-
|
|
4963
|
+
fs4.unlinkSync(pidPath);
|
|
4507
4964
|
} catch {
|
|
4508
4965
|
}
|
|
4509
4966
|
throw new CliError({
|
|
@@ -4535,7 +4992,7 @@ async function startDaemon(opts) {
|
|
|
4535
4992
|
}
|
|
4536
4993
|
function stopDaemon(format = "text") {
|
|
4537
4994
|
const pidPath = getPidPath();
|
|
4538
|
-
if (!
|
|
4995
|
+
if (!fs4.existsSync(pidPath)) {
|
|
4539
4996
|
if (format === "json") {
|
|
4540
4997
|
console.log(JSON.stringify({
|
|
4541
4998
|
stopped: false,
|
|
@@ -4546,7 +5003,7 @@ function stopDaemon(format = "text") {
|
|
|
4546
5003
|
console.log("Canonry is not running (no PID file found)");
|
|
4547
5004
|
return;
|
|
4548
5005
|
}
|
|
4549
|
-
const pid = parseInt(
|
|
5006
|
+
const pid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
4550
5007
|
if (isNaN(pid)) {
|
|
4551
5008
|
if (format === "json") {
|
|
4552
5009
|
console.log(JSON.stringify({
|
|
@@ -4557,7 +5014,7 @@ function stopDaemon(format = "text") {
|
|
|
4557
5014
|
} else {
|
|
4558
5015
|
console.error("Invalid PID file. Removing it.");
|
|
4559
5016
|
}
|
|
4560
|
-
|
|
5017
|
+
fs4.unlinkSync(pidPath);
|
|
4561
5018
|
return;
|
|
4562
5019
|
}
|
|
4563
5020
|
if (!isProcessAlive(pid)) {
|
|
@@ -4571,12 +5028,12 @@ function stopDaemon(format = "text") {
|
|
|
4571
5028
|
} else {
|
|
4572
5029
|
console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
|
|
4573
5030
|
}
|
|
4574
|
-
|
|
5031
|
+
fs4.unlinkSync(pidPath);
|
|
4575
5032
|
return;
|
|
4576
5033
|
}
|
|
4577
5034
|
try {
|
|
4578
5035
|
process.kill(pid, "SIGTERM");
|
|
4579
|
-
|
|
5036
|
+
fs4.unlinkSync(pidPath);
|
|
4580
5037
|
if (format === "json") {
|
|
4581
5038
|
console.log(JSON.stringify({
|
|
4582
5039
|
stopped: true,
|
|
@@ -4600,9 +5057,9 @@ function stopDaemon(format = "text") {
|
|
|
4600
5057
|
|
|
4601
5058
|
// src/commands/init.ts
|
|
4602
5059
|
import crypto2 from "crypto";
|
|
4603
|
-
import
|
|
5060
|
+
import fs5 from "fs";
|
|
4604
5061
|
import readline from "readline";
|
|
4605
|
-
import
|
|
5062
|
+
import path4 from "path";
|
|
4606
5063
|
function prompt(question) {
|
|
4607
5064
|
const rl = readline.createInterface({
|
|
4608
5065
|
input: process.stdin,
|
|
@@ -4639,8 +5096,8 @@ async function initCommand(opts) {
|
|
|
4639
5096
|
return;
|
|
4640
5097
|
}
|
|
4641
5098
|
const configDir = getConfigDir();
|
|
4642
|
-
if (!
|
|
4643
|
-
|
|
5099
|
+
if (!fs5.existsSync(configDir)) {
|
|
5100
|
+
fs5.mkdirSync(configDir, { recursive: true });
|
|
4644
5101
|
}
|
|
4645
5102
|
const bootstrapEnv = getBootstrapEnv(process.env, {
|
|
4646
5103
|
GEMINI_API_KEY: opts?.geminiKey,
|
|
@@ -4755,7 +5212,7 @@ async function initCommand(opts) {
|
|
|
4755
5212
|
const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
|
|
4756
5213
|
const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
|
|
4757
5214
|
const keyPrefix = rawApiKey.slice(0, 9);
|
|
4758
|
-
const databasePath =
|
|
5215
|
+
const databasePath = path4.join(configDir, "data.db");
|
|
4759
5216
|
const db = createClient(databasePath);
|
|
4760
5217
|
migrate(db);
|
|
4761
5218
|
db.insert(apiKeys).values({
|
|
@@ -5108,10 +5565,10 @@ var SYSTEM_CLI_COMMANDS = [
|
|
|
5108
5565
|
];
|
|
5109
5566
|
|
|
5110
5567
|
// src/cli-commands/wordpress.ts
|
|
5111
|
-
import
|
|
5568
|
+
import fs6 from "fs";
|
|
5112
5569
|
|
|
5113
5570
|
// src/commands/wordpress.ts
|
|
5114
|
-
function
|
|
5571
|
+
function getClient17() {
|
|
5115
5572
|
return createApiClient();
|
|
5116
5573
|
}
|
|
5117
5574
|
async function promptForAppPassword() {
|
|
@@ -5268,7 +5725,7 @@ async function wordpressConnect(project, opts) {
|
|
|
5268
5725
|
details: { project }
|
|
5269
5726
|
});
|
|
5270
5727
|
}
|
|
5271
|
-
const client =
|
|
5728
|
+
const client = getClient17();
|
|
5272
5729
|
const result = await client.wordpressConnect(project, {
|
|
5273
5730
|
url: opts.url,
|
|
5274
5731
|
stagingUrl: opts.stagingUrl,
|
|
@@ -5285,7 +5742,7 @@ async function wordpressConnect(project, opts) {
|
|
|
5285
5742
|
printWordpressStatus(project, result);
|
|
5286
5743
|
}
|
|
5287
5744
|
async function wordpressDisconnect(project, format) {
|
|
5288
|
-
const client =
|
|
5745
|
+
const client = getClient17();
|
|
5289
5746
|
await client.wordpressDisconnect(project);
|
|
5290
5747
|
if (format === "json") {
|
|
5291
5748
|
printJson({ project, disconnected: true });
|
|
@@ -5294,7 +5751,7 @@ async function wordpressDisconnect(project, format) {
|
|
|
5294
5751
|
console.log(`WordPress disconnected from project "${project}".`);
|
|
5295
5752
|
}
|
|
5296
5753
|
async function wordpressStatus(project, format) {
|
|
5297
|
-
const client =
|
|
5754
|
+
const client = getClient17();
|
|
5298
5755
|
const result = await client.wordpressStatus(project);
|
|
5299
5756
|
if (format === "json") {
|
|
5300
5757
|
printJson(result);
|
|
@@ -5303,7 +5760,7 @@ async function wordpressStatus(project, format) {
|
|
|
5303
5760
|
printWordpressStatus(project, result);
|
|
5304
5761
|
}
|
|
5305
5762
|
async function wordpressPages(project, opts) {
|
|
5306
|
-
const client =
|
|
5763
|
+
const client = getClient17();
|
|
5307
5764
|
const result = await client.wordpressPages(project, opts.env);
|
|
5308
5765
|
if (opts.format === "json") {
|
|
5309
5766
|
printJson(result);
|
|
@@ -5312,7 +5769,7 @@ async function wordpressPages(project, opts) {
|
|
|
5312
5769
|
printPages(project, result.env, result.pages);
|
|
5313
5770
|
}
|
|
5314
5771
|
async function wordpressPage(project, slug, opts) {
|
|
5315
|
-
const client =
|
|
5772
|
+
const client = getClient17();
|
|
5316
5773
|
const result = await client.wordpressPage(project, slug, opts.env);
|
|
5317
5774
|
if (opts.format === "json") {
|
|
5318
5775
|
printJson(result);
|
|
@@ -5321,7 +5778,7 @@ async function wordpressPage(project, slug, opts) {
|
|
|
5321
5778
|
printPageDetail(result);
|
|
5322
5779
|
}
|
|
5323
5780
|
async function wordpressCreatePage(project, body) {
|
|
5324
|
-
const client =
|
|
5781
|
+
const client = getClient17();
|
|
5325
5782
|
const result = await client.wordpressCreatePage(project, body);
|
|
5326
5783
|
if (body.format === "json") {
|
|
5327
5784
|
printJson(result);
|
|
@@ -5332,7 +5789,7 @@ async function wordpressCreatePage(project, body) {
|
|
|
5332
5789
|
printPageDetail(result);
|
|
5333
5790
|
}
|
|
5334
5791
|
async function wordpressUpdatePage(project, body) {
|
|
5335
|
-
const client =
|
|
5792
|
+
const client = getClient17();
|
|
5336
5793
|
const result = await client.wordpressUpdatePage(project, body);
|
|
5337
5794
|
if (body.format === "json") {
|
|
5338
5795
|
printJson(result);
|
|
@@ -5343,7 +5800,7 @@ async function wordpressUpdatePage(project, body) {
|
|
|
5343
5800
|
printPageDetail(result);
|
|
5344
5801
|
}
|
|
5345
5802
|
async function wordpressSetMeta(project, body) {
|
|
5346
|
-
const client =
|
|
5803
|
+
const client = getClient17();
|
|
5347
5804
|
const result = await client.wordpressSetMeta(project, body);
|
|
5348
5805
|
if (body.format === "json") {
|
|
5349
5806
|
printJson(result);
|
|
@@ -5354,7 +5811,7 @@ async function wordpressSetMeta(project, body) {
|
|
|
5354
5811
|
printPageDetail(result);
|
|
5355
5812
|
}
|
|
5356
5813
|
async function wordpressSchema(project, slug, opts) {
|
|
5357
|
-
const client =
|
|
5814
|
+
const client = getClient17();
|
|
5358
5815
|
const result = await client.wordpressSchema(project, slug, opts.env);
|
|
5359
5816
|
if (opts.format === "json") {
|
|
5360
5817
|
printJson(result);
|
|
@@ -5365,7 +5822,7 @@ async function wordpressSchema(project, slug, opts) {
|
|
|
5365
5822
|
printSchemaBlocks(result.blocks);
|
|
5366
5823
|
}
|
|
5367
5824
|
async function wordpressSetSchema(project, body) {
|
|
5368
|
-
const client =
|
|
5825
|
+
const client = getClient17();
|
|
5369
5826
|
const result = await client.wordpressSetSchema(project, body);
|
|
5370
5827
|
if (body.format === "json") {
|
|
5371
5828
|
printJson(result);
|
|
@@ -5374,7 +5831,7 @@ async function wordpressSetSchema(project, body) {
|
|
|
5374
5831
|
printManualAssist(`Schema update for "${body.slug}"`, result);
|
|
5375
5832
|
}
|
|
5376
5833
|
async function wordpressLlmsTxt(project, opts) {
|
|
5377
|
-
const client =
|
|
5834
|
+
const client = getClient17();
|
|
5378
5835
|
const result = await client.wordpressLlmsTxt(project, opts.env);
|
|
5379
5836
|
if (opts.format === "json") {
|
|
5380
5837
|
printJson(result);
|
|
@@ -5385,7 +5842,7 @@ async function wordpressLlmsTxt(project, opts) {
|
|
|
5385
5842
|
console.log(result.content ?? "(not found)");
|
|
5386
5843
|
}
|
|
5387
5844
|
async function wordpressSetLlmsTxt(project, body) {
|
|
5388
|
-
const client =
|
|
5845
|
+
const client = getClient17();
|
|
5389
5846
|
const result = await client.wordpressSetLlmsTxt(project, body);
|
|
5390
5847
|
if (body.format === "json") {
|
|
5391
5848
|
printJson(result);
|
|
@@ -5394,7 +5851,7 @@ async function wordpressSetLlmsTxt(project, body) {
|
|
|
5394
5851
|
printManualAssist(`llms.txt update for "${project}"`, result);
|
|
5395
5852
|
}
|
|
5396
5853
|
async function wordpressAudit(project, opts) {
|
|
5397
|
-
const client =
|
|
5854
|
+
const client = getClient17();
|
|
5398
5855
|
const result = await client.wordpressAudit(project, opts.env);
|
|
5399
5856
|
if (opts.format === "json") {
|
|
5400
5857
|
printJson(result);
|
|
@@ -5408,7 +5865,7 @@ async function wordpressAudit(project, opts) {
|
|
|
5408
5865
|
printAuditIssues(result.issues);
|
|
5409
5866
|
}
|
|
5410
5867
|
async function wordpressDiff(project, slug, format) {
|
|
5411
|
-
const client =
|
|
5868
|
+
const client = getClient17();
|
|
5412
5869
|
const result = await client.wordpressDiff(project, slug);
|
|
5413
5870
|
if (format === "json") {
|
|
5414
5871
|
printJson(result);
|
|
@@ -5417,7 +5874,7 @@ async function wordpressDiff(project, slug, format) {
|
|
|
5417
5874
|
printDiff(result);
|
|
5418
5875
|
}
|
|
5419
5876
|
async function wordpressStagingStatus(project, format) {
|
|
5420
|
-
const client =
|
|
5877
|
+
const client = getClient17();
|
|
5421
5878
|
const result = await client.wordpressStagingStatus(project);
|
|
5422
5879
|
if (format === "json") {
|
|
5423
5880
|
printJson(result);
|
|
@@ -5431,7 +5888,7 @@ async function wordpressStagingStatus(project, format) {
|
|
|
5431
5888
|
console.log(` Admin URL: ${result.adminUrl}`);
|
|
5432
5889
|
}
|
|
5433
5890
|
async function wordpressStagingPush(project, format) {
|
|
5434
|
-
const client =
|
|
5891
|
+
const client = getClient17();
|
|
5435
5892
|
const result = await client.wordpressStagingPush(project);
|
|
5436
5893
|
if (format === "json") {
|
|
5437
5894
|
printJson(result);
|
|
@@ -5478,7 +5935,7 @@ function resolveContent(input, command, usage, options) {
|
|
|
5478
5935
|
}
|
|
5479
5936
|
if (contentFile) {
|
|
5480
5937
|
try {
|
|
5481
|
-
return
|
|
5938
|
+
return fs6.readFileSync(contentFile, "utf-8");
|
|
5482
5939
|
} catch (error) {
|
|
5483
5940
|
const message = error instanceof Error ? error.message : String(error);
|
|
5484
5941
|
throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
|
|
@@ -5831,6 +6288,7 @@ var REGISTERED_CLI_COMMANDS = [
|
|
|
5831
6288
|
...KEYWORD_CLI_COMMANDS,
|
|
5832
6289
|
...COMPETITOR_CLI_COMMANDS,
|
|
5833
6290
|
...SETTINGS_CLI_COMMANDS,
|
|
6291
|
+
...SNAPSHOT_CLI_COMMANDS,
|
|
5834
6292
|
...RUN_CLI_COMMANDS,
|
|
5835
6293
|
...OPERATOR_CLI_COMMANDS,
|
|
5836
6294
|
...SCHEDULE_CLI_COMMANDS,
|
|
@@ -5870,6 +6328,7 @@ Usage:
|
|
|
5870
6328
|
canonry keyword generate <project> Auto-generate key phrases (--provider, --count, --save)
|
|
5871
6329
|
canonry competitor add <project> <domain> Add competitors
|
|
5872
6330
|
canonry competitor list <project> List competitors
|
|
6331
|
+
canonry snapshot <company> --domain <domain> One-shot AI perception report
|
|
5873
6332
|
canonry run <project> Trigger a run (all providers)
|
|
5874
6333
|
canonry run <project> --provider <name> Trigger a run for a specific provider
|
|
5875
6334
|
canonry run <project> --location <label> Run with a specific location
|
|
@@ -5971,6 +6430,9 @@ Options:
|
|
|
5971
6430
|
--language <lang> Language code (default: en)
|
|
5972
6431
|
--provider <name> Provider to use (gemini, openai, claude, perplexity, local, cdp:chatgpt, or cdp for all CDP targets)
|
|
5973
6432
|
--format <fmt> Output format: text (default) or json
|
|
6433
|
+
--phrases <list> Comma-separated category queries (snapshot)
|
|
6434
|
+
--competitors <list> Comma-separated competitor hints (snapshot)
|
|
6435
|
+
--pdf <path> Write a PDF snapshot report to a file
|
|
5974
6436
|
--location <label> Run with a specific configured location
|
|
5975
6437
|
--all-locations Run for every configured location
|
|
5976
6438
|
--no-location Explicitly skip location context
|