@gitterm/sdk 0.1.2 → 0.2.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/client.ts
2
- import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client";
2
+ import {
3
+ createTRPCClient,
4
+ httpBatchLink,
5
+ httpSubscriptionLink,
6
+ splitLink,
7
+ TRPCClientError
8
+ } from "@trpc/client";
9
+ import { EventSource } from "eventsource";
3
10
 
4
11
  // src/config.ts
5
12
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
@@ -51,6 +58,14 @@ async function deleteConfig(configPath) {
51
58
  }
52
59
 
53
60
  // src/errors.ts
61
+ var WORKSPACE_LIFECYCLE_ERROR_CODES = [
62
+ "WORKSPACE_NOT_RUNNING",
63
+ "WORKSPACE_TERMINATED",
64
+ "WORKSPACE_NON_RECOVERABLE",
65
+ "WORKSPACE_START_TIMEOUT",
66
+ "WORKSPACE_RESTART_FAILED"
67
+ ];
68
+
54
69
  class GittermError extends Error {
55
70
  code;
56
71
  cause;
@@ -69,6 +84,15 @@ class WorkspaceLifecycleError extends GittermError {
69
84
  }
70
85
  }
71
86
 
87
+ class AgentRunError extends GittermError {
88
+ run;
89
+ constructor(run, code) {
90
+ super(code, run.error ?? (code === "INPUT_REQUIRED" ? "The agent needs input; supply a handler or use runs.respond()" : `Agent run ${run.status}`));
91
+ this.run = run;
92
+ this.name = "AgentRunError";
93
+ }
94
+ }
95
+
72
96
  // src/transport.ts
73
97
  var LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
74
98
  function normalizeServerUrl(value) {
@@ -99,6 +123,152 @@ function createNoRedirectFetch(fetchImpl = fetch) {
99
123
  };
100
124
  }
101
125
 
