@typeship-ax/mcp 0.6.0 → 0.8.0

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 (79) hide show
  1. package/README.md +1 -1
  2. package/api.json +5597 -3010
  3. package/api.md +433 -54
  4. package/dist/core/http.d.ts +6 -92
  5. package/dist/core/http.d.ts.map +1 -1
  6. package/dist/core/http.js +70 -209
  7. package/dist/core/pagination.d.ts.map +1 -1
  8. package/dist/core/pagination.js +6 -34
  9. package/dist/dates.d.ts +0 -2
  10. package/dist/dates.d.ts.map +1 -1
  11. package/dist/dates.js +0 -1
  12. package/dist/docs.d.ts +11 -0
  13. package/dist/docs.d.ts.map +1 -0
  14. package/dist/docs.js +114 -0
  15. package/dist/errors.d.ts +27 -27
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +7 -7
  18. package/dist/index.d.ts +19 -11
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +23 -13
  21. package/dist/mcp-protocol.d.ts +19 -24
  22. package/dist/mcp-protocol.d.ts.map +1 -1
  23. package/dist/mcp-protocol.js +160 -124
  24. package/dist/mcp.js +16 -19
  25. package/dist/ops.d.ts +5 -0
  26. package/dist/ops.d.ts.map +1 -1
  27. package/dist/ops.js +31 -17
  28. package/dist/resources/account.d.ts +2 -2
  29. package/dist/resources/account.d.ts.map +1 -1
  30. package/dist/resources/api-keys.d.ts +10 -5
  31. package/dist/resources/api-keys.d.ts.map +1 -1
  32. package/dist/resources/api-keys.js +3 -1
  33. package/dist/resources/definition-revisions.d.ts +58 -0
  34. package/dist/resources/definition-revisions.d.ts.map +1 -0
  35. package/dist/resources/definition-revisions.js +110 -0
  36. package/dist/resources/definitions.d.ts +24 -0
  37. package/dist/resources/definitions.d.ts.map +1 -0
  38. package/dist/resources/definitions.js +51 -0
  39. package/dist/resources/generate.d.ts +5 -5
  40. package/dist/resources/generate.d.ts.map +1 -1
  41. package/dist/resources/generate.js +3 -3
  42. package/dist/resources/generations.d.ts +3 -3
  43. package/dist/resources/generations.d.ts.map +1 -1
  44. package/dist/resources/generations.js +1 -1
  45. package/dist/resources/projects.d.ts +66 -26
  46. package/dist/resources/projects.d.ts.map +1 -1
  47. package/dist/resources/projects.js +87 -13
  48. package/dist/resources/targets.d.ts +86 -0
  49. package/dist/resources/targets.d.ts.map +1 -0
  50. package/dist/resources/targets.js +184 -0
  51. package/dist/schemas.d.ts.map +1 -1
  52. package/dist/schemas.js +119 -62
  53. package/dist/types.d.ts +1761 -222
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +9 -3
  56. package/package.json +1 -1
  57. package/src/core/http.ts +75 -293
  58. package/src/core/pagination.ts +6 -30
  59. package/src/dates.ts +0 -1
  60. package/src/docs.ts +101 -0
  61. package/src/errors.ts +30 -30
  62. package/src/index.ts +23 -13
  63. package/src/mcp-protocol.ts +170 -120
  64. package/src/mcp.ts +22 -22
  65. package/src/ops.ts +43 -17
  66. package/src/resources/account.ts +3 -3
  67. package/src/resources/api-keys.ts +22 -7
  68. package/src/resources/definition-revisions.ts +198 -0
  69. package/src/resources/definitions.ts +97 -0
  70. package/src/resources/generate.ts +6 -6
  71. package/src/resources/generations.ts +4 -4
  72. package/src/resources/projects.ts +182 -37
  73. package/src/resources/targets.ts +346 -0
  74. package/src/schemas.ts +119 -62
  75. package/src/types.ts +1947 -281
  76. package/dist/resources/spec-revisions.d.ts +0 -47
  77. package/dist/resources/spec-revisions.d.ts.map +0 -1
  78. package/dist/resources/spec-revisions.js +0 -90
  79. package/src/resources/spec-revisions.ts +0 -150
