@bogyie/opencode-kiro-plugin 0.2.8 → 0.2.9

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  OpenCode server plugin that registers Kiro as an OpenAI-compatible provider and adapts requests to Kiro backends.
4
4
 
5
- Status: early implementation. The CodeWhisperer streaming transport, CLI chat fallback, model resolver, multimodal request mapping, streaming text, and tool-call chunk mapping are implemented with unit tests. The ACP backend implements JSON-RPC stdio framing, initialize/session/model/prompt flow, streaming text from `AgentMessageChunk`, and basic `ToolCall` streaming; full ACP tool progress/result parity is still in progress.
5
+ Status: early implementation. The direct Kiro REST/EventStream transport, CLI chat fallback, model resolver, multimodal request mapping, streaming text, and tool-call chunk mapping are implemented with unit tests. The ACP backend implements JSON-RPC stdio framing, initialize/session/model/prompt flow, streaming text from `AgentMessageChunk`, and basic `ToolCall` streaming; full ACP tool progress/result parity is still in progress.
6
6
 
7
7
  ## Install
8
8
 
@@ -35,11 +35,12 @@ The plugin resolves credentials in this order:
35
35
 
36
36
  1. `KIRO_API_KEY`
37
37
  2. OpenCode auth input for provider `kiro`
38
- 3. `kiro-cli whoami` diagnostics for CLI session visibility
38
+ 3. Local Kiro CLI session token from the Kiro CLI SQLite store
39
+ 4. `kiro-cli whoami` diagnostics for CLI session visibility
39
40
 
40
41
  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
 
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.
43
+ 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. `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
44
 
44
45
  Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
45
46
 
@@ -67,8 +68,8 @@ Configure the backend through plugin options:
67
68
 
68
69
  Supported values:
69
70
 
70
- - `auto`: use CodeWhisperer fetch transport when an API key is available; otherwise use streaming `cli-chat` with the local Kiro CLI login.
71
- - `fetch`: require the direct Kiro/CodeWhisperer fetch path. If no usable auth is available, requests fail with a structured backend/auth error.
71
+ - `auto`: use the direct Kiro REST/EventStream fetch transport. It uses `KIRO_API_KEY` or OpenCode auth when present, otherwise the active Kiro CLI session token.
72
+ - `fetch`: require the direct Kiro REST/EventStream fetch path. If no usable auth is available, requests fail with a structured backend/auth error.
72
73
  - `cli-chat`: spawn `kiro-cli chat --no-interactive --model <model>` and stream stdout chunks as they arrive. Chunk granularity is controlled by Kiro CLI.
73
74
  - `acp`: launch `kiro-cli acp`, initialize a session, optionally set the requested model, send the prompt, and collect `AgentMessageChunk` notifications until `TurnEnd`.
74
75
 
@@ -137,9 +138,9 @@ The plugin injects `provider.kiro` automatically. You only need to add `provider
137
138
  - `KIRO_ACP_TIMEOUT`: ACP did not send a `TurnEnd` notification before the prompt timeout.
138
139
  - `KIRO_ACP_PROCESS_ERROR` or `KIRO_ACP_PROCESS_EXITED`: `kiro-cli acp` could not start or exited while a request was pending.
139
140
 
140
- Direct fetch mode uses AWS SDK standard retry behavior. Tune `maxAttempts` and `requestTimeoutMs` if you need stricter failure boundaries in automation. Fetch mode also accepts `endpoint`, `profileArn`, `userAgent`, and `agentMode` for controlled environments. `cli-chat` uses `requestTimeoutMs` for the `kiro-cli chat --no-interactive` child process, and ACP uses it while waiting for `session/prompt` completion and `TurnEnd`.
141
+ Direct fetch mode calls Kiro's `generateAssistantResponse` endpoint and can fall back from the `q` endpoint to the CodeWhisperer endpoint on quota or upstream failures. Tune `maxAttempts` and `requestTimeoutMs` if you need stricter failure boundaries in automation. Fetch mode also accepts `endpoint`, `profileArn`, `userAgent`, and `agentMode` for controlled environments. `cli-chat` uses `requestTimeoutMs` for the `kiro-cli chat --no-interactive` child process, and ACP uses it while waiting for `session/prompt` completion and `TurnEnd`.
141
142
 
