@ainyc/canonry 1.26.0 → 1.27.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.
@@ -72,6 +72,10 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
72
72
  } catch {
73
73
  }
74
74
  }
75
+ if ("CANONRY_BASE_PATH" in process.env) {
76
+ const val = process.env.CANONRY_BASE_PATH.trim();
77
+ parsed.basePath = val || void 0;
78
+ }
75
79
  if (parsed.basePath) {
76
80
  const normalizedBase = "/" + parsed.basePath.replace(/^\/|\/$/g, "");
77
81
  try {
@@ -5047,6 +5051,14 @@ async function settingsRoutes(app, opts) {
5047
5051
  const err = validationError("baseUrl is required for local provider");
5048
5052
  return reply.status(err.statusCode).send(err.toJSON());
5049
5053
  }
5054
+ } else if (name === "gemini" && !apiKey) {
5055
+ const geminiSummary = (opts.providerSummary ?? []).find((p) => p.name === "gemini");
5056
+ if (!geminiSummary?.vertexConfigured) {
5057
+ const err = validationError(
5058
+ "apiKey is required for Gemini unless Vertex AI is configured (set GEMINI_VERTEX_PROJECT env var or vertexProject in config file)"
5059
+ );
5060
+ return reply.status(err.statusCode).send(err.toJSON());
5061
+ }
5050
5062
  } else {
5051
5063
  if (!apiKey || typeof apiKey !== "string") {
5052
5064
  const err = validationError("apiKey is required");
@@ -7627,18 +7639,58 @@ async function apiRoutes(app, opts) {
7627
7639
 
7628
7640
  // ../provider-gemini/src/normalize.ts
7629
7641
  import { GoogleGenerativeAI } from "@google/generative-ai";
7642
+ import { VertexAI } from "@google-cloud/vertexai";
7630
7643
  var DEFAULT_MODEL = "gemini-3-flash";
7631
7644
  var VALIDATION_PATTERN = /^gemini-/;
7645
+ function isVertexConfig(config) {
7646
+ return !!config.vertexProject;
7647
+ }
7632
7648
  function resolveModel(config) {
7633
7649
  const m = config.model;
7634
7650
  if (!m) return DEFAULT_MODEL;
7635
7651
  if (VALIDATION_PATTERN.test(m)) return m;
7652
+ const backend = isVertexConfig(config) ? "Vertex AI" : "AI Studio";
7636
7653
  console.warn(
7637
- `[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini AI Studio API (generativelanguage.googleapis.com) which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
7654
+ `[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini ${backend} API which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
7638
7655
  );
7639
7656
  return DEFAULT_MODEL;
7640
7657
  }
7658
+ function wrapVertexResponse(response) {
7659
+ return {
7660
+ text: () => {
7661
+ const parts = response.candidates?.[0]?.content?.parts;
7662
+ return parts?.map((p) => p.text ?? "").join("") ?? "";
7663
+ },
7664
+ candidates: response.candidates?.map((c) => ({
7665
+ content: c.content,
7666
+ finishReason: c.finishReason,
7667
+ groundingMetadata: c.groundingMetadata
7668
+ })),
7669
+ usageMetadata: response.usageMetadata
7670
+ };
7671
+ }
7672
+ function createVertexModel(config, model, tools) {
7673
+ const vertexOpts = {
7674
+ project: config.vertexProject,
7675
+ location: config.vertexRegion || "us-central1",
7676
+ googleAuthOptions: config.vertexCredentials ? { keyFilename: config.vertexCredentials } : void 0
7677
+ };
7678
+ const vertexAI = new VertexAI(vertexOpts);
7679
+ return vertexAI.getGenerativeModel({
7680
+ model,
7681
+ ...tools ? { tools } : {}
7682
+ });
7683
+ }
7641
7684
  function validateConfig(config) {
7685
+ if (isVertexConfig(config)) {
7686
+ const model2 = resolveModel(config);
7687
+ return {
7688
+ ok: true,
7689
+ provider: "gemini",
7690
+ message: "config valid (Vertex AI)",
7691
+ model: model2
7692
+ };
7693
+ }
7642
7694
  if (!config.apiKey || config.apiKey.length === 0) {
7643
7695
  return { ok: false, provider: "gemini", message: "missing api key" };
7644
7696
  }
@@ -7655,16 +7707,29 @@ async function healthcheck(config) {
7655
7707
  const validation = validateConfig(config);
7656
7708
  if (!validation.ok) return validation;
7657
7709
  try {
7658
- const genAI = new GoogleGenerativeAI(config.apiKey);
7659
- const model = genAI.getGenerativeModel({ model: resolveModel(config) });
7660
- const result = await model.generateContent('Say "ok"');
7661
- const text2 = result.response.text();
7662
- return {
7663
- ok: text2.length > 0,
7664
- provider: "gemini",
7665
- message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
7666
- model: config.model ?? DEFAULT_MODEL
7667
- };
7710
+ const model = resolveModel(config);
7711
+ if (isVertexConfig(config)) {
7712
+ const generativeModel = createVertexModel(config, model);
7713
+ const result = await generativeModel.generateContent('Say "ok"');
7714
+ const text2 = result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7715
+ return {
7716
+ ok: text2.length > 0,
7717
+ provider: "gemini",
7718
+ message: text2.length > 0 ? "gemini vertex ai verified" : "empty response from gemini vertex ai",
7719
+ model
7720
+ };
7721
+ } else {
7722
+ const genAI = new GoogleGenerativeAI(config.apiKey);
7723
+ const generativeModel = genAI.getGenerativeModel({ model });
7724
+ const result = await generativeModel.generateContent('Say "ok"');
7725
+ const text2 = result.response.text();
7726
+ return {
7727
+ ok: text2.length > 0,
7728
+ provider: "gemini",
7729
+ message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
7730
+ model
7731
+ };
7732
+ }
7668
7733
  } catch (err) {
7669
7734
  return {
7670
7735
  ok: false,
@@ -7676,23 +7741,41 @@ async function healthcheck(config) {
7676
7741
  }
7677
7742
  async function executeTrackedQuery(input) {
7678
7743
  const model = resolveModel(input.config);
7679
- const genAI = new GoogleGenerativeAI(input.config.apiKey);
7680
- const generativeModel = genAI.getGenerativeModel({
7681
- model,
7682
- tools: [{ googleSearch: {} }]
7683
- });
7684
7744
  const prompt = buildPrompt(input.keyword, input.location);
7685
- const result = await generativeModel.generateContent(prompt);
7686
- const response = result.response;
7687
- const groundingMetadata = extractGroundingMetadata(response);
7688
- const searchQueries = extractSearchQueries(response);
7689
- return {
7690
- provider: "gemini",
7691
- rawResponse: responseToRecord(response),
7692
- model,
7693
- groundingSources: groundingMetadata,
7694
- searchQueries
7695
- };
7745
+ if (isVertexConfig(input.config)) {
7746
+ const vertexSearchTool = { googleSearchRetrieval: {} };
7747
+ const generativeModel = createVertexModel(input.config, model, [vertexSearchTool]);
7748
+ const result = await generativeModel.generateContent(prompt);
7749
+ const response = result.response;
7750
+ const unified = wrapVertexResponse(response);
7751
+ const groundingMetadata = extractGroundingMetadataFromUnified(unified);
7752
+ const searchQueries = extractSearchQueriesFromUnified(unified);
7753
+ return {
7754
+ provider: "gemini",
7755
+ rawResponse: unifiedToRecord(unified),
7756
+ model,
7757
+ groundingSources: groundingMetadata,
7758
+ searchQueries
7759
+ };
7760
+ } else {
7761
+ const searchTool = { googleSearch: {} };
7762
+ const genAI = new GoogleGenerativeAI(input.config.apiKey);
7763
+ const generativeModel = genAI.getGenerativeModel({
7764
+ model,
7765
+ tools: [searchTool]
7766
+ });
7767
+ const result = await generativeModel.generateContent(prompt);
7768
+ const response = result.response;
7769
+ const groundingMetadata = extractGroundingMetadata(response);
7770
+ const searchQueries = extractSearchQueries(response);
7771
+ return {
7772
+ provider: "gemini",
7773
+ rawResponse: responseToRecord(response),
7774
+ model,
7775
+ groundingSources: groundingMetadata,
7776
+ searchQueries
7777
+ };
7778
+ }
7696
7779
  }
7697
7780
  function normalizeResult(raw) {
7698
7781
  const answerText = extractAnswerText(raw.rawResponse);
@@ -7738,6 +7821,22 @@ function extractGroundingMetadata(response) {
7738
7821
  return [];
7739
7822
  }
7740
7823
  }
7824
+ function extractGroundingMetadataFromUnified(response) {
7825
+ try {
7826
+ const candidate = response.candidates?.[0];
7827
+ if (!candidate) return [];
7828
+ const metadata = candidate.groundingMetadata;
7829
+ if (!metadata) return [];
7830
+ const chunks = metadata.groundingChunks;
7831
+ if (!chunks) return [];
7832
+ return chunks.filter((chunk) => chunk.web?.uri).map((chunk) => ({
7833
+ uri: chunk.web.uri,
7834
+ title: chunk.web?.title ?? ""
7835
+ }));
7836
+ } catch {
7837
+ return [];
7838
+ }
7839
+ }
7741
7840
  function extractSearchQueries(response) {
7742
7841
  try {
7743
7842
  const candidate = response.candidates?.[0];
@@ -7746,6 +7845,14 @@ function extractSearchQueries(response) {
7746
7845
  return [];
7747
7846
  }
7748
7847
  }
7848
+ function extractSearchQueriesFromUnified(response) {
7849
+ try {
7850
+ const candidate = response.candidates?.[0];
7851
+ return candidate?.groundingMetadata?.webSearchQueries ?? [];
7852
+ } catch {
7853
+ return [];
7854
+ }
7855
+ }
7749
7856
  function extractCitedDomains(raw) {
7750
7857
  const domains = /* @__PURE__ */ new Set();
7751
7858
  for (const source of raw.groundingSources) {
@@ -7793,10 +7900,16 @@ function extractDomainFromUri(uri) {
7793
7900
  }
7794
7901
  async function generateText(prompt, config) {
7795
7902
  const model = resolveModel(config);
7796
- const genAI = new GoogleGenerativeAI(config.apiKey);
7797
- const generativeModel = genAI.getGenerativeModel({ model });
7798
- const result = await generativeModel.generateContent(prompt);
7799
- return result.response.text();
7903
+ if (isVertexConfig(config)) {
7904
+ const generativeModel = createVertexModel(config, model);
7905
+ const result = await generativeModel.generateContent(prompt);
7906
+ return result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7907
+ } else {
7908
+ const genAI = new GoogleGenerativeAI(config.apiKey);
7909
+ const generativeModel = genAI.getGenerativeModel({ model });
7910
+ const result = await generativeModel.generateContent(prompt);
7911
+ return result.response.text();
7912
+ }
7800
7913
  }
7801
7914
  function responseToRecord(response) {
7802
7915
  try {
@@ -7816,13 +7929,34 @@ function responseToRecord(response) {
7816
7929
  return { error: "failed to serialize response" };
7817
7930
  }
7818
7931
  }
7932
+ function unifiedToRecord(response) {
7933
+ try {
7934
+ const candidates = response.candidates?.map((c) => ({
7935
+ content: c.content,
7936
+ finishReason: c.finishReason,
7937
+ groundingMetadata: c.groundingMetadata ? {
7938
+ webSearchQueries: c.groundingMetadata.webSearchQueries,
7939
+ groundingChunks: c.groundingMetadata.groundingChunks
7940
+ } : void 0
7941
+ }));
7942
+ return {
7943
+ candidates: candidates ?? [],
7944
+ usageMetadata: response.usageMetadata ?? null
7945
+ };
7946
+ } catch {
7947
+ return { error: "failed to serialize response" };
7948
+ }
7949
+ }
7819
7950
 
7820
7951
  // ../provider-gemini/src/adapter.ts
7821
7952
  function toGeminiConfig(config) {
7822
7953
  return {
7823
7954
  apiKey: config.apiKey ?? "",
7824
7955
  model: config.model,
7825
- quotaPolicy: config.quotaPolicy
7956
+ quotaPolicy: config.quotaPolicy,
7957
+ vertexProject: config.vertexProject,
7958
+ vertexRegion: config.vertexRegion,
7959
+ vertexCredentials: config.vertexCredentials
7826
7960
  };
7827
7961
  }
7828
7962
  var geminiAdapter = {
@@ -10872,19 +11006,22 @@ async function createServer(opts) {
10872
11006
  }
10873
11007
  log6.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
10874
11008
  const p = providers[k];
10875
- return p?.apiKey || p?.baseUrl;
11009
+ return p?.apiKey || p?.baseUrl || p?.vertexProject;
10876
11010
  }) });
10877
11011
  for (const adapter of API_ADAPTERS) {
10878
11012
  const entry = providers[adapter.name];
10879
11013
  if (!entry) continue;
10880
- const isConfigured = adapter.name === "local" ? !!entry.baseUrl : !!entry.apiKey;
11014
+ const isConfigured = adapter.name === "local" ? !!entry.baseUrl : adapter.name === "gemini" ? !!(entry.apiKey || entry.vertexProject) : !!entry.apiKey;
10881
11015
  if (isConfigured) {
10882
11016
  registry.register(adapter, {
10883
11017
  provider: adapter.name,
10884
11018
  apiKey: entry.apiKey,
10885
11019
  baseUrl: entry.baseUrl,
10886
11020
  model: entry.model,
10887
- quotaPolicy: entry.quota ?? DEFAULT_QUOTA
11021
+ quotaPolicy: entry.quota ?? DEFAULT_QUOTA,
11022
+ vertexProject: entry.vertexProject,
11023
+ vertexRegion: entry.vertexRegion,
11024
+ vertexCredentials: entry.vertexCredentials
10888
11025
  });
10889
11026
  }
10890
11027
  }
@@ -10918,7 +11055,8 @@ async function createServer(opts) {
10918
11055
  modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
10919
11056
  model: registry.get(adapter.name)?.config.model,
10920
11057
  configured: !!registry.get(adapter.name),
10921
- quota: registry.get(adapter.name)?.config.quotaPolicy
11058
+ quota: registry.get(adapter.name)?.config.quotaPolicy,
11059
+ vertexConfigured: adapter.name === "gemini" ? !!opts.config.providers?.gemini?.vertexProject : void 0
10922
11060
  }));
10923
11061
  const googleSettingsSummary = {
10924
11062
  configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
@@ -11212,7 +11350,12 @@ async function createServer(opts) {
11212
11350
  apiKey: apiKey || existing?.apiKey,
11213
11351
  baseUrl: baseUrl || existing?.baseUrl,
11214
11352
  model: model || existing?.model,
11215
- quota: mergedQuota
11353
+ quota: mergedQuota,
11354
+ // Preserve Vertex AI config (Gemini provider) — these are set via
11355
+ // config file or env vars, not through the dashboard update payload
11356
+ vertexProject: existing?.vertexProject,
11357
+ vertexRegion: existing?.vertexRegion,
11358
+ vertexCredentials: existing?.vertexCredentials
11216
11359
  };
11217
11360
  try {
11218
11361
  saveConfig(opts.config);
@@ -11226,13 +11369,19 @@ async function createServer(opts) {
11226
11369
  apiKey: apiKey || existing?.apiKey,
11227
11370
  baseUrl: baseUrl || existing?.baseUrl,
11228
11371
  model: model || existing?.model,
11229
- quotaPolicy: quota
11372
+ quotaPolicy: quota,
11373
+ vertexProject: existing?.vertexProject,
11374
+ vertexRegion: existing?.vertexRegion,
11375
+ vertexCredentials: existing?.vertexCredentials
11230
11376
  });
11231
11377
  const entry = providerSummary.find((p) => p.name === name);
11232
11378
  if (entry) {
11233
11379
  entry.configured = true;
11234
11380
  entry.model = model || registry.get(name)?.config.model;
11235
11381
  entry.quota = quota;
11382
+ if (name === "gemini") {
11383
+ entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
11384
+ }
11236
11385
  }
11237
11386
  const afterConfig = summarizeProviderConfig(name, opts.config.providers[name]);
11238
11387
  if (JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
@@ -11428,7 +11577,12 @@ async function createServer(opts) {
11428
11577
  return reply.status(404).send({ error: "Not found" });
11429
11578
  });
11430
11579
  }
11431
- const healthHandler = async () => ({ status: "ok", service: "canonry", version: PKG_VERSION });
11580
+ const healthHandler = async () => ({
11581
+ status: "ok",
11582
+ service: "canonry",
11583
+ version: PKG_VERSION,
11584
+ ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {}
11585
+ });
11432
11586
  app.get("/health", healthHandler);
11433
11587
  if (basePath) {
11434
11588
  app.get(`${basePath}health`, healthHandler);
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  setGoogleAuthConfig,
20
20
  showFirstRunNotice,
21
21
  trackEvent
22
- } from "./chunk-L6TL4UQI.js";
22
+ } from "./chunk-UTRZ3UB5.js";
23
23
 
24
24
  // src/cli.ts
25
25
  import { pathToFileURL } from "url";
@@ -148,14 +148,56 @@ Usage: ${spec.usage}`, {
148
148
  }
149
149
 
150
150
  // src/client.ts
151
+ function createApiClient() {
152
+ const config = loadConfig();
153
+ const basePathResolved = !!config.basePath || "CANONRY_BASE_PATH" in process.env;
154
+ return new ApiClient(config.apiUrl, config.apiKey, { skipProbe: basePathResolved });
155
+ }
151
156
  var ApiClient = class {
152
157
  baseUrl;
158
+ originUrl;
153
159
  apiKey;
154
- constructor(baseUrl, apiKey) {
155
- this.baseUrl = baseUrl.replace(/\/$/, "") + "/api/v1";
160
+ probePromise = null;
161
+ probeSkipped;
162
+ constructor(baseUrl, apiKey, opts) {
163
+ this.originUrl = baseUrl.replace(/\/$/, "");
164
+ this.baseUrl = this.originUrl + "/api/v1";
156
165
  this.apiKey = apiKey;
166
+ this.probeSkipped = opts?.skipProbe ?? false;
167
+ }
168
+ /**
169
+ * On first API call, probe /health to auto-discover basePath when the user
170
+ * hasn't configured one locally. This lets `canonry run` in a separate shell
171
+ * discover that the server is running at e.g. /canonry/ without requiring
172
+ * config.yaml edits or CANONRY_BASE_PATH in every shell.
173
+ */
174
+ probeBasePath() {
175
+ if (this.probeSkipped) return Promise.resolve();
176
+ if (!this.probePromise) {
177
+ this.probePromise = (async () => {
178
+ try {
179
+ const origin = new URL(this.originUrl).origin;
180
+ const res = await fetch(`${origin}/health`, {
181
+ signal: AbortSignal.timeout(2e3)
182
+ });
183
+ if (res.ok) {
184
+ const body = await res.json();
185
+ if (body.basePath && typeof body.basePath === "string") {
186
+ const normalized = "/" + body.basePath.replace(/^\/|\/$/g, "");
187
+ if (normalized !== "/") {
188
+ this.originUrl = origin + normalized;
189
+ this.baseUrl = this.originUrl + "/api/v1";
190
+ }
191
+ }
192
+ }
193
+ } catch {
194
+ }
195
+ })();
196
+ }
197
+ return this.probePromise;
157
198
  }
158
199
  async request(method, path4, body) {
200
+ await this.probeBasePath();
159
201
  const url = `${this.baseUrl}${path4}`;
160
202
  const serializedBody = body != null ? JSON.stringify(body) : void 0;
161
203
  const headers = {
@@ -440,8 +482,7 @@ var ApiClient = class {
440
482
 
441
483
  // src/commands/bing.ts
442
484
  function getClient() {
443
- const config = loadConfig();
444
- return new ApiClient(config.apiUrl, config.apiKey);
485
+ return createApiClient();
445
486
  }
446
487
  async function bingConnect(project, opts) {
447
488
  let apiKey = opts?.apiKey;
@@ -963,8 +1004,7 @@ var BING_CLI_COMMANDS = [
963
1004
 
964
1005
  // src/commands/cdp.ts
965
1006
  function getClient2() {
966
- const config = loadConfig();
967
- return new ApiClient(config.apiUrl, config.apiKey);
1007
+ return createApiClient();
968
1008
  }
969
1009
  async function cdpConnect(opts) {
970
1010
  const config = loadConfig();
@@ -1155,8 +1195,7 @@ var CDP_CLI_COMMANDS = [
1155
1195
 
1156
1196
  // src/commands/ga.ts
1157
1197
  function getClient3() {
1158
- const config = loadConfig();
1159
- return new ApiClient(config.apiUrl, config.apiKey);
1198
+ return createApiClient();
1160
1199
  }
1161
1200
  async function gaConnect(project, opts) {
1162
1201
  if (!opts.propertyId) {
@@ -1398,8 +1437,7 @@ var GA_CLI_COMMANDS = [
1398
1437
 
1399
1438
  // src/commands/competitor.ts
1400
1439
  function getClient4() {
1401
- const config = loadConfig();
1402
- return new ApiClient(config.apiUrl, config.apiKey);
1440
+ return createApiClient();
1403
1441
  }
1404
1442
  async function addCompetitors(project, domains, format) {
1405
1443
  const client = getClient4();
@@ -1480,8 +1518,7 @@ var COMPETITOR_CLI_COMMANDS = [
1480
1518
 
1481
1519
  // src/commands/google.ts
1482
1520
  function getClient5() {
1483
- const config = loadConfig();
1484
- return new ApiClient(config.apiUrl, config.apiKey);
1521
+ return createApiClient();
1485
1522
  }
1486
1523
  async function waitForRunStatus(client, runId, config) {
1487
1524
  const start = Date.now();
@@ -2308,8 +2345,7 @@ var GOOGLE_CLI_COMMANDS = [
2308
2345
  // src/commands/keyword.ts
2309
2346
  import fs from "fs";
2310
2347
  function getClient6() {
2311
- const config = loadConfig();
2312
- return new ApiClient(config.apiUrl, config.apiKey);
2348
+ return createApiClient();
2313
2349
  }
2314
2350
  async function addKeywords(project, keywords, format) {
2315
2351
  const client = getClient6();
@@ -2553,8 +2589,7 @@ var KEYWORD_CLI_COMMANDS = [
2553
2589
 
2554
2590
  // src/commands/notify.ts
2555
2591
  function getClient7() {
2556
- const config = loadConfig();
2557
- return new ApiClient(config.apiUrl, config.apiKey);
2592
+ return createApiClient();
2558
2593
  }
2559
2594
  async function addNotification(project, opts) {
2560
2595
  const client = getClient7();
@@ -2726,8 +2761,7 @@ async function applyConfigFile(filePath) {
2726
2761
  }
2727
2762
  const content = fs2.readFileSync(filePath, "utf-8");
2728
2763
  const docs = parseAllDocuments(content);
2729
- const clientConfig = loadConfig();
2730
- const client = new ApiClient(clientConfig.apiUrl, clientConfig.apiKey);
2764
+ const client = createApiClient();
2731
2765
  const errors = [];
2732
2766
  const applied = [];
2733
2767
  for (let i = 0; i < docs.length; i++) {
@@ -2789,8 +2823,7 @@ async function applyConfigs(filePaths, format) {
2789
2823
 
2790
2824
  // src/commands/analytics.ts
2791
2825
  function getClient8() {
2792
- const config = loadConfig();
2793
- return new ApiClient(config.apiUrl, config.apiKey);
2826
+ return createApiClient();
2794
2827
  }
2795
2828
  async function showAnalytics(project, options) {
2796
2829
  const client = getClient8();
@@ -2897,8 +2930,7 @@ Source Origin Breakdown`);
2897
2930
 
2898
2931
  // src/commands/evidence.ts
2899
2932
  function getClient9() {
2900
- const config = loadConfig();
2901
- return new ApiClient(config.apiUrl, config.apiKey);
2933
+ return createApiClient();
2902
2934
  }
2903
2935
  async function showEvidence(project, format) {
2904
2936
  const client = getClient9();
@@ -2933,8 +2965,7 @@ async function showEvidence(project, format) {
2933
2965
  // src/commands/export-cmd.ts
2934
2966
  import { stringify } from "yaml";
2935
2967
  async function exportProject(project, opts) {
2936
- const config = loadConfig();
2937
- const client = new ApiClient(config.apiUrl, config.apiKey);
2968
+ const client = createApiClient();
2938
2969
  const data = await client.getExport(project);
2939
2970
  if (opts.includeResults) {
2940
2971
  try {
@@ -2955,8 +2986,7 @@ async function exportProject(project, opts) {
2955
2986
 
2956
2987
  // src/commands/history.ts
2957
2988
  function getClient10() {
2958
- const config = loadConfig();
2959
- return new ApiClient(config.apiUrl, config.apiKey);
2989
+ return createApiClient();
2960
2990
  }
2961
2991
  async function showHistory(project, format) {
2962
2992
  const client = getClient10();
@@ -2995,8 +3025,7 @@ async function showHistory(project, format) {
2995
3025
 
2996
3026
  // src/commands/status.ts
2997
3027
  function getClient11() {
2998
- const config = loadConfig();
2999
- return new ApiClient(config.apiUrl, config.apiKey);
3028
+ return createApiClient();
3000
3029
  }
3001
3030
  async function showStatus(project, format) {
3002
3031
  const client = getClient11();
@@ -3108,8 +3137,7 @@ var OPERATOR_CLI_COMMANDS = [
3108
3137
 
3109
3138
  // src/commands/project.ts
3110
3139
  function getClient12() {
3111
- const config = loadConfig();
3112
- return new ApiClient(config.apiUrl, config.apiKey);
3140
+ return createApiClient();
3113
3141
  }
3114
3142
  async function createProject(name, opts) {
3115
3143
  const client = getClient12();
@@ -3445,8 +3473,7 @@ var PROJECT_CLI_COMMANDS = [
3445
3473
 
3446
3474
  // src/commands/run.ts
3447
3475
  function getClient13() {
3448
- const config = loadConfig();
3449
- return new ApiClient(config.apiUrl, config.apiKey);
3476
+ return createApiClient();
3450
3477
  }
3451
3478
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
3452
3479
  async function triggerRun(project, opts) {
@@ -3775,8 +3802,7 @@ var RUN_CLI_COMMANDS = [
3775
3802
 
3776
3803
  // src/commands/schedule.ts
3777
3804
  function getClient14() {
3778
- const config = loadConfig();
3779
- return new ApiClient(config.apiUrl, config.apiKey);
3805
+ return createApiClient();
3780
3806
  }
3781
3807
  async function setSchedule(project, opts) {
3782
3808
  const client = getClient14();
@@ -3939,8 +3965,7 @@ var SCHEDULE_CLI_COMMANDS = [
3939
3965
 
3940
3966
  // src/commands/settings.ts
3941
3967
  function getClient15() {
3942
- const config = loadConfig();
3943
- return new ApiClient(config.apiUrl, config.apiKey);
3968
+ return createApiClient();
3944
3969
  }
3945
3970
  async function setProvider(name, opts) {
3946
3971
  const client = getClient15();
@@ -4149,6 +4174,10 @@ var envSchema = z.object({
4149
4174
  GEMINI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
4150
4175
  GEMINI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
4151
4176
  GEMINI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
4177
+ // Gemini Vertex AI (alternative to API key auth)
4178
+ GEMINI_VERTEX_PROJECT: z.string().optional(),
4179
+ GEMINI_VERTEX_REGION: z.string().optional(),
4180
+ GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
4152
4181
  // OpenAI
4153
4182
  OPENAI_API_KEY: z.string().optional(),
4154
4183
  OPENAI_MODEL: z.string().optional(),
@@ -4174,6 +4203,9 @@ var bootstrapEnvSchema = z.object({
4174
4203
  CANONRY_DATABASE_PATH: z.string().optional(),
4175
4204
  GEMINI_API_KEY: z.string().optional(),
4176
4205
  GEMINI_MODEL: z.string().optional(),
4206
+ GEMINI_VERTEX_PROJECT: z.string().optional(),
4207
+ GEMINI_VERTEX_REGION: z.string().optional(),
4208
+ GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
4177
4209
  OPENAI_API_KEY: z.string().optional(),
4178
4210
  OPENAI_MODEL: z.string().optional(),
4179
4211
  ANTHROPIC_API_KEY: z.string().optional(),
@@ -4190,15 +4222,18 @@ function getBootstrapEnv(source, overrides) {
4190
4222
  const filtered = overrides ? Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)) : {};
4191
4223
  const parsed = bootstrapEnvSchema.parse({ ...source, ...filtered });
4192
4224
  const providers = {};
4193
- if (parsed.GEMINI_API_KEY) {
4225
+ if (parsed.GEMINI_API_KEY || parsed.GEMINI_VERTEX_PROJECT) {
4194
4226
  providers.gemini = {
4195
- apiKey: parsed.GEMINI_API_KEY,
4227
+ apiKey: parsed.GEMINI_API_KEY ?? "",
4196
4228
  model: parsed.GEMINI_MODEL || "gemini-3-flash",
4197
4229
  quota: providerQuotaPolicySchema.parse({
4198
4230
  maxConcurrency: 2,
4199
4231
  maxRequestsPerMinute: 10,
4200
4232
  maxRequestsPerDay: 500
4201
- })
4233
+ }),
4234
+ vertexProject: parsed.GEMINI_VERTEX_PROJECT,
4235
+ vertexRegion: parsed.GEMINI_VERTEX_REGION,
4236
+ vertexCredentials: parsed.GEMINI_VERTEX_CREDENTIALS
4202
4237
  };
4203
4238
  }
4204
4239
  if (parsed.OPENAI_API_KEY) {
package/dist/index.d.ts CHANGED
@@ -8,6 +8,12 @@ interface ProviderConfigEntry {
8
8
  baseUrl?: string;
9
9
  model?: string;
10
10
  quota?: ProviderQuotaPolicy;
11
+ /** Vertex AI GCP project ID (Gemini provider only) */
12
+ vertexProject?: string;
13
+ /** Vertex AI region, e.g. "us-central1" (Gemini provider only) */
14
+ vertexRegion?: string;
15
+ /** Path to service account JSON for Vertex AI auth (falls back to ADC) */
16
+ vertexCredentials?: string;
11
17
  }
12
18
  interface CdpConfigEntry {
13
19
  host?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-L6TL4UQI.js";
4
+ } from "./chunk-UTRZ3UB5.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.26.0",
3
+ "version": "1.27.1",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "@anthropic-ai/sdk": "^0.78.0",
38
38
  "@fastify/static": "^8.1.0",
39
+ "@google-cloud/vertexai": "^1.10.3",
39
40
  "@google/generative-ai": "^0.24.1",
40
41
  "better-sqlite3": "^12.6.2",
41
42
  "drizzle-orm": "^0.45.1",
@@ -52,17 +53,17 @@
52
53
  "@types/node-cron": "^3.0.11",
53
54
  "tsup": "^8.5.1",
54
55
  "tsx": "^4.19.0",
55
- "@ainyc/canonry-api-routes": "0.0.0",
56
- "@ainyc/canonry-contracts": "0.0.0",
57
56
  "@ainyc/canonry-config": "0.0.0",
58
- "@ainyc/canonry-db": "0.0.0",
57
+ "@ainyc/canonry-contracts": "0.0.0",
59
58
  "@ainyc/canonry-provider-claude": "0.0.0",
59
+ "@ainyc/canonry-api-routes": "0.0.0",
60
+ "@ainyc/canonry-provider-gemini": "0.0.0",
61
+ "@ainyc/canonry-db": "0.0.0",
60
62
  "@ainyc/canonry-provider-cdp": "0.0.0",
61
63
  "@ainyc/canonry-provider-local": "0.0.0",
62
64
  "@ainyc/canonry-integration-bing": "0.0.0",
63
65
  "@ainyc/canonry-integration-google": "0.0.0",
64
66
  "@ainyc/canonry-provider-openai": "0.0.0",
65
- "@ainyc/canonry-provider-gemini": "0.0.0",
66
67
  "@ainyc/canonry-provider-perplexity": "0.0.0"
67
68
  },
68
69
  "scripts": {