126
+ // src/runs.ts
127
+ function terminal(status) {
128
+ return status === "completed" || status === "failed" || status === "cancelled";
129
+ }
130
+ function aborted(signal) {
131
+ if (signal?.aborted)
132
+ throw new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled");
133
+ }
134
+ async function abortable(operation, signal) {
135
+ if (signal.aborted)
136
+ operation.catch(() => {});
137
+ aborted(signal);
138
+ let onAbort;
139
+ try {
140
+ return await Promise.race([
141
+ operation,
142
+ new Promise((_, reject) => {
143
+ onAbort = () => reject(new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled"));
144
+ signal.addEventListener("abort", onAbort, { once: true });
145
+ })
146
+ ]);
147
+ } finally {
148
+ if (onAbort)
149
+ signal.removeEventListener("abort", onAbort);
150
+ }
151
+ }
152
+ function delay(ms, signal) {
153
+ aborted(signal);
154
+ return new Promise((resolve, reject) => {
155
+ const onAbort = () => {
156
+ clearTimeout(timer);
157
+ reject(new GittermError("ABORTED", "Operation aborted"));
158
+ };
159
+ const timer = setTimeout(() => {
160
+ signal.removeEventListener("abort", onAbort);
161
+ resolve();
162
+ }, ms);
163
+ signal.addEventListener("abort", onAbort, { once: true });
164
+ });
165
+ }
166
+ async function deadline(options, operation) {
167
+ aborted(options?.signal);
168
+ const timeoutMs = options?.timeoutMs ?? 30 * 60000;
169
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
170
+ throw new GittermError("BAD_REQUEST", "timeoutMs must be finite and non-negative");
171
+ const controller = new AbortController;
172
+ const onAbort = () => controller.abort();
173
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
174
+ let timedOut = false;
175
+ const timer = setTimeout(() => {
176
+ timedOut = true;
177
+ controller.abort();
178
+ }, timeoutMs);
179
+ try {
180
+ return await operation(controller.signal);
181
+ } catch (error) {
182
+ if (timedOut)
183
+ throw new GittermError("TIMEOUT", "Run observation timed out; the agent was not cancelled", {
184
+ cause: error
185
+ });
186
+ aborted(options?.signal);
187
+ throw error;
188
+ } finally {
189
+ clearTimeout(timer);
190
+ options?.signal?.removeEventListener("abort", onAbort);
191
+ controller.abort();
192
+ }
193
+ }
194
+ async function* runEvents(states) {
195
+ const seen = new Set;
196
+ let pending = new Set;
197
+ let status;
198
+ for await (const run of states) {
199
+ if (run.status !== status) {
200
+ status = run.status;
201
+ yield { type: "run.status", run };
202
+ }
203
+ const current = new Set(run.pendingInputs.map((request) => request.id));
204
+ for (const requestId of pending) {
205
+ if (!current.has(requestId))
206
+ yield { type: "input.resolved", run, requestId };
207
+ }
208
+ pending = current;
209
+ for (const request of run.pendingInputs) {
210
+ if (seen.has(request.id))
211
+ continue;
212
+ seen.add(request.id);
213
+ yield { type: "input.required", run, request };
214
+ }
215
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
216
+ yield { type: `run.${run.status}`, run };
217
+ return;
218
+ }
219
+ }
220
+ }
221
+ function runHelpers(operations) {
222
+ return {
223
+ events: (ref, options) => runEvents(operations.watch(ref, options)),
224
+ wait: (ref, options) => deadline(options, async (signal) => {
225
+ for await (const run of operations.watch(ref, { signal })) {
226
+ if (terminal(run.status) || run.status === "awaiting_input" && options?.until !== "terminal")
227
+ return run;
228
+ }
229
+ throw new GittermError("NETWORK", "Run stream ended before a terminal state");
230
+ }),
231
+ result: (ref, options) => deadline(options, async (signal) => {
232
+ for await (const event of runEvents(operations.watch(ref, { signal }))) {
233
+ aborted(signal);
234
+ if (event.type === "run.completed")
235
+ return event.run;
236
+ if (event.type === "run.failed")
237
+ throw new AgentRunError(event.run, "RUN_FAILED");
238
+ if (event.type === "run.cancelled")
239
+ throw new AgentRunError(event.run, "RUN_CANCELLED");
240
+ if (event.type !== "input.required")
241
+ continue;
242
+ const { request } = event;
243
+ let reply;
244
+ if (request.kind === "permission") {
245
+ if (!options?.onPermission)
246
+ throw new AgentRunError(event.run, "INPUT_REQUIRED");
247
+ reply = {
248
+ type: "permission",
249
+ response: await abortable(Promise.resolve(options.onPermission(request, { signal })), signal)
250
+ };
251
+ } else {
252
+ if (!options?.onQuestion)
253
+ throw new AgentRunError(event.run, "INPUT_REQUIRED");
254
+ reply = {
255
+ type: "question",
256
+ ...await abortable(Promise.resolve(options.onQuestion(request, { signal })), signal)
257
+ };
258
+ }
259
+ aborted(signal);
260
+ try {
261
+ await abortable(operations.respond(ref, { requestId: request.id, reply }), signal);
262
+ } catch (error) {
263
+ if (!(error instanceof GittermError) || error.code !== "INPUT_NOT_PENDING")
264
+ throw error;
265
+ }
266
+ }
267
+ throw new GittermError("NETWORK", "Run stream ended before a terminal state");
268
+ })
269
+ };
270
+ }
271
+
102
272
  // src/client.ts
