opencode-kilocode-auth 1.0.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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Types for the Kilo Code OpenCode plugin
3
+ */
4
+
5
+ export interface KilocodeAuthDetails {
6
+ type: "kilocode";
7
+ token: string;
8
+ organizationId?: string;
9
+ model?: string;
10
+ userEmail?: string;
11
+ }
12
+
13
+ export interface NonKilocodeAuthDetails {
14
+ type: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export type AuthDetails = KilocodeAuthDetails | NonKilocodeAuthDetails;
19
+
20
+ export type GetAuth = () => Promise<AuthDetails>;
21
+
22
+ export interface ProviderModel {
23
+ cost?: {
24
+ input: number;
25
+ output: number;
26
+ };
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface Provider {
31
+ models?: Record<string, ProviderModel>;
32
+ options?: Record<string, unknown>;
33
+ }
34
+
35
+ export interface LoaderResult {
36
+ apiKey: string;
37
+ fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
38
+ }
39
+
40
+ export interface DeviceAuthInitiateResponse {
41
+ code: string;
42
+ verificationUrl: string;
43
+ expiresIn: number;
44
+ }
45
+
46
+ export interface DeviceAuthPollResponse {
47
+ status: "pending" | "approved" | "denied" | "expired";
48
+ token?: string;
49
+ userEmail?: string;
50
+ }
51
+
52
+ export interface KilocodeOrganization {
53
+ id: string;
54
+ name: string;
55
+ role: string;
56
+ }
57
+
58
+ export interface KilocodeProfileData {
59
+ id: string;
60
+ email: string;
61
+ name?: string;
62
+ organizations?: KilocodeOrganization[];
63
+ }
64
+
65
+ export interface ModelInfo {
66
+ id: string;
67
+ name: string;
68
+ displayName?: string;
69
+ contextWindow: number;
70
+ maxTokens?: number;
71
+ supportsImages?: boolean;
72
+ inputPrice?: number;
73
+ outputPrice?: number;
74
+ description?: string;
75
+ preferredIndex?: number;
76
+ }
77
+
78
+ export interface KilocodeAuthResult {
79
+ type: "success";
80
+ token: string;
81
+ userEmail?: string;
82
+ organizationId?: string;
83
+ model?: string;
84
+ }
85
+
86
+ export interface KilocodeAuthFailed {
87
+ type: "failed";
88
+ error: string;
89
+ }
90
+
91
+ export type KilocodeTokenExchangeResult = KilocodeAuthResult | KilocodeAuthFailed;
92
+
93
+ export interface AuthMethod {
94
+ provider?: string;
95
+ label: string;
96
+ type: "oauth" | "api" | "device";
97
+ authorize?: () => Promise<{
98
+ url: string;
99
+ instructions: string;
100
+ method: string;
101
+ callback: (() => Promise<KilocodeTokenExchangeResult>) | ((input: string) => Promise<KilocodeTokenExchangeResult>);
102
+ }>;
103
+ }
104
+
105
+ export interface PluginClient {
106
+ auth: {
107
+ set(input: { path: { id: string }; body: KilocodeAuthDetails }): Promise<void>;
108
+ };
109
+ }
110
+
111
+ export interface PluginContext {
112
+ client: PluginClient;
113
+ }
114
+
115
+ export interface PluginResult {
116
+ auth: {
117
+ provider: string;
118
+ loader: (getAuth: GetAuth, provider: Provider) => Promise<LoaderResult | null>;
119
+ methods: AuthMethod[];
120
+ };
121
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,231 @@
1
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin"
2
+ import {
3
+ KILOCODE_PROVIDER_ID,
4
+ KILOCODE_API_BASE_URL,
5
+ DEFAULT_HEADERS,
6
+ DEFAULT_MODEL_ID,
7
+ DEVICE_AUTH_POLL_INTERVAL_MS,
8
+ } from "./constants"
9
+ import {
10
+ initiateDeviceAuth,
11
+ pollDeviceAuth,
12
+ getKilocodeProfile,
13
+ getKilocodeDefaultModel,
14
+ getKilocodeModels,
15
+ openBrowserUrl,
16
+ getApiUrl,
17
+ } from "./kilocode/auth"
18
+
19
+ interface KilocodeAuth {
20
+ type: "oauth"
21
+ refresh: string // token
22
+ access?: string
23
+ expires?: number
24
+ organizationId?: string
25
+ model?: string
26
+ }
27
+
28
+ /**
29
+ * Kilo Code Authentication Plugin for OpenCode
30
+ *
31
+ * Provides authentication with Kilo Code API and access to 300+ AI models
32
+ * including the free Giga Potato model optimized for agentic programming.
33
+ */
34
+ export async function KilocodeAuthPlugin(input: PluginInput): Promise<Hooks> {
35
+ const sdk = input.client
36
+
37
+ return {
38
+ auth: {
39
+ provider: KILOCODE_PROVIDER_ID,
40
+
41
+ async loader(getAuth, provider) {
42
+ const auth = await getAuth()
43
+ if (!auth || auth.type !== "oauth") return {}
44
+
45
+ const kilocodeAuth = auth as KilocodeAuth
46
+ const token = kilocodeAuth.refresh
47
+
48
+ if (!token) return {}
49
+
50
+ // Get model from config or auth
51
+ const modelFromAuth = kilocodeAuth.model || DEFAULT_MODEL_ID
52
+
53
+ // Set costs to 0 for all models (Kilo Code handles billing)
54
+ if (provider && provider.models) {
55
+ for (const model of Object.values(provider.models)) {
56
+ if (model) {
57
+ model.cost = {
58
+ input: 0,
59
+ output: 0,
60
+ cache: { read: 0, write: 0 },
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ // Build base URL for OpenRouter-compatible API
67
+ const baseURL = getApiUrl("/api/openrouter/v1", token)
68
+
69
+ return {
70
+ baseURL,
71
+ apiKey: token,
72
+
73
+ async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
74
+ const currentAuth = await getAuth()
75
+ if (!currentAuth || currentAuth.type !== "oauth") {
76
+ return fetch(requestInput, init)
77
+ }
78
+
79
+ const currentKilocodeAuth = currentAuth as KilocodeAuth
80
+ const currentToken = currentKilocodeAuth.refresh
81
+
82
+ // Build headers
83
+ const headers = new Headers()
84
+ if (init?.headers) {
85
+ if (init.headers instanceof Headers) {
86
+ init.headers.forEach((value, key) => headers.set(key, value))
87
+ } else if (Array.isArray(init.headers)) {
88
+ for (const [key, value] of init.headers) {
89
+ if (value !== undefined) headers.set(key, String(value))
90
+ }
91
+ } else {
92
+ for (const [key, value] of Object.entries(init.headers)) {
93
+ if (value !== undefined) headers.set(key, String(value))
94
+ }
95
+ }
96
+ }
97
+
98
+ // Set Kilo Code authorization
99
+ headers.set("Authorization", `Bearer ${currentToken}`)
100
+ headers.set("X-KILOCODE-EDITORNAME", "opencode")
101
+
102
+ // Add organization header if present
103
+ if (currentKilocodeAuth.organizationId) {
104
+ headers.set("X-KILOCODE-ORGANIZATIONID", currentKilocodeAuth.organizationId)
105
+ }
106
+
107
+ // Rewrite URL to Kilo Code endpoint
108
+ const parsed =
109
+ requestInput instanceof URL
110
+ ? requestInput
111
+ : new URL(typeof requestInput === "string" ? requestInput : (requestInput as Request).url)
112
+
113
+ // Map to Kilo Code OpenRouter-compatible endpoint
114
+ let url: URL
115
+ if (parsed.pathname.includes("/chat/completions") || parsed.pathname.includes("/v1/chat/completions")) {
116
+ url = new URL(getApiUrl("/api/openrouter/v1/chat/completions", currentToken))
117
+ } else if (parsed.pathname.includes("/models")) {
118
+ url = new URL(getApiUrl("/api/openrouter/models", currentToken))
119
+ } else {
120
+ url = new URL(getApiUrl(`/api/openrouter${parsed.pathname}`, currentToken))
121
+ }
122
+
123
+ return fetch(url, {
124
+ ...init,
125
+ headers,
126
+ })
127
+ },
128
+ }
129
+ },
130
+
131
+ methods: [
132
+ {
133
+ type: "oauth",
134
+ label: "Login with Kilo Code",
135
+ async authorize() {
136
+ // Initiate device auth flow
137
+ const authData = await initiateDeviceAuth()
138
+ const { code, verificationUrl, expiresIn } = authData
139
+
140
+ // Open browser for user
141
+ openBrowserUrl(verificationUrl)
142
+
143
+ return {
144
+ url: verificationUrl,
145
+ instructions: `Enter code: ${code}`,
146
+ method: "auto" as const,
147
+
148
+ async callback() {
149
+ // Poll for authorization
150
+ const maxAttempts = Math.ceil((expiresIn * 1000) / DEVICE_AUTH_POLL_INTERVAL_MS)
151
+ let attempt = 0
152
+
153
+ while (attempt < maxAttempts) {
154
+ await new Promise((resolve) => setTimeout(resolve, DEVICE_AUTH_POLL_INTERVAL_MS))
155
+
156
+ try {
157
+ const pollResult = await pollDeviceAuth(code)
158
+
159
+ if (pollResult.status === "approved" && pollResult.token) {
160
+ // Get profile for organization
161
+ let organizationId: string | undefined
162
+ try {
163
+ const profile = await getKilocodeProfile(pollResult.token)
164
+ if (profile.organizations && profile.organizations.length > 0) {
165
+ organizationId = profile.organizations[0].id
166
+ }
167
+ } catch {
168
+ // Continue without organization
169
+ }
170
+
171
+ // Get default model
172
+ const model = await getKilocodeDefaultModel(pollResult.token, organizationId)
173
+
174
+ return {
175
+ type: "success" as const,
176
+ refresh: pollResult.token,
177
+ access: pollResult.token,
178
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000, // 1 year (tokens don't expire normally)
179
+ // Store extra info in the auth
180
+ organizationId,
181
+ model,
182
+ } as any
183
+ }
184
+
185
+ if (pollResult.status === "denied") {
186
+ return { type: "failed" as const }
187
+ }
188
+
189
+ if (pollResult.status === "expired") {
190
+ return { type: "failed" as const }
191
+ }
192
+ } catch {
193
+ // Continue polling on error
194
+ }
195
+
196
+ attempt++
197
+ }
198
+
199
+ return { type: "failed" as const }
200
+ },
201
+ }
202
+ },
203
+ },
204
+ {
205
+ type: "api",
206
+ label: "Enter Kilo Code API Token",
207
+ },
208
+ ],
209
+ },
210
+
211
+ // Add custom headers for Kilo Code requests
212
+ "chat.headers": async (input, output) => {
213
+ if (input.model.providerID !== KILOCODE_PROVIDER_ID) return
214
+
215
+ output.headers["X-KILOCODE-EDITORNAME"] = "opencode"
216
+ output.headers["X-KILOCODE-SESSIONID"] = input.sessionID
217
+ },
218
+ }
219
+ }
220
+
221
+ // Export utilities for external use
222
+ export {
223
+ initiateDeviceAuth,
224
+ pollDeviceAuth,
225
+ getKilocodeProfile,
226
+ getKilocodeDefaultModel,
227
+ getKilocodeModels,
228
+ getApiUrl,
229
+ }
230
+
231
+ export type { KilocodeAuth }