@omercnet/paseo-gas-city 0.1.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.
@@ -0,0 +1,668 @@
1
+ import { lookup as dnsLookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ import { z } from "zod";
4
+ import { GAS_CITY_LIMITS } from "../shared/limits";
5
+
6
+ const DEFAULT_TIMEOUT_MS = 5_000;
7
+ const DEFAULT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
8
+ const MAX_REDIRECTS = 3;
9
+
10
+ const nonnegativeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
11
+ const optionalString = z.string().optional();
12
+ const nullableItems = <T extends z.ZodType>(item: T, maximum: number) =>
13
+ z.object({
14
+ items: z.array(item).max(maximum).nullable(),
15
+ total: nonnegativeInteger,
16
+ next_cursor: optionalString,
17
+ partial: z.boolean().optional(),
18
+ partial_errors: z.array(z.string()).max(GAS_CITY_LIMITS.diagnostics).nullable().optional(),
19
+ });
20
+
21
+ export const UpstreamHealthSchema = z
22
+ .object({
23
+ status: z.string(),
24
+ version: z.string(),
25
+ build_id: optionalString,
26
+ uptime_sec: nonnegativeInteger,
27
+ cities_total: nonnegativeInteger,
28
+ cities_running: nonnegativeInteger,
29
+ packs_lock_sha256: optionalString,
30
+ startup: z
31
+ .object({
32
+ ready: z.boolean(),
33
+ phase: optionalString,
34
+ phases_completed: z
35
+ .array(z.string())
36
+ .max(GAS_CITY_LIMITS.diagnostics)
37
+ .nullable()
38
+ .optional(),
39
+ })
40
+ .strict()
41
+ .optional(),
42
+ })
43
+ .strict();
44
+
45
+ export const UpstreamCitySchema = z
46
+ .object({
47
+ name: z.string(),
48
+ path: z.string(),
49
+ running: z.boolean(),
50
+ status: optionalString,
51
+ error: optionalString,
52
+ phases_completed: z.array(z.string()).max(GAS_CITY_LIMITS.diagnostics).nullable().optional(),
53
+ })
54
+ .strict();
55
+
56
+ export const UpstreamCitiesSchema = z
57
+ .object({
58
+ items: z.array(UpstreamCitySchema).max(GAS_CITY_LIMITS.cities).nullable(),
59
+ total: nonnegativeInteger,
60
+ })
61
+ .strict();
62
+
63
+ const UpstreamRigGitSchema = z
64
+ .object({
65
+ branch: z.string(),
66
+ clean: z.boolean(),
67
+ changed_files: nonnegativeInteger,
68
+ ahead: nonnegativeInteger,
69
+ behind: nonnegativeInteger,
70
+ })
71
+ .strict();
72
+
73
+ export const UpstreamRigSchema = z
74
+ .object({
75
+ name: z.string(),
76
+ path: z.string(),
77
+ suspended: z.boolean(),
78
+ prefix: optionalString,
79
+ default_branch: optionalString,
80
+ agent_count: nonnegativeInteger,
81
+ running_count: nonnegativeInteger,
82
+ last_activity: optionalString,
83
+ git: UpstreamRigGitSchema.optional(),
84
+ })
85
+ .strict();
86
+
87
+ export const UpstreamRigsSchema = nullableItems(UpstreamRigSchema, GAS_CITY_LIMITS.rigs).strict();
88
+
89
+ const countGroup = (keys: readonly string[]) =>
90
+ z.object(Object.fromEntries(keys.map((key) => [key, nonnegativeInteger]))).strict();
91
+
92
+ export const UpstreamStatusSchema = z
93
+ .object({
94
+ name: z.string(),
95
+ path: z.string(),
96
+ version: optionalString,
97
+ uptime_sec: nonnegativeInteger,
98
+ suspended: z.boolean(),
99
+ agent_count: nonnegativeInteger,
100
+ rig_count: nonnegativeInteger,
101
+ running: nonnegativeInteger,
102
+ agents: countGroup(["total", "running", "suspended", "quarantined"]),
103
+ rigs: countGroup(["total", "suspended"]),
104
+ work: countGroup(["in_progress", "ready", "open"]),
105
+ mail: z.object({}).passthrough(),
106
+ session_counts_detail: countGroup(["active", "suspended"]).optional(),
107
+ partial: z.boolean().optional(),
108
+ partial_errors: z.array(z.string()).max(GAS_CITY_LIMITS.diagnostics).nullable().optional(),
109
+ agent_details: z.array(z.unknown()).nullable().optional(),
110
+ named_session_details: z.array(z.unknown()).nullable().optional(),
111
+ rig_details: z.array(z.unknown()).nullable().optional(),
112
+ beads: z.unknown().optional(),
113
+ beads_version: optionalString,
114
+ dolt_version: optionalString,
115
+ conditional_writes: z.unknown().optional(),
116
+ store_health: z.unknown().optional(),
117
+ })
118
+ .strict();
119
+
120
+ export const UpstreamSessionSchema = z
121
+ .object({
122
+ id: z.string(),
123
+ kind: optionalString,
124
+ template: z.string(),
125
+ state: z.string(),
126
+ reason: optionalString,
127
+ title: z.string(),
128
+ alias: optionalString,
129
+ provider: z.string(),
130
+ display_name: optionalString,
131
+ session_name: z.string(),
132
+ work_dir: optionalString,
133
+ created_at: z.string(),
134
+ last_active: optionalString,
135
+ last_nudge_delivered_at: optionalString,
136
+ attached: z.boolean(),
137
+ rig: optionalString,
138
+ pool: optionalString,
139
+ agent_kind: optionalString,
140
+ running: z.boolean(),
141
+ active_bead: optionalString,
142
+ last_output: optionalString,
143
+ model: optionalString,
144
+ context_pct: z.number().int().optional(),
145
+ context_window: z.number().int().optional(),
146
+ activity: optionalString,
147
+ submission_capabilities: z
148
+ .object({
149
+ message: z.boolean().optional(),
150
+ submit: z.boolean().optional(),
151
+ respond: z.boolean().optional(),
152
+ })
153
+ .passthrough()
154
+ .optional(),
155
+ configured_named_session: z.boolean().optional(),
156
+ options: z.record(z.string(), z.string()).optional(),
157
+ metadata: z.record(z.string(), z.string()).optional(),
158
+ })
159
+ .strict();
160
+
161
+ export const UpstreamSessionsSchema = nullableItems(
162
+ UpstreamSessionSchema,
163
+ GAS_CITY_LIMITS.sessions,
164
+ ).strict();
165
+
166
+ export const UpstreamConvoySchema = z
167
+ .object({
168
+ id: z.string(),
169
+ title: z.string(),
170
+ status: z.string(),
171
+ issue_type: z.string(),
172
+ created_at: z.string(),
173
+ updated_at: optionalString,
174
+ priority: z.number().int().min(0).max(4).optional(),
175
+ assignee: optionalString,
176
+ is_blocked: z.boolean().optional(),
177
+ metadata: z.record(z.string(), z.string()).optional(),
178
+ })
179
+ .passthrough();
180
+
181
+ export const UpstreamConvoysSchema = nullableItems(
182
+ UpstreamConvoySchema,
183
+ GAS_CITY_LIMITS.convoys,
184
+ ).strict();
185
+
186
+ export const UpstreamEventSchema = z
187
+ .object({
188
+ seq: nonnegativeInteger,
189
+ type: z.string(),
190
+ ts: z.string(),
191
+ actor: z.string(),
192
+ subject: optionalString,
193
+ message: optionalString,
194
+ city: optionalString,
195
+ session_id: optionalString,
196
+ run_id: optionalString,
197
+ step_id: optionalString,
198
+ payload: z.unknown().optional(),
199
+ workflow: z.unknown().optional(),
200
+ })
201
+ .strict();
202
+
203
+ export const UpstreamEventsSchema = nullableItems(z.unknown(), GAS_CITY_LIMITS.events).strict();
204
+
205
+ export const UpstreamSupervisorEventsSchema = z
206
+ .object({
207
+ event_cursor: z.string(),
208
+ items: z.array(z.unknown()).max(GAS_CITY_LIMITS.events).nullable(),
209
+ total: nonnegativeInteger,
210
+ })
211
+ .strict();
212
+
213
+ export const UpstreamWorkItemSchema = z
214
+ .object({
215
+ id: z.string(),
216
+ title: z.string(),
217
+ status: z.string(),
218
+ issue_type: z.string(),
219
+ priority: z.number().int().min(0).max(4).optional(),
220
+ created_at: z.string(),
221
+ updated_at: optionalString,
222
+ assignee: optionalString,
223
+ is_blocked: z.boolean().optional(),
224
+ })
225
+ .passthrough();
226
+
227
+ export const UpstreamWorkItemsSchema = nullableItems(
228
+ UpstreamWorkItemSchema,
229
+ GAS_CITY_LIMITS.workItems,
230
+ ).strict();
231
+
232
+ export const UpstreamPendingSchema = nullableItems(
233
+ z
234
+ .object({
235
+ session_id: z.string(),
236
+ request_id: z.string(),
237
+ kind: z.string(),
238
+ })
239
+ .strict(),
240
+ GAS_CITY_LIMITS.attentionItems,
241
+ ).strict();
242
+
243
+ export const UpstreamSlingResultSchema = z
244
+ .object({
245
+ status: z.string(),
246
+ target: z.string(),
247
+ formula: optionalString,
248
+ bead: optionalString,
249
+ workflow_id: optionalString,
250
+ root_bead_id: optionalString,
251
+ attached_bead_id: optionalString,
252
+ mode: optionalString,
253
+ warnings: z.array(z.string()).max(GAS_CITY_LIMITS.warnings).nullable().optional(),
254
+ dashboard_url: optionalString,
255
+ run: z.unknown().optional(),
256
+ })
257
+ .strict();
258
+
259
+ export const UpstreamSessionActionResultSchema = z
260
+ .object({
261
+ status: z.string(),
262
+ id: optionalString,
263
+ request_id: optionalString,
264
+ event_cursor: z.union([z.string(), z.number().int().nonnegative()]).optional(),
265
+ })
266
+ .strict();
267
+
268
+ export type UpstreamHealth = z.infer<typeof UpstreamHealthSchema>;
269
+ export type UpstreamCity = z.infer<typeof UpstreamCitySchema>;
270
+ export type UpstreamRig = z.infer<typeof UpstreamRigSchema>;
271
+ export type UpstreamStatus = z.infer<typeof UpstreamStatusSchema>;
272
+ export type UpstreamSession = z.infer<typeof UpstreamSessionSchema>;
273
+ export type UpstreamConvoy = z.infer<typeof UpstreamConvoySchema>;
274
+ export type UpstreamEvent = z.infer<typeof UpstreamEventSchema>;
275
+ export type UpstreamWorkItem = z.infer<typeof UpstreamWorkItemSchema>;
276
+
277
+ export type GasCityClientErrorCode =
278
+ | "invalid-endpoint"
279
+ | "endpoint-not-allowed"
280
+ | "timeout"
281
+ | "unreachable"
282
+ | "upstream-error"
283
+ | "invalid-response"
284
+ | "canceled"
285
+ | "response-too-large";
286
+
287
+ export class GasCityClientError extends Error {
288
+ readonly code: GasCityClientErrorCode;
289
+ readonly status: number | null;
290
+ readonly correlationId: string | null;
291
+
292
+ constructor(
293
+ code: GasCityClientErrorCode,
294
+ message: string,
295
+ options: { status?: number; correlationId?: string | null; cause?: unknown } = {},
296
+ ) {
297
+ super(message, { cause: options.cause });
298
+ this.name = "GasCityClientError";
299
+ this.code = code;
300
+ this.status = options.status ?? null;
301
+ this.correlationId = options.correlationId ?? null;
302
+ }
303
+ }
304
+
305
+ type Lookup = (
306
+ hostname: string,
307
+ options: { all: true; verbatim: true },
308
+ ) => Promise<Array<{ address: string; family: number }>>;
309
+ type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
310
+
311
+ export interface GasCityClientOptions {
312
+ endpointUrl: string;
313
+ allowRemoteEndpoint: boolean;
314
+ timeoutMs?: number;
315
+ maxResponseBytes?: number;
316
+ fetch?: Fetch;
317
+ lookup?: Lookup;
318
+ }
319
+
320
+ interface RequestOptions<T extends z.ZodType> {
321
+ schema: T;
322
+ method?: "GET" | "POST";
323
+ body?: unknown;
324
+ mutation?: boolean;
325
+ signal?: AbortSignal;
326
+ }
327
+
328
+ function isLoopbackAddress(address: string): boolean {
329
+ const normalized = address.toLowerCase().replace(/^\[|\]$/g, "");
330
+ return (
331
+ normalized === "::1" ||
332
+ normalized.startsWith("::ffff:127.") ||
333
+ /^127(?:\.\d{1,3}){3}$/.test(normalized)
334
+ );
335
+ }
336
+
337
+ function parseEndpoint(value: string): URL {
338
+ let endpoint: URL;
339
+ try {
340
+ endpoint = new URL(value);
341
+ } catch (cause) {
342
+ throw new GasCityClientError("invalid-endpoint", "Gas City endpoint is invalid.", { cause });
343
+ }
344
+ if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:") {
345
+ throw new GasCityClientError("invalid-endpoint", "Gas City endpoint must use HTTP or HTTPS.");
346
+ }
347
+ if (endpoint.username || endpoint.password) {
348
+ throw new GasCityClientError(
349
+ "invalid-endpoint",
350
+ "Gas City endpoint cannot contain credentials.",
351
+ );
352
+ }
353
+ if (endpoint.hash) {
354
+ throw new GasCityClientError(
355
+ "invalid-endpoint",
356
+ "Gas City endpoint cannot contain a fragment.",
357
+ );
358
+ }
359
+ if (endpoint.search) {
360
+ throw new GasCityClientError("invalid-endpoint", "Gas City endpoint cannot contain a query.");
361
+ }
362
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, "")}/`;
363
+ return endpoint;
364
+ }
365
+
366
+ async function assertEndpointAllowed(
367
+ url: URL,
368
+ allowRemoteEndpoint: boolean,
369
+ lookup: Lookup,
370
+ ): Promise<void> {
371
+ if (allowRemoteEndpoint) return;
372
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
373
+ if (isIP(hostname)) {
374
+ if (isLoopbackAddress(hostname)) return;
375
+ throw new GasCityClientError(
376
+ "endpoint-not-allowed",
377
+ "Gas City endpoint must be loopback unless remote endpoints are enabled.",
378
+ );
379
+ }
380
+ if (hostname !== "localhost") {
381
+ throw new GasCityClientError(
382
+ "endpoint-not-allowed",
383
+ "Gas City endpoint must use an explicit loopback host.",
384
+ );
385
+ }
386
+ let addresses: Array<{ address: string; family: number }>;
387
+ try {
388
+ addresses = await lookup(hostname, { all: true, verbatim: true });
389
+ } catch (cause) {
390
+ throw new GasCityClientError("unreachable", "Gas City supervisor is unreachable.", { cause });
391
+ }
392
+ if (
393
+ !Array.isArray(addresses) ||
394
+ addresses.length === 0 ||
395
+ addresses.some(({ address }) => !isLoopbackAddress(address))
396
+ ) {
397
+ throw new GasCityClientError(
398
+ "endpoint-not-allowed",
399
+ "Gas City endpoint did not resolve exclusively to loopback addresses.",
400
+ );
401
+ }
402
+ }
403
+
404
+ async function readBoundedBody(response: Response, maximum: number): Promise<string> {
405
+ const contentLength = response.headers.get("content-length");
406
+ if (contentLength && Number(contentLength) > maximum) {
407
+ throw new GasCityClientError(
408
+ "response-too-large",
409
+ "Gas City response exceeded the size limit.",
410
+ {
411
+ status: response.status,
412
+ correlationId: response.headers.get("x-gc-request-id"),
413
+ },
414
+ );
415
+ }
416
+ if (!response.body) return "";
417
+ const reader = response.body.getReader();
418
+ const decoder = new TextDecoder();
419
+ let size = 0;
420
+ let text = "";
421
+ while (true) {
422
+ const { done, value } = await reader.read();
423
+ if (done) break;
424
+ size += value.byteLength;
425
+ if (size > maximum) {
426
+ try {
427
+ await reader.cancel();
428
+ } catch {
429
+ // Preserve the bounded-response error if a custom stream rejects cancellation.
430
+ }
431
+ throw new GasCityClientError(
432
+ "response-too-large",
433
+ "Gas City response exceeded the size limit.",
434
+ {
435
+ status: response.status,
436
+ correlationId: response.headers.get("x-gc-request-id"),
437
+ },
438
+ );
439
+ }
440
+ text += decoder.decode(value, { stream: true });
441
+ }
442
+ return text + decoder.decode();
443
+ }
444
+
445
+ export class GasCityClient {
446
+ readonly endpoint: URL;
447
+ private readonly allowRemoteEndpoint: boolean;
448
+ private readonly timeoutMs: number;
449
+ private readonly maxResponseBytes: number;
450
+ private readonly fetchImpl: Fetch;
451
+ private readonly lookup: Lookup;
452
+
453
+ constructor(options: GasCityClientOptions) {
454
+ this.endpoint = parseEndpoint(options.endpointUrl);
455
+ this.allowRemoteEndpoint = options.allowRemoteEndpoint;
456
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
457
+ this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
458
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
459
+ this.lookup = options.lookup ?? (dnsLookup as Lookup);
460
+ }
461
+
462
+ private async request<T extends z.ZodType>(
463
+ path: string,
464
+ options: RequestOptions<T>,
465
+ ): Promise<z.output<T>> {
466
+ let url = new URL(path.replace(/^\//, ""), this.endpoint);
467
+ let redirects = 0;
468
+ let timedOut = false;
469
+ const controller = new AbortController();
470
+ const timeout = setTimeout(() => {
471
+ timedOut = true;
472
+ controller.abort();
473
+ }, this.timeoutMs);
474
+ const abort = () => controller.abort(options.signal?.reason);
475
+ options.signal?.addEventListener("abort", abort, { once: true });
476
+ try {
477
+ while (true) {
478
+ await assertEndpointAllowed(url, this.allowRemoteEndpoint, this.lookup);
479
+ let response: Response;
480
+ try {
481
+ response = await this.fetchImpl(url, {
482
+ method: options.method ?? "GET",
483
+ headers: {
484
+ accept: "application/json",
485
+ ...(options.body === undefined ? {} : { "content-type": "application/json" }),
486
+ ...(options.mutation ? { "X-GC-Request": crypto.randomUUID() } : {}),
487
+ },
488
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
489
+ redirect: "manual",
490
+ signal: controller.signal,
491
+ });
492
+ } catch (cause) {
493
+ if (controller.signal.aborted) {
494
+ const code = timedOut ? "timeout" : "canceled";
495
+ const message = timedOut
496
+ ? "Gas City request timed out."
497
+ : "Gas City request was canceled.";
498
+ throw new GasCityClientError(code, message, { cause });
499
+ }
500
+ throw new GasCityClientError("unreachable", "Gas City supervisor is unreachable.", {
501
+ cause,
502
+ });
503
+ }
504
+
505
+ if (response.status >= 300 && response.status < 400) {
506
+ const location = response.headers.get("location");
507
+ if (options.mutation || !location || redirects++ >= MAX_REDIRECTS) {
508
+ throw new GasCityClientError("upstream-error", "Gas City rejected the request.", {
509
+ status: response.status,
510
+ correlationId: response.headers.get("x-gc-request-id"),
511
+ });
512
+ }
513
+ url = new URL(location, url);
514
+ continue;
515
+ }
516
+
517
+ const correlationId = response.headers.get("x-gc-request-id");
518
+ const text = await readBoundedBody(response, this.maxResponseBytes);
519
+ if (!response.ok) {
520
+ console.error("[paseo-gas-city] Gas City request failed", {
521
+ correlationId,
522
+ status: response.status,
523
+ path: url.pathname,
524
+ });
525
+ throw new GasCityClientError("upstream-error", "Gas City rejected the request.", {
526
+ status: response.status,
527
+ correlationId,
528
+ });
529
+ }
530
+
531
+ let value: unknown;
532
+ try {
533
+ value = text === "" ? {} : JSON.parse(text);
534
+ } catch (cause) {
535
+ console.error("[paseo-gas-city] Gas City returned invalid JSON", {
536
+ correlationId,
537
+ path: url.pathname,
538
+ });
539
+ throw new GasCityClientError(
540
+ "invalid-response",
541
+ "Gas City returned an invalid response.",
542
+ {
543
+ correlationId,
544
+ cause,
545
+ },
546
+ );
547
+ }
548
+ const parsed = options.schema.safeParse(value);
549
+ if (!parsed.success) {
550
+ console.error("[paseo-gas-city] Gas City response validation failed", {
551
+ correlationId,
552
+ path: url.pathname,
553
+ issues: parsed.error.issues.slice(0, 8),
554
+ });
555
+ throw new GasCityClientError(
556
+ "invalid-response",
557
+ "Gas City returned an invalid response.",
558
+ {
559
+ correlationId,
560
+ cause: parsed.error,
561
+ },
562
+ );
563
+ }
564
+ return parsed.data;
565
+ }
566
+ } finally {
567
+ clearTimeout(timeout);
568
+ options.signal?.removeEventListener("abort", abort);
569
+ }
570
+ }
571
+
572
+ health(signal?: AbortSignal) {
573
+ return this.request("health", { schema: UpstreamHealthSchema, signal });
574
+ }
575
+
576
+ cities(signal?: AbortSignal) {
577
+ return this.request("v0/cities", { schema: UpstreamCitiesSchema, signal });
578
+ }
579
+
580
+ cityStatus(cityName: string, signal?: AbortSignal) {
581
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/status`, {
582
+ schema: UpstreamStatusSchema,
583
+ signal,
584
+ });
585
+ }
586
+
587
+ rigs(cityName: string, signal?: AbortSignal) {
588
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/rigs`, {
589
+ schema: UpstreamRigsSchema,
590
+ signal,
591
+ });
592
+ }
593
+
594
+ sessions(cityName: string, signal?: AbortSignal) {
595
+ return this.request(
596
+ `v0/city/${encodeURIComponent(cityName)}/sessions?limit=${GAS_CITY_LIMITS.sessions}`,
597
+ { schema: UpstreamSessionsSchema, signal },
598
+ );
599
+ }
600
+
601
+ convoys(cityName: string, signal?: AbortSignal) {
602
+ return this.request(
603
+ `v0/city/${encodeURIComponent(cityName)}/convoys?limit=${GAS_CITY_LIMITS.convoys}`,
604
+ { schema: UpstreamConvoysSchema, signal },
605
+ );
606
+ }
607
+
608
+ work(cityName: string, rigName: string | null, limit: number, signal?: AbortSignal) {
609
+ const query = new URLSearchParams({ limit: String(limit) });
610
+ if (rigName) query.set("rig", rigName);
611
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/beads?${query}`, {
612
+ schema: UpstreamWorkItemsSchema,
613
+ signal,
614
+ });
615
+ }
616
+
617
+ pending(cityName: string, signal?: AbortSignal) {
618
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/pending`, {
619
+ schema: UpstreamPendingSchema,
620
+ signal,
621
+ });
622
+ }
623
+
624
+ cityEvents(cityName: string, cursor: string | null, limit: number, signal?: AbortSignal) {
625
+ const query = new URLSearchParams({ limit: String(limit) });
626
+ if (cursor) query.set("cursor", cursor);
627
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/events?${query}`, {
628
+ schema: UpstreamEventsSchema,
629
+ signal,
630
+ });
631
+ }
632
+
633
+ supervisorEvents(limit: number, signal?: AbortSignal) {
634
+ return this.request(`v0/events?limit=${limit}`, {
635
+ schema: UpstreamSupervisorEventsSchema,
636
+ signal,
637
+ });
638
+ }
639
+
640
+ sling(cityName: string, body: unknown, signal?: AbortSignal) {
641
+ return this.request(`v0/city/${encodeURIComponent(cityName)}/sling`, {
642
+ schema: UpstreamSlingResultSchema,
643
+ method: "POST",
644
+ body,
645
+ mutation: true,
646
+ signal,
647
+ });
648
+ }
649
+
650
+ sessionAction(
651
+ cityName: string,
652
+ sessionId: string,
653
+ action: string,
654
+ body: unknown,
655
+ signal?: AbortSignal,
656
+ ) {
657
+ return this.request(
658
+ `v0/city/${encodeURIComponent(cityName)}/session/${encodeURIComponent(sessionId)}/${action}`,
659
+ {
660
+ schema: UpstreamSessionActionResultSchema,
661
+ method: "POST",
662
+ body,
663
+ mutation: true,
664
+ signal,
665
+ },
666
+ );
667
+ }
668
+ }