@ainyc/canonry 4.32.0 → 4.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-CUMjedc6.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-47V0U52s.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-CNKAwZMB.css">
17
17
  </head>
18
18
  <body>
@@ -22,7 +22,7 @@ import {
22
22
  trafficConnectVercelRequestSchema,
23
23
  trafficConnectWordpressRequestSchema,
24
24
  trafficEventKindSchema
25
- } from "./chunk-5M4PP6P4.js";
25
+ } from "./chunk-XW3F5EEW.js";
26
26
 
27
27
  // src/config.ts
28
28
  import fs from "fs";
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-5M4PP6P4.js";
11
+ } from "./chunk-XW3F5EEW.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-LVX5TOYA.js";
8
+ } from "./chunk-5EBN7736.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -70,7 +70,7 @@ import {
70
70
  schedules,
71
71
  trafficSources,
72
72
  usageCounters
73
- } from "./chunk-LUAJVZVZ.js";
73
+ } from "./chunk-BJXHETQW.js";
74
74
  import {
75
75
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
76
  AGENT_PROVIDER_IDS,
@@ -162,6 +162,7 @@ import {
162
162
  reportHorizonLabel,
163
163
  reportSeverityLabel,
164
164
  resolveConfigSpecQueries,
165
+ resolveLocations,
165
166
  resolveSnapshotRequestQueries,
166
167
  runInProgress,
167
168
  runNotCancellable,
@@ -178,7 +179,7 @@ import {
178
179
  visibilityStateFromAnswerMentioned,
179
180
  windowCutoff,
180
181
  wordpressEnvSchema
181
- } from "./chunk-5M4PP6P4.js";
182
+ } from "./chunk-XW3F5EEW.js";
182
183
 
183
184
  // src/telemetry.ts
184
185
  import crypto from "crypto";
@@ -10387,7 +10388,7 @@ var routeCatalog = [
10387
10388
  method: "post",
10388
10389
  path: "/api/v1/projects/{name}/discover/run",
10389
10390
  summary: "Start a tracked-basket discovery session",
10390
- description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.85 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running" }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`.',
10391
+ description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.85 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running", consolidated }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`. Concurrent/duplicate requests for the same (project, ICP) are consolidated onto a single in-flight session: the response carries `consolidated: true` and `200 OK` instead of `201`, and the request\'s `dedupThreshold` / `maxProbes` are ignored (the in-flight session keeps its original config).',
10391
10392
  tags: ["discovery"],
10392
10393
  parameters: [nameParameter],
10393
10394
  requestBody: {
@@ -10399,14 +10400,20 @@ var routeCatalog = [
10399
10400
  properties: {
10400
10401
  icpDescription: { type: "string", description: "Free-text ICP. Required if the project does not have spec.icpDescription stored." },
10401
10402
  dedupThreshold: { type: "number", description: "Cosine similarity threshold for clustering. Defaults to 0.85." },
10402
- maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." }
10403
+ maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." },
10404
+ locations: {
10405
+ type: "array",
10406
+ items: { type: "string" },
10407
+ description: "Optional override of the project location labels used to geo-constrain seed generation. Each label must match a configured project location; an unknown label is a 400. Omit to use every project location."
10408
+ }
10403
10409
  }
10404
10410
  }
10405
10411
  }
10406
10412
  }
10407
10413
  },
10408
10414
  responses: {
10409
- 201: { description: "Discovery session enqueued; returns { runId, sessionId, status }." },
10415
+ 200: { description: "An in-flight session with the same project + ICP was reused; returns { runId, sessionId, status, consolidated: true }. The request's dedupThreshold / maxProbes are ignored." },
10416
+ 201: { description: "New discovery session enqueued; returns { runId, sessionId, status, consolidated: false }." },
10410
10417
  400: { description: "Missing or invalid ICP / parameters." },
10411
10418
  404: { description: "Project not found." }
10412
10419
  }
