@bogyie/opencode-kiro-plugin 0.3.0 → 0.3.5

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
- When no usable Kiro CLI session is available, open OpenCode's provider connector, choose Kiro, and select `Kiro CLI login`. The plugin 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. The same auth method also works from OpenCode's CLI provider connect flow.
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.
42
42
 
43
- The plugin does not start login during OpenCode startup. 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 a model request fails because credentials are missing or rejected, the plugin starts one shared Kiro CLI login session and still returns `KIRO_AUTH_ERROR` for that failed request; retry after the login completes. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and applies the same login-on-auth-failure behavior. `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 --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.
44
44
 
45
45
  Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
46
46
 
@@ -77,7 +77,7 @@ Supported values:
77
77
 
78
78
  ## Model Churn Handling
79
79
 
80
- The resolver intentionally avoids a hard whitelist. By default, 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; bundled metadata is not injected as available models by default.
80
+ The resolver intentionally avoids a hard whitelist. By default, 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; if discovery cannot return any models, the plugin exposes an `auto` placeholder so OpenCode keeps the Kiro provider visible for connection.
81
81
 
82
82
  Useful options:
83
83
 
@@ -127,7 +127,7 @@ This keeps new Kiro model ids usable before the package is updated without adver
127
127
 
128
128
  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.
129
129
 
130
- `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. 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.
130
+ `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. If this command fails with an auth error, the plugin starts Kiro CLI login, waits for it to finish, and retries the command once. 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.
131
131
 
132
132
  ## Troubleshooting
133
133
 
package/dist/auth.d.ts CHANGED
@@ -37,6 +37,11 @@ export interface KiroLoginSession {
37
37
  waitForPrompt(timeoutMs?: number): Promise<boolean>;
38
38
  waitForAuth(runner?: CommandRunner): Promise<boolean>;
39
39
  }
40
+ export interface KiroLoginFlowOptions {
41
+ readonly spawner?: ProcessSpawner;
42
+ readonly runner?: CommandRunner;
43
+ readonly promptTimeoutMs?: number;
44
+ }
40
45
  export declare const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
41
46
  export declare const DEFAULT_KIRO_CLI_DB_PATH: string;
42
47
  export declare const DEFAULT_KIRO_CLI_TOKEN_KEYS: readonly ["kirocli:odic:token", "codewhisperer:odic:token"];
@@ -47,6 +52,7 @@ export declare function extractKiroLoginUrl(output: string): string;
47
52
  export declare function extractKiroLoginCode(output: string): string | undefined;
48
53
  export declare function startKiroCliLogin(spawner?: ProcessSpawner): KiroLoginSession;
49
54
  export declare function startKiroCliLoginOnce(spawner?: ProcessSpawner): KiroLoginSession;
55
+ export declare function runKiroLoginFlowOnce(options?: KiroLoginFlowOptions): Promise<boolean>;
50
56
  export declare function redacted(value: string | undefined): string | undefined;
51
57
  export declare function detectAuth(env?: NodeJS.ProcessEnv, runner?: CommandRunner): Promise<AuthDiagnostics>;
52
58
  export declare function resolveApiKey(auth: () => Promise<{
package/dist/auth.js CHANGED
@@ -11,6 +11,7 @@ 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 sharedLoginFlow;
14
15
  export async function runCommand(command, args, options = {}) {
15
16
  try {
16
17
  const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
@@ -95,11 +96,59 @@ export async function readKiroCliSessionCredential(options = {}, runner = runCom
95
96
  function delay(ms) {
96
97
  return new Promise((resolve) => setTimeout(resolve, ms));
97
98
  }
98
- function firstUrl(text) {
99
- return /https?:\/\/[^\s"'<>]+/.exec(text)?.[0];
99
+ function urls(text) {
100
+ return text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
101
+ }
102
+ function isLocalCallbackUrl(value) {
103
+ try {
104
+ const url = new URL(value);
105
+ const hostname = url.hostname.toLowerCase();
106
+ return ((hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") &&
107
+ url.pathname.includes("/callback"));
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ function loginUrlFromLocalCallbackUrl(value) {
114
+ if (!isLocalCallbackUrl(value))
115
+ return undefined;
116
+ try {
117
+ const issuerUrl = new URL(value).searchParams.get("issuer_url");
118
+ if (!issuerUrl || isLocalCallbackUrl(issuerUrl))
119
+ return undefined;
120
+ const parsed = new URL(issuerUrl);
121
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.toString() : undefined;
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ }
127
+ function isKiroSigninUrl(value) {
128
+ try {
129
+ const url = new URL(value);
130
+ return url.hostname.toLowerCase() === "app.kiro.dev" && url.pathname === "/signin";
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ function firstLoginUrl(text) {
137
+ const candidates = urls(text);
138
+ const kiroSigninUrl = candidates.find(isKiroSigninUrl);
139
+ if (kiroSigninUrl)
140
+ return kiroSigninUrl;
141
+ for (const url of candidates) {
142
+ const callbackLoginUrl = loginUrlFromLocalCallbackUrl(url);
143
+ if (callbackLoginUrl)
144
+ return callbackLoginUrl;
145
+ if (!isLocalCallbackUrl(url))
146
+ return url;
147
+ }
148
+ return undefined;
100
149
  }
101
150
  export function extractKiroLoginUrl(output) {
102
- return firstUrl(output) ?? KIRO_LOGIN_URL;
151
+ return firstLoginUrl(output) ?? KIRO_LOGIN_URL;
103
152
  }
104
153
  export function extractKiroLoginCode(output) {
105
154
  return /\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/.exec(output)?.[0] ?? /\b[A-Z0-9]{8,}\b/.exec(output)?.[0];
@@ -135,13 +184,13 @@ export function startKiroCliLogin(spawner = (command, args) => spawn(command, [.
135
184
  while (Date.now() < deadline) {
136
185
  if (extractKiroLoginCode(output))
137
186
  return true;
138
- if (firstUrl(output))
187
+ if (firstLoginUrl(output))
139
188
  return true;
140
189
  if (exited && exitCode !== 0)
141
190
  return false;
142
191
  await delay(100);
143
192
  }
144
- return Boolean(extractKiroLoginCode(output) || firstUrl(output));
193
+ return Boolean(extractKiroLoginCode(output) || firstLoginUrl(output));
145
194
  },
146
195
  async waitForAuth(runner = runCommand) {
147
196
  const deadline = Date.now() + 10 * 60 * 1000;
@@ -191,6 +240,16 @@ export function startKiroCliLoginOnce(spawner = (command, args) => spawn(command
191
240
  sharedLoginStartedAt = now;
192
241
  return wrapped;
193
242
  }
243
+ export async function runKiroLoginFlowOnce(options = {}) {
244
+ sharedLoginFlow ??= (async () => {
245
+ const session = startKiroCliLoginOnce(options.spawner);
246
+ await session.waitForPrompt(options.promptTimeoutMs);
247
+ return session.waitForAuth(options.runner);
248
+ })().finally(() => {
249
+ sharedLoginFlow = undefined;
250
+ });
251
+ return sharedLoginFlow;
252
+ }
194
253
  export function redacted(value) {
195
254
  if (!value)
196
255
  return undefined;
@@ -6,7 +6,7 @@ import { type ChildProcess } from "node:child_process";
6
6
  export interface CliChatTransportOptions {
7
7
  readonly runner?: CommandRunner;
8
8
  readonly spawner?: CliChatSpawner;
9
- readonly loginStarter?: () => void;
9
+ readonly login?: () => Promise<boolean>;
10
10
  readonly trustAllTools?: boolean;
11
11
  readonly requestTimeoutMs?: number;
12
12
  }
@@ -1,4 +1,4 @@
1
- import { runCommand, startKiroCliLoginOnce } from "./auth.js";
1
+ import { runCommand, runKiroLoginFlowOnce } from "./auth.js";
2
2
  import { KiroPluginError } from "./errors.js";
3
3
  import { spawn } from "node:child_process";
4
4
  export function promptForCli(request) {
@@ -84,34 +84,22 @@ class AsyncQueue {
84
84
  export class KiroCliChatTransport {
85
85
  #runner;
86
86
  #spawner;
87
- #loginStarter;
87
+ #login;
88
88
  #trustAllTools;
89
89
  #requestTimeoutMs;
90
90
  constructor(options = {}) {
91
91
  this.#runner = options.runner ?? runCommand;
92
92
  this.#spawner = options.spawner ?? ((command, args) => spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] }));
93
- this.#loginStarter = options.loginStarter ?? (() => {
94
- startKiroCliLoginOnce();
95
- });
93
+ this.#login = options.login ?? runKiroLoginFlowOnce;
96
94
  this.#trustAllTools = options.trustAllTools === true;
97
95
  this.#requestTimeoutMs = options.requestTimeoutMs ?? 120_000;
98
96
  }
99
- #startLoginAfterAuthFailure() {
100
- try {
101
- this.#loginStarter();
102
- }
103
- catch {
104
- // Keep the original model-call auth error visible to OpenCode.
105
- }
106
- }
107
- async generate(request) {
97
+ async #runCliChat(request) {
108
98
  const result = await this.#runner("kiro-cli", cliChatArgs(request, { trustAllTools: this.#trustAllTools }), {
109
99
  timeoutMs: this.#requestTimeoutMs,
110
100
  });
111
101
  if (!result.ok) {
112
102
  const authError = result.stderr.toLowerCase().includes("not logged in");
113
- if (authError)
114
- this.#startLoginAfterAuthFailure();
115
103
  throw new KiroPluginError(result.stderr.trim() || "kiro-cli chat failed", authError ? "KIRO_AUTH_ERROR" : "KIRO_CLI_FAILED", authError ? 401 : 502);
116
104
  }
117
105
  const text = sanitizeCliChatOutput(result.stdout);
@@ -123,7 +111,20 @@ export class KiroCliChatTransport {
123
111
  modelId: request.modelId,
124
112
  };
125
113
  }
126
- async *stream(request) {
114
+ async generate(request) {
115
+ try {
116
+ return await this.#runCliChat(request);
117
+ }
118
+ catch (error) {
119
+ if (!(error instanceof KiroPluginError) || error.code !== "KIRO_AUTH_ERROR")
120
+ throw error;
121
+ const authenticated = await this.#login();
122
+ if (!authenticated)
123
+ throw error;
124
+ return this.#runCliChat(request);
125
+ }
126
+ }
127
+ async *#streamCliChat(request) {
127
128
  const child = this.#spawner("kiro-cli", cliChatArgs(request, { trustAllTools: this.#trustAllTools }));
128
129
  const queue = new AsyncQueue();
129
130
  let rawStdout = "";
@@ -163,8 +164,6 @@ export class KiroCliChatTransport {
163
164
  return;
164
165
  }
165
166
  const authError = stderr.toLowerCase().includes("not logged in");
166
- if (authError)
167
- this.#startLoginAfterAuthFailure();
168
167
  queue.fail(new KiroPluginError(stderr.trim() || `kiro-cli chat exited${code === null ? "" : ` with code ${code}`}${signal ? ` and signal ${signal}` : ""}`, authError ? "KIRO_AUTH_ERROR" : "KIRO_CLI_FAILED", authError ? 401 : 502));
169
168
  });
170
169
  try {
@@ -176,4 +175,17 @@ export class KiroCliChatTransport {
176
175
  child.kill();
177
176
  }
178
177
  }
178
+ async *stream(request) {
179
+ try {
180
+ yield* this.#streamCliChat(request);
181
+ }
182
+ catch (error) {
183
+ if (!(error instanceof KiroPluginError) || error.code !== "KIRO_AUTH_ERROR")
184
+ throw error;
185
+ const authenticated = await this.#login();
186
+ if (!authenticated)
187
+ throw error;
188
+ yield* this.#streamCliChat(request);
189
+ }
190
+ }
179
191
  }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { AcpConnection, AcpNotificationHandler, AcpStdioClientOptions, Json
3
3
  export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
4
4
  export type { AcpSessionClient, KiroAcpTransportOptions } from "./acp-transport.js";
5
5
  export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
6
- export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
6
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
7
7
  export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
8
8
  export { createKiroFetch } from "./fetch-adapter.js";
9
9
  export { loadOptions } from "./config.js";
@@ -12,7 +12,7 @@ export { additionalModelRequestFields, CodeWhispererKiroTransport, collectAssist
12
12
  export { KiroRestTransport, toKiroRestPayload } from "./kiro-rest-transport.js";
13
13
  export type { KiroRestTransportOptions } from "./kiro-rest-transport.js";
14
14
  export { ModelCache } from "./model-cache.js";
15
- export { discoverModelsFromCommand, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
15
+ export { discoverModelsFromCommand, isModelDiscoveryAuthFailure, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
16
16
  export { ModelResolutionError, ModelResolver, normalizeModelName } from "./model-resolver.js";
17
17
  export { FALLBACK_MODELS } from "./models.js";
18
18
  export { toKiroGenerateRequest } from "./request-adapter.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { KiroPlugin } from "./plugin.js";
2
2
  export { AcpJsonRpcClient, createAcpStdioClient, decodeJsonRpc, encodeJsonRpc } from "./acp-client.js";
3
3
  export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
4
4
  export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
5
- export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
5
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
6
6
  export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
7
7
  export { createKiroFetch } from "./fetch-adapter.js";
8
8
  export { loadOptions } from "./config.js";
@@ -10,7 +10,7 @@ export { getKiroCliStatus, getKiroCliVersion } from "./kiro-cli.js";
10
10
  export { additionalModelRequestFields, CodeWhispererKiroTransport, collectAssistantText, createCodeWhispererClient, streamAssistantText, toGenerateAssistantResponseInput, usageFromMetadataEvent, } from "./kiro-transport.js";
11
11
  export { KiroRestTransport, toKiroRestPayload } from "./kiro-rest-transport.js";
12
12
  export { ModelCache } from "./model-cache.js";
13
- export { discoverModelsFromCommand, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
13
+ export { discoverModelsFromCommand, isModelDiscoveryAuthFailure, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
14
14
  export { ModelResolutionError, ModelResolver, normalizeModelName } from "./model-resolver.js";
15
15
  export { FALLBACK_MODELS } from "./models.js";
16
16
  export { toKiroGenerateRequest } from "./request-adapter.js";
@@ -17,7 +17,7 @@ export type KiroRestFetch = (input: RequestInfo | URL, init?: RequestInit) => Pr
17
17
  export interface KiroRestTransportDependencies {
18
18
  readonly fetcher?: KiroRestFetch;
19
19
  readonly credentialProvider?: KiroCredentialProvider;
20
- readonly loginStarter?: () => void;
20
+ readonly login?: () => Promise<boolean>;
21
21
  }
22
22
  export declare function toKiroRestPayload(request: KiroGenerateRequest, profileArn?: string): Record<string, unknown>;
23
23
  export declare class KiroRestTransport implements KiroTransport {
@@ -1,5 +1,5 @@
1
1
  import { KiroPluginError } from "./errors.js";
2
- import { readKiroCliSessionCredential, regionFromProfileArn, startKiroCliLoginOnce, } from "./auth.js";
2
+ import { readKiroCliSessionCredential, regionFromProfileArn, runKiroLoginFlowOnce, } from "./auth.js";
3
3
  const DEFAULT_USER_AGENT = "KiroIDE";
4
4
  const DEFAULT_AGENT_MODE = "vibe";
5
5
  const STREAMING_SDK_VERSION = "1.0.34";
@@ -313,28 +313,17 @@ export class KiroRestTransport {
313
313
  #options;
314
314
  #fetcher;
315
315
  #credentialProvider;
316
- #loginStarter;
316
+ #login;
317
317
  constructor(options, dependencies = {}) {
318
318
  this.#options = options;
319
319
  this.#fetcher = dependencies.fetcher ?? fetch;
320
320
  this.#credentialProvider = dependencies.credentialProvider ?? (() => readKiroCliSessionCredential());
321
- this.#loginStarter = dependencies.loginStarter ?? (() => {
322
- startKiroCliLoginOnce();
323
- });
324
- }
325
- #startLoginAfterAuthFailure() {
326
- try {
327
- this.#loginStarter();
328
- }
329
- catch {
330
- // Keep the original model-call auth error visible to OpenCode.
331
- }
321
+ this.#login = dependencies.login ?? runKiroLoginFlowOnce;
332
322
  }
333
323
  async #credentials() {
334
324
  const session = this.#options.accessToken ? undefined : await this.#credentialProvider();
335
325
  const accessToken = this.#options.accessToken ?? session?.accessToken;
336
326
  if (!accessToken) {
337
- this.#startLoginAfterAuthFailure();
338
327
  throw new KiroPluginError("No Kiro API token or Kiro CLI session token found. Connect Kiro from OpenCode's provider connector or run `kiro-cli login`.", "KIRO_AUTH_ERROR", 401);
339
328
  }
340
329
  const profileArn = this.#options.profileArn ?? session?.profileArn;
@@ -347,7 +336,7 @@ export class KiroRestTransport {
347
336
  async generate(request) {
348
337
  return collectChunks(this.stream(request), request.modelId);
349
338
  }
350
- async *stream(request) {
339
+ async *#streamOnce(request) {
351
340
  const credentials = await this.#credentials();
352
341
  const payload = toKiroRestPayload(request, credentials.profileArn);
353
342
  let lastError;
@@ -368,8 +357,6 @@ export class KiroRestTransport {
368
357
  lastError = new KiroPluginError(message, code, response.status);
369
358
  if (response.status === 429 || response.status >= 500)
370
359
  continue;
371
- if (code === "KIRO_AUTH_ERROR")
372
- this.#startLoginAfterAuthFailure();
373
360
  throw lastError;
374
361
  }
375
362
  if (!response.body)
@@ -424,4 +411,17 @@ export class KiroRestTransport {
424
411
  if (lastError)
425
412
  throw lastError;
426
413
  }
414
+ async *stream(request) {
415
+ try {
416
+ yield* this.#streamOnce(request);
417
+ }
418
+ catch (error) {
419
+ if (!(error instanceof KiroPluginError) || error.code !== "KIRO_AUTH_ERROR")
420
+ throw error;
421
+ const authenticated = await this.#login();
422
+ if (!authenticated)
423
+ throw error;
424
+ yield* this.#streamOnce(request);
425
+ }
426
+ }
427
427
  }
@@ -1,5 +1,12 @@
1
1
  import type { CommandRunner } from "./auth.js";
2
2
  import type { CachedModelInfo, ModelCache } from "./model-cache.js";
3
+ export type KiroLoginFlowRunner = () => Promise<boolean>;
4
+ export interface ModelDiscoveryCommandOptions {
5
+ readonly runner?: CommandRunner;
6
+ readonly loginOnAuthFailure?: boolean;
7
+ readonly login?: KiroLoginFlowRunner;
8
+ }
3
9
  export declare function parseDiscoveredModels(raw: string): CachedModelInfo[];
4
- export declare function discoverModelsFromCommand(command: string, args: ReadonlyArray<string>, runner?: CommandRunner): Promise<CachedModelInfo[]>;
5
- export declare function refreshModelCacheFromCommand(cache: ModelCache, command: string, args: ReadonlyArray<string>, runner?: CommandRunner): Promise<CachedModelInfo[]>;
10
+ export declare function isModelDiscoveryAuthFailure(output: string): boolean;
11
+ export declare function discoverModelsFromCommand(command: string, args: ReadonlyArray<string>, runnerOrOptions?: CommandRunner | ModelDiscoveryCommandOptions): Promise<CachedModelInfo[]>;
12
+ export declare function refreshModelCacheFromCommand(cache: ModelCache, command: string, args: ReadonlyArray<string>, runnerOrOptions?: CommandRunner | ModelDiscoveryCommandOptions): Promise<CachedModelInfo[]>;
@@ -1,4 +1,4 @@
1
- import { runCommand } from "./auth.js";
1
+ import { runCommand, runKiroLoginFlowOnce } from "./auth.js";
2
2
  import { normalizeModelName } from "./model-resolver.js";
3
3
  function record(value) {
4
4
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
@@ -76,17 +76,39 @@ export function parseDiscoveredModels(raw) {
76
76
  }
77
77
  return dedupe(fromLines(trimmed));
78
78
  }
79
+ export function isModelDiscoveryAuthFailure(output) {
80
+ const text = output.toLowerCase();
81
+ return (text.includes("not logged in") ||
82
+ text.includes("not authenticated") ||
83
+ text.includes("unauthorized") ||
84
+ text.includes("authorization") ||
85
+ text.includes("authentication") ||
86
+ text.includes("auth token") ||
87
+ text.includes("token expired") ||
88
+ text.includes("expired token"));
89
+ }
79
90
  function dedupe(models) {
80
91
  return [...new Map(models.map((model) => [model.id, model])).values()].sort((a, b) => a.id.localeCompare(b.id));
81
92
  }
82
- export async function discoverModelsFromCommand(command, args, runner = runCommand) {
93
+ export async function discoverModelsFromCommand(command, args, runnerOrOptions = runCommand) {
94
+ const options = typeof runnerOrOptions === "function" ? { runner: runnerOrOptions } : runnerOrOptions;
95
+ const runner = options.runner ?? runCommand;
83
96
  const result = await runner(command, [...args]);
84
- if (!result.ok)
85
- return [];
97
+ if (!result.ok) {
98
+ if (!options.loginOnAuthFailure || !isModelDiscoveryAuthFailure(`${result.stdout}\n${result.stderr}`))
99
+ return [];
100
+ const loggedIn = await (options.login ?? runKiroLoginFlowOnce)();
101
+ if (!loggedIn)
102
+ return [];
103
+ const retry = await runner(command, [...args]);
104
+ if (!retry.ok)
105
+ return [];
106
+ return parseDiscoveredModels(retry.stdout);
107
+ }
86
108
  return parseDiscoveredModels(result.stdout);
87
109
  }
88
- export async function refreshModelCacheFromCommand(cache, command, args, runner = runCommand) {
89
- const models = await discoverModelsFromCommand(command, args, runner);
110
+ export async function refreshModelCacheFromCommand(cache, command, args, runnerOrOptions = runCommand) {
111
+ const models = await discoverModelsFromCommand(command, args, runnerOrOptions);
90
112
  if (models.length > 0)
91
113
  cache.update(models);
92
114
  return models;
package/dist/plugin.js CHANGED
@@ -9,6 +9,8 @@ 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" };
12
14
  function discoveredProviderModels(cache) {
13
15
  return Object.fromEntries(cache.all().map((model) => [
14
16
  model.id,
@@ -33,7 +35,7 @@ function modelRecord(value) {
33
35
  function visibleProviderModels(cache, configuredModels, hiddenModels, disabledModels) {
34
36
  const discovered = discoveredProviderModels(cache);
35
37
  const modelIDs = new Set([...Object.keys(discovered), ...Object.keys(configuredModels), ...Object.keys(hiddenModels)]);
36
- return Object.fromEntries([...modelIDs]
38
+ const models = Object.fromEntries([...modelIDs]
37
39
  .filter((id) => !disabledModels.has(normalizeModelName(id)))
38
40
  .map((id) => [
39
41
  id,
@@ -42,6 +44,9 @@ function visibleProviderModels(cache, configuredModels, hiddenModels, disabledMo
42
44
  ...(configuredModels[id] ?? {}),
43
45
  },
44
46
  ]));
47
+ if (Object.keys(models).length > 0 || disabledModels.has(PLACEHOLDER_MODEL_ID))
48
+ return models;
49
+ return { [PLACEHOLDER_MODEL_ID]: PLACEHOLDER_MODEL };
45
50
  }
46
51
  function bearerToken(init) {
47
52
  const header = new Headers(init?.headers).get("authorization");
@@ -110,11 +115,11 @@ export function createKiroPlugin() {
110
115
  const discoverIfStale = async () => {
111
116
  if (options.modelDiscovery === "off" ||
112
117
  options.modelDiscoveryCommand.length === 0 ||
113
- (discoveryAttempted && !modelCache.isStale())) {
118
+ discoveryAttempted) {
114
119
  return;
115
120
  }
116
121
  discoveryAttempted = true;
117
- discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
122
+ discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1), { loginOnAuthFailure: true })
118
123
  .catch(() => undefined)
119
124
  .then(() => {
120
125
  discovery = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.3.5",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,12 +36,12 @@
36
36
  ],
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "git+https://github.com/bogyie/opencode-kiro-plugin.git"
39
+ "url": "git+https://github.com/Bogyie/opencode-kiro-plugin.git"
40
40
  },
41
41
  "bugs": {
42
- "url": "https://github.com/bogyie/opencode-kiro-plugin/issues"
42
+ "url": "https://github.com/Bogyie/opencode-kiro-plugin/issues"
43
43
  },
44
- "homepage": "https://github.com/bogyie/opencode-kiro-plugin#readme",
44
+ "homepage": "https://github.com/Bogyie/opencode-kiro-plugin#readme",
45
45
  "license": "MIT",
46
46
  "publishConfig": {
47
47
  "access": "public",