@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.
- package/README.md +143 -54
- package/dist/client.d.ts +33 -11
- package/dist/direct/index.js +293 -414
- package/dist/direct/types.d.ts +5 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +42 -40
- package/dist/types.d.ts +94 -32
- package/dist/workspace-client.d.ts +9 -0
- package/package.json +1 -1
package/dist/direct/index.js
CHANGED
|
@@ -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.
|
|
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/
|
|
294
|
-
function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
432
|
-
return
|
|
433
|
-
`).trim();
|
|
205
|
+
function permissionTitle(permission, patterns) {
|
|
206
|
+
return patterns.length > 0 ? `${permission}: ${patterns.join(", ")}` : permission;
|
|
434
207
|
}
|
|
435
|
-
function
|
|
436
|
-
|
|
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
|
|
452
|
-
const tool = asRecord(raw.tool);
|
|
453
|
-
const questions = Array.isArray(raw.questions) ? raw.questions.map(asRecord) : [];
|
|
211
|
+
function missingSessionSnapshot() {
|
|
454
212
|
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
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(
|
|
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
|
|
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
|
|
479
|
+
const changed = () => sessionId ? { type: "session.changed", sessionId } : null;
|
|
775
480
|
if (type.startsWith("session.step.") || type.startsWith("session.tool."))
|
|
776
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
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 = {
|
|
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,
|
|
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 ? {
|
|
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
|
|
1791
|
-
while (Date.now() <
|
|
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
|
|
2344
|
-
while (Date.now() <
|
|
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
|
|
2419
|
-
await waitForDirectRuntime(
|
|
2420
|
-
return { externalId: serializeHandle2(handle), runtime:
|
|
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
|
|
2456
|
-
await waitForDirectRuntime(
|
|
2457
|
-
return
|
|
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
|
|
2634
|
-
while (Date.now() <
|
|
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
|
|
2649
|
-
while (Date.now() <
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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
|
|
3316
|
-
while (Date.now() <=
|
|
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 =
|
|
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
|
|
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() >=
|
|
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
|
|
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() >=
|
|
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
|
|
3469
|
-
sessionId =
|
|
3470
|
-
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({
|