@bogyie/opencode-kiro-plugin 0.2.6 → 0.2.8

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
@@ -37,7 +37,9 @@ The plugin resolves credentials in this order:
37
37
  2. OpenCode auth input for provider `kiro`
38
38
  3. `kiro-cli whoami` diagnostics for CLI session visibility
39
39
 
40
- Direct fetch mode requires an API key/token usable by the Kiro/CodeWhisperer client. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state.
40
+ When no usable Kiro CLI session is available, choose the `Kiro CLI login` auth method in OpenCode. The plugin starts `kiro-cli login --use-device-flow`, opens the browser through the official CLI login flow, and waits until `kiro-cli whoami` succeeds.
41
+
42
+ Direct fetch mode requires an API key/token usable by the Kiro/CodeWhisperer client. `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.
41
43
 
42
44
  Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
43
45
 
@@ -65,9 +67,9 @@ Configure the backend through plugin options:
65
67
 
66
68
  Supported values:
67
69
 
68
- - `auto`: use CodeWhisperer fetch transport when an API key is available; otherwise use CLI chat fallback.
70
+ - `auto`: use CodeWhisperer fetch transport when an API key is available; otherwise use streaming `cli-chat` with the local Kiro CLI login.
69
71
  - `fetch`: require the direct Kiro/CodeWhisperer fetch path. If no usable auth is available, requests fail with a structured backend/auth error.
70
- - `cli-chat`: call `kiro-cli chat --no-interactive --model <model>`. This is official and stable, and uses the model selected in OpenCode.
72
+ - `cli-chat`: spawn `kiro-cli chat --no-interactive --model <model>` and stream stdout chunks as they arrive. Chunk granularity is controlled by Kiro CLI.
71
73
  - `acp`: launch `kiro-cli acp`, initialize a session, optionally set the requested model, send the prompt, and collect `AgentMessageChunk` notifications until `TurnEnd`.
72
74
 
73
75
  `trustAllTools` affects both `cli-chat` and ACP permission handling. In ACP mode, permission requests are rejected by default and allowed only when `trustAllTools: true`.
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ChildProcess } from "node:child_process";
1
2
  export type AuthMethod = "api-key" | "cli-session" | "none";
2
3
  export interface AuthDiagnostics {
3
4
  readonly authenticated: boolean;
@@ -16,7 +17,16 @@ export interface CommandRunOptions {
16
17
  readonly timeoutMs?: number;
17
18
  }
18
19
  export type CommandRunner = (command: string, args: ReadonlyArray<string>, options?: CommandRunOptions) => Promise<CommandResult>;
20
+ export type ProcessSpawner = (command: string, args: ReadonlyArray<string>) => ChildProcess;
21
+ export interface KiroLoginSession {
22
+ readonly url: string;
23
+ readonly instructions: string;
24
+ waitForAuth(runner?: CommandRunner): Promise<boolean>;
25
+ }
26
+ export declare const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
19
27
  export declare function runCommand(command: string, args: ReadonlyArray<string>, options?: CommandRunOptions): Promise<CommandResult>;
28
+ export declare function extractKiroLoginUrl(output: string): string;
29
+ export declare function startKiroCliLogin(spawner?: ProcessSpawner): KiroLoginSession;
20
30
  export declare function redacted(value: string | undefined): string | undefined;
21
31
  export declare function detectAuth(env?: NodeJS.ProcessEnv, runner?: CommandRunner): Promise<AuthDiagnostics>;
22
32
  export declare function resolveApiKey(auth: () => Promise<{
package/dist/auth.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { spawn } from "node:child_process";
2
3
  import { promisify } from "node:util";
3
4
  const execFileAsync = promisify(execFile);
5
+ export const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
4
6
  export async function runCommand(command, args, options = {}) {
5
7
  try {
6
8
  const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
@@ -16,6 +18,58 @@ export async function runCommand(command, args, options = {}) {
16
18
  };
17
19
  }
18
20
  }
21
+ function delay(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ function firstUrl(text) {
25
+ return /https?:\/\/[^\s"'<>]+/.exec(text)?.[0];
26
+ }
27
+ export function extractKiroLoginUrl(output) {
28
+ return firstUrl(output) ?? KIRO_LOGIN_URL;
29
+ }
30
+ export function startKiroCliLogin(spawner = (command, args) => spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] })) {
31
+ const child = spawner("kiro-cli", ["login", "--use-device-flow"]);
32
+ let output = "";
33
+ let exited = false;
34
+ let exitCode = null;
35
+ child.stdout?.on("data", (chunk) => {
36
+ output += chunk.toString("utf8");
37
+ });
38
+ child.stderr?.on("data", (chunk) => {
39
+ output += chunk.toString("utf8");
40
+ });
41
+ child.on("exit", (code) => {
42
+ exited = true;
43
+ exitCode = code;
44
+ });
45
+ return {
46
+ get url() {
47
+ return extractKiroLoginUrl(output);
48
+ },
49
+ get instructions() {
50
+ const url = extractKiroLoginUrl(output);
51
+ const code = /\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/.exec(output)?.[0] ?? /\b[A-Z0-9]{8,}\b/.exec(output)?.[0];
52
+ return [
53
+ "Complete Kiro CLI login in the browser.",
54
+ `URL: ${url}`,
55
+ ...(code ? [`Code: ${code}`] : []),
56
+ "The plugin will continue after `kiro-cli whoami` succeeds.",
57
+ ].join("\n");
58
+ },
59
+ async waitForAuth(runner = runCommand) {
60
+ const deadline = Date.now() + 10 * 60 * 1000;
61
+ while (Date.now() < deadline) {
62
+ const auth = await detectAuth(process.env, runner);
63
+ if (auth.authenticated)
64
+ return true;
65
+ if (exited && exitCode !== 0)
66
+ return false;
67
+ await delay(2000);
68
+ }
69
+ return false;
70
+ },
71
+ };
72
+ }
19
73
  export function redacted(value) {
20
74
  if (!value)
21
75
  return undefined;
@@ -1,17 +1,23 @@
1
1
  import type { CommandRunner } from "./auth.js";
2
2
  import type { KiroTransport } from "./fetch-adapter.js";
3
3
  import type { KiroGenerateRequest } from "./request-adapter.js";
4
- import type { KiroGenerateResponse } from "./response-adapter.js";
4
+ import type { KiroGenerateResponse, KiroStreamEvent } from "./response-adapter.js";
5
+ import { type ChildProcess } from "node:child_process";
5
6
  export interface CliChatTransportOptions {
6
7
  readonly runner?: CommandRunner;
8
+ readonly spawner?: CliChatSpawner;
9
+ readonly loginStarter?: () => void;
7
10
  readonly trustAllTools?: boolean;
8
11
  readonly requestTimeoutMs?: number;
9
12
  }
13
+ export type CliChatSpawner = (command: string, args: ReadonlyArray<string>) => ChildProcess;
10
14
  export declare function promptForCli(request: KiroGenerateRequest): string;
11
15
  export declare function cliChatArgs(request: KiroGenerateRequest, options?: Pick<CliChatTransportOptions, "trustAllTools">): string[];
12
16
  export declare function sanitizeCliChatOutput(output: string): string;
17
+ export declare function sanitizeCliChatStreamingOutput(output: string): string;
13
18
  export declare class KiroCliChatTransport implements KiroTransport {
14
19
  #private;
15
20
  constructor(options?: CliChatTransportOptions);
16
21
  generate(request: KiroGenerateRequest): Promise<KiroGenerateResponse>;
22
+ stream(request: KiroGenerateRequest): AsyncIterable<KiroStreamEvent>;
17
23
  }
@@ -1,5 +1,14 @@
1
- import { runCommand } from "./auth.js";
1
+ import { runCommand, startKiroCliLogin } from "./auth.js";
2
2
  import { KiroPluginError } from "./errors.js";
3
+ import { spawn } from "node:child_process";
4
+ let lastLoginStartAt = 0;
5
+ function startKiroCliLoginOnce() {
6
+ const now = Date.now();
7
+ if (now - lastLoginStartAt < 30_000)
8
+ return;
9
+ lastLoginStartAt = now;
10
+ startKiroCliLogin();
11
+ }
3
12
  export function promptForCli(request) {
4
13
  const parts = [
5
14
  request.system ? `System:\n${request.system}` : "",
@@ -29,21 +38,86 @@ export function sanitizeCliChatOutput(output) {
29
38
  }
30
39
  return lines.join("\n").trim();
31
40
  }
41
+ export function sanitizeCliChatStreamingOutput(output) {
42
+ const text = output.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\r/g, "");
43
+ const lines = text
44
+ .split("\n")
45
+ .filter((line) => !line.includes("Credits:"));
46
+ if (lines[0]?.trimStart().startsWith(">")) {
47
+ lines[0] = lines[0].replace(/^\s*>\s*/, "");
48
+ }
49
+ return lines.join("\n");
50
+ }
51
+ class AsyncQueue {
52
+ #values = [];
53
+ #waiting = [];
54
+ #closed = false;
55
+ #error;
56
+ push(value) {
57
+ if (this.#closed || this.#error)
58
+ return;
59
+ this.#values.push(value);
60
+ this.#wake();
61
+ }
62
+ close() {
63
+ this.#closed = true;
64
+ this.#wake();
65
+ }
66
+ fail(error) {
67
+ this.#error = error;
68
+ this.#wake();
69
+ }
70
+ #wake() {
71
+ for (const resolve of this.#waiting.splice(0))
72
+ resolve();
73
+ }
74
+ async *[Symbol.asyncIterator]() {
75
+ for (;;) {
76
+ if (this.#values.length > 0) {
77
+ const value = this.#values.shift();
78
+ if (value !== undefined)
79
+ yield value;
80
+ continue;
81
+ }
82
+ if (this.#error)
83
+ throw this.#error;
84
+ if (this.#closed)
85
+ return;
86
+ await new Promise((resolve) => {
87
+ this.#waiting.push(resolve);
88
+ });
89
+ }
90
+ }
91
+ }
32
92
  export class KiroCliChatTransport {
33
93
  #runner;
94
+ #spawner;
95
+ #loginStarter;
34
96
  #trustAllTools;
35
97
  #requestTimeoutMs;
36
98
  constructor(options = {}) {
37
99
  this.#runner = options.runner ?? runCommand;
100
+ this.#spawner = options.spawner ?? ((command, args) => spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] }));
101
+ this.#loginStarter = options.loginStarter ?? startKiroCliLoginOnce;
38
102
  this.#trustAllTools = options.trustAllTools === true;
39
103
  this.#requestTimeoutMs = options.requestTimeoutMs ?? 120_000;
40
104
  }
105
+ #startLoginAfterAuthFailure() {
106
+ try {
107
+ this.#loginStarter();
108
+ }
109
+ catch {
110
+ // Preserve the original auth error. Login launch failures are diagnostic-only here.
111
+ }
112
+ }
41
113
  async generate(request) {
42
114
  const result = await this.#runner("kiro-cli", cliChatArgs(request, { trustAllTools: this.#trustAllTools }), {
43
115
  timeoutMs: this.#requestTimeoutMs,
44
116
  });
45
117
  if (!result.ok) {
46
118
  const authError = result.stderr.toLowerCase().includes("not logged in");
119
+ if (authError)
120
+ this.#startLoginAfterAuthFailure();
47
121
  throw new KiroPluginError(result.stderr.trim() || "kiro-cli chat failed", authError ? "KIRO_AUTH_ERROR" : "KIRO_CLI_FAILED", authError ? 401 : 502);
48
122
  }
49
123
  const text = sanitizeCliChatOutput(result.stdout);
@@ -55,4 +129,57 @@ export class KiroCliChatTransport {
55
129
  modelId: request.modelId,
56
130
  };
57
131
  }
132
+ async *stream(request) {
133
+ const child = this.#spawner("kiro-cli", cliChatArgs(request, { trustAllTools: this.#trustAllTools }));
134
+ const queue = new AsyncQueue();
135
+ let rawStdout = "";
136
+ let emittedText = "";
137
+ let stderr = "";
138
+ let sawText = false;
139
+ const timer = setTimeout(() => {
140
+ child.kill();
141
+ queue.fail(new KiroPluginError("Timed out waiting for kiro-cli chat response.", "KIRO_TIMEOUT", 504));
142
+ }, this.#requestTimeoutMs);
143
+ child.stdout?.on("data", (chunk) => {
144
+ rawStdout += chunk.toString("utf8");
145
+ const cleaned = sanitizeCliChatStreamingOutput(rawStdout);
146
+ if (!cleaned.startsWith(emittedText))
147
+ return;
148
+ const delta = cleaned.slice(emittedText.length);
149
+ emittedText = cleaned;
150
+ if (!delta)
151
+ return;
152
+ sawText = true;
153
+ queue.push({ type: "text", text: delta, modelId: request.modelId });
154
+ });
155
+ child.stderr?.on("data", (chunk) => {
156
+ stderr += chunk.toString("utf8");
157
+ });
158
+ child.on("error", (error) => {
159
+ clearTimeout(timer);
160
+ queue.fail(new KiroPluginError(error.message, "KIRO_CLI_FAILED", 502));
161
+ });
162
+ child.on("exit", (code, signal) => {
163
+ clearTimeout(timer);
164
+ if (code === 0) {
165
+ if (sawText)
166
+ queue.close();
167
+ else
168
+ queue.fail(new KiroPluginError(stderr.trim() || "kiro-cli chat completed without returning assistant text", "KIRO_EMPTY_RESPONSE", 502));
169
+ return;
170
+ }
171
+ const authError = stderr.toLowerCase().includes("not logged in");
172
+ if (authError)
173
+ this.#startLoginAfterAuthFailure();
174
+ 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));
175
+ });
176
+ try {
177
+ yield* queue;
178
+ }
179
+ finally {
180
+ clearTimeout(timer);
181
+ if (!child.killed && child.exitCode === null)
182
+ child.kill();
183
+ }
184
+ }
58
185
  }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { AcpJsonRpcClient, createAcpStdioClient, decodeJsonRpc, encodeJsonRpc }
2
2
  export type { AcpConnection, AcpNotificationHandler, AcpStdioClientOptions, JsonRpcMessage } from "./acp-client.js";
3
3
  export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
4
4
  export type { AcpSessionClient, KiroAcpTransportOptions } from "./acp-transport.js";
5
- export { createKiroPlugin, KiroPlugin } from "./plugin.js";
6
- export { detectAuth, redacted, resolveApiKey } from "./auth.js";
5
+ export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
6
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, redacted, resolveApiKey, startKiroCliLogin } 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";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  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
- export { createKiroPlugin, KiroPlugin } from "./plugin.js";
5
- export { detectAuth, redacted, resolveApiKey } from "./auth.js";
4
+ export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
5
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, redacted, resolveApiKey, startKiroCliLogin } 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";
package/dist/plugin.d.ts CHANGED
@@ -2,7 +2,10 @@ import type { Plugin } from "@opencode-ai/plugin";
2
2
  import type { KiroAcpTransportOptions } from "./acp-transport.js";
3
3
  import type { KiroPluginOptions } from "./config.js";
4
4
  import type { KiroTransportOptions } from "./kiro-transport.js";
5
+ type EffectiveBackend = "fetch" | "cli-chat" | "acp" | "none";
6
+ export declare function effectiveBackend(options: Pick<KiroPluginOptions, "backend">, accessToken?: string): EffectiveBackend;
5
7
  export declare function acpTransportOptions(options: Pick<KiroPluginOptions, "requestTimeoutMs" | "trustAllTools">): KiroAcpTransportOptions;
6
8
  export declare function fetchTransportOptions(options: Pick<KiroPluginOptions, "region" | "endpoint" | "profileArn" | "userAgent" | "agentMode" | "maxAttempts" | "requestTimeoutMs">, accessToken: string): KiroTransportOptions;
7
9
  export declare function createKiroPlugin(): Plugin;
8
10
  export declare const KiroPlugin: Plugin;
11
+ export {};
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 } from "./auth.js";
3
+ import { detectAuth, resolveApiKey, startKiroCliLogin } 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";
@@ -48,25 +48,34 @@ function bearerToken(init) {
48
48
  const match = header?.match(/^Bearer\s+(.+)$/i);
49
49
  return match?.[1] || undefined;
50
50
  }
51
- function localTransport(options, accessToken) {
51
+ export function effectiveBackend(options, accessToken) {
52
+ const apiKey = accessToken || process.env.KIRO_API_KEY;
52
53
  if (options.backend === "acp")
54
+ return "acp";
55
+ if (options.backend === "cli-chat")
56
+ return "cli-chat";
57
+ if (options.backend === "fetch")
58
+ return apiKey && apiKey !== "kiro-plugin-local-transport" ? "fetch" : "none";
59
+ if (apiKey && apiKey !== "kiro-plugin-local-transport")
60
+ return "fetch";
61
+ return "cli-chat";
62
+ }
63
+ function localTransport(options, accessToken) {
64
+ const backend = effectiveBackend(options, accessToken);
65
+ if (backend === "acp")
53
66
  return new KiroAcpTransport(acpTransportOptions(options));
54
- if (options.backend === "cli-chat") {
67
+ if (backend === "cli-chat") {
55
68
  return new KiroCliChatTransport({
56
69
  trustAllTools: options.trustAllTools,
57
70
  ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
58
71
  });
59
72
  }
60
- const apiKey = accessToken || process.env.KIRO_API_KEY;
61
- if (apiKey && apiKey !== "kiro-plugin-local-transport") {
73
+ if (backend === "fetch") {
74
+ const apiKey = accessToken || process.env.KIRO_API_KEY;
75
+ if (!apiKey || apiKey === "kiro-plugin-local-transport")
76
+ return undefined;
62
77
  return new CodeWhispererKiroTransport(fetchTransportOptions(options, apiKey));
63
78
  }
64
- if (options.backend === "auto") {
65
- return new KiroCliChatTransport({
66
- trustAllTools: options.trustAllTools,
67
- ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
68
- });
69
- }
70
79
  return undefined;
71
80
  }
72
81
  export function acpTransportOptions(options) {
@@ -159,6 +168,22 @@ export function createKiroPlugin() {
159
168
  auth: {
160
169
  provider: options.providerID,
161
170
  methods: [
171
+ {
172
+ type: "oauth",
173
+ label: "Kiro CLI login",
174
+ authorize: async () => {
175
+ const session = startKiroCliLogin();
176
+ return {
177
+ url: session.url,
178
+ instructions: session.instructions,
179
+ method: "auto",
180
+ callback: async () => {
181
+ const authenticated = await session.waitForAuth();
182
+ return authenticated ? { type: "success", key: "kiro-plugin-local-transport" } : { type: "failed" };
183
+ },
184
+ };
185
+ },
186
+ },
162
187
  {
163
188
  type: "api",
164
189
  label: "Kiro API key",
@@ -200,6 +225,7 @@ export function createKiroPlugin() {
200
225
  output: [
201
226
  `provider: ${options.providerID}`,
202
227
  `backend: ${options.backend}`,
228
+ `effective backend: ${effectiveBackend(options)}`,
203
229
  `region: ${auth.region}`,
204
230
  `auth: ${auth.method}`,
205
231
  `authenticated: ${auth.authenticated ? "yes" : "no"}`,
@@ -209,6 +235,7 @@ export function createKiroPlugin() {
209
235
  metadata: {
210
236
  providerID: options.providerID,
211
237
  backend: options.backend,
238
+ effectiveBackend: effectiveBackend(options),
212
239
  region: auth.region,
213
240
  authMethod: auth.method,
214
241
  authenticated: auth.authenticated,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",