@@ -20003,7 +20010,8 @@ async function doctorRoutes(app, opts) {
20003
20010
 
20004
20011
  // ../api-routes/src/discovery/routes.ts
20005
20012
  import crypto21 from "crypto";
20006
- import { eq as eq25, desc as desc13 } from "drizzle-orm";
20013
+ import { and as and16, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20014
+ var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
20007
20015
  async function discoveryRoutes(app, opts) {
20008
20016
  app.post("/projects/:name/discover/run", async (request, reply) => {
20009
20017
  const project = resolveProject(app.db, request.params.name);
@@ -20022,15 +20030,33 @@ async function discoveryRoutes(app, opts) {
20022
20030
  "icpDescription is required. Pass it in the request body or store it on the project (spec.icpDescription)."
20023
20031
  );
20024
20032
  }
20033
+ const locations = resolveLocations(
20034
+ parseJsonColumn(project.locations, []),
20035
+ parsed.data.locations
20036
+ );
20025
20037
  if (!opts.onDiscoveryRunRequested) {
20026
20038
  throw validationError("Discovery is not available on this deployment.", {
20027
20039
  reason: "no-discovery-handler"
20028
20040
  });
20029
20041
  }
20030
20042
  const now = (/* @__PURE__ */ new Date()).toISOString();
20031
- const sessionId = crypto21.randomUUID();
20032
- const runId = crypto21.randomUUID();
20033
- app.db.transaction((tx) => {
20043
+ const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
20044
+ const decision = app.db.transaction((tx) => {
20045
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and16(
20046
+ eq25(discoverySessions.projectId, project.id),
20047
+ eq25(discoverySessions.icpDescription, icpDescription),
20048
+ inArray8(discoverySessions.status, [
20049
+ DiscoverySessionStatuses.queued,
20050
+ DiscoverySessionStatuses.seeding,
20051
+ DiscoverySessionStatuses.probing
20052
+ ]),
20053
+ gte4(discoverySessions.createdAt, ageFloorIso)
20054
+ )).orderBy(desc13(discoverySessions.createdAt)).get();
20055
+ if (existing && existing.runId) {
20056
+ return { reused: true, sessionId: existing.id, runId: existing.runId };
20057
+ }
20058
+ const sessionId = crypto21.randomUUID();
20059
+ const runId = crypto21.randomUUID();
20034
20060
  tx.insert(discoverySessions).values({
20035
20061
  id: sessionId,
20036
20062
  projectId: project.id,
@@ -20056,16 +20082,31 @@ async function discoveryRoutes(app, opts) {
20056
20082
  entityType: "discovery_session",
20057
20083
  entityId: sessionId
20058
20084
  });
20085
+ return { reused: false, sessionId, runId };
20059
20086
  });
20087
+ if (decision.reused) {
20088
+ return reply.status(200).send({
20089
+ runId: decision.runId,
20090
+ sessionId: decision.sessionId,
20091
+ status: "running",
20092
+ consolidated: true
20093
+ });
20094
+ }
20060
20095
  opts.onDiscoveryRunRequested({
20061
- runId,
20062
- sessionId,
20096
+ runId: decision.runId,
20097
+ sessionId: decision.sessionId,
20063
20098
  projectId: project.id,
20064
20099
  icpDescription,
20065
20100
  dedupThreshold: parsed.data.dedupThreshold,
20066
- maxProbes: parsed.data.maxProbes
20101
+ maxProbes: parsed.data.maxProbes,
20102
+ locations
20103
+ });
20104
+ return reply.status(201).send({
20105
+ runId: decision.runId,
20106
+ sessionId: decision.sessionId,
20107
+ status: "running",
20108
+ consolidated: false
20067
20109
  });
20068
- return reply.status(201).send({ runId, sessionId, status: "running" });
20069
20110
  });
20070
20111
  app.get(
20071
20112
  "/projects/:name/discover/sessions",
@@ -20343,7 +20384,8 @@ async function executeDiscovery(opts) {
20343
20384
  }).where(eq26(discoverySessions.id, opts.sessionId)).run();
20344
20385
  const seedResult = await opts.deps.seed({
20345
20386
  project: opts.project,
20346
- icpDescription: opts.icpDescription
20387
+ icpDescription: opts.icpDescription,
20388
+ locations: opts.locations ?? []
20347
20389
  });
20348
20390
  const rawCandidates = dedupeStrings(seedResult.candidates);
20349
20391
  const seedCountRaw = rawCandidates.length;
@@ -23316,7 +23358,7 @@ import crypto24 from "crypto";
23316
23358
  import fs7 from "fs";
23317
23359
  import path9 from "path";
23318
23360
  import os5 from "os";
23319
- import { and as and16, eq as eq27, inArray as inArray8, sql as sql10 } from "drizzle-orm";
23361
+ import { and as and17, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23320
23362
 
23321
23363
  // src/run-telemetry.ts
23322
23364
  import crypto23 from "crypto";
@@ -23657,7 +23699,7 @@ var JobRunner = class {
23657
23699
  this.registry = registry;
23658
23700
  }
23659
23701
  recoverStaleRuns() {
23660
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray8(runs.status, ["running", "queued"])).all();
23702
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray9(runs.status, ["running", "queued"])).all();
23661
23703
  if (stale.length === 0) return;
23662
23704
  const now = (/* @__PURE__ */ new Date()).toISOString();
23663
23705
  for (const run of stale) {
@@ -23695,7 +23737,7 @@ var JobRunner = class {
23695
23737
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
23696
23738
  }
23697
23739
  if (existingRun.status === "queued") {
23698
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and16(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23740
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and17(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23699
23741
  }
23700
23742
  this.throwIfRunCancelled(runId);
23701
23743
  const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
@@ -23720,7 +23762,7 @@ var JobRunner = class {
23720
23762
  }
23721
23763
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
23722
23764
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23723
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray8(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23765
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and17(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23724
23766
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
23725
23767
  const competitorDomains = projectCompetitors.map((c) => c.domain);
23726
23768
  const allDomains = effectiveDomains({
@@ -24056,7 +24098,7 @@ function buildPhases(input) {
24056
24098
 
24057
24099
  // src/gsc-sync.ts
24058
24100
  import crypto25 from "crypto";
24059
- import { eq as eq28, and as and17, sql as sql11 } from "drizzle-orm";
24101
+ import { eq as eq28, and as and18, sql as sql11 } from "drizzle-orm";
24060
24102
  var log2 = createLogger("GscSync");
24061
24103
  function formatDate3(d) {
24062
24104
  return d.toISOString().split("T")[0];
@@ -24108,7 +24150,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24108
24150
  });
24109
24151
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
24110
24152
  db.delete(gscSearchData).where(
24111
- and17(
24153
+ and18(
24112
24154
  eq28(gscSearchData.projectId, projectId),
24113
24155
  sql11`${gscSearchData.date} >= ${startDate}`,
24114
24156
  sql11`${gscSearchData.date} <= ${endDate}`
@@ -24197,7 +24239,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24197
24239
  }
24198
24240
  }
24199
24241
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
24200
- db.delete(gscCoverageSnapshots).where(and17(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24242
+ db.delete(gscCoverageSnapshots).where(and18(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24201
24243
  db.insert(gscCoverageSnapshots).values({
24202
24244
  id: crypto25.randomUUID(),
24203
24245
  projectId,
@@ -24220,7 +24262,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24220
24262
 
24221
24263
  // src/gsc-inspect-sitemap.ts
24222
24264
  import crypto26 from "crypto";
24223
- import { eq as eq29, and as and18 } from "drizzle-orm";
24265
+ import { eq as eq29, and as and19 } from "drizzle-orm";
24224
24266
 
24225
24267
  // src/sitemap-parser.ts
24226
24268
  var log3 = createLogger("SitemapParser");
@@ -24436,7 +24478,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
24436
24478
  }
24437
24479
  }
24438
24480
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
24439
- db.delete(gscCoverageSnapshots).where(and18(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24481
+ db.delete(gscCoverageSnapshots).where(and19(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24440
24482
  db.insert(gscCoverageSnapshots).values({
24441
24483
  id: crypto26.randomUUID(),
24442
24484
  projectId,
@@ -24647,7 +24689,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
24647
24689
  // src/commoncrawl-sync.ts
24648
24690
  import crypto28 from "crypto";
24649
24691
  import path10 from "path";
24650
- import { and as and19, eq as eq31, sql as sql12 } from "drizzle-orm";
24692
+ import { and as and20, eq as eq31, sql as sql12 } from "drizzle-orm";
24651
24693
  var log6 = createLogger("CommonCrawlSync");
24652
24694
  var INSERT_CHUNK_SIZE = 1e4;
24653
24695
  function defaultDeps() {
@@ -24838,7 +24880,7 @@ function computeSummary(rows) {
24838
24880
  // src/backlink-extract.ts
24839
24881
  import crypto29 from "crypto";
24840
24882
  import fs8 from "fs";
24841
- import { and as and20, desc as desc15, eq as eq32 } from "drizzle-orm";
24883
+ import { and as and21, desc as desc15, eq as eq32 } from "drizzle-orm";
24842
24884
  var log7 = createLogger("BacklinkExtract");
24843
24885
  function defaultDeps2() {
24844
24886
  return {
@@ -24884,7 +24926,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
24884
24926
  const targetDomain = project.canonicalDomain;
24885
24927
  db.transaction((tx) => {
24886
24928
  tx.delete(backlinkDomains).where(
24887
- and20(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24929
+ and21(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24888
24930
  ).run();
24889
24931
  if (rows.length > 0) {
24890
24932
  const values = rows.map((r) => ({
@@ -24984,6 +25026,7 @@ async function executeDiscoveryRun(opts) {
24984
25026
  icpDescription: opts.icpDescription,
24985
25027
  dedupThreshold: opts.dedupThreshold,
24986
25028
  maxProbes: opts.maxProbes,
25029
+ locations: opts.locations,
24987
25030
  deps
24988
25031
  });
24989
25032
  writeDiscoveryInsight(opts.db, {
@@ -25141,12 +25184,32 @@ function extractClassificationCategory(line) {
25141
25184
  }
25142
25185
  return null;
25143
25186
  }
25187
+ function formatLocationLine(location) {
25188
+ return [location.city, location.region, location.country].map((part) => part.trim()).filter(Boolean).join(", ");
25189
+ }
25190
+ function buildLocationConstraint(locations) {
25191
+ if (locations.length === 0) return [];
25192
+ const formatted = locations.map(formatLocationLine);
25193
+ if (locations.length === 1) {
25194
+ return [
25195
+ `The business serves ${formatted[0]}. Every query must be relevant to that service area \u2014 work the city or region into the query the way a real searcher would.`
25196
+ ];
25197
+ }
25198
+ const perLocation = Math.max(1, Math.floor(DEFAULT_SEED_COUNT / locations.length));
25199
+ return [
25200
+ "The business serves these locations:",
25201
+ ...formatted.map((line) => ` - ${line}`),
25202
+ `Generate at least ${perLocation} queries for EACH service area listed above so coverage stays balanced \u2014 do not let one area dominate. Every query must be relevant to at least one of these service areas, working the city or region into the query the way a real searcher would.`
25203
+ ];
25204
+ }
25144
25205
  function buildSeedPrompt(input) {
25206
+ const locationConstraint = buildLocationConstraint(input.locations ?? []);
25145
25207
  return [
25146
25208
  "You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
25147
25209
  "",
25148
25210
  `Customer: ${input.project.name} (domains: ${input.project.canonicalDomains.join(", ")})`,
25149
25211
  `ICP: ${input.icpDescription}`,
25212
+ ...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
25150
25213
  "",
25151
25214
  "Brainstorm a wide set of queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity) when they are about to make a decision in this space. Aim for 30+ candidates covering:",
25152
25215
  ' - Comparison queries ("best X for Y")',
@@ -25280,7 +25343,7 @@ var ProviderRegistry = class {
25280
25343
 
25281
25344
  // src/scheduler.ts
25282
25345
  import cron from "node-cron";
25283
- import { and as and21, eq as eq34 } from "drizzle-orm";
25346
+ import { and as and22, eq as eq34 } from "drizzle-orm";
25284
25347
  var log9 = createLogger("Scheduler");
25285
25348
  function taskKey(projectId, kind) {
25286
25349
  return `${projectId}::${kind}`;
@@ -25325,7 +25388,7 @@ var Scheduler = class {
25325
25388
  this.stopTask(key, existing, "Stopped");
25326
25389
  this.tasks.delete(key);
25327
25390
  }
25328
- const schedule = this.db.select().from(schedules).where(and21(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
25391
+ const schedule = this.db.select().from(schedules).where(and22(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
25329
25392
  if (schedule && schedule.enabled === 1) {
25330
25393
  this.registerCronTask(schedule);
25331
25394
  }
@@ -25449,7 +25512,7 @@ var Scheduler = class {
25449
25512
  };
25450
25513
 
25451
25514
  // src/notifier.ts
25452
- import { eq as eq35, desc as desc16, and as and22, inArray as inArray9, or as or4 } from "drizzle-orm";
25515
+ import { eq as eq35, desc as desc16, and as and23, inArray as inArray10, or as or4 } from "drizzle-orm";
25453
25516
  import crypto31 from "crypto";
25454
25517
  var log10 = createLogger("Notifier");
25455
25518
  var Notifier = class {
@@ -25556,7 +25619,7 @@ var Notifier = class {
25556
25619
  computeTransitions(runId, projectId) {
25557
25620
  const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
25558
25621
  if (!thisRun) return [];
25559
- const groupSiblings = this.db.select().from(runs).where(and22(
25622
+ const groupSiblings = this.db.select().from(runs).where(and23(
25560
25623
  eq35(runs.projectId, projectId),
25561
25624
  eq35(runs.kind, thisRun.kind),
25562
25625
  eq35(runs.createdAt, thisRun.createdAt)
@@ -25582,7 +25645,7 @@ var Notifier = class {
25582
25645
  );
25583
25646
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
25584
25647
  const recentRuns = this.db.select().from(runs).where(
25585
- and22(
25648
+ and23(
25586
25649
  eq35(runs.projectId, projectId),
25587
25650
  eq35(runs.kind, thisRun.kind),
25588
25651
  or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
@@ -25602,13 +25665,13 @@ var Notifier = class {
25602
25665
  provider: querySnapshots.provider,
25603
25666
  location: querySnapshots.location,
25604
25667
  citationState: querySnapshots.citationState
25605
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
25668
+ }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
25606
25669
  const previousSnapshots = this.db.select({
25607
25670
  queryId: querySnapshots.queryId,
25608
25671
  provider: querySnapshots.provider,
25609
25672
  location: querySnapshots.location,
25610
25673
  citationState: querySnapshots.citationState
25611
- }).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
25674
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
25612
25675
  const prevMap = /* @__PURE__ */ new Map();
25613
25676
  for (const s of previousSnapshots) {
25614
25677
  prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
@@ -26119,7 +26182,7 @@ function resolveSessionProviderAndModel(config, opts) {
26119
26182
 
26120
26183
  // src/agent/memory-store.ts
26121
26184
  import crypto32 from "crypto";
26122
- import { and as and23, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
26185
+ import { and as and24, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
26123
26186
  var COMPACTION_KEY_PREFIX = "compaction:";
26124
26187
  var COMPACTION_NOTES_PER_SESSION = 3;
26125
26188
  function rowToDto2(row) {
@@ -26164,12 +26227,12 @@ function upsertMemoryEntry(db, args) {
26164
26227
  updatedAt: now
26165
26228
  }
26166
26229
  }).run();
26167
- const row = db.select().from(agentMemory).where(and23(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
26230
+ const row = db.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
26168
26231
  if (!row) throw new Error("memory upsert produced no row");
26169
26232
  return rowToDto2(row);
26170
26233
  }
26171
26234
  function deleteMemoryEntry(db, projectId, key) {
26172
- const result = db.delete(agentMemory).where(and23(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
26235
+ const result = db.delete(agentMemory).where(and24(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
26173
26236
  const changes = result.changes ?? 0;
26174
26237
  return changes > 0;
26175
26238
  }
@@ -26198,7 +26261,7 @@ function writeCompactionNote(db, args) {
26198
26261
  }).run();
26199
26262
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
26200
26263
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
26201
- and23(
26264
+ and24(
26202
26265
  eq37(agentMemory.projectId, args.projectId),
26203
26266
  like2(agentMemory.key, `${sessionPrefix}%`)
26204
26267
  )
@@ -26207,7 +26270,7 @@ function writeCompactionNote(db, args) {
26207
26270
  if (stale.length > 0) {
26208
26271
  tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
26209
26272
  }
26210
- const row = tx.select().from(agentMemory).where(and23(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
26273
+ const row = tx.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
26211
26274
  if (row) inserted = rowToDto2(row);
26212
26275
  });
26213
26276
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -28308,7 +28371,8 @@ async function createServer(opts) {
28308
28371
  projectId: input.projectId,
28309
28372
  icpDescription: input.icpDescription,
28310
28373
  dedupThreshold: input.dedupThreshold,
28311
- maxProbes: input.maxProbes
28374
+ maxProbes: input.maxProbes,
28375
+ locations: input.locations
28312
28376
  }).then(() => runCoordinator.onRunCompleted(input.runId, input.projectId)).catch((err) => {
28313
28377
  app.log.error({ runId: input.runId, err }, "Discovery run failed");
28314
28378
  });
@@ -300,6 +300,74 @@ var notificationCreateRequestSchema = z3.object({
300
300
 
301
301
  // ../contracts/src/project.ts
302
302
  import { z as z4 } from "zod";
303
+
304
+ // ../contracts/src/errors.ts
305
+ var AppError = class extends Error {
306
+ code;
307
+ statusCode;
308
+ details;
309
+ constructor(code, message, statusCode, details) {
310
+ super(message);
311
+ this.name = "AppError";
312
+ this.code = code;
313
+ this.statusCode = statusCode;
314
+ this.details = details;
315
+ }
316
+ toJSON() {
317
+ return {
318
+ error: {
319
+ code: this.code,
320
+ message: this.message,
321
+ ...this.details ? { details: this.details } : {}
322
+ }
323
+ };
324
+ }
325
+ };
326
+ function notFound(entity, id) {
327
+ return new AppError("NOT_FOUND", `${entity} '${id}' not found`, 404);
328
+ }
329
+ function validationError(message, details) {
330
+ return new AppError("VALIDATION_ERROR", message, 400, details);
331
+ }
332
+ function authRequired() {
333
+ return new AppError("AUTH_REQUIRED", "Authentication required", 401);
334
+ }
335
+ function authInvalid() {
336
+ return new AppError("AUTH_INVALID", "Invalid API key", 401);
337
+ }
338
+ function providerError(message, details) {
339
+ return new AppError("PROVIDER_ERROR", message, 502, details);
340
+ }
341
+ function runInProgress(projectName) {
342
+ return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
343
+ }
344
+ function runNotCancellable(runId, status) {
345
+ return new AppError("RUN_NOT_CANCELLABLE", `Run '${runId}' is already in terminal state '${status}' and cannot be cancelled`, 409);
346
+ }
347
+ function unsupportedKind(kind) {
348
+ return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
349
+ }
350
+ function notImplemented(message) {
351
+ return new AppError("NOT_IMPLEMENTED", message, 501);
352
+ }
353
+ function deliveryFailed(message) {
354
+ return new AppError("DELIVERY_FAILED", message, 502);
355
+ }
356
+ function agentBusy(projectName) {
357
+ return new AppError(
358
+ "AGENT_BUSY",
359
+ `Aero is already running a turn for '${projectName}'. Retry after the current turn settles.`,
360
+ 409
361
+ );
362
+ }
363
+ function missingDependency(message, details) {
364
+ return new AppError("MISSING_DEPENDENCY", message, 422, details);
365
+ }
366
+ function internalError(message, details) {
367
+ return new AppError("INTERNAL_ERROR", message, 500, details);
368
+ }
369
+
370
+ // ../contracts/src/project.ts
303
371
  var configSourceSchema = z4.enum(["cli", "api", "config-file"]);
304
372
  function findDuplicateLocationLabels(locations) {
305
373
  const seen = /* @__PURE__ */ new Set();
@@ -317,6 +385,26 @@ function hasLocationLabel(locations, label) {
317
385
  if (!label) return true;
318
386
  return locations.some((location) => location.label === label);
319
387
  }
388
+ function resolveLocations(projectLocations, requestedLabels) {
389
+ const normalizedRequest = (requestedLabels ?? []).map((label) => label.trim()).filter((label) => label.length > 0);
390
+ if (normalizedRequest.length === 0) return [...projectLocations];
391
+ const byLabel = new Map(projectLocations.map((loc) => [loc.label.toLowerCase(), loc]));
392
+ const resolved = [];
393
+ const seen = /* @__PURE__ */ new Set();
394
+ for (const label of normalizedRequest) {
395
+ const key = label.toLowerCase();
396
+ if (seen.has(key)) continue;
397
+ const match = byLabel.get(key);
398
+ if (!match) {
399
+ throw validationError(
400
+ `Location "${label}" is not configured for this project. Add it to the project's locations or omit the locations override.`
401
+ );
402
+ }
403
+ seen.add(key);
404
+ resolved.push(match);
405
+ }
406
+ return resolved;
407
+ }
320
408
  var projectUpsertRequestSchema = z4.object({
321
409
  displayName: z4.string().min(1),
322
410
  canonicalDomain: z4.string().min(1),
@@ -542,72 +630,6 @@ function resolveConfigSpecQueries(spec) {
542
630
  return spec.queries ?? spec.keywords ?? [];
543
631
  }
544
632
 
545
- // ../contracts/src/errors.ts
546
- var AppError = class extends Error {
547
- code;
548
- statusCode;
549
- details;
550
- constructor(code, message, statusCode, details) {
551
- super(message);
552
- this.name = "AppError";
553
- this.code = code;
554
- this.statusCode = statusCode;
555
- this.details = details;
556
- }
557
- toJSON() {
558
- return {
559
- error: {
560
- code: this.code,
561
- message: this.message,
562
- ...this.details ? { details: this.details } : {}
563
- }
564
- };
565
- }
566
- };
567
- function notFound(entity, id) {
568
- return new AppError("NOT_FOUND", `${entity} '${id}' not found`, 404);
569
- }
570
- function validationError(message, details) {
571
- return new AppError("VALIDATION_ERROR", message, 400, details);
572
- }
573
- function authRequired() {
574
- return new AppError("AUTH_REQUIRED", "Authentication required", 401);
575
- }
576
- function authInvalid() {
577
- return new AppError("AUTH_INVALID", "Invalid API key", 401);
578
- }
579
- function providerError(message, details) {
580
- return new AppError("PROVIDER_ERROR", message, 502, details);
581
- }
582
- function runInProgress(projectName) {
583
- return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
584
- }
585
- function runNotCancellable(runId, status) {
586
- return new AppError("RUN_NOT_CANCELLABLE", `Run '${runId}' is already in terminal state '${status}' and cannot be cancelled`, 409);
587
- }
588
- function unsupportedKind(kind) {
589
- return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
590
- }
591
- function notImplemented(message) {
592
- return new AppError("NOT_IMPLEMENTED", message, 501);
593
- }
594
- function deliveryFailed(message) {
595
- return new AppError("DELIVERY_FAILED", message, 502);
596
- }
597
- function agentBusy(projectName) {
598
- return new AppError(
599
- "AGENT_BUSY",
600
- `Aero is already running a turn for '${projectName}'. Retry after the current turn settles.`,
601
- 409
602
- );
603
- }
604
- function missingDependency(message, details) {
605
- return new AppError("MISSING_DEPENDENCY", message, 422, details);
606
- }
607
- function internalError(message, details) {
608
- return new AppError("INTERNAL_ERROR", message, 500, details);
609
- }
610
-
611
633
  // ../contracts/src/google.ts
612
634
  import { z as z6 } from "zod";
613
635
  var googleConnectionTypeSchema = z6.enum(["gsc", "ga4"]);
@@ -2442,7 +2464,15 @@ var DISCOVERY_MAX_PROBES_CAP = 500;
2442
2464
  var discoveryRunRequestSchema = z21.object({
2443
2465
  icpDescription: z21.string().min(1).optional(),
2444
2466
  dedupThreshold: z21.number().min(0).max(1).optional(),
2445
- maxProbes: z21.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional()
2467
+ maxProbes: z21.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional(),
2468
+ /**
2469
+ * Optional override of the project's location labels, constraining seed
2470
+ * generation to a subset of the configured service areas. Each label must
2471
+ * match a configured project location (resolved server-side via
2472
+ * `resolveLocations`). Omitted means "use every project location" — a
2473
+ * project with no locations is unaffected.
2474
+ */
2475
+ locations: z21.array(z21.string().min(1)).optional()
2446
2476
  });
2447
2477
  var discoveryPromoteRequestSchema = z21.object({
2448
2478
  buckets: z21.array(discoveryBucketSchema).min(1).optional(),
@@ -2614,20 +2644,6 @@ export {
2614
2644
  getProviderLocationHandling,
2615
2645
  notificationEventSchema,
2616
2646
  notificationCreateRequestSchema,
2617
- findDuplicateLocationLabels,
2618
- hasLocationLabel,
2619
- projectUpsertRequestSchema,
2620
- queryBatchRequestSchema,
2621
- keywordBatchRequestSchema,
2622
- queryGenerateRequestSchema,
2623
- keywordGenerateRequestSchema,
2624
- competitorBatchRequestSchema,
2625
- normalizeProjectDomain,
2626
- registrableDomain,
2627
- brandLabelFromDomain,
2628
- effectiveDomains,
2629
- projectConfigSchema,
2630
- resolveConfigSpecQueries,
2631
2647
  AppError,
2632
2648
  notFound,
2633
2649
  validationError,
@@ -2642,6 +2658,21 @@ export {
2642
2658
  agentBusy,
2643
2659
  missingDependency,
2644
2660
  internalError,
2661
+ findDuplicateLocationLabels,
2662
+ hasLocationLabel,
2663
+ resolveLocations,
2664
+ projectUpsertRequestSchema,
2665
+ queryBatchRequestSchema,
2666
+ keywordBatchRequestSchema,
2667
+ queryGenerateRequestSchema,
2668
+ keywordGenerateRequestSchema,
2669
+ competitorBatchRequestSchema,
2670
+ normalizeProjectDomain,
2671
+ registrableDomain,
2672
+ brandLabelFromDomain,
2673
+ effectiveDomains,
2674
+ projectConfigSchema,
2675
+ resolveConfigSpecQueries,
2645
2676
  wordpressEnvSchema,
2646
2677
  AgentProviderIds,
2647
2678
  AGENT_PROVIDER_IDS,