@gitterm/sdk 0.3.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);
@@ -823,12 +528,6 @@ function parseV2Signal(raw) {
823
528
  return null;
824
529
  }
825
530
  }
826
- // ../agent-runtime/src/opencode-credentials.ts
827
- var OPENCODE_V2_IMPORT_CREDENTIALS = String.raw`AUTH_FILE="\${OPENCODE_DATA_DIR:-\${XDG_DATA_HOME:-$HOME/.local/share}/opencode}/auth.json"
828
- if [ -f "$AUTH_FILE" ]; then
829
- opencode auth list >/dev/null
830
- rm -f "$AUTH_FILE"
831
- fi`;
832
531
  // ../agent-runtime/src/derive.ts
833
532
  var MISSING_ASSISTANT_GRACE_MS = 1e4;
834
533
  function deriveRunState(snapshot, context) {
@@ -866,9 +565,10 @@ function deriveRunState(snapshot, context) {
866
565
  return { status: "running", errorMessage: null };
867
566
  return { status: "completed", errorMessage: null };
868
567
  }
568
+
869
569
  // ../agent-runtime/src/index.ts
870
570
  function getRuntime(target) {
871
- return target.api === "v2" ? createV2Runtime(target) : createV1Runtime(target);
571
+ return createV2Runtime(target);
872
572
  }
873
573
 
874
574
  // ../agent-runtime/src/replies.ts
@@ -1083,7 +783,6 @@ function directRuntime(workspace, fetchImpl, signal) {
1083
783
  directory: workspace.runtime.directory,
1084
784
  password: workspace.runtime.password ?? null,
1085
785
  headers: workspace.runtime.headers,
1086
- api: workspace.opencodeApi,
1087
786
  fetch: fetchImpl,
1088
787
  signal
1089
788
  });
@@ -1516,6 +1215,220 @@ main().catch(() => {
1516
1215
  });
