devin-oauth-opencode 0.2.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 +12 -0
- package/bin/setup.js +199 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +88 -0
- package/dist/browser-auth.d.ts +34 -0
- package/dist/browser-auth.js +126 -0
- package/dist/cloud.d.ts +21 -0
- package/dist/cloud.js +332 -0
- package/dist/devin.d.ts +34 -0
- package/dist/devin.js +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +108 -0
- package/dist/model-discovery.d.ts +37 -0
- package/dist/model-discovery.js +340 -0
- package/dist/models.d.ts +27 -0
- package/dist/models.js +150 -0
- package/dist/proxy.d.ts +10 -0
- package/dist/proxy.js +405 -0
- package/package.json +64 -0
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { buildDevinProviderModels, getDevinModels } from "./models";
|
|
3
|
+
import { streamDevinChat } from "./devin";
|
|
4
|
+
const DEVIN_PROXY_HOST = "127.0.0.1";
|
|
5
|
+
export const DEVIN_PROXY_DEFAULT_PORT = 65_534;
|
|
6
|
+
const PROXY_PROTOCOL_VERSION = 3;
|
|
7
|
+
const SSE_HEADERS = {
|
|
8
|
+
"Content-Type": "text/event-stream",
|
|
9
|
+
"Cache-Control": "no-cache",
|
|
10
|
+
Connection: "keep-alive",
|
|
11
|
+
};
|
|
12
|
+
let proxyServer;
|
|
13
|
+
let proxyPort;
|
|
14
|
+
let proxyStartPromise;
|
|
15
|
+
let chatTransport = streamDevinChat;
|
|
16
|
+
let customChatTransport = false;
|
|
17
|
+
export function setChatTransportForTests(transport) {
|
|
18
|
+
chatTransport = transport ?? streamDevinChat;
|
|
19
|
+
customChatTransport = Boolean(transport);
|
|
20
|
+
}
|
|
21
|
+
export function getProxyPort() {
|
|
22
|
+
return proxyPort;
|
|
23
|
+
}
|
|
24
|
+
export function stopProxy() {
|
|
25
|
+
proxyServer?.stop(true);
|
|
26
|
+
proxyServer = undefined;
|
|
27
|
+
proxyPort = undefined;
|
|
28
|
+
proxyStartPromise = undefined;
|
|
29
|
+
setChatTransportForTests();
|
|
30
|
+
}
|
|
31
|
+
export async function startProxy(forceLocal = false) {
|
|
32
|
+
if (proxyServer && proxyPort)
|
|
33
|
+
return proxyPort;
|
|
34
|
+
if (proxyStartPromise)
|
|
35
|
+
return proxyStartPromise;
|
|
36
|
+
proxyStartPromise = startProxyInner(forceLocal).finally(() => {
|
|
37
|
+
proxyStartPromise = undefined;
|
|
38
|
+
});
|
|
39
|
+
return proxyStartPromise;
|
|
40
|
+
}
|
|
41
|
+
async function startProxyInner(forceLocal) {
|
|
42
|
+
if (!forceLocal && !customChatTransport) {
|
|
43
|
+
const existing = await fetch(`http://${DEVIN_PROXY_HOST}:${DEVIN_PROXY_DEFAULT_PORT}/health`).catch(() => null);
|
|
44
|
+
const health = existing?.ok ? await existing.json().catch(() => null) : null;
|
|
45
|
+
if (health?.proxyProtocolVersion === PROXY_PROTOCOL_VERSION) {
|
|
46
|
+
proxyPort = DEVIN_PROXY_DEFAULT_PORT;
|
|
47
|
+
return proxyPort;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const bind = (port) => Bun.serve({
|
|
51
|
+
hostname: DEVIN_PROXY_HOST,
|
|
52
|
+
port,
|
|
53
|
+
idleTimeout: 255,
|
|
54
|
+
fetch: handleProxyRequest,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
proxyServer = bind(DEVIN_PROXY_DEFAULT_PORT);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof Error && "code" in error && error.code === "EADDRINUSE"))
|
|
61
|
+
throw error;
|
|
62
|
+
proxyServer = bind(0);
|
|
63
|
+
}
|
|
64
|
+
proxyPort = proxyServer.port;
|
|
65
|
+
if (!proxyPort)
|
|
66
|
+
throw new Error("Failed to bind Devin proxy");
|
|
67
|
+
return proxyPort;
|
|
68
|
+
}
|
|
69
|
+
export function proxyBaseURL(port = proxyPort ?? DEVIN_PROXY_DEFAULT_PORT) {
|
|
70
|
+
return `http://${DEVIN_PROXY_HOST}:${port}/v1`;
|
|
71
|
+
}
|
|
72
|
+
async function handleProxyRequest(req) {
|
|
73
|
+
const url = new URL(req.url);
|
|
74
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
75
|
+
return jsonResponse({
|
|
76
|
+
ok: true,
|
|
77
|
+
proxyProtocolVersion: PROXY_PROTOCOL_VERSION,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (req.method === "GET" && (url.pathname === "/v1/models" || url.pathname === "/models")) {
|
|
81
|
+
return jsonResponse({
|
|
82
|
+
object: "list",
|
|
83
|
+
data: getDevinModels().map((model) => ({
|
|
84
|
+
id: model.id,
|
|
85
|
+
object: "model",
|
|
86
|
+
created: 0,
|
|
87
|
+
owned_by: "devin",
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (req.method === "POST" && (url.pathname === "/v1/chat/completions" || url.pathname === "/chat/completions")) {
|
|
92
|
+
try {
|
|
93
|
+
const body = (await req.json());
|
|
94
|
+
if (!Array.isArray(body.messages)) {
|
|
95
|
+
return openAIError(400, "messages must be an array");
|
|
96
|
+
}
|
|
97
|
+
if (hasToolContext(body)) {
|
|
98
|
+
return await handleToolPlanning(body);
|
|
99
|
+
}
|
|
100
|
+
if (body.stream === false) {
|
|
101
|
+
return jsonResponse(await createNonStreamingCompletion(body));
|
|
102
|
+
}
|
|
103
|
+
return createStreamingCompletion(body);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return openAIError(500, "Chat completion failed", error instanceof Error ? error.message : String(error));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return openAIError(404, `Unsupported path: ${url.pathname}`);
|
|
110
|
+
}
|
|
111
|
+
function jsonResponse(value, status = 200) {
|
|
112
|
+
return new Response(JSON.stringify(value), {
|
|
113
|
+
status,
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function openAIError(status, message, details) {
|
|
118
|
+
return jsonResponse({
|
|
119
|
+
error: {
|
|
120
|
+
message: details ? `${message}\n${details}` : message,
|
|
121
|
+
type: "devin_error",
|
|
122
|
+
code: null,
|
|
123
|
+
},
|
|
124
|
+
}, status);
|
|
125
|
+
}
|
|
126
|
+
function completionId() {
|
|
127
|
+
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
128
|
+
}
|
|
129
|
+
async function collectTransport(request) {
|
|
130
|
+
const result = await chatTransport(request);
|
|
131
|
+
if (typeof result === "string")
|
|
132
|
+
return result;
|
|
133
|
+
const chunks = [];
|
|
134
|
+
for await (const chunk of result) {
|
|
135
|
+
chunks.push(chunk);
|
|
136
|
+
}
|
|
137
|
+
return chunks.join("");
|
|
138
|
+
}
|
|
139
|
+
async function createNonStreamingCompletion(request) {
|
|
140
|
+
const content = await collectTransport(request);
|
|
141
|
+
return {
|
|
142
|
+
id: completionId(),
|
|
143
|
+
object: "chat.completion",
|
|
144
|
+
created: Math.floor(Date.now() / 1000),
|
|
145
|
+
model: request.model ?? getDevinModels()[0].id,
|
|
146
|
+
choices: [
|
|
147
|
+
{
|
|
148
|
+
index: 0,
|
|
149
|
+
message: { role: "assistant", content },
|
|
150
|
+
finish_reason: "stop",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function createStreamingCompletion(request) {
|
|
156
|
+
const id = completionId();
|
|
157
|
+
const created = Math.floor(Date.now() / 1000);
|
|
158
|
+
const model = request.model ?? getDevinModels()[0].id;
|
|
159
|
+
return createStreamingResponse(id, created, model, async function* () {
|
|
160
|
+
const result = await chatTransport(request);
|
|
161
|
+
const iterable = typeof result === "string" ? [result] : result;
|
|
162
|
+
for await (const chunk of iterable) {
|
|
163
|
+
if (chunk)
|
|
164
|
+
yield { content: chunk };
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function createStreamingResponse(id, created, model, chunks) {
|
|
169
|
+
const encoder = new TextEncoder();
|
|
170
|
+
const stream = new ReadableStream({
|
|
171
|
+
async start(controller) {
|
|
172
|
+
const send = (value) => {
|
|
173
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}\n\n`));
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
send({
|
|
177
|
+
id,
|
|
178
|
+
object: "chat.completion.chunk",
|
|
179
|
+
created,
|
|
180
|
+
model,
|
|
181
|
+
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
|
182
|
+
});
|
|
183
|
+
let finishReason = "stop";
|
|
184
|
+
for await (const chunk of chunks()) {
|
|
185
|
+
if (chunk.finishReason)
|
|
186
|
+
finishReason = chunk.finishReason;
|
|
187
|
+
if (chunk.toolCalls) {
|
|
188
|
+
send({
|
|
189
|
+
id,
|
|
190
|
+
object: "chat.completion.chunk",
|
|
191
|
+
created,
|
|
192
|
+
model,
|
|
193
|
+
choices: [{ index: 0, delta: { tool_calls: chunk.toolCalls }, finish_reason: null }],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (!chunk.content)
|
|
197
|
+
continue;
|
|
198
|
+
send({
|
|
199
|
+
id,
|
|
200
|
+
object: "chat.completion.chunk",
|
|
201
|
+
created,
|
|
202
|
+
model,
|
|
203
|
+
choices: [{ index: 0, delta: { content: chunk.content }, finish_reason: null }],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
send({
|
|
207
|
+
id,
|
|
208
|
+
object: "chat.completion.chunk",
|
|
209
|
+
created,
|
|
210
|
+
model,
|
|
211
|
+
choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
|
|
212
|
+
});
|
|
213
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
214
|
+
controller.close();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
send({ error: { message: error instanceof Error ? error.message : String(error) } });
|
|
218
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
219
|
+
controller.close();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
return new Response(stream, { headers: SSE_HEADERS });
|
|
224
|
+
}
|
|
225
|
+
function hasToolContext(request) {
|
|
226
|
+
return Boolean(request.tools?.length ||
|
|
227
|
+
request.messages.some((message) => message.role === "tool" || message.tool_calls?.length));
|
|
228
|
+
}
|
|
229
|
+
function textFromContent(content) {
|
|
230
|
+
if (content == null)
|
|
231
|
+
return "";
|
|
232
|
+
if (typeof content === "string")
|
|
233
|
+
return content;
|
|
234
|
+
return content
|
|
235
|
+
.filter((part) => part.type === "text" && part.text)
|
|
236
|
+
.map((part) => part.text)
|
|
237
|
+
.join("\n");
|
|
238
|
+
}
|
|
239
|
+
function buildToolPrompt(request) {
|
|
240
|
+
const tools = request.tools?.map((tool) => {
|
|
241
|
+
const fn = tool.function;
|
|
242
|
+
return `- ${fn?.name ?? "unknown"}${fn?.description ? `: ${fn.description}` : ""}\n${JSON.stringify(fn?.parameters ?? {}, null, 2)}`;
|
|
243
|
+
}).join("\n") || "(none)";
|
|
244
|
+
const conversation = request.messages.map((message) => {
|
|
245
|
+
if (message.role === "assistant" && message.tool_calls?.length) {
|
|
246
|
+
return `ASSISTANT TOOL_CALLS: ${JSON.stringify(message.tool_calls)}`;
|
|
247
|
+
}
|
|
248
|
+
if (message.role === "tool") {
|
|
249
|
+
return `TOOL RESULT${message.tool_call_id ? ` ${message.tool_call_id}` : ""}: ${textFromContent(message.content)}`;
|
|
250
|
+
}
|
|
251
|
+
return `${message.role.toUpperCase()}: ${textFromContent(message.content)}`;
|
|
252
|
+
}).join("\n\n");
|
|
253
|
+
return [
|
|
254
|
+
"You are running inside OpenCode and can either call tools or answer directly.",
|
|
255
|
+
"Return exactly one JSON object and no prose.",
|
|
256
|
+
"To call tools: {\"action\":\"tool_call\",\"tool_calls\":[{\"name\":\"tool_name\",\"arguments\":{}}]}",
|
|
257
|
+
"To answer: {\"action\":\"final\",\"content\":\"...\"}",
|
|
258
|
+
"",
|
|
259
|
+
"Available tools:",
|
|
260
|
+
tools,
|
|
261
|
+
"",
|
|
262
|
+
"Conversation:",
|
|
263
|
+
conversation,
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
function isRecord(value) {
|
|
267
|
+
return typeof value === "object" && value !== null;
|
|
268
|
+
}
|
|
269
|
+
function toolCallFromRecord(value) {
|
|
270
|
+
if (!isRecord(value) || typeof value.name !== "string")
|
|
271
|
+
return null;
|
|
272
|
+
return { name: value.name, arguments: value.arguments ?? {} };
|
|
273
|
+
}
|
|
274
|
+
function toolCallsFromArray(value) {
|
|
275
|
+
if (!Array.isArray(value))
|
|
276
|
+
return [];
|
|
277
|
+
return value.map(toolCallFromRecord).filter((toolCall) => Boolean(toolCall));
|
|
278
|
+
}
|
|
279
|
+
function parseToolArguments(value) {
|
|
280
|
+
if (typeof value !== "string")
|
|
281
|
+
return value ?? {};
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(value);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function stringifyToolArguments(value) {
|
|
290
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? {});
|
|
291
|
+
}
|
|
292
|
+
function parseToolPlan(output) {
|
|
293
|
+
const jsonLines = output
|
|
294
|
+
.split("\n")
|
|
295
|
+
.map((line) => line.trim())
|
|
296
|
+
.filter((line) => line.startsWith("{") && line.endsWith("}"));
|
|
297
|
+
if (jsonLines.length > 1) {
|
|
298
|
+
const toolCalls = [];
|
|
299
|
+
for (const line of jsonLines) {
|
|
300
|
+
try {
|
|
301
|
+
const toolCall = toolCallFromRecord(JSON.parse(line));
|
|
302
|
+
if (toolCall)
|
|
303
|
+
toolCalls.push({ name: toolCall.name, arguments: parseToolArguments(toolCall.arguments) });
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (toolCalls.length > 0)
|
|
310
|
+
return { action: "tool_call", tool_calls: toolCalls };
|
|
311
|
+
}
|
|
312
|
+
const start = output.indexOf("{");
|
|
313
|
+
const end = output.lastIndexOf("}");
|
|
314
|
+
if (start === -1 || end <= start)
|
|
315
|
+
return null;
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(output.slice(start, end + 1));
|
|
318
|
+
if (isRecord(parsed) && parsed.action === "final" && typeof parsed.content === "string") {
|
|
319
|
+
return { action: "final", content: parsed.content };
|
|
320
|
+
}
|
|
321
|
+
const directToolCall = toolCallFromRecord(parsed);
|
|
322
|
+
if (directToolCall) {
|
|
323
|
+
return {
|
|
324
|
+
action: "tool_call",
|
|
325
|
+
tool_calls: [{ name: directToolCall.name, arguments: parseToolArguments(directToolCall.arguments) }],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (isRecord(parsed) && Array.isArray(parsed.tool_calls)) {
|
|
329
|
+
const toolCalls = toolCallsFromArray(parsed.tool_calls)
|
|
330
|
+
.map((tool) => ({ name: tool.name, arguments: parseToolArguments(tool.arguments) }));
|
|
331
|
+
if (toolCalls.length > 0)
|
|
332
|
+
return { action: "tool_call", tool_calls: toolCalls };
|
|
333
|
+
}
|
|
334
|
+
if (isRecord(parsed) && parsed.action === "tool_call" && Array.isArray(parsed.tool_calls)) {
|
|
335
|
+
const toolCalls = toolCallsFromArray(parsed.tool_calls)
|
|
336
|
+
.map((tool) => ({ name: tool.name, arguments: parseToolArguments(tool.arguments) }));
|
|
337
|
+
if (toolCalls.length > 0)
|
|
338
|
+
return { action: "tool_call", tool_calls: toolCalls };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
async function handleToolPlanning(request) {
|
|
347
|
+
const content = await collectTransport({
|
|
348
|
+
model: request.model,
|
|
349
|
+
messages: [{ role: "user", content: buildToolPrompt(request) }],
|
|
350
|
+
stream: false,
|
|
351
|
+
});
|
|
352
|
+
const plan = parseToolPlan(content);
|
|
353
|
+
const id = completionId();
|
|
354
|
+
const created = Math.floor(Date.now() / 1000);
|
|
355
|
+
const model = request.model ?? getDevinModels()[0].id;
|
|
356
|
+
if (plan?.action === "tool_call" && plan.tool_calls.length > 0) {
|
|
357
|
+
const toolCalls = plan.tool_calls.map((tool, index) => ({
|
|
358
|
+
id: `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}_${index}`,
|
|
359
|
+
type: "function",
|
|
360
|
+
function: {
|
|
361
|
+
name: tool.name,
|
|
362
|
+
arguments: stringifyToolArguments(tool.arguments),
|
|
363
|
+
},
|
|
364
|
+
}));
|
|
365
|
+
if (request.stream !== false) {
|
|
366
|
+
const streamToolCalls = toolCalls.map((toolCall, index) => ({ index, ...toolCall }));
|
|
367
|
+
return createStreamingResponse(id, created, model, async function* () {
|
|
368
|
+
yield { toolCalls: streamToolCalls, finishReason: "tool_calls" };
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return jsonResponse({
|
|
372
|
+
id,
|
|
373
|
+
object: "chat.completion",
|
|
374
|
+
created,
|
|
375
|
+
model,
|
|
376
|
+
choices: [
|
|
377
|
+
{
|
|
378
|
+
index: 0,
|
|
379
|
+
message: { role: "assistant", content: "", tool_calls: toolCalls },
|
|
380
|
+
finish_reason: "tool_calls",
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const finalContent = plan?.action === "final" ? plan.content : content;
|
|
386
|
+
if (request.stream !== false) {
|
|
387
|
+
return createStreamingResponse(id, created, model, async function* () {
|
|
388
|
+
yield { content: finalContent };
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return jsonResponse({
|
|
392
|
+
id,
|
|
393
|
+
object: "chat.completion",
|
|
394
|
+
created,
|
|
395
|
+
model,
|
|
396
|
+
choices: [
|
|
397
|
+
{
|
|
398
|
+
index: 0,
|
|
399
|
+
message: { role: "assistant", content: finalContent },
|
|
400
|
+
finish_reason: "stop",
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
export { buildDevinProviderModels };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devin-oauth-opencode",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "OpenCode plugin for Devin browser auth and cloud model access.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"devin-oauth-opencode": "bin/setup.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin/setup.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"package.json"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && bun build ./bin/setup.ts --target=node --format=esm --outfile=bin/setup.js --external=jsonc-parser",
|
|
28
|
+
"bench": "bun scripts/bench.ts",
|
|
29
|
+
"test": "bun test/smoke.ts",
|
|
30
|
+
"test:live": "bun test/live.ts",
|
|
31
|
+
"prepack": "npm run build",
|
|
32
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/jaredboynton/devin-oauth-opencode.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/jaredboynton/devin-oauth-opencode#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/jaredboynton/devin-oauth-opencode/issues"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"opencode",
|
|
44
|
+
"opencode-plugin",
|
|
45
|
+
"devin",
|
|
46
|
+
"oauth",
|
|
47
|
+
"openai-compatible"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@opencode-ai/plugin": "^1.2.27",
|
|
57
|
+
"jsonc-parser": "^3.3.1",
|
|
58
|
+
"zod": "^3.24.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/bun": "^1.3.11",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
}
|
|
64
|
+
}
|