@bogyie/opencode-kiro-plugin 0.3.20 → 0.3.21

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
@@ -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 so they are passed to `kiro-cli login --use-device-flow --license pro --identity-provider <start-url> --region <region>`.
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
 
@@ -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,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import { KiroAcpTransport } from "./acp-transport.js";
3
- import { credentialFromKiroDeviceAuthKey, detectAuth, isKiroDeviceAuthKey, KIRO_LOCAL_TRANSPORT_KEY, readKiroCliSessionCredential, resolveApiKey, startKiroCliLoginOnce, runKiroLoginFlowOnce, } from "./auth.js";
3
+ import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, KIRO_LOCAL_TRANSPORT_KEY, pollKiroDeviceToken, readKiroCliSessionCredential, resolveApiKey, startKiroCliLoginOnce, 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";
@@ -14,6 +14,7 @@ const PLACEHOLDER_MODEL_ID = "auto";
14
14
  const PLACEHOLDER_MODEL = { name: "Auto" };
15
15
  const OPENCODE_AGENT_HEADER = "x-opencode-kiro-agent";
16
16
  const LOGIN_SUPPRESSED_AGENTS = new Set(["title"]);
17
+ const LOGIN_METHODS = new Set(["builder-id", "google", "github", "organization"]);
17
18
  function discoveredProviderModels(cache) {
18
19
  return Object.fromEntries(cache.all().map((model) => [
19
20
  model.id,
@@ -78,16 +79,74 @@ function shouldLoginOnAuthFailure(init) {
78
79
  const agent = new Headers(init?.headers).get(OPENCODE_AGENT_HEADER);
79
80
  return !agent || !LOGIN_SUPPRESSED_AGENTS.has(agent);
80
81
  }
82
+ function inputString(inputs, key) {
83
+ const value = inputs?.[key]?.trim();
84
+ return value || undefined;
85
+ }
86
+ function loginOptions(base, inputs) {
87
+ const inputMethod = inputString(inputs, "method");
88
+ const method = inputMethod && LOGIN_METHODS.has(inputMethod) ? inputMethod : undefined;
89
+ const identityProvider = inputString(inputs, "identityProvider");
90
+ const region = inputString(inputs, "region");
91
+ return {
92
+ ...base,
93
+ ...(method ? { method } : {}),
94
+ ...(identityProvider ? { identityProvider } : {}),
95
+ ...(region ? { region } : {}),
96
+ useDeviceFlow: true,
97
+ };
98
+ }
99
+ function isOrganizationLogin(login) {
100
+ return login.method === "organization" || login.license === "pro" || Boolean(login.identityProvider || login.region);
101
+ }
102
+ function loginPrompts(options) {
103
+ const prompts = [];
104
+ const login = options.login;
105
+ const hasLoginRoute = Boolean(login.method || login.license || login.identityProvider || login.region);
106
+ if (!hasLoginRoute) {
107
+ prompts.push({
108
+ type: "select",
109
+ key: "method",
110
+ message: "Select Kiro login method",
111
+ options: [
112
+ { label: "Use with Builder ID", value: "builder-id" },
113
+ { label: "Use with Google", value: "google" },
114
+ { label: "Use with GitHub", value: "github" },
115
+ { label: "Use with Your Organization", value: "organization" },
116
+ ],
117
+ });
118
+ }
119
+ const organizationIsConfigured = login.method === "organization" || login.license === "pro" || Boolean(login.identityProvider || login.region);
120
+ const organizationWhen = hasLoginRoute ? undefined : { key: "method", op: "eq", value: "organization" };
121
+ if (organizationIsConfigured || !hasLoginRoute) {
122
+ if (!login.identityProvider) {
123
+ prompts.push({
124
+ type: "text",
125
+ key: "identityProvider",
126
+ message: "IAM Identity Center Start URL",
127
+ placeholder: "https://example.awsapps.com/start",
128
+ ...(organizationWhen ? { when: organizationWhen } : {}),
129
+ });
130
+ }
131
+ if (!login.region) {
132
+ prompts.push({
133
+ type: "text",
134
+ key: "region",
135
+ message: "IAM Identity Center region",
136
+ placeholder: "ap-northeast-2",
137
+ ...(organizationWhen ? { when: organizationWhen } : {}),
138
+ });
139
+ }
140
+ }
141
+ return prompts.length > 0 ? prompts : undefined;
142
+ }
81
143
  function localTransport(options, accessToken, behavior = {}) {
82
144
  const backend = effectiveBackend(options, accessToken);
83
145
  const login = () => {
84
146
  if (behavior.loginOnAuthFailure === false)
85
147
  return Promise.resolve(false);
86
148
  return runKiroLoginFlowOnce({
87
- login: {
88
- ...options.login,
89
- useDeviceFlow: true,
90
- },
149
+ login: loginOptions(options.login),
91
150
  });
92
151
  };
93
152
  if (backend === "acp")
@@ -182,10 +241,7 @@ export function createKiroPlugin() {
182
241
  return false;
183
242
  startupLoginAttempted = true;
184
243
  startupLogin = (async () => {
185
- const session = startKiroCliLoginOnce({
186
- ...options.login,
187
- useDeviceFlow: true,
188
- });
244
+ const session = startKiroCliLoginOnce(loginOptions(options.login));
189
245
  const prompted = await session.waitForPrompt(options.requestTimeoutMs);
190
246
  if (!prompted)
191
247
  return false;
@@ -221,11 +277,37 @@ export function createKiroPlugin() {
221
277
  localServer = await startLocalKiroServer(localFetch);
222
278
  return localServer;
223
279
  };
224
- const authorizeCliDeviceLogin = async () => {
225
- const session = startKiroCliLoginOnce({
226
- ...options.login,
227
- useDeviceFlow: true,
228
- });
280
+ const authorizeCliDeviceLogin = async (inputs) => {
281
+ const login = loginOptions(options.login, inputs);
282
+ if (isOrganizationLogin(login) && login.identityProvider) {
283
+ const authorization = await authorizeKiroDevice(login);
284
+ const url = kiroDeviceVerificationUrl(login.identityProvider, authorization.userCode);
285
+ return {
286
+ url,
287
+ instructions: `Open ${url} and confirm code: ${authorization.userCode}`,
288
+ method: "auto",
289
+ callback: async () => {
290
+ try {
291
+ const credential = await pollKiroDeviceToken(authorization, {
292
+ region: options.region,
293
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
294
+ });
295
+ await refreshModels(true).catch(() => []);
296
+ return {
297
+ type: "success",
298
+ key: encodeKiroDeviceAuthKey(credential),
299
+ metadata: {
300
+ source: "kiro-device-auth",
301
+ },
302
+ };
303
+ }
304
+ catch {
305
+ return { type: "failed" };
306
+ }
307
+ },
308
+ };
309
+ }
310
+ const session = startKiroCliLoginOnce(login);
229
311
  await session.waitForPrompt(options.requestTimeoutMs);
230
312
  return {
231
313
  url: session.url,
@@ -246,6 +328,7 @@ export function createKiroPlugin() {
246
328
  },
247
329
  };
248
330
  };
331
+ const authPrompts = loginPrompts(options);
249
332
  return {
250
333
  dispose: async () => {
251
334
  await localServer?.close();
@@ -275,7 +358,8 @@ export function createKiroPlugin() {
275
358
  {
276
359
  type: "oauth",
277
360
  label: "Kiro device login",
278
- authorize: async () => authorizeCliDeviceLogin(),
361
+ ...(authPrompts ? { prompts: authPrompts } : {}),
362
+ authorize: async (inputs) => authorizeCliDeviceLogin(inputs),
279
363
  },
280
364
  {
281
365
  type: "api",
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.21",
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": [