@bogyie/opencode-kiro-plugin 0.3.7 → 0.3.9
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 +8 -8
- package/dist/plugin.js +86 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ The plugin resolves credentials in this order:
|
|
|
38
38
|
3. Local Kiro CLI session token from the Kiro CLI SQLite store
|
|
39
39
|
4. `kiro-cli whoami` diagnostics for CLI session visibility
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
OpenCode startup does not start Kiro login, model discovery, or provider injection. Kiro is added through OpenCode's provider connector, or by explicitly defining `provider.kiro` yourself. Model discovery runs only when you explicitly call the `kiro_refresh_models` plugin tool; if discovery succeeds, later model-list requests use the latest in-memory cache. If discovery fails, the previous cache remains in use.
|
|
42
42
|
|
|
43
|
-
You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. The connector uses AWS OIDC device authorization directly, so it does not rely on a localhost callback URL. After login succeeds, the plugin stores the access token, refresh token, OIDC client credentials, region, and optional profile ARN in OpenCode auth. Direct fetch mode reuses that stored credential and refreshes the access token before expiry; it does not ask you to log in again while the refresh token remains valid. If no OpenCode device credential or API key is configured, direct fetch reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
|
|
43
|
+
You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. When `login.identityProvider` and `login.region` are configured, use `Kiro device login (configured)` to start login without re-entering those values. Use `Kiro device login (custom)` only when you need to override the Start URL, OIDC region, or profile ARN for that login. The connector uses AWS OIDC device authorization directly, so it does not rely on a localhost callback URL. After login succeeds, the plugin stores the access token, refresh token, OIDC client credentials, region, and optional profile ARN in OpenCode auth. Direct fetch mode reuses that stored credential and refreshes the access token before expiry; it does not ask you to log in again while the refresh token remains valid. If no OpenCode device credential or API key is configured, direct fetch reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If an API/model call fails with an auth error, the selected transport starts the Kiro login flow from the configured `login` options and retries the request once. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
|
|
44
44
|
|
|
45
45
|
For AWS IAM Identity Center login, configure the default device-flow Start URL separately from the API region:
|
|
46
46
|
|
|
@@ -62,9 +62,9 @@ For AWS IAM Identity Center login, configure the default device-flow Start URL s
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
For IAM Identity Center, the plugin opens the AWS portal device URL, such as `https://example.awsapps.com/start/#/device?user_code=...`, instead of a `localhost` callback URL.
|
|
66
66
|
|
|
67
|
-
Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
|
|
67
|
+
Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Use `kiro_refresh_models` when you explicitly want to run the configured model discovery command and update the in-memory model cache. Secrets are redacted in diagnostics.
|
|
68
68
|
|
|
69
69
|
## Backend Modes
|
|
70
70
|
|
|
@@ -105,7 +105,7 @@ Supported values:
|
|
|
105
105
|
|
|
106
106
|
## Model Churn Handling
|
|
107
107
|
|
|
108
|
-
The resolver intentionally avoids a hard whitelist.
|
|
108
|
+
The resolver intentionally avoids a hard whitelist. Startup does not add a fallback `auto` model. When you run `kiro_refresh_models`, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json`. Runtime discovery is the source of truth for the model picker once it has succeeded; failed refreshes do not clear the last successful cache.
|
|
109
109
|
|
|
110
110
|
Useful options:
|
|
111
111
|
|
|
@@ -151,11 +151,11 @@ Resolution order:
|
|
|
151
151
|
6. Hidden/manual model mapping
|
|
152
152
|
7. Optimistic pass-through unless disabled
|
|
153
153
|
|
|
154
|
-
This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker. Use `extraModels` only when you explicitly want a model to appear even though it is not listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
|
|
154
|
+
This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker after discovery has succeeded. Use `extraModels` only when you explicitly want a model to appear even though it is not listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
|
|
155
155
|
|
|
156
|
-
The plugin
|
|
156
|
+
The plugin does not inject `provider.kiro` during startup. Add Kiro through OpenCode's provider connector, or define `provider.kiro` yourself when you need explicit model-picker metadata such as a display name or context limit. Use plugin `modelAliases` for aliases that should resolve one requested model id to another.
|
|
157
157
|
|
|
158
|
-
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to
|
|
158
|
+
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to disable `kiro_refresh_models`. Discovery is best-effort and never runs during OpenCode startup; authenticate manually through the connector or let an API/model request handle auth failure. Discovery stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, Kiro CLI list-models JSON, Kiro CLI plain list output, or one model id per line.
|
|
159
159
|
|
|
160
160
|
## Troubleshooting
|
|
161
161
|
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import { KiroAcpTransport } from "./acp-transport.js";
|
|
3
|
-
import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, resolveApiKey, runKiroLoginFlowOnce, } from "./auth.js";
|
|
3
|
+
import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, readKiroCliSessionCredential, resolveApiKey, runKiroLoginFlowOnce, } from "./auth.js";
|
|
4
4
|
import { KiroCliChatTransport } from "./cli-transport.js";
|
|
5
5
|
import { loadOptions } from "./config.js";
|
|
6
6
|
import { createKiroFetch } from "./fetch-adapter.js";
|
|
@@ -9,8 +9,6 @@ import { ModelCache } from "./model-cache.js";
|
|
|
9
9
|
import { refreshModelCacheFromCommand } from "./model-discovery.js";
|
|
10
10
|
import { ModelResolver, normalizeModelName } from "./model-resolver.js";
|
|
11
11
|
import { KiroRestTransport } from "./kiro-rest-transport.js";
|
|
12
|
-
const PLACEHOLDER_MODEL_ID = "auto";
|
|
13
|
-
const PLACEHOLDER_MODEL = { name: "Auto" };
|
|
14
12
|
function discoveredProviderModels(cache) {
|
|
15
13
|
return Object.fromEntries(cache.all().map((model) => [
|
|
16
14
|
model.id,
|
|
@@ -44,9 +42,7 @@ function visibleProviderModels(cache, configuredModels, hiddenModels, disabledMo
|
|
|
44
42
|
...(configuredModels[id] ?? {}),
|
|
45
43
|
},
|
|
46
44
|
]));
|
|
47
|
-
|
|
48
|
-
return models;
|
|
49
|
-
return { [PLACEHOLDER_MODEL_ID]: PLACEHOLDER_MODEL };
|
|
45
|
+
return models;
|
|
50
46
|
}
|
|
51
47
|
function bearerToken(init) {
|
|
52
48
|
const header = new Headers(init?.headers).get("authorization");
|
|
@@ -84,8 +80,8 @@ function localTransport(options, accessToken) {
|
|
|
84
80
|
const apiKey = accessToken || process.env.KIRO_API_KEY;
|
|
85
81
|
if (isKiroDeviceAuthKey(apiKey)) {
|
|
86
82
|
return new KiroRestTransport(fetchTransportOptions(options), {
|
|
87
|
-
credentialProvider: () => credentialFromKiroDeviceAuthKey(apiKey).catch(() => undefined),
|
|
88
|
-
login:
|
|
83
|
+
credentialProvider: async () => (await credentialFromKiroDeviceAuthKey(apiKey).catch(() => undefined)) ?? readKiroCliSessionCredential(),
|
|
84
|
+
login: () => runKiroLoginFlowOnce({ login: options.login }),
|
|
89
85
|
});
|
|
90
86
|
}
|
|
91
87
|
return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey), {
|
|
@@ -124,20 +120,25 @@ export function createKiroPlugin() {
|
|
|
124
120
|
modelCache.update(Object.keys(options.extraModels).map((id) => ({ id: normalizeModelName(id) })));
|
|
125
121
|
}
|
|
126
122
|
let discovery;
|
|
127
|
-
let
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return;
|
|
123
|
+
let lastModelDiscoveryAt = 0;
|
|
124
|
+
const refreshModels = async (force = false) => {
|
|
125
|
+
const discoveryIsStale = lastModelDiscoveryAt === 0 || Date.now() - lastModelDiscoveryAt > options.modelCacheTtlSeconds * 1000;
|
|
126
|
+
if (options.modelDiscovery === "off" || options.modelDiscoveryCommand.length === 0 || (!force && !discoveryIsStale)) {
|
|
127
|
+
return [];
|
|
133
128
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
129
|
+
if (!discovery) {
|
|
130
|
+
discovery = refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
|
|
131
|
+
.then((models) => {
|
|
132
|
+
if (models.length > 0)
|
|
133
|
+
lastModelDiscoveryAt = Date.now();
|
|
134
|
+
return models;
|
|
135
|
+
})
|
|
136
|
+
.catch(() => [])
|
|
137
|
+
.finally(() => {
|
|
138
|
+
discovery = undefined;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return (await discovery);
|
|
141
142
|
};
|
|
142
143
|
const resolver = new ModelResolver({
|
|
143
144
|
cache: modelCache,
|
|
@@ -159,16 +160,50 @@ export function createKiroPlugin() {
|
|
|
159
160
|
localServer = await startLocalKiroServer(localFetch);
|
|
160
161
|
return localServer;
|
|
161
162
|
};
|
|
163
|
+
const authorizeDeviceLogin = async (inputs) => {
|
|
164
|
+
const startUrl = inputString(inputs, "startUrl") ?? options.login.identityProvider;
|
|
165
|
+
const idcRegion = inputString(inputs, "idcRegion") ?? options.login.region ?? "us-east-1";
|
|
166
|
+
const profileArn = inputString(inputs, "profileArn") ?? options.profileArn;
|
|
167
|
+
const authorization = await authorizeKiroDevice({
|
|
168
|
+
region: idcRegion,
|
|
169
|
+
...(startUrl ? { identityProvider: startUrl } : {}),
|
|
170
|
+
});
|
|
171
|
+
const url = startUrl ? kiroDeviceVerificationUrl(authorization.startUrl, authorization.userCode) : authorization.verificationUrlComplete;
|
|
172
|
+
return {
|
|
173
|
+
url,
|
|
174
|
+
instructions: `Open the verification URL and complete Kiro sign-in.\nCode: ${authorization.userCode}`,
|
|
175
|
+
method: "auto",
|
|
176
|
+
callback: async () => {
|
|
177
|
+
const credential = await pollKiroDeviceToken(authorization, {
|
|
178
|
+
region: options.region,
|
|
179
|
+
...(profileArn ? { profileArn } : {}),
|
|
180
|
+
});
|
|
181
|
+
const key = encodeKiroDeviceAuthKey(credential);
|
|
182
|
+
return {
|
|
183
|
+
type: "success",
|
|
184
|
+
key,
|
|
185
|
+
access: key,
|
|
186
|
+
refresh: credential.refreshToken,
|
|
187
|
+
expires: credential.expiresAt,
|
|
188
|
+
metadata: {
|
|
189
|
+
source: "kiro-device-auth",
|
|
190
|
+
region: credential.region,
|
|
191
|
+
oidcRegion: credential.oidcRegion,
|
|
192
|
+
...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
};
|
|
162
198
|
return {
|
|
163
199
|
dispose: async () => {
|
|
164
200
|
await localServer?.close();
|
|
165
201
|
localServer = undefined;
|
|
166
202
|
},
|
|
167
203
|
config: async (config) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const provider = config.provider[options.providerID];
|
|
204
|
+
const provider = config.provider?.[options.providerID];
|
|
205
|
+
if (!provider)
|
|
206
|
+
return;
|
|
172
207
|
const server = await ensureLocalServer();
|
|
173
208
|
userModelOverrides ??= modelRecord(provider.models);
|
|
174
209
|
configuredModels = {
|
|
@@ -186,7 +221,12 @@ export function createKiroPlugin() {
|
|
|
186
221
|
methods: [
|
|
187
222
|
{
|
|
188
223
|
type: "oauth",
|
|
189
|
-
label: "Kiro device login",
|
|
224
|
+
label: options.login.identityProvider ? "Kiro device login (configured)" : "Kiro device login",
|
|
225
|
+
authorize: async () => authorizeDeviceLogin(),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: "oauth",
|
|
229
|
+
label: "Kiro device login (custom)",
|
|
190
230
|
prompts: [
|
|
191
231
|
{
|
|
192
232
|
type: "text",
|
|
@@ -213,44 +253,7 @@ export function createKiroPlugin() {
|
|
|
213
253
|
placeholder: "arn:aws:codewhisperer:us-east-1:123456789012:profile/XXXXXXXXXX",
|
|
214
254
|
},
|
|
215
255
|
],
|
|
216
|
-
authorize: async (inputs) =>
|
|
217
|
-
const promptInputs = inputs;
|
|
218
|
-
const startUrl = inputString(promptInputs, "startUrl") ?? options.login.identityProvider;
|
|
219
|
-
const idcRegion = inputString(promptInputs, "idcRegion") ?? options.login.region ?? "us-east-1";
|
|
220
|
-
const profileArn = inputString(promptInputs, "profileArn") ?? options.profileArn;
|
|
221
|
-
const authorization = await authorizeKiroDevice({
|
|
222
|
-
region: idcRegion,
|
|
223
|
-
...(startUrl ? { identityProvider: startUrl } : {}),
|
|
224
|
-
});
|
|
225
|
-
const url = startUrl
|
|
226
|
-
? kiroDeviceVerificationUrl(authorization.startUrl, authorization.userCode)
|
|
227
|
-
: authorization.verificationUrlComplete;
|
|
228
|
-
return {
|
|
229
|
-
url,
|
|
230
|
-
instructions: `Open the verification URL and complete Kiro sign-in.\nCode: ${authorization.userCode}`,
|
|
231
|
-
method: "auto",
|
|
232
|
-
callback: async () => {
|
|
233
|
-
const credential = await pollKiroDeviceToken(authorization, {
|
|
234
|
-
region: options.region,
|
|
235
|
-
...(profileArn ? { profileArn } : {}),
|
|
236
|
-
});
|
|
237
|
-
const key = encodeKiroDeviceAuthKey(credential);
|
|
238
|
-
return {
|
|
239
|
-
type: "success",
|
|
240
|
-
key,
|
|
241
|
-
access: key,
|
|
242
|
-
refresh: credential.refreshToken,
|
|
243
|
-
expires: credential.expiresAt,
|
|
244
|
-
metadata: {
|
|
245
|
-
source: "kiro-device-auth",
|
|
246
|
-
region: credential.region,
|
|
247
|
-
oidcRegion: credential.oidcRegion,
|
|
248
|
-
...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
},
|
|
256
|
+
authorize: async (inputs) => authorizeDeviceLogin(inputs),
|
|
254
257
|
},
|
|
255
258
|
{
|
|
256
259
|
type: "api",
|
|
@@ -272,7 +275,6 @@ export function createKiroPlugin() {
|
|
|
272
275
|
},
|
|
273
276
|
],
|
|
274
277
|
loader: async (auth) => {
|
|
275
|
-
await discoverIfStale();
|
|
276
278
|
const apiKey = await resolveApiKey(auth);
|
|
277
279
|
const server = await ensureLocalServer();
|
|
278
280
|
return {
|
|
@@ -286,7 +288,6 @@ export function createKiroPlugin() {
|
|
|
286
288
|
description: "Show Kiro plugin backend, auth, region, and discovered model status.",
|
|
287
289
|
args: {},
|
|
288
290
|
execute: async () => {
|
|
289
|
-
await discoverIfStale();
|
|
290
291
|
const auth = await detectAuth();
|
|
291
292
|
return {
|
|
292
293
|
title: "Kiro status",
|
|
@@ -311,11 +312,29 @@ export function createKiroPlugin() {
|
|
|
311
312
|
};
|
|
312
313
|
},
|
|
313
314
|
}),
|
|
315
|
+
kiro_refresh_models: tool({
|
|
316
|
+
description: "Refresh the cached Kiro model list on demand.",
|
|
317
|
+
args: {},
|
|
318
|
+
execute: async () => {
|
|
319
|
+
const models = await refreshModels(true);
|
|
320
|
+
const cached = modelCache.ids();
|
|
321
|
+
return {
|
|
322
|
+
title: models.length > 0 ? "Kiro models refreshed" : "Kiro model refresh skipped",
|
|
323
|
+
output: models.length > 0
|
|
324
|
+
? `refreshed: ${models.length}\ncached: ${cached.length}`
|
|
325
|
+
: `No models were discovered. Keeping cached models: ${cached.length}`,
|
|
326
|
+
metadata: {
|
|
327
|
+
refreshed: models.length > 0,
|
|
328
|
+
discovered: models.length,
|
|
329
|
+
cached: cached.length,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
314
334
|
},
|
|
315
335
|
provider: {
|
|
316
336
|
id: options.providerID,
|
|
317
337
|
models: async (provider) => {
|
|
318
|
-
await discoverIfStale();
|
|
319
338
|
const existing = modelRecord(provider.models);
|
|
320
339
|
const visibleModels = visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels);
|
|
321
340
|
const server = await ensureLocalServer();
|