142
- OpenAI-compatible `temperature`, `max_tokens`, `max_completion_tokens`, `reasoning_effort`, `reasoning.effort`, and `thinking.effort` are preserved for direct fetch mode through Kiro's `additionalModelRequestFields` path on a best-effort basis.
143
+ OpenAI-compatible `temperature`, `max_tokens`, and `max_completion_tokens` are preserved for direct fetch mode through Kiro's `inferenceConfig` fields on a best-effort basis.
143
144
 
144
145
  When Kiro emits token metadata in direct fetch mode, non-streaming responses map it to OpenAI-compatible `usage` fields.
145
146
  When Kiro emits reasoning text, it is preserved separately from assistant text as `reasoning_content`.
package/dist/auth.d.ts CHANGED
@@ -18,13 +18,29 @@ export interface CommandRunOptions {
18
18
  }
19
19
  export type CommandRunner = (command: string, args: ReadonlyArray<string>, options?: CommandRunOptions) => Promise<CommandResult>;
20
20
  export type ProcessSpawner = (command: string, args: ReadonlyArray<string>) => ChildProcess;
21
+ export interface KiroCliSessionCredential {
22
+ readonly accessToken: string;
23
+ readonly refreshToken?: string;
24
+ readonly expiresAt?: string;
25
+ readonly profileArn?: string;
26
+ readonly region: string;
27
+ readonly source: string;
28
+ }
29
+ export interface KiroCliSessionCredentialOptions {
30
+ readonly dbPath?: string;
31
+ readonly tokenKeys?: ReadonlyArray<string>;
32
+ }
21
33
  export interface KiroLoginSession {
22
34
  readonly url: string;
23
35
  readonly instructions: string;
24
36
  waitForAuth(runner?: CommandRunner): Promise<boolean>;
25
37
  }
26
38
  export declare const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
39
+ export declare const DEFAULT_KIRO_CLI_DB_PATH: string;
40
+ export declare const DEFAULT_KIRO_CLI_TOKEN_KEYS: readonly ["kirocli:odic:token", "codewhisperer:odic:token"];
27
41
  export declare function runCommand(command: string, args: ReadonlyArray<string>, options?: CommandRunOptions): Promise<CommandResult>;
42
+ export declare function regionFromProfileArn(profileArn: string | undefined): string | undefined;
43
+ export declare function readKiroCliSessionCredential(options?: KiroCliSessionCredentialOptions, runner?: CommandRunner): Promise<KiroCliSessionCredential | undefined>;
28
44
  export declare function extractKiroLoginUrl(output: string): string;
29
45
  export declare function startKiroCliLogin(spawner?: ProcessSpawner): KiroLoginSession;
30
46
  export declare function redacted(value: string | undefined): string | undefined;
package/dist/auth.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
3
6
  import { promisify } from "node:util";
4
7
  const execFileAsync = promisify(execFile);
5
8
  export const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
9
+ export const DEFAULT_KIRO_CLI_DB_PATH = join(homedir(), "Library", "Application Support", "kiro-cli", "data.sqlite3");
10
+ export const DEFAULT_KIRO_CLI_TOKEN_KEYS = ["kirocli:odic:token", "codewhisperer:odic:token"];
6
11
  export async function runCommand(command, args, options = {}) {
7
12
  try {
8
13
  const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
@@ -18,6 +23,72 @@ export async function runCommand(command, args, options = {}) {
18
23
  };
19
24
  }
20
25
  }
