@irogane/kaji 0.2.0-beta.11

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.
Files changed (136) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +598 -0
  3. package/contracts/README.md +14 -0
  4. package/contracts/beta-core-v1.json +52 -0
  5. package/contracts/cli/init-cases-v1.json +27 -0
  6. package/contracts/errors/error-codes.json +48 -0
  7. package/contracts/errors/integration-recovery-v1.json +127 -0
  8. package/contracts/errors/provider-normalization.json +111 -0
  9. package/contracts/events/conformance-invalid.json +20 -0
  10. package/contracts/events/conformance.json +511 -0
  11. package/contracts/events/new-kaji-event-v1.schema.json +1021 -0
  12. package/contracts/events/stored-kaji-event-v1.schema.json +1025 -0
  13. package/contracts/feature-tiers-v1.json +488 -0
  14. package/contracts/integrations/abi-index-v1.json +8 -0
  15. package/contracts/integrations/conformance-invalid.json +443 -0
  16. package/contracts/integrations/conformance-valid.json +121 -0
  17. package/contracts/integrations/copy-provenance-v1.schema.json +61 -0
  18. package/contracts/integrations/echo-tool-abi-v1.json +37 -0
  19. package/contracts/integrations/github-api-conformance-v1.json +644 -0
  20. package/contracts/integrations/github-tool-abi-typescript-v1.json +369 -0
  21. package/contracts/integrations/github-tool-abi-v1.json +146 -0
  22. package/contracts/integrations/gmail-api-conformance-v1.json +750 -0
  23. package/contracts/integrations/gmail-tool-abi-v1.json +62 -0
  24. package/contracts/integrations/index.schema.json +37 -0
  25. package/contracts/integrations/manifest.schema.json +119 -0
  26. package/contracts/parity/expected-normalized.json +4906 -0
  27. package/contracts/parity/scenarios.json +100 -0
  28. package/contracts/parity/scenarios.schema.json +214 -0
  29. package/contracts/providers/cost-conformance.json +112 -0
  30. package/contracts/release/github-proof-v1.schema.json +138 -0
  31. package/contracts/release/gmail-proof-v1.schema.json +138 -0
  32. package/contracts/release/kaji-ts-consumer-handoff-v1.schema.json +1289 -0
  33. package/contracts/release/publisher-identity-receipt-v1.schema.json +319 -0
  34. package/contracts/release/typescript-onboarding-evidence-v1.schema.json +740 -0
  35. package/contracts/tools/conformance-invalid.json +271 -0
  36. package/contracts/tools/conformance-valid.json +78 -0
  37. package/contracts/tools/tool-schema-v1.schema.json +18 -0
  38. package/dist/anthropic.cjs +1231 -0
  39. package/dist/anthropic.cjs.map +1 -0
  40. package/dist/anthropic.d.cts +26 -0
  41. package/dist/anthropic.d.ts +26 -0
  42. package/dist/anthropic.js +270 -0
  43. package/dist/anthropic.js.map +1 -0
  44. package/dist/auth.cjs +1507 -0
  45. package/dist/auth.cjs.map +1 -0
  46. package/dist/auth.d.cts +129 -0
  47. package/dist/auth.d.ts +129 -0
  48. package/dist/auth.js +1039 -0
  49. package/dist/auth.js.map +1 -0
  50. package/dist/base-B9FRMcP8.d.cts +140 -0
  51. package/dist/base-nHQd1VtS.d.ts +140 -0
  52. package/dist/chunk-AAM33KAO.js +4367 -0
  53. package/dist/chunk-AAM33KAO.js.map +1 -0
  54. package/dist/chunk-KAJ6BM64.js +153 -0
  55. package/dist/chunk-KAJ6BM64.js.map +1 -0
  56. package/dist/chunk-KCAXIOZS.js +308 -0
  57. package/dist/chunk-KCAXIOZS.js.map +1 -0
  58. package/dist/chunk-LSJ4AVO2.js +243 -0
  59. package/dist/chunk-LSJ4AVO2.js.map +1 -0
  60. package/dist/chunk-TM7ZGOJX.js +716 -0
  61. package/dist/chunk-TM7ZGOJX.js.map +1 -0
  62. package/dist/cli/bin.d.ts +2 -0
  63. package/dist/cli/bin.js +13 -0
  64. package/dist/cli/bin.js.map +1 -0
  65. package/dist/cli/chunk-2RCWPRVY.js +6277 -0
  66. package/dist/cli/chunk-2RCWPRVY.js.map +1 -0
  67. package/dist/cli/chunk-SEBX54TR.js +681 -0
  68. package/dist/cli/chunk-SEBX54TR.js.map +1 -0
  69. package/dist/cli/index.d.ts +232 -0
  70. package/dist/cli/index.js +11 -0
  71. package/dist/cli/index.js.map +1 -0
  72. package/dist/cli/init-worker.d.ts +2 -0
  73. package/dist/cli/init-worker.js +18 -0
  74. package/dist/cli/init-worker.js.map +1 -0
  75. package/dist/cli/integration-copy-worker.js +48 -0
  76. package/dist/cli/integration-copy-worker.js.map +1 -0
  77. package/dist/cli/package-entry-cjs.cjs +21 -0
  78. package/dist/cli/package-entry-cjs.cjs.map +1 -0
  79. package/dist/cli/package-entry-cjs.d.cts +2 -0
  80. package/dist/cli/package-entry.d.ts +2 -0
  81. package/dist/cli/package-entry.js +13 -0
  82. package/dist/cli/package-entry.js.map +1 -0
  83. package/dist/context-BaFHrQHv.d.cts +21 -0
  84. package/dist/context-BaFHrQHv.d.ts +21 -0
  85. package/dist/context-C-YPY-GS.d.cts +1538 -0
  86. package/dist/context-C-YPY-GS.d.ts +1538 -0
  87. package/dist/index.cjs +12346 -0
  88. package/dist/index.cjs.map +1 -0
  89. package/dist/index.d.cts +1560 -0
  90. package/dist/index.d.ts +1560 -0
  91. package/dist/index.js +7852 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/integrations/github.cjs +2092 -0
  94. package/dist/integrations/github.cjs.map +1 -0
  95. package/dist/integrations/github.d.cts +21 -0
  96. package/dist/integrations/github.d.ts +21 -0
  97. package/dist/integrations/github.js +2088 -0
  98. package/dist/integrations/github.js.map +1 -0
  99. package/dist/integrations.cjs +3370 -0
  100. package/dist/integrations.cjs.map +1 -0
  101. package/dist/integrations.d.cts +202 -0
  102. package/dist/integrations.d.ts +202 -0
  103. package/dist/integrations.js +2650 -0
  104. package/dist/integrations.js.map +1 -0
  105. package/dist/observability-Cj--OkME.d.cts +96 -0
  106. package/dist/observability-Cj--OkME.d.ts +96 -0
  107. package/dist/openai.cjs +1235 -0
  108. package/dist/openai.cjs.map +1 -0
  109. package/dist/openai.d.cts +32 -0
  110. package/dist/openai.d.ts +32 -0
  111. package/dist/openai.js +272 -0
  112. package/dist/openai.js.map +1 -0
  113. package/dist/testing.cjs +554 -0
  114. package/dist/testing.cjs.map +1 -0
  115. package/dist/testing.d.cts +43 -0
  116. package/dist/testing.d.ts +43 -0
  117. package/dist/testing.js +118 -0
  118. package/dist/testing.js.map +1 -0
  119. package/package.json +188 -0
  120. package/registry/echo/index.ts +53 -0
  121. package/registry/echo/manifest.json +53 -0
  122. package/registry/github/LICENSE +105 -0
  123. package/registry/github/client.ts +1727 -0
  124. package/registry/github/index.ts +263 -0
  125. package/registry/github/manifest.json +227 -0
  126. package/registry/github/owner-fixtures.json +10 -0
  127. package/registry/github/tests/github.test.ts +32 -0
  128. package/registry/gmail/LICENSE +105 -0
  129. package/registry/gmail/client.ts +548 -0
  130. package/registry/gmail/index.ts +165 -0
  131. package/registry/gmail/manifest.json +102 -0
  132. package/registry/gmail/owner-fixtures.json +10 -0
  133. package/registry/gmail/tests/gmail.test.ts +32 -0
  134. package/registry/index.json +21 -0
  135. package/registry/index.schema.json +37 -0
  136. package/registry/schema.json +119 -0
