@ainyc/canonry 4.76.1 → 4.77.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 (29) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -1
  2. package/assets/assets/{BacklinksPage-Dvc3mk9A.js → BacklinksPage-CwXveumn.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-DbjEIcrG.js → ChartPrimitives-DntKGI5J.js} +1 -1
  4. package/assets/assets/ProjectPage-CVudiU8X.js +6 -0
  5. package/assets/assets/{RunRow-CgojRiy8.js → RunRow-DMtYXaxG.js} +1 -1
  6. package/assets/assets/{RunsPage-CWEcQv7p.js → RunsPage-Cz-YlucO.js} +1 -1
  7. package/assets/assets/{SettingsPage-CCT4jasH.js → SettingsPage-BCuG3C-0.js} +1 -1
  8. package/assets/assets/TrafficPage-DV8X47wa.js +1 -0
  9. package/assets/assets/TrafficSourceDetailPage-BmYhK9jm.js +1 -0
  10. package/assets/assets/arrow-left-CUmHyNnF.js +1 -0
  11. package/assets/assets/extract-error-message-DFjy9_zi.js +1 -0
  12. package/assets/assets/{index-DRhoqa2-.css → index-BgWgJE7S.css} +1 -1
  13. package/assets/assets/{index-CKbfZ6rA.js → index-D9smxU6R.js} +5 -5
  14. package/assets/assets/{trash-2-b62WCytM.js → trash-2-B_UtEEm8.js} +1 -1
  15. package/assets/index.html +2 -2
  16. package/dist/{chunk-2FV5GIUB.js → chunk-BPZWX7YI.js} +119 -20
  17. package/dist/{chunk-5QINOJ26.js → chunk-FB43IMZT.js} +35 -17
  18. package/dist/{chunk-JNAKRK77.js → chunk-KPN22EWK.js} +20 -2
  19. package/dist/{chunk-WFMEK34V.js → chunk-XI6YSTGE.js} +1 -1
  20. package/dist/cli.js +15 -8
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-ITD2CTKH.js → intelligence-service-C76ZRMF5.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +12 -12
  25. package/assets/assets/ProjectPage-CeZYtdbm.js +0 -6
  26. package/assets/assets/TrafficPage-DtdlxDFZ.js +0 -1
  27. package/assets/assets/TrafficSourceDetailPage-CPBuL-HQ.js +0 -1
  28. package/assets/assets/extract-error-message--ubvhQzu.js +0 -1
  29. package/assets/assets/server-traffic-Dskh-6z1.js +0 -1
@@ -1 +1 @@
1
- import{c}from"./index-CKbfZ6rA.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-D9smxU6R.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
package/assets/index.html CHANGED
@@ -12,12 +12,12 @@
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-CKbfZ6rA.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-D9smxU6R.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Dq7p98wZ.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-B57xfQbP.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-ClRVR6aX.js">
19
19
  <link rel="modulepreload" crossorigin href="./assets/vendor-markdown-DK7fbRNb.js">
20
- <link rel="stylesheet" crossorigin href="./assets/index-DRhoqa2-.css">
20
+ <link rel="stylesheet" crossorigin href="./assets/index-BgWgJE7S.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -11,6 +11,7 @@ import {
11
11
  ContentActions,
12
12
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
13
13
  DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
14
+ DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
14
15
  DISCOVERY_PROMOTE_COMPETITOR_CAP,
15
16
  DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
16
17
  DiscoveryBuckets,
@@ -186,6 +187,7 @@ import {
186
187
  schedulableRunKindSchema,
187
188
  scheduleDtoSchema,
188
189
  scheduleUpsertRequestSchema,
190
+ seedCollapseWarning,
189
191
  serializeRunError,
190
192
  settingsDtoSchema,
191
193
  siteAuditPagesResponseSchema,
@@ -230,7 +232,7 @@ import {
230
232
  wordpressSchemaDeployResultDtoSchema,
231
233
  wordpressSchemaStatusResultDtoSchema,
232
234
  wordpressStatusDtoSchema
233
- } from "./chunk-JNAKRK77.js";
235
+ } from "./chunk-KPN22EWK.js";
234
236
 
235
237
  // src/intelligence-service.ts
236
238
  import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
