@canonry/canonry 7.77.1 → 7.78.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.
Files changed (139) hide show
  1. package/assets/agent-workspace/skills/aero/references/seo-analysis.md +13 -2
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -2
  3. package/assets/assets/{AgentSetupPage-BZXWC3x6.js → AgentSetupPage-BGdvyQFL.js} +4 -4
  4. package/assets/assets/{AuditHistoryPanel-DcrUuwRu.js → AuditHistoryPanel-Bb5wq-Bk.js} +1 -1
  5. package/assets/assets/{BacklinksPage-BSpHJBiy.js → BacklinksPage-i1cfxTDj.js} +1 -1
  6. package/assets/assets/{HistoryPage-0a0CK9zF.js → HistoryPage-C2NRdNrn.js} +1 -1
  7. package/assets/assets/MeasurementPropertyPage-CwPuJMBy.js +4 -0
  8. package/assets/assets/ProjectPage-Bbnbg7c9.js +11 -0
  9. package/assets/assets/{RunsPage-D0vbz1Ho.js → RunsPage-CJHJ7kHk.js} +1 -1
  10. package/assets/assets/{SettingsPage-DjRm5cmn.js → SettingsPage-r8VrtFyJ.js} +1 -1
  11. package/assets/assets/SiteHealthSection-D1oSuo9O.js +2 -0
  12. package/assets/assets/ToneBadge-CUe-WrzN.js +60 -0
  13. package/assets/assets/{TrafficPage-Z8Z_z0k9.js → TrafficPage-DY2ykwH1.js} +1 -1
  14. package/assets/assets/TrafficSourceDetailPage-D3KHgRvF.js +1 -0
  15. package/assets/assets/{account-context-zaSxLBst.js → account-context-B50QqDEA.js} +1 -1
  16. package/assets/assets/{card-COqALGzB.js → card-BE2fqwVq.js} +1 -1
  17. package/assets/assets/{extract-error-message-DKve0SNr.js → extract-error-message-xuN_xeRJ.js} +1 -1
  18. package/assets/assets/{format-helpers-DKrHlaV7.js → format-helpers-CiC6gALY.js} +1 -1
  19. package/assets/assets/{index-CKZK95pI.css → index-CHHZDc6d.css} +1 -1
  20. package/assets/assets/{index-fzqtwr6m.js → index-Dfm8z5cG.js} +23 -23
  21. package/assets/assets/{query-client-COM9uzxL.js → query-client-C5GhDMY7.js} +1 -1
  22. package/assets/assets/react-query-CmHyfYFq.js +1 -0
  23. package/assets/assets/{react-sigma_core.esm.min-e91U3Myi.js → react-sigma_core.esm.min-Bjdtl0TQ.js} +1 -1
  24. package/assets/assets/{service-BgFTYqJA.js → service-B2i_ZzWP.js} +1 -1
  25. package/assets/index.html +8 -8
  26. package/dist/{ads-M6IAQDG7.js → ads-MIR5MCHL.js} +3 -3
  27. package/dist/{agent-X2T5X5K3.js → agent-IXRL5RBI.js} +7 -7
  28. package/dist/{agent-ask-55LQH4TI.js → agent-ask-K42H6H2T.js} +3 -3
  29. package/dist/{agent-conversations-O6WC2NPV.js → agent-conversations-2T3W4DHO.js} +3 -3
  30. package/dist/{agent-memory-6DWFMB4I.js → agent-memory-LJBNSTSP.js} +3 -3
  31. package/dist/{agent-providers-Q4YDVQX3.js → agent-providers-QR4LY3AP.js} +3 -3
  32. package/dist/{agent-transcript-SJKCP2AO.js → agent-transcript-JIHJWHV6.js} +3 -3
  33. package/dist/{analytics-4JOQMWH7.js → analytics-JRV7DBBU.js} +3 -3
  34. package/dist/{apply-EAKKKIZD.js → apply-ZFWDSAV4.js} +3 -3
  35. package/dist/{backfill-PASKIVGP.js → backfill-LU2AQIPY.js} +9 -9
  36. package/dist/{backlinks-TICPC323.js → backlinks-NAC2373F.js} +3 -3
  37. package/dist/{bing-MC7TSL4E.js → bing-L3LDBJOC.js} +3 -3
  38. package/dist/{bootstrap-X2BW64W7.js → bootstrap-I6DA4KWB.js} +10 -10
  39. package/dist/{cdp-TCJKITCR.js → cdp-4F74XLTH.js} +7 -7
  40. package/dist/{chunk-CCNPBUQ5.js → chunk-367K6XVX.js} +2 -2
  41. package/dist/{chunk-JPOIDGGF.js → chunk-3MGM3U6V.js} +1 -1
  42. package/dist/{chunk-2BDWXHYF.js → chunk-45RGH7XI.js} +3 -3
  43. package/dist/{chunk-RPJJOBKN.js → chunk-6ACWXQNZ.js} +3 -1
  44. package/dist/{chunk-B7JHQ3FW.js → chunk-6P6YBNKN.js} +7 -7
  45. package/dist/{chunk-YMOQSEO3.js → chunk-6PRFVLIK.js} +15 -4
  46. package/dist/{chunk-D5XPB7MM.js → chunk-6VBLPCCX.js} +1 -1
  47. package/dist/{chunk-5QR7D4RQ.js → chunk-7HB6QLX4.js} +1 -1
  48. package/dist/{chunk-7UVCZZQJ.js → chunk-AI44ZCMK.js} +1 -1
  49. package/dist/{chunk-X7UC755Q.js → chunk-C5WSQXP5.js} +26 -2
  50. package/dist/{chunk-CQMNWC5K.js → chunk-EFMZSYRD.js} +160 -26
  51. package/dist/{chunk-FXIZBJZ4.js → chunk-EHXNYXAS.js} +3 -3
  52. package/dist/{chunk-2WBT6TEJ.js → chunk-FCYGY4FT.js} +1 -1
  53. package/dist/{chunk-XYYWKCJQ.js → chunk-JERATA5R.js} +1 -1
  54. package/dist/{chunk-P3BWUKJL.js → chunk-JEUL5QZP.js} +4 -4
  55. package/dist/{chunk-PSNTEGNY.js → chunk-KZB6HLFT.js} +1 -1
  56. package/dist/{chunk-UGNNR7EO.js → chunk-L2KMR7CM.js} +2 -2
  57. package/dist/{chunk-H4ANN32B.js → chunk-L6F2HOQH.js} +8 -8
  58. package/dist/{chunk-GDKMVOZI.js → chunk-M3BUP6XD.js} +104 -13
  59. package/dist/{chunk-4QHTXT4A.js → chunk-MUYTDD35.js} +40 -4
  60. package/dist/{chunk-TESDUMVK.js → chunk-O4WBYEGV.js} +1 -1
  61. package/dist/{chunk-FCGYYZHL.js → chunk-PM2DB3HE.js} +164 -10
  62. package/dist/{chunk-TRW4XLQK.js → chunk-QXSOXDUR.js} +2 -2
  63. package/dist/{chunk-2SCPPWRW.js → chunk-RSVANBNW.js} +4 -4
  64. package/dist/{chunk-UB65XBH2.js → chunk-SAHH2FXO.js} +3 -3
  65. package/dist/{chunk-46KV7CZU.js → chunk-SOQVI7EZ.js} +2 -2
  66. package/dist/{chunk-Z2FY6AGZ.js → chunk-TZRYVW75.js} +2 -2
  67. package/dist/{chunk-C3UFOS7J.js → chunk-UH44UCRM.js} +2 -2
  68. package/dist/{chunk-TJYS2XCI.js → chunk-UQOC5ZQE.js} +4 -4
  69. package/dist/{chunk-2BRVGCVU.js → chunk-VLOCDWCS.js} +7 -1
  70. package/dist/{chunk-6EZQHF67.js → chunk-WCFANFR2.js} +371 -340
  71. package/dist/{chunk-ZXYCPRNY.js → chunk-WSWHLKKJ.js} +1 -1
  72. package/dist/{chunk-NDTU7FGL.js → chunk-WVCDUKJ2.js} +1 -1
  73. package/dist/{chunk-JZT3ZRD3.js → chunk-WXIL2EFM.js} +4 -4
  74. package/dist/{chunk-JSCO223C.js → chunk-ZHFE6J3A.js} +1 -1
  75. package/dist/{chunk-HD2VVIPD.js → chunk-ZUC7CJVD.js} +5 -5
  76. package/dist/{citations-IA3OCV53.js → citations-2XELMCCV.js} +3 -3
  77. package/dist/{cli-catalog-ZVFLJXID.js → cli-catalog-KC6JYHSW.js} +3 -3
  78. package/dist/{cli-commands-O7BYY3W5.js → cli-commands-WCOAB6XU.js} +7 -7
  79. package/dist/cli.js +13 -13
  80. package/dist/{competitor-4FVAZAQZ.js → competitor-IK4GYTZO.js} +3 -3
  81. package/dist/{content-OVBF6U7Z.js → content-SMQPJXPC.js} +3 -3
  82. package/dist/{dashboard-UGZSSIWK.js → dashboard-EE24ROVG.js} +6 -6
  83. package/dist/{demo-5CU3JDHC.js → demo-7XH2N65A.js} +1 -1
  84. package/dist/{demo-server-43QV3UKM.js → demo-server-CVB65UYW.js} +12 -12
  85. package/dist/{discover-R5BYAPOU.js → discover-3KHUCVE7.js} +3 -3
  86. package/dist/{discover-eval-3FA4BR2L.js → discover-eval-DOEP4ZQ7.js} +3 -3
  87. package/dist/{doctor-2Q5LVV2P.js → doctor-3T6RKSE7.js} +3 -3
  88. package/dist/{evidence-LMGRT5IO.js → evidence-4NFL5KM3.js} +3 -3
  89. package/dist/{export-cmd-LBLZ273Z.js → export-cmd-BQC6XLJ7.js} +3 -3
  90. package/dist/{ga-PBIWRU67.js → ga-HBRA4UUK.js} +3 -3
  91. package/dist/{gbp-NYG7SW6P.js → gbp-2WMBTDJV.js} +3 -3
  92. package/dist/{google-57XEYVOU.js → google-JFUE4BNJ.js} +4 -4
  93. package/dist/{google-marketing-XPE6CVQI.js → google-marketing-5ROIKBC7.js} +1 -1
  94. package/dist/{health-cmd-SJVYQZQL.js → health-cmd-7BMVCCG2.js} +3 -3
  95. package/dist/{history-5XYZNWGP.js → history-QA4ZXCVH.js} +3 -3
  96. package/dist/index.d.ts +216 -23
  97. package/dist/index.js +26 -26
  98. package/dist/{init-KXR6PJEQ.js → init-O4G2MDMC.js} +12 -12
  99. package/dist/{insights-L7OOH34I.js → insights-K4UX2PSI.js} +3 -3
  100. package/dist/intelligence-service-D7DS4UKT.js +16 -0
  101. package/dist/{journeys-VXBGDFBO.js → journeys-UWEWJXGT.js} +3 -3
  102. package/dist/{keys-YMB5HAAG.js → keys-CL5YYE7Q.js} +3 -3
  103. package/dist/{keyword-RZEMKH3K.js → keyword-IFCC5IZT.js} +3 -3
  104. package/dist/{logs-ONZWSYXS.js → logs-2A3HBUBK.js} +3 -3
  105. package/dist/{mcp-KK6EFHAZ.js → mcp-B6L7PKZ4.js} +4 -4
  106. package/dist/mcp.js +10 -10
  107. package/dist/{measurement-plan-LE7INWW6.js → measurement-plan-GYSO6CXX.js} +5 -5
  108. package/dist/{notify-T6T5LYRR.js → notify-XM23I2B3.js} +3 -3
  109. package/dist/{organic-evidence-P2GZ766W.js → organic-evidence-U7HZMXKP.js} +3 -3
  110. package/dist/{overview-P2AILIWJ.js → overview-TSAK5DQP.js} +4 -4
  111. package/dist/{project-7MHQSOJD.js → project-L7DLDEPI.js} +3 -3
  112. package/dist/{query-EUDMU6L7.js → query-FM5HDHO4.js} +3 -3
  113. package/dist/{research-4DASCVGY.js → research-7G5JU44N.js} +3 -3
  114. package/dist/{results-export-GKXHX55L.js → results-export-JJDGG6ZP.js} +3 -3
  115. package/dist/{run-6LJGMJ5S.js → run-ZVIQVG23.js} +4 -4
  116. package/dist/{schedule-D74P2BO5.js → schedule-TV7LWSYI.js} +3 -3
  117. package/dist/{search-W7AOYWRM.js → search-7KRZLZ6S.js} +3 -3
  118. package/dist/{serve-UNVCQSRC.js → serve-WSOG525W.js} +27 -27
  119. package/dist/{session-6NGNVCKP.js → session-OUGQSGVV.js} +14 -14
  120. package/dist/{settings-LGD64PFE.js → settings-AOXQJOHN.js} +3 -3
  121. package/dist/{skills-CYHWYN42.js → skills-DWXA55QA.js} +2 -2
  122. package/dist/{snapshot-PDYGSZZH.js → snapshot-B44CXKFU.js} +3 -3
  123. package/dist/{src-SXB3MOP3.js → src-IBGSUYEG.js} +2 -2
  124. package/dist/{status-UZ4SE7ZP.js → status-RNBSFZ6Q.js} +4 -4
  125. package/dist/{technical-aeo-BNH7NV44.js → technical-aeo-UAOKK5QH.js} +3 -3
  126. package/dist/{telemetry-UWDS7QNJ.js → telemetry-55PUOMA7.js} +4 -4
  127. package/dist/{tools-EDPBQCI2.js → tools-HRGP4FXN.js} +11 -11
  128. package/dist/{traffic-NI6ACQO4.js → traffic-M4OHG5JW.js} +5 -5
  129. package/dist/{users-XTZ65NLC.js → users-GU7MULRM.js} +3 -3
  130. package/dist/{visibility-stats-EPF6MHYQ.js → visibility-stats-4IDTDXDC.js} +3 -3
  131. package/dist/{wordpress-ZACJ3H5M.js → wordpress-CYXB4R5Y.js} +3 -3
  132. package/package.json +1 -1
  133. package/assets/assets/MeasurementPropertyPage-DbfTFN3F.js +0 -4
  134. package/assets/assets/ProjectPage-D0m-Xpp3.js +0 -9
  135. package/assets/assets/SiteHealthSection-BCdPVFEJ.js +0 -2
  136. package/assets/assets/ToneBadge-DSJSzqI6.js +0 -60
  137. package/assets/assets/TrafficSourceDetailPage-DJyDO527.js +0 -1
  138. package/assets/assets/react-query-BVRWpXWq.js +0 -1
  139. package/dist/intelligence-service-NXZYQJBH.js +0 -16
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  registeredProviderNames
3
- } from "./chunk-2WBT6TEJ.js";
3
+ } from "./chunk-FCYGY4FT.js";
4
4
  import {
5
5
  configExists,
6
6
  loadConfigRaw
7
- } from "./chunk-2BRVGCVU.js";
7
+ } from "./chunk-VLOCDWCS.js";
8
8
 
