@gitterm/sdk 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1802 @@
1
+ // src/direct/client.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { createOpencodeClient } from "@opencode-ai/sdk";
4
+ import {
5
+ createOpencodeClient as createOpencodeV2Client
6
+ } from "@opencode-ai/sdk/v2";
7
+
8
+ // src/direct/ascii.ts
9
+ import { BoxApi, Configuration, waitUntilReady } from "@asciidev/box-sdk";
10
+
11
+ // src/direct/provisioning.ts
12
+ var DIRECT_OPENCODE_PORT = 4096;
13
+ var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
14
+ var DIRECT_OPENCODE_SERVER_IMAGE = "opeoginni/gitterm-opencode-server:latest";
15
+ var DIRECT_E2B_TEMPLATES = {
16
+ standard: "gitterm-opencode-server",
17
+ large: "gitterm-opencode-server-lg"
18
+ };
19
+ function resolveDirectImage(image) {
20
+ return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
21
+ }
22
+ function directModelAuth(credential) {
23
+ if (credential.type === "oauth") {
24
+ if (!credential.refreshToken.trim())
25
+ throw new Error("OAuth refreshToken is required");
26
+ if (credential.expiresAt != null && (!Number.isFinite(credential.expiresAt) || credential.expiresAt < 0)) {
27
+ throw new Error("OAuth expiresAt must be a non-negative Unix epoch time in milliseconds");
28
+ }
29
+ return {
30
+ type: "oauth",
31
+ refresh: credential.refreshToken,
32
+ access: credential.accessToken ?? "",
33
+ expires: credential.expiresAt ?? 0,
34
+ ...credential.accountId ? { accountId: credential.accountId } : {},
35
+ ...credential.enterpriseUrl ? { enterpriseUrl: credential.enterpriseUrl } : {}
36
+ };
37
+ }
38
+ if (!credential.apiKey.trim())
39
+ throw new Error("Model credential apiKey is required");
40
+ return {
41
+ type: "api",
42
+ key: credential.apiKey,
43
+ ...credential.metadata ? { metadata: credential.metadata } : {}
44
+ };
45
+ }
46
+ function base64(value) {
47
+ return Buffer.from(value).toString("base64");
48
+ }
49
+ function validateName(value, kind) {
50
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) || value === "." || value === "..") {
51
+ throw new Error(`Invalid ${kind}: ${value}`);
52
+ }
53
+ return value;
54
+ }
55
+ function repositoryName(url) {
56
+ let pathname;
57
+ try {
58
+ pathname = new URL(url).pathname;
59
+ } catch {
60
+ pathname = url;
61
+ }
62
+ const name = pathname.replace(/\/$/, "").split("/").at(-1)?.replace(/\.git$/, "") ?? "";
63
+ return validateName(name, "repository name");
64
+ }
65
+ function buildDirectProvisioningPlan(input) {
66
+ if (!input.repo) {
67
+ const repositoryOnly = [
68
+ input.branch ? "branch" : undefined,
69
+ input.checkoutRef ? "checkoutRef" : undefined,
70
+ input.baseCommit ? "baseCommit" : undefined,
71
+ input.repositoryCredentials ? "repositoryCredentials" : undefined
72
+ ].filter(Boolean);
73
+ if (repositoryOnly.length) {
74
+ throw new Error(`${repositoryOnly.join(", ")} require repo`);
75
+ }
76
+ }
77
+ const credentials = new Map;
78
+ for (const credential of input.modelCredentials ?? []) {
79
+ const providerName = credential.providerName.trim();
80
+ if (!providerName)
81
+ throw new Error("Model credential providerName is required");
82
+ if (credentials.has(providerName)) {
83
+ throw new Error(`Duplicate model credential: ${providerName}`);
84
+ }
85
+ credentials.set(providerName, directModelAuth(credential));
86
+ }
87
+ const configuredPlugins = Array.isArray(input.opencode?.config?.plugin) ? input.opencode.config.plugin.filter((plugin) => typeof plugin === "string") : [];
88
+ const plugins = [...new Set([...configuredPlugins, ...input.opencode?.plugins ?? []])];
89
+ const environmentVariables = { ...input.environmentVariables };
90
+ delete environmentVariables.OPENCODE_SERVER_USERNAME;
91
+ delete environmentVariables.GITTERM_DIRECT_PROVIDER;
92
+ const config = {
93
+ $schema: "https://opencode.ai/config.json",
94
+ ...input.opencode?.config,
95
+ username: "Gitterm direct",
96
+ ...plugins.length ? { plugin: plugins } : {}
97
+ };
98
+ const files = [
99
+ {
100
+ path: "~/.local/share/opencode/auth.json",
101
+ contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
102
+ },
103
+ {
104
+ path: "~/.config/opencode/opencode.json",
105
+ contentBase64: base64(JSON.stringify(config))
106
+ },
107
+ {
108
+ path: "~/.config/opencode/AGENTS.md",
109
+ contentBase64: base64("You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.")
110
+ },
111
+ ...(input.opencode?.skills ?? []).map((skill) => ({
112
+ path: `~/.config/opencode/skills/${validateName(skill.name, "skill name")}/SKILL.md`,
113
+ contentBase64: base64(skill.content)
114
+ }))
115
+ ];
116
+ return {
117
+ workspaceId: input.id,
118
+ lifecycle: input.lifecycle,
119
+ repository: input.repo ? {
120
+ url: input.repo,
121
+ name: repositoryName(input.repo),
122
+ branch: input.branch,
123
+ checkoutRef: input.checkoutRef,
124
+ baseCommit: input.baseCommit,
125
+ authUsername: input.repositoryCredentials?.username,
126
+ authToken: input.repositoryCredentials?.token
127
+ } : undefined,
128
+ agent: {
129
+ files,
130
+ environmentVariables: {
131
+ ...environmentVariables,
132
+ OPENCODE_SERVER_PASSWORD: input.password
133
+ },
134
+ command: DIRECT_OPENCODE_COMMAND,
135
+ port: DIRECT_OPENCODE_PORT
136
+ },
137
+ setupCommands: input.setupCommands ?? []
138
+ };
139
+ }
140
+ function railwayContainerEnvironment(plan) {
141
+ const repository = plan.repository;
142
+ return {
143
+ ...plan.agent.environmentVariables,
144
+ ...repository ? {
145
+ REPO_URL: repository.url,
146
+ REPO_NAME: repository.name,
147
+ ...repository.branch ? { REPO_BRANCH: repository.branch } : {},
148
+ ...repository.checkoutRef ? { REPO_CHECKOUT_REF: repository.checkoutRef } : {},
149
+ ...repository.baseCommit ? { REPO_BASE_COMMIT: repository.baseCommit } : {},
150
+ ...repository.authToken ? {
151
+ GITTERM_GIT_USERNAME: repository.authUsername ?? "x-access-token",
152
+ GITTERM_GIT_TOKEN: repository.authToken
153
+ } : {}
154
+ } : {},
155
+ AGENT_FILES_BASE64: base64(JSON.stringify(plan.agent.files)),
156
+ ...plan.setupCommands.length ? { WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(plan.setupCommands)) } : {},
157
+ GITTERM_DIRECT_PROVIDER: "railway"
158
+ };
159
+ }
160
+ function setupCommandScript(commands) {
161
+ return ["set -eu", ...commands].join(`
162
+ `);
163
+ }
164
+ function shellQuote(value) {
165
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
166
+ }
167
+ function cloneRepositoryScript(repository, directory) {
168
+ const ref = repository.checkoutRef ?? repository.branch;
169
+ const commands = [
170
+ `mkdir -p ${shellQuote(directory)}`,
171
+ ...repository.authToken ? [
172
+ "mkdir -p /tmp/gitterm",
173
+ `printf %s ${shellQuote(repository.authToken)} > /tmp/gitterm/git-token`,
174
+ `printf %s ${shellQuote(repository.authUsername ?? "x-access-token")} > /tmp/gitterm/git-username`,
175
+ "chmod 600 /tmp/gitterm/git-token /tmp/gitterm/git-username",
176
+ `git config --global credential.helper ${shellQuote('!f() { [ "$1" = get ] || exit 0; printf "%s\\n" "username=$(cat /tmp/gitterm/git-username)" "password=$(cat /tmp/gitterm/git-token)"; }; f')}`
177
+ ] : [],
178
+ `GIT_TERMINAL_PROMPT=0 git clone ${ref ? `--branch ${shellQuote(ref)} ` : ""}${shellQuote(repository.url)} ${shellQuote(directory)}`,
179
+ ...repository.baseCommit ? [
180
+ `GIT_TERMINAL_PROMPT=0 git -C ${shellQuote(directory)} fetch --depth 1 origin ${shellQuote(repository.baseCommit)}`,
181
+ `git -C ${shellQuote(directory)} checkout --detach ${shellQuote(repository.baseCommit)}`
182
+ ] : [],
183
+ ...repository.authToken ? ["git config --global --unset-all credential.helper || true", "rm -rf /tmp/gitterm"] : []
184
+ ];
185
+ return setupCommandScript(commands);
186
+ }
187
+ async function pinFloatingDockerImage(image) {
188
+ if (image.includes("@sha256:"))
189
+ return image;
190
+ const slash = image.lastIndexOf("/");
191
+ const colon = image.lastIndexOf(":");
192
+ const name = colon > slash ? image.slice(0, colon) : image;
193
+ const tag = colon > slash ? image.slice(colon + 1) : "latest";
194
+ if (!["latest", "lts", "stable"].includes(tag.toLowerCase()))
195
+ return image;
196
+ if (name.includes("/") && name.split("/").length !== 2)
197
+ return image;
198
+ const response = await fetch(`https://hub.docker.com/v2/repositories/${name}/tags/${encodeURIComponent(tag)}`);
199
+ if (!response.ok) {
200
+ throw new Error(`Could not resolve a digest for Docker image ${image} (${response.status})`);
201
+ }
202
+ const body = await response.json();
203
+ const digest = body.digest || body.images?.find((entry) => entry.digest)?.digest;
204
+ if (!digest?.startsWith("sha256:")) {
205
+ throw new Error(`Docker Hub did not return a digest for ${image}`);
206
+ }
207
+ return `${name}@${digest}`;
208
+ }
209
+ function basicAuthHeader(password) {
210
+ return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
211
+ }
212
+ async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
213
+ const deadline = Date.now() + timeoutMs;
214
+ while (Date.now() < deadline) {
215
+ const controller = new AbortController;
216
+ const timer = setTimeout(() => controller.abort(), 5000);
217
+ const ready = await fetch(runtime.url, {
218
+ headers: {
219
+ ...runtime.headers,
220
+ ...runtime.password ? { Authorization: basicAuthHeader(runtime.password) } : {}
221
+ },
222
+ signal: controller.signal
223
+ }).then((response) => response.ok).catch(() => false).finally(() => clearTimeout(timer));
224
+ if (ready)
225
+ return;
226
+ await new Promise((resolve) => setTimeout(resolve, 500));
227
+ }
228
+ throw new Error("Timed out waiting for the OpenCode runtime");
229
+ }
230
+
231
+ // src/direct/ascii.ts
232
+ var HOME = "/home/user";
233
+ function serializeHandle(handle) {
234
+ return JSON.stringify(handle);
235
+ }
236
+ function parseHandle(value) {
237
+ try {
238
+ const handle = JSON.parse(value);
239
+ if (!handle.boxId || !handle.repoDir || !handle.serve?.command || !handle.serve.port) {
240
+ throw new Error("missing required fields");
241
+ }
242
+ return handle;
243
+ } catch {
244
+ throw new Error("Invalid Ascii Box direct workspace handle");
245
+ }
246
+ }
247
+ function createAsciiDirectProvider(config) {
248
+ if (!config.apiKey.trim())
249
+ throw new Error("Ascii apiKey is required");
250
+ const client = new BoxApi(new Configuration({
251
+ basePath: "https://ascii.dev/api/box/v1",
252
+ accessToken: config.apiKey
253
+ }));
254
+ async function runCommand(boxId, command, cwd, timeoutSeconds = 60) {
255
+ const result = await client.command({
256
+ boxId,
257
+ commandRequest: { command, cwd, timeoutSeconds }
258
+ });
259
+ if (result.type !== "command.finished") {
260
+ throw new Error("Ascii Box command did not finish");
261
+ }
262
+ if (!result.success || result.exitCode !== 0) {
263
+ throw new Error(`Ascii Box command failed: ${result.stderr || result.stdout}`);
264
+ }
265
+ return result.stdout;
266
+ }
267
+ async function startRuntime(handle) {
268
+ await runCommand(handle.boxId, `nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null &`, handle.repoDir);
269
+ }
270
+ async function getPublicUrl(handle) {
271
+ await runCommand(handle.boxId, `host ${handle.serve.port}`);
272
+ const output = await runCommand(handle.boxId, `host url ${handle.serve.port}`);
273
+ const value = output.match(/https:\/\/\S+/)?.[0];
274
+ if (!value) {
275
+ throw new Error(`Ascii Box did not return a hosted URL for port ${handle.serve.port}`);
276
+ }
277
+ const hosted = new URL(value);
278
+ hosted.search = "";
279
+ return hosted.toString();
280
+ }
281
+ async function deleteBox(boxId) {
282
+ const response = await fetch(`https://ascii.dev/api/box/v1/boxes/${boxId}`, {
283
+ method: "DELETE",
284
+ headers: { Authorization: `Bearer ${config.apiKey}` }
285
+ });
286
+ if (!response.ok && response.status !== 404) {
287
+ throw new Error(`Ascii Box deletion failed (${response.status})`);
288
+ }
289
+ }
290
+ return {
291
+ name: "ascii",
292
+ capabilities: {
293
+ persistence: "supported",
294
+ recommendedLifecycle: "ephemeral",
295
+ supportsPause: true,
296
+ ephemeralPause: "stateful",
297
+ supportsKeepAlive: true
298
+ },
299
+ async create(input) {
300
+ const plan = input.provisioning;
301
+ const directory = `${HOME}/${plan.repository?.name ?? "workspace"}`;
302
+ const created = await client.create({
303
+ createBoxRequest: {
304
+ type: config.size,
305
+ ttlSeconds: config.timeoutMs === null ? null : Math.ceil((config.timeoutMs ?? 10 * 60000) / 1000),
306
+ noEnv: true,
307
+ env: plan.agent.environmentVariables
308
+ }
309
+ });
310
+ const handle = {
311
+ boxId: created.box.id,
312
+ repoDir: directory,
313
+ serve: { command: plan.agent.command, port: plan.agent.port }
314
+ };
315
+ try {
316
+ await waitUntilReady(client, handle.boxId);
317
+ await client.update({
318
+ boxId: handle.boxId,
319
+ updateBoxRequest: { name: `gitterm-${input.id}` }
320
+ });
321
+ await runCommand(handle.boxId, `mkdir -p ${shellQuote(directory)}`);
322
+ for (const command of config.runtimeSetupCommands ?? [
323
+ "npm install -g opencode-ai --no-audit --fund=false"
324
+ ]) {
325
+ await runCommand(handle.boxId, command, undefined, 600);
326
+ }
327
+ if (plan.repository) {
328
+ await runCommand(handle.boxId, cloneRepositoryScript(plan.repository, directory), "/", 600);
329
+ }
330
+ for (const file of plan.agent.files) {
331
+ const path = file.path.startsWith("~/") ? file.path.slice(2) : file.path;
332
+ await client.writeFile({
333
+ boxId: handle.boxId,
334
+ fileWriteRequest: { path, content: file.contentBase64, encoding: "base64" }
335
+ });
336
+ }
337
+ if (plan.setupCommands.length) {
338
+ await runCommand(handle.boxId, setupCommandScript(plan.setupCommands), directory, 600);
339
+ }
340
+ await startRuntime(handle);
341
+ const runtime = {
342
+ url: await getPublicUrl(handle),
343
+ directory,
344
+ password: input.password
345
+ };
346
+ await waitForDirectRuntime(runtime);
347
+ return { externalId: serializeHandle(handle), runtime };
348
+ } catch (error) {
349
+ await deleteBox(handle.boxId).catch(() => {
350
+ return;
351
+ });
352
+ throw error;
353
+ }
354
+ },
355
+ async status(workspace) {
356
+ try {
357
+ const box = (await client.get({ boxId: parseHandle(workspace.externalId).boxId })).box;
358
+ if (["ready", "idle", "running"].includes(box.state))
359
+ return "running";
360
+ if (["init", "provisioning", "provisioned", "cloning", "archiving"].includes(box.state)) {
361
+ return "pending";
362
+ }
363
+ if (box.state === "archived")
364
+ return "paused";
365
+ return "terminated";
366
+ } catch (error) {
367
+ if (error instanceof Error && /not found|404|does not exist/i.test(error.message)) {
368
+ return "terminated";
369
+ }
370
+ throw error;
371
+ }
372
+ },
373
+ async pause(workspace) {
374
+ await client.stop({ boxId: parseHandle(workspace.externalId).boxId });
375
+ },
376
+ async resume(workspace) {
377
+ const handle = parseHandle(workspace.externalId);
378
+ await client.resume({ boxId: handle.boxId, resumeRequest: { noEnv: true } });
379
+ await waitUntilReady(client, handle.boxId);
380
+ await startRuntime(handle);
381
+ const runtime = {
382
+ ...workspace.runtime,
383
+ url: await getPublicUrl(handle),
384
+ headers: undefined
385
+ };
386
+ await waitForDirectRuntime(runtime);
387
+ return runtime;
388
+ },
389
+ async terminate(workspace) {
390
+ await deleteBox(parseHandle(workspace.externalId).boxId);
391
+ },
392
+ async keepAlive(workspace, timeoutMs) {
393
+ await client.update({
394
+ boxId: parseHandle(workspace.externalId).boxId,
395
+ updateBoxRequest: { ttlSeconds: Math.ceil(timeoutMs / 1000) }
396
+ });
397
+ }
398
+ };
399
+ }
400
+
401
+ // src/direct/daytona.ts
402
+ import { Daytona, Image } from "@daytonaio/sdk";
403
+ var WORKSPACE_ROOT = "/workspace";
404
+ var AGENT_SESSION_ID = "gitterm-direct-agent";
405
+ function parseHandle2(externalId) {
406
+ const handle = JSON.parse(externalId);
407
+ if (typeof handle.id !== "string" || typeof handle.directory !== "string" || typeof handle.command !== "string" || typeof handle.port !== "number") {
408
+ throw new Error("Invalid Daytona direct workspace handle");
409
+ }
410
+ return handle;
411
+ }
412
+ function createDaytonaDirectProvider(config) {
413
+ if (!config.apiKey.trim())
414
+ throw new Error("Daytona apiKey is required");
415
+ if (!config.target.trim())
416
+ throw new Error("Daytona target is required");
417
+ const client = () => new Daytona({ apiKey: config.apiKey, target: config.target });
418
+ const getSandbox = async (externalId) => {
419
+ const handle = parseHandle2(externalId);
420
+ return { handle, sandbox: await client().get(handle.id) };
421
+ };
422
+ const execute = async (sandbox, command, cwd) => {
423
+ const result = await sandbox.process.executeCommand(command, cwd);
424
+ if (result.exitCode !== 0) {
425
+ throw new Error(`Daytona command failed with exit code ${result.exitCode}: ${result.result ?? command}`);
426
+ }
427
+ };
428
+ const startAgent = async (sandbox, handle) => {
429
+ await sandbox.process.createSession(AGENT_SESSION_ID).catch(() => {
430
+ return;
431
+ });
432
+ await sandbox.process.executeSessionCommand(AGENT_SESSION_ID, {
433
+ command: `cd ${shellQuote(handle.directory)}`
434
+ });
435
+ await sandbox.process.executeSessionCommand(AGENT_SESSION_ID, {
436
+ command: `${handle.command} > /tmp/gitterm-agent.log 2>&1`,
437
+ runAsync: true
438
+ });
439
+ };
440
+ const runtimeFor = async (sandbox, handle, password) => {
441
+ const preview = await sandbox.getPreviewLink(handle.port);
442
+ return {
443
+ url: preview.url,
444
+ directory: handle.directory,
445
+ password,
446
+ ...preview.token ? {
447
+ headers: {
448
+ "x-daytona-preview-token": preview.token,
449
+ "X-Daytona-Skip-Preview-Warning": "true"
450
+ }
451
+ } : {}
452
+ };
453
+ };
454
+ return {
455
+ name: "daytona",
456
+ capabilities: {
457
+ persistence: "supported",
458
+ recommendedLifecycle: "persistent",
459
+ supportsPause: true,
460
+ ephemeralPause: "stateful",
461
+ supportsKeepAlive: true
462
+ },
463
+ async create(input) {
464
+ const plan = input.provisioning;
465
+ const directory = `${WORKSPACE_ROOT}/${plan.repository?.name ?? "workspace"}`;
466
+ const handle = {
467
+ id: "",
468
+ directory,
469
+ command: plan.agent.command,
470
+ port: plan.agent.port
471
+ };
472
+ const common = {
473
+ name: `gitterm-${input.id}`,
474
+ envVars: plan.agent.environmentVariables,
475
+ labels: { gitterm_workspace: input.id },
476
+ autoDeleteInterval: input.lifecycle === "ephemeral" ? 0 : -1
477
+ };
478
+ const sandbox = await client().create({
479
+ ...common,
480
+ image: Image.base(await pinFloatingDockerImage(resolveDirectImage(config.image))).entrypoint(["sleep", "infinity"]),
481
+ resources: {
482
+ ...config.cpu != null ? { cpu: config.cpu } : {},
483
+ ...config.memory != null ? { memory: config.memory } : {},
484
+ ...config.disk != null ? { disk: config.disk } : {}
485
+ }
486
+ }, { timeout: 210 });
487
+ handle.id = sandbox.id;
488
+ try {
489
+ await execute(sandbox, `mkdir -p ${shellQuote(directory)}`);
490
+ if (plan.repository) {
491
+ const repository = plan.repository;
492
+ const url = repository.url.endsWith(".git") ? repository.url : `${repository.url}.git`;
493
+ await sandbox.git.clone(url, directory, repository.checkoutRef ?? repository.branch, undefined, repository.authToken ? repository.authUsername ?? "x-access-token" : undefined, repository.authToken);
494
+ if (repository.baseCommit) {
495
+ await execute(sandbox, `git fetch --depth 1 origin ${shellQuote(repository.baseCommit)} && git checkout --detach ${shellQuote(repository.baseCommit)}`, directory);
496
+ }
497
+ }
498
+ const home = await sandbox.getUserHomeDir() ?? "/home/daytona";
499
+ for (const file of plan.agent.files) {
500
+ const target = file.path.replace(/^~/, home);
501
+ const parent = target.slice(0, target.lastIndexOf("/"));
502
+ await execute(sandbox, `mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(target)}`);
503
+ }
504
+ if (plan.setupCommands.length) {
505
+ await execute(sandbox, setupCommandScript(plan.setupCommands), directory);
506
+ }
507
+ await startAgent(sandbox, handle);
508
+ const runtime = await runtimeFor(sandbox, handle, input.password);
509
+ await waitForDirectRuntime(runtime);
510
+ return { externalId: JSON.stringify(handle), runtime };
511
+ } catch (error) {
512
+ await sandbox.delete().catch(() => {
513
+ return;
514
+ });
515
+ throw error;
516
+ }
517
+ },
518
+ async status(workspace) {
519
+ try {
520
+ const { sandbox } = await getSandbox(workspace.externalId);
521
+ await sandbox.refreshData();
522
+ switch (sandbox.state) {
523
+ case "started":
524
+ return "running";
525
+ case "paused":
526
+ case "stopped":
527
+ case "archived":
528
+ return "paused";
529
+ case "error":
530
+ case "build_failed":
531
+ return "failed";
532
+ case "destroyed":
533
+ case "destroying":
534
+ return "terminated";
535
+ default:
536
+ return "pending";
537
+ }
538
+ } catch (error) {
539
+ if (error instanceof Error && /not found|404|does not exist/i.test(error.message)) {
540
+ return "terminated";
541
+ }
542
+ throw error;
543
+ }
544
+ },
545
+ async pause(workspace) {
546
+ const { sandbox } = await getSandbox(workspace.externalId);
547
+ await sandbox.refreshData();
548
+ if (sandbox.state !== "stopped")
549
+ await sandbox.stop();
550
+ },
551
+ async resume(workspace) {
552
+ const { handle, sandbox } = await getSandbox(workspace.externalId);
553
+ await sandbox.refreshData();
554
+ const previousState = sandbox.state;
555
+ if (previousState !== "started")
556
+ await sandbox.start();
557
+ if (previousState !== "started") {
558
+ await startAgent(sandbox, handle);
559
+ }
560
+ const runtime = await runtimeFor(sandbox, handle, workspace.runtime.password);
561
+ await waitForDirectRuntime(runtime);
562
+ return runtime;
563
+ },
564
+ async terminate(workspace) {
565
+ const { sandbox } = await getSandbox(workspace.externalId);
566
+ await sandbox.delete();
567
+ },
568
+ async keepAlive(workspace, timeoutMs) {
569
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
570
+ throw new Error("Daytona keep-alive timeout must be positive");
571
+ }
572
+ const { sandbox } = await getSandbox(workspace.externalId);
573
+ await sandbox.setAutostopInterval(Math.max(1, Math.ceil(timeoutMs / 60000)));
574
+ await sandbox.refreshActivity();
575
+ }
576
+ };
577
+ }
578
+
579
+ // src/direct/e2b.ts
580
+ import { Sandbox } from "e2b";
581
+ var WORKSPACE_ROOT2 = "/home/user/workspace";
582
+ function createE2BDirectProvider(config) {
583
+ if (!config.apiKey.trim())
584
+ throw new Error("E2B apiKey is required");
585
+ const size = config.size ?? "standard";
586
+ if (size !== "standard" && size !== "large") {
587
+ throw new Error("E2B size must be standard or large");
588
+ }
589
+ const templateId = config.templateId?.trim() || DIRECT_E2B_TEMPLATES[size];
590
+ const connect = (externalId) => Sandbox.connect(externalId, { apiKey: config.apiKey });
591
+ return {
592
+ name: "e2b",
593
+ capabilities: {
594
+ persistence: "supported",
595
+ recommendedLifecycle: "ephemeral",
596
+ supportsPause: true,
597
+ ephemeralPause: "stateful",
598
+ supportsKeepAlive: true
599
+ },
600
+ async create(input) {
601
+ const plan = input.provisioning;
602
+ const sandbox = await Sandbox.create(templateId, {
603
+ apiKey: config.apiKey,
604
+ timeoutMs: config.timeoutMs ?? 10 * 60000,
605
+ lifecycle: { onTimeout: input.lifecycle === "persistent" ? "pause" : "kill" },
606
+ network: { allowPublicTraffic: false },
607
+ envs: plan.agent.environmentVariables
608
+ });
609
+ const directory = `${WORKSPACE_ROOT2}/${plan.repository?.name ?? "workspace"}`;
610
+ try {
611
+ await sandbox.commands.run(`mkdir -p ${shellQuote(WORKSPACE_ROOT2)}`);
612
+ if (plan.repository) {
613
+ await sandbox.git.clone(plan.repository.url, {
614
+ path: directory,
615
+ branch: plan.repository.checkoutRef ?? plan.repository.branch,
616
+ username: plan.repository.authToken ? plan.repository.authUsername ?? "x-access-token" : undefined,
617
+ password: plan.repository.authToken
618
+ });
619
+ } else {
620
+ await sandbox.commands.run(`mkdir -p ${shellQuote(directory)}`);
621
+ }
622
+ if (plan.repository?.baseCommit) {
623
+ await sandbox.commands.run(`git -C ${shellQuote(directory)} fetch --depth 1 origin ${shellQuote(plan.repository.baseCommit)} && git -C ${shellQuote(directory)} checkout --detach ${shellQuote(plan.repository.baseCommit)}`);
624
+ }
625
+ for (const file of plan.agent.files) {
626
+ const path = file.path.replace(/^~/, "/home/user");
627
+ const parent = path.slice(0, path.lastIndexOf("/"));
628
+ await sandbox.commands.run(`mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
629
+ }
630
+ if (plan.setupCommands.length) {
631
+ await sandbox.commands.run(setupCommandScript(plan.setupCommands), { cwd: directory });
632
+ }
633
+ await sandbox.commands.run(plan.agent.command, {
634
+ cwd: directory,
635
+ background: true,
636
+ envs: plan.agent.environmentVariables
637
+ });
638
+ const token = sandbox.trafficAccessToken;
639
+ if (!token)
640
+ throw new Error("E2B traffic access token is missing");
641
+ const runtime = {
642
+ url: `https://${sandbox.getHost(plan.agent.port)}`,
643
+ directory,
644
+ password: input.password,
645
+ headers: { "e2b-traffic-access-token": token }
646
+ };
647
+ await waitForDirectRuntime(runtime);
648
+ return { externalId: sandbox.sandboxId, runtime };
649
+ } catch (error) {
650
+ await sandbox.kill().catch(() => {
651
+ return;
652
+ });
653
+ throw error;
654
+ }
655
+ },
656
+ async status(workspace) {
657
+ try {
658
+ const info = await Sandbox.getInfo(workspace.externalId, { apiKey: config.apiKey });
659
+ return info.state === "paused" ? "paused" : info.state === "running" ? "running" : "terminated";
660
+ } catch (error) {
661
+ if (error instanceof Error && /not found|404|does not exist/i.test(error.message)) {
662
+ return "terminated";
663
+ }
664
+ throw error;
665
+ }
666
+ },
667
+ async pause(workspace) {
668
+ await (await connect(workspace.externalId)).pause({ apiKey: config.apiKey });
669
+ },
670
+ async resume(workspace) {
671
+ const sandbox = await connect(workspace.externalId);
672
+ const token = sandbox.trafficAccessToken;
673
+ if (!token)
674
+ throw new Error("E2B traffic access token is missing");
675
+ const runtime = {
676
+ ...workspace.runtime,
677
+ url: `https://${sandbox.getHost(4096)}`,
678
+ headers: { "e2b-traffic-access-token": token }
679
+ };
680
+ await waitForDirectRuntime(runtime);
681
+ return runtime;
682
+ },
683
+ async terminate(workspace) {
684
+ await Sandbox.kill(workspace.externalId, { apiKey: config.apiKey });
685
+ },
686
+ async keepAlive(workspace, timeoutMs) {
687
+ await Sandbox.setTimeout(workspace.externalId, timeoutMs, { apiKey: config.apiKey });
688
+ }
689
+ };
690
+ }
691
+
692
+ // src/direct/exedev.ts
693
+ var HOME2 = "/home/exedev";
694
+ function serializeHandle2(handle) {
695
+ return JSON.stringify(handle);
696
+ }
697
+ function parseHandle3(value) {
698
+ try {
699
+ const handle = JSON.parse(value);
700
+ if (!handle.vmName || !handle.repoDir || !handle.serve?.command || !handle.serve.port) {
701
+ throw new Error("missing required fields");
702
+ }
703
+ return handle;
704
+ } catch {
705
+ throw new Error("Invalid exe.dev direct workspace handle");
706
+ }
707
+ }
708
+ function findToken(value) {
709
+ if (typeof value === "string")
710
+ return value.match(/exe[01]\.[A-Za-z0-9._-]+/)?.[0];
711
+ if (Array.isArray(value))
712
+ return value.map(findToken).find(Boolean);
713
+ if (value && typeof value === "object") {
714
+ return Object.values(value).map(findToken).find(Boolean);
715
+ }
716
+ return;
717
+ }
718
+ function resultStatus(value) {
719
+ const data = value;
720
+ const status = data?.status ?? data?.vms?.[0]?.status;
721
+ if (status === "running")
722
+ return "running";
723
+ if (status === "paused")
724
+ return "paused";
725
+ if (status === "creating")
726
+ return "pending";
727
+ return "terminated";
728
+ }
729
+ function createExeDevDirectProvider(config) {
730
+ if (!config.apiToken.trim())
731
+ throw new Error("exe.dev apiToken is required");
732
+ async function execute(command) {
733
+ const response = await fetch("https://exe.dev/exec", {
734
+ method: "POST",
735
+ headers: { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "text/plain" },
736
+ body: command
737
+ });
738
+ const text = await response.text();
739
+ if (!response.ok) {
740
+ const permissionHint = response.status === 401 || response.status === 403 || /permission|forbidden/i.test(text) ? " exe.dev tokens need cmds covering new, ls, ssh, share, ssh-key, pause, resume, and rm." : "";
741
+ throw new Error(`exe.dev command failed (${response.status}): ${text}.${permissionHint}`);
742
+ }
743
+ try {
744
+ return JSON.parse(text);
745
+ } catch {
746
+ return text;
747
+ }
748
+ }
749
+ const runVmCommand = (handle, command) => execute(`ssh ${handle.vmName} -- bash -lc ${shellQuote(command)}`);
750
+ async function startRuntime(handle) {
751
+ await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null &`);
752
+ }
753
+ async function accessToken(vmName) {
754
+ const token = findToken(await execute(`ssh-key generate-api-key --vm=${vmName} --label=gitterm-direct --exp=never`));
755
+ if (!token)
756
+ throw new Error("exe.dev did not return a VM access token");
757
+ return token;
758
+ }
759
+ async function waitUntilRunning(vmName) {
760
+ const deadline = Date.now() + 60000;
761
+ while (Date.now() < deadline) {
762
+ const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
763
+ if (status === "running")
764
+ return;
765
+ await new Promise((resolve) => setTimeout(resolve, 500));
766
+ }
767
+ throw new Error("Timed out waiting for exe.dev VM");
768
+ }
769
+ async function runtime(handle, password) {
770
+ const token = await accessToken(handle.vmName);
771
+ return {
772
+ url: `https://${handle.vmName}.exe.xyz`,
773
+ directory: handle.repoDir,
774
+ password,
775
+ headers: { "X-Exedev-Authorization": `Bearer ${token}` }
776
+ };
777
+ }
778
+ return {
779
+ name: "exedev",
780
+ capabilities: {
781
+ persistence: "supported",
782
+ recommendedLifecycle: "ephemeral",
783
+ supportsPause: true,
784
+ ephemeralPause: "stateful",
785
+ supportsKeepAlive: false
786
+ },
787
+ async create(input) {
788
+ const plan = input.provisioning;
789
+ const vmSuffix = input.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 20).toLowerCase();
790
+ const vmName = `gitterm-${vmSuffix}`;
791
+ const handle = {
792
+ vmName,
793
+ repoDir: `${HOME2}/${plan.repository?.name ?? "workspace"}`,
794
+ serve: { command: plan.agent.command, port: plan.agent.port }
795
+ };
796
+ const createArgs = [
797
+ `new --name=${vmName}`,
798
+ "--no-email",
799
+ `--tag=${shellQuote(`gitterm-${input.id}`)}`,
800
+ `--image=${shellQuote(resolveDirectImage(config.image))}`,
801
+ config.cpu ? `--cpu=${config.cpu}` : "",
802
+ config.memory ? `--memory=${shellQuote(config.memory)}` : "",
803
+ config.disk ? `--disk=${shellQuote(config.disk)}` : "",
804
+ ...Object.entries(plan.agent.environmentVariables).map(([key, value]) => `--env=${shellQuote(`${key}=${value}`)}`)
805
+ ].filter(Boolean).join(" ");
806
+ await execute(createArgs);
807
+ try {
808
+ await waitUntilRunning(vmName);
809
+ await runVmCommand(handle, `mkdir -p ${shellQuote(handle.repoDir)}`);
810
+ for (const command of config.runtimeSetupCommands ?? []) {
811
+ await runVmCommand(handle, command);
812
+ }
813
+ if (plan.repository) {
814
+ await runVmCommand(handle, cloneRepositoryScript(plan.repository, handle.repoDir));
815
+ }
816
+ for (const file of plan.agent.files) {
817
+ const path = file.path.startsWith("~/") ? `${HOME2}/${file.path.slice(2)}` : file.path;
818
+ await runVmCommand(handle, `mkdir -p ${shellQuote(path.slice(0, path.lastIndexOf("/")))} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
819
+ }
820
+ if (plan.setupCommands.length) {
821
+ await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && ${setupCommandScript(plan.setupCommands)}`);
822
+ }
823
+ await startRuntime(handle);
824
+ await execute(`share port ${vmName} ${handle.serve.port}`);
825
+ await execute(`share set-private ${vmName}`);
826
+ const directRuntime = await runtime(handle, input.password);
827
+ await waitForDirectRuntime(directRuntime);
828
+ return { externalId: serializeHandle2(handle), runtime: directRuntime };
829
+ } catch (error) {
830
+ await execute(`rm ${vmName}`).catch(() => {
831
+ return;
832
+ });
833
+ throw error;
834
+ }
835
+ },
836
+ async status(workspace) {
837
+ try {
838
+ return resultStatus(await execute(`ls ${parseHandle3(workspace.externalId).vmName}`));
839
+ } catch (error) {
840
+ if (error instanceof Error && /not found|404|does not exist/i.test(error.message)) {
841
+ return "terminated";
842
+ }
843
+ throw error;
844
+ }
845
+ },
846
+ async pause(workspace) {
847
+ await execute(`pause ${parseHandle3(workspace.externalId).vmName}`);
848
+ },
849
+ async resume(workspace) {
850
+ const handle = parseHandle3(workspace.externalId);
851
+ await execute(`resume ${handle.vmName}`);
852
+ await waitUntilRunning(handle.vmName);
853
+ await execute(`share port ${handle.vmName} ${handle.serve.port}`);
854
+ await execute(`share set-private ${handle.vmName}`);
855
+ if (!workspace.runtime.password)
856
+ throw new Error("exe.dev runtime password is missing");
857
+ const directRuntime = await runtime(handle, workspace.runtime.password);
858
+ await waitForDirectRuntime(directRuntime);
859
+ return directRuntime;
860
+ },
861
+ async terminate(workspace) {
862
+ await execute(`rm ${parseHandle3(workspace.externalId).vmName}`);
863
+ }
864
+ };
865
+ }
866
+
867
+ // src/direct/railway.ts
868
+ var DEFAULT_API_URL = "https://backboard.railway.app/graphql/v2";
869
+ var OPENCODE_SERVER_PORT = 7681;
870
+ var WORKSPACE_ROOT3 = "/workspace";
871
+ var DEPLOYMENT_TIMEOUT_MS = 5 * 60000;
872
+ var SERVICE_CREATE = `
873
+ mutation DirectServiceCreate($input: ServiceCreateInput!) {
874
+ serviceCreate(input: $input) { id }
875
+ }
876
+ `;
877
+ var SERVICE_INSTANCE_UPDATE = `
878
+ mutation DirectServiceInstanceUpdate(
879
+ $environmentId: String!
880
+ $serviceId: String!
881
+ $input: ServiceInstanceUpdateInput!
882
+ ) {
883
+ serviceInstanceUpdate(
884
+ environmentId: $environmentId
885
+ serviceId: $serviceId
886
+ input: $input
887
+ )
888
+ }
889
+ `;
890
+ var VOLUME_CREATE = `
891
+ mutation DirectVolumeCreate($input: VolumeCreateInput!) {
892
+ volumeCreate(input: $input) { id }
893
+ }
894
+ `;
895
+ var DOMAIN_CREATE = `
896
+ mutation DirectDomainCreate($input: ServiceDomainCreateInput!) {
897
+ serviceDomainCreate(input: $input) { id domain }
898
+ }
899
+ `;
900
+ var SERVICE_DEPLOY = `
901
+ mutation DirectServiceDeploy($environmentId: String!, $serviceId: String!) {
902
+ serviceInstanceDeploy(
903
+ environmentId: $environmentId
904
+ serviceId: $serviceId
905
+ latestCommit: true
906
+ )
907
+ }
908
+ `;
909
+ var LATEST_DEPLOYMENT = `
910
+ query DirectLatestDeployment($environmentId: String!, $serviceId: String!) {
911
+ serviceInstance(environmentId: $environmentId, serviceId: $serviceId) {
912
+ latestDeployment { id status }
913
+ }
914
+ }
915
+ `;
916
+ var DEPLOYMENT_REMOVE = `
917
+ mutation DirectDeploymentRemove($id: String!) { deploymentRemove(id: $id) }
918
+ `;
919
+ var SERVICE_DELETE = `
920
+ mutation DirectServiceDelete($id: String!) { serviceDelete(id: $id) }
921
+ `;
922
+ var VOLUME_DELETE = `
923
+ mutation DirectVolumeDelete($id: String!) { volumeDelete(volumeId: $id) }
924
+ `;
925
+ var DOMAIN_DELETE = `
926
+ mutation DirectDomainDelete($id: String!) { serviceDomainDelete(id: $id) }
927
+ `;
928
+ var VARIABLE_UPSERT = `
929
+ mutation DirectVariableUpsert($input: VariableUpsertInput!) {
930
+ variableUpsert(input: $input)
931
+ }
932
+ `;
933
+ function parseHandle4(externalId) {
934
+ try {
935
+ const handle = JSON.parse(externalId);
936
+ if (typeof handle.serviceId !== "string" || typeof handle.domainId !== "string" || typeof handle.domain !== "string" || typeof handle.deploymentId !== "string") {
937
+ throw new Error("missing fields");
938
+ }
939
+ return {
940
+ serviceId: handle.serviceId,
941
+ volumeId: handle.volumeId,
942
+ domainId: handle.domainId,
943
+ domain: handle.domain,
944
+ deploymentId: handle.deploymentId
945
+ };
946
+ } catch {
947
+ throw new Error("Invalid Railway direct workspace externalId");
948
+ }
949
+ }
950
+ function workspaceStatus(status) {
951
+ switch (status) {
952
+ case "SUCCESS":
953
+ return "running";
954
+ case "SLEEPING":
955
+ case "REMOVED":
956
+ return "paused";
957
+ case "CRASHED":
958
+ case "FAILED":
959
+ case "SKIPPED":
960
+ return "failed";
961
+ case "BUILDING":
962
+ case "DEPLOYING":
963
+ case "INITIALIZING":
964
+ case "NEEDS_APPROVAL":
965
+ case "QUEUED":
966
+ case "REMOVING":
967
+ case "WAITING":
968
+ return "pending";
969
+ }
970
+ }
971
+ function createRailwayDirectProvider(config) {
972
+ if (!config.apiToken.trim())
973
+ throw new Error("Railway apiToken is required");
974
+ if (!config.projectId.trim())
975
+ throw new Error("Railway projectId is required");
976
+ if (!config.environmentId.trim())
977
+ throw new Error("Railway environmentId is required");
978
+ const apiUrl = config.apiUrl?.trim() || DEFAULT_API_URL;
979
+ const runtimePort = config.runtimePort ?? OPENCODE_SERVER_PORT;
980
+ if (!Number.isInteger(runtimePort) || runtimePort < 1 || runtimePort > 65535) {
981
+ throw new Error("Railway runtimePort must be a valid TCP port");
982
+ }
983
+ const request = async (query, variables) => {
984
+ const response = await fetch(apiUrl, {
985
+ method: "POST",
986
+ headers: {
987
+ "Content-Type": "application/json",
988
+ Authorization: `Bearer ${config.apiToken}`
989
+ },
990
+ body: JSON.stringify({ query, variables })
991
+ });
992
+ if (!response.ok) {
993
+ throw new Error(`Railway API request failed (${response.status} ${response.statusText})`);
994
+ }
995
+ const result = await response.json();
996
+ if (result.errors?.length) {
997
+ throw new Error(`Railway GraphQL error: ${result.errors.map((error) => error.message).join(", ")}`);
998
+ }
999
+ if (!result.data)
1000
+ throw new Error("Railway GraphQL response did not include data");
1001
+ return result.data;
1002
+ };
1003
+ const latestDeployment = async (serviceId) => {
1004
+ const result = await request(LATEST_DEPLOYMENT, { environmentId: config.environmentId, serviceId });
1005
+ return result.serviceInstance?.latestDeployment ?? undefined;
1006
+ };
1007
+ const waitForDeployment = async (serviceId, previousDeploymentId) => {
1008
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1009
+ while (Date.now() < deadline) {
1010
+ const deployment = await latestDeployment(serviceId);
1011
+ if (deployment && deployment.id !== previousDeploymentId) {
1012
+ if (deployment.status === "SUCCESS")
1013
+ return deployment;
1014
+ if (["CRASHED", "FAILED", "REMOVED", "SKIPPED"].includes(deployment.status)) {
1015
+ throw new Error(`Railway deployment ${deployment.id} ended with ${deployment.status}`);
1016
+ }
1017
+ }
1018
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1019
+ }
1020
+ throw new Error("Timed out waiting for Railway deployment");
1021
+ };
1022
+ const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
1023
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1024
+ while (Date.now() < deadline) {
1025
+ const deployment = await latestDeployment(serviceId);
1026
+ if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
1027
+ return;
1028
+ if (deployment.status === "FAILED" || deployment.status === "CRASHED") {
1029
+ throw new Error(`Railway deployment ${deployment.id} ended with ${deployment.status}`);
1030
+ }
1031
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1032
+ }
1033
+ throw new Error("Timed out waiting for Railway deployment removal");
1034
+ };
1035
+ const removeResources = async (handle) => {
1036
+ let failure;
1037
+ for (const operation of [
1038
+ handle.domainId ? () => request(DOMAIN_DELETE, { id: handle.domainId }) : undefined,
1039
+ handle.serviceId ? () => request(SERVICE_DELETE, { id: handle.serviceId }) : undefined,
1040
+ handle.volumeId ? () => request(VOLUME_DELETE, { id: handle.volumeId }) : undefined
1041
+ ]) {
1042
+ if (!operation)
1043
+ continue;
1044
+ await operation().catch((error) => {
1045
+ failure ??= error;
1046
+ });
1047
+ }
1048
+ if (failure)
1049
+ throw failure;
1050
+ };
1051
+ return {
1052
+ name: "railway",
1053
+ capabilities: {
1054
+ persistence: "supported",
1055
+ recommendedLifecycle: "persistent",
1056
+ supportsPause: true,
1057
+ ephemeralPause: "state-losing",
1058
+ supportsKeepAlive: false
1059
+ },
1060
+ async create(input) {
1061
+ const plan = input.provisioning;
1062
+ const handle = {};
1063
+ const region = config.region?.trim();
1064
+ try {
1065
+ const created = await request(SERVICE_CREATE, {
1066
+ input: {
1067
+ projectId: config.projectId,
1068
+ environmentId: config.environmentId,
1069
+ name: input.id,
1070
+ variables: railwayContainerEnvironment(plan)
1071
+ }
1072
+ });
1073
+ handle.serviceId = created.serviceCreate.id;
1074
+ await request(SERVICE_INSTANCE_UPDATE, {
1075
+ environmentId: config.environmentId,
1076
+ serviceId: handle.serviceId,
1077
+ input: {
1078
+ source: { image: resolveDirectImage(config.image) },
1079
+ ...region ? { multiRegionConfig: { [region]: { numReplicas: 1 } } } : {}
1080
+ }
1081
+ });
1082
+ if (input.lifecycle === "persistent") {
1083
+ const volume = await request(VOLUME_CREATE, {
1084
+ input: {
1085
+ projectId: config.projectId,
1086
+ environmentId: config.environmentId,
1087
+ serviceId: handle.serviceId,
1088
+ mountPath: WORKSPACE_ROOT3,
1089
+ ...region ? { region } : {}
1090
+ }
1091
+ });
1092
+ handle.volumeId = volume.volumeCreate.id;
1093
+ }
1094
+ const domain = await request(DOMAIN_CREATE, {
1095
+ input: {
1096
+ environmentId: config.environmentId,
1097
+ serviceId: handle.serviceId,
1098
+ targetPort: runtimePort
1099
+ }
1100
+ });
1101
+ handle.domainId = domain.serviceDomainCreate.id;
1102
+ handle.domain = domain.serviceDomainCreate.domain;
1103
+ const previous = await latestDeployment(handle.serviceId);
1104
+ await request(SERVICE_DEPLOY, {
1105
+ environmentId: config.environmentId,
1106
+ serviceId: handle.serviceId
1107
+ });
1108
+ const deployment = await waitForDeployment(handle.serviceId, previous?.id);
1109
+ handle.deploymentId = deployment.id;
1110
+ const runtime = {
1111
+ url: `https://${handle.domain}`,
1112
+ directory: `${WORKSPACE_ROOT3}/${plan.repository?.name ?? "workspace"}`,
1113
+ password: input.password
1114
+ };
1115
+ await waitForDirectRuntime(runtime);
1116
+ if (plan.repository?.authToken && handle.serviceId) {
1117
+ await request(VARIABLE_UPSERT, {
1118
+ input: {
1119
+ environmentId: config.environmentId,
1120
+ projectId: config.projectId,
1121
+ serviceId: handle.serviceId,
1122
+ name: "GITTERM_GIT_TOKEN",
1123
+ value: "",
1124
+ skipDeploys: true
1125
+ }
1126
+ });
1127
+ }
1128
+ if (!handle.serviceId || !handle.domainId || !handle.domain || !handle.deploymentId) {
1129
+ throw new Error("Railway provisioning completed without all resource identifiers");
1130
+ }
1131
+ const completedHandle = {
1132
+ serviceId: handle.serviceId,
1133
+ volumeId: handle.volumeId,
1134
+ domainId: handle.domainId,
1135
+ domain: handle.domain,
1136
+ deploymentId: handle.deploymentId
1137
+ };
1138
+ return { externalId: JSON.stringify(completedHandle), runtime };
1139
+ } catch (error) {
1140
+ const identifiers = Object.entries(handle).filter(([, value]) => value).map(([key, value]) => `${key}=${value}`).join(", ");
1141
+ try {
1142
+ await removeResources(handle);
1143
+ } catch (cleanupError) {
1144
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; cleanup failed${identifiers ? ` (${identifiers})` : ""}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, { cause: cleanupError });
1145
+ }
1146
+ throw error;
1147
+ }
1148
+ },
1149
+ async status(workspace) {
1150
+ const handle = parseHandle4(workspace.externalId);
1151
+ try {
1152
+ const deployment = await latestDeployment(handle.serviceId);
1153
+ return deployment ? workspaceStatus(deployment.status) : "terminated";
1154
+ } catch (error) {
1155
+ if (error instanceof Error && /not found|does not exist/i.test(error.message)) {
1156
+ return "terminated";
1157
+ }
1158
+ throw error;
1159
+ }
1160
+ },
1161
+ async pause(workspace) {
1162
+ const handle = parseHandle4(workspace.externalId);
1163
+ const deployment = await latestDeployment(handle.serviceId);
1164
+ if (!deployment || deployment.status === "REMOVED")
1165
+ return;
1166
+ await request(DEPLOYMENT_REMOVE, { id: deployment.id });
1167
+ await waitForDeploymentRemoval(handle.serviceId, deployment.id);
1168
+ },
1169
+ async resume(workspace) {
1170
+ const handle = parseHandle4(workspace.externalId);
1171
+ const previous = await latestDeployment(handle.serviceId);
1172
+ await request(SERVICE_DEPLOY, {
1173
+ environmentId: config.environmentId,
1174
+ serviceId: handle.serviceId
1175
+ });
1176
+ await waitForDeployment(handle.serviceId, previous?.id);
1177
+ await waitForDirectRuntime(workspace.runtime);
1178
+ return workspace.runtime;
1179
+ },
1180
+ async terminate(workspace) {
1181
+ await removeResources(parseHandle4(workspace.externalId));
1182
+ }
1183
+ };
1184
+ }
1185
+
1186
+ // src/direct/vercel.ts
1187
+ import { Sandbox as Sandbox2 } from "@vercel/sandbox";
1188
+ var WORKSPACE_ROOT4 = "/vercel/sandbox";
1189
+ function parseHandle5(externalId) {
1190
+ const handle = JSON.parse(externalId);
1191
+ if (typeof handle.name !== "string" || typeof handle.directory !== "string" || typeof handle.command !== "string" || typeof handle.port !== "number") {
1192
+ throw new Error("Invalid Vercel direct workspace handle");
1193
+ }
1194
+ return handle;
1195
+ }
1196
+ function createVercelDirectProvider(config) {
1197
+ if (!config.apiToken.trim())
1198
+ throw new Error("Vercel apiToken is required");
1199
+ if (!config.teamId.trim())
1200
+ throw new Error("Vercel teamId is required");
1201
+ if (!config.projectId.trim())
1202
+ throw new Error("Vercel projectId is required");
1203
+ const credentials = {
1204
+ token: config.apiToken,
1205
+ teamId: config.teamId,
1206
+ projectId: config.projectId
1207
+ };
1208
+ const getSandbox = async (externalId, resume) => {
1209
+ const handle = parseHandle5(externalId);
1210
+ const sandbox = await Sandbox2.get({ name: handle.name, resume, ...credentials });
1211
+ return { handle, sandbox };
1212
+ };
1213
+ const run = async (sandbox, command, cwd, env) => {
1214
+ const result = await sandbox.runCommand({
1215
+ cmd: "bash",
1216
+ args: ["-lc", command],
1217
+ cwd,
1218
+ env
1219
+ });
1220
+ if (result.exitCode !== 0) {
1221
+ throw new Error(`Vercel command failed with exit code ${result.exitCode}: ${(await result.stderr()).trim() || command}`);
1222
+ }
1223
+ };
1224
+ const startAgent = async (sandbox, handle) => {
1225
+ await sandbox.runCommand({
1226
+ cmd: "bash",
1227
+ args: ["-lc", `exec ${handle.command} > /tmp/gitterm-agent.log 2>&1`],
1228
+ cwd: handle.directory,
1229
+ detached: true
1230
+ });
1231
+ };
1232
+ return {
1233
+ name: "vercel",
1234
+ capabilities: {
1235
+ persistence: "supported",
1236
+ recommendedLifecycle: "persistent",
1237
+ supportsPause: true,
1238
+ ephemeralPause: "stateful",
1239
+ supportsKeepAlive: true
1240
+ },
1241
+ async create(input) {
1242
+ const plan = input.provisioning;
1243
+ const handle = {
1244
+ name: `gitterm-${input.id}`,
1245
+ directory: `${WORKSPACE_ROOT4}/${plan.repository?.name ?? "workspace"}`,
1246
+ command: plan.agent.command,
1247
+ port: plan.agent.port
1248
+ };
1249
+ const sandbox = await Sandbox2.create({
1250
+ ...credentials,
1251
+ name: handle.name,
1252
+ persistent: true,
1253
+ keepLastSnapshots: { count: 1 },
1254
+ ports: [handle.port],
1255
+ env: plan.agent.environmentVariables,
1256
+ timeout: config.timeoutMs ?? 10 * 60000,
1257
+ tags: { gitterm_workspace: input.id },
1258
+ ...config.image ? { image: config.image } : { runtime: config.runtime ?? "node24" },
1259
+ ...config.vcpus != null ? { resources: { vcpus: config.vcpus } } : {}
1260
+ });
1261
+ try {
1262
+ await run(sandbox, `mkdir -p ${shellQuote(handle.directory)}`);
1263
+ for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g opencode-ai --no-audit --fund=false"])) {
1264
+ await run(sandbox, command);
1265
+ }
1266
+ if (plan.repository) {
1267
+ const repository = plan.repository;
1268
+ const url = repository.url.endsWith(".git") ? repository.url : `${repository.url}.git`;
1269
+ const askPassPath = "/tmp/gitterm-git-askpass";
1270
+ if (repository.authToken) {
1271
+ await sandbox.writeFiles([
1272
+ {
1273
+ path: askPassPath,
1274
+ mode: 448,
1275
+ content: `#!/bin/sh
1276
+ case "$1" in
1277
+ *Username*) printf '%s\\n' "$GITTERM_GIT_USERNAME" ;;
1278
+ *) printf '%s\\n' "$GITTERM_GIT_TOKEN" ;;
1279
+ esac
1280
+ `
1281
+ }
1282
+ ]);
1283
+ }
1284
+ try {
1285
+ const ref = repository.checkoutRef ?? repository.branch;
1286
+ await run(sandbox, `git clone${ref ? ` --branch ${shellQuote(ref)}` : ""} ${shellQuote(url)} ${shellQuote(handle.directory)}`, undefined, repository.authToken ? {
1287
+ GIT_ASKPASS: askPassPath,
1288
+ GIT_TERMINAL_PROMPT: "0",
1289
+ GITTERM_GIT_USERNAME: repository.authUsername ?? "x-access-token",
1290
+ GITTERM_GIT_TOKEN: repository.authToken
1291
+ } : undefined);
1292
+ } finally {
1293
+ if (repository.authToken) {
1294
+ await sandbox.runCommand("rm", ["-f", askPassPath]).catch(() => {
1295
+ return;
1296
+ });
1297
+ }
1298
+ }
1299
+ if (repository.baseCommit) {
1300
+ await run(sandbox, `git fetch --depth 1 origin ${shellQuote(repository.baseCommit)} && git checkout --detach ${shellQuote(repository.baseCommit)}`, handle.directory);
1301
+ }
1302
+ }
1303
+ const homeResult = await sandbox.runCommand("printenv", ["HOME"]);
1304
+ const home = (await homeResult.stdout()).trim() || "/home/vercel-sandbox";
1305
+ for (const file of plan.agent.files) {
1306
+ const target = file.path.replace(/^~/, home);
1307
+ const parent = target.slice(0, target.lastIndexOf("/"));
1308
+ await run(sandbox, `mkdir -p ${shellQuote(parent)}`);
1309
+ await sandbox.writeFiles([
1310
+ { path: target, content: Buffer.from(file.contentBase64, "base64") }
1311
+ ]);
1312
+ }
1313
+ if (plan.setupCommands.length) {
1314
+ await run(sandbox, setupCommandScript(plan.setupCommands), handle.directory);
1315
+ }
1316
+ await startAgent(sandbox, handle);
1317
+ const runtime = {
1318
+ url: sandbox.domain(handle.port),
1319
+ directory: handle.directory,
1320
+ password: input.password
1321
+ };
1322
+ await waitForDirectRuntime(runtime);
1323
+ return { externalId: JSON.stringify(handle), runtime };
1324
+ } catch (error) {
1325
+ await sandbox.delete().catch(() => {
1326
+ return;
1327
+ });
1328
+ throw error;
1329
+ }
1330
+ },
1331
+ async status(workspace) {
1332
+ try {
1333
+ const { sandbox } = await getSandbox(workspace.externalId, false);
1334
+ if (sandbox.status === "running")
1335
+ return "running";
1336
+ if (["pending", "stopping", "snapshotting"].includes(sandbox.status))
1337
+ return "pending";
1338
+ if (sandbox.status === "stopped")
1339
+ return "paused";
1340
+ if (sandbox.status === "failed")
1341
+ return "failed";
1342
+ return "terminated";
1343
+ } catch (error) {
1344
+ if (error instanceof Error && /not found|404|does not exist/i.test(error.message)) {
1345
+ return "terminated";
1346
+ }
1347
+ throw error;
1348
+ }
1349
+ },
1350
+ async pause(workspace) {
1351
+ const { sandbox } = await getSandbox(workspace.externalId, false);
1352
+ if (sandbox.status !== "stopped")
1353
+ await sandbox.stop();
1354
+ },
1355
+ async resume(workspace) {
1356
+ const handle = parseHandle5(workspace.externalId);
1357
+ const sandbox = await Sandbox2.get({
1358
+ name: handle.name,
1359
+ resume: true,
1360
+ ...credentials,
1361
+ onResume: async (resumed) => startAgent(resumed, handle)
1362
+ });
1363
+ const runtime = {
1364
+ ...workspace.runtime,
1365
+ url: sandbox.domain(handle.port),
1366
+ directory: handle.directory
1367
+ };
1368
+ await waitForDirectRuntime(runtime);
1369
+ return runtime;
1370
+ },
1371
+ async terminate(workspace) {
1372
+ const { sandbox } = await getSandbox(workspace.externalId, false);
1373
+ await sandbox.delete();
1374
+ },
1375
+ async keepAlive(workspace, timeoutMs) {
1376
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1377
+ throw new Error("Vercel keep-alive timeout must be positive");
1378
+ }
1379
+ const { sandbox } = await getSandbox(workspace.externalId, false);
1380
+ await sandbox.update({ timeout: timeoutMs });
1381
+ }
1382
+ };
1383
+ }
1384
+
1385
+ // src/direct/client.ts
1386
+ function resolveProvider(provider) {
1387
+ if ("create" in provider)
1388
+ return provider;
1389
+ switch (provider.type) {
1390
+ case "ascii":
1391
+ return createAsciiDirectProvider(provider);
1392
+ case "daytona":
1393
+ return createDaytonaDirectProvider(provider);
1394
+ case "e2b":
1395
+ return createE2BDirectProvider(provider);
1396
+ case "exedev":
1397
+ return createExeDevDirectProvider(provider);
1398
+ case "railway":
1399
+ return createRailwayDirectProvider(provider);
1400
+ case "vercel":
1401
+ return createVercelDirectProvider(provider);
1402
+ }
1403
+ }
1404
+ function runtimeClient(workspace) {
1405
+ const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
1406
+ return createOpencodeClient({
1407
+ baseUrl: workspace.runtime.url,
1408
+ directory: workspace.runtime.directory,
1409
+ headers: {
1410
+ ...workspace.runtime.headers,
1411
+ ...authorization ? { Authorization: authorization } : {}
1412
+ }
1413
+ });
1414
+ }
1415
+ function authClient(workspace) {
1416
+ const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
1417
+ return createOpencodeV2Client({
1418
+ baseUrl: workspace.runtime.url,
1419
+ directory: workspace.runtime.directory,
1420
+ headers: {
1421
+ ...workspace.runtime.headers,
1422
+ ...authorization ? { Authorization: authorization } : {}
1423
+ }
1424
+ });
1425
+ }
1426
+ function errorMessage(error) {
1427
+ if (!error)
1428
+ return "OpenCode request failed";
1429
+ if (typeof error === "string")
1430
+ return error;
1431
+ if (typeof error === "object" && "data" in error) {
1432
+ const data = error.data;
1433
+ if (typeof data?.message === "string")
1434
+ return data.message;
1435
+ }
1436
+ if (typeof error === "object" && "message" in error) {
1437
+ const message = error.message;
1438
+ if (typeof message === "string")
1439
+ return message;
1440
+ }
1441
+ return error instanceof Error ? error.message : JSON.stringify(error);
1442
+ }
1443
+ function authStatus(status) {
1444
+ const common = {
1445
+ createdAt: Number(status.time.created),
1446
+ expiresAt: Number(status.time.expires)
1447
+ };
1448
+ if (status.status === "failed")
1449
+ return { status: "failed", message: status.message, ...common };
1450
+ return { status: status.status, ...common };
1451
+ }
1452
+ function mapStatus(status, errorName, assistantCompleted = true) {
1453
+ if (errorName === "MessageAbortedError")
1454
+ return "cancelled";
1455
+ if (errorName)
1456
+ return "failed";
1457
+ if (assistantCompleted)
1458
+ return "completed";
1459
+ if (status?.type === "retry")
1460
+ return "retrying";
1461
+ return "running";
1462
+ }
1463
+ function pollTiming(wait, fallbackTimeoutMs) {
1464
+ const timeoutMs = wait.timeoutMs ?? fallbackTimeoutMs;
1465
+ const pollIntervalMs = wait.pollIntervalMs ?? 1000;
1466
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
1467
+ throw new Error("timeoutMs must be a finite, non-negative number");
1468
+ }
1469
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0) {
1470
+ throw new Error("pollIntervalMs must be a finite, non-negative number");
1471
+ }
1472
+ return { timeoutMs, pollIntervalMs };
1473
+ }
1474
+ function modelParts(model) {
1475
+ if (!model)
1476
+ return;
1477
+ const separator = model.indexOf("/");
1478
+ if (separator <= 0 || separator === model.length - 1) {
1479
+ throw new Error('model must use the "provider/model" format');
1480
+ }
1481
+ return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
1482
+ }
1483
+ function createDirectGittermClient(options) {
1484
+ const provider = resolveProvider(options.provider);
1485
+ function assertWorkspace(workspace) {
1486
+ if (workspace.provider !== provider.name) {
1487
+ throw new Error(`Workspace belongs to ${workspace.provider}, not ${provider.name}`);
1488
+ }
1489
+ }
1490
+ function assertRunWorkspace(run, workspace) {
1491
+ assertWorkspace(workspace);
1492
+ if (run.workspaceId !== workspace.id) {
1493
+ throw new Error(`Run belongs to workspace ${run.workspaceId}, not ${workspace.id}`);
1494
+ }
1495
+ }
1496
+ function assertAuthAttempt(attempt, workspace) {
1497
+ assertWorkspace(workspace);
1498
+ if (attempt.workspaceId !== workspace.id) {
1499
+ throw new Error(`OAuth attempt belongs to workspace ${attempt.workspaceId}, not ${workspace.id}`);
1500
+ }
1501
+ }
1502
+ async function getAuthStatus(attempt, workspace) {
1503
+ assertAuthAttempt(attempt, workspace);
1504
+ const result = await authClient(workspace).v2.integration.attempt.status({
1505
+ attemptID: attempt.id
1506
+ });
1507
+ if (result.error || !result.data)
1508
+ throw new Error(errorMessage(result.error));
1509
+ return authStatus(result.data.data);
1510
+ }
1511
+ async function getRun(run, workspace) {
1512
+ assertRunWorkspace(run, workspace);
1513
+ const client = runtimeClient(workspace);
1514
+ const [statuses, messages] = await Promise.all([
1515
+ client.session.status({ query: { directory: workspace.runtime.directory } }),
1516
+ client.session.messages({
1517
+ path: { id: run.sessionId },
1518
+ query: { directory: workspace.runtime.directory }
1519
+ })
1520
+ ]);
1521
+ if (statuses.error || !statuses.data)
1522
+ throw new Error(errorMessage(statuses.error));
1523
+ if (messages.error || !messages.data)
1524
+ throw new Error(errorMessage(messages.error));
1525
+ const related = messages.data.filter((message) => message.info.id === run.messageId || message.info.role === "assistant" && message.info.parentID === run.messageId);
1526
+ const assistant = related.findLast((message) => message.info.role === "assistant");
1527
+ const assistantError = assistant?.info.role === "assistant" ? assistant.info.error : undefined;
1528
+ const assistantCompleted = assistant?.info.role === "assistant" && assistant.info.time.completed != null;
1529
+ const finalText = assistant?.parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.type === "text" ? part.text : "").join(`
1530
+ `).trim();
1531
+ const sessionStatus = statuses.data[run.sessionId];
1532
+ const status = assistant ? mapStatus(sessionStatus, assistantError?.name, Boolean(assistantCompleted)) : mapStatus(sessionStatus, undefined, false);
1533
+ return {
1534
+ ...run,
1535
+ status,
1536
+ error: assistantError ? errorMessage(assistantError) : null,
1537
+ finalText: finalText || null
1538
+ };
1539
+ }
1540
+ return {
1541
+ provider: { name: provider.name, capabilities: provider.capabilities },
1542
+ auth: {
1543
+ async setCredential(workspace, credential) {
1544
+ assertWorkspace(workspace);
1545
+ const providerName = credential.providerName.trim();
1546
+ if (!providerName) {
1547
+ throw new Error("Model credential providerName is required");
1548
+ }
1549
+ const result = await runtimeClient(workspace).auth.set({
1550
+ path: { id: providerName },
1551
+ query: { directory: workspace.runtime.directory },
1552
+ body: directModelAuth(credential)
1553
+ });
1554
+ if (result.error)
1555
+ throw new Error(errorMessage(result.error));
1556
+ },
1557
+ async list(workspace) {
1558
+ assertWorkspace(workspace);
1559
+ const result = await authClient(workspace).v2.integration.list();
1560
+ if (result.error || !result.data)
1561
+ throw new Error(errorMessage(result.error));
1562
+ return result.data.data;
1563
+ },
1564
+ async get(workspace, integrationId) {
1565
+ assertWorkspace(workspace);
1566
+ const result = await authClient(workspace).v2.integration.get({
1567
+ integrationID: integrationId
1568
+ });
1569
+ if (result.error || !result.data)
1570
+ throw new Error(errorMessage(result.error));
1571
+ return result.data.data;
1572
+ },
1573
+ async connectKey(input) {
1574
+ assertWorkspace(input.workspace);
1575
+ const result = await authClient(input.workspace).v2.integration.connect.key({
1576
+ integrationID: input.integrationId,
1577
+ key: input.key,
1578
+ label: input.label
1579
+ });
1580
+ if (result.error)
1581
+ throw new Error(errorMessage(result.error));
1582
+ },
1583
+ async connectOAuth(input) {
1584
+ assertWorkspace(input.workspace);
1585
+ const result = await authClient(input.workspace).v2.integration.connect.oauth({
1586
+ integrationID: input.integrationId,
1587
+ methodID: input.methodId,
1588
+ inputs: input.inputs ?? {},
1589
+ label: input.label
1590
+ });
1591
+ if (result.error || !result.data)
1592
+ throw new Error(errorMessage(result.error));
1593
+ const attempt = result.data.data;
1594
+ return {
1595
+ id: attempt.attemptID,
1596
+ workspaceId: input.workspace.id,
1597
+ integrationId: input.integrationId,
1598
+ url: attempt.url,
1599
+ instructions: attempt.instructions,
1600
+ mode: attempt.mode,
1601
+ createdAt: Number(attempt.time.created),
1602
+ expiresAt: Number(attempt.time.expires)
1603
+ };
1604
+ },
1605
+ async status(attempt, workspace) {
1606
+ return getAuthStatus(attempt, workspace);
1607
+ },
1608
+ async complete(attempt, workspace, code) {
1609
+ assertAuthAttempt(attempt, workspace);
1610
+ if (attempt.mode !== "code")
1611
+ throw new Error("Only code-based OAuth attempts are completed manually");
1612
+ if (!code.trim())
1613
+ throw new Error("OAuth authorization code is required");
1614
+ const result = await authClient(workspace).v2.integration.attempt.complete({
1615
+ attemptID: attempt.id,
1616
+ code
1617
+ });
1618
+ if (result.error)
1619
+ throw new Error(errorMessage(result.error));
1620
+ },
1621
+ async wait(attempt, workspace, wait = {}) {
1622
+ assertAuthAttempt(attempt, workspace);
1623
+ const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
1624
+ const deadline = Date.now() + timeoutMs;
1625
+ while (Date.now() <= deadline) {
1626
+ const status = await getAuthStatus(attempt, workspace);
1627
+ if (status.status === "complete")
1628
+ return status;
1629
+ if (status.status === "failed")
1630
+ throw new Error(status.message);
1631
+ if (status.status === "expired")
1632
+ throw new Error("OAuth attempt expired");
1633
+ const remaining = deadline - Date.now();
1634
+ if (remaining <= 0)
1635
+ break;
1636
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
1637
+ }
1638
+ throw new Error(`OAuth attempt timed out after ${timeoutMs}ms`);
1639
+ },
1640
+ async cancel(attempt, workspace) {
1641
+ assertAuthAttempt(attempt, workspace);
1642
+ const result = await authClient(workspace).v2.integration.attempt.cancel({
1643
+ attemptID: attempt.id
1644
+ });
1645
+ if (result.error)
1646
+ throw new Error(errorMessage(result.error));
1647
+ }
1648
+ },
1649
+ workspaces: {
1650
+ async create(input = {}) {
1651
+ const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
1652
+ if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
1653
+ throw new Error(`${provider.name} does not support persistent direct workspaces`);
1654
+ }
1655
+ const id = input.id ?? randomUUID();
1656
+ const password = randomUUID();
1657
+ const provisioning = buildDirectProvisioningPlan({ ...input, id, lifecycle, password });
1658
+ const created = await provider.create({ ...input, id, lifecycle, password, provisioning });
1659
+ return {
1660
+ id,
1661
+ provider: provider.name,
1662
+ externalId: created.externalId,
1663
+ status: "running",
1664
+ lifecycle,
1665
+ runtime: created.runtime,
1666
+ createdAt: new Date().toISOString()
1667
+ };
1668
+ },
1669
+ async status(workspace) {
1670
+ assertWorkspace(workspace);
1671
+ return { ...workspace, status: await provider.status(workspace) };
1672
+ },
1673
+ async pause(workspace) {
1674
+ assertWorkspace(workspace);
1675
+ if (!provider.pause)
1676
+ throw new Error(`${provider.name} does not support pause`);
1677
+ if (workspace.lifecycle === "ephemeral" && provider.capabilities.ephemeralPause !== "stateful") {
1678
+ throw new Error(`${provider.name} cannot pause an ephemeral workspace without losing state`);
1679
+ }
1680
+ await provider.pause(workspace);
1681
+ return { ...workspace, status: "paused" };
1682
+ },
1683
+ async resume(workspace) {
1684
+ assertWorkspace(workspace);
1685
+ if (!provider.resume)
1686
+ throw new Error(`${provider.name} does not support resume`);
1687
+ const runtime = await provider.resume(workspace);
1688
+ return {
1689
+ ...workspace,
1690
+ status: "running",
1691
+ runtime: { ...workspace.runtime, ...runtime }
1692
+ };
1693
+ },
1694
+ async terminate(workspace) {
1695
+ assertWorkspace(workspace);
1696
+ await provider.terminate(workspace);
1697
+ return { ...workspace, status: "terminated" };
1698
+ },
1699
+ async keepAlive(workspace, timeoutMs) {
1700
+ assertWorkspace(workspace);
1701
+ if (!provider.keepAlive)
1702
+ throw new Error(`${provider.name} does not support keep-alive`);
1703
+ await provider.keepAlive(workspace, timeoutMs);
1704
+ }
1705
+ },
1706
+ runs: {
1707
+ async create(input) {
1708
+ assertWorkspace(input.workspace);
1709
+ const client = runtimeClient(input.workspace);
1710
+ let sessionId = input.sessionId;
1711
+ let title = input.title ?? "Agent run";
1712
+ if (!sessionId) {
1713
+ const created = await client.session.create({
1714
+ body: input.title ? { title: input.title } : undefined,
1715
+ query: { directory: input.workspace.runtime.directory }
1716
+ });
1717
+ if (created.error || !created.data)
1718
+ throw new Error(errorMessage(created.error));
1719
+ sessionId = created.data.id;
1720
+ title = created.data.title;
1721
+ }
1722
+ const messageId = `msg_${randomUUID().replaceAll("-", "")}`;
1723
+ const prompted = await client.session.promptAsync({
1724
+ path: { id: sessionId },
1725
+ query: { directory: input.workspace.runtime.directory },
1726
+ body: {
1727
+ messageID: messageId,
1728
+ parts: [{ type: "text", text: input.prompt }],
1729
+ agent: input.agent,
1730
+ model: modelParts(input.model)
1731
+ }
1732
+ });
1733
+ if (prompted.error)
1734
+ throw new Error(errorMessage(prompted.error));
1735
+ return {
1736
+ id: randomUUID(),
1737
+ workspaceId: input.workspace.id,
1738
+ sessionId,
1739
+ messageId,
1740
+ title,
1741
+ status: "running",
1742
+ error: null,
1743
+ finalText: null,
1744
+ submittedAt: new Date().toISOString()
1745
+ };
1746
+ },
1747
+ get: getRun,
1748
+ async wait(run, workspace, wait = {}) {
1749
+ const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
1750
+ const deadline = Date.now() + timeoutMs;
1751
+ let current = run;
1752
+ while (Date.now() < deadline) {
1753
+ current = await getRun(current, workspace);
1754
+ if (!["running", "retrying"].includes(current.status))
1755
+ return current;
1756
+ const remaining = deadline - Date.now();
1757
+ if (remaining <= 0)
1758
+ break;
1759
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
1760
+ }
1761
+ throw new Error(`Agent run timed out after ${timeoutMs}ms`);
1762
+ },
1763
+ async messages(run, workspace) {
1764
+ assertRunWorkspace(run, workspace);
1765
+ const result = await runtimeClient(workspace).session.messages({
1766
+ path: { id: run.sessionId },
1767
+ query: { directory: workspace.runtime.directory }
1768
+ });
1769
+ if (result.error || !result.data)
1770
+ throw new Error(errorMessage(result.error));
1771
+ return result.data.filter((message) => message.info.id === run.messageId || message.info.role === "assistant" && message.info.parentID === run.messageId).map((message) => ({
1772
+ id: message.info.id,
1773
+ role: message.info.role,
1774
+ text: message.parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.type === "text" ? part.text : "").join(`
1775
+ `).trim(),
1776
+ error: message.info.role === "assistant" && message.info.error ? errorMessage(message.info.error) : null
1777
+ }));
1778
+ },
1779
+ async cancel(run, workspace) {
1780
+ const current = await getRun(run, workspace);
1781
+ if (!["running", "retrying"].includes(current.status))
1782
+ return false;
1783
+ const result = await runtimeClient(workspace).session.abort({
1784
+ path: { id: run.sessionId },
1785
+ query: { directory: workspace.runtime.directory }
1786
+ });
1787
+ if (result.error)
1788
+ throw new Error(errorMessage(result.error));
1789
+ return result.data === true;
1790
+ }
1791
+ }
1792
+ };
1793
+ }
1794
+ export {
1795
+ createVercelDirectProvider,
1796
+ createRailwayDirectProvider,
1797
+ createExeDevDirectProvider,
1798
+ createE2BDirectProvider,
1799
+ createDirectGittermClient,
1800
+ createDaytonaDirectProvider,
1801
+ createAsciiDirectProvider
1802
+ };