@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/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-B6Mi9Fd1.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-D0EPNRDs.css">
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
- scheduleUpsertRequestSchema
15
- } from "./chunk-6QTH5NS5.js";
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
- return this.request("GET", `/projects/${encodeURIComponent(project)}/schedule`);
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
- await this.request("DELETE", `/projects/${encodeURIComponent(project)}/schedule`);
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: "Get the scheduled run configuration for a Canonry project.",
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: projectInputSchema,
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: "Delete the scheduled run configuration for a Canonry project.",
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: projectInputSchema,
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-6QTH5NS5.js";
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("idx_schedules_project").on(table.projectId)
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
- CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id);
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,