9
9
  // src/setup-state.ts
10
10
  import fs from "fs";
@@ -28,7 +28,7 @@ async function buildSetupState() {
28
28
  }
29
29
  if (dbPath && fs.existsSync(dbPath)) {
30
30
  try {
31
- const { createClient, projects, queries: queriesTable } = await import("./src-SXB3MOP3.js");
31
+ const { createClient, projects, queries: queriesTable } = await import("./src-IBGSUYEG.js");
32
32
  const db = createClient(dbPath);
33
33
  project_count = db.select({ id: projects.id }).from(projects).all().length;
34
34
  const firstQuery = db.select({ id: queriesTable.id }).from(queriesTable).limit(1).all();
@@ -47,7 +47,7 @@ function queueServeHandoff() {
47
47
  async function runPendingServeHandoff(format) {
48
48
  if (!pendingServeHandoff) return;
49
49
  pendingServeHandoff = false;
50
- const { serveCommand } = await import("./serve-UNVCQSRC.js");
50
+ const { serveCommand } = await import("./serve-WSOG525W.js");
51
51
  await serveCommand(format);
52
52
  }
53
53
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cloudflareQueueNameSchema
3
- } from "./chunk-FCGYYZHL.js";
3
+ } from "./chunk-PM2DB3HE.js";
4
4
 
5
5
  // src/cloudflare-traffic-config.ts
6
6
  function unsupportedDeliveryMode(deliveryMode) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-X7UC755Q.js";
3
+ } from "./chunk-C5WSQXP5.js";
4
4
  import {
5
5
  emitJsonl
6
6
  } from "./chunk-Z7IUAJOF.js";