@@ -0,0 +1,1727 @@
1
+ // This is YOUR GitHub integration client. Edit it.
2
+ //
3
+ // This client implements all 15 GitHub routes, but index.ts in this directory
4
+ // (the CLI-copied registry bundle) only registers 6 as tools. The other 9
5
+ // (get_commit, get_pull_request, list_pull_request_files, list_check_runs,
6
+ // get_workflow_run, list_workflow_jobs, list_file_commits, get_release,
7
+ // list_deployments) are exposed separately via the npm package surface at
8
+ // ts/src/integrations/github.ts + package-tools.ts, which composes the
9
+ // CLI-copied 6 with these 9 for the full 15-tool catalog. See
10
+ // ts/tests/github-registry.test.ts for coverage of both surfaces.
11
+
12
+ import { Buffer } from "node:buffer";
13
+
14
+ import {
15
+ IntegrationAuthRequiredError,
16
+ IntegrationExecutionError,
17
+ IntegrationPolicyError,
18
+ IntegrationRateLimitedError,
19
+ IntegrationTransientReadError,
20
+ snapshotIntegrationResult,
21
+ type BoundedResponse,
22
+ type FixedOriginRequester,
23
+ } from "@irogane/kaji/integrations";
24
+
25
+ type ToolExecutionContext = Parameters<FixedOriginRequester["request"]>[2];
26
+
27
+ const REPOSITORY = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
28
+ const SCOPE_QUALIFIER = /(?:repo|org|user):/i;
29
+ const MAX_SEARCH_RESULT_BYTES = 32 * 1024;
30
+ const MAX_FILE_BYTES = 48 * 1024;
31
+ const MAX_MODEL_RESULT_BYTES = 60 * 1024;
32
+ const MAX_MODEL_TEXT_BYTES = 8 * 1024;
33
+ const MAX_PROVIDER_TEXT_BYTES = 1024 * 1024;
34
+ const MAX_TOKEN_CHARACTERS = 4_096;
35
+ const MAX_URL_CHARACTERS = 2_048;
36
+ const GENERAL_ACCEPT = "application/vnd.github+json";
37
+ const SEARCH_ACCEPT = "application/vnd.github.text-match+json";
38
+ const GITHUB_API_VERSION = "2026-03-10";
39
+ const GITHUB_CATALOG_VERSION = "0.2.0";
40
+ const GITHUB_USER_AGENT = `kaji-github/${GITHUB_CATALOG_VERSION}`;
41
+
42
+ type Route =
43
+ | "search_code"
44
+ | "get_file"
45
+ | "list_issues"
46
+ | "get_issue"
47
+ | "create_issue"
48
+ | "add_comment"
49
+ | "get_commit"
50
+ | "get_pull_request"
51
+ | "list_pull_request_files"
52
+ | "list_check_runs"
53
+ | "get_workflow_run"
54
+ | "list_workflow_jobs"
55
+ | "list_file_commits"
56
+ | "get_release"
57
+ | "list_deployments";
58
+
59
+ type Query = Readonly<Record<string, string | number>>;
60
+ type RequestBody = Readonly<Record<string, unknown>>;
61
+
62
+ export interface GitHubClientOptions {
63
+ readonly tokenFor: (context: ToolExecutionContext) => Promise<string>;
64
+ readonly repositories: readonly string[];
65
+ readonly http: FixedOriginRequester;
66
+ }
67
+
68
+ interface GitHubClientRuntime {
69
+ readonly sleep: (delayMs: number, signal: AbortSignal) => Promise<void>;
70
+ readonly monotonicNow: () => number;
71
+ }
72
+
73
+ interface RequestJsonOptions {
74
+ readonly method: "GET" | "POST";
75
+ readonly repository: string;
76
+ readonly path: string;
77
+ readonly query?: Query;
78
+ readonly body?: RequestBody;
79
+ readonly mutation?: boolean;
80
+ }
81
+
82
+ class ProviderShapeError extends Error {}
83
+
84
+ class UnknownMutationError extends Error {
85
+ readonly error_code = "TOOL_EXECUTION_FAILED";
86
+ readonly reason_code = "github_mutation_unknown";
87
+ readonly recovery_code = "RECONCILE_GITHUB_MUTATION";
88
+ readonly doc_url = "https://kaji.dev/docs/integrations/recovery-v1#github-mutation-unknown";
89
+
90
+ constructor() {
91
+ super("GitHub mutation outcome is unknown");
92
+ this.name = "UnknownMutationError";
93
+ }
94
+ }
95
+
96
+ interface PageInput {
97
+ readonly page?: number;
98
+ readonly perPage?: number;
99
+ }
100
+
101
+ interface BoundedText {
102
+ readonly text: string | null;
103
+ readonly truncated: boolean;
104
+ }
105
+
106
+ interface TextFieldPath {
107
+ readonly path: readonly string[];
108
+ readonly truncatedPath: readonly string[];
109
+ }
110
+
111
+ function policyError(): IntegrationPolicyError {
112
+ return new IntegrationPolicyError();
113
+ }
114
+
115
+ function authError(): IntegrationAuthRequiredError {
116
+ return new IntegrationAuthRequiredError("github_token_missing");
117
+ }
118
+
119
+ function apiError(): IntegrationExecutionError {
120
+ return new IntegrationExecutionError("api_rejected");
121
+ }
122
+
123
+ function transientError(): IntegrationTransientReadError {
124
+ return new IntegrationTransientReadError();
125
+ }
126
+
127
+ function rateError(): IntegrationRateLimitedError {
128
+ return new IntegrationRateLimitedError();
129
+ }
130
+
131
+ function requireRepository(value: unknown, allowed: ReadonlySet<string>): string {
132
+ if (
133
+ typeof value !== "string" ||
134
+ !REPOSITORY.test(value) ||
135
+ !repositoryComponentsAreSafe(value) ||
136
+ !allowed.has(value)
137
+ ) {
138
+ throw policyError();
139
+ }
140
+ return value;
141
+ }
142
+
143
+ function repositoryComponentsAreSafe(value: string): boolean {
144
+ return value.split("/").every((part) => part !== "." && part !== "..");
145
+ }
146
+
147
+ function policyString(value: unknown, minimum: number, maximum: number): string {
148
+ if (typeof value !== "string") {
149
+ throw policyError();
150
+ }
151
+ assertScalarString(value, policyError);
152
+ const length = Array.from(value).length;
153
+ if (length < minimum || length > maximum) throw policyError();
154
+ return value;
155
+ }
156
+
157
+ function policyInteger(value: unknown, minimum: number, maximum: number): number {
158
+ if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) {
159
+ throw policyError();
160
+ }
161
+ return value as number;
162
+ }
163
+
164
+ function providerCharacterString(value: unknown, minimum: number, maximum: number): string {
165
+ if (typeof value !== "string") {
166
+ throw new ProviderShapeError();
167
+ }
168
+ assertScalarString(value, () => new ProviderShapeError());
169
+ const length = Array.from(value).length;
170
+ if (length < minimum || length > maximum) throw new ProviderShapeError();
171
+ return value;
172
+ }
173
+
174
+ function providerGitHubUrl(
175
+ value: unknown,
176
+ origin: "https://github.com" | "https://api.github.com",
177
+ pathSegments: readonly string[],
178
+ ): string {
179
+ const raw = providerCharacterString(value, 1, MAX_URL_CHARACTERS);
180
+ const encodedSegments = pathSegments.map((segment) => {
181
+ providerCharacterString(segment, 1, MAX_URL_CHARACTERS);
182
+ if (
183
+ segment === "." ||
184
+ segment === ".." ||
185
+ segment.includes("\\") ||
186
+ Array.from(segment, (character) => character.charCodeAt(0)).some(
187
+ (codePoint) => codePoint < 0x20 || codePoint === 0x7f,
188
+ )
189
+ ) {
190
+ throw new ProviderShapeError();
191
+ }
192
+ return encodeURIComponent(segment).replace(
193
+ /[!'()*]/g,
194
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
195
+ );
196
+ });
197
+ const expected = `${origin}/${encodedSegments.join("/")}`;
198
+ let parsed: URL;
199
+ try {
200
+ parsed = new URL(raw);
201
+ } catch {
202
+ throw new ProviderShapeError();
203
+ }
204
+ if (
205
+ raw !== expected ||
206
+ parsed.href !== raw ||
207
+ parsed.protocol !== "https:" ||
208
+ parsed.origin !== origin ||
209
+ parsed.username !== "" ||
210
+ parsed.password !== "" ||
211
+ parsed.port !== "" ||
212
+ parsed.search !== "" ||
213
+ parsed.hash !== ""
214
+ ) {
215
+ throw new ProviderShapeError();
216
+ }
217
+ return raw;
218
+ }
219
+
220
+ function providerByteString(value: unknown, minimum: number, maximum: number): string {
221
+ if (typeof value !== "string") {
222
+ throw new ProviderShapeError();
223
+ }
224
+ assertScalarString(value, () => new ProviderShapeError());
225
+ if (Array.from(value).length < minimum || utf8Size(value) > maximum) {
226
+ throw new ProviderShapeError();
227
+ }
228
+ return value;
229
+ }
230
+
231
+ function providerInteger(value: unknown, minimum = 0): number {
232
+ if (!Number.isSafeInteger(value) || (value as number) < minimum) {
233
+ throw new ProviderShapeError();
234
+ }
235
+ return value as number;
236
+ }
237
+
238
+ function providerBoolean(value: unknown): boolean {
239
+ if (typeof value !== "boolean") throw new ProviderShapeError();
240
+ return value;
241
+ }
242
+
243
+ function providerNullableCharacterString(
244
+ value: unknown,
245
+ minimum: number,
246
+ maximum: number,
247
+ ): string | null {
248
+ return value === null ? null : providerCharacterString(value, minimum, maximum);
249
+ }
250
+
251
+ function providerOptionalNullableCharacterString(
252
+ value: unknown,
253
+ minimum: number,
254
+ maximum: number,
255
+ ): string | null {
256
+ return value === undefined || value === null
257
+ ? null
258
+ : providerCharacterString(value, minimum, maximum);
259
+ }
260
+
261
+ function objectValue(value: unknown): Record<string, unknown> {
262
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
263
+ throw new ProviderShapeError();
264
+ }
265
+ const prototype = Object.getPrototypeOf(value);
266
+ if (prototype !== Object.prototype && prototype !== null) throw new ProviderShapeError();
267
+ return value as Record<string, unknown>;
268
+ }
269
+
270
+ function arrayValue(value: unknown): unknown[] {
271
+ if (!Array.isArray(value)) throw new ProviderShapeError();
272
+ return value;
273
+ }
274
+
275
+ function assertScalarString(value: string, makeError: () => Error): void {
276
+ for (let index = 0; index < value.length; index += 1) {
277
+ const unit = value.charCodeAt(index);
278
+ if (unit >= 0xd800 && unit <= 0xdbff) {
279
+ const next = value.charCodeAt(index + 1);
280
+ if (next < 0xdc00 || next > 0xdfff) throw makeError();
281
+ index += 1;
282
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
283
+ throw makeError();
284
+ }
285
+ }
286
+ }
287
+
288
+ function utf8Size(value: string): number {
289
+ assertScalarString(value, () => new ProviderShapeError());
290
+ return new TextEncoder().encode(value).byteLength;
291
+ }
292
+
293
+ function truncateUtf8(value: string, maximum: number): string {
294
+ const bytes = new TextEncoder().encode(value);
295
+ if (bytes.byteLength <= maximum) return value;
296
+ for (let end = maximum; end >= Math.max(0, maximum - 3); end -= 1) {
297
+ try {
298
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes.slice(0, end));
299
+ } catch {
300
+ // A UTF-8 sequence is at most four bytes, so three backtracks are sufficient.
301
+ }
302
+ }
303
+ throw new ProviderShapeError();
304
+ }
305
+
306
+ function serializedBytes(value: unknown): number {
307
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
308
+ }
309
+
310
+ function boundedText(value: unknown, nullable = false): BoundedText {
311
+ if (value === null && nullable) return { text: null, truncated: false };
312
+ const text = providerByteString(value, 0, MAX_PROVIDER_TEXT_BYTES);
313
+ const truncated = utf8Size(text) > MAX_MODEL_TEXT_BYTES;
314
+ return {
315
+ text: truncated ? truncateUtf8(text, MAX_MODEL_TEXT_BYTES) : text,
316
+ truncated,
317
+ };
318
+ }
319
+
320
+ function valueAtPath(root: Record<string, unknown>, path: readonly string[]): unknown {
321
+ let value: unknown = root;
322
+ for (const segment of path) {
323
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
324
+ throw new ProviderShapeError();
325
+ }
326
+ value = (value as Record<string, unknown>)[segment];
327
+ }
328
+ return value;
329
+ }
330
+
331
+ function setValueAtPath(
332
+ root: Record<string, unknown>,
333
+ path: readonly string[],
334
+ value: unknown,
335
+ ): void {
336
+ let target = root;
337
+ for (const segment of path.slice(0, -1)) {
338
+ const next = target[segment];
339
+ if (next === null || typeof next !== "object" || Array.isArray(next)) {
340
+ throw new ProviderShapeError();
341
+ }
342
+ target = next as Record<string, unknown>;
343
+ }
344
+ target[path.at(-1)!] = value;
345
+ }
346
+
347
+ function shrinkTextFieldsToFit<T extends Record<string, unknown>>(
348
+ row: T,
349
+ fields: readonly TextFieldPath[],
350
+ fits: (candidate: T) => boolean,
351
+ ): T | undefined {
352
+ const candidate = structuredClone(row);
353
+ if (fits(candidate)) return candidate;
354
+ for (const field of fields) {
355
+ const current = valueAtPath(candidate, field.path);
356
+ if (current === null) continue;
357
+ if (typeof current !== "string") throw new ProviderShapeError();
358
+ const byteLength = utf8Size(current);
359
+ let low = 0;
360
+ let high = byteLength;
361
+ let best: string | undefined;
362
+ while (low <= high) {
363
+ const middle = Math.floor((low + high) / 2);
364
+ const text = truncateUtf8(current, middle);
365
+ const trial = structuredClone(candidate);
366
+ setValueAtPath(trial, field.path, text);
367
+ setValueAtPath(trial, field.truncatedPath, true);
368
+ if (fits(trial)) {
369
+ best = text;
370
+ low = middle + 1;
371
+ } else {
372
+ high = middle - 1;
373
+ }
374
+ }
375
+ if (best !== undefined) {
376
+ setValueAtPath(candidate, field.path, best);
377
+ setValueAtPath(candidate, field.truncatedPath, true);
378
+ return candidate;
379
+ }
380
+ setValueAtPath(candidate, field.path, "");
381
+ setValueAtPath(candidate, field.truncatedPath, true);
382
+ }
383
+ return fits(candidate) ? candidate : undefined;
384
+ }
385
+
386
+ function boundedRows(
387
+ value: unknown,
388
+ maximum = 20,
389
+ ): Readonly<{
390
+ rows: readonly unknown[];
391
+ omitted: number;
392
+ }> {
393
+ const rows = arrayValue(value);
394
+ return {
395
+ rows: rows.slice(0, maximum),
396
+ omitted: Math.max(0, rows.length - maximum),
397
+ };
398
+ }
399
+
400
+ function fitRows<T extends Record<string, unknown>, R extends Record<string, unknown>>(
401
+ rows: readonly T[],
402
+ build: (items: readonly T[], omittedCount: number) => R,
403
+ options: Readonly<{
404
+ initialOmitted?: number;
405
+ textFields?: readonly TextFieldPath[];
406
+ shrink?: (row: T, fits: (candidate: T) => boolean) => T | undefined;
407
+ }> = {},
408
+ ): R {
409
+ const items: T[] = [];
410
+ let omitted = options.initialOmitted ?? 0;
411
+ for (const row of rows) {
412
+ const fits = (candidate: T): boolean =>
413
+ serializedBytes(build([...items, candidate], omitted)) <= MAX_MODEL_RESULT_BYTES;
414
+ let candidate: T | undefined = row;
415
+ if (!fits(candidate)) {
416
+ if (options.shrink !== undefined) {
417
+ candidate = options.shrink(candidate, fits);
418
+ } else if (options.textFields !== undefined) {
419
+ candidate = shrinkTextFieldsToFit(candidate, options.textFields, fits);
420
+ }
421
+ }
422
+ if (candidate === undefined || !fits(candidate)) {
423
+ omitted += 1;
424
+ } else {
425
+ items.push(candidate);
426
+ }
427
+ }
428
+ let result = build(items, omitted);
429
+ while (serializedBytes(result) > MAX_MODEL_RESULT_BYTES && items.length > 0) {
430
+ items.pop();
431
+ omitted += 1;
432
+ result = build(items, omitted);
433
+ }
434
+ if (serializedBytes(result) > MAX_MODEL_RESULT_BYTES) throw new ProviderShapeError();
435
+ return result;
436
+ }
437
+
438
+ function assertModelResultBudget<T extends Record<string, unknown>>(value: T): T {
439
+ if (serializedBytes(value) > MAX_MODEL_RESULT_BYTES) throw new ProviderShapeError();
440
+ return value;
441
+ }
442
+
443
+ function fitObjectToModelBudget<T extends Record<string, unknown>>(
444
+ value: T,
445
+ textFields: readonly TextFieldPath[],
446
+ ): T | undefined {
447
+ return shrinkTextFieldsToFit(
448
+ value,
449
+ textFields,
450
+ (candidate) => serializedBytes(candidate) <= MAX_MODEL_RESULT_BYTES,
451
+ );
452
+ }
453
+
454
+ function requireObjectWithinModelBudget<T extends Record<string, unknown>>(
455
+ value: T,
456
+ textFields: readonly TextFieldPath[],
457
+ ): T {
458
+ const fitted = fitObjectToModelBudget(value, textFields);
459
+ if (fitted === undefined) throw new ProviderShapeError();
460
+ return fitted;
461
+ }
462
+
463
+ function encodeComponent(value: string | number): string {
464
+ try {
465
+ return encodeURIComponent(String(value)).replace(
466
+ /[!'()*]/g,
467
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
468
+ );
469
+ } catch {
470
+ throw policyError();
471
+ }
472
+ }
473
+
474
+ function queryString(query: Query | undefined): string {
475
+ if (query === undefined || Object.keys(query).length === 0) return "";
476
+ const pairs: string[] = [];
477
+ for (const key of Object.keys(query).sort()) {
478
+ const value = query[key];
479
+ if (key.length === 0 || (typeof value !== "string" && typeof value !== "number")) {
480
+ throw policyError();
481
+ }
482
+ pairs.push(`${encodeComponent(key)}=${encodeComponent(value)}`);
483
+ }
484
+ return `?${pairs.join("&")}`;
485
+ }
486
+
487
+ function validateContentPath(value: unknown): string {
488
+ const path = policyString(value, 1, 512);
489
+ const parts = path.split("/");
490
+ if (path.includes("\\") || parts.some((part) => part === "" || part === "." || part === "..")) {
491
+ throw policyError();
492
+ }
493
+ return path;
494
+ }
495
+
496
+ function encodeContentPath(value: unknown): string {
497
+ return validateContentPath(value).split("/").map(encodeComponent).join("/");
498
+ }
499
+
500
+ function validateEncodedContentPath(value: string): void {
501
+ const decoded: string[] = [];
502
+ for (const part of value.split("/")) {
503
+ let plain: string;
504
+ try {
505
+ plain = decodeURIComponent(part);
506
+ } catch {
507
+ throw policyError();
508
+ }
509
+ if (
510
+ plain === "" ||
511
+ plain === "." ||
512
+ plain === ".." ||
513
+ plain.includes("/") ||
514
+ plain.includes("\\") ||
515
+ encodeComponent(plain) !== part
516
+ ) {
517
+ throw policyError();
518
+ }
519
+ decoded.push(plain);
520
+ }
521
+ policyString(decoded.join("/"), 1, 512);
522
+ }
523
+
524
+ function encodePathSegment(value: unknown, maximum = 100): string {
525
+ const segment = policyString(value, 1, maximum);
526
+ if (segment === "." || segment === ".." || segment.includes("\\")) throw policyError();
527
+ return encodeComponent(segment);
528
+ }
529
+
530
+ function validateEncodedPathSegment(value: string, maximum = 100): void {
531
+ let decoded: string;
532
+ try {
533
+ decoded = decodeURIComponent(value);
534
+ } catch {
535
+ throw policyError();
536
+ }
537
+ if (
538
+ decoded === "." ||
539
+ decoded === ".." ||
540
+ decoded.includes("\\") ||
541
+ encodeComponent(decoded) !== value
542
+ ) {
543
+ throw policyError();
544
+ }
545
+ policyString(decoded, 1, maximum);
546
+ }
547
+
548
+ function validatePage(page: unknown, perPage: unknown): void {
549
+ policyInteger(page, 1, 1_000);
550
+ policyInteger(perPage, 1, 20);
551
+ }
552
+
553
+ function validateFilter(value: unknown): asserts value is "latest" | "all" {
554
+ if (value !== "latest" && value !== "all") throw policyError();
555
+ }
556
+
557
+ function validateDeploymentSha(value: unknown): string {
558
+ const sha = policyString(value, 1, 64);
559
+ if (!/^[0-9A-Fa-f]{1,64}$/.test(sha)) throw policyError();
560
+ return sha;
561
+ }
562
+
563
+ function normalizedToken(value: unknown): string {
564
+ if (typeof value !== "string" || value.includes("\r") || value.includes("\n")) {
565
+ throw authError();
566
+ }
567
+ const token = value.trim();
568
+ if (token.length === 0 || Array.from(token).length > MAX_TOKEN_CHARACTERS) throw authError();
569
+ assertScalarString(token, authError);
570
+ return token;
571
+ }
572
+
573
+ function retryAfter(headers: Readonly<Record<string, string>>): number | undefined {
574
+ const raw = headers["retry-after"];
575
+ if (raw === undefined) return undefined;
576
+ const normalized = raw.trim();
577
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized)) return undefined;
578
+ const delaySeconds = Number(normalized);
579
+ if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || delaySeconds > 2) return undefined;
580
+ return delaySeconds;
581
+ }
582
+
583
+ function isRateLimited(response: BoundedResponse): boolean {
584
+ return (
585
+ response.status === 429 ||
586
+ (response.status === 403 &&
587
+ (retryAfter(response.headers) !== undefined ||
588
+ response.headers["x-ratelimit-remaining"] === "0"))
589
+ );
590
+ }
591
+
592
+ function validateSearchInput(query: unknown, page: unknown, perPage: unknown): string {
593
+ const value = policyString(query, 1, 256);
594
+ if (SCOPE_QUALIFIER.test(value)) throw policyError();
595
+ policyInteger(page, 1, 50);
596
+ policyInteger(perPage, 1, 20);
597
+ return value;
598
+ }
599
+
600
+ function validateListInput(state: unknown, page: unknown, perPage: unknown): void {
601
+ if (state !== "open" && state !== "closed" && state !== "all") throw policyError();
602
+ validatePage(page, perPage);
603
+ }
604
+
605
+ function validateCreateInput(title: unknown, body: unknown): void {
606
+ policyString(title, 1, 256);
607
+ const value = policyString(body, 0, 16_384);
608
+ if (new TextEncoder().encode(value).byteLength > 16_384) throw policyError();
609
+ }
610
+
611
+ function validateCommentBody(body: unknown): void {
612
+ const value = policyString(body, 1, 16_384);
613
+ if (new TextEncoder().encode(value).byteLength > 16_384) throw policyError();
614
+ }
615
+
616
+ function routeFor(options: RequestJsonOptions): Route {
617
+ const { method, repository, path, query, body, mutation = false } = options;
618
+ const prefix = `/repos/${repository}`;
619
+ const queryKeys = Object.keys(query ?? {}).sort();
620
+ const bodyKeys = Object.keys(body ?? {}).sort();
621
+ if (method === "GET" && !mutation && path === "/search/code") {
622
+ if (query === undefined || queryKeys.join(",") !== "page,per_page,q" || body !== undefined) {
623
+ throw policyError();
624
+ }
625
+ const suffix = ` repo:${repository}`;
626
+ const value = query.q;
627
+ if (typeof value !== "string" || !value.endsWith(suffix)) throw policyError();
628
+ validateSearchInput(value.slice(0, -suffix.length), query.page, query.per_page);
629
+ return "search_code";
630
+ }
631
+ if (method === "GET" && !mutation && path.startsWith(`${prefix}/contents/`)) {
632
+ if (body !== undefined || queryKeys.some((key) => key !== "ref")) throw policyError();
633
+ validateEncodedContentPath(path.slice(`${prefix}/contents/`.length));
634
+ if (query?.ref !== undefined) policyString(query.ref, 1, 100);
635
+ return "get_file";
636
+ }
637
+ if (path === `${prefix}/issues`) {
638
+ if (method === "GET" && !mutation) {
639
+ if (
640
+ query === undefined ||
641
+ queryKeys.join(",") !== "page,per_page,state" ||
642
+ body !== undefined
643
+ ) {
644
+ throw policyError();
645
+ }
646
+ validateListInput(query.state, query.page, query.per_page);
647
+ return "list_issues";
648
+ }
649
+ if (method === "POST" && mutation) {
650
+ if (query !== undefined || body === undefined || bodyKeys.join(",") !== "body,title") {
651
+ throw policyError();
652
+ }
653
+ validateCreateInput(body.title, body.body);
654
+ return "create_issue";
655
+ }
656
+ }
657
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
658
+ const issue = new RegExp(`^${escaped}/issues/([1-9][0-9]*)$`).exec(path);
659
+ if (
660
+ issue !== null &&
661
+ method === "GET" &&
662
+ !mutation &&
663
+ query === undefined &&
664
+ body === undefined
665
+ ) {
666
+ policyInteger(Number(issue[1]), 1, Number.MAX_SAFE_INTEGER);
667
+ return "get_issue";
668
+ }
669
+ const comment = new RegExp(`^${escaped}/issues/([1-9][0-9]*)/comments$`).exec(path);
670
+ if (comment !== null && method === "POST" && mutation && query === undefined) {
671
+ if (body === undefined || bodyKeys.join(",") !== "body") throw policyError();
672
+ policyInteger(Number(comment[1]), 1, Number.MAX_SAFE_INTEGER);
673
+ validateCommentBody(body.body);
674
+ return "add_comment";
675
+ }
676
+ const commit = new RegExp(`^${escaped}/commits/([^/]+)$`).exec(path);
677
+ if (commit !== null && method === "GET" && !mutation && body === undefined) {
678
+ if (query === undefined || queryKeys.join(",") !== "page,per_page") throw policyError();
679
+ validateEncodedPathSegment(commit[1]!);
680
+ validatePage(query.page, query.per_page);
681
+ return "get_commit";
682
+ }
683
+ const pull = new RegExp(`^${escaped}/pulls/([1-9][0-9]*)$`).exec(path);
684
+ if (pull !== null && method === "GET" && !mutation && query === undefined && body === undefined) {
685
+ policyInteger(Number(pull[1]), 1, Number.MAX_SAFE_INTEGER);
686
+ return "get_pull_request";
687
+ }
688
+ const pullFiles = new RegExp(`^${escaped}/pulls/([1-9][0-9]*)/files$`).exec(path);
689
+ if (pullFiles !== null && method === "GET" && !mutation && body === undefined) {
690
+ if (query === undefined || queryKeys.join(",") !== "page,per_page") throw policyError();
691
+ policyInteger(Number(pullFiles[1]), 1, Number.MAX_SAFE_INTEGER);
692
+ validatePage(query.page, query.per_page);
693
+ return "list_pull_request_files";
694
+ }
695
+ const checks = new RegExp(`^${escaped}/commits/([^/]+)/check-runs$`).exec(path);
696
+ if (checks !== null && method === "GET" && !mutation && body === undefined) {
697
+ if (query === undefined || queryKeys.join(",") !== "filter,page,per_page") {
698
+ throw policyError();
699
+ }
700
+ validateEncodedPathSegment(checks[1]!);
701
+ validateFilter(query.filter);
702
+ validatePage(query.page, query.per_page);
703
+ return "list_check_runs";
704
+ }
705
+ const workflowRun = new RegExp(`^${escaped}/actions/runs/([1-9][0-9]*)$`).exec(path);
706
+ if (
707
+ workflowRun !== null &&
708
+ method === "GET" &&
709
+ !mutation &&
710
+ query === undefined &&
711
+ body === undefined
712
+ ) {
713
+ policyInteger(Number(workflowRun[1]), 1, Number.MAX_SAFE_INTEGER);
714
+ return "get_workflow_run";
715
+ }
716
+ const workflowJobs = new RegExp(`^${escaped}/actions/runs/([1-9][0-9]*)/jobs$`).exec(path);
717
+ if (workflowJobs !== null && method === "GET" && !mutation && body === undefined) {
718
+ if (query === undefined || queryKeys.join(",") !== "filter,page,per_page") {
719
+ throw policyError();
720
+ }
721
+ policyInteger(Number(workflowJobs[1]), 1, Number.MAX_SAFE_INTEGER);
722
+ validateFilter(query.filter);
723
+ validatePage(query.page, query.per_page);
724
+ return "list_workflow_jobs";
725
+ }
726
+ if (path === `${prefix}/commits` && method === "GET" && !mutation && body === undefined) {
727
+ if (
728
+ query === undefined ||
729
+ (queryKeys.join(",") !== "page,path,per_page" &&
730
+ queryKeys.join(",") !== "page,path,per_page,sha")
731
+ ) {
732
+ throw policyError();
733
+ }
734
+ validateContentPath(query.path);
735
+ validatePage(query.page, query.per_page);
736
+ if (query.sha !== undefined) policyString(query.sha, 1, 100);
737
+ return "list_file_commits";
738
+ }
739
+ const release = new RegExp(`^${escaped}/releases/tags/([^/]+)$`).exec(path);
740
+ if (
741
+ release !== null &&
742
+ method === "GET" &&
743
+ !mutation &&
744
+ query === undefined &&
745
+ body === undefined
746
+ ) {
747
+ validateEncodedPathSegment(release[1]!);
748
+ return "get_release";
749
+ }
750
+ if (path === `${prefix}/deployments` && method === "GET" && !mutation && body === undefined) {
751
+ if (query === undefined || !queryKeys.includes("page") || !queryKeys.includes("per_page")) {
752
+ throw policyError();
753
+ }
754
+ if (
755
+ queryKeys.some(
756
+ (key) => !["environment", "page", "per_page", "ref", "sha", "task"].includes(key),
757
+ )
758
+ ) {
759
+ throw policyError();
760
+ }
761
+ validatePage(query.page, query.per_page);
762
+ for (const key of ["environment", "ref", "task"] as const) {
763
+ if (query[key] !== undefined) policyString(query[key], 1, 100);
764
+ }
765
+ if (query.sha !== undefined) validateDeploymentSha(query.sha);
766
+ return "list_deployments";
767
+ }
768
+ throw policyError();
769
+ }
770
+
771
+ function defaultSleep(delayMs: number, signal: AbortSignal): Promise<void> {
772
+ if (signal.aborted) return Promise.reject(signal.reason);
773
+ return new Promise((resolve, reject) => {
774
+ const timer = setTimeout(() => {
775
+ signal.removeEventListener("abort", onAbort);
776
+ resolve();
777
+ }, delayMs);
778
+ const onAbort = () => {
779
+ clearTimeout(timer);
780
+ reject(signal.reason);
781
+ };
782
+ signal.addEventListener("abort", onAbort, { once: true });
783
+ });
784
+ }
785
+
786
+ function isAbortOrTimeout(error: unknown): boolean {
787
+ return (
788
+ error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")
789
+ );
790
+ }
791
+
792
+ export class GitHubClient {
793
+ private readonly repositories: ReadonlySet<string>;
794
+ private readonly tokenFor: (context: ToolExecutionContext) => Promise<string>;
795
+ private readonly http: FixedOriginRequester;
796
+ private readonly sleep: GitHubClientRuntime["sleep"];
797
+ private readonly monotonicNow: GitHubClientRuntime["monotonicNow"];
798
+
799
+ constructor(
800
+ options: GitHubClientOptions,
801
+ runtime: GitHubClientRuntime = { sleep: defaultSleep, monotonicNow: () => performance.now() },
802
+ ) {
803
+ if (
804
+ typeof options?.tokenFor !== "function" ||
805
+ !Array.isArray(options.repositories) ||
806
+ typeof options.http?.request !== "function" ||
807
+ typeof runtime.sleep !== "function" ||
808
+ typeof runtime.monotonicNow !== "function"
809
+ ) {
810
+ throw policyError();
811
+ }
812
+ const repositories = Object.freeze([...options.repositories]);
813
+ if (
814
+ repositories.some(
815
+ (repository) => !REPOSITORY.test(repository) || !repositoryComponentsAreSafe(repository),
816
+ )
817
+ ) {
818
+ throw policyError();
819
+ }
820
+ this.repositories = new Set(repositories);
821
+ this.tokenFor = options.tokenFor;
822
+ this.http = options.http;
823
+ this.sleep = runtime.sleep;
824
+ this.monotonicNow = runtime.monotonicNow;
825
+ }
826
+
827
+ async searchCode(
828
+ context: ToolExecutionContext,
829
+ input: Readonly<{ repository: string; query: string; page?: number; perPage?: number }>,
830
+ ): Promise<unknown> {
831
+ const repository = requireRepository(input.repository, this.repositories);
832
+ const page = input.page ?? 1;
833
+ const perPage = input.perPage ?? 10;
834
+ const query = validateSearchInput(input.query, page, perPage);
835
+ return this.requestJson(context, {
836
+ method: "GET",
837
+ repository,
838
+ path: "/search/code",
839
+ query: { q: `${query} repo:${repository}`, page, per_page: perPage },
840
+ });
841
+ }
842
+
843
+ async getFile(
844
+ context: ToolExecutionContext,
845
+ input: Readonly<{ repository: string; path: string; ref?: string }>,
846
+ ): Promise<unknown> {
847
+ const repository = requireRepository(input.repository, this.repositories);
848
+ const path = encodeContentPath(input.path);
849
+ const query = input.ref === undefined ? undefined : { ref: policyString(input.ref, 1, 100) };
850
+ return this.requestJson(context, {
851
+ method: "GET",
852
+ repository,
853
+ path: `/repos/${repository}/contents/${path}`,
854
+ ...(query === undefined ? {} : { query }),
855
+ });
856
+ }
857
+
858
+ async listIssues(
859
+ context: ToolExecutionContext,
860
+ input: Readonly<{
861
+ repository: string;
862
+ state?: "open" | "closed" | "all";
863
+ page?: number;
864
+ perPage?: number;
865
+ }>,
866
+ ): Promise<unknown> {
867
+ const repository = requireRepository(input.repository, this.repositories);
868
+ const state = input.state ?? "open";
869
+ const page = input.page ?? 1;
870
+ const perPage = input.perPage ?? 10;
871
+ validateListInput(state, page, perPage);
872
+ return this.requestJson(context, {
873
+ method: "GET",
874
+ repository,
875
+ path: `/repos/${repository}/issues`,
876
+ query: { state, page, per_page: perPage },
877
+ });
878
+ }
879
+
880
+ async getIssue(
881
+ context: ToolExecutionContext,
882
+ input: Readonly<{ repository: string; issueNumber: number }>,
883
+ ): Promise<unknown> {
884
+ const repository = requireRepository(input.repository, this.repositories);
885
+ const issueNumber = policyInteger(input.issueNumber, 1, Number.MAX_SAFE_INTEGER);
886
+ return this.requestJson(context, {
887
+ method: "GET",
888
+ repository,
889
+ path: `/repos/${repository}/issues/${issueNumber}`,
890
+ });
891
+ }
892
+
893
+ async createIssue(
894
+ context: ToolExecutionContext,
895
+ input: Readonly<{ repository: string; title: string; body: string }>,
896
+ ): Promise<unknown> {
897
+ const repository = requireRepository(input.repository, this.repositories);
898
+ validateCreateInput(input.title, input.body);
899
+ return this.requestJson(context, {
900
+ method: "POST",
901
+ repository,
902
+ path: `/repos/${repository}/issues`,
903
+ body: { title: input.title, body: input.body },
904
+ mutation: true,
905
+ });
906
+ }
907
+
908
+ async addComment(
909
+ context: ToolExecutionContext,
910
+ input: Readonly<{ repository: string; issueNumber: number; body: string }>,
911
+ ): Promise<unknown> {
912
+ const repository = requireRepository(input.repository, this.repositories);
913
+ const issueNumber = policyInteger(input.issueNumber, 1, Number.MAX_SAFE_INTEGER);
914
+ validateCommentBody(input.body);
915
+ return this.requestJson(context, {
916
+ method: "POST",
917
+ repository,
918
+ path: `/repos/${repository}/issues/${issueNumber}/comments`,
919
+ body: { body: input.body },
920
+ mutation: true,
921
+ });
922
+ }
923
+
924
+ async getCommit(
925
+ context: ToolExecutionContext,
926
+ input: Readonly<{ repository: string; ref: string } & PageInput>,
927
+ ): Promise<unknown> {
928
+ const repository = requireRepository(input.repository, this.repositories);
929
+ const ref = encodePathSegment(input.ref);
930
+ const page = input.page ?? 1;
931
+ const perPage = input.perPage ?? 10;
932
+ validatePage(page, perPage);
933
+ return this.requestJson(context, {
934
+ method: "GET",
935
+ repository,
936
+ path: `/repos/${repository}/commits/${ref}`,
937
+ query: { page, per_page: perPage },
938
+ });
939
+ }
940
+
941
+ async getPullRequest(
942
+ context: ToolExecutionContext,
943
+ input: Readonly<{ repository: string; pullNumber: number }>,
944
+ ): Promise<unknown> {
945
+ const repository = requireRepository(input.repository, this.repositories);
946
+ const pullNumber = policyInteger(input.pullNumber, 1, Number.MAX_SAFE_INTEGER);
947
+ return this.requestJson(context, {
948
+ method: "GET",
949
+ repository,
950
+ path: `/repos/${repository}/pulls/${pullNumber}`,
951
+ });
952
+ }
953
+
954
+ async listPullRequestFiles(
955
+ context: ToolExecutionContext,
956
+ input: Readonly<{ repository: string; pullNumber: number } & PageInput>,
957
+ ): Promise<unknown> {
958
+ const repository = requireRepository(input.repository, this.repositories);
959
+ const pullNumber = policyInteger(input.pullNumber, 1, Number.MAX_SAFE_INTEGER);
960
+ const page = input.page ?? 1;
961
+ const perPage = input.perPage ?? 10;
962
+ validatePage(page, perPage);
963
+ return this.requestJson(context, {
964
+ method: "GET",
965
+ repository,
966
+ path: `/repos/${repository}/pulls/${pullNumber}/files`,
967
+ query: { page, per_page: perPage },
968
+ });
969
+ }
970
+
971
+ async listCheckRuns(
972
+ context: ToolExecutionContext,
973
+ input: Readonly<{
974
+ repository: string;
975
+ ref: string;
976
+ filter?: "latest" | "all";
977
+ }> &
978
+ PageInput,
979
+ ): Promise<unknown> {
980
+ const repository = requireRepository(input.repository, this.repositories);
981
+ const ref = encodePathSegment(input.ref);
982
+ const filter = input.filter ?? "latest";
983
+ const page = input.page ?? 1;
984
+ const perPage = input.perPage ?? 10;
985
+ validateFilter(filter);
986
+ validatePage(page, perPage);
987
+ return this.requestJson(context, {
988
+ method: "GET",
989
+ repository,
990
+ path: `/repos/${repository}/commits/${ref}/check-runs`,
991
+ query: { filter, page, per_page: perPage },
992
+ });
993
+ }
994
+
995
+ async getWorkflowRun(
996
+ context: ToolExecutionContext,
997
+ input: Readonly<{ repository: string; runId: number }>,
998
+ ): Promise<unknown> {
999
+ const repository = requireRepository(input.repository, this.repositories);
1000
+ const runId = policyInteger(input.runId, 1, Number.MAX_SAFE_INTEGER);
1001
+ return this.requestJson(context, {
1002
+ method: "GET",
1003
+ repository,
1004
+ path: `/repos/${repository}/actions/runs/${runId}`,
1005
+ });
1006
+ }
1007
+
1008
+ async listWorkflowJobs(
1009
+ context: ToolExecutionContext,
1010
+ input: Readonly<{
1011
+ repository: string;
1012
+ runId: number;
1013
+ filter?: "latest" | "all";
1014
+ }> &
1015
+ PageInput,
1016
+ ): Promise<unknown> {
1017
+ const repository = requireRepository(input.repository, this.repositories);
1018
+ const runId = policyInteger(input.runId, 1, Number.MAX_SAFE_INTEGER);
1019
+ const filter = input.filter ?? "latest";
1020
+ const page = input.page ?? 1;
1021
+ const perPage = input.perPage ?? 10;
1022
+ validateFilter(filter);
1023
+ validatePage(page, perPage);
1024
+ return this.requestJson(context, {
1025
+ method: "GET",
1026
+ repository,
1027
+ path: `/repos/${repository}/actions/runs/${runId}/jobs`,
1028
+ query: { filter, page, per_page: perPage },
1029
+ });
1030
+ }
1031
+
1032
+ async listFileCommits(
1033
+ context: ToolExecutionContext,
1034
+ input: Readonly<{ repository: string; path: string; ref?: string } & PageInput>,
1035
+ ): Promise<unknown> {
1036
+ const repository = requireRepository(input.repository, this.repositories);
1037
+ const path = validateContentPath(input.path);
1038
+ const page = input.page ?? 1;
1039
+ const perPage = input.perPage ?? 10;
1040
+ validatePage(page, perPage);
1041
+ const ref = input.ref === undefined ? undefined : policyString(input.ref, 1, 100);
1042
+ return this.requestJson(context, {
1043
+ method: "GET",
1044
+ repository,
1045
+ path: `/repos/${repository}/commits`,
1046
+ query: { page, path, per_page: perPage, ...(ref === undefined ? {} : { sha: ref }) },
1047
+ });
1048
+ }
1049
+
1050
+ async getRelease(
1051
+ context: ToolExecutionContext,
1052
+ input: Readonly<{ repository: string; tag: string }>,
1053
+ ): Promise<unknown> {
1054
+ const repository = requireRepository(input.repository, this.repositories);
1055
+ const tag = encodePathSegment(input.tag);
1056
+ return this.requestJson(context, {
1057
+ method: "GET",
1058
+ repository,
1059
+ path: `/repos/${repository}/releases/tags/${tag}`,
1060
+ });
1061
+ }
1062
+
1063
+ async listDeployments(
1064
+ context: ToolExecutionContext,
1065
+ input: Readonly<{
1066
+ repository: string;
1067
+ ref?: string;
1068
+ sha?: string;
1069
+ environment?: string;
1070
+ task?: string;
1071
+ }> &
1072
+ PageInput,
1073
+ ): Promise<unknown> {
1074
+ const repository = requireRepository(input.repository, this.repositories);
1075
+ const page = input.page ?? 1;
1076
+ const perPage = input.perPage ?? 10;
1077
+ validatePage(page, perPage);
1078
+ const ref = input.ref === undefined ? undefined : policyString(input.ref, 1, 100);
1079
+ const sha = input.sha === undefined ? undefined : validateDeploymentSha(input.sha);
1080
+ const environment =
1081
+ input.environment === undefined ? undefined : policyString(input.environment, 1, 100);
1082
+ const task = input.task === undefined ? undefined : policyString(input.task, 1, 100);
1083
+ return this.requestJson(context, {
1084
+ method: "GET",
1085
+ repository,
1086
+ path: `/repos/${repository}/deployments`,
1087
+ query: {
1088
+ page,
1089
+ per_page: perPage,
1090
+ ...(ref === undefined ? {} : { ref }),
1091
+ ...(sha === undefined ? {} : { sha }),
1092
+ ...(environment === undefined ? {} : { environment }),
1093
+ ...(task === undefined ? {} : { task }),
1094
+ },
1095
+ });
1096
+ }
1097
+
1098
+ async requestJson(context: ToolExecutionContext, options: RequestJsonOptions): Promise<unknown> {
1099
+ const repository = requireRepository(options.repository, this.repositories);
1100
+ const route = routeFor({ ...options, repository });
1101
+ const mutation = options.mutation ?? false;
1102
+ const pathAndQuery = options.path + queryString(options.query);
1103
+ const requestBody =
1104
+ options.body === undefined
1105
+ ? undefined
1106
+ : new TextEncoder().encode(JSON.stringify(snapshotIntegrationResult(options.body)));
1107
+ const identityHeaders = {
1108
+ accept: route === "search_code" ? SEARCH_ACCEPT : GENERAL_ACCEPT,
1109
+ "user-agent": GITHUB_USER_AGENT,
1110
+ "x-github-api-version": GITHUB_API_VERSION,
1111
+ } as const;
1112
+
1113
+ if (context.signal.aborted) throw context.signal.reason;
1114
+ let token: string;
1115
+ try {
1116
+ token = await this.tokenFor(context);
1117
+ } catch (error) {
1118
+ if (isAbortOrTimeout(error) || error instanceof IntegrationExecutionError) throw error;
1119
+ throw authError();
1120
+ }
1121
+ if (context.signal.aborted) throw context.signal.reason;
1122
+ const headers = Object.freeze({
1123
+ ...identityHeaders,
1124
+ authorization: `Bearer ${normalizedToken(token)}`,
1125
+ ...(requestBody === undefined ? {} : { "content-type": "application/json" }),
1126
+ });
1127
+
1128
+ let response: BoundedResponse | undefined;
1129
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1130
+ try {
1131
+ response = await this.http.request(
1132
+ pathAndQuery,
1133
+ {
1134
+ method: options.method,
1135
+ headers,
1136
+ ...(requestBody === undefined ? {} : { body: requestBody }),
1137
+ },
1138
+ context,
1139
+ );
1140
+ } catch (error) {
1141
+ if (isAbortOrTimeout(error) || error instanceof IntegrationExecutionError) throw error;
1142
+ if (mutation) throw new UnknownMutationError();
1143
+ throw transientError();
1144
+ }
1145
+
1146
+ if (isRateLimited(response)) {
1147
+ const delay = retryAfter(response.headers);
1148
+ if (
1149
+ options.method === "GET" &&
1150
+ attempt === 0 &&
1151
+ delay !== undefined &&
1152
+ this.deadlineAllows(context, delay * 1_000)
1153
+ ) {
1154
+ await this.sleep(delay * 1_000, context.signal);
1155
+ if (context.signal.aborted) throw context.signal.reason;
1156
+ continue;
1157
+ }
1158
+ throw rateError();
1159
+ }
1160
+ break;
1161
+ }
1162
+ if (response === undefined) throw rateError();
1163
+
1164
+ if (response.status === 401) throw authError();
1165
+ if (response.status === 403) throw apiError();
1166
+ if (response.status < 200 || response.status >= 300) {
1167
+ if (mutation && response.status >= 500) throw new UnknownMutationError();
1168
+ if (response.status === 404 || response.status === 422 || response.status < 500) {
1169
+ throw apiError();
1170
+ }
1171
+ if (mutation) throw new UnknownMutationError();
1172
+ throw transientError();
1173
+ }
1174
+
1175
+ try {
1176
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(response.bytes);
1177
+ const document: unknown = JSON.parse(decoded);
1178
+ return snapshotIntegrationResult(this.normalize(route, repository, document, options));
1179
+ } catch {
1180
+ if (mutation) throw new UnknownMutationError();
1181
+ throw transientError();
1182
+ }
1183
+ }
1184
+
1185
+ private deadlineAllows(context: ToolExecutionContext, delayMs: number): boolean {
1186
+ return (
1187
+ context.deadlineMonotonicMs === undefined ||
1188
+ context.deadlineMonotonicMs - this.monotonicNow() > delayMs
1189
+ );
1190
+ }
1191
+
1192
+ private normalize(
1193
+ route: Route,
1194
+ repository: string,
1195
+ document: unknown,
1196
+ options: RequestJsonOptions,
1197
+ ): unknown {
1198
+ if (route === "search_code") return this.normalizeSearch(repository, document);
1199
+ if (route === "get_file") return this.normalizeFile(document);
1200
+ if (route === "list_issues") return this.normalizeIssueList(document);
1201
+ if (route === "get_issue" || route === "create_issue") return this.normalizeIssue(document);
1202
+ if (route === "add_comment") return this.normalizeComment(document);
1203
+ const page = options.query?.page as number | undefined;
1204
+ const perPage = options.query?.per_page as number | undefined;
1205
+ if (route === "get_commit") return this.normalizeCommit(repository, document, page!, perPage!);
1206
+ if (route === "get_pull_request") return this.normalizePullRequest(repository, document);
1207
+ if (route === "list_pull_request_files") {
1208
+ return this.normalizePullRequestFiles(document, page!, perPage!);
1209
+ }
1210
+ if (route === "list_check_runs") {
1211
+ return this.normalizeCheckRuns(repository, document, page!, perPage!);
1212
+ }
1213
+ if (route === "get_workflow_run") return this.normalizeWorkflowRun(repository, document);
1214
+ if (route === "list_workflow_jobs") {
1215
+ return this.normalizeWorkflowJobs(
1216
+ repository,
1217
+ options.path.split("/").at(-2)!,
1218
+ document,
1219
+ page!,
1220
+ perPage!,
1221
+ );
1222
+ }
1223
+ if (route === "list_file_commits") {
1224
+ return this.normalizeFileCommits(repository, document, page!, perPage!);
1225
+ }
1226
+ if (route === "get_release") return this.normalizeRelease(repository, document);
1227
+ return this.normalizeDeployments(repository, document, page!, perPage!);
1228
+ }
1229
+
1230
+ private normalizeSearch(repository: string, document: unknown): unknown {
1231
+ const root = objectValue(document);
1232
+ const totalCount = providerInteger(root.total_count);
1233
+ const rows = arrayValue(root.items);
1234
+ for (const value of rows) {
1235
+ const item = objectValue(value);
1236
+ if (objectValue(item.repository).full_name !== repository) throw new ProviderShapeError();
1237
+ }
1238
+ const items = rows.slice(0, 20).map((value) => {
1239
+ const item = objectValue(value);
1240
+ const matches = item.text_matches ?? [];
1241
+ let fragment = "";
1242
+ if (matches !== null) {
1243
+ const matchRows = arrayValue(matches);
1244
+ if (matchRows.length > 0) {
1245
+ fragment = providerByteString(objectValue(matchRows[0]).fragment, 0, 1_048_576);
1246
+ }
1247
+ }
1248
+ return {
1249
+ path: providerCharacterString(item.path, 1, 512),
1250
+ sha: providerCharacterString(item.sha, 1, 64),
1251
+ fragment: truncateUtf8(fragment, 1_024),
1252
+ };
1253
+ });
1254
+ const result = snapshotIntegrationResult({ total_count: totalCount, items });
1255
+ if (new TextEncoder().encode(JSON.stringify(result)).byteLength > MAX_SEARCH_RESULT_BYTES) {
1256
+ throw new ProviderShapeError();
1257
+ }
1258
+ return result;
1259
+ }
1260
+
1261
+ private normalizeFile(document: unknown): unknown {
1262
+ const root = objectValue(document);
1263
+ if (root.type !== "file" || root.encoding !== "base64") throw new ProviderShapeError();
1264
+ const path = providerCharacterString(root.path, 1, 512);
1265
+ const sha = providerCharacterString(root.sha, 1, 64);
1266
+ const size = providerInteger(root.size);
1267
+ const content = providerCharacterString(root.content, 0, 1_048_576);
1268
+ if (size > MAX_FILE_BYTES) return { path, sha, size, content_omitted: true };
1269
+ const normalized = content.replace(/[\r\n]/g, "");
1270
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(normalized)) {
1271
+ throw new ProviderShapeError();
1272
+ }
1273
+ const decoded = Buffer.from(normalized, "base64");
1274
+ if (decoded.toString("base64") !== normalized || decoded.byteLength !== size) {
1275
+ throw new ProviderShapeError();
1276
+ }
1277
+ if (decoded.byteLength > MAX_FILE_BYTES) return { path, sha, size, content_omitted: true };
1278
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
1279
+ return { path, sha, size, content: text, content_omitted: false };
1280
+ }
1281
+
1282
+ private normalizeIssueList(document: unknown): unknown {
1283
+ const items = arrayValue(document)
1284
+ .slice(0, 20)
1285
+ .map((value) => {
1286
+ const issue = objectValue(value);
1287
+ const body = issue.body === null ? "" : providerByteString(issue.body, 0, 1_048_576);
1288
+ const state = issue.state;
1289
+ if (state !== "open" && state !== "closed") throw new ProviderShapeError();
1290
+ return {
1291
+ number: providerInteger(issue.number, 1),
1292
+ state,
1293
+ title: providerCharacterString(issue.title, 1, 256),
1294
+ body_preview: truncateUtf8(body, 1_024),
1295
+ };
1296
+ });
1297
+ return { items };
1298
+ }
1299
+
1300
+ private normalizeIssue(document: unknown): unknown {
1301
+ const issue = objectValue(document);
1302
+ const state = issue.state;
1303
+ if (state !== "open" && state !== "closed") throw new ProviderShapeError();
1304
+ const body = issue.body === null ? "" : providerByteString(issue.body, 0, 16_384);
1305
+ if (utf8Size(body) > 16_384) throw new ProviderShapeError();
1306
+ return {
1307
+ number: providerInteger(issue.number, 1),
1308
+ state,
1309
+ title: providerCharacterString(issue.title, 1, 256),
1310
+ body,
1311
+ url: providerCharacterString(issue.html_url, 1, MAX_URL_CHARACTERS),
1312
+ };
1313
+ }
1314
+
1315
+ private normalizeComment(document: unknown): unknown {
1316
+ const comment = objectValue(document);
1317
+ return {
1318
+ id: providerInteger(comment.id, 1),
1319
+ url: providerCharacterString(comment.html_url, 1, MAX_URL_CHARACTERS),
1320
+ };
1321
+ }
1322
+
1323
+ private normalizeCommit(
1324
+ repository: string,
1325
+ document: unknown,
1326
+ page: number,
1327
+ perPage: number,
1328
+ ): unknown {
1329
+ const root = objectValue(document);
1330
+ const commit = objectValue(root.commit);
1331
+ const message = boundedText(commit.message);
1332
+ const repositorySegments = repository.split("/");
1333
+ const authorValue = commit.author;
1334
+ const author =
1335
+ authorValue === null || authorValue === undefined
1336
+ ? null
1337
+ : (() => {
1338
+ const value = objectValue(authorValue);
1339
+ return {
1340
+ name: providerCharacterString(value.name, 1, 256),
1341
+ date: providerCharacterString(value.date, 1, 100),
1342
+ };
1343
+ })();
1344
+ const statsValue = objectValue(root.stats);
1345
+ const sha = providerCharacterString(root.sha, 1, 64);
1346
+ const base = {
1347
+ sha,
1348
+ url: providerGitHubUrl(root.html_url, "https://github.com", [
1349
+ ...repositorySegments,
1350
+ "commit",
1351
+ sha,
1352
+ ]),
1353
+ author,
1354
+ message: message.text!,
1355
+ message_truncated: message.truncated,
1356
+ stats: {
1357
+ additions: providerInteger(statsValue.additions),
1358
+ deletions: providerInteger(statsValue.deletions),
1359
+ total: providerInteger(statsValue.total),
1360
+ },
1361
+ page,
1362
+ per_page: perPage,
1363
+ };
1364
+ const parentSource = boundedRows(root.parents);
1365
+ const parents = parentSource.rows.map((value) => {
1366
+ const parent = objectValue(value);
1367
+ const parentSha = providerCharacterString(parent.sha, 1, 64);
1368
+ return {
1369
+ sha: parentSha,
1370
+ url: providerGitHubUrl(parent.html_url, "https://github.com", [
1371
+ ...repositorySegments,
1372
+ "commit",
1373
+ parentSha,
1374
+ ]),
1375
+ };
1376
+ });
1377
+ const fileSource = boundedRows(root.files);
1378
+ const files = fileSource.rows.map((value) => {
1379
+ const file = objectValue(value);
1380
+ return {
1381
+ filename: providerCharacterString(file.filename, 1, 512),
1382
+ status: providerCharacterString(file.status, 1, 100),
1383
+ additions: providerInteger(file.additions),
1384
+ deletions: providerInteger(file.deletions),
1385
+ changes: providerInteger(file.changes),
1386
+ };
1387
+ });
1388
+ const complete = {
1389
+ ...base,
1390
+ parents,
1391
+ parents_omitted_count: parentSource.omitted,
1392
+ files,
1393
+ files_omitted_count: fileSource.omitted,
1394
+ };
1395
+ const fittedComplete = fitObjectToModelBudget(complete, [
1396
+ { path: ["message"], truncatedPath: ["message_truncated"] },
1397
+ ]);
1398
+ if (fittedComplete !== undefined) return fittedComplete;
1399
+ const rowFittingBase = { ...base, message: "", message_truncated: true };
1400
+ const parentResult = fitRows(
1401
+ parents,
1402
+ (items, omittedCount) => ({
1403
+ ...rowFittingBase,
1404
+ parents: items,
1405
+ parents_omitted_count: omittedCount,
1406
+ files: [] as readonly Record<string, unknown>[],
1407
+ files_omitted_count: 0,
1408
+ }),
1409
+ { initialOmitted: parentSource.omitted },
1410
+ );
1411
+ return fitRows(
1412
+ files,
1413
+ (items, omittedCount) => ({
1414
+ ...rowFittingBase,
1415
+ parents: parentResult.parents,
1416
+ parents_omitted_count: parentResult.parents_omitted_count,
1417
+ files: items,
1418
+ files_omitted_count: omittedCount,
1419
+ }),
1420
+ { initialOmitted: fileSource.omitted },
1421
+ );
1422
+ }
1423
+
1424
+ private normalizePullRequest(repository: string, document: unknown): unknown {
1425
+ const root = objectValue(document);
1426
+ const title = boundedText(root.title);
1427
+ const body = boundedText(root.body ?? null, true);
1428
+ const base = objectValue(root.base);
1429
+ const head = objectValue(root.head);
1430
+ const number = providerInteger(root.number, 1);
1431
+ return requireObjectWithinModelBudget(
1432
+ {
1433
+ number,
1434
+ state: providerCharacterString(root.state, 1, 100),
1435
+ title: title.text!,
1436
+ title_truncated: title.truncated,
1437
+ body: body.text,
1438
+ body_truncated: body.truncated,
1439
+ base_sha: providerCharacterString(base.sha, 1, 64),
1440
+ head_sha: providerCharacterString(head.sha, 1, 64),
1441
+ merge_sha: providerNullableCharacterString(root.merge_commit_sha, 1, 64),
1442
+ url: providerGitHubUrl(root.html_url, "https://github.com", [
1443
+ ...repository.split("/"),
1444
+ "pull",
1445
+ String(number),
1446
+ ]),
1447
+ },
1448
+ [
1449
+ { path: ["body"], truncatedPath: ["body_truncated"] },
1450
+ { path: ["title"], truncatedPath: ["title_truncated"] },
1451
+ ],
1452
+ );
1453
+ }
1454
+
1455
+ private normalizePullRequestFiles(document: unknown, page: number, perPage: number): unknown {
1456
+ const source = boundedRows(document);
1457
+ const rows = source.rows.map((value) => {
1458
+ const root = objectValue(value);
1459
+ const patch = boundedText(root.patch ?? null, true);
1460
+ return {
1461
+ filename: providerCharacterString(root.filename, 1, 512),
1462
+ status: providerCharacterString(root.status, 1, 100),
1463
+ additions: providerInteger(root.additions),
1464
+ deletions: providerInteger(root.deletions),
1465
+ changes: providerInteger(root.changes),
1466
+ patch: patch.text,
1467
+ patch_truncated: patch.truncated,
1468
+ };
1469
+ });
1470
+ return fitRows(
1471
+ rows,
1472
+ (items, omittedCount) => ({ items, omitted_count: omittedCount, page, per_page: perPage }),
1473
+ {
1474
+ initialOmitted: source.omitted,
1475
+ textFields: [{ path: ["patch"], truncatedPath: ["patch_truncated"] }],
1476
+ },
1477
+ );
1478
+ }
1479
+
1480
+ private normalizeCheckRuns(
1481
+ repository: string,
1482
+ document: unknown,
1483
+ page: number,
1484
+ perPage: number,
1485
+ ): unknown {
1486
+ const root = objectValue(document);
1487
+ const totalCount = providerInteger(root.total_count);
1488
+ const source = boundedRows(root.check_runs);
1489
+ const rows = source.rows.map((value) => {
1490
+ const run = objectValue(value);
1491
+ const output = objectValue(run.output);
1492
+ const title = boundedText(output.title ?? null, true);
1493
+ const summary = boundedText(output.summary ?? null, true);
1494
+ const text = boundedText(output.text ?? null, true);
1495
+ const id = providerInteger(run.id, 1);
1496
+ return {
1497
+ id,
1498
+ name: providerCharacterString(run.name, 1, 256),
1499
+ status: providerCharacterString(run.status, 1, 100),
1500
+ conclusion: providerNullableCharacterString(run.conclusion, 1, 100),
1501
+ url: providerGitHubUrl(run.html_url, "https://github.com", [
1502
+ ...repository.split("/"),
1503
+ "runs",
1504
+ String(id),
1505
+ ]),
1506
+ output: {
1507
+ title: title.text,
1508
+ title_truncated: title.truncated,
1509
+ summary: summary.text,
1510
+ summary_truncated: summary.truncated,
1511
+ text: text.text,
1512
+ text_truncated: text.truncated,
1513
+ },
1514
+ };
1515
+ });
1516
+ return fitRows(
1517
+ rows,
1518
+ (items, omittedCount) => ({
1519
+ total_count: totalCount,
1520
+ items,
1521
+ omitted_count: omittedCount,
1522
+ page,
1523
+ per_page: perPage,
1524
+ }),
1525
+ {
1526
+ initialOmitted: source.omitted,
1527
+ textFields: [
1528
+ {
1529
+ path: ["output", "title"],
1530
+ truncatedPath: ["output", "title_truncated"],
1531
+ },
1532
+ {
1533
+ path: ["output", "summary"],
1534
+ truncatedPath: ["output", "summary_truncated"],
1535
+ },
1536
+ { path: ["output", "text"], truncatedPath: ["output", "text_truncated"] },
1537
+ ],
1538
+ },
1539
+ );
1540
+ }
1541
+
1542
+ private normalizeWorkflowRun(repository: string, document: unknown): unknown {
1543
+ const root = objectValue(document);
1544
+ const id = providerInteger(root.id, 1);
1545
+ return assertModelResultBudget({
1546
+ id,
1547
+ workflow: providerCharacterString(root.name, 1, 256),
1548
+ event: providerCharacterString(root.event, 1, 100),
1549
+ status: providerCharacterString(root.status, 1, 100),
1550
+ conclusion: providerNullableCharacterString(root.conclusion, 1, 100),
1551
+ head_sha: providerCharacterString(root.head_sha, 1, 64),
1552
+ attempt: providerInteger(root.run_attempt, 1),
1553
+ url: providerGitHubUrl(root.html_url, "https://github.com", [
1554
+ ...repository.split("/"),
1555
+ "actions",
1556
+ "runs",
1557
+ String(id),
1558
+ ]),
1559
+ });
1560
+ }
1561
+
1562
+ private normalizeWorkflowJobs(
1563
+ repository: string,
1564
+ runId: string,
1565
+ document: unknown,
1566
+ page: number,
1567
+ perPage: number,
1568
+ ): unknown {
1569
+ const root = objectValue(document);
1570
+ const totalCount = providerInteger(root.total_count);
1571
+ const source = boundedRows(root.jobs);
1572
+ const rows = source.rows.map((value) => {
1573
+ const job = objectValue(value);
1574
+ const stepSource =
1575
+ job.steps === undefined || job.steps === null
1576
+ ? { rows: [] as readonly unknown[], omitted: 0 }
1577
+ : boundedRows(job.steps);
1578
+ const steps = stepSource.rows.map((stepValue) => {
1579
+ const step = objectValue(stepValue);
1580
+ return {
1581
+ number: providerInteger(step.number, 1),
1582
+ name: providerCharacterString(step.name, 1, 256),
1583
+ status: providerCharacterString(step.status, 1, 100),
1584
+ conclusion: providerOptionalNullableCharacterString(step.conclusion, 1, 100),
1585
+ started_at: providerOptionalNullableCharacterString(step.started_at, 1, 100),
1586
+ completed_at: providerOptionalNullableCharacterString(step.completed_at, 1, 100),
1587
+ };
1588
+ });
1589
+ const id = providerInteger(job.id, 1);
1590
+ return {
1591
+ id,
1592
+ name: providerCharacterString(job.name, 1, 256),
1593
+ status: providerCharacterString(job.status, 1, 100),
1594
+ conclusion: providerOptionalNullableCharacterString(job.conclusion, 1, 100),
1595
+ started_at: providerOptionalNullableCharacterString(job.started_at, 1, 100),
1596
+ completed_at: providerOptionalNullableCharacterString(job.completed_at, 1, 100),
1597
+ url: providerGitHubUrl(job.html_url, "https://github.com", [
1598
+ ...repository.split("/"),
1599
+ "actions",
1600
+ "runs",
1601
+ runId,
1602
+ "job",
1603
+ String(id),
1604
+ ]),
1605
+ steps,
1606
+ steps_omitted_count: stepSource.omitted,
1607
+ };
1608
+ });
1609
+ return fitRows(
1610
+ rows,
1611
+ (items, omittedCount) => ({
1612
+ total_count: totalCount,
1613
+ items,
1614
+ omitted_count: omittedCount,
1615
+ page,
1616
+ per_page: perPage,
1617
+ }),
1618
+ {
1619
+ initialOmitted: source.omitted,
1620
+ shrink: (row, fits) => {
1621
+ const candidate = structuredClone(row);
1622
+ const steps = candidate.steps as Record<string, unknown>[];
1623
+ while (!fits(candidate) && steps.length > 0) {
1624
+ steps.pop();
1625
+ candidate.steps_omitted_count = (candidate.steps_omitted_count as number) + 1;
1626
+ }
1627
+ return fits(candidate) ? candidate : undefined;
1628
+ },
1629
+ },
1630
+ );
1631
+ }
1632
+
1633
+ private normalizeFileCommits(
1634
+ repository: string,
1635
+ document: unknown,
1636
+ page: number,
1637
+ perPage: number,
1638
+ ): unknown {
1639
+ const source = boundedRows(document);
1640
+ const rows = source.rows.map((value) => {
1641
+ const root = objectValue(value);
1642
+ const commit = objectValue(root.commit);
1643
+ const message = boundedText(commit.message);
1644
+ const authorValue = commit.author;
1645
+ const authorDate =
1646
+ authorValue === null || authorValue === undefined
1647
+ ? null
1648
+ : providerOptionalNullableCharacterString(objectValue(authorValue).date, 1, 100);
1649
+ const sha = providerCharacterString(root.sha, 1, 64);
1650
+ return {
1651
+ sha,
1652
+ message: message.text!,
1653
+ message_truncated: message.truncated,
1654
+ author_date: authorDate,
1655
+ url: providerGitHubUrl(root.html_url, "https://github.com", [
1656
+ ...repository.split("/"),
1657
+ "commit",
1658
+ sha,
1659
+ ]),
1660
+ };
1661
+ });
1662
+ return fitRows(
1663
+ rows,
1664
+ (items, omittedCount) => ({ items, omitted_count: omittedCount, page, per_page: perPage }),
1665
+ {
1666
+ initialOmitted: source.omitted,
1667
+ textFields: [{ path: ["message"], truncatedPath: ["message_truncated"] }],
1668
+ },
1669
+ );
1670
+ }
1671
+
1672
+ private normalizeRelease(repository: string, document: unknown): unknown {
1673
+ const root = objectValue(document);
1674
+ const body = boundedText(root.body ?? null, true);
1675
+ const tag = providerCharacterString(root.tag_name, 1, 100);
1676
+ return requireObjectWithinModelBudget(
1677
+ {
1678
+ tag,
1679
+ target: providerCharacterString(root.target_commitish, 1, 100),
1680
+ draft: providerBoolean(root.draft),
1681
+ prerelease: providerBoolean(root.prerelease),
1682
+ published_at: providerOptionalNullableCharacterString(root.published_at, 1, 100),
1683
+ body: body.text,
1684
+ body_truncated: body.truncated,
1685
+ url: providerGitHubUrl(root.html_url, "https://github.com", [
1686
+ ...repository.split("/"),
1687
+ "releases",
1688
+ "tag",
1689
+ ...tag.split("/"),
1690
+ ]),
1691
+ },
1692
+ [{ path: ["body"], truncatedPath: ["body_truncated"] }],
1693
+ );
1694
+ }
1695
+
1696
+ private normalizeDeployments(
1697
+ repository: string,
1698
+ document: unknown,
1699
+ page: number,
1700
+ perPage: number,
1701
+ ): unknown {
1702
+ const source = boundedRows(document);
1703
+ const rows = source.rows.map((value) => {
1704
+ const root = objectValue(value);
1705
+ const id = providerInteger(root.id, 1);
1706
+ return {
1707
+ id,
1708
+ ref: providerCharacterString(root.ref, 1, 100),
1709
+ sha: providerCharacterString(root.sha, 1, 64),
1710
+ environment: providerCharacterString(root.environment, 1, 100),
1711
+ task: providerCharacterString(root.task, 1, 100),
1712
+ created_at: providerCharacterString(root.created_at, 1, 100),
1713
+ url: providerGitHubUrl(root.url, "https://api.github.com", [
1714
+ "repos",
1715
+ ...repository.split("/"),
1716
+ "deployments",
1717
+ String(id),
1718
+ ]),
1719
+ };
1720
+ });
1721
+ return fitRows(
1722
+ rows,
1723
+ (items, omittedCount) => ({ items, omitted_count: omittedCount, page, per_page: perPage }),
1724
+ { initialOmitted: source.omitted },
1725
+ );
1726
+ }
1727
+ }