@gitterm/sdk 0.4.0 → 0.6.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.
@@ -1,6 +1,5 @@
1
1
  // src/direct/client.ts
2
2
  import { randomUUID } from "node:crypto";
3
- import { createOpencodeClient as createOpencodeClient2 } from "@opencode-ai/sdk";
4
3
 
5
4
  // ../agent-runtime/src/normalize.ts
6
5
  var OPENCODE_CUSTOM_OPTION_LABEL = /^type your own answer\W*$/i;
@@ -32,103 +31,6 @@ function normalizeQuestion(question) {
32
31
  return changed ? { ...question, options, custom } : question;
33
32
  }
34
33
 
35
- // ../agent-runtime/src/opencode.ts
36
- import { createOpencodeClient } from "@opencode-ai/sdk";
37
-
38
- // ../agent-runtime/src/types.ts
39
- function parseModelRef(model) {
40
- if (!model)
41
- return;
42
- const separator = model.indexOf("/");
43
- if (separator <= 0 || separator === model.length - 1) {
44
- throw new Error('model must use the "provider/model" format');
45
- }
46
- return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
47
- }
48
- function permissionTitle(permission, patterns) {
49
- return patterns.length > 0 ? `${permission}: ${patterns.join(", ")}` : permission;
50
- }
51
- function sessionStatusOf(status) {
52
- return status === "busy" ? "busy" : status === "retry" ? "retry" : "idle";
53
- }
54
- function missingSessionSnapshot() {
55
- return {
56
- sessionExists: false,
57
- busy: false,
58
- retry: false,
59
- messages: [],
60
- finalText: null,
61
- assistant: { exists: false, completed: false, error: null },
62
- pendingInputs: []
63
- };
64
- }
65
- var TOOL_OUTPUT_LIMIT = 4000;
66
- function truncateToolOutput(value) {
67
- return value.length > TOOL_OUTPUT_LIMIT ? `${value.slice(0, TOOL_OUTPUT_LIMIT)}
68
- …[truncated ${value.length - TOOL_OUTPUT_LIMIT} chars]` : value;
69
- }
70
-
71
- // ../agent-runtime/src/opencode.ts
72
- function isOpencodeRunMessage(info, messageId) {
73
- return info.id === messageId || info.role === "assistant" && info.parentID === messageId;
74
- }
75
- function snapshotParts(parts) {
76
- const result = [];
77
- for (const part of parts) {
78
- if (part.type === "text") {
79
- if (!part.ignored && part.text.trim())
80
- result.push({ type: "text", text: part.text });
81
- } else if (part.type === "tool") {
82
- const state = part.state;
83
- result.push({
84
- type: "tool",
85
- callId: part.callID,
86
- tool: part.tool,
87
- status: state.status,
88
- title: "title" in state && state.title ? state.title : null,
89
- input: state.input,
90
- output: state.status === "completed" ? truncateToolOutput(state.output) : null,
91
- error: state.status === "error" ? state.error : null,
92
- startedAt: "time" in state ? new Date(state.time.start).toISOString() : null,
93
- completedAt: (state.status === "completed" || state.status === "error") && state.time.end ? new Date(state.time.end).toISOString() : null
94
- });
95
- }
96
- }
97
- return result;
98
- }
99
- function findLastOpencodeRunAssistant(messages, messageId) {
100
- return messages.findLast((message) => message.info.role === "assistant" && isOpencodeRunMessage(message.info, messageId));
101
- }
102
- function formatOpencodeError(error, model) {
103
- if (!error)
104
- return "OpenCode request failed";
105
- if (typeof error === "string")
106
- return error;
107
- let message;
108
- if (typeof error === "object" && "data" in error) {
109
- const data = error.data;
110
- if (typeof data?.message === "string")
111
- message = data.message;
112
- const name = "name" in error ? error.name : undefined;
113
- const providerID = typeof data?.providerID === "string" ? data.providerID : model?.providerID;
114
- const authenticationFailed = name === "ProviderAuthError" || (message ? /invalid api key|authentication|unauthorized/i.test(message) : false);
115
- if (authenticationFailed && providerID) {
116
- const selectedModel = model ? ` for model "${model.providerID}/${model.modelID}"` : "";
117
- return `Model provider credential "${providerID}" was rejected${selectedModel}: ${message ?? "Authentication failed"}`;
118
- }
119
- }
120
- return message ?? JSON.stringify(error) ?? "OpenCode request failed";
121
- }
122
- function createWorkspaceOpencodeClient(input) {
123
- const authorization = input.password ? `Basic ${Buffer.from(`opencode:${input.password}`).toString("base64")}` : undefined;
124
- return createOpencodeClient({
125
- baseUrl: input.url,
126
- directory: input.directory,
127
- headers: { ...input.headers, ...authorization ? { Authorization: authorization } : {} },
128
- fetch: input.fetch
129
- });
130
- }
131
-
132
34
  // ../agent-runtime/src/http.ts