@@ -1014,6 +1016,7 @@ var discoverySessions = sqliteTable("discovery_sessions", {
1014
1016
  aspirationalCount: integer("aspirational_count"),
1015
1017
  wastedCount: integer("wasted_count"),
1016
1018
  competitorMap: text("competitor_map", { mode: "json" }).$type().notNull().default([]),
1019
+ warning: text("warning"),
1017
1020
  error: text("error"),
1018
1021
  startedAt: text("started_at"),
1019
1022
  finishedAt: text("finished_at"),
@@ -2863,6 +2866,16 @@ var MIGRATION_VERSIONS = [
2863
2866
  `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_run ON site_audit_pages(run_id)`,
2864
2867
  `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_project_score ON site_audit_pages(project_id, overall_score)`
2865
2868
  ]
2869
+ },
2870
+ {
2871
+ // Non-fatal operator warning on a discovery session (e.g. the seed-dedup
2872
+ // degenerate-collapse guard). The session still completes; the warning
2873
+ // flags that its coverage may be misleading.
2874
+ version: 76,
2875
+ name: "discovery-session-warning",
2876
+ statements: [
2877
+ `ALTER TABLE discovery_sessions ADD COLUMN warning TEXT`
2878
+ ]
2866
2879
  }
2867
2880
  ];
2868
2881
  function isDuplicateColumnError(err) {
@@ -16652,7 +16665,7 @@ var routeCatalog = [
16652
16665
  method: "post",
16653
16666
  path: "/api/v1/projects/{name}/discover/run",
16654
16667
  summary: "Start a tracked-basket discovery session",
16655
- 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).',
16668
+ description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.95 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).',
16656
16669
  tags: ["discovery"],
16657
16670
  parameters: [nameParameter],
