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.
- package/README.md +229 -0
- package/index.ts +11 -0
- package/package.json +21 -0
- package/src/cli/select-model.ts +111 -0
- package/src/constants.ts +62 -0
- package/src/kilocode/auth.ts +281 -0
- package/src/kilocode/models.ts +242 -0
- package/src/plugin/request.ts +165 -0
- package/src/plugin/types.ts +121 -0
- package/src/plugin.ts +231 -0
|
@@ -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 }
|