1517
1216
  `;
1518
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
+
1519
1432
  // src/direct/provisioning.ts
1520
1433
  var DIRECT_OPENCODE_PORT = 4096;
1521
1434
  var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
@@ -1617,14 +1530,22 @@ function buildDirectProvisioningPlan(input) {
1617
1530
  throw new GittermError("MODEL_CREDENTIAL_INVALID", "Direct mode cannot inherit dashboard credentials");
1618
1531
  }
1619
1532
  for (const [key, source] of Object.entries(input.models?.providers ?? {})) {
1620
- const credential = { ...source, providerName: key };
1533
+ const credential = {
1534
+ ...source,
1535
+ providerName: key
1536
+ };
1621
1537
  const providerName = key.trim();
1622
1538
  if (!providerName)
1623
1539
  throw new Error("Model credential providerName is required");
1624
1540
  if (credentials.has(providerName)) {
1625
1541
  throw new Error(`Duplicate model credential: ${providerName}`);
1626
1542
  }
1627
- 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
+ });
1628
1549
  }
1629
1550
  const configuredPlugins = Array.isArray(input.opencode?.config?.plugin) ? input.opencode.config.plugin.filter((plugin) => typeof plugin === "string") : [];
1630
1551
  const plugins = [...new Set([...configuredPlugins, ...input.opencode?.plugins ?? []])];
@@ -1651,10 +1572,7 @@ function buildDirectProvisioningPlan(input) {
1651
1572
  } : undefined);
1652
1573
  const files = [
1653
1574
  ...github?.files ?? [],
1654
- {
1655
- path: "~/.local/share/opencode/auth.json",
1656
- contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
1657
- },
1575
+ ...opencodeCredentialFiles([...credentials.values()]),
1658
1576
  {
1659
1577
  path: "~/.config/opencode/opencode.json",
1660
1578
  contentBase64: base64(JSON.stringify(config))
@@ -1698,11 +1616,7 @@ function buildDirectProvisioningPlan(input) {
1698
1616
  port: DIRECT_OPENCODE_PORT
1699
1617
  },
1700
1618
  setup: {
1701
- beforeAgent: [
1702
- ...(input.opencode?.api ?? "v2") === "v2" ? [OPENCODE_V2_IMPORT_CREDENTIALS] : [],
1703
- ...github ? [github.setup] : [],
1704
- ...input.setup?.beforeAgent ?? []
1705
- ],
1619
+ beforeAgent: [...github ? [github.setup] : [], ...input.setup?.beforeAgent ?? []],
1706
1620
  afterAgent: input.setup?.afterAgent ?? []
1707
1621
  }
1708
1622
  };
@@ -1727,7 +1641,9 @@ function railwayContainerEnvironment(plan) {
1727
1641
  } : {}
1728
1642
  } : {},
1729
1643
  AGENT_FILES_BASE64: base64(JSON.stringify(plan.agent.files)),
1730
- ...beforeAgent.length ? { WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(beforeAgent)) } : {},
1644
+ ...beforeAgent.length ? {
1645
+ WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(beforeAgent))
1646
+ } : {},
1731
1647
  GITTERM_DIRECT_PROVIDER: "railway"
1732
1648
  };
1733
1649
  }
@@ -1787,8 +1703,8 @@ function basicAuthHeader(password) {
1787
1703
  return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
1788
1704
  }
1789
1705
  async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
1790
- const deadline2 = Date.now() + timeoutMs;
1791
- while (Date.now() < deadline2) {
1706
+ const deadline = Date.now() + timeoutMs;
1707
+ while (Date.now() < deadline) {
1792
1708
  const controller = new AbortController;
1793
1709
  const timer = setTimeout(() => controller.abort(), 5000);
1794
1710
  const ready = await fetch(runtime.url, {
@@ -2340,8 +2256,8 @@ function createExeDevDirectProvider(config) {
2340
2256
  return token;
2341
2257
  }
2342
2258
  async function waitUntilRunning(vmName) {
2343
- const deadline2 = Date.now() + 60000;
2344
- while (Date.now() < deadline2) {
2259
+ const deadline = Date.now() + 60000;
2260
+ while (Date.now() < deadline) {
2345
2261
  const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
2346
2262
  if (status === "running")
2347
2263
  return;
@@ -2415,9 +2331,9 @@ function createExeDevDirectProvider(config) {
2415
2331
  await startRuntime(handle);
2416
2332
  await execute(`share port ${vmName} ${handle.serve.port}`);
2417
2333
  await execute(`share set-private ${vmName}`);
2418
- const directRuntime2 = await runtime(handle, input.password);
2419
- await waitForDirectRuntime(directRuntime2);
2420
- 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 };
2421
2337
  } catch (error) {
2422
2338
  if (handle.owned) {
2423
2339
  await execute(`rm ${vmName}`).catch(() => {
@@ -2452,9 +2368,9 @@ function createExeDevDirectProvider(config) {
2452
2368
  await execute(`share set-private ${handle.vmName}`);
2453
2369
  if (!workspace.runtime.password)
2454
2370
  throw new Error("exe.dev runtime password is missing");
2455
- const directRuntime2 = await runtime(handle, workspace.runtime.password);
2456
- await waitForDirectRuntime(directRuntime2);
2457
- return directRuntime2;
2371
+ const directRuntime = await runtime(handle, workspace.runtime.password);
2372
+ await waitForDirectRuntime(directRuntime);
2373
+ return directRuntime;
2458
2374
  },
2459
2375
  async terminate(workspace) {
2460
2376
  const handle = parseHandle3(workspace.externalId);
@@ -2630,8 +2546,8 @@ function createRailwayDirectProvider(config) {
2630
2546
  };
2631
2547
  const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
2632
2548
  const waitForDeployment = async (serviceId, previousDeploymentId) => {
2633
- const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2634
- while (Date.now() < deadline2) {
2549
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2550
+ while (Date.now() < deadline) {
2635
2551
  const deployment = await latestDeployment(serviceId);
2636
2552
  if (deployment && deployment.id !== previousDeploymentId) {
2637
2553
  if (deployment.status === "SUCCESS")
@@ -2645,8 +2561,8 @@ function createRailwayDirectProvider(config) {
2645
2561
  throw new Error("Timed out waiting for Railway deployment");
2646
2562
  };
2647
2563
  const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
2648
- const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2649
- while (Date.now() < deadline2) {
2564
+ const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
2565
+ while (Date.now() < deadline) {
2650
2566
  const deployment = await latestDeployment(serviceId);
2651
2567
  if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
2652
2568
  return;
@@ -3035,21 +2951,7 @@ function resolveProvider(provider) {
3035
2951
  return createVercelDirectProvider(provider);
3036
2952
  }
3037
2953
  }
3038
- function runtimeClient(workspace, fetchImpl) {
3039
- const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
3040
- return createOpencodeClient2({
3041
- fetch: fetchImpl,
3042
- baseUrl: workspace.runtime.url,
3043
- directory: workspace.runtime.directory,
3044
- headers: {
3045
- ...workspace.runtime.headers,
3046
- ...authorization ? { Authorization: authorization } : {}
3047
- }
3048
- });
3049
- }
3050
2954
  function createAuthClient(workspace, fetchImpl) {
3051
- if (workspace.opencodeApi !== "v2")
3052
- throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
3053
2955
  const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
3054
2956
  return createOpencodeV2Client({
3055
2957
  fetch: fetchImpl,
@@ -3137,7 +3039,6 @@ function createDirectGittermClient(options) {
3137
3039
  url: workspace.runtime.url,
3138
3040
  directory: workspace.runtime.directory,
3139
3041
  password: workspace.runtime.password ?? null,
3140
- api: workspace.opencodeApi,
3141
3042
  headers: workspace.runtime.headers,
3142
3043
  fetch: options.fetch
3143
3044
  });
@@ -3162,23 +3063,14 @@ function createDirectGittermClient(options) {
3162
3063
  return authStatus(result.data.data);
3163
3064
  }
3164
3065
  async function startPty(workspace, command, title) {
3165
- const result = await http(workspace).json(workspace.opencodeApi === "v2" ? "/api/pty" : `/pty?directory=${encodeURIComponent(workspace.runtime.directory)}`, {
3066
+ return http(workspace).json("/api/pty", {
3166
3067
  method: "POST",
3167
3068
  json: { command: "bash", args: ["-lc", command], cwd: workspace.runtime.directory, title }
3168
3069
  });
3169
- return result;
3170
3070
  }
3171
3071
  async function setupFile(workspace, name) {
3172
3072
  try {
3173
- if (workspace.opencodeApi === "v2") {
3174
- return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
3175
- }
3176
- const result = await runtimeClient(workspace, options.fetch).file.read({
3177
- query: { directory: workspace.runtime.directory, path: `${SETUP_DIR}/${name}` }
3178
- });
3179
- if (result.error || !result.data || result.data.type !== "text")
3180
- return null;
3181
- return result.data.content.trim();
3073
+ return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
3182
3074
  } catch (error) {
3183
3075
  if (error instanceof RuntimeHttpError && error.status === 404)
3184
3076
  return null;
@@ -3230,20 +3122,10 @@ function createDirectGittermClient(options) {
3230
3122
  if (!providerName) {
3231
3123
  throw new Error("Model credential providerName is required");
3232
3124
  }
3233
- if (workspace.opencodeApi === "v2") {
3234
- if (credential.source !== "apiKey")
3235
- throw new GittermError("BAD_REQUEST", "Use connectOAuth() for v2 credential rotation, or inject an OAuth bundle when creating the workspace");
3236
- directModelAuth(credential);
3237
- await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
3238
- return;
3239
- }
3240
- const result = await runtimeClient(workspace, options.fetch).auth.set({
3241
- path: { id: providerName },
3242
- query: { directory: workspace.runtime.directory },
3243
- body: directModelAuth(credential)
3244
- });
3245
- if (result.error)
3246
- 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 } });
3247
3129
  },
3248
3130
  async list(workspace) {
3249
3131
  assertWorkspace(workspace);
@@ -3312,8 +3194,8 @@ function createDirectGittermClient(options) {
3312
3194
  async wait(attempt, workspace, wait = {}) {
3313
3195
  assertAuthAttempt(attempt, workspace);
3314
3196
  const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
3315
- const deadline2 = Date.now() + timeoutMs;
3316
- while (Date.now() <= deadline2) {
3197
+ const deadline = Date.now() + timeoutMs;
3198
+ while (Date.now() <= deadline) {
3317
3199
  const status = await getAuthStatus(attempt, workspace);
3318
3200
  if (status.status === "complete")
3319
3201
  return status;
@@ -3321,7 +3203,7 @@ function createDirectGittermClient(options) {
3321
3203
  throw new Error(status.message);
3322
3204
  if (status.status === "expired")
3323
3205
  throw new Error("OAuth attempt expired");
3324
- const remaining = deadline2 - Date.now();
3206
+ const remaining = deadline - Date.now();
3325
3207
  if (remaining <= 0)
3326
3208
  break;
3327
3209
  await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
@@ -3339,8 +3221,6 @@ function createDirectGittermClient(options) {
3339
3221
  },
3340
3222
  workspaces: {
3341
3223
  async create(input = {}) {
3342
- if (input.opencode?.api && !["v1", "v2"].includes(input.opencode.api))
3343
- throw new GittermError("BAD_REQUEST", "opencode.api must be v1 or v2");
3344
3224
  const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
3345
3225
  if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
3346
3226
  throw new Error(`${provider.name} does not support persistent direct workspaces`);
@@ -3357,7 +3237,6 @@ function createDirectGittermClient(options) {
3357
3237
  status: "running",
3358
3238
  lifecycle,
3359
3239
  runtime: created.runtime,
3360
- opencodeApi: input.opencode?.api ?? "v1",
3361
3240
  setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
3362
3241
  createdAt: new Date().toISOString()
3363
3242
  };
@@ -3412,7 +3291,7 @@ function createDirectGittermClient(options) {
3412
3291
  setupStatus: getSetupStatus,
3413
3292
  async waitForSetup(workspace, wait = {}) {
3414
3293
  const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
3415
- const deadline2 = Date.now() + timeoutMs;
3294
+ const deadline = Date.now() + timeoutMs;
3416
3295
  while (true) {
3417
3296
  const status = await getSetupStatus(workspace);
3418
3297
  if (status.status === "not_requested" || status.status === "succeeded")
@@ -3421,7 +3300,7 @@ function createDirectGittermClient(options) {
3421
3300
  throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
3422
3301
  ${status.log}` : ""}`);
3423
3302
  }
3424
- if (Date.now() >= deadline2) {
3303
+ if (Date.now() >= deadline) {
3425
3304
  throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
3426
3305
  }
3427
3306
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
@@ -3440,7 +3319,7 @@ ${status.log}` : ""}`);
3440
3319
  if (input.model && !/^[^/]+\/.+$/.test(input.model))
3441
3320
  throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
3442
3321
  if (input.waitForSetup) {
3443
- const deadline2 = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
3322
+ const deadline = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
3444
3323
  while (true) {
3445
3324
  aborted(input.signal);
3446
3325
  const status = await getSetupStatus(input.workspace);
@@ -3448,7 +3327,7 @@ ${status.log}` : ""}`);
3448
3327
  break;
3449
3328
  if (status.status === "failed")
3450
3329
  throw new GittermError("BAD_REQUEST", `Workspace setup failed: ${status.log ?? ""}`);
3451
- if (Date.now() >= deadline2)
3330
+ if (Date.now() >= deadline)
3452
3331
  throw new GittermError("TIMEOUT", "Workspace setup timed out");
3453
3332
  await new Promise((resolve) => setTimeout(resolve, 1000));
3454
3333
  }
@@ -3465,9 +3344,9 @@ ${status.log}` : ""}`);
3465
3344
  try {
3466
3345
  aborted(input.signal);
3467
3346
  if (!sessionId) {
3468
- const session2 = await runtime.createSession(input);
3469
- sessionId = session2.id;
3470
- title = session2.title;
3347
+ const session = await runtime.createSession(input);
3348
+ sessionId = session.id;
3349
+ title = session.title;
3471
3350
  }
3472
3351
  aborted(input.signal);
3473
3352
  await runtime.prompt({