@bogyie/opencode-kiro-plugin 0.3.20 → 0.3.22

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
 
@@ -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 --license pro --use-device-flow`.
82
+ For IAM Identity Center, configure `login.method: "organization"`, `login.identityProvider`, and `login.region` in plugin options. The connector opens the Identity Center device URL for that Start URL and 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
 
@@ -204,6 +204,21 @@ For a real Kiro smoke test that uses your local `kiro-cli` login, runs runtime m
204
204
  npm run smoke:kiro
205
205
  ```
206
206
 
207
+ For the same real checks under the test runner, including actual `kiro-cli chat --list-models --format json` and real local plugin API calls:
208
+
209
+ ```sh
210
+ npm run test:real
211
+ ```
212
+
213
+ To also run the real IAM Identity Center device login flow and wait for completion, set the explicit opt-in variables:
214
+
215
+ ```sh
216
+ OPENCODE_KIRO_REAL_LOGIN=1 \
217
+ KIRO_REAL_IDENTITY_PROVIDER=https://example.awsapps.com/start \
218
+ KIRO_REAL_IDENTITY_REGION=ap-northeast-2 \
219
+ npm run test:real
220
+ ```
221
+
207
222
  For real Kiro/OpenCode validation, use [docs/e2e-validation.md](docs/e2e-validation.md).
208
223
 
209
224
  ## Release
package/dist/auth.d.ts CHANGED
@@ -35,6 +35,9 @@ export interface KiroLoginSession {
35
35
  readonly url: string;
36
36
  readonly code: string | undefined;
37
37
  readonly instructions: string;
38
+ readonly output: string;
39
+ readonly exitCode: number | null;
40
+ readonly args: ReadonlyArray<string>;
38
41
  waitForPrompt(timeoutMs?: number): Promise<boolean>;
39
42
  waitForAuth(runner?: CommandRunner): Promise<boolean>;
40
43
  }
package/dist/auth.js CHANGED
@@ -425,15 +425,20 @@ function loginOptionsAndSpawner(optionsOrSpawner, spawner) {
425
425
  }
426
426
  export function kiroCliLoginArgs(options = {}) {
427
427
  const args = ["login"];
428
- const license = options.license ?? (options.method === "organization" ? "pro" : options.method ? "free" : undefined);
428
+ const license = options.license ??
429
+ (options.method === "organization" || options.identityProvider || options.region
430
+ ? "pro"
431
+ : options.method
432
+ ? "free"
433
+ : undefined);
434
+ if (options.useDeviceFlow)
435
+ args.push("--use-device-flow");
429
436
  if (license)
430
437
  args.push("--license", license);
431
438
  if (options.identityProvider)
432
439
  args.push("--identity-provider", options.identityProvider);
433
440
  if (options.region)
434
441
  args.push("--region", options.region);
435
- if (options.useDeviceFlow)
436
- args.push("--use-device-flow");
437
442
  args.push(...(options.extraArgs ?? []));
438
443
  return args;
439
444
  }
