@buildinternet/releases-api-types 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/releases-api-types",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,10 @@
16
16
  "src",
17
17
  "README.md"
18
18
  ],
19
+ "dependencies": {
20
+ "@buildinternet/releases-core": "^0.18.0",
21
+ "zod": "^4.3.6"
22
+ },
19
23
  "publishConfig": {
20
24
  "access": "public"
21
25
  }
package/src/api-types.ts CHANGED
@@ -1,44 +1,132 @@
1
1
  /**
2
- * Shared API response types.
3
- *
4
- * This is the single source of truth for all API response shapes.
5
- * Consumed by: web frontend, CLI client, and (optionally) API worker routes.
2
+ * Shared API response types — single source of truth for the wire protocol.
3
+ * Consumed by: web frontend, MCP worker, OSS CLI, and the API worker.
6
4
  */
7
5
 
6
+ import type { z } from "zod";
7
+
8
+ export type {
9
+ SourceType,
10
+ SourceDiscovery,
11
+ SourceFetchPriority,
12
+ } from "@buildinternet/releases-core/source-enums";
13
+ import type {
14
+ MediaItemSchema,
15
+ PaginationSchema,
16
+ StatsSchema,
17
+ ErrorResponseSchema,
18
+ ReleaseTypeSchema,
19
+ ReleaseItemSchema,
20
+ ReleaseSummaryItemSchema,
21
+ OverviewPageItemSchema,
22
+ } from "./schemas/shared.js";
23
+ import type {
24
+ OrgListItemSchema,
25
+ OrgListResponseSchema,
26
+ OrgAccountItemSchema,
27
+ OrgAccountsResponseSchema,
28
+ OrgTagsResponseSchema,
29
+ CreateOrgBodySchema,
30
+ UpdateOrgBodySchema,
31
+ OrgDetailSchema,
32
+ } from "./schemas/orgs.js";
33
+ import type {
34
+ SourceListItemSchema,
35
+ SourceWithOrgSchema,
36
+ SourceListResponseSchema,
37
+ SourceListResultSchema,
38
+ SourceDetailSchema,
39
+ SourceMutationResponseSchema,
40
+ SourceOrgRefSchema,
41
+ SourcePatchInputSchema,
42
+ CreateSourceBodySchema,
43
+ ChangelogFileSummarySchema,
44
+ SourceChangelogResponseSchema,
45
+ } from "./schemas/sources.js";
46
+ import type {
47
+ ProductRowSchema,
48
+ ProductListItemSchema,
49
+ ProductListResponseSchema,
50
+ ProductDetailSourceSchema,
51
+ ProductDetailSchema,
52
+ CreateProductBodySchema,
53
+ UpdateProductBodySchema,
54
+ AdoptProductBodySchema,
55
+ ProductAdoptResultSchema,
56
+ ProductAdoptDryRunSchema,
57
+ ProductAdoptResponseSchema,
58
+ ProductDeleteResponseSchema,
59
+ } from "./schemas/products.js";
60
+
61
+ export {
62
+ MediaItemSchema,
63
+ PaginationSchema,
64
+ ListResponseSchema,
65
+ StatsSchema,
66
+ ErrorResponseSchema,
67
+ ReleaseTypeSchema,
68
+ ReleaseItemSchema,
69
+ ReleaseSummaryItemSchema,
70
+ OverviewPageItemSchema,
71
+ CategorySchema,
72
+ } from "./schemas/shared.js";
73
+ export {
74
+ OrgListItemSchema,
75
+ OrgListResponseSchema,
76
+ OrgAccountItemSchema,
77
+ OrgAccountsResponseSchema,
78
+ OrgTagsResponseSchema,
79
+ CreateOrgBodySchema,
80
+ UpdateOrgBodySchema,
81
+ OrgDetailSchema,
82
+ } from "./schemas/orgs.js";
83
+ export {
84
+ SourceListItemSchema,
85
+ SourceWithOrgSchema,
86
+ SourceListResponseSchema,
87
+ SourceListResultSchema,
88
+ SourceDetailSchema,
89
+ SourceMutationResponseSchema,
90
+ SourceOrgRefSchema,
91
+ SourcePatchInputSchema,
92
+ CreateSourceBodySchema,
93
+ ChangelogFileSummarySchema,
94
+ SourceChangelogResponseSchema,
95
+ } from "./schemas/sources.js";
96
+ export {
97
+ ProductRowSchema,
98
+ ProductListItemSchema,
99
+ ProductListResponseSchema,
100
+ ProductDetailSourceSchema,
101
+ ProductDetailSchema,
102
+ CreateProductBodySchema,
103
+ UpdateProductBodySchema,
104
+ AdoptProductBodySchema,
105
+ ProductAdoptResultSchema,
106
+ ProductAdoptDryRunSchema,
107
+ ProductAdoptResponseSchema,
108
+ ProductDeleteResponseSchema,
109
+ } from "./schemas/products.js";
110
+
8
111
  // ── Media ──
9
112
 
10
- export interface MediaItem {
11
- type: "image" | "video" | "gif";
12
- url: string;
13
- alt?: string;
14
- r2Url?: string;
15
- }
113
+ export type MediaItem = z.infer<typeof MediaItemSchema>;
16
114
 
17
115
  // ── Stats ──
18
116
 
19
- export interface Stats {
20
- orgs: number;
21
- sources: number;
22
- releases: number;
23
- products: number;
24
- }
117
+ export type Stats = z.infer<typeof StatsSchema>;
25
118
 
26
119
  // ── Pagination ──
27
120
 
28
- export interface Pagination {
29
- page: number;
30
- pageSize: number;
31
- returned: number;
32
- totalItems?: number;
33
- totalPages?: number;
34
- hasMore: boolean;
35
- }
121
+ export type Pagination = z.infer<typeof PaginationSchema>;
36
122
 
37
123
  export interface ListResponse<T> {
38
124
  items: T[];
39
125
  pagination: Pagination;
40
126
  }
41
127
 
128
+ export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
129
+
42
130
  // ── Sitemap (bulk URL emission) ──
43
131
 