103
273
  function envValue(name) {
104
274
  const value = typeof process !== "undefined" ? process.env[name] : undefined;
@@ -129,6 +299,29 @@ function normalizeBaseCommit(value) {
129
299
  return null;
130
300
  return /^[0-9a-f]{40}([0-9a-f]{24})?$/i.test(trimmed) ? trimmed.toLowerCase() : trimmed;
131
301
  }
302
+ function workspaceIdOf(ref) {
303
+ return typeof ref === "string" ? ref : ref.id;
304
+ }
305
+ function runTargetOf(ref) {
306
+ return { workspaceId: ref.workspaceId, runId: ref.id };
307
+ }
308
+ function throwIfAborted(signal, what) {
309
+ if (signal?.aborted)
310
+ throw new GittermError("ABORTED", `Aborted while ${what}`);
311
+ }
312
+ function sleep(ms, signal, what) {
313
+ return new Promise((resolve, reject) => {
314
+ const timer = setTimeout(() => {
315
+ signal?.removeEventListener("abort", onAbort);
316
+ resolve();
317
+ }, ms);
318
+ const onAbort = () => {
319
+ clearTimeout(timer);
320
+ reject(new GittermError("ABORTED", `Aborted while ${what}`));
321
+ };
322
+ signal?.addEventListener("abort", onAbort, { once: true });
323
+ });
324
+ }
132
325
  function normalizeWorkspace(workspace) {
133
326
  if (!workspace)
134
327
  return null;
@@ -157,6 +350,10 @@ function normalizeWorkspace(workspace) {
157
350
  name: workspace.image.name,
158
351
  imageId: workspace.image.imageId
159
352
  } : null,
353
+ metadata: workspace.metadata ?? {},
354
+ opencodeApi: workspace.opencodeApi ?? "v1",
355
+ autoTerminateAt: toIso(workspace.autoTerminateAt),
356
+ customImage: workspace.customImage ?? null,
160
357
  startedAt: toIso(workspace.startedAt),
161
358
  pausedAt: toIso(workspace.pausedAt),
162
359
  terminatedAt: toIso(workspace.terminatedAt),
@@ -193,6 +390,8 @@ function mapTrpcCode(code) {
193
390
  return "BAD_REQUEST";
194
391
  case "CONFLICT":
195
392
  return "CONFLICT";
393
+ case "TIMEOUT":
394
+ return "TIMEOUT";
196
395
  default:
197
396
  return "SERVER_ERROR";
198
397
  }
@@ -201,58 +400,113 @@ function credentialErrorCode(message) {
201
400
  const match = /^(MODEL_CREDENTIAL_(?:UNAVAILABLE|DUPLICATE_PROVIDER|INVALID|REQUIRED)):\s*/.exec(message);
202
401
  return match?.[1];
203
402
  }
403
+ function toGittermError(serverUrl, error) {
404
+ if (error instanceof GittermError)
405
+ return error;
406
+ if (error instanceof TRPCClientError) {
407
+ const data = error.data;
408
+ const trpcCode = data?.code;
409
+ if (!error.data) {
410
+ return new GittermError("NETWORK", `Could not reach the GitTerm server at ${serverUrl}`, {
411
+ cause: error
412
+ });
413
+ }
414
+ const code = mapTrpcCode(trpcCode);
415
+ const structuredLifecycleCode = WORKSPACE_LIFECYCLE_ERROR_CODES.find((candidate) => candidate === data?.workspaceLifecycleCode);
416
+ if (structuredLifecycleCode) {
417
+ return new WorkspaceLifecycleError(structuredLifecycleCode, error.message, { cause: error });
418
+ }
419
+ if (error.message.startsWith("INPUT_NOT_PENDING:"))
420
+ return new GittermError("INPUT_NOT_PENDING", error.message, { cause: error });
421
+ const credentialCode = credentialErrorCode(error.message);
422
+ if (credentialCode)
423
+ return new GittermError(credentialCode, error.message, { cause: error });
424
+ const lifecycleCode = WORKSPACE_LIFECYCLE_ERROR_CODES.find((candidate) => error.message.startsWith(`${candidate}:`));
425
+ if (lifecycleCode) {
426
+ return new WorkspaceLifecycleError(lifecycleCode, error.message, { cause: error });
427
+ }
428
+ return new GittermError(code, code === "UNAUTHORIZED" ? `Authentication failed: ${error.message}. Check that the API token is valid and has not expired.` : error.message, { cause: error });
429
+ }
430
+ return new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
431
+ }
204
432
  async function runWithServer(serverUrl, operation) {
205
433
  try {
206
434
  return await operation();
207
435
  } catch (error) {
208
- if (error instanceof GittermError)
209
- throw error;
210
- if (error instanceof TRPCClientError) {
211
- const trpcCode = error.data?.code;
212
- if (!error.data) {
213
- throw new GittermError("NETWORK", `Could not reach the GitTerm server at ${serverUrl}`, {
214
- cause: error
215
- });
216
- }
217
- const code = mapTrpcCode(trpcCode);
218
- const credentialCode = credentialErrorCode(error.message);
219
- if (credentialCode)
220
- throw new GittermError(credentialCode, error.message, { cause: error });
221
- if (/WORKSPACE_TERMINATED/.test(error.message)) {
222
- throw new WorkspaceLifecycleError("WORKSPACE_TERMINATED", error.message, { cause: error });
223
- }
224
- if (/WORKSPACE_NON_RECOVERABLE/.test(error.message)) {
225
- throw new WorkspaceLifecycleError("WORKSPACE_NON_RECOVERABLE", error.message, {
226
- cause: error
227
- });
228
- }
229
- if (/WORKSPACE_START_TIMEOUT/.test(error.message)) {
230
- throw new WorkspaceLifecycleError("WORKSPACE_START_TIMEOUT", error.message, {
231
- cause: error
232
- });
233
- }
234
- if (/WORKSPACE_RESTART_FAILED/.test(error.message)) {
235
- throw new WorkspaceLifecycleError("WORKSPACE_RESTART_FAILED", error.message, {
236
- cause: error
237
- });
238
- }
239
- throw new GittermError(code, code === "UNAUTHORIZED" ? `Authentication failed: ${error.message}. Check that the API token is valid and has not expired.` : error.message, { cause: error });
436
+ throw toGittermError(serverUrl, error);
437
+ }
438
+ }
439
+ function isTerminalRun(status) {
440
+ return status === "completed" || status === "failed" || status === "cancelled";
441
+ }
442
+ async function* watchRun(trpc, serverUrl, target, options) {
443
+ const what = `watching run ${target.runId}`;
444
+ throwIfAborted(options?.signal, what);
445
+ const queue = [];
446
+ let wake;
447
+ const push = (item) => {
448
+ queue.push(item);
449
+ wake?.();
450
+ wake = undefined;
451
+ };
452
+ const onAbort = () => push({ error: new GittermError("ABORTED", `Aborted while ${what}`) });
453
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
454
+ let last;
455
+ const subscription = trpc.run.lifecycle.subscribe(target, {
456
+ onData: (event) => {
457
+ last = event.run;
458
+ push({ run: last });
459
+ },
460
+ onError: (error) => push({ error }),
461
+ onComplete: () => push(last && isTerminalRun(last.status) ? { done: true } : {
462
+ error: new GittermError("NETWORK", `Run ${target.runId} event stream ended unexpectedly`)
463
+ })
464
+ });
465
+ try {
466
+ while (true) {
467
+ throwIfAborted(options?.signal, what);
468
+ if (queue.length === 0)
469
+ await new Promise((resolve) => wake = resolve);
470
+ const item = queue.shift();
471
+ if ("done" in item)
472
+ return;
473
+ if ("error" in item)
474
+ throw toGittermError(serverUrl, item.error);
475
+ yield item.run;
476
+ if (isTerminalRun(item.run.status))
477
+ return;
240
478
  }
241
- throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
479
+ } finally {
480
+ options?.signal?.removeEventListener("abort", onAbort);
481
+ subscription.unsubscribe();
242
482
  }
243
483
  }
244
484
  function createGittermClient(options = {}) {
245
485
  const credentials = resolveCredentials(options);
486
+ const fetchImpl = createNoRedirectFetch(options.fetch);
487
+ const authorization = `Bearer ${credentials.token}`;
246
488
  const trpc = createTRPCClient({
247
489
  links: [
248
- httpBatchLink({
249
- url: toTrpcUrl(credentials.serverUrl),
250
- fetch: createNoRedirectFetch(options.fetch),
251
- headers: () => ({ authorization: `Bearer ${credentials.token}` })
490
+ splitLink({
491
+ condition: (operation) => operation.type === "subscription",
492
+ true: httpSubscriptionLink({
493
+ url: toTrpcUrl(credentials.serverUrl),
494
+ EventSource,
495
+ eventSourceOptions: {
496
+ fetch: (url, init) => fetchImpl(url, { ...init, headers: { ...init.headers, authorization } })
497
+ }
498
+ }),
499
+ false: httpBatchLink({
500
+ url: toTrpcUrl(credentials.serverUrl),
501
+ fetch: fetchImpl,
502
+ headers: () => ({ authorization })
503
+ })
252
504
  })
253
505
  ]
254
506
  });
255
507
  const run = (operation) => runWithServer(credentials.serverUrl, operation);
508
+ const watch = (ref, watchOptions) => watchRun(trpc, credentials.serverUrl, runTargetOf(ref), watchOptions);
509
+ const respond = (ref, input) => run(async () => trpc.run.respond.mutate({ ...runTargetOf(ref), ...input }));
256
510
  const normalizeCreateResult = (result) => {
257
511
  const workspace = normalizeWorkspace(result.workspace);
258
512
  if (!workspace)
@@ -279,9 +533,11 @@ function createGittermClient(options = {}) {
279
533
  const waitForWorkspaceSetup = async (workspaceId, waitOptions) => {
280
534
  const timeoutMs = waitOptions?.timeoutMs ?? 10 * 60000;
281
535
  const pollIntervalMs = waitOptions?.pollIntervalMs ?? 2000;
282
- const deadline = Date.now() + timeoutMs;
536
+ const signal = waitOptions?.signal;
537
+ const deadline2 = Date.now() + timeoutMs;
283
538
  while (true) {
284
- const result = await trpc.workspace.getSetupStatus.query({ workspaceId });
539
+ throwIfAborted(signal, "waiting for workspace setup");
540
+ const result = await trpc.workspace.getSetupStatus.query({ workspaceId }, { signal });
285
541
  if (result.status === "not_requested" || result.status === "succeeded")
286
542
  return result;
287
543
  if (result.status === "failed") {
@@ -289,12 +545,12 @@ function createGittermClient(options = {}) {
289
545
  throw new GittermError("BAD_REQUEST", `Workspace setup failed${result.exitCode === null ? "" : ` with exit code ${result.exitCode}`}${log ? `
290
546
  ${log}` : ""}`);
291
547
  }
292
- if (Date.now() >= deadline) {
548
+ if (Date.now() >= deadline2) {
293
549
  const log = result.log?.trim();
294
- throw new GittermError("NETWORK", `Timed out waiting for workspace ${workspaceId} setup (last status: ${result.status})${log ? `
550
+ throw new GittermError("TIMEOUT", `Timed out waiting for workspace ${workspaceId} setup (last status: ${result.status})${log ? `
295
551
  ${log}` : ""}`);
296
552
  }
297
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
553
+ await sleep(pollIntervalMs, signal, "waiting for workspace setup");
298
554
  }
299
555
  };
300
556
  return {
@@ -316,27 +572,32 @@ ${log}` : ""}`);
316
572
  list: (input) => run(async () => {
317
573
  const result = await trpc.workspace.listWorkspaces.query(input);
318
574
  return {
319
- workspaces: result.workspaces.map((workspace) => normalizeWorkspace(workspace)),
575
+ workspaces: result.workspaces.map(normalizeWorkspace).filter((workspace) => workspace !== null),
320
576
  pagination: result.pagination
321
577
  };
322
578
  }),
323
- get: (workspaceId) => run(async () => {
324
- const result = await trpc.workspace.getWorkspace.query({ workspaceId });
579
+ get: (ref) => run(async () => {
580
+ const result = await trpc.workspace.getWorkspace.query({
581
+ workspaceId: workspaceIdOf(ref)
582
+ });
325
583
  const workspace = normalizeWorkspace(result.workspace);
326
584
  if (!workspace)
327
585
  throw new GittermError("NOT_FOUND", "Workspace not found");
328
586
  return workspace;
329
587
  }),
330
- getRuntimeAccess: (workspaceId) => run(async () => {
331
- const result = await trpc.workspace.getRuntimeAccess.query({ workspaceId });
588
+ getRuntimeAccess: (ref) => run(async () => {
589
+ const result = await trpc.workspace.getRuntimeAccess.query({
590
+ workspaceId: workspaceIdOf(ref)
591
+ });
332
592
  return normalizeRuntime(result);
333
593
  }),
334
- ensureRunning: (workspaceId, options2) => run(async () => {
594
+ ensureRunning: (ref, waitOptions) => run(async () => {
595
+ throwIfAborted(waitOptions?.signal, "ensuring the workspace is running");
335
596
  const result = await trpc.workspace.ensureRunning.mutate({
336
- workspaceId,
337
- timeoutMs: options2?.timeoutMs,
338
- pollIntervalMs: options2?.pollIntervalMs
339
- });
597
+ workspaceId: workspaceIdOf(ref),
598
+ timeoutMs: waitOptions?.timeoutMs,
599
+ pollIntervalMs: waitOptions?.pollIntervalMs
600
+ }, { signal: waitOptions?.signal });
340
601
  const workspace = normalizeWorkspace(result.workspace);
341
602
  if (!workspace)
342
603
  throw new GittermError("SERVER_ERROR", "ensureRunning failed");
@@ -345,44 +606,62 @@ ${log}` : ""}`);
345
606
  runtime: normalizeRuntime(result.runtime)
346
607
  };
347
608
  }),
348
- pause: (workspaceId) => run(async () => {
349
- const result = await trpc.workspace.pauseWorkspace.mutate({ workspaceId });
609
+ pause: (ref) => run(async () => {
610
+ const result = await trpc.workspace.pauseWorkspace.mutate({
611
+ workspaceId: workspaceIdOf(ref)
612
+ });
350
613
  return { durationMinutes: result.durationMinutes };
351
614
  }),
352
- restart: (workspaceId) => run(async () => {
353
- const result = await trpc.workspace.restartWorkspace.mutate({ workspaceId });
615
+ restart: (ref) => run(async () => {
616
+ const result = await trpc.workspace.restartWorkspace.mutate({
617
+ workspaceId: workspaceIdOf(ref)
618
+ });
354
619
  return { status: result.status };
355
620
  }),
356
- terminate: (workspaceId) => run(async () => {
357
- const result = await trpc.workspace.deleteWorkspace.mutate({ workspaceId });
621
+ terminate: (ref) => run(async () => {
622
+ const result = await trpc.workspace.deleteWorkspace.mutate({
623
+ workspaceId: workspaceIdOf(ref)
624
+ });
358
625
  return {
359
626
  workspace: normalizeWorkspace(result.workspace),
360
627
  cleanupInBackground: result.cleanupInBackground
361
628
  };
362
629
  }),
363
630
  create: createWorkspace,
364
- setupStatus: (workspaceId) => run(async () => trpc.workspace.getSetupStatus.query({ workspaceId })),
365
- waitForSetup: (workspaceId, waitOptions) => run(() => waitForWorkspaceSetup(workspaceId, waitOptions))
631
+ models: (ref) => run(() => trpc.workspace.getModelAccess.query({ workspaceId: workspaceIdOf(ref) })),
632
+ setupStatus: (ref) => run(async () => trpc.workspace.getSetupStatus.query({ workspaceId: workspaceIdOf(ref) })),
633
+ waitForSetup: (ref, waitOptions) => run(() => waitForWorkspaceSetup(workspaceIdOf(ref), waitOptions))
366
634
  },
367
635
  runs: {
368
- create: (input) => run(async () => trpc.run.create.mutate(input)),
369
- get: (workspaceId, runId) => run(async () => trpc.run.get.query({ workspaceId, runId })),
370
- messages: (workspaceId, runId) => run(async () => trpc.run.messages.query({ workspaceId, runId })),
371
- cancel: (workspaceId, runId) => run(async () => trpc.run.cancel.mutate({ workspaceId, runId })),
372
- wait: (workspaceId, runId, waitOptions) => run(async () => {
373
- const timeoutMs = waitOptions?.timeoutMs ?? 30 * 60000;
374
- const pollIntervalMs = waitOptions?.pollIntervalMs ?? 2000;
375
- const deadline = Date.now() + timeoutMs;
376
- while (true) {
377
- const result = await trpc.run.get.query({ workspaceId, runId });
378
- if (result.status !== "pending" && result.status !== "running" && result.status !== "retrying")
379
- return result;
380
- if (Date.now() >= deadline) {
381
- throw new GittermError("NETWORK", `Timed out waiting for run ${runId}`);
382
- }
383
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
636
+ create: (input) => run(async () => {
637
+ const { signal, workspace, ...rest } = input;
638
+ const request = {
639
+ ...rest,
640
+ workspaceId: workspaceIdOf(workspace),
641
+ context: rest.context?.type === "continue" ? { type: "continue", runId: rest.context.run.id } : rest.context
642
+ };
643
+ if (request.waitForSetup) {
644
+ await waitForWorkspaceSetup(request.workspaceId, {
645
+ timeoutMs: request.setupTimeoutMs,
646
+ signal
647
+ });
384
648
  }
385
- })
649
+ throwIfAborted(signal, "creating the run");
650
+ return trpc.run.create.mutate({ ...request, idempotencyKey: request.idempotencyKey ?? crypto.randomUUID() }, { signal });
651
+ }),
652
+ list: (ref, listOptions) => run(async () => trpc.run.list.query({
653
+ workspaceId: workspaceIdOf(ref),
654
+ ...listOptions
655
+ })),
656
+ get: (ref) => run(async () => trpc.run.get.query(runTargetOf(ref))),
657
+ messages: (ref) => run(async () => {
658
+ const messages = await trpc.run.messages.query(runTargetOf(ref));
659
+ return messages.map((message) => ({ ...message, parts: message.parts ?? [] }));
660
+ }),
661
+ cancel: (ref) => run(async () => trpc.run.cancel.mutate(runTargetOf(ref))),
662
+ respond,
663
+ watch,
664
+ ...runHelpers({ watch, respond })
386
665
  },
387
666
  catalog: {
388
667
  agentTypes: (input) => run(async () => {
@@ -395,6 +674,18 @@ ${log}` : ""}`);
395
674
  }),
396
675
  workspaceOptions: () => run(async () => trpc.workspace.getWorkspaceCatalog.query())
397
676
  },
677
+ models: {
678
+ list: (listOptions) => run(async () => {
679
+ const result = await trpc.modelCredentials.listModels.query();
680
+ return result.models.map((model) => ({
681
+ id: model.modelId,
682
+ name: model.displayName,
683
+ provider: model.modelId.slice(0, model.modelId.indexOf("/")),
684
+ isFree: model.isFree,
685
+ isRecommended: model.isRecommended
686
+ })).filter((model) => !listOptions?.provider || model.provider === listOptions.provider);
687
+ })
688
+ },
398
689
  credentials: {
399
690
  list: () => run(async () => {
400
691
  const result = await trpc.modelCredentials.listMyCredentials.query();
@@ -411,6 +702,7 @@ ${log}` : ""}`);
411
702
  return result.providers.map((provider) => ({
412
703
  id: provider.id,
413
704
  name: provider.name,
705
+ logicalProviderKey: provider.logicalProviderKey,
414
706
  displayName: provider.displayName,
415
707
  authType: provider.authType,
416
708
  isRecommended: provider.isRecommended
@@ -420,7 +712,7 @@ ${log}` : ""}`);
420
712
  };
421
713
  }
422
714
  // src/device-login.ts
423
- function sleep(ms) {
715
+ function sleep2(ms) {
424
716
  return new Promise((resolve) => setTimeout(resolve, ms));
425
717
  }
426
718
  async function loginWithDeviceCode(serverUrl, options = {}) {
@@ -440,8 +732,8 @@ async function loginWithDeviceCode(serverUrl, options = {}) {
440
732
  intervalSeconds: codeJson.intervalSeconds,
441
733
  expiresInSeconds: codeJson.expiresInSeconds
442
734
  });
443
- const deadline = Date.now() + codeJson.expiresInSeconds * 1000;
444
- while (Date.now() < deadline) {
735
+ const deadline2 = Date.now() + codeJson.expiresInSeconds * 1000;
736
+ while (Date.now() < deadline2) {
445
737
  const tokenRes = await fetchImpl(new URL("/api/device/token", normalizedServerUrl), {
446
738
  method: "POST",
447
739
  headers: { "content-type": "application/json" },
@@ -455,7 +747,7 @@ async function loginWithDeviceCode(serverUrl, options = {}) {
455
747
  const errText = await tokenRes.text().catch(() => "");
456
748
  throw new Error(`Login failed: ${tokenRes.status} ${errText}`);
457
749
  }
458
- await sleep(Math.max(1, codeJson.intervalSeconds) * 1000);
750
+ await sleep2(Math.max(1, codeJson.intervalSeconds) * 1000);
459
751
  }
460
752
  throw new Error("Device code expired; try again.");
461
753
  }
@@ -525,6 +817,7 @@ function createGittermWorkspaceClient(options = {}) {
525
817
  };
526
818
  }
527
819
  export {
820
+ AgentRunError,
528
821
  DEFAULT_GITTERM_SERVER_URL,
529
822
  GittermError,
530
823
  WorkspaceLifecycleError,
package/dist/runs.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { AgentRun, AgentRunEvent, AgentRunReply, AgentRunResult, RunResultOptions, RunWaitOptions, RunWatchOptions } from "./types.js";
2
+ export declare function terminal(status: AgentRun["status"]): boolean;
3
+ export declare function aborted(signal?: AbortSignal): void;
4
+ export declare function abortable<T>(operation: Promise<T>, signal: AbortSignal): Promise<T>;
5
+ export declare function delay(ms: number, signal: AbortSignal): Promise<void>;
6
+ /** Deduplication is subscriber-local, not an exactly-once delivery guarantee. */
7
+ export declare function runEvents<T extends AgentRun>(states: AsyncIterable<T>): AsyncGenerator<AgentRunEvent<T>>;
8
+ /** Same interaction helpers for hosted and direct runs. */
9
+ export declare function runHelpers<Ref, T extends AgentRun>(operations: {
10
+ watch(ref: Ref, options?: RunWatchOptions): AsyncIterable<T>;
11
+ respond(ref: Ref, input: {
12
+ requestId: string;
13
+ reply: AgentRunReply;
14
+ }): Promise<T>;
15
+ }): {
16
+ events: (ref: Ref, options?: RunWatchOptions) => AsyncIterable<AgentRunEvent<T>>;
17
+ wait: (ref: Ref, options?: RunWaitOptions) => Promise<T>;
18
+ result: (ref: Ref, options?: RunResultOptions) => Promise<AgentRunResult<T>>;
19
+ };