@bogyie/opencode-kiro-plugin 0.3.4 → 0.3.6

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,31 @@ 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
- When no usable Kiro CLI session is available, OpenCode's startup model discovery may start the same Kiro CLI login flow automatically. The plugin runs `kiro-cli login --use-device-flow`, waits for login to complete through `kiro-cli whoami`, and then runs the model discovery command one more time. If that login fails or times out, discovery fails for that startup attempt; it does not loop.
41
+ When no usable Kiro CLI session is available, OpenCode's startup model discovery may start the same Kiro CLI login flow automatically. The plugin runs `kiro-cli login`, waits for login to complete through `kiro-cli whoami`, and then runs the model discovery command one more time. If that login fails or times out, discovery fails for that startup attempt; it does not loop.
42
42
 
43
- You can also open OpenCode's provider connector, choose Kiro, and select `Kiro CLI login`. The connector flow starts `kiro-cli login --use-device-flow`, waits until the verification URL/device code is visible, returns them to the connector, and then waits until `kiro-cli whoami` succeeds. Direct fetch mode uses a usable API key/token when one is configured; otherwise it reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If credentials expire during use, direct fetch and `cli-chat` start the shared Kiro login flow, wait for it to complete, and retry the failed 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 also open OpenCode's provider connector, choose Kiro, and select `Kiro CLI login`. The connector flow starts `kiro-cli login`, waits until the login URL/device code is visible, returns it to the connector, and then waits until `kiro-cli whoami` succeeds. Direct fetch mode uses a usable API key/token when one is configured; otherwise it reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If credentials expire during use, direct fetch and `cli-chat` start the shared Kiro login flow, wait for it to complete, and retry the failed 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
+
45
+ For AWS IAM Identity Center login, configure the CLI login arguments separately from the API region:
46
+
47
+ ```jsonc
48
+ {
49
+ "plugin": [
50
+ [
51
+ "@bogyie/opencode-kiro-plugin",
52
+ {
53
+ "region": "ap-northeast-2",
54
+ "login": {
55
+ "license": "pro",
56
+ "identityProvider": "https://example.awsapps.com/start",
57
+ "region": "ap-northeast-2"
58
+ }
59
+ }
60
+ ]
61
+ ]
62
+ }
63
+ ```
64
+
65
+ This runs `kiro-cli login --license pro --identity-provider https://example.awsapps.com/start --region ap-northeast-2`. Do not enable `login.useDeviceFlow` for the normal browser callback flow; Kiro CLI needs to keep its localhost callback listener alive while the browser redirects back.
44
66
 
45
67
  Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
46
68
 
