@gitterm/sdk 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -258,7 +258,7 @@ const { workspace, runtime } = await client.workspaces.create({
258
258
  The username defaults to `x-access-token`. Inline repository credentials take precedence over
259
259
  `gitIntegrationId` and authenticate repository validation, cloning, and runtime Git operations such
260
260
  as pull and push. Without inline credentials, `gitIntegrationId` continues to use the connected
261
- dashboard integration. Omitting `modelCredentialIds` and `modelCredentials` likewise continues to
261
+ dashboard integration. Omitting `modelCredentials` likewise continues to
262
262
  use dashboard-managed model credentials.
263
263
 
264
264
  GitTerm does not save inline PATs in its application database. Inline PATs must be delivered to the
@@ -294,21 +294,102 @@ const status = await client.auth.status();
294
294
 
295
295
  ```ts
296
296
  client.auth.status(); // -> { userId, email, name, plan, authMethod }
297
- client.workspaces.list(options?); // -> { workspaces, pagination }
298
- client.workspaces.get(workspaceId);
299
- client.workspaces.getRuntimeAccess(workspaceId); // read-only; never resumes compute
300
- client.workspaces.ensureRunning(workspaceId, options?);
301
- client.workspaces.pause(workspaceId);
302
- client.workspaces.restart(workspaceId);
303
- client.workspaces.terminate(workspaceId);
297
+ client.workspaces.list(options?); // -> { workspaces, pagination }; filter by status or metadata
298
+ client.workspaces.get(workspace); // workspace = id string or any object with an `id`
299
+ client.workspaces.getRuntimeAccess(workspace); // read-only; never resumes compute
300
+ client.workspaces.ensureRunning(workspace, options?); // resume if paused, wait until running
301
+ client.workspaces.setupStatus(workspace);
302
+ client.workspaces.waitForSetup(workspace, options?);
303
+ client.workspaces.pause(workspace); // -> { durationMinutes } of the usage session just closed
304
+ client.workspaces.restart(workspace);
305
+ client.workspaces.terminate(workspace); // -> { workspace, cleanupInBackground }
304
306
  client.workspaces.create({
305
307
  repo: "https://github.com/acme/product",
306
308
  });
309
+ client.runs.create(input);
310
+ client.runs.list(workspace, options?); // -> { runs, pagination }
311
+ client.runs.get(run); // run = AgentRun, { workspaceId, runId }, or (workspaceId, runId)
312
+ client.runs.messages(run);
313
+ client.runs.cancel(run);
314
+ client.runs.wait(run, options?); // resolves when terminal or awaiting_input (push-based)
315
+ client.runs.respond(run, { requestId, reply }); // answer a permission prompt or agent question
307
316
  client.catalog.agentTypes();
308
317
  client.catalog.cloudProviders();
309
318
  client.catalog.workspaceOptions();
310
319
  ```
311
320
 
321
+ Every wait (`ensureRunning`, `waitForSetup`, and `runs.create` with `waitForSetup`) accepts
322
+ `{ timeoutMs, pollIntervalMs, signal }`. `runs.wait` is push-based (server-sent events) and
323
+ accepts `{ timeoutMs, signal, until }`. Pass an `AbortSignal` to stop waiting on shutdown; the
324
+ promise rejects with code `ABORTED`.
325
+
326
+ `pause()` returns `durationMinutes`, the length of the usage session it just closed, which is
327
+ what billing records. `terminate()` returns `cleanupInBackground: true` when the provider
328
+ finishes tearing down resources after the call returns.
329
+
330
+ #### Tagging and lifetime
331
+
332
+ `metadata` attaches caller-owned tags to a workspace so you can find it again without a
333
+ lookup table of your own, and `autoTerminateAfterMs` caps how long it can live:
334
+
335
+ ```ts
336
+ await client.workspaces.create({
337
+ repo: "https://github.com/acme/product",
338
+ metadata: { tenant: "acme", channel: "C0123" },
339
+ autoTerminateAfterMs: 2 * 60 * 60 * 1000, // gone in 2h even if this process crashes
340
+ });
341
+
342
+ const { workspaces } = await client.workspaces.list({
343
+ metadata: { tenant: "acme", channel: "C0123" },
344
+ });
345
+ ```
346
+
347
+ Metadata allows up to 20 keys of letters, digits, `_ . : -`, with values up to 500 characters,
348
+ and `list` matches workspaces containing every given pair. `autoTerminateAfterMs` ranges from
349
+ 1 minute to 30 days; the reaper terminates the workspace once the time passes regardless of
350
+ activity, and `workspace.autoTerminateAt` shows the deadline. Idle workspaces are still paused
351
+ by the platform's idle policy independently of this.
352
+
353
+ #### Bring your own image
354
+
355
+ Pass `image` to run your own build instead of the catalog image. On the managed service the
356
+ image must be pullable without credentials; creation fails immediately with the reason if it
357
+ isn't, rather than leaving a workspace stuck pulling.
358
+
359
+ ```ts
360
+ await client.workspaces.create({
361
+ repo: "https://github.com/acme/product",
362
+ provider: { type: "railway" },
363
+ image: "ghcr.io/acme/agent-runner:1.4.0",
364
+ });
365
+
366
+ // E2B takes a public template id or alias instead of a registry reference.
367
+ await client.workspaces.create({
368
+ repo: "https://github.com/acme/product",
369
+ provider: { type: "e2b" },
370
+ image: "acme-python-runner",
371
+ });
372
+ ```
373
+
374
+ Supported on Railway, AWS, Daytona, exe.dev, and E2B. Vercel and Cloudflare run fixed
375
+ runtimes and reject `image`. Floating tags such as `latest` are pinned to the digest verified
376
+ at create time, so a restart months later runs the same image; `workspace.customImage` shows
377
+ what was pinned.
378
+
379
+ The image has to behave like the stock one, because GitTerm's entrypoint does the clone, writes
380
+ agent files, runs setup phases, and starts the agent. Layer on the published base and keep its
381
+ entrypoint:
382
+
383
+ ```dockerfile
384
+ FROM opeoginni/gitterm-opencode-server:latest
385
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip poppler-utils \
386
+ && rm -rf /var/lib/apt/lists/*
387
+ ```
388
+
389
+ Rules: do not override `ENTRYPOINT` or `CMD`; keep global installs outside `/workspace`, which
390
+ is a persisted volume; keep `opencode` and `@gitterm/cli` on `PATH`; keep port 7681. For E2B,
391
+ build your template from the same base and publish it so any account can start it.
392
+
312
393
  The server defaults the agent to `opencode`, selects the user's preferred provider,
313
394
  uses that provider's default machine profile, and applies the provider's persistence policy.
314
395
  Override only the placement decisions your integration cares about:
@@ -385,12 +466,104 @@ run UI review or browser capture tools in the sandbox, upload the resulting medi
385
466
  the changelog in the checked-out repository, then terminate the workspace. Use an
386
467
  `idempotencyKey` based on the release SHA when the workflow may be retried.
387
468
 
469
+ ### Workspace lifecycle and readiness
470
+
471
+ A workspace has one of four statuses:
472
+
473
+ | Status | Meaning |
474
+ | ------------ | ----------------------------------------------------------------------------------------- |
475
+ | `pending` | Compute is being provisioned or resumed. `runtime.url` is `null`. |
476
+ | `running` | The provider reports the sandbox or container up. The agent process may still be booting. |
477
+ | `paused` | Stopped but resumable. |
478
+ | `terminated` | Gone for good. |
479
+
480
+ Whether `create()` returns `running` or `pending` depends on the provider. Sandbox providers
481
+ (E2B, Daytona, Vercel, Ascii, exe.dev, Cloudflare) settle immediately and return `running`.
482
+ Railway settles by webhook and returns `pending` until its deployment reports success, usually
483
+ within a minute. Code that only ever ran against a sandbox provider will see `pending` for the
484
+ first time when it moves to Railway.
485
+
486
+ Three things have to be true before a prompt can be delivered, and the SDK handles each:
487
+
488
+ 1. **The workspace is `running`.** `runs.create()` waits for a `pending` workspace to
489
+ transition on its own, up to `startTimeoutMs` (default 120s). It does not resume a `paused`
490
+ workspace; call `ensureRunning()` for that, which resumes, polls until `running`, and returns
491
+ the runtime access.
492
+ 2. **The agent answers.** `running` is the provider's view. `runs.create()` then probes the
493
+ workspace URL until the agent server responds before submitting the prompt, so callers never
494
+ need their own health check.
495
+ 3. **Setup finished (optional).** `afterAgent` commands run in the background once the agent is
496
+ reachable. Pass `waitForSetup: true` to `runs.create()` to block on them, or poll with
497
+ `setupStatus()` / `waitForSetup()`. Status `waiting` means the agent isn't reachable yet, so
498
+ setup hasn't started; `running`, `succeeded`, and `failed` describe the phase itself;
499
+ `not_requested` means there were no `afterAgent` commands. The SDK polls setup from the
500
+ client so a long `npm ci` never holds one HTTP request open; that polling needs the
501
+ `workspace:read` scope on the token in addition to `run:write`. `waiting` cannot last
502
+ forever: while you poll, the server also reads the setup state files inside the workspace
503
+ (through the agent when the provider has no exec channel), and if nothing has progressed
504
+ after 15 minutes it marks the setup `failed` with a log explaining what stalled.
505
+
506
+ The shortest correct sequence is therefore:
507
+
508
+ ```ts
509
+ const { workspace } = await client.workspaces.create({ repo, setup: { afterAgent: ["npm ci"] } });
510
+ const run = await client.runs.create({
511
+ workspaceId: workspace.id,
512
+ idempotencyKey: `review-${sha}`,
513
+ waitForSetup: true,
514
+ prompt: "Review the open pull request",
515
+ });
516
+ const done = await client.runs.wait(workspace.id, run.id);
517
+ ```
518
+
519
+ For a workspace you didn't just create, start with `ensureRunning()`:
520
+
521
+ ```ts
522
+ const { workspace, runtime } = await client.workspaces.ensureRunning(workspaceId);
523
+ // runtime.url is set; workspace.status is "running"
524
+ ```
525
+
526
+ Failures surface as `WorkspaceLifecycleError` with stable codes: `WORKSPACE_NOT_RUNNING` (the
527
+ workspace is `paused`, or stopped while waiting), `WORKSPACE_TERMINATED`,
528
+ `WORKSPACE_START_TIMEOUT` (still `pending` after the timeout, or no runtime URL),
529
+ `WORKSPACE_NON_RECOVERABLE`, and `WORKSPACE_RESTART_FAILED`.
530
+
531
+ Direct provider mode behaves differently: `direct.workspaces.create()` waits for the OpenCode
532
+ runtime to answer before returning, so a direct workspace is ready for `runs.create()` as soon
533
+ as `create()` resolves.
534
+
388
535
  ### Agent runs
389
536
 
390
537
  Runs use durable GitTerm IDs backed by the workspace's native OpenCode session. Reusing an
391
538
  idempotency key with the same input returns the original run, and terminal results remain
392
- available after the workspace is paused. Completion means the native session became idle;
393
- it does not claim that a pull request, upload, or other product outcome succeeded.
539
+ available after the workspace is paused. `idempotencyKey` defaults to a random UUID; pass your
540
+ own whenever a retry could otherwise submit the same prompt twice. Completion means the native
541
+ session became idle; it does not claim that a pull request, upload, or other product outcome
542
+ succeeded.
543
+
544
+ Every run carries `createdAt`, `submittedAt` (when the prompt reached the agent), and
545
+ `completedAt`. `runs.list(workspace, { status: "active" })` returns what is still in flight, so a
546
+ process recovering after a restart can pick up where it left off without having persisted run
547
+ ids itself. The server watches every active run's OpenCode session and keeps the run current;
548
+ `get`/`list`/`messages` read that state without touching the workspace.
549
+
550
+ Statuses: `pending` → `running` (or `retrying` while OpenCode backs off from the model
551
+ provider) → `completed` | `failed` | `cancelled`. A run that stops to ask something is
552
+ `awaiting_input` until you answer it (below).
553
+
554
+ `runs.messages()` returns each turn's concatenated `text` plus an ordered `parts` array. Tool
555
+ calls appear as `{ type: "tool", tool, status, title, input, output, error }` with output
556
+ truncated to 4000 characters, which is usually enough to see why a run went wrong:
557
+
558
+ ```ts
559
+ for (const message of await client.runs.messages(run)) {
560
+ for (const part of message.parts) {
561
+ if (part.type === "tool" && part.status === "error") {
562
+ console.log(part.tool, part.title, part.error);
563
+ }
564
+ }
565
+ }
566
+ ```
394
567
 
395
568
  ```ts
396
569
  const { workspace } = await client.workspaces.create({
@@ -409,6 +582,50 @@ const completed = await client.runs.wait(workspace.id, run.id);
409
582
  const messages = await client.runs.messages(workspace.id, run.id);
410
583
  ```
411
584
 
585
+ #### Questions and permission prompts
586
+
587
+ When the agent calls OpenCode's `question` tool, or a tool needs approval under your
588
+ `permission` config, the run becomes `awaiting_input` and `run.pendingInputs` describes what it
589
+ is waiting for. `runs.wait()` returns at that point by default; answer with `runs.respond()` and
590
+ wait again. This is what makes a chat bot able to relay "should I do A or B?" to a human.
591
+
592
+ ```ts
593
+ let run = await client.runs.wait(workspace.id, created.id);
594
+ while (run.status === "awaiting_input") {
595
+ for (const request of run.pendingInputs) {
596
+ if (request.kind === "permission") {
597
+ // request.title, e.g. "bash: rm -rf dist"; also permission / patterns / always
598
+ await client.runs.respond(run, {
599
+ requestId: request.id,
600
+ reply: { type: "permission", response: "once" }, // "once" | "always" | "reject"
601
+ });
602
+ } else {
603
+ // request.questions[i] has header, question, options[{ label, description }], multiple, custom
604
+ const answers = request.questions.map((question) => [question.options[0]!.label]);
605
+ await client.runs.respond(run, {
606
+ requestId: request.id,
607
+ reply: { type: "question", answers },
608
+ });
609
+ // or: reply: { type: "question", reject: true }
610
+ }
611
+ }
612
+ run = await client.runs.wait(workspace.id, created.id);
613
+ }
614
+ ```
615
+
616
+ Rejecting a permission or dismissing a question ends the turn: OpenCode records a failed tool
617
+ call and the run finishes. Pass `{ until: "terminal" }` to `runs.wait()` to keep waiting through
618
+ `awaiting_input` (for example when a separate process answers). A run left `awaiting_input` does
619
+ not keep its workspace awake; if nobody answers before the workspace's idle timeout pauses it,
620
+ the run is cancelled with `"Workspace paused while waiting for input"`. To avoid prompts entirely
621
+ in headless runs, allow the relevant tools in `opencode.config.permission` when creating the
622
+ workspace.
623
+
624
+ `runs.wait()` uses a lightweight lifecycle stream internally; it does not mirror OpenCode's
625
+ native message, tool, or token events. `runs.messages()` provides the persisted transcript at
626
+ lifecycle checkpoints and completion. For live execution details, use
627
+ `workspaces.getRuntimeAccess()` with the official OpenCode SDK.
628
+
412
629
  Runs are isolated by default and can execute in parallel. To preserve conversational context,
413
630
  continue a terminal run; continued runs sharing context must remain sequential:
414
631
 
@@ -423,57 +640,65 @@ const next = await client.runs.create({
423
640
 
424
641
  ### Model provider credentials
425
642
 
426
- Workspaces can receive model credentials two ways, and they compose:
427
-
428
- **Dashboard credentials** — list the account's credential metadata, choose one active credential
429
- per provider, and pass the IDs when creating a workspace. The SDK never returns credential
430
- secrets.
643
+ `modelCredentials` takes one entry per provider. Each entry selects where that provider's
644
+ credential comes from, and the three forms compose in one array:
431
645
 
432
646
  ```ts
433
- const credentials = await client.credentials.list();
434
- const selected = credentials.filter(
435
- (credential) =>
436
- credential.isActive && ["anthropic", "openai"].includes(credential.logicalProviderKey),
437
- );
438
-
439
647
  const { workspace } = await client.workspaces.create({
440
648
  repo: "https://github.com/acme/product",
441
- modelCredentialIds: selected.map((credential) => credential.id),
649
+ modelCredentials: [
650
+ { providerName: "openai", label: "work" }, // dashboard credential, by label
651
+ { providerName: "anthropic" }, // that provider's dashboard default
652
+ { providerName: "google", apiKey: process.env.GOOGLE_API_KEY! }, // inline, this workspace only
653
+ ],
442
654
  });
443
655
  ```
444
656
 
445
- **Inline credentials** — pass API keys directly for this workspace only. They are injected into
446
- the provisioned agent and never stored in the dashboard. Use
447
- `client.credentials.listProviders()` for valid provider names; OAuth providers (e.g. GitHub
448
- Copilot) can only be connected through the dashboard.
657
+ **Dashboard credentials** are addressed by `providerName` plus the `label` you gave them in the
658
+ dashboard (labels are unique per provider). Omit `label` to use the provider's default. The SDK
659
+ never returns credential secrets; `client.credentials.list()` returns metadata including labels
660
+ if you need to discover them, and requires an API token with the `workspace:write` scope.
449
661
 
450
- ```ts
451
- const { workspace } = await client.workspaces.create({
452
- repo: "https://github.com/acme/product",
453
- modelCredentials: [{ providerName: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY! }],
454
- });
662
+ **Inline credentials** pass an API key directly for this workspace only. They are injected into
663
+ the provisioned agent and never stored in the dashboard. Use `client.credentials.listProviders()`
664
+ for valid provider names; OAuth providers (e.g. GitHub Copilot) can only be connected through
665
+ the dashboard.
455
666
 
456
667
  const run = await client.runs.create({
457
- workspaceId: workspace.workspaceId,
458
- model: "anthropic/claude-sonnet-4-20250514",
459
- prompt: "Record before/after videos of the changes in PR #42",
668
+ workspaceId: workspace.workspaceId,
669
+ model: "anthropic/claude-sonnet-4-20250514",
670
+ prompt: "Record before/after videos of the changes in PR #42",
460
671
  });
461
- ```
672
+
673
+ ````
462
674
 
463
675
  Rules and errors:
464
676
 
465
- - Omit both fields to inject the dashboard defaults.
677
+ - Omit `modelCredentials` to inject every dashboard default.
678
+ - Naming any dashboard credential (an entry without `apiKey`) switches off the implicit defaults,
679
+ so list each provider the workspace needs. Inline-only arrays keep the defaults for the other
680
+ providers.
466
681
  - One credential per logical provider. An inline credential always overrides the dashboard
467
- credential (default or selected) for the same provider; two credentials for the same provider
468
- within one field throw `MODEL_CREDENTIAL_DUPLICATE_PROVIDER`.
469
- - Unknown providers or inline keys for OAuth-only providers throw `MODEL_CREDENTIAL_INVALID`;
470
- missing, inactive, or unowned dashboard selections throw `MODEL_CREDENTIAL_UNAVAILABLE`.
682
+ credential for the same provider; two entries for the same provider throw
683
+ `MODEL_CREDENTIAL_DUPLICATE_PROVIDER`.
684
+ - Unknown providers or inline keys for OAuth-only providers throw `MODEL_CREDENTIAL_INVALID`.
685
+ A label that doesn't exist throws `MODEL_CREDENTIAL_UNAVAILABLE` and the message lists the
686
+ labels that do.
471
687
  - A run that requests a credential-backed `provider/model` not available in its workspace throws
472
688
  `MODEL_CREDENTIAL_REQUIRED` before the prompt is submitted.
473
689
 
474
- Use `client.runs.cancel(workspaceId, runId)` to abort the current run. GitTerm keeps the
475
- underlying OpenCode session private. For native session control, use
476
- `workspaces.getRuntimeAccess()` and connect with the official OpenCode SDK.
690
+ Use `client.runs.cancel(workspaceId, runId)` to abort the current run; a run that is
691
+ `awaiting_input` has its pending prompt rejected first. GitTerm keeps the underlying OpenCode
692
+ session private. For native session control, use `workspaces.getRuntimeAccess()` and connect
693
+ with the official OpenCode SDK.
694
+
695
+ ### OpenCode API versions
696
+
697
+ Managed workspaces continue to run `opencode-ai@latest` (`opencodeApi: "v1"`). OpenCode 2
698
+ changes the server API; callers testing it must supply their own compatible image and create the
699
+ workspace with `opencode: { api: "v2" }`. Runs, questions, permissions, and events behave the
700
+ same from the SDK's point of view; the flag only tells GitTerm which protocol to speak to the
701
+ workspace. `v2` is experimental until OpenCode 2 ships.
477
702
 
478
703
  ### Errors
479
704
 
@@ -489,13 +714,13 @@ try {
489
714
  // "Not logged in. Run: gitterm login"
490
715
  }
491
716
  }
492
- ```
717
+ ````
493
718
 
494
719
  Workspace lifecycle failures are also exposed as `WorkspaceLifecycleError`, with stable
495
- `WORKSPACE_TERMINATED`, `WORKSPACE_NON_RECOVERABLE`, `WORKSPACE_START_TIMEOUT`, and
496
- `WORKSPACE_RESTART_FAILED` codes. General codes are
720
+ `WORKSPACE_NOT_RUNNING`, `WORKSPACE_TERMINATED`, `WORKSPACE_NON_RECOVERABLE`,
721
+ `WORKSPACE_START_TIMEOUT`, and `WORKSPACE_RESTART_FAILED` codes. General codes are
497
722
  `NOT_LOGGED_IN`, `UNAUTHORIZED`, `NOT_FOUND`, `FORBIDDEN`, `BAD_REQUEST`, `CONFLICT`,
498
- `SERVER_ERROR`, and `NETWORK`.
723
+ `SERVER_ERROR`, `NETWORK`, and `ABORTED` (a wait was cancelled through its `signal`).
499
724
 
500
725
  The package ships self-contained declarations from `dist`; TypeScript consumers do not
501
726
  need GitTerm's API package or tRPC server types.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentType, AgentRun, AgentRunCreateInput, AgentRunMessage, AuthStatus, CloudProvider, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceCatalog, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo } from "./types.js";
1
+ import type { AgentType, AgentRun, AgentRunCreateInput, AgentRunListOptions, AgentRunListResult, AgentRunMessage, AgentRunReply, AuthStatus, CloudProvider, RunRef, RunWaitOptions, WaitOptions, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRef, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceCatalog, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo } from "./types.js";
2
2
  export type GittermClientOptions = {
3
3
  serverUrl?: string;
4
4
  token?: string;
@@ -12,33 +12,41 @@ export type GittermClient = {
12
12
  };
13
13
  workspaces: {
14
14
  list(input?: WorkspaceListOptions): Promise<WorkspaceListResult>;
15
- get(workspaceId: string): Promise<Workspace>;
16
- getRuntimeAccess(workspaceId: string): Promise<WorkspaceRuntimeAccess>;
17
- ensureRunning(workspaceId: string, options?: {
18
- timeoutMs?: number;
19
- pollIntervalMs?: number;
20
- }): Promise<WorkspaceEnsureRunningResult>;
21
- pause(workspaceId: string): Promise<WorkspacePauseResult>;
22
- restart(workspaceId: string): Promise<WorkspaceRestartResult>;
23
- terminate(workspaceId: string): Promise<WorkspaceTerminateResult>;
15
+ get(workspace: WorkspaceRef): Promise<Workspace>;
16
+ getRuntimeAccess(workspace: WorkspaceRef): Promise<WorkspaceRuntimeAccess>;
17
+ /** Resume a paused workspace if needed and wait until it is running. */
18
+ ensureRunning(workspace: WorkspaceRef, options?: WaitOptions): Promise<WorkspaceEnsureRunningResult>;
19
+ pause(workspace: WorkspaceRef): Promise<WorkspacePauseResult>;
20
+ restart(workspace: WorkspaceRef): Promise<WorkspaceRestartResult>;
21
+ terminate(workspace: WorkspaceRef): Promise<WorkspaceTerminateResult>;
24
22
  create(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
25
- setupStatus(workspaceId: string): Promise<WorkspaceSetupStatus>;
26
- waitForSetup(workspaceId: string, options?: {
27
- timeoutMs?: number;
28
- pollIntervalMs?: number;
29
- }): Promise<WorkspaceSetupStatus>;
23
+ setupStatus(workspace: WorkspaceRef): Promise<WorkspaceSetupStatus>;
24
+ waitForSetup(workspace: WorkspaceRef, options?: WaitOptions): Promise<WorkspaceSetupStatus>;
30
25
  };
31
26
  runs: {
32
27
  create(input: AgentRunCreateInput): Promise<AgentRun>;
28
+ list(workspace: WorkspaceRef, options?: AgentRunListOptions): Promise<AgentRunListResult>;
29
+ get(run: RunRef): Promise<AgentRun>;
33
30
  get(workspaceId: string, runId: string): Promise<AgentRun>;
31
+ messages(run: RunRef): Promise<AgentRunMessage[]>;
34
32
  messages(workspaceId: string, runId: string): Promise<AgentRunMessage[]>;
33
+ cancel(run: RunRef): Promise<{
34
+ cancelled: boolean;
35
+ }>;
35
36
  cancel(workspaceId: string, runId: string): Promise<{
36
37
  cancelled: boolean;
37
38
  }>;
38
- wait(workspaceId: string, runId: string, options?: {
39
- timeoutMs?: number;
40
- pollIntervalMs?: number;
39
+ /** Answer the permission prompt or question a run is `awaiting_input` on. */
40
+ respond(run: RunRef, input: {
41
+ requestId: string;
42
+ reply: AgentRunReply;
41
43
  }): Promise<AgentRun>;
44
+ /**
45
+ * Resolve once the run needs attention: terminal, or `awaiting_input`
46
+ * unless `until: "terminal"`. Push-based; no polling.
47
+ */
48
+ wait(run: RunRef, options?: RunWaitOptions): Promise<AgentRun>;
49
+ wait(workspaceId: string, runId: string, options?: RunWaitOptions): Promise<AgentRun>;
42
50
  };
43
51
  catalog: {
44
52
  agentTypes(input?: {
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export type GittermErrorCode = "NOT_LOGGED_IN" | "UNAUTHORIZED" | "NOT_FOUND" | "FORBIDDEN" | "BAD_REQUEST" | "CONFLICT" | "SERVER_ERROR" | "NETWORK" | CredentialErrorCode | WorkspaceLifecycleErrorCode;
1
+ export type GittermErrorCode = "NOT_LOGGED_IN" | "UNAUTHORIZED" | "NOT_FOUND" | "FORBIDDEN" | "BAD_REQUEST" | "CONFLICT" | "SERVER_ERROR" | "NETWORK" | "ABORTED" | CredentialErrorCode | WorkspaceLifecycleErrorCode;
2
2
  export type CredentialErrorCode = "MODEL_CREDENTIAL_UNAVAILABLE" | "MODEL_CREDENTIAL_DUPLICATE_PROVIDER" | "MODEL_CREDENTIAL_INVALID" | "MODEL_CREDENTIAL_REQUIRED";
3
- export type WorkspaceLifecycleErrorCode = "WORKSPACE_TERMINATED" | "WORKSPACE_NON_RECOVERABLE" | "WORKSPACE_START_TIMEOUT" | "WORKSPACE_RESTART_FAILED";
3
+ export type WorkspaceLifecycleErrorCode = "WORKSPACE_NOT_RUNNING" | "WORKSPACE_TERMINATED" | "WORKSPACE_NON_RECOVERABLE" | "WORKSPACE_START_TIMEOUT" | "WORKSPACE_RESTART_FAILED";
4
+ export declare const WORKSPACE_LIFECYCLE_ERROR_CODES: readonly WorkspaceLifecycleErrorCode[];
4
5
  export declare class GittermError extends Error {
5
6
  readonly code: GittermErrorCode;
6
7
  readonly cause?: unknown;
package/dist/index.d.ts CHANGED
@@ -6,4 +6,4 @@ export type { DeviceCodeInfo, LoginWithDeviceCodeOptions } from "./device-login.
6
6
  export { GittermError, WorkspaceLifecycleError } from "./errors.js";
7
7
  export { createGittermWorkspaceClient, getWorkspaceEnvironment, type GittermWorkspaceClient, type WorkspaceClientOptions, type WorkspaceEnvironment, type WorkspacePort, type WorkspaceSelf, } from "./workspace-client.js";
8
8
  export type { CredentialErrorCode, GittermErrorCode, WorkspaceLifecycleErrorCode, } from "./errors.js";
9
- export type { AgentRun, AgentRunCreateInput, AgentRunMessage, AgentRunStatus, AgentType, AgentKey, AuthStatus, BuiltInAgentKey, CloudProvider, ProviderKey, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceCatalog, WorkspaceEnsureRunningResult, WorkspaceHostingType, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspaceStatus, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo, WorkspaceModelCredentialInput, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceProviderSelection, } from "./types.js";
9
+ export type { AgentRun, AgentRunCreateInput, AgentRunInputRequest, AgentRunMessage, AgentRunMessagePart, AgentRunReply, AgentRunStatus, RunWaitOptions, OpencodeApi, AgentType, AgentKey, AuthStatus, BuiltInAgentKey, CloudProvider, ProviderKey, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceCatalog, WorkspaceEnsureRunningResult, WorkspaceHostingType, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspaceStatus, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo, WorkspaceModelCredentialInput, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceProviderSelection, } from "./types.js";
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;
@@ -129,6 +144,37 @@ function normalizeBaseCommit(value) {
129
144
  return null;
130
145
  return /^[0-9a-f]{40}([0-9a-f]{24})?$/i.test(trimmed) ? trimmed.toLowerCase() : trimmed;
131
146
  }
147
+ function workspaceIdOf(ref) {
148
+ return typeof ref === "string" ? ref : ref.id;
149
+ }
150
+ function runTargetOf(refOrWorkspaceId, runId) {
151
+ if (typeof refOrWorkspaceId === "string") {
152
+ if (!runId)
153
+ throw new GittermError("BAD_REQUEST", "runId is required");
154
+ return { workspaceId: refOrWorkspaceId, runId };
155
+ }
156
+ return {
157
+ workspaceId: refOrWorkspaceId.workspaceId,
158
+ runId: "runId" in refOrWorkspaceId ? refOrWorkspaceId.runId : refOrWorkspaceId.id
159
+ };
160
+ }
161
+ function throwIfAborted(signal, what) {
162
+ if (signal?.aborted)
163
+ throw new GittermError("ABORTED", `Aborted while ${what}`);
164
+ }
165
+ function sleep(ms, signal, what) {
166
+ return new Promise((resolve, reject) => {
167
+ const timer = setTimeout(() => {
168
+ signal?.removeEventListener("abort", onAbort);
169
+ resolve();
170
+ }, ms);
171
+ const onAbort = () => {
172
+ clearTimeout(timer);
173
+ reject(new GittermError("ABORTED", `Aborted while ${what}`));
174
+ };
175
+ signal?.addEventListener("abort", onAbort, { once: true });
176
+ });
177
+ }
132
178
  function normalizeWorkspace(workspace) {
133
179
  if (!workspace)
134
180
  return null;
@@ -157,6 +203,10 @@ function normalizeWorkspace(workspace) {
157
203
  name: workspace.image.name,
158
204
  imageId: workspace.image.imageId
159
205
  } : null,
206
+ metadata: workspace.metadata ?? {},
207
+ opencodeApi: workspace.opencodeApi ?? "v1",
208
+ autoTerminateAt: toIso(workspace.autoTerminateAt),
209
+ customImage: workspace.customImage ?? null,
160
210
  startedAt: toIso(workspace.startedAt),
161
211
  pausedAt: toIso(workspace.pausedAt),
162
212
  terminatedAt: toIso(workspace.terminatedAt),
@@ -218,37 +268,75 @@ async function runWithServer(serverUrl, operation) {
218
268
  const credentialCode = credentialErrorCode(error.message);
219
269
  if (credentialCode)
220
270
  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
- });
271
+ const lifecycleCode = WORKSPACE_LIFECYCLE_ERROR_CODES.find((candidate) => error.message.includes(candidate));
272
+ if (lifecycleCode) {
273
+ throw new WorkspaceLifecycleError(lifecycleCode, error.message, { cause: error });
238
274
  }
239
275
  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 });
240
276
  }
241
277
  throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
242
278
  }
243
279
  }
280
+ function settlesWait(status, until) {
281
+ if (status === "completed" || status === "failed" || status === "cancelled")
282
+ return true;
283
+ return status === "awaiting_input" && until !== "terminal";
284
+ }
285
+ function waitForRun(trpc, target, options) {
286
+ const timeoutMs = options?.timeoutMs ?? 30 * 60000;
287
+ const what = `waiting for run ${target.runId}`;
288
+ return new Promise((resolve, reject) => {
289
+ throwIfAborted(options?.signal, what);
290
+ let last;
291
+ let subscription;
292
+ const finish = (settle) => {
293
+ clearTimeout(timer);
294
+ options?.signal?.removeEventListener("abort", onAbort);
295
+ settle();
296
+ subscription?.unsubscribe();
297
+ };
298
+ const onAbort = () => finish(() => reject(new GittermError("ABORTED", `Aborted while ${what}`)));
299
+ const timer = setTimeout(() => finish(() => reject(new GittermError("NETWORK", `Timed out ${what}`))), timeoutMs);
300
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
301
+ subscription = trpc.run.lifecycle.subscribe(target, {
302
+ onData: (event) => {
303
+ last = event.run;
304
+ if (settlesWait(last.status, options?.until)) {
305
+ const run = last;
306
+ finish(() => resolve(run));
307
+ }
308
+ },
309
+ onError: (error) => finish(() => reject(error)),
310
+ onComplete: () => finish(() => {
311
+ if (last && settlesWait(last.status, options?.until))
312
+ resolve(last);
313
+ else {
314
+ reject(new GittermError("NETWORK", `Run ${target.runId} event stream ended unexpectedly`));
315
+ }
316
+ })
317
+ });
318
+ });
319
+ }
244
320
  function createGittermClient(options = {}) {
245
321
  const credentials = resolveCredentials(options);
322
+ const fetchImpl = createNoRedirectFetch(options.fetch);
323
+ const authorization = `Bearer ${credentials.token}`;
246
324
  const trpc = createTRPCClient({
247
325
  links: [
248
- httpBatchLink({
249
- url: toTrpcUrl(credentials.serverUrl),
250
- fetch: createNoRedirectFetch(options.fetch),
251
- headers: () => ({ authorization: `Bearer ${credentials.token}` })
326
+ splitLink({
327
+ condition: (operation) => operation.type === "subscription",
328
+ true: httpSubscriptionLink({
329
+ url: toTrpcUrl(credentials.serverUrl),
330
+ EventSource,
331
+ eventSourceOptions: {
332
+ fetch: (url, init) => fetchImpl(url, { ...init, headers: { ...init.headers, authorization } })
333
+ }
334
+ }),
335
+ false: httpBatchLink({
336
+ url: toTrpcUrl(credentials.serverUrl),
337
+ fetch: fetchImpl,
338
+ headers: () => ({ authorization })
339
+ })
252
340
  })
253
341
  ]
254
342
  });
@@ -279,9 +367,11 @@ function createGittermClient(options = {}) {
279
367
  const waitForWorkspaceSetup = async (workspaceId, waitOptions) => {
280
368
  const timeoutMs = waitOptions?.timeoutMs ?? 10 * 60000;
281
369
  const pollIntervalMs = waitOptions?.pollIntervalMs ?? 2000;
370
+ const signal = waitOptions?.signal;
282
371
  const deadline = Date.now() + timeoutMs;
283
372
  while (true) {
284
- const result = await trpc.workspace.getSetupStatus.query({ workspaceId });
373
+ throwIfAborted(signal, "waiting for workspace setup");
374
+ const result = await trpc.workspace.getSetupStatus.query({ workspaceId }, { signal });
285
375
  if (result.status === "not_requested" || result.status === "succeeded")
286
376
  return result;
287
377
  if (result.status === "failed") {
@@ -294,7 +384,7 @@ ${log}` : ""}`);
294
384
  throw new GittermError("NETWORK", `Timed out waiting for workspace ${workspaceId} setup (last status: ${result.status})${log ? `
295
385
  ${log}` : ""}`);
296
386
  }
297
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
387
+ await sleep(pollIntervalMs, signal, "waiting for workspace setup");
298
388
  }
299
389
  };
300
390
  return {
@@ -316,27 +406,32 @@ ${log}` : ""}`);
316
406
  list: (input) => run(async () => {
317
407
  const result = await trpc.workspace.listWorkspaces.query(input);
318
408
  return {
319
- workspaces: result.workspaces.map((workspace) => normalizeWorkspace(workspace)),
409
+ workspaces: result.workspaces.map(normalizeWorkspace).filter((workspace) => workspace !== null),
320
410
  pagination: result.pagination
321
411
  };
322
412
  }),
323
- get: (workspaceId) => run(async () => {
324
- const result = await trpc.workspace.getWorkspace.query({ workspaceId });
413
+ get: (ref) => run(async () => {
414
+ const result = await trpc.workspace.getWorkspace.query({
415
+ workspaceId: workspaceIdOf(ref)
416
+ });
325
417
  const workspace = normalizeWorkspace(result.workspace);
326
418
  if (!workspace)
327
419
  throw new GittermError("NOT_FOUND", "Workspace not found");
328
420
  return workspace;
329
421
  }),
330
- getRuntimeAccess: (workspaceId) => run(async () => {
331
- const result = await trpc.workspace.getRuntimeAccess.query({ workspaceId });
422
+ getRuntimeAccess: (ref) => run(async () => {
423
+ const result = await trpc.workspace.getRuntimeAccess.query({
424
+ workspaceId: workspaceIdOf(ref)
425
+ });
332
426
  return normalizeRuntime(result);
333
427
  }),
334
- ensureRunning: (workspaceId, options2) => run(async () => {
428
+ ensureRunning: (ref, waitOptions) => run(async () => {
429
+ throwIfAborted(waitOptions?.signal, "ensuring the workspace is running");
335
430
  const result = await trpc.workspace.ensureRunning.mutate({
336
- workspaceId,
337
- timeoutMs: options2?.timeoutMs,
338
- pollIntervalMs: options2?.pollIntervalMs
339
- });
431
+ workspaceId: workspaceIdOf(ref),
432
+ timeoutMs: waitOptions?.timeoutMs,
433
+ pollIntervalMs: waitOptions?.pollIntervalMs
434
+ }, { signal: waitOptions?.signal });
340
435
  const workspace = normalizeWorkspace(result.workspace);
341
436
  if (!workspace)
342
437
  throw new GittermError("SERVER_ERROR", "ensureRunning failed");
@@ -345,43 +440,62 @@ ${log}` : ""}`);
345
440
  runtime: normalizeRuntime(result.runtime)
346
441
  };
347
442
  }),
348
- pause: (workspaceId) => run(async () => {
349
- const result = await trpc.workspace.pauseWorkspace.mutate({ workspaceId });
443
+ pause: (ref) => run(async () => {
444
+ const result = await trpc.workspace.pauseWorkspace.mutate({
445
+ workspaceId: workspaceIdOf(ref)
446
+ });
350
447
  return { durationMinutes: result.durationMinutes };
351
448
  }),
352
- restart: (workspaceId) => run(async () => {
353
- const result = await trpc.workspace.restartWorkspace.mutate({ workspaceId });
449
+ restart: (ref) => run(async () => {
450
+ const result = await trpc.workspace.restartWorkspace.mutate({
451
+ workspaceId: workspaceIdOf(ref)
452
+ });
354
453
  return { status: result.status };
355
454
  }),
356
- terminate: (workspaceId) => run(async () => {
357
- const result = await trpc.workspace.deleteWorkspace.mutate({ workspaceId });
455
+ terminate: (ref) => run(async () => {
456
+ const result = await trpc.workspace.deleteWorkspace.mutate({
457
+ workspaceId: workspaceIdOf(ref)
458
+ });
358
459
  return {
359
460
  workspace: normalizeWorkspace(result.workspace),
360
461
  cleanupInBackground: result.cleanupInBackground
361
462
  };
362
463
  }),
363
464
  create: createWorkspace,
364
- setupStatus: (workspaceId) => run(async () => trpc.workspace.getSetupStatus.query({ workspaceId })),
365
- waitForSetup: (workspaceId, waitOptions) => run(() => waitForWorkspaceSetup(workspaceId, waitOptions))
465
+ setupStatus: (ref) => run(async () => trpc.workspace.getSetupStatus.query({ workspaceId: workspaceIdOf(ref) })),
466
+ waitForSetup: (ref, waitOptions) => run(() => waitForWorkspaceSetup(workspaceIdOf(ref), waitOptions))
366
467
  },
367
468
  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));
469
+ create: (input) => run(async () => {
470
+ const { signal, ...request } = input;
471
+ if (request.waitForSetup) {
472
+ await waitForWorkspaceSetup(request.workspaceId, {
473
+ timeoutMs: request.setupTimeoutMs,
474
+ signal
475
+ });
384
476
  }
477
+ throwIfAborted(signal, "creating the run");
478
+ return trpc.run.create.mutate({ ...request, idempotencyKey: request.idempotencyKey ?? crypto.randomUUID() }, { signal });
479
+ }),
480
+ list: (ref, listOptions) => run(async () => trpc.run.list.query({
481
+ workspaceId: workspaceIdOf(ref),
482
+ ...listOptions
483
+ })),
484
+ get: (refOrWorkspaceId, runId) => run(async () => {
485
+ const target = runTargetOf(refOrWorkspaceId, runId);
486
+ return trpc.run.get.query(target);
487
+ }),
488
+ messages: (refOrWorkspaceId, runId) => run(async () => {
489
+ const target = runTargetOf(refOrWorkspaceId, runId);
490
+ const messages = await trpc.run.messages.query(target);
491
+ return messages.map((message) => ({ ...message, parts: message.parts ?? [] }));
492
+ }),
493
+ cancel: (refOrWorkspaceId, runId) => run(async () => trpc.run.cancel.mutate(runTargetOf(refOrWorkspaceId, runId))),
494
+ respond: (ref, input) => run(async () => trpc.run.respond.mutate({ ...runTargetOf(ref), ...input })),
495
+ wait: (refOrWorkspaceId, runIdOrOptions, maybeOptions) => run(async () => {
496
+ const target = typeof refOrWorkspaceId === "string" ? runTargetOf(refOrWorkspaceId, runIdOrOptions) : runTargetOf(refOrWorkspaceId);
497
+ const waitOptions = typeof refOrWorkspaceId === "string" ? maybeOptions : runIdOrOptions;
498
+ return waitForRun(trpc, target, waitOptions);
385
499
  })
386
500
  },
387
501
  catalog: {
@@ -420,7 +534,7 @@ ${log}` : ""}`);
420
534
  };
421
535
  }
422
536
  // src/device-login.ts
423
- function sleep(ms) {
537
+ function sleep2(ms) {
424
538
  return new Promise((resolve) => setTimeout(resolve, ms));
425
539
  }
426
540
  async function loginWithDeviceCode(serverUrl, options = {}) {
@@ -455,7 +569,7 @@ async function loginWithDeviceCode(serverUrl, options = {}) {
455
569
  const errText = await tokenRes.text().catch(() => "");
456
570
  throw new Error(`Login failed: ${tokenRes.status} ${errText}`);
457
571
  }
458
- await sleep(Math.max(1, codeJson.intervalSeconds) * 1000);
572
+ await sleep2(Math.max(1, codeJson.intervalSeconds) * 1000);
459
573
  }
460
574
  throw new Error("Device code expired; try again.");
461
575
  }
package/dist/types.d.ts CHANGED
@@ -6,6 +6,13 @@ export type AuthStatus = {
6
6
  plan: string;
7
7
  authMethod: "session" | "apiToken";
8
8
  };
9
+ /**
10
+ * `pending` — compute is being provisioned or resumed; `runtime.url` is null.
11
+ * `running` — the provider reports the sandbox/container up. The agent may
12
+ * still be booting; `runs.create()` waits for it to answer.
13
+ * `paused` — stopped but resumable with `workspaces.ensureRunning()`.
14
+ * `terminated` — gone for good.
15
+ */
9
16
  export type WorkspaceStatus = "pending" | "running" | "paused" | "terminated";
10
17
  export type WorkspaceHostingType = "cloud" | "local";
11
18
  export type Workspace = {
@@ -33,12 +40,40 @@ export type Workspace = {
33
40
  name: string;
34
41
  imageId: string;
35
42
  } | null;
43
+ /** Caller-owned tags supplied at create time. */
44
+ metadata: Record<string, string>;
45
+ /** OpenCode HTTP API generation the workspace's agent server speaks. */
46
+ opencodeApi: OpencodeApi;
47
+ /** When set, the workspace is terminated at this time regardless of activity. */
48
+ autoTerminateAt: string | null;
49
+ /** The caller-supplied image or E2B template this workspace runs, if any. */
50
+ customImage: string | null;
36
51
  startedAt: string | null;
37
52
  pausedAt: string | null;
38
53
  terminatedAt: string | null;
39
54
  lastActiveAt: string | null;
40
55
  updatedAt: string | null;
41
56
  };
57
+ /** A workspace id, or any object carrying one (e.g. a `Workspace`). */
58
+ export type WorkspaceRef = string | {
59
+ id: string;
60
+ };
61
+ /** A run id pair, or any object carrying one (e.g. an `AgentRun`). */
62
+ export type RunRef = {
63
+ workspaceId: string;
64
+ id: string;
65
+ } | {
66
+ workspaceId: string;
67
+ runId: string;
68
+ };
69
+ /** `v1` = OpenCode 1.x (`/event`, `/session/*`); `v2` = OpenCode 2 (`/api/*`), experimental until 2.0 ships. */
70
+ export type OpencodeApi = "v1" | "v2";
71
+ export type WaitOptions = {
72
+ timeoutMs?: number;
73
+ pollIntervalMs?: number;
74
+ /** Abort the wait early; the promise rejects with code `ABORTED`. */
75
+ signal?: AbortSignal;
76
+ };
42
77
  export type WorkspaceRuntimeAccess = {
43
78
  workspaceId: string;
44
79
  status: WorkspaceStatus;
@@ -62,6 +97,8 @@ export type WorkspaceListOptions = {
62
97
  limit?: number;
63
98
  offset?: number;
64
99
  status?: "all" | "active" | "terminated";
100
+ /** Only workspaces whose metadata contains every given key/value. */
101
+ metadata?: Record<string, string>;
65
102
  };
66
103
  export type WorkspaceListResult = {
67
104
  workspaces: Workspace[];
@@ -156,6 +193,27 @@ export type AgentKey = BuiltInAgentKey | (string & {});
156
193
  export type WorkspaceCreateInput = {
157
194
  idempotencyKey?: string;
158
195
  name?: string;
196
+ /**
197
+ * Caller-owned tags (e.g. tenant or channel ids). Up to 20 keys of letters,
198
+ * digits, `_ . : -`; values up to 500 chars. Returned on the workspace and
199
+ * filterable with `workspaces.list({ metadata })`.
200
+ */
201
+ metadata?: Record<string, string>;
202
+ /**
203
+ * Terminate automatically this long after creation, whatever its activity.
204
+ * Between 1 minute and 30 days. Use it as a cost guardrail for ephemeral
205
+ * workspaces in case the caller crashes before `terminate()`.
206
+ */
207
+ autoTerminateAfterMs?: number;
208
+ /**
209
+ * Run your own image instead of the catalog image. Registry-backed providers
210
+ * (railway, aws, daytona, exedev) take an OCI reference such as
211
+ * `ghcr.io/acme/agent-runner:1.4.0`; E2B takes a public template id or
212
+ * alias. The managed service only runs images that can be pulled without
213
+ * credentials, and pins floating tags to a digest at create time. Build on
214
+ * `opeoginni/gitterm-opencode-server` and keep its entrypoint.
215
+ */
216
+ image?: string;
159
217
  repo: string;
160
218
  branch?: string;
161
219
  /** Commit SHA to pin the checkout to after cloning `branch`/`checkoutRef`. */
@@ -176,13 +234,10 @@ export type WorkspaceCreateInput = {
176
234
  /** Defaults from the selected provider. */
177
235
  persistent?: boolean;
178
236
  workspaceProfile?: "standard" | "ssh-enabled";
179
- /** Credential IDs from client.credentials.list(). Omit to inject dashboard defaults. */
180
- modelCredentialIds?: string[];
181
237
  /**
182
- * Inline API keys for this workspace only — injected at provision time and
183
- * never stored in the dashboard. An inline key overrides any dashboard
184
- * credential for the same provider. OAuth providers can't be supplied
185
- * inline; connect those in the dashboard.
238
+ * One entry per provider. Omit entirely to inject the dashboard defaults.
239
+ * Naming any dashboard credential (an entry without `apiKey`) turns off the
240
+ * implicit defaults, so list every provider the workspace needs.
186
241
  */
187
242
  modelCredentials?: WorkspaceModelCredentialInput[];
188
243
  /** Ephemeral environment variables injected into this workspace only. */
@@ -218,6 +273,11 @@ export type WorkspaceCreateInput = {
218
273
  * webfetch: "allow" } } disables tool approval prompts in headless runs.
219
274
  */
220
275
  config?: Record<string, unknown>;
276
+ /**
277
+ * OpenCode API generation served by the image. Defaults to `v1`. Use `v2`
278
+ * with an image built on OpenCode 2 (`@opencode-ai/cli`); experimental.
279
+ */
280
+ api?: OpencodeApi;
221
281
  };
222
282
  };
223
283
  export type WorkspaceRestartResult = {
@@ -234,7 +294,54 @@ export type WorkspaceEnsureRunningResult = {
234
294
  workspace: Workspace;
235
295
  runtime: WorkspaceRuntimeAccess;
236
296
  };
237
- export type AgentRunStatus = "pending" | "running" | "retrying" | "completed" | "failed" | "cancelled";
297
+ export type AgentRunStatus = "pending" | "running" | "retrying"
298
+ /** Blocked on a permission prompt or agent question; answer with `runs.respond()`. */
299
+ | "awaiting_input" | "completed" | "failed" | "cancelled";
300
+ /** A prompt the agent is blocked on. `permission` = OpenCode tool approval; `question` = the agent's `question` tool. */
301
+ export type AgentRunInputRequest = {
302
+ id: string;
303
+ kind: "permission";
304
+ createdAt: string;
305
+ toolCallId: string | null;
306
+ /** Permission action, e.g. "bash" or "edit". */
307
+ permission: string;
308
+ /** What the action applies to, e.g. the shell command or file paths. */
309
+ patterns: string[];
310
+ /** Patterns OpenCode remembers when answered with "always". */
311
+ always: string[];
312
+ title: string;
313
+ } | {
314
+ id: string;
315
+ kind: "question";
316
+ createdAt: string;
317
+ toolCallId: string | null;
318
+ questions: Array<{
319
+ key: string;
320
+ header: string;
321
+ question: string;
322
+ options: Array<{
323
+ label: string;
324
+ description: string;
325
+ value?: string;
326
+ }>;
327
+ /** More than one option may be selected. */
328
+ multiple: boolean;
329
+ /** A free-text answer outside `options` is accepted. */
330
+ custom: boolean;
331
+ }>;
332
+ };
333
+ export type AgentRunReply = {
334
+ type: "permission";
335
+ response: "once" | "always" | "reject";
336
+ }
337
+ /** One entry per question, each the selected option labels (or a custom answer when allowed). */
338
+ | {
339
+ type: "question";
340
+ answers: string[][];
341
+ } | {
342
+ type: "question";
343
+ reject: true;
344
+ };
238
345
  export type AgentRun = {
239
346
  id: string;
240
347
  workspaceId: string;
@@ -242,17 +349,43 @@ export type AgentRun = {
242
349
  status: AgentRunStatus;
243
350
  error: string | null;
244
351
  finalText: string | null;
352
+ /** Non-empty exactly while `status` is `awaiting_input`. */
353
+ pendingInputs: AgentRunInputRequest[];
245
354
  context: {
246
355
  type: "isolated";
247
356
  } | {
248
357
  type: "continued";
249
358
  runId: string;
250
359
  };
360
+ createdAt: string;
361
+ /** When the prompt reached the agent; null while `pending`. */
362
+ submittedAt: string | null;
363
+ completedAt: string | null;
364
+ };
365
+ export type AgentRunListOptions = {
366
+ /** `active` = pending/running/retrying/awaiting_input; `terminal` = completed/failed/cancelled. */
367
+ status?: "all" | "active" | "terminal";
368
+ /** 1–50, default 20. */
369
+ limit?: number;
370
+ offset?: number;
371
+ };
372
+ export type AgentRunListResult = {
373
+ runs: AgentRun[];
374
+ pagination: {
375
+ total: number;
376
+ limit: number;
377
+ offset: number;
378
+ hasMore: boolean;
379
+ };
251
380
  };
252
381
  export type AgentRunCreateInput = {
253
382
  workspaceId: string;
254
- /** Stable key used to return the same run when a request is retried. */
255
- idempotencyKey: string;
383
+ /**
384
+ * Stable key used to return the same run when a request is retried.
385
+ * Defaults to a random UUID, which gives no retry protection; pass your own
386
+ * for anything a retry could duplicate.
387
+ */
388
+ idempotencyKey?: string;
256
389
  prompt: string;
257
390
  title?: string;
258
391
  agent?: string;
@@ -265,20 +398,67 @@ export type AgentRunCreateInput = {
265
398
  type: "continue";
266
399
  runId: string;
267
400
  };
268
- /** Wait for workspace setup commands before submitting the prompt. */
401
+ /**
402
+ * Wait for the `afterAgent` setup phase to succeed before submitting the
403
+ * prompt. Fails the call with the setup log if that phase failed.
404
+ */
269
405
  waitForSetup?: boolean;
270
406
  /** How long to wait for setup, in ms. Server maximum is 600000 (10 minutes). */
271
407
  setupTimeoutMs?: number;
408
+ /**
409
+ * How long to wait for a `pending` workspace to become `running`, in ms.
410
+ * Defaults to 120000; server maximum is 240000. Paused workspaces are not
411
+ * resumed — call `workspaces.ensureRunning()` first.
412
+ */
413
+ startTimeoutMs?: number;
414
+ /** Abort while waiting for setup or start; rejects with code `ABORTED`. */
415
+ signal?: AbortSignal;
416
+ };
417
+ export type AgentRunMessagePart = {
418
+ type: "text";
419
+ text: string;
420
+ } | {
421
+ type: "tool";
422
+ callId: string;
423
+ tool: string;
424
+ status: "pending" | "running" | "completed" | "error";
425
+ title: string | null;
426
+ input: Record<string, unknown>;
427
+ /** Tool output when completed, truncated to 4000 characters. */
428
+ output: string | null;
429
+ error: string | null;
430
+ startedAt: string | null;
431
+ completedAt: string | null;
432
+ };
433
+ export type RunWaitOptions = {
434
+ /** Default 30 minutes. */
435
+ timeoutMs?: number;
436
+ /** Abort the wait early; the promise rejects with code `ABORTED`. */
437
+ signal?: AbortSignal;
438
+ /**
439
+ * `input-or-terminal` (default) also returns when the run is `awaiting_input`
440
+ * so the caller can answer with `runs.respond()`; `terminal` keeps waiting.
441
+ */
442
+ until?: "input-or-terminal" | "terminal";
272
443
  };
273
444
  export type AgentRunMessage = {
274
445
  id: string;
275
446
  role: "user" | "assistant";
276
447
  createdAt: string;
277
448
  completedAt: string | null;
449
+ /** Concatenated text parts. */
278
450
  text: string;
279
451
  error: string | null;
452
+ /** Ordered text and tool-call parts, for seeing what the agent actually did. */
453
+ parts: AgentRunMessagePart[];
280
454
  };
281
455
  export type WorkspaceSetupStatus = {
456
+ /**
457
+ * `not_requested` — no `afterAgent` commands. `waiting` — the agent isn't
458
+ * reachable yet, so setup hasn't started. `running`, `succeeded`, `failed`
459
+ * describe the `afterAgent` phase itself. A setup stuck in `waiting` for
460
+ * 15 minutes is marked `failed` with the reason in `log`.
461
+ */
282
462
  status: "not_requested" | "waiting" | "running" | "succeeded" | "failed";
283
463
  exitCode: number | null;
284
464
  startedAt: string | null;
@@ -298,13 +478,26 @@ export type ModelProviderInfo = {
298
478
  isRecommended: boolean;
299
479
  };
300
480
  /**
301
- * An API key passed directly to workspaces.create(). `providerName` must be an
302
- * API-key provider from credentials.listProviders(), e.g. "anthropic" or
303
- * "openai"; unknown or OAuth-only providers throw MODEL_CREDENTIAL_INVALID.
481
+ * Selects the credential for one provider on workspaces.create().
482
+ * `providerName` comes from credentials.listProviders(), e.g. "anthropic" or
483
+ * "openai"; unknown providers throw MODEL_CREDENTIAL_INVALID.
484
+ *
485
+ * - `{ providerName, apiKey }` — inline key for this workspace only, never
486
+ * stored. Overrides any dashboard credential for the same provider.
487
+ * OAuth-only providers throw MODEL_CREDENTIAL_INVALID.
488
+ * - `{ providerName, label }` — the dashboard credential with that label.
489
+ * Unknown labels throw MODEL_CREDENTIAL_UNAVAILABLE listing the labels
490
+ * that exist.
491
+ * - `{ providerName }` — that provider's dashboard default credential.
304
492
  */
305
493
  export type WorkspaceModelCredentialInput = {
306
494
  providerName: string;
307
495
  apiKey: string;
496
+ label?: never;
497
+ } | {
498
+ providerName: string;
499
+ label?: string;
500
+ apiKey?: never;
308
501
  };
309
502
  /** Safe dashboard credential metadata. Secret material is never returned by the SDK. */
310
503
  export type ModelCredential = {
@@ -314,7 +507,8 @@ export type ModelCredential = {
314
507
  providerDisplayName: string;
315
508
  logicalProviderKey: string;
316
509
  authType: string;
317
- label: string | null;
510
+ /** Unique per provider; use it in `modelCredentials: [{ providerName, label }]`. */
511
+ label: string;
318
512
  keyHash: string;
319
513
  isActive: boolean;
320
514
  isDefault: boolean;
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@gitterm/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
7
7
  "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
8
10
  "exports": {
9
11
  ".": {
10
12
  "types": "./dist/index.d.ts",
@@ -23,7 +25,7 @@
23
25
  "access": "public"
24
26
  },
25
27
  "scripts": {
26
- "build": "rm -rf dist && bun build src/index.ts src/direct/index.ts --outdir dist --target node --format esm --external @trpc/client --external @opencode-ai/sdk --external @opencode-ai/sdk/v2 --external e2b --external @asciidev/box-sdk --external @daytonaio/sdk --external @vercel/sandbox && tsc -p tsconfig.build.json",
28
+ "build": "rm -rf dist && bun build src/index.ts src/direct/index.ts --outdir dist --target node --format esm --external @trpc/client --external eventsource --external @opencode-ai/sdk --external @opencode-ai/sdk/v2 --external e2b --external @asciidev/box-sdk --external @daytonaio/sdk --external @vercel/sandbox && tsc -p tsconfig.build.json",
27
29
  "prepublishOnly": "bun run check-types && bun run build",
28
30
  "check-types": "tsc --noEmit"
29
31
  },
@@ -33,7 +35,8 @@
33
35
  "@opencode-ai/sdk": "^1.18.25",
34
36
  "@trpc/client": "^11.8.1",
35
37
  "@vercel/sandbox": "^3.2.1",
36
- "e2b": "^2.46.1"
38
+ "e2b": "^2.46.1",
39
+ "eventsource": "^5.1.1"
37
40
  },
38
41
  "devDependencies": {
39
42
  "@gitterm/api": "workspace:*",
@@ -42,6 +45,6 @@
42
45
  "typescript": "^5.8.2"
43
46
  },
44
47
  "engines": {
45
- "node": ">=20.18.1"
48
+ "node": ">=22.12.0"
46
49
  }
47
50
  }