16658
16671
  requestBody: {
@@ -16663,7 +16676,7 @@ var routeCatalog = [
16663
16676
  type: "object",
16664
16677
  properties: {
16665
16678
  icpDescription: { type: "string", description: "Free-text ICP. Required if the project does not have spec.icpDescription stored." },
16666
- dedupThreshold: { type: "number", description: "Cosine similarity threshold for clustering. Defaults to 0.85." },
16679
+ dedupThreshold: { type: "number", description: "Cosine similarity threshold for clustering. Defaults to 0.95." },
16667
16680
  maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." },
16668
16681
  locations: {
16669
16682
  type: "array",
@@ -19140,6 +19153,68 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
19140
19153
  propertyId: conn.propertyId ?? null
19141
19154
  };
19142
19155
  }
19156
+ function parseGscApiDisabled(message) {
19157
+ if (!/accessNotConfigured|SERVICE_DISABLED|has not been used in project|is disabled/i.test(message)) {
19158
+ return null;
19159
+ }
19160
+ const projectNumber = message.match(/[?&]project=(\d+)/)?.[1] ?? message.match(/project\s+(\d+)/i)?.[1] ?? null;
19161
+ const base = "https://console.developers.google.com/apis/api";
19162
+ const qs = projectNumber ? `?project=${projectNumber}` : "";
19163
+ return {
19164
+ projectNumber,
19165
+ enableUrl: `${base}/searchconsole.googleapis.com/overview${qs}`,
19166
+ indexingApiUrl: `${base}/indexing.googleapis.com/overview${qs}`
19167
+ };
19168
+ }
19169
+ function googleAuthErrorStatus(err) {
19170
+ if (err.statusCode != null) return err.statusCode;
19171
+ const match = err.message.match(/failed \((\d{3})\)/);
19172
+ return match ? Number(match[1]) : null;
19173
+ }
19174
+ function gscErrorToAppError(err, context) {
19175
+ if (err instanceof AppError) return err;
19176
+ if (err instanceof GoogleApiError) {
19177
+ if (err.status === 429) {
19178
+ return quotaExceeded("Google Search Console API (rate limited; retries exhausted)");
19179
+ }
19180
+ if (err.status === 403) {
19181
+ const disabled = parseGscApiDisabled(err.message);
19182
+ if (disabled) {
19183
+ const inProject = disabled.projectNumber ? ` (project ${disabled.projectNumber})` : "";
19184
+ return forbidden(
19185
+ `${context}: the Google Search Console API is not enabled for your Google Cloud project${inProject}. Enable the Search Console API and the Indexing API, wait ~2\u20135 minutes, then retry: ${disabled.enableUrl}`,
19186
+ { reason: "gsc-api-disabled", upstreamStatus: 403, ...disabled }
19187
+ );
19188
+ }
19189
+ return forbidden(
19190
+ `${context}: the connected Google account does not have access to a verified Search Console property for this domain. Connect the account that owns the property.`,
19191
+ { reason: "gsc-no-property-access", upstreamStatus: 403 }
19192
+ );
19193
+ }
19194
+ if (err.status === 401) {
19195
+ return forbidden(
19196
+ `${context}: the Google connection has expired or was revoked. Reconnect Google Search Console.`,
19197
+ { reason: "gsc-reconnect", upstreamStatus: 401 }
19198
+ );
19199
+ }
19200
+ return providerError(`${context}: ${err.message}`, { upstreamStatus: err.status });
19201
+ }
19202
+ if (err instanceof GoogleAuthError) {
19203
+ const status = googleAuthErrorStatus(err);
19204
+ if (status === 429) return quotaExceeded("Google OAuth token refresh (rate limited)");
19205
+ if (status != null && status >= 400 && status < 500) {
19206
+ return forbidden(
19207
+ `${context}: the stored Google credentials are no longer valid (token refresh failed). Reconnect Google Search Console.`,
19208
+ { reason: "gsc-reconnect", upstreamStatus: status }
19209
+ );
19210
+ }
19211
+ return providerError(
19212
+ `${context}: ${err.message}. Reconnect Google Search Console if this persists.`,
19213
+ status != null ? { upstreamStatus: status } : void 0
19214
+ );
19215
+ }
19216
+ return providerError(`${context}: ${err instanceof Error ? err.message : String(err)}`);
19217
+ }
19143
19218
  async function googleRoutes(app, opts) {
19144
19219
  if (opts.googleStateSecret === void 0) {
19145
19220
  app.log.warn(
@@ -19362,9 +19437,13 @@ async function googleRoutes(app, opts) {
19362
19437
  }
19363
19438
  const store = requireConnectionStore();
19364
19439
  const project = resolveProject(app.db, request.params.name);
19365
- const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19366
- const sites = await listSites(accessToken);
19367
- return { sites };
19440
+ try {
19441
+ const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19442
+ const sites = await listSites(accessToken);
19443
+ return { sites };
19444
+ } catch (err) {
19445
+ throw gscErrorToAppError(err, "Failed to list Search Console properties");
19446
+ }
19368
19447
  });
19369
19448
  app.post("/projects/:name/google/gsc/sync", async (request) => {
19370
19449
  const store = requireConnectionStore();
@@ -19457,11 +19536,16 @@ async function googleRoutes(app, opts) {
19457
19536
  if (!url) {
19458
19537
  throw validationError("url is required");
19459
19538
  }
19460
- const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19461
- if (!propertyId) {
19462
- throw validationError("No GSC property configured for this connection");
19539
+ let result;
19540
+ try {
19541
+ const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19542
+ if (!propertyId) {
19543
+ throw validationError("No GSC property configured for this connection");
19544
+ }
19545
+ result = await inspectUrl(accessToken, url, propertyId);
19546
+ } catch (err) {
19547
+ throw gscErrorToAppError(err, "Failed to inspect URL in Search Console");
19463
19548
  }
19464
- const result = await inspectUrl(accessToken, url, propertyId);
19465
19549
  const ir = result.inspectionResult;
19466
19550
  const idx = ir.indexStatusResult;
19467
19551
  const mob = ir.mobileUsabilityResult;
@@ -19669,12 +19753,16 @@ async function googleRoutes(app, opts) {
19669
19753
  }
19670
19754
  const store = requireConnectionStore();
19671
19755
  const project = resolveProject(app.db, request.params.name);
19672
- const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19673
- if (!propertyId) {
19674
- throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
19756
+ try {
19757
+ const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19758
+ if (!propertyId) {
19759
+ throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
19760
+ }
19761
+ const sitemaps = await listSitemaps(accessToken, propertyId);
19762
+ return { sitemaps };
19763
+ } catch (err) {
19764
+ throw gscErrorToAppError(err, "Failed to list Search Console sitemaps");
19675
19765
  }
19676
- const sitemaps = await listSitemaps(accessToken, propertyId);
19677
- return { sitemaps };
19678
19766
  });
19679
19767
  app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
19680
19768
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
@@ -19690,8 +19778,13 @@ async function googleRoutes(app, opts) {
19690
19778
  if (!conn.propertyId) {
19691
19779
  throw validationError("No GSC property configured for this connection");
19692
19780
  }
19693
- const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19694
- const sitemaps = await listSitemaps(accessToken, conn.propertyId);
19781
+ let sitemaps;
19782
+ try {
19783
+ const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19784
+ sitemaps = await listSitemaps(accessToken, conn.propertyId);
19785
+ } catch (err) {
19786
+ throw gscErrorToAppError(err, "Failed to discover Search Console sitemaps");
19787
+ }
19695
19788
  if (sitemaps.length === 0) {
19696
19789
  throw validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
19697
19790
  }
@@ -31655,6 +31748,7 @@ function serializeSession(row) {
31655
31748
  aspirationalCount: row.aspirationalCount ?? null,
31656
31749
  wastedCount: row.wastedCount ?? null,
31657
31750
  competitorMap: parseCompetitorMap(row.competitorMap),
31751
+ warning: row.warning ?? null,
31658
31752
  error: row.error ?? null,
31659
31753
  startedAt: row.startedAt ?? null,
31660
31754
  finishedAt: row.finishedAt ?? null,
@@ -31690,7 +31784,6 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
31690
31784
  // ../api-routes/src/discovery/orchestrate.ts
31691
31785
  import crypto26 from "crypto";
31692
31786
  import { eq as eq31 } from "drizzle-orm";
31693
- var DEFAULT_DEDUP_THRESHOLD = 0.85;
31694
31787
  var DEFAULT_MAX_PROBES = 100;
31695
31788
  var ABSOLUTE_MAX_PROBES = 500;
31696
31789
  function classifyProbeBucket(input) {
@@ -31736,7 +31829,7 @@ async function pickCanonicals(candidates, deps, dedupThreshold) {
31736
31829
  return clusters.map(pickClusterRepresentative);
31737
31830
  }
31738
31831
  async function executeDiscovery(opts) {
31739
- const dedupThreshold = opts.dedupThreshold ?? DEFAULT_DEDUP_THRESHOLD;
31832
+ const dedupThreshold = opts.dedupThreshold ?? DISCOVERY_DEFAULT_DEDUP_THRESHOLD;
31740
31833
  const requestedMax = opts.maxProbes ?? DEFAULT_MAX_PROBES;
31741
31834
  const maxProbes = Math.min(Math.max(1, requestedMax), ABSOLUTE_MAX_PROBES);
31742
31835
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -31757,13 +31850,19 @@ async function executeDiscovery(opts) {
31757
31850
  { embed: opts.deps.embed },
31758
31851
  dedupThreshold
31759
31852
  );
31853
+ const warning = seedCollapseWarning({
31854
+ seedCountRaw,
31855
+ canonicalCount: canonicals.length,
31856
+ dedupThreshold
31857
+ });
31760
31858
  const probedCanonicals = canonicals.slice(0, maxProbes);
31761
31859
  const seedCount = probedCanonicals.length;
31762
31860
  opts.db.update(discoverySessions).set({
31763
31861
  status: DiscoverySessionStatuses.probing,
31764
31862
  seedProvider: seedResult.provider,
31765
31863
  seedCountRaw,
31766
- seedCount
31864
+ seedCount,
31865
+ warning
31767
31866
  }).where(eq31(discoverySessions.id, opts.sessionId)).run();
31768
31867
  const probeRows = [];
31769
31868
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  loadConfigRaw,
11
11
  saveConfigPatch
12
- } from "./chunk-WFMEK34V.js";
12
+ } from "./chunk-XI6YSTGE.js";
13
13
  import {
14
14
  CC_CACHE_DIR,
15
15
  DUCKDB_SPEC,
@@ -97,7 +97,7 @@ import {
97
97
  siteAuditPages,
98
98
  siteAuditSnapshots,
99
99
  usageCounters
100
- } from "./chunk-2FV5GIUB.js";
100
+ } from "./chunk-BPZWX7YI.js";
101
101
  import {
102
102
  AGENT_MEMORY_VALUE_MAX_BYTES,
103
103
  AGENT_PROVIDER_IDS,
@@ -149,7 +149,7 @@ import {
149
149
  validationError,
150
150
  winnabilityClassLabel,
151
151
  withRetry
152
- } from "./chunk-JNAKRK77.js";
152
+ } from "./chunk-KPN22EWK.js";
153
153
 
154
154
  // src/telemetry.ts
155
155
  import crypto from "crypto";
@@ -476,15 +476,17 @@ function resolveModel(config) {
476
476
  return config.model || DEFAULT_MODEL;
477
477
  }
478
478
  function createClient2(config) {
479
+ const httpOptions = config.baseUrl ? { httpOptions: { baseUrl: config.baseUrl } } : {};
479
480
  if (isVertexConfig(config)) {
480
481
  return new GoogleGenAI({
481
482
  vertexai: true,
482
483
  project: config.vertexProject,
483
484
  location: config.vertexRegion || "us-central1",
484
- ...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {}
485
+ ...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {},
486
+ ...httpOptions
485
487
  });
486
488
  }
487
- return new GoogleGenAI({ apiKey: config.apiKey });
489
+ return new GoogleGenAI({ apiKey: config.apiKey, ...httpOptions });
488
490
  }
489
491
  function validateConfig(config) {
490
492
  if ("vertexProject" in config && config.vertexProject !== void 0 && config.vertexProject.trim().length === 0) {
@@ -749,7 +751,7 @@ async function embedQueries(queries2, options) {
749
751
  if (!options.apiKey && !options.client) {
750
752
  throw new Error("embedQueries: missing apiKey");
751
753
  }
752
- const client = options.client ?? createGeminiEmbedClient(options.apiKey);
754
+ const client = options.client ?? createGeminiEmbedClient(options.apiKey, options.baseUrl);
753
755
  return client.embedBatch(queries2, {
754
756
  model: options.model ?? DEFAULT_EMBED_MODEL,
755
757
  taskType: CLUSTERING_TASK_TYPE,
@@ -770,8 +772,11 @@ function extractEmbeddingVectors(response, expectedLength) {
770
772
  return e.values;
771
773
  });
772
774
  }
773
- function createGeminiEmbedClient(apiKey) {
774
- const genai = new GoogleGenAI2({ apiKey });
775
+ function createEmbedGenAI(apiKey, baseUrl) {
776
+ return new GoogleGenAI2({ apiKey, ...baseUrl ? { httpOptions: { baseUrl } } : {} });
777
+ }
778
+ function createGeminiEmbedClient(apiKey, baseUrl) {
779
+ const genai = createEmbedGenAI(apiKey, baseUrl);
775
780
  return {
776
781
  async embedBatch(queries2, opts) {
777
782
  const response = await genai.models.embedContent({
@@ -792,6 +797,7 @@ function toGeminiConfig(config) {
792
797
  return {
793
798
  apiKey: config.apiKey ?? "",
794
799
  model: config.model,
800
+ baseUrl: config.baseUrl,
795
801
  quotaPolicy: config.quotaPolicy,
796
802
  vertexProject: config.vertexProject,
797
803
  vertexRegion: config.vertexRegion,
@@ -895,6 +901,12 @@ async function withRetry3(fn, options = {}) {
895
901
 
896
902
  // ../provider-openai/src/normalize.ts
897
903
  var DEFAULT_MODEL2 = "gpt-5.4";
904
+ function createClient3(config) {
905
+ return new OpenAI({
906
+ apiKey: config.apiKey,
907
+ ...config.baseUrl ? { baseURL: config.baseUrl } : {}
908
+ });
909
+ }
898
910
  function validateConfig2(config) {
899
911
  if (!config.apiKey || config.apiKey.length === 0) {
900
912
  return { ok: false, provider: "openai", message: "missing api key" };
@@ -910,7 +922,7 @@ async function healthcheck2(config) {
910
922
  const validation = validateConfig2(config);
911
923
  if (!validation.ok) return validation;
912
924
  try {
913
- const client = new OpenAI({ apiKey: config.apiKey });
925
+ const client = createClient3(config);
914
926
  const response = await withRetry3(
915
927
  () => client.responses.create({
916
928
  model: config.model ?? DEFAULT_MODEL2,
@@ -935,7 +947,7 @@ async function healthcheck2(config) {
935
947
  }
936
948
  async function executeTrackedQuery2(input) {
937
949
  const model = input.config.model ?? DEFAULT_MODEL2;
938
- const client = new OpenAI({ apiKey: input.config.apiKey });
950
+ const client = createClient3(input.config);
939
951
  const webSearchTool = { type: "web_search" };
940
952
  if (input.location) {
941
953
  webSearchTool.user_location = {
@@ -1108,7 +1120,7 @@ function extractDomainFromUri2(uri) {
1108
1120
  }
1109
1121
  async function generateText2(prompt, config) {
1110
1122
  const model = config.model ?? DEFAULT_MODEL2;
1111
- const client = new OpenAI({ apiKey: config.apiKey });
1123
+ const client = createClient3(config);
1112
1124
  const response = await withRetry3(
1113
1125
  () => client.responses.create({
1114
1126
  model,
@@ -1130,6 +1142,7 @@ function toOpenAIConfig(config) {
1130
1142
  return {
1131
1143
  apiKey: config.apiKey ?? "",
1132
1144
  model: config.model,
1145
+ baseUrl: config.baseUrl,
1133
1146
  quotaPolicy: config.quotaPolicy
1134
1147
  };
1135
1148
  }
@@ -4933,7 +4946,7 @@ function buildDefaultDeps(registry) {
4933
4946
  },
4934
4947
  async embed(queries2) {
4935
4948
  if (cfg.apiKey) {
4936
- return embedQueries(queries2, { apiKey: cfg.apiKey });
4949
+ return embedQueries(queries2, { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
4937
4950
  }
4938
4951
  throw new Error("Discovery currently requires a Gemini API key. Vertex-mode embeddings are not yet implemented.");
4939
4952
  },
@@ -5761,7 +5774,7 @@ function readStoredGroundingSources(rawResponse) {
5761
5774
  return result;
5762
5775
  }
5763
5776
  async function backfillInsightsCommand(project, opts) {
5764
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-ITD2CTKH.js");
5777
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-C76ZRMF5.js");
5765
5778
  const config = loadConfig();
5766
5779
  const db = createClient(config.database);
5767
5780
  migrate(db);
@@ -9452,11 +9465,16 @@ var BROWSER_ADAPTERS = [
9452
9465
  var adapterMap = Object.fromEntries(
9453
9466
  API_ADAPTERS.map((a) => [a.name, a])
9454
9467
  );
9455
- function summarizeProviderConfig(provider, config) {
9468
+ function summarizeProviderConfig(config) {
9456
9469
  return {
9457
9470
  configured: Boolean(config?.apiKey || config?.baseUrl),
9458
9471
  model: config?.model ?? null,
9459
- baseUrl: provider === "local" ? config?.baseUrl ?? null : null,
9472
+ // baseUrl is surfaced for ALL providers, not just local — gemini/openai now
9473
+ // honor a custom endpoint, so repointing one must show in the settings
9474
+ // summary AND produce an audit diff. Omitting it for API providers would let
9475
+ // an endpoint redirect (a credential-exfiltration vector on a box where the
9476
+ // provider key is the carrier) happen with no audit trail.
9477
+ baseUrl: config?.baseUrl ?? null,
9460
9478
  quota: { ...config?.quota ?? DEFAULT_QUOTA }
9461
9479
  };
9462
9480
  }
@@ -10330,7 +10348,7 @@ async function createServer(opts) {
10330
10348
  if (!adapterMap[name]) return null;
10331
10349
  if (!opts.config.providers) opts.config.providers = {};
10332
10350
  const existing = opts.config.providers[name];
10333
- const beforeConfig = summarizeProviderConfig(name, existing);
10351
+ const beforeConfig = summarizeProviderConfig(existing);
10334
10352
  const mergedQuota = incomingQuota ? { ...existing?.quota ?? DEFAULT_QUOTA, ...incomingQuota } : existing?.quota;
10335
10353
  opts.config.providers[name] = {
10336
10354
  apiKey: apiKey || existing?.apiKey,
@@ -10369,7 +10387,7 @@ async function createServer(opts) {
10369
10387
  entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
10370
10388
  }
10371
10389
  }
10372
- const afterConfig = summarizeProviderConfig(name, opts.config.providers[name]);
10390
+ const afterConfig = summarizeProviderConfig(opts.config.providers[name]);
10373
10391
  if (JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
10374
10392
  const diff = JSON.stringify({
10375
10393
  before: existing ? beforeConfig : null,
@@ -41,8 +41,8 @@ function authRequired(message = "Authentication required") {
41
41
  function authInvalid() {
42
42
  return new AppError("AUTH_INVALID", "Invalid API key", 401);
43
43
  }
44
- function forbidden(message = "Forbidden") {
45
- return new AppError("FORBIDDEN", message, 403);
44
+ function forbidden(message = "Forbidden", details) {
45
+ return new AppError("FORBIDDEN", message, 403, details);
46
46
  }
47
47
  function quotaExceeded(metric) {
48
48
  return new AppError("QUOTA_EXCEEDED", `Quota exceeded for ${metric}`, 429);
@@ -1870,6 +1870,12 @@ var discoverySessionDtoSchema = z17.object({
1870
1870
  aspirationalCount: z17.number().int().nullable().default(null),
1871
1871
  wastedCount: z17.number().int().nullable().default(null),
1872
1872
  competitorMap: z17.array(discoveryCompetitorMapEntrySchema).default([]),
1873
+ /**
1874
+ * Non-fatal operator warning recorded while the session ran (currently the
1875
+ * seed dedup collapse guard). The session still completes; the warning flags
1876
+ * that its coverage may be misleading.
1877
+ */
1878
+ warning: z17.string().nullable().optional(),
1873
1879
  error: z17.string().nullable().optional(),
1874
1880
  startedAt: z17.string().nullable().optional(),
1875
1881
  finishedAt: z17.string().nullable().optional(),
@@ -1879,6 +1885,16 @@ var discoverySessionDetailDtoSchema = discoverySessionDtoSchema.extend({
1879
1885
  probes: z17.array(discoveryProbeDtoSchema).default([])
1880
1886
  });
1881
1887
  var DISCOVERY_MAX_PROBES_CAP = 500;
1888
+ var DISCOVERY_DEFAULT_DEDUP_THRESHOLD = 0.95;
1889
+ var DISCOVERY_SEED_COLLAPSE_RATIO = 0.2;
1890
+ var DISCOVERY_SEED_COLLAPSE_MIN_RAW = 10;
1891
+ function seedCollapseWarning(input) {
1892
+ const { seedCountRaw, canonicalCount, dedupThreshold } = input;
1893
+ if (seedCountRaw < DISCOVERY_SEED_COLLAPSE_MIN_RAW) return null;
1894
+ if (canonicalCount / seedCountRaw >= DISCOVERY_SEED_COLLAPSE_RATIO) return null;
1895
+ const noun = canonicalCount === 1 ? "query" : "queries";
1896
+ return `Seed dedup collapsed ${seedCountRaw} raw candidates into ${canonicalCount} canonical ${noun} at threshold ${dedupThreshold}. Distinct intents were likely merged into one cluster; re-run with a higher --dedup-threshold.`;
1897
+ }
1882
1898
  var discoveryRunRequestSchema = z17.object({
1883
1899
  icpDescription: z17.string().min(1).optional(),
1884
1900
  dedupThreshold: z17.number().min(0).max(1).optional(),
@@ -4440,6 +4456,8 @@ export {
4440
4456
  discoverySessionDtoSchema,
4441
4457
  discoverySessionDetailDtoSchema,
4442
4458
  DISCOVERY_MAX_PROBES_CAP,
4459
+ DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
4460
+ seedCollapseWarning,
4443
4461
  discoveryRunRequestSchema,
4444
4462
  discoveryPromoteRequestSchema,
4445
4463
  discoveryPromotePreviewSchema,
@@ -22,7 +22,7 @@ import {
22
22
  trafficConnectVercelRequestSchema,
23
23
  trafficConnectWordpressRequestSchema,
24
24
  trafficEventKindSchema
25
- } from "./chunk-JNAKRK77.js";
25
+ } from "./chunk-KPN22EWK.js";
26
26
 
27
27
  // src/config.ts
28
28
  import fs from "fs";
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-5QINOJ26.js";
30
+ } from "./chunk-FB43IMZT.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
@@ -44,7 +44,7 @@ import {
44
44
  saveConfig,
45
45
  saveConfigPatch,
46
46
  usageError
47
- } from "./chunk-WFMEK34V.js";
47
+ } from "./chunk-XI6YSTGE.js";
48
48
  import {
49
49
  apiKeys,
50
50
  createClient,
@@ -52,7 +52,7 @@ import {
52
52
  projects,
53
53
  queries,
54
54
  renderReportHtml
55
- } from "./chunk-2FV5GIUB.js";
55
+ } from "./chunk-BPZWX7YI.js";
56
56
  import {
57
57
  CcReleaseSyncStatuses,
58
58
  CheckScopes,
@@ -72,7 +72,7 @@ import {
72
72
  providerQuotaPolicySchema,
73
73
  resolveProviderInput,
74
74
  winnabilityClassSchema
75
- } from "./chunk-JNAKRK77.js";
75
+ } from "./chunk-KPN22EWK.js";
76
76
 
77
77
  // src/cli.ts
78
78
  import { pathToFileURL } from "url";
@@ -1797,6 +1797,7 @@ function printSessionDetail(session) {
1797
1797
  console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})`);
1798
1798
  }
1799
1799
  }
1800
+ if (session.warning) console.log(` Warning: ${session.warning}`);
1800
1801
  if (session.error) console.log(` Error: ${session.error}`);
1801
1802
  if (session.startedAt) console.log(` Started: ${session.startedAt}`);
1802
1803
  if (session.finishedAt) console.log(` Finished: ${session.finishedAt}`);
@@ -1920,7 +1921,7 @@ Usage: ${usage}`,
1920
1921
  var DISCOVER_CLI_COMMANDS = [
1921
1922
  {
1922
1923
  path: ["discover", "run"],
1923
- usage: 'canonry discover run <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.85] [--max-probes 100] [--wait] [--format json]',
1924
+ usage: 'canonry discover run <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.95] [--max-probes 100] [--wait] [--format json]',
1924
1925
  options: {
1925
1926
  icp: stringOption(),
1926
1927
  "icp-angle": multiStringOption(),
@@ -1930,7 +1931,7 @@ var DISCOVER_CLI_COMMANDS = [
1930
1931
  wait: { type: "boolean", default: false }
1931
1932
  },
1932
1933
  run: async (input) => {
1933
- const usage = 'canonry discover run <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.85] [--max-probes 100] [--wait] [--format json]';
1934
+ const usage = 'canonry discover run <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.95] [--max-probes 100] [--wait] [--format json]';
1934
1935
  const project = requireProject(input, "discover.run", usage);
1935
1936
  await discoverRun(project, {
1936
1937
  icp: getString(input.values, "icp"),
@@ -1949,7 +1950,7 @@ var DISCOVER_CLI_COMMANDS = [
1949
1950
  },
1950
1951
  {
1951
1952
  path: ["discover", "seed"],
1952
- usage: 'canonry discover seed <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.85] [--max-probes 100] [--wait] [--format json]',
1953
+ usage: 'canonry discover seed <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.95] [--max-probes 100] [--wait] [--format json]',
1953
1954
  options: {
1954
1955
  icp: stringOption(),
1955
1956
  "icp-angle": multiStringOption(),
@@ -1959,7 +1960,7 @@ var DISCOVER_CLI_COMMANDS = [
1959
1960
  wait: { type: "boolean", default: false }
1960
1961
  },
1961
1962
  run: async (input) => {
1962
- const usage = 'canonry discover seed <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.85] [--max-probes 100] [--wait] [--format json]';
1963
+ const usage = 'canonry discover seed <project> [--icp "..."] [--icp-angle "..."] [--locations michigan,florida] [--dedup-threshold 0.95] [--max-probes 100] [--wait] [--format json]';
1963
1964
  const project = requireProject(input, "discover.seed", usage);
1964
1965
  await discoverSeed(project, {
1965
1966
  icp: getString(input.values, "icp"),
@@ -9632,6 +9633,7 @@ var envSchema = z.object({
9632
9633
  // Gemini
9633
9634
  GEMINI_API_KEY: z.string().optional(),
9634
9635
  GEMINI_MODEL: z.string().optional(),
9636
+ GEMINI_BASE_URL: z.string().optional(),
9635
9637
  GEMINI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
9636
9638
  GEMINI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
9637
9639
  GEMINI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
@@ -9642,6 +9644,7 @@ var envSchema = z.object({
9642
9644
  // OpenAI
9643
9645
  OPENAI_API_KEY: z.string().optional(),
9644
9646
  OPENAI_MODEL: z.string().optional(),
9647
+ OPENAI_BASE_URL: z.string().optional(),
9645
9648
  OPENAI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
9646
9649
  OPENAI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
9647
9650
  OPENAI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
@@ -9668,11 +9671,13 @@ var bootstrapEnvSchema = z.object({
9668
9671
  CANONRY_DATABASE_PATH: z.string().optional(),
9669
9672
  GEMINI_API_KEY: z.string().optional(),
9670
9673
  GEMINI_MODEL: z.string().optional(),
9674
+ GEMINI_BASE_URL: z.string().optional(),
9671
9675
  GEMINI_VERTEX_PROJECT: z.string().optional(),
9672
9676
  GEMINI_VERTEX_REGION: z.string().optional(),
9673
9677
  GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
9674
9678
  OPENAI_API_KEY: z.string().optional(),
9675
9679
  OPENAI_MODEL: z.string().optional(),
9680
+ OPENAI_BASE_URL: z.string().optional(),
9676
9681
  ANTHROPIC_API_KEY: z.string().optional(),
9677
9682
  ANTHROPIC_MODEL: z.string().optional(),
9678
9683
  PERPLEXITY_API_KEY: z.string().optional(),
@@ -9691,6 +9696,7 @@ function getBootstrapEnv(source, overrides) {
9691
9696
  providers.gemini = {
9692
9697
  apiKey: parsed.GEMINI_API_KEY ?? "",
9693
9698
  model: parsed.GEMINI_MODEL || "gemini-2.5-flash",
9699
+ baseUrl: parsed.GEMINI_BASE_URL,
9694
9700
  quota: providerQuotaPolicySchema.parse({
9695
9701
  maxConcurrency: 2,
9696
9702
  maxRequestsPerMinute: 10,
@@ -9705,6 +9711,7 @@ function getBootstrapEnv(source, overrides) {
9705
9711
  providers.openai = {
9706
9712
  apiKey: parsed.OPENAI_API_KEY,
9707
9713
  model: parsed.OPENAI_MODEL || "gpt-5.4",
9714
+ baseUrl: parsed.OPENAI_BASE_URL,
9708
9715
  quota: providerQuotaPolicySchema.parse({
9709
9716
  maxConcurrency: 2,
9710
9717
  maxRequestsPerMinute: 10,
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-5QINOJ26.js";
3
+ } from "./chunk-FB43IMZT.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-WFMEK34V.js";
7
- import "./chunk-2FV5GIUB.js";
8
- import "./chunk-JNAKRK77.js";
6
+ } from "./chunk-XI6YSTGE.js";
7
+ import "./chunk-BPZWX7YI.js";
8
+ import "./chunk-KPN22EWK.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-2FV5GIUB.js";
4
- import "./chunk-JNAKRK77.js";
3
+ } from "./chunk-BPZWX7YI.js";
4
+ import "./chunk-KPN22EWK.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };