@bogyie/opencode-kiro-plugin 0.3.21 → 0.3.23

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
- During OpenCode startup, the plugin checks Kiro auth in the blocking `config` hook. If no `KIRO_API_KEY` or active `kiro-cli` session exists, it starts the configured `kiro-cli login --use-device-flow` flow once and waits for login to finish before model discovery and provider registration continue. It then runs best-effort model discovery with `kiro-cli chat --list-models --format json` so the model picker can track the current Kiro CLI list. If startup discovery succeeds, the result is stored in a local cache and used immediately. If startup discovery fails, the plugin keeps the last stored model list. If no stored list exists yet, it injects an `auto` placeholder model so Kiro still appears in OpenCode's provider connector.
41
+ During OpenCode startup, the plugin checks Kiro auth in the blocking `config` hook. If no `KIRO_API_KEY` or active `kiro-cli` session exists, it starts the configured login flow once and waits for completion before provider registration continues. Personal login methods use `kiro-cli login --use-device-flow`. IAM Identity Center login uses direct OIDC device authorization for the configured Start URL and region, so it opens the Identity Center device URL and avoids localhost callback failures. After auth succeeds, startup runs best-effort model discovery with `kiro-cli chat --list-models --format json` when the Kiro CLI session is available. If startup discovery fails, the plugin keeps the last stored model list. If no stored list exists yet, it injects an `auto` placeholder model so Kiro still appears in OpenCode's provider connector.
42
42
 
43
- You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. The connector runs `kiro-cli login --use-device-flow`, waits for the local Kiro CLI session to become authenticated, then stores a local transport marker in OpenCode auth. Configure `login.method` as `builder-id`, `google`, `github`, or `organization` to preselect the Kiro CLI login method. The plugin maps `builder-id`, `google`, and `github` to `--license free`; it maps `organization` to `--license pro`. If `login.identityProvider`, `login.region`, `login.license`, or `login.extraArgs` are configured, those options are also passed to `kiro-cli login` along with `--use-device-flow`. After login succeeds, the plugin also tries to refresh the runtime model cache. If no OpenCode connector marker or API key is configured, direct fetch still 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 same configured Kiro CLI 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`. Personal login methods use `kiro-cli login --use-device-flow`, wait for the local Kiro CLI session to become authenticated, then store a local transport marker in OpenCode auth. IAM Identity Center login uses the OIDC device authorization flow directly, waits for token completion, and stores a `kiro-device:` credential in OpenCode auth. Configure `login.method` as `builder-id`, `google`, `github`, or `organization` to preselect the login method. The plugin maps `builder-id`, `google`, and `github` to `--license free`; it maps `organization` to the IAM Identity Center device flow. If no OpenCode connector credential or API key is configured, direct fetch still reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If an API/model call using a CLI-backed local transport fails with an auth error, the selected transport starts the configured Kiro CLI login flow and retries the request once. Requests using stored `kiro-device:` credentials refresh tokens directly and do not fall back to opening Kiro CLI login pages. `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 personal Kiro login with GitHub:
46
46
 
@@ -59,7 +59,7 @@ For personal Kiro login with GitHub:
59
59
  }
60
60
  ```
61
61
 
62
- For AWS IAM Identity Center login, configure the default device-flow Start URL separately from the API region:
62
+ For AWS IAM Identity Center login, configure the default device-flow Start URL separately from the Kiro LLM API region:
63
63
 
64
64
  ```jsonc