@@ -459,13 +464,28 @@ function loginMethodPromptSeen(output) {
459
464
  function loginPromptReady(output) {
460
465
  return Boolean(extractKiroLoginCode(output) || firstLoginUrl(output));
461
466
  }
467
+ function identityPromptSeen(output) {
468
+ return /enter start url/i.test(output);
469
+ }
470
+ function regionPromptSeen(output) {
471
+ return /enter region/i.test(output);
472
+ }
473
+ function loginUrl(output, options) {
474
+ const code = extractKiroLoginCode(output);
475
+ if (code && options.identityProvider)
476
+ return kiroDeviceVerificationUrl(options.identityProvider, code);
477
+ return extractKiroLoginUrl(output);
478
+ }
462
479
  export function startKiroCliLogin(optionsOrSpawner, spawner) {
463
480
  const resolved = loginOptionsAndSpawner(optionsOrSpawner, spawner);
464
- const child = resolved.spawner("kiro-cli", kiroCliLoginArgs(resolved.options));
481
+ const args = kiroCliLoginArgs(resolved.options);
482
+ const child = resolved.spawner("kiro-cli", args);
465
483
  let output = "";
466
484
  let exited = false;
467
485
  let exitCode = null;
468
486
  let selectedLoginMethod = false;
487
+ let wroteIdentityProvider = false;
488
+ let wroteRegion = false;
469
489
  const maybeSelectLoginMethod = () => {
470
490
  if (!resolved.options.method ||
471
491
  resolved.options.method === "organization" ||
@@ -476,13 +496,25 @@ export function startKiroCliLogin(optionsOrSpawner, spawner) {
476
496
  child.stdin?.write(loginMethodSelection(resolved.options.method));
477
497
  selectedLoginMethod = true;
478
498
  };
499
+ const maybeAnswerIdentityPrompts = () => {
500
+ if (resolved.options.identityProvider && !wroteIdentityProvider && identityPromptSeen(output)) {
501
+ child.stdin?.write(`${resolved.options.identityProvider}\n`);
502
+ wroteIdentityProvider = true;
503
+ }
504
+ if (resolved.options.region && !wroteRegion && regionPromptSeen(output)) {
505
+ child.stdin?.write(`${resolved.options.region}\n`);
506
+ wroteRegion = true;
507
+ }
508
+ };
479
509
  child.stdout?.on("data", (chunk) => {
480
510
  output += chunk.toString("utf8");
481
511
  maybeSelectLoginMethod();
512
+ maybeAnswerIdentityPrompts();
482
513
  });
483
514
  child.stderr?.on("data", (chunk) => {
484
515
  output += chunk.toString("utf8");
485
516
  maybeSelectLoginMethod();
517
+ maybeAnswerIdentityPrompts();
486
518
  });
487
519
  child.on("exit", (code) => {
488
520
  exited = true;
@@ -490,15 +522,27 @@ export function startKiroCliLogin(optionsOrSpawner, spawner) {
490
522
  });
491
523
  return {
492
524
  get url() {
493
- return extractKiroLoginUrl(output);
525
+ return loginUrl(output, resolved.options);
494
526
  },
495
527
  get code() {
496
528
  return extractKiroLoginCode(output);
497
529
  },
498
530
  get instructions() {
499
531
  const code = extractKiroLoginCode(output);
532
+ if (code && resolved.options.identityProvider) {
533
+ return `Open ${kiroDeviceVerificationUrl(resolved.options.identityProvider, code)} and confirm code: ${code}`;
534
+ }
500
535
  return code ? `Enter code: ${code}` : "Complete Kiro CLI login in your browser. This window will close automatically.";
501
536
  },
537
+ get output() {
538
+ return output;
539
+ },
540
+ get exitCode() {
541
+ return exitCode;
542
+ },
543
+ get args() {
544
+ return args;
545
+ },
502
546
  async waitForPrompt(timeoutMs = 15_000) {
503
547
  const deadline = Date.now() + timeoutMs;
504
548
  while (Date.now() < deadline) {
@@ -542,6 +586,15 @@ export function startKiroCliLoginOnce(optionsOrSpawner, spawner) {
542
586
  get instructions() {
543
587
  return session.instructions;
544
588
  },
589
+ get output() {
590
+ return session.output;
591
+ },
592
+ get exitCode() {
593
+ return session.exitCode;
594
+ },
595
+ get args() {
596
+ return session.args;
597
+ },
545
598
  waitForPrompt(timeoutMs) {
546
599
  return session.waitForPrompt(timeoutMs);
547
600
  },
@@ -61,18 +61,33 @@ function fromLines(raw) {
61
61
  .map(fromItem)
62
62
  .filter((item) => item !== undefined);
63
63
  }
64
+ function jsonCandidates(raw) {
65
+ const trimmed = raw.trim();
66
+ const candidates = [trimmed];
67
+ const objectStart = trimmed.indexOf("{");
68
+ const objectEnd = trimmed.lastIndexOf("}");
69
+ if (objectStart >= 0 && objectEnd > objectStart)
70
+ candidates.push(trimmed.slice(objectStart, objectEnd + 1));
71
+ const arrayStart = trimmed.indexOf("[");
72
+ const arrayEnd = trimmed.lastIndexOf("]");
73
+ if (arrayStart >= 0 && arrayEnd > arrayStart)
74
+ candidates.push(trimmed.slice(arrayStart, arrayEnd + 1));
75
+ return [...new Set(candidates)];
76
+ }
64
77
  export function parseDiscoveredModels(raw) {
65
78
  const trimmed = raw.trim();
66
79
  if (!trimmed)
67
80
  return [];
68
- try {
69
- const parsed = JSON.parse(trimmed);
70
- const models = fromJson(parsed);
71
- if (models.length > 0)
72
- return dedupe(models);
73
- }
74
- catch {
75
- // Fall back to line parsing below.
81
+ for (const candidate of jsonCandidates(trimmed)) {
82
+ try {
83
+ const parsed = JSON.parse(candidate);
84
+ const models = fromJson(parsed);
85
+ if (models.length > 0)
86
+ return dedupe(models);
87
+ }
88
+ catch {
89
+ // Try the next JSON candidate, then fall back to line parsing below.
90
+ }
76
91
  }
77
92
  return dedupe(fromLines(trimmed));
78
93
  }
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { spawn } from "node:child_process";
2
3
  import { KiroAcpTransport } from "./acp-transport.js";
3
- import { credentialFromKiroDeviceAuthKey, detectAuth, isKiroDeviceAuthKey, KIRO_LOCAL_TRANSPORT_KEY, readKiroCliSessionCredential, resolveApiKey, startKiroCliLoginOnce, runKiroLoginFlowOnce, } from "./auth.js";
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";
5
6
  import { loadOptions } from "./config.js";
6
7
  import { createKiroFetch } from "./fetch-adapter.js";
@@ -14,6 +15,7 @@ const PLACEHOLDER_MODEL_ID = "auto";
14
15
  const PLACEHOLDER_MODEL = { name: "Auto" };
15
16
  const OPENCODE_AGENT_HEADER = "x-opencode-kiro-agent";
16
17
  const LOGIN_SUPPRESSED_AGENTS = new Set(["title"]);
18
+ const LOGIN_METHODS = new Set(["builder-id", "google", "github", "organization"]);
17
19
  function discoveredProviderModels(cache) {
18
20
  return Object.fromEntries(cache.all().map((model) => [
19
21
  model.id,
@@ -78,16 +80,89 @@ function shouldLoginOnAuthFailure(init) {
78
80
  const agent = new Headers(init?.headers).get(OPENCODE_AGENT_HEADER);
79
81
  return !agent || !LOGIN_SUPPRESSED_AGENTS.has(agent);
80
82
  }
83
+ function inputString(inputs, key) {
84
+ const value = inputs?.[key]?.trim();
85
+ return value || undefined;
86
+ }
87
+ function loginOptions(base, inputs) {
88
+ const inputMethod = inputString(inputs, "method");
89
+ const method = inputMethod && LOGIN_METHODS.has(inputMethod) ? inputMethod : undefined;
90
+ const identityProvider = inputString(inputs, "identityProvider");
91
+ const region = inputString(inputs, "region");
92
+ return {
93
+ ...base,
94
+ ...(method ? { method } : {}),
95
+ ...(identityProvider ? { identityProvider } : {}),
96
+ ...(region ? { region } : {}),
97
+ useDeviceFlow: true,
98
+ };
99
+ }
100
+ function isOrganizationLogin(login) {
101
+ return login.method === "organization" || login.license === "pro" || Boolean(login.identityProvider || login.region);
102
+ }
103
+ function isKiroCliModelDiscoveryCommand(command) {
104
+ const executable = command[0]?.split(/[\\/]/).pop();
105
+ return executable === "kiro-cli" && command.includes("chat") && command.includes("--list-models");
106
+ }
107
+ function openExternalUrl(url) {
108
+ if (process.env.OPENCODE_KIRO_BROWSER_OPEN === "0")
109
+ return;
110
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
111
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
112
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
113
+ child.on("error", () => undefined);
114
+ child.unref();
115
+ }
116
+ function loginPrompts(options) {
117
+ const prompts = [];
118
+ const login = options.login;
119
+ const hasLoginRoute = Boolean(login.method || login.license || login.identityProvider || login.region);
120
+ if (!hasLoginRoute) {
121
+ prompts.push({
122
+ type: "select",
123
+ key: "method",
124
+ message: "Select Kiro login method",
125
+ options: [
126
+ { label: "Use with Builder ID", value: "builder-id" },
127
+ { label: "Use with Google", value: "google" },
128
+ { label: "Use with GitHub", value: "github" },
129
+ { label: "Use with Your Organization", value: "organization" },
130
+ ],
131
+ });
132
+ }
133
+ const organizationIsConfigured = login.method === "organization" || login.license === "pro" || Boolean(login.identityProvider || login.region);
134
+ const organizationWhen = hasLoginRoute ? undefined : { key: "method", op: "eq", value: "organization" };
135
+ if (organizationIsConfigured || !hasLoginRoute) {
136
+ if (!login.identityProvider) {
137
+ prompts.push({
138
+ type: "text",
139
+ key: "identityProvider",
140
+ message: "IAM Identity Center Start URL",
141
+ placeholder: "https://example.awsapps.com/start",
142
+ ...(organizationWhen ? { when: organizationWhen } : {}),
143
+ });
144
+ }
145
+ if (!login.region) {
146
+ prompts.push({
147
+ type: "text",
148
+ key: "region",
149
+ message: "IAM Identity Center region",
150
+ placeholder: "ap-northeast-2",
151
+ ...(organizationWhen ? { when: organizationWhen } : {}),
152
+ });
153
+ }
154
+ }
155
+ return prompts.length > 0 ? prompts : undefined;
156
+ }
81
157
  function localTransport(options, accessToken, behavior = {}) {
82
158
  const backend = effectiveBackend(options, accessToken);
83
159
  const login = () => {
84
160
  if (behavior.loginOnAuthFailure === false)
85
161
  return Promise.resolve(false);
162
+ if (isKiroDeviceAuthKey(accessToken))
163
+ return Promise.resolve(false);
86
164
  return runKiroLoginFlowOnce({
87
- login: {
88
- ...options.login,
89
- useDeviceFlow: true,
90
- },
165
+ login: loginOptions(options.login),
91
166
  });
92
167
  };
93
168
  if (backend === "acp")
@@ -148,6 +223,7 @@ export function createKiroPlugin() {
148
223
  }
149
224
  let discovery;
150
225
  let lastModelDiscoveryAt = 0;
226
+ let startupDeviceAuthKey;
151
227
  let startupLogin;
152
228
  let startupLoginAttempted = false;
153
229
  const refreshModels = async (force = false) => {
@@ -156,7 +232,14 @@ export function createKiroPlugin() {
156
232
  return [];
157
233
  }
158
234
  if (!discovery) {
159
- discovery = discoverModelsFromCommand(options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
235
+ discovery = (async () => {
236
+ if (isKiroCliModelDiscoveryCommand(options.modelDiscoveryCommand)) {
237
+ const auth = await detectAuth().catch(() => undefined);
238
+ if (!auth?.authenticated)
239
+ return [];
240
+ }
241
+ return discoverModelsFromCommand(options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1));
242
+ })()
160
243
  .then((models) => {
161
244
  if (models.length > 0) {
162
245
  lastModelDiscoveryAt = Date.now();
@@ -173,6 +256,8 @@ export function createKiroPlugin() {
173
256
  return discovery;
174
257
  };
175
258
  const ensureStartupAuthenticated = async () => {
259
+ if (startupDeviceAuthKey)
260
+ return true;
176
261
  const current = await detectAuth().catch(() => undefined);
177
262
  if (current?.authenticated)
178
263
  return true;
@@ -182,10 +267,23 @@ export function createKiroPlugin() {
182
267
  return false;
183
268
  startupLoginAttempted = true;
184
269
  startupLogin = (async () => {
185
- const session = startKiroCliLoginOnce({
186
- ...options.login,
187
- useDeviceFlow: true,
188
- });
270
+ const login = loginOptions(options.login);
271
+ if (isOrganizationLogin(login) && login.identityProvider) {
272
+ try {
273
+ const authorization = await authorizeKiroDevice(login);
274
+ openExternalUrl(kiroDeviceVerificationUrl(login.identityProvider, authorization.userCode));
275
+ const credential = await pollKiroDeviceToken(authorization, {
276
+ region: options.region,
277
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
278
+ });
279
+ startupDeviceAuthKey = encodeKiroDeviceAuthKey(credential);
280
+ return true;
281
+ }
282
+ catch {
283
+ return false;
284
+ }
285
+ }
286
+ const session = startKiroCliLoginOnce(login);
189
287
  const prompted = await session.waitForPrompt(options.requestTimeoutMs);
190
288
  if (!prompted)
191
289
  return false;
@@ -206,7 +304,7 @@ export function createKiroPlugin() {
206
304
  if (localServer)
207
305
  return localServer;
208
306
  const localFetch = async (input, init) => {
209
- const transport = localTransport(options, bearerToken(init), {
307
+ const transport = localTransport(options, bearerToken(init) || startupDeviceAuthKey, {
210
308
  loginOnAuthFailure: shouldLoginOnAuthFailure(init),
211
309
  });
212
310
  return createKiroFetch({
@@ -221,11 +319,39 @@ export function createKiroPlugin() {
221
319
  localServer = await startLocalKiroServer(localFetch);
222
320
  return localServer;
223
321
  };
224
- const authorizeCliDeviceLogin = async () => {
225
- const session = startKiroCliLoginOnce({
226
- ...options.login,
227
- useDeviceFlow: true,
228
- });
322
+ const authorizeCliDeviceLogin = async (inputs) => {
323
+ const login = loginOptions(options.login, inputs);
324
+ if (isOrganizationLogin(login) && login.identityProvider) {
325
+ const authorization = await authorizeKiroDevice(login);
326
+ const url = kiroDeviceVerificationUrl(login.identityProvider, authorization.userCode);
327
+ return {
328
+ url,
329
+ instructions: `Open ${url} and confirm code: ${authorization.userCode}`,
330
+ method: "auto",
331
+ callback: async () => {
332
+ try {
333
+ const credential = await pollKiroDeviceToken(authorization, {
334
+ region: options.region,
335
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
336
+ });
337
+ const key = encodeKiroDeviceAuthKey(credential);
338
+ startupDeviceAuthKey = key;
339
+ await refreshModels(true).catch(() => []);
340
+ return {
341
+ type: "success",
342
+ key,
343
+ metadata: {
344
+ source: "kiro-device-auth",
345
+ },
346
+ };
347
+ }
348
+ catch {
349
+ return { type: "failed" };
350
+ }
351
+ },
352
+ };
353
+ }
354
+ const session = startKiroCliLoginOnce(login);
229
355
  await session.waitForPrompt(options.requestTimeoutMs);
230
356
  return {
231
357
  url: session.url,
@@ -246,6 +372,7 @@ export function createKiroPlugin() {
246
372
  },
247
373
  };
248
374
  };
375
+ const authPrompts = loginPrompts(options);
249
376
  return {
250
377
  dispose: async () => {
251
378
  await localServer?.close();
@@ -275,7 +402,8 @@ export function createKiroPlugin() {
275
402
  {
276
403
  type: "oauth",
277
404
  label: "Kiro device login",
278
- authorize: async () => authorizeCliDeviceLogin(),
405
+ ...(authPrompts ? { prompts: authPrompts } : {}),
406
+ authorize: async (inputs) => authorizeCliDeviceLogin(inputs),
279
407
  },
280
408
  {
281
409
  type: "api",
@@ -300,7 +428,7 @@ export function createKiroPlugin() {
300
428
  const apiKey = await resolveApiKey(auth);
301
429
  const server = await ensureLocalServer();
302
430
  return {
303
- apiKey: apiKey || KIRO_LOCAL_TRANSPORT_KEY,
431
+ apiKey: apiKey || startupDeviceAuthKey || KIRO_LOCAL_TRANSPORT_KEY,
304
432
  baseURL: server.baseURL,
305
433
  };
306
434
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,6 +26,7 @@
26
26
  "smoke:package": "npm run build && node scripts/smoke-package.mjs",
27
27
  "smoke:kiro": "npm run build && node scripts/real-kiro-smoke.mjs",
28
28
  "test": "bun test test/*.test.ts",
29
+ "test:real": "OPENCODE_KIRO_REAL=1 bun test test/real-kiro-cli.test.ts",
29
30
  "typecheck": "tsc --noEmit"
30
31
  },
31
32
  "keywords": [