@opengeni/sdk 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.
package/dist/index.js ADDED
@@ -0,0 +1,955 @@
1
+ // src/errors.ts
2
+ var OpenGeniApiError = class extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body) {
6
+ super(`OpenGeni API ${status}: ${body || "(empty body)"}`);
7
+ this.name = "OpenGeniApiError";
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ };
12
+ var OpenGeniStreamError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "OpenGeniStreamError";
16
+ }
17
+ };
18
+ function isAbortError(error) {
19
+ return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
20
+ }
21
+ function isRetryableStreamError(error) {
22
+ if (error instanceof OpenGeniApiError) {
23
+ return error.status === 408 || error.status === 409 || error.status === 425 || error.status === 429 || error.status >= 500;
24
+ }
25
+ return error instanceof TypeError;
26
+ }
27
+
28
+ // src/sse.ts
29
+ async function* parseSseStream(stream) {
30
+ const reader = stream.getReader();
31
+ const decoder = new TextDecoder();
32
+ let buffer = "";
33
+ let id;
34
+ let event;
35
+ let dataLines = null;
36
+ const dispatch = () => {
37
+ const message = dataLines === null ? null : {
38
+ ...id !== void 0 ? { id } : {},
39
+ ...event !== void 0 ? { event } : {},
40
+ data: dataLines.join("\n")
41
+ };
42
+ id = void 0;
43
+ event = void 0;
44
+ dataLines = null;
45
+ return message;
46
+ };
47
+ const handleLine = (line) => {
48
+ if (line === "") {
49
+ return dispatch();
50
+ }
51
+ if (line.startsWith(":")) {
52
+ return null;
53
+ }
54
+ const colon = line.indexOf(":");
55
+ const field = colon === -1 ? line : line.slice(0, colon);
56
+ let value = colon === -1 ? "" : line.slice(colon + 1);
57
+ if (value.startsWith(" ")) {
58
+ value = value.slice(1);
59
+ }
60
+ if (field === "data") {
61
+ (dataLines ??= []).push(value);
62
+ } else if (field === "event") {
63
+ event = value;
64
+ } else if (field === "id") {
65
+ id = value;
66
+ }
67
+ return null;
68
+ };
69
+ try {
70
+ while (true) {
71
+ const { done, value } = await reader.read();
72
+ if (done) {
73
+ break;
74
+ }
75
+ buffer += decoder.decode(value, { stream: true });
76
+ let newline = buffer.indexOf("\n");
77
+ while (newline !== -1) {
78
+ let line = buffer.slice(0, newline);
79
+ buffer = buffer.slice(newline + 1);
80
+ if (line.endsWith("\r")) {
81
+ line = line.slice(0, -1);
82
+ }
83
+ const message = handleLine(line);
84
+ if (message) {
85
+ yield message;
86
+ }
87
+ newline = buffer.indexOf("\n");
88
+ }
89
+ }
90
+ } finally {
91
+ await reader.cancel().catch(() => {
92
+ });
93
+ reader.releaseLock();
94
+ }
95
+ }
96
+
97
+ // src/stream.ts
98
+ async function* streamSessionEvents(transport, options = {}) {
99
+ const signal = options.signal;
100
+ const reconnect = options.reconnect ?? true;
101
+ const baseDelayMs = options.reconnectDelayMs ?? 500;
102
+ const maxDelayMs = options.maxReconnectDelayMs ?? 1e4;
103
+ const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
104
+ let cursor = options.after ?? 0;
105
+ let failedAttempts = 0;
106
+ let delayMs = baseDelayMs;
107
+ let everConnected = false;
108
+ while (!signal?.aborted) {
109
+ options.onStateChange?.(everConnected || failedAttempts > 0 ? "reconnecting" : "connecting");
110
+ const cursorAtOpen = cursor;
111
+ try {
112
+ const body = await transport.openStream(cursor, signal);
113
+ everConnected = true;
114
+ failedAttempts = 0;
115
+ delayMs = baseDelayMs;
116
+ options.onStateChange?.("live");
117
+ for await (const message of parseSseStream(body)) {
118
+ if (signal?.aborted) {
119
+ return;
120
+ }
121
+ const event = parseSessionEvent(message.data);
122
+ if (!event || event.sequence <= cursor) {
123
+ continue;
124
+ }
125
+ if (event.sequence > cursor + 1) {
126
+ for await (const missed of backfillEvents(transport, cursor, event.sequence - 1)) {
127
+ cursor = missed.sequence;
128
+ yield missed;
129
+ if (signal?.aborted) {
130
+ return;
131
+ }
132
+ }
133
+ }
134
+ cursor = event.sequence;
135
+ yield event;
136
+ }
137
+ if (!reconnect) {
138
+ return;
139
+ }
140
+ if (cursor === cursorAtOpen) {
141
+ await sleep(baseDelayMs, signal);
142
+ }
143
+ continue;
144
+ } catch (error) {
145
+ if (signal?.aborted || isAbortError(error)) {
146
+ return;
147
+ }
148
+ if (!reconnect || !isRetryableStreamError(error)) {
149
+ throw error;
150
+ }
151
+ failedAttempts += 1;
152
+ if (failedAttempts > maxAttempts) {
153
+ throw new OpenGeniStreamError(
154
+ `event stream gave up after ${maxAttempts} consecutive failed reconnect attempts: ${error instanceof Error ? error.message : String(error)}`
155
+ );
156
+ }
157
+ }
158
+ await sleep(delayMs, signal);
159
+ delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
160
+ }
161
+ }
162
+ async function* backfillEvents(transport, fromExclusive, toInclusive) {
163
+ let cursor = fromExclusive;
164
+ while (cursor < toInclusive) {
165
+ const page = await transport.listEvents(cursor, Math.min(500, toInclusive - cursor));
166
+ const advancing = page.filter((event) => event.sequence > cursor && event.sequence <= toInclusive).sort((a, b) => a.sequence - b.sequence);
167
+ if (advancing.length === 0) {
168
+ throw new OpenGeniStreamError(
169
+ `event replay backfill stalled: expected sequences ${cursor + 1}..${toInclusive} but the replay endpoint returned none of them`
170
+ );
171
+ }
172
+ for (const event of advancing) {
173
+ if (event.sequence !== cursor + 1) {
174
+ throw new OpenGeniStreamError(
175
+ `event replay backfill is missing sequence ${cursor + 1} (replay endpoint skipped to ${event.sequence}); refusing to deliver with a gap`
176
+ );
177
+ }
178
+ cursor = event.sequence;
179
+ yield event;
180
+ }
181
+ }
182
+ }
183
+ function parseSessionEvent(data) {
184
+ let parsed;
185
+ try {
186
+ parsed = JSON.parse(data);
187
+ } catch {
188
+ return null;
189
+ }
190
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.sequence !== "number" || typeof parsed.type !== "string" || typeof parsed.id !== "string") {
191
+ return null;
192
+ }
193
+ return parsed;
194
+ }
195
+ async function sleep(delayMs, signal) {
196
+ if (signal?.aborted || delayMs <= 0) {
197
+ return;
198
+ }
199
+ await new Promise((resolve) => {
200
+ const timer = setTimeout(done, delayMs);
201
+ function done() {
202
+ clearTimeout(timer);
203
+ signal?.removeEventListener("abort", done);
204
+ resolve();
205
+ }
206
+ signal?.addEventListener("abort", done, { once: true });
207
+ });
208
+ }
209
+
210
+ // src/client.ts
211
+ var OpenGeniClient = class {
212
+ baseUrl;
213
+ options;
214
+ fetchImpl;
215
+ constructor(options) {
216
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
217
+ this.options = options;
218
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
219
+ }
220
+ // --- Session lifecycle ---------------------------------------------------
221
+ async createSession(workspaceId, request) {
222
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/sessions`, request);
223
+ }
224
+ async getSession(workspaceId, sessionId) {
225
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}`);
226
+ }
227
+ async listSessions(workspaceId, options = {}) {
228
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/sessions`, void 0, {
229
+ ...options.limit !== void 0 ? { limit: String(options.limit) } : {}
230
+ });
231
+ }
232
+ async listTurns(workspaceId, sessionId, options = {}) {
233
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns`, void 0, {
234
+ ...options.limit !== void 0 ? { limit: String(options.limit) } : {}
235
+ });
236
+ }
237
+ // --- Scheduled tasks -------------------------------------------------------
238
+ async listScheduledTasks(workspaceId, options = {}) {
239
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks`, void 0, {
240
+ ...options.limit !== void 0 ? { limit: String(options.limit) } : {}
241
+ });
242
+ }
243
+ async getScheduledTask(workspaceId, taskId) {
244
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
245
+ }
246
+ // --- Events: replay, send, stream ----------------------------------------
247
+ /** Replay durable events by sequence: events with `sequence > after`, ascending. */
248
+ async listEvents(workspaceId, sessionId, options = {}) {
249
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, void 0, {
250
+ ...options.after !== void 0 ? { after: String(options.after) } : {},
251
+ ...options.limit !== void 0 ? { limit: String(options.limit) } : {}
252
+ });
253
+ }
254
+ /** POST a user/control event to the session. Returns the accepted event. */
255
+ async sendEvent(workspaceId, sessionId, event) {
256
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, event);
257
+ }
258
+ async sendMessage(workspaceId, sessionId, message) {
259
+ const input = typeof message === "string" ? { text: message } : message;
260
+ const { clientEventId, ...payload } = input;
261
+ return await this.sendEvent(workspaceId, sessionId, {
262
+ type: "user.message",
263
+ ...clientEventId !== void 0 ? { clientEventId } : {},
264
+ payload
265
+ });
266
+ }
267
+ async interrupt(workspaceId, sessionId, options = {}) {
268
+ return await this.sendEvent(workspaceId, sessionId, {
269
+ type: "user.interrupt",
270
+ ...options.clientEventId !== void 0 ? { clientEventId: options.clientEventId } : {},
271
+ payload: options.reason !== void 0 ? { reason: options.reason } : {}
272
+ });
273
+ }
274
+ async sendApprovalDecision(workspaceId, sessionId, decision) {
275
+ const { clientEventId, ...payload } = decision;
276
+ return await this.sendEvent(workspaceId, sessionId, {
277
+ type: "user.approvalDecision",
278
+ ...clientEventId !== void 0 ? { clientEventId } : {},
279
+ payload
280
+ });
281
+ }
282
+ /**
283
+ * Live-stream a session's events with automatic reconnect, resume from the
284
+ * last seen sequence, gap backfill, and duplicate suppression. See
285
+ * {@link streamSessionEvents} for the delivery guarantees.
286
+ */
287
+ streamEvents(workspaceId, sessionId, options = {}) {
288
+ return streamSessionEvents(this.eventStreamTransport(workspaceId, sessionId), options);
289
+ }
290
+ /** The transport `streamEvents` runs on; useful for custom streaming layers. */
291
+ eventStreamTransport(workspaceId, sessionId) {
292
+ return {
293
+ openStream: async (after, signal) => await this.openEventStream(workspaceId, sessionId, { after, ...signal ? { signal } : {} }),
294
+ listEvents: async (after, limit) => await this.listEvents(workspaceId, sessionId, { after, limit })
295
+ };
296
+ }
297
+ /** Open one raw SSE connection (no reconnect). Most callers want `streamEvents`. */
298
+ async openEventStream(workspaceId, sessionId, options = {}) {
299
+ const url = this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events/stream`, {
300
+ after: String(options.after ?? 0)
301
+ });
302
+ const response = await this.fetchImpl(url, {
303
+ method: "GET",
304
+ headers: { ...this.headers(), Accept: "text/event-stream" },
305
+ ...options.signal ? { signal: options.signal } : {}
306
+ });
307
+ if (!response.ok) {
308
+ throw new OpenGeniApiError(response.status, await safeText(response));
309
+ }
310
+ if (!response.body) {
311
+ throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
312
+ }
313
+ return response.body;
314
+ }
315
+ // --- Turn queue ------------------------------------------------------------
316
+ /** Edit a still-queued turn (prompt, model, resources, tools, ...). */
317
+ async updateQueuedTurn(workspaceId, sessionId, turnId, update) {
318
+ return await this.requestJson(
319
+ "PATCH",
320
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
321
+ update
322
+ );
323
+ }
324
+ /**
325
+ * Reorder the queued turns. `turnIds` must all reference queued turns; the
326
+ * server assigns positions in the given order and returns the queue.
327
+ */
328
+ async reorderQueuedTurns(workspaceId, sessionId, turnIds) {
329
+ return await this.requestJson(
330
+ "POST",
331
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/reorder`,
332
+ { turnIds }
333
+ );
334
+ }
335
+ /** Cancel a queued turn before it is claimed. Returns the cancelled turn. */
336
+ async deleteQueuedTurn(workspaceId, sessionId, turnId) {
337
+ return await this.requestJson(
338
+ "DELETE",
339
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`
340
+ );
341
+ }
342
+ /**
343
+ * Steer: deliver a message *now* instead of behind the queue. Sends the
344
+ * message, promotes its queued turn to the front, and interrupts the
345
+ * running turn so the session picks the steer turn up next. On a session
346
+ * that is not running this degrades gracefully to a plain queued message.
347
+ *
348
+ * The steer turn is located by `triggerEventId` across ALL turns (retried
349
+ * briefly in case the server is still materializing it) — not just the
350
+ * queued ones, because the worker can claim the steer turn before it is
351
+ * ever observed queued, and a claimed steer turn means the message is
352
+ * already being delivered: interrupting then would cancel the very message
353
+ * being steered. If the turn cannot be found while other turns are queued,
354
+ * the interrupt is also skipped — stopping the running turn would otherwise
355
+ * promote someone else's queued work over this message — and the call
356
+ * degrades to a plain queued send (`interrupted: false`).
357
+ */
358
+ async steerMessage(workspaceId, sessionId, message) {
359
+ const accepted = await this.sendMessage(workspaceId, sessionId, message);
360
+ let steerTurn = null;
361
+ let queued = [];
362
+ for (let attempt = 0; attempt < 4; attempt += 1) {
363
+ if (attempt > 0) {
364
+ await delay(150 * attempt);
365
+ }
366
+ const turns = await this.listTurns(workspaceId, sessionId);
367
+ queued = turns.filter((turn) => turn.status === "queued").sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt));
368
+ steerTurn = turns.find((turn) => turn.triggerEventId === accepted.id) ?? null;
369
+ if (steerTurn) {
370
+ break;
371
+ }
372
+ }
373
+ const steerTurnQueued = steerTurn?.status === "queued";
374
+ if (steerTurn && steerTurnQueued && queued.length > 1) {
375
+ const front = steerTurn;
376
+ await this.reorderQueuedTurns(workspaceId, sessionId, [
377
+ front.id,
378
+ ...queued.filter((turn) => turn.id !== front.id).map((turn) => turn.id)
379
+ ]);
380
+ }
381
+ const canDeliverNext = steerTurnQueued || steerTurn === null && queued.length === 0;
382
+ const session = await this.getSession(workspaceId, sessionId);
383
+ const steerTurnAlreadyActive = steerTurn !== null && session.activeTurnId === steerTurn.id;
384
+ const interrupted = canDeliverNext && !steerTurnAlreadyActive && (session.status === "running" || session.status === "requires_action");
385
+ if (interrupted) {
386
+ await this.interrupt(workspaceId, sessionId, { reason: "steer" });
387
+ }
388
+ return { accepted, turn: steerTurn, interrupted };
389
+ }
390
+ // --- Goals -------------------------------------------------------------------
391
+ /** The session's goal. 404s when the session never had one. */
392
+ async getGoal(workspaceId, sessionId) {
393
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`);
394
+ }
395
+ async updateGoal(workspaceId, sessionId, request) {
396
+ return await this.requestJson("PATCH", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`, request);
397
+ }
398
+ /** Pause the goal loop: the session stops self-continuing until resumed. */
399
+ async pauseGoal(workspaceId, sessionId, options = {}) {
400
+ return await this.updateGoal(workspaceId, sessionId, {
401
+ status: "paused",
402
+ ...options.rationale !== void 0 ? { rationale: options.rationale } : {}
403
+ });
404
+ }
405
+ /** Resume a paused goal: resets counters and re-arms the continuation loop. */
406
+ async resumeGoal(workspaceId, sessionId) {
407
+ return await this.updateGoal(workspaceId, sessionId, { status: "active" });
408
+ }
409
+ // --- Operator context controls (/clear, /compact) ---------------------------
410
+ /**
411
+ * Clear the session's conversation context. Destructive and audit-preserving:
412
+ * the server supersedes (never deletes) the live history and emits a
413
+ * `session.context.cleared` event. Refused (409) while a turn is in flight or
414
+ * awaiting action. `confirm:true` is sent so an accidental call cannot wipe
415
+ * context — the destructive intent is explicit on the wire.
416
+ */
417
+ async clearSessionContext(workspaceId, sessionId) {
418
+ await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/clear`, { confirm: true });
419
+ }
420
+ /**
421
+ * Trigger conversation compaction now. On the client-managed (Azure) path this
422
+ * queues a forced compaction the worker honors before the next turn
423
+ * (`status:"queued"`); on a server-managed provider or when compaction is off
424
+ * it is a no-op (`status:"noop"`) with an explanatory message.
425
+ */
426
+ async compactSessionContext(workspaceId, sessionId) {
427
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/compact`, {});
428
+ }
429
+ // --- Access + workspaces -----------------------------------------------------
430
+ /** The caller's access context: subject, account + workspace grants, defaults. */
431
+ async getAccessContext() {
432
+ return await this.requestJson("GET", "/v1/access/me");
433
+ }
434
+ async listWorkspaces() {
435
+ return await this.requestJson("GET", "/v1/workspaces");
436
+ }
437
+ async createWorkspace(request) {
438
+ return await this.requestJson("POST", "/v1/workspaces", request);
439
+ }
440
+ async getWorkspace(workspaceId) {
441
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}`);
442
+ }
443
+ async updateWorkspace(workspaceId, request) {
444
+ return await this.requestJson("PATCH", `/v1/workspaces/${workspaceId}`, request);
445
+ }
446
+ // --- Scheduled tasks (write + runs) -------------------------------------------
447
+ async createScheduledTask(workspaceId, request) {
448
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks`, request);
449
+ }
450
+ async updateScheduledTask(workspaceId, taskId, request) {
451
+ return await this.requestJson("PATCH", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`, request);
452
+ }
453
+ async pauseScheduledTask(workspaceId, taskId) {
454
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/pause`);
455
+ }
456
+ async resumeScheduledTask(workspaceId, taskId) {
457
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/resume`);
458
+ }
459
+ /**
460
+ * Fire the task immediately (manual trigger), independent of its schedule.
461
+ * Pass a stable `triggerId` to make a retried trigger idempotent — the same
462
+ * token charges once and starts one run. Omit it and each call is distinct.
463
+ */
464
+ async triggerScheduledTask(workspaceId, taskId, options = {}) {
465
+ return await this.requestJson(
466
+ "POST",
467
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/trigger`,
468
+ options.triggerId ? { triggerId: options.triggerId } : void 0
469
+ );
470
+ }
471
+ async deleteScheduledTask(workspaceId, taskId) {
472
+ await this.requestJson("DELETE", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
473
+ }
474
+ async listScheduledTaskRuns(workspaceId, taskId, options = {}) {
475
+ return await this.requestJson(
476
+ "GET",
477
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/runs`,
478
+ void 0,
479
+ { ...options.limit !== void 0 ? { limit: String(options.limit) } : {} }
480
+ );
481
+ }
482
+ // --- Environments --------------------------------------------------------------
483
+ // Variable values are write-only: reads return name/version metadata only.
484
+ async listEnvironments(workspaceId) {
485
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/environments`);
486
+ }
487
+ async createEnvironment(workspaceId, request) {
488
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/environments`, request);
489
+ }
490
+ async getEnvironment(workspaceId, environmentId) {
491
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
492
+ }
493
+ async updateEnvironment(workspaceId, environmentId, request) {
494
+ return await this.requestJson(
495
+ "PATCH",
496
+ `/v1/workspaces/${workspaceId}/environments/${environmentId}`,
497
+ request
498
+ );
499
+ }
500
+ async deleteEnvironment(workspaceId, environmentId) {
501
+ await this.requestJson("DELETE", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
502
+ }
503
+ /** Create or rotate a variable. The value never comes back on any read. */
504
+ async setEnvironmentVariable(workspaceId, environmentId, name, value) {
505
+ return await this.requestJson(
506
+ "PUT",
507
+ `/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`,
508
+ { value }
509
+ );
510
+ }
511
+ async deleteEnvironmentVariable(workspaceId, environmentId, name) {
512
+ await this.requestJson(
513
+ "DELETE",
514
+ `/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`
515
+ );
516
+ }
517
+ // --- Files -----------------------------------------------------------------------
518
+ /** Step 1 of the upload flow: returns the pre-signed PUT target. */
519
+ async beginFileUpload(workspaceId, request) {
520
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/files/uploads`, request);
521
+ }
522
+ /** Step 3 of the upload flow: server verifies the object and marks it ready. */
523
+ async completeFileUpload(workspaceId, uploadId) {
524
+ const response = await this.requestJson(
525
+ "POST",
526
+ `/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`
527
+ );
528
+ return response.file;
529
+ }
530
+ /**
531
+ * The whole upload flow as one call: begin -> PUT the bytes to the signed
532
+ * URL (with its required headers; no API auth is sent to object storage)
533
+ * -> complete. Returns the ready `FileAsset`.
534
+ */
535
+ async uploadFile(workspaceId, input) {
536
+ const body = input.data instanceof Uint8Array ? new Blob([input.data.slice()]) : input.data;
537
+ const sizeBytes = typeof body === "string" ? new TextEncoder().encode(body).byteLength : body instanceof Blob ? body.size : body.byteLength;
538
+ const upload = await this.beginFileUpload(workspaceId, {
539
+ filename: input.filename,
540
+ contentType: input.contentType,
541
+ sizeBytes,
542
+ ...input.sha256 !== void 0 ? { sha256: input.sha256 } : {}
543
+ });
544
+ const putResponse = await this.fetchImpl(upload.putUrl, {
545
+ method: "PUT",
546
+ // The backend's requiredHeaders already carry the canonical lowercase
547
+ // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
548
+ // a `Content-Type` key here: WHATWG Headers treats the two casings as the
549
+ // same header and comma-joins their values (e.g. "text/plain, text/plain"),
550
+ // which the object store persists verbatim and COMPLETE then rejects (422),
551
+ // and which breaks S3's presigned-URL signature.
552
+ headers: { ...upload.requiredHeaders },
553
+ body
554
+ });
555
+ if (!putResponse.ok) {
556
+ throw new OpenGeniApiError(putResponse.status, await safeText(putResponse));
557
+ }
558
+ return await this.completeFileUpload(workspaceId, upload.uploadId);
559
+ }
560
+ async getFile(workspaceId, fileId) {
561
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/files/${fileId}`);
562
+ }
563
+ /** Mint a short-lived signed download URL for a ready file. */
564
+ async createFileDownloadUrl(workspaceId, fileId) {
565
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/files/${fileId}/download-url`);
566
+ }
567
+ // --- Documents ----------------------------------------------------------------------
568
+ async createDocumentBase(workspaceId, request) {
569
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/document-bases`, request);
570
+ }
571
+ async listDocumentBases(workspaceId) {
572
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/document-bases`);
573
+ }
574
+ async getDocumentBase(workspaceId, baseId) {
575
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}`);
576
+ }
577
+ /** Index an uploaded file into the base. The file must be `ready`. */
578
+ async addDocument(workspaceId, baseId, request) {
579
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`, request);
580
+ }
581
+ async listDocuments(workspaceId, baseId) {
582
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`);
583
+ }
584
+ /** Retry indexing for a failed document. */
585
+ async reindexDocument(workspaceId, baseId, documentId) {
586
+ return await this.requestJson(
587
+ "POST",
588
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents/${documentId}/reindex`
589
+ );
590
+ }
591
+ async searchDocuments(workspaceId, baseId, request) {
592
+ return await this.requestJson(
593
+ "POST",
594
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}/search`,
595
+ request
596
+ );
597
+ }
598
+ // --- Capability packs ------------------------------------------------------------------
599
+ /** Built-in + registered packs, with the workspace's installations. */
600
+ async listPacks(workspaceId) {
601
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/packs`);
602
+ }
603
+ /** Register (or replace) a workspace-scoped pack from a manifest. */
604
+ async registerPack(workspaceId, manifest) {
605
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/packs`, manifest);
606
+ }
607
+ async getPack(workspaceId, packId) {
608
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
609
+ }
610
+ async enablePack(workspaceId, packId, request = {}) {
611
+ return await this.requestJson(
612
+ "POST",
613
+ `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}/enable`,
614
+ request
615
+ );
616
+ }
617
+ /** Unregister a workspace-scoped pack (built-in packs cannot be deleted). */
618
+ async deletePack(workspaceId, packId) {
619
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
620
+ }
621
+ async listPackInstallations(workspaceId) {
622
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/packs/installations`);
623
+ }
624
+ // --- Capabilities -------------------------------------------------------------------------
625
+ async listCapabilities(workspaceId) {
626
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/capabilities`);
627
+ }
628
+ /** Add a manual capability catalog item (e.g. a remote MCP server). */
629
+ async createCapability(workspaceId, request) {
630
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/capabilities`, request);
631
+ }
632
+ async enableCapability(workspaceId, capabilityId, request = {}) {
633
+ return await this.requestJson(
634
+ "POST",
635
+ `/v1/workspaces/${workspaceId}/capabilities/${encodeURIComponent(capabilityId)}/enable`,
636
+ request
637
+ );
638
+ }
639
+ async disableCapability(workspaceId, capabilityId) {
640
+ return await this.requestJson(
641
+ "POST",
642
+ `/v1/workspaces/${workspaceId}/capabilities/${encodeURIComponent(capabilityId)}/disable`
643
+ );
644
+ }
645
+ /** Search the official MCP registry for installable capabilities. */
646
+ async discoverMcpCapabilities(workspaceId, options = {}) {
647
+ return await this.requestJson(
648
+ "GET",
649
+ `/v1/workspaces/${workspaceId}/capabilities/discovery/mcp-registry`,
650
+ void 0,
651
+ {
652
+ ...options.query !== void 0 ? { query: options.query } : {},
653
+ ...options.limit !== void 0 ? { limit: String(options.limit) } : {}
654
+ }
655
+ );
656
+ }
657
+ // --- GitHub ----------------------------------------------------------------------------------
658
+ /** GitHub App configuration status + a signed install URL when configured. */
659
+ async getGitHubApp(workspaceId) {
660
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/github/app`);
661
+ }
662
+ /**
663
+ * Browser entry point that plants the CSRF cookie and forwards to GitHub's
664
+ * install page. Open this in a browser (it redirects); `state` comes from
665
+ * `getGitHubApp().installUrl` or a github_connect_link tool.
666
+ */
667
+ githubConnectUrl(workspaceId, state) {
668
+ return this.url(`/v1/workspaces/${workspaceId}/github/connect`, { state });
669
+ }
670
+ async listGitHubRepositories(workspaceId) {
671
+ return await this.requestJson("GET", `/v1/workspaces/${workspaceId}/github/repositories`);
672
+ }
673
+ /** Re-sync the installation's repository list from GitHub. */
674
+ async syncGitHubRepositories(workspaceId) {
675
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/github/repositories/sync`);
676
+ }
677
+ /** Build a GitHub App manifest + the GitHub URL to submit it to. */
678
+ async createGitHubAppManifest(workspaceId, request = {}) {
679
+ return await this.requestJson(
680
+ "POST",
681
+ `/v1/workspaces/${workspaceId}/github/app-manifest`,
682
+ request
683
+ );
684
+ }
685
+ // --- API keys ----------------------------------------------------------------------------------
686
+ async listApiKeys(workspaceId) {
687
+ const response = await this.requestJson("GET", `/v1/workspaces/${workspaceId}/api-keys`);
688
+ return response.apiKeys;
689
+ }
690
+ /** The returned `token` is shown once; only its prefix is stored. */
691
+ async createApiKey(workspaceId, request) {
692
+ return await this.requestJson("POST", `/v1/workspaces/${workspaceId}/api-keys`, request);
693
+ }
694
+ /** Revoke an API key. Returns the revoked key. */
695
+ async deleteApiKey(workspaceId, apiKeyId) {
696
+ return await this.requestJson("DELETE", `/v1/workspaces/${workspaceId}/api-keys/${apiKeyId}`);
697
+ }
698
+ // --- Billing (account-scoped) --------------------------------------------------------------------
699
+ async getBilling(options = {}) {
700
+ return await this.requestJson("GET", "/v1/billing", void 0, {
701
+ ...options.accountId !== void 0 ? { accountId: options.accountId } : {}
702
+ });
703
+ }
704
+ async getBillingUsage(options = {}) {
705
+ return await this.requestJson("GET", "/v1/billing/usage", void 0, {
706
+ ...options.accountId !== void 0 ? { accountId: options.accountId } : {},
707
+ ...options.workspaceId !== void 0 ? { workspaceId: options.workspaceId } : {}
708
+ });
709
+ }
710
+ async getBillingEntitlements(options = {}) {
711
+ return await this.requestJson("GET", "/v1/billing/entitlements", void 0, {
712
+ ...options.accountId !== void 0 ? { accountId: options.accountId } : {}
713
+ });
714
+ }
715
+ /** Start a Stripe checkout for prepaid credits. */
716
+ async createBillingCheckout(request) {
717
+ return await this.requestJson("POST", "/v1/billing/checkout", request);
718
+ }
719
+ // --- Internals -------------------------------------------------------------
720
+ headers() {
721
+ const extra = typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
722
+ return {
723
+ ...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {},
724
+ ...extra
725
+ };
726
+ }
727
+ url(path, query = {}) {
728
+ const params = new URLSearchParams(query).toString();
729
+ return `${this.baseUrl}${path}${params ? `?${params}` : ""}`;
730
+ }
731
+ async requestJson(method, path, body, query = {}) {
732
+ const response = await this.fetchImpl(this.url(path, query), {
733
+ method,
734
+ headers: {
735
+ ...this.headers(),
736
+ Accept: "application/json",
737
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
738
+ },
739
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
740
+ });
741
+ if (!response.ok) {
742
+ throw new OpenGeniApiError(response.status, await safeText(response));
743
+ }
744
+ return await response.json();
745
+ }
746
+ /** Like `requestJson` for endpoints that respond with no body (204). */
747
+ async requestVoid(method, path, body) {
748
+ const response = await this.fetchImpl(this.url(path), {
749
+ method,
750
+ headers: {
751
+ ...this.headers(),
752
+ Accept: "application/json",
753
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
754
+ },
755
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
756
+ });
757
+ if (!response.ok) {
758
+ throw new OpenGeniApiError(response.status, await safeText(response));
759
+ }
760
+ }
761
+ };
762
+ async function safeText(response) {
763
+ try {
764
+ return await response.text();
765
+ } catch {
766
+ return "";
767
+ }
768
+ }
769
+ function delay(ms) {
770
+ return new Promise((resolve) => setTimeout(resolve, ms));
771
+ }
772
+
773
+ // src/proxy.ts
774
+ function formatSseEvent(event) {
775
+ return `id: ${event.sequence}
776
+ event: ${event.type}
777
+ data: ${JSON.stringify(event)}
778
+
779
+ `;
780
+ }
781
+ function sessionEventsToSseStream(events, options = {}) {
782
+ const encoder = new TextEncoder();
783
+ const iterator = events[Symbol.asyncIterator]();
784
+ let heartbeat;
785
+ let cancelled = false;
786
+ const stopHeartbeat = () => {
787
+ if (heartbeat !== void 0) {
788
+ clearInterval(heartbeat);
789
+ heartbeat = void 0;
790
+ }
791
+ };
792
+ return new ReadableStream({
793
+ start: (controller) => {
794
+ if (options.heartbeatMs !== void 0) {
795
+ heartbeat = setInterval(() => {
796
+ try {
797
+ controller.enqueue(encoder.encode(": ping\n\n"));
798
+ } catch {
799
+ stopHeartbeat();
800
+ }
801
+ }, options.heartbeatMs);
802
+ }
803
+ },
804
+ pull: async (controller) => {
805
+ let result;
806
+ try {
807
+ result = await iterator.next();
808
+ } catch (error) {
809
+ stopHeartbeat();
810
+ throw error;
811
+ }
812
+ if (cancelled) {
813
+ return;
814
+ }
815
+ if (result.done) {
816
+ stopHeartbeat();
817
+ controller.close();
818
+ return;
819
+ }
820
+ controller.enqueue(encoder.encode(formatSseEvent(result.value)));
821
+ },
822
+ cancel: () => {
823
+ cancelled = true;
824
+ stopHeartbeat();
825
+ options.onCancel?.();
826
+ void Promise.resolve(iterator.return?.(void 0)).then(
827
+ () => void 0,
828
+ () => void 0
829
+ );
830
+ }
831
+ });
832
+ }
833
+ function sessionEventsToSseResponse(events, options = {}) {
834
+ return new Response(sessionEventsToSseStream(events, options), {
835
+ headers: {
836
+ "Content-Type": "text/event-stream; charset=utf-8",
837
+ "Cache-Control": "no-cache, no-transform",
838
+ Connection: "keep-alive"
839
+ }
840
+ });
841
+ }
842
+ function resumeSequenceFromRequest(request) {
843
+ const url = new URL(request.url);
844
+ const raw = url.searchParams.get("after") ?? request.headers.get("Last-Event-ID");
845
+ const parsed = raw === null ? 0 : Number(raw);
846
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
847
+ }
848
+ function proxySessionEventStream(client, workspaceId, sessionId, options = {}) {
849
+ const { after, heartbeatMs, signal, ...streamOptions } = options;
850
+ const upstream = new AbortController();
851
+ if (signal?.aborted) {
852
+ upstream.abort();
853
+ } else {
854
+ signal?.addEventListener("abort", () => upstream.abort(), { once: true });
855
+ }
856
+ const resolvedAfter = after instanceof Request ? resumeSequenceFromRequest(after) : after ?? 0;
857
+ const events = client.streamEvents(workspaceId, sessionId, {
858
+ ...streamOptions,
859
+ after: resolvedAfter,
860
+ signal: upstream.signal
861
+ });
862
+ return sessionEventsToSseResponse(events, {
863
+ ...heartbeatMs !== void 0 ? { heartbeatMs } : {},
864
+ onCancel: () => upstream.abort()
865
+ });
866
+ }
867
+
868
+ // src/types.ts
869
+ var SESSION_EVENT_TYPES = [
870
+ "session.created",
871
+ "session.status.changed",
872
+ "session.requiresAction",
873
+ "session.context.compacted",
874
+ "session.context.cleared",
875
+ "user.message",
876
+ "user.interrupt",
877
+ "user.approvalDecision",
878
+ "turn.queued",
879
+ "turn.updated",
880
+ "turn.started",
881
+ "turn.completed",
882
+ "turn.failed",
883
+ "turn.cancelled",
884
+ "turn.preempted",
885
+ "agent.message.delta",
886
+ "agent.message.completed",
887
+ "agent.reasoning.delta",
888
+ "agent.toolCall.created",
889
+ "agent.toolCall.output",
890
+ "agent.updated",
891
+ "sandbox.operation.started",
892
+ "sandbox.operation.completed",
893
+ "sandbox.operation.failed",
894
+ "sandbox.command.output.delta",
895
+ "artifact.created",
896
+ "goal.set",
897
+ "goal.updated",
898
+ "goal.completed",
899
+ "goal.paused",
900
+ "goal.resumed",
901
+ "goal.continuation"
902
+ ];
903
+ var KNOWN_PERMISSIONS = [
904
+ "account:read",
905
+ "account:admin",
906
+ "members:manage",
907
+ "workspace:create",
908
+ "billing:read",
909
+ "billing:manage",
910
+ "workspace:read",
911
+ "workspace:admin",
912
+ "sessions:create",
913
+ "sessions:read",
914
+ "sessions:control",
915
+ "files:upload",
916
+ "files:read",
917
+ "documents:manage",
918
+ "documents:search",
919
+ "scheduled_tasks:manage",
920
+ "scheduled_tasks:run",
921
+ "github:manage",
922
+ "github:use",
923
+ "api_keys:manage",
924
+ "environments:manage",
925
+ "environments:use",
926
+ "goals:manage"
927
+ ];
928
+ var KNOWN_USAGE_EVENT_TYPES = [
929
+ "agent_run.created",
930
+ "agent_run.completed",
931
+ "model.tokens",
932
+ "model.cost",
933
+ "file.uploaded",
934
+ "file.deleted",
935
+ "document.indexed",
936
+ "scheduled_task.fired",
937
+ "api_key.request"
938
+ ];
939
+ export {
940
+ KNOWN_PERMISSIONS,
941
+ KNOWN_USAGE_EVENT_TYPES,
942
+ OpenGeniApiError,
943
+ OpenGeniClient,
944
+ OpenGeniStreamError,
945
+ SESSION_EVENT_TYPES,
946
+ formatSseEvent,
947
+ isRetryableStreamError,
948
+ parseSseStream,
949
+ proxySessionEventStream,
950
+ resumeSequenceFromRequest,
951
+ sessionEventsToSseResponse,
952
+ sessionEventsToSseStream,
953
+ streamSessionEvents
954
+ };
955
+ //# sourceMappingURL=index.js.map