package/src/core/http.ts CHANGED
@@ -17,7 +17,12 @@ export interface RequestOptions {
17
17
  export interface ResponseMeta {
18
18
  status: number;
19
19
  headers: Headers;
20
- /** x-request-id / request-id header when the server sends one. */
20
+ /** Parsed wire body before it is narrowed to the generated response type.
21
+ * Use this escape hatch for additive fields or variants introduced after
22
+ * this generator edition. */
23
+ rawBody?: unknown;
24
+ /** Request identifier from the JSON response body, or from headers for
25
+ * raw and bodyless responses. */
21
26
  requestId?: string;
22
27
  }
23
28
 
@@ -37,15 +42,6 @@ export interface RequestContext {
37
42
  attempt: number;
38
43
  }
39
44
 
40
- /** One server-sent event from a text/event-stream response. */
41
- export interface SseEvent {
42
- /** The `event:` field; undefined for unnamed events. */
43
- event?: string;
44
- /** The `id:` field, when the server sends one. */
45
- id?: string;
46
- /** Concatenated `data:` lines. Parse as JSON if your API sends JSON. */
47
- data: string;
48
- }
49
45
 
50
46
  /**
51
47
  * Every SDK call returns a discriminated result instead of throwing.
@@ -85,97 +81,6 @@ export class UnexpectedApiError extends ApiError<number, unknown> {
85
81
  }
86
82
  }
87
83
 
88
- /** A 200 response whose GraphQL payload carried errors. */
89
- export class GraphQLRequestError extends Error {
90
- /** The raw errors array from the GraphQL response. */
91
- readonly errors: { message?: string; path?: unknown[]; extensions?: unknown }[];
92
- readonly response: ResponseMeta;
93
-
94
- constructor(errors: unknown[], response: ResponseMeta) {
95
- const first = (errors[0] as { message?: string } | undefined)?.message;
96
- super(first ?? "GraphQL request returned errors");
97
- this.name = "GraphQLRequestError";
98
- this.errors = errors as GraphQLRequestError["errors"];
99
- this.response = response;
100
- }
101
- }
102
-
103
- // ---------------------------------------------------------------------------
104
- // GraphQL selections — typed field picking over the schema's result types
105
- // ---------------------------------------------------------------------------
106
-
107
- type Primitive = string | number | boolean | bigint | symbol | null | undefined;
108
- type Unwrap<T> = NonNullable<T> extends readonly (infer U)[] ? Unwrap<U> : NonNullable<T>;
109
- type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
110
- type TypeNameOf<T> = T extends { __typename?: infer N } ? Extract<N, string> : never;
111
- type Prettify<T> = { [K in keyof T]: T[K] } & {};
112
-
113
- /**
114
- * A selection over a GraphQL result type `T`: `true` picks a leaf field, a
115
- * nested object picks inside an object field, and `on` picks per concrete
116
- * type when `T` is a union or interface (`{ on: { Transaction: { amount: true } } }`).
117
- * Keys are checked against the schema, so a typo is a compile error.
118
- */
119
- export type Selection<T> = Unwrap<T> extends Primitive ? never : SelectionObject<Unwrap<T>>;
120
-
121
- type FieldSelection<V> = Unwrap<V> extends Primitive ? true : SelectionObject<Unwrap<V>>;
122
-
123
- type SelectionObject<T> = {
124
- [K in Extract<keyof T, string> as K extends "__typename" ? never : K]?: FieldSelection<T[K]>;
125
- } & { __typename?: true } & (IsUnion<T> extends true
126
- ? { on?: { [N in TypeNameOf<T>]?: SelectionObject<Extract<T, { __typename?: N }>> } }
127
- : { on?: never });
128
-
129
- /** The result type a {@link Selection} `S` produces from result type `T`:
130
- * exactly the fields picked, nullability and lists preserved, union members
131
- * narrowed by `__typename`. */
132
- export type Selected<T, S> = T extends readonly (infer U)[]
133
- ? Selected<U, S>[]
134
- : T extends Primitive
135
- ? T
136
- : S extends object
137
- ? Prettify<SelectedMember<T, S>>
138
- : never;
139
-
140
- type SelectedMember<T, S> = (S extends { on?: infer O }
141
- ? O extends object
142
- ? TypeNameOf<T> extends keyof O
143
- ? PickSelected<T, NonNullable<O[TypeNameOf<T>]>>
144
- : {}
145
- : {}
146
- : {}) &
147
- PickSelected<T, S>;
148
-
149
- type PickSelected<T, S> = {
150
- -readonly [K in keyof S as K extends "on" ? never : K extends keyof T ? (S[K] extends true | object ? K : never) : never]-?: K extends "__typename"
151
- ? NonNullable<T[K & keyof T]>
152
- : S[K] extends true
153
- ? Exclude<T[K & keyof T], undefined>
154
- : Selected<Exclude<T[K & keyof T], undefined>, S[K]>;
155
- };
156
-
157
- /** A selection object or a raw selection set (`"{ id name }"`). */
158
- export type SelectionInput = string | Record<string, unknown>;
159
-
160
- /** Serialize a selection object to a GraphQL selection set. Strings pass
161
- * through untouched. */
162
- export function selectionToString(selection: SelectionInput): string {
163
- if (typeof selection === "string") return selection;
164
- const parts: string[] = [];
165
- for (const [key, value] of Object.entries(selection)) {
166
- if (!value) continue;
167
- if (key === "on" && typeof value === "object") {
168
- for (const [typeName, sub] of Object.entries(value as Record<string, unknown>)) {
169
- if (sub && typeof sub === "object") parts.push("... on " + typeName + " " + selectionToString(sub as Record<string, unknown>));
170
- }
171
- } else if (value === true) {
172
- parts.push(key);
173
- } else if (typeof value === "object") {
174
- parts.push(key + " " + selectionToString(value as Record<string, unknown>));
175
- }
176
- }
177
- return "{ " + (parts.length > 0 ? parts.join(" ") : "__typename") + " }";
178
- }
179
84
 
180
85
  // ---------------------------------------------------------------------------
181
86
  // Optional runtime validation — zero-dependency, schema table in schemas.ts
@@ -341,14 +246,9 @@ export interface CoreRequest {
341
246
  errors?: Record<string, ErrorCtor>;
342
247
  /** Idempotent requests are retried automatically. */
343
248
  idempotent?: boolean;
344
- /** Success body is text/event-stream: yield SseEvents instead of parsing. */
345
- stream?: boolean;
346
249
  /** Header name auto-filled with one UUID per call (stable across retries)
347
250
  * when the caller doesn't supply a value. */
348
251
  idempotencyKey?: string;
349
- /** GraphQL: unwrap body.data[field] and turn body.errors into a
350
- * GraphQLRequestError. */
351
- graphqlField?: string;
352
252
  /** Key into the schemas table for optional runtime validation. */
353
253
  schemaKey?: string;
354
254
  /** Operation-level retry policy (x-typeship-retries), merged over the
@@ -509,16 +409,20 @@ export class HttpCore {
509
409
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
510
410
  let response: Response;
511
411
  const attemptStarted = Date.now();
412
+ const emitResponseDebug = (value: Response, body?: unknown) => this.config.debug?.({
413
+ method: req.method,
414
+ path: req.path,
415
+ status: value.status,
416
+ durationMs: Date.now() - attemptStarted,
417
+ attempt: attempt + 1,
418
+ requestId:
419
+ requestIdFromBody(body) ??
420
+ value.headers.get("request-id") ??
421
+ value.headers.get("x-request-id") ??
422
+ undefined,
423
+ });
512
424
  try {
513
425
  response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
514
- this.config.debug?.({
515
- method: req.method,
516
- path: req.path,
517
- status: response.status,
518
- durationMs: Date.now() - attemptStarted,
519
- attempt: attempt + 1,
520
- requestId: response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined,
521
- });
522
426
  } catch (cause) {
523
427
  lastError = cause;
524
428
  this.config.debug?.({
@@ -541,13 +445,11 @@ export class HttpCore {
541
445
  }
542
446
 
543
447
  if (response.ok) {
544
- if (req.stream) {
545
- return { ok: true, data: sseEvents(response) as T, response: meta(response) };
546
- }
547
448
  let data: T;
548
449
  try {
549
450
  data = (await parseBody(response, req.method)) as T;
550
451
  } catch (cause) {
452
+ emitResponseDebug(response);
551
453
  const error = new TransportError(
552
454
  "The response body read was aborted before completing",
553
455
  cause,
@@ -555,9 +457,13 @@ export class HttpCore {
555
457
  await this.config.onError?.(error, { method: req.method, path: req.path });
556
458
  return { ok: false, error, response: meta(response) };
557
459
  }
558
- if (opSchemas?.res && this.config.validate!.responses && data !== undefined) {
460
+ emitResponseDebug(response, data);
461
+ const responseMeta = meta(response, data);
462
+ let responseData: unknown = data;
463
+ let shouldValidateResponse = responseData !== undefined;
464
+ if (opSchemas?.res && this.config.validate!.responses && shouldValidateResponse) {
559
465
  const violations: Violation[] = [];
560
- validateAgainstSchema(data, opSchemas.res, "response", violations, this.config.schemaDefs);
466
+ validateAgainstSchema(responseData, opSchemas.res, "response", violations, this.config.schemaDefs);
561
467
  if (violations.length > 0) {
562
468
  const validationError = new ValidationError("response", violations);
563
469
  if (this.config.validate!.mode === "warn") {
@@ -565,21 +471,11 @@ export class HttpCore {
565
471
  } else {
566
472
  const error = validationError as unknown as E;
567
473
  await this.config.onError?.(error, { method: req.method, path: req.path });
568
- return { ok: false, error, response: meta(response) };
474
+ return { ok: false, error, response: responseMeta };
569
475
  }
570
476
  }
571
477
  }
572
- if (req.graphqlField) {
573
- const payload = data as { data?: Record<string, unknown>; errors?: unknown[] } | undefined;
574
- const responseMeta = meta(response);
575
- if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
576
- const gqlError = new GraphQLRequestError(payload.errors, responseMeta) as unknown as E;
577
- await this.config.onError?.(gqlError, { method: req.method, path: req.path });
578
- return { ok: false, error: gqlError, response: responseMeta };
579
- }
580
- return { ok: true, data: payload?.data?.[req.graphqlField] as T, response: responseMeta };
581
- }
582
- return { ok: true, data, response: meta(response) };
478
+ return { ok: true, data, response: responseMeta };
583
479
  }
584
480
 
585
481
  // 429 is safe to retry regardless of idempotency; other retryable
@@ -588,7 +484,15 @@ export class HttpCore {
588
484
  retryableStatuses.has(response.status) &&
589
485
  (retryAllowed || response.status === 429);
590
486
  if (attempt < maxRetries && retryableStatus) {
591
- await sleep(retryAfterMs(response) ?? backoff(attempt, policy));
487
+ const delay = retryAfterMs(response) ?? backoff(attempt, policy);
488
+ let retryBody: unknown;
489
+ try {
490
+ retryBody = await parseBody(response, req.method);
491
+ } catch {
492
+ retryBody = undefined;
493
+ }
494
+ emitResponseDebug(response, retryBody);
495
+ await sleep(delay);
592
496
  continue;
593
497
  }
594
498
 
@@ -598,7 +502,8 @@ export class HttpCore {
598
502
  } catch {
599
503
  body = undefined; // error responses keep their status even if the body read aborts
600
504
  }
601
- const responseMeta = meta(response);
505
+ emitResponseDebug(response, body);
506
+ const responseMeta = meta(response, body);
602
507
  const Ctor =
603
508
  req.errors?.[String(response.status)] ??
604
509
  req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
@@ -645,28 +550,14 @@ export class HttpCore {
645
550
  };
646
551
  await this.config.onRequest?.(context);
647
552
 
648
- // Streaming responses are exempt from the attempt timeout once headers
649
- // arrive (an event stream may stay open far longer than timeoutMs);
650
- // everything else keeps the timeout armed through the body read, so a
651
- // stalled body aborts instead of hanging.
652
553
  const signals: AbortSignal[] = [];
653
- let clearStreamTimeout: (() => void) | undefined;
654
- if (req.stream) {
655
- const headersTimeout = new AbortController();
656
- const timer = setTimeout(
657
- () => headersTimeout.abort(new DOMException("Timed out waiting for response headers", "TimeoutError")),
658
- timeoutMs,
659
- );
660
- (timer as { unref?: () => void }).unref?.();
661
- clearStreamTimeout = () => clearTimeout(timer);
662
- signals.push(headersTimeout.signal);
663
- } else {
554
+ {
664
555
  signals.push(AbortSignal.timeout(timeoutMs));
665
556
  }
666
557
  if (req.options?.signal) signals.push(req.options.signal);
667
558
 
668
559
  try {
669
- const signal = AbortSignal.any(signals);
560
+ const signal = composeSignals(signals);
670
561
  const response = this.manualRedirects
671
562
  ? await this.followRedirects(context, body, signal)
672
563
  : await this.config.fetch(context.url, {
@@ -678,7 +569,6 @@ export class HttpCore {
678
569
  await this.config.onResponse?.(response, context);
679
570
  return response;
680
571
  } finally {
681
- clearStreamTimeout?.();
682
572
  }
683
573
  }
684
574
 
@@ -765,55 +655,6 @@ async function resolveAuthValue(value: AuthValue): Promise<string> {
765
655
  return typeof value === "function" ? await value() : value;
766
656
  }
767
657
 
768
- /** Parse a text/event-stream body into SseEvents, lazily. */
769
- async function* sseEvents(response: Response): AsyncGenerator<SseEvent, void, undefined> {
770
- if (!response.body) return;
771
- const reader = response.body.getReader();
772
- const decoder = new TextDecoder();
773
- let buffer = "";
774
- let dataLines: string[] = [];
775
- let eventName: string | undefined;
776
- let eventId: string | undefined;
777
-
778
- const flush = (): SseEvent | undefined => {
779
- if (dataLines.length === 0) return undefined;
780
- const event: SseEvent = { data: dataLines.join("\n") };
781
- if (eventName !== undefined) event.event = eventName;
782
- if (eventId !== undefined) event.id = eventId;
783
- dataLines = [];
784
- eventName = undefined;
785
- return event;
786
- };
787
-
788
- try {
789
- while (true) {
790
- const { done, value } = await reader.read();
791
- if (done) break;
792
- buffer += decoder.decode(value, { stream: true });
793
- let newline: number;
794
- while ((newline = buffer.indexOf("\n")) !== -1) {
795
- const hasCr = newline > 0 && buffer[newline - 1] === "\r";
796
- const line = buffer.slice(0, hasCr ? newline - 1 : newline);
797
- buffer = buffer.slice(newline + 1);
798
- if (line === "") {
799
- const event = flush();
800
- if (event) yield event;
801
- } else if (line.startsWith("data:")) {
802
- dataLines.push(line.slice(5).replace(/^ /, ""));
803
- } else if (line.startsWith("event:")) {
804
- eventName = line.slice(6).replace(/^ /, "");
805
- } else if (line.startsWith("id:")) {
806
- eventId = line.slice(3).replace(/^ /, "");
807
- }
808
- // comments (":") and "retry:" are intentionally ignored
809
- }
810
- }
811
- const last = flush();
812
- if (last) yield last;
813
- } finally {
814
- reader.releaseLock();
815
- }
816
- }
817
658
 
818
659
  /** Default rendering for debug events (the boolean debug:true sink). */
819
660
  export function formatDebugEvent(name: string, event: DebugEvent): string {
@@ -833,74 +674,6 @@ export function bearerAuth(token: AuthValue): AuthValue {
833
674
  return "Bearer " + token;
834
675
  }
835
676
 
836
- export interface ClientCredentialsConfig {
837
- clientId: string;
838
- clientSecret: string;
839
- tokenUrl: string;
840
- scopes?: string[];
841
- /** Extra token-request parameters, e.g. the "audience" some
842
- * authorization servers require for API-valid access tokens. */
843
- tokenParams?: Record<string, string>;
844
- /** How credentials reach the token endpoint. Default "post"
845
- * (client_secret_post, form fields); "basic" sends an Authorization
846
- * header (client_secret_basic). */
847
- authMethod?: "post" | "basic";
848
- fetchImpl?: typeof fetch;
849
- }
850
-
851
- /**
852
- * OAuth2 client-credentials token source: fetches from the token URL,
853
- * caches until expiry (60s early refresh), and shares one in-flight
854
- * request across concurrent callers. Returned function plugs in as an
855
- * Authorization AuthValue, resolved before every attempt.
856
- */
857
- export function oauthClientCredentials(config: ClientCredentialsConfig): () => Promise<string> {
858
- let token: string | undefined;
859
- let expiresAt = 0;
860
- let inflight: Promise<string> | undefined;
861
- const fetchImpl = config.fetchImpl ?? fetch;
862
-
863
- async function fetchToken(): Promise<string> {
864
- const params = new URLSearchParams({ grant_type: "client_credentials" });
865
- if (config.scopes !== undefined && config.scopes.length > 0) {
866
- params.set("scope", config.scopes.join(" "));
867
- }
868
- for (const [key, value] of Object.entries(config.tokenParams ?? {})) {
869
- params.set(key, value);
870
- }
871
- const headers: Record<string, string> = {
872
- "Content-Type": "application/x-www-form-urlencoded",
873
- Accept: "application/json",
874
- };
875
- if (config.authMethod === "basic") {
876
- headers["Authorization"] = "Basic " + toBase64(config.clientId + ":" + config.clientSecret);
877
- } else {
878
- params.set("client_id", config.clientId);
879
- params.set("client_secret", config.clientSecret);
880
- }
881
- const response = await fetchImpl(config.tokenUrl, { method: "POST", headers, body: params.toString() });
882
- const body = (await response.json().catch(() => null)) as
883
- | { access_token?: string; expires_in?: number; error?: string }
884
- | null;
885
- if (!response.ok || typeof body?.access_token !== "string") {
886
- throw new TransportError(
887
- "OAuth token request failed (HTTP " + response.status + (body?.error ? ": " + body.error : "") + ")",
888
- body,
889
- );
890
- }
891
- token = body.access_token;
892
- expiresAt = body.expires_in !== undefined
893
- ? Date.now() + body.expires_in * 1000 - 60_000
894
- : Number.MAX_SAFE_INTEGER;
895
- return token;
896
- }
897
-
898
- return async () => {
899
- if (token !== undefined && Date.now() < expiresAt) return "Bearer " + token;
900
- inflight ??= fetchToken().finally(() => { inflight = undefined; });
901
- return "Bearer " + (await inflight);
902
- };
903
- }
904
677
 
905
678
  /**
906
679
  * Bracket-style deep encoding shared by query strings and form bodies:
@@ -925,31 +698,8 @@ function serializeBody(req: CoreRequest): { body: NonNullable<RequestInit["body"
925
698
  switch (req.bodyKind ?? "json") {
926
699
  case "json":
927
700
  return { body: JSON.stringify(req.body), contentType: "application/json" };
928
- case "form": {
929
- const params = new URLSearchParams();
930
- for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
931
- appendDeep(params, k, v);
932
- }
933
- // URLSearchParams sets its own content type with the charset suffix.
934
- return { body: params };
935
- }
936
- case "multipart": {
937
- const form = new FormData();
938
- for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
939
- if (v === undefined || v === null) continue;
940
- form.append(
941
- k,
942
- v instanceof Blob ? v : typeof v === "object" ? JSON.stringify(v) : String(v),
943
- );
944
- }
945
- // Let fetch set the boundary header.
946
- return { body: form };
947
- }
948
- case "text":
949
- return { body: String(req.body), contentType: "text/plain" };
950
- case "binary":
951
- return { body: req.body as NonNullable<RequestInit["body"]> };
952
701
  }
702
+ throw new Error("Unsupported request body kind: " + String(req.bodyKind));
953
703
  }
954
704
 
955
705
  async function parseBody(response: Response, method: string): Promise<unknown> {
@@ -968,17 +718,49 @@ async function parseBody(response: Response, method: string): Promise<unknown> {
968
718
  }
969
719
  }
970
720
 
971
- function meta(response: Response): ResponseMeta {
721
+ function requestIdFromBody(body: unknown): string | undefined {
722
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
723
+ const value = (body as Record<string, unknown>).request_id ?? (body as Record<string, unknown>).requestId;
724
+ return typeof value === "string" && value.length > 0 ? value : undefined;
725
+ }
726
+
727
+ function meta(response: Response, body?: unknown): ResponseMeta {
972
728
  return {
973
729
  status: response.status,
974
730
  headers: response.headers,
731
+ ...(body !== undefined ? { rawBody: body } : {}),
975
732
  requestId:
976
- response.headers.get("x-request-id") ??
733
+ requestIdFromBody(body) ??
977
734
  response.headers.get("request-id") ??
735
+ response.headers.get("x-request-id") ??
978
736
  undefined,
979
737
  };
980
738
  }
981
739
 
740
+ /** AbortSignal.any reached Node in 18.17. Keep the package's Node 18 floor
741
+ * honest for earlier 18.x releases and for web runtimes without it. */
742
+ function composeSignals(signals: AbortSignal[]): AbortSignal {
743
+ const nativeAny = (AbortSignal as typeof AbortSignal & { any?: (items: AbortSignal[]) => AbortSignal }).any;
744
+ if (nativeAny) return nativeAny.call(AbortSignal, signals);
745
+ const controller = new AbortController();
746
+ const listeners = new Map<AbortSignal, () => void>();
747
+ const abortFrom = (signal: AbortSignal) => {
748
+ for (const [item, listener] of listeners) item.removeEventListener("abort", listener);
749
+ listeners.clear();
750
+ controller.abort(signal.reason);
751
+ };
752
+ for (const signal of signals) {
753
+ if (signal.aborted) {
754
+ abortFrom(signal);
755
+ break;
756
+ }
757
+ const listener = () => abortFrom(signal);
758
+ listeners.set(signal, listener);
759
+ signal.addEventListener("abort", listener, { once: true });
760
+ }
761
+ return controller.signal;
762
+ }
763
+
982
764
  function retryAfterMs(response: Response): number | undefined {
983
765
  const header = response.headers.get("retry-after");
984
766
  if (!header) return undefined;
@@ -74,26 +74,11 @@ export class Page<Item, E = unknown> {
74
74
  case "cursor": {
75
75
  const next = getPath(this.body, config.nextCursorField!);
76
76
  if (next === undefined || next === null || next === "") return null;
77
+ if (next === params[config.cursorParam!]) return null;
77
78
  return { ...params, [config.cursorParam!]: next };
78
79
  }
79
- case "cursorFromLastId": {
80
- if (items.length === 0) return null;
81
- const last = items[items.length - 1] as Record<string, unknown>;
82
- const id = last?.[config.idField ?? "id"];
83
- if (id === undefined || id === null) return null;
84
- return { ...params, [config.cursorParam!]: id };
85
- }
86
- case "page": {
87
- if (!this.looksLikeMore(items)) return null;
88
- const current = Number(params[config.pageParam!] ?? 1);
89
- return { ...params, [config.pageParam!]: current + 1 };
90
- }
91
- case "offset": {
92
- if (!this.looksLikeMore(items)) return null;
93
- const current = Number(params[config.offsetParam!] ?? 0);
94
- return { ...params, [config.offsetParam!]: current + items.length };
95
- }
96
80
  }
81
+ return null;
97
82
  }
98
83
 
99
84
  /** Fetch the next page, or null when this is the last one. Throws the typed error on failure. */
@@ -159,26 +144,17 @@ export function paginate<Item, E>(
159
144
  req: CoreRequest,
160
145
  config: PageConfig,
161
146
  ): PagePromise<Item, E> {
162
- const isGraphql = req.graphqlField !== undefined;
147
+ const isGraphql = false
148
+ ;
163
149
  const fetchPage: FetchPage<Item, E> = async (params) => {
164
- const nextReq = isGraphql
165
- ? {
166
- ...req,
167
- body: {
168
- ...(req.body as Record<string, unknown>),
169
- variables: { ...((req.body as { variables?: Record<string, unknown> })?.variables ?? {}), ...params },
170
- },
171
- }
172
- : { ...req, query: params };
150
+ let nextReq: CoreRequest = { ...req, query: params };
173
151
  const result = await core.request<unknown, E>(nextReq);
174
152
  if (!result.ok) return result;
175
153
  const page = new Page<Item, E>(fetchPage, config, params, result.data, result.response);
176
154
  return { ok: true, data: page, response: result.response };
177
155
  };
178
156
  const initial: Record<string, unknown> = {};
179
- const seed = isGraphql
180
- ? ((req.body as { variables?: Record<string, unknown> })?.variables ?? {})
181
- : (req.query ?? {});
157
+ let seed = req.query ?? {};
182
158
  for (const [k, v] of Object.entries(seed)) {
183
159
  if (v !== undefined) initial[k] = v;
184
160
  }
package/src/dates.ts CHANGED
@@ -29,7 +29,6 @@ const CALENDAR_UNITS = new Set(["mo", "month", "y", "yr", "year"]);
29
29
  const HINT = "Use a signed form: -7d or -P7D for seven days ago, +7d or +P7D for seven days ahead; or an absolute ISO 8601 value.";
30
30
 
31
31
  /** The forms a date-shaped argument accepts, for descriptions and help. */
32
- export const RELATIVE_DATE_HINT = "Accepts ISO 8601 or a relative form: -P7D, -7d, 7 days ago, today, now.";
33
32
 
34
33
  function shift(base: Date, amount: number, unit: string): Date {
35
34
  const d = new Date(base.getTime());
package/src/docs.ts ADDED
@@ -0,0 +1,101 @@
1
+ /** Shared documentation-fetch boundary for generated CLI and MCP surfaces. */
2
+
3
+ export const MAX_DOCS_TEXT_BYTES = 2_000_000;
4
+
5
+ export function resolveDocsPageUrl(base: string | null, pathOrFile: string): string | null {
6
+ if (base === null) return null;
7
+ try {
8
+ const baseUrl = new URL(base);
9
+ if ((baseUrl.protocol !== "https:" && baseUrl.protocol !== "http:") || baseUrl.username || baseUrl.password) return null;
10
+ const target = /^https?:\/\//.test(pathOrFile)
11
+ ? new URL(pathOrFile)
12
+ : new URL(pathOrFile.replace(/^\/+/, ""), baseUrl.toString().replace(/\/+$/, "") + "/");
13
+ return target.origin === baseUrl.origin && !target.username && !target.password ? target.toString() : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /** Resolve conventional docs files while honoring an explicitly configured
20
+ * llms.txt location. Guide links may live on either the docs-site origin or
21
+ * the index origin; no other origin is accepted. */
22
+ export function resolveDocsContentUrl(
23
+ base: string | null,
24
+ indexUrl: string | null,
25
+ pathOrFile: string,
26
+ ): string | null {
27
+ const exactIndex = safeHttpUrl(indexUrl);
28
+ if (pathOrFile === "llms.txt" && exactIndex) return exactIndex;
29
+ if (pathOrFile === "llms-full.txt" && exactIndex) {
30
+ try { return new URL("llms-full.txt", exactIndex).toString(); } catch { return null; }
31
+ }
32
+ const primary = resolveDocsPageUrl(base, pathOrFile);
33
+ if (primary) return primary;
34
+ if (!exactIndex) return null;
35
+ try {
36
+ const target = /^https?:\/\//.test(pathOrFile)
37
+ ? new URL(pathOrFile)
38
+ : new URL(pathOrFile.replace(/^\/+/, ""), exactIndex);
39
+ const allowed = new URL(exactIndex);
40
+ return target.origin === allowed.origin && !target.username && !target.password ? target.toString() : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function safeHttpUrl(value: string | null): string | null {
47
+ if (!value) return null;
48
+ try {
49
+ const parsed = new URL(value);
50
+ return (parsed.protocol === "https:" || parsed.protocol === "http:") && !parsed.username && !parsed.password
51
+ ? parsed.toString()
52
+ : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Markdown-preferred fetch with same-origin redirects, one deadline, and a
59
+ * streaming byte cap. Returns null for every invalid or failed read. */
60
+ export async function fetchDocsText(url: string): Promise<string | null> {
61
+ try {
62
+ const initial = new URL(url);
63
+ if ((initial.protocol !== "https:" && initial.protocol !== "http:") || initial.username || initial.password) return null;
64
+ const allowedOrigin = initial.origin;
65
+ let current = initial.toString();
66
+ const signal = AbortSignal.timeout(10_000);
67
+ for (let redirects = 0; redirects <= 3; redirects += 1) {
68
+ const response = await fetch(current, { headers: { Accept: "text/markdown, text/plain, */*" }, redirect: "manual", signal });
69
+ if (response.status >= 300 && response.status < 400) {
70
+ const location = response.headers.get("location");
71
+ if (!location || redirects === 3) return null;
72
+ const next = new URL(location, current);
73
+ if (next.origin !== allowedOrigin || next.username || next.password) return null;
74
+ current = next.toString();
75
+ continue;
76
+ }
77
+ if (!response.ok) return null;
78
+ const declared = Number(response.headers.get("content-length"));
79
+ if (Number.isFinite(declared) && declared > MAX_DOCS_TEXT_BYTES) return null;
80
+ if (!response.body) return "";
81
+ const reader = response.body.getReader();
82
+ const decoder = new TextDecoder();
83
+ let bytes = 0;
84
+ let text = "";
85
+ for (;;) {
86
+ const chunk = await reader.read();
87
+ if (chunk.done) break;
88
+ bytes += chunk.value.byteLength;
89
+ if (bytes > MAX_DOCS_TEXT_BYTES) {
90
+ await reader.cancel();
91
+ return null;
92
+ }
93
+ text += decoder.decode(chunk.value, { stream: true });
94
+ }
95
+ return text + decoder.decode();
96
+ }
97
+ return null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }