@canonry/canonry 4.170.0 → 4.171.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.
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  loadConfigRaw,
11
11
  saveConfigPatch
12
- } from "./chunk-BMW3AURU.js";
12
+ } from "./chunk-IIFDS3K2.js";
13
13
  import {
14
14
  CC_CACHE_DIR,
15
15
  DUCKDB_SPEC,
@@ -145,7 +145,7 @@ import {
145
145
  siteCrawlSnapshots,
146
146
  toAlertView,
147
147
  usageCounters
148
- } from "./chunk-CCZNCH7Y.js";
148
+ } from "./chunk-O226BLC7.js";
149
149
  import {
150
150
  AGENT_MEMORY_VALUE_MAX_BYTES,
151
151
  AGENT_PROVIDER_IDS,
@@ -11829,7 +11829,7 @@ function readStoredGroundingSources(rawResponse) {
11829
11829
  return result;
11830
11830
  }
11831
11831
  async function backfillInsightsCommand(project, opts) {
11832
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-64IPZ4NU.js");
11832
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-KT6K4ODY.js");
11833
11833
  const config = loadConfig();
11834
11834
  const db = createClient(config.database);
11835
11835
  migrate(db);
@@ -18003,6 +18003,37 @@ async function createServer(opts) {
18003
18003
  // Resolved fresh each call so a key added at runtime (settings API) shows
18004
18004
  // up immediately in the `config.agent-providers` doctor check.
18005
18005
  getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
18006
+ getGoogleMarketingDoctorInput: (ctx) => {
18007
+ if (!ctx.project) return null;
18008
+ const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
18009
+ const gtm = opts.db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, ctx.project.id)).get();
18010
+ const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
18011
+ const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
18012
+ const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
18013
+ const gtmConnected = Boolean(gtmCredential?.accessToken);
18014
+ return {
18015
+ googleAds: googleAds && googleAdsConnected ? {
18016
+ credentialsPresent: Boolean(
18017
+ googleAdsCredential?.accessToken || googleAdsCredential?.refreshToken
18018
+ ),
18019
+ grantedScopes: googleAds.scopes,
18020
+ selectedLoginCustomerId: googleAds.selectedLoginCustomerId,
18021
+ selectedCustomerId: googleAds.selectedCustomerId,
18022
+ latestSnapshotAt: [
18023
+ googleAds.lastInventorySnapshotAt,
18024
+ googleAds.lastMetricsSnapshotAt
18025
+ ].filter((value) => Boolean(value)).sort().at(-1) ?? null
18026
+ } : null,
18027
+ gtm: gtm && gtmConnected ? {
18028
+ credentialsPresent: Boolean(gtmCredential?.accessToken || gtmCredential?.refreshToken),
18029
+ grantedScopes: gtm.scopes,
18030
+ selectedAccountId: gtm.selectedAccountId,
18031
+ selectedContainerId: gtm.selectedContainerId,
18032
+ selectedWorkspaceId: gtm.selectedWorkspaceId,
18033
+ latestSnapshotAt: gtm.lastSnapshotAt
18034
+ } : null
18035
+ };
18036
+ },
18006
18037
  googleConnectionStore,
18007
18038
  googleStateSecret,
18008
18039
  publicUrl: googlePublicUrl,
@@ -4,6 +4,7 @@ import {
4
4
  CodingAgents,
5
5
  DISCOVERY_MAX_PROBES_CAP,
6
6
  DISCOVERY_PROBE_CONCURRENCY_CAP,
7
+ GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX,
7
8
  SKILL_MANIFEST_FILENAME,
8
9
  SkillsClients,
9
10
  adsActivateTreeRequestSchema,
@@ -19,6 +20,8 @@ import {
19
20
  adsPauseRequestSchema,
20
21
  adsUnresolvedOperationListQuerySchema,
21
22
  backlinkSourceSchema,
23
+ canonicalizeGtmAccountId,
24
+ canonicalizeGtmResourceSelection,
22
25
  classifySkillFile,
23
26
  coerceSkillManifest,
24
27
  competitorBatchRequestSchema,
@@ -9350,6 +9353,54 @@ var adsAdPauseInputSchema = z3.object({
9350
9353
  adId: z3.string().min(1),
9351
9354
  request: adsPauseRequestSchema
9352
9355
  });
9356
+ var googleMarketingSnapshotPageInputSchema = z3.object({
9357
+ project: projectNameSchema,
9358
+ limit: z3.number().int().min(1).max(GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX).optional(),
9359
+ cursor: z3.string().trim().min(1).optional()
9360
+ }).strict();
9361
+ var googleMarketingSnapshotInputSchema = z3.object({
9362
+ project: projectNameSchema,
9363
+ snapshotId: z3.string().trim().min(1)
9364
+ }).strict();
9365
+ var gtmAccountInputSchema = z3.object({
9366
+ project: projectNameSchema,
9367
+ accountId: z3.string().trim().min(1)
9368
+ }).strict().superRefine((input, context) => {
9369
+ if (!canonicalizeGtmAccountId(input.accountId)) {
9370
+ context.addIssue({
9371
+ code: z3.ZodIssueCode.custom,
9372
+ path: ["accountId"],
9373
+ message: "Expected a safe GTM account ID or accounts/{id} resource path."
9374
+ });
9375
+ }
9376
+ });
9377
+ var gtmContainerInputSchema = z3.object({
9378
+ project: projectNameSchema,
9379
+ accountId: z3.string().trim().min(1),
9380
+ containerId: z3.string().trim().min(1)
9381
+ }).strict().superRefine((input, context) => {
9382
+ if (!canonicalizeGtmResourceSelection(input)) {
9383
+ context.addIssue({
9384
+ code: z3.ZodIssueCode.custom,
9385
+ path: ["containerId"],
9386
+ message: "Expected matching safe GTM account/container IDs or resource paths."
9387
+ });
9388
+ }
9389
+ });
9390
+ function canonicalGtmMcpAccountId(accountId) {
9391
+ const canonical = canonicalizeGtmAccountId(accountId);
9392
+ if (!canonical) throw new Error("Invalid GTM account input.");
9393
+ return canonical;
9394
+ }
9395
+ function canonicalGtmMcpSelection(accountId, containerId) {
9396
+ const canonical = canonicalizeGtmResourceSelection({ accountId, containerId });
9397
+ if (!canonical) throw new Error("Invalid GTM account/container input.");
9398
+ return canonical;
9399
+ }
9400
+ var conversionTrackingContractInputSchema = z3.object({
9401
+ project: projectNameSchema,
9402
+ contractId: z3.string().trim().min(1)
9403
+ }).strict();
9353
9404
  var keywordsInputSchema = z3.object({
9354
9405
  project: projectNameSchema,
9355
9406
  request: keywordBatchRequestSchema
@@ -11692,6 +11743,187 @@ var canonryMcpTools = [
11692
11743
  checkDeadLinks: input.checkDeadLinks
11693
11744
  })
11694
11745
  }),
11746
+ // ----- Google Marketing: Google Ads + Google Tag Manager -----
11747
+ // OAuth, selection, disconnect, and contract writes deliberately stay on
11748
+ // the operator CLI/dashboard. The agent surface can only inspect bounded
11749
+ // evidence, trigger bounded read-only syncs, and assess stored integrity.
11750
+ defineTool({
11751
+ name: "canonry_google_ads_status",
11752
+ title: "Get Google Ads connection status",
11753
+ description: "Read the project-local Google Ads connection, selected customer, and stored-evidence freshness. This never exposes credentials or changes a Google Ads account.",
11754
+ access: "read",
11755
+ tier: "google-ads",
11756
+ inputSchema: projectInputSchema,
11757
+ annotations: readAnnotations(),
11758
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/status"],
11759
+ handler: (client2, input) => client2.getGoogleAdsStatus(input.project)
11760
+ }),
11761
+ defineTool({
11762
+ name: "canonry_google_ads_customers",
11763
+ title: "List live Google Ads customers",
11764
+ description: "Read the bounded set of Google Ads customers available to the project OAuth connection. This makes live provider GET requests and requires google-marketing.read-live; customer selection remains an explicit operator action.",
11765
+ access: "read",
11766
+ tier: "google-ads",
11767
+ inputSchema: projectInputSchema,
11768
+ annotations: readAnnotations(true),
11769
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/customers"],
11770
+ handler: (client2, input) => client2.listGoogleAdsCustomers(input.project)
11771
+ }),
11772
+ defineTool({
11773
+ name: "canonry_google_ads_snapshots",
11774
+ title: "List stored Google Ads evidence",
11775
+ description: "List a bounded page of redacted, append-only Google Ads snapshots. Stored reads are quota-free and do not call Google.",
11776
+ access: "read",
11777
+ tier: "google-ads",
11778
+ inputSchema: googleMarketingSnapshotPageInputSchema,
11779
+ annotations: readAnnotations(),
11780
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/snapshots"],
11781
+ handler: (client2, input) => client2.listGoogleAdsSnapshots(input.project, {
11782
+ limit: input.limit,
11783
+ cursor: input.cursor
11784
+ })
11785
+ }),
11786
+ defineTool({
11787
+ name: "canonry_google_ads_snapshot_get",
11788
+ title: "Get stored Google Ads evidence",
11789
+ description: "Read one redacted Google Ads evidence snapshot by ID. It can show conversion actions and effective campaign goals, but cannot prove a browser conversion fired.",
11790
+ access: "read",
11791
+ tier: "google-ads",
11792
+ inputSchema: googleMarketingSnapshotInputSchema,
11793
+ annotations: readAnnotations(),
11794
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/snapshots/{snapshotId}"],
11795
+ handler: (client2, input) => client2.getGoogleAdsSnapshot(input.project, input.snapshotId)
11796
+ }),
11797
+ defineTool({
11798
+ name: "canonry_google_ads_sync",
11799
+ title: "Sync Google Ads conversion evidence",
11800
+ description: "Queue a bounded read-only Google Ads evidence sync using GETs and SearchStream POSTs. It creates a local run and sanitized snapshots, but never changes campaigns, conversion actions, goals, bids, or budgets. Requires google-marketing.read-live and write authority.",
11801
+ access: "write",
11802
+ tier: "google-ads",
11803
+ inputSchema: projectInputSchema,
11804
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
11805
+ openApiOperations: ["POST /api/v1/projects/{name}/google-ads/sync"],
11806
+ handler: (client2, input) => client2.triggerGoogleAdsSync(input.project)
11807
+ }),
11808
+ defineTool({
11809
+ name: "canonry_gtm_status",
11810
+ title: "Get Google Tag Manager connection status",
11811
+ description: "Read the project-local GTM connection, selected account/container/workspace, and stored-evidence freshness. This never exposes credentials or edits GTM.",
11812
+ access: "read",
11813
+ tier: "gtm",
11814
+ inputSchema: projectInputSchema,
11815
+ annotations: readAnnotations(),
11816
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/status"],
11817
+ handler: (client2, input) => client2.getGtmStatus(input.project)
11818
+ }),
11819
+ defineTool({
11820
+ name: "canonry_gtm_accounts",
11821
+ title: "List live GTM accounts",
11822
+ description: "Read the bounded set of GTM accounts available to the project OAuth connection. This makes live provider GET requests and requires google-marketing.read-live; resource selection remains operator-only.",
11823
+ access: "read",
11824
+ tier: "gtm",
11825
+ inputSchema: projectInputSchema,
11826
+ annotations: readAnnotations(true),
11827
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts"],
11828
+ handler: (client2, input) => client2.listGtmAccounts(input.project)
11829
+ }),
11830
+ defineTool({
11831
+ name: "canonry_gtm_containers",
11832
+ title: "List live GTM containers",
11833
+ description: "Read the bounded set of containers in one GTM account. This is a live GET-only provider read; it cannot edit or publish a container.",
11834
+ access: "read",
11835
+ tier: "gtm",
11836
+ inputSchema: gtmAccountInputSchema,
11837
+ annotations: readAnnotations(true),
11838
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts/{accountId}/containers"],
11839
+ handler: (client2, input) => client2.listGtmContainers(
11840
+ input.project,
11841
+ canonicalGtmMcpAccountId(input.accountId)
11842
+ )
11843
+ }),
11844
+ defineTool({
11845
+ name: "canonry_gtm_workspaces",
11846
+ title: "List live GTM workspaces",
11847
+ description: "Read the bounded set of workspaces in one GTM container. This is a live GET-only provider read; it cannot edit a workspace or publish a version.",
11848
+ access: "read",
11849
+ tier: "gtm",
11850
+ inputSchema: gtmContainerInputSchema,
11851
+ annotations: readAnnotations(true),
11852
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts/{accountId}/containers/{containerId}/workspaces"],
11853
+ handler: (client2, input) => {
11854
+ const selection = canonicalGtmMcpSelection(input.accountId, input.containerId);
11855
+ return client2.listGtmWorkspaces(input.project, selection.accountId, selection.containerId);
11856
+ }
11857
+ }),
11858
+ defineTool({
11859
+ name: "canonry_gtm_snapshots",
11860
+ title: "List stored GTM evidence",
11861
+ description: "List a bounded page of redacted, append-only GTM live/draft graph snapshots. Stored reads are quota-free and do not call Google.",
11862
+ access: "read",
11863
+ tier: "gtm",
11864
+ inputSchema: googleMarketingSnapshotPageInputSchema,
11865
+ annotations: readAnnotations(),
11866
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/snapshots"],
11867
+ handler: (client2, input) => client2.listGtmSnapshots(input.project, {
11868
+ limit: input.limit,
11869
+ cursor: input.cursor
11870
+ })
11871
+ }),
11872
+ defineTool({
11873
+ name: "canonry_gtm_snapshot_get",
11874
+ title: "Get stored GTM evidence",
11875
+ description: "Read one redacted GTM live/draft graph snapshot by ID. It can prove static tag and trigger configuration only; it cannot prove a browser event fired.",
11876
+ access: "read",
11877
+ tier: "gtm",
11878
+ inputSchema: googleMarketingSnapshotInputSchema,
11879
+ annotations: readAnnotations(),
11880
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/snapshots/{snapshotId}"],
11881
+ handler: (client2, input) => client2.getGtmSnapshot(input.project, input.snapshotId)
11882
+ }),
11883
+ defineTool({
11884
+ name: "canonry_gtm_sync",
11885
+ title: "Sync GTM conversion evidence",
11886
+ description: "Queue a bounded GTM GET-only evidence sync. It creates a local run and sanitized live/draft snapshots, but never edits a workspace or publishes a container version. Requires google-marketing.read-live and write authority.",
11887
+ access: "write",
11888
+ tier: "gtm",
11889
+ inputSchema: projectInputSchema,
11890
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
11891
+ openApiOperations: ["POST /api/v1/projects/{name}/gtm/sync"],
11892
+ handler: (client2, input) => client2.triggerGtmSync(input.project)
11893
+ }),
11894
+ defineTool({
11895
+ name: "canonry_conversion_tracking_contracts",
11896
+ title: "List conversion-tracking contracts",
11897
+ description: "List the project\u2019s declared business contracts linking an application event to Google Ads and GTM identifiers. Contract creation and changes remain operator-only.",
11898
+ access: "read",
11899
+ tier: "conversion-tracking",
11900
+ inputSchema: projectInputSchema,
11901
+ annotations: readAnnotations(),
11902
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts"],
11903
+ handler: (client2, input) => client2.listConversionTrackingContracts(input.project)
11904
+ }),
11905
+ defineTool({
11906
+ name: "canonry_conversion_tracking_contract_get",
11907
+ title: "Get conversion-tracking contract",
11908
+ description: "Read one declared conversion-tracking contract. It declares intended semantics; use integrity assessment to evaluate stored evidence.",
11909
+ access: "read",
11910
+ tier: "conversion-tracking",
11911
+ inputSchema: conversionTrackingContractInputSchema,
11912
+ annotations: readAnnotations(),
11913
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts/{contractId}"],
11914
+ handler: (client2, input) => client2.getConversionTrackingContract(input.project, input.contractId)
11915
+ }),
11916
+ defineTool({
11917
+ name: "canonry_conversion_tracking_integrity",
11918
+ title: "Assess stored conversion integrity",
11919
+ description: "Evaluate a declared contract against stored redacted Google Ads and GTM evidence. This does not call providers and cannot treat static configuration as proof of a browser event or observed Google Ads conversion.",
11920
+ access: "read",
11921
+ tier: "conversion-tracking",
11922
+ inputSchema: conversionTrackingContractInputSchema,
11923
+ annotations: readAnnotations(),
11924
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts/{contractId}/integrity"],
11925
+ handler: (client2, input) => client2.getConversionTrackingIntegrity(input.project, input.contractId)
11926
+ }),
11695
11927
  // ----- OpenAI ads (ChatGPT ads) -----
11696
11928
  defineTool({
11697
11929
  name: "canonry_ads_status",
@@ -61640,6 +61640,251 @@ var WORDPRESS_PUBLISH_CHECKS = [
61640
61640
  }
61641
61641
  ];
61642
61642
 
61643
+ // ../api-routes/src/doctor/checks/google-marketing.ts
61644
+ var GOOGLE_ADS_REQUIRED_OAUTH_SCOPES = [
61645
+ "https://www.googleapis.com/auth/adwords"
61646
+ ];
61647
+ var GTM_REQUIRED_OAUTH_SCOPES = [
61648
+ "https://www.googleapis.com/auth/tagmanager.readonly"
61649
+ ];
61650
+ var FRESH_SNAPSHOT_WARN_DAYS = 7;
61651
+ var FRESH_SNAPSHOT_FAIL_DAYS = 30;
61652
+ var DAY_MS = 24 * 60 * 60 * 1e3;
61653
+ function unavailableStatus() {
61654
+ return {
61655
+ status: CheckStatuses.skipped,
61656
+ code: "google-marketing.status-unavailable",
61657
+ summary: "Google marketing status metadata is not configured for this deployment.",
61658
+ remediation: null
61659
+ };
61660
+ }
61661
+ function noProjectStatus() {
61662
+ return {
61663
+ status: CheckStatuses.skipped,
61664
+ code: "google-marketing.no-project",
61665
+ summary: "Project context required.",
61666
+ remediation: null
61667
+ };
61668
+ }
61669
+ function notConnectedStatus(provider, requiredScopes) {
61670
+ return {
61671
+ status: CheckStatuses.skipped,
61672
+ code: provider === "Google Ads" ? "google-ads.auth.not-connected" : "gtm.auth.not-connected",
61673
+ summary: `${provider} is not connected for this project.`,
61674
+ remediation: null,
61675
+ details: { requiredScopes: [...requiredScopes] }
61676
+ };
61677
+ }
61678
+ function credentialsOutput(provider, credentialsPresent) {
61679
+ if (credentialsPresent) {
61680
+ return {
61681
+ status: CheckStatuses.ok,
61682
+ code: `${provider}.auth.credentials-metadata-present`,
61683
+ summary: `${provider === "google-ads" ? "Google Ads" : "Google Tag Manager"} credential metadata is present.`,
61684
+ remediation: null,
61685
+ details: { credentialsPresent }
61686
+ };
61687
+ }
61688
+ return {
61689
+ status: CheckStatuses.fail,
61690
+ code: `${provider}.auth.credentials-metadata-missing`,
61691
+ summary: `${provider === "google-ads" ? "Google Ads" : "Google Tag Manager"} connection metadata exists but credentials are unavailable.`,
61692
+ remediation: "Reconnect this Google integration to restore its credential metadata.",
61693
+ details: { credentialsPresent }
61694
+ };
61695
+ }
61696
+ function scopesOutput(provider, grantedScopes, requiredScopes) {
61697
+ const granted = [...new Set(grantedScopes)];
61698
+ const missing = requiredScopes.filter((scope) => !granted.includes(scope));
61699
+ const label = provider === "google-ads" ? "Google Ads" : "Google Tag Manager";
61700
+ const scopeDescription = provider === "google-ads" ? "OAuth scope" : "read-only OAuth scope";
61701
+ if (missing.length === 0) {
61702
+ return {
61703
+ status: CheckStatuses.ok,
61704
+ code: `${provider}.auth.scopes-ok`,
61705
+ summary: `${label} has the required ${scopeDescription}.`,
61706
+ remediation: null,
61707
+ details: { requiredScopes: [...requiredScopes], grantedScopes: granted, missingScopes: missing }
61708
+ };
61709
+ }
61710
+ return {
61711
+ status: CheckStatuses.fail,
61712
+ code: `${provider}.auth.required-scope-missing`,
61713
+ summary: `${label} is missing a required ${scopeDescription}.`,
61714
+ remediation: `Reconnect the integration and grant every required ${scopeDescription}.`,
61715
+ details: { requiredScopes: [...requiredScopes], grantedScopes: granted, missingScopes: missing }
61716
+ };
61717
+ }
61718
+ function snapshotOutput(provider, latestSnapshotAt, now) {
61719
+ const label = provider === "google-ads" ? "Google Ads" : "Google Tag Manager";
61720
+ if (!latestSnapshotAt) {
61721
+ return {
61722
+ status: CheckStatuses.warn,
61723
+ code: `${provider}.data.snapshot-never-captured`,
61724
+ summary: `${label} is connected but has no stored snapshot.`,
61725
+ remediation: "Run a read-only snapshot sync to establish a fresh configuration record.",
61726
+ details: { latestSnapshotAt: null }
61727
+ };
61728
+ }
61729
+ const snapshotMs = Date.parse(latestSnapshotAt);
61730
+ if (!Number.isFinite(snapshotMs)) {
61731
+ return {
61732
+ status: CheckStatuses.fail,
61733
+ code: `${provider}.data.snapshot-timestamp-invalid`,
61734
+ summary: `${label} has an invalid latest snapshot timestamp.`,
61735
+ remediation: "Run a new snapshot sync to replace the invalid freshness metadata.",
61736
+ details: { latestSnapshotAt }
61737
+ };
61738
+ }
61739
+ const ageDays = (now.getTime() - snapshotMs) / DAY_MS;
61740
+ const details = { latestSnapshotAt, ageDays: Math.max(0, Math.round(ageDays)) };
61741
+ if (ageDays < 0) {
61742
+ return {
61743
+ status: CheckStatuses.warn,
61744
+ code: `${provider}.data.snapshot-in-future`,
61745
+ summary: `${label} latest snapshot timestamp is in the future.`,
61746
+ remediation: "Check the host clock and run a new snapshot sync.",
61747
+ details
61748
+ };
61749
+ }
61750
+ if (ageDays > FRESH_SNAPSHOT_FAIL_DAYS) {
61751
+ return {
61752
+ status: CheckStatuses.fail,
61753
+ code: `${provider}.data.snapshot-stale`,
61754
+ summary: `${label} latest snapshot is ${Math.round(ageDays)} days old (> ${FRESH_SNAPSHOT_FAIL_DAYS}d).`,
61755
+ remediation: "Run a read-only snapshot sync and verify its schedule.",
61756
+ details
61757
+ };
61758
+ }
61759
+ if (ageDays > FRESH_SNAPSHOT_WARN_DAYS) {
61760
+ return {
61761
+ status: CheckStatuses.warn,
61762
+ code: `${provider}.data.snapshot-aging`,
61763
+ summary: `${label} latest snapshot is ${Math.round(ageDays)} days old (> ${FRESH_SNAPSHOT_WARN_DAYS}d).`,
61764
+ remediation: "Schedule a read-only snapshot sync to keep configuration evidence fresh.",
61765
+ details
61766
+ };
61767
+ }
61768
+ return {
61769
+ status: CheckStatuses.ok,
61770
+ code: `${provider}.data.snapshot-fresh`,
61771
+ summary: `${label} latest snapshot is ${Math.round(ageDays)} day(s) old.`,
61772
+ remediation: null,
61773
+ details
61774
+ };
61775
+ }
61776
+ function evaluateGoogleMarketingDoctor(input, now = /* @__PURE__ */ new Date()) {
61777
+ const ads = input.googleAds;
61778
+ const gtm = input.gtm;
61779
+ const adsCredentials = ads ? credentialsOutput("google-ads", ads.credentialsPresent) : notConnectedStatus("Google Ads", GOOGLE_ADS_REQUIRED_OAUTH_SCOPES);
61780
+ const adsScopes = ads ? scopesOutput("google-ads", ads.grantedScopes, GOOGLE_ADS_REQUIRED_OAUTH_SCOPES) : notConnectedStatus("Google Ads", GOOGLE_ADS_REQUIRED_OAUTH_SCOPES);
61781
+ const adsContext = !ads ? notConnectedStatus("Google Ads", GOOGLE_ADS_REQUIRED_OAUTH_SCOPES) : !ads.selectedCustomerId ? {
61782
+ status: CheckStatuses.fail,
61783
+ code: "google-ads.account.customer-not-selected",
61784
+ summary: "Google Ads has no selected customer context for this project.",
61785
+ remediation: "Select the customer whose campaigns and conversion goals this project should read.",
61786
+ details: {
61787
+ loginCustomerId: ads.selectedLoginCustomerId,
61788
+ customerId: ads.selectedCustomerId
61789
+ }
61790
+ } : {
61791
+ status: CheckStatuses.ok,
61792
+ code: "google-ads.account.context-selected",
61793
+ summary: "Google Ads customer context is selected.",
61794
+ remediation: null,
61795
+ details: {
61796
+ loginCustomerId: ads.selectedLoginCustomerId,
61797
+ customerId: ads.selectedCustomerId
61798
+ }
61799
+ };
61800
+ const adsSnapshot = ads ? snapshotOutput("google-ads", ads.latestSnapshotAt, now) : notConnectedStatus("Google Ads", GOOGLE_ADS_REQUIRED_OAUTH_SCOPES);
61801
+ const gtmCredentials = gtm ? credentialsOutput("gtm", gtm.credentialsPresent) : notConnectedStatus("Google Tag Manager", GTM_REQUIRED_OAUTH_SCOPES);
61802
+ const gtmScopes = gtm ? scopesOutput("gtm", gtm.grantedScopes, GTM_REQUIRED_OAUTH_SCOPES) : notConnectedStatus("Google Tag Manager", GTM_REQUIRED_OAUTH_SCOPES);
61803
+ const gtmContext = !gtm ? notConnectedStatus("Google Tag Manager", GTM_REQUIRED_OAUTH_SCOPES) : !gtm.selectedAccountId || !gtm.selectedContainerId ? {
61804
+ status: CheckStatuses.fail,
61805
+ code: "gtm.container.account-or-container-not-selected",
61806
+ summary: "GTM needs both a selected account and container for this project.",
61807
+ remediation: "Select the GTM account and container whose configuration this project should inspect.",
61808
+ details: {
61809
+ accountId: gtm.selectedAccountId,
61810
+ containerId: gtm.selectedContainerId,
61811
+ workspaceId: gtm.selectedWorkspaceId
61812
+ }
61813
+ } : !gtm.selectedWorkspaceId ? {
61814
+ status: CheckStatuses.warn,
61815
+ code: "gtm.container.workspace-not-selected",
61816
+ summary: "GTM account and container are selected, but no draft workspace is selected.",
61817
+ remediation: "Select a draft workspace when this project needs workspace-level configuration evidence.",
61818
+ details: {
61819
+ accountId: gtm.selectedAccountId,
61820
+ containerId: gtm.selectedContainerId,
61821
+ workspaceId: gtm.selectedWorkspaceId
61822
+ }
61823
+ } : {
61824
+ status: CheckStatuses.ok,
61825
+ code: "gtm.container.context-selected",
61826
+ summary: "GTM account, container, and workspace context are selected.",
61827
+ remediation: null,
61828
+ details: {
61829
+ accountId: gtm.selectedAccountId,
61830
+ containerId: gtm.selectedContainerId,
61831
+ workspaceId: gtm.selectedWorkspaceId
61832
+ }
61833
+ };
61834
+ const gtmSnapshot = gtm ? snapshotOutput("gtm", gtm.latestSnapshotAt, now) : notConnectedStatus("Google Tag Manager", GTM_REQUIRED_OAUTH_SCOPES);
61835
+ const gtmRuntime = !gtm ? {
61836
+ status: CheckStatuses.skipped,
61837
+ code: "gtm.runtime.not-connected",
61838
+ summary: "GTM is not connected, so API configuration and runtime firing cannot be assessed.",
61839
+ remediation: null
61840
+ } : {
61841
+ status: CheckStatuses.skipped,
61842
+ code: "gtm.runtime.firing-not-proven",
61843
+ summary: "GTM API configuration does not prove that a tag fired in a real browser session.",
61844
+ remediation: "Verify runtime firing with browser-side evidence or a trusted conversion receipt.",
61845
+ details: { runtimeFiringProven: false }
61846
+ };
61847
+ return [
61848
+ adsCredentials,
61849
+ adsScopes,
61850
+ adsContext,
61851
+ adsSnapshot,
61852
+ gtmCredentials,
61853
+ gtmScopes,
61854
+ gtmContext,
61855
+ gtmSnapshot,
61856
+ gtmRuntime
61857
+ ];
61858
+ }
61859
+ var CHECK_METADATA = [
61860
+ ["google-ads.auth.connection", CheckCategories.auth, "Google Ads credential metadata"],
61861
+ ["google-ads.auth.scopes", CheckCategories.auth, "Google Ads granted scopes"],
61862
+ ["google-ads.account.context", CheckCategories.auth, "Google Ads customer context"],
61863
+ ["google-ads.data.recent-snapshot", CheckCategories.integrations, "Google Ads latest snapshot"],
61864
+ ["gtm.auth.connection", CheckCategories.auth, "GTM credential metadata"],
61865
+ ["gtm.auth.scopes", CheckCategories.auth, "GTM granted scopes"],
61866
+ ["gtm.container.context", CheckCategories.auth, "GTM account, container, and workspace context"],
61867
+ ["gtm.data.recent-snapshot", CheckCategories.integrations, "GTM latest snapshot"],
61868
+ ["gtm.runtime.firing", CheckCategories.integrations, "GTM runtime firing evidence"]
61869
+ ];
61870
+ function createGoogleMarketingDoctorChecks(resolveInput, now = () => /* @__PURE__ */ new Date()) {
61871
+ return CHECK_METADATA.map(([id, category, title], index2) => ({
61872
+ id,
61873
+ category,
61874
+ scope: CheckScopes.project,
61875
+ title,
61876
+ run: (ctx) => {
61877
+ if (!ctx.project) return noProjectStatus();
61878
+ const input = resolveInput(ctx);
61879
+ if (!input) return unavailableStatus();
61880
+ return evaluateGoogleMarketingDoctor(input, now())[index2];
61881
+ }
61882
+ }));
61883
+ }
61884
+ var GOOGLE_MARKETING_DOCTOR_CHECKS = createGoogleMarketingDoctorChecks(
61885
+ (ctx) => ctx.getGoogleMarketingDoctorInput?.(ctx)
61886
+ );
61887
+
61643
61888
  // ../api-routes/src/doctor/registry.ts
61644
61889
  var ALL_CHECKS = [
61645
61890
  // Runtime-state checks run first so file-system gone errors surface
@@ -61652,6 +61897,7 @@ var ALL_CHECKS = [
61652
61897
  ...WORDPRESS_PUBLISH_CHECKS,
61653
61898
  ...GA_AUTH_CHECKS,
61654
61899
  ...ADS_CHECKS,
61900
+ ...GOOGLE_MARKETING_DOCTOR_CHECKS,
61655
61901
  ...PROVIDERS_CHECKS,
61656
61902
  ...TRAFFIC_SOURCE_CHECKS,
61657
61903
  ...BACKLINKS_CHECKS,
@@ -61748,7 +61994,8 @@ async function doctorRoutes(app, opts) {
61748
61994
  trafficSourceValidators: opts.trafficSourceValidators,
61749
61995
  runtimeStatePaths: opts.runtimeStatePaths,
61750
61996
  bundledSkills: opts.bundledSkills,
61751
- getAgentPluginState: opts.getAgentPluginState
61997
+ getAgentPluginState: opts.getAgentPluginState,
61998
+ getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
61752
61999
  };
61753
62000
  return runChecks(ctx, ALL_CHECKS, { checkIds });
61754
62001
  });
@@ -61776,7 +62023,8 @@ async function doctorRoutes(app, opts) {
61776
62023
  trafficSourceValidators: opts.trafficSourceValidators,
61777
62024
  runtimeStatePaths: opts.runtimeStatePaths,
61778
62025
  bundledSkills: opts.bundledSkills,
61779
- getAgentPluginState: opts.getAgentPluginState
62026
+ getAgentPluginState: opts.getAgentPluginState,
62027
+ getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
61780
62028
  };
61781
62029
  return runChecks(ctx, ALL_CHECKS, { checkIds });
61782
62030
  });
@@ -64704,7 +64952,8 @@ async function apiRoutes(app, opts) {
64704
64952
  trafficSourceValidators: buildTrafficSourceValidators(opts),
64705
64953
  runtimeStatePaths: opts.runtimeStatePaths,
64706
64954
  bundledSkills: opts.bundledSkills,
64707
- getAgentPluginState: opts.getAgentPluginState
64955
+ getAgentPluginState: opts.getAgentPluginState,
64956
+ getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
64708
64957
  });
64709
64958
  if (opts.registerAuthenticatedRoutes) {
64710
64959
  await opts.registerAuthenticatedRoutes(api);
@@ -5,7 +5,7 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-BMW3AURU.js";
8
+ } from "./chunk-IIFDS3K2.js";
9
9
  import {
10
10
  SKILL_MANIFEST_FILENAME,
11
11
  SkillsClients