@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 +30 -2
- package/dist/auth.d.ts +11 -2
- package/dist/auth.js +102 -17
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -0
- package/dist/plugin.js +7 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
54
|
-
export declare function
|
|
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
|
|
100
|
-
return /https?:\/\/[^\s"'<>]
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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 (
|
|
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) ||
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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,
|