@ainyc/canonry 4.15.0 → 4.17.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 +57 -223
- package/assets/assets/index-4fWsYFLp.css +1 -0
- package/assets/assets/index-C5-Gvl6o.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-C32VL5BB.js → chunk-6TWKC3DP.js} +147 -12
- package/dist/{chunk-7HBZCGRL.js → chunk-PAZCY4FF.js} +99 -4
- package/dist/{chunk-6QTH5NS5.js → chunk-Q2OED5JQ.js} +75 -1
- package/dist/{chunk-DLSQXNUN.js → chunk-ZGHD3IAV.js} +485 -150
- package/dist/cli.js +248 -45
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-BCKXIKIL.js → intelligence-service-X3PQLBUV.js} +2 -2
- package/dist/mcp.js +9 -3
- package/package.json +7 -7
- package/assets/assets/index-B6Mi9Fd1.js +0 -302
- package/assets/assets/index-D0EPNRDs.css +0 -1
package/assets/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-C5-Gvl6o.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="./assets/index-4fWsYFLp.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root"></div>
|
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
queryBatchRequestSchema,
|
|
12
12
|
queryGenerateRequestSchema,
|
|
13
13
|
runTriggerRequestSchema,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
schedulableRunKindSchema,
|
|
15
|
+
scheduleUpsertRequestSchema,
|
|
16
|
+
trafficConnectCloudRunRequestSchema,
|
|
17
|
+
trafficEventKindSchema
|
|
18
|
+
} from "./chunk-Q2OED5JQ.js";
|
|
16
19
|
|
|
17
20
|
// src/config.ts
|
|
18
21
|
import fs from "fs";
|
|
@@ -545,11 +548,13 @@ var ApiClient = class {
|
|
|
545
548
|
async putSchedule(project, body) {
|
|
546
549
|
return this.request("PUT", `/projects/${encodeURIComponent(project)}/schedule`, body);
|
|
547
550
|
}
|
|
548
|
-
async getSchedule(project) {
|
|
549
|
-
|
|
551
|
+
async getSchedule(project, kind) {
|
|
552
|
+
const qs = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
|
553
|
+
return this.request("GET", `/projects/${encodeURIComponent(project)}/schedule${qs}`);
|
|
550
554
|
}
|
|
551
|
-
async deleteSchedule(project) {
|
|
552
|
-
|
|
555
|
+
async deleteSchedule(project, kind) {
|
|
556
|
+
const qs = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
|
557
|
+
await this.request("DELETE", `/projects/${encodeURIComponent(project)}/schedule${qs}`);
|
|
553
558
|
}
|
|
554
559
|
async createNotification(project, body) {
|
|
555
560
|
return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications`, body);
|
|
@@ -768,6 +773,37 @@ var ApiClient = class {
|
|
|
768
773
|
body ?? {}
|
|
769
774
|
);
|
|
770
775
|
}
|
|
776
|
+
async trafficListSources(project) {
|
|
777
|
+
return this.request(
|
|
778
|
+
"GET",
|
|
779
|
+
`/projects/${encodeURIComponent(project)}/traffic/sources`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
async trafficStatus(project) {
|
|
783
|
+
return this.request(
|
|
784
|
+
"GET",
|
|
785
|
+
`/projects/${encodeURIComponent(project)}/traffic/status`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
async trafficGetSource(project, sourceId) {
|
|
789
|
+
return this.request(
|
|
790
|
+
"GET",
|
|
791
|
+
`/projects/${encodeURIComponent(project)}/traffic/sources/${encodeURIComponent(sourceId)}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
async trafficListEvents(project, params) {
|
|
795
|
+
const search = {};
|
|
796
|
+
if (params?.since) search.since = params.since;
|
|
797
|
+
if (params?.until) search.until = params.until;
|
|
798
|
+
if (params?.kind) search.kind = params.kind;
|
|
799
|
+
if (params?.limit !== void 0) search.limit = String(params.limit);
|
|
800
|
+
if (params?.sourceId) search.sourceId = params.sourceId;
|
|
801
|
+
const qs = Object.keys(search).length ? "?" + new URLSearchParams(search).toString() : "";
|
|
802
|
+
return this.request(
|
|
803
|
+
"GET",
|
|
804
|
+
`/projects/${encodeURIComponent(project)}/traffic/events${qs}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
771
807
|
async wordpressConnect(project, body) {
|
|
772
808
|
return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
|
|
773
809
|
}
|
|
@@ -1114,6 +1150,10 @@ var scheduleSetInputSchema = z2.object({
|
|
|
1114
1150
|
project: projectNameSchema,
|
|
1115
1151
|
schedule: scheduleUpsertRequestSchema
|
|
1116
1152
|
});
|
|
1153
|
+
var scheduleReadInputSchema = z2.object({
|
|
1154
|
+
project: projectNameSchema,
|
|
1155
|
+
kind: schedulableRunKindSchema.optional().describe('Schedulable run kind. Defaults to "answer-visibility" if omitted.')
|
|
1156
|
+
});
|
|
1117
1157
|
var agentWebhookAttachInputSchema = z2.object({
|
|
1118
1158
|
project: projectNameSchema,
|
|
1119
1159
|
url: z2.string().url()
|
|
@@ -1141,6 +1181,27 @@ var memoryForgetInputSchema = z2.object({
|
|
|
1141
1181
|
project: projectNameSchema,
|
|
1142
1182
|
key: z2.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe("Exact key of the note to remove. No-op (status=missing) when no note exists for that key.")
|
|
1143
1183
|
});
|
|
1184
|
+
var trafficConnectCloudRunInputSchema = z2.object({
|
|
1185
|
+
project: projectNameSchema,
|
|
1186
|
+
request: trafficConnectCloudRunRequestSchema
|
|
1187
|
+
});
|
|
1188
|
+
var trafficSyncInputSchema = z2.object({
|
|
1189
|
+
project: projectNameSchema,
|
|
1190
|
+
sourceId: z2.string().min(1).describe("Traffic source ID returned by canonry_traffic_connect_cloud_run or canonry_traffic_sources_list."),
|
|
1191
|
+
sinceMinutes: z2.number().int().positive().max(7 * 24 * 60).optional().describe("Lookback window in minutes. Defaults to the source's configured window (60 min) when omitted; clamped forward to lastSyncedAt to avoid double-counting.")
|
|
1192
|
+
});
|
|
1193
|
+
var trafficEventsInputSchema = z2.object({
|
|
1194
|
+
project: projectNameSchema,
|
|
1195
|
+
since: z2.string().optional().describe("ISO 8601 lower bound. Defaults to 24h ago when omitted."),
|
|
1196
|
+
until: z2.string().optional().describe("ISO 8601 upper bound. Defaults to now when omitted."),
|
|
1197
|
+
kind: z2.union([trafficEventKindSchema, z2.literal("all")]).optional().describe('Filter to "crawler" or "ai-referral"; "all" (default) returns both.'),
|
|
1198
|
+
sourceId: z2.string().min(1).optional().describe("Restrict to a single traffic source ID."),
|
|
1199
|
+
limit: z2.number().int().positive().max(5e3).optional().describe("Max combined rows. Defaults to 500, max 5000. Totals always reflect the full window.")
|
|
1200
|
+
});
|
|
1201
|
+
var trafficSourceIdInputSchema = z2.object({
|
|
1202
|
+
project: projectNameSchema,
|
|
1203
|
+
sourceId: z2.string().min(1).describe("Traffic source ID.")
|
|
1204
|
+
});
|
|
1144
1205
|
var AGENT_WEBHOOK_EVENTS = [
|
|
1145
1206
|
notificationEventSchema.enum["run.completed"],
|
|
1146
1207
|
notificationEventSchema.enum["insight.critical"],
|
|
@@ -1444,13 +1505,13 @@ var canonryMcpTools = [
|
|
|
1444
1505
|
defineTool({
|
|
1445
1506
|
name: "canonry_schedule_get",
|
|
1446
1507
|
title: "Get schedule",
|
|
1447
|
-
description:
|
|
1508
|
+
description: 'Get the scheduled run configuration for a Canonry project. Pass `kind` to read a non-default schedule (e.g. "traffic-sync"); defaults to "answer-visibility".',
|
|
1448
1509
|
access: "read",
|
|
1449
1510
|
tier: "setup",
|
|
1450
|
-
inputSchema:
|
|
1511
|
+
inputSchema: scheduleReadInputSchema,
|
|
1451
1512
|
annotations: readAnnotations(),
|
|
1452
1513
|
openApiOperations: ["GET /api/v1/projects/{name}/schedule"],
|
|
1453
|
-
handler: (client, input) => client.getSchedule(input.project)
|
|
1514
|
+
handler: (client, input) => client.getSchedule(input.project, input.kind)
|
|
1454
1515
|
}),
|
|
1455
1516
|
defineTool({
|
|
1456
1517
|
name: "canonry_backlinks_latest_release",
|
|
@@ -1653,6 +1714,80 @@ var canonryMcpTools = [
|
|
|
1653
1714
|
openApiOperations: ["GET /api/v1/projects/{name}/ga/session-history"],
|
|
1654
1715
|
handler: (client, input) => client.gaSessionHistory(input.project, compactStringParams(input, ["window"]))
|
|
1655
1716
|
}),
|
|
1717
|
+
defineTool({
|
|
1718
|
+
name: "canonry_traffic_sources_list",
|
|
1719
|
+
title: "List traffic sources",
|
|
1720
|
+
description: "List server-side traffic sources for a Canonry project (Cloud Run, etc.). Returns non-archived sources with status, last sync timestamp, last error, and the stored config (gcpProjectId, serviceName, location, authMode). Pair with canonry_traffic_source_get for last-24h totals on a single source.",
|
|
1721
|
+
access: "read",
|
|
1722
|
+
tier: "traffic",
|
|
1723
|
+
inputSchema: projectInputSchema,
|
|
1724
|
+
annotations: readAnnotations(),
|
|
1725
|
+
openApiOperations: ["GET /api/v1/projects/{name}/traffic/sources"],
|
|
1726
|
+
handler: (client, input) => client.trafficListSources(input.project)
|
|
1727
|
+
}),
|
|
1728
|
+
defineTool({
|
|
1729
|
+
name: "canonry_traffic_source_get",
|
|
1730
|
+
title: "Get traffic source detail",
|
|
1731
|
+
description: "Get one traffic source plus 24h totals (crawler hits, AI-referral hits, raw event sample count) and the latest traffic-sync run summary. Use to confirm a source is healthy and observing traffic before drilling into events.",
|
|
1732
|
+
access: "read",
|
|
1733
|
+
tier: "traffic",
|
|
1734
|
+
inputSchema: trafficSourceIdInputSchema,
|
|
1735
|
+
annotations: readAnnotations(),
|
|
1736
|
+
openApiOperations: ["GET /api/v1/projects/{name}/traffic/sources/{id}"],
|
|
1737
|
+
handler: (client, input) => client.trafficGetSource(input.project, input.sourceId)
|
|
1738
|
+
}),
|
|
1739
|
+
defineTool({
|
|
1740
|
+
name: "canonry_traffic_status",
|
|
1741
|
+
title: "Traffic status (all sources)",
|
|
1742
|
+
description: "Single-call composite returning every non-archived traffic source plus its last-24h totals (crawler hits, AI-referral hits, sample count) and latest source-scoped traffic-sync run. Same per-entry shape as canonry_traffic_source_get, but one call covers all sources \u2014 prefer this over a list+per-source fan-out.",
|
|
1743
|
+
access: "read",
|
|
1744
|
+
tier: "traffic",
|
|
1745
|
+
inputSchema: projectInputSchema,
|
|
1746
|
+
annotations: readAnnotations(),
|
|
1747
|
+
openApiOperations: ["GET /api/v1/projects/{name}/traffic/status"],
|
|
1748
|
+
handler: (client, input) => client.trafficStatus(input.project)
|
|
1749
|
+
}),
|
|
1750
|
+
defineTool({
|
|
1751
|
+
name: "canonry_traffic_events",
|
|
1752
|
+
title: "List traffic events",
|
|
1753
|
+
description: 'Read crawler and AI-referral hourly rollups from server-side traffic sources. Returns a discriminated list (kind="crawler" rows carry botId/operator/verificationStatus; kind="ai-referral" rows carry product/sourceDomain/evidenceType) plus totals over the full window even when limit truncates rows. Window defaults to last 24h.',
|
|
1754
|
+
access: "read",
|
|
1755
|
+
tier: "traffic",
|
|
1756
|
+
inputSchema: trafficEventsInputSchema,
|
|
1757
|
+
annotations: readAnnotations(),
|
|
1758
|
+
openApiOperations: ["GET /api/v1/projects/{name}/traffic/events"],
|
|
1759
|
+
handler: (client, input) => {
|
|
1760
|
+
const params = {};
|
|
1761
|
+
if (input.since) params.since = input.since;
|
|
1762
|
+
if (input.until) params.until = input.until;
|
|
1763
|
+
if (input.kind) params.kind = input.kind;
|
|
1764
|
+
if (input.sourceId) params.sourceId = input.sourceId;
|
|
1765
|
+
if (input.limit !== void 0) params.limit = input.limit;
|
|
1766
|
+
return client.trafficListEvents(input.project, params);
|
|
1767
|
+
}
|
|
1768
|
+
}),
|
|
1769
|
+
defineTool({
|
|
1770
|
+
name: "canonry_traffic_connect_cloud_run",
|
|
1771
|
+
title: "Connect Cloud Run traffic source",
|
|
1772
|
+
description: "Connect a Google Cloud Run service as a server-side traffic source. v1 requires service-account JSON content (paste the file contents into `keyJson`); OAuth-mode is not yet supported. Reconnecting an existing source updates the credential and config in place. The private key is stored in ~/.canonry/config.yaml (not the DB) and never echoed back.",
|
|
1773
|
+
access: "write",
|
|
1774
|
+
tier: "traffic",
|
|
1775
|
+
inputSchema: trafficConnectCloudRunInputSchema,
|
|
1776
|
+
annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
|
|
1777
|
+
openApiOperations: ["POST /api/v1/projects/{name}/traffic/connect/cloud-run"],
|
|
1778
|
+
handler: (client, input) => client.trafficConnectCloudRun(input.project, input.request)
|
|
1779
|
+
}),
|
|
1780
|
+
defineTool({
|
|
1781
|
+
name: "canonry_traffic_sync",
|
|
1782
|
+
title: "Sync Cloud Run traffic source",
|
|
1783
|
+
description: "Pull the most recent Cloud Logging entries for a Cloud Run traffic source, classify them as crawler / AI-referral / unknown, and upsert hourly rollups + raw samples. Returns totals, bucket counts, and the run id. The window auto-clamps forward to lastSyncedAt to avoid double-counting on back-to-back calls.",
|
|
1784
|
+
access: "write",
|
|
1785
|
+
tier: "traffic",
|
|
1786
|
+
inputSchema: trafficSyncInputSchema,
|
|
1787
|
+
annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
|
|
1788
|
+
openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/sync"],
|
|
1789
|
+
handler: (client, input) => client.trafficSync(input.project, input.sourceId, input.sinceMinutes !== void 0 ? { sinceMinutes: input.sinceMinutes } : void 0)
|
|
1790
|
+
}),
|
|
1656
1791
|
defineTool({
|
|
1657
1792
|
name: "canonry_project_upsert",
|
|
1658
1793
|
title: "Create or replace project",
|
|
@@ -1838,14 +1973,14 @@ var canonryMcpTools = [
|
|
|
1838
1973
|
defineTool({
|
|
1839
1974
|
name: "canonry_schedule_delete",
|
|
1840
1975
|
title: "Delete schedule",
|
|
1841
|
-
description:
|
|
1976
|
+
description: 'Delete the scheduled run configuration for a Canonry project. Pass `kind` to delete a non-default schedule (e.g. "traffic-sync"); defaults to "answer-visibility".',
|
|
1842
1977
|
access: "write",
|
|
1843
1978
|
tier: "setup",
|
|
1844
|
-
inputSchema:
|
|
1979
|
+
inputSchema: scheduleReadInputSchema,
|
|
1845
1980
|
annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
|
|
1846
1981
|
openApiOperations: ["DELETE /api/v1/projects/{name}/schedule"],
|
|
1847
1982
|
handler: async (client, input) => {
|
|
1848
|
-
await client.deleteSchedule(input.project);
|
|
1983
|
+
await client.deleteSchedule(input.project, input.kind);
|
|
1849
1984
|
}
|
|
1850
1985
|
}),
|
|
1851
1986
|
defineTool({
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
categoryLabel,
|
|
9
9
|
determineAnswerMentioned,
|
|
10
10
|
normalizeProjectDomain
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-Q2OED5JQ.js";
|
|
12
12
|
|
|
13
13
|
// src/intelligence-service.ts
|
|
14
14
|
import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
|
|
@@ -104,13 +104,15 @@ var runs = sqliteTable("runs", {
|
|
|
104
104
|
status: text("status").notNull().default("queued"),
|
|
105
105
|
trigger: text("trigger").notNull().default("manual"),
|
|
106
106
|
location: text("location"),
|
|
107
|
+
sourceId: text("source_id"),
|
|
107
108
|
startedAt: text("started_at"),
|
|
108
109
|
finishedAt: text("finished_at"),
|
|
109
110
|
error: text("error"),
|
|
110
111
|
createdAt: text("created_at").notNull()
|
|
111
112
|
}, (table) => [
|
|
112
113
|
index("idx_runs_project").on(table.projectId),
|
|
113
|
-
index("idx_runs_status").on(table.status)
|
|
114
|
+
index("idx_runs_status").on(table.status),
|
|
115
|
+
index("idx_runs_source").on(table.sourceId)
|
|
114
116
|
]);
|
|
115
117
|
var querySnapshots = sqliteTable("query_snapshots", {
|
|
116
118
|
id: text("id").primaryKey(),
|
|
@@ -164,17 +166,24 @@ var apiKeys = sqliteTable("api_keys", {
|
|
|
164
166
|
var schedules = sqliteTable("schedules", {
|
|
165
167
|
id: text("id").primaryKey(),
|
|
166
168
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
169
|
+
// Run kind dispatched by this schedule. Must be a value of `RunKinds` —
|
|
170
|
+
// currently 'answer-visibility' and 'traffic-sync' are user-facing schedulable kinds.
|
|
171
|
+
// Defaults to 'answer-visibility' for backward compatibility with rows
|
|
172
|
+
// created before migration 53.
|
|
173
|
+
kind: text("kind").notNull().default("answer-visibility"),
|
|
167
174
|
cronExpr: text("cron_expr").notNull(),
|
|
168
175
|
preset: text("preset"),
|
|
169
176
|
timezone: text("timezone").notNull().default("UTC"),
|
|
170
177
|
enabled: integer("enabled").notNull().default(1),
|
|
171
178
|
providers: text("providers").notNull().default("[]"),
|
|
179
|
+
/** Optional traffic-source UUID for traffic-sync schedules. Null for other kinds. */
|
|
180
|
+
sourceId: text("source_id"),
|
|
172
181
|
lastRunAt: text("last_run_at"),
|
|
173
182
|
nextRunAt: text("next_run_at"),
|
|
174
183
|
createdAt: text("created_at").notNull(),
|
|
175
184
|
updatedAt: text("updated_at").notNull()
|
|
176
185
|
}, (table) => [
|
|
177
|
-
uniqueIndex("
|
|
186
|
+
uniqueIndex("idx_schedules_project_kind").on(table.projectId, table.kind)
|
|
178
187
|
]);
|
|
179
188
|
var notifications = sqliteTable("notifications", {
|
|
180
189
|
id: text("id").primaryKey(),
|
|
@@ -555,6 +564,11 @@ var trafficSources = sqliteTable("traffic_sources", {
|
|
|
555
564
|
lastSyncedAt: text("last_synced_at"),
|
|
556
565
|
lastCursor: text("last_cursor"),
|
|
557
566
|
lastError: text("last_error"),
|
|
567
|
+
// JSON-encoded array of normalized event IDs (e.g. `cloud-run:<ts>:<insertId>`)
|
|
568
|
+
// observed in the most recent successful sync. Bounded ring buffer used to
|
|
569
|
+
// dedupe across sync runs at the boundary timestamp where lastSyncedAt
|
|
570
|
+
// clamping alone leaves a small overlap window.
|
|
571
|
+
lastEventIds: text("last_event_ids"),
|
|
558
572
|
archivedAt: text("archived_at"),
|
|
559
573
|
configJson: text("config_json").notNull().default("{}"),
|
|
560
574
|
createdAt: text("created_at").notNull(),
|
|
@@ -793,7 +807,10 @@ CREATE TABLE IF NOT EXISTS notifications (
|
|
|
793
807
|
|
|
794
808
|
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
|
|
795
809
|
CREATE INDEX IF NOT EXISTS idx_usage_scope_period ON usage_counters(scope, period);
|
|
796
|
-
|
|
810
|
+
-- NOTE: the (project_id) UNIQUE INDEX that used to live here was replaced by
|
|
811
|
+
-- v53's (project_id, kind) index. MIGRATION_SQL re-runs on every boot, so we
|
|
812
|
+
-- must NOT recreate the single-column index \u2014 it would conflict with v53 and
|
|
813
|
+
-- break traffic-sync schedule creation.
|
|
797
814
|
CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id);
|
|
798
815
|
|
|
799
816
|
-- Migration tracking: records which version has been applied.
|
|
@@ -1586,6 +1603,84 @@ var MIGRATION_VERSIONS = [
|
|
|
1586
1603
|
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
|
|
1587
1604
|
ON ga_ai_referrals(project_id, date, source, medium, source_dimension, channel_group, landing_page)`));
|
|
1588
1605
|
}
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
version: 51,
|
|
1609
|
+
name: "runs-source-id",
|
|
1610
|
+
statements: [
|
|
1611
|
+
`ALTER TABLE runs ADD COLUMN source_id TEXT`,
|
|
1612
|
+
`CREATE INDEX IF NOT EXISTS idx_runs_source ON runs(source_id)`
|
|
1613
|
+
]
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
version: 52,
|
|
1617
|
+
name: "traffic-sources-last-event-ids",
|
|
1618
|
+
statements: [
|
|
1619
|
+
// JSON-encoded array of normalized event IDs from the previous sync,
|
|
1620
|
+
// used for cross-sync boundary-window dedupe so a longer default
|
|
1621
|
+
// sync window (or any overlapping re-sync) cannot double-count.
|
|
1622
|
+
`ALTER TABLE traffic_sources ADD COLUMN last_event_ids TEXT`
|
|
1623
|
+
]
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
version: 53,
|
|
1627
|
+
name: "schedules-kind-and-source",
|
|
1628
|
+
// The legacy schedules table carries an inline `UNIQUE(project_id)`
|
|
1629
|
+
// constraint (see MIGRATION_SQL). SQLite doesn't support dropping inline
|
|
1630
|
+
// table constraints, so we use the canonical table-rebuild pattern:
|
|
1631
|
+
// create a new table with the desired schema, copy the data, drop the
|
|
1632
|
+
// old, rename. All 4 statements run inside the migration runner's
|
|
1633
|
+
// single transaction so a partial failure rolls everything back.
|
|
1634
|
+
statements: [
|
|
1635
|
+
// (project_id, kind) uniqueness is enforced by the explicit
|
|
1636
|
+
// `CREATE UNIQUE INDEX idx_schedules_project_kind` below — that's the
|
|
1637
|
+
// canonical drizzle-side index name (see schema.ts), so don't duplicate
|
|
1638
|
+
// it as an inline UNIQUE() in CREATE TABLE.
|
|
1639
|
+
`CREATE TABLE IF NOT EXISTS schedules_v53 (
|
|
1640
|
+
id TEXT PRIMARY KEY,
|
|
1641
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1642
|
+
kind TEXT NOT NULL DEFAULT 'answer-visibility',
|
|
1643
|
+
cron_expr TEXT NOT NULL,
|
|
1644
|
+
preset TEXT,
|
|
1645
|
+
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
1646
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1647
|
+
providers TEXT NOT NULL DEFAULT '[]',
|
|
1648
|
+
source_id TEXT,
|
|
1649
|
+
last_run_at TEXT,
|
|
1650
|
+
next_run_at TEXT,
|
|
1651
|
+
created_at TEXT NOT NULL,
|
|
1652
|
+
updated_at TEXT NOT NULL
|
|
1653
|
+
)`,
|
|
1654
|
+
`INSERT INTO schedules_v53 (
|
|
1655
|
+
id, project_id, kind, cron_expr, preset, timezone, enabled,
|
|
1656
|
+
providers, source_id, last_run_at, next_run_at, created_at, updated_at
|
|
1657
|
+
)
|
|
1658
|
+
SELECT id, project_id, 'answer-visibility', cron_expr, preset, timezone, enabled,
|
|
1659
|
+
providers, NULL, last_run_at, next_run_at, created_at, updated_at
|
|
1660
|
+
FROM schedules`,
|
|
1661
|
+
`DROP TABLE schedules`,
|
|
1662
|
+
`ALTER TABLE schedules_v53 RENAME TO schedules`,
|
|
1663
|
+
// The legacy single-column unique index doesn't survive the table
|
|
1664
|
+
// rename, but explicitly DROP IF EXISTS to keep the migration
|
|
1665
|
+
// idempotent across edge-case re-runs.
|
|
1666
|
+
`DROP INDEX IF EXISTS idx_schedules_project`,
|
|
1667
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project_kind ON schedules(project_id, kind)`
|
|
1668
|
+
]
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
version: 54,
|
|
1672
|
+
name: "drop-resurrected-schedules-project-index",
|
|
1673
|
+
// v53 dropped `idx_schedules_project`, but `MIGRATION_SQL` (which runs on
|
|
1674
|
+
// every boot, before versioned migrations) was still creating it. On any
|
|
1675
|
+
// boot AFTER the one that applied v53, Phase 1 re-created the legacy
|
|
1676
|
+
// single-column UNIQUE index, which then collided with the new
|
|
1677
|
+
// (project_id, kind) semantics and broke traffic-sync schedule creation
|
|
1678
|
+
// (`UNIQUE constraint failed: schedules.project_id`). MIGRATION_SQL no
|
|
1679
|
+
// longer creates that index; this migration removes it from any DB that
|
|
1680
|
+
// already booted past v53 with the resurrected index.
|
|
1681
|
+
statements: [
|
|
1682
|
+
`DROP INDEX IF EXISTS idx_schedules_project`
|
|
1683
|
+
]
|
|
1589
1684
|
}
|
|
1590
1685
|
];
|
|
1591
1686
|
function isDuplicateColumnError(err) {
|
|
@@ -1022,25 +1022,35 @@ var snapshotReportSchema = z9.object({
|
|
|
1022
1022
|
|
|
1023
1023
|
// ../contracts/src/schedule.ts
|
|
1024
1024
|
import { z as z10 } from "zod";
|
|
1025
|
+
var schedulableRunKindSchema = z10.enum(["answer-visibility", "traffic-sync"]);
|
|
1026
|
+
var SchedulableRunKinds = schedulableRunKindSchema.enum;
|
|
1025
1027
|
var scheduleDtoSchema = z10.object({
|
|
1026
1028
|
id: z10.string(),
|
|
1027
1029
|
projectId: z10.string(),
|
|
1030
|
+
/** Run kind dispatched when this schedule fires. Defaults to 'answer-visibility' for legacy rows. */
|
|
1031
|
+
kind: schedulableRunKindSchema,
|
|
1028
1032
|
cronExpr: z10.string(),
|
|
1029
1033
|
preset: z10.string().nullable().optional(),
|
|
1030
1034
|
timezone: z10.string().default("UTC"),
|
|
1031
1035
|
enabled: z10.boolean().default(true),
|
|
1032
1036
|
providers: z10.array(providerNameSchema).default([]),
|
|
1037
|
+
/** Traffic-source UUID for `kind === 'traffic-sync'` schedules. Null otherwise. */
|
|
1038
|
+
sourceId: z10.string().nullable().optional(),
|
|
1033
1039
|
lastRunAt: z10.string().nullable().optional(),
|
|
1034
1040
|
nextRunAt: z10.string().nullable().optional(),
|
|
1035
1041
|
createdAt: z10.string(),
|
|
1036
1042
|
updatedAt: z10.string()
|
|
1037
1043
|
});
|
|
1038
1044
|
var scheduleUpsertRequestSchema = z10.object({
|
|
1045
|
+
/** Run kind. Defaults to 'answer-visibility' so existing callers don't have to change. */
|
|
1046
|
+
kind: schedulableRunKindSchema.optional(),
|
|
1039
1047
|
preset: z10.string().optional(),
|
|
1040
1048
|
cron: z10.string().optional(),
|
|
1041
1049
|
timezone: z10.string().optional().default("UTC"),
|
|
1042
1050
|
enabled: z10.boolean().optional().default(true),
|
|
1043
|
-
providers: z10.array(providerNameSchema).optional().default([])
|
|
1051
|
+
providers: z10.array(providerNameSchema).optional().default([]),
|
|
1052
|
+
/** Required when kind === 'traffic-sync'. Forbidden for other kinds. Validated server-side. */
|
|
1053
|
+
sourceId: z10.string().optional()
|
|
1044
1054
|
}).refine(
|
|
1045
1055
|
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
1046
1056
|
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
@@ -2251,6 +2261,65 @@ var trafficSyncResponseSchema = z20.object({
|
|
|
2251
2261
|
windowStart: z20.string(),
|
|
2252
2262
|
windowEnd: z20.string()
|
|
2253
2263
|
});
|
|
2264
|
+
var trafficSourceTotalsSchema = z20.object({
|
|
2265
|
+
crawlerHits: z20.number().int().nonnegative(),
|
|
2266
|
+
aiReferralHits: z20.number().int().nonnegative(),
|
|
2267
|
+
sampleCount: z20.number().int().nonnegative()
|
|
2268
|
+
});
|
|
2269
|
+
var trafficSourceListResponseSchema = z20.object({
|
|
2270
|
+
sources: z20.array(trafficSourceDtoSchema)
|
|
2271
|
+
});
|
|
2272
|
+
var trafficSourceDetailDtoSchema = trafficSourceDtoSchema.extend({
|
|
2273
|
+
totals24h: trafficSourceTotalsSchema,
|
|
2274
|
+
latestRun: z20.object({
|
|
2275
|
+
runId: z20.string(),
|
|
2276
|
+
status: runStatusSchema,
|
|
2277
|
+
startedAt: z20.string().nullable(),
|
|
2278
|
+
finishedAt: z20.string().nullable(),
|
|
2279
|
+
error: z20.string().nullable()
|
|
2280
|
+
}).nullable()
|
|
2281
|
+
});
|
|
2282
|
+
var trafficStatusResponseSchema = z20.object({
|
|
2283
|
+
sources: z20.array(trafficSourceDetailDtoSchema)
|
|
2284
|
+
});
|
|
2285
|
+
var trafficEventKindSchema = z20.enum(["crawler", "ai-referral"]);
|
|
2286
|
+
var TrafficEventKinds = trafficEventKindSchema.enum;
|
|
2287
|
+
var trafficCrawlerEventEntrySchema = z20.object({
|
|
2288
|
+
kind: z20.literal(TrafficEventKinds.crawler),
|
|
2289
|
+
sourceId: z20.string(),
|
|
2290
|
+
tsHour: z20.string(),
|
|
2291
|
+
botId: z20.string(),
|
|
2292
|
+
operator: z20.string(),
|
|
2293
|
+
verificationStatus: z20.string(),
|
|
2294
|
+
pathNormalized: z20.string(),
|
|
2295
|
+
status: z20.number().int(),
|
|
2296
|
+
hits: z20.number().int().nonnegative()
|
|
2297
|
+
});
|
|
2298
|
+
var trafficAiReferralEventEntrySchema = z20.object({
|
|
2299
|
+
kind: z20.literal(TrafficEventKinds["ai-referral"]),
|
|
2300
|
+
sourceId: z20.string(),
|
|
2301
|
+
tsHour: z20.string(),
|
|
2302
|
+
product: z20.string(),
|
|
2303
|
+
operator: z20.string(),
|
|
2304
|
+
sourceDomain: z20.string(),
|
|
2305
|
+
evidenceType: z20.string(),
|
|
2306
|
+
landingPathNormalized: z20.string(),
|
|
2307
|
+
status: z20.number().int(),
|
|
2308
|
+
hits: z20.number().int().nonnegative()
|
|
2309
|
+
});
|
|
2310
|
+
var trafficEventEntrySchema = z20.discriminatedUnion("kind", [
|
|
2311
|
+
trafficCrawlerEventEntrySchema,
|
|
2312
|
+
trafficAiReferralEventEntrySchema
|
|
2313
|
+
]);
|
|
2314
|
+
var trafficEventsResponseSchema = z20.object({
|
|
2315
|
+
windowStart: z20.string(),
|
|
2316
|
+
windowEnd: z20.string(),
|
|
2317
|
+
totals: z20.object({
|
|
2318
|
+
crawlerHits: z20.number().int().nonnegative(),
|
|
2319
|
+
aiReferralHits: z20.number().int().nonnegative()
|
|
2320
|
+
}),
|
|
2321
|
+
events: z20.array(trafficEventEntrySchema)
|
|
2322
|
+
});
|
|
2254
2323
|
|
|
2255
2324
|
// ../contracts/src/formatting.ts
|
|
2256
2325
|
function formatRatio(value) {
|
|
@@ -2347,6 +2416,8 @@ export {
|
|
|
2347
2416
|
formatRunErrorOneLine,
|
|
2348
2417
|
snapshotRequestSchema,
|
|
2349
2418
|
resolveSnapshotRequestQueries,
|
|
2419
|
+
schedulableRunKindSchema,
|
|
2420
|
+
SchedulableRunKinds,
|
|
2350
2421
|
scheduleUpsertRequestSchema,
|
|
2351
2422
|
parseWindow,
|
|
2352
2423
|
windowCutoff,
|
|
@@ -2389,6 +2460,9 @@ export {
|
|
|
2389
2460
|
TrafficEventConfidences,
|
|
2390
2461
|
TrafficSourceStatuses,
|
|
2391
2462
|
TrafficSourceAuthModes,
|
|
2463
|
+
trafficConnectCloudRunRequestSchema,
|
|
2464
|
+
trafficEventKindSchema,
|
|
2465
|
+
TrafficEventKinds,
|
|
2392
2466
|
formatRatio,
|
|
2393
2467
|
formatNumber,
|
|
2394
2468
|
formatDate,
|