@gitterm/sdk 0.4.0 → 0.6.1
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 +136 -53
- package/dist/client.d.ts +33 -11
- package/dist/direct/index.js +287 -403
- package/dist/direct/types.d.ts +5 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -52
- package/dist/transport.d.ts +7 -0
- package/dist/types.d.ts +94 -33
- 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);
|
|
@@ -860,9 +565,10 @@ function deriveRunState(snapshot, context) {
|
|
|
860
565
|
return { status: "running", errorMessage: null };
|
|
861
566
|
return { status: "completed", errorMessage: null };
|
|
862
567
|
}
|
|
568
|
+
|
|
863
569
|
// ../agent-runtime/src/index.ts
|
|
864
570
|
function getRuntime(target) {
|
|
865
|
-
return
|
|
571
|
+
return createV2Runtime(target);
|
|
866
572
|
}
|
|
867
573
|
|
|
868
574
|
// ../agent-runtime/src/replies.ts
|
|
@@ -1077,7 +783,6 @@ function directRuntime(workspace, fetchImpl, signal) {
|
|
|
1077
783
|
directory: workspace.runtime.directory,
|
|
1078
784
|
password: workspace.runtime.password ?? null,
|
|
1079
785
|
headers: workspace.runtime.headers,
|
|
1080
|
-
api: workspace.opencodeApi,
|
|
1081
786
|
fetch: fetchImpl,
|
|
1082
787
|
signal
|
|
1083
788
|
});
|
|
@@ -1510,6 +1215,220 @@ main().catch(() => {
|
|
|
1510
1215
|
});
|
|
1511
1216
|
`;
|
|
1512
1217
|
|
|
1218
|
+
// ../agent-runtime/src/opencode-credentials.ts
|
|
1219
|
+
var OPENCODE_CREDENTIALS_PATH = "~/.gitterm/opencode/credentials.json";
|
|
1220
|
+
var OPENCODE_CREDENTIALS_PLUGIN_PATH = "~/.config/opencode/plugins/gitterm-credentials.js";
|
|
1221
|
+
var OPENCODE_CREDENTIAL_LABEL = "Gitterm";
|
|
1222
|
+
function opencodeCredentialFiles(entries) {
|
|
1223
|
+
return [
|
|
1224
|
+
{
|
|
1225
|
+
path: OPENCODE_CREDENTIALS_PATH,
|
|
1226
|
+
contentBase64: Buffer.from(JSON.stringify(entries)).toString("base64"),
|
|
1227
|
+
mode: 384
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
path: OPENCODE_CREDENTIALS_PLUGIN_PATH,
|
|
1231
|
+
contentBase64: Buffer.from(OPENCODE_CREDENTIALS_PLUGIN).toString("base64")
|
|
1232
|
+
}
|
|
1233
|
+
];
|
|
1234
|
+
}
|
|
1235
|
+
var OPENCODE_CREDENTIALS_PLUGIN = String.raw`
|
|
1236
|
+
import { readFileSync } from "node:fs";
|
|
1237
|
+
import os from "node:os";
|
|
1238
|
+
import path from "node:path";
|
|
1239
|
+
|
|
1240
|
+
const METHOD_ID = "gitterm-import";
|
|
1241
|
+
const FILE = path.join(process.env.HOME || os.homedir(), ".gitterm/opencode/credentials.json");
|
|
1242
|
+
const DEVICE_FLOW = ["github-copilot", "opencode", "xai"];
|
|
1243
|
+
// OpenCode only enables ChatGPT routing for its own method IDs. The browser method cannot
|
|
1244
|
+
// complete in a remote workspace (its callback is loopback), so managed accounts take it over.
|
|
1245
|
+
const MANAGED_METHOD = { openai: "chatgpt-browser" };
|
|
1246
|
+
const log = (message, detail) => console.log("[gitterm-credentials] " + message + (detail ? ": " + detail : ""));
|
|
1247
|
+
|
|
1248
|
+
// Groups entries by integration and orders each group so the active account is created
|
|
1249
|
+
// last: OpenCode makes the most recently created credential the active one.
|
|
1250
|
+
function readCredentials() {
|
|
1251
|
+
try {
|
|
1252
|
+
const entries = JSON.parse(readFileSync(FILE, "utf8"));
|
|
1253
|
+
if (!Array.isArray(entries)) return new Map();
|
|
1254
|
+
const groups = new Map();
|
|
1255
|
+
for (const entry of entries) {
|
|
1256
|
+
const integration = typeof entry?.integration === "string" ? entry.integration.replace(/\/+$/, "") : "";
|
|
1257
|
+
const value = entry?.value;
|
|
1258
|
+
if (!integration || typeof entry.label !== "string" || !entry.label) continue;
|
|
1259
|
+
if (!value || (value.type !== "api" && value.type !== "oauth")) continue;
|
|
1260
|
+
if (!groups.has(integration)) groups.set(integration, []);
|
|
1261
|
+
groups.get(integration).push({ label: entry.label, active: entry.active === true, value });
|
|
1262
|
+
}
|
|
1263
|
+
for (const group of groups.values()) {
|
|
1264
|
+
const active = group.findIndex((entry) => entry.active);
|
|
1265
|
+
if (active !== -1 && active !== group.length - 1) group.push(...group.splice(active, 1));
|
|
1266
|
+
}
|
|
1267
|
+
return groups;
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
if (error?.code !== "ENOENT") log("unreadable credentials file", String(error));
|
|
1270
|
+
return new Map();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function builtinOAuthMethod(id, methods) {
|
|
1275
|
+
const preferred = id === "openai" ? "chatgpt-browser" : DEVICE_FLOW.includes(id) ? "device" : "oauth";
|
|
1276
|
+
const oauth = methods.filter((method) => method.type === "oauth" && method.id !== METHOD_ID);
|
|
1277
|
+
return (oauth.find((method) => method.id === preferred) ?? oauth[0])?.id;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Only answers the key method asks for: OpenCode rejects answers when it shows no form
|
|
1281
|
+
// (for example because CLOUDFLARE_ACCOUNT_ID is already set).
|
|
1282
|
+
function keyAnswer(methods, metadata) {
|
|
1283
|
+
const form = methods.find((method) => method.type === "key")?.form;
|
|
1284
|
+
if (!metadata || !Array.isArray(form)) return undefined;
|
|
1285
|
+
const answer = {};
|
|
1286
|
+
for (const field of form) {
|
|
1287
|
+
if (typeof field?.key === "string" && typeof metadata[field.key] === "string") answer[field.key] = metadata[field.key];
|
|
1288
|
+
}
|
|
1289
|
+
return Object.keys(answer).length ? answer : undefined;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const isManaged = (value) => value?.type === "oauth" && typeof value.metadata?.gittermCredentialId === "string";
|
|
1293
|
+
|
|
1294
|
+
// One request per credential at a time; GitTerm serializes across workspaces.
|
|
1295
|
+
const inflight = new Map();
|
|
1296
|
+
function managedRefresh(credential) {
|
|
1297
|
+
const id = credential.metadata?.gittermCredentialId;
|
|
1298
|
+
if (typeof id !== "string") return Promise.resolve(credential);
|
|
1299
|
+
if (!inflight.has(id)) {
|
|
1300
|
+
inflight.set(id, requestToken(id).then(
|
|
1301
|
+
(token) => ({ ...credential, access: token.access, expires: token.expires, metadata: { ...credential.metadata, ...token.metadata } }),
|
|
1302
|
+
).finally(() => inflight.delete(id)));
|
|
1303
|
+
}
|
|
1304
|
+
return inflight.get(id);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function requestToken(credentialId) {
|
|
1308
|
+
const url = process.env.WORKSPACE_API_URL;
|
|
1309
|
+
const auth = process.env.WORKSPACE_AGENT_AUTH_TOKEN;
|
|
1310
|
+
if (!url || !auth) throw new Error("GitTerm workspace identity is unavailable");
|
|
1311
|
+
const response = await fetch(url.replace(/\/$/, "") + "/workspaceOps.modelCredential", {
|
|
1312
|
+
method: "POST",
|
|
1313
|
+
headers: { Authorization: "Bearer " + auth, "Content-Type": "application/json" },
|
|
1314
|
+
body: JSON.stringify({ credentialId }),
|
|
1315
|
+
redirect: "error",
|
|
1316
|
+
signal: AbortSignal.timeout(30000),
|
|
1317
|
+
});
|
|
1318
|
+
const body = await response.json().catch(() => undefined);
|
|
1319
|
+
const data = body?.result?.data?.json ?? body?.result?.data;
|
|
1320
|
+
if (!response.ok || typeof data?.access !== "string" || typeof data?.expires !== "number") {
|
|
1321
|
+
const message = body?.error?.json?.message ?? body?.error?.message ?? "status " + response.status;
|
|
1322
|
+
log("GitTerm token request failed", message);
|
|
1323
|
+
throw new Error("GitTerm could not provide a token: " + message);
|
|
1324
|
+
}
|
|
1325
|
+
return data;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function toOAuthCredential(methodID, value) {
|
|
1329
|
+
const metadata = {
|
|
1330
|
+
...(value.metadata && typeof value.metadata === "object" ? value.metadata : {}),
|
|
1331
|
+
...(value.accountId ? { accountID: value.accountId } : {}),
|
|
1332
|
+
...(value.enterpriseUrl ? { enterpriseUrl: value.enterpriseUrl } : {}),
|
|
1333
|
+
};
|
|
1334
|
+
return {
|
|
1335
|
+
type: "oauth",
|
|
1336
|
+
methodID,
|
|
1337
|
+
refresh: value.refresh,
|
|
1338
|
+
access: value.access ?? "",
|
|
1339
|
+
expires: value.expires ?? 0,
|
|
1340
|
+
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function importOAuth(ctx, id, label) {
|
|
1345
|
+
const attempt = (await ctx.integration.oauth.connect({ integrationID: id, methodID: METHOD_ID, label })).data;
|
|
1346
|
+
for (let i = 0; i < 100; i++) {
|
|
1347
|
+
const status = (await ctx.integration.oauth.status({ integrationID: id, attemptID: attempt.attemptID })).data;
|
|
1348
|
+
if (status.status !== "pending") return status;
|
|
1349
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1350
|
+
}
|
|
1351
|
+
return { status: "failed", message: "timed out" };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export default {
|
|
1355
|
+
id: "gitterm-credentials",
|
|
1356
|
+
async setup(ctx) {
|
|
1357
|
+
const groups = readCredentials();
|
|
1358
|
+
// One import method per integration; the pending OAuth entry is handed over per attempt.
|
|
1359
|
+
const pending = new Map();
|
|
1360
|
+
const oauthIntegrations = [...groups].filter(([, entries]) => entries.some((entry) => entry.value.type === "oauth"));
|
|
1361
|
+
if (oauthIntegrations.length) {
|
|
1362
|
+
await ctx.integration.transform((editor) => {
|
|
1363
|
+
for (const [id, entries] of oauthIntegrations) {
|
|
1364
|
+
if (!editor.get(id)) continue;
|
|
1365
|
+
const builtin = builtinOAuthMethod(id, editor.method.list(id));
|
|
1366
|
+
const managed = entries.some((entry) => isManaged(entry.value));
|
|
1367
|
+
if (!builtin && !managed) continue;
|
|
1368
|
+
const managedMethod = MANAGED_METHOD[id] ?? METHOD_ID;
|
|
1369
|
+
const authorize = async () => {
|
|
1370
|
+
const value = pending.get(id);
|
|
1371
|
+
return {
|
|
1372
|
+
url: "",
|
|
1373
|
+
instructions: "Importing the credential provided by Gitterm",
|
|
1374
|
+
mode: "auto",
|
|
1375
|
+
callback: Promise.resolve(toOAuthCredential(isManaged(value) ? managedMethod : builtin, value)),
|
|
1376
|
+
};
|
|
1377
|
+
};
|
|
1378
|
+
editor.method.update({
|
|
1379
|
+
integrationID: id,
|
|
1380
|
+
method: { id: METHOD_ID, type: "oauth", label: "Gitterm" },
|
|
1381
|
+
authorize,
|
|
1382
|
+
refresh: managedRefresh,
|
|
1383
|
+
});
|
|
1384
|
+
if (managed && managedMethod !== METHOD_ID) {
|
|
1385
|
+
editor.method.update({
|
|
1386
|
+
integrationID: id,
|
|
1387
|
+
method: { id: managedMethod, type: "oauth", label: "Managed by Gitterm" },
|
|
1388
|
+
authorize,
|
|
1389
|
+
refresh: managedRefresh,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
for (const [id, entries] of groups) {
|
|
1396
|
+
const info = await ctx.integration.get({ integrationID: id }).then((result) => result.data, () => undefined);
|
|
1397
|
+
if (!info) {
|
|
1398
|
+
log("unknown integration, skipping", id);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const existing = new Set(
|
|
1402
|
+
info.connections.filter((connection) => connection.type === "credential").map((connection) => connection.label),
|
|
1403
|
+
);
|
|
1404
|
+
const importable = info.methods.some((method) => method.type === "oauth" && method.id === METHOD_ID);
|
|
1405
|
+
for (const entry of entries) {
|
|
1406
|
+
const target = id + " (" + entry.label + ")";
|
|
1407
|
+
if (existing.has(entry.label)) continue;
|
|
1408
|
+
try {
|
|
1409
|
+
if (entry.value.type === "api") {
|
|
1410
|
+
const answer = keyAnswer(info.methods, entry.value.metadata);
|
|
1411
|
+
await ctx.integration.connect.key({ integrationID: id, key: entry.value.key, label: entry.label, ...(answer ? { answer } : {}) });
|
|
1412
|
+
log("imported API key", target);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (!importable) {
|
|
1416
|
+
log("no OAuth method available, skipping", target);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
pending.set(id, entry.value);
|
|
1420
|
+
const result = await importOAuth(ctx, id, entry.label);
|
|
1421
|
+
if (result.status === "complete") log("imported OAuth credential", target);
|
|
1422
|
+
else log("OAuth import " + result.status, target + (result.message ? " (" + result.message + ")" : ""));
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
log("import failed", target + " (" + String(error?.message ?? error) + ")");
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
};
|
|
1430
|
+
`;
|
|
1431
|
+
|
|
1513
1432
|
// src/direct/provisioning.ts
|
|
1514
1433
|
var DIRECT_OPENCODE_PORT = 4096;
|
|
1515
1434
|
var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
|
|
@@ -1621,7 +1540,12 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1621
1540
|
if (credentials.has(providerName)) {
|
|
1622
1541
|
throw new Error(`Duplicate model credential: ${providerName}`);
|
|
1623
1542
|
}
|
|
1624
|
-
credentials.set(providerName,
|
|
1543
|
+
credentials.set(providerName, {
|
|
1544
|
+
integration: providerName,
|
|
1545
|
+
label: credential.label?.trim() || OPENCODE_CREDENTIAL_LABEL,
|
|
1546
|
+
active: true,
|
|
1547
|
+
value: directModelAuth(credential)
|
|
1548
|
+
});
|
|
1625
1549
|
}
|
|
1626
1550
|
const configuredPlugins = Array.isArray(input.opencode?.config?.plugin) ? input.opencode.config.plugin.filter((plugin) => typeof plugin === "string") : [];
|
|
1627
1551
|
const plugins = [...new Set([...configuredPlugins, ...input.opencode?.plugins ?? []])];
|
|
@@ -1648,10 +1572,7 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1648
1572
|
} : undefined);
|
|
1649
1573
|
const files = [
|
|
1650
1574
|
...github?.files ?? [],
|
|
1651
|
-
|
|
1652
|
-
path: "~/.local/share/opencode/auth.json",
|
|
1653
|
-
contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
|
|
1654
|
-
},
|
|
1575
|
+
...opencodeCredentialFiles([...credentials.values()]),
|
|
1655
1576
|
{
|
|
1656
1577
|
path: "~/.config/opencode/opencode.json",
|
|
1657
1578
|
contentBase64: base64(JSON.stringify(config))
|
|
@@ -1782,8 +1703,8 @@ function basicAuthHeader(password) {
|
|
|
1782
1703
|
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
1783
1704
|
}
|
|
1784
1705
|
async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
|
|
1785
|
-
const
|
|
1786
|
-
while (Date.now() <
|
|
1706
|
+
const deadline = Date.now() + timeoutMs;
|
|
1707
|
+
while (Date.now() < deadline) {
|
|
1787
1708
|
const controller = new AbortController;
|
|
1788
1709
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
1789
1710
|
const ready = await fetch(runtime.url, {
|
|
@@ -1892,7 +1813,7 @@ function createAsciiDirectProvider(config) {
|
|
|
1892
1813
|
});
|
|
1893
1814
|
await runCommand(handle.boxId, `mkdir -p ${shellQuote(directory)}`);
|
|
1894
1815
|
for (const command of config.runtimeSetupCommands ?? [
|
|
1895
|
-
"npm install -g opencode
|
|
1816
|
+
"npm install -g @opencode/cli@2 --no-audit --fund=false"
|
|
1896
1817
|
]) {
|
|
1897
1818
|
await runCommand(handle.boxId, command, undefined, 600);
|
|
1898
1819
|
}
|
|
@@ -2335,8 +2256,8 @@ function createExeDevDirectProvider(config) {
|
|
|
2335
2256
|
return token;
|
|
2336
2257
|
}
|
|
2337
2258
|
async function waitUntilRunning(vmName) {
|
|
2338
|
-
const
|
|
2339
|
-
while (Date.now() <
|
|
2259
|
+
const deadline = Date.now() + 60000;
|
|
2260
|
+
while (Date.now() < deadline) {
|
|
2340
2261
|
const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
|
|
2341
2262
|
if (status === "running")
|
|
2342
2263
|
return;
|
|
@@ -2410,9 +2331,9 @@ function createExeDevDirectProvider(config) {
|
|
|
2410
2331
|
await startRuntime(handle);
|
|
2411
2332
|
await execute(`share port ${vmName} ${handle.serve.port}`);
|
|
2412
2333
|
await execute(`share set-private ${vmName}`);
|
|
2413
|
-
const
|
|
2414
|
-
await waitForDirectRuntime(
|
|
2415
|
-
return { externalId: serializeHandle2(handle), runtime:
|
|
2334
|
+
const directRuntime = await runtime(handle, input.password);
|
|
2335
|
+
await waitForDirectRuntime(directRuntime);
|
|
2336
|
+
return { externalId: serializeHandle2(handle), runtime: directRuntime };
|
|
2416
2337
|
} catch (error) {
|
|
2417
2338
|
if (handle.owned) {
|
|
2418
2339
|
await execute(`rm ${vmName}`).catch(() => {
|
|
@@ -2447,9 +2368,9 @@ function createExeDevDirectProvider(config) {
|
|
|
2447
2368
|
await execute(`share set-private ${handle.vmName}`);
|
|
2448
2369
|
if (!workspace.runtime.password)
|
|
2449
2370
|
throw new Error("exe.dev runtime password is missing");
|
|
2450
|
-
const
|
|
2451
|
-
await waitForDirectRuntime(
|
|
2452
|
-
return
|
|
2371
|
+
const directRuntime = await runtime(handle, workspace.runtime.password);
|
|
2372
|
+
await waitForDirectRuntime(directRuntime);
|
|
2373
|
+
return directRuntime;
|
|
2453
2374
|
},
|
|
2454
2375
|
async terminate(workspace) {
|
|
2455
2376
|
const handle = parseHandle3(workspace.externalId);
|
|
@@ -2625,8 +2546,8 @@ function createRailwayDirectProvider(config) {
|
|
|
2625
2546
|
};
|
|
2626
2547
|
const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
|
|
2627
2548
|
const waitForDeployment = async (serviceId, previousDeploymentId) => {
|
|
2628
|
-
const
|
|
2629
|
-
while (Date.now() <
|
|
2549
|
+
const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2550
|
+
while (Date.now() < deadline) {
|
|
2630
2551
|
const deployment = await latestDeployment(serviceId);
|
|
2631
2552
|
if (deployment && deployment.id !== previousDeploymentId) {
|
|
2632
2553
|
if (deployment.status === "SUCCESS")
|
|
@@ -2640,8 +2561,8 @@ function createRailwayDirectProvider(config) {
|
|
|
2640
2561
|
throw new Error("Timed out waiting for Railway deployment");
|
|
2641
2562
|
};
|
|
2642
2563
|
const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
|
|
2643
|
-
const
|
|
2644
|
-
while (Date.now() <
|
|
2564
|
+
const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2565
|
+
while (Date.now() < deadline) {
|
|
2645
2566
|
const deployment = await latestDeployment(serviceId);
|
|
2646
2567
|
if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
|
|
2647
2568
|
return;
|
|
@@ -2884,7 +2805,7 @@ function createVercelDirectProvider(config) {
|
|
|
2884
2805
|
});
|
|
2885
2806
|
try {
|
|
2886
2807
|
await run(sandbox, `mkdir -p ${shellQuote(handle.directory)}`);
|
|
2887
|
-
for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g opencode
|
|
2808
|
+
for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g @opencode/cli@2 --no-audit --fund=false"])) {
|
|
2888
2809
|
await run(sandbox, command);
|
|
2889
2810
|
}
|
|
2890
2811
|
if (plan.repository) {
|
|
@@ -3030,21 +2951,7 @@ function resolveProvider(provider) {
|
|
|
3030
2951
|
return createVercelDirectProvider(provider);
|
|
3031
2952
|
}
|
|
3032
2953
|
}
|
|
3033
|
-
function runtimeClient(workspace, fetchImpl) {
|
|
3034
|
-
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
3035
|
-
return createOpencodeClient2({
|
|
3036
|
-
fetch: fetchImpl,
|
|
3037
|
-
baseUrl: workspace.runtime.url,
|
|
3038
|
-
directory: workspace.runtime.directory,
|
|
3039
|
-
headers: {
|
|
3040
|
-
...workspace.runtime.headers,
|
|
3041
|
-
...authorization ? { Authorization: authorization } : {}
|
|
3042
|
-
}
|
|
3043
|
-
});
|
|
3044
|
-
}
|
|
3045
2954
|
function createAuthClient(workspace, fetchImpl) {
|
|
3046
|
-
if (workspace.opencodeApi !== "v2")
|
|
3047
|
-
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
|
|
3048
2955
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
3049
2956
|
return createOpencodeV2Client({
|
|
3050
2957
|
fetch: fetchImpl,
|
|
@@ -3132,7 +3039,6 @@ function createDirectGittermClient(options) {
|
|
|
3132
3039
|
url: workspace.runtime.url,
|
|
3133
3040
|
directory: workspace.runtime.directory,
|
|
3134
3041
|
password: workspace.runtime.password ?? null,
|
|
3135
|
-
api: workspace.opencodeApi,
|
|
3136
3042
|
headers: workspace.runtime.headers,
|
|
3137
3043
|
fetch: options.fetch
|
|
3138
3044
|
});
|
|
@@ -3157,23 +3063,14 @@ function createDirectGittermClient(options) {
|
|
|
3157
3063
|
return authStatus(result.data.data);
|
|
3158
3064
|
}
|
|
3159
3065
|
async function startPty(workspace, command, title) {
|
|
3160
|
-
|
|
3066
|
+
return http(workspace).json("/api/pty", {
|
|
3161
3067
|
method: "POST",
|
|
3162
3068
|
json: { command: "bash", args: ["-lc", command], cwd: workspace.runtime.directory, title }
|
|
3163
3069
|
});
|
|
3164
|
-
return result;
|
|
3165
3070
|
}
|
|
3166
3071
|
async function setupFile(workspace, name) {
|
|
3167
3072
|
try {
|
|
3168
|
-
|
|
3169
|
-
return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
|
|
3170
|
-
}
|
|
3171
|
-
const result = await runtimeClient(workspace, options.fetch).file.read({
|
|
3172
|
-
query: { directory: workspace.runtime.directory, path: `${SETUP_DIR}/${name}` }
|
|
3173
|
-
});
|
|
3174
|
-
if (result.error || !result.data || result.data.type !== "text")
|
|
3175
|
-
return null;
|
|
3176
|
-
return result.data.content.trim();
|
|
3073
|
+
return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
|
|
3177
3074
|
} catch (error) {
|
|
3178
3075
|
if (error instanceof RuntimeHttpError && error.status === 404)
|
|
3179
3076
|
return null;
|
|
@@ -3225,20 +3122,10 @@ function createDirectGittermClient(options) {
|
|
|
3225
3122
|
if (!providerName) {
|
|
3226
3123
|
throw new Error("Model credential providerName is required");
|
|
3227
3124
|
}
|
|
3228
|
-
if (
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3235
|
-
const result = await runtimeClient(workspace, options.fetch).auth.set({
|
|
3236
|
-
path: { id: providerName },
|
|
3237
|
-
query: { directory: workspace.runtime.directory },
|
|
3238
|
-
body: directModelAuth(credential)
|
|
3239
|
-
});
|
|
3240
|
-
if (result.error)
|
|
3241
|
-
throw new Error(errorMessage(result.error));
|
|
3125
|
+
if (credential.source !== "apiKey")
|
|
3126
|
+
throw new GittermError("BAD_REQUEST", "Use connectOAuth() for OAuth credential rotation, or inject an OAuth bundle when creating the workspace");
|
|
3127
|
+
directModelAuth(credential);
|
|
3128
|
+
await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
|
|
3242
3129
|
},
|
|
3243
3130
|
async list(workspace) {
|
|
3244
3131
|
assertWorkspace(workspace);
|
|
@@ -3307,8 +3194,8 @@ function createDirectGittermClient(options) {
|
|
|
3307
3194
|
async wait(attempt, workspace, wait = {}) {
|
|
3308
3195
|
assertAuthAttempt(attempt, workspace);
|
|
3309
3196
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
|
|
3310
|
-
const
|
|
3311
|
-
while (Date.now() <=
|
|
3197
|
+
const deadline = Date.now() + timeoutMs;
|
|
3198
|
+
while (Date.now() <= deadline) {
|
|
3312
3199
|
const status = await getAuthStatus(attempt, workspace);
|
|
3313
3200
|
if (status.status === "complete")
|
|
3314
3201
|
return status;
|
|
@@ -3316,7 +3203,7 @@ function createDirectGittermClient(options) {
|
|
|
3316
3203
|
throw new Error(status.message);
|
|
3317
3204
|
if (status.status === "expired")
|
|
3318
3205
|
throw new Error("OAuth attempt expired");
|
|
3319
|
-
const remaining =
|
|
3206
|
+
const remaining = deadline - Date.now();
|
|
3320
3207
|
if (remaining <= 0)
|
|
3321
3208
|
break;
|
|
3322
3209
|
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
@@ -3334,8 +3221,6 @@ function createDirectGittermClient(options) {
|
|
|
3334
3221
|
},
|
|
3335
3222
|
workspaces: {
|
|
3336
3223
|
async create(input = {}) {
|
|
3337
|
-
if (input.opencode?.api && !["v1", "v2"].includes(input.opencode.api))
|
|
3338
|
-
throw new GittermError("BAD_REQUEST", "opencode.api must be v1 or v2");
|
|
3339
3224
|
const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
|
|
3340
3225
|
if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
|
|
3341
3226
|
throw new Error(`${provider.name} does not support persistent direct workspaces`);
|
|
@@ -3352,7 +3237,6 @@ function createDirectGittermClient(options) {
|
|
|
3352
3237
|
status: "running",
|
|
3353
3238
|
lifecycle,
|
|
3354
3239
|
runtime: created.runtime,
|
|
3355
|
-
opencodeApi: input.opencode?.api ?? "v1",
|
|
3356
3240
|
setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
|
|
3357
3241
|
createdAt: new Date().toISOString()
|
|
3358
3242
|
};
|
|
@@ -3407,7 +3291,7 @@ function createDirectGittermClient(options) {
|
|
|
3407
3291
|
setupStatus: getSetupStatus,
|
|
3408
3292
|
async waitForSetup(workspace, wait = {}) {
|
|
3409
3293
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
|
|
3410
|
-
const
|
|
3294
|
+
const deadline = Date.now() + timeoutMs;
|
|
3411
3295
|
while (true) {
|
|
3412
3296
|
const status = await getSetupStatus(workspace);
|
|
3413
3297
|
if (status.status === "not_requested" || status.status === "succeeded")
|
|
@@ -3416,7 +3300,7 @@ function createDirectGittermClient(options) {
|
|
|
3416
3300
|
throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
|
|
3417
3301
|
${status.log}` : ""}`);
|
|
3418
3302
|
}
|
|
3419
|
-
if (Date.now() >=
|
|
3303
|
+
if (Date.now() >= deadline) {
|
|
3420
3304
|
throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
|
|
3421
3305
|
}
|
|
3422
3306
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
@@ -3435,7 +3319,7 @@ ${status.log}` : ""}`);
|
|
|
3435
3319
|
if (input.model && !/^[^/]+\/.+$/.test(input.model))
|
|
3436
3320
|
throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
|
|
3437
3321
|
if (input.waitForSetup) {
|
|
3438
|
-
const
|
|
3322
|
+
const deadline = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
|
|
3439
3323
|
while (true) {
|
|
3440
3324
|
aborted(input.signal);
|
|
3441
3325
|
const status = await getSetupStatus(input.workspace);
|
|
@@ -3443,7 +3327,7 @@ ${status.log}` : ""}`);
|
|
|
3443
3327
|
break;
|
|
3444
3328
|
if (status.status === "failed")
|
|
3445
3329
|
throw new GittermError("BAD_REQUEST", `Workspace setup failed: ${status.log ?? ""}`);
|
|
3446
|
-
if (Date.now() >=
|
|
3330
|
+
if (Date.now() >= deadline)
|
|
3447
3331
|
throw new GittermError("TIMEOUT", "Workspace setup timed out");
|
|
3448
3332
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3449
3333
|
}
|
|
@@ -3460,9 +3344,9 @@ ${status.log}` : ""}`);
|
|
|
3460
3344
|
try {
|
|
3461
3345
|
aborted(input.signal);
|
|
3462
3346
|
if (!sessionId) {
|
|
3463
|
-
const
|
|
3464
|
-
sessionId =
|
|
3465
|
-
title =
|
|
3347
|
+
const session = await runtime.createSession(input);
|
|
3348
|
+
sessionId = session.id;
|
|
3349
|
+
title = session.title;
|
|
3466
3350
|
}
|
|
3467
3351
|
aborted(input.signal);
|
|
3468
3352
|
await runtime.prompt({
|