26
+ function sqlString(value) {
27
+ return `'${value.replaceAll("'", "''")}'`;
28
+ }
29
+ function jsonObject(value) {
30
+ try {
31
+ const parsed = JSON.parse(value);
32
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ function stringField(input, ...keys) {
39
+ for (const key of keys) {
40
+ const value = input?.[key];
41
+ if (typeof value === "string" && value.trim())
42
+ return value.trim();
43
+ }
44
+ return undefined;
45
+ }
46
+ export function regionFromProfileArn(profileArn) {
47
+ const match = /^arn:aws:codewhisperer:([^:]+):/.exec(profileArn?.trim() ?? "");
48
+ return match?.[1];
49
+ }
50
+ async function sqliteValue(dbPath, table, key, runner) {
51
+ if (!existsSync(dbPath))
52
+ return undefined;
53
+ const result = await runner("sqlite3", [dbPath, `select value from ${table} where key=${sqlString(key)} limit 1`], {
54
+ timeoutMs: 5000,
55
+ });
56
+ if (!result.ok)
57
+ return undefined;
58
+ const value = result.stdout.trim();
59
+ return value || undefined;
60
+ }
61
+ function profileArnFromState(value) {
62
+ if (!value)
63
+ return undefined;
64
+ if (value.startsWith("arn:aws:codewhisperer:"))
65
+ return value;
66
+ const parsed = jsonObject(value);
67
+ return stringField(parsed, "profileArn", "arn", "id");
68
+ }
69
+ export async function readKiroCliSessionCredential(options = {}, runner = runCommand) {
70
+ const dbPath = options.dbPath ?? DEFAULT_KIRO_CLI_DB_PATH;
71
+ const tokenKeys = options.tokenKeys ?? DEFAULT_KIRO_CLI_TOKEN_KEYS;
72
+ const profileArn = profileArnFromState(await sqliteValue(dbPath, "state", "api.codewhisperer.profile", runner));
73
+ for (const key of tokenKeys) {
74
+ const raw = await sqliteValue(dbPath, "auth_kv", key, runner);
75
+ const parsed = raw ? jsonObject(raw) : undefined;
76
+ const accessToken = stringField(parsed, "access_token", "accessToken", "token");
77
+ if (!accessToken)
78
+ continue;
79
+ const refreshToken = stringField(parsed, "refresh_token", "refreshToken");
80
+ const expiresAt = stringField(parsed, "expires_at", "expiresAt", "expiration");
81
+ return {
82
+ accessToken,
83
+ ...(refreshToken ? { refreshToken } : {}),
84
+ ...(expiresAt ? { expiresAt } : {}),
85
+ ...(profileArn ? { profileArn } : {}),
86
+ region: regionFromProfileArn(profileArn) ?? "us-east-1",
87
+ source: key,
88
+ };
89
+ }
90
+ return undefined;
91
+ }
21
92
  function delay(ms) {
22
93
  return new Promise((resolve) => setTimeout(resolve, ms));
23
94
  }
package/dist/index.d.ts CHANGED
@@ -3,12 +3,14 @@ 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, redacted, resolveApiKey, startKiroCliLogin } from "./auth.js";
6
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, 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";
10
10
  export { getKiroCliStatus, getKiroCliVersion } from "./kiro-cli.js";
11
11
  export { additionalModelRequestFields, CodeWhispererKiroTransport, collectAssistantText, createCodeWhispererClient, streamAssistantText, toGenerateAssistantResponseInput, usageFromMetadataEvent, } from "./kiro-transport.js";
12
+ export { KiroRestTransport, toKiroRestPayload } from "./kiro-rest-transport.js";
13
+ export type { KiroRestTransportOptions } from "./kiro-rest-transport.js";
12
14
  export { ModelCache } from "./model-cache.js";
13
15
  export { discoverModelsFromCommand, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
14
16
  export { ModelResolutionError, ModelResolver, normalizeModelName } from "./model-resolver.js";
package/dist/index.js CHANGED
@@ -2,12 +2,13 @@ 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, redacted, resolveApiKey, startKiroCliLogin } from "./auth.js";
5
+ export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, 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";
9
9
  export { getKiroCliStatus, getKiroCliVersion } from "./kiro-cli.js";
10
10
  export { additionalModelRequestFields, CodeWhispererKiroTransport, collectAssistantText, createCodeWhispererClient, streamAssistantText, toGenerateAssistantResponseInput, usageFromMetadataEvent, } from "./kiro-transport.js";
11
+ export { KiroRestTransport, toKiroRestPayload } from "./kiro-rest-transport.js";
11
12
  export { ModelCache } from "./model-cache.js";
12
13
  export { discoverModelsFromCommand, parseDiscoveredModels, refreshModelCacheFromCommand } from "./model-discovery.js";
13
14
  export { ModelResolutionError, ModelResolver, normalizeModelName } from "./model-resolver.js";
@@ -0,0 +1,27 @@
1
+ import { type KiroCliSessionCredential } from "./auth.js";
2
+ import type { KiroTransport } from "./fetch-adapter.js";
3
+ import type { KiroGenerateRequest } from "./request-adapter.js";
4
+ import type { KiroGenerateResponse, KiroStreamEvent } from "./response-adapter.js";
5
+ export interface KiroRestTransportOptions {
6
+ readonly region: string;
7
+ readonly accessToken?: string;
8
+ readonly endpoint?: string;
9
+ readonly profileArn?: string;
10
+ readonly userAgent?: string;
11
+ readonly agentMode?: string;
12
+ readonly maxAttempts?: number;
13
+ readonly requestTimeoutMs?: number;
14
+ }
15
+ export type KiroCredentialProvider = () => Promise<KiroCliSessionCredential | undefined>;
16
+ export type KiroRestFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
17
+ export interface KiroRestTransportDependencies {
18
+ readonly fetcher?: KiroRestFetch;
19
+ readonly credentialProvider?: KiroCredentialProvider;
20
+ }
21
+ export declare function toKiroRestPayload(request: KiroGenerateRequest, profileArn?: string): Record<string, unknown>;
22
+ export declare class KiroRestTransport implements KiroTransport {
23
+ #private;
24
+ constructor(options: KiroRestTransportOptions, dependencies?: KiroRestTransportDependencies);
25
+ generate(request: KiroGenerateRequest): Promise<KiroGenerateResponse>;
26
+ stream(request: KiroGenerateRequest): AsyncIterable<KiroStreamEvent>;
27
+ }
@@ -0,0 +1,390 @@
1
+ import { KiroPluginError } from "./errors.js";
2
+ import { readKiroCliSessionCredential, regionFromProfileArn, } from "./auth.js";
3
+ const DEFAULT_USER_AGENT = "KiroIDE";
4
+ const DEFAULT_AGENT_MODE = "vibe";
5
+ const STREAMING_SDK_VERSION = "1.0.34";
6
+ function base64(bytes) {
7
+ return Buffer.from(bytes).toString("base64");
8
+ }
9
+ function conversationId(request) {
10
+ const seed = `${request.modelId}:${request.prompt}:${request.system ?? ""}`;
11
+ let hash = 0;
12
+ for (let i = 0; i < seed.length; i += 1) {
13
+ hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
14
+ }
15
+ return `opencode-${hash.toString(16)}-${Date.now().toString(36)}`;
16
+ }
17
+ export function toKiroRestPayload(request, profileArn) {
18
+ const history = [
19
+ ...(request.system
20
+ ? [
21
+ {
22
+ userInputMessage: {
23
+ content: request.system,
24
+ modelId: request.modelId,
25
+ origin: "AI_EDITOR",
26
+ },
27
+ },
28
+ {
29
+ assistantResponseMessage: {
30
+ content: "I will follow these instructions.",
31
+ },
32
+ },
33
+ ]
34
+ : []),
35
+ ...request.history.map((turn) => turn.role === "assistant"
36
+ ? {
37
+ assistantResponseMessage: {
38
+ content: turn.content,
39
+ },
40
+ }
41
+ : {
42
+ userInputMessage: {
43
+ content: turn.content,
44
+ modelId: request.modelId,
45
+ origin: "AI_EDITOR",
46
+ },
47
+ }),
48
+ ];
49
+ const tools = request.tools.map((item) => ({
50
+ toolSpecification: {
51
+ name: item.name,
52
+ ...(item.description ? { description: item.description } : {}),
53
+ inputSchema: { json: item.inputSchema },
54
+ },
55
+ }));
56
+ const toolResults = request.toolResults.map((item) => ({
57
+ toolUseId: item.toolUseId,
58
+ content: [{ text: item.content }],
59
+ status: "success",
60
+ ...(item.toolName ? { toolName: item.toolName } : {}),
61
+ }));
62
+ const userInputMessageContext = tools.length > 0 || toolResults.length > 0
63
+ ? {
64
+ ...(tools.length > 0 ? { tools } : {}),
65
+ ...(toolResults.length > 0 ? { toolResults } : {}),
66
+ }
67
+ : undefined;
68
+ const inferenceConfig = request.modelOptions.maxTokens !== undefined || request.modelOptions.temperature !== undefined
69
+ ? {
70
+ ...(request.modelOptions.maxTokens !== undefined ? { maxTokens: request.modelOptions.maxTokens } : {}),
71
+ ...(request.modelOptions.temperature !== undefined ? { temperature: request.modelOptions.temperature } : {}),
72
+ }
73
+ : undefined;
74
+ return {
75
+ conversationState: {
76
+ chatTriggerType: "MANUAL",
77
+ conversationId: conversationId(request),
78
+ currentMessage: {
79
+ userInputMessage: {
80
+ content: request.prompt || "Continue.",
81
+ modelId: request.modelId,
82
+ origin: "AI_EDITOR",
83
+ ...(request.images.length > 0
84
+ ? {
85
+ images: request.images.map((image) => ({
86
+ format: image.format,
87
+ source: { bytes: base64(image.bytes) },
88
+ })),
89
+ }
90
+ : {}),
91
+ ...(userInputMessageContext ? { userInputMessageContext } : {}),
92
+ },
93
+ },
94
+ ...(history.length > 0 ? { history } : {}),
95
+ },
96
+ ...(profileArn ? { profileArn } : {}),
97
+ ...(inferenceConfig ? { inferenceConfig } : {}),
98
+ };
99
+ }
100
+ function modelUserAgent(userAgent) {
101
+ if (userAgent !== DEFAULT_USER_AGENT)
102
+ return userAgent;
103
+ const nodeVersion = process.version.replace(/^v/, "");
104
+ const platform = process.platform === "darwin" ? "darwin" : process.platform;
105
+ return `aws-sdk-js/${STREAMING_SDK_VERSION} ua/2.1 os/${platform} lang/js md/nodejs#${nodeVersion} api/codewhispererstreaming#${STREAMING_SDK_VERSION} m/E ${DEFAULT_USER_AGENT}`;
106
+ }
107
+ function endpointCandidates(options, region) {
108
+ if (options.endpoint) {
109
+ return [options.endpoint.endsWith("/generateAssistantResponse") ? options.endpoint : `${options.endpoint.replace(/\/+$/, "")}/generateAssistantResponse`];
110
+ }
111
+ const qEndpoint = `https://q.${region}.amazonaws.com/generateAssistantResponse`;
112
+ if (region !== "us-east-1")
113
+ return [qEndpoint];
114
+ return [qEndpoint, "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse"];
115
+ }
116
+ function headersFor(endpoint, accessToken, options) {
117
+ const headers = new Headers({
118
+ authorization: `Bearer ${accessToken}`,
119
+ "content-type": "application/json",
120
+ accept: "*/*",
121
+ "user-agent": modelUserAgent(options.userAgent ?? DEFAULT_USER_AGENT),
122
+ "x-amz-user-agent": `aws-sdk-js/${STREAMING_SDK_VERSION} ${options.userAgent ?? DEFAULT_USER_AGENT}`,
123
+ "x-amzn-kiro-agent-mode": options.agentMode ?? DEFAULT_AGENT_MODE,
124
+ "x-amzn-codewhisperer-optout": "true",
125
+ "amz-sdk-request": `attempt=1; max=${options.maxAttempts ?? 3}`,
126
+ "amz-sdk-invocation-id": crypto.randomUUID(),
127
+ });
128
+ if (new URL(endpoint).hostname.startsWith("codewhisperer.")) {
129
+ headers.set("x-amz-target", "AmazonCodeWhispererStreamingService.GenerateAssistantResponse");
130
+ }
131
+ return headers;
132
+ }
133
+ function timeoutSignal(timeoutMs) {
134
+ if (!timeoutMs)
135
+ return undefined;
136
+ return AbortSignal.timeout(timeoutMs);
137
+ }
138
+ function stringValue(value) {
139
+ return typeof value === "string" ? value : undefined;
140
+ }
141
+ function numberValue(value) {
142
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
143
+ }
144
+ function tokenUsage(payload) {
145
+ const usage = payload.tokenUsage;
146
+ if (!usage || typeof usage !== "object" || Array.isArray(usage))
147
+ return undefined;
148
+ const record = usage;
149
+ const uncached = numberValue(record.uncachedInputTokens) ?? 0;
150
+ const cacheRead = numberValue(record.cacheReadInputTokens) ?? 0;
151
+ const cacheWrite = numberValue(record.cacheWriteInputTokens) ?? 0;
152
+ const outputTokens = numberValue(record.outputTokens);
153
+ const inputTokens = numberValue(record.inputTokens) ?? (uncached + cacheRead + cacheWrite || undefined);
154
+ return inputTokens || outputTokens
155
+ ? {
156
+ ...(inputTokens ? { inputTokens } : {}),
157
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
158
+ }
159
+ : undefined;
160
+ }
161
+ function parseHeaders(buffer) {
162
+ const headers = {};
163
+ let offset = 0;
164
+ while (offset < buffer.length) {
165
+ const nameLength = buffer[offset];
166
+ if (nameLength === undefined)
167
+ break;
168
+ offset += 1;
169
+ const name = buffer.subarray(offset, offset + nameLength).toString("utf8");
170
+ offset += nameLength;
171
+ const type = buffer[offset];
172
+ offset += 1;
173
+ if (type === 7) {
174
+ const valueLength = buffer.readUInt16BE(offset);
175
+ offset += 2;
176
+ headers[name] = buffer.subarray(offset, offset + valueLength).toString("utf8");
177
+ offset += valueLength;
178
+ continue;
179
+ }
180
+ if (type === 6) {
181
+ const valueLength = buffer.readUInt16BE(offset);
182
+ offset += 2;
183
+ headers[name] = buffer.subarray(offset, offset + valueLength);
184
+ offset += valueLength;
185
+ continue;
186
+ }
187
+ if (type === 0 || type === 1) {
188
+ headers[name] = type === 0;
189
+ continue;
190
+ }
191
+ if (type === 2) {
192
+ headers[name] = buffer.readInt8(offset);
193
+ offset += 1;
194
+ continue;
195
+ }
196
+ if (type === 3) {
197
+ headers[name] = buffer.readInt16BE(offset);
198
+ offset += 2;
199
+ continue;
200
+ }
201
+ if (type === 4) {
202
+ headers[name] = buffer.readInt32BE(offset);
203
+ offset += 4;
204
+ continue;
205
+ }
206
+ if (type === 5) {
207
+ headers[name] = Number(buffer.readBigInt64BE(offset));
208
+ offset += 8;
209
+ continue;
210
+ }
211
+ break;
212
+ }
213
+ return headers;
214
+ }
215
+ async function* decodeEventStream(body) {
216
+ const reader = body.getReader();
217
+ let buffer = Buffer.alloc(0);
218
+ try {
219
+ while (true) {
220
+ const item = await reader.read();
221
+ if (item.done)
222
+ break;
223
+ buffer = Buffer.concat([buffer, Buffer.from(item.value)]);
224
+ while (buffer.length >= 16) {
225
+ const totalLength = buffer.readUInt32BE(0);
226
+ const headersLength = buffer.readUInt32BE(4);
227
+ if (totalLength < 16)
228
+ throw new KiroPluginError("Kiro returned an invalid event stream frame.", "KIRO_STREAM_ERROR", 502);
229
+ if (buffer.length < totalLength)
230
+ break;
231
+ const frame = buffer.subarray(0, totalLength);
232
+ buffer = buffer.subarray(totalLength);
233
+ const headers = parseHeaders(frame.subarray(12, 12 + headersLength));
234
+ const payloadBytes = frame.subarray(12 + headersLength, totalLength - 4);
235
+ if (payloadBytes.length === 0)
236
+ continue;
237
+ const payload = JSON.parse(payloadBytes.toString("utf8"));
238
+ yield {
239
+ eventType: stringValue(headers[":event-type"]) ?? "unknown",
240
+ payload,
241
+ };
242
+ }
243
+ }
244
+ }
245
+ finally {
246
+ reader.releaseLock();
247
+ }
248
+ }
249
+ function normalizeChunk(chunk, previous) {
250
+ const old = previous.value;
251
+ if (!old) {
252
+ previous.value = chunk;
253
+ return chunk;
254
+ }
255
+ if (chunk === old || old.startsWith(chunk))
256
+ return "";
257
+ if (chunk.startsWith(old)) {
258
+ previous.value = chunk;
259
+ return chunk.slice(old.length);
260
+ }
261
+ previous.value = chunk;
262
+ return chunk;
263
+ }
264
+ async function collectChunks(chunks, fallbackModelId) {
265
+ let text = "";
266
+ let reasoning = "";
267
+ let usage;
268
+ const toolCalls = [];
269
+ for await (const chunk of chunks) {
270
+ if (chunk.type === "tool_call") {
271
+ toolCalls.push(chunk);
272
+ continue;
273
+ }
274
+ if (chunk.type === "reasoning") {
275
+ reasoning += chunk.text;
276
+ continue;
277
+ }
278
+ text += chunk.text;
279
+ if (chunk.usage)
280
+ usage = chunk.usage;
281
+ }
282
+ return {
283
+ text,
284
+ modelId: fallbackModelId,
285
+ ...(reasoning ? { reasoning } : {}),
286
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
287
+ ...(usage ? { usage } : {}),
288
+ };
289
+ }
290
+ export class KiroRestTransport {
291
+ #options;
292
+ #fetcher;
293
+ #credentialProvider;
294
+ constructor(options, dependencies = {}) {
295
+ this.#options = options;
296
+ this.#fetcher = dependencies.fetcher ?? fetch;
297
+ this.#credentialProvider = dependencies.credentialProvider ?? (() => readKiroCliSessionCredential());
298
+ }
299
+ async #credentials() {
300
+ const session = this.#options.accessToken ? undefined : await this.#credentialProvider();
301
+ const accessToken = this.#options.accessToken ?? session?.accessToken;
302
+ if (!accessToken) {
303
+ throw new KiroPluginError("No Kiro API token or Kiro CLI session token found. Run Kiro login and try again.", "KIRO_AUTH_ERROR", 401);
304
+ }
305
+ const profileArn = this.#options.profileArn ?? session?.profileArn;
306
+ return {
307
+ accessToken,
308
+ ...(profileArn ? { profileArn } : {}),
309
+ region: regionFromProfileArn(profileArn) ?? session?.region ?? this.#options.region,
310
+ };
311
+ }
312
+ async generate(request) {
313
+ return collectChunks(this.stream(request), request.modelId);
314
+ }
315
+ async *stream(request) {
316
+ const credentials = await this.#credentials();
317
+ const payload = toKiroRestPayload(request, credentials.profileArn);
318
+ let lastError;
319
+ for (const endpoint of endpointCandidates(this.#options, credentials.region)) {
320
+ const init = {
321
+ method: "POST",
322
+ headers: headersFor(endpoint, credentials.accessToken, this.#options),
323
+ body: JSON.stringify(payload),
324
+ };
325
+ const signal = timeoutSignal(this.#options.requestTimeoutMs);
326
+ if (signal)
327
+ init.signal = signal;
328
+ const response = await this.#fetcher(endpoint, init);
329
+ if (!response.ok) {
330
+ const errorBody = await response.text();
331
+ const message = errorBody || `Kiro API returned HTTP ${response.status}`;
332
+ const code = response.status === 401 || response.status === 403 ? "KIRO_AUTH_ERROR" : response.status === 429 ? "KIRO_RATE_LIMIT" : "KIRO_UPSTREAM_ERROR";
333
+ lastError = new KiroPluginError(message, code, response.status);
334
+ if (response.status === 429 || response.status >= 500)
335
+ continue;
336
+ throw lastError;
337
+ }
338
+ if (!response.body)
339
+ return;
340
+ const assistant = { value: "" };
341
+ const reasoning = { value: "" };
342
+ const toolInputs = new Map();
343
+ for await (const event of decodeEventStream(response.body)) {
344
+ if (event.eventType === "assistantResponseEvent") {
345
+ const content = stringValue(event.payload.content);
346
+ if (!content)
347
+ continue;
348
+ const text = normalizeChunk(content, assistant);
349
+ if (text)
350
+ yield { type: "text", text, modelId: request.modelId };
351
+ continue;
352
+ }
353
+ if (event.eventType === "reasoningContentEvent") {
354
+ const content = stringValue(event.payload.text);
355
+ if (!content)
356
+ continue;
357
+ const text = normalizeChunk(content, reasoning);
358
+ if (text)
359
+ yield { type: "reasoning", text, modelId: request.modelId };
360
+ continue;
361
+ }
362
+ if (event.eventType === "toolUseEvent") {
363
+ const id = stringValue(event.payload.toolUseId) ?? stringValue(event.payload.id) ?? crypto.randomUUID();
364
+ const name = stringValue(event.payload.name) ?? stringValue(event.payload.toolName);
365
+ if (!name)
366
+ continue;
367
+ const current = toolInputs.get(id) ?? { name, input: "" };
368
+ const input = event.payload.input;
369
+ current.input += typeof input === "string" ? input : input && typeof input === "object" ? JSON.stringify(input) : "";
370
+ current.name = name;
371
+ toolInputs.set(id, current);
372
+ if (event.payload.stop === true) {
373
+ yield { type: "tool_call", id, name: current.name, arguments: current.input, modelId: request.modelId };
374
+ toolInputs.delete(id);
375
+ }
376
+ continue;
377
+ }
378
+ const usage = tokenUsage(event.payload);
379
+ if (usage)
380
+ yield { type: "text", text: "", usage, modelId: request.modelId };
381
+ }
382
+ for (const [id, toolCall] of toolInputs) {
383
+ yield { type: "tool_call", id, name: toolCall.name, arguments: toolCall.input, modelId: request.modelId };
384
+ }
385
+ return;
386
+ }
387
+ if (lastError)
388
+ throw lastError;
389
+ }
390
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  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
- import type { KiroTransportOptions } from "./kiro-transport.js";
4
+ import type { KiroRestTransportOptions } from "./kiro-rest-transport.js";
5
5
  type EffectiveBackend = "fetch" | "cli-chat" | "acp" | "none";
6
6
  export declare function effectiveBackend(options: Pick<KiroPluginOptions, "backend">, accessToken?: string): EffectiveBackend;
7
7
  export declare function acpTransportOptions(options: Pick<KiroPluginOptions, "requestTimeoutMs" | "trustAllTools">): KiroAcpTransportOptions;
8
- export declare function fetchTransportOptions(options: Pick<KiroPluginOptions, "region" | "endpoint" | "profileArn" | "userAgent" | "agentMode" | "maxAttempts" | "requestTimeoutMs">, accessToken: string): KiroTransportOptions;
8
+ export declare function fetchTransportOptions(options: Pick<KiroPluginOptions, "region" | "endpoint" | "profileArn" | "userAgent" | "agentMode" | "maxAttempts" | "requestTimeoutMs">, accessToken?: string): KiroRestTransportOptions;
9
9
  export declare function createKiroPlugin(): Plugin;
10
10
  export declare const KiroPlugin: Plugin;
11
11
  export {};
package/dist/plugin.js CHANGED
@@ -8,7 +8,7 @@ import { startLocalKiroServer } from "./local-server.js";
8
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
- import { CodeWhispererKiroTransport } from "./kiro-transport.js";
11
+ import { KiroRestTransport } from "./kiro-rest-transport.js";
12
12
  function discoveredProviderModels(cache) {
13
13
  return Object.fromEntries(cache.all().map((model) => [
14
14
  model.id,
@@ -55,10 +55,10 @@ export function effectiveBackend(options, accessToken) {
55
55
  if (options.backend === "cli-chat")
56
56
  return "cli-chat";
57
57
  if (options.backend === "fetch")
58
- return apiKey && apiKey !== "kiro-plugin-local-transport" ? "fetch" : "none";
58
+ return "fetch";
59
59
  if (apiKey && apiKey !== "kiro-plugin-local-transport")
60
60
  return "fetch";
61
- return "cli-chat";
61
+ return "fetch";
62
62
  }
63
63
  function localTransport(options, accessToken) {
64
64
  const backend = effectiveBackend(options, accessToken);
@@ -72,9 +72,7 @@ function localTransport(options, accessToken) {
72
72
  }
73
73
  if (backend === "fetch") {
74
74
  const apiKey = accessToken || process.env.KIRO_API_KEY;
75
- if (!apiKey || apiKey === "kiro-plugin-local-transport")
76
- return undefined;
77
- return new CodeWhispererKiroTransport(fetchTransportOptions(options, apiKey));
75
+ return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey));
78
76
  }
79
77
  return undefined;
80
78
  }
@@ -87,7 +85,7 @@ export function acpTransportOptions(options) {
87
85
  export function fetchTransportOptions(options, accessToken) {
88
86
  return {
89
87
  region: options.region,
90
- accessToken,
88
+ ...(accessToken ? { accessToken } : {}),
91
89
  maxAttempts: options.maxAttempts,
92
90
  ...(options.endpoint ? { endpoint: options.endpoint } : {}),
93
91
  ...(options.profileArn ? { profileArn: options.profileArn } : {}),
@@ -16,7 +16,7 @@ npm run build
16
16
 
17
17
  ## Backend: fetch
18
18
 
19
- Use this when you have a token/API key that works with the CodeWhisperer/Kiro transport.
19
+ Use this for the default direct Kiro REST/EventStream path. It can use `KIRO_API_KEY`, OpenCode auth, or the active Kiro CLI session token from the local Kiro CLI SQLite store.
20
20
 
21
21
  Config:
22
22
 
@@ -41,7 +41,9 @@ Config:
41
41
  Environment:
42
42
 
43
43
  ```sh
44
- export KIRO_API_KEY="..."
44
+ kiro-cli whoami
45
+ # Optional explicit token override:
46
+ # export KIRO_API_KEY="..."
45
47
  ```
46
48
 
47
49
  Checks:
@@ -49,7 +51,7 @@ Checks:
49
51
  - Text prompt returns a normal assistant response.
50
52
  - `stream: true` prompt renders incrementally.
51
53
  - A prompt that triggers tool use returns OpenAI-compatible `tool_calls` in both streaming and non-streaming modes.
52
- - A data URL image or PDF prompt reaches Kiro without external URL fetching.
54
+ - A data URL image prompt reaches Kiro without external URL fetching.
53
55
  - Invalid/expired auth returns `KIRO_AUTH_ERROR`, not a raw SDK exception.
54
56
  - Rate/quota failure returns `KIRO_RATE_LIMIT`.
55
57
  - Lowering `requestTimeoutMs` produces `KIRO_TIMEOUT`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",