@@ -19,7 +19,7 @@ import {
19
19
  formatMicros,
20
20
  formatRunErrorOneLine,
21
21
  resolveProviderInput
22
- } from "./chunk-FCGYYZHL.js";
22
+ } from "./chunk-PM2DB3HE.js";
23
23
 
24
24
  // src/commands/run.ts
25
25
  function getClient() {
@@ -7,27 +7,27 @@ import {
7
7
  reparseStoredResult4,
8
8
  reparseStoredResult5,
9
9
  resolveStoredApiResponse
10
- } from "./chunk-C3UFOS7J.js";
10
+ } from "./chunk-UH44UCRM.js";
11
11
  import {
12
12
  loadConfig
13
- } from "./chunk-2BRVGCVU.js";
13
+ } from "./chunk-VLOCDWCS.js";
14
14
  import {
15
15
  isMachineFormat
16
16
  } from "./chunk-HGMYEDAQ.js";
17
17
  import {
18
18
  determineCitationState
19
- } from "./chunk-XYYWKCJQ.js";
19
+ } from "./chunk-JERATA5R.js";
20
20
  import {
21
21
  backfillProjectAnswerMentions,
22
22
  competitorIdentityColumns,
23
23
  createRunCompetitorResolver,
24
24
  readMarketCompetitorNames,
25
25
  refreshSnapshotMentionIdentity
26
- } from "./chunk-GDKMVOZI.js";
26
+ } from "./chunk-M3BUP6XD.js";
27
27
  import {
28
28
  classifyAiUserFetch,
29
29
  classifyCrawler
30
- } from "./chunk-TESDUMVK.js";
30
+ } from "./chunk-O4WBYEGV.js";
31
31
  import {
32
32
  aiUserFetchEventsHourly,
33
33
  auditLog,
@@ -42,7 +42,7 @@ import {
42
42
  querySnapshots,
43
43
  rawEventSamples,
44
44
  runs
45
- } from "./chunk-D5XPB7MM.js";
45
+ } from "./chunk-6VBLPCCX.js";
46
46
  import {
47
47
  ProviderNames,
48
48
  RetrievalContracts,
@@ -59,7 +59,7 @@ import {
59
59
  extractRecommendedCompetitors,
60
60
  normalizeUrlPath,
61
61
  openRouterRouteModel
62
- } from "./chunk-FCGYYZHL.js";
62
+ } from "./chunk-PM2DB3HE.js";
63
63
 
