@bogyie/opencode-kiro-plugin 0.3.8 → 0.3.10

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 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
- OpenCode startup does not start Kiro login. The provider is registered immediately with an `auto` placeholder model, then model discovery warms the in-memory cache in the background. If discovery succeeds, later model-list requests use the latest cached list; if discovery fails, the previous cache remains in use and Kiro stays visible with `auto`.
41
+ OpenCode startup does not start Kiro login or model discovery. The plugin injects a minimal `provider.kiro` shell so Kiro appears in OpenCode's provider connector, but it does not add a default model. 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. If an API/model call fails with an auth error, the selected transport starts the Kiro login flow 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.
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
- The connector can also prompt for the Start URL, OIDC region, and optional profile ARN. 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.
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. Startup always begins with the `auto` placeholder so OpenCode can show Kiro without waiting for Kiro auth or model discovery. By default, the plugin then reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json` in the background. Runtime discovery is the source of truth for the model picker once it has succeeded; failed refreshes do not clear the last successful cache.
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
 
@@ -153,9 +153,9 @@ Resolution order:
153
153
 
154
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 injects `provider.kiro` automatically. You only need to add `provider.kiro.models` yourself when overriding OpenCode 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.
156
+ The plugin injects only the minimal `provider.kiro` shell needed for OpenCode's provider connector. Define `provider.kiro.models` yourself only 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 skip runtime discovery. Discovery is best-effort and never starts login 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.
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
- if (Object.keys(models).length > 0 || disabledModels.has(PLACEHOLDER_MODEL_ID))
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: async () => false,
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), {
@@ -125,24 +121,24 @@ export function createKiroPlugin() {
125
121
  }
126
122
  let discovery;
127
123
  let lastModelDiscoveryAt = 0;
128
- const refreshModelsIfStale = () => {
124
+ const refreshModels = async (force = false) => {
129
125
  const discoveryIsStale = lastModelDiscoveryAt === 0 || Date.now() - lastModelDiscoveryAt > options.modelCacheTtlSeconds * 1000;
130
- if (options.modelDiscovery === "off" || options.modelDiscoveryCommand.length === 0 || !discoveryIsStale || discovery) {
131
- return discovery;
126
+ if (options.modelDiscovery === "off" || options.modelDiscoveryCommand.length === 0 || (!force && !discoveryIsStale)) {
127
+ return [];
132
128
  }
133
- discovery = refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
134
- .then((models) => {
135
- if (models.length > 0)
136
- lastModelDiscoveryAt = Date.now();
137
- })
138
- .catch(() => undefined)
139
- .then(() => {
140
- discovery = undefined;
141
- });
142
- return discovery;
143
- };
144
- const refreshModelsInBackground = () => {
145
- void refreshModelsIfStale();
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);
146
142
  };
147
143
  const resolver = new ModelResolver({
148
144
  cache: modelCache,
@@ -164,13 +160,47 @@ export function createKiroPlugin() {
164
160
  localServer = await startLocalKiroServer(localFetch);
165
161
  return localServer;
166
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
+ };
167
198
  return {
168
199
  dispose: async () => {
169
200
  await localServer?.close();
170
201
  localServer = undefined;
171
202
  },
172
203
  config: async (config) => {
173
- refreshModelsInBackground();
174
204
  config.provider ??= {};
175
205
  config.provider[options.providerID] ??= {};
176
206
  const provider = config.provider[options.providerID];
@@ -191,7 +221,12 @@ export function createKiroPlugin() {
191
221
  methods: [
192
222
  {
193
223
  type: "oauth",
194
- 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)",
195
230
  prompts: [
196
231
  {
197
232
  type: "text",
@@ -218,44 +253,7 @@ export function createKiroPlugin() {
218
253
  placeholder: "arn:aws:codewhisperer:us-east-1:123456789012:profile/XXXXXXXXXX",
219
254
  },
220
255
  ],
221
- authorize: async (inputs) => {
222
- const promptInputs = inputs;
223
- const startUrl = inputString(promptInputs, "startUrl") ?? options.login.identityProvider;
224
- const idcRegion = inputString(promptInputs, "idcRegion") ?? options.login.region ?? "us-east-1";
225
- const profileArn = inputString(promptInputs, "profileArn") ?? options.profileArn;
226
- const authorization = await authorizeKiroDevice({
227
- region: idcRegion,
228
- ...(startUrl ? { identityProvider: startUrl } : {}),
229
- });
230
- const url = startUrl
231
- ? kiroDeviceVerificationUrl(authorization.startUrl, authorization.userCode)
232
- : authorization.verificationUrlComplete;
233
- return {
234
- url,
235
- instructions: `Open the verification URL and complete Kiro sign-in.\nCode: ${authorization.userCode}`,
236
- method: "auto",
237
- callback: async () => {
238
- const credential = await pollKiroDeviceToken(authorization, {
239
- region: options.region,
240
- ...(profileArn ? { profileArn } : {}),
241
- });
242
- const key = encodeKiroDeviceAuthKey(credential);
243
- return {
244
- type: "success",
245
- key,
246
- access: key,
247
- refresh: credential.refreshToken,
248
- expires: credential.expiresAt,
249
- metadata: {
250
- source: "kiro-device-auth",
251
- region: credential.region,
252
- oidcRegion: credential.oidcRegion,
253
- ...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
254
- },
255
- };
256
- },
257
- };
258
- },
256
+ authorize: async (inputs) => authorizeDeviceLogin(inputs),
259
257
  },
260
258
  {
261
259
  type: "api",
@@ -290,7 +288,6 @@ export function createKiroPlugin() {
290
288
  description: "Show Kiro plugin backend, auth, region, and discovered model status.",
291
289
  args: {},
292
290
  execute: async () => {
293
- refreshModelsInBackground();
294
291
  const auth = await detectAuth();
295
292
  return {
296
293
  title: "Kiro status",
@@ -315,11 +312,29 @@ export function createKiroPlugin() {
315
312
  };
316
313
  },
317
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
+ }),
318
334
  },
319
335
  provider: {
320
336
  id: options.providerID,
321
337
  models: async (provider) => {
322
- refreshModelsInBackground();
323
338
  const existing = modelRecord(provider.models);
324
339
  const visibleModels = visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels);
325
340
  const server = await ensureLocalServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",