@ainyc/canonry 4.60.1 → 4.61.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 (26) hide show
  1. package/assets/agent-workspace/skills/aero/references/regression-playbook.md +2 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -0
  3. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +23 -2
  4. package/assets/assets/{BacklinksPage-Dj4AVTma.js → BacklinksPage-CsGOAPNN.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-7SFwUlCh.js → ChartPrimitives-Bjow7aaC.js} +1 -1
  6. package/assets/assets/{ProjectPage-4tWuU1ZR.js → ProjectPage-BZoMD93_.js} +1 -1
  7. package/assets/assets/{RunRow-CgPJfmWX.js → RunRow-ve7H_XIu.js} +1 -1
  8. package/assets/assets/{RunsPage-Cdo4jBn4.js → RunsPage-mYmYevh0.js} +1 -1
  9. package/assets/assets/{SettingsPage-N5iccPoU.js → SettingsPage-DoRiIJK5.js} +1 -1
  10. package/assets/assets/{TrafficPage-pSlQPdDg.js → TrafficPage-1LFyX4OT.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BQx3Evf6.js → TrafficSourceDetailPage-BYDJtQdO.js} +1 -1
  12. package/assets/assets/{extract-error-message-BOembgFV.js → extract-error-message-Bt6jcL_M.js} +1 -1
  13. package/assets/assets/{index-DjKFsFsl.js → index-BQxaYi-t.js} +70 -70
  14. package/assets/assets/{server-traffic-DNgNJ4Ht.js → server-traffic-C5f87b84.js} +1 -1
  15. package/assets/assets/{trash-2-DOznxxMW.js → trash-2-BFSmyr_7.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-CKWHFAVB.js → chunk-PITZUUFV.js} +33 -1
  18. package/dist/{chunk-3G3GAT3E.js → chunk-QZN3J35I.js} +489 -156
  19. package/dist/{chunk-DXWUBWBD.js → chunk-URPUUKLC.js} +22 -0
  20. package/dist/{chunk-X4ZZFZQZ.js → chunk-ZTVBTGDW.js} +217 -68
  21. package/dist/cli.js +32 -4
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-UYVVKQ2K.js → intelligence-service-RWVBWSB5.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +10 -9
@@ -851,6 +851,10 @@ var gbpLocationDtoSchema = z7.object({
851
851
  primaryCategoryDisplayName: z7.string().nullable(),
852
852
  storefrontAddress: z7.string().nullable(),
853
853
  websiteUri: z7.string().nullable(),
854
+ // Google Maps Place ID + public Maps link (from location metadata; null when
855
+ // the location is not on Maps). `placeId` is the join key to the Places API.
856
+ placeId: z7.string().nullable(),
857
+ mapsUri: z7.string().nullable(),
854
858
  selected: z7.boolean(),
855
859
  syncedAt: z7.string().nullable(),
856
860
  createdAt: z7.string(),
@@ -944,6 +948,22 @@ var gbpLodgingListResponseSchema = z7.object({
944
948
  lodging: z7.array(gbpLodgingDtoSchema),
945
949
  total: z7.number().int().nonnegative()
946
950
  });
951
+ var gbpPlaceDetailsDtoSchema = z7.object({
952
+ locationName: z7.string(),
953
+ placeId: z7.string(),
954
+ /** Field-mask SKU tier the snapshot was fetched at ('atmosphere' | 'pro'). */
955
+ tier: z7.string(),
956
+ /** Amenities the public listing advertises, derived from `place`. */
957
+ amenities: z7.array(z7.string()),
958
+ /** When this listing was last fetched from Places — advances on every fetch, even when the content is unchanged (this is what the refresh-cadence gate reads). */
959
+ syncedAt: z7.string(),
960
+ /** Raw Place Details resource as Google returned it. */
961
+ place: z7.record(z7.string(), z7.unknown())
962
+ });
963
+ var gbpPlaceDetailsListResponseSchema = z7.object({
964
+ places: z7.array(gbpPlaceDetailsDtoSchema),
965
+ total: z7.number().int().nonnegative()
966
+ });
947
967
  var gbpSummaryDtoSchema = z7.object({
948
968
  scope: z7.object({
949
969
  locationName: z7.string().nullable(),
@@ -2099,6 +2119,7 @@ var projectSearchInsightHitSchema = z18.object({
2099
2119
  "competitor-lost",
2100
2120
  // Google Business Profile (local-AEO) insight types — see InsightType.
2101
2121
  "gbp-lodging-gap",
2122
+ "gbp-listing-discrepancy",
2102
2123
  "gbp-cta-gap",
2103
2124
  "gbp-metric-drop",
2104
2125
  "gbp-keyword-drop"
@@ -3887,6 +3908,7 @@ export {
3887
3908
  gbpKeywordImpressionListResponseSchema,
3888
3909
  gbpPlaceActionListResponseSchema,
3889
3910
  gbpLodgingListResponseSchema,
3911
+ gbpPlaceDetailsListResponseSchema,
3890
3912
  gbpSummaryDtoSchema,
3891
3913
  withRetry,
3892
3914
  isRetryableHttpError,
@@ -8,7 +8,7 @@ import {
8
8
  loadConfig,
9
9
  loadConfigRaw,
10
10
  saveConfigPatch
11
- } from "./chunk-CKWHFAVB.js";
11
+ } from "./chunk-PITZUUFV.js";
12
12
  import {
13
13
  CC_CACHE_DIR,
14
14
  DUCKDB_SPEC,
@@ -54,14 +54,17 @@ import {
54
54
  gbpLocations,
55
55
  gbpLodgingSnapshots,
56
56
  gbpPlaceActions,
57
+ gbpPlaceDetails,
57
58
  getCrawlIssues,
58
59
  getLodging,
60
+ getPlaceDetails,
59
61
  getUrlInfo,
60
62
  groupRunsByCreatedAt,
61
63
  gscCoverageSnapshots,
62
64
  gscSearchData,
63
65
  gscUrlInspections,
64
66
  hashLodging,
67
+ hashPlaceDetails,
65
68
  insights,
66
69
  inspectUrl,
67
70
  installDuckdb,
@@ -90,7 +93,7 @@ import {
90
93
  runs,
91
94
  schedules,
92
95
  usageCounters
93
- } from "./chunk-3G3GAT3E.js";
96
+ } from "./chunk-QZN3J35I.js";
94
97
  import {
95
98
  AGENT_MEMORY_VALUE_MAX_BYTES,
96
99
  AGENT_PROVIDER_IDS,
@@ -138,7 +141,7 @@ import {
138
141
  skillsClientSchema,
139
142
  validationError,
140
143
  withRetry
141
- } from "./chunk-DXWUBWBD.js";
144
+ } from "./chunk-URPUUKLC.js";
142
145
 
143
146
  // src/telemetry.ts
144
147
  import crypto from "crypto";
@@ -2692,6 +2695,21 @@ var perplexityAdapter = {
2692
2695
  }
2693
2696
  };
2694
2697
 
2698
+ // src/places-config.ts
2699
+ var DEFAULT_TIER = "atmosphere";
2700
+ var DEFAULT_REFRESH_INTERVAL_DAYS = 7;
2701
+ function parseTier(raw) {
2702
+ return raw === "atmosphere" || raw === "pro" || raw === "off" ? raw : void 0;
2703
+ }
2704
+ function getPlacesConfig(config) {
2705
+ const envKey = process.env.GOOGLE_PLACES_API_KEY?.trim();
2706
+ const apiKey = envKey || config.places?.apiKey || void 0;
2707
+ const tier = parseTier(process.env.GOOGLE_PLACES_TIER?.trim()) ?? config.places?.tier ?? DEFAULT_TIER;
2708
+ const envInterval = Number(process.env.GOOGLE_PLACES_REFRESH_INTERVAL_DAYS);
2709
+ const refreshIntervalDays = Number.isFinite(envInterval) && envInterval > 0 ? envInterval : config.places?.refreshIntervalDays ?? DEFAULT_REFRESH_INTERVAL_DAYS;
2710
+ return { apiKey, tier, refreshIntervalDays };
2711
+ }
2712
+
2695
2713
  // src/google-config.ts
2696
2714
  function ensureConnections(config) {
2697
2715
  if (!config.google) config.google = {};
@@ -3542,6 +3560,71 @@ function buildPhases(input) {
3542
3560
  // src/gsc-sync.ts
3543
3561
  import crypto4 from "crypto";
3544
3562
  import { eq as eq2, and as and2, sql as sql2 } from "drizzle-orm";
3563
+
3564
+ // src/gsc-inspect-paced.ts
3565
+ var INSPECT_BASE_DELAY_MS = 1e3;
3566
+ var INSPECT_PACING_JITTER_MS = 250;
3567
+ var INSPECT_MAX_RETRIES = 3;
3568
+ var INSPECT_MAX_BACKOFF_MS = 3e4;
3569
+ var INSPECT_FAILFAST_THRESHOLD = 5;
3570
+ function isRetryableGscInspectError(err) {
3571
+ if (err != null && typeof err === "object" && "status" in err) {
3572
+ if (err.status === 403) return true;
3573
+ }
3574
+ return isRetryableHttpError(err);
3575
+ }
3576
+ function defaultSleep(ms) {
3577
+ return new Promise((resolve) => setTimeout(resolve, ms));
3578
+ }
3579
+ async function inspectUrlsPaced(urls, cb, deps = {}) {
3580
+ const sleep3 = deps.sleep ?? defaultSleep;
3581
+ const jitter = deps.jitter ?? Math.random;
3582
+ let inspected = 0;
3583
+ let errors = 0;
3584
+ let consecutiveRetryableFailures = 0;
3585
+ for (let index = 0; index < urls.length; index++) {
3586
+ const url = urls[index];
3587
+ try {
3588
+ const result = await withRetry(() => cb.inspectOne(url), {
3589
+ maxRetries: INSPECT_MAX_RETRIES,
3590
+ baseDelayMs: INSPECT_BASE_DELAY_MS,
3591
+ maxDelayMs: INSPECT_MAX_BACKOFF_MS,
3592
+ isRetryable: isRetryableGscInspectError,
3593
+ sleep: sleep3,
3594
+ onRetry: ({ attempt, delayMs, err }) => deps.log?.info("inspect.retry", {
3595
+ url,
3596
+ attempt,
3597
+ delayMs: Math.round(delayMs),
3598
+ error: err instanceof Error ? err.message : String(err)
3599
+ })
3600
+ });
3601
+ cb.onResult(url, result, index);
3602
+ inspected++;
3603
+ consecutiveRetryableFailures = 0;
3604
+ } catch (err) {
3605
+ errors++;
3606
+ cb.onError(url, err, index);
3607
+ if (isRetryableGscInspectError(err)) {
3608
+ consecutiveRetryableFailures++;
3609
+ if (consecutiveRetryableFailures >= INSPECT_FAILFAST_THRESHOLD) {
3610
+ deps.log?.error("inspect.circuit-break", {
3611
+ consecutiveFailures: consecutiveRetryableFailures,
3612
+ inspected,
3613
+ errors,
3614
+ remaining: urls.length - index - 1
3615
+ });
3616
+ return { inspected, errors, aborted: true, abortError: err };
3617
+ }
3618
+ }
3619
+ }
3620
+ if (index < urls.length - 1) {
3621
+ await sleep3(INSPECT_BASE_DELAY_MS + jitter() * INSPECT_PACING_JITTER_MS);
3622
+ }
3623
+ }
3624
+ return { inspected, errors, aborted: false };
3625
+ }
3626
+
3627
+ // src/gsc-sync.ts
3545
3628
  var log2 = createLogger("GscSync");
3546
3629
  function formatDate(d) {
3547
3630
  return d.toISOString().split("T")[0];
@@ -3570,6 +3653,7 @@ async function executeGscSync(db, runId, projectId, opts) {
3570
3653
  if (!conn.propertyId) {
3571
3654
  throw new Error('No GSC property selected. Use "canonry google properties" to list available sites, then set one with the API.');
3572
3655
  }
3656
+ const propertyId = conn.propertyId;
3573
3657
  let accessToken = conn.accessToken;
3574
3658
  const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
3575
3659
  if (Date.now() > expiresAt - 5 * 60 * 1e3) {
@@ -3631,35 +3715,53 @@ async function executeGscSync(db, runId, projectId, opts) {
3631
3715
  }
3632
3716
  const topPages = [...pageClicks.entries()].sort((a, b) => b[1] - a[1]).slice(0, 50).map(([page]) => page);
3633
3717
  log2.info("inspect.start", { runId, projectId, urlCount: topPages.length });
3634
- for (const pageUrl of topPages) {
3635
- try {
3636
- const result = await inspectUrl(accessToken, pageUrl, conn.propertyId);
3637
- const ir = result.inspectionResult;
3638
- const idx = ir.indexStatusResult;
3639
- const mob = ir.mobileUsabilityResult;
3640
- const rich = ir.richResultsResult;
3641
- const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
3642
- db.insert(gscUrlInspections).values({
3643
- id: crypto4.randomUUID(),
3644
- projectId,
3645
- syncRunId: runId,
3646
- url: pageUrl,
3647
- indexingState: idx?.indexingState ?? null,
3648
- verdict: idx?.verdict ?? null,
3649
- coverageState: idx?.coverageState ?? null,
3650
- pageFetchState: idx?.pageFetchState ?? null,
3651
- robotsTxtState: idx?.robotsTxtState ?? null,
3652
- crawlTime: idx?.lastCrawlTime ?? null,
3653
- lastCrawlResult: idx?.crawlResult ?? null,
3654
- isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
3655
- richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
3656
- referringUrls: idx?.referringUrls ?? [],
3657
- inspectedAt,
3658
- createdAt: inspectedAt
3659
- }).run();
3660
- } catch (err) {
3661
- log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
3718
+ const inspectOutcome = await inspectUrlsPaced(
3719
+ topPages,
3720
+ {
3721
+ inspectOne: (pageUrl) => inspectUrl(accessToken, pageUrl, propertyId),
3722
+ onResult: (pageUrl, result) => {
3723
+ const ir = result.inspectionResult;
3724
+ const idx = ir.indexStatusResult;
3725
+ const mob = ir.mobileUsabilityResult;
3726
+ const rich = ir.richResultsResult;
3727
+ const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
3728
+ db.insert(gscUrlInspections).values({
3729
+ id: crypto4.randomUUID(),
3730
+ projectId,
3731
+ syncRunId: runId,
3732
+ url: pageUrl,
3733
+ indexingState: idx?.indexingState ?? null,
3734
+ verdict: idx?.verdict ?? null,
3735
+ coverageState: idx?.coverageState ?? null,
3736
+ pageFetchState: idx?.pageFetchState ?? null,
3737
+ robotsTxtState: idx?.robotsTxtState ?? null,
3738
+ crawlTime: idx?.lastCrawlTime ?? null,
3739
+ lastCrawlResult: idx?.crawlResult ?? null,
3740
+ isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
3741
+ richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
3742
+ referringUrls: idx?.referringUrls ?? [],
3743
+ inspectedAt,
3744
+ createdAt: inspectedAt
3745
+ }).run();
3746
+ },
3747
+ onError: (pageUrl, err) => {
3748
+ log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
3749
+ }
3750
+ },
3751
+ {
3752
+ log: {
3753
+ info: (action, ctx) => log2.info(action, { runId, projectId, ...ctx }),
3754
+ error: (action, ctx) => log2.error(action, { runId, projectId, ...ctx })
3755
+ }
3662
3756
  }
3757
+ );
3758
+ if (inspectOutcome.aborted) {
3759
+ log2.error("inspect.stopped-early", {
3760
+ runId,
3761
+ projectId,
3762
+ inspected: inspectOutcome.inspected,
3763
+ note: "URL inspection stopped early after sustained rate/access failures; search-analytics data was still saved"
3764
+ });
3663
3765
  }
3664
3766
  const allInspections = db.select().from(gscUrlInspections).where(eq2(gscUrlInspections.projectId, projectId)).all();
3665
3767
  const latestByUrl = /* @__PURE__ */ new Map();
@@ -3706,6 +3808,7 @@ async function executeGscSync(db, runId, projectId, opts) {
3706
3808
  // src/gbp-sync.ts
3707
3809
  import crypto5 from "crypto";
3708
3810
  import { eq as eq3, and as and3, desc, inArray as inArray2, lt } from "drizzle-orm";
3811
+ var MS_PER_DAY = 864e5;
3709
3812
  var log3 = createLogger("GbpSync");
3710
3813
  var LOCATION_CONCURRENCY = 4;
3711
3814
  var DEFAULT_DAYS_OF_METRICS = 30;
@@ -3765,6 +3868,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
3765
3868
  const keywordsEnd = monthMinus(0);
3766
3869
  const trendMonths = Array.from({ length: KEYWORD_TREND_MONTHS }, (_, i) => monthMinus(i + 1));
3767
3870
  const keywordRetentionCutoff = monthKey(monthMinus(KEYWORD_HISTORY_RETENTION_MONTHS));
3871
+ const { apiKey: placesApiKey, tier: placesTier, refreshIntervalDays: placesRefreshDays } = getPlacesConfig(opts.config);
3768
3872
  log3.info("sync.start", { runId, projectId, locations: locationRows.length, daysOfMetrics, monthsOfKeywords });
3769
3873
  const errors = /* @__PURE__ */ new Map();
3770
3874
  let okCount = 0;
@@ -3796,6 +3900,25 @@ async function executeGbpSync(db, runId, projectId, opts) {
3796
3900
  const lodgingHash = lodging ? hashLodging(lodging) : null;
3797
3901
  const latestLodging = lodging ? db.select().from(gbpLodgingSnapshots).where(and3(eq3(gbpLodgingSnapshots.projectId, projectId), eq3(gbpLodgingSnapshots.locationName, loc.locationName))).orderBy(desc(gbpLodgingSnapshots.syncedAt)).limit(1).get() : void 0;
3798
3902
  const lodgingChanged = lodging !== null && latestLodging?.contentHash !== lodgingHash;
3903
+ let placeToWrite = null;
3904
+ let placeToTouch = null;
3905
+ if (placesTier !== "off" && placesApiKey && lodging !== null && loc.placeId) {
3906
+ const latestPlace = db.select().from(gbpPlaceDetails).where(and3(eq3(gbpPlaceDetails.projectId, projectId), eq3(gbpPlaceDetails.locationName, loc.locationName))).orderBy(desc(gbpPlaceDetails.syncedAt)).limit(1).get();
3907
+ const ageDays = latestPlace ? (Date.now() - new Date(latestPlace.syncedAt).getTime()) / MS_PER_DAY : Infinity;
3908
+ if (ageDays >= placesRefreshDays) {
3909
+ try {
3910
+ const place = await getPlaceDetails(loc.placeId, placesApiKey, { tier: placesTier });
3911
+ const hash = hashPlaceDetails(place);
3912
+ if (!latestPlace || latestPlace.contentHash !== hash) {
3913
+ placeToWrite = { contentHash: hash, attributes: place, tier: placesTier, placeId: loc.placeId };
3914
+ } else {
3915
+ placeToTouch = latestPlace.id;
3916
+ }
3917
+ } catch (placesErr) {
3918
+ log3.warn("places.failed", { runId, location: loc.locationName, error: placesErr instanceof Error ? placesErr.message : String(placesErr) });
3919
+ }
3920
+ }
3921
+ }
3799
3922
  const insertNow = (/* @__PURE__ */ new Date()).toISOString();
3800
3923
  db.transaction((tx) => {
3801
3924
  tx.delete(gbpDailyMetrics).where(and3(eq3(gbpDailyMetrics.projectId, projectId), eq3(gbpDailyMetrics.locationName, loc.locationName))).run();
@@ -3878,6 +4001,21 @@ async function executeGbpSync(db, runId, projectId, opts) {
3878
4001
  syncRunId: runId
3879
4002
  }).run();
3880
4003
  }
4004
+ if (placeToWrite) {
4005
+ tx.insert(gbpPlaceDetails).values({
4006
+ id: crypto5.randomUUID(),
4007
+ projectId,
4008
+ locationName: loc.locationName,
4009
+ placeId: placeToWrite.placeId,
4010
+ contentHash: placeToWrite.contentHash,
4011
+ tier: placeToWrite.tier,
4012
+ attributes: placeToWrite.attributes,
4013
+ syncedAt: insertNow,
4014
+ syncRunId: runId
4015
+ }).run();
4016
+ } else if (placeToTouch) {
4017
+ tx.update(gbpPlaceDetails).set({ syncedAt: insertNow, syncRunId: runId }).where(eq3(gbpPlaceDetails.id, placeToTouch)).run();
4018
+ }
3881
4019
  tx.update(gbpLocations).set({ syncedAt: insertNow, updatedAt: insertNow }).where(eq3(gbpLocations.id, loc.id)).run();
3882
4020
  });
3883
4021
  okCount++;
@@ -4057,6 +4195,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
4057
4195
  if (!conn.propertyId) {
4058
4196
  throw new Error('No GSC property selected. Use "canonry google properties" to list available sites, then set one.');
4059
4197
  }
4198
+ const propertyId = conn.propertyId;
4060
4199
  let accessToken = conn.accessToken;
4061
4200
  const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
4062
4201
  if (Date.now() > expiresAt - 5 * 60 * 1e3) {
@@ -4076,43 +4215,52 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
4076
4215
  if (urls.length === 0) {
4077
4216
  throw new Error("No URLs found in sitemap");
4078
4217
  }
4079
- let inspected = 0;
4080
- let errors = 0;
4081
- for (const pageUrl of urls) {
4082
- try {
4083
- const result = await inspectUrl(accessToken, pageUrl, conn.propertyId);
4084
- const ir = result.inspectionResult;
4085
- const idx = ir.indexStatusResult;
4086
- const mob = ir.mobileUsabilityResult;
4087
- const rich = ir.richResultsResult;
4088
- const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
4089
- db.insert(gscUrlInspections).values({
4090
- id: crypto6.randomUUID(),
4091
- projectId,
4092
- syncRunId: runId,
4093
- url: pageUrl,
4094
- indexingState: idx?.indexingState ?? null,
4095
- verdict: idx?.verdict ?? null,
4096
- coverageState: idx?.coverageState ?? null,
4097
- pageFetchState: idx?.pageFetchState ?? null,
4098
- robotsTxtState: idx?.robotsTxtState ?? null,
4099
- crawlTime: idx?.lastCrawlTime ?? null,
4100
- lastCrawlResult: idx?.crawlResult ?? null,
4101
- isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
4102
- richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
4103
- referringUrls: idx?.referringUrls ?? [],
4104
- inspectedAt,
4105
- createdAt: inspectedAt
4106
- }).run();
4107
- inspected++;
4108
- log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${inspected}/${urls.length}` });
4109
- } catch (err) {
4110
- errors++;
4111
- log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
4112
- }
4113
- if (inspected + errors < urls.length) {
4114
- await new Promise((r) => setTimeout(r, 1e3));
4218
+ const { inspected, errors, aborted, abortError } = await inspectUrlsPaced(
4219
+ urls,
4220
+ {
4221
+ inspectOne: (pageUrl) => inspectUrl(accessToken, pageUrl, propertyId),
4222
+ onResult: (pageUrl, result, index) => {
4223
+ const ir = result.inspectionResult;
4224
+ const idx = ir.indexStatusResult;
4225
+ const mob = ir.mobileUsabilityResult;
4226
+ const rich = ir.richResultsResult;
4227
+ const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
4228
+ db.insert(gscUrlInspections).values({
4229
+ id: crypto6.randomUUID(),
4230
+ projectId,
4231
+ syncRunId: runId,
4232
+ url: pageUrl,
4233
+ indexingState: idx?.indexingState ?? null,
4234
+ verdict: idx?.verdict ?? null,
4235
+ coverageState: idx?.coverageState ?? null,
4236
+ pageFetchState: idx?.pageFetchState ?? null,
4237
+ robotsTxtState: idx?.robotsTxtState ?? null,
4238
+ crawlTime: idx?.lastCrawlTime ?? null,
4239
+ lastCrawlResult: idx?.crawlResult ?? null,
4240
+ isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
4241
+ richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
4242
+ referringUrls: idx?.referringUrls ?? [],
4243
+ inspectedAt,
4244
+ createdAt: inspectedAt
4245
+ }).run();
4246
+ log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${index + 1}/${urls.length}` });
4247
+ },
4248
+ onError: (pageUrl, err) => {
4249
+ log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
4250
+ }
4251
+ },
4252
+ {
4253
+ log: {
4254
+ info: (action, ctx) => log5.info(action, { runId, projectId, ...ctx }),
4255
+ error: (action, ctx) => log5.error(action, { runId, projectId, ...ctx })
4256
+ }
4115
4257
  }
4258
+ );
4259
+ if (aborted) {
4260
+ const detail = abortError instanceof Error ? abortError.message : String(abortError);
4261
+ throw new Error(
4262
+ `URL inspection aborted after ${INSPECT_FAILFAST_THRESHOLD} consecutive rate/access failures (likely GSC URL Inspection quota exhaustion or property access loss). Last error: ${detail}`
4263
+ );
4116
4264
  }
4117
4265
  const allInspections = db.select().from(gscUrlInspections).where(eq4(gscUrlInspections.projectId, projectId)).all();
4118
4266
  const latestByUrl = /* @__PURE__ */ new Map();
@@ -5468,7 +5616,7 @@ function readStoredGroundingSources(rawResponse) {
5468
5616
  return result;
5469
5617
  }
5470
5618
  async function backfillInsightsCommand(project, opts) {
5471
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-UYVVKQ2K.js");
5619
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-RWVBWSB5.js");
5472
5620
  const config = loadConfig();
5473
5621
  const db = createClient(config.database);
5474
5622
  migrate(db);
@@ -9682,6 +9830,7 @@ async function createServer(opts) {
9682
9830
  registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
9683
9831
  },
9684
9832
  getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
9833
+ getPlacesConfig: () => getPlacesConfig(opts.config),
9685
9834
  googleConnectionStore,
9686
9835
  googleStateSecret,
9687
9836
  publicUrl: opts.config.publicUrl,
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-X4ZZFZQZ.js";
30
+ } from "./chunk-ZTVBTGDW.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
@@ -43,7 +43,7 @@ import {
43
43
  saveConfig,
44
44
  saveConfigPatch,
45
45
  usageError
46
- } from "./chunk-CKWHFAVB.js";
46
+ } from "./chunk-PITZUUFV.js";
47
47
  import {
48
48
  apiKeys,
49
49
  createClient,
@@ -51,7 +51,7 @@ import {
51
51
  projects,
52
52
  queries,
53
53
  renderReportHtml
54
- } from "./chunk-3G3GAT3E.js";
54
+ } from "./chunk-QZN3J35I.js";
55
55
  import {
56
56
  CcReleaseSyncStatuses,
57
57
  CheckScopes,
@@ -67,7 +67,7 @@ import {
67
67
  notificationEventSchema,
68
68
  providerQuotaPolicySchema,
69
69
  resolveProviderInput
70
- } from "./chunk-DXWUBWBD.js";
70
+ } from "./chunk-URPUUKLC.js";
71
71
 
72
72
  // src/cli.ts
73
73
  import { pathToFileURL } from "url";
@@ -3069,6 +3069,23 @@ async function gbpLodging(project, opts) {
3069
3069
  console.log(` ${l.locationName} ${l.populatedGroupCount} attribute group(s)${note}`);
3070
3070
  }
3071
3071
  }
3072
+ async function gbpPlaces(project, opts) {
3073
+ const client = getClient6();
3074
+ const response = await client.listGbpPlaces(project, { locationName: opts.location });
3075
+ if (opts.format === "json") {
3076
+ console.log(JSON.stringify(response, null, 2));
3077
+ return;
3078
+ }
3079
+ if (response.places.length === 0) {
3080
+ console.log('No Places data \u2014 set a Places API key (places.apiKey / GOOGLE_PLACES_API_KEY) and run "canonry gbp sync" for lodging locations.');
3081
+ return;
3082
+ }
3083
+ console.log(`${response.total} Places listing snapshot(s) \u2014 the amenities Google's public listing advertises:`);
3084
+ for (const p of response.places) {
3085
+ const amenities = p.amenities.length > 0 ? p.amenities.join(", ") : "(none detected)";
3086
+ console.log(` ${p.locationName} [${p.tier}] ${amenities}`);
3087
+ }
3088
+ }
3072
3089
  function fmtDelta(pct2) {
3073
3090
  if (pct2 === null) return "n/a";
3074
3091
  return `${pct2 >= 0 ? "+" : ""}${pct2}%`;
@@ -3261,6 +3278,17 @@ var GBP_CLI_COMMANDS = [
3261
3278
  await gbpLodging(project, { location: getString(input.values, "location"), format: input.format });
3262
3279
  }
3263
3280
  },
3281
+ {
3282
+ path: ["gbp", "places"],
3283
+ usage: "canonry gbp places <project> [--location <name>] [--format json]",
3284
+ options: {
3285
+ location: stringOption()
3286
+ },
3287
+ run: async (input) => {
3288
+ const project = requireProject(input, "gbp.places", "canonry gbp places <project> [--location <name>] [--format json]");
3289
+ await gbpPlaces(project, { location: getString(input.values, "location"), format: input.format });
3290
+ }
3291
+ },
3264
3292
  {
3265
3293
  path: ["gbp", "summary"],
3266
3294
  usage: "canonry gbp summary <project> [--location <name>] [--format json]",
package/dist/index.d.ts CHANGED
@@ -144,6 +144,21 @@ interface AgentConfigEntry {
144
144
  /** Agent mode. Only 'disabled' is valid until the native loop ships. */
145
145
  mode?: 'disabled';
146
146
  }
147
+ /**
148
+ * Google Places API config — supplemental rendered-listing data for GBP
149
+ * lodging locations (#648). The API key authenticates Place Details calls
150
+ * (`X-Goog-Api-Key`); it is NOT OAuth and is unrelated to `google.clientId`.
151
+ * - `tier`: 'atmosphere' (default; amenity booleans for the cross-reference,
152
+ * 1k free calls/month) | 'pro' (cheaper, accessibility-only) | 'off'.
153
+ * - `refreshIntervalDays`: minimum age before a location's Place Details is
154
+ * re-fetched during gbp-sync (default 7) — the cost lever, since amenities
155
+ * change rarely.
156
+ */
157
+ interface PlacesConfigEntry {
158
+ apiKey?: string;
159
+ tier?: 'atmosphere' | 'pro' | 'off';
160
+ refreshIntervalDays?: number;
161
+ }
147
162
  interface CanonryConfig {
148
163
  apiUrl: string;
149
164
  publicUrl?: string;
@@ -172,6 +187,7 @@ interface CanonryConfig {
172
187
  lastUpdateCheckAt?: string;
173
188
  lastKnownLatestVersion?: string;
174
189
  agent?: AgentConfigEntry;
190
+ places?: PlacesConfigEntry;
175
191
  }
176
192
  declare function loadConfig(): CanonryConfig;
177
193
 
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-X4ZZFZQZ.js";
3
+ } from "./chunk-ZTVBTGDW.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-CKWHFAVB.js";
7
- import "./chunk-3G3GAT3E.js";
8
- import "./chunk-DXWUBWBD.js";
6
+ } from "./chunk-PITZUUFV.js";
7
+ import "./chunk-QZN3J35I.js";
8
+ import "./chunk-URPUUKLC.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-3G3GAT3E.js";
4
- import "./chunk-DXWUBWBD.js";
3
+ } from "./chunk-QZN3J35I.js";
4
+ import "./chunk-URPUUKLC.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-CKWHFAVB.js";
7
- import "./chunk-DXWUBWBD.js";
6
+ } from "./chunk-PITZUUFV.js";
7
+ import "./chunk-URPUUKLC.js";
8
8
 
9
9
  // src/mcp/cli.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.60.1",
3
+ "version": "4.61.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -61,25 +61,26 @@
61
61
  "@types/node-cron": "^3.0.11",
62
62
  "tsup": "^8.5.1",
63
63
  "tsx": "^4.19.0",
64
- "@ainyc/canonry-api-client": "0.0.0",
65
- "@ainyc/canonry-contracts": "0.0.0",
66
64
  "@ainyc/canonry-config": "0.0.0",
67
- "@ainyc/canonry-db": "0.0.0",
65
+ "@ainyc/canonry-api-client": "0.0.0",
68
66
  "@ainyc/canonry-api-routes": "0.0.0",
67
+ "@ainyc/canonry-db": "0.0.0",
69
68
  "@ainyc/canonry-integration-bing": "0.0.0",
69
+ "@ainyc/canonry-contracts": "0.0.0",
70
70
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
71
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
72
- "@ainyc/canonry-integration-google": "0.0.0",
73
71
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
72
+ "@ainyc/canonry-integration-google": "0.0.0",
73
+ "@ainyc/canonry-integration-google-places": "0.0.0",
74
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
74
75
  "@ainyc/canonry-integration-traffic": "0.0.0",
75
- "@ainyc/canonry-integration-wordpress": "0.0.0",
76
76
  "@ainyc/canonry-intelligence": "0.0.0",
77
77
  "@ainyc/canonry-provider-cdp": "0.0.0",
78
78
  "@ainyc/canonry-provider-claude": "0.0.0",
79
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
79
80
  "@ainyc/canonry-provider-gemini": "0.0.0",
80
- "@ainyc/canonry-provider-local": "0.0.0",
81
81
  "@ainyc/canonry-provider-openai": "0.0.0",
82
- "@ainyc/canonry-provider-perplexity": "0.0.0"
82
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
83
+ "@ainyc/canonry-provider-local": "0.0.0"
83
84
  },
84
85
  "scripts": {
85
86
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",