@ainyc/canonry 1.28.2 → 1.30.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/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-GF5TI2U2.js";
24
+ } from "./chunk-LMSO32GF.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, path4) {
100
- if (args.length < path4.length) return false;
101
- return path4.every((segment, index) => args[index] === segment);
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, path4, body) {
201
+ async request(method, path5, body) {
201
202
  await this.probeBasePath();
202
- const url = `${this.baseUrl}${path4}`;
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
  }
@@ -506,6 +510,9 @@ var ApiClient = class {
506
510
  async wordpressSetMeta(project, body) {
507
511
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/page/meta`, body);
508
512
  }
513
+ async wordpressBulkSetMeta(project, body) {
514
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/pages/meta/bulk`, body);
515
+ }
509
516
  async wordpressSchema(project, slug, env) {
510
517
  const params = new URLSearchParams({ slug });
511
518
  if (env) params.set("env", env);
@@ -514,6 +521,18 @@ var ApiClient = class {
514
521
  async wordpressSetSchema(project, body) {
515
522
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/manual`, body);
516
523
  }
524
+ async wordpressSchemaDeploy(project, body) {
525
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/deploy`, body);
526
+ }
527
+ async wordpressSchemaStatus(project, env) {
528
+ const params = new URLSearchParams();
529
+ if (env) params.set("env", env);
530
+ const qs = params.toString();
531
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/schema/status${qs ? `?${qs}` : ""}`);
532
+ }
533
+ async wordpressOnboard(project, body) {
534
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/onboard`, body);
535
+ }
517
536
  async wordpressLlmsTxt(project, env) {
518
537
  const qs = env ? `?env=${encodeURIComponent(env)}` : "";
519
538
  return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/llms-txt${qs}`);
@@ -1275,9 +1294,9 @@ async function gaConnect(project, opts) {
1275
1294
  propertyId: opts.propertyId
1276
1295
  };
1277
1296
  if (opts.keyFile) {
1278
- const fs6 = await import("fs");
1297
+ const fs7 = await import("fs");
1279
1298
  try {
1280
- const content = fs6.readFileSync(opts.keyFile, "utf-8");
1299
+ const content = fs7.readFileSync(opts.keyFile, "utf-8");
1281
1300
  JSON.parse(content);
1282
1301
  body.keyJson = content;
1283
1302
  } catch (e) {
@@ -1338,7 +1357,8 @@ async function gaSync(project, opts) {
1338
1357
  return;
1339
1358
  }
1340
1359
  console.log(`GA4 sync complete for "${project}".`);
1341
- console.log(` Rows synced: ${result.rowCount}`);
1360
+ console.log(` Page rows: ${result.rowCount}`);
1361
+ console.log(` AI rows: ${result.aiReferralCount}`);
1342
1362
  console.log(` Period: ${result.days} days`);
1343
1363
  console.log(` Synced at: ${result.syncedAt}`);
1344
1364
  }
@@ -1351,7 +1371,7 @@ async function gaTraffic(project, opts) {
1351
1371
  console.log(JSON.stringify(result, null, 2));
1352
1372
  return;
1353
1373
  }
1354
- if (result.topPages.length === 0) {
1374
+ if (result.topPages.length === 0 && result.aiReferrals.length === 0) {
1355
1375
  console.log('No GA4 traffic data. Run "canonry ga sync <project>" first.');
1356
1376
  return;
1357
1377
  }
@@ -1361,14 +1381,28 @@ async function gaTraffic(project, opts) {
1361
1381
  console.log(` Organic Sessions: ${result.totalOrganicSessions}`);
1362
1382
  console.log(` Total Users: ${result.totalUsers}`);
1363
1383
  console.log();
1364
- const pageWidth = Math.min(60, Math.max(15, ...result.topPages.map((r) => r.landingPage.length)));
1365
- console.log(` ${"LANDING PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}${"USERS".padEnd(8)}`);
1366
- console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1367
- for (const row of result.topPages) {
1368
- const page = row.landingPage.length > pageWidth ? row.landingPage.slice(0, pageWidth - 3) + "..." : row.landingPage;
1369
- console.log(
1370
- ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${String(row.users).padEnd(8)}`
1371
- );
1384
+ if (result.aiReferrals.length > 0) {
1385
+ console.log(" AI REFERRAL SOURCES");
1386
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1387
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1388
+ for (const ref of result.aiReferrals) {
1389
+ console.log(
1390
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
1391
+ );
1392
+ }
1393
+ console.log();
1394
+ }
1395
+ if (result.topPages.length > 0) {
1396
+ const pageWidth = Math.min(60, Math.max(15, ...result.topPages.map((r) => r.landingPage.length)));
1397
+ console.log(` TOP LANDING PAGES`);
1398
+ console.log(` ${"PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}${"USERS".padEnd(8)}`);
1399
+ console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1400
+ for (const row of result.topPages) {
1401
+ const page = row.landingPage.length > pageWidth ? row.landingPage.slice(0, pageWidth - 3) + "..." : row.landingPage;
1402
+ console.log(
1403
+ ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${String(row.users).padEnd(8)}`
1404
+ );
1405
+ }
1372
1406
  }
1373
1407
  if (result.lastSyncedAt) {
1374
1408
  console.log(`
@@ -4212,9 +4246,462 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
4212
4246
  }
4213
4247
  ];
4214
4248
 
4249
+ // src/snapshot-pdf.ts
4250
+ import fs3 from "fs";
4251
+ import path from "path";
4252
+ import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
4253
+ var PAGE_WIDTH = 612;
4254
+ var PAGE_HEIGHT = 792;
4255
+ var MARGIN = 48;
4256
+ var BRAND = rgb(0.58, 0, 0);
4257
+ var INK = rgb(0.1, 0.1, 0.1);
4258
+ var MUTED = rgb(0.38, 0.38, 0.38);
4259
+ var LINE = rgb(0.82, 0.8, 0.76);
4260
+ var PASS = rgb(0.18, 0.49, 0.31);
4261
+ var CAUTION = rgb(0.72, 0.45, 0.2);
4262
+ var FAIL = rgb(0.7, 0.15, 0.15);
4263
+ var PdfWriter = class {
4264
+ constructor(doc, regular, bold) {
4265
+ this.doc = doc;
4266
+ this.regular = regular;
4267
+ this.bold = bold;
4268
+ this.addPage();
4269
+ }
4270
+ usableWidth = PAGE_WIDTH - MARGIN * 2;
4271
+ page;
4272
+ y = 0;
4273
+ addPage() {
4274
+ this.page = this.doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
4275
+ this.y = PAGE_HEIGHT - MARGIN;
4276
+ }
4277
+ ensureSpace(height) {
4278
+ if (this.y - height < MARGIN) {
4279
+ this.addPage();
4280
+ }
4281
+ }
4282
+ heading(text, size = 18) {
4283
+ this.ensureSpace(size + 12);
4284
+ this.page.drawText(text, {
4285
+ x: MARGIN,
4286
+ y: this.y,
4287
+ size,
4288
+ font: this.bold,
4289
+ color: BRAND
4290
+ });
4291
+ this.y -= size + 8;
4292
+ }
4293
+ subheading(text, size = 12) {
4294
+ this.ensureSpace(size + 8);
4295
+ this.page.drawText(text, {
4296
+ x: MARGIN,
4297
+ y: this.y,
4298
+ size,
4299
+ font: this.bold,
4300
+ color: INK
4301
+ });
4302
+ this.y -= size + 6;
4303
+ }
4304
+ paragraph(text, opts) {
4305
+ const size = opts?.size ?? 10;
4306
+ const color = opts?.color ?? INK;
4307
+ const lineHeight = opts?.lineHeight ?? size + 4;
4308
+ const lines = wrapText(this.regular, text, size, this.usableWidth);
4309
+ this.ensureSpace(lines.length * lineHeight + 4);
4310
+ for (const line of lines) {
4311
+ this.page.drawText(line, {
4312
+ x: MARGIN,
4313
+ y: this.y,
4314
+ size,
4315
+ font: this.regular,
4316
+ color
4317
+ });
4318
+ this.y -= lineHeight;
4319
+ }
4320
+ this.y -= 2;
4321
+ }
4322
+ bullet(text) {
4323
+ const lines = wrapText(this.regular, text, 10, this.usableWidth - 14);
4324
+ this.ensureSpace(lines.length * 14 + 2);
4325
+ this.page.drawText("-", {
4326
+ x: MARGIN,
4327
+ y: this.y,
4328
+ size: 10,
4329
+ font: this.bold,
4330
+ color: BRAND
4331
+ });
4332
+ let first = true;
4333
+ for (const line of lines) {
4334
+ this.page.drawText(line, {
4335
+ x: MARGIN + 14,
4336
+ y: this.y,
4337
+ size: 10,
4338
+ font: this.regular,
4339
+ color: INK
4340
+ });
4341
+ this.y -= 14;
4342
+ if (first) first = false;
4343
+ }
4344
+ this.y -= 2;
4345
+ }
4346
+ rule() {
4347
+ this.ensureSpace(8);
4348
+ this.page.drawLine({
4349
+ start: { x: MARGIN, y: this.y },
4350
+ end: { x: PAGE_WIDTH - MARGIN, y: this.y },
4351
+ thickness: 1,
4352
+ color: LINE
4353
+ });
4354
+ this.y -= 10;
4355
+ }
4356
+ keyValue(label, value) {
4357
+ const size = 10;
4358
+ const labelWidth = this.bold.widthOfTextAtSize(`${label}: `, size);
4359
+ this.ensureSpace(16);
4360
+ this.page.drawText(`${label}:`, {
4361
+ x: MARGIN,
4362
+ y: this.y,
4363
+ size,
4364
+ font: this.bold,
4365
+ color: INK
4366
+ });
4367
+ const lines = wrapText(this.regular, value, size, this.usableWidth - labelWidth - 4);
4368
+ let currentY = this.y;
4369
+ for (const line of lines) {
4370
+ this.page.drawText(line, {
4371
+ x: MARGIN + labelWidth + 4,
4372
+ y: currentY,
4373
+ size,
4374
+ font: this.regular,
4375
+ color: INK
4376
+ });
4377
+ currentY -= 14;
4378
+ }
4379
+ this.y = currentY - 2;
4380
+ }
4381
+ table(headers, rows, widths) {
4382
+ const columnWidths = widths ?? headers.map(() => this.usableWidth / headers.length);
4383
+ const headerHeight = 20;
4384
+ this.ensureSpace(headerHeight + 10);
4385
+ let x = MARGIN;
4386
+ for (let i = 0; i < headers.length; i++) {
4387
+ const width = columnWidths[i];
4388
+ this.page.drawRectangle({
4389
+ x,
4390
+ y: this.y - headerHeight + 4,
4391
+ width,
4392
+ height: headerHeight,
4393
+ color: BRAND
4394
+ });
4395
+ const lines = wrapText(this.bold, headers[i], 9, width - 8);
4396
+ let lineY = this.y - 10;
4397
+ for (const line of lines) {
4398
+ this.page.drawText(line, {
4399
+ x: x + 4,
4400
+ y: lineY,
4401
+ size: 9,
4402
+ font: this.bold,
4403
+ color: rgb(1, 0.98, 0.93)
4404
+ });
4405
+ lineY -= 10;
4406
+ }
4407
+ x += width;
4408
+ }
4409
+ this.y -= headerHeight + 4;
4410
+ for (const row of rows) {
4411
+ const lineCounts = row.map((cell, index) => wrapText(this.regular, cell, 9, columnWidths[index] - 8).length);
4412
+ const rowHeight = Math.max(18, Math.max(...lineCounts) * 11 + 6);
4413
+ this.ensureSpace(rowHeight + 4);
4414
+ let cellX = MARGIN;
4415
+ for (let i = 0; i < row.length; i++) {
4416
+ const width = columnWidths[i];
4417
+ this.page.drawRectangle({
4418
+ x: cellX,
4419
+ y: this.y - rowHeight + 4,
4420
+ width,
4421
+ height: rowHeight,
4422
+ borderColor: LINE,
4423
+ borderWidth: 0.5
4424
+ });
4425
+ const lines = wrapText(this.regular, row[i], 9, width - 8);
4426
+ let lineY = this.y - 10;
4427
+ for (const line of lines) {
4428
+ this.page.drawText(line, {
4429
+ x: cellX + 4,
4430
+ y: lineY,
4431
+ size: 9,
4432
+ font: this.regular,
4433
+ color: INK
4434
+ });
4435
+ lineY -= 11;
4436
+ }
4437
+ cellX += width;
4438
+ }
4439
+ this.y -= rowHeight + 2;
4440
+ }
4441
+ this.y -= 4;
4442
+ }
4443
+ };
4444
+ async function writeSnapshotPdf(report, outputPath) {
4445
+ const doc = await PDFDocument.create();
4446
+ doc.setTitle(`${report.companyName} AI Perception Snapshot`);
4447
+ doc.setAuthor("Canonry");
4448
+ doc.setSubject("AEO snapshot report");
4449
+ doc.setProducer("Canonry");
4450
+ doc.setCreator("Canonry");
4451
+ const regular = await doc.embedFont(StandardFonts.Helvetica);
4452
+ const bold = await doc.embedFont(StandardFonts.HelveticaBold);
4453
+ const pdf = new PdfWriter(doc, regular, bold);
4454
+ renderCover(pdf, report);
4455
+ renderSummary(pdf, report);
4456
+ renderAudit(pdf, report);
4457
+ renderCompetitors(pdf, report);
4458
+ renderQueries(pdf, report);
4459
+ const bytes = await doc.save();
4460
+ const resolvedPath = path.resolve(outputPath);
4461
+ fs3.mkdirSync(path.dirname(resolvedPath), { recursive: true });
4462
+ fs3.writeFileSync(resolvedPath, bytes);
4463
+ return resolvedPath;
4464
+ }
4465
+ function renderCover(pdf, report) {
4466
+ pdf.heading("AI Perception Snapshot", 24);
4467
+ pdf.paragraph(report.companyName, { size: 15, color: INK, lineHeight: 18 });
4468
+ pdf.paragraph(report.domain, { size: 11, color: MUTED, lineHeight: 14 });
4469
+ pdf.rule();
4470
+ pdf.keyValue("Generated", new Date(report.generatedAt).toLocaleString("en-US", {
4471
+ year: "numeric",
4472
+ month: "long",
4473
+ day: "numeric",
4474
+ hour: "numeric",
4475
+ minute: "2-digit"
4476
+ }));
4477
+ pdf.keyValue("AEO Audit", `${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
4478
+ pdf.keyValue("Visibility Gap", report.summary.visibilityGap);
4479
+ pdf.paragraph(report.profile.summary, { size: 11, color: INK, lineHeight: 16 });
4480
+ pdf.rule();
4481
+ }
4482
+ function renderSummary(pdf, report) {
4483
+ pdf.heading("What This Means");
4484
+ for (const line of report.summary.whatThisMeans) {
4485
+ pdf.bullet(line);
4486
+ }
4487
+ pdf.subheading("Recommended Actions");
4488
+ for (const action of report.summary.recommendedActions) {
4489
+ pdf.bullet(action);
4490
+ }
4491
+ pdf.rule();
4492
+ }
4493
+ function renderAudit(pdf, report) {
4494
+ pdf.heading("Audit Snapshot");
4495
+ pdf.paragraph(report.audit.summary, { size: 10, color: MUTED, lineHeight: 14 });
4496
+ const factorRows = [...report.audit.factors].sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)).slice(0, 5).map((factor) => [
4497
+ factor.name,
4498
+ formatAuditFactorScore(factor),
4499
+ factor.status
4500
+ ]);
4501
+ if (factorRows.length > 0) {
4502
+ pdf.table(["Weakest factor", "Score / Weight", "Status"], factorRows, [270, 120, 126]);
4503
+ }
4504
+ pdf.rule();
4505
+ }
4506
+ function renderCompetitors(pdf, report) {
4507
+ pdf.heading("Recommended Instead");
4508
+ if (report.summary.topCompetitors.length === 0) {
4509
+ pdf.paragraph("No clear competitor cluster was extracted from the responses.", {
4510
+ size: 10,
4511
+ color: MUTED
4512
+ });
4513
+ pdf.rule();
4514
+ return;
4515
+ }
4516
+ pdf.table(
4517
+ ["Competitor", "Mentions"],
4518
+ report.summary.topCompetitors.map((entry) => [entry.name, String(entry.count)]),
4519
+ [420, 96]
4520
+ );
4521
+ pdf.rule();
4522
+ }
4523
+ function renderQueries(pdf, report) {
4524
+ pdf.heading("Provider Comparison");
4525
+ for (const query of report.queryResults) {
4526
+ pdf.subheading(query.phrase, 11);
4527
+ for (const result of query.providerResults) {
4528
+ const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
4529
+ const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
4530
+ const competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
4531
+ const line = `${result.displayName}: ${status}${accuracy}${competitors}`;
4532
+ pdf.bullet(line);
4533
+ if (result.error) {
4534
+ pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
4535
+ } else if (result.accuracyNotes) {
4536
+ const color = result.describedAccurately === "yes" ? PASS : result.describedAccurately === "no" ? FAIL : CAUTION;
4537
+ pdf.paragraph(result.accuracyNotes, { size: 9, color, lineHeight: 12 });
4538
+ }
4539
+ }
4540
+ pdf.rule();
4541
+ }
4542
+ }
4543
+ function wrapText(font, text, size, maxWidth) {
4544
+ const normalized = text.replace(/\s+/g, " ").trim();
4545
+ if (!normalized) return [""];
4546
+ const words = normalized.split(" ");
4547
+ const lines = [];
4548
+ let current = "";
4549
+ for (const word of words) {
4550
+ const next = current ? `${current} ${word}` : word;
4551
+ if (font.widthOfTextAtSize(next, size) <= maxWidth) {
4552
+ current = next;
4553
+ continue;
4554
+ }
4555
+ if (current) {
4556
+ lines.push(current);
4557
+ current = word;
4558
+ continue;
4559
+ }
4560
+ let chunk = "";
4561
+ for (const char of word) {
4562
+ const candidate = `${chunk}${char}`;
4563
+ if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
4564
+ chunk = candidate;
4565
+ } else {
4566
+ if (chunk) lines.push(chunk);
4567
+ chunk = char;
4568
+ }
4569
+ }
4570
+ current = chunk;
4571
+ }
4572
+ if (current) lines.push(current);
4573
+ return lines;
4574
+ }
4575
+
4576
+ // src/commands/snapshot.ts
4577
+ function getClient16() {
4578
+ return createApiClient();
4579
+ }
4580
+ async function createSnapshotReport(companyName, opts) {
4581
+ const client = getClient16();
4582
+ const report = await client.createSnapshot({
4583
+ companyName,
4584
+ domain: opts.domain,
4585
+ ...opts.phrases && opts.phrases.length > 0 ? { phrases: opts.phrases } : {},
4586
+ ...opts.competitors && opts.competitors.length > 0 ? { competitors: opts.competitors } : {}
4587
+ });
4588
+ let savedPdfPath;
4589
+ if (opts.pdf) {
4590
+ savedPdfPath = await writeSnapshotPdf(report, opts.pdf);
4591
+ }
4592
+ if (opts.format === "json") {
4593
+ console.log(JSON.stringify(report, null, 2));
4594
+ if (savedPdfPath) {
4595
+ process.stderr.write(`Saved PDF: ${savedPdfPath}
4596
+ `);
4597
+ }
4598
+ return;
4599
+ }
4600
+ console.log(formatSnapshotText(report));
4601
+ if (savedPdfPath) {
4602
+ console.log(`
4603
+ PDF saved: ${savedPdfPath}`);
4604
+ }
4605
+ }
4606
+ function formatSnapshotText(report) {
4607
+ const lines = [];
4608
+ lines.push(`Snapshot: ${report.companyName} (${report.domain})`);
4609
+ lines.push(`AEO audit: ${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
4610
+ lines.push(report.summary.visibilityGap);
4611
+ lines.push("");
4612
+ if (report.summary.topCompetitors.length > 0) {
4613
+ lines.push(
4614
+ `Top competitors AI recommended instead: ${report.summary.topCompetitors.map((entry) => `${entry.name} (${entry.count})`).join(", ")}`
4615
+ );
4616
+ lines.push("");
4617
+ }
4618
+ if (report.summary.whatThisMeans.length > 0) {
4619
+ lines.push("What this means:");
4620
+ for (const item of report.summary.whatThisMeans) {
4621
+ lines.push(` - ${item}`);
4622
+ }
4623
+ lines.push("");
4624
+ }
4625
+ const providerWidth = Math.max(
4626
+ 8,
4627
+ ...report.queryResults.flatMap((query) => query.providerResults.map((result) => result.displayName.length))
4628
+ );
4629
+ for (const query of report.queryResults) {
4630
+ lines.push(`"${query.phrase}"`);
4631
+ for (const result of query.providerResults) {
4632
+ lines.push(` ${result.displayName.padEnd(providerWidth)} ${formatProviderLine(result)}`);
4633
+ }
4634
+ lines.push("");
4635
+ }
4636
+ if (report.summary.recommendedActions.length > 0) {
4637
+ lines.push("Recommended actions:");
4638
+ for (const action of report.summary.recommendedActions) {
4639
+ lines.push(` - ${action}`);
4640
+ }
4641
+ }
4642
+ return lines.join("\n").trimEnd();
4643
+ }
4644
+ function formatProviderLine(result) {
4645
+ if (result.error) {
4646
+ return `ERROR: ${result.error}`;
4647
+ }
4648
+ const bits = [];
4649
+ bits.push(result.mentioned ? "YES mentioned" : "NO mention");
4650
+ if (result.cited) bits.push("cited");
4651
+ if (result.describedAccurately !== "not-mentioned") {
4652
+ bits.push(`accuracy=${result.describedAccurately}`);
4653
+ }
4654
+ if (result.recommendedCompetitors.length > 0) {
4655
+ bits.push(`recommended instead: ${result.recommendedCompetitors.join(", ")}`);
4656
+ }
4657
+ if (result.incorrectClaims.length > 0) {
4658
+ bits.push(`incorrect: ${result.incorrectClaims.join("; ")}`);
4659
+ }
4660
+ return bits.join(" | ");
4661
+ }
4662
+
4663
+ // src/cli-commands/snapshot.ts
4664
+ function parseCsvOption(value) {
4665
+ if (!value) return void 0;
4666
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
4667
+ return parts.length > 0 ? [...new Set(parts)] : void 0;
4668
+ }
4669
+ var SNAPSHOT_CLI_COMMANDS = [
4670
+ {
4671
+ path: ["snapshot"],
4672
+ usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]',
4673
+ options: {
4674
+ domain: stringOption(),
4675
+ phrases: stringOption(),
4676
+ competitors: stringOption(),
4677
+ pdf: stringOption()
4678
+ },
4679
+ run: async (input) => {
4680
+ const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]';
4681
+ const companyName = requirePositional(input, 0, {
4682
+ command: "snapshot",
4683
+ usage,
4684
+ message: "company name is required"
4685
+ });
4686
+ const domain = requireStringOption(input, "domain", {
4687
+ command: "snapshot",
4688
+ usage,
4689
+ message: "--domain is required"
4690
+ });
4691
+ await createSnapshotReport(companyName, {
4692
+ domain,
4693
+ phrases: parseCsvOption(getString(input.values, "phrases")),
4694
+ competitors: parseCsvOption(getString(input.values, "competitors")),
4695
+ pdf: getString(input.values, "pdf"),
4696
+ format: input.format
4697
+ });
4698
+ }
4699
+ }
4700
+ ];
4701
+
4215
4702
  // src/commands/bootstrap.ts
4216
4703
  import crypto from "crypto";
4217
- import path from "path";
4704
+ import path2 from "path";
4218
4705
  import { eq } from "drizzle-orm";
4219
4706
 
4220
4707
  // ../config/src/index.ts
@@ -4360,7 +4847,7 @@ async function bootstrapCommand(_opts) {
4360
4847
  );
4361
4848
  }
4362
4849
  const configDir = getConfigDir();
4363
- const databasePath = env.databasePath || path.join(configDir, "data.db");
4850
+ const databasePath = env.databasePath || path2.join(configDir, "data.db");
4364
4851
  const existing = configExists();
4365
4852
  const existingConfig = existing ? loadConfig() : void 0;
4366
4853
  let rawApiKey;
@@ -4428,10 +4915,10 @@ async function bootstrapCommand(_opts) {
4428
4915
 
4429
4916
  // src/commands/daemon.ts
4430
4917
  import { spawn } from "child_process";
4431
- import fs3 from "fs";
4432
- import path2 from "path";
4918
+ import fs4 from "fs";
4919
+ import path3 from "path";
4433
4920
  function getPidPath() {
4434
- return path2.join(getConfigDir(), "canonry.pid");
4921
+ return path3.join(getConfigDir(), "canonry.pid");
4435
4922
  }
4436
4923
  function isProcessAlive(pid) {
4437
4924
  try {
@@ -4458,8 +4945,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
4458
4945
  async function startDaemon(opts) {
4459
4946
  const pidPath = getPidPath();
4460
4947
  const format = opts.format ?? "text";
4461
- if (fs3.existsSync(pidPath)) {
4462
- const existingPid = parseInt(fs3.readFileSync(pidPath, "utf-8").trim(), 10);
4948
+ if (fs4.existsSync(pidPath)) {
4949
+ const existingPid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
4463
4950
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
4464
4951
  throw new CliError({
4465
4952
  code: "DAEMON_ALREADY_RUNNING",
@@ -4470,9 +4957,9 @@ async function startDaemon(opts) {
4470
4957
  }
4471
4958
  });
4472
4959
  }
4473
- fs3.unlinkSync(pidPath);
4960
+ fs4.unlinkSync(pidPath);
4474
4961
  }
4475
- const cliPath = path2.resolve(new URL(import.meta.url).pathname);
4962
+ const cliPath = path3.resolve(new URL(import.meta.url).pathname);
4476
4963
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
4477
4964
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
4478
4965
  if (opts.port) args.push("--port", opts.port);
@@ -4491,10 +4978,10 @@ async function startDaemon(opts) {
4491
4978
  });
4492
4979
  }
4493
4980
  const configDir = getConfigDir();
4494
- if (!fs3.existsSync(configDir)) {
4495
- fs3.mkdirSync(configDir, { recursive: true });
4981
+ if (!fs4.existsSync(configDir)) {
4982
+ fs4.mkdirSync(configDir, { recursive: true });
4496
4983
  }
4497
- fs3.writeFileSync(pidPath, String(child.pid), "utf-8");
4984
+ fs4.writeFileSync(pidPath, String(child.pid), "utf-8");
4498
4985
  const port = opts.port ?? "4100";
4499
4986
  const host = opts.host ?? "127.0.0.1";
4500
4987
  if (format !== "json") {
@@ -4503,7 +4990,7 @@ async function startDaemon(opts) {
4503
4990
  const ready = await waitForReady(host, port);
4504
4991
  if (!ready) {
4505
4992
  try {
4506
- fs3.unlinkSync(pidPath);
4993
+ fs4.unlinkSync(pidPath);
4507
4994
  } catch {
4508
4995
  }
4509
4996
  throw new CliError({
@@ -4535,7 +5022,7 @@ async function startDaemon(opts) {
4535
5022
  }
4536
5023
  function stopDaemon(format = "text") {
4537
5024
  const pidPath = getPidPath();
4538
- if (!fs3.existsSync(pidPath)) {
5025
+ if (!fs4.existsSync(pidPath)) {
4539
5026
  if (format === "json") {
4540
5027
  console.log(JSON.stringify({
4541
5028
  stopped: false,
@@ -4546,7 +5033,7 @@ function stopDaemon(format = "text") {
4546
5033
  console.log("Canonry is not running (no PID file found)");
4547
5034
  return;
4548
5035
  }
4549
- const pid = parseInt(fs3.readFileSync(pidPath, "utf-8").trim(), 10);
5036
+ const pid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
4550
5037
  if (isNaN(pid)) {
4551
5038
  if (format === "json") {
4552
5039
  console.log(JSON.stringify({
@@ -4557,7 +5044,7 @@ function stopDaemon(format = "text") {
4557
5044
  } else {
4558
5045
  console.error("Invalid PID file. Removing it.");
4559
5046
  }
4560
- fs3.unlinkSync(pidPath);
5047
+ fs4.unlinkSync(pidPath);
4561
5048
  return;
4562
5049
  }
4563
5050
  if (!isProcessAlive(pid)) {
@@ -4571,12 +5058,12 @@ function stopDaemon(format = "text") {
4571
5058
  } else {
4572
5059
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
4573
5060
  }
4574
- fs3.unlinkSync(pidPath);
5061
+ fs4.unlinkSync(pidPath);
4575
5062
  return;
4576
5063
  }
4577
5064
  try {
4578
5065
  process.kill(pid, "SIGTERM");
4579
- fs3.unlinkSync(pidPath);
5066
+ fs4.unlinkSync(pidPath);
4580
5067
  if (format === "json") {
4581
5068
  console.log(JSON.stringify({
4582
5069
  stopped: true,
@@ -4600,9 +5087,9 @@ function stopDaemon(format = "text") {
4600
5087
 
4601
5088
  // src/commands/init.ts
4602
5089
  import crypto2 from "crypto";
4603
- import fs4 from "fs";
5090
+ import fs5 from "fs";
4604
5091
  import readline from "readline";
4605
- import path3 from "path";
5092
+ import path4 from "path";
4606
5093
  function prompt(question) {
4607
5094
  const rl = readline.createInterface({
4608
5095
  input: process.stdin,
@@ -4639,8 +5126,8 @@ async function initCommand(opts) {
4639
5126
  return;
4640
5127
  }
4641
5128
  const configDir = getConfigDir();
4642
- if (!fs4.existsSync(configDir)) {
4643
- fs4.mkdirSync(configDir, { recursive: true });
5129
+ if (!fs5.existsSync(configDir)) {
5130
+ fs5.mkdirSync(configDir, { recursive: true });
4644
5131
  }
4645
5132
  const bootstrapEnv = getBootstrapEnv(process.env, {
4646
5133
  GEMINI_API_KEY: opts?.geminiKey,
@@ -4755,7 +5242,7 @@ async function initCommand(opts) {
4755
5242
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
4756
5243
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
4757
5244
  const keyPrefix = rawApiKey.slice(0, 9);
4758
- const databasePath = path3.join(configDir, "data.db");
5245
+ const databasePath = path4.join(configDir, "data.db");
4759
5246
  const db = createClient(databasePath);
4760
5247
  migrate(db);
4761
5248
  db.insert(apiKeys).values({
@@ -5108,10 +5595,10 @@ var SYSTEM_CLI_COMMANDS = [
5108
5595
  ];
5109
5596
 
5110
5597
  // src/cli-commands/wordpress.ts
5111
- import fs5 from "fs";
5598
+ import fs6 from "fs";
5112
5599
 
5113
5600
  // src/commands/wordpress.ts
5114
- function getClient16() {
5601
+ function getClient17() {
5115
5602
  return createApiClient();
5116
5603
  }
5117
5604
  async function promptForAppPassword() {
@@ -5268,7 +5755,7 @@ async function wordpressConnect(project, opts) {
5268
5755
  details: { project }
5269
5756
  });
5270
5757
  }
5271
- const client = getClient16();
5758
+ const client = getClient17();
5272
5759
  const result = await client.wordpressConnect(project, {
5273
5760
  url: opts.url,
5274
5761
  stagingUrl: opts.stagingUrl,
@@ -5285,7 +5772,7 @@ async function wordpressConnect(project, opts) {
5285
5772
  printWordpressStatus(project, result);
5286
5773
  }
5287
5774
  async function wordpressDisconnect(project, format) {
5288
- const client = getClient16();
5775
+ const client = getClient17();
5289
5776
  await client.wordpressDisconnect(project);
5290
5777
  if (format === "json") {
5291
5778
  printJson({ project, disconnected: true });
@@ -5294,7 +5781,7 @@ async function wordpressDisconnect(project, format) {
5294
5781
  console.log(`WordPress disconnected from project "${project}".`);
5295
5782
  }
5296
5783
  async function wordpressStatus(project, format) {
5297
- const client = getClient16();
5784
+ const client = getClient17();
5298
5785
  const result = await client.wordpressStatus(project);
5299
5786
  if (format === "json") {
5300
5787
  printJson(result);
@@ -5303,7 +5790,7 @@ async function wordpressStatus(project, format) {
5303
5790
  printWordpressStatus(project, result);
5304
5791
  }
5305
5792
  async function wordpressPages(project, opts) {
5306
- const client = getClient16();
5793
+ const client = getClient17();
5307
5794
  const result = await client.wordpressPages(project, opts.env);
5308
5795
  if (opts.format === "json") {
5309
5796
  printJson(result);
@@ -5312,7 +5799,7 @@ async function wordpressPages(project, opts) {
5312
5799
  printPages(project, result.env, result.pages);
5313
5800
  }
5314
5801
  async function wordpressPage(project, slug, opts) {
5315
- const client = getClient16();
5802
+ const client = getClient17();
5316
5803
  const result = await client.wordpressPage(project, slug, opts.env);
5317
5804
  if (opts.format === "json") {
5318
5805
  printJson(result);
@@ -5321,7 +5808,7 @@ async function wordpressPage(project, slug, opts) {
5321
5808
  printPageDetail(result);
5322
5809
  }
5323
5810
  async function wordpressCreatePage(project, body) {
5324
- const client = getClient16();
5811
+ const client = getClient17();
5325
5812
  const result = await client.wordpressCreatePage(project, body);
5326
5813
  if (body.format === "json") {
5327
5814
  printJson(result);
@@ -5332,7 +5819,7 @@ async function wordpressCreatePage(project, body) {
5332
5819
  printPageDetail(result);
5333
5820
  }
5334
5821
  async function wordpressUpdatePage(project, body) {
5335
- const client = getClient16();
5822
+ const client = getClient17();
5336
5823
  const result = await client.wordpressUpdatePage(project, body);
5337
5824
  if (body.format === "json") {
5338
5825
  printJson(result);
@@ -5343,7 +5830,7 @@ async function wordpressUpdatePage(project, body) {
5343
5830
  printPageDetail(result);
5344
5831
  }
5345
5832
  async function wordpressSetMeta(project, body) {
5346
- const client = getClient16();
5833
+ const client = getClient17();
5347
5834
  const result = await client.wordpressSetMeta(project, body);
5348
5835
  if (body.format === "json") {
5349
5836
  printJson(result);
@@ -5353,8 +5840,90 @@ async function wordpressSetMeta(project, body) {
5353
5840
  `);
5354
5841
  printPageDetail(result);
5355
5842
  }
5843
+ async function wordpressBulkSetMeta(project, opts) {
5844
+ const fs7 = await import("fs/promises");
5845
+ const path5 = await import("path");
5846
+ const filePath = path5.resolve(opts.from);
5847
+ let raw;
5848
+ try {
5849
+ raw = await fs7.readFile(filePath, "utf8");
5850
+ } catch {
5851
+ throw new CliError({
5852
+ code: "FILE_READ_ERROR",
5853
+ message: `Cannot read file: ${filePath}`,
5854
+ displayMessage: `Error: cannot read file "${opts.from}". Check the path and permissions.`,
5855
+ details: { path: filePath }
5856
+ });
5857
+ }
5858
+ let parsed;
5859
+ try {
5860
+ parsed = JSON.parse(raw);
5861
+ } catch {
5862
+ throw new CliError({
5863
+ code: "INVALID_JSON",
5864
+ message: `File is not valid JSON: ${filePath}`,
5865
+ displayMessage: `Error: "${opts.from}" is not valid JSON.`,
5866
+ details: { path: filePath }
5867
+ });
5868
+ }
5869
+ const entries = Object.entries(parsed).map(([slug, meta]) => ({
5870
+ slug,
5871
+ title: meta.title,
5872
+ description: meta.description,
5873
+ noindex: meta.noindex
5874
+ }));
5875
+ if (entries.length === 0) {
5876
+ throw new CliError({
5877
+ code: "EMPTY_META_FILE",
5878
+ message: "Meta file contains no entries",
5879
+ displayMessage: `Error: "${opts.from}" contains no entries. Expected JSON object keyed by slug.`,
5880
+ details: { path: filePath }
5881
+ });
5882
+ }
5883
+ const client = getClient17();
5884
+ const result = await client.wordpressBulkSetMeta(project, { entries, env: opts.env });
5885
+ if (opts.format === "json") {
5886
+ printJson(result);
5887
+ return;
5888
+ }
5889
+ const applied = result.results.filter((r) => r.status === "applied");
5890
+ const skipped = result.results.filter((r) => r.status === "skipped");
5891
+ const manual = result.results.filter((r) => r.status === "manual");
5892
+ console.log(`Bulk SEO meta update (${result.env}, strategy: ${result.strategy}):
5893
+ `);
5894
+ if (applied.length > 0) {
5895
+ console.log(` Applied (${applied.length}):`);
5896
+ for (const r of applied) {
5897
+ console.log(` ${r.slug}`);
5898
+ }
5899
+ }
5900
+ if (skipped.length > 0) {
5901
+ console.log(`
5902
+ Skipped (${skipped.length}):`);
5903
+ for (const r of skipped) {
5904
+ console.log(` ${r.slug}: ${r.error ?? "unknown reason"}`);
5905
+ }
5906
+ }
5907
+ if (manual.length > 0) {
5908
+ console.log(`
5909
+ Manual action required (${manual.length}):`);
5910
+ console.log(" No SEO plugin with REST-writable meta fields was detected.");
5911
+ console.log(" Install Yoast SEO, Rank Math, or AIOSEO, or update these pages manually:\n");
5912
+ for (const r of manual) {
5913
+ if (r.manualAssist) {
5914
+ console.log(` ${r.slug}:`);
5915
+ console.log(` Admin: ${r.manualAssist.adminUrl ?? "-"}`);
5916
+ console.log(` Values: ${r.manualAssist.content}`);
5917
+ } else {
5918
+ console.log(` ${r.slug}`);
5919
+ }
5920
+ }
5921
+ }
5922
+ console.log(`
5923
+ Total: ${applied.length} applied, ${skipped.length} skipped, ${manual.length} manual`);
5924
+ }
5356
5925
  async function wordpressSchema(project, slug, opts) {
5357
- const client = getClient16();
5926
+ const client = getClient17();
5358
5927
  const result = await client.wordpressSchema(project, slug, opts.env);
5359
5928
  if (opts.format === "json") {
5360
5929
  printJson(result);
@@ -5365,7 +5934,7 @@ async function wordpressSchema(project, slug, opts) {
5365
5934
  printSchemaBlocks(result.blocks);
5366
5935
  }
5367
5936
  async function wordpressSetSchema(project, body) {
5368
- const client = getClient16();
5937
+ const client = getClient17();
5369
5938
  const result = await client.wordpressSetSchema(project, body);
5370
5939
  if (body.format === "json") {
5371
5940
  printJson(result);
@@ -5373,8 +5942,170 @@ async function wordpressSetSchema(project, body) {
5373
5942
  }
5374
5943
  printManualAssist(`Schema update for "${body.slug}"`, result);
5375
5944
  }
5945
+ async function wordpressSchemaDeploy(project, opts) {
5946
+ const fs7 = await import("fs/promises");
5947
+ const path5 = await import("path");
5948
+ const yaml = await import("yaml").catch(() => null);
5949
+ const filePath = path5.resolve(opts.profile);
5950
+ let raw;
5951
+ try {
5952
+ raw = await fs7.readFile(filePath, "utf8");
5953
+ } catch {
5954
+ throw new CliError({
5955
+ code: "FILE_READ_ERROR",
5956
+ message: `Cannot read file: ${filePath}`,
5957
+ displayMessage: `Error: cannot read file "${opts.profile}". Check the path and permissions.`,
5958
+ details: { path: filePath }
5959
+ });
5960
+ }
5961
+ let parsed;
5962
+ try {
5963
+ if (yaml?.parse) {
5964
+ parsed = yaml.parse(raw);
5965
+ } else {
5966
+ parsed = JSON.parse(raw);
5967
+ }
5968
+ } catch {
5969
+ throw new CliError({
5970
+ code: "INVALID_PROFILE",
5971
+ message: `File is not valid YAML or JSON: ${filePath}`,
5972
+ displayMessage: `Error: "${opts.profile}" is not valid YAML or JSON.`,
5973
+ details: { path: filePath }
5974
+ });
5975
+ }
5976
+ const profile = parsed;
5977
+ if (!profile?.business?.name || !profile?.pages || Object.keys(profile.pages).length === 0) {
5978
+ throw new CliError({
5979
+ code: "INVALID_PROFILE",
5980
+ message: "Profile must have business.name and non-empty pages",
5981
+ displayMessage: "Error: profile file must contain business.name and at least one page entry.",
5982
+ details: { path: filePath }
5983
+ });
5984
+ }
5985
+ const client = getClient17();
5986
+ const result = await client.wordpressSchemaDeploy(project, { profile: parsed, env: opts.env });
5987
+ if (opts.format === "json") {
5988
+ printJson(result);
5989
+ return;
5990
+ }
5991
+ console.log(`Schema deploy (${result.env}):
5992
+ `);
5993
+ for (const r of result.results) {
5994
+ const types = r.schemasInjected?.join(", ") ?? "";
5995
+ switch (r.status) {
5996
+ case "deployed":
5997
+ console.log(` ${r.slug}: deployed (${types})`);
5998
+ break;
5999
+ case "stripped":
6000
+ console.log(` ${r.slug}: STRIPPED \u2014 WordPress removed <script> tags. Manual action required.`);
6001
+ if (r.manualAssist) {
6002
+ console.log(` Admin: ${r.manualAssist.adminUrl ?? "-"}`);
6003
+ for (const step of r.manualAssist.nextSteps) {
6004
+ console.log(` - ${step}`);
6005
+ }
6006
+ }
6007
+ break;
6008
+ case "skipped":
6009
+ console.log(` ${r.slug}: skipped \u2014 ${r.error ?? "unknown"}`);
6010
+ break;
6011
+ case "failed":
6012
+ console.log(` ${r.slug}: FAILED \u2014 ${r.error ?? "unknown"}`);
6013
+ break;
6014
+ }
6015
+ }
6016
+ const deployed = result.results.filter((r) => r.status === "deployed").length;
6017
+ const stripped = result.results.filter((r) => r.status === "stripped").length;
6018
+ const skipped = result.results.filter((r) => r.status === "skipped").length;
6019
+ const failed = result.results.filter((r) => r.status === "failed").length;
6020
+ console.log(`
6021
+ Total: ${deployed} deployed, ${stripped} stripped, ${skipped} skipped, ${failed} failed`);
6022
+ }
6023
+ async function wordpressSchemaStatus(project, opts) {
6024
+ const client = getClient17();
6025
+ const result = await client.wordpressSchemaStatus(project, opts.env);
6026
+ if (opts.format === "json") {
6027
+ printJson(result);
6028
+ return;
6029
+ }
6030
+ console.log(`Schema status (${result.env}):
6031
+ `);
6032
+ if (result.pages.length === 0) {
6033
+ console.log(" No pages found.");
6034
+ return;
6035
+ }
6036
+ const slugWidth = Math.max(4, ...result.pages.map((p) => p.slug.length));
6037
+ console.log(` ${"SLUG".padEnd(slugWidth)} CANONRY THIRD-PARTY`);
6038
+ console.log(` ${"\u2500".repeat(slugWidth)} ${"\u2500".repeat(17)} ${"\u2500".repeat(20)}`);
6039
+ for (const page of result.pages) {
6040
+ const canonry = page.canonrySchemas.length > 0 ? page.canonrySchemas.join(", ") : "-";
6041
+ const thirdParty = page.thirdPartySchemas.length > 0 ? page.thirdPartySchemas.join(", ") : "-";
6042
+ console.log(` ${page.slug.padEnd(slugWidth)} ${canonry.padEnd(17)} ${thirdParty}`);
6043
+ }
6044
+ }
6045
+ async function wordpressOnboard(project, opts) {
6046
+ const appPassword = opts.appPassword ?? await promptForAppPassword();
6047
+ if (!appPassword) {
6048
+ throw new CliError({
6049
+ code: "WORDPRESS_APP_PASSWORD_REQUIRED",
6050
+ message: "WordPress Application Password is required",
6051
+ displayMessage: "Error: WordPress Application Password is required (pass --app-password or enter interactively).",
6052
+ details: { project }
6053
+ });
6054
+ }
6055
+ let profileData;
6056
+ if (opts.profile) {
6057
+ const fs7 = await import("fs/promises");
6058
+ const path5 = await import("path");
6059
+ const yaml = await import("yaml").catch(() => null);
6060
+ const filePath = path5.resolve(opts.profile);
6061
+ let raw;
6062
+ try {
6063
+ raw = await fs7.readFile(filePath, "utf8");
6064
+ } catch {
6065
+ throw new CliError({
6066
+ code: "FILE_READ_ERROR",
6067
+ message: `Cannot read file: ${filePath}`,
6068
+ displayMessage: `Error: cannot read file "${opts.profile}".`,
6069
+ details: { path: filePath }
6070
+ });
6071
+ }
6072
+ try {
6073
+ profileData = yaml?.parse ? yaml.parse(raw) : JSON.parse(raw);
6074
+ } catch {
6075
+ throw new CliError({
6076
+ code: "INVALID_PROFILE",
6077
+ message: `File is not valid YAML or JSON: ${filePath}`,
6078
+ displayMessage: `Error: "${opts.profile}" is not valid YAML or JSON.`,
6079
+ details: { path: filePath }
6080
+ });
6081
+ }
6082
+ }
6083
+ const client = getClient17();
6084
+ const result = await client.wordpressOnboard(project, {
6085
+ url: opts.url,
6086
+ username: opts.user,
6087
+ appPassword,
6088
+ stagingUrl: opts.stagingUrl,
6089
+ defaultEnv: opts.defaultEnv,
6090
+ profile: profileData,
6091
+ skipSchema: opts.skipSchema,
6092
+ skipSubmit: opts.skipSubmit
6093
+ });
6094
+ if (opts.format === "json") {
6095
+ printJson(result);
6096
+ return;
6097
+ }
6098
+ console.log(`WordPress onboarding for "${project}":
6099
+ `);
6100
+ for (const step of result.steps) {
6101
+ const icon = step.status === "completed" ? "+" : step.status === "skipped" ? "-" : "x";
6102
+ console.log(` [${icon}] ${step.name}: ${step.status}`);
6103
+ if (step.summary) console.log(` ${step.summary}`);
6104
+ if (step.error) console.log(` Error: ${step.error}`);
6105
+ }
6106
+ }
5376
6107
  async function wordpressLlmsTxt(project, opts) {
5377
- const client = getClient16();
6108
+ const client = getClient17();
5378
6109
  const result = await client.wordpressLlmsTxt(project, opts.env);
5379
6110
  if (opts.format === "json") {
5380
6111
  printJson(result);
@@ -5385,7 +6116,7 @@ async function wordpressLlmsTxt(project, opts) {
5385
6116
  console.log(result.content ?? "(not found)");
5386
6117
  }
5387
6118
  async function wordpressSetLlmsTxt(project, body) {
5388
- const client = getClient16();
6119
+ const client = getClient17();
5389
6120
  const result = await client.wordpressSetLlmsTxt(project, body);
5390
6121
  if (body.format === "json") {
5391
6122
  printJson(result);
@@ -5394,7 +6125,7 @@ async function wordpressSetLlmsTxt(project, body) {
5394
6125
  printManualAssist(`llms.txt update for "${project}"`, result);
5395
6126
  }
5396
6127
  async function wordpressAudit(project, opts) {
5397
- const client = getClient16();
6128
+ const client = getClient17();
5398
6129
  const result = await client.wordpressAudit(project, opts.env);
5399
6130
  if (opts.format === "json") {
5400
6131
  printJson(result);
@@ -5408,7 +6139,7 @@ async function wordpressAudit(project, opts) {
5408
6139
  printAuditIssues(result.issues);
5409
6140
  }
5410
6141
  async function wordpressDiff(project, slug, format) {
5411
- const client = getClient16();
6142
+ const client = getClient17();
5412
6143
  const result = await client.wordpressDiff(project, slug);
5413
6144
  if (format === "json") {
5414
6145
  printJson(result);
@@ -5417,7 +6148,7 @@ async function wordpressDiff(project, slug, format) {
5417
6148
  printDiff(result);
5418
6149
  }
5419
6150
  async function wordpressStagingStatus(project, format) {
5420
- const client = getClient16();
6151
+ const client = getClient17();
5421
6152
  const result = await client.wordpressStagingStatus(project);
5422
6153
  if (format === "json") {
5423
6154
  printJson(result);
@@ -5431,7 +6162,7 @@ async function wordpressStagingStatus(project, format) {
5431
6162
  console.log(` Admin URL: ${result.adminUrl}`);
5432
6163
  }
5433
6164
  async function wordpressStagingPush(project, format) {
5434
- const client = getClient16();
6165
+ const client = getClient17();
5435
6166
  const result = await client.wordpressStagingPush(project);
5436
6167
  if (format === "json") {
5437
6168
  printJson(result);
@@ -5478,7 +6209,7 @@ function resolveContent(input, command, usage, options) {
5478
6209
  }
5479
6210
  if (contentFile) {
5480
6211
  try {
5481
- return fs5.readFileSync(contentFile, "utf-8");
6212
+ return fs6.readFileSync(contentFile, "utf-8");
5482
6213
  } catch (error) {
5483
6214
  const message = error instanceof Error ? error.message : String(error);
5484
6215
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -5650,15 +6381,27 @@ var WORDPRESS_CLI_COMMANDS = [
5650
6381
  },
5651
6382
  {
5652
6383
  path: ["wordpress", "set-meta"],
5653
- usage: "canonry wordpress set-meta <project> <slug> [--title <title>] [--description <text>] [--noindex|--index] [--live|--staging] [--format json]",
6384
+ usage: "canonry wordpress set-meta <project> <slug> [--title <title>] [--description <text>] [--noindex|--index] [--from <file>] [--live|--staging] [--format json]",
5654
6385
  options: {
5655
6386
  title: stringOption(),
5656
6387
  description: stringOption(),
5657
6388
  noindex: { type: "boolean", default: false },
5658
6389
  index: { type: "boolean", default: false },
6390
+ from: stringOption(),
5659
6391
  ...envOptions
5660
6392
  },
5661
6393
  run: async (input) => {
6394
+ const fromFile = getString(input.values, "from");
6395
+ if (fromFile) {
6396
+ const usage2 = "canonry wordpress set-meta <project> --from <file> [--live|--staging] [--format json]";
6397
+ const project2 = requireProject(input, "wordpress.set-meta", usage2);
6398
+ await wordpressBulkSetMeta(project2, {
6399
+ from: fromFile,
6400
+ env: resolveEnv(input, "wordpress.set-meta", usage2),
6401
+ format: input.format
6402
+ });
6403
+ return;
6404
+ }
5662
6405
  const usage = "canonry wordpress set-meta <project> <slug> [--title <title>] [--description <text>] [--noindex|--index] [--live|--staging] [--format json]";
5663
6406
  const project = requireProject(input, "wordpress.set-meta", usage);
5664
6407
  const slug = requirePositional(input, 1, {
@@ -5676,6 +6419,41 @@ var WORDPRESS_CLI_COMMANDS = [
5676
6419
  });
5677
6420
  }
5678
6421
  },
6422
+ {
6423
+ path: ["wordpress", "schema", "deploy"],
6424
+ usage: "canonry wordpress schema deploy <project> --profile <file> [--live|--staging] [--format json]",
6425
+ options: {
6426
+ profile: stringOption(),
6427
+ ...envOptions
6428
+ },
6429
+ run: async (input) => {
6430
+ const usage = "canonry wordpress schema deploy <project> --profile <file> [--live|--staging] [--format json]";
6431
+ const project = requireProject(input, "wordpress.schema.deploy", usage);
6432
+ const profile = requireStringOption(input, "profile", {
6433
+ message: "--profile is required",
6434
+ command: "wordpress.schema.deploy",
6435
+ usage
6436
+ });
6437
+ await wordpressSchemaDeploy(project, {
6438
+ profile,
6439
+ env: resolveEnv(input, "wordpress.schema.deploy", usage),
6440
+ format: input.format
6441
+ });
6442
+ }
6443
+ },
6444
+ {
6445
+ path: ["wordpress", "schema", "status"],
6446
+ usage: "canonry wordpress schema status <project> [--live|--staging] [--format json]",
6447
+ options: envOptions,
6448
+ run: async (input) => {
6449
+ const usage = "canonry wordpress schema status <project> [--live|--staging] [--format json]";
6450
+ const project = requireProject(input, "wordpress.schema.status", usage);
6451
+ await wordpressSchemaStatus(project, {
6452
+ env: resolveEnv(input, "wordpress.schema.status", usage),
6453
+ format: input.format
6454
+ });
6455
+ }
6456
+ },
5679
6457
  {
5680
6458
  path: ["wordpress", "schema"],
5681
6459
  usage: "canonry wordpress schema <project> <slug> [--live|--staging] [--format json]",
@@ -5757,6 +6535,45 @@ var WORDPRESS_CLI_COMMANDS = [
5757
6535
  });
5758
6536
  }
5759
6537
  },
6538
+ {
6539
+ path: ["wordpress", "onboard"],
6540
+ usage: "canonry wordpress onboard <project> --url <url> --user <user> [--app-password <pw>] [--profile <file>] [--skip-schema] [--skip-submit] [--live|--staging] [--format json]",
6541
+ options: {
6542
+ url: stringOption(),
6543
+ user: stringOption(),
6544
+ "app-password": stringOption(),
6545
+ "staging-url": stringOption(),
6546
+ profile: stringOption(),
6547
+ "skip-schema": { type: "boolean", default: false },
6548
+ "skip-submit": { type: "boolean", default: false },
6549
+ ...envOptions
6550
+ },
6551
+ run: async (input) => {
6552
+ const usage = "canonry wordpress onboard <project> --url <url> --user <user> [--app-password <pw>] [--profile <file>] [--format json]";
6553
+ const project = requireProject(input, "wordpress.onboard", usage);
6554
+ const url = requireStringOption(input, "url", {
6555
+ message: "--url is required",
6556
+ command: "wordpress.onboard",
6557
+ usage
6558
+ });
6559
+ const user = requireStringOption(input, "user", {
6560
+ message: "--user is required",
6561
+ command: "wordpress.onboard",
6562
+ usage
6563
+ });
6564
+ await wordpressOnboard(project, {
6565
+ url,
6566
+ user,
6567
+ appPassword: getString(input.values, "app-password"),
6568
+ stagingUrl: getString(input.values, "staging-url"),
6569
+ defaultEnv: resolveEnv(input, "wordpress.onboard", usage),
6570
+ profile: getString(input.values, "profile"),
6571
+ skipSchema: getBoolean(input.values, "skip-schema"),
6572
+ skipSubmit: getBoolean(input.values, "skip-submit"),
6573
+ format: input.format
6574
+ });
6575
+ }
6576
+ },
5760
6577
  {
5761
6578
  path: ["wordpress", "audit"],
5762
6579
  usage: "canonry wordpress audit <project> [--live|--staging] [--format json]",
@@ -5831,6 +6648,7 @@ var REGISTERED_CLI_COMMANDS = [
5831
6648
  ...KEYWORD_CLI_COMMANDS,
5832
6649
  ...COMPETITOR_CLI_COMMANDS,
5833
6650
  ...SETTINGS_CLI_COMMANDS,
6651
+ ...SNAPSHOT_CLI_COMMANDS,
5834
6652
  ...RUN_CLI_COMMANDS,
5835
6653
  ...OPERATOR_CLI_COMMANDS,
5836
6654
  ...SCHEDULE_CLI_COMMANDS,
@@ -5870,6 +6688,7 @@ Usage:
5870
6688
  canonry keyword generate <project> Auto-generate key phrases (--provider, --count, --save)
5871
6689
  canonry competitor add <project> <domain> Add competitors
5872
6690
  canonry competitor list <project> List competitors
6691
+ canonry snapshot <company> --domain <domain> One-shot AI perception report
5873
6692
  canonry run <project> Trigger a run (all providers)
5874
6693
  canonry run <project> --provider <name> Trigger a run for a specific provider
5875
6694
  canonry run <project> --location <label> Run with a specific location
@@ -5933,8 +6752,12 @@ Usage:
5933
6752
  canonry wordpress create-page <project> Create a WordPress page (--title, --slug, --content/--content-file)
5934
6753
  canonry wordpress update-page <project> <slug> Update a WordPress page (--content/--content-file)
5935
6754
  canonry wordpress set-meta <project> <slug> Update REST-exposed SEO meta
6755
+ canonry wordpress set-meta <project> --from <file> Bulk update SEO meta from JSON file
5936
6756
  canonry wordpress schema <project> <slug> Read rendered JSON-LD schema
6757
+ canonry wordpress schema deploy <project> --profile <file> Deploy JSON-LD schema to pages
6758
+ canonry wordpress schema status <project> Show schema status per page
5937
6759
  canonry wordpress set-schema <project> <slug> Generate manual schema handoff
6760
+ canonry wordpress onboard <project> --url <url> --user <user> Full onboarding workflow
5938
6761
  canonry wordpress llms-txt <project> Read /llms.txt
5939
6762
  canonry wordpress set-llms-txt <project> Generate manual llms.txt handoff
5940
6763
  canonry wordpress audit <project> Audit WordPress pages for SEO/content issues
@@ -5971,6 +6794,9 @@ Options:
5971
6794
  --language <lang> Language code (default: en)
5972
6795
  --provider <name> Provider to use (gemini, openai, claude, perplexity, local, cdp:chatgpt, or cdp for all CDP targets)
5973
6796
  --format <fmt> Output format: text (default) or json
6797
+ --phrases <list> Comma-separated category queries (snapshot)
6798
+ --competitors <list> Comma-separated competitor hints (snapshot)
6799
+ --pdf <path> Write a PDF snapshot report to a file
5974
6800
  --location <label> Run with a specific configured location
5975
6801
  --all-locations Run for every configured location
5976
6802
  --no-location Explicitly skip location context