65
65
  {
@@ -67,7 +67,7 @@ For AWS IAM Identity Center login, configure the default device-flow Start URL s
67
67
  [
68
68
  "@bogyie/opencode-kiro-plugin",
69
69
  {
70
- "region": "ap-northeast-2",
70
+ "region": "us-east-1",
71
71
  "login": {
72
72
  "method": "organization",
73
73
  "identityProvider": "https://example.awsapps.com/start",
@@ -79,7 +79,7 @@ For AWS IAM Identity Center login, configure the default device-flow Start URL s
79
79
  }
80
80
  ```
81
81
 
82
- For IAM Identity Center, configure `login.method: "organization"`, `login.identityProvider`, and `login.region` in plugin options so they are passed to `kiro-cli login --use-device-flow --license pro --identity-provider <start-url> --region <region>`.
82
+ For IAM Identity Center, configure `login.method: "organization"`, `login.identityProvider`, and `login.region` in plugin options. The top-level `region` controls the Kiro LLM/API endpoint and should usually remain `us-east-1`; `login.region` controls the IAM Identity Center OIDC/device authorization endpoint, for example `ap-northeast-2`. The connector opens the Identity Center device URL for that Start URL and login region, then waits until the device authorization completes.
83
83
 
84
84
  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 and stored model cache. Secrets are redacted in diagnostics.
85
85
 
@@ -122,7 +122,7 @@ Supported values:
122
122
 
123
123
  ## Model Churn Handling
124
124
 
125
- The resolver intentionally avoids a hard whitelist. At startup, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json` and stores successful results in a local cache. Failed refreshes do not clear the last successful cache. If there is no successful cache yet, startup falls back to the `auto` placeholder so the provider can still be connected before discovery succeeds.
125
+ The resolver intentionally avoids a hard whitelist. At startup, after auth succeeds, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json` when `kiro-cli whoami` succeeds, then stores successful results in a local cache. Direct IAM Identity Center device auth is available immediately for chat requests, but it does not write a Kiro CLI session, so CLI-based model discovery may keep using the previous cache until the Kiro CLI is also logged in. Failed or skipped refreshes do not clear the last successful cache. If there is no successful cache yet, startup falls back to the `auto` placeholder so the provider can still be connected before discovery succeeds.
126
126
 
127
127
  Useful options:
128
128
 
@@ -172,7 +172,7 @@ This keeps new Kiro model ids usable before the package is updated without adver
172
172
 
173
173
  The plugin injects `provider.kiro` with the `auto` placeholder 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.
174
174
 
175
- `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to disable startup discovery and `kiro_refresh_models`; in that mode the plugin uses any stored cache and then the `auto` fallback. During OpenCode startup, the plugin checks Kiro auth in the blocking `config` hook. If no `KIRO_API_KEY` or active `kiro-cli` session exists, it starts the configured `kiro-cli login --use-device-flow` flow once and waits for login to finish before model discovery and provider registration continue. `/v1/models` still uses only the discovery/cache path: it never invokes the chat transport or login fallback, and returns the cached list or `auto` if discovery fails. OpenCode title-generation requests also suppress login fallback so startup helper traffic cannot open a second Kiro login window. A successful connector login also triggers a forced model refresh. 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.
175
+ `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to disable startup discovery and `kiro_refresh_models`; in that mode the plugin uses any stored cache and then the `auto` fallback. `/v1/models` uses only the discovery/cache path: it never invokes the chat transport or login fallback, and returns the cached list or `auto` if discovery fails. For the default Kiro CLI discovery command, startup and forced refresh first check `kiro-cli whoami`; if the CLI is not authenticated, discovery is skipped instead of prompting for another login. OpenCode title-generation requests also suppress login fallback so startup helper traffic cannot open a second Kiro login window. A successful connector login also triggers a forced model refresh only when that refresh can run without prompting. 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.
176
176
 
177
177
  ## Troubleshooting
178
178
 
@@ -183,6 +183,8 @@ The plugin injects `provider.kiro` with the `auto` placeholder needed for OpenCo
183
183
  - `KIRO_ACP_TIMEOUT`: ACP did not send a `TurnEnd` notification before the prompt timeout.
184
184
  - `KIRO_ACP_PROCESS_ERROR` or `KIRO_ACP_PROCESS_EXITED`: `kiro-cli acp` could not start or exited while a request was pending.
185
185
 
186
+ If IAM Identity Center login succeeds but chat requests cannot connect or fail against the LLM endpoint, check that top-level `region` is the Kiro LLM/API region, not necessarily the IAM Identity Center region. A common organization setup is `"region": "us-east-1"` with `"login.region": "ap-northeast-2"`.
187
+
186
188
  Direct fetch mode calls Kiro's `generateAssistantResponse` endpoint and can fall back from the `q` endpoint to the CodeWhisperer endpoint on quota or upstream failures. Tune `maxAttempts` and `requestTimeoutMs` if you need stricter failure boundaries in automation. Fetch mode also accepts `endpoint`, `profileArn`, `userAgent`, and `agentMode` for controlled environments. `cli-chat` uses `requestTimeoutMs` for the `kiro-cli chat --no-interactive` child process, and ACP uses it while waiting for `session/prompt` completion and `TurnEnd`.
187
189
 
188
190
  OpenAI-compatible `temperature`, `max_tokens`, and `max_completion_tokens` are preserved for direct fetch mode through Kiro's `inferenceConfig` fields on a best-effort basis.
package/dist/plugin.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { KiroPluginOptions } from "./config.js";
4
4
  import type { KiroRestTransportOptions } from "./kiro-rest-transport.js";
5
5
  type EffectiveBackend = "fetch" | "cli-chat" | "acp" | "none";
6
6
  export declare function effectiveBackend(options: Pick<KiroPluginOptions, "backend">, accessToken?: string): EffectiveBackend;
7
+ export declare function __resetKiroPluginSharedStateForTest(): void;
7
8
  export declare function acpTransportOptions(options: Pick<KiroPluginOptions, "requestTimeoutMs" | "trustAllTools">): KiroAcpTransportOptions;
8
9
  export declare function fetchTransportOptions(options: Pick<KiroPluginOptions, "region" | "endpoint" | "profileArn" | "userAgent" | "agentMode" | "maxAttempts" | "requestTimeoutMs">, accessToken?: string): KiroRestTransportOptions;
9
10
  export declare function createKiroPlugin(): Plugin;
package/dist/plugin.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { spawn } from "node:child_process";
2
3
  import { KiroAcpTransport } from "./acp-transport.js";
3
4
  import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, KIRO_LOCAL_TRANSPORT_KEY, pollKiroDeviceToken, readKiroCliSessionCredential, resolveApiKey, startKiroCliLoginOnce, runKiroLoginFlowOnce, } from "./auth.js";
4
5
  import { KiroCliChatTransport } from "./cli-transport.js";
@@ -15,6 +16,7 @@ const PLACEHOLDER_MODEL = { name: "Auto" };
15
16
  const OPENCODE_AGENT_HEADER = "x-opencode-kiro-agent";
16
17
  const LOGIN_SUPPRESSED_AGENTS = new Set(["title"]);
17
18
  const LOGIN_METHODS = new Set(["builder-id", "google", "github", "organization"]);
19
+ let sharedDeviceAuthKey;
18
20
  function discoveredProviderModels(cache) {
19
21
  return Object.fromEntries(cache.all().map((model) => [
20
22
  model.id,
@@ -99,6 +101,26 @@ function loginOptions(base, inputs) {
99
101
  function isOrganizationLogin(login) {
100
102
  return login.method === "organization" || login.license === "pro" || Boolean(login.identityProvider || login.region);
101
103
  }
104
+ function isKiroCliModelDiscoveryCommand(command) {
105
+ const executable = command[0]?.split(/[\\/]/).pop();
106
+ return executable === "kiro-cli" && command.includes("chat") && command.includes("--list-models");
107
+ }
108
+ function openExternalUrl(url) {
109
+ if (process.env.OPENCODE_KIRO_BROWSER_OPEN === "0")
110
+ return;
111
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
112
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
113
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
114
+ child.on("error", () => undefined);
115
+ child.unref();
116
+ }
117
+ function rememberDeviceAuthKey(key) {
118
+ sharedDeviceAuthKey = key;
119
+ return key;
120
+ }
121
+ export function __resetKiroPluginSharedStateForTest() {
122
+ sharedDeviceAuthKey = undefined;
123
+ }
102
124
  function loginPrompts(options) {
103
125
  const prompts = [];
104
126
  const login = options.login;
@@ -145,6 +167,8 @@ function localTransport(options, accessToken, behavior = {}) {
145
167
  const login = () => {
146
168
  if (behavior.loginOnAuthFailure === false)
147
169
  return Promise.resolve(false);
170
+ if (isKiroDeviceAuthKey(accessToken))
171
+ return Promise.resolve(false);
148
172
  return runKiroLoginFlowOnce({
149
173
  login: loginOptions(options.login),
150
174
  });
@@ -207,6 +231,7 @@ export function createKiroPlugin() {
207
231
  }
208
232
  let discovery;
209
233
  let lastModelDiscoveryAt = 0;
234
+ let startupDeviceAuthKey = sharedDeviceAuthKey;
210
235
  let startupLogin;
211
236
  let startupLoginAttempted = false;
212
237
  const refreshModels = async (force = false) => {
@@ -215,7 +240,14 @@ export function createKiroPlugin() {
215
240
  return [];
216
241
  }
217
242
  if (!discovery) {
218
- discovery = discoverModelsFromCommand(options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
243
+ discovery = (async () => {
244
+ if (isKiroCliModelDiscoveryCommand(options.modelDiscoveryCommand)) {
245
+ const auth = await detectAuth().catch(() => undefined);
246
+ if (!auth?.authenticated)
247
+ return [];
248
+ }
249
+ return discoverModelsFromCommand(options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1));
250
+ })()
219
251
  .then((models) => {
220
252
  if (models.length > 0) {
221
253
  lastModelDiscoveryAt = Date.now();
@@ -232,6 +264,9 @@ export function createKiroPlugin() {
232
264
  return discovery;
233
265
  };
234
266
  const ensureStartupAuthenticated = async () => {
267
+ startupDeviceAuthKey ??= sharedDeviceAuthKey;
268
+ if (startupDeviceAuthKey)
269
+ return true;
235
270
  const current = await detectAuth().catch(() => undefined);
236
271
  if (current?.authenticated)
237
272
  return true;
@@ -241,7 +276,23 @@ export function createKiroPlugin() {
241
276
  return false;
242
277
  startupLoginAttempted = true;
243
278
  startupLogin = (async () => {
244
- const session = startKiroCliLoginOnce(loginOptions(options.login));
279
+ const login = loginOptions(options.login);
280
+ if (isOrganizationLogin(login) && login.identityProvider) {
281
+ try {
282
+ const authorization = await authorizeKiroDevice(login);
283
+ openExternalUrl(kiroDeviceVerificationUrl(login.identityProvider, authorization.userCode));
284
+ const credential = await pollKiroDeviceToken(authorization, {
285
+ region: options.region,
286
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
287
+ });
288
+ startupDeviceAuthKey = rememberDeviceAuthKey(encodeKiroDeviceAuthKey(credential));
289
+ return true;
290
+ }
291
+ catch {
292
+ return false;
293
+ }
294
+ }
295
+ const session = startKiroCliLoginOnce(login);
245
296
  const prompted = await session.waitForPrompt(options.requestTimeoutMs);
246
297
  if (!prompted)
247
298
  return false;
@@ -258,22 +309,23 @@ export function createKiroPlugin() {
258
309
  disabledModels: options.disabledModels,
259
310
  disablePassThrough: options.disableModelPassThrough,
260
311
  });
312
+ const localFetch = async (input, init) => {
313
+ startupDeviceAuthKey ??= sharedDeviceAuthKey;
314
+ const transport = localTransport(options, bearerToken(init) || startupDeviceAuthKey || sharedDeviceAuthKey, {
315
+ loginOnAuthFailure: shouldLoginOnAuthFailure(init),
316
+ });
317
+ return createKiroFetch({
318
+ resolver,
319
+ models: async () => {
320
+ await refreshModels(true).catch(() => []);
321
+ return Object.keys(visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels));
322
+ },
323
+ ...(transport ? { transport } : {}),
324
+ })(input, init);
325
+ };
261
326
  const ensureLocalServer = async () => {
262
327
  if (localServer)
263
328
  return localServer;
264
- const localFetch = async (input, init) => {
265
- const transport = localTransport(options, bearerToken(init), {
266
- loginOnAuthFailure: shouldLoginOnAuthFailure(init),
267
- });
268
- return createKiroFetch({
269
- resolver,
270
- models: async () => {
271
- await refreshModels(true).catch(() => []);
272
- return Object.keys(visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels));
273
- },
274
- ...(transport ? { transport } : {}),
275
- })(input, init);
276
- };
277
329
  localServer = await startLocalKiroServer(localFetch);
278
330
  return localServer;
279
331
  };
@@ -292,10 +344,12 @@ export function createKiroPlugin() {
292
344
  region: options.region,
293
345
  ...(options.profileArn ? { profileArn: options.profileArn } : {}),
294
346
  });
347
+ const key = rememberDeviceAuthKey(encodeKiroDeviceAuthKey(credential));
348
+ startupDeviceAuthKey = key;
295
349
  await refreshModels(true).catch(() => []);
296
350
  return {
297
351
  type: "success",
298
- key: encodeKiroDeviceAuthKey(credential),
352
+ key,
299
353
  metadata: {
300
354
  source: "kiro-device-auth",
301
355
  },
@@ -350,6 +404,8 @@ export function createKiroPlugin() {
350
404
  provider.npm = "@ai-sdk/openai-compatible";
351
405
  provider.api = server.baseURL;
352
406
  provider.options ??= {};
407
+ provider.options.baseURL = server.baseURL;
408
+ provider.options.apiKey ??= KIRO_LOCAL_TRANSPORT_KEY;
353
409
  provider.models = visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels);
354
410
  },
355
411
  auth: {
@@ -382,10 +438,12 @@ export function createKiroPlugin() {
382
438
  ],
383
439
  loader: async (auth) => {
384
440
  const apiKey = await resolveApiKey(auth);
441
+ startupDeviceAuthKey ??= sharedDeviceAuthKey;
385
442
  const server = await ensureLocalServer();
386
443
  return {
387
- apiKey: apiKey || KIRO_LOCAL_TRANSPORT_KEY,
444
+ apiKey: apiKey || startupDeviceAuthKey || sharedDeviceAuthKey || KIRO_LOCAL_TRANSPORT_KEY,
388
445
  baseURL: server.baseURL,
446
+ fetch: localFetch,
389
447
  };
390
448
  },
391
449
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",