64
64
  // src/commands/backfill.ts
65
65
  import { setImmediate as yieldToEventLoop } from "timers/promises";
@@ -437,7 +437,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
437
437
  }
438
438
  }
439
439
  async function backfillInsightsCommand(project, opts) {
440
- const { IntelligenceService } = await import("./intelligence-service-NXZYQJBH.js");
440
+ const { IntelligenceService } = await import("./intelligence-service-D7DS4UKT.js");
441
441
  const config = loadConfig();
442
442
  const db = createClient(config.database);
443
443
  migrate(db);
@@ -9,14 +9,14 @@ import {
9
9
  normalizeCloudflareWorkerEvent,
10
10
  sessionizeJourneyRequests,
11
11
  verifyRequestSignature
12
- } from "./chunk-TESDUMVK.js";
12
+ } from "./chunk-O4WBYEGV.js";
13
13
  import {
14
14
  currentOutcomeAttribution,
15
15
  getRequestContext,
16
16
  registerRequestContext,
17
17
  requestAuditIdentity,
18
18
  sanitizeRequestContext
19
- } from "./chunk-JPOIDGGF.js";
19
+ } from "./chunk-3MGM3U6V.js";
20
20
  import {
21
21
  SentimentIdempotencyConflict,
22
22
  SentimentRepository,
@@ -157,7 +157,7 @@ import {
157
157
  userSessions,
158
158
  users,
159
159
  writeNegativeReviewMaxStars
160
- } from "./chunk-D5XPB7MM.js";
160
+ } from "./chunk-6VBLPCCX.js";
161
161
  import {
162
162
  ADS_ACTIVATE_SCOPE,
163
163
  ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
@@ -371,6 +371,8 @@ import {
371
371
  SeoAdapterOutcomes,
372
372
  SeoAvailability,
373
373
  SeoCapabilities,
374
+ SeoKeywordGapKinds,
375
+ SeoKeywordGapModes,
374
376
  SeoMarketRankingSearchStatuses,
375
377
  SeoMarketRankingSorts,
376
378
  SeoRunStatuses,
@@ -534,6 +536,8 @@ import {
534
536
  compactDateToIso,
535
537
  compareMarketRankingsBy,
536
538
  compareSemver,
539
+ compareSeoCompetitorsBy,
540
+ compareSeoKeywordGapsBy,
537
541
  compareText,
538
542
  competitorAliasBlockRequestSchema,
539
543
  competitorAliasProjectIdentity,
@@ -1058,9 +1062,16 @@ import {
1058
1062
  sentimentSettingsUpdateSchema,
1059
1063
  sentimentSummaryRequestSchema,
1060
1064
  sentimentSummarySchema,
1065
+ seoCompetitorListSchema,
1066
+ seoCompetitorQuerySchema,
1067
+ seoCompetitorSortSchema,
1061
1068
  seoContextSchema,
1062
1069
  seoDomainIntelligenceListSchema,
1063
1070
  seoEvidenceQuerySchema,
1071
+ seoKeywordGapKindSchema,
1072
+ seoKeywordGapListSchema,
1073
+ seoKeywordGapQuerySchema,
1074
+ seoKeywordGapSortSchema,
1064
1075
  seoKeywordMetricsListSchema,
1065
1076
  seoMarketRankingFilterSchema,
1066
1077
  seoMarketRankingListSchema,
@@ -1196,7 +1207,7 @@ import {
1196
1207
  wordpressSchemaDeployResultDtoSchema,
1197
1208
  wordpressSchemaStatusResultDtoSchema,
1198
1209
  wordpressStatusDtoSchema
1199
- } from "./chunk-FCGYYZHL.js";
1210
+ } from "./chunk-PM2DB3HE.js";
1200
1211
 
1201
1212
  // ../api-routes/src/user-session.ts
1202
1213
  import crypto2 from "crypto";
@@ -15435,7 +15446,9 @@ function resultRows(tasks) {
15435
15446
  keywordMetrics: kept.flatMap((task) => task.results.keywordMetrics),
15436
15447
  domainIntelligence: kept.flatMap((task) => task.results.domainIntelligence),
15437
15448
  rankedKeywords: kept.flatMap((task) => task.results.rankedKeywords),
15438
- marketRankings: tasks.flatMap(marketRankingRows)
15449
+ marketRankings: tasks.flatMap(marketRankingRows),
15450
+ competitors: kept.flatMap((task) => task.results.competitors ?? []),
15451
+ keywordGaps: kept.flatMap((task) => task.results.keywordGaps ?? [])
15439
15452
  };
15440
15453
  }
15441
15454
  function marketRankingRows(task) {
@@ -15471,14 +15484,14 @@ function scopedPlanTargets(plan, scope) {
15471
15484
  return { targets: plan.targets.filter((target) => targetKeys.includes(target.stableKey)), market, targetKeys };
15472
15485
  }
15473
15486
  function scopeCapabilities(kind, targets) {
15474
- if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"], SeoCapabilities["market-rankings"]];
15487
+ if (kind === SeoScopeKinds.project) return Object.values(SeoCapabilities);
15475
15488
  const exactHosts = targets.length > 0 && targets.every((target) => target.urlMatchers.length > 0 && target.urlMatchers.every((matcher) => matcher.kind === "host"));
15476
15489
  const markets = kind === SeoScopeKinds.group || kind === SeoScopeKinds.market ? [SeoCapabilities["market-rankings"]] : [];
15477
15490
  return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"], ...markets] : [SeoCapabilities["keyword-metrics"], ...markets];
15478
15491
  }
15479
15492
  function scopeReason(capabilities) {
15480
15493
  const markets = capabilities.includes(SeoCapabilities["market-rankings"]);
15481
- return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Whole-domain intelligence requires project scope.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Path and entity Properties cannot be replaced by whole-domain data.`;
15494
+ return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Use project scope for domain intelligence, competitor discovery and keyword gaps.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Use project scope for domain research; path and entity Properties cannot be replaced by whole-domain data.`;
15482
15495
  }
15483
15496
  function cancelSeoRefresh(db, projectId, id) {
15484
15497
  db.transaction((tx) => {
@@ -15503,6 +15516,7 @@ function resolveScope2(db, projectId, input) {
15503
15516
  const { market, targetKeys, targets } = scopedPlanTargets(plan, input.scope);
15504
15517
  const supported = scopeCapabilities(input.scope.kind, targets);
15505
15518
  if (input.capabilities.some((capability) => !supported.includes(capability))) {
15519
+ if (input.capabilities.includes(SeoCapabilities["competitor-discovery"]) || input.capabilities.includes(SeoCapabilities["keyword-gaps"])) throw validationError("Competitor discovery and keyword gaps compare whole domains. Select project scope with an explicit domain Target; Property, group and market scope cannot be broadened automatically.");
15506
15520
  if (supported.includes(SeoCapabilities["ranked-keywords"])) throw validationError("Domain intelligence requires project scope with a whole-domain Target. This Advanced scope supports exact-host ranked keywords and explicit keyword metrics.");
15507
15521
  throw validationError("This Advanced scope contains path or entity Properties and supports explicit keyword metrics only. Select a host-only scope for ranked keywords, or project scope for whole-domain intelligence.");
15508
15522
  }
@@ -15515,13 +15529,24 @@ function resolveScope2(db, projectId, input) {
15515
15529
  }
15516
15530
  function normalizeProfile(input) {
15517
15531
  if (new Set(input.capabilities).size !== input.capabilities.length) throw validationError("Select each SEO capability once.");
15518
- const targets = input.targets.map((target) => {
15532
+ const normalized = [...input.targets, ...input.competitors ?? []].map((target) => {
15519
15533
  const host = normalizeHostname(target.value);
15520
15534
  if (!host) throw validationError("SEO Targets must be plain hostnames without paths or ports.");
15521
15535
  if (target.kind === SeoTargetKinds.host && target.includeSubdomains) throw validationError("Exact host Targets cannot include subdomains.");
15522
15536
  return { ...target, value: host };
15523
15537
  });
15538
+ const targets = normalized.slice(0, input.targets.length);
15539
+ const comparisonTargets = normalized.slice(input.targets.length);
15524
15540
  if (new Set(targets.map((target) => `${target.kind}:${target.value}:${target.includeSubdomains}`)).size !== targets.length) throw validationError("SEO Targets must be distinct.");
15541
+ const competitive = input.capabilities.includes(SeoCapabilities["competitor-discovery"]) || input.capabilities.includes(SeoCapabilities["keyword-gaps"]);
15542
+ if (competitive && [...targets, ...comparisonTargets].some((target) => target.kind !== SeoTargetKinds.domain || !target.includeSubdomains || registrableDomain(target.value) !== target.value)) throw validationError("Competitor discovery and keyword gaps require root domain Targets with subdomains included. Exact host comparisons are not supported by this source.");
15543
+ if (input.capabilities.includes(SeoCapabilities["keyword-gaps"])) {
15544
+ if (!comparisonTargets.length) throw validationError("Keyword gaps requires at least one explicit competitor domain.");
15545
+ if (new Set(comparisonTargets.map((target) => target.value)).size !== comparisonTargets.length) throw validationError("Competitor domains must be distinct.");
15546
+ if (comparisonTargets.some((competitor) => targets.some((target) => target.value === competitor.value))) throw validationError("A keyword-gap competitor must differ from every Target domain.");
15547
+ } else if (input.competitors !== void 0) {
15548
+ throw validationError("Comparison competitors apply only to keyword gaps.");
15549
+ }
15525
15550
  const keywords = [...new Set(input.keywords)];
15526
15551
  if (input.capabilities.includes(SeoCapabilities["keyword-metrics"]) && !keywords.length) throw validationError("Keyword metrics requires at least one explicit keyword.");
15527
15552
  if (input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"]) && !targets.length) throw validationError("Domain capabilities require at least one explicit Target.");
@@ -15535,7 +15560,7 @@ function normalizeProfile(input) {
15535
15560
  } else if (input.rankings) {
15536
15561
  throw validationError("Search settings apply only to market rankings.");
15537
15562
  }
15538
- return { ...input, targets, keywords };
15563
+ return { ...input, targets, keywords, ...input.competitors === void 0 ? {} : { competitors: comparisonTargets } };
15539
15564
  }
15540
15565
  var NO_MARKET_RANKINGS = { searches: [] };
15541
15566
  function advancedMarketQueries(plan, scope) {
@@ -15630,6 +15655,18 @@ function marketRankingPlan(db, projectId, input) {
15630
15655
  }
15631
15656
  function taskRequests(profile, rankings) {
15632
15657
  return profile.capabilities.flatMap((capability) => {
15658
+ if (capability === SeoCapabilities["keyword-gaps"]) {
15659
+ return profile.targets.flatMap((target) => (profile.competitors ?? []).flatMap((competitor) => Object.values(SeoKeywordGapModes).map((mode) => ({
15660
+ capability,
15661
+ target,
15662
+ keywords: [],
15663
+ locationCode: profile.locationCode,
15664
+ languageCode: profile.languageCode,
15665
+ limit: profile.limit,
15666
+ tag: "",
15667
+ comparison: { competitor, mode }
15668
+ }))));
15669
+ }
15633
15670
  if (capability === SeoCapabilities["market-rankings"]) {
15634
15671
  const settings = profile.rankings;
15635
15672
  return rankings.searches.map((search) => ({
@@ -15891,7 +15928,7 @@ async function seoRoutes(app, opts) {
15891
15928
  const row = job(resolveProject(app.db, request.params.name).id, request.params.id);
15892
15929
  const rows = tasks(row.id);
15893
15930
  const results = resultRows(rows);
15894
- return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15931
+ return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, comparison: task.request.comparison ?? null, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20), competitors: results.competitors.slice(0, 20), keywordGaps: results.keywordGaps.slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length, competitors: results.competitors.length, keywordGaps: results.keywordGaps.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15895
15932
  });
15896
15933
  app.post("/projects/:name/seo/runs/:id/cancel", { config: { paidRead: true, writeScope: SEO_RUN_SCOPE } }, async (request) => {
15897
15934
  requireScope(request, SEO_RUN_SCOPE);
@@ -15910,7 +15947,7 @@ async function seoRoutes(app, opts) {
15910
15947
  const results = resultRows(rows);
15911
15948
  const primary = row?.profile.targets[0];
15912
15949
  const domain = primary ? results.domainIntelligence.find((result) => result.target.kind === primary.kind && result.target.value === primary.value && result.target.includeSubdomains === primary.includeSubdomains) : null;
15913
- return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15950
+ return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, competitorCount: results.competitors.length, keywordGapCount: results.keywordGaps.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15914
15951
  });
15915
15952
  for (const [path8, key] of [["keyword-metrics", "keywordMetrics"], ["domain-intelligence", "domainIntelligence"], ["ranked-keywords", "rankedKeywords"]]) app.get(`/projects/:name/seo/${path8}`, async (request) => {
15916
15953
  const query = queryInput(request.query);
@@ -15920,6 +15957,30 @@ async function seoRoutes(app, opts) {
15920
15957
  const offset = query.offset ?? 0;
15921
15958
  return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15922
15959
  });
15960
+ app.get("/projects/:name/seo/competitors", async (request) => {
15961
+ const raw = request.query;
15962
+ const parsed = seoCompetitorQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
15963
+ if (!parsed.success) throw validationError("Invalid SEO competitor selection.");
15964
+ const query = parsed.data;
15965
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
15966
+ const items = row ? resultRows(tasks(row.id)).competitors : [];
15967
+ if (query.sort) items.sort(compareSeoCompetitorsBy(query.sort));
15968
+ const limit = query.limit ?? 50;
15969
+ const offset = query.offset ?? 0;
15970
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15971
+ });
15972
+ app.get("/projects/:name/seo/keyword-gaps", async (request) => {
15973
+ const raw = request.query;
15974
+ const parsed = seoKeywordGapQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
15975
+ if (!parsed.success) throw validationError("Invalid SEO keyword-gap selection.");
15976
+ const query = parsed.data;
15977
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
15978
+ const items = (row ? resultRows(tasks(row.id)).keywordGaps : []).filter((item2) => !query.kind || (query.kind === SeoKeywordGapKinds.shared ? item2.kind !== SeoKeywordGapKinds.missing : item2.kind === query.kind));
15979
+ if (query.sort) items.sort(compareSeoKeywordGapsBy(query.sort));
15980
+ const limit = query.limit ?? 50;
15981
+ const offset = query.offset ?? 0;
15982
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15983
+ });
15923
15984
  app.get("/projects/:name/seo/market-rankings", async (request) => {
15924
15985
  const raw = request.query ?? {};
15925
15986
  const parsed = seoMarketRankingQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
@@ -34192,6 +34253,8 @@ var SCHEMA_TABLE = {
34192
34253
  SeoRunList: seoRunListSchema,
34193
34254
  SeoOverview: seoOverviewSchema,
34194
34255
  SeoKeywordMetricsList: seoKeywordMetricsListSchema,
34256
+ SeoCompetitorList: seoCompetitorListSchema,
34257
+ SeoKeywordGapList: seoKeywordGapListSchema,
34195
34258
  SeoDomainIntelligenceList: seoDomainIntelligenceListSchema,
34196
34259
  SeoRankedKeywordList: seoRankedKeywordListSchema,
34197
34260
  SeoMarketRankingList: seoMarketRankingListSchema,
@@ -35425,12 +35488,40 @@ var routeCatalog = [
35425
35488
  409: errorResponse("Selection, intent or execution identity changed.")
35426
35489
  }
35427
35490
  },
35491
+ {
35492
+ method: "get",
35493
+ path: "/api/v1/projects/{name}/seo/competitors",
35494
+ summary: "Read stored organic search competitors",
35495
+ tags: ["seo"],
35496
+ description: "Stored whole-domain organic competitor discovery only; never calls a provider. Available for Simple and Advanced project profiles. Each row names its target and source locale. Results are bounded provider database observations, not an exhaustive competitor inventory or a market/Property measurement.",
35497
+ parameters: [nameParameter, ...seoEvidenceParameters, { name: "sort", in: "query", description: "Sort the complete saved result before pagination: domain, shared searches, average position or estimated traffic. Prefix with - for descending. Missing values stay last in both directions. Omitted preserves stored order.", schema: { type: "string", enum: [...seoCompetitorSortSchema.options] } }],
35498
+ responses: {
35499
+ 200: jsonResponse("Saved organic competitor evidence.", "SeoCompetitorList"),
35500
+ 400: errorResponse("Invalid scope or input."),
35501
+ 403: errorResponse("Insufficient authority."),
35502
+ 404: errorResponse("Project or selected evidence not found.")
35503
+ }
35504
+ },
35505
+ {
35506
+ method: "get",
35507
+ path: "/api/v1/projects/{name}/seo/keyword-gaps",
35508
+ summary: "Read stored organic keyword comparisons",
35509
+ tags: ["seo"],
35510
+ description: "Stored whole-domain comparisons only; never calls a provider. Missing means the source database has a competitor ranking and no target ranking, not absence from Google. Shared includes outranked searches where both domains rank; outranked selects only those with the competitor ahead. Filters apply before pagination; total counts saved rows, not the full provider keyword universe. A signed rankGap is target position minus competitor position and is null when the target is missing.",
35511
+ parameters: [nameParameter, ...seoEvidenceParameters, { name: "kind", in: "query", description: "shared includes outranked; missing and outranked select that exact kind. Omitted returns all saved comparisons.", schema: { type: "string", enum: [...seoKeywordGapKindSchema.options] } }, { name: "sort", in: "query", description: "Sort after filtering and before pagination: keyword, kind, target rank, competitor rank, rank gap or monthly search volume. Prefix with - for descending. Missing values stay last in both directions. Omitted preserves stored order.", schema: { type: "string", enum: [...seoKeywordGapSortSchema.options] } }],
35512
+ responses: {
35513
+ 200: jsonResponse("Saved keyword comparison evidence.", "SeoKeywordGapList"),
35514
+ 400: errorResponse("Invalid scope or input."),
35515
+ 403: errorResponse("Insufficient authority."),
35516
+ 404: errorResponse("Project or selected evidence not found.")
35517
+ }
35518
+ },
35428
35519
  {
35429
35520
  method: "get",
35430
35521
  path: "/api/v1/projects/{name}/seo/market-rankings",
35431
35522
  summary: "Read stored market rankings",
35432
35523
  tags: ["seo"],
35433
- description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank. outcome says where each row landed (a partial search without the site is partly-checked, never not-ranked) and rivalAhead whether a tracked competitor ranks above the site.",
35524
+ description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank. outcome says where each row landed (a partial search without the site is partly-checked, never not-ranked) and rivalAhead whether a tracked competitor ranks above the site. topCompetitor is the best tracked rival, and rankGap is the target position minus that rival position, null when either rank is unavailable.",
35434
35525
  parameters: [
35435
35526
  nameParameter,
35436
35527
  ...seoEvidenceParameters,
@@ -35474,7 +35565,7 @@ var routeCatalog = [
35474
35565
  path: "/api/v1/projects/{name}/seo/profiles/{id}",
35475
35566
  summary: "Save an SEO profile",
35476
35567
  tags: ["seo"],
35477
- description: "Requires seo.write and an administrator session. Saving never starts provider work. Scope, locale, targets, capabilities, keywords and row limit are identity parameters.",
35568
+ description: "Requires seo.write and an administrator session. Saving never starts provider work. Scope, locale, targets, comparison competitors, capabilities, keywords and row limit are identity parameters. Keyword gaps requires one to five explicit competitor root domains; shared and missing datasets are reserved separately for each target/competitor pair. Competitor discovery and keyword gaps support project scope with whole-domain targets only, including Advanced projects.",
35478
35569
  parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Exact project-scoped identifier.", schema: stringSchema }],
35479
35570
  requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SeoProfileInput" } } } },
35480
35571
  responses: {
@@ -2,11 +2,11 @@ import {
2
2
  dedupeGscSitemapUrls,
3
3
  resolveDiscoveredGscSitemapUrls,
4
4
  submitGscSitemapBatches
5
- } from "./chunk-JSCO223C.js";
5
+ } from "./chunk-ZHFE6J3A.js";
6
6
  import {
7
7
  measurementDraftOperationSchema,
8
8
  runMeasurementDraftAction
9
- } from "./chunk-5QR7D4RQ.js";
9
+ } from "./chunk-7HB6QLX4.js";
10
10
  import {
11
11
  CliError
12
12
  } from "./chunk-HGMYEDAQ.js";
@@ -138,9 +138,13 @@ import {
138
138
  sentimentSettingsUpdateSchema,
139
139
  sentimentSummaryIncludeSchema,
140
140
  sentimentSummaryReadSchema,
141
+ seoCompetitorListSchema,
142
+ seoCompetitorQuerySchema,
141
143
  seoContextSchema,
142
144
  seoDomainIntelligenceListSchema,
143
145
  seoEvidenceQuerySchema,
146
+ seoKeywordGapListSchema,
147
+ seoKeywordGapQuerySchema,
144
148
  seoKeywordMetricsListSchema,
145
149
  seoMarketRankingListSchema,
146
150
  seoMarketRankingQuerySchema,
@@ -178,7 +182,7 @@ import {
178
182
  trafficSeriesGranularitySchema,
179
183
  visibilityCompareSelectionSchema,
180
184
  visibilityReportRequestSchema
181
- } from "./chunk-FCGYYZHL.js";
185
+ } from "./chunk-PM2DB3HE.js";
182
186
 
183
187
  // src/mcp/tool-registry.ts
184
188
  import { z as z2 } from "zod";
@@ -1131,6 +1135,8 @@ var sentimentPreviewInputSchema = sentimentBackfillSelectionSchema.safeExtend({
1131
1135
  var sentimentBackfillInputSchema = sentimentBackfillRequestSchema.extend({ project: projectNameSchema });
1132
1136
  var seoIdInputSchema = projectInputSchema.extend({ id: z2.string().min(1) });
1133
1137
  var seoEvidenceInputSchema = seoEvidenceQuerySchema.extend({ project: projectNameSchema });
1138
+ var seoCompetitorInputSchema = seoCompetitorQuerySchema.extend({ project: projectNameSchema });
1139
+ var seoKeywordGapInputSchema = seoKeywordGapQuerySchema.extend({ project: projectNameSchema });
1134
1140
  var seoMarketRankingInputSchema = seoMarketRankingQuerySchema.extend({ project: projectNameSchema });
1135
1141
  var seoMarketRankingSummaryInputSchema = seoMarketRankingSummaryQuerySchema.extend({ project: projectNameSchema });
1136
1142
  var rawCanonryMcpTools = [
@@ -1344,10 +1350,40 @@ var rawCanonryMcpTools = [
1344
1350
  return client.getSeoRankedKeywords(project, query);
1345
1351
  }
1346
1352
  }),
1353
+ defineTool({
1354
+ name: "canonry_seo_competitors",
1355
+ title: "SEO organic competitors",
1356
+ description: "Read saved whole-domain organic competitor discovery only. No provider calls or automatic competitor pins. Preserve target, location, language, source date and runId while paging. Shared-search counts and traffic are provider database estimates; saved rows are bounded by the profile limit. Optional sort orders all saved rows before paging; a leading minus descends, missing values stay last, and omission preserves source order. Preserve sort while paging.",
1357
+ access: "read",
1358
+ tier: "seo",
1359
+ inputSchema: seoCompetitorInputSchema,
1360
+ outputSchema: seoCompetitorListSchema,
1361
+ annotations: readAnnotations(),
1362
+ openApiOperations: ["GET /api/v1/projects/{name}/seo/competitors"],
1363
+ handler: (client, input) => {
1364
+ const { project, ...query } = input;
1365
+ return client.getSeoCompetitors(project, query);
1366
+ }
1367
+ }),
1368
+ defineTool({
1369
+ name: "canonry_seo_keyword_gaps",
1370
+ title: "SEO keyword gaps",
1371
+ description: "Read saved whole-domain organic search comparisons only. No provider calls. kind=shared includes all searches where both domains rank, including outranked; missing means the source database has a rival rank and no target rank; outranked means the rival ranks higher. rankGap is target rank minus rival rank and null without both. Preserve exact targets, locale, source date and runId while paging. Results are a bounded source sample, not all possible opportunities. Optional sort orders all filtered rows before paging; a leading minus descends, missing values stay last, and omission preserves source order. Preserve sort while paging.",
1372
+ access: "read",
1373
+ tier: "seo",
1374
+ inputSchema: seoKeywordGapInputSchema,
1375
+ outputSchema: seoKeywordGapListSchema,
1376
+ annotations: readAnnotations(),
1377
+ openApiOperations: ["GET /api/v1/projects/{name}/seo/keyword-gaps"],
1378
+ handler: (client, input) => {
1379
+ const { project, ...query } = input;
1380
+ return client.getSeoKeywordGaps(project, query);
1381
+ }
1382
+ }),
1347
1383
  defineTool({
1348
1384
  name: "canonry_seo_market_rankings",
1349
1385
  title: "SEO market rankings",
1350
- description: "Read stored SEO data only. No provider calls. The site's Google position for each tracked non-brand query in each market, searched from that market's location; these are the same tracked queries AI visibility measures, and searchText is the exact text searched. searchStatus says how each search ended: a null rank in a completed or partial row means not in the checked results (depth, lower when only part of the results came back); a failed or cancelled row has no rank because the search returned nothing. outcome says where each row landed (page-1, ranks-11-20, ranks-21-plus, not-ranked, partly-checked for a partial search without the site, failed, cancelled) and rivalAhead whether a tracked competitor ranks above the site. Filter with marketKey, regionKey, outcome (needs-work = off page 1 with a result) and q; sort=rank puts the closest to page 1 first. Keep runId fixed while paging.",
1386
+ description: "Read stored SEO data only. No provider calls. The site's Google position for each tracked non-brand query in each market, searched from that market's location; these are the same tracked queries AI visibility measures, and searchText is the exact text searched. searchStatus says how each search ended: a null rank in a completed or partial row means not in the checked results (depth, lower when only part of the results came back); a failed or cancelled row has no rank because the search returned nothing. outcome says where each row landed (page-1, ranks-11-20, ranks-21-plus, not-ranked, partly-checked for a partial search without the site, failed, cancelled) and rivalAhead whether a tracked competitor ranks above the site. topCompetitor is the best tracked rival; rankGap is target rank minus rival rank and null without both. Filter with marketKey, regionKey, outcome (needs-work = off page 1 with a result) and q; sort=rank puts the closest to page 1 first. Keep runId fixed while paging.",
1351
1387
  access: "read",
1352
1388
  tier: "seo",
1353
1389
  inputSchema: seoMarketRankingInputSchema,
@@ -8,7 +8,7 @@ import {
8
8
  classifyJourneyEntry,
9
9
  hostMatchesDomain,
10
10
  hostOf
11
- } from "./chunk-FCGYYZHL.js";
11
+ } from "./chunk-PM2DB3HE.js";
12
12
 
13
13
  // ../integration-traffic/src/ip-ranges/amazonbot.json
14
14
  var amazonbot_default = {