@ainyc/canonry 1.20.1 → 1.21.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/README.md +16 -2
- package/assets/assets/index-7DjD4Oje.css +1 -0
- package/assets/assets/index-BFA6dYNt.js +246 -0
- package/assets/index.html +2 -2
- package/bin/canonry.mjs +1 -1
- package/dist/{chunk-YXYC7NGJ.js → chunk-2JBQ7NMO.js} +1079 -555
- package/dist/cli.js +4191 -3095
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1 -1
- package/package.json +7 -6
- package/assets/assets/index-CaypGSmN.js +0 -246
- package/assets/assets/index-D-2VeJdG.css +0 -1
|
@@ -35,7 +35,7 @@ function loadConfig() {
|
|
|
35
35
|
throw new Error(
|
|
36
36
|
`Config not found at ${configPath}.
|
|
37
37
|
Run "canonry init" to set up interactively, or "canonry init --gemini-key <key>" for non-interactive setup.
|
|
38
|
-
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
38
|
+
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -72,6 +72,16 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
|
|
|
72
72
|
} catch {
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
if (parsed.basePath) {
|
|
76
|
+
const normalizedBase = "/" + parsed.basePath.replace(/^\/|\/$/g, "");
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(parsed.apiUrl);
|
|
79
|
+
if (normalizedBase !== "/" && !url.pathname.startsWith(normalizedBase)) {
|
|
80
|
+
parsed.apiUrl = url.origin + normalizedBase;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
return parsed;
|
|
76
86
|
}
|
|
77
87
|
function saveConfig(config) {
|
|
@@ -167,7 +177,7 @@ import { fileURLToPath } from "url";
|
|
|
167
177
|
import Fastify from "fastify";
|
|
168
178
|
|
|
169
179
|
// ../contracts/src/config-schema.ts
|
|
170
|
-
import { z as
|
|
180
|
+
import { z as z4 } from "zod";
|
|
171
181
|
|
|
172
182
|
// ../contracts/src/provider.ts
|
|
173
183
|
import { z } from "zod";
|
|
@@ -176,30 +186,18 @@ var providerQuotaPolicySchema = z.object({
|
|
|
176
186
|
maxRequestsPerMinute: z.number().int().positive(),
|
|
177
187
|
maxRequestsPerDay: z.number().int().positive()
|
|
178
188
|
});
|
|
179
|
-
var
|
|
180
|
-
var
|
|
181
|
-
var PROVIDER_MODE = {
|
|
182
|
-
gemini: "api",
|
|
183
|
-
openai: "api",
|
|
184
|
-
claude: "api",
|
|
185
|
-
local: "api",
|
|
186
|
-
"cdp:chatgpt": "browser"
|
|
187
|
-
};
|
|
189
|
+
var providerNameSchema = z.string().min(1);
|
|
190
|
+
var apiProviderNameSchema = z.string().min(1);
|
|
188
191
|
function isBrowserProvider(name) {
|
|
189
|
-
return
|
|
192
|
+
return name.startsWith("cdp:");
|
|
190
193
|
}
|
|
191
194
|
var CDP_TARGETS = ["cdp:chatgpt"];
|
|
192
|
-
function parseProviderName(input) {
|
|
193
|
-
const lower = input.trim().toLowerCase();
|
|
194
|
-
return PROVIDER_NAMES.includes(lower) ? lower : void 0;
|
|
195
|
-
}
|
|
196
195
|
function resolveProviderInput(input) {
|
|
197
196
|
const lower = input.trim().toLowerCase();
|
|
198
197
|
if (lower === "cdp") {
|
|
199
198
|
return [...CDP_TARGETS];
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
return parsed ? [parsed] : [];
|
|
200
|
+
return lower ? [lower] : [];
|
|
203
201
|
}
|
|
204
202
|
var locationContextSchema = z.object({
|
|
205
203
|
label: z.string().min(1),
|
|
@@ -229,118 +227,148 @@ var notificationDtoSchema = z2.object({
|
|
|
229
227
|
updatedAt: z2.string()
|
|
230
228
|
});
|
|
231
229
|
|
|
230
|
+
// ../contracts/src/project.ts
|
|
231
|
+
import { z as z3 } from "zod";
|
|
232
|
+
var configSourceSchema = z3.enum(["cli", "api", "config-file"]);
|
|
233
|
+
function findDuplicateLocationLabels(locations) {
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
236
|
+
for (const location of locations) {
|
|
237
|
+
if (seen.has(location.label)) {
|
|
238
|
+
duplicates.add(location.label);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
seen.add(location.label);
|
|
242
|
+
}
|
|
243
|
+
return [...duplicates];
|
|
244
|
+
}
|
|
245
|
+
function hasLocationLabel(locations, label) {
|
|
246
|
+
if (!label) return true;
|
|
247
|
+
return locations.some((location) => location.label === label);
|
|
248
|
+
}
|
|
249
|
+
var projectUpsertRequestSchema = z3.object({
|
|
250
|
+
displayName: z3.string().min(1),
|
|
251
|
+
canonicalDomain: z3.string().min(1),
|
|
252
|
+
ownedDomains: z3.array(z3.string().min(1)).optional(),
|
|
253
|
+
country: z3.string().length(2),
|
|
254
|
+
language: z3.string().min(2),
|
|
255
|
+
tags: z3.array(z3.string()).optional(),
|
|
256
|
+
labels: z3.record(z3.string(), z3.string()).optional(),
|
|
257
|
+
providers: z3.array(providerNameSchema).optional(),
|
|
258
|
+
locations: z3.array(locationContextSchema).optional(),
|
|
259
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
260
|
+
configSource: configSourceSchema.optional()
|
|
261
|
+
});
|
|
262
|
+
var projectDtoSchema = z3.object({
|
|
263
|
+
id: z3.string(),
|
|
264
|
+
name: z3.string(),
|
|
265
|
+
displayName: z3.string().optional(),
|
|
266
|
+
canonicalDomain: z3.string(),
|
|
267
|
+
ownedDomains: z3.array(z3.string()).default([]),
|
|
268
|
+
country: z3.string().length(2),
|
|
269
|
+
language: z3.string().min(2),
|
|
270
|
+
tags: z3.array(z3.string()).default([]),
|
|
271
|
+
labels: z3.record(z3.string(), z3.string()).default({}),
|
|
272
|
+
locations: z3.array(locationContextSchema).default([]),
|
|
273
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
274
|
+
configSource: configSourceSchema.default("cli"),
|
|
275
|
+
configRevision: z3.number().int().positive().default(1),
|
|
276
|
+
createdAt: z3.string().optional(),
|
|
277
|
+
updatedAt: z3.string().optional()
|
|
278
|
+
});
|
|
279
|
+
function normalizeProjectDomain(input) {
|
|
280
|
+
let domain = input.trim().toLowerCase();
|
|
281
|
+
try {
|
|
282
|
+
if (domain.includes("://")) {
|
|
283
|
+
domain = new URL(domain).hostname.toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
return domain.replace(/^www\./, "");
|
|
288
|
+
}
|
|
289
|
+
function effectiveDomains(project) {
|
|
290
|
+
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const result = [];
|
|
293
|
+
for (const d of all) {
|
|
294
|
+
const trimmed = d.trim();
|
|
295
|
+
if (!trimmed) continue;
|
|
296
|
+
const norm = normalizeProjectDomain(trimmed);
|
|
297
|
+
if (seen.has(norm)) continue;
|
|
298
|
+
seen.add(norm);
|
|
299
|
+
result.push(trimmed);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
232
304
|
// ../contracts/src/config-schema.ts
|
|
233
|
-
var configMetadataSchema =
|
|
234
|
-
name:
|
|
305
|
+
var configMetadataSchema = z4.object({
|
|
306
|
+
name: z4.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
|
|
235
307
|
message: "Name must be a lowercase slug (letters, numbers, hyphens)"
|
|
236
308
|
}),
|
|
237
|
-
labels:
|
|
309
|
+
labels: z4.record(z4.string(), z4.string()).optional().default({})
|
|
238
310
|
});
|
|
239
|
-
var configScheduleSchema =
|
|
240
|
-
preset:
|
|
241
|
-
cron:
|
|
242
|
-
timezone:
|
|
243
|
-
providers:
|
|
311
|
+
var configScheduleSchema = z4.object({
|
|
312
|
+
preset: z4.string().optional(),
|
|
313
|
+
cron: z4.string().optional(),
|
|
314
|
+
timezone: z4.string().optional().default("UTC"),
|
|
315
|
+
providers: z4.array(providerNameSchema).optional().default([])
|
|
244
316
|
}).refine(
|
|
245
317
|
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
246
318
|
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
247
319
|
).optional();
|
|
248
|
-
var configNotificationSchema =
|
|
249
|
-
channel:
|
|
250
|
-
url:
|
|
251
|
-
events:
|
|
320
|
+
var configNotificationSchema = z4.object({
|
|
321
|
+
channel: z4.literal("webhook"),
|
|
322
|
+
url: z4.string().url(),
|
|
323
|
+
events: z4.array(notificationEventSchema).min(1)
|
|
252
324
|
});
|
|
253
|
-
var configGoogleSchema =
|
|
254
|
-
gsc:
|
|
255
|
-
propertyUrl:
|
|
325
|
+
var configGoogleSchema = z4.object({
|
|
326
|
+
gsc: z4.object({
|
|
327
|
+
propertyUrl: z4.string()
|
|
256
328
|
}).optional(),
|
|
257
|
-
syncSchedule:
|
|
258
|
-
preset:
|
|
259
|
-
cron:
|
|
329
|
+
syncSchedule: z4.object({
|
|
330
|
+
preset: z4.string().optional(),
|
|
331
|
+
cron: z4.string().optional()
|
|
260
332
|
}).optional()
|
|
261
333
|
}).optional();
|
|
262
|
-
var configSpecSchema =
|
|
263
|
-
displayName:
|
|
264
|
-
canonicalDomain:
|
|
265
|
-
ownedDomains:
|
|
266
|
-
country:
|
|
267
|
-
language:
|
|
268
|
-
keywords:
|
|
269
|
-
competitors:
|
|
270
|
-
providers:
|
|
271
|
-
locations:
|
|
272
|
-
defaultLocation:
|
|
334
|
+
var configSpecSchema = z4.object({
|
|
335
|
+
displayName: z4.string().min(1),
|
|
336
|
+
canonicalDomain: z4.string().min(1),
|
|
337
|
+
ownedDomains: z4.array(z4.string().min(1)).optional().default([]),
|
|
338
|
+
country: z4.string().length(2),
|
|
339
|
+
language: z4.string().min(2),
|
|
340
|
+
keywords: z4.array(z4.string().min(1)).optional().default([]),
|
|
341
|
+
competitors: z4.array(z4.string().min(1)).optional().default([]),
|
|
342
|
+
providers: z4.array(providerNameSchema).optional().default([]),
|
|
343
|
+
locations: z4.array(locationContextSchema).optional().default([]),
|
|
344
|
+
defaultLocation: z4.string().optional(),
|
|
273
345
|
schedule: configScheduleSchema,
|
|
274
|
-
notifications:
|
|
346
|
+
notifications: z4.array(configNotificationSchema).optional().default([]),
|
|
275
347
|
google: configGoogleSchema
|
|
348
|
+
}).superRefine((spec, ctx) => {
|
|
349
|
+
const duplicateLabels = findDuplicateLocationLabels(spec.locations);
|
|
350
|
+
if (duplicateLabels.length > 0) {
|
|
351
|
+
ctx.addIssue({
|
|
352
|
+
code: "custom",
|
|
353
|
+
message: `Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`,
|
|
354
|
+
path: ["locations"]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (!hasLocationLabel(spec.locations, spec.defaultLocation)) {
|
|
358
|
+
ctx.addIssue({
|
|
359
|
+
code: "custom",
|
|
360
|
+
message: `defaultLocation "${spec.defaultLocation}" must match a configured location label`,
|
|
361
|
+
path: ["defaultLocation"]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
276
364
|
});
|
|
277
|
-
var projectConfigSchema =
|
|
278
|
-
apiVersion:
|
|
279
|
-
kind:
|
|
365
|
+
var projectConfigSchema = z4.object({
|
|
366
|
+
apiVersion: z4.literal("canonry/v1"),
|
|
367
|
+
kind: z4.literal("Project"),
|
|
280
368
|
metadata: configMetadataSchema,
|
|
281
369
|
spec: configSpecSchema
|
|
282
370
|
});
|
|
283
371
|
|
|
284
|
-
// ../contracts/src/models.ts
|
|
285
|
-
var MODEL_REGISTRY = {
|
|
286
|
-
gemini: {
|
|
287
|
-
defaultModel: "gemini-3-flash",
|
|
288
|
-
validationPattern: /^gemini-/,
|
|
289
|
-
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
290
|
-
knownModels: [
|
|
291
|
-
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
292
|
-
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
293
|
-
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
294
|
-
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
295
|
-
]
|
|
296
|
-
},
|
|
297
|
-
openai: {
|
|
298
|
-
defaultModel: "gpt-5.4",
|
|
299
|
-
validationPattern: /^(gpt-|o\d)/,
|
|
300
|
-
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
301
|
-
knownModels: [
|
|
302
|
-
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
303
|
-
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
304
|
-
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
305
|
-
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
306
|
-
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
307
|
-
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
308
|
-
]
|
|
309
|
-
},
|
|
310
|
-
claude: {
|
|
311
|
-
defaultModel: "claude-sonnet-4-6",
|
|
312
|
-
validationPattern: /^claude-/,
|
|
313
|
-
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
314
|
-
knownModels: [
|
|
315
|
-
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
316
|
-
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
317
|
-
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
local: {
|
|
321
|
-
defaultModel: "llama3",
|
|
322
|
-
validationPattern: /./,
|
|
323
|
-
validationHint: "any model name accepted",
|
|
324
|
-
knownModels: [
|
|
325
|
-
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
326
|
-
]
|
|
327
|
-
},
|
|
328
|
-
"cdp:chatgpt": {
|
|
329
|
-
defaultModel: "chatgpt-web",
|
|
330
|
-
validationPattern: /./,
|
|
331
|
-
validationHint: "model is detected from the ChatGPT web UI",
|
|
332
|
-
knownModels: [
|
|
333
|
-
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
function getDefaultModel(provider) {
|
|
338
|
-
return MODEL_REGISTRY[provider].defaultModel;
|
|
339
|
-
}
|
|
340
|
-
function isValidModelName(provider, model) {
|
|
341
|
-
return MODEL_REGISTRY[provider].validationPattern.test(model);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
372
|
// ../contracts/src/errors.ts
|
|
345
373
|
var AppError = class extends Error {
|
|
346
374
|
code;
|
|
@@ -384,177 +412,135 @@ function runNotCancellable(runId, status) {
|
|
|
384
412
|
function unsupportedKind(kind) {
|
|
385
413
|
return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
|
|
386
414
|
}
|
|
415
|
+
function notImplemented(message) {
|
|
416
|
+
return new AppError("NOT_IMPLEMENTED", message, 501);
|
|
417
|
+
}
|
|
387
418
|
|
|
388
419
|
// ../contracts/src/google.ts
|
|
389
|
-
import { z as z4 } from "zod";
|
|
390
|
-
var googleConnectionTypeSchema = z4.enum(["gsc", "ga4"]);
|
|
391
|
-
var googleConnectionDtoSchema = z4.object({
|
|
392
|
-
id: z4.string(),
|
|
393
|
-
domain: z4.string(),
|
|
394
|
-
connectionType: googleConnectionTypeSchema,
|
|
395
|
-
propertyId: z4.string().nullable().optional(),
|
|
396
|
-
sitemapUrl: z4.string().nullable().optional(),
|
|
397
|
-
scopes: z4.array(z4.string()).default([]),
|
|
398
|
-
createdAt: z4.string(),
|
|
399
|
-
updatedAt: z4.string()
|
|
400
|
-
});
|
|
401
|
-
var gscSearchDataDtoSchema = z4.object({
|
|
402
|
-
date: z4.string(),
|
|
403
|
-
query: z4.string(),
|
|
404
|
-
page: z4.string(),
|
|
405
|
-
country: z4.string().nullable().optional(),
|
|
406
|
-
device: z4.string().nullable().optional(),
|
|
407
|
-
clicks: z4.number(),
|
|
408
|
-
impressions: z4.number(),
|
|
409
|
-
ctr: z4.number(),
|
|
410
|
-
position: z4.number()
|
|
411
|
-
});
|
|
412
|
-
var gscUrlInspectionDtoSchema = z4.object({
|
|
413
|
-
id: z4.string(),
|
|
414
|
-
url: z4.string(),
|
|
415
|
-
indexingState: z4.string().nullable().optional(),
|
|
416
|
-
verdict: z4.string().nullable().optional(),
|
|
417
|
-
coverageState: z4.string().nullable().optional(),
|
|
418
|
-
pageFetchState: z4.string().nullable().optional(),
|
|
419
|
-
robotsTxtState: z4.string().nullable().optional(),
|
|
420
|
-
crawlTime: z4.string().nullable().optional(),
|
|
421
|
-
lastCrawlResult: z4.string().nullable().optional(),
|
|
422
|
-
isMobileFriendly: z4.boolean().nullable().optional(),
|
|
423
|
-
richResults: z4.array(z4.string()).default([]),
|
|
424
|
-
inspectedAt: z4.string()
|
|
425
|
-
});
|
|
426
|
-
var indexTransitionSchema = z4.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
427
|
-
var gscDeindexedRowSchema = z4.object({
|
|
428
|
-
url: z4.string(),
|
|
429
|
-
previousState: z4.string().nullable(),
|
|
430
|
-
currentState: z4.string().nullable(),
|
|
431
|
-
transitionDate: z4.string()
|
|
432
|
-
});
|
|
433
|
-
var gscReasonGroupSchema = z4.object({
|
|
434
|
-
reason: z4.string(),
|
|
435
|
-
count: z4.number(),
|
|
436
|
-
urls: z4.array(gscUrlInspectionDtoSchema).default([])
|
|
437
|
-
});
|
|
438
|
-
var gscCoverageSummaryDtoSchema = z4.object({
|
|
439
|
-
summary: z4.object({
|
|
440
|
-
total: z4.number(),
|
|
441
|
-
indexed: z4.number(),
|
|
442
|
-
notIndexed: z4.number(),
|
|
443
|
-
deindexed: z4.number(),
|
|
444
|
-
percentage: z4.number()
|
|
445
|
-
}),
|
|
446
|
-
lastInspectedAt: z4.string().nullable(),
|
|
447
|
-
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
448
|
-
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
449
|
-
deindexed: z4.array(gscDeindexedRowSchema).default([]),
|
|
450
|
-
reasonGroups: z4.array(gscReasonGroupSchema).default([])
|
|
451
|
-
});
|
|
452
|
-
var indexingNotificationDtoSchema = z4.object({
|
|
453
|
-
url: z4.string(),
|
|
454
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
455
|
-
notifiedAt: z4.string()
|
|
456
|
-
});
|
|
457
|
-
var indexingRequestResultDtoSchema = z4.object({
|
|
458
|
-
url: z4.string(),
|
|
459
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
460
|
-
notifiedAt: z4.string(),
|
|
461
|
-
status: z4.enum(["success", "error"]),
|
|
462
|
-
error: z4.string().optional()
|
|
463
|
-
});
|
|
464
|
-
var gscCoverageSnapshotDtoSchema = z4.object({
|
|
465
|
-
date: z4.string(),
|
|
466
|
-
indexed: z4.number(),
|
|
467
|
-
notIndexed: z4.number(),
|
|
468
|
-
reasonBreakdown: z4.record(z4.string(), z4.number()).default({})
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// ../contracts/src/bing.ts
|
|
472
420
|
import { z as z5 } from "zod";
|
|
473
|
-
var
|
|
421
|
+
var googleConnectionTypeSchema = z5.enum(["gsc", "ga4"]);
|
|
422
|
+
var googleConnectionDtoSchema = z5.object({
|
|
474
423
|
id: z5.string(),
|
|
475
424
|
domain: z5.string(),
|
|
476
|
-
|
|
425
|
+
connectionType: googleConnectionTypeSchema,
|
|
426
|
+
propertyId: z5.string().nullable().optional(),
|
|
427
|
+
sitemapUrl: z5.string().nullable().optional(),
|
|
428
|
+
scopes: z5.array(z5.string()).default([]),
|
|
477
429
|
createdAt: z5.string(),
|
|
478
430
|
updatedAt: z5.string()
|
|
479
431
|
});
|
|
480
|
-
var
|
|
432
|
+
var gscSearchDataDtoSchema = z5.object({
|
|
433
|
+
date: z5.string(),
|
|
434
|
+
query: z5.string(),
|
|
435
|
+
page: z5.string(),
|
|
436
|
+
country: z5.string().nullable().optional(),
|
|
437
|
+
device: z5.string().nullable().optional(),
|
|
438
|
+
clicks: z5.number(),
|
|
439
|
+
impressions: z5.number(),
|
|
440
|
+
ctr: z5.number(),
|
|
441
|
+
position: z5.number()
|
|
442
|
+
});
|
|
443
|
+
var gscUrlInspectionDtoSchema = z5.object({
|
|
481
444
|
id: z5.string(),
|
|
482
445
|
url: z5.string(),
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
446
|
+
indexingState: z5.string().nullable().optional(),
|
|
447
|
+
verdict: z5.string().nullable().optional(),
|
|
448
|
+
coverageState: z5.string().nullable().optional(),
|
|
449
|
+
pageFetchState: z5.string().nullable().optional(),
|
|
450
|
+
robotsTxtState: z5.string().nullable().optional(),
|
|
451
|
+
crawlTime: z5.string().nullable().optional(),
|
|
452
|
+
lastCrawlResult: z5.string().nullable().optional(),
|
|
453
|
+
isMobileFriendly: z5.boolean().nullable().optional(),
|
|
454
|
+
richResults: z5.array(z5.string()).default([]),
|
|
487
455
|
inspectedAt: z5.string()
|
|
488
456
|
});
|
|
489
|
-
var
|
|
457
|
+
var indexTransitionSchema = z5.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
458
|
+
var gscDeindexedRowSchema = z5.object({
|
|
459
|
+
url: z5.string(),
|
|
460
|
+
previousState: z5.string().nullable(),
|
|
461
|
+
currentState: z5.string().nullable(),
|
|
462
|
+
transitionDate: z5.string()
|
|
463
|
+
});
|
|
464
|
+
var gscReasonGroupSchema = z5.object({
|
|
465
|
+
reason: z5.string(),
|
|
466
|
+
count: z5.number(),
|
|
467
|
+
urls: z5.array(gscUrlInspectionDtoSchema).default([])
|
|
468
|
+
});
|
|
469
|
+
var gscCoverageSummaryDtoSchema = z5.object({
|
|
490
470
|
summary: z5.object({
|
|
491
471
|
total: z5.number(),
|
|
492
472
|
indexed: z5.number(),
|
|
493
473
|
notIndexed: z5.number(),
|
|
474
|
+
deindexed: z5.number(),
|
|
494
475
|
percentage: z5.number()
|
|
495
476
|
}),
|
|
496
477
|
lastInspectedAt: z5.string().nullable(),
|
|
497
|
-
indexed: z5.array(
|
|
498
|
-
notIndexed: z5.array(
|
|
478
|
+
indexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
479
|
+
notIndexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
480
|
+
deindexed: z5.array(gscDeindexedRowSchema).default([]),
|
|
481
|
+
reasonGroups: z5.array(gscReasonGroupSchema).default([])
|
|
499
482
|
});
|
|
500
|
-
var
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
ctr: z5.number(),
|
|
505
|
-
averagePosition: z5.number()
|
|
483
|
+
var indexingNotificationDtoSchema = z5.object({
|
|
484
|
+
url: z5.string(),
|
|
485
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
486
|
+
notifiedAt: z5.string()
|
|
506
487
|
});
|
|
507
|
-
var
|
|
488
|
+
var indexingRequestResultDtoSchema = z5.object({
|
|
508
489
|
url: z5.string(),
|
|
490
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
491
|
+
notifiedAt: z5.string(),
|
|
509
492
|
status: z5.enum(["success", "error"]),
|
|
510
|
-
submittedAt: z5.string(),
|
|
511
493
|
error: z5.string().optional()
|
|
512
494
|
});
|
|
495
|
+
var gscCoverageSnapshotDtoSchema = z5.object({
|
|
496
|
+
date: z5.string(),
|
|
497
|
+
indexed: z5.number(),
|
|
498
|
+
notIndexed: z5.number(),
|
|
499
|
+
reasonBreakdown: z5.record(z5.string(), z5.number()).default({})
|
|
500
|
+
});
|
|
513
501
|
|
|
514
|
-
// ../contracts/src/
|
|
502
|
+
// ../contracts/src/bing.ts
|
|
515
503
|
import { z as z6 } from "zod";
|
|
516
|
-
var
|
|
517
|
-
var projectDtoSchema = z6.object({
|
|
504
|
+
var bingConnectionDtoSchema = z6.object({
|
|
518
505
|
id: z6.string(),
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
506
|
+
domain: z6.string(),
|
|
507
|
+
siteUrl: z6.string().nullable().optional(),
|
|
508
|
+
createdAt: z6.string(),
|
|
509
|
+
updatedAt: z6.string()
|
|
510
|
+
});
|
|
511
|
+
var bingUrlInspectionDtoSchema = z6.object({
|
|
512
|
+
id: z6.string(),
|
|
513
|
+
url: z6.string(),
|
|
514
|
+
httpCode: z6.number().nullable().optional(),
|
|
515
|
+
inIndex: z6.boolean().nullable().optional(),
|
|
516
|
+
lastCrawledDate: z6.string().nullable().optional(),
|
|
517
|
+
inIndexDate: z6.string().nullable().optional(),
|
|
518
|
+
inspectedAt: z6.string()
|
|
519
|
+
});
|
|
520
|
+
var bingCoverageSummaryDtoSchema = z6.object({
|
|
521
|
+
summary: z6.object({
|
|
522
|
+
total: z6.number(),
|
|
523
|
+
indexed: z6.number(),
|
|
524
|
+
notIndexed: z6.number(),
|
|
525
|
+
percentage: z6.number()
|
|
526
|
+
}),
|
|
527
|
+
lastInspectedAt: z6.string().nullable(),
|
|
528
|
+
indexed: z6.array(bingUrlInspectionDtoSchema).default([]),
|
|
529
|
+
notIndexed: z6.array(bingUrlInspectionDtoSchema).default([])
|
|
530
|
+
});
|
|
531
|
+
var bingKeywordStatsDtoSchema = z6.object({
|
|
532
|
+
query: z6.string(),
|
|
533
|
+
impressions: z6.number(),
|
|
534
|
+
clicks: z6.number(),
|
|
535
|
+
ctr: z6.number(),
|
|
536
|
+
averagePosition: z6.number()
|
|
537
|
+
});
|
|
538
|
+
var bingSubmitResultDtoSchema = z6.object({
|
|
539
|
+
url: z6.string(),
|
|
540
|
+
status: z6.enum(["success", "error"]),
|
|
541
|
+
submittedAt: z6.string(),
|
|
542
|
+
error: z6.string().optional()
|
|
533
543
|
});
|
|
534
|
-
function normalizeProjectDomain(input) {
|
|
535
|
-
let domain = input.trim().toLowerCase();
|
|
536
|
-
try {
|
|
537
|
-
if (domain.includes("://")) {
|
|
538
|
-
domain = new URL(domain).hostname.toLowerCase();
|
|
539
|
-
}
|
|
540
|
-
} catch {
|
|
541
|
-
}
|
|
542
|
-
return domain.replace(/^www\./, "");
|
|
543
|
-
}
|
|
544
|
-
function effectiveDomains(project) {
|
|
545
|
-
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
546
|
-
const seen = /* @__PURE__ */ new Set();
|
|
547
|
-
const result = [];
|
|
548
|
-
for (const d of all) {
|
|
549
|
-
const trimmed = d.trim();
|
|
550
|
-
if (!trimmed) continue;
|
|
551
|
-
const norm = normalizeProjectDomain(trimmed);
|
|
552
|
-
if (seen.has(norm)) continue;
|
|
553
|
-
seen.add(norm);
|
|
554
|
-
result.push(trimmed);
|
|
555
|
-
}
|
|
556
|
-
return result;
|
|
557
|
-
}
|
|
558
544
|
|
|
559
545
|
// ../contracts/src/run.ts
|
|
560
546
|
import { z as z7 } from "zod";
|
|
@@ -622,6 +608,16 @@ var scheduleDtoSchema = z8.object({
|
|
|
622
608
|
createdAt: z8.string(),
|
|
623
609
|
updatedAt: z8.string()
|
|
624
610
|
});
|
|
611
|
+
var scheduleUpsertRequestSchema = z8.object({
|
|
612
|
+
preset: z8.string().optional(),
|
|
613
|
+
cron: z8.string().optional(),
|
|
614
|
+
timezone: z8.string().optional().default("UTC"),
|
|
615
|
+
enabled: z8.boolean().optional().default(true),
|
|
616
|
+
providers: z8.array(providerNameSchema).optional().default([])
|
|
617
|
+
}).refine(
|
|
618
|
+
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
619
|
+
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
620
|
+
);
|
|
625
621
|
|
|
626
622
|
// ../contracts/src/source-categories.ts
|
|
627
623
|
var SOURCE_CATEGORY_RULES = [
|
|
@@ -1214,7 +1210,44 @@ var MIGRATIONS = [
|
|
|
1214
1210
|
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
1215
1211
|
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`,
|
|
1216
1212
|
// v11: CDP browser provider — screenshot path for captured evidence
|
|
1217
|
-
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT
|
|
1213
|
+
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT`,
|
|
1214
|
+
// v12: Bing Webmaster Tools — bing_connections table
|
|
1215
|
+
`CREATE TABLE IF NOT EXISTS bing_connections (
|
|
1216
|
+
id TEXT PRIMARY KEY,
|
|
1217
|
+
domain TEXT NOT NULL,
|
|
1218
|
+
site_url TEXT,
|
|
1219
|
+
created_at TEXT NOT NULL,
|
|
1220
|
+
updated_at TEXT NOT NULL
|
|
1221
|
+
)`,
|
|
1222
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_conn_domain ON bing_connections(domain)`,
|
|
1223
|
+
// v12: Bing Webmaster Tools — bing_url_inspections table
|
|
1224
|
+
`CREATE TABLE IF NOT EXISTS bing_url_inspections (
|
|
1225
|
+
id TEXT PRIMARY KEY,
|
|
1226
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1227
|
+
url TEXT NOT NULL,
|
|
1228
|
+
http_code INTEGER,
|
|
1229
|
+
in_index INTEGER,
|
|
1230
|
+
last_crawled_date TEXT,
|
|
1231
|
+
in_index_date TEXT,
|
|
1232
|
+
inspected_at TEXT NOT NULL,
|
|
1233
|
+
created_at TEXT NOT NULL
|
|
1234
|
+
)`,
|
|
1235
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_project_url ON bing_url_inspections(project_id, url)`,
|
|
1236
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_url_time ON bing_url_inspections(url, inspected_at)`,
|
|
1237
|
+
// v12: Bing Webmaster Tools — bing_keyword_stats table
|
|
1238
|
+
`CREATE TABLE IF NOT EXISTS bing_keyword_stats (
|
|
1239
|
+
id TEXT PRIMARY KEY,
|
|
1240
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1241
|
+
query TEXT NOT NULL,
|
|
1242
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
1243
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
1244
|
+
ctr TEXT NOT NULL DEFAULT '0',
|
|
1245
|
+
average_position TEXT NOT NULL DEFAULT '0',
|
|
1246
|
+
synced_at TEXT NOT NULL,
|
|
1247
|
+
created_at TEXT NOT NULL
|
|
1248
|
+
)`,
|
|
1249
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
|
|
1250
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`
|
|
1218
1251
|
];
|
|
1219
1252
|
function migrate(db) {
|
|
1220
1253
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -1297,17 +1330,46 @@ function writeAuditLog(db, entry) {
|
|
|
1297
1330
|
async function projectRoutes(app, opts) {
|
|
1298
1331
|
app.put("/projects/:name", async (request, reply) => {
|
|
1299
1332
|
const { name } = request.params;
|
|
1300
|
-
const
|
|
1301
|
-
if (!
|
|
1302
|
-
const err = validationError("
|
|
1333
|
+
const parsedBody = projectUpsertRequestSchema.safeParse(request.body);
|
|
1334
|
+
if (!parsedBody.success) {
|
|
1335
|
+
const err = validationError("Invalid project payload", {
|
|
1336
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
1337
|
+
path: issue.path.join("."),
|
|
1338
|
+
message: issue.message
|
|
1339
|
+
}))
|
|
1340
|
+
});
|
|
1303
1341
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1304
1342
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1343
|
+
const body = parsedBody.data;
|
|
1344
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1345
|
+
if (validNames.length && body.providers?.length) {
|
|
1346
|
+
const invalid = body.providers.filter((p) => !validNames.includes(p));
|
|
1347
|
+
if (invalid.length) {
|
|
1348
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1349
|
+
invalidProviders: invalid,
|
|
1350
|
+
validProviders: validNames
|
|
1351
|
+
});
|
|
1352
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1353
|
+
}
|
|
1308
1354
|
}
|
|
1309
1355
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1310
1356
|
const existing = app.db.select().from(projects).where(eq3(projects.name, name)).get();
|
|
1357
|
+
const existingLocations = existing ? JSON.parse(existing.locations || "[]") : [];
|
|
1358
|
+
const nextLocations = body.locations ?? existingLocations;
|
|
1359
|
+
const duplicateLabels = findDuplicateLocationLabels(nextLocations);
|
|
1360
|
+
if (duplicateLabels.length > 0) {
|
|
1361
|
+
const err = validationError(`Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`, {
|
|
1362
|
+
duplicateLabels
|
|
1363
|
+
});
|
|
1364
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1365
|
+
}
|
|
1366
|
+
const nextDefaultLocation = body.defaultLocation !== void 0 ? body.defaultLocation ?? null : existing?.defaultLocation ?? null;
|
|
1367
|
+
if (!hasLocationLabel(nextLocations, nextDefaultLocation)) {
|
|
1368
|
+
const err = validationError(`defaultLocation "${nextDefaultLocation}" must match a configured location label`, {
|
|
1369
|
+
defaultLocation: nextDefaultLocation
|
|
1370
|
+
});
|
|
1371
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1372
|
+
}
|
|
1311
1373
|
if (existing) {
|
|
1312
1374
|
app.db.update(projects).set({
|
|
1313
1375
|
displayName: body.displayName,
|
|
@@ -1318,8 +1380,8 @@ async function projectRoutes(app, opts) {
|
|
|
1318
1380
|
tags: JSON.stringify(body.tags ?? []),
|
|
1319
1381
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1320
1382
|
providers: JSON.stringify(body.providers ?? []),
|
|
1321
|
-
locations: JSON.stringify(
|
|
1322
|
-
defaultLocation:
|
|
1383
|
+
locations: JSON.stringify(nextLocations),
|
|
1384
|
+
defaultLocation: nextDefaultLocation,
|
|
1323
1385
|
configSource: body.configSource ?? "api",
|
|
1324
1386
|
configRevision: existing.configRevision + 1,
|
|
1325
1387
|
updatedAt: now
|
|
@@ -1346,8 +1408,8 @@ async function projectRoutes(app, opts) {
|
|
|
1346
1408
|
tags: JSON.stringify(body.tags ?? []),
|
|
1347
1409
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1348
1410
|
providers: JSON.stringify(body.providers ?? []),
|
|
1349
|
-
locations: JSON.stringify(
|
|
1350
|
-
defaultLocation:
|
|
1411
|
+
locations: JSON.stringify(nextLocations),
|
|
1412
|
+
defaultLocation: nextDefaultLocation,
|
|
1351
1413
|
configSource: body.configSource ?? "api",
|
|
1352
1414
|
configRevision: 1,
|
|
1353
1415
|
createdAt: now,
|
|
@@ -1712,9 +1774,13 @@ async function keywordRoutes(app, opts) {
|
|
|
1712
1774
|
const err = validationError('Body must contain a "provider" string');
|
|
1713
1775
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1714
1776
|
}
|
|
1715
|
-
const provider =
|
|
1716
|
-
|
|
1717
|
-
|
|
1777
|
+
const provider = body.provider.trim().toLowerCase();
|
|
1778
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1779
|
+
if (validNames.length && !validNames.includes(provider)) {
|
|
1780
|
+
const err = validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
|
|
1781
|
+
provider: body.provider,
|
|
1782
|
+
validProviders: validNames
|
|
1783
|
+
});
|
|
1718
1784
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1719
1785
|
}
|
|
1720
1786
|
if (body.count !== void 0 && (typeof body.count !== "number" || !Number.isFinite(body.count) || !Number.isInteger(body.count))) {
|
|
@@ -1723,7 +1789,8 @@ async function keywordRoutes(app, opts) {
|
|
|
1723
1789
|
}
|
|
1724
1790
|
const count = Math.min(Math.max(body.count ?? 5, 1), 20);
|
|
1725
1791
|
if (!opts.onGenerateKeywords) {
|
|
1726
|
-
|
|
1792
|
+
const err = notImplemented("Key phrase generation is not supported in this deployment");
|
|
1793
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1727
1794
|
}
|
|
1728
1795
|
const existingRows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1729
1796
|
const existingKeywords = existingRows.map((r) => r.keyword);
|
|
@@ -1739,7 +1806,10 @@ async function keywordRoutes(app, opts) {
|
|
|
1739
1806
|
} catch (err) {
|
|
1740
1807
|
request.log.error({ err }, "Key phrase generation failed");
|
|
1741
1808
|
return reply.status(500).send({
|
|
1742
|
-
error:
|
|
1809
|
+
error: {
|
|
1810
|
+
code: "INTERNAL_ERROR",
|
|
1811
|
+
message: err instanceof Error ? err.message : "Failed to generate key phrases"
|
|
1812
|
+
}
|
|
1743
1813
|
});
|
|
1744
1814
|
}
|
|
1745
1815
|
});
|
|
@@ -1813,7 +1883,7 @@ function resolveProjectSafe2(app, name, reply) {
|
|
|
1813
1883
|
|
|
1814
1884
|
// ../api-routes/src/runs.ts
|
|
1815
1885
|
import crypto8 from "crypto";
|
|
1816
|
-
import { eq as eq7, asc } from "drizzle-orm";
|
|
1886
|
+
import { eq as eq7, asc, desc } from "drizzle-orm";
|
|
1817
1887
|
|
|
1818
1888
|
// ../api-routes/src/run-queue.ts
|
|
1819
1889
|
import crypto7 from "crypto";
|
|
@@ -1860,12 +1930,19 @@ async function runRoutes(app, opts) {
|
|
|
1860
1930
|
const trigger = request.body?.trigger ?? "manual";
|
|
1861
1931
|
const rawProviders = request.body?.providers;
|
|
1862
1932
|
if (rawProviders?.length) {
|
|
1863
|
-
const
|
|
1864
|
-
const
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
1933
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
1934
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1935
|
+
if (validNames.length) {
|
|
1936
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
1937
|
+
if (invalid.length) {
|
|
1938
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1939
|
+
invalidProviders: invalid,
|
|
1940
|
+
validProviders: validNames
|
|
1941
|
+
});
|
|
1942
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1943
|
+
}
|
|
1867
1944
|
}
|
|
1868
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
1945
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1869
1946
|
}
|
|
1870
1947
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1871
1948
|
let resolvedLocation;
|
|
@@ -1944,7 +2021,9 @@ async function runRoutes(app, opts) {
|
|
|
1944
2021
|
app.get("/projects/:name/runs", async (request, reply) => {
|
|
1945
2022
|
const project = resolveProjectSafe3(app, request.params.name, reply);
|
|
1946
2023
|
if (!project) return;
|
|
1947
|
-
const
|
|
2024
|
+
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
2025
|
+
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
2026
|
+
const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
1948
2027
|
return reply.send(rows.map(formatRun));
|
|
1949
2028
|
});
|
|
1950
2029
|
app.get("/runs", async (_request, reply) => {
|
|
@@ -1963,12 +2042,19 @@ async function runRoutes(app, opts) {
|
|
|
1963
2042
|
}
|
|
1964
2043
|
const rawProviders = request.body?.providers;
|
|
1965
2044
|
if (rawProviders?.length) {
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
1968
|
-
if (
|
|
1969
|
-
|
|
2045
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
2046
|
+
const validNames = opts.validProviderNames ?? [];
|
|
2047
|
+
if (validNames.length) {
|
|
2048
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
2049
|
+
if (invalid.length) {
|
|
2050
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2051
|
+
invalidProviders: invalid,
|
|
2052
|
+
validProviders: validNames
|
|
2053
|
+
});
|
|
2054
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2055
|
+
}
|
|
1970
2056
|
}
|
|
1971
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
2057
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1972
2058
|
}
|
|
1973
2059
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1974
2060
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2371,6 +2457,23 @@ async function applyRoutes(app, opts) {
|
|
|
2371
2457
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
2372
2458
|
}
|
|
2373
2459
|
const config = parsed.data;
|
|
2460
|
+
const validNames = opts?.validProviderNames ?? [];
|
|
2461
|
+
if (validNames.length) {
|
|
2462
|
+
const allProviders = [
|
|
2463
|
+
...config.spec.providers ?? [],
|
|
2464
|
+
...config.spec.schedule?.providers ?? []
|
|
2465
|
+
];
|
|
2466
|
+
if (allProviders.length) {
|
|
2467
|
+
const invalid = allProviders.filter((p) => !validNames.includes(p));
|
|
2468
|
+
if (invalid.length) {
|
|
2469
|
+
const err = validationError(`Invalid provider(s): ${[...new Set(invalid)].join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2470
|
+
invalidProviders: [...new Set(invalid)],
|
|
2471
|
+
validProviders: validNames
|
|
2472
|
+
});
|
|
2473
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2374
2477
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2375
2478
|
const name = config.metadata.name;
|
|
2376
2479
|
const existing = app.db.select().from(projects).where(eq8(projects.name, name)).get();
|
|
@@ -2578,16 +2681,16 @@ async function applyRoutes(app, opts) {
|
|
|
2578
2681
|
}
|
|
2579
2682
|
|
|
2580
2683
|
// ../api-routes/src/history.ts
|
|
2581
|
-
import { eq as eq9, desc, inArray } from "drizzle-orm";
|
|
2684
|
+
import { eq as eq9, desc as desc2, inArray } from "drizzle-orm";
|
|
2582
2685
|
async function historyRoutes(app) {
|
|
2583
2686
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
2584
2687
|
const project = resolveProjectSafe4(app, request.params.name, reply);
|
|
2585
2688
|
if (!project) return;
|
|
2586
|
-
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(
|
|
2689
|
+
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
2587
2690
|
return reply.send(rows.map(formatAuditEntry));
|
|
2588
2691
|
});
|
|
2589
2692
|
app.get("/history", async (_request, reply) => {
|
|
2590
|
-
const rows = app.db.select().from(auditLog).orderBy(
|
|
2693
|
+
const rows = app.db.select().from(auditLog).orderBy(desc2(auditLog.createdAt)).all();
|
|
2591
2694
|
return reply.send(rows.map(formatAuditEntry));
|
|
2592
2695
|
});
|
|
2593
2696
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -2612,7 +2715,7 @@ async function historyRoutes(app) {
|
|
|
2612
2715
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
2613
2716
|
location: querySnapshots.location,
|
|
2614
2717
|
createdAt: querySnapshots.createdAt
|
|
2615
|
-
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(
|
|
2718
|
+
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
|
|
2616
2719
|
const locationFilter = request.query.location;
|
|
2617
2720
|
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
2618
2721
|
const total = filtered.length;
|
|
@@ -2793,7 +2896,7 @@ function resolveProjectSafe4(app, name, reply) {
|
|
|
2793
2896
|
}
|
|
2794
2897
|
|
|
2795
2898
|
// ../api-routes/src/analytics.ts
|
|
2796
|
-
import { eq as eq10, desc as
|
|
2899
|
+
import { eq as eq10, desc as desc3, inArray as inArray2 } from "drizzle-orm";
|
|
2797
2900
|
async function analyticsRoutes(app) {
|
|
2798
2901
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
2799
2902
|
const project = resolveProjectSafe5(app, request.params.name, reply);
|
|
@@ -2837,7 +2940,7 @@ async function analyticsRoutes(app) {
|
|
|
2837
2940
|
if (!project) return;
|
|
2838
2941
|
const window = parseWindow(request.query.window);
|
|
2839
2942
|
const cutoff = windowCutoff(window);
|
|
2840
|
-
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
2943
|
+
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
|
|
2841
2944
|
if (!latestRun) {
|
|
2842
2945
|
return reply.send({ cited: [], gap: [], uncited: [], runId: "", window });
|
|
2843
2946
|
}
|
|
@@ -2919,7 +3022,7 @@ async function analyticsRoutes(app) {
|
|
|
2919
3022
|
if (!project) return;
|
|
2920
3023
|
const window = parseWindow(request.query.window);
|
|
2921
3024
|
const cutoff = windowCutoff(window);
|
|
2922
|
-
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
3025
|
+
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2923
3026
|
if (windowRuns.length === 0) {
|
|
2924
3027
|
return reply.send({ overall: [], byKeyword: {}, runId: "", window });
|
|
2925
3028
|
}
|
|
@@ -3035,14 +3138,16 @@ function computeBuckets(snapshots, projectRuns, bucketDays) {
|
|
|
3035
3138
|
const startISO = start.toISOString();
|
|
3036
3139
|
const endISO = end.toISOString();
|
|
3037
3140
|
const inBucket = snapshots.filter((s) => s.createdAt >= startISO && s.createdAt < endISO);
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3141
|
+
if (inBucket.length > 0) {
|
|
3142
|
+
const metric = computeProviderMetric(inBucket);
|
|
3143
|
+
buckets.push({
|
|
3144
|
+
startDate: startISO,
|
|
3145
|
+
endDate: endISO,
|
|
3146
|
+
citationRate: metric.citationRate,
|
|
3147
|
+
cited: metric.cited,
|
|
3148
|
+
total: metric.total
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3046
3151
|
start = end;
|
|
3047
3152
|
}
|
|
3048
3153
|
return buckets;
|
|
@@ -3130,7 +3235,7 @@ var providerNameParameter = {
|
|
|
3130
3235
|
in: "path",
|
|
3131
3236
|
required: true,
|
|
3132
3237
|
description: "Provider name.",
|
|
3133
|
-
schema: { type: "string", enum: ["gemini", "openai", "claude", "local"] }
|
|
3238
|
+
schema: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] }
|
|
3134
3239
|
};
|
|
3135
3240
|
var locationLabelParameter = {
|
|
3136
3241
|
name: "label",
|
|
@@ -3439,7 +3544,7 @@ var routeCatalog = [
|
|
|
3439
3544
|
type: "object",
|
|
3440
3545
|
required: ["provider"],
|
|
3441
3546
|
properties: {
|
|
3442
|
-
provider: { type: "string", enum: ["gemini", "openai", "claude", "local"] },
|
|
3547
|
+
provider: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] },
|
|
3443
3548
|
count: integerSchema
|
|
3444
3549
|
}
|
|
3445
3550
|
}
|
|
@@ -3518,7 +3623,7 @@ var routeCatalog = [
|
|
|
3518
3623
|
path: "/api/v1/projects/{name}/runs",
|
|
3519
3624
|
summary: "List project runs",
|
|
3520
3625
|
tags: ["runs"],
|
|
3521
|
-
parameters: [nameParameter],
|
|
3626
|
+
parameters: [nameParameter, limitQueryParameter],
|
|
3522
3627
|
responses: {
|
|
3523
3628
|
200: { description: "Runs returned." }
|
|
3524
3629
|
}
|
|
@@ -4601,31 +4706,40 @@ async function settingsRoutes(app, opts) {
|
|
|
4601
4706
|
bing: opts.bing ?? { configured: false }
|
|
4602
4707
|
}));
|
|
4603
4708
|
app.put("/settings/providers/:name", async (request, reply) => {
|
|
4604
|
-
const providerName = parseProviderName(request.params.name);
|
|
4605
4709
|
const { apiKey, baseUrl, model, quota } = request.body ?? {};
|
|
4606
|
-
|
|
4607
|
-
|
|
4710
|
+
const name = request.params.name;
|
|
4711
|
+
const adapters = opts.providerAdapters ?? [];
|
|
4712
|
+
const apiAdapters = adapters.filter((a) => a.mode === "api");
|
|
4713
|
+
const adapterInfo = apiAdapters.find((a) => a.name === name);
|
|
4714
|
+
if (!adapterInfo) {
|
|
4715
|
+
const validNames = apiAdapters.map((a) => a.name);
|
|
4716
|
+
const err = validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
|
|
4717
|
+
provider: name,
|
|
4718
|
+
validProviders: validNames
|
|
4719
|
+
});
|
|
4720
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4608
4721
|
}
|
|
4609
|
-
const name = providerName;
|
|
4610
4722
|
if (name === "local") {
|
|
4611
4723
|
if (!baseUrl || typeof baseUrl !== "string") {
|
|
4612
|
-
|
|
4724
|
+
const err = validationError("baseUrl is required for local provider");
|
|
4725
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4613
4726
|
}
|
|
4614
4727
|
} else {
|
|
4615
4728
|
if (!apiKey || typeof apiKey !== "string") {
|
|
4616
|
-
|
|
4729
|
+
const err = validationError("apiKey is required");
|
|
4730
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4617
4731
|
}
|
|
4618
4732
|
}
|
|
4619
4733
|
if (model !== void 0) {
|
|
4620
|
-
|
|
4621
|
-
if (!registry.validationPattern.test(model)) {
|
|
4734
|
+
if (!adapterInfo.modelValidationPattern.test(model)) {
|
|
4622
4735
|
return reply.status(400).send({
|
|
4623
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${
|
|
4736
|
+
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}` }
|
|
4624
4737
|
});
|
|
4625
4738
|
}
|
|
4626
4739
|
}
|
|
4627
4740
|
if (!opts.onProviderUpdate) {
|
|
4628
|
-
|
|
4741
|
+
const err = notImplemented("Provider configuration updates are not supported in this deployment");
|
|
4742
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4629
4743
|
}
|
|
4630
4744
|
if (quota !== void 0) {
|
|
4631
4745
|
if (typeof quota !== "object" || quota === null) {
|
|
@@ -4642,7 +4756,12 @@ async function settingsRoutes(app, opts) {
|
|
|
4642
4756
|
}
|
|
4643
4757
|
const result = opts.onProviderUpdate(name, apiKey ?? "", model, baseUrl, quota);
|
|
4644
4758
|
if (!result) {
|
|
4645
|
-
return reply.status(500).send({
|
|
4759
|
+
return reply.status(500).send({
|
|
4760
|
+
error: {
|
|
4761
|
+
code: "INTERNAL_ERROR",
|
|
4762
|
+
message: "Failed to update provider configuration"
|
|
4763
|
+
}
|
|
4764
|
+
});
|
|
4646
4765
|
}
|
|
4647
4766
|
return result;
|
|
4648
4767
|
});
|
|
@@ -4654,11 +4773,17 @@ async function settingsRoutes(app, opts) {
|
|
|
4654
4773
|
});
|
|
4655
4774
|
}
|
|
4656
4775
|
if (!opts.onGoogleUpdate) {
|
|
4657
|
-
|
|
4776
|
+
const err = notImplemented("Google OAuth configuration updates are not supported in this deployment");
|
|
4777
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4658
4778
|
}
|
|
4659
4779
|
const result = opts.onGoogleUpdate(clientId, clientSecret);
|
|
4660
4780
|
if (!result) {
|
|
4661
|
-
return reply.status(500).send({
|
|
4781
|
+
return reply.status(500).send({
|
|
4782
|
+
error: {
|
|
4783
|
+
code: "INTERNAL_ERROR",
|
|
4784
|
+
message: "Failed to update Google OAuth configuration"
|
|
4785
|
+
}
|
|
4786
|
+
});
|
|
4662
4787
|
}
|
|
4663
4788
|
return result;
|
|
4664
4789
|
});
|
|
@@ -4670,11 +4795,17 @@ async function settingsRoutes(app, opts) {
|
|
|
4670
4795
|
});
|
|
4671
4796
|
}
|
|
4672
4797
|
if (!opts.onBingUpdate) {
|
|
4673
|
-
|
|
4798
|
+
const err = notImplemented("Bing configuration updates are not supported in this deployment");
|
|
4799
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4674
4800
|
}
|
|
4675
4801
|
const result = opts.onBingUpdate(apiKey);
|
|
4676
4802
|
if (!result) {
|
|
4677
|
-
return reply.status(500).send({
|
|
4803
|
+
return reply.status(500).send({
|
|
4804
|
+
error: {
|
|
4805
|
+
code: "INTERNAL_ERROR",
|
|
4806
|
+
message: "Failed to update Bing configuration"
|
|
4807
|
+
}
|
|
4808
|
+
});
|
|
4678
4809
|
}
|
|
4679
4810
|
return result;
|
|
4680
4811
|
});
|
|
@@ -4684,7 +4815,8 @@ async function settingsRoutes(app, opts) {
|
|
|
4684
4815
|
async function telemetryRoutes(app, opts) {
|
|
4685
4816
|
app.get("/telemetry", async (_request, reply) => {
|
|
4686
4817
|
if (!opts.getTelemetryStatus) {
|
|
4687
|
-
|
|
4818
|
+
const err = notImplemented("Telemetry status is not available in this deployment");
|
|
4819
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4688
4820
|
}
|
|
4689
4821
|
const status = opts.getTelemetryStatus();
|
|
4690
4822
|
return {
|
|
@@ -4694,11 +4826,13 @@ async function telemetryRoutes(app, opts) {
|
|
|
4694
4826
|
});
|
|
4695
4827
|
app.put("/telemetry", async (request, reply) => {
|
|
4696
4828
|
if (!opts.setTelemetryEnabled) {
|
|
4697
|
-
|
|
4829
|
+
const err = notImplemented("Telemetry configuration is not available in this deployment");
|
|
4830
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4698
4831
|
}
|
|
4699
4832
|
const { enabled } = request.body ?? {};
|
|
4700
4833
|
if (typeof enabled !== "boolean") {
|
|
4701
|
-
|
|
4834
|
+
const err = validationError("enabled (boolean) is required");
|
|
4835
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4702
4836
|
}
|
|
4703
4837
|
opts.setTelemetryEnabled(enabled);
|
|
4704
4838
|
const status = opts.getTelemetryStatus?.();
|
|
@@ -4716,11 +4850,27 @@ async function scheduleRoutes(app, opts) {
|
|
|
4716
4850
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
4717
4851
|
const project = resolveProjectSafe6(app, request.params.name, reply);
|
|
4718
4852
|
if (!project) return;
|
|
4719
|
-
const
|
|
4720
|
-
if (!
|
|
4721
|
-
|
|
4722
|
-
|
|
4853
|
+
const parsedBody = scheduleUpsertRequestSchema.safeParse(request.body);
|
|
4854
|
+
if (!parsedBody.success) {
|
|
4855
|
+
const err = validationError("Invalid schedule payload", {
|
|
4856
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
4857
|
+
path: issue.path.join("."),
|
|
4858
|
+
message: issue.message
|
|
4859
|
+
}))
|
|
4723
4860
|
});
|
|
4861
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4862
|
+
}
|
|
4863
|
+
const { preset, cron: cron2, timezone, providers, enabled } = parsedBody.data;
|
|
4864
|
+
const validNames = opts.validProviderNames ?? [];
|
|
4865
|
+
if (validNames.length && providers?.length) {
|
|
4866
|
+
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
4867
|
+
if (invalid.length) {
|
|
4868
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
4869
|
+
invalidProviders: invalid,
|
|
4870
|
+
validProviders: validNames
|
|
4871
|
+
});
|
|
4872
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4873
|
+
}
|
|
4724
4874
|
}
|
|
4725
4875
|
if (!isValidTimezone(timezone)) {
|
|
4726
4876
|
return reply.status(400).send({
|
|
@@ -4991,7 +5141,7 @@ function resolveProjectSafe7(app, name, reply) {
|
|
|
4991
5141
|
|
|
4992
5142
|
// ../api-routes/src/google.ts
|
|
4993
5143
|
import crypto13 from "crypto";
|
|
4994
|
-
import { eq as eq13, and as and3, desc as
|
|
5144
|
+
import { eq as eq13, and as and3, desc as desc4, sql as sql2 } from "drizzle-orm";
|
|
4995
5145
|
|
|
4996
5146
|
// ../integration-google/src/constants.ts
|
|
4997
5147
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -5432,7 +5582,7 @@ async function googleRoutes(app, opts) {
|
|
|
5432
5582
|
if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
|
|
5433
5583
|
if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
5434
5584
|
if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
5435
|
-
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(
|
|
5585
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
5436
5586
|
return rows.map((r) => ({
|
|
5437
5587
|
date: r.date,
|
|
5438
5588
|
query: r.query,
|
|
@@ -5510,7 +5660,7 @@ async function googleRoutes(app, opts) {
|
|
|
5510
5660
|
const { url, limit } = request.query;
|
|
5511
5661
|
const conditions = [eq13(gscUrlInspections.projectId, project.id)];
|
|
5512
5662
|
if (url) conditions.push(eq13(gscUrlInspections.url, url));
|
|
5513
|
-
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(
|
|
5663
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
5514
5664
|
return rows.map((r) => ({
|
|
5515
5665
|
id: r.id,
|
|
5516
5666
|
url: r.url,
|
|
@@ -5529,7 +5679,7 @@ async function googleRoutes(app, opts) {
|
|
|
5529
5679
|
});
|
|
5530
5680
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
5531
5681
|
const project = resolveProject(app.db, request.params.name);
|
|
5532
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5682
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5533
5683
|
const byUrl = /* @__PURE__ */ new Map();
|
|
5534
5684
|
for (const row of allInspections) {
|
|
5535
5685
|
const existing = byUrl.get(row.url);
|
|
@@ -5557,7 +5707,7 @@ async function googleRoutes(app, opts) {
|
|
|
5557
5707
|
});
|
|
5558
5708
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
5559
5709
|
const project = resolveProject(app.db, request.params.name);
|
|
5560
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5710
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5561
5711
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5562
5712
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
5563
5713
|
for (const row of allInspections) {
|
|
@@ -5649,7 +5799,7 @@ async function googleRoutes(app, opts) {
|
|
|
5649
5799
|
const project = resolveProject(app.db, request.params.name);
|
|
5650
5800
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
5651
5801
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
5652
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(
|
|
5802
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(desc4(gscCoverageSnapshots.date)).limit(limit).all();
|
|
5653
5803
|
return rows.map((r) => ({
|
|
5654
5804
|
date: r.date,
|
|
5655
5805
|
indexed: r.indexed,
|
|
@@ -5802,7 +5952,7 @@ async function googleRoutes(app, opts) {
|
|
|
5802
5952
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
5803
5953
|
let urlsToNotify = request.body?.urls ?? [];
|
|
5804
5954
|
if (request.body?.allUnindexed) {
|
|
5805
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5955
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5806
5956
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5807
5957
|
for (const row of allInspections) {
|
|
5808
5958
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -5877,7 +6027,7 @@ async function googleRoutes(app, opts) {
|
|
|
5877
6027
|
|
|
5878
6028
|
// ../api-routes/src/bing.ts
|
|
5879
6029
|
import crypto14 from "crypto";
|
|
5880
|
-
import { eq as eq14, and as and4, desc as
|
|
6030
|
+
import { eq as eq14, and as and4, desc as desc5 } from "drizzle-orm";
|
|
5881
6031
|
|
|
5882
6032
|
// ../integration-bing/src/constants.ts
|
|
5883
6033
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -6081,7 +6231,7 @@ async function bingRoutes(app, opts) {
|
|
|
6081
6231
|
const project = resolveProject(app.db, request.params.name);
|
|
6082
6232
|
const conn = requireConnection(store, project.canonicalDomain, reply);
|
|
6083
6233
|
if (!conn) return;
|
|
6084
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6234
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
6085
6235
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6086
6236
|
for (const row of allInspections) {
|
|
6087
6237
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6131,7 +6281,7 @@ async function bingRoutes(app, opts) {
|
|
|
6131
6281
|
const project = resolveProject(app.db, request.params.name);
|
|
6132
6282
|
const { url, limit } = request.query;
|
|
6133
6283
|
const whereClause = url ? and4(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
|
|
6134
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
6284
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc5(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
6135
6285
|
return filtered.map((r) => ({
|
|
6136
6286
|
id: r.id,
|
|
6137
6287
|
url: r.url,
|
|
@@ -6193,7 +6343,7 @@ async function bingRoutes(app, opts) {
|
|
|
6193
6343
|
}
|
|
6194
6344
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
6195
6345
|
if (request.body?.allUnindexed) {
|
|
6196
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6346
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
6197
6347
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6198
6348
|
for (const row of allInspections) {
|
|
6199
6349
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6289,51 +6439,61 @@ async function cdpRoutes(app, opts) {
|
|
|
6289
6439
|
const { snapshotId } = request.params;
|
|
6290
6440
|
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq15(querySnapshots.id, snapshotId)).get();
|
|
6291
6441
|
if (!snapshot?.screenshotPath) {
|
|
6292
|
-
|
|
6442
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6443
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6293
6444
|
}
|
|
6294
6445
|
const base = path2.resolve(getScreenshotDir());
|
|
6295
6446
|
const fullPath = path2.resolve(path2.join(base, snapshot.screenshotPath));
|
|
6296
6447
|
if (!fullPath.startsWith(base + path2.sep) && fullPath !== base) {
|
|
6297
|
-
|
|
6448
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6449
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6298
6450
|
}
|
|
6299
6451
|
if (!fs2.existsSync(fullPath)) {
|
|
6300
|
-
|
|
6452
|
+
const err = notFound("Screenshot file", snapshotId);
|
|
6453
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6301
6454
|
}
|
|
6302
6455
|
const stream = fs2.createReadStream(fullPath);
|
|
6303
6456
|
return reply.type("image/png").send(stream);
|
|
6304
6457
|
});
|
|
6305
6458
|
app.put("/settings/cdp", async (request, reply) => {
|
|
6306
6459
|
if (!opts.onCdpConfigure) {
|
|
6307
|
-
|
|
6460
|
+
const err = notImplemented("CDP configuration not supported in this deployment");
|
|
6461
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6308
6462
|
}
|
|
6309
6463
|
const { host, port = 9222 } = request.body;
|
|
6310
6464
|
if (!host || typeof host !== "string") {
|
|
6311
|
-
|
|
6465
|
+
const err = validationError("host is required");
|
|
6466
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6312
6467
|
}
|
|
6313
6468
|
const ALLOWED_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
6314
6469
|
if (!ALLOWED_HOSTS.includes(host)) {
|
|
6315
|
-
|
|
6470
|
+
const err = validationError("host must be localhost, 127.0.0.1, or ::1");
|
|
6471
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6316
6472
|
}
|
|
6317
6473
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6318
|
-
|
|
6474
|
+
const err = validationError("port must be an integer between 1 and 65535");
|
|
6475
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6319
6476
|
}
|
|
6320
6477
|
await opts.onCdpConfigure(host, port);
|
|
6321
6478
|
return reply.code(200).send({ endpoint: `ws://${host}:${port}` });
|
|
6322
6479
|
});
|
|
6323
6480
|
app.get("/cdp/status", async (_request, reply) => {
|
|
6324
6481
|
if (!opts.getCdpStatus) {
|
|
6325
|
-
|
|
6482
|
+
const err = notImplemented("CDP not configured");
|
|
6483
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6326
6484
|
}
|
|
6327
6485
|
const status = await opts.getCdpStatus();
|
|
6328
6486
|
return reply.send(status);
|
|
6329
6487
|
});
|
|
6330
6488
|
app.post("/cdp/screenshot", async (request, reply) => {
|
|
6331
6489
|
if (!opts.onCdpScreenshot) {
|
|
6332
|
-
|
|
6490
|
+
const err = notImplemented("CDP not configured");
|
|
6491
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6333
6492
|
}
|
|
6334
6493
|
const { query, targets } = request.body;
|
|
6335
6494
|
if (!query || typeof query !== "string") {
|
|
6336
|
-
|
|
6495
|
+
const err = validationError("query is required");
|
|
6496
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6337
6497
|
}
|
|
6338
6498
|
const results = await opts.onCdpScreenshot(query, targets);
|
|
6339
6499
|
return reply.code(200).send({ results });
|
|
@@ -6344,7 +6504,10 @@ async function cdpRoutes(app, opts) {
|
|
|
6344
6504
|
const project = resolveProject(app.db, request.params.name);
|
|
6345
6505
|
const { runId } = request.params;
|
|
6346
6506
|
const run = app.db.select().from(runs).where(and5(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
|
|
6347
|
-
if (!run)
|
|
6507
|
+
if (!run) {
|
|
6508
|
+
const err = notFound("Run", runId);
|
|
6509
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6510
|
+
}
|
|
6348
6511
|
const snapshots = app.db.select({
|
|
6349
6512
|
id: querySnapshots.id,
|
|
6350
6513
|
keywordId: querySnapshots.keywordId,
|
|
@@ -6465,15 +6628,21 @@ async function apiRoutes(app, opts) {
|
|
|
6465
6628
|
await app.register(async (api) => {
|
|
6466
6629
|
await api.register(openApiRoutes, opts.openApiInfo ?? {});
|
|
6467
6630
|
await api.register(projectRoutes, {
|
|
6468
|
-
onProjectDeleted: opts.onProjectDeleted
|
|
6631
|
+
onProjectDeleted: opts.onProjectDeleted,
|
|
6632
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6469
6633
|
});
|
|
6470
6634
|
await api.register(keywordRoutes, {
|
|
6471
|
-
onGenerateKeywords: opts.onGenerateKeywords
|
|
6635
|
+
onGenerateKeywords: opts.onGenerateKeywords,
|
|
6636
|
+
validProviderNames: opts.providerAdapters?.filter((a) => a.mode === "api").map((a) => a.name)
|
|
6472
6637
|
});
|
|
6473
6638
|
await api.register(competitorRoutes);
|
|
6474
|
-
await api.register(runRoutes, {
|
|
6639
|
+
await api.register(runRoutes, {
|
|
6640
|
+
onRunCreated: opts.onRunCreated,
|
|
6641
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6642
|
+
});
|
|
6475
6643
|
await api.register(applyRoutes, {
|
|
6476
6644
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6645
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
6477
6646
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
6478
6647
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
6479
6648
|
propertyId,
|
|
@@ -6485,6 +6654,7 @@ async function apiRoutes(app, opts) {
|
|
|
6485
6654
|
await api.register(analyticsRoutes);
|
|
6486
6655
|
await api.register(settingsRoutes, {
|
|
6487
6656
|
providerSummary: opts.providerSummary,
|
|
6657
|
+
providerAdapters: opts.providerAdapters,
|
|
6488
6658
|
onProviderUpdate: opts.onProviderUpdate,
|
|
6489
6659
|
google: opts.googleSettingsSummary,
|
|
6490
6660
|
onGoogleUpdate: opts.onGoogleSettingsUpdate,
|
|
@@ -6492,7 +6662,8 @@ async function apiRoutes(app, opts) {
|
|
|
6492
6662
|
onBingUpdate: opts.onBingSettingsUpdate
|
|
6493
6663
|
});
|
|
6494
6664
|
await api.register(scheduleRoutes, {
|
|
6495
|
-
onScheduleUpdated: opts.onScheduleUpdated
|
|
6665
|
+
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6666
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6496
6667
|
});
|
|
6497
6668
|
await api.register(notificationRoutes);
|
|
6498
6669
|
await api.register(telemetryRoutes, {
|
|
@@ -6520,11 +6691,12 @@ async function apiRoutes(app, opts) {
|
|
|
6520
6691
|
|
|
6521
6692
|
// ../provider-gemini/src/normalize.ts
|
|
6522
6693
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
6523
|
-
var DEFAULT_MODEL =
|
|
6694
|
+
var DEFAULT_MODEL = "gemini-3-flash";
|
|
6695
|
+
var VALIDATION_PATTERN = /^gemini-/;
|
|
6524
6696
|
function resolveModel(config) {
|
|
6525
6697
|
const m = config.model;
|
|
6526
6698
|
if (!m) return DEFAULT_MODEL;
|
|
6527
|
-
if (
|
|
6699
|
+
if (VALIDATION_PATTERN.test(m)) return m;
|
|
6528
6700
|
console.warn(
|
|
6529
6701
|
`[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}.`
|
|
6530
6702
|
);
|
|
@@ -6535,7 +6707,7 @@ function validateConfig(config) {
|
|
|
6535
6707
|
return { ok: false, provider: "gemini", message: "missing api key" };
|
|
6536
6708
|
}
|
|
6537
6709
|
const model = resolveModel(config);
|
|
6538
|
-
const warning = config.model && !
|
|
6710
|
+
const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
|
|
6539
6711
|
return {
|
|
6540
6712
|
ok: true,
|
|
6541
6713
|
provider: "gemini",
|
|
@@ -6719,12 +6891,26 @@ function toGeminiConfig(config) {
|
|
|
6719
6891
|
}
|
|
6720
6892
|
var geminiAdapter = {
|
|
6721
6893
|
name: "gemini",
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6894
|
+
displayName: "Gemini",
|
|
6895
|
+
mode: "api",
|
|
6896
|
+
keyUrl: "https://aistudio.google.com/apikey",
|
|
6897
|
+
modelRegistry: {
|
|
6898
|
+
defaultModel: "gemini-3-flash",
|
|
6899
|
+
validationPattern: /^gemini-/,
|
|
6900
|
+
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
6901
|
+
knownModels: [
|
|
6902
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
6903
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
6904
|
+
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
6905
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
6906
|
+
]
|
|
6907
|
+
},
|
|
6908
|
+
validateConfig(config) {
|
|
6909
|
+
const result = validateConfig(toGeminiConfig(config));
|
|
6910
|
+
return {
|
|
6911
|
+
ok: result.ok,
|
|
6912
|
+
provider: "gemini",
|
|
6913
|
+
message: result.message,
|
|
6728
6914
|
model: result.model
|
|
6729
6915
|
};
|
|
6730
6916
|
},
|
|
@@ -6777,7 +6963,7 @@ var geminiAdapter = {
|
|
|
6777
6963
|
|
|
6778
6964
|
// ../provider-openai/src/normalize.ts
|
|
6779
6965
|
import OpenAI from "openai";
|
|
6780
|
-
var DEFAULT_MODEL2 =
|
|
6966
|
+
var DEFAULT_MODEL2 = "gpt-5.4";
|
|
6781
6967
|
function validateConfig2(config) {
|
|
6782
6968
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
6783
6969
|
return { ok: false, provider: "openai", message: "missing api key" };
|
|
@@ -6971,6 +7157,22 @@ function toOpenAIConfig(config) {
|
|
|
6971
7157
|
}
|
|
6972
7158
|
var openaiAdapter = {
|
|
6973
7159
|
name: "openai",
|
|
7160
|
+
displayName: "OpenAI",
|
|
7161
|
+
mode: "api",
|
|
7162
|
+
keyUrl: "https://platform.openai.com/api-keys",
|
|
7163
|
+
modelRegistry: {
|
|
7164
|
+
defaultModel: "gpt-5.4",
|
|
7165
|
+
validationPattern: /^(gpt-|o\d)/,
|
|
7166
|
+
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
7167
|
+
knownModels: [
|
|
7168
|
+
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
7169
|
+
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
7170
|
+
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
7171
|
+
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
7172
|
+
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
7173
|
+
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
7174
|
+
]
|
|
7175
|
+
},
|
|
6974
7176
|
validateConfig(config) {
|
|
6975
7177
|
const result = validateConfig2(toOpenAIConfig(config));
|
|
6976
7178
|
return {
|
|
@@ -7029,7 +7231,7 @@ var openaiAdapter = {
|
|
|
7029
7231
|
|
|
7030
7232
|
// ../provider-claude/src/normalize.ts
|
|
7031
7233
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7032
|
-
var DEFAULT_MODEL3 =
|
|
7234
|
+
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
7033
7235
|
function validateConfig3(config) {
|
|
7034
7236
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
7035
7237
|
return { ok: false, provider: "claude", message: "missing api key" };
|
|
@@ -7217,6 +7419,19 @@ function toClaudeConfig(config) {
|
|
|
7217
7419
|
}
|
|
7218
7420
|
var claudeAdapter = {
|
|
7219
7421
|
name: "claude",
|
|
7422
|
+
displayName: "Claude",
|
|
7423
|
+
mode: "api",
|
|
7424
|
+
keyUrl: "https://platform.claude.com/settings/keys",
|
|
7425
|
+
modelRegistry: {
|
|
7426
|
+
defaultModel: "claude-sonnet-4-6",
|
|
7427
|
+
validationPattern: /^claude-/,
|
|
7428
|
+
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
7429
|
+
knownModels: [
|
|
7430
|
+
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
7431
|
+
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
7432
|
+
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
7433
|
+
]
|
|
7434
|
+
},
|
|
7220
7435
|
validateConfig(config) {
|
|
7221
7436
|
const result = validateConfig3(toClaudeConfig(config));
|
|
7222
7437
|
return {
|
|
@@ -7275,7 +7490,7 @@ var claudeAdapter = {
|
|
|
7275
7490
|
|
|
7276
7491
|
// ../provider-local/src/normalize.ts
|
|
7277
7492
|
import OpenAI2 from "openai";
|
|
7278
|
-
var DEFAULT_MODEL4 =
|
|
7493
|
+
var DEFAULT_MODEL4 = "llama3";
|
|
7279
7494
|
function validateConfig4(config) {
|
|
7280
7495
|
if (!config.baseUrl || config.baseUrl.length === 0) {
|
|
7281
7496
|
return { ok: false, provider: "local", message: "missing base URL" };
|
|
@@ -7404,6 +7619,16 @@ function toLocalConfig(config) {
|
|
|
7404
7619
|
}
|
|
7405
7620
|
var localAdapter = {
|
|
7406
7621
|
name: "local",
|
|
7622
|
+
displayName: "Local",
|
|
7623
|
+
mode: "api",
|
|
7624
|
+
modelRegistry: {
|
|
7625
|
+
defaultModel: "llama3",
|
|
7626
|
+
validationPattern: /./,
|
|
7627
|
+
validationHint: "any model name accepted",
|
|
7628
|
+
knownModels: [
|
|
7629
|
+
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
7630
|
+
]
|
|
7631
|
+
},
|
|
7407
7632
|
validateConfig(config) {
|
|
7408
7633
|
const result = validateConfig4(toLocalConfig(config));
|
|
7409
7634
|
return {
|
|
@@ -7920,6 +8145,16 @@ function getScreenshotDir2() {
|
|
|
7920
8145
|
}
|
|
7921
8146
|
var cdpChatgptAdapter = {
|
|
7922
8147
|
name: "cdp:chatgpt",
|
|
8148
|
+
displayName: "ChatGPT (Browser)",
|
|
8149
|
+
mode: "browser",
|
|
8150
|
+
modelRegistry: {
|
|
8151
|
+
defaultModel: "chatgpt-web",
|
|
8152
|
+
validationPattern: /./,
|
|
8153
|
+
validationHint: "model is detected from the ChatGPT web UI",
|
|
8154
|
+
knownModels: [
|
|
8155
|
+
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
8156
|
+
]
|
|
8157
|
+
},
|
|
7923
8158
|
validateConfig(config) {
|
|
7924
8159
|
if (!config.cdpEndpoint) {
|
|
7925
8160
|
return {
|
|
@@ -8000,6 +8235,215 @@ var cdpChatgptAdapter = {
|
|
|
8000
8235
|
}
|
|
8001
8236
|
};
|
|
8002
8237
|
|
|
8238
|
+
// ../provider-perplexity/src/normalize.ts
|
|
8239
|
+
import OpenAI3 from "openai";
|
|
8240
|
+
var DEFAULT_MODEL5 = "sonar";
|
|
8241
|
+
var BASE_URL = "https://api.perplexity.ai";
|
|
8242
|
+
function validateConfig5(config) {
|
|
8243
|
+
if (!config.apiKey || config.apiKey.length === 0) {
|
|
8244
|
+
return { ok: false, provider: "perplexity", message: "missing api key" };
|
|
8245
|
+
}
|
|
8246
|
+
return {
|
|
8247
|
+
ok: true,
|
|
8248
|
+
provider: "perplexity",
|
|
8249
|
+
message: "config valid",
|
|
8250
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8251
|
+
};
|
|
8252
|
+
}
|
|
8253
|
+
async function healthcheck5(config) {
|
|
8254
|
+
const validation = validateConfig5(config);
|
|
8255
|
+
if (!validation.ok) return validation;
|
|
8256
|
+
try {
|
|
8257
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8258
|
+
const response = await client.chat.completions.create({
|
|
8259
|
+
model: config.model ?? DEFAULT_MODEL5,
|
|
8260
|
+
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
8261
|
+
});
|
|
8262
|
+
const text2 = response.choices[0]?.message?.content ?? "";
|
|
8263
|
+
return {
|
|
8264
|
+
ok: text2.length > 0,
|
|
8265
|
+
provider: "perplexity",
|
|
8266
|
+
message: text2.length > 0 ? "perplexity api key verified" : "empty response from perplexity",
|
|
8267
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8268
|
+
};
|
|
8269
|
+
} catch (err) {
|
|
8270
|
+
return {
|
|
8271
|
+
ok: false,
|
|
8272
|
+
provider: "perplexity",
|
|
8273
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8274
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
async function executeTrackedQuery5(input) {
|
|
8279
|
+
const model = input.config.model ?? DEFAULT_MODEL5;
|
|
8280
|
+
const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
|
|
8281
|
+
const response = await client.chat.completions.create({
|
|
8282
|
+
model,
|
|
8283
|
+
messages: [
|
|
8284
|
+
{ role: "user", content: input.keyword }
|
|
8285
|
+
]
|
|
8286
|
+
});
|
|
8287
|
+
const rawResponse = responseToRecord4(response);
|
|
8288
|
+
const citations = extractCitations(rawResponse);
|
|
8289
|
+
const groundingSources = citations.map((url) => ({
|
|
8290
|
+
uri: url,
|
|
8291
|
+
title: ""
|
|
8292
|
+
}));
|
|
8293
|
+
return {
|
|
8294
|
+
provider: "perplexity",
|
|
8295
|
+
rawResponse,
|
|
8296
|
+
model,
|
|
8297
|
+
groundingSources,
|
|
8298
|
+
searchQueries: [input.keyword]
|
|
8299
|
+
};
|
|
8300
|
+
}
|
|
8301
|
+
function normalizeResult6(raw) {
|
|
8302
|
+
const answerText = extractAnswerText3(raw.rawResponse);
|
|
8303
|
+
const citedDomains = extractCitedDomains5(raw.groundingSources);
|
|
8304
|
+
return {
|
|
8305
|
+
provider: "perplexity",
|
|
8306
|
+
answerText,
|
|
8307
|
+
citedDomains,
|
|
8308
|
+
groundingSources: raw.groundingSources,
|
|
8309
|
+
searchQueries: raw.searchQueries
|
|
8310
|
+
};
|
|
8311
|
+
}
|
|
8312
|
+
function extractCitations(rawResponse) {
|
|
8313
|
+
if (Array.isArray(rawResponse.citations)) {
|
|
8314
|
+
return rawResponse.citations.filter((c) => typeof c === "string");
|
|
8315
|
+
}
|
|
8316
|
+
const apiResponse = rawResponse.apiResponse;
|
|
8317
|
+
if (apiResponse !== null && typeof apiResponse === "object" && !Array.isArray(apiResponse)) {
|
|
8318
|
+
const nested = apiResponse.citations;
|
|
8319
|
+
if (Array.isArray(nested)) {
|
|
8320
|
+
return nested.filter((c) => typeof c === "string");
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
8323
|
+
return [];
|
|
8324
|
+
}
|
|
8325
|
+
function extractAnswerText3(rawResponse) {
|
|
8326
|
+
try {
|
|
8327
|
+
const choices = rawResponse.choices;
|
|
8328
|
+
if (!choices?.length) return "";
|
|
8329
|
+
return choices[0].message?.content ?? "";
|
|
8330
|
+
} catch {
|
|
8331
|
+
return "";
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
function extractCitedDomains5(groundingSources) {
|
|
8335
|
+
const domains = /* @__PURE__ */ new Set();
|
|
8336
|
+
for (const source of groundingSources) {
|
|
8337
|
+
const domain = extractDomainFromUri4(source.uri);
|
|
8338
|
+
if (domain) domains.add(domain);
|
|
8339
|
+
}
|
|
8340
|
+
return [...domains];
|
|
8341
|
+
}
|
|
8342
|
+
function extractDomainFromUri4(uri) {
|
|
8343
|
+
try {
|
|
8344
|
+
const url = new URL(uri);
|
|
8345
|
+
return url.hostname.replace(/^www\./, "");
|
|
8346
|
+
} catch {
|
|
8347
|
+
return null;
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
async function generateText5(prompt, config) {
|
|
8351
|
+
const model = config.model ?? DEFAULT_MODEL5;
|
|
8352
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8353
|
+
const response = await client.chat.completions.create({
|
|
8354
|
+
model,
|
|
8355
|
+
messages: [{ role: "user", content: prompt }]
|
|
8356
|
+
});
|
|
8357
|
+
return response.choices[0]?.message?.content ?? "";
|
|
8358
|
+
}
|
|
8359
|
+
function responseToRecord4(response) {
|
|
8360
|
+
try {
|
|
8361
|
+
return JSON.parse(JSON.stringify(response));
|
|
8362
|
+
} catch {
|
|
8363
|
+
return { error: "failed to serialize response" };
|
|
8364
|
+
}
|
|
8365
|
+
}
|
|
8366
|
+
|
|
8367
|
+
// ../provider-perplexity/src/adapter.ts
|
|
8368
|
+
function toPerplexityConfig(config) {
|
|
8369
|
+
return {
|
|
8370
|
+
apiKey: config.apiKey ?? "",
|
|
8371
|
+
model: config.model,
|
|
8372
|
+
quotaPolicy: config.quotaPolicy
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
var perplexityAdapter = {
|
|
8376
|
+
name: "perplexity",
|
|
8377
|
+
displayName: "Perplexity",
|
|
8378
|
+
mode: "api",
|
|
8379
|
+
keyUrl: "https://www.perplexity.ai/settings/api",
|
|
8380
|
+
modelRegistry: {
|
|
8381
|
+
defaultModel: "sonar",
|
|
8382
|
+
validationPattern: /^sonar/,
|
|
8383
|
+
validationHint: "expected a sonar model (e.g. sonar, sonar-pro, sonar-reasoning)",
|
|
8384
|
+
knownModels: [
|
|
8385
|
+
{ id: "sonar", displayName: "Sonar", tier: "standard" },
|
|
8386
|
+
{ id: "sonar-pro", displayName: "Sonar Pro", tier: "flagship" },
|
|
8387
|
+
{ id: "sonar-reasoning", displayName: "Sonar Reasoning", tier: "flagship" },
|
|
8388
|
+
{ id: "sonar-reasoning-pro", displayName: "Sonar Reasoning Pro", tier: "flagship" }
|
|
8389
|
+
]
|
|
8390
|
+
},
|
|
8391
|
+
validateConfig(config) {
|
|
8392
|
+
const result = validateConfig5(toPerplexityConfig(config));
|
|
8393
|
+
return {
|
|
8394
|
+
ok: result.ok,
|
|
8395
|
+
provider: "perplexity",
|
|
8396
|
+
message: result.message,
|
|
8397
|
+
model: result.model
|
|
8398
|
+
};
|
|
8399
|
+
},
|
|
8400
|
+
async healthcheck(config) {
|
|
8401
|
+
const result = await healthcheck5(toPerplexityConfig(config));
|
|
8402
|
+
return {
|
|
8403
|
+
ok: result.ok,
|
|
8404
|
+
provider: "perplexity",
|
|
8405
|
+
message: result.message,
|
|
8406
|
+
model: result.model
|
|
8407
|
+
};
|
|
8408
|
+
},
|
|
8409
|
+
async executeTrackedQuery(input, config) {
|
|
8410
|
+
const raw = await executeTrackedQuery5({
|
|
8411
|
+
keyword: input.keyword,
|
|
8412
|
+
canonicalDomains: input.canonicalDomains,
|
|
8413
|
+
competitorDomains: input.competitorDomains,
|
|
8414
|
+
config: toPerplexityConfig(config),
|
|
8415
|
+
location: input.location
|
|
8416
|
+
});
|
|
8417
|
+
return {
|
|
8418
|
+
provider: "perplexity",
|
|
8419
|
+
rawResponse: raw.rawResponse,
|
|
8420
|
+
model: raw.model,
|
|
8421
|
+
groundingSources: raw.groundingSources,
|
|
8422
|
+
searchQueries: raw.searchQueries
|
|
8423
|
+
};
|
|
8424
|
+
},
|
|
8425
|
+
normalizeResult(raw) {
|
|
8426
|
+
const perplexityRaw = {
|
|
8427
|
+
provider: "perplexity",
|
|
8428
|
+
rawResponse: raw.rawResponse,
|
|
8429
|
+
model: raw.model,
|
|
8430
|
+
groundingSources: raw.groundingSources,
|
|
8431
|
+
searchQueries: raw.searchQueries
|
|
8432
|
+
};
|
|
8433
|
+
const normalized = normalizeResult6(perplexityRaw);
|
|
8434
|
+
return {
|
|
8435
|
+
provider: "perplexity",
|
|
8436
|
+
answerText: normalized.answerText,
|
|
8437
|
+
citedDomains: normalized.citedDomains,
|
|
8438
|
+
groundingSources: normalized.groundingSources,
|
|
8439
|
+
searchQueries: normalized.searchQueries
|
|
8440
|
+
};
|
|
8441
|
+
},
|
|
8442
|
+
async generateText(prompt, config) {
|
|
8443
|
+
return generateText5(prompt, toPerplexityConfig(config));
|
|
8444
|
+
}
|
|
8445
|
+
};
|
|
8446
|
+
|
|
8003
8447
|
// src/google-config.ts
|
|
8004
8448
|
function ensureConnections(config) {
|
|
8005
8449
|
if (!config.google) config.google = {};
|
|
@@ -8078,7 +8522,75 @@ import crypto15 from "crypto";
|
|
|
8078
8522
|
import fs4 from "fs";
|
|
8079
8523
|
import path5 from "path";
|
|
8080
8524
|
import os4 from "os";
|
|
8081
|
-
import { eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8525
|
+
import { and as and6, eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8526
|
+
var RunCancelledError = class extends Error {
|
|
8527
|
+
constructor(runId) {
|
|
8528
|
+
super(`Run ${runId} was cancelled`);
|
|
8529
|
+
this.name = "RunCancelledError";
|
|
8530
|
+
}
|
|
8531
|
+
};
|
|
8532
|
+
var ProviderExecutionGate = class {
|
|
8533
|
+
constructor(maxConcurrency, maxPerMinute) {
|
|
8534
|
+
this.maxConcurrency = maxConcurrency;
|
|
8535
|
+
this.maxPerMinute = maxPerMinute;
|
|
8536
|
+
}
|
|
8537
|
+
window = [];
|
|
8538
|
+
waiters = [];
|
|
8539
|
+
rateLimitChain = Promise.resolve();
|
|
8540
|
+
inFlight = 0;
|
|
8541
|
+
async run(task) {
|
|
8542
|
+
await this.acquire();
|
|
8543
|
+
try {
|
|
8544
|
+
await this.waitForRateLimit();
|
|
8545
|
+
return await task();
|
|
8546
|
+
} finally {
|
|
8547
|
+
this.release();
|
|
8548
|
+
}
|
|
8549
|
+
}
|
|
8550
|
+
async acquire() {
|
|
8551
|
+
if (this.inFlight < Math.max(1, this.maxConcurrency)) {
|
|
8552
|
+
this.inFlight++;
|
|
8553
|
+
return;
|
|
8554
|
+
}
|
|
8555
|
+
await new Promise((resolve) => {
|
|
8556
|
+
this.waiters.push(resolve);
|
|
8557
|
+
});
|
|
8558
|
+
this.inFlight++;
|
|
8559
|
+
}
|
|
8560
|
+
release() {
|
|
8561
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
8562
|
+
const next = this.waiters.shift();
|
|
8563
|
+
next?.();
|
|
8564
|
+
}
|
|
8565
|
+
async waitForRateLimit() {
|
|
8566
|
+
let releaseChain;
|
|
8567
|
+
const previousChain = this.rateLimitChain;
|
|
8568
|
+
this.rateLimitChain = new Promise((resolve) => {
|
|
8569
|
+
releaseChain = resolve;
|
|
8570
|
+
});
|
|
8571
|
+
await previousChain;
|
|
8572
|
+
try {
|
|
8573
|
+
const now = Date.now();
|
|
8574
|
+
const windowStart = now - 6e4;
|
|
8575
|
+
while (this.window.length > 0 && this.window[0] < windowStart) {
|
|
8576
|
+
this.window.shift();
|
|
8577
|
+
}
|
|
8578
|
+
if (this.window.length >= this.maxPerMinute) {
|
|
8579
|
+
const oldestInWindow = this.window[0];
|
|
8580
|
+
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
8581
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
8582
|
+
const nowAfterWait = Date.now();
|
|
8583
|
+
const newWindowStart = nowAfterWait - 6e4;
|
|
8584
|
+
while (this.window.length > 0 && this.window[0] < newWindowStart) {
|
|
8585
|
+
this.window.shift();
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
this.window.push(Date.now());
|
|
8589
|
+
} finally {
|
|
8590
|
+
releaseChain?.();
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
};
|
|
8082
8594
|
var JobRunner = class {
|
|
8083
8595
|
db;
|
|
8084
8596
|
registry;
|
|
@@ -8099,13 +8611,34 @@ var JobRunner = class {
|
|
|
8099
8611
|
async executeRun(runId, projectId, providerOverride, locationOverride) {
|
|
8100
8612
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8101
8613
|
const startTime = Date.now();
|
|
8614
|
+
let runLocation;
|
|
8615
|
+
let activeProviders = [];
|
|
8616
|
+
let projectKeywords = [];
|
|
8617
|
+
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
8102
8618
|
try {
|
|
8103
|
-
|
|
8619
|
+
const existingRun = this.getRunState(runId);
|
|
8620
|
+
if (!existingRun) {
|
|
8621
|
+
throw new Error(`Run ${runId} not found`);
|
|
8622
|
+
}
|
|
8623
|
+
if (existingRun.status === "cancelled") {
|
|
8624
|
+
this.handleCancelledRun(runId, projectId, startTime, {
|
|
8625
|
+
providerCount: 0,
|
|
8626
|
+
providers: [],
|
|
8627
|
+
keywordCount: 0
|
|
8628
|
+
});
|
|
8629
|
+
return;
|
|
8630
|
+
}
|
|
8631
|
+
if (existingRun.status !== "queued" && existingRun.status !== "running") {
|
|
8632
|
+
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
8633
|
+
}
|
|
8634
|
+
if (existingRun.status === "queued") {
|
|
8635
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq16(runs.id, runId), eq16(runs.status, "queued"))).run();
|
|
8636
|
+
}
|
|
8637
|
+
this.throwIfRunCancelled(runId);
|
|
8104
8638
|
const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
|
|
8105
8639
|
if (!project) {
|
|
8106
8640
|
throw new Error(`Project ${projectId} not found`);
|
|
8107
8641
|
}
|
|
8108
|
-
let runLocation;
|
|
8109
8642
|
if (locationOverride === null) {
|
|
8110
8643
|
runLocation = void 0;
|
|
8111
8644
|
} else if (locationOverride) {
|
|
@@ -8117,16 +8650,26 @@ var JobRunner = class {
|
|
|
8117
8650
|
}
|
|
8118
8651
|
}
|
|
8119
8652
|
const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
|
|
8120
|
-
|
|
8653
|
+
activeProviders = this.registry.getForProject(projectProviders);
|
|
8121
8654
|
if (activeProviders.length === 0) {
|
|
8122
8655
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
8123
8656
|
}
|
|
8124
8657
|
console.log(`[JobRunner] Run ${runId}: dispatching to ${activeProviders.length} providers: ${activeProviders.map((p) => p.adapter.name).join(", ")}`);
|
|
8125
|
-
|
|
8658
|
+
projectKeywords = this.db.select().from(keywords).where(eq16(keywords.projectId, projectId)).all();
|
|
8126
8659
|
const projectCompetitors = this.db.select().from(competitors).where(eq16(competitors.projectId, projectId)).all();
|
|
8127
8660
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
8661
|
+
const allDomains = effectiveDomains({
|
|
8662
|
+
canonicalDomain: project.canonicalDomain,
|
|
8663
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8664
|
+
});
|
|
8665
|
+
const executionContext = {
|
|
8666
|
+
providerCount: activeProviders.length,
|
|
8667
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8668
|
+
keywordCount: projectKeywords.length,
|
|
8669
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8670
|
+
};
|
|
8128
8671
|
const queriesPerProvider = projectKeywords.length;
|
|
8129
|
-
const todayPeriod =
|
|
8672
|
+
const todayPeriod = getCurrentUsageDay();
|
|
8130
8673
|
for (const p of activeProviders) {
|
|
8131
8674
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
8132
8675
|
const providerUsage = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
@@ -8137,27 +8680,31 @@ var JobRunner = class {
|
|
|
8137
8680
|
);
|
|
8138
8681
|
}
|
|
8139
8682
|
}
|
|
8140
|
-
const
|
|
8141
|
-
for (const
|
|
8142
|
-
|
|
8683
|
+
const executionGates = /* @__PURE__ */ new Map();
|
|
8684
|
+
for (const provider of activeProviders) {
|
|
8685
|
+
executionGates.set(
|
|
8686
|
+
provider.adapter.name,
|
|
8687
|
+
new ProviderExecutionGate(
|
|
8688
|
+
provider.config.quotaPolicy.maxConcurrency,
|
|
8689
|
+
provider.config.quotaPolicy.maxRequestsPerMinute
|
|
8690
|
+
)
|
|
8691
|
+
);
|
|
8143
8692
|
}
|
|
8144
8693
|
const providerErrors = /* @__PURE__ */ new Map();
|
|
8145
8694
|
let totalSnapshotsInserted = 0;
|
|
8146
8695
|
const apiProviders = activeProviders.filter((p) => !isBrowserProvider(p.adapter.name));
|
|
8147
8696
|
const browserProviders = activeProviders.filter((p) => isBrowserProvider(p.adapter.name));
|
|
8148
|
-
|
|
8149
|
-
const
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8160
|
-
});
|
|
8697
|
+
const processKeywordForProvider = async (registeredProvider, kw) => {
|
|
8698
|
+
const { adapter, config } = registeredProvider;
|
|
8699
|
+
const providerName = adapter.name;
|
|
8700
|
+
const gate = executionGates.get(providerName);
|
|
8701
|
+
if (!gate) {
|
|
8702
|
+
throw new Error(`Missing execution gate for provider ${providerName}`);
|
|
8703
|
+
}
|
|
8704
|
+
try {
|
|
8705
|
+
await gate.run(async () => {
|
|
8706
|
+
this.throwIfRunCancelled(runId);
|
|
8707
|
+
providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
8161
8708
|
const raw = await adapter.executeTrackedQuery(
|
|
8162
8709
|
{
|
|
8163
8710
|
keyword: kw.keyword,
|
|
@@ -8167,6 +8714,7 @@ var JobRunner = class {
|
|
|
8167
8714
|
},
|
|
8168
8715
|
config
|
|
8169
8716
|
);
|
|
8717
|
+
this.throwIfRunCancelled(runId);
|
|
8170
8718
|
const normalized = adapter.normalizeResult(raw);
|
|
8171
8719
|
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
8172
8720
|
const citationState = determineCitationState(normalized, allDomains);
|
|
@@ -8222,96 +8770,29 @@ var JobRunner = class {
|
|
|
8222
8770
|
}
|
|
8223
8771
|
totalSnapshotsInserted++;
|
|
8224
8772
|
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
8225
|
-
}
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8773
|
+
});
|
|
8774
|
+
} catch (err) {
|
|
8775
|
+
if (err instanceof RunCancelledError) {
|
|
8776
|
+
throw err;
|
|
8229
8777
|
}
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
const { adapter, config } = registeredProvider;
|
|
8234
|
-
const providerName = adapter.name;
|
|
8235
|
-
try {
|
|
8236
|
-
await this.waitForRateLimit(
|
|
8237
|
-
minuteWindows.get(providerName),
|
|
8238
|
-
config.quotaPolicy.maxRequestsPerMinute
|
|
8239
|
-
);
|
|
8240
|
-
const allDomains = effectiveDomains({
|
|
8241
|
-
canonicalDomain: project.canonicalDomain,
|
|
8242
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8243
|
-
});
|
|
8244
|
-
const raw = await adapter.executeTrackedQuery(
|
|
8245
|
-
{
|
|
8246
|
-
keyword: kw.keyword,
|
|
8247
|
-
canonicalDomains: allDomains,
|
|
8248
|
-
competitorDomains,
|
|
8249
|
-
location: runLocation
|
|
8250
|
-
},
|
|
8251
|
-
config
|
|
8252
|
-
);
|
|
8253
|
-
const normalized = adapter.normalizeResult(raw);
|
|
8254
|
-
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
8255
|
-
const citationState = determineCitationState(normalized, allDomains);
|
|
8256
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
8257
|
-
let screenshotRelPath = null;
|
|
8258
|
-
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
8259
|
-
const snapshotId = crypto15.randomUUID();
|
|
8260
|
-
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
8261
|
-
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
8262
|
-
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
8263
|
-
fs4.renameSync(raw.screenshotPath, destPath);
|
|
8264
|
-
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
8265
|
-
this.db.insert(querySnapshots).values({
|
|
8266
|
-
id: snapshotId,
|
|
8267
|
-
runId,
|
|
8268
|
-
keywordId: kw.id,
|
|
8269
|
-
provider: providerName,
|
|
8270
|
-
model: raw.model,
|
|
8271
|
-
citationState,
|
|
8272
|
-
answerText: normalized.answerText,
|
|
8273
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
8274
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
8275
|
-
location: runLocation?.label ?? null,
|
|
8276
|
-
screenshotPath: screenshotRelPath,
|
|
8277
|
-
rawResponse: JSON.stringify({
|
|
8278
|
-
model: raw.model,
|
|
8279
|
-
groundingSources: normalized.groundingSources,
|
|
8280
|
-
searchQueries: normalized.searchQueries,
|
|
8281
|
-
apiResponse: raw.rawResponse
|
|
8282
|
-
}),
|
|
8283
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8284
|
-
}).run();
|
|
8285
|
-
} else {
|
|
8286
|
-
this.db.insert(querySnapshots).values({
|
|
8287
|
-
id: crypto15.randomUUID(),
|
|
8288
|
-
runId,
|
|
8289
|
-
keywordId: kw.id,
|
|
8290
|
-
provider: providerName,
|
|
8291
|
-
model: raw.model,
|
|
8292
|
-
citationState,
|
|
8293
|
-
answerText: normalized.answerText,
|
|
8294
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
8295
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
8296
|
-
location: runLocation?.label ?? null,
|
|
8297
|
-
rawResponse: JSON.stringify({
|
|
8298
|
-
model: raw.model,
|
|
8299
|
-
groundingSources: normalized.groundingSources,
|
|
8300
|
-
searchQueries: normalized.searchQueries,
|
|
8301
|
-
apiResponse: raw.rawResponse
|
|
8302
|
-
}),
|
|
8303
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8304
|
-
}).run();
|
|
8305
|
-
}
|
|
8306
|
-
totalSnapshotsInserted++;
|
|
8307
|
-
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
8308
|
-
} catch (err) {
|
|
8309
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8310
|
-
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8778
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8779
|
+
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8780
|
+
if (!providerErrors.has(providerName)) {
|
|
8311
8781
|
providerErrors.set(providerName, msg);
|
|
8312
8782
|
}
|
|
8313
8783
|
}
|
|
8784
|
+
};
|
|
8785
|
+
await Promise.all(apiProviders.map(async (registeredProvider) => {
|
|
8786
|
+
await Promise.all(projectKeywords.map(async (kw) => {
|
|
8787
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8788
|
+
}));
|
|
8789
|
+
}));
|
|
8790
|
+
for (const registeredProvider of browserProviders) {
|
|
8791
|
+
for (const kw of projectKeywords) {
|
|
8792
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8793
|
+
}
|
|
8314
8794
|
}
|
|
8795
|
+
this.throwIfRunCancelled(runId);
|
|
8315
8796
|
const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
|
|
8316
8797
|
const someFailed = providerErrors.size > 0;
|
|
8317
8798
|
if (allFailed) {
|
|
@@ -8323,18 +8804,16 @@ var JobRunner = class {
|
|
|
8323
8804
|
} else {
|
|
8324
8805
|
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
|
|
8325
8806
|
}
|
|
8807
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8326
8808
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
8327
8809
|
trackEvent("run.completed", {
|
|
8328
8810
|
status: finalStatus,
|
|
8329
|
-
providerCount:
|
|
8330
|
-
providers:
|
|
8331
|
-
keywordCount:
|
|
8811
|
+
providerCount: executionContext.providerCount,
|
|
8812
|
+
providers: executionContext.providers,
|
|
8813
|
+
keywordCount: executionContext.keywordCount,
|
|
8332
8814
|
durationMs: Date.now() - startTime,
|
|
8333
|
-
...
|
|
8815
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
8334
8816
|
});
|
|
8335
|
-
for (const p of activeProviders) {
|
|
8336
|
-
this.incrementUsage(`${projectId}:${p.adapter.name}`, "queries", queriesPerProvider);
|
|
8337
|
-
}
|
|
8338
8817
|
this.incrementUsage(projectId, "runs", 1);
|
|
8339
8818
|
if (this.onRunCompleted) {
|
|
8340
8819
|
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
@@ -8342,18 +8821,31 @@ var JobRunner = class {
|
|
|
8342
8821
|
});
|
|
8343
8822
|
}
|
|
8344
8823
|
} catch (err) {
|
|
8824
|
+
const executionContext = {
|
|
8825
|
+
providerCount: activeProviders.length,
|
|
8826
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8827
|
+
keywordCount: projectKeywords.length,
|
|
8828
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8829
|
+
};
|
|
8830
|
+
if (err instanceof RunCancelledError || this.isRunCancelled(runId)) {
|
|
8831
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8832
|
+
this.handleCancelledRun(runId, projectId, startTime, executionContext);
|
|
8833
|
+
return;
|
|
8834
|
+
}
|
|
8345
8835
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
8346
8836
|
this.db.update(runs).set({
|
|
8347
8837
|
status: "failed",
|
|
8348
8838
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8349
8839
|
error: errorMessage
|
|
8350
8840
|
}).where(eq16(runs.id, runId)).run();
|
|
8841
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8351
8842
|
trackEvent("run.completed", {
|
|
8352
8843
|
status: "failed",
|
|
8353
|
-
providerCount:
|
|
8354
|
-
providers:
|
|
8355
|
-
keywordCount:
|
|
8356
|
-
durationMs: Date.now() - startTime
|
|
8844
|
+
providerCount: executionContext.providerCount,
|
|
8845
|
+
providers: executionContext.providers,
|
|
8846
|
+
keywordCount: executionContext.keywordCount,
|
|
8847
|
+
durationMs: Date.now() - startTime,
|
|
8848
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
8357
8849
|
});
|
|
8358
8850
|
if (this.onRunCompleted) {
|
|
8359
8851
|
this.onRunCompleted(runId, projectId).catch((notifErr) => {
|
|
@@ -8362,27 +8854,9 @@ var JobRunner = class {
|
|
|
8362
8854
|
}
|
|
8363
8855
|
}
|
|
8364
8856
|
}
|
|
8365
|
-
async waitForRateLimit(window, maxPerMinute) {
|
|
8366
|
-
const now = Date.now();
|
|
8367
|
-
const windowStart = now - 6e4;
|
|
8368
|
-
while (window.length > 0 && window[0] < windowStart) {
|
|
8369
|
-
window.shift();
|
|
8370
|
-
}
|
|
8371
|
-
if (window.length >= maxPerMinute) {
|
|
8372
|
-
const oldestInWindow = window[0];
|
|
8373
|
-
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
8374
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
8375
|
-
const nowAfterWait = Date.now();
|
|
8376
|
-
const newWindowStart = nowAfterWait - 6e4;
|
|
8377
|
-
while (window.length > 0 && window[0] < newWindowStart) {
|
|
8378
|
-
window.shift();
|
|
8379
|
-
}
|
|
8380
|
-
}
|
|
8381
|
-
window.push(Date.now());
|
|
8382
|
-
}
|
|
8383
8857
|
incrementUsage(scope, metric, count) {
|
|
8384
8858
|
const now = /* @__PURE__ */ new Date();
|
|
8385
|
-
const period =
|
|
8859
|
+
const period = now.toISOString().slice(0, 10);
|
|
8386
8860
|
const id = crypto15.randomUUID();
|
|
8387
8861
|
const existing = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
8388
8862
|
if (existing) {
|
|
@@ -8398,10 +8872,52 @@ var JobRunner = class {
|
|
|
8398
8872
|
}).run();
|
|
8399
8873
|
}
|
|
8400
8874
|
}
|
|
8875
|
+
flushProviderUsage(projectId, providerDispatchCounts) {
|
|
8876
|
+
for (const [providerName, count] of providerDispatchCounts.entries()) {
|
|
8877
|
+
if (count <= 0) continue;
|
|
8878
|
+
this.incrementUsage(`${projectId}:${providerName}`, "queries", count);
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
getRunState(runId) {
|
|
8882
|
+
return this.db.select({
|
|
8883
|
+
status: runs.status,
|
|
8884
|
+
finishedAt: runs.finishedAt,
|
|
8885
|
+
error: runs.error
|
|
8886
|
+
}).from(runs).where(eq16(runs.id, runId)).get();
|
|
8887
|
+
}
|
|
8888
|
+
isRunCancelled(runId) {
|
|
8889
|
+
return this.getRunState(runId)?.status === "cancelled";
|
|
8890
|
+
}
|
|
8891
|
+
throwIfRunCancelled(runId) {
|
|
8892
|
+
if (this.isRunCancelled(runId)) {
|
|
8893
|
+
throw new RunCancelledError(runId);
|
|
8894
|
+
}
|
|
8895
|
+
}
|
|
8896
|
+
handleCancelledRun(runId, projectId, startTime, context) {
|
|
8897
|
+
const currentRun = this.getRunState(runId);
|
|
8898
|
+
if (currentRun && !currentRun.finishedAt) {
|
|
8899
|
+
this.db.update(runs).set({
|
|
8900
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8901
|
+
error: currentRun.error ?? "Cancelled by user"
|
|
8902
|
+
}).where(eq16(runs.id, runId)).run();
|
|
8903
|
+
}
|
|
8904
|
+
trackEvent("run.completed", {
|
|
8905
|
+
status: "cancelled",
|
|
8906
|
+
providerCount: context.providerCount,
|
|
8907
|
+
providers: context.providers,
|
|
8908
|
+
keywordCount: context.keywordCount,
|
|
8909
|
+
durationMs: Date.now() - startTime,
|
|
8910
|
+
...context.location ? { location: context.location } : {}
|
|
8911
|
+
});
|
|
8912
|
+
if (this.onRunCompleted) {
|
|
8913
|
+
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
8914
|
+
console.error("[JobRunner] Notification callback failed:", err);
|
|
8915
|
+
});
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
8401
8918
|
};
|
|
8402
|
-
function
|
|
8403
|
-
|
|
8404
|
-
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
8919
|
+
function getCurrentUsageDay() {
|
|
8920
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8405
8921
|
}
|
|
8406
8922
|
function domainMatches(domain, canonicalDomain) {
|
|
8407
8923
|
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
@@ -8467,7 +8983,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
8467
8983
|
|
|
8468
8984
|
// src/gsc-sync.ts
|
|
8469
8985
|
import crypto16 from "crypto";
|
|
8470
|
-
import { eq as eq17, and as
|
|
8986
|
+
import { eq as eq17, and as and7, sql as sql3 } from "drizzle-orm";
|
|
8471
8987
|
function formatDate(d) {
|
|
8472
8988
|
return d.toISOString().split("T")[0];
|
|
8473
8989
|
}
|
|
@@ -8518,7 +9034,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8518
9034
|
});
|
|
8519
9035
|
console.log(`[GSC Sync] Received ${rows.length} rows`);
|
|
8520
9036
|
db.delete(gscSearchData).where(
|
|
8521
|
-
|
|
9037
|
+
and7(
|
|
8522
9038
|
eq17(gscSearchData.projectId, projectId),
|
|
8523
9039
|
sql3`${gscSearchData.date} >= ${startDate}`,
|
|
8524
9040
|
sql3`${gscSearchData.date} <= ${endDate}`
|
|
@@ -8607,7 +9123,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8607
9123
|
}
|
|
8608
9124
|
}
|
|
8609
9125
|
const snapshotDate = formatDate(/* @__PURE__ */ new Date());
|
|
8610
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9126
|
+
db.delete(gscCoverageSnapshots).where(and7(eq17(gscCoverageSnapshots.projectId, projectId), eq17(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
8611
9127
|
db.insert(gscCoverageSnapshots).values({
|
|
8612
9128
|
id: crypto16.randomUUID(),
|
|
8613
9129
|
projectId,
|
|
@@ -8630,7 +9146,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8630
9146
|
|
|
8631
9147
|
// src/gsc-inspect-sitemap.ts
|
|
8632
9148
|
import crypto17 from "crypto";
|
|
8633
|
-
import { eq as eq18, and as
|
|
9149
|
+
import { eq as eq18, and as and8 } from "drizzle-orm";
|
|
8634
9150
|
|
|
8635
9151
|
// src/sitemap-parser.ts
|
|
8636
9152
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -8793,7 +9309,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
8793
9309
|
}
|
|
8794
9310
|
}
|
|
8795
9311
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8796
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9312
|
+
db.delete(gscCoverageSnapshots).where(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
8797
9313
|
db.insert(gscCoverageSnapshots).values({
|
|
8798
9314
|
id: crypto17.randomUUID(),
|
|
8799
9315
|
projectId,
|
|
@@ -8985,7 +9501,7 @@ var Scheduler = class {
|
|
|
8985
9501
|
};
|
|
8986
9502
|
|
|
8987
9503
|
// src/notifier.ts
|
|
8988
|
-
import { eq as eq20, desc as
|
|
9504
|
+
import { eq as eq20, desc as desc6, and as and9, or as or2 } from "drizzle-orm";
|
|
8989
9505
|
import crypto18 from "crypto";
|
|
8990
9506
|
var Notifier = class {
|
|
8991
9507
|
db;
|
|
@@ -9048,11 +9564,11 @@ var Notifier = class {
|
|
|
9048
9564
|
}
|
|
9049
9565
|
computeTransitions(runId, projectId) {
|
|
9050
9566
|
const recentRuns = this.db.select().from(runs).where(
|
|
9051
|
-
|
|
9567
|
+
and9(
|
|
9052
9568
|
eq20(runs.projectId, projectId),
|
|
9053
9569
|
or2(eq20(runs.status, "completed"), eq20(runs.status, "partial"))
|
|
9054
9570
|
)
|
|
9055
|
-
).orderBy(
|
|
9571
|
+
).orderBy(desc6(runs.createdAt)).limit(2).all();
|
|
9056
9572
|
if (recentRuns.length < 2) return [];
|
|
9057
9573
|
const currentRunId = recentRuns[0].id;
|
|
9058
9574
|
const previousRunId = recentRuns[1].id;
|
|
@@ -9255,6 +9771,20 @@ var DEFAULT_QUOTA = {
|
|
|
9255
9771
|
maxRequestsPerMinute: 10,
|
|
9256
9772
|
maxRequestsPerDay: 1e3
|
|
9257
9773
|
};
|
|
9774
|
+
var API_ADAPTERS = [
|
|
9775
|
+
geminiAdapter,
|
|
9776
|
+
openaiAdapter,
|
|
9777
|
+
claudeAdapter,
|
|
9778
|
+
localAdapter,
|
|
9779
|
+
perplexityAdapter
|
|
9780
|
+
];
|
|
9781
|
+
var BROWSER_ADAPTERS = [
|
|
9782
|
+
cdpChatgptAdapter
|
|
9783
|
+
];
|
|
9784
|
+
var ALL_ADAPTERS = [...API_ADAPTERS, ...BROWSER_ADAPTERS];
|
|
9785
|
+
var adapterMap = Object.fromEntries(
|
|
9786
|
+
API_ADAPTERS.map((a) => [a.name, a])
|
|
9787
|
+
);
|
|
9258
9788
|
function summarizeProviderConfig(provider, config) {
|
|
9259
9789
|
return {
|
|
9260
9790
|
configured: Boolean(config?.apiKey || config?.baseUrl),
|
|
@@ -9291,38 +9821,19 @@ async function createServer(opts) {
|
|
|
9291
9821
|
const p = providers[k];
|
|
9292
9822
|
return p?.apiKey || p?.baseUrl;
|
|
9293
9823
|
}));
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
quotaPolicy: providers.openai.quota ?? DEFAULT_QUOTA
|
|
9308
|
-
});
|
|
9309
|
-
}
|
|
9310
|
-
if (providers.claude?.apiKey) {
|
|
9311
|
-
registry.register(claudeAdapter, {
|
|
9312
|
-
provider: "claude",
|
|
9313
|
-
apiKey: providers.claude.apiKey,
|
|
9314
|
-
model: providers.claude.model,
|
|
9315
|
-
quotaPolicy: providers.claude.quota ?? DEFAULT_QUOTA
|
|
9316
|
-
});
|
|
9317
|
-
}
|
|
9318
|
-
if (providers.local?.baseUrl) {
|
|
9319
|
-
registry.register(localAdapter, {
|
|
9320
|
-
provider: "local",
|
|
9321
|
-
apiKey: providers.local.apiKey,
|
|
9322
|
-
baseUrl: providers.local.baseUrl,
|
|
9323
|
-
model: providers.local.model,
|
|
9324
|
-
quotaPolicy: providers.local.quota ?? DEFAULT_QUOTA
|
|
9325
|
-
});
|
|
9824
|
+
for (const adapter of API_ADAPTERS) {
|
|
9825
|
+
const entry = providers[adapter.name];
|
|
9826
|
+
if (!entry) continue;
|
|
9827
|
+
const isConfigured = adapter.name === "local" ? !!entry.baseUrl : !!entry.apiKey;
|
|
9828
|
+
if (isConfigured) {
|
|
9829
|
+
registry.register(adapter, {
|
|
9830
|
+
provider: adapter.name,
|
|
9831
|
+
apiKey: entry.apiKey,
|
|
9832
|
+
baseUrl: entry.baseUrl,
|
|
9833
|
+
model: entry.model,
|
|
9834
|
+
quotaPolicy: entry.quota ?? DEFAULT_QUOTA
|
|
9835
|
+
});
|
|
9836
|
+
}
|
|
9326
9837
|
}
|
|
9327
9838
|
const cdpConfig = opts.config.cdp;
|
|
9328
9839
|
if (cdpConfig?.host || cdpConfig?.port) {
|
|
@@ -9347,17 +9858,25 @@ async function createServer(opts) {
|
|
|
9347
9858
|
});
|
|
9348
9859
|
}
|
|
9349
9860
|
});
|
|
9350
|
-
const providerSummary =
|
|
9351
|
-
name,
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9861
|
+
const providerSummary = API_ADAPTERS.map((adapter) => ({
|
|
9862
|
+
name: adapter.name,
|
|
9863
|
+
displayName: adapter.displayName,
|
|
9864
|
+
keyUrl: adapter.keyUrl,
|
|
9865
|
+
modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
|
|
9866
|
+
model: registry.get(adapter.name)?.config.model,
|
|
9867
|
+
configured: !!registry.get(adapter.name),
|
|
9868
|
+
quota: registry.get(adapter.name)?.config.quotaPolicy
|
|
9355
9869
|
}));
|
|
9356
9870
|
const googleSettingsSummary = {
|
|
9357
9871
|
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
9358
9872
|
};
|
|
9359
9873
|
const bingSettingsSummary = {
|
|
9360
|
-
configured
|
|
9874
|
+
// Treat Bing as configured if there is at least one connection with an API key,
|
|
9875
|
+
// OR if a global bing.apiKey is set. The CLI stores keys per-connection
|
|
9876
|
+
// (bing.connections[].apiKey), so checking only bing.apiKey missed existing connections.
|
|
9877
|
+
configured: Boolean(
|
|
9878
|
+
opts.config.bing?.apiKey || opts.config.bing?.connections?.some((c) => c.apiKey)
|
|
9879
|
+
)
|
|
9361
9880
|
};
|
|
9362
9881
|
const bingConnectionStore = {
|
|
9363
9882
|
getConnection: (domain) => {
|
|
@@ -9391,7 +9910,6 @@ async function createServer(opts) {
|
|
|
9391
9910
|
return true;
|
|
9392
9911
|
}
|
|
9393
9912
|
};
|
|
9394
|
-
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
9395
9913
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto19.randomBytes(32).toString("hex");
|
|
9396
9914
|
const googleConnectionStore = {
|
|
9397
9915
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -9455,6 +9973,13 @@ async function createServer(opts) {
|
|
|
9455
9973
|
version: PKG_VERSION
|
|
9456
9974
|
},
|
|
9457
9975
|
providerSummary,
|
|
9976
|
+
providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
|
|
9977
|
+
name: a.name,
|
|
9978
|
+
displayName: a.displayName,
|
|
9979
|
+
mode: a.mode,
|
|
9980
|
+
modelValidationPattern: a.modelRegistry.validationPattern,
|
|
9981
|
+
modelValidationHint: a.modelRegistry.validationHint
|
|
9982
|
+
})),
|
|
9458
9983
|
googleSettingsSummary,
|
|
9459
9984
|
bingSettingsSummary,
|
|
9460
9985
|
bingConnectionStore,
|
|
@@ -9465,7 +9990,7 @@ async function createServer(opts) {
|
|
|
9465
9990
|
},
|
|
9466
9991
|
onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {
|
|
9467
9992
|
const name = providerName;
|
|
9468
|
-
if (!
|
|
9993
|
+
if (!adapterMap[name]) return null;
|
|
9469
9994
|
if (!opts.config.providers) opts.config.providers = {};
|
|
9470
9995
|
const existing = opts.config.providers[name];
|
|
9471
9996
|
const beforeConfig = summarizeProviderConfig(name, existing);
|
|
@@ -9748,14 +10273,6 @@ function parseKeywordResponse(raw, count) {
|
|
|
9748
10273
|
}
|
|
9749
10274
|
|
|
9750
10275
|
export {
|
|
9751
|
-
providerQuotaPolicySchema,
|
|
9752
|
-
resolveProviderInput,
|
|
9753
|
-
notificationEventSchema,
|
|
9754
|
-
getDefaultModel,
|
|
9755
|
-
effectiveDomains,
|
|
9756
|
-
apiKeys,
|
|
9757
|
-
createClient,
|
|
9758
|
-
migrate,
|
|
9759
10276
|
getConfigDir,
|
|
9760
10277
|
getConfigPath,
|
|
9761
10278
|
loadConfig,
|
|
@@ -9766,6 +10283,13 @@ export {
|
|
|
9766
10283
|
isFirstRun,
|
|
9767
10284
|
showFirstRunNotice,
|
|
9768
10285
|
trackEvent,
|
|
10286
|
+
providerQuotaPolicySchema,
|
|
10287
|
+
resolveProviderInput,
|
|
10288
|
+
notificationEventSchema,
|
|
10289
|
+
effectiveDomains,
|
|
9769
10290
|
setGoogleAuthConfig,
|
|
10291
|
+
apiKeys,
|
|
10292
|
+
createClient,
|
|
10293
|
+
migrate,
|
|
9770
10294
|
createServer
|
|
9771
10295
|
};
|