@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/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-2WEJYH27.js";
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, 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
  }
@@ -1275,9 +1279,9 @@ async function gaConnect(project, opts) {
1275
1279
  propertyId: opts.propertyId
1276
1280
  };
1277
1281
  if (opts.keyFile) {
1278
- const fs6 = await import("fs");
1282
+ const fs7 = await import("fs");
1279
1283
  try {
1280
- const content = fs6.readFileSync(opts.keyFile, "utf-8");
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 path from "path";
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 || path.join(configDir, "data.db");
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 fs3 from "fs";
4432
- import path2 from "path";
4888
+ import fs4 from "fs";
4889
+ import path3 from "path";
4433
4890
  function getPidPath() {
4434
- return path2.join(getConfigDir(), "canonry.pid");
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 (fs3.existsSync(pidPath)) {
4462
- const existingPid = parseInt(fs3.readFileSync(pidPath, "utf-8").trim(), 10);
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
- fs3.unlinkSync(pidPath);
4930
+ fs4.unlinkSync(pidPath);
4474
4931
  }
4475
- const cliPath = path2.resolve(new URL(import.meta.url).pathname);
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 (!fs3.existsSync(configDir)) {
4495
- fs3.mkdirSync(configDir, { recursive: true });
4951
+ if (!fs4.existsSync(configDir)) {
4952
+ fs4.mkdirSync(configDir, { recursive: true });
4496
4953
  }
4497
- fs3.writeFileSync(pidPath, String(child.pid), "utf-8");
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
- fs3.unlinkSync(pidPath);
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 (!fs3.existsSync(pidPath)) {
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(fs3.readFileSync(pidPath, "utf-8").trim(), 10);
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
- fs3.unlinkSync(pidPath);
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
- fs3.unlinkSync(pidPath);
5031
+ fs4.unlinkSync(pidPath);
4575
5032
  return;
4576
5033
  }
4577
5034
  try {
4578
5035
  process.kill(pid, "SIGTERM");
4579
- fs3.unlinkSync(pidPath);
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 fs4 from "fs";
5060
+ import fs5 from "fs";
4604
5061
  import readline from "readline";
4605
- import path3 from "path";
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 (!fs4.existsSync(configDir)) {
4643
- fs4.mkdirSync(configDir, { recursive: true });
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 = path3.join(configDir, "data.db");
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 fs5 from "fs";
5568
+ import fs6 from "fs";
5112
5569
 
5113
5570
  // src/commands/wordpress.ts
5114
- function getClient16() {
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 = getClient16();
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 fs5.readFileSync(contentFile, "utf-8");
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