@@ -56,6 +78,12 @@ Configure the backend through plugin options:
56
78
  {
57
79
  "backend": "auto",
58
80
  "region": "us-east-1",
81
+ // Optional Kiro CLI login overrides:
82
+ // "login": {
83
+ // "license": "pro",
84
+ // "identityProvider": "https://example.awsapps.com/start",
85
+ // "region": "ap-northeast-2"
86
+ // },
59
87
  "endpoint": "https://q.us-east-1.amazonaws.com",
60
88
  "maxAttempts": 3,
61
89
  "requestTimeoutMs": 120000,
package/dist/auth.d.ts CHANGED
@@ -37,10 +37,18 @@ export interface KiroLoginSession {
37
37
  waitForPrompt(timeoutMs?: number): Promise<boolean>;
38
38
  waitForAuth(runner?: CommandRunner): Promise<boolean>;
39
39
  }
40
+ export interface KiroCliLoginOptions {
41
+ readonly license?: "free" | "pro";
42
+ readonly identityProvider?: string;
43
+ readonly region?: string;
44
+ readonly useDeviceFlow?: boolean;
45
+ readonly extraArgs?: ReadonlyArray<string>;
46
+ }
40
47
  export interface KiroLoginFlowOptions {
41
48
  readonly spawner?: ProcessSpawner;
42
49
  readonly runner?: CommandRunner;
43
50
  readonly promptTimeoutMs?: number;
51
+ readonly login?: KiroCliLoginOptions;
44
52
  }
45
53
  export declare const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
46
54
  export declare const DEFAULT_KIRO_CLI_DB_PATH: string;
@@ -50,8 +58,9 @@ export declare function regionFromProfileArn(profileArn: string | undefined): st
50
58
  export declare function readKiroCliSessionCredential(options?: KiroCliSessionCredentialOptions, runner?: CommandRunner): Promise<KiroCliSessionCredential | undefined>;
51
59
  export declare function extractKiroLoginUrl(output: string): string;
52
60
  export declare function extractKiroLoginCode(output: string): string | undefined;
53
- export declare function startKiroCliLogin(spawner?: ProcessSpawner): KiroLoginSession;
54
- export declare function startKiroCliLoginOnce(spawner?: ProcessSpawner): KiroLoginSession;
61
+ export declare function kiroCliLoginArgs(options?: KiroCliLoginOptions): string[];
62
+ export declare function startKiroCliLogin(optionsOrSpawner?: KiroCliLoginOptions | ProcessSpawner, spawner?: ProcessSpawner): KiroLoginSession;
63
+ export declare function startKiroCliLoginOnce(optionsOrSpawner?: KiroCliLoginOptions | ProcessSpawner, spawner?: ProcessSpawner): KiroLoginSession;
55
64
  export declare function runKiroLoginFlowOnce(options?: KiroLoginFlowOptions): Promise<boolean>;
56
65
  export declare function redacted(value: string | undefined): string | undefined;
57
66
  export declare function detectAuth(env?: NodeJS.ProcessEnv, runner?: CommandRunner): Promise<AuthDiagnostics>;
package/dist/auth.js CHANGED
@@ -11,7 +11,9 @@ export const DEFAULT_KIRO_CLI_TOKEN_KEYS = ["kirocli:odic:token", "codewhisperer
11
11
  const LOGIN_REUSE_WINDOW_MS = 2 * 60 * 1000;
12
12
  let sharedLoginSession;
13
13
  let sharedLoginStartedAt = 0;
14
+ let sharedLoginSessionKey = "";
14
15
  let sharedLoginFlow;
16
+ let sharedLoginFlowKey = "";
15
17
  export async function runCommand(command, args, options = {}) {
16
18
  try {
17
19
  const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
@@ -96,17 +98,90 @@ export async function readKiroCliSessionCredential(options = {}, runner = runCom
96
98
  function delay(ms) {
97
99
  return new Promise((resolve) => setTimeout(resolve, ms));
98
100
  }
99
- function firstUrl(text) {
100
- return /https?:\/\/[^\s"'<>]+/.exec(text)?.[0];
101
+ function urls(text) {
102
+ return text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
103
+ }
104
+ function isLocalCallbackUrl(value) {
105
+ try {
106
+ const url = new URL(value);
107
+ const hostname = url.hostname.toLowerCase();
108
+ return ((hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") &&
109
+ url.pathname.includes("/callback"));
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ function loginUrlFromLocalCallbackUrl(value) {
116
+ if (!isLocalCallbackUrl(value))
117
+ return undefined;
118
+ try {
119
+ const issuerUrl = new URL(value).searchParams.get("issuer_url");
120
+ if (!issuerUrl || isLocalCallbackUrl(issuerUrl))
121
+ return undefined;
122
+ const parsed = new URL(issuerUrl);
123
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.toString() : undefined;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ function isKiroSigninUrl(value) {
130
+ try {
131
+ const url = new URL(value);
132
+ return url.hostname.toLowerCase() === "app.kiro.dev" && url.pathname === "/signin";
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ function firstLoginUrl(text) {
139
+ const candidates = urls(text);
140
+ const kiroSigninUrl = candidates.find(isKiroSigninUrl);
141
+ if (kiroSigninUrl)
142
+ return kiroSigninUrl;
143
+ for (const url of candidates) {
144
+ const callbackLoginUrl = loginUrlFromLocalCallbackUrl(url);
145
+ if (callbackLoginUrl)
146
+ return callbackLoginUrl;
147
+ if (!isLocalCallbackUrl(url))
148
+ return url;
149
+ }
150
+ return undefined;
101
151
  }
102
152
  export function extractKiroLoginUrl(output) {
103
- return firstUrl(output) ?? KIRO_LOGIN_URL;
153
+ return firstLoginUrl(output) ?? KIRO_LOGIN_URL;
104
154
  }
105
155
  export function extractKiroLoginCode(output) {
106
156
  return /\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/.exec(output)?.[0] ?? /\b[A-Z0-9]{8,}\b/.exec(output)?.[0];
107
157
  }
108
- export function startKiroCliLogin(spawner = (command, args) => spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] })) {
109
- const child = spawner("kiro-cli", ["login", "--use-device-flow"]);
158
+ function defaultSpawner(command, args) {
159
+ return spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
160
+ }
161
+ function loginKey(options = {}) {
162
+ return JSON.stringify(kiroCliLoginArgs(options));
163
+ }
164
+ function loginOptionsAndSpawner(optionsOrSpawner, spawner) {
165
+ if (typeof optionsOrSpawner === "function")
166
+ return { options: {}, spawner: optionsOrSpawner };
167
+ return { options: optionsOrSpawner ?? {}, spawner: spawner ?? defaultSpawner };
168
+ }
169
+ export function kiroCliLoginArgs(options = {}) {
170
+ const args = ["login"];
171
+ if (options.license)
172
+ args.push("--license", options.license);
173
+ if (options.identityProvider)
174
+ args.push("--identity-provider", options.identityProvider);
175
+ if (options.region)
176
+ args.push("--region", options.region);
177
+ if (options.useDeviceFlow)
178
+ args.push("--use-device-flow");
179
+ args.push(...(options.extraArgs ?? []));
180
+ return args;
181
+ }
182
+ export function startKiroCliLogin(optionsOrSpawner, spawner) {
183
+ const resolved = loginOptionsAndSpawner(optionsOrSpawner, spawner);
184
+ const child = resolved.spawner("kiro-cli", kiroCliLoginArgs(resolved.options));
110
185
  let output = "";
111
186
  let exited = false;
112
187
  let exitCode = null;
@@ -136,13 +211,13 @@ export function startKiroCliLogin(spawner = (command, args) => spawn(command, [.
136
211
  while (Date.now() < deadline) {
137
212
  if (extractKiroLoginCode(output))
138
213
  return true;
139
- if (firstUrl(output))
214
+ if (firstLoginUrl(output))
140
215
  return true;
141
216
  if (exited && exitCode !== 0)
142
217
  return false;
143
218
  await delay(100);
144
219
  }
145
- return Boolean(extractKiroLoginCode(output) || firstUrl(output));
220
+ return Boolean(extractKiroLoginCode(output) || firstLoginUrl(output));
146
221
  },
147
222
  async waitForAuth(runner = runCommand) {
148
223
  const deadline = Date.now() + 10 * 60 * 1000;
@@ -158,11 +233,14 @@ export function startKiroCliLogin(spawner = (command, args) => spawn(command, [.
158
233
  },
159
234
  };
160
235
  }
161
- export function startKiroCliLoginOnce(spawner = (command, args) => spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] })) {
236
+ export function startKiroCliLoginOnce(optionsOrSpawner, spawner) {
237
+ const resolved = loginOptionsAndSpawner(optionsOrSpawner, spawner);
238
+ const key = loginKey(resolved.options);
162
239
  const now = Date.now();
163
- if (sharedLoginSession && now - sharedLoginStartedAt < LOGIN_REUSE_WINDOW_MS)
240
+ if (sharedLoginSession && sharedLoginSessionKey === key && now - sharedLoginStartedAt < LOGIN_REUSE_WINDOW_MS) {
164
241
  return sharedLoginSession;
165
- const session = startKiroCliLogin(spawner);
242
+ }
243
+ const session = startKiroCliLogin(resolved.options, resolved.spawner);
166
244
  const wrapped = {
167
245
  get url() {
168
246
  return session.url;
@@ -184,22 +262,29 @@ export function startKiroCliLoginOnce(spawner = (command, args) => spawn(command
184
262
  if (sharedLoginSession === wrapped) {
185
263
  sharedLoginSession = undefined;
186
264
  sharedLoginStartedAt = 0;
265
+ sharedLoginSessionKey = "";
187
266
  }
188
267
  }
189
268
  },
190
269
  };
191
270
  sharedLoginSession = wrapped;
192
271
  sharedLoginStartedAt = now;
272
+ sharedLoginSessionKey = key;
193
273
  return wrapped;
194
274
  }
195
275
  export async function runKiroLoginFlowOnce(options = {}) {
196
- sharedLoginFlow ??= (async () => {
197
- const session = startKiroCliLoginOnce(options.spawner);
198
- await session.waitForPrompt(options.promptTimeoutMs);
199
- return session.waitForAuth(options.runner);
200
- })().finally(() => {
201
- sharedLoginFlow = undefined;
202
- });
276
+ const key = loginKey(options.login);
277
+ if (!sharedLoginFlow || sharedLoginFlowKey !== key) {
278
+ sharedLoginFlowKey = key;
279
+ sharedLoginFlow = (async () => {
280
+ const session = startKiroCliLoginOnce(options.login, options.spawner);
281
+ await session.waitForPrompt(options.promptTimeoutMs);
282
+ return session.waitForAuth(options.runner);
283
+ })().finally(() => {
284
+ sharedLoginFlow = undefined;
285
+ sharedLoginFlowKey = "";
286
+ });
287
+ }
203
288
  return sharedLoginFlow;
204
289
  }
205
290
  export function redacted(value) {
package/dist/config.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { KiroCliLoginOptions } from "./auth.js";
1
2
  export type BackendMode = "auto" | "fetch" | "cli-chat" | "acp";
2
3
  export type ModelDiscoveryMode = "auto" | "off";
3
4
  export interface KiroPluginOptions {
4
5
  readonly providerID: string;
5
6
  readonly region: string;
7
+ readonly login: KiroCliLoginOptions;
6
8
  readonly endpoint?: string;
7
9
  readonly backend: BackendMode;
8
10
  readonly modelDiscovery: ModelDiscoveryMode;
package/dist/config.js CHANGED
@@ -43,6 +43,20 @@ function optionalString(value) {
43
43
  const trimmed = value.trim();
44
44
  return trimmed || undefined;
45
45
  }
46
+ function loginOptions(value) {
47
+ const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
48
+ const license = input.license === "free" || input.license === "pro" ? input.license : undefined;
49
+ const identityProvider = optionalString(input.identityProvider);
50
+ const region = optionalString(input.region);
51
+ const extraArgs = stringArray(input.extraArgs);
52
+ return {
53
+ ...(license ? { license } : {}),
54
+ ...(identityProvider ? { identityProvider } : {}),
55
+ ...(region ? { region } : {}),
56
+ useDeviceFlow: input.useDeviceFlow === true,
57
+ ...(extraArgs.length > 0 ? { extraArgs } : {}),
58
+ };
59
+ }
46
60
  export function loadOptions(raw = {}) {
47
61
  const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
48
62
  const backend = typeof input.backend === "string" && BACKENDS.has(input.backend) ? input.backend : "auto";
@@ -58,6 +72,7 @@ export function loadOptions(raw = {}) {
58
72
  return {
59
73
  providerID: typeof input.providerID === "string" && input.providerID ? input.providerID : DEFAULT_PROVIDER_ID,
60
74
  region: typeof input.region === "string" && input.region ? input.region : DEFAULT_REGION,
75
+ login: loginOptions(input.login),
61
76
  ...(endpoint ? { endpoint } : {}),
62
77
  backend,
63
78
  modelDiscovery,
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 { detectAuth, resolveApiKey, startKiroCliLoginOnce } from "./auth.js";
3
+ import { detectAuth, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLoginOnce } 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";
@@ -73,11 +73,14 @@ function localTransport(options, accessToken) {
73
73
  return new KiroCliChatTransport({
74
74
  trustAllTools: options.trustAllTools,
75
75
  ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
76
+ login: () => runKiroLoginFlowOnce({ login: options.login }),
76
77
  });
77
78
  }
78
79
  if (backend === "fetch") {
79
80
  const apiKey = accessToken || process.env.KIRO_API_KEY;
80
- return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey));
81
+ return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey), {
82
+ login: () => runKiroLoginFlowOnce({ login: options.login }),
83
+ });
81
84
  }
82
85
  return undefined;
83
86
  }
@@ -119,7 +122,7 @@ export function createKiroPlugin() {
119
122
  return;
120
123
  }
121
124
  discoveryAttempted = true;
122
- discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1), { loginOnAuthFailure: true })
125
+ discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1), { loginOnAuthFailure: true, login: () => runKiroLoginFlowOnce({ login: options.login }) })
123
126
  .catch(() => undefined)
124
127
  .then(() => {
125
128
  discovery = undefined;
@@ -175,7 +178,7 @@ export function createKiroPlugin() {
175
178
  type: "oauth",
176
179
  label: "Kiro CLI login",
177
180
  authorize: async () => {
178
- const session = startKiroCliLoginOnce();
181
+ const session = startKiroCliLoginOnce(options.login);
179
182
  await session.waitForPrompt();
180
183
  return {
181
184
  url: session.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",