@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.
- package/assets/agent-workspace/skills/aero/references/seo-analysis.md +13 -2
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -2
- package/assets/assets/{AgentSetupPage-BZXWC3x6.js → AgentSetupPage-BGdvyQFL.js} +4 -4
- package/assets/assets/{AuditHistoryPanel-DcrUuwRu.js → AuditHistoryPanel-Bb5wq-Bk.js} +1 -1
- package/assets/assets/{BacklinksPage-BSpHJBiy.js → BacklinksPage-i1cfxTDj.js} +1 -1
- package/assets/assets/{HistoryPage-0a0CK9zF.js → HistoryPage-C2NRdNrn.js} +1 -1
- package/assets/assets/MeasurementPropertyPage-CwPuJMBy.js +4 -0
- package/assets/assets/ProjectPage-Bbnbg7c9.js +11 -0
- package/assets/assets/{RunsPage-D0vbz1Ho.js → RunsPage-CJHJ7kHk.js} +1 -1
- package/assets/assets/{SettingsPage-DjRm5cmn.js → SettingsPage-r8VrtFyJ.js} +1 -1
- package/assets/assets/SiteHealthSection-D1oSuo9O.js +2 -0
- package/assets/assets/ToneBadge-CUe-WrzN.js +60 -0
- package/assets/assets/{TrafficPage-Z8Z_z0k9.js → TrafficPage-DY2ykwH1.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-D3KHgRvF.js +1 -0
- package/assets/assets/{account-context-zaSxLBst.js → account-context-B50QqDEA.js} +1 -1
- package/assets/assets/{card-COqALGzB.js → card-BE2fqwVq.js} +1 -1
- package/assets/assets/{extract-error-message-DKve0SNr.js → extract-error-message-xuN_xeRJ.js} +1 -1
- package/assets/assets/{format-helpers-DKrHlaV7.js → format-helpers-CiC6gALY.js} +1 -1
- package/assets/assets/{index-CKZK95pI.css → index-CHHZDc6d.css} +1 -1
- package/assets/assets/{index-fzqtwr6m.js → index-Dfm8z5cG.js} +23 -23
- package/assets/assets/{query-client-COM9uzxL.js → query-client-C5GhDMY7.js} +1 -1
- package/assets/assets/react-query-CmHyfYFq.js +1 -0
- package/assets/assets/{react-sigma_core.esm.min-e91U3Myi.js → react-sigma_core.esm.min-Bjdtl0TQ.js} +1 -1
- package/assets/assets/{service-BgFTYqJA.js → service-B2i_ZzWP.js} +1 -1
- package/assets/index.html +8 -8
- package/dist/{ads-M6IAQDG7.js → ads-MIR5MCHL.js} +3 -3
- package/dist/{agent-X2T5X5K3.js → agent-IXRL5RBI.js} +7 -7
- package/dist/{agent-ask-55LQH4TI.js → agent-ask-K42H6H2T.js} +3 -3
- package/dist/{agent-conversations-O6WC2NPV.js → agent-conversations-2T3W4DHO.js} +3 -3
- package/dist/{agent-memory-6DWFMB4I.js → agent-memory-LJBNSTSP.js} +3 -3
- package/dist/{agent-providers-Q4YDVQX3.js → agent-providers-QR4LY3AP.js} +3 -3
- package/dist/{agent-transcript-SJKCP2AO.js → agent-transcript-JIHJWHV6.js} +3 -3
- package/dist/{analytics-4JOQMWH7.js → analytics-JRV7DBBU.js} +3 -3
- package/dist/{apply-EAKKKIZD.js → apply-ZFWDSAV4.js} +3 -3
- package/dist/{backfill-PASKIVGP.js → backfill-LU2AQIPY.js} +9 -9
- package/dist/{backlinks-TICPC323.js → backlinks-NAC2373F.js} +3 -3
- package/dist/{bing-MC7TSL4E.js → bing-L3LDBJOC.js} +3 -3
- package/dist/{bootstrap-X2BW64W7.js → bootstrap-I6DA4KWB.js} +10 -10
- package/dist/{cdp-TCJKITCR.js → cdp-4F74XLTH.js} +7 -7
- package/dist/{chunk-CCNPBUQ5.js → chunk-367K6XVX.js} +2 -2
- package/dist/{chunk-JPOIDGGF.js → chunk-3MGM3U6V.js} +1 -1
- package/dist/{chunk-2BDWXHYF.js → chunk-45RGH7XI.js} +3 -3
- package/dist/{chunk-RPJJOBKN.js → chunk-6ACWXQNZ.js} +3 -1
- package/dist/{chunk-B7JHQ3FW.js → chunk-6P6YBNKN.js} +7 -7
- package/dist/{chunk-YMOQSEO3.js → chunk-6PRFVLIK.js} +15 -4
- package/dist/{chunk-D5XPB7MM.js → chunk-6VBLPCCX.js} +1 -1
- package/dist/{chunk-5QR7D4RQ.js → chunk-7HB6QLX4.js} +1 -1
- package/dist/{chunk-7UVCZZQJ.js → chunk-AI44ZCMK.js} +1 -1
- package/dist/{chunk-X7UC755Q.js → chunk-C5WSQXP5.js} +26 -2
- package/dist/{chunk-CQMNWC5K.js → chunk-EFMZSYRD.js} +160 -26
- package/dist/{chunk-FXIZBJZ4.js → chunk-EHXNYXAS.js} +3 -3
- package/dist/{chunk-2WBT6TEJ.js → chunk-FCYGY4FT.js} +1 -1
- package/dist/{chunk-XYYWKCJQ.js → chunk-JERATA5R.js} +1 -1
- package/dist/{chunk-P3BWUKJL.js → chunk-JEUL5QZP.js} +4 -4
- package/dist/{chunk-PSNTEGNY.js → chunk-KZB6HLFT.js} +1 -1
- package/dist/{chunk-UGNNR7EO.js → chunk-L2KMR7CM.js} +2 -2
- package/dist/{chunk-H4ANN32B.js → chunk-L6F2HOQH.js} +8 -8
- package/dist/{chunk-GDKMVOZI.js → chunk-M3BUP6XD.js} +104 -13
- package/dist/{chunk-4QHTXT4A.js → chunk-MUYTDD35.js} +40 -4
- package/dist/{chunk-TESDUMVK.js → chunk-O4WBYEGV.js} +1 -1
- package/dist/{chunk-FCGYYZHL.js → chunk-PM2DB3HE.js} +164 -10
- package/dist/{chunk-TRW4XLQK.js → chunk-QXSOXDUR.js} +2 -2
- package/dist/{chunk-2SCPPWRW.js → chunk-RSVANBNW.js} +4 -4
- package/dist/{chunk-UB65XBH2.js → chunk-SAHH2FXO.js} +3 -3
- package/dist/{chunk-46KV7CZU.js → chunk-SOQVI7EZ.js} +2 -2
- package/dist/{chunk-Z2FY6AGZ.js → chunk-TZRYVW75.js} +2 -2
- package/dist/{chunk-C3UFOS7J.js → chunk-UH44UCRM.js} +2 -2
- package/dist/{chunk-TJYS2XCI.js → chunk-UQOC5ZQE.js} +4 -4
- package/dist/{chunk-2BRVGCVU.js → chunk-VLOCDWCS.js} +7 -1
- package/dist/{chunk-6EZQHF67.js → chunk-WCFANFR2.js} +371 -340
- package/dist/{chunk-ZXYCPRNY.js → chunk-WSWHLKKJ.js} +1 -1
- package/dist/{chunk-NDTU7FGL.js → chunk-WVCDUKJ2.js} +1 -1
- package/dist/{chunk-JZT3ZRD3.js → chunk-WXIL2EFM.js} +4 -4
- package/dist/{chunk-JSCO223C.js → chunk-ZHFE6J3A.js} +1 -1
- package/dist/{chunk-HD2VVIPD.js → chunk-ZUC7CJVD.js} +5 -5
- package/dist/{citations-IA3OCV53.js → citations-2XELMCCV.js} +3 -3
- package/dist/{cli-catalog-ZVFLJXID.js → cli-catalog-KC6JYHSW.js} +3 -3
- package/dist/{cli-commands-O7BYY3W5.js → cli-commands-WCOAB6XU.js} +7 -7
- package/dist/cli.js +13 -13
- package/dist/{competitor-4FVAZAQZ.js → competitor-IK4GYTZO.js} +3 -3
- package/dist/{content-OVBF6U7Z.js → content-SMQPJXPC.js} +3 -3
- package/dist/{dashboard-UGZSSIWK.js → dashboard-EE24ROVG.js} +6 -6
- package/dist/{demo-5CU3JDHC.js → demo-7XH2N65A.js} +1 -1
- package/dist/{demo-server-43QV3UKM.js → demo-server-CVB65UYW.js} +12 -12
- package/dist/{discover-R5BYAPOU.js → discover-3KHUCVE7.js} +3 -3
- package/dist/{discover-eval-3FA4BR2L.js → discover-eval-DOEP4ZQ7.js} +3 -3
- package/dist/{doctor-2Q5LVV2P.js → doctor-3T6RKSE7.js} +3 -3
- package/dist/{evidence-LMGRT5IO.js → evidence-4NFL5KM3.js} +3 -3
- package/dist/{export-cmd-LBLZ273Z.js → export-cmd-BQC6XLJ7.js} +3 -3
- package/dist/{ga-PBIWRU67.js → ga-HBRA4UUK.js} +3 -3
- package/dist/{gbp-NYG7SW6P.js → gbp-2WMBTDJV.js} +3 -3
- package/dist/{google-57XEYVOU.js → google-JFUE4BNJ.js} +4 -4
- package/dist/{google-marketing-XPE6CVQI.js → google-marketing-5ROIKBC7.js} +1 -1
- package/dist/{health-cmd-SJVYQZQL.js → health-cmd-7BMVCCG2.js} +3 -3
- package/dist/{history-5XYZNWGP.js → history-QA4ZXCVH.js} +3 -3
- package/dist/index.d.ts +216 -23
- package/dist/index.js +26 -26
- package/dist/{init-KXR6PJEQ.js → init-O4G2MDMC.js} +12 -12
- package/dist/{insights-L7OOH34I.js → insights-K4UX2PSI.js} +3 -3
- package/dist/intelligence-service-D7DS4UKT.js +16 -0
- package/dist/{journeys-VXBGDFBO.js → journeys-UWEWJXGT.js} +3 -3
- package/dist/{keys-YMB5HAAG.js → keys-CL5YYE7Q.js} +3 -3
- package/dist/{keyword-RZEMKH3K.js → keyword-IFCC5IZT.js} +3 -3
- package/dist/{logs-ONZWSYXS.js → logs-2A3HBUBK.js} +3 -3
- package/dist/{mcp-KK6EFHAZ.js → mcp-B6L7PKZ4.js} +4 -4
- package/dist/mcp.js +10 -10
- package/dist/{measurement-plan-LE7INWW6.js → measurement-plan-GYSO6CXX.js} +5 -5
- package/dist/{notify-T6T5LYRR.js → notify-XM23I2B3.js} +3 -3
- package/dist/{organic-evidence-P2GZ766W.js → organic-evidence-U7HZMXKP.js} +3 -3
- package/dist/{overview-P2AILIWJ.js → overview-TSAK5DQP.js} +4 -4
- package/dist/{project-7MHQSOJD.js → project-L7DLDEPI.js} +3 -3
- package/dist/{query-EUDMU6L7.js → query-FM5HDHO4.js} +3 -3
- package/dist/{research-4DASCVGY.js → research-7G5JU44N.js} +3 -3
- package/dist/{results-export-GKXHX55L.js → results-export-JJDGG6ZP.js} +3 -3
- package/dist/{run-6LJGMJ5S.js → run-ZVIQVG23.js} +4 -4
- package/dist/{schedule-D74P2BO5.js → schedule-TV7LWSYI.js} +3 -3
- package/dist/{search-W7AOYWRM.js → search-7KRZLZ6S.js} +3 -3
- package/dist/{serve-UNVCQSRC.js → serve-WSOG525W.js} +27 -27
- package/dist/{session-6NGNVCKP.js → session-OUGQSGVV.js} +14 -14
- package/dist/{settings-LGD64PFE.js → settings-AOXQJOHN.js} +3 -3
- package/dist/{skills-CYHWYN42.js → skills-DWXA55QA.js} +2 -2
- package/dist/{snapshot-PDYGSZZH.js → snapshot-B44CXKFU.js} +3 -3
- package/dist/{src-SXB3MOP3.js → src-IBGSUYEG.js} +2 -2
- package/dist/{status-UZ4SE7ZP.js → status-RNBSFZ6Q.js} +4 -4
- package/dist/{technical-aeo-BNH7NV44.js → technical-aeo-UAOKK5QH.js} +3 -3
- package/dist/{telemetry-UWDS7QNJ.js → telemetry-55PUOMA7.js} +4 -4
- package/dist/{tools-EDPBQCI2.js → tools-HRGP4FXN.js} +11 -11
- package/dist/{traffic-NI6ACQO4.js → traffic-M4OHG5JW.js} +5 -5
- package/dist/{users-XTZ65NLC.js → users-GU7MULRM.js} +3 -3
- package/dist/{visibility-stats-EPF6MHYQ.js → visibility-stats-4IDTDXDC.js} +3 -3
- package/dist/{wordpress-ZACJ3H5M.js → wordpress-CYXB4R5Y.js} +3 -3
- package/package.json +1 -1
- package/assets/assets/MeasurementPropertyPage-DbfTFN3F.js +0 -4
- package/assets/assets/ProjectPage-D0m-Xpp3.js +0 -9
- package/assets/assets/SiteHealthSection-BCdPVFEJ.js +0 -2
- package/assets/assets/ToneBadge-DSJSzqI6.js +0 -60
- package/assets/assets/TrafficSourceDetailPage-DJyDO527.js +0 -1
- package/assets/assets/react-query-BVRWpXWq.js +0 -1
- package/dist/intelligence-service-NXZYQJBH.js +0 -16
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
registeredProviderNames
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FCYGY4FT.js";
|
|
4
4
|
import {
|
|
5
5
|
configExists,
|
|
6
6
|
loadConfigRaw
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
createApiClient
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
10
|
+
} from "./chunk-UH44UCRM.js";
|
|
11
11
|
import {
|
|
12
12
|
loadConfig
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
26
|
+
} from "./chunk-M3BUP6XD.js";
|
|
27
27
|
import {
|
|
28
28
|
classifyAiUserFetch,
|
|
29
29
|
classifyCrawler
|
|
30
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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
|
|
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-
|
|
5
|
+
} from "./chunk-ZHFE6J3A.js";
|
|
6
6
|
import {
|
|
7
7
|
measurementDraftOperationSchema,
|
|
8
8
|
runMeasurementDraftAction
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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,
|