44
132
  export interface SitemapPayload {
@@ -49,227 +137,29 @@ export interface SitemapPayload {
49
137
 
50
138
  // ── Organizations ──
51
139
 
52
- export interface OrgListItem {
53
- slug: string;
54
- name: string;
55
- domain: string | null;
56
- avatarUrl: string | null;
57
- githubHandle: string | null;
58
- sourceCount: number;
59
- releaseCount: number;
60
- recentReleaseCount: number;
61
- lastActivity: string | null;
62
- topProducts: string[];
63
- sparkline: number[];
64
- }
65
-
66
- export type OrgListResponse = ListResponse<OrgListItem>;
140
+ export type OrgListItem = z.infer<typeof OrgListItemSchema>;
141
+ export type OrgListResponse = z.infer<typeof OrgListResponseSchema>;
142
+ export type OrgAccountItem = z.infer<typeof OrgAccountItemSchema>;
143
+ export type OrgAccountsResponse = z.infer<typeof OrgAccountsResponseSchema>;
144
+ export type OrgTagsResponse = z.infer<typeof OrgTagsResponseSchema>;
145
+ export type CreateOrgBody = z.infer<typeof CreateOrgBodySchema>;
146
+ export type UpdateOrgBody = z.infer<typeof UpdateOrgBodySchema>;
67
147
 
68
- export interface OrgAccountItem {
69
- platform: string;
70
- handle: string;
71
- }
72
-
73
- export type OrgAccountsResponse = ListResponse<OrgAccountItem>;
74
- export type OrgTagsResponse = ListResponse<string>;
75
-
76
- export interface OrgDetail {
77
- id?: string;
78
- slug: string;
79
- name: string;
80
- domain: string | null;
81
- description?: string | null;
82
- category?: string | null;
83
- avatarUrl: string | null;
84
- tags?: string[];
85
- sourceCount: number;
86
- releaseCount: number;
87
- releasesLast30Days: number;
88
- avgReleasesPerWeek: number;
89
- lastFetchedAt: string | null;
90
- lastPolledAt: string | null;
91
- trackingSince: string;
92
- aliases?: string[];
93
- accounts: OrgAccountItem[];
94
- products: Array<{
95
- id: string;
96
- slug: string;
97
- name: string;
98
- url: string | null;
99
- description: string | null;
100
- sourceCount: number;
101
- }>;
102
- sources: SourceListItem[];
103
- overview?: OverviewPageItem | null;
104
- playbook?: { scope: "playbook"; content: string; updatedAt: string } | null;
105
- }
148
+ export type OrgDetail = z.infer<typeof OrgDetailSchema>;
106
149
 
107
150
  // ── Sources ──
108
151
 
109
- export interface SourceListItem {
110
- slug: string;
111
- name: string;
112
- type: string;
113
- url?: string;
114
- orgSlug?: string | null;
115
- releaseCount: number;
116
- latestVersion: string | null;
117
- latestDate: string | null;
118
- latestAddedAt?: string | null;
119
- isPrimary?: boolean;
120
- isHidden?: boolean;
121
- /**
122
- * How the row was created. Optional on the wire so older API responses
123
- * (mid-deploy or pinned old workers) degrade gracefully — consumers that
124
- * see `undefined` should treat it as `"curated"`.
125
- */
126
- discovery?: "curated" | "agent" | "on_demand";
127
- fetchPriority?: "normal" | "low" | "paused" | null;
128
- lastFetchedAt?: string | null;
129
- lastPolledAt?: string | null;
130
- changeDetectedAt?: string | null;
131
- consecutiveNoChange?: number | null;
132
- consecutiveErrors?: number | null;
133
- nextFetchAfter?: string | null;
134
- medianGapDays?: number | null;
135
- lastRetieredAt?: string | null;
136
- metadata?: string | null;
137
- productName?: string | null;
138
- productSlug?: string | null;
139
- }
140
-
141
- /**
142
- * Canonical shape returned by GET /v1/sources (list) and used by both
143
- * local-mode queries and the remote API client. Superset of SourceListItem
144
- * with required fields (id, orgName, orgSlug) that the CLI `list` command needs.
145
- */
146
- export interface SourceWithOrg {
147
- id: string;
148
- name: string;
149
- slug: string;
150
- type: string;
151
- url: string;
152
- orgName: string | null;
153
- orgSlug: string | null;
154
- productName: string | null;
155
- productSlug: string | null;
156
- isPrimary: boolean;
157
- isHidden: boolean | null;
158
- discovery?: "curated" | "agent" | "on_demand";
159
- metadata: string | null;
160
- releaseCount: number;
161
- latestVersion: string | null;
162
- latestDate: string | null;
163
- lastFetchedAt: string | null;
164
- lastPolledAt: string | null;
165
- fetchPriority: string | null;
166
- changeDetectedAt: string | null;
167
- consecutiveNoChange: number | null;
168
- consecutiveErrors: number | null;
169
- nextFetchAfter: string | null;
170
- medianGapDays: number | null;
171
- lastRetieredAt: string | null;
172
- }
173
-
174
- /** Fields accepted by PATCH /v1/sources/:slug. */
175
- export interface SourcePatchInput {
176
- name?: string;
177
- url?: string;
178
- type?: string;
179
- slug?: string;
180
- metadata?: string;
181
- orgId?: string | null;
182
- productId?: string | null;
183
- lastFetchedAt?: string | null;
184
- lastContentHash?: string | null;
185
- fetchPriority?: string;
186
- consecutiveNoChange?: number;
187
- consecutiveErrors?: number;
188
- nextFetchAfter?: string | null;
189
- isPrimary?: boolean;
190
- isHidden?: boolean;
191
- changeDetectedAt?: string | null;
192
- lastPolledAt?: string | null;
193
- }
194
-
195
- /** Lightweight summary of a changelog file — used for the file index. */
196
- export interface ChangelogFileSummary {
197
- path: string;
198
- filename: string;
199
- url: string;
200
- bytes: number;
201
- fetchedAt: string;
202
- }
203
-
204
- export interface SourceChangelogResponse {
205
- path: string;
206
- filename: string;
207
- url: string;
208
- rawUrl: string;
209
- content: string;
210
- bytes: number;
211
- fetchedAt: string;
212
- /** Character offset of the first character in `content` within the full file. */
213
- offset: number;
214
- /** The limit (in chars) that was applied to produce this slice. */
215
- limit: number;
216
- /** Next offset to request for the next slice, or null if `content` is the tail. */
217
- nextOffset: number | null;
218
- /** Total length of the full file in characters. */
219
- totalChars: number;
220
- /** Requested token budget when in token mode (cl100k_base). */
221
- tokens?: number;
222
- /** Encoded token count of the returned `content`. Set in token mode. */
223
- sliceTokens?: number;
224
- /** Full-file token count (cl100k_base). Always populated. */
225
- totalTokens: number;
226
- /** True when the upstream file exceeded the 1MB cap and content was sliced. */
227
- truncated: boolean;
228
- /** Byte offset where the file was truncated, or null when not truncated. */
229
- truncatedAt: number | null;
230
- /**
231
- * Index of every changelog file tracked for this source (root plus any
232
- * discovered per-package files). Always present even for single-file
233
- * sources so clients can lazily render a file picker.
234
- */
235
- files: ChangelogFileSummary[];
236
- }
237
-
238
- export interface SourceDetail {
239
- slug: string;
240
- name: string;
241
- type: string;
242
- url: string;
243
- /**
244
- * Hidden sources are reachable by direct URL but excluded from listings,
245
- * sitemap, and AI features. Set on on-demand lookups and admin-suppressed
246
- * rows; absent on canonical curated/agent sources.
247
- */
248
- isHidden?: boolean;
249
- /**
250
- * Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
251
- * `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
252
- * Optional for graceful degradation against older API responses.
253
- */
254
- discovery?: "curated" | "agent" | "on_demand";
255
- changelogUrl?: string | null;
256
- hasChangelogFile?: boolean;
257
- org: { slug: string; name: string } | null;
258
- releaseCount: number;
259
- releasesLast30Days: number;
260
- avgReleasesPerWeek: number;
261
- latestVersion: string | null;
262
- latestDate: string | null;
263
- lastFetchedAt: string | null;
264
- lastPolledAt: string | null;
265
- trackingSince: string;
266
- releases: ReleaseItem[];
267
- pagination: Pagination;
268
- summaries: {
269
- rolling: ReleaseSummaryItem | null;
270
- monthly: ReleaseSummaryItem[];
271
- };
272
- }
152
+ export type SourceListItem = z.infer<typeof SourceListItemSchema>;
153
+ export type SourceWithOrg = z.infer<typeof SourceWithOrgSchema>;
154
+ export type SourceListResponse = z.infer<typeof SourceListResponseSchema>;
155
+ export type SourceListResult = z.infer<typeof SourceListResultSchema>;
156
+ export type SourcePatchInput = z.infer<typeof SourcePatchInputSchema>;
157
+ export type CreateSourceBody = z.infer<typeof CreateSourceBodySchema>;
158
+ export type ChangelogFileSummary = z.infer<typeof ChangelogFileSummarySchema>;
159
+ export type SourceChangelogResponse = z.infer<typeof SourceChangelogResponseSchema>;
160
+ export type SourceDetail = z.infer<typeof SourceDetailSchema>;
161
+ export type SourceMutationResponse = z.infer<typeof SourceMutationResponseSchema>;
162
+ export type SourceOrgRef = z.infer<typeof SourceOrgRefSchema>;
273
163
 
274
164
  // ── Admin telemetry: orgs rollup ──
275
165
 
@@ -305,20 +195,9 @@ export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
305
195
  * Optional on the wire so older API responses (mid-deploy or pinned old workers)
306
196
  * degrade gracefully — consumers that see `undefined` should treat it as `"feature"`.
307
197
  */
308
- export type ReleaseType = "feature" | "rollup";
198
+ export type ReleaseType = z.infer<typeof ReleaseTypeSchema>;
309
199
 
310
- export interface ReleaseItem {
311
- id?: string;
312
- version: string | null;
313
- title: string;
314
- summary: string;
315
- content?: string;
316
- publishedAt: string | null;
317
- url: string | null;
318
- media?: MediaItem[];
319
- /** Release type. See {@link ReleaseType}. */
320
- type?: ReleaseType;
321
- }
200
+ export type ReleaseItem = z.infer<typeof ReleaseItemSchema>;
322
201
 
323
202
  export interface ReleaseDetail {
324
203
  id: string;
@@ -352,14 +231,7 @@ export type ReleaseCoverageResponse =
352
231
  | { role: "coverage"; canonical: ReleaseCoverageRow; covers: [] }
353
232
  | { role: "canonical"; canonical: null; covers: ReleaseCoverageRow[] };
354
233
 
355
- export interface ReleaseSummaryItem {
356
- year?: number | null;
357
- month?: number | null;
358
- windowDays?: number | null;
359
- summary: string;
360
- releaseCount: number;
361
- generatedAt: string;
362
- }
234
+ export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
363
235
 
364
236
  // ── Search ──
365
237
 
@@ -497,6 +369,50 @@ export interface SearchChunkHit {
497
369
  score: number;
498
370
  }
499
371
 
372
+ // ── Lookup by domain (entity resolution) ──
373
+
374
+ /**
375
+ * Returned by GET /v1/lookups/by-domain. Pure resolution: the server
376
+ * normalizes the input domain, exact-matches against
377
+ * `organizations.domain` (primary) and `domain_aliases.domain` (alias for
378
+ * either an org or a product), and returns whatever it finds. Unknown
379
+ * domains 404 — there is no on-demand probing for domains, unlike the
380
+ * GitHub coordinate path.
381
+ */
382
+ export interface DomainLookupResponse {
383
+ /** Normalized domain that was matched (lowercased, no scheme/path/www). */
384
+ domain: string;
385
+ /**
386
+ * Owning organization, if one exists. `matchedVia` distinguishes a hit
387
+ * on `organizations.domain` (`"primary"`) from an alias hit
388
+ * (`"alias"`) so the UI can render the difference.
389
+ */
390
+ org: {
391
+ id: string;
392
+ slug: string;
393
+ name: string;
394
+ domain: string | null;
395
+ description: string | null;
396
+ category: string | null;
397
+ avatarUrl: string | null;
398
+ matchedVia: "primary" | "alias";
399
+ } | null;
400
+ /**
401
+ * Products whose alias points at this domain. Multiple are possible
402
+ * when an org owns several products that all set the same alias —
403
+ * unusual, but allowed.
404
+ */
405
+ products: Array<{
406
+ id: string;
407
+ slug: string;
408
+ name: string;
409
+ orgId: string;
410
+ orgSlug: string;
411
+ orgName: string;
412
+ category: string | null;
413
+ }>;
414
+ }
415
+
500
416
  // ── Lookup (on-demand GitHub index) ──
501
417
 
502
418
  export type LookupStatus = "indexed" | "existing" | "empty" | "not_found" | "deferred";
@@ -535,6 +451,21 @@ export interface LookupResultPayload {
535
451
 
536
452
  export interface UnifiedSearchResponse {
537
453
  query: string;
454
+ /**
455
+ * Normalized form of `?domain=` when the caller passed it. The server
456
+ * does the normalization once and echoes the canonical form back so
457
+ * clients don't have to re-normalize for analytics or display. Absent
458
+ * when no domain filter was applied.
459
+ */
460
+ domain?: string;
461
+ /**
462
+ * Outcome of the `?domain=` resolution step. `"matched"` means the
463
+ * domain resolved to a known org and results below are scoped to it.
464
+ * `"not_found"` means the domain didn't match anything; in that case
465
+ * orgs/catalog/releases will all be empty arrays — the caller can
466
+ * distinguish "no hits in scope" from "scope didn't exist."
467
+ */
468
+ domainStatus?: "matched" | "not_found";
538
469
  orgs: SearchOrgHit[];
539
470
  /** Products and standalone sources folded into a single list. */
540
471
  catalog: SearchCatalogHit[];
@@ -559,16 +490,7 @@ export interface UnifiedSearchResponse {
559
490
 
560
491
  // ── Overview Pages ──
561
492
 
562
- export interface OverviewPageItem {
563
- scope: "org" | "product";
564
- orgSlug?: string | null;
565
- productSlug?: string | null;
566
- content: string;
567
- releaseCount: number;
568
- lastContributingReleaseAt: string | null;
569
- generatedAt: string;
570
- updatedAt: string;
571
- }
493
+ export type OverviewPageItem = z.infer<typeof OverviewPageItemSchema>;
572
494
 
573
495
  /** @deprecated Use OverviewPageItem */
574
496
  export type KnowledgePageItem = OverviewPageItem;
@@ -698,41 +620,29 @@ export interface OrgReleasesResponse {
698
620
  pagination: { nextCursor: string | null; limit: number };
699
621
  }
700
622
 
701
- // ── Products ──
702
-
703
- export interface ProductListItem {
704
- id: string;
705
- name: string;
706
- slug: string;
707
- orgId: string;
708
- url: string | null;
709
- description: string | null;
710
- category: string | null;
711
- createdAt: string;
712
- sourceCount: number;
623
+ // Source-scoped release feed — the source identity is encoded in the URL, so
624
+ // items omit the redundant `source` block carried by the org feed.
625
+ export interface SourceReleasesResponse {
626
+ releases: ReleaseItem[];
627
+ pagination: { nextCursor: string | null; limit: number };
713
628
  }
714
629
 
715
- export type ProductListResponse = ListResponse<ProductListItem>;
716
-
717
- export interface ProductDetail {
718
- id: string;
719
- name: string;
720
- slug: string;
721
- orgId: string;
722
- url: string | null;
723
- description: string | null;
724
- category: string | null;
725
- createdAt: string;
726
- sources: Array<{ id: string; slug: string; name: string; type: string; url: string }>;
727
- tags: string[];
728
- }
630
+ // ── Products ──
729
631
 
730
- export interface ProductAdoptResult {
731
- product: ProductDetail;
732
- sourcesMoved: number;
733
- accountsMoved: number;
734
- sourceOrgDeleted: string;
735
- }
632
+ // `Category` lives in @buildinternet/releases-core/categories — import it from
633
+ // there. CategorySchema is re-exported here for OpenAPI / Zod consumers only.
634
+ export type ProductRow = z.infer<typeof ProductRowSchema>;
635
+ export type ProductListItem = z.infer<typeof ProductListItemSchema>;
636
+ export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
637
+ export type ProductDetailSource = z.infer<typeof ProductDetailSourceSchema>;
638
+ export type ProductDetail = z.infer<typeof ProductDetailSchema>;
639
+ export type CreateProductBody = z.infer<typeof CreateProductBodySchema>;
640
+ export type UpdateProductBody = z.infer<typeof UpdateProductBodySchema>;
641
+ export type AdoptProductBody = z.infer<typeof AdoptProductBodySchema>;
642
+ export type ProductAdoptResult = z.infer<typeof ProductAdoptResultSchema>;
643
+ export type ProductAdoptDryRun = z.infer<typeof ProductAdoptDryRunSchema>;
644
+ export type ProductAdoptResponse = z.infer<typeof ProductAdoptResponseSchema>;
645
+ export type ProductDeleteResponse = z.infer<typeof ProductDeleteResponseSchema>;
736
646
 
737
647
  // ── Taxonomy (categories + tags) ──
738
648
 
@@ -877,8 +787,28 @@ export interface Session {
877
787
  sessionId: string;
878
788
  company: string;
879
789
  type: "onboard" | "update";
790
+ /**
791
+ * Sub-agent **role** label that ran this session — `"coordinator"` is the
792
+ * parent orchestrator (typically Sonnet) when a multi-agent session
793
+ * delegates, `"sonnet"` / `"haiku"` are direct single-agent runs.
794
+ * Surfaces on the detail GET.
795
+ *
796
+ * This is a logical role label, not the runtime model identifier. The
797
+ * resolved Anthropic model string (e.g. `claude-sonnet-4-6`,
798
+ * `claude-haiku-4-5`) lives on `usage.model` when the session reported
799
+ * one — consult that field for the concrete model.
800
+ */
801
+ agent?: "sonnet" | "haiku" | "coordinator";
802
+ /** Identifies the client that started this session (e.g. hostname). */
803
+ runner?: string;
804
+ /** Correlation ID for end-to-end tracing across CLI → API → managed agent. */
805
+ correlationId?: string;
806
+ /** Anthropic session ID for linking to console logs. */
807
+ anthropicSessionId?: string;
880
808
  status: "running" | "complete" | "error" | "cancelled";
881
809
  step?: string;
810
+ sourcesFound?: number;
811
+ sourcesValidated?: number;
882
812
  totalSources?: number;
883
813
  sourcesFetched?: number;
884
814
  releasesFound?: number;
@@ -895,6 +825,40 @@ export interface Session {
895
825
  stopReason?: string;
896
826
  /** Number of provider `session.error` events observed before terminal. */
897
827
  retryCount?: number;
828
+ /** Non-fatal warnings collected during the session. */
829
+ warnings?: string[];
830
+ /**
831
+ * Token usage + estimated cost from the managed-agents session. `estimatedUsd`
832
+ * is a snapshot of Anthropic list prices at session-completion time.
833
+ */
834
+ usage?: {
835
+ inputTokens?: number;
836
+ outputTokens?: number;
837
+ cacheWriteTokens?: number;
838
+ cacheReadTokens?: number;
839
+ model?: string;
840
+ estimatedUsd?: number;
841
+ };
842
+ /**
843
+ * Final agent-reported state for terminal `onboard` sessions — the JSON
844
+ * blob the agent passed to its `releases_report_state` tool, plus an
845
+ * `agentSessionId` field stitched in server-side. Empty on `update`
846
+ * sessions and pre-report errors. Typed as `Record<string, unknown>`
847
+ * because the shape is owned by the discovery system prompt and may
848
+ * grow new fields without a wire bump; the keys produced today are:
849
+ *
850
+ * - `product` (`string`) — company name
851
+ * - `domain` (`string | null`) — discovered domain
852
+ * - `githubOrg` (`string | null`) — discovered GitHub org
853
+ * - `startedAt`, `updatedAt` (ISO strings)
854
+ * - `status` (e.g. `"awaiting_review"`)
855
+ * - `sources` (`Array<{ url, type, slug, label, confidence,
856
+ * validated, validationError?, releaseCount, releasesFetched,
857
+ * fetched, contentDepth }>`)
858
+ * - `agentSessionId` (`string`) — Anthropic session ID, useful for
859
+ * cross-referencing console logs
860
+ */
861
+ result?: Record<string, unknown>;
898
862
  activeSources?: string[];
899
863
  cancelRequested?: boolean;
900
864
  }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { CategorySchema, ListResponseSchema, OverviewPageItemSchema } from "./shared.js";
3
+ import { SourceListItemSchema } from "./sources.js";
4
+ import { ProductListItemSchema } from "./products.js";
5
+
6
+ export const OrgListItemSchema = z.object({
7
+ id: z.string(),
8
+ slug: z.string(),
9
+ name: z.string(),
10
+ domain: z.string().nullable(),
11
+ description: z.string().nullable(),
12
+ category: CategorySchema.nullable(),
13
+ avatarUrl: z.string().nullable(),
14
+ sourceCount: z.number().int().min(0),
15
+ releaseCount: z.number().int().min(0),
16
+ recentReleaseCount: z.number().int().min(0),
17
+ lastActivity: z.string().nullable(),
18
+ topProducts: z.array(z.string()),
19
+ sparkline: z.array(z.number().int().min(0)).length(30),
20
+ });
21
+
22
+ export const OrgListResponseSchema = ListResponseSchema(OrgListItemSchema);
23
+
24
+ export const OrgAccountItemSchema = z.object({
25
+ platform: z.string(),
26
+ handle: z.string(),
27
+ });
28
+
29
+ export const OrgAccountsResponseSchema = ListResponseSchema(OrgAccountItemSchema);
30
+ export const OrgTagsResponseSchema = ListResponseSchema(z.string());
31
+
32
+ export const CreateOrgBodySchema = z.object({
33
+ name: z.string().min(1),
34
+ slug: z.string().optional(),
35
+ domain: z.string().optional(),
36
+ description: z.string().optional(),
37
+ category: CategorySchema.optional(),
38
+ tags: z.array(z.string()).optional(),
39
+ });
40
+
41
+ export const UpdateOrgBodySchema = z.object({
42
+ name: z.string().optional(),
43
+ slug: z.string().optional(),
44
+ domain: z.string().nullable().optional(),
45
+ description: z.string().nullable().optional(),
46
+ category: CategorySchema.nullable().optional(),
47
+ tags: z.array(z.string()).optional(),
48
+ aliases: z.array(z.string()).optional(),
49
+ });
50
+
51
+ // Org detail's products query selects a strict subset of `ProductListItem` —
52
+ // no category/orgId/createdAt because the parent already names the org and
53
+ // the org detail UI doesn't surface those columns.
54
+ const OrgDetailProductSchema = ProductListItemSchema.pick({
55
+ id: true,
56
+ slug: true,
57
+ name: true,
58
+ url: true,
59
+ description: true,
60
+ sourceCount: true,
61
+ });
62
+
63
+ const OrgDetailPlaybookSchema = z.object({
64
+ scope: z.literal("playbook"),
65
+ content: z.string(),
66
+ updatedAt: z.string(),
67
+ });
68
+
69
+ export const OrgDetailSchema = z.object({
70
+ id: z.string().optional(),
71
+ slug: z.string(),
72
+ name: z.string(),
73
+ domain: z.string().nullable(),
74
+ description: z.string().nullable().optional(),
75
+ category: CategorySchema.nullable().optional(),
76
+ avatarUrl: z.string().nullable(),
77
+ tags: z.array(z.string()).optional(),
78
+ sourceCount: z.number().int().min(0),
79
+ releaseCount: z.number().int().min(0),
80
+ releasesLast30Days: z.number().int().min(0),
81
+ avgReleasesPerWeek: z.number(),
82
+ lastFetchedAt: z.string().nullable(),
83
+ lastPolledAt: z.string().nullable(),
84
+ trackingSince: z.string(),
85
+ aliases: z.array(z.string()).optional(),
86
+ accounts: z.array(OrgAccountItemSchema),
87
+ products: z.array(OrgDetailProductSchema),
88
+ sources: z.array(SourceListItemSchema),
89
+ overview: OverviewPageItemSchema.nullable().optional(),
90
+ playbook: OrgDetailPlaybookSchema.nullable().optional(),
91
+ });
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ import { CategorySchema, ListResponseSchema } from "./shared.js";
3
+ import { SourceTypeSchema } from "./sources.js";
4
+
5
+ /**
6
+ * Raw `products` table row, returned by `POST /v1/products` and
7
+ * `PATCH /v1/products/:slug`. The OSS CLI types these responses against
8
+ * `Product` from `@buildinternet/releases-core/schema` (the drizzle row
9
+ * type), so `embeddedAt` and `deletedAt` stay on the wire even though
10
+ * they're internal columns. `deletedAt` is always `null` on these paths
11
+ * (live rows only); `embeddedAt` reflects when the product was last
12
+ * indexed for semantic search.
13
+ */
14
+ export const ProductRowSchema = z.object({
15
+ id: z.string(),
16
+ name: z.string(),
17
+ slug: z.string(),
18
+ orgId: z.string(),
19
+ url: z.string().nullable(),
20
+ description: z.string().nullable(),
21
+ category: CategorySchema.nullable(),
22
+ createdAt: z.string(),
23
+ embeddedAt: z.string().nullable(),
24
+ deletedAt: z.string().nullable(),
25
+ });
26
+
27
+ /**
28
+ * Per-product row returned by `GET /v1/products`. Adds `sourceCount` to
29
+ * the row shape but omits the internal `embeddedAt` / `deletedAt` columns
30
+ * (the list handler explicitly selects only the user-facing fields).
31
+ */
32
+ export const ProductListItemSchema = z.object({
33
+ id: z.string(),
34
+ name: z.string(),
35
+ slug: z.string(),
36
+ orgId: z.string(),
37
+ url: z.string().nullable(),
38
+ description: z.string().nullable(),
39
+ category: CategorySchema.nullable(),
40
+ createdAt: z.string(),
41
+ sourceCount: z.number().int().min(0),
42
+ });
43
+
44
+ export const ProductListResponseSchema = ListResponseSchema(ProductListItemSchema);
45
+
46
+ /**
47
+ * Embedded source row returned in `ProductDetail.sources`. The detail
48
+ * handler explicitly selects only this small subset; reuses the shared
49
+ * `SourceTypeSchema` enum so source types stay in one place.
50
+ */
51
+ export const ProductDetailSourceSchema = z.object({
52
+ id: z.string(),
53
+ slug: z.string(),
54
+ name: z.string(),
55
+ type: SourceTypeSchema,
56
+ url: z.string(),
57
+ });
58
+
59
+ /**
60
+ * Returned by `GET /v1/products/:identifier` (and the org-scoped twin).
61
+ * Spreads the raw product row and adds `sources`, `tags`, `aliases`.
62
+ */
63
+ export const ProductDetailSchema = ProductRowSchema.extend({
64
+ sources: z.array(ProductDetailSourceSchema),
65
+ tags: z.array(z.string()),
66
+ aliases: z.array(z.string()),
67
+ });
68
+
69
+ /** Body accepted by `POST /v1/products`. */
70
+ export const CreateProductBodySchema = z.object({
71
+ name: z.string().min(1),
72
+ orgId: z.string().optional(),
73
+ orgSlug: z.string().optional(),
74
+ slug: z.string().optional(),
75
+ url: z.string().optional(),
76
+ description: z.string().optional(),
77
+ category: CategorySchema.optional(),
78
+ tags: z.array(z.string()).optional(),
79
+ });
80
+
81
+ /** Body accepted by `PATCH /v1/products/:slug`. */
82
+ export const UpdateProductBodySchema = z.object({
83
+ name: z.string().optional(),
84
+ url: z.string().nullable().optional(),
85
+ description: z.string().nullable().optional(),
86
+ category: CategorySchema.nullable().optional(),
87
+ tags: z.array(z.string()).optional(),
88
+ aliases: z.array(z.string()).optional(),
89
+ });
90
+
91
+ /**
92
+ * Body accepted by `POST /v1/products/adopt`.
93
+ *
94
+ * `mergeInto` (slug or `prod_…` ID) folds the source org into an existing
95
+ * product under `targetOrgSlug` instead of creating a new one. When set,
96
+ * `slug` and `url` must be omitted (the existing product's values stand).
97
+ */
98
+ export const AdoptProductBodySchema = z.object({
99
+ sourceOrgSlug: z.string().min(1),
100
+ targetOrgSlug: z.string().min(1),
101
+ slug: z.string().optional(),
102
+ url: z.string().optional(),
103
+ mergeInto: z.string().optional(),
104
+ dryRun: z.boolean().optional(),
105
+ });
106
+
107
+ /**
108
+ * Live (non-dryRun) result from `POST /v1/products/adopt`.
109
+ *
110
+ * `mergedInto` is set when the request used `mergeInto` — its value is the
111
+ * existing product's slug, signalling that no new product was created.
112
+ */
113
+ export const ProductAdoptResultSchema = z.object({
114
+ product: ProductRowSchema,
115
+ mergedInto: z.string().optional(),
116
+ sourcesMoved: z.number().int().min(0),
117
+ accountsMoved: z.number().int().min(0),
118
+ sourceOrgDeleted: z.string(),
119
+ });
120
+
121
+ /** Dry-run preview from `POST /v1/products/adopt` with `dryRun: true`. */
122
+ export const ProductAdoptDryRunSchema = z.object({
123
+ dryRun: z.literal(true),
124
+ mergeInto: z.string().optional(),
125
+ product: z.object({
126
+ name: z.string(),
127
+ slug: z.string(),
128
+ url: z.string().nullable(),
129
+ orgSlug: z.string(),
130
+ }),
131
+ sourcesToMove: z.array(z.string()),
132
+ sourceOrgToDelete: z.string(),
133
+ });
134
+
135
+ /** Response shape returned by `POST /v1/products/adopt` (union of live + dry-run). */
136
+ export const ProductAdoptResponseSchema = z.union([
137
+ ProductAdoptResultSchema,
138
+ ProductAdoptDryRunSchema,
139
+ ]);
140
+
141
+ /** Response shape returned by `DELETE /v1/products/:identifier`. */
142
+ export const ProductDeleteResponseSchema = z.object({
143
+ deleted: z.literal(true),
144
+ hard: z.literal(true).optional(),
145
+ deletedAt: z.string().optional(),
146
+ });
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { RELEASE_TYPES } from "@buildinternet/releases-core/schema";
3
+ import { CATEGORIES } from "@buildinternet/releases-core/categories";
4
+
5
+ export const CategorySchema = z.enum(CATEGORIES);
6
+
7
+ export const MediaItemSchema = z.object({
8
+ type: z.enum(["image", "video", "gif"]),
9
+ url: z.string(),
10
+ alt: z.string().optional(),
11
+ r2Url: z.string().optional(),
12
+ });
13
+
14
+ export const PaginationSchema = z.object({
15
+ page: z.number().int().min(1),
16
+ pageSize: z.number().int().min(1),
17
+ returned: z.number().int().min(0),
18
+ totalItems: z.number().int().min(0).optional(),
19
+ totalPages: z.number().int().min(0).optional(),
20
+ hasMore: z.boolean(),
21
+ });
22
+
23
+ export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
24
+ z.object({
25
+ items: z.array(item),
26
+ pagination: PaginationSchema,
27
+ });
28
+
29
+ export const ErrorResponseSchema = z.object({
30
+ error: z.string(),
31
+ message: z.string(),
32
+ });
33
+
34
+ export const StatsSchema = z.object({
35
+ orgs: z.number().int().min(0),
36
+ sources: z.number().int().min(0),
37
+ releases: z.number().int().min(0),
38
+ products: z.number().int().min(0),
39
+ });
40
+
41
+ export const ReleaseTypeSchema = z.enum(RELEASE_TYPES);
42
+
43
+ export const ReleaseItemSchema = z.object({
44
+ id: z.string().optional(),
45
+ version: z.string().nullable(),
46
+ title: z.string(),
47
+ summary: z.string(),
48
+ content: z.string().optional(),
49
+ publishedAt: z.string().nullable(),
50
+ url: z.string().nullable(),
51
+ media: z.array(MediaItemSchema).optional(),
52
+ // `.optional()` — older API responses (mid-deploy or pinned old workers)
53
+ // may omit `type`; consumers should treat `undefined` as `"feature"`.
54
+ type: ReleaseTypeSchema.optional(),
55
+ // Pre-release flag (alpha/beta/rc/preview/nightly). Computed at ingest —
56
+ // GitHub uses the API's authoritative `prerelease` field; other adapters
57
+ // fall back to a SemVer-prerelease regex. `.optional()` for the same
58
+ // older-response degrade-gracefully reason as `type`.
59
+ prerelease: z.boolean().optional(),
60
+ });
61
+
62
+ export const ReleaseSummaryItemSchema = z.object({
63
+ year: z.number().int().nullable().optional(),
64
+ month: z.number().int().nullable().optional(),
65
+ windowDays: z.number().int().nullable().optional(),
66
+ summary: z.string(),
67
+ releaseCount: z.number().int().min(0),
68
+ generatedAt: z.string(),
69
+ });
70
+
71
+ export const OverviewPageItemSchema = z.object({
72
+ scope: z.enum(["org", "product"]),
73
+ orgSlug: z.string().nullable().optional(),
74
+ productSlug: z.string().nullable().optional(),
75
+ content: z.string(),
76
+ releaseCount: z.number().int().min(0),
77
+ lastContributingReleaseAt: z.string().nullable(),
78
+ generatedAt: z.string(),
79
+ updatedAt: z.string(),
80
+ });
@@ -0,0 +1,267 @@
1
+ import { z } from "zod";
2
+ import {
3
+ SOURCE_TYPES,
4
+ SOURCE_DISCOVERY,
5
+ SOURCE_FETCH_PRIORITIES,
6
+ } from "@buildinternet/releases-core/source-enums";
7
+ import {
8
+ ListResponseSchema,
9
+ PaginationSchema,
10
+ ReleaseItemSchema,
11
+ ReleaseSummaryItemSchema,
12
+ } from "./shared.js";
13
+
14
+ export const SourceTypeSchema = z.enum(SOURCE_TYPES);
15
+ export const SourceDiscoverySchema = z.enum(SOURCE_DISCOVERY);
16
+ export const SourceFetchPrioritySchema = z.enum(SOURCE_FETCH_PRIORITIES);
17
+
18
+ /**
19
+ * Per-source row used in list/embedded contexts (e.g. nested inside OrgDetail).
20
+ * Most fields are optional — embedded shapes (org detail) only carry the
21
+ * subset needed for that view.
22
+ */
23
+ export const SourceListItemSchema = z.object({
24
+ id: z.string().optional(),
25
+ slug: z.string(),
26
+ name: z.string(),
27
+ type: SourceTypeSchema,
28
+ url: z.string().optional(),
29
+ // `orgSlug` is populated on the standalone `/v1/sources` list path; the
30
+ // `OrgDetail.sources` embedded path omits it (the parent already names the org).
31
+ orgSlug: z.string().nullable().optional(),
32
+ releaseCount: z.number().int().min(0),
33
+ latestVersion: z.string().nullable(),
34
+ latestDate: z.string().nullable(),
35
+ latestAddedAt: z.string().nullable().optional(),
36
+ isPrimary: z.boolean().optional(),
37
+ isHidden: z.boolean().optional(),
38
+ /**
39
+ * How the row was created. Optional on the wire so older API responses
40
+ * (mid-deploy or pinned old workers) degrade gracefully — consumers that
41
+ * see `undefined` should treat it as `"curated"`.
42
+ */
43
+ discovery: SourceDiscoverySchema.optional(),
44
+ fetchPriority: SourceFetchPrioritySchema.nullable().optional(),
45
+ lastFetchedAt: z.string().nullable().optional(),
46
+ lastPolledAt: z.string().nullable().optional(),
47
+ changeDetectedAt: z.string().nullable().optional(),
48
+ consecutiveNoChange: z.number().int().nullable().optional(),
49
+ consecutiveErrors: z.number().int().nullable().optional(),
50
+ nextFetchAfter: z.string().nullable().optional(),
51
+ medianGapDays: z.number().nullable().optional(),
52
+ lastRetieredAt: z.string().nullable().optional(),
53
+ metadata: z.string().nullable().optional(),
54
+ productName: z.string().nullable().optional(),
55
+ productSlug: z.string().nullable().optional(),
56
+ });
57
+
58
+ /**
59
+ * Canonical row returned by `GET /v1/sources` (list). Superset of
60
+ * `SourceListItem` with required `id` / `orgName` / `orgSlug` fields used by
61
+ * the CLI `list` command.
62
+ */
63
+ export const SourceWithOrgSchema = z.object({
64
+ id: z.string(),
65
+ name: z.string(),
66
+ slug: z.string(),
67
+ type: SourceTypeSchema,
68
+ url: z.string(),
69
+ orgName: z.string().nullable(),
70
+ orgSlug: z.string().nullable(),
71
+ productName: z.string().nullable(),
72
+ productSlug: z.string().nullable(),
73
+ isPrimary: z.boolean(),
74
+ isHidden: z.boolean().nullable(),
75
+ discovery: SourceDiscoverySchema.optional(),
76
+ metadata: z.string().nullable(),
77
+ releaseCount: z.number().int().min(0),
78
+ latestVersion: z.string().nullable(),
79
+ latestDate: z.string().nullable(),
80
+ lastFetchedAt: z.string().nullable(),
81
+ lastPolledAt: z.string().nullable(),
82
+ fetchPriority: SourceFetchPrioritySchema.nullable(),
83
+ changeDetectedAt: z.string().nullable(),
84
+ consecutiveNoChange: z.number().int().nullable(),
85
+ consecutiveErrors: z.number().int().nullable(),
86
+ nextFetchAfter: z.string().nullable(),
87
+ medianGapDays: z.number().nullable(),
88
+ lastRetieredAt: z.string().nullable(),
89
+ });
90
+
91
+ export const SourceListResponseSchema = ListResponseSchema(SourceWithOrgSchema);
92
+
93
+ /**
94
+ * `GET /v1/sources` returns a bare array by default and an envelope when
95
+ * `?envelope=true` is passed. The OpenAPI spec advertises the union so
96
+ * either client expectation is documented.
97
+ */
98
+ export const SourceListResultSchema = z.union([
99
+ z.array(SourceWithOrgSchema),
100
+ SourceListResponseSchema,
101
+ ]);
102
+
103
+ const SourceDetailSummariesSchema = z.object({
104
+ rolling: ReleaseSummaryItemSchema.nullable(),
105
+ monthly: z.array(ReleaseSummaryItemSchema),
106
+ });
107
+
108
+ /**
109
+ * Resolved attribution block returned alongside a source row. `id` is
110
+ * additive vs the legacy `{ slug, name }` shape — older clients that ignored
111
+ * it still parse cleanly. `productId` / `productSlug` are sibling top-level
112
+ * fields on the source response (kept flat to mirror `orgId`), not nested
113
+ * inside this block.
114
+ */
115
+ export const SourceOrgRefSchema = z.object({
116
+ id: z.string(),
117
+ slug: z.string(),
118
+ name: z.string(),
119
+ });
120
+
121
+ export const SourceDetailSchema = z.object({
122
+ id: z.string(),
123
+ slug: z.string(),
124
+ name: z.string(),
125
+ type: SourceTypeSchema,
126
+ url: z.string(),
127
+ orgId: z.string().nullable(),
128
+ productId: z.string().nullable(),
129
+ productSlug: z.string().nullable(),
130
+ // Hidden sources are reachable by direct URL but excluded from listings,
131
+ // sitemap, and AI features. On-demand lookups and admin-suppressed rows
132
+ // set this true; canonical curated/agent sources leave it false.
133
+ isHidden: z.boolean(),
134
+ // Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
135
+ // `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
136
+ // Optional for graceful degradation against older API responses.
137
+ discovery: SourceDiscoverySchema.optional(),
138
+ isPrimary: z.boolean(),
139
+ metadata: z.string(),
140
+ changelogUrl: z.string().nullable().optional(),
141
+ hasChangelogFile: z.boolean().optional(),
142
+ org: SourceOrgRefSchema.nullable(),
143
+ releaseCount: z.number().int().min(0),
144
+ releasesLast30Days: z.number().int().min(0),
145
+ avgReleasesPerWeek: z.number(),
146
+ latestVersion: z.string().nullable(),
147
+ latestDate: z.string().nullable(),
148
+ lastFetchedAt: z.string().nullable(),
149
+ lastPolledAt: z.string().nullable(),
150
+ trackingSince: z.string(),
151
+ releases: z.array(ReleaseItemSchema),
152
+ pagination: PaginationSchema,
153
+ summaries: SourceDetailSummariesSchema,
154
+ });
155
+
156
+ /**
157
+ * Response returned by `POST /v1/sources` and `PATCH /v1/sources/:slug`.
158
+ *
159
+ * Pre-#794 these endpoints returned the bare drizzle row, leaving callers to
160
+ * round-trip a separate `GET` to confirm the resolved org/product attribution.
161
+ * Now they return the same source row plus a resolved `org { id, slug, name }`
162
+ * block and `productSlug`, so an agent can answer "did the write take?" from
163
+ * the response alone.
164
+ *
165
+ * Wire fields are loosely-typed (`z.unknown()`) for the timestamps/counters
166
+ * that aren't load-bearing for callers — the row is large and we don't want
167
+ * to hand-mirror every column. Stable, agent-relevant fields are typed.
168
+ */
169
+ export const SourceMutationResponseSchema = z.looseObject({
170
+ id: z.string(),
171
+ slug: z.string(),
172
+ name: z.string(),
173
+ type: SourceTypeSchema,
174
+ url: z.string(),
175
+ orgId: z.string().nullable(),
176
+ productId: z.string().nullable(),
177
+ productSlug: z.string().nullable(),
178
+ org: SourceOrgRefSchema.nullable(),
179
+ metadata: z.string().nullable(),
180
+ isPrimary: z.boolean().nullable().optional(),
181
+ isHidden: z.boolean().nullable().optional(),
182
+ discovery: SourceDiscoverySchema.optional(),
183
+ createdAt: z.string().optional(),
184
+ });
185
+
186
+ /** Body accepted by `PATCH /v1/sources/:slug`. */
187
+ export const SourcePatchInputSchema = z.object({
188
+ name: z.string().optional(),
189
+ url: z.string().optional(),
190
+ type: SourceTypeSchema.optional(),
191
+ slug: z.string().optional(),
192
+ metadata: z.string().optional(),
193
+ orgId: z.string().nullable().optional(),
194
+ productId: z.string().nullable().optional(),
195
+ lastFetchedAt: z.string().nullable().optional(),
196
+ lastContentHash: z.string().nullable().optional(),
197
+ fetchPriority: SourceFetchPrioritySchema.optional(),
198
+ consecutiveNoChange: z.number().int().optional(),
199
+ consecutiveErrors: z.number().int().optional(),
200
+ nextFetchAfter: z.string().nullable().optional(),
201
+ isPrimary: z.boolean().optional(),
202
+ isHidden: z.boolean().optional(),
203
+ changeDetectedAt: z.string().nullable().optional(),
204
+ lastPolledAt: z.string().nullable().optional(),
205
+ });
206
+
207
+ /** Body accepted by `POST /v1/sources`. */
208
+ export const CreateSourceBodySchema = z.object({
209
+ name: z.string().min(1),
210
+ url: z.string().min(1),
211
+ type: SourceTypeSchema.optional(),
212
+ slug: z.string().optional(),
213
+ orgId: z.string().optional(),
214
+ orgSlug: z.string().optional(),
215
+ /**
216
+ * Either accepted; `productId` wins when both are supplied. `productSlug`
217
+ * resolves within the requested org (slug uniqueness is per-org). The
218
+ * legacy `productId` ingestion path is preserved for callers that already
219
+ * resolve products client-side; new agent flows should pass `productSlug`.
220
+ */
221
+ productId: z.string().optional(),
222
+ productSlug: z.string().optional(),
223
+ metadata: z.string().optional(),
224
+ isPrimary: z.boolean().optional(),
225
+ });
226
+
227
+ export const ChangelogFileSummarySchema = z.object({
228
+ path: z.string(),
229
+ filename: z.string(),
230
+ url: z.string(),
231
+ bytes: z.number().int().min(0),
232
+ fetchedAt: z.string(),
233
+ });
234
+
235
+ export const SourceChangelogResponseSchema = z.object({
236
+ path: z.string(),
237
+ filename: z.string(),
238
+ url: z.string(),
239
+ rawUrl: z.string(),
240
+ content: z.string(),
241
+ bytes: z.number().int().min(0),
242
+ fetchedAt: z.string(),
243
+ /** Character offset of the first character in `content` within the full file. */
244
+ offset: z.number().int().min(0),
245
+ /** The limit (in chars) that was applied to produce this slice. */
246
+ limit: z.number().int().min(0),
247
+ /** Next offset to request for the next slice, or null if `content` is the tail. */
248
+ nextOffset: z.number().int().nullable(),
249
+ /** Total length of the full file in characters. */
250
+ totalChars: z.number().int().min(0),
251
+ /** Requested token budget when in token mode (cl100k_base). */
252
+ tokens: z.number().int().optional(),
253
+ /** Encoded token count of the returned `content`. Set in token mode. */
254
+ sliceTokens: z.number().int().optional(),
255
+ /** Full-file token count (cl100k_base). Always populated. */
256
+ totalTokens: z.number().int().min(0),
257
+ /** True when the upstream file exceeded the 1MB cap and content was sliced. */
258
+ truncated: z.boolean(),
259
+ /** Byte offset where the file was truncated, or null when not truncated. */
260
+ truncatedAt: z.number().int().nullable(),
261
+ /**
262
+ * Index of every changelog file tracked for this source (root plus any
263
+ * discovered per-package files). Always present even for single-file
264
+ * sources so clients can lazily render a file picker.
265
+ */
266
+ files: z.array(ChangelogFileSummarySchema),
267
+ });