133
35
  class RuntimeHttpError extends Error {
134
36
  status;
@@ -149,7 +51,7 @@ function authorizationHeader(password) {
149
51
  }
150
52
  function runtimeUrl(target, path) {
151
53
  const url = new URL(`${target.url.replace(/\/$/, "")}${path}`);
152
- if (target.api === "v2" && target.directory && !url.searchParams.has("location")) {
54
+ if (target.directory && !url.searchParams.has("location")) {
153
55
  url.searchParams.set("location", JSON.stringify({ directory: target.directory }));
154
56
  }
155
57
  return url.toString();
@@ -290,233 +192,37 @@ function asStringArray(value) {
290
192
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
291
193
  }
292
194
 
293
- // ../agent-runtime/src/v1.ts
294
- function createV1Runtime(target) {
295
- const client = () => createWorkspaceOpencodeClient(target);
296
- const http = createRuntimeHttp(target);
297
- const directoryQuery = `?directory=${encodeURIComponent(target.directory)}`;
298
- return {
299
- api: "v1",
300
- async createSession(input) {
301
- const created = await client().session.create({
302
- body: input.title ? { title: input.title } : undefined,
303
- query: { directory: target.directory }
304
- });
305
- if (created.error || !created.data)
306
- throw new Error(formatOpencodeError(created.error));
307
- return { id: created.data.id, title: created.data.title };
308
- },
309
- async prompt(input) {
310
- const model = parseModelRef(input.model);
311
- const prompted = await client().session.promptAsync({
312
- path: { id: input.sessionId },
313
- query: { directory: target.directory },
314
- body: {
315
- messageID: input.messageId,
316
- parts: [{ type: "text", text: input.prompt }],
317
- agent: input.agent,
318
- model
319
- }
320
- });
321
- if (prompted.error)
322
- throw new Error(formatOpencodeError(prompted.error));
323
- },
324
- async abort(sessionId) {
325
- const result = await client().session.abort({
326
- path: { id: sessionId },
327
- query: { directory: target.directory }
328
- });
329
- if (result.error)
330
- throw new Error(formatOpencodeError(result.error));
331
- },
332
- async deleteSession(sessionId) {
333
- const result = await client().session.delete({
334
- path: { id: sessionId },
335
- query: { directory: target.directory }
336
- });
337
- if (result.error)
338
- throw new Error(formatOpencodeError(result.error));
339
- },
340
- async snapshot(sessionId, messageId) {
341
- const api = client();
342
- const [session, statuses, messages, permissions, questions] = await Promise.all([
343
- api.session.get({
344
- path: { id: sessionId },
345
- query: { directory: target.directory },
346
- signal: target.signal
347
- }),
348
- api.session.status({ query: { directory: target.directory }, signal: target.signal }),
349
- api.session.messages({
350
- path: { id: sessionId },
351
- query: { directory: target.directory },
352
- signal: target.signal
353
- }),
354
- http.json(`/permission${directoryQuery}`),
355
- http.json(`/question${directoryQuery}`)
356
- ]);
357
- if (session.response.status === 404)
358
- return missingSessionSnapshot();
359
- if (session.error || !session.data)
360
- throw new Error(formatOpencodeError(session.error));
361
- if (statuses.error || !statuses.data)
362
- throw new Error(formatOpencodeError(statuses.error));
363
- if (messages.error || !messages.data)
364
- throw new Error(formatOpencodeError(messages.error));
365
- const status = statuses.data[sessionId];
366
- const userIndex = messages.data.findIndex((message) => message.info.id === messageId);
367
- const superseded = userIndex >= 0 && messages.data.slice(userIndex + 1).some((message) => message.info.role === "user");
368
- const assistant = findLastOpencodeRunAssistant(messages.data, messageId);
369
- const assistantInfo = assistant?.info.role === "assistant" ? assistant.info : undefined;
370
- const assistantModel = assistantInfo ? { providerID: assistantInfo.providerID, modelID: assistantInfo.modelID } : undefined;
371
- const finalText = assistant ? textOf(assistant.parts) : "";
372
- const pendingInputs = [
373
- ...(Array.isArray(permissions) ? permissions : []).map(asRecord).filter((permission) => permission.sessionID === sessionId).map(permissionRequest),
374
- ...(Array.isArray(questions) ? questions : []).map(asRecord).filter((question) => question.sessionID === sessionId).map(questionRequest)
375
- ];
376
- return {
377
- sessionExists: true,
378
- superseded,
379
- busy: status?.type === "busy",
380
- retry: status?.type === "retry",
381
- messages: messages.data.filter((message) => isOpencodeRunMessage(message.info, messageId)).map((message) => ({
382
- id: message.info.id,
383
- role: message.info.role,
384
- createdAt: new Date(message.info.time.created).toISOString(),
385
- completedAt: message.info.role === "assistant" && message.info.time.completed ? new Date(message.info.time.completed).toISOString() : null,
386
- text: textOf(message.parts),
387
- parts: snapshotParts(message.parts),
388
- error: message.info.role === "assistant" && message.info.error ? formatOpencodeError(message.info.error, {
389
- providerID: message.info.providerID,
390
- modelID: message.info.modelID
391
- }) : null
392
- })),
393
- finalText: finalText || null,
394
- assistant: {
395
- exists: Boolean(assistant),
396
- completed: assistantInfo?.time.completed != null,
397
- error: assistantInfo?.error ? {
398
- kind: assistantInfo.error.name === "MessageAbortedError" ? "aborted" : "error",
399
- message: formatOpencodeError(assistantInfo.error, assistantModel)
400
- } : null
401
- },
402
- pendingInputs: superseded ? [] : pendingInputs
403
- };
404
- },
405
- subscribe: (signal) => signalStream(target, `/event${directoryQuery}`, parseV1Signal, signal),
406
- async replyPermission(sessionId, requestId, reply) {
407
- const result = await client().postSessionIdPermissionsPermissionId({
408
- path: { id: sessionId, permissionID: requestId },
409
- query: { directory: target.directory },
410
- body: { response: reply }
411
- });
412
- if (result.response.status === 404)
413
- throw new RuntimeHttpError(404, "", null);
414
- if (result.error)
415
- throw new Error(formatOpencodeError(result.error));
416
- },
417
- async replyQuestion(_sessionId, request, answers) {
418
- await http.send(`/question/${encodeURIComponent(request.id)}/reply${directoryQuery}`, {
419
- method: "POST",
420
- json: { answers }
421
- });
422
- },
423
- async rejectQuestion(_sessionId, requestId) {
424
- await http.send(`/question/${encodeURIComponent(requestId)}/reject${directoryQuery}`, {
425
- method: "POST",
426
- json: {}
427
- });
428
- }
429
- };
195
+ // ../agent-runtime/src/types.ts
196
+ function parseModelRef(model) {
197
+ if (!model)
198
+ return;
199
+ const separator = model.indexOf("/");
200
+ if (separator <= 0 || separator === model.length - 1) {
201
+ throw new Error('model must use the "provider/model" format');
202
+ }
203
+ return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
430
204
  }
431
- function textOf(parts) {
432
- return parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.text ?? "").join(`
433
- `).trim();
205
+ function permissionTitle(permission, patterns) {
206
+ return patterns.length > 0 ? `${permission}: ${patterns.join(", ")}` : permission;
434
207
  }
435
- function permissionRequest(raw) {
436
- const tool = asRecord(raw.tool);
437
- const permission = asString(raw.permission) ?? asString(raw.type) ?? "permission";
438
- const patterns = asStringArray(raw.patterns).length > 0 ? asStringArray(raw.patterns) : asStringArray(Array.isArray(raw.pattern) ? raw.pattern : [raw.pattern]);
439
- const createdMs = asRecord(raw.time).created;
440
- return {
441
- id: asString(raw.id) ?? "",
442
- kind: "permission",
443
- createdAt: typeof createdMs === "number" ? new Date(createdMs).toISOString() : null,
444
- toolCallId: asString(tool.callID) ?? asString(raw.callID),
445
- permission,
446
- patterns,
447
- always: asStringArray(raw.always),
448
- title: asString(raw.title) ?? permissionTitle(permission, patterns)
449
- };
208
+ function sessionStatusOf(status) {
209
+ return status === "busy" ? "busy" : status === "retry" ? "retry" : "idle";
450
210
  }
451
- function questionRequest(raw) {
452
- const tool = asRecord(raw.tool);
453
- const questions = Array.isArray(raw.questions) ? raw.questions.map(asRecord) : [];
211
+ function missingSessionSnapshot() {
454
212
  return {
455
- id: asString(raw.id) ?? "",
456
- kind: "question",
457
- createdAt: null,
458
- toolCallId: asString(tool.callID),
459
- questions: questions.map((question, index) => normalizeQuestion({
460
- key: `q${index}`,
461
- header: asString(question.header) ?? `Question ${index + 1}`,
462
- question: asString(question.question) ?? "",
463
- options: (Array.isArray(question.options) ? question.options.map(asRecord) : []).map((option) => ({
464
- label: asString(option.label) ?? "",
465
- description: asString(option.description) ?? ""
466
- })),
467
- multiple: question.multiple === true,
468
- custom: question.custom !== false
469
- }))
213
+ sessionExists: false,
214
+ busy: false,
215
+ retry: false,
216
+ messages: [],
217
+ finalText: null,
218
+ assistant: { exists: false, completed: false, error: null },
219
+ pendingInputs: []
470
220
  };
471
221
  }
472
- function changed(sessionId) {
473
- return sessionId ? { type: "session.changed", sessionId } : null;
474
- }
475
- function parseV1Signal(raw) {
476
- const properties = asRecord(raw.properties);
477
- switch (raw.type) {
478
- case "message.updated":
479
- case "message.removed":
480
- return changed(asString(asRecord(properties.info).sessionID) ?? asString(properties.sessionID));
481
- case "message.part.updated":
482
- case "message.part.removed":
483
- return changed(asString(asRecord(properties.part).sessionID) ?? asString(properties.sessionID));
484
- case "session.status": {
485
- const sessionId = asString(properties.sessionID);
486
- const status = sessionStatusOf(asString(asRecord(properties.status).type));
487
- return sessionId ? { type: "session.status", sessionId, status } : null;
488
- }
489
- case "session.idle": {
490
- const sessionId = asString(properties.sessionID);
491
- return sessionId ? { type: "session.status", sessionId, status: "idle" } : null;
492
- }
493
- case "session.error": {
494
- const sessionId = asString(properties.sessionID);
495
- return sessionId ? { type: "session.error", sessionId, message: formatOpencodeError(properties.error) } : null;
496
- }
497
- case "session.deleted": {
498
- const sessionId = asString(asRecord(properties.info).id);
499
- return sessionId ? { type: "session.deleted", sessionId } : null;
500
- }
501
- case "permission.asked":
502
- case "permission.updated": {
503
- const sessionId = asString(properties.sessionID);
504
- return sessionId ? { type: "input.asked", sessionId, request: permissionRequest(properties) } : null;
505
- }
506
- case "question.asked": {
507
- const sessionId = asString(properties.sessionID);
508
- return sessionId ? { type: "input.asked", sessionId, request: questionRequest(properties) } : null;
509
- }
510
- case "permission.replied":
511
- case "question.replied":
512
- case "question.rejected": {
513
- const sessionId = asString(properties.sessionID);
514
- const requestId = asString(properties.requestID) ?? asString(properties.permissionID);
515
- return sessionId && requestId ? { type: "input.resolved", sessionId, requestId } : null;
516
- }
517
- default:
518
- return null;
519
- }
222
+ var TOOL_OUTPUT_LIMIT = 4000;
223
+ function truncateToolOutput(value) {
224
+ return value.length > TOOL_OUTPUT_LIMIT ? `${value.slice(0, TOOL_OUTPUT_LIMIT)}
225
+ …[truncated ${value.length - TOOL_OUTPUT_LIMIT} chars]` : value;
520
226
  }
521
227
 
522
228
  // ../agent-runtime/src/v2.ts
@@ -538,7 +244,6 @@ function createV2Runtime(target) {
538
244
  }
539
245
  }
540
246
  return {
541
- api: "v2",
542
247
  async createSession(input) {
543
248
  const modelRef = parseModelRef(input.model);
544
249
  const created = await http.json("/api/session", {
@@ -602,7 +307,7 @@ function createV2Runtime(target) {
602
307
  } : null
603
308
  },
604
309
  pendingInputs: superseded ? [] : [
605
- ...(permissions.data ?? []).map(asRecord).map(permissionRequest2),
310
+ ...(permissions.data ?? []).map(asRecord).map(permissionRequest),
606
311
  ...(forms.data ?? []).map(asRecord).map(formRequest)
607
312
  ]
608
313
  };
@@ -720,7 +425,7 @@ function normalizeMessage(message) {
720
425
  error: message.error ? asString(error.message) ?? "Agent request failed" : null
721
426
  };
722
427
  }
723
- function permissionRequest2(raw) {
428
+ function permissionRequest(raw) {
724
429
  const action = asString(raw.action) ?? "permission";
725
430
  const patterns = asStringArray(raw.resources);
726
431
  return {
@@ -771,15 +476,15 @@ function parseV2Signal(raw) {
771
476
  const data = asRecord(raw.data);
772
477
  const type = asString(raw.type) ?? "";
773
478
  const sessionId = asString(data.sessionID);
774
- const changed2 = () => sessionId ? { type: "session.changed", sessionId } : null;
479
+ const changed = () => sessionId ? { type: "session.changed", sessionId } : null;
775
480
  if (type.startsWith("session.step.") || type.startsWith("session.tool."))
776
- return changed2();
481
+ return changed();
777
482
  switch (type) {
778
483
  case "session.text.ended":
779
484
  case "session.reasoning.ended":
780
485
  case "session.message.content.updated":
781
486
  case "session.inbox.delivered":
782
- return changed2();
487
+ return changed();
783
488
  case "session.execution.started":
784
489
  return sessionId ? { type: "session.status", sessionId, status: "busy" } : null;
785
490
  case "session.execution.succeeded":
@@ -804,7 +509,7 @@ function parseV2Signal(raw) {
804
509
  return deleted ? { type: "session.deleted", sessionId: deleted } : null;
805
510
  }
806
511
  case "permission.asked":
807
- return sessionId ? { type: "input.asked", sessionId, request: permissionRequest2(data) } : null;
512
+ return sessionId ? { type: "input.asked", sessionId, request: permissionRequest(data) } : null;
808
513
  case "form.created": {
809
514
  const form = asRecord(data.form);
810
515
  const formSession = asString(form.sessionID);
@@ -860,9 +565,10 @@ function deriveRunState(snapshot, context) {
860
565
  return { status: "running", errorMessage: null };
861
566
  return { status: "completed", errorMessage: null };
862
567
  }
568
+
863
569
  // ../agent-runtime/src/index.ts
864
570
  function getRuntime(target) {
865
- return target.api === "v2" ? createV2Runtime(target) : createV1Runtime(target);
571
+ return createV2Runtime(target);
866
572
  }
867
573
 
868
574
  // ../agent-runtime/src/replies.ts
@@ -1077,7 +783,6 @@ function directRuntime(workspace, fetchImpl, signal) {
1077
783
  directory: workspace.runtime.directory,
1078
784
  password: workspace.runtime.password ?? null,
1079
785
  headers: workspace.runtime.headers,
1080
- api: workspace.opencodeApi,
1081
786
  fetch: fetchImpl,
1082
787
  signal
1083
788
  });
@@ -1510,6 +1215,220 @@ main().catch(() => {
1510
1215
  });
1511
1216
  `;
1512
1217
 
1218
+ // ../agent-runtime/src/opencode-credentials.ts
1219
+ var OPENCODE_CREDENTIALS_PATH = "~/.gitterm/opencode/credentials.json";
1220
+ var OPENCODE_CREDENTIALS_PLUGIN_PATH = "~/.config/opencode/plugins/gitterm-credentials.js";
1221
+ var OPENCODE_CREDENTIAL_LABEL = "Gitterm";
1222
+ function opencodeCredentialFiles(entries) {
1223
+ return [
1224
+ {
1225
+ path: OPENCODE_CREDENTIALS_PATH,
1226
+ contentBase64: Buffer.from(JSON.stringify(entries)).toString("base64"),
1227
+ mode: 384
1228
+ },
1229
+ {
1230
+ path: OPENCODE_CREDENTIALS_PLUGIN_PATH,
1231
+ contentBase64: Buffer.from(OPENCODE_CREDENTIALS_PLUGIN).toString("base64")
1232
+ }
1233
+ ];
1234
+ }
1235
+ var OPENCODE_CREDENTIALS_PLUGIN = String.raw`
1236
+ import { readFileSync } from "node:fs";
1237
+ import os from "node:os";
1238
+ import path from "node:path";
1239
+
1240
+ const METHOD_ID = "gitterm-import";
1241
+ const FILE = path.join(process.env.HOME || os.homedir(), ".gitterm/opencode/credentials.json");
1242
+ const DEVICE_FLOW = ["github-copilot", "opencode", "xai"];
1243
+ // OpenCode only enables ChatGPT routing for its own method IDs. The browser method cannot
1244
+ // complete in a remote workspace (its callback is loopback), so managed accounts take it over.
1245
+ const MANAGED_METHOD = { openai: "chatgpt-browser" };
1246
+ const log = (message, detail) => console.log("[gitterm-credentials] " + message + (detail ? ": " + detail : ""));
1247
+
1248
+ // Groups entries by integration and orders each group so the active account is created
1249
+ // last: OpenCode makes the most recently created credential the active one.
1250
+ function readCredentials() {
1251
+ try {
1252
+ const entries = JSON.parse(readFileSync(FILE, "utf8"));
1253
+ if (!Array.isArray(entries)) return new Map();
1254
+ const groups = new Map();
1255
+ for (const entry of entries) {
1256
+ const integration = typeof entry?.integration === "string" ? entry.integration.replace(/\/+$/, "") : "";
1257
+ const value = entry?.value;
1258
+ if (!integration || typeof entry.label !== "string" || !entry.label) continue;
1259
+ if (!value || (value.type !== "api" && value.type !== "oauth")) continue;
1260
+ if (!groups.has(integration)) groups.set(integration, []);
1261
+ groups.get(integration).push({ label: entry.label, active: entry.active === true, value });
1262
+ }
1263
+ for (const group of groups.values()) {
1264
+ const active = group.findIndex((entry) => entry.active);
1265
+ if (active !== -1 && active !== group.length - 1) group.push(...group.splice(active, 1));
1266
+ }
1267
+ return groups;
1268
+ } catch (error) {
1269
+ if (error?.code !== "ENOENT") log("unreadable credentials file", String(error));
1270
+ return new Map();
1271
+ }
1272
+ }
1273
+
1274
+ function builtinOAuthMethod(id, methods) {
1275
+ const preferred = id === "openai" ? "chatgpt-browser" : DEVICE_FLOW.includes(id) ? "device" : "oauth";
1276
+ const oauth = methods.filter((method) => method.type === "oauth" && method.id !== METHOD_ID);
1277
+ return (oauth.find((method) => method.id === preferred) ?? oauth[0])?.id;
1278
+ }
1279
+
1280
+ // Only answers the key method asks for: OpenCode rejects answers when it shows no form
1281
+ // (for example because CLOUDFLARE_ACCOUNT_ID is already set).
1282
+ function keyAnswer(methods, metadata) {
1283
+ const form = methods.find((method) => method.type === "key")?.form;
1284
+ if (!metadata || !Array.isArray(form)) return undefined;
1285
+ const answer = {};
1286
+ for (const field of form) {
1287
+ if (typeof field?.key === "string" && typeof metadata[field.key] === "string") answer[field.key] = metadata[field.key];
1288
+ }
1289
+ return Object.keys(answer).length ? answer : undefined;
1290
+ }
1291
+
1292
+ const isManaged = (value) => value?.type === "oauth" && typeof value.metadata?.gittermCredentialId === "string";
1293
+
1294
+ // One request per credential at a time; GitTerm serializes across workspaces.
1295
+ const inflight = new Map();
1296
+ function managedRefresh(credential) {
1297
+ const id = credential.metadata?.gittermCredentialId;
1298
+ if (typeof id !== "string") return Promise.resolve(credential);
1299
+ if (!inflight.has(id)) {
1300
+ inflight.set(id, requestToken(id).then(
1301
+ (token) => ({ ...credential, access: token.access, expires: token.expires, metadata: { ...credential.metadata, ...token.metadata } }),
1302
+ ).finally(() => inflight.delete(id)));
1303
+ }
1304
+ return inflight.get(id);
1305
+ }
1306
+
1307
+ async function requestToken(credentialId) {
1308
+ const url = process.env.WORKSPACE_API_URL;
1309
+ const auth = process.env.WORKSPACE_AGENT_AUTH_TOKEN;
1310
+ if (!url || !auth) throw new Error("GitTerm workspace identity is unavailable");
1311
+ const response = await fetch(url.replace(/\/$/, "") + "/workspaceOps.modelCredential", {
1312
+ method: "POST",
1313
+ headers: { Authorization: "Bearer " + auth, "Content-Type": "application/json" },
1314
+ body: JSON.stringify({ credentialId }),
1315
+ redirect: "error",
1316
+ signal: AbortSignal.timeout(30000),
1317
+ });
1318
+ const body = await response.json().catch(() => undefined);
1319
+ const data = body?.result?.data?.json ?? body?.result?.data;
1320
+ if (!response.ok || typeof data?.access !== "string" || typeof data?.expires !== "number") {
1321
+ const message = body?.error?.json?.message ?? body?.error?.message ?? "status " + response.status;
1322
+ log("GitTerm token request failed", message);
1323
+ throw new Error("GitTerm could not provide a token: " + message);
1324
+ }
1325
+ return data;
1326
+ }
1327
+
1328
+ function toOAuthCredential(methodID, value) {
1329
+ const metadata = {
1330
+ ...(value.metadata && typeof value.metadata === "object" ? value.metadata : {}),
1331
+ ...(value.accountId ? { accountID: value.accountId } : {}),
1332
+ ...(value.enterpriseUrl ? { enterpriseUrl: value.enterpriseUrl } : {}),
1333
+ };
1334
+ return {
1335
+ type: "oauth",
1336
+ methodID,
1337
+ refresh: value.refresh,
1338
+ access: value.access ?? "",
1339
+ expires: value.expires ?? 0,
1340
+ ...(Object.keys(metadata).length ? { metadata } : {}),
1341
+ };
1342
+ }
1343
+
1344
+ async function importOAuth(ctx, id, label) {
1345
+ const attempt = (await ctx.integration.oauth.connect({ integrationID: id, methodID: METHOD_ID, label })).data;
1346
+ for (let i = 0; i < 100; i++) {
1347
+ const status = (await ctx.integration.oauth.status({ integrationID: id, attemptID: attempt.attemptID })).data;
1348
+ if (status.status !== "pending") return status;
1349
+ await new Promise((resolve) => setTimeout(resolve, 100));
1350
+ }
1351
+ return { status: "failed", message: "timed out" };
1352
+ }
1353
+
1354
+ export default {
1355
+ id: "gitterm-credentials",
1356
+ async setup(ctx) {
1357
+ const groups = readCredentials();
1358
+ // One import method per integration; the pending OAuth entry is handed over per attempt.
1359
+ const pending = new Map();
1360
+ const oauthIntegrations = [...groups].filter(([, entries]) => entries.some((entry) => entry.value.type === "oauth"));
1361
+ if (oauthIntegrations.length) {
1362
+ await ctx.integration.transform((editor) => {
1363
+ for (const [id, entries] of oauthIntegrations) {
1364
+ if (!editor.get(id)) continue;
1365
+ const builtin = builtinOAuthMethod(id, editor.method.list(id));
1366
+ const managed = entries.some((entry) => isManaged(entry.value));
1367
+ if (!builtin && !managed) continue;
1368
+ const managedMethod = MANAGED_METHOD[id] ?? METHOD_ID;
1369
+ const authorize = async () => {
1370
+ const value = pending.get(id);
1371
+ return {
1372
+ url: "",
1373
+ instructions: "Importing the credential provided by Gitterm",
1374
+ mode: "auto",
1375
+ callback: Promise.resolve(toOAuthCredential(isManaged(value) ? managedMethod : builtin, value)),
1376
+ };
1377
+ };
1378
+ editor.method.update({
1379
+ integrationID: id,
1380
+ method: { id: METHOD_ID, type: "oauth", label: "Gitterm" },
1381
+ authorize,
1382
+ refresh: managedRefresh,
1383
+ });
1384
+ if (managed && managedMethod !== METHOD_ID) {
1385
+ editor.method.update({
1386
+ integrationID: id,
1387
+ method: { id: managedMethod, type: "oauth", label: "Managed by Gitterm" },
1388
+ authorize,
1389
+ refresh: managedRefresh,
1390
+ });
1391
+ }
1392
+ }
1393
+ });
1394
+ }
1395
+ for (const [id, entries] of groups) {
1396
+ const info = await ctx.integration.get({ integrationID: id }).then((result) => result.data, () => undefined);
1397
+ if (!info) {
1398
+ log("unknown integration, skipping", id);
1399
+ continue;
1400
+ }
1401
+ const existing = new Set(
1402
+ info.connections.filter((connection) => connection.type === "credential").map((connection) => connection.label),
1403
+ );
1404
+ const importable = info.methods.some((method) => method.type === "oauth" && method.id === METHOD_ID);
1405
+ for (const entry of entries) {
1406
+ const target = id + " (" + entry.label + ")";
1407
+ if (existing.has(entry.label)) continue;
1408
+ try {
1409
+ if (entry.value.type === "api") {
1410
+ const answer = keyAnswer(info.methods, entry.value.metadata);
1411
+ await ctx.integration.connect.key({ integrationID: id, key: entry.value.key, label: entry.label, ...(answer ? { answer } : {}) });
1412
+ log("imported API key", target);
1413
+ continue;
1414
+ }
1415
+ if (!importable) {
1416
+ log("no OAuth method available, skipping", target);
1417
+ continue;
1418
+ }
1419
+ pending.set(id, entry.value);
1420
+ const result = await importOAuth(ctx, id, entry.label);
1421
+ if (result.status === "complete") log("imported OAuth credential", target);
1422
+ else log("OAuth import " + result.status, target + (result.message ? " (" + result.message + ")" : ""));
1423
+ } catch (error) {
1424
+ log("import failed", target + " (" + String(error?.message ?? error) + ")");
1425
+ }
1426
+ }
1427
+ }
1428
+ },
1429
+ };
1430
+ `;
1431
+
1513
1432
  // src/direct/provisioning.ts
1514
1433
  var DIRECT_OPENCODE_PORT = 4096;
1515
1434
  var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
@@ -1621,7 +1540,12 @@ function buildDirectProvisioningPlan(input) {
1621
1540
  if (credentials.has(providerName)) {
1622
1541
  throw new Error(`Duplicate model credential: ${providerName}`);
1623
1542
  }
1624
- credentials.set(providerName, directModelAuth(credential));
1543
+ credentials.set(providerName, {
1544
+ integration: providerName,
1545
+ label: credential.label?.trim() || OPENCODE_CREDENTIAL_LABEL,
1546
+ active: true,
1547
+ value: directModelAuth(credential)
1548
+ });
1625
1549
  }
1626
1550
  const configuredPlugins = Array.isArray(input.opencode?.config?.plugin) ? input.opencode.config.plugin.filter((plugin) => typeof plugin === "string") : [];
1627
1551
  const plugins = [...new Set([...configuredPlugins, ...input.opencode?.plugins ?? []])];
@@ -1648,10 +1572,7 @@ function buildDirectProvisioningPlan(input) {
1648
1572
  } : undefined);
1649
1573
  const files = [
1650
1574
  ...github?.files ?? [],
1651
- {
1652
- path: "~/.local/share/opencode/auth.json",
1653
- contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
1654
- },
1575
+ ...opencodeCredentialFiles([...credentials.values()]),
1655
1576
  {
1656
1577
  path: "~/.config/opencode/opencode.json",
1657
1578
  contentBase64: base64(JSON.stringify(config))
@@ -1782,8 +1703,8 @@ function basicAuthHeader(password) {
1782
1703
  return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
1783
1704
  }
1784
1705
  async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
1785
- const deadline2 = Date.now() + timeoutMs;
1786
- while (Date.now() < deadline2) {
1706
+ const deadline = Date.now() + timeoutMs;
1707
+ while (Date.now() < deadline) {
1787
1708
  const controller = new AbortController;
1788
1709
  const timer = setTimeout(() => controller.abort(), 5000);
1789
1710
  const ready = await fetch(runtime.url, {
@@ -1892,7 +1813,7 @@ function createAsciiDirectProvider(config) {
1892
1813
  });
1893
1814
  await runCommand(handle.boxId, `mkdir -p ${shellQuote(directory)}`);
1894
1815
  for (const command of config.runtimeSetupCommands ?? [
1895
- "npm install -g opencode-ai --no-audit --fund=false"
1816
+ "npm install -g @opencode/cli@2 --no-audit --fund=false"
1896
1817
  ]) {
1897
1818
  await runCommand(handle.boxId, command, undefined, 600);
1898
1819
  }
@@ -2335,8 +2256,8 @@ function createExeDevDirectProvider(config) {
2335
2256
  return token;
2336
2257
  }
2337
2258
  async function waitUntilRunning(vmName) {
2338
- const deadline2 = Date.now() + 60000;
2339
- while (Date.now() < deadline2) {
2259
+ const deadline = Date.now() + 60000;
2260
+ while (Date.now() < deadline) {
2340
2261
  const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
2341
2262
  if (status === "running")
2342
2263
  return;
@@ -2410,9 +2331,9 @@ function createExeDevDirectProvider(config) {
2410
2331
  await startRuntime(handle);
2411
2332
  await execute(`share port ${vmName} ${handle.serve.port}`);
2412
2333
  await execute(`share set-private ${vmName}`);
2413
- const directRuntime2 = await runtime(handle, input.password);
2414
- await waitForDirectRuntime(directRuntime2);
2415
- return { externalId: serializeHandle2(handle), runtime: directRuntime2 };
2334
+ const directRuntime = await runtime(handle, input.password);
2335
+ await waitForDirectRuntime(directRuntime);
2336
+ return { externalId: serializeHandle2(handle), runtime: directRuntime };
2416
2337
  } catch (error) {
2417
2338
  if (handle.owned) {
2418
2339
  await execute(`rm ${vmName}`).catch(() => {
@@ -2447,9 +2368,9 @@ function createExeDevDirectProvider(config) {
2447
2368
  await execute(`share set-private ${handle.vmName}`);
2448
2369
  if (!workspace.runtime.password)
2449
2370
  throw new Error("exe.dev runtime password is missing");
2450
- const directRuntime2 = await runtime(handle, workspace.runtime.password);
2451
- await waitForDirectRuntime(directRuntime2);
2452
- return directRuntime2;
2371
+ const directRuntime = await runtime(handle, workspace.runtime.password);
2372
+ await waitForDirectRuntime(directRuntime);
2373
+ return directRuntime;
2453
2374
  },
2454
2375
  async terminate(workspace) {
2455
2376
  const handle = parseHandle3(workspace.externalId);
@@ -2625,8 +2546,8 @@ function createRailwayDirectProvider(config) {
2625
2546
  };
2626
2547
  const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
2627
2548
  const waitForDeployment = async (serviceId, previousDeploymentId) => {
2628
- const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2629
- while (Date.now() < deadline2) {
2549
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2550
+ while (Date.now() < deadline) {
2630
2551
  const deployment = await latestDeployment(serviceId);
2631
2552
  if (deployment && deployment.id !== previousDeploymentId) {
2632
2553
  if (deployment.status === "SUCCESS")
@@ -2640,8 +2561,8 @@ function createRailwayDirectProvider(config) {
2640
2561
  throw new Error("Timed out waiting for Railway deployment");
2641
2562
  };
2642
2563
  const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
2643
- const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2644
- while (Date.now() < deadline2) {
2564
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2565
+ while (Date.now() < deadline) {
2645
2566
  const deployment = await latestDeployment(serviceId);
2646
2567
  if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
2647
2568
  return;
@@ -2884,7 +2805,7 @@ function createVercelDirectProvider(config) {
2884
2805
  });
2885
2806
  try {
2886
2807
  await run(sandbox, `mkdir -p ${shellQuote(handle.directory)}`);
2887
- for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g opencode-ai --no-audit --fund=false"])) {
2808
+ for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g @opencode/cli@2 --no-audit --fund=false"])) {
2888
2809
  await run(sandbox, command);
2889
2810
  }
2890
2811
  if (plan.repository) {
@@ -3030,21 +2951,7 @@ function resolveProvider(provider) {
3030
2951
  return createVercelDirectProvider(provider);
3031
2952
  }
3032
2953
  }
3033
- function runtimeClient(workspace, fetchImpl) {
3034
- const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
3035
- return createOpencodeClient2({
3036
- fetch: fetchImpl,
3037
- baseUrl: workspace.runtime.url,
3038
- directory: workspace.runtime.directory,
3039
- headers: {
3040
- ...workspace.runtime.headers,
3041
- ...authorization ? { Authorization: authorization } : {}
3042
- }
3043
- });
3044
- }
3045
2954
  function createAuthClient(workspace, fetchImpl) {
3046
- if (workspace.opencodeApi !== "v2")
3047
- throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
3048
2955
  const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
3049
2956
  return createOpencodeV2Client({
3050
2957
  fetch: fetchImpl,
@@ -3132,7 +3039,6 @@ function createDirectGittermClient(options) {
3132
3039
  url: workspace.runtime.url,
3133
3040
  directory: workspace.runtime.directory,
3134
3041
  password: workspace.runtime.password ?? null,
3135
- api: workspace.opencodeApi,
3136
3042
  headers: workspace.runtime.headers,
3137
3043
  fetch: options.fetch
3138
3044
  });
@@ -3157,23 +3063,14 @@ function createDirectGittermClient(options) {
3157
3063
  return authStatus(result.data.data);
3158
3064
  }
3159
3065
  async function startPty(workspace, command, title) {
3160
- const result = await http(workspace).json(workspace.opencodeApi === "v2" ? "/api/pty" : `/pty?directory=${encodeURIComponent(workspace.runtime.directory)}`, {
3066
+ return http(workspace).json("/api/pty", {
3161
3067
  method: "POST",
3162
3068
  json: { command: "bash", args: ["-lc", command], cwd: workspace.runtime.directory, title }
3163
3069
  });
3164
- return result;
3165
3070
  }
3166
3071
  async function setupFile(workspace, name) {
3167
3072
  try {
3168
- if (workspace.opencodeApi === "v2") {
3169
- return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
3170
- }
3171
- const result = await runtimeClient(workspace, options.fetch).file.read({
3172
- query: { directory: workspace.runtime.directory, path: `${SETUP_DIR}/${name}` }
3173
- });
3174
- if (result.error || !result.data || result.data.type !== "text")
3175
- return null;
3176
- return result.data.content.trim();
3073
+ return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
3177
3074
  } catch (error) {
3178
3075
  if (error instanceof RuntimeHttpError && error.status === 404)
3179
3076
  return null;
@@ -3225,20 +3122,10 @@ function createDirectGittermClient(options) {
3225
3122
  if (!providerName) {
3226
3123
  throw new Error("Model credential providerName is required");
3227
3124
  }
3228
- if (workspace.opencodeApi === "v2") {
3229
- if (credential.source !== "apiKey")
3230
- throw new GittermError("BAD_REQUEST", "Use connectOAuth() for v2 credential rotation, or inject an OAuth bundle when creating the workspace");
3231
- directModelAuth(credential);
3232
- await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
3233
- return;
3234
- }
3235
- const result = await runtimeClient(workspace, options.fetch).auth.set({
3236
- path: { id: providerName },
3237
- query: { directory: workspace.runtime.directory },
3238
- body: directModelAuth(credential)
3239
- });
3240
- if (result.error)
3241
- throw new Error(errorMessage(result.error));
3125
+ if (credential.source !== "apiKey")
3126
+ throw new GittermError("BAD_REQUEST", "Use connectOAuth() for OAuth credential rotation, or inject an OAuth bundle when creating the workspace");
3127
+ directModelAuth(credential);
3128
+ await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
3242
3129
  },
3243
3130
  async list(workspace) {
3244
3131
  assertWorkspace(workspace);
@@ -3307,8 +3194,8 @@ function createDirectGittermClient(options) {
3307
3194
  async wait(attempt, workspace, wait = {}) {
3308
3195
  assertAuthAttempt(attempt, workspace);
3309
3196
  const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
3310
- const deadline2 = Date.now() + timeoutMs;
3311
- while (Date.now() <= deadline2) {
3197
+ const deadline = Date.now() + timeoutMs;
3198
+ while (Date.now() <= deadline) {
3312
3199
  const status = await getAuthStatus(attempt, workspace);
3313
3200
  if (status.status === "complete")
3314
3201
  return status;
@@ -3316,7 +3203,7 @@ function createDirectGittermClient(options) {
3316
3203
  throw new Error(status.message);
3317
3204
  if (status.status === "expired")
3318
3205
  throw new Error("OAuth attempt expired");
3319
- const remaining = deadline2 - Date.now();
3206
+ const remaining = deadline - Date.now();
3320
3207
  if (remaining <= 0)
3321
3208
  break;
3322
3209
  await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
@@ -3334,8 +3221,6 @@ function createDirectGittermClient(options) {
3334
3221
  },
3335
3222
  workspaces: {
3336
3223
  async create(input = {}) {
3337
- if (input.opencode?.api && !["v1", "v2"].includes(input.opencode.api))
3338
- throw new GittermError("BAD_REQUEST", "opencode.api must be v1 or v2");
3339
3224
  const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
3340
3225
  if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
3341
3226
  throw new Error(`${provider.name} does not support persistent direct workspaces`);
@@ -3352,7 +3237,6 @@ function createDirectGittermClient(options) {
3352
3237
  status: "running",
3353
3238
  lifecycle,
3354
3239
  runtime: created.runtime,
3355
- opencodeApi: input.opencode?.api ?? "v1",
3356
3240
  setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
3357
3241
  createdAt: new Date().toISOString()
3358
3242
  };
@@ -3407,7 +3291,7 @@ function createDirectGittermClient(options) {
3407
3291
  setupStatus: getSetupStatus,
3408
3292
  async waitForSetup(workspace, wait = {}) {
3409
3293
  const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
3410
- const deadline2 = Date.now() + timeoutMs;
3294
+ const deadline = Date.now() + timeoutMs;
3411
3295
  while (true) {
3412
3296
  const status = await getSetupStatus(workspace);
3413
3297
  if (status.status === "not_requested" || status.status === "succeeded")
@@ -3416,7 +3300,7 @@ function createDirectGittermClient(options) {
3416
3300
  throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
3417
3301
  ${status.log}` : ""}`);
3418
3302
  }
3419
- if (Date.now() >= deadline2) {
3303
+ if (Date.now() >= deadline) {
3420
3304
  throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
3421
3305
  }
3422
3306
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
@@ -3435,7 +3319,7 @@ ${status.log}` : ""}`);
3435
3319
  if (input.model && !/^[^/]+\/.+$/.test(input.model))
3436
3320
  throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
3437
3321
  if (input.waitForSetup) {
3438
- const deadline2 = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
3322
+ const deadline = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
3439
3323
  while (true) {
3440
3324
  aborted(input.signal);
3441
3325
  const status = await getSetupStatus(input.workspace);
@@ -3443,7 +3327,7 @@ ${status.log}` : ""}`);
3443
3327
  break;
3444
3328
  if (status.status === "failed")
3445
3329
  throw new GittermError("BAD_REQUEST", `Workspace setup failed: ${status.log ?? ""}`);
3446
- if (Date.now() >= deadline2)
3330
+ if (Date.now() >= deadline)
3447
3331
  throw new GittermError("TIMEOUT", "Workspace setup timed out");
3448
3332
  await new Promise((resolve) => setTimeout(resolve, 1000));
3449
3333
  }
@@ -3460,9 +3344,9 @@ ${status.log}` : ""}`);
3460
3344
  try {
3461
3345
  aborted(input.signal);
3462
3346
  if (!sessionId) {
3463
- const session2 = await runtime.createSession(input);
3464
- sessionId = session2.id;
3465
- title = session2.title;
3347
+ const session = await runtime.createSession(input);
3348
+ sessionId = session.id;
3349
+ title = session.title;
3466
3350
  }
3467
3351
  aborted(input.signal);
3468
3352
  await runtime.prompt({