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 ADDED
@@ -0,0 +1,12 @@
1
+ # devin-oauth-opencode
2
+
3
+ OpenCode plugin for Devin browser auth and cloud model access through a local OpenAI-compatible proxy.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npx devin-oauth-opencode setup --global
9
+ opencode auth login --provider devin
10
+ ```
11
+
12
+ The setup command patches OpenCode config so the `devin` provider is visible. Login stores auth through OpenCode, and model requests are routed through a local proxy to Devin's cloud transport.
package/bin/setup.js ADDED
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/setup.ts
4
+ import { realpathSync } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { applyEdits, format, modify, parse } from "jsonc-parser";
10
+ var DEFAULT_PLUGIN_SPEC = "devin-oauth-opencode@latest";
11
+ var PACKAGE_NAME = "devin-oauth-opencode";
12
+ var SCHEMA = "https://opencode.ai/config.json";
13
+ var PROVIDER_ID = "devin";
14
+ var PROVIDER_API = "http://127.0.0.1:65534/v1";
15
+ var PROVIDER_NPM = "@ai-sdk/openai-compatible";
16
+ var DEVIN_MODELS = {
17
+ "glm-5-2": {
18
+ name: "GLM-5.2 High",
19
+ reasoning: true,
20
+ temperature: true,
21
+ attachment: false,
22
+ tool_call: true,
23
+ limit: { context: 200000, input: 200000, output: 64000 }
24
+ },
25
+ "swe-1.6-fast": {
26
+ name: "SWE 1.6 Fast",
27
+ reasoning: true,
28
+ temperature: true,
29
+ attachment: false,
30
+ tool_call: true,
31
+ limit: { context: 200000, input: 200000, output: 64000 }
32
+ },
33
+ "swe-1.6": {
34
+ name: "SWE 1.6",
35
+ reasoning: true,
36
+ temperature: true,
37
+ attachment: false,
38
+ tool_call: true,
39
+ limit: { context: 200000, input: 200000, output: 64000 }
40
+ },
41
+ "swe-1.5-fast": {
42
+ name: "SWE 1.5 Fast",
43
+ reasoning: true,
44
+ temperature: true,
45
+ attachment: false,
46
+ tool_call: true,
47
+ limit: { context: 200000, input: 200000, output: 64000 }
48
+ }
49
+ };
50
+ var GENERATED_MODEL_KEYS = new Set([
51
+ "name",
52
+ "reasoning",
53
+ "temperature",
54
+ "attachment",
55
+ "tool_call",
56
+ "limit"
57
+ ]);
58
+ function isRecord(value) {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+ function sanitizeGeneratedModelOverride(override) {
62
+ if (!isRecord(override))
63
+ return {};
64
+ const sanitized = { ...override };
65
+ for (const key of GENERATED_MODEL_KEYS)
66
+ delete sanitized[key];
67
+ return sanitized;
68
+ }
69
+ function providerModelsWithRepairs(existingModels) {
70
+ const models = {};
71
+ for (const [id, model] of Object.entries(DEVIN_MODELS)) {
72
+ models[id] = {
73
+ ...model,
74
+ ...sanitizeGeneratedModelOverride(existingModels[id])
75
+ };
76
+ }
77
+ for (const [id, model] of Object.entries(existingModels)) {
78
+ if (id in DEVIN_MODELS)
79
+ continue;
80
+ models[id] = model;
81
+ }
82
+ return models;
83
+ }
84
+ function globalConfigPath() {
85
+ const configHome = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
86
+ return path.join(configHome, "opencode", "opencode.json");
87
+ }
88
+ function projectConfigPath(cwd) {
89
+ return path.join(cwd, "opencode.json");
90
+ }
91
+ function targetPath(options) {
92
+ if (options.configPath)
93
+ return path.resolve(options.configPath);
94
+ if (options.mode === "project")
95
+ return projectConfigPath(options.cwd ?? process.cwd());
96
+ return globalConfigPath();
97
+ }
98
+ function applyJsonPatch(text, jsonPath, value) {
99
+ return applyEdits(text, modify(text, jsonPath, value, {
100
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: `
101
+ ` }
102
+ }));
103
+ }
104
+ function pluginPackageName(specifier) {
105
+ const normalized = specifier.startsWith("npm:") ? specifier.slice(4) : specifier;
106
+ if (normalized.startsWith("file://")) {
107
+ return path.basename(new URL(normalized).pathname).replace(/\.[cm]?[jt]s$/, "");
108
+ }
109
+ const lastAt = normalized.lastIndexOf("@");
110
+ return lastAt > 0 ? normalized.slice(0, lastAt) : normalized;
111
+ }
112
+ function patchConfigText(input, pluginSpec = DEFAULT_PLUGIN_SPEC) {
113
+ let text = input.trim() ? input : "{}";
114
+ const initial = parse(text) ?? {};
115
+ if (!initial.$schema)
116
+ text = applyJsonPatch(text, ["$schema"], SCHEMA);
117
+ const currentPlugins = (parse(text) ?? {}).plugin;
118
+ const plugins = Array.isArray(currentPlugins) ? [...currentPlugins] : [];
119
+ if (!plugins.some((plugin) => typeof plugin === "string" && pluginPackageName(plugin) === PACKAGE_NAME)) {
120
+ plugins.push(pluginSpec);
121
+ }
122
+ text = applyJsonPatch(text, ["plugin"], plugins);
123
+ const next = parse(text) ?? {};
124
+ const existingProvider = next.provider?.[PROVIDER_ID] ?? {};
125
+ const existingModels = existingProvider.models ?? {};
126
+ text = applyJsonPatch(text, ["provider", PROVIDER_ID], {
127
+ ...existingProvider,
128
+ name: existingProvider.name ?? "Devin",
129
+ npm: existingProvider.npm ?? PROVIDER_NPM,
130
+ api: existingProvider.api ?? PROVIDER_API,
131
+ models: providerModelsWithRepairs(existingModels)
132
+ });
133
+ return applyEdits(text, format(text, undefined, {
134
+ insertSpaces: true,
135
+ tabSize: 2,
136
+ eol: `
137
+ `
138
+ }));
139
+ }
140
+ async function setupOpenCodeConfig(options = {}) {
141
+ const file = targetPath(options);
142
+ let existing = "";
143
+ try {
144
+ existing = await fs.readFile(file, "utf8");
145
+ } catch (error) {
146
+ if (error?.code !== "ENOENT")
147
+ throw error;
148
+ }
149
+ const updated = patchConfigText(existing, options.pluginSpec ?? DEFAULT_PLUGIN_SPEC);
150
+ await fs.mkdir(path.dirname(file), { recursive: true });
151
+ await fs.writeFile(file, updated.endsWith(`
152
+ `) ? updated : `${updated}
153
+ `, "utf8");
154
+ return file;
155
+ }
156
+ function parseArgs(argv) {
157
+ const options = { mode: "global" };
158
+ for (let index = 0;index < argv.length; index++) {
159
+ const arg = argv[index];
160
+ if (arg === "setup")
161
+ continue;
162
+ if (arg === "--project")
163
+ options.mode = "project";
164
+ else if (arg === "--global")
165
+ options.mode = "global";
166
+ else if (arg === "--path")
167
+ options.configPath = argv[++index];
168
+ else if (arg === "--plugin")
169
+ options.pluginSpec = argv[++index];
170
+ else if (arg === "--help" || arg === "-h") {
171
+ console.log("Usage: devin-oauth-opencode setup [--global|--project] [--path <opencode.json>] [--plugin <specifier>]");
172
+ process.exit(0);
173
+ }
174
+ }
175
+ return options;
176
+ }
177
+ function isDirectExecution(moduleUrl = import.meta.url, argvPath = process.argv[1]) {
178
+ if (!argvPath)
179
+ return false;
180
+ const modulePath = fileURLToPath(moduleUrl);
181
+ try {
182
+ return realpathSync(modulePath) === realpathSync(argvPath);
183
+ } catch {
184
+ return path.resolve(modulePath) === path.resolve(argvPath);
185
+ }
186
+ }
187
+ if (isDirectExecution()) {
188
+ setupOpenCodeConfig(parseArgs(process.argv.slice(2))).then((file) => {
189
+ console.log(`Updated OpenCode config: ${file}`);
190
+ }).catch((error) => {
191
+ console.error(error instanceof Error ? error.message : String(error));
192
+ process.exit(1);
193
+ });
194
+ }
195
+ export {
196
+ setupOpenCodeConfig,
197
+ patchConfigText,
198
+ isDirectExecution
199
+ };
package/dist/auth.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare enum DevinErrorCode {
2
+ ApiKeyMissing = "API_KEY_MISSING",
3
+ ConnectionFailed = "CONNECTION_FAILED",
4
+ StreamError = "STREAM_ERROR"
5
+ }
6
+ export declare class DevinError extends Error {
7
+ readonly code: DevinErrorCode;
8
+ readonly details?: unknown | undefined;
9
+ constructor(message: string, code: DevinErrorCode, details?: unknown | undefined);
10
+ }
11
+ export declare function setDevinApiKeyForSession(apiKey?: string): void;
12
+ export declare function getApiKey(): string;
13
+ export declare function hasSessionOrEnvApiKey(): boolean;
14
+ export declare function getDevinVersion(): string;
15
+ export declare function getLatestDevinVersion(fetchImpl?: typeof fetch, options?: {
16
+ timeoutMs?: number;
17
+ }): Promise<string>;
package/dist/auth.js ADDED
@@ -0,0 +1,88 @@
1
+ export var DevinErrorCode;
2
+ (function (DevinErrorCode) {
3
+ DevinErrorCode["ApiKeyMissing"] = "API_KEY_MISSING";
4
+ DevinErrorCode["ConnectionFailed"] = "CONNECTION_FAILED";
5
+ DevinErrorCode["StreamError"] = "STREAM_ERROR";
6
+ })(DevinErrorCode || (DevinErrorCode = {}));
7
+ export class DevinError extends Error {
8
+ code;
9
+ details;
10
+ constructor(message, code, details) {
11
+ super(message);
12
+ this.code = code;
13
+ this.details = details;
14
+ this.name = "DevinError";
15
+ }
16
+ }
17
+ const DEFAULT_DEVIN_VERSION = "3000.1.23";
18
+ const DEVIN_CLI_MANIFEST_URL = "https://static.devin.ai/cli/current/manifest.json";
19
+ let sessionApiKeyOverride;
20
+ let cachedLatestDevinVersion;
21
+ let latestVersionPromise;
22
+ export function setDevinApiKeyForSession(apiKey) {
23
+ sessionApiKeyOverride = apiKey?.trim() || undefined;
24
+ }
25
+ export function getApiKey() {
26
+ if (sessionApiKeyOverride) {
27
+ return sessionApiKeyOverride;
28
+ }
29
+ if (process.env.DEVIN_API_KEY?.trim()) {
30
+ return process.env.DEVIN_API_KEY.trim();
31
+ }
32
+ throw new DevinError("Devin API key not found. Run `opencode auth login --provider devin` or set DEVIN_API_KEY.", DevinErrorCode.ApiKeyMissing);
33
+ }
34
+ export function hasSessionOrEnvApiKey() {
35
+ return Boolean(sessionApiKeyOverride || process.env.DEVIN_API_KEY?.trim());
36
+ }
37
+ export function getDevinVersion() {
38
+ return DEFAULT_DEVIN_VERSION;
39
+ }
40
+ function parseVersionFromManifest(value) {
41
+ try {
42
+ const version = JSON.parse(value).version;
43
+ return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version.trim())
44
+ ? version.trim()
45
+ : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ export async function getLatestDevinVersion(fetchImpl = fetch, options = {}) {
52
+ const shouldCache = fetchImpl === fetch;
53
+ if (shouldCache && cachedLatestDevinVersion)
54
+ return cachedLatestDevinVersion;
55
+ if (fetchImpl === fetch && latestVersionPromise)
56
+ return latestVersionPromise;
57
+ const resolveVersion = async () => {
58
+ const controller = new AbortController();
59
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 1_500);
60
+ try {
61
+ const response = await fetchImpl(DEVIN_CLI_MANIFEST_URL, {
62
+ signal: controller.signal,
63
+ });
64
+ if (response.ok) {
65
+ const version = parseVersionFromManifest(await response.text());
66
+ if (version) {
67
+ if (shouldCache)
68
+ cachedLatestDevinVersion = version;
69
+ return version;
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // Use the known-compatible version if the manifest is unavailable.
75
+ }
76
+ finally {
77
+ clearTimeout(timeout);
78
+ }
79
+ return getDevinVersion();
80
+ };
81
+ if (fetchImpl !== fetch) {
82
+ return resolveVersion();
83
+ }
84
+ latestVersionPromise = resolveVersion().finally(() => {
85
+ latestVersionPromise = undefined;
86
+ });
87
+ return latestVersionPromise;
88
+ }
@@ -0,0 +1,34 @@
1
+ export interface DevinBrowserAuth {
2
+ apiKey: string;
3
+ name?: string;
4
+ email?: string;
5
+ }
6
+ export interface BrowserLoginOptions {
7
+ state?: string;
8
+ loginHint?: string;
9
+ }
10
+ export interface DevinLoopbackAuth {
11
+ url: string;
12
+ verifier: string;
13
+ state: string;
14
+ waitForCode(): Promise<string>;
15
+ }
16
+ type FetchLike = typeof fetch;
17
+ export declare function createPkcePair(): {
18
+ verifier: string;
19
+ challenge: string;
20
+ };
21
+ export declare function buildDevinContinueUrl(options: BrowserLoginOptions & {
22
+ redirectUri: string;
23
+ challenge: string;
24
+ state: string;
25
+ }): string;
26
+ declare function extractCode(params: URLSearchParams): string;
27
+ export declare function startDevinLoopbackAuth(options?: BrowserLoginOptions): DevinLoopbackAuth;
28
+ export declare function exchangeDevinCode(code: string, verifier: string, fetchImpl?: FetchLike): Promise<DevinBrowserAuth>;
29
+ export declare const __test: {
30
+ buildDevinContinueUrl: typeof buildDevinContinueUrl;
31
+ createPkcePair: typeof createPkcePair;
32
+ extractCode: typeof extractCode;
33
+ };
34
+ export {};
@@ -0,0 +1,126 @@
1
+ import crypto from "node:crypto";
2
+ const DEVIN_CONTINUE_URL = "https://app.devin.ai/auth/cli/continue";
3
+ const DEVIN_TOKEN_URL = "https://api.devin.ai/auth/cli/token";
4
+ const DEVIN_SESSION_TOKEN_PREFIX = "devin-session-token$";
5
+ const LOOPBACK_HOST = "127.0.0.1";
6
+ const LOOPBACK_TIMEOUT_MS = 120_000;
7
+ function base64Url(buffer) {
8
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9
+ }
10
+ export function createPkcePair() {
11
+ const verifier = base64Url(crypto.randomBytes(32));
12
+ const challenge = base64Url(crypto.createHash("sha256").update(verifier).digest());
13
+ return { verifier, challenge };
14
+ }
15
+ export function buildDevinContinueUrl(options) {
16
+ const url = new URL(DEVIN_CONTINUE_URL);
17
+ url.searchParams.set("redirect_uri", options.redirectUri);
18
+ url.searchParams.set("state", options.state);
19
+ url.searchParams.set("prompt", "select_account");
20
+ url.searchParams.set("code_challenge", options.challenge);
21
+ url.searchParams.set("code_challenge_method", "S256");
22
+ url.searchParams.set("cli_pkce_marker", "1");
23
+ if (options.loginHint?.trim()) {
24
+ url.searchParams.set("login_hint", options.loginHint.trim());
25
+ }
26
+ return url.toString();
27
+ }
28
+ function extractCode(params) {
29
+ return (params.get("code") ?? params.get("authorization_code") ?? "").trim();
30
+ }
31
+ export function startDevinLoopbackAuth(options = {}) {
32
+ const { verifier, challenge } = createPkcePair();
33
+ const state = options.state ?? crypto.randomUUID();
34
+ let resolveCode;
35
+ let rejectCode;
36
+ let settled = false;
37
+ const codePromise = new Promise((resolve, reject) => {
38
+ resolveCode = resolve;
39
+ rejectCode = reject;
40
+ });
41
+ const server = Bun.serve({
42
+ hostname: LOOPBACK_HOST,
43
+ port: 0,
44
+ fetch(req) {
45
+ const url = new URL(req.url);
46
+ if (url.pathname === "/callback-token") {
47
+ const code = extractCode(url.searchParams);
48
+ if (!settled) {
49
+ settled = true;
50
+ if (code)
51
+ resolveCode(code);
52
+ else
53
+ rejectCode(new Error("Devin callback did not include an authorization code"));
54
+ }
55
+ return new Response("OK", { headers: { "content-type": "text/plain" } });
56
+ }
57
+ if (url.pathname === "/callback") {
58
+ const code = extractCode(url.searchParams);
59
+ if (code) {
60
+ if (!settled) {
61
+ settled = true;
62
+ resolveCode(code);
63
+ }
64
+ return new Response("Devin authentication complete. You can close this tab.", {
65
+ headers: { "content-type": "text/plain" },
66
+ });
67
+ }
68
+ return new Response(`<!doctype html>
69
+ <html><body>
70
+ <p>Devin authentication complete. You can close this tab.</p>
71
+ <script>
72
+ const params = new URLSearchParams(location.hash.slice(1));
73
+ fetch('/callback-token?' + params.toString()).catch(() => {});
74
+ </script>
75
+ </body></html>`, { headers: { "content-type": "text/html" } });
76
+ }
77
+ return new Response("Not found", { status: 404 });
78
+ },
79
+ });
80
+ const timeout = setTimeout(() => {
81
+ if (!settled) {
82
+ settled = true;
83
+ rejectCode(new Error("Devin authentication timed out"));
84
+ }
85
+ }, LOOPBACK_TIMEOUT_MS);
86
+ const redirectUri = `http://${LOOPBACK_HOST}:${server.port}/callback`;
87
+ return {
88
+ url: buildDevinContinueUrl({ ...options, redirectUri, challenge, state }),
89
+ verifier,
90
+ state,
91
+ async waitForCode() {
92
+ try {
93
+ return await codePromise;
94
+ }
95
+ finally {
96
+ clearTimeout(timeout);
97
+ setTimeout(() => server.stop(true), 100);
98
+ }
99
+ },
100
+ };
101
+ }
102
+ export async function exchangeDevinCode(code, verifier, fetchImpl = fetch) {
103
+ const trimmedCode = code.trim();
104
+ if (!trimmedCode) {
105
+ throw new Error("Devin authorization code is required");
106
+ }
107
+ const response = await fetchImpl(DEVIN_TOKEN_URL, {
108
+ method: "POST",
109
+ headers: { "content-type": "application/json" },
110
+ body: JSON.stringify({ code: trimmedCode, code_verifier: verifier }),
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Devin token exchange failed with HTTP ${response.status}`);
114
+ }
115
+ const payload = (await response.json());
116
+ const token = payload.token?.trim();
117
+ if (!token) {
118
+ throw new Error("Devin token exchange response did not include a token");
119
+ }
120
+ return { apiKey: `${DEVIN_SESSION_TOKEN_PREFIX}${token}` };
121
+ }
122
+ export const __test = {
123
+ buildDevinContinueUrl,
124
+ createPkcePair,
125
+ extractCode,
126
+ };
@@ -0,0 +1,21 @@
1
+ import type { ChatCompletionRequest } from "./devin";
2
+ declare function parseFields(buffer: Buffer): Array<{
3
+ fieldNum: number;
4
+ wireType: number;
5
+ value: bigint | Buffer;
6
+ }>;
7
+ declare function parseConnectEnvelopes(buffer: Buffer): Array<{
8
+ flags: number;
9
+ body: Buffer;
10
+ }>;
11
+ export declare function closeDevinCloudConnection(): void;
12
+ export declare function warmDevinCloudConnection(): Promise<void>;
13
+ declare function buildGetChatMessageRequest(request: ChatCompletionRequest, apiKey: string, version: string): Buffer;
14
+ export declare function streamDevinCloudChat(request: ChatCompletionRequest): AsyncGenerator<string, void, unknown>;
15
+ export declare function collectDevinCloudChat(request: ChatCompletionRequest): Promise<string>;
16
+ export declare const __test: {
17
+ buildGetChatMessageRequest: typeof buildGetChatMessageRequest;
18
+ parseFields: typeof parseFields;
19
+ parseConnectEnvelopes: typeof parseConnectEnvelopes;
20
+ };
21
+ export {};