@ainyc/canonry 1.7.0 → 1.8.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
@@ -4,6 +4,7 @@ import {
4
4
  configExists,
5
5
  createClient,
6
6
  createServer,
7
+ effectiveDomains,
7
8
  getConfigDir,
8
9
  getConfigPath,
9
10
  getOrCreateAnonymousId,
@@ -11,11 +12,12 @@ import {
11
12
  isTelemetryEnabled,
12
13
  loadConfig,
13
14
  migrate,
15
+ notificationEventSchema,
14
16
  providerQuotaPolicySchema,
15
17
  saveConfig,
16
18
  showFirstRunNotice,
17
19
  trackEvent
18
- } from "./chunk-2QG7TZ4A.js";
20
+ } from "./chunk-3FHF3YVA.js";
19
21
 
20
22
  // src/cli.ts
21
23
  import { parseArgs } from "util";
@@ -316,6 +318,107 @@ Canonry server running at http://${host === "0.0.0.0" ? "localhost" : host}:${po
316
318
  }
317
319
  }
318
320
 
321
+ // src/commands/daemon.ts
322
+ import { spawn } from "child_process";
323
+ import fs2 from "fs";
324
+ import path3 from "path";
325
+ function getPidPath() {
326
+ return path3.join(getConfigDir(), "canonry.pid");
327
+ }
328
+ function isProcessAlive(pid) {
329
+ try {
330
+ process.kill(pid, 0);
331
+ return true;
332
+ } catch (err) {
333
+ if (err.code === "EPERM") return true;
334
+ return false;
335
+ }
336
+ }
337
+ async function waitForReady(host, port, maxMs = 1e4) {
338
+ const url = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/health`;
339
+ const deadline = Date.now() + maxMs;
340
+ while (Date.now() < deadline) {
341
+ try {
342
+ const res = await fetch(url);
343
+ if (res.ok) return true;
344
+ } catch {
345
+ }
346
+ await new Promise((r) => setTimeout(r, 200));
347
+ }
348
+ return false;
349
+ }
350
+ async function startDaemon(opts) {
351
+ const pidPath = getPidPath();
352
+ if (fs2.existsSync(pidPath)) {
353
+ const existingPid = parseInt(fs2.readFileSync(pidPath, "utf-8").trim(), 10);
354
+ if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
355
+ console.error(`Canonry is already running (PID: ${existingPid})`);
356
+ process.exit(1);
357
+ }
358
+ fs2.unlinkSync(pidPath);
359
+ }
360
+ const cliPath = path3.resolve(new URL(import.meta.url).pathname);
361
+ const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
362
+ const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
363
+ if (opts.port) args.push("--port", opts.port);
364
+ if (opts.host) args.push("--host", opts.host);
365
+ const child = spawn(process.execPath, args, {
366
+ detached: true,
367
+ stdio: "ignore"
368
+ });
369
+ child.unref();
370
+ if (!child.pid) {
371
+ console.error("Failed to start Canonry server");
372
+ process.exit(1);
373
+ }
374
+ const configDir = getConfigDir();
375
+ if (!fs2.existsSync(configDir)) {
376
+ fs2.mkdirSync(configDir, { recursive: true });
377
+ }
378
+ fs2.writeFileSync(pidPath, String(child.pid), "utf-8");
379
+ const port = opts.port ?? "4100";
380
+ const host = opts.host ?? "127.0.0.1";
381
+ process.stderr.write("Waiting for server to start...");
382
+ const ready = await waitForReady(host, port);
383
+ if (!ready) {
384
+ try {
385
+ fs2.unlinkSync(pidPath);
386
+ } catch {
387
+ }
388
+ console.error("\nFailed to start: server did not respond within 10s");
389
+ process.exit(1);
390
+ }
391
+ process.stderr.write("\n");
392
+ console.log(`Canonry started (PID: ${child.pid}), listening on http://${host === "0.0.0.0" ? "localhost" : host}:${port}`);
393
+ }
394
+ function stopDaemon() {
395
+ const pidPath = getPidPath();
396
+ if (!fs2.existsSync(pidPath)) {
397
+ console.log("Canonry is not running (no PID file found)");
398
+ return;
399
+ }
400
+ const pid = parseInt(fs2.readFileSync(pidPath, "utf-8").trim(), 10);
401
+ if (isNaN(pid)) {
402
+ console.error("Invalid PID file. Removing it.");
403
+ fs2.unlinkSync(pidPath);
404
+ return;
405
+ }
406
+ if (!isProcessAlive(pid)) {
407
+ console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
408
+ fs2.unlinkSync(pidPath);
409
+ return;
410
+ }
411
+ try {
412
+ process.kill(pid, "SIGTERM");
413
+ fs2.unlinkSync(pidPath);
414
+ console.log(`Canonry stopped (PID: ${pid})`);
415
+ } catch (err) {
416
+ const msg = err instanceof Error ? err.message : String(err);
417
+ console.error(`Failed to stop Canonry (PID: ${pid}): ${msg}`);
418
+ process.exit(1);
419
+ }
420
+ }
421
+
319
422
  // src/client.ts
