@ainyc/canonry 4.91.1 → 4.93.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 +1 -0
  2. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +7 -0
  3. package/assets/assets/{BacklinksPage-DHC1po5Q.js → BacklinksPage-nkgAbbjk.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-Cu2xEVle.js → ChartPrimitives-kdfylr1f.js} +1 -1
  5. package/assets/assets/ProjectPage-BQYmq7FP.js +6 -0
  6. package/assets/assets/{RunRow-BPqbBlXl.js → RunRow-P-n5OkiY.js} +1 -1
  7. package/assets/assets/{RunsPage-BJx3_Nel.js → RunsPage-DfBb4WCS.js} +1 -1
  8. package/assets/assets/{SettingsPage-CXTIHdsv.js → SettingsPage-CRGuFgeK.js} +1 -1
  9. package/assets/assets/{TrafficPage-iibCIBqX.js → TrafficPage-CMqP0AGZ.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-WU30E_59.js → TrafficSourceDetailPage-XRGzkKWk.js} +1 -1
  11. package/assets/assets/{arrow-left-Ds_f14R8.js → arrow-left-BCR55g4n.js} +1 -1
  12. package/assets/assets/{extract-error-message-D250utEX.js → extract-error-message-BG1AlucT.js} +1 -1
  13. package/assets/assets/{index-qiysJOcG.js → index-BPjIZIDO.js} +73 -73
  14. package/assets/assets/{index-fLUYE9Z5.css → index-DiykVUPr.css} +1 -1
  15. package/assets/assets/{trash-2-CcVWAeec.js → trash-2-Cme1gNjU.js} +1 -1
  16. package/assets/index.html +2 -2
  17. package/dist/{chunk-KFJPJBW3.js → chunk-ADTLQHJY.js} +69 -5
  18. package/dist/{chunk-WMBOHP7E.js → chunk-AUQNKEHV.js} +20 -0
  19. package/dist/{chunk-4KQYRCVC.js → chunk-KBDQ5OE4.js} +181 -56
  20. package/dist/{chunk-6RVUSM4Y.js → chunk-PK46TKGM.js} +33 -1
  21. package/dist/cli.js +36 -4
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-QB6HQRWU.js → intelligence-service-4ATCJITP.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +10 -10
  26. package/assets/assets/ProjectPage-DXYcaMVO.js +0 -6
@@ -137,6 +137,7 @@ import {
137
137
  ga4SyncResponseDtoSchema,
138
138
  gateHarvestedSearchQueries,
139
139
  gbpAccountListResponseSchema,
140
+ gbpAttributesListResponseSchema,
140
141
  gbpDailyMetricListResponseSchema,
141
142
  gbpDiscoverRequestSchema,
142
143
  gbpKeywordImpressionListResponseSchema,
@@ -258,7 +259,7 @@ import {
258
259
  wordpressSchemaDeployResultDtoSchema,
259
260
  wordpressSchemaStatusResultDtoSchema,
260
261
  wordpressStatusDtoSchema
261
- } from "./chunk-WMBOHP7E.js";
262
+ } from "./chunk-AUQNKEHV.js";
262
263
 
263
264
  // src/intelligence-service.ts
264
265
  import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -302,6 +303,7 @@ __export(schema_exports, {
302
303
  gaTrafficSnapshots: () => gaTrafficSnapshots,
303
304
  gaTrafficSummaries: () => gaTrafficSummaries,
304
305
  gaTrafficWindowSummaries: () => gaTrafficWindowSummaries,
306
+ gbpAttributesSnapshots: () => gbpAttributesSnapshots,
305
307
  gbpDailyMetrics: () => gbpDailyMetrics,
306
308
  gbpKeywordImpressions: () => gbpKeywordImpressions,
307
309
  gbpKeywordMonthly: () => gbpKeywordMonthly,
@@ -1276,6 +1278,18 @@ var gbpPlaceDetails = sqliteTable("gbp_place_details", {
1276
1278
  }, (table) => [
1277
1279
  index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
1278
1280
  ]);
1281
+ var gbpAttributesSnapshots = sqliteTable("gbp_attributes_snapshots", {
1282
+ id: text("id").primaryKey(),
1283
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1284
+ locationName: text("location_name").notNull(),
1285
+ contentHash: text("content_hash").notNull(),
1286
+ attributes: text("attributes", { mode: "json" }).$type().notNull().default([]),
1287
+ attributeCount: integer("attribute_count").notNull().default(0),
1288
+ syncedAt: text("synced_at").notNull(),
1289
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
1290
+ }, (table) => [
1291
+ index("idx_gbp_attributes_loc").on(table.projectId, table.locationName, table.syncedAt)
1292
+ ]);
1279
1293
  var adsConnections = sqliteTable("ads_connections", {
1280
1294
  id: text("id").primaryKey(),
1281
1295
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -3166,6 +3180,23 @@ var MIGRATION_VERSIONS = [
3166
3180
  }
3167
3181
  }
3168
3182
  }
3183
+ },
3184
+ {
3185
+ version: 82,
3186
+ name: "gbp-attributes-snapshots",
3187
+ statements: [
3188
+ `CREATE TABLE IF NOT EXISTS gbp_attributes_snapshots (
3189
+ id TEXT PRIMARY KEY,
3190
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3191
+ location_name TEXT NOT NULL,
3192
+ content_hash TEXT NOT NULL,
3193
+ attributes TEXT NOT NULL DEFAULT '[]',
3194
+ attribute_count INTEGER NOT NULL DEFAULT 0,
3195
+ synced_at TEXT NOT NULL,
3196
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
3197
+ )`,
3198
+ `CREATE INDEX IF NOT EXISTS idx_gbp_attributes_loc ON gbp_attributes_snapshots(project_id, location_name, synced_at)`
3199
+ ]
3169
3200
  }
3170
3201
  ];
3171
3202
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -5269,6 +5300,18 @@ function analyzeGbp(signals) {
5269
5300
  });
5270
5301
  }
5271
5302
  }
5303
+ if (loc.descriptionMissing) {
5304
+ drafts.push({
5305
+ ...base,
5306
+ type: "gbp-description-missing",
5307
+ severity: "low",
5308
+ title: `${loc.displayName}: no business description set`,
5309
+ recommendation: {
5310
+ action: "Add a business description (up to 750 characters) in the Google Business Profile.",
5311
+ reason: "The owner description is the cheapest owner-controlled prose an AI answer engine can lift to describe the business, and it seeds the entity attributes (specialties, service area, differentiators) models draw on. It reads straight from the GBP API, so an empty one is a real, easily-closed gap."
5312
+ }
5313
+ });
5314
+ }
5272
5315
  if (loc.placeActionCount > 0 && !loc.hasDirectMerchantCta) {
5273
5316
  drafts.push({
5274
5317
  ...base,
@@ -13766,6 +13809,7 @@ var SCHEMA_TABLE = {
13766
13809
  GbpLocationDto: gbpLocationDtoSchema,
13767
13810
  GbpLocationListResponse: gbpLocationListResponseSchema,
13768
13811
  GbpLodgingListResponse: gbpLodgingListResponseSchema,
13812
+ GbpAttributesListResponse: gbpAttributesListResponseSchema,
13769
13813
  GbpPlaceActionListResponse: gbpPlaceActionListResponseSchema,
13770
13814
  GbpPlaceDetailsListResponse: gbpPlaceDetailsListResponseSchema,
13771
13815
  GbpSummaryDto: gbpSummaryDtoSchema,
@@ -15779,6 +15823,20 @@ var routeCatalog = [
15779
15823
  404: errorResponse("Project not found.")
15780
15824
  }
15781
15825
  },
15826
+ {
15827
+ method: "get",
15828
+ path: "/api/v1/projects/{name}/gbp/attributes",
15829
+ summary: "List latest Google Business Profile owner-set attribute snapshots per location",
15830
+ tags: ["gbp"],
15831
+ parameters: [
15832
+ nameParameter,
15833
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
15834
+ ],
15835
+ responses: {
15836
+ 200: jsonResponse("Attribute snapshots returned.", "GbpAttributesListResponse"),
15837
+ 404: errorResponse("Project not found.")
15838
+ }
15839
+ },
15782
15840
  {
15783
15841
  method: "get",
15784
15842
  path: "/api/v1/projects/{name}/gbp/places",
@@ -18517,7 +18575,7 @@ function formatNotification(row) {
18517
18575
  }
18518
18576
 
18519
18577
  // ../api-routes/src/google.ts
18520
- import crypto17 from "crypto";
18578
+ import crypto18 from "crypto";
18521
18579
  import { eq as eq21, and as and14, desc as desc11, sql as sql8, inArray as inArray10 } from "drizzle-orm";
18522
18580
 
18523
18581
  // ../api-routes/src/gbp-summary.ts
@@ -20070,6 +20128,46 @@ function stableStringify2(value) {
20070
20128
  return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify2(obj[k])}`).join(",")}}`;
20071
20129
  }
20072
20130
 
20131
+ // ../integration-google-business-profile/src/attributes-client.ts
20132
+ import crypto17 from "crypto";
20133
+ async function getAttributes(accessToken, locationName, opts = {}) {
20134
+ const url = `${GBP_BUSINESS_INFO_BASE}/${locationName}/attributes`;
20135
+ let res;
20136
+ try {
20137
+ res = await gbpFetchGet(url, accessToken, opts);
20138
+ } catch (err) {
20139
+ if (err instanceof GbpApiError && err.status === 404) return [];
20140
+ throw err;
20141
+ }
20142
+ return (res.attributes ?? []).map(normalizeAttribute).filter((a) => a.name.length > 0);
20143
+ }
20144
+ function normalizeAttribute(raw) {
20145
+ const values = [];
20146
+ for (const v of raw.values ?? []) {
20147
+ if (typeof v === "boolean" || typeof v === "string") values.push(v);
20148
+ }
20149
+ for (const s of raw.repeatedEnumValue?.setValues ?? []) values.push(s);
20150
+ const uris = [];
20151
+ for (const u of raw.uriValues ?? []) {
20152
+ if (u?.uri) uris.push(u.uri);
20153
+ }
20154
+ return { name: raw.name ?? "", valueType: raw.valueType ?? "", values, uris };
20155
+ }
20156
+ function countAttributes(attrs) {
20157
+ return attrs.length;
20158
+ }
20159
+ function hashAttributes(attrs) {
20160
+ const sorted = [...attrs].sort((a, b) => a.name.localeCompare(b.name));
20161
+ return crypto17.createHash("sha256").update(stableStringify3(sorted)).digest("hex");
20162
+ }
20163
+ function stableStringify3(value) {
20164
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
20165
+ if (Array.isArray(value)) return `[${value.map(stableStringify3).join(",")}]`;
20166
+ const obj = value;
20167
+ const keys = Object.keys(obj).sort();
20168
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify3(obj[k])}`).join(",")}}`;
20169
+ }
20170
+
20073
20171
  // ../api-routes/src/google.ts
20074
20172
  function scopesForConnectionType(type) {
20075
20173
  switch (type) {
@@ -20082,7 +20180,7 @@ function scopesForConnectionType(type) {
20082
20180
  }
20083
20181
  }
20084
20182
  function signState(payload, secret) {
20085
- return crypto17.createHmac("sha256", secret).update(payload).digest("hex");
20183
+ return crypto18.createHmac("sha256", secret).update(payload).digest("hex");
20086
20184
  }
20087
20185
  function buildSignedState(data, secret) {
20088
20186
  const payload = JSON.stringify(data);
@@ -20093,7 +20191,7 @@ function verifySignedState(encoded, secret) {
20093
20191
  try {
20094
20192
  const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
20095
20193
  const expected = signState(payload, secret);
20096
- if (!crypto17.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
20194
+ if (!crypto18.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
20097
20195
  return JSON.parse(payload);
20098
20196
  } catch {
20099
20197
  return null;
@@ -20429,7 +20527,7 @@ async function googleRoutes(app, opts) {
20429
20527
  throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
20430
20528
  }
20431
20529
  const now = (/* @__PURE__ */ new Date()).toISOString();
20432
- const runId = crypto17.randomUUID();
20530
+ const runId = crypto18.randomUUID();
20433
20531
  app.db.insert(runs).values({
20434
20532
  id: runId,
20435
20533
  projectId: project.id,
@@ -20527,7 +20625,7 @@ async function googleRoutes(app, opts) {
20527
20625
  const mob = ir.mobileUsabilityResult;
20528
20626
  const rich = ir.richResultsResult;
20529
20627
  const now = (/* @__PURE__ */ new Date()).toISOString();
20530
- const id = crypto17.randomUUID();
20628
+ const id = crypto18.randomUUID();
20531
20629
  app.db.insert(gscUrlInspections).values({
20532
20630
  id,
20533
20631
  projectId: project.id,
@@ -20771,7 +20869,7 @@ async function googleRoutes(app, opts) {
20771
20869
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
20772
20870
  });
20773
20871
  const now = (/* @__PURE__ */ new Date()).toISOString();
20774
- const runId = crypto17.randomUUID();
20872
+ const runId = crypto18.randomUUID();
20775
20873
  app.db.insert(runs).values({
20776
20874
  id: runId,
20777
20875
  projectId: project.id,
@@ -20797,7 +20895,7 @@ async function googleRoutes(app, opts) {
20797
20895
  throw validationError("No GSC property configured for this connection");
20798
20896
  }
20799
20897
  const now = (/* @__PURE__ */ new Date()).toISOString();
20800
- const runId = crypto17.randomUUID();
20898
+ const runId = crypto18.randomUUID();
20801
20899
  app.db.insert(runs).values({
20802
20900
  id: runId,
20803
20901
  projectId: project.id,
@@ -21078,7 +21176,7 @@ async function googleRoutes(app, opts) {
21078
21176
  }).where(eq21(gbpLocations.id, existing.id)).run();
21079
21177
  } else {
21080
21178
  tx.insert(gbpLocations).values({
21081
- id: crypto17.randomUUID(),
21179
+ id: crypto18.randomUUID(),
21082
21180
  projectId: project.id,
21083
21181
  accountName,
21084
21182
  locationName: remote.name,
@@ -21202,7 +21300,7 @@ async function googleRoutes(app, opts) {
21202
21300
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
21203
21301
  }
21204
21302
  const now = (/* @__PURE__ */ new Date()).toISOString();
21205
- const runId = crypto17.randomUUID();
21303
+ const runId = crypto18.randomUUID();
21206
21304
  app.db.insert(runs).values({
21207
21305
  id: runId,
21208
21306
  projectId: project.id,
@@ -21279,6 +21377,23 @@ async function googleRoutes(app, opts) {
21279
21377
  }));
21280
21378
  return { lodging, total: lodging.length };
21281
21379
  });
21380
+ app.get("/projects/:name/gbp/attributes", async (request) => {
21381
+ const project = resolveProject(app.db, request.params.name);
21382
+ const conditions = [eq21(gbpAttributesSnapshots.projectId, project.id)];
21383
+ if (request.query.locationName) conditions.push(eq21(gbpAttributesSnapshots.locationName, request.query.locationName));
21384
+ const rows = app.db.select().from(gbpAttributesSnapshots).where(and14(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
21385
+ const latestByLocation = /* @__PURE__ */ new Map();
21386
+ for (const row of rows) {
21387
+ if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
21388
+ }
21389
+ const attributes = [...latestByLocation.values()].map((r) => ({
21390
+ locationName: r.locationName,
21391
+ attributeCount: r.attributeCount,
21392
+ syncedAt: r.syncedAt,
21393
+ attributes: r.attributes
21394
+ }));
21395
+ return { attributes, total: attributes.length };
21396
+ });
21282
21397
  app.get("/projects/:name/gbp/places", async (request) => {
21283
21398
  const project = resolveProject(app.db, request.params.name);
21284
21399
  const conditions = [eq21(gbpPlaceDetails.projectId, project.id)];
@@ -21348,7 +21463,7 @@ async function googleRoutes(app, opts) {
21348
21463
  }
21349
21464
 
21350
21465
  // ../api-routes/src/ads.ts
21351
- import crypto18 from "crypto";
21466
+ import crypto19 from "crypto";
21352
21467
  import { eq as eq22, and as and15, asc as asc2, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
21353
21468
  function statusDto(row) {
21354
21469
  if (!row) return { connected: false };
@@ -21411,7 +21526,7 @@ async function adsRoutes(app, opts) {
21411
21526
  }).where(eq22(adsConnections.id, existingRow.id)).run();
21412
21527
  } else {
21413
21528
  tx.insert(adsConnections).values({
21414
- id: crypto18.randomUUID(),
21529
+ id: crypto19.randomUUID(),
21415
21530
  projectId: project.id,
21416
21531
  adAccountId: account.id,
21417
21532
  displayName: account.name,
@@ -21473,7 +21588,7 @@ async function adsRoutes(app, opts) {
21473
21588
  const existing = { runId: inFlight.id, status: inFlight.status };
21474
21589
  return existing;
21475
21590
  }
21476
- const runId = crypto18.randomUUID();
21591
+ const runId = crypto19.randomUUID();
21477
21592
  app.db.insert(runs).values({
21478
21593
  id: runId,
21479
21594
  projectId: project.id,
@@ -21608,7 +21723,7 @@ async function adsRoutes(app, opts) {
21608
21723
  }
21609
21724
 
21610
21725
  // ../api-routes/src/bing.ts
21611
- import crypto19 from "crypto";
21726
+ import crypto20 from "crypto";
21612
21727
  import { eq as eq23, and as and16, desc as desc12 } from "drizzle-orm";
21613
21728
 
21614
21729
  // ../integration-bing/src/constants.ts
@@ -22040,7 +22155,7 @@ async function bingRoutes(app, opts) {
22040
22155
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
22041
22156
  const now = (/* @__PURE__ */ new Date()).toISOString();
22042
22157
  app.db.insert(bingCoverageSnapshots).values({
22043
- id: crypto19.randomUUID(),
22158
+ id: crypto20.randomUUID(),
22044
22159
  projectId: project.id,
22045
22160
  syncRunId: snapshotRunId,
22046
22161
  date: snapshotDate,
@@ -22111,7 +22226,7 @@ async function bingRoutes(app, opts) {
22111
22226
  throw validationError("url is required");
22112
22227
  }
22113
22228
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
22114
- const runId = crypto19.randomUUID();
22229
+ const runId = crypto20.randomUUID();
22115
22230
  app.db.insert(runs).values({
22116
22231
  id: runId,
22117
22232
  projectId: project.id,
@@ -22132,7 +22247,7 @@ async function bingRoutes(app, opts) {
22132
22247
  discoveryDate: result.DiscoveryDate ?? null
22133
22248
  });
22134
22249
  const now = (/* @__PURE__ */ new Date()).toISOString();
22135
- const id = crypto19.randomUUID();
22250
+ const id = crypto20.randomUUID();
22136
22251
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
22137
22252
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
22138
22253
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -22202,7 +22317,7 @@ async function bingRoutes(app, opts) {
22202
22317
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
22203
22318
  }
22204
22319
  const now = (/* @__PURE__ */ new Date()).toISOString();
22205
- const runId = crypto19.randomUUID();
22320
+ const runId = crypto20.randomUUID();
22206
22321
  app.db.insert(runs).values({
22207
22322
  id: runId,
22208
22323
  projectId: project.id,
@@ -22486,7 +22601,7 @@ async function cdpRoutes(app, opts) {
22486
22601
  }
22487
22602
 
22488
22603
  // ../api-routes/src/ga.ts
22489
- import crypto20 from "crypto";
22604
+ import crypto21 from "crypto";
22490
22605
  import { eq as eq25, desc as desc13, and as and18, sql as sql9 } from "drizzle-orm";
22491
22606
  function gaLog(level, action, ctx) {
22492
22607
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
@@ -22742,7 +22857,7 @@ async function ga4Routes(app, opts) {
22742
22857
  const syncAi = !only || only === "ai";
22743
22858
  const syncSocial = !only || only === "social";
22744
22859
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
22745
- const runId = crypto20.randomUUID();
22860
+ const runId = crypto21.randomUUID();
22746
22861
  app.db.insert(runs).values({
22747
22862
  id: runId,
22748
22863
  projectId: project.id,
@@ -22790,7 +22905,7 @@ async function ga4Routes(app, opts) {
22790
22905
  ).run();
22791
22906
  for (const row of rows) {
22792
22907
  tx.insert(gaTrafficSnapshots).values({
22793
- id: crypto20.randomUUID(),
22908
+ id: crypto21.randomUUID(),
22794
22909
  projectId: project.id,
22795
22910
  date: row.date,
22796
22911
  landingPage: row.landingPage,
@@ -22814,7 +22929,7 @@ async function ga4Routes(app, opts) {
22814
22929
  ).run();
22815
22930
  for (const row of aiReferrals) {
22816
22931
  tx.insert(gaAiReferrals).values({
22817
- id: crypto20.randomUUID(),
22932
+ id: crypto21.randomUUID(),
22818
22933
  projectId: project.id,
22819
22934
  date: row.date,
22820
22935
  source: row.source,
@@ -22840,7 +22955,7 @@ async function ga4Routes(app, opts) {
22840
22955
  ).run();
22841
22956
  for (const row of socialReferrals) {
22842
22957
  tx.insert(gaSocialReferrals).values({
22843
- id: crypto20.randomUUID(),
22958
+ id: crypto21.randomUUID(),
22844
22959
  projectId: project.id,
22845
22960
  date: row.date,
22846
22961
  source: row.source,
@@ -22856,7 +22971,7 @@ async function ga4Routes(app, opts) {
22856
22971
  if (syncSummary) {
22857
22972
  tx.delete(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).run();
22858
22973
  tx.insert(gaTrafficSummaries).values({
22859
- id: crypto20.randomUUID(),
22974
+ id: crypto21.randomUUID(),
22860
22975
  projectId: project.id,
22861
22976
  periodStart: summary.periodStart,
22862
22977
  periodEnd: summary.periodEnd,
@@ -22869,7 +22984,7 @@ async function ga4Routes(app, opts) {
22869
22984
  tx.delete(gaTrafficWindowSummaries).where(eq25(gaTrafficWindowSummaries.projectId, project.id)).run();
22870
22985
  for (const ws of windowSummaries) {
22871
22986
  tx.insert(gaTrafficWindowSummaries).values({
22872
- id: crypto20.randomUUID(),
22987
+ id: crypto21.randomUUID(),
22873
22988
  projectId: project.id,
22874
22989
  windowKey: ws.windowKey,
22875
22990
  periodStart: ws.periodStart,
@@ -23440,7 +23555,7 @@ function parseSchemaPageEntry(entry) {
23440
23555
  }
23441
23556
 
23442
23557
  // ../integration-wordpress/src/wordpress-client.ts
23443
- import crypto21 from "crypto";
23558
+ import crypto22 from "crypto";
23444
23559
  function validateUsername(username) {
23445
23560
  if (!username || typeof username !== "string" || username.trim().length === 0) {
23446
23561
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -23653,7 +23768,7 @@ function buildSnippet(content) {
23653
23768
  return `${text2.slice(0, 157)}...`;
23654
23769
  }
23655
23770
  function contentHash(content) {
23656
- return crypto21.createHash("sha256").update(content).digest("hex");
23771
+ return crypto22.createHash("sha256").update(content).digest("hex");
23657
23772
  }
23658
23773
  function buildAmbiguousSlugMessage(slug, pages) {
23659
23774
  const candidates = pages.map((page) => {
@@ -24953,7 +25068,7 @@ async function wordpressRoutes(app, opts) {
24953
25068
  }
24954
25069
 
24955
25070
  // ../api-routes/src/backlinks.ts
24956
- import crypto22 from "crypto";
25071
+ import crypto23 from "crypto";
24957
25072
  import { and as and20, asc as asc3, desc as desc14, eq as eq26, sql as sql10 } from "drizzle-orm";
24958
25073
 
24959
25074
  // ../integration-commoncrawl/src/constants.ts
@@ -25591,7 +25706,7 @@ async function backlinksRoutes(app, opts) {
25591
25706
  const refreshed = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.id, existing.id)).get();
25592
25707
  return reply.status(200).send(mapSyncRow(refreshed));
25593
25708
  }
25594
- const id = crypto22.randomUUID();
25709
+ const id = crypto23.randomUUID();
25595
25710
  app.db.insert(ccReleaseSyncs).values({
25596
25711
  id,
25597
25712
  release,
@@ -25648,7 +25763,7 @@ async function backlinksRoutes(app, opts) {
25648
25763
  throw validationError("Invalid release id");
25649
25764
  }
25650
25765
  const now = (/* @__PURE__ */ new Date()).toISOString();
25651
- const runId = crypto22.randomUUID();
25766
+ const runId = crypto23.randomUUID();
25652
25767
  app.db.insert(runs).values({
25653
25768
  id: runId,
25654
25769
  projectId: project.id,
@@ -25750,7 +25865,7 @@ async function backlinksRoutes(app, opts) {
25750
25865
  );
25751
25866
  }
25752
25867
  const now = (/* @__PURE__ */ new Date()).toISOString();
25753
- const runId = crypto22.randomUUID();
25868
+ const runId = crypto23.randomUUID();
25754
25869
  app.db.insert(runs).values({
25755
25870
  id: runId,
25756
25871
  projectId: project.id,
@@ -25767,12 +25882,12 @@ async function backlinksRoutes(app, opts) {
25767
25882
  }
25768
25883
 
25769
25884
  // ../api-routes/src/traffic.ts
25770
- import crypto24 from "crypto";
25885
+ import crypto25 from "crypto";
25771
25886
  import { Agent as UndiciAgent } from "undici";
25772
25887
  import { and as and21, desc as desc15, eq as eq27, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
25773
25888
 
25774
25889
  // ../integration-cloud-run/src/auth.ts
25775
- import crypto23 from "crypto";
25890
+ import crypto24 from "crypto";
25776
25891
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
25777
25892
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
25778
25893
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -25803,7 +25918,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
25803
25918
  const headerB64 = encode(header);
25804
25919
  const payloadB64 = encode(payload);
25805
25920
  const signingInput = `${headerB64}.${payloadB64}`;
25806
- const sign = crypto23.createSign("RSA-SHA256");
25921
+ const sign = crypto24.createSign("RSA-SHA256");
25807
25922
  sign.update(signingInput);
25808
25923
  const signature = sign.sign(privateKey, "base64url");
25809
25924
  return `${signingInput}.${signature}`;
@@ -29711,7 +29826,7 @@ async function runBackfillTask(options) {
29711
29826
  }
29712
29827
  })();
29713
29828
  tx.insert(rawEventSamples).values({
29714
- id: crypto24.randomUUID(),
29829
+ id: crypto25.randomUUID(),
29715
29830
  projectId: project.id,
29716
29831
  sourceId: sourceRow.id,
29717
29832
  ts: sample.observedAt,
@@ -29837,7 +29952,7 @@ async function trafficRoutes(app, opts) {
29837
29952
  }).where(eq27(trafficSources.id, activeSource.id)).run();
29838
29953
  sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
29839
29954
  } else {
29840
- const newId = crypto24.randomUUID();
29955
+ const newId = crypto25.randomUUID();
29841
29956
  app.db.insert(trafficSources).values({
29842
29957
  id: newId,
29843
29958
  projectId: project.id,
@@ -29918,7 +30033,7 @@ async function trafficRoutes(app, opts) {
29918
30033
  }).where(eq27(trafficSources.id, activeSource.id)).run();
29919
30034
  sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
29920
30035
  } else {
29921
- const newId = crypto24.randomUUID();
30036
+ const newId = crypto25.randomUUID();
29922
30037
  app.db.insert(trafficSources).values({
29923
30038
  id: newId,
29924
30039
  projectId: project.id,
@@ -30002,7 +30117,7 @@ async function trafficRoutes(app, opts) {
30002
30117
  }).where(eq27(trafficSources.id, activeSource.id)).run();
30003
30118
  row = tx.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
30004
30119
  } else {
30005
- const newId = crypto24.randomUUID();
30120
+ const newId = crypto25.randomUUID();
30006
30121
  tx.insert(trafficSources).values({
30007
30122
  id: newId,
30008
30123
  projectId: project.id,
@@ -30036,7 +30151,7 @@ async function trafficRoutes(app, opts) {
30036
30151
  let created = false;
30037
30152
  if (!existingSchedule) {
30038
30153
  tx.insert(schedules).values({
30039
- id: crypto24.randomUUID(),
30154
+ id: crypto25.randomUUID(),
30040
30155
  projectId: project.id,
30041
30156
  kind: SchedulableRunKinds["traffic-sync"],
30042
30157
  cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
@@ -30091,7 +30206,7 @@ async function trafficRoutes(app, opts) {
30091
30206
  const windowEnd = /* @__PURE__ */ new Date();
30092
30207
  const startedAt = windowEnd.toISOString();
30093
30208
  const syncStartedAtMs = windowEnd.getTime();
30094
- const runId = crypto24.randomUUID();
30209
+ const runId = crypto25.randomUUID();
30095
30210
  app.db.insert(runs).values({
30096
30211
  id: runId,
30097
30212
  projectId: project.id,
@@ -30474,7 +30589,7 @@ async function trafficRoutes(app, opts) {
30474
30589
  }
30475
30590
  })();
30476
30591
  tx.insert(rawEventSamples).values({
30477
- id: crypto24.randomUUID(),
30592
+ id: crypto25.randomUUID(),
30478
30593
  projectId: project.id,
30479
30594
  sourceId: sourceRow.id,
30480
30595
  ts: sample.observedAt,
@@ -30712,7 +30827,7 @@ async function trafficRoutes(app, opts) {
30712
30827
  };
30713
30828
  }
30714
30829
  const startedAt = windowEnd.toISOString();
30715
- const runId = crypto24.randomUUID();
30830
+ const runId = crypto25.randomUUID();
30716
30831
  app.db.insert(runs).values({
30717
30832
  id: runId,
30718
30833
  projectId: project.id,
@@ -31013,7 +31128,7 @@ async function trafficRoutes(app, opts) {
31013
31128
  }
31014
31129
 
31015
31130
  // ../api-routes/src/doctor/checks/agent.ts
31016
- import crypto25 from "crypto";
31131
+ import crypto26 from "crypto";
31017
31132
  import fs6 from "fs";
31018
31133
  import path7 from "path";
31019
31134
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -31166,7 +31281,7 @@ function isInstalled(dir) {
31166
31281
  }
31167
31282
  function hashInstalledFile(filePath) {
31168
31283
  try {
31169
- return crypto25.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
31284
+ return crypto26.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
31170
31285
  } catch {
31171
31286
  return void 0;
31172
31287
  }
@@ -33068,7 +33183,7 @@ async function doctorRoutes(app, opts) {
33068
33183
  }
33069
33184
 
33070
33185
  // ../api-routes/src/discovery/routes.ts
33071
- import crypto26 from "crypto";
33186
+ import crypto27 from "crypto";
33072
33187
  import { and as and25, desc as desc16, eq as eq34, gte as gte6, inArray as inArray12 } from "drizzle-orm";
33073
33188
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
33074
33189
  async function discoveryRoutes(app, opts) {
@@ -33114,8 +33229,8 @@ async function discoveryRoutes(app, opts) {
33114
33229
  if (existing && existing.runId) {
33115
33230
  return { reused: true, sessionId: existing.id, runId: existing.runId };
33116
33231
  }
33117
- const sessionId = crypto26.randomUUID();
33118
- const runId = crypto26.randomUUID();
33232
+ const sessionId = crypto27.randomUUID();
33233
+ const runId = crypto27.randomUUID();
33119
33234
  tx.insert(discoverySessions).values({
33120
33235
  id: sessionId,
33121
33236
  projectId: project.id,
@@ -33363,7 +33478,7 @@ async function discoveryRoutes(app, opts) {
33363
33478
  app.db.transaction((tx) => {
33364
33479
  for (const query of promotedQueries) {
33365
33480
  tx.insert(queries).values({
33366
- id: crypto26.randomUUID(),
33481
+ id: crypto27.randomUUID(),
33367
33482
  projectId: project.id,
33368
33483
  query,
33369
33484
  provenance,
@@ -33372,7 +33487,7 @@ async function discoveryRoutes(app, opts) {
33372
33487
  }
33373
33488
  for (const domain of promotedCompetitors) {
33374
33489
  tx.insert(competitors).values({
33375
- id: crypto26.randomUUID(),
33490
+ id: crypto27.randomUUID(),
33376
33491
  projectId: project.id,
33377
33492
  domain,
33378
33493
  provenance,
@@ -33450,7 +33565,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
33450
33565
  }
33451
33566
 
33452
33567
  // ../api-routes/src/discovery/orchestrate.ts
33453
- import crypto27 from "crypto";
33568
+ import crypto28 from "crypto";
33454
33569
  import { eq as eq35 } from "drizzle-orm";
33455
33570
  var DEFAULT_MAX_PROBES = 100;
33456
33571
  var ABSOLUTE_MAX_PROBES = 500;
@@ -33544,7 +33659,7 @@ async function executeDiscovery(opts) {
33544
33659
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
33545
33660
  buckets[bucket]++;
33546
33661
  opts.db.insert(discoveryProbes).values({
33547
- id: crypto27.randomUUID(),
33662
+ id: crypto28.randomUUID(),
33548
33663
  sessionId: opts.sessionId,
33549
33664
  projectId: opts.project.id,
33550
33665
  query,
@@ -33589,7 +33704,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
33589
33704
  const domain = normalizeDomain(entry.domain);
33590
33705
  if (!domain) continue;
33591
33706
  db.insert(domainClassifications).values({
33592
- id: crypto27.randomUUID(),
33707
+ id: crypto28.randomUUID(),
33593
33708
  projectId,
33594
33709
  domain,
33595
33710
  competitorType: entry.competitorType,
@@ -33629,7 +33744,7 @@ function dedupeStrings(input) {
33629
33744
  }
33630
33745
 
33631
33746
  // ../api-routes/src/technical-aeo.ts
33632
- import crypto28 from "crypto";
33747
+ import crypto29 from "crypto";
33633
33748
  import { and as and26, asc as asc4, count, desc as desc17, eq as eq36, inArray as inArray13 } from "drizzle-orm";
33634
33749
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
33635
33750
  function emptyScore(projectName) {
@@ -33756,7 +33871,7 @@ async function technicalAeoRoutes(app, opts) {
33756
33871
  return { runId: existing.id, status: existing.status };
33757
33872
  }
33758
33873
  const now = (/* @__PURE__ */ new Date()).toISOString();
33759
- const runId = crypto28.randomUUID();
33874
+ const runId = crypto29.randomUUID();
33760
33875
  app.db.insert(runs).values({
33761
33876
  id: runId,
33762
33877
  projectId: project.id,
@@ -34107,7 +34222,7 @@ function buildTrafficSourceValidators(opts) {
34107
34222
  }
34108
34223
 
34109
34224
  // src/intelligence-service.ts
34110
- import crypto29 from "crypto";
34225
+ import crypto30 from "crypto";
34111
34226
 
34112
34227
  // src/logger.ts
34113
34228
  var IS_TTY = process.stdout.isTTY === true;
@@ -34471,6 +34586,8 @@ var IntelligenceService = class {
34471
34586
  const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and27(eq37(gbpDailyMetrics.projectId, projectId), eq37(gbpDailyMetrics.locationName, locationName))).all();
34472
34587
  const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and27(eq37(gbpPlaceActions.projectId, projectId), eq37(gbpPlaceActions.locationName, locationName))).all();
34473
34588
  const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and27(eq37(gbpLodgingSnapshots.projectId, projectId), eq37(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
34589
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and27(eq37(gbpLocations.projectId, projectId), eq37(gbpLocations.locationName, locationName))).get();
34590
+ const descriptionMissing = !(ownerRow?.description ?? "").trim();
34474
34591
  const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and27(eq37(gbpPlaceDetails.projectId, projectId), eq37(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
34475
34592
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
34476
34593
  const summary = buildGbpSummary({
@@ -34495,6 +34612,7 @@ var IntelligenceService = class {
34495
34612
  metricDeltaPct: summary.performance.deltaPct,
34496
34613
  lodgingCapable: summary.lodging.lodgingLocationCount > 0,
34497
34614
  lodgingEmpty: summary.lodging.emptyLodgingCount > 0,
34615
+ descriptionMissing,
34498
34616
  placesAmenities,
34499
34617
  placeActionCount: summary.placeActions.total,
34500
34618
  hasDirectMerchantCta: summary.placeActions.hasDirectMerchantCta,
@@ -34769,7 +34887,7 @@ var IntelligenceService = class {
34769
34887
  }).run();
34770
34888
  }
34771
34889
  tx.insert(healthSnapshots).values({
34772
- id: crypto29.randomUUID(),
34890
+ id: crypto30.randomUUID(),
34773
34891
  projectId,
34774
34892
  runId,
34775
34893
  overallCitedRate: String(result.health.overallCitedRate),
@@ -34964,6 +35082,7 @@ export {
34964
35082
  gbpPlaceActions,
34965
35083
  gbpLodgingSnapshots,
34966
35084
  gbpPlaceDetails,
35085
+ gbpAttributesSnapshots,
34967
35086
  adsConnections,
34968
35087
  adsCampaigns,
34969
35088
  adsAdGroups,
@@ -34993,12 +35112,18 @@ export {
34993
35112
  fetchSearchAnalytics,
34994
35113
  inspectUrl,
34995
35114
  GBP_DAILY_METRICS,
35115
+ listLocations,
35116
+ formatStorefrontAddress,
35117
+ buildLocationProfileFields,
34996
35118
  fetchDailyMetrics,
34997
35119
  listMonthlyKeywords,
34998
35120
  listPlaceActionLinks,
34999
35121
  getLodging,
35000
35122
  countPopulatedGroups,
35001
35123
  hashLodging,
35124
+ getAttributes,
35125
+ countAttributes,
35126
+ hashAttributes,
35002
35127
  getUrlInfo,
35003
35128
  getCrawlIssues,
35004
35129
  getLinkCounts,