320
423
  var ApiClient = class {
321
424
  baseUrl;
@@ -324,8 +427,8 @@ var ApiClient = class {
324
427
  this.baseUrl = baseUrl.replace(/\/$/, "") + "/api/v1";
325
428
  this.apiKey = apiKey;
326
429
  }
327
- async request(method, path3, body) {
328
- const url = `${this.baseUrl}${path3}`;
430
+ async request(method, path4, body) {
431
+ const url = `${this.baseUrl}${path4}`;
329
432
  const headers = {
330
433
  "Authorization": `Bearer ${this.apiKey}`,
331
434
  "Content-Type": "application/json"
@@ -464,39 +567,56 @@ async function createProject(name, opts) {
464
567
  const result = await client.putProject(name, {
465
568
  displayName: opts.displayName,
466
569
  canonicalDomain: opts.domain,
570
+ ownedDomains: opts.ownedDomains ?? [],
467
571
  country: opts.country,
468
572
  language: opts.language
469
573
  });
470
574
  console.log(`Project created: ${result.name} (${result.id})`);
471
575
  }
472
- async function listProjects() {
576
+ async function listProjects(format) {
473
577
  const client = getClient();
474
578
  const projects = await client.listProjects();
579
+ if (format === "json") {
580
+ console.log(JSON.stringify(projects, null, 2));
581
+ return;
582
+ }
475
583
  if (projects.length === 0) {
476
584
  console.log("No projects found.");
477
585
  return;
478
586
  }
479
587
  console.log("Projects:\n");
480
588
  const nameWidth = Math.max(4, ...projects.map((p) => p.name.length));
481
- const domainWidth = Math.max(6, ...projects.map((p) => p.canonicalDomain.length));
589
+ const domainLabel = (p) => {
590
+ const extra = Math.max(0, effectiveDomains(p).length - 1);
591
+ return extra > 0 ? `${p.canonicalDomain} (+${extra})` : p.canonicalDomain;
592
+ };
593
+ const domainWidth = Math.max(6, ...projects.map((p) => domainLabel(p).length));
482
594
  console.log(
483
595
  ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
484
596
  );
485
597
  console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
486
598
  for (const p of projects) {
487
599
  console.log(
488
- ` ${p.name.padEnd(nameWidth)} ${p.canonicalDomain.padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
600
+ ` ${p.name.padEnd(nameWidth)} ${domainLabel(p).padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
489
601
  );
490
602
  }
491
603
  }
492
- async function showProject(name) {
604
+ async function showProject(name, format) {
493
605
  const client = getClient();
494
606
  const project = await client.getProject(name);
607
+ if (format === "json") {
608
+ console.log(JSON.stringify(project, null, 2));
609
+ return;
610
+ }
495
611
  console.log(`Project: ${project.displayName}
496
612
  `);
497
613
  console.log(` Name: ${project.name}`);
498
614
  console.log(` ID: ${project.id}`);
499
615
  console.log(` Domain: ${project.canonicalDomain}`);
616
+ const secondaryDomains = effectiveDomains(project).slice(1);
617
+ if (secondaryDomains.length > 0) {
618
+ console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
619
+ }
500
620
  console.log(` Country: ${project.country}`);
501
621
  console.log(` Language: ${project.language}`);
502
622
  console.log(` Config source: ${project.configSource}`);
@@ -507,6 +627,27 @@ async function showProject(name) {
507
627
  console.log(` Created: ${project.createdAt}`);
508
628
  console.log(` Updated: ${project.updatedAt}`);
509
629
  }
630
+ async function updateProjectSettings(name, opts) {
631
+ const client = getClient();
632
+ const project = await client.getProject(name);
633
+ let ownedDomains = opts.ownedDomains ?? project.ownedDomains ?? [];
634
+ if (opts.addOwnedDomain) {
635
+ const toAdd = opts.addOwnedDomain.filter((d) => !ownedDomains.includes(d));
636
+ ownedDomains = [...ownedDomains, ...toAdd];
637
+ }
638
+ if (opts.removeOwnedDomain) {
639
+ const toRemove = new Set(opts.removeOwnedDomain);
640
+ ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
641
+ }
642
+ const result = await client.putProject(name, {
643
+ displayName: opts.displayName ?? project.displayName,
644
+ canonicalDomain: opts.domain ?? project.canonicalDomain,
645
+ ownedDomains,
646
+ country: opts.country ?? project.country,
647
+ language: opts.language ?? project.language
648
+ });
649
+ console.log(`Project updated: ${result.name}`);
650
+ }
510
651
  async function deleteProject(name) {
511
652
  const client = getClient();
512
653
  await client.deleteProject(name);
@@ -514,7 +655,7 @@ async function deleteProject(name) {
514
655
  }
515
656
 
516
657
  // src/commands/keyword.ts
517
- import fs2 from "fs";
658
+ import fs3 from "fs";
518
659
  function getClient2() {
519
660
  const config = loadConfig();
520
661
  return new ApiClient(config.apiUrl, config.apiKey);
@@ -524,9 +665,13 @@ async function addKeywords(project, keywords) {
524
665
  await client.appendKeywords(project, keywords);
525
666
  console.log(`Added ${keywords.length} key phrase(s) to "${project}".`);
526
667
  }
527
- async function listKeywords(project) {
668
+ async function listKeywords(project, format) {
528
669
  const client = getClient2();
529
670
  const kws = await client.listKeywords(project);
671
+ if (format === "json") {
672
+ console.log(JSON.stringify(kws, null, 2));
673
+ return;
674
+ }
530
675
  if (kws.length === 0) {
531
676
  console.log(`No key phrases found for "${project}".`);
532
677
  return;
@@ -538,10 +683,10 @@ async function listKeywords(project) {
538
683
  }
539
684
  }
540
685
  async function importKeywords(project, filePath) {
541
- if (!fs2.existsSync(filePath)) {
686
+ if (!fs3.existsSync(filePath)) {
542
687
  throw new Error(`File not found: ${filePath}`);
543
688
  }
544
- const content = fs2.readFileSync(filePath, "utf-8");
689
+ const content = fs3.readFileSync(filePath, "utf-8");
545
690
  const keywords = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
546
691
  if (keywords.length === 0) {
547
692
  console.log("No key phrases found in file.");
@@ -582,9 +727,13 @@ async function addCompetitors(project, domains) {
582
727
  await client.putCompetitors(project, allDomains);
583
728
  console.log(`Added ${domains.length} competitor(s) to "${project}".`);
584
729
  }
585
- async function listCompetitors(project) {
730
+ async function listCompetitors(project, format) {
586
731
  const client = getClient3();
587
732
  const comps = await client.listCompetitors(project);
733
+ if (format === "json") {
734
+ console.log(JSON.stringify(comps, null, 2));
735
+ return;
736
+ }
588
737
  if (comps.length === 0) {
589
738
  console.log(`No competitors found for "${project}".`);
590
739
  return;
@@ -601,6 +750,7 @@ function getClient4() {
601
750
  const config = loadConfig();
602
751
  return new ApiClient(config.apiUrl, config.apiKey);
603
752
  }
753
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed"]);
604
754
  async function triggerRun(project, opts) {
605
755
  const client = getClient4();
606
756
  const body = {};
@@ -608,6 +758,21 @@ async function triggerRun(project, opts) {
608
758
  body.providers = [opts.provider];
609
759
  }
610
760
  const run = await client.triggerRun(project, body);
761
+ if (opts?.wait) {
762
+ process.stderr.write(`Run ${run.id} started`);
763
+ const result = await pollRun(client, run.id);
764
+ if (opts?.format === "json") {
765
+ console.log(JSON.stringify(result, null, 2));
766
+ } else {
767
+ process.stderr.write("\n");
768
+ printRunDetail(result);
769
+ }
770
+ return;
771
+ }
772
+ if (opts?.format === "json") {
773
+ console.log(JSON.stringify(run, null, 2));
774
+ return;
775
+ }
611
776
  console.log(`Run created: ${run.id}`);
612
777
  console.log(` Kind: ${run.kind}`);
613
778
  console.log(` Status: ${run.status}`);
@@ -615,9 +780,72 @@ async function triggerRun(project, opts) {
615
780
  console.log(` Provider: ${opts.provider}`);
616
781
  }
617
782
  }
618
- async function listRuns(project) {
783
+ async function triggerRunAll(opts) {
784
+ const client = getClient4();
785
+ const projects = await client.listProjects();
786
+ if (projects.length === 0) {
787
+ if (opts?.format === "json") {
788
+ console.log("[]");
789
+ } else {
790
+ console.log("No projects found.");
791
+ }
792
+ return;
793
+ }
794
+ const body = {};
795
+ if (opts?.provider) {
796
+ body.providers = [opts.provider];
797
+ }
798
+ const results = [];
799
+ for (const p of projects) {
800
+ try {
801
+ const run = await client.triggerRun(p.name, body);
802
+ results.push({ project: p.name, runId: run.id, status: run.status });
803
+ } catch (err) {
804
+ const msg = err instanceof Error ? err.message : String(err);
805
+ results.push({ project: p.name, runId: "", status: "error", error: msg });
806
+ }
807
+ }
808
+ if (opts?.wait) {
809
+ const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
810
+ if (pending.length > 0) {
811
+ process.stderr.write(`Waiting for ${pending.length} run(s)`);
812
+ await Promise.all(pending.map(async (r) => {
813
+ const final = await pollRun(client, r.runId);
814
+ r.status = final.status;
815
+ }));
816
+ process.stderr.write("\n");
817
+ }
818
+ }
819
+ if (opts?.format === "json") {
820
+ console.log(JSON.stringify(results, null, 2));
821
+ return;
822
+ }
823
+ console.log(`Triggered ${results.length} run(s):
824
+ `);
825
+ console.log(" PROJECT RUN ID STATUS");
826
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
827
+ for (const r of results) {
828
+ const proj = r.project.padEnd(31);
829
+ const id = (r.runId || "(failed)").padEnd(36);
830
+ console.log(` ${proj} ${id} ${r.status}`);
831
+ }
832
+ }
833
+ async function showRun(id, format) {
834
+ const client = getClient4();
835
+ const run = await client.getRun(id);
836
+ if (format === "json") {
837
+ console.log(JSON.stringify(run, null, 2));
838
+ return;
839
+ }
840
+ printRunDetail(run);
841
+ }
842
+ async function listRuns(project, format) {
619
843
  const client = getClient4();
620
844
  const runs = await client.listRuns(project);
845
+ if (format === "json") {
846
+ console.log(JSON.stringify(runs, null, 2));
847
+ return;
848
+ }
621
849
  if (runs.length === 0) {
622
850
  console.log(`No runs found for "${project}".`);
623
851
  return;
@@ -632,39 +860,77 @@ async function listRuns(project) {
632
860
  );
633
861
  }
634
862
  }
863
+ var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
864
+ async function pollRun(client, runId) {
865
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
866
+ for (; ; ) {
867
+ await new Promise((r) => setTimeout(r, 2e3));
868
+ if (Date.now() > deadline) {
869
+ throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS / 1e3}s`);
870
+ }
871
+ const run = await client.getRun(runId);
872
+ process.stderr.write(".");
873
+ if (TERMINAL_STATUSES.has(run.status)) {
874
+ return run;
875
+ }
876
+ }
877
+ }
878
+ function printRunDetail(run) {
879
+ console.log(`Run: ${run.id}`);
880
+ console.log(` Status: ${run.status}`);
881
+ console.log(` Kind: ${run.kind}`);
882
+ if (run.trigger) console.log(` Trigger: ${run.trigger}`);
883
+ if (run.startedAt) console.log(` Started: ${run.startedAt}`);
884
+ if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
885
+ if (run.createdAt) console.log(` Created: ${run.createdAt}`);
886
+ if (run.error) console.log(` Error: ${run.error}`);
887
+ const snapshots = run.snapshots;
888
+ if (snapshots && snapshots.length > 0) {
889
+ console.log(`
890
+ Snapshots: ${snapshots.length}`);
891
+ for (const s of snapshots) {
892
+ const state = s.citationState === "cited" ? " cited " : " not-cited";
893
+ console.log(` ${state} ${s.provider} ${s.keyword}`);
894
+ }
895
+ }
896
+ }
635
897
 
636
898
  // src/commands/status.ts
637
899
  function getClient5() {
638
900
  const config = loadConfig();
639
901
  return new ApiClient(config.apiUrl, config.apiKey);
640
902
  }
641
- async function showStatus(project) {
903
+ async function showStatus(project, format) {
642
904
  const client = getClient5();
643
905
  const projectData = await client.getProject(project);
906
+ let runs = [];
907
+ try {
908
+ runs = await client.listRuns(project);
909
+ } catch {
910
+ }
911
+ if (format === "json") {
912
+ console.log(JSON.stringify({ project: projectData, runs }, null, 2));
913
+ return;
914
+ }
644
915
  console.log(`Status: ${projectData.displayName} (${projectData.name})
645
916
  `);
646
917
  console.log(` Domain: ${projectData.canonicalDomain}`);
647
918
  console.log(` Country: ${projectData.country}`);
648
919
  console.log(` Language: ${projectData.language}`);
649
- try {
650
- const runs = await client.listRuns(project);
651
- if (runs.length > 0) {
652
- const latest = runs[runs.length - 1];
653
- console.log(`
920
+ if (runs.length > 0) {
921
+ const latest = runs[runs.length - 1];
922
+ console.log(`
654
923
  Latest run:`);
655
- console.log(` ID: ${latest.id}`);
656
- console.log(` Status: ${latest.status}`);
657
- console.log(` Created: ${latest.createdAt}`);
658
- if (latest.finishedAt) {
659
- console.log(` Finished: ${latest.finishedAt}`);
660
- }
661
- console.log(`
662
- Total runs: ${runs.length}`);
663
- } else {
664
- console.log('\n No runs yet. Use "canonry run" to trigger one.');
924
+ console.log(` ID: ${latest.id}`);
925
+ console.log(` Status: ${latest.status}`);
926
+ console.log(` Created: ${latest.createdAt}`);
927
+ if (latest.finishedAt) {
928
+ console.log(` Finished: ${latest.finishedAt}`);
665
929
  }
666
- } catch {
667
- console.log("\n Run info unavailable.");
930
+ console.log(`
931
+ Total runs: ${runs.length}`);
932
+ } else {
933
+ console.log('\n No runs yet. Use "canonry run" to trigger one.');
668
934
  }
669
935
  }
670
936
 
@@ -673,9 +939,13 @@ function getClient6() {
673
939
  const config = loadConfig();
674
940
  return new ApiClient(config.apiUrl, config.apiKey);
675
941
  }
676
- async function showEvidence(project) {
942
+ async function showEvidence(project, format) {
677
943
  const client = getClient6();
678
944
  const timeline = await client.getTimeline(project);
945
+ if (format === "json") {
946
+ console.log(JSON.stringify(timeline, null, 2));
947
+ return;
948
+ }
679
949
  if (timeline.length === 0) {
680
950
  console.log('No keyword evidence yet. Trigger a run first with "canonry run".');
681
951
  return;
@@ -700,10 +970,14 @@ function getClient7() {
700
970
  const config = loadConfig();
701
971
  return new ApiClient(config.apiUrl, config.apiKey);
702
972
  }
703
- async function showHistory(project) {
973
+ async function showHistory(project, format) {
704
974
  const client = getClient7();
705
975
  try {
706
976
  const entries = await client.getHistory(project);
977
+ if (format === "json") {
978
+ console.log(JSON.stringify(entries, null, 2));
979
+ return;
980
+ }
707
981
  if (entries.length === 0) {
708
982
  console.log(`No audit history for "${project}".`);
709
983
  return;
@@ -725,13 +999,13 @@ async function showHistory(project) {
725
999
  }
726
1000
 
727
1001
  // src/commands/apply.ts
728
- import fs3 from "fs";
1002
+ import fs4 from "fs";
729
1003
  import { parseAllDocuments } from "yaml";
730
1004
  async function applyConfig(filePath) {
731
- if (!fs3.existsSync(filePath)) {
1005
+ if (!fs4.existsSync(filePath)) {
732
1006
  throw new Error(`File not found: ${filePath}`);
733
1007
  }
734
- const content = fs3.readFileSync(filePath, "utf-8");
1008
+ const content = fs4.readFileSync(filePath, "utf-8");
735
1009
  const docs = parseAllDocuments(content);
736
1010
  const clientConfig = loadConfig();
737
1011
  const client = new ApiClient(clientConfig.apiUrl, clientConfig.apiKey);
@@ -789,10 +1063,17 @@ async function setProvider(name, opts) {
789
1063
  if (result.model) {
790
1064
  console.log(` Model: ${result.model}`);
791
1065
  }
1066
+ if (result.quota) {
1067
+ console.log(` Quota: ${result.quota.maxConcurrency} concurrent \xB7 ${result.quota.maxRequestsPerMinute}/min \xB7 ${result.quota.maxRequestsPerDay}/day`);
1068
+ }
792
1069
  }
793
- async function showSettings() {
1070
+ async function showSettings(format) {
794
1071
  const client = getClient8();
795
1072
  const settings = await client.getSettings();
1073
+ if (format === "json") {
1074
+ console.log(JSON.stringify(settings, null, 2));
1075
+ return;
1076
+ }
796
1077
  console.log("Provider settings:\n");
797
1078
  for (const provider of settings.providers) {
798
1079
  const status = provider.configured ? "configured" : "not configured";
@@ -822,9 +1103,13 @@ async function setSchedule(project, opts) {
822
1103
  console.log(`Schedule set for "${project}":`);
823
1104
  printSchedule(result);
824
1105
  }
825
- async function showSchedule(project) {
1106
+ async function showSchedule(project, format) {
826
1107
  const client = getClient9();
827
1108
  const result = await client.getSchedule(project);
1109
+ if (format === "json") {
1110
+ console.log(JSON.stringify(result, null, 2));
1111
+ return;
1112
+ }
828
1113
  printSchedule(result);
829
1114
  }
830
1115
  async function enableSchedule(project) {
@@ -884,9 +1169,13 @@ async function addNotification(project, opts) {
884
1169
  console.log(`Notification created for "${project}":`);
885
1170
  printNotification(result);
886
1171
  }
887
- async function listNotifications(project) {
1172
+ async function listNotifications(project, format) {
888
1173
  const client = getClient10();
889
1174
  const results = await client.listNotifications(project);
1175
+ if (format === "json") {
1176
+ console.log(JSON.stringify(results, null, 2));
1177
+ return;
1178
+ }
890
1179
  if (results.length === 0) {
891
1180
  console.log(`No notifications configured for "${project}"`);
892
1181
  return;
@@ -912,6 +1201,23 @@ async function testNotification(project, id) {
912
1201
  console.error(`Test webhook failed: HTTP ${result.status}`);
913
1202
  }
914
1203
  }
1204
+ var EVENT_DESCRIPTIONS = {
1205
+ "citation.lost": "A keyword lost its citation status",
1206
+ "citation.gained": "A keyword gained citation status",
1207
+ "run.completed": "A visibility run completed successfully",
1208
+ "run.failed": "A visibility run failed"
1209
+ };
1210
+ function listEvents(format) {
1211
+ const events = notificationEventSchema.options;
1212
+ if (format === "json") {
1213
+ console.log(JSON.stringify(events.map((e) => ({ event: e, description: EVENT_DESCRIPTIONS[e] ?? "" })), null, 2));
1214
+ return;
1215
+ }
1216
+ console.log("Available notification events:\n");
1217
+ for (const event of events) {
1218
+ console.log(` ${event.padEnd(20)} ${EVENT_DESCRIPTIONS[event] ?? ""}`);
1219
+ }
1220
+ }
915
1221
  function printNotification(n) {
916
1222
  console.log(` ID: ${n.id}`);
917
1223
  console.log(` Channel: ${n.channel}`);
@@ -987,8 +1293,11 @@ Usage:
987
1293
  canonry init [--force] Initialize config and database (interactive)
988
1294
  canonry init --gemini-key <key> Initialize non-interactively (also reads env vars)
989
1295
  canonry bootstrap [--force] Bootstrap config/database from env vars
990
- canonry serve Start the local server
1296
+ canonry serve Start the local server (foreground)
1297
+ canonry start Start the server as a background daemon
1298
+ canonry stop Stop the background daemon
991
1299
  canonry project create <name> Create a project
1300
+ canonry project update <name> Update project settings
992
1301
  canonry project list List all projects
993
1302
  canonry project show <name> Show project details
994
1303
  canonry project delete <name> Delete a project
@@ -1000,6 +1309,9 @@ Usage:
1000
1309
  canonry competitor list <project> List competitors
1001
1310
  canonry run <project> Trigger a run (all providers)
1002
1311
  canonry run <project> --provider <name> Trigger a run for a specific provider
1312
+ canonry run <project> --wait Trigger and wait for completion
1313
+ canonry run --all Trigger runs for all projects
1314
+ canonry run show <id> Show run details and snapshots
1003
1315
  canonry runs <project> List runs for a project
1004
1316
  canonry status <project> Show project summary
1005
1317
  canonry evidence <project> Show per-phrase results
@@ -1015,8 +1327,9 @@ Usage:
1015
1327
  canonry notify list <project> List notifications
1016
1328
  canonry notify remove <project> <id> Remove notification
1017
1329
  canonry notify test <project> <id> Send test webhook
1330
+ canonry notify events List available notification event types
1018
1331
  canonry settings Show active provider and quota settings
1019
- canonry settings provider <name> Update a provider config (--api-key, --base-url, --model)
1332
+ canonry settings provider <name> Update a provider config
1020
1333
  canonry telemetry status Show telemetry status
1021
1334
  canonry telemetry enable Enable anonymous telemetry
1022
1335
  canonry telemetry disable Disable anonymous telemetry
@@ -1032,19 +1345,37 @@ Options:
1032
1345
  --local-key <key> Local LLM API key (or LOCAL_API_KEY env var)
1033
1346
  --port <port> Server port (default: 4100)
1034
1347
  --host <host> Server bind address (default: 127.0.0.1)
1035
- --domain <domain> Canonical domain for project create
1348
+ --domain <domain> Canonical domain for project create/update
1349
+ --owned-domain <domain> Additional owned domain for citation matching (repeatable)
1350
+ --add-domain <domain> Add an owned domain (project update, repeatable)
1351
+ --remove-domain <domain> Remove an owned domain (project update, repeatable)
1352
+ --display-name <name> Display name for project create/update
1036
1353
  --country <code> Country code (default: US)
1037
1354
  --language <lang> Language code (default: en)
1038
- --provider <name> Provider to use (gemini, openai, claude)
1355
+ --provider <name> Provider to use (gemini, openai, claude, local)
1356
+ --format <fmt> Output format: text (default) or json
1357
+ --wait Wait for run to complete before returning
1358
+ --all Run all projects (with 'run' command)
1039
1359
  --include-results Include results in export
1040
1360
  --preset <preset> Schedule preset (daily, weekly, twice-daily, daily@HH, weekly@DAY)
1041
1361
  --cron <expr> Cron expression for schedule
1042
1362
  --timezone <tz> IANA timezone for schedule (default: UTC)
1043
1363
  --webhook <url> Webhook URL for notifications
1044
1364
  --events <list> Comma-separated notification events
1365
+ --api-key <key> Provider API key (settings provider)
1366
+ --base-url <url> Provider base URL (settings provider)
1367
+ --model <name> Provider model name (settings provider)
1368
+ --max-concurrent <n> Max concurrent requests per provider
1369
+ --max-per-minute <n> Max requests per minute per provider
1370
+ --max-per-day <n> Max requests per day per provider
1045
1371
  `.trim();
1046
1372
  var _require = createRequire(import.meta.url);
1047
1373
  var { version: VERSION } = _require("../package.json");
1374
+ function extractFormat(cmdArgs) {
1375
+ const idx = cmdArgs.indexOf("--format");
1376
+ if (idx !== -1 && cmdArgs[idx + 1] === "json") return "json";
1377
+ return "text";
1378
+ }
1048
1379
  async function main() {
1049
1380
  const args = process.argv.slice(2);
1050
1381
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -1056,6 +1387,7 @@ async function main() {
1056
1387
  return;
1057
1388
  }
1058
1389
  const command = args[0];
1390
+ const format = extractFormat(args);
1059
1391
  if (command !== "telemetry" && command !== "init" && isTelemetryEnabled() && isFirstRun()) {
1060
1392
  showFirstRunNotice();
1061
1393
  getOrCreateAnonymousId();
@@ -1111,6 +1443,22 @@ async function main() {
1111
1443
  await serveCommand();
1112
1444
  break;
1113
1445
  }
1446
+ case "start": {
1447
+ const { values } = parseArgs({
1448
+ args: args.slice(1),
1449
+ options: {
1450
+ port: { type: "string", short: "p", default: "4100" },
1451
+ host: { type: "string", short: "H" }
1452
+ },
1453
+ allowPositionals: false
1454
+ });
1455
+ await startDaemon({ port: values.port, host: values.host });
1456
+ break;
1457
+ }
1458
+ case "stop": {
1459
+ stopDaemon();
1460
+ break;
1461
+ }
1114
1462
  case "project": {
1115
1463
  const subcommand = args[1];
1116
1464
  switch (subcommand) {
@@ -1124,22 +1472,56 @@ async function main() {
1124
1472
  args: args.slice(3),
1125
1473
  options: {
1126
1474
  domain: { type: "string", short: "d" },
1475
+ "owned-domain": { type: "string", multiple: true },
1127
1476
  country: { type: "string", default: "US" },
1128
1477
  language: { type: "string", default: "en" },
1129
- "display-name": { type: "string" }
1478
+ "display-name": { type: "string" },
1479
+ format: { type: "string" }
1130
1480
  },
1131
1481
  allowPositionals: false
1132
1482
  });
1133
1483
  await createProject(name, {
1134
1484
  domain: values.domain ?? name,
1485
+ ownedDomains: values["owned-domain"] ?? [],
1135
1486
  country: values.country ?? "US",
1136
1487
  language: values.language ?? "en",
1137
1488
  displayName: values["display-name"] ?? name
1138
1489
  });
1139
1490
  break;
1140
1491
  }
1492
+ case "update": {
1493
+ const name = args[2];
1494
+ if (!name) {
1495
+ console.error("Error: project name is required");
1496
+ process.exit(1);
1497
+ }
1498
+ const { values } = parseArgs({
1499
+ args: args.slice(3),
1500
+ options: {
1501
+ domain: { type: "string", short: "d" },
1502
+ "owned-domain": { type: "string", multiple: true },
1503
+ "add-domain": { type: "string", multiple: true },
1504
+ "remove-domain": { type: "string", multiple: true },
1505
+ country: { type: "string" },
1506
+ language: { type: "string" },
1507
+ "display-name": { type: "string" },
1508
+ format: { type: "string" }
1509
+ },
1510
+ allowPositionals: false
1511
+ });
1512
+ await updateProjectSettings(name, {
1513
+ displayName: values["display-name"],
1514
+ domain: values.domain,
1515
+ ownedDomains: values["owned-domain"],
1516
+ addOwnedDomain: values["add-domain"],
1517
+ removeOwnedDomain: values["remove-domain"],
1518
+ country: values.country,
1519
+ language: values.language
1520
+ });
1521
+ break;
1522
+ }
1141
1523
  case "list":
1142
- await listProjects();
1524
+ await listProjects(format);
1143
1525
  break;
1144
1526
  case "show": {
1145
1527
  const name = args[2];
@@ -1147,7 +1529,7 @@ async function main() {
1147
1529
  console.error("Error: project name is required");
1148
1530
  process.exit(1);
1149
1531
  }
1150
- await showProject(name);
1532
+ await showProject(name, format);
1151
1533
  break;
1152
1534
  }
1153
1535
  case "delete": {
@@ -1161,7 +1543,7 @@ async function main() {
1161
1543
  }
1162
1544
  default:
1163
1545
  console.error(`Unknown project subcommand: ${subcommand ?? "(none)"}`);
1164
- console.log("Available: create, list, show, delete");
1546
+ console.log("Available: create, update, list, show, delete");
1165
1547
  process.exit(1);
1166
1548
  }
1167
1549
  break;
@@ -1171,7 +1553,7 @@ async function main() {
1171
1553
  switch (subcommand) {
1172
1554
  case "add": {
1173
1555
  const project = args[2];
1174
- const kws = args.slice(3);
1556
+ const kws = args.slice(3).filter((a, i, arr) => !a.startsWith("--") && !(i > 0 && arr[i - 1].startsWith("--")));
1175
1557
  if (!project || kws.length === 0) {
1176
1558
  console.error("Error: project name and at least one key phrase required");
1177
1559
  process.exit(1);
@@ -1185,7 +1567,7 @@ async function main() {
1185
1567
  console.error("Error: project name is required");
1186
1568
  process.exit(1);
1187
1569
  }
1188
- await listKeywords(project);
1570
+ await listKeywords(project, format);
1189
1571
  break;
1190
1572
  }
1191
1573
  case "import": {
@@ -1209,7 +1591,8 @@ async function main() {
1209
1591
  options: {
1210
1592
  provider: { type: "string" },
1211
1593
  count: { type: "string" },
1212
- save: { type: "boolean", default: false }
1594
+ save: { type: "boolean", default: false },
1595
+ format: { type: "string" }
1213
1596
  },
1214
1597
  allowPositionals: false
1215
1598
  });
@@ -1235,7 +1618,7 @@ async function main() {
1235
1618
  switch (subcommand) {
1236
1619
  case "add": {
1237
1620
  const project = args[2];
1238
- const domains = args.slice(3);
1621
+ const domains = args.slice(3).filter((a, i, arr) => !a.startsWith("--") && !(i > 0 && arr[i - 1].startsWith("--")));
1239
1622
  if (!project || domains.length === 0) {
1240
1623
  console.error("Error: project name and at least one domain required");
1241
1624
  process.exit(1);
@@ -1249,7 +1632,7 @@ async function main() {
1249
1632
  console.error("Error: project name is required");
1250
1633
  process.exit(1);
1251
1634
  }
1252
- await listCompetitors(project);
1635
+ await listCompetitors(project, format);
1253
1636
  break;
1254
1637
  }
1255
1638
  default:
@@ -1260,19 +1643,48 @@ async function main() {
1260
1643
  break;
1261
1644
  }
1262
1645
  case "run": {
1263
- const project = args[1];
1264
- if (!project) {
1265
- console.error("Error: project name is required");
1266
- process.exit(1);
1646
+ if (args[1] === "show") {
1647
+ const id = args[2];
1648
+ if (!id) {
1649
+ console.error("Error: run ID is required");
1650
+ process.exit(1);
1651
+ }
1652
+ await showRun(id, format);
1653
+ break;
1267
1654
  }
1268
1655
  const runParsed = parseArgs({
1269
- args: args.slice(2),
1656
+ args: args.slice(1),
1270
1657
  options: {
1271
- provider: { type: "string" }
1658
+ provider: { type: "string" },
1659
+ wait: { type: "boolean", default: false },
1660
+ all: { type: "boolean", default: false },
1661
+ format: { type: "string" }
1272
1662
  },
1273
- allowPositionals: false
1663
+ allowPositionals: true
1274
1664
  });
1275
- await triggerRun(project, { provider: runParsed.values.provider });
1665
+ const runFormat = runParsed.values.format === "json" ? "json" : format;
1666
+ if (runParsed.values.all) {
1667
+ if (runParsed.positionals.length > 0) {
1668
+ console.error("Error: --all cannot be combined with a project name");
1669
+ process.exit(1);
1670
+ }
1671
+ await triggerRunAll({
1672
+ provider: runParsed.values.provider,
1673
+ wait: runParsed.values.wait,
1674
+ format: runFormat
1675
+ });
1676
+ } else {
1677
+ const project = runParsed.positionals[0];
1678
+ if (!project) {
1679
+ console.error("Error: project name is required (or use --all)");
1680
+ process.exit(1);
1681
+ }
1682
+ await triggerRun(project, {
1683
+ provider: runParsed.values.provider,
1684
+ wait: runParsed.values.wait,
1685
+ format: runFormat
1686
+ });
1687
+ }
1276
1688
  break;
1277
1689
  }
1278
1690
  case "runs": {
@@ -1281,7 +1693,7 @@ async function main() {
1281
1693
  console.error("Error: project name is required");
1282
1694
  process.exit(1);
1283
1695
  }
1284
- await listRuns(project);
1696
+ await listRuns(project, format);
1285
1697
  break;
1286
1698
  }
1287
1699
  case "status": {
@@ -1290,7 +1702,7 @@ async function main() {
1290
1702
  console.error("Error: project name is required");
1291
1703
  process.exit(1);
1292
1704
  }
1293
- await showStatus(project);
1705
+ await showStatus(project, format);
1294
1706
  break;
1295
1707
  }
1296
1708
  case "evidence": {
@@ -1299,7 +1711,7 @@ async function main() {
1299
1711
  console.error("Error: project name is required");
1300
1712
  process.exit(1);
1301
1713
  }
1302
- await showEvidence(project);
1714
+ await showEvidence(project, format);
1303
1715
  break;
1304
1716
  }
1305
1717
  case "history": {
@@ -1308,7 +1720,7 @@ async function main() {
1308
1720
  console.error("Error: project name is required");
1309
1721
  process.exit(1);
1310
1722
  }
1311
- await showHistory(project);
1723
+ await showHistory(project, format);
1312
1724
  break;
1313
1725
  }
1314
1726
  case "export": {
@@ -1357,7 +1769,8 @@ async function main() {
1357
1769
  preset: { type: "string" },
1358
1770
  cron: { type: "string" },
1359
1771
  timezone: { type: "string" },
1360
- provider: { type: "string", multiple: true }
1772
+ provider: { type: "string", multiple: true },
1773
+ format: { type: "string" }
1361
1774
  },
1362
1775
  allowPositionals: false
1363
1776
  });
@@ -1374,7 +1787,7 @@ async function main() {
1374
1787
  break;
1375
1788
  }
1376
1789
  case "show":
1377
- await showSchedule(schedProject);
1790
+ await showSchedule(schedProject, format);
1378
1791
  break;
1379
1792
  case "enable":
1380
1793
  await enableSchedule(schedProject);
@@ -1394,6 +1807,10 @@ async function main() {
1394
1807
  }
1395
1808
  case "notify": {
1396
1809
  const notifSubcommand = args[1];
1810
+ if (notifSubcommand === "events") {
1811
+ listEvents(format);
1812
+ break;
1813
+ }
1397
1814
  const notifProject = args[2];
1398
1815
  if (!notifProject && notifSubcommand !== void 0) {
1399
1816
  console.error("Error: project name is required");
@@ -1405,7 +1822,8 @@ async function main() {
1405
1822
  args: args.slice(3),
1406
1823
  options: {
1407
1824
  webhook: { type: "string" },
1408
- events: { type: "string" }
1825
+ events: { type: "string" },
1826
+ format: { type: "string" }
1409
1827
  },
1410
1828
  allowPositionals: false
1411
1829
  });
@@ -1414,7 +1832,7 @@ async function main() {
1414
1832
  process.exit(1);
1415
1833
  }
1416
1834
  if (!values.events) {
1417
- console.error("Error: --events is required (comma-separated)");
1835
+ console.error('Error: --events is required (comma-separated). Use "canonry notify events" to see valid events.');
1418
1836
  process.exit(1);
1419
1837
  }
1420
1838
  await addNotification(notifProject, {
@@ -1424,7 +1842,7 @@ async function main() {
1424
1842
  break;
1425
1843
  }
1426
1844
  case "list":
1427
- await listNotifications(notifProject);
1845
+ await listNotifications(notifProject, format);
1428
1846
  break;
1429
1847
  case "remove": {
1430
1848
  const notifId = args[3];
@@ -1446,7 +1864,7 @@ async function main() {
1446
1864
  }
1447
1865
  default:
1448
1866
  console.error(`Unknown notify subcommand: ${notifSubcommand ?? "(none)"}`);
1449
- console.log("Available: add, list, remove, test");
1867
+ console.log("Available: add, list, remove, test, events");
1450
1868
  process.exit(1);
1451
1869
  }
1452
1870
  break;
@@ -1464,7 +1882,11 @@ async function main() {
1464
1882
  options: {
1465
1883
  "api-key": { type: "string" },
1466
1884
  "base-url": { type: "string" },
1467
- model: { type: "string" }
1885
+ model: { type: "string" },
1886
+ "max-concurrent": { type: "string" },
1887
+ "max-per-minute": { type: "string" },
1888
+ "max-per-day": { type: "string" },
1889
+ format: { type: "string" }
1468
1890
  },
1469
1891
  allowPositionals: false
1470
1892
  });
@@ -1479,9 +1901,18 @@ async function main() {
1479
1901
  process.exit(1);
1480
1902
  }
1481
1903
  }
1482
- await setProvider(name, { apiKey: values["api-key"], baseUrl: values["base-url"], model: values.model });
1904
+ const quota = {};
1905
+ if (values["max-concurrent"]) quota.maxConcurrency = parseInt(values["max-concurrent"], 10);
1906
+ if (values["max-per-minute"]) quota.maxRequestsPerMinute = parseInt(values["max-per-minute"], 10);
1907
+ if (values["max-per-day"]) quota.maxRequestsPerDay = parseInt(values["max-per-day"], 10);
1908
+ await setProvider(name, {
1909
+ apiKey: values["api-key"],
1910
+ baseUrl: values["base-url"],
1911
+ model: values.model,
1912
+ quota: Object.keys(quota).length > 0 ? quota : void 0
1913
+ });
1483
1914
  } else {
1484
- await showSettings();
1915
+ await showSettings(format);
1485
1916
  }
1486
1917
  break;
1487
1918
  }