@zhafron/opencode-kiro-auth 1.1.2 → 1.2.0
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 +62 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/kiro/auth.d.ts +1 -1
- package/dist/kiro/auth.js +2 -3
- package/dist/plugin/accounts.d.ts +2 -0
- package/dist/plugin/accounts.js +19 -1
- package/dist/plugin/config/loader.js +8 -6
- package/dist/plugin/config/schema.d.ts +18 -15
- package/dist/plugin/config/schema.js +12 -10
- package/dist/plugin/server.d.ts +1 -1
- package/dist/plugin/server.js +32 -2
- package/dist/plugin/storage.js +20 -14
- package/dist/plugin.js +52 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to the latest Clau
|
|
|
9
9
|
- Automated token refresh and rate limit handling with exponential backoff.
|
|
10
10
|
- Native thinking mode support via virtual model mappings.
|
|
11
11
|
- Decoupled storage for credentials and real-time usage metadata.
|
|
12
|
+
- Configurable request timeout and iteration limits to prevent hangs.
|
|
13
|
+
- Automatic port selection for auth server to avoid conflicts.
|
|
14
|
+
- Usage tracking with automatic retry on sync failures.
|
|
12
15
|
|
|
13
16
|
## Installation
|
|
14
17
|
|
|
@@ -68,12 +71,71 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
|
68
71
|
3. Follow the terminal instructions to complete the AWS Builder ID authentication.
|
|
69
72
|
4. Configuration template will be automatically created at `~/.config/opencode/kiro.json` on first load.
|
|
70
73
|
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
The plugin supports extensive configuration options. Edit `~/.config/opencode/kiro.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"account_selection_strategy": "lowest-usage",
|
|
81
|
+
"default_region": "us-east-1",
|
|
82
|
+
"rate_limit_retry_delay_ms": 5000,
|
|
83
|
+
"rate_limit_max_retries": 3,
|
|
84
|
+
"max_request_iterations": 100,
|
|
85
|
+
"request_timeout_ms": 300000,
|
|
86
|
+
"token_expiry_buffer_ms": 120000,
|
|
87
|
+
"usage_sync_max_retries": 3,
|
|
88
|
+
"auth_server_port_start": 19847,
|
|
89
|
+
"auth_server_port_range": 10,
|
|
90
|
+
"usage_tracking_enabled": true,
|
|
91
|
+
"enable_log_api_request": false
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Configuration Options
|
|
96
|
+
|
|
97
|
+
- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`)
|
|
98
|
+
- `default_region`: AWS region (`us-east-1`, `us-west-2`)
|
|
99
|
+
- `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms)
|
|
100
|
+
- `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10)
|
|
101
|
+
- `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000)
|
|
102
|
+
- `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms)
|
|
103
|
+
- `token_expiry_buffer_ms`: Token refresh buffer time (30000-300000ms)
|
|
104
|
+
- `usage_sync_max_retries`: Retry attempts for usage sync (0-5)
|
|
105
|
+
- `auth_server_port_start`: Starting port for auth server (1024-65535)
|
|
106
|
+
- `auth_server_port_range`: Number of ports to try (1-100)
|
|
107
|
+
- `usage_tracking_enabled`: Enable usage tracking and toast notifications
|
|
108
|
+
- `enable_log_api_request`: Enable detailed API request logging
|
|
109
|
+
|
|
110
|
+
### Environment Variables
|
|
111
|
+
|
|
112
|
+
All configuration options can be overridden via environment variables:
|
|
113
|
+
|
|
114
|
+
- `KIRO_ACCOUNT_SELECTION_STRATEGY`
|
|
115
|
+
- `KIRO_DEFAULT_REGION`
|
|
116
|
+
- `KIRO_RATE_LIMIT_RETRY_DELAY_MS`
|
|
117
|
+
- `KIRO_RATE_LIMIT_MAX_RETRIES`
|
|
118
|
+
- `KIRO_MAX_REQUEST_ITERATIONS`
|
|
119
|
+
- `KIRO_REQUEST_TIMEOUT_MS`
|
|
120
|
+
- `KIRO_TOKEN_EXPIRY_BUFFER_MS`
|
|
121
|
+
- `KIRO_USAGE_SYNC_MAX_RETRIES`
|
|
122
|
+
- `KIRO_AUTH_SERVER_PORT_START`
|
|
123
|
+
- `KIRO_AUTH_SERVER_PORT_RANGE`
|
|
124
|
+
- `KIRO_USAGE_TRACKING_ENABLED`
|
|
125
|
+
- `KIRO_ENABLE_LOG_API_REQUEST`
|
|
126
|
+
|
|
71
127
|
## Storage
|
|
72
128
|
|
|
129
|
+
**Linux/macOS:**
|
|
73
130
|
- Credentials: `~/.config/opencode/kiro-accounts.json`
|
|
74
131
|
- Usage Tracking: `~/.config/opencode/kiro-usage.json`
|
|
75
132
|
- Plugin Config: `~/.config/opencode/kiro.json`
|
|
76
133
|
|
|
134
|
+
**Windows:**
|
|
135
|
+
- Credentials: `%APPDATA%\opencode\kiro-accounts.json`
|
|
136
|
+
- Usage Tracking: `%APPDATA%\opencode\kiro-usage.json`
|
|
137
|
+
- Plugin Config: `%APPDATA%\opencode\kiro.json`
|
|
138
|
+
|
|
77
139
|
## Acknowledgements
|
|
78
140
|
|
|
79
141
|
Special thanks to [AIClient-2-API](https://github.com/justlovemaki/AIClient-2-API) for providing the foundational Kiro authentication logic and request patterns.
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -33,7 +33,6 @@ export const KIRO_CONSTANTS = {
|
|
|
33
33
|
BASE_URL: 'https://q.{{region}}.amazonaws.com/generateAssistantResponse',
|
|
34
34
|
USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits',
|
|
35
35
|
DEFAULT_REGION: 'us-east-1',
|
|
36
|
-
ACCESS_TOKEN_EXPIRY_BUFFER_MS: 60000,
|
|
37
36
|
AXIOS_TIMEOUT: 120000,
|
|
38
37
|
USER_AGENT: 'KiroIDE',
|
|
39
38
|
KIRO_VERSION: '0.7.5',
|
package/dist/kiro/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KiroAuthDetails, RefreshParts } from '../plugin/types';
|
|
2
2
|
export declare function decodeRefreshToken(refresh: string): RefreshParts;
|
|
3
|
-
export declare function accessTokenExpired(auth: KiroAuthDetails): boolean;
|
|
3
|
+
export declare function accessTokenExpired(auth: KiroAuthDetails, bufferMs?: number): boolean;
|
|
4
4
|
export declare function validateAuthDetails(auth: KiroAuthDetails): boolean;
|
|
5
5
|
export declare function encodeRefreshToken(parts: RefreshParts): string;
|
package/dist/kiro/auth.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { KIRO_CONSTANTS } from '../constants';
|
|
2
1
|
export function decodeRefreshToken(refresh) {
|
|
3
2
|
const parts = refresh.split('|');
|
|
4
3
|
if (parts.length < 2)
|
|
@@ -9,10 +8,10 @@ export function decodeRefreshToken(refresh) {
|
|
|
9
8
|
return { refreshToken, clientId: parts[1], clientSecret: parts[2], authMethod: 'idc' };
|
|
10
9
|
return { refreshToken, authMethod: 'idc' };
|
|
11
10
|
}
|
|
12
|
-
export function accessTokenExpired(auth) {
|
|
11
|
+
export function accessTokenExpired(auth, bufferMs = 120000) {
|
|
13
12
|
if (!auth.access || !auth.expires)
|
|
14
13
|
return true;
|
|
15
|
-
return Date.now() >= auth.expires -
|
|
14
|
+
return Date.now() >= auth.expires - bufferMs;
|
|
16
15
|
}
|
|
17
16
|
export function validateAuthDetails(auth) {
|
|
18
17
|
return !!auth.refresh && auth.authMethod === 'idc' && !!auth.clientId && !!auth.clientSecret;
|
|
@@ -6,11 +6,13 @@ export declare class AccountManager {
|
|
|
6
6
|
private cursor;
|
|
7
7
|
private strategy;
|
|
8
8
|
private lastToastTime;
|
|
9
|
+
private lastUsageToastTime;
|
|
9
10
|
constructor(accounts: ManagedAccount[], usage: Record<string, UsageMetadata>, strategy?: AccountSelectionStrategy);
|
|
10
11
|
static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
|
|
11
12
|
getAccountCount(): number;
|
|
12
13
|
getAccounts(): ManagedAccount[];
|
|
13
14
|
shouldShowToast(debounce?: number): boolean;
|
|
15
|
+
shouldShowUsageToast(debounce?: number): boolean;
|
|
14
16
|
getMinWaitTime(): number;
|
|
15
17
|
getCurrentOrNext(): ManagedAccount | null;
|
|
16
18
|
updateUsage(id: string, meta: {
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -11,6 +11,7 @@ export class AccountManager {
|
|
|
11
11
|
cursor;
|
|
12
12
|
strategy;
|
|
13
13
|
lastToastTime = 0;
|
|
14
|
+
lastUsageToastTime = 0;
|
|
14
15
|
constructor(accounts, usage, strategy = 'sticky') {
|
|
15
16
|
this.accounts = accounts;
|
|
16
17
|
this.usage = usage;
|
|
@@ -46,6 +47,12 @@ export class AccountManager {
|
|
|
46
47
|
this.lastToastTime = Date.now();
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
50
|
+
shouldShowUsageToast(debounce = 30000) {
|
|
51
|
+
if (Date.now() - this.lastUsageToastTime < debounce)
|
|
52
|
+
return false;
|
|
53
|
+
this.lastUsageToastTime = Date.now();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
49
56
|
getMinWaitTime() {
|
|
50
57
|
const now = Date.now();
|
|
51
58
|
const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
|
|
@@ -104,9 +111,20 @@ export class AccountManager {
|
|
|
104
111
|
this.accounts[i] = a;
|
|
105
112
|
}
|
|
106
113
|
removeAccount(a) {
|
|
114
|
+
const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
|
|
115
|
+
if (removedIndex === -1)
|
|
116
|
+
return;
|
|
107
117
|
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
108
118
|
delete this.usage[a.id];
|
|
109
|
-
|
|
119
|
+
if (this.accounts.length === 0) {
|
|
120
|
+
this.cursor = 0;
|
|
121
|
+
}
|
|
122
|
+
else if (this.cursor >= this.accounts.length) {
|
|
123
|
+
this.cursor = this.accounts.length - 1;
|
|
124
|
+
}
|
|
125
|
+
else if (removedIndex <= this.cursor && this.cursor > 0) {
|
|
126
|
+
this.cursor--;
|
|
127
|
+
}
|
|
110
128
|
}
|
|
111
129
|
updateFromAuth(a, auth) {
|
|
112
130
|
const acc = this.accounts.find((x) => x.id === a.id);
|
|
@@ -87,11 +87,6 @@ function applyEnvOverrides(config) {
|
|
|
87
87
|
const env = process.env;
|
|
88
88
|
return {
|
|
89
89
|
...config,
|
|
90
|
-
session_recovery: parseBooleanEnv(env.KIRO_SESSION_RECOVERY, config.session_recovery),
|
|
91
|
-
auto_resume: parseBooleanEnv(env.KIRO_AUTO_RESUME, config.auto_resume),
|
|
92
|
-
proactive_token_refresh: parseBooleanEnv(env.KIRO_PROACTIVE_TOKEN_REFRESH, config.proactive_token_refresh),
|
|
93
|
-
token_refresh_interval_seconds: parseNumberEnv(env.KIRO_TOKEN_REFRESH_INTERVAL_SECONDS, config.token_refresh_interval_seconds),
|
|
94
|
-
token_refresh_buffer_seconds: parseNumberEnv(env.KIRO_TOKEN_REFRESH_BUFFER_SECONDS, config.token_refresh_buffer_seconds),
|
|
95
90
|
account_selection_strategy: env.KIRO_ACCOUNT_SELECTION_STRATEGY
|
|
96
91
|
? AccountSelectionStrategySchema.catch('lowest-usage').parse(env.KIRO_ACCOUNT_SELECTION_STRATEGY)
|
|
97
92
|
: config.account_selection_strategy,
|
|
@@ -100,7 +95,14 @@ function applyEnvOverrides(config) {
|
|
|
100
95
|
: config.default_region,
|
|
101
96
|
rate_limit_retry_delay_ms: parseNumberEnv(env.KIRO_RATE_LIMIT_RETRY_DELAY_MS, config.rate_limit_retry_delay_ms),
|
|
102
97
|
rate_limit_max_retries: parseNumberEnv(env.KIRO_RATE_LIMIT_MAX_RETRIES, config.rate_limit_max_retries),
|
|
103
|
-
|
|
98
|
+
max_request_iterations: parseNumberEnv(env.KIRO_MAX_REQUEST_ITERATIONS, config.max_request_iterations),
|
|
99
|
+
request_timeout_ms: parseNumberEnv(env.KIRO_REQUEST_TIMEOUT_MS, config.request_timeout_ms),
|
|
100
|
+
token_expiry_buffer_ms: parseNumberEnv(env.KIRO_TOKEN_EXPIRY_BUFFER_MS, config.token_expiry_buffer_ms),
|
|
101
|
+
usage_sync_max_retries: parseNumberEnv(env.KIRO_USAGE_SYNC_MAX_RETRIES, config.usage_sync_max_retries),
|
|
102
|
+
auth_server_port_start: parseNumberEnv(env.KIRO_AUTH_SERVER_PORT_START, config.auth_server_port_start),
|
|
103
|
+
auth_server_port_range: parseNumberEnv(env.KIRO_AUTH_SERVER_PORT_RANGE, config.auth_server_port_range),
|
|
104
|
+
usage_tracking_enabled: parseBooleanEnv(env.KIRO_USAGE_TRACKING_ENABLED, config.usage_tracking_enabled),
|
|
105
|
+
enable_log_api_request: parseBooleanEnv(env.KIRO_ENABLE_LOG_API_REQUEST, config.enable_log_api_request)
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
export function loadConfig(directory) {
|
|
@@ -5,41 +5,44 @@ export declare const RegionSchema: z.ZodEnum<["us-east-1", "us-west-2"]>;
|
|
|
5
5
|
export type Region = z.infer<typeof RegionSchema>;
|
|
6
6
|
export declare const KiroConfigSchema: z.ZodObject<{
|
|
7
7
|
$schema: z.ZodOptional<z.ZodString>;
|
|
8
|
-
session_recovery: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
-
auto_resume: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
-
proactive_token_refresh: z.ZodDefault<z.ZodBoolean>;
|
|
11
|
-
token_refresh_interval_seconds: z.ZodDefault<z.ZodNumber>;
|
|
12
|
-
token_refresh_buffer_seconds: z.ZodDefault<z.ZodNumber>;
|
|
13
8
|
account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
|
|
14
9
|
default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-west-2"]>>;
|
|
15
10
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
16
11
|
rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
max_request_iterations: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
request_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
token_expiry_buffer_ms: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
usage_sync_max_retries: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
auth_server_port_start: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
auth_server_port_range: z.ZodDefault<z.ZodNumber>;
|
|
17
18
|
usage_tracking_enabled: z.ZodDefault<z.ZodBoolean>;
|
|
18
19
|
enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
|
|
19
20
|
}, "strip", z.ZodTypeAny, {
|
|
20
|
-
session_recovery: boolean;
|
|
21
|
-
auto_resume: boolean;
|
|
22
|
-
proactive_token_refresh: boolean;
|
|
23
|
-
token_refresh_interval_seconds: number;
|
|
24
|
-
token_refresh_buffer_seconds: number;
|
|
25
21
|
account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
|
|
26
22
|
default_region: "us-east-1" | "us-west-2";
|
|
27
23
|
rate_limit_retry_delay_ms: number;
|
|
28
24
|
rate_limit_max_retries: number;
|
|
25
|
+
max_request_iterations: number;
|
|
26
|
+
request_timeout_ms: number;
|
|
27
|
+
token_expiry_buffer_ms: number;
|
|
28
|
+
usage_sync_max_retries: number;
|
|
29
|
+
auth_server_port_start: number;
|
|
30
|
+
auth_server_port_range: number;
|
|
29
31
|
usage_tracking_enabled: boolean;
|
|
30
32
|
enable_log_api_request: boolean;
|
|
31
33
|
$schema?: string | undefined;
|
|
32
34
|
}, {
|
|
33
35
|
$schema?: string | undefined;
|
|
34
|
-
session_recovery?: boolean | undefined;
|
|
35
|
-
auto_resume?: boolean | undefined;
|
|
36
|
-
proactive_token_refresh?: boolean | undefined;
|
|
37
|
-
token_refresh_interval_seconds?: number | undefined;
|
|
38
|
-
token_refresh_buffer_seconds?: number | undefined;
|
|
39
36
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
40
37
|
default_region?: "us-east-1" | "us-west-2" | undefined;
|
|
41
38
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
42
39
|
rate_limit_max_retries?: number | undefined;
|
|
40
|
+
max_request_iterations?: number | undefined;
|
|
41
|
+
request_timeout_ms?: number | undefined;
|
|
42
|
+
token_expiry_buffer_ms?: number | undefined;
|
|
43
|
+
usage_sync_max_retries?: number | undefined;
|
|
44
|
+
auth_server_port_start?: number | undefined;
|
|
45
|
+
auth_server_port_range?: number | undefined;
|
|
43
46
|
usage_tracking_enabled?: boolean | undefined;
|
|
44
47
|
enable_log_api_request?: boolean | undefined;
|
|
45
48
|
}>;
|
|
@@ -3,28 +3,30 @@ export const AccountSelectionStrategySchema = z.enum(['sticky', 'round-robin', '
|
|
|
3
3
|
export const RegionSchema = z.enum(['us-east-1', 'us-west-2']);
|
|
4
4
|
export const KiroConfigSchema = z.object({
|
|
5
5
|
$schema: z.string().optional(),
|
|
6
|
-
session_recovery: z.boolean().default(true),
|
|
7
|
-
auto_resume: z.boolean().default(true),
|
|
8
|
-
proactive_token_refresh: z.boolean().default(true),
|
|
9
|
-
token_refresh_interval_seconds: z.number().min(60).max(3600).default(300),
|
|
10
|
-
token_refresh_buffer_seconds: z.number().min(60).max(1800).default(600),
|
|
11
6
|
account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
|
|
12
7
|
default_region: RegionSchema.default('us-east-1'),
|
|
13
8
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
|
14
9
|
rate_limit_max_retries: z.number().min(0).max(10).default(3),
|
|
10
|
+
max_request_iterations: z.number().min(10).max(1000).default(100),
|
|
11
|
+
request_timeout_ms: z.number().min(60000).max(600000).default(300000),
|
|
12
|
+
token_expiry_buffer_ms: z.number().min(30000).max(300000).default(120000),
|
|
13
|
+
usage_sync_max_retries: z.number().min(0).max(5).default(3),
|
|
14
|
+
auth_server_port_start: z.number().min(1024).max(65535).default(19847),
|
|
15
|
+
auth_server_port_range: z.number().min(1).max(100).default(10),
|
|
15
16
|
usage_tracking_enabled: z.boolean().default(true),
|
|
16
17
|
enable_log_api_request: z.boolean().default(false)
|
|
17
18
|
});
|
|
18
19
|
export const DEFAULT_CONFIG = {
|
|
19
|
-
session_recovery: true,
|
|
20
|
-
auto_resume: true,
|
|
21
|
-
proactive_token_refresh: true,
|
|
22
|
-
token_refresh_interval_seconds: 300,
|
|
23
|
-
token_refresh_buffer_seconds: 600,
|
|
24
20
|
account_selection_strategy: 'lowest-usage',
|
|
25
21
|
default_region: 'us-east-1',
|
|
26
22
|
rate_limit_retry_delay_ms: 5000,
|
|
27
23
|
rate_limit_max_retries: 3,
|
|
24
|
+
max_request_iterations: 100,
|
|
25
|
+
request_timeout_ms: 300000,
|
|
26
|
+
token_expiry_buffer_ms: 120000,
|
|
27
|
+
usage_sync_max_retries: 3,
|
|
28
|
+
auth_server_port_start: 19847,
|
|
29
|
+
auth_server_port_range: 10,
|
|
28
30
|
usage_tracking_enabled: true,
|
|
29
31
|
enable_log_api_request: false
|
|
30
32
|
};
|
package/dist/plugin/server.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface IDCAuthData {
|
|
|
18
18
|
expiresIn: number;
|
|
19
19
|
region: KiroRegion;
|
|
20
20
|
}
|
|
21
|
-
export declare function startIDCAuthServer(authData: IDCAuthData,
|
|
21
|
+
export declare function startIDCAuthServer(authData: IDCAuthData, startPort?: number, portRange?: number): Promise<{
|
|
22
22
|
url: string;
|
|
23
23
|
waitForAuth: () => Promise<KiroIDCTokenResult>;
|
|
24
24
|
}>;
|
package/dist/plugin/server.js
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { getIDCAuthHtml, getSuccessHtml, getErrorHtml } from './auth-page';
|
|
3
3
|
import * as logger from './logger';
|
|
4
|
-
|
|
5
|
-
return new Promise((resolve
|
|
4
|
+
async function tryPort(port) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const testServer = createServer();
|
|
7
|
+
testServer.once('error', () => resolve(false));
|
|
8
|
+
testServer.once('listening', () => {
|
|
9
|
+
testServer.close();
|
|
10
|
+
resolve(true);
|
|
11
|
+
});
|
|
12
|
+
testServer.listen(port, '127.0.0.1');
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function findAvailablePort(startPort, range) {
|
|
16
|
+
for (let i = 0; i < range; i++) {
|
|
17
|
+
const port = startPort + i;
|
|
18
|
+
const available = await tryPort(port);
|
|
19
|
+
if (available)
|
|
20
|
+
return port;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`No available ports in range ${startPort}-${startPort + range - 1}. Please close other applications using these ports.`);
|
|
23
|
+
}
|
|
24
|
+
export async function startIDCAuthServer(authData, startPort = 19847, portRange = 10) {
|
|
25
|
+
return new Promise(async (resolve, reject) => {
|
|
26
|
+
let port;
|
|
27
|
+
try {
|
|
28
|
+
port = await findAvailablePort(startPort, portRange);
|
|
29
|
+
logger.log(`Auth server will use port ${port}`);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error('Failed to find available port', error);
|
|
33
|
+
reject(error);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
6
36
|
let server = null;
|
|
7
37
|
let timeoutId = null;
|
|
8
38
|
let resolver = null;
|
package/dist/plugin/storage.js
CHANGED
|
@@ -63,13 +63,16 @@ async function withLock(path, fn) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
export async function loadAccounts() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const path = getStoragePath();
|
|
67
|
+
return withLock(path, async () => {
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { version: 1, accounts: [], activeIndex: -1 };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
73
76
|
}
|
|
74
77
|
export async function saveAccounts(storage) {
|
|
75
78
|
const path = getStoragePath();
|
|
@@ -86,13 +89,16 @@ export async function saveAccounts(storage) {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
export async function loadUsage() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const path = getUsagePath();
|
|
93
|
+
return withLock(path, async () => {
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
96
|
+
return JSON.parse(content);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { version: 1, usage: {} };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
96
102
|
}
|
|
97
103
|
export async function saveUsage(storage) {
|
|
98
104
|
const path = getUsagePath();
|
package/dist/plugin.js
CHANGED
|
@@ -16,6 +16,13 @@ const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
|
|
|
16
16
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
17
|
const isNetworkError = (e) => e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message);
|
|
18
18
|
const extractModel = (url) => url.match(/models\/([^/:]+)/)?.[1] || null;
|
|
19
|
+
const formatUsageMessage = (usedCount, limitCount, email) => {
|
|
20
|
+
if (limitCount > 0) {
|
|
21
|
+
const percentage = Math.round((usedCount / limitCount) * 100);
|
|
22
|
+
return `Usage (${email}): ${usedCount}/${limitCount} (${percentage}%)`;
|
|
23
|
+
}
|
|
24
|
+
return `Usage (${email}): ${usedCount}`;
|
|
25
|
+
};
|
|
19
26
|
export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
20
27
|
const config = loadConfig(directory);
|
|
21
28
|
const showToast = (message, variant) => {
|
|
@@ -39,7 +46,19 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
39
46
|
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig;
|
|
40
47
|
const budget = body.providerOptions?.thinkingConfig?.thinkingBudget || 20000;
|
|
41
48
|
let retry = 0;
|
|
49
|
+
let iterations = 0;
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const maxIterations = config.max_request_iterations;
|
|
52
|
+
const timeoutMs = config.request_timeout_ms;
|
|
42
53
|
while (true) {
|
|
54
|
+
iterations++;
|
|
55
|
+
const elapsed = Date.now() - startTime;
|
|
56
|
+
if (iterations > maxIterations) {
|
|
57
|
+
throw new Error(`Request exceeded max iterations (${maxIterations}). All accounts may be unhealthy or rate-limited.`);
|
|
58
|
+
}
|
|
59
|
+
if (elapsed > timeoutMs) {
|
|
60
|
+
throw new Error(`Request timeout after ${Math.ceil(elapsed / 1000)}s. Max timeout: ${Math.ceil(timeoutMs / 1000)}s.`);
|
|
61
|
+
}
|
|
43
62
|
const count = am.getAccountCount();
|
|
44
63
|
if (count === 0)
|
|
45
64
|
throw new Error('No accounts. Login first.');
|
|
@@ -51,18 +70,25 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
51
70
|
continue;
|
|
52
71
|
}
|
|
53
72
|
if (count > 1 && am.shouldShowToast())
|
|
54
|
-
showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
73
|
+
showToast(`Using ${acc.realEmail || acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
74
|
+
if (am.shouldShowUsageToast() &&
|
|
75
|
+
acc.usedCount !== undefined &&
|
|
76
|
+
acc.limitCount !== undefined) {
|
|
77
|
+
const percentage = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
|
|
78
|
+
const variant = percentage >= 80 ? 'warning' : 'info';
|
|
79
|
+
showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.realEmail || acc.email), variant);
|
|
80
|
+
}
|
|
55
81
|
let auth = am.toAuthDetails(acc);
|
|
56
|
-
if (accessTokenExpired(auth)) {
|
|
82
|
+
if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
|
|
57
83
|
try {
|
|
58
|
-
logger.log(`Refreshing token for ${acc.email}`);
|
|
84
|
+
logger.log(`Refreshing token for ${acc.realEmail || acc.email}`);
|
|
59
85
|
auth = await refreshAccessToken(auth);
|
|
60
86
|
am.updateFromAuth(acc, auth);
|
|
61
87
|
await am.saveToDisk();
|
|
62
88
|
}
|
|
63
89
|
catch (e) {
|
|
64
90
|
const msg = e instanceof KiroTokenRefreshError ? e.message : String(e);
|
|
65
|
-
showToast(`Refresh failed for ${acc.email}: ${msg}`, 'error');
|
|
91
|
+
showToast(`Refresh failed for ${acc.realEmail || acc.email}: ${msg}`, 'error');
|
|
66
92
|
if (e instanceof KiroTokenRefreshError && e.code === 'invalid_grant') {
|
|
67
93
|
am.removeAccount(acc);
|
|
68
94
|
await am.saveToDisk();
|
|
@@ -108,13 +134,24 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
108
134
|
}, apiTimestamp);
|
|
109
135
|
}
|
|
110
136
|
if (res.ok) {
|
|
111
|
-
if (config.usage_tracking_enabled)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
if (config.usage_tracking_enabled) {
|
|
138
|
+
const syncUsage = async (attempt = 0) => {
|
|
139
|
+
try {
|
|
140
|
+
const u = await fetchUsageLimits(auth);
|
|
141
|
+
updateAccountQuota(acc, u, am);
|
|
142
|
+
await am.saveToDisk();
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
if (attempt < config.usage_sync_max_retries) {
|
|
146
|
+
const delay = 1000 * Math.pow(2, attempt);
|
|
147
|
+
await sleep(delay);
|
|
148
|
+
return syncUsage(attempt + 1);
|
|
149
|
+
}
|
|
150
|
+
logger.warn(`Usage sync failed for ${acc.realEmail || acc.email} after ${attempt + 1} attempts: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
syncUsage().catch(() => { });
|
|
154
|
+
}
|
|
118
155
|
if (prep.streaming) {
|
|
119
156
|
const s = transformKiroStream(res, model, prep.conversationId);
|
|
120
157
|
return new Response(new ReadableStream({
|
|
@@ -164,7 +201,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
164
201
|
});
|
|
165
202
|
}
|
|
166
203
|
if (res.status === 401 && retry < config.rate_limit_max_retries) {
|
|
167
|
-
logger.warn(`Unauthorized (401) on ${acc.email}, retrying...`);
|
|
204
|
+
logger.warn(`Unauthorized (401) on ${acc.realEmail || acc.email}, retrying...`);
|
|
168
205
|
retry++;
|
|
169
206
|
continue;
|
|
170
207
|
}
|
|
@@ -173,7 +210,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
173
210
|
am.markRateLimited(acc, wait);
|
|
174
211
|
await am.saveToDisk();
|
|
175
212
|
if (count > 1) {
|
|
176
|
-
showToast(`Rate limited on ${acc.email}. Switching account...`, 'warning');
|
|
213
|
+
showToast(`Rate limited on ${acc.realEmail || acc.email}. Switching account...`, 'warning');
|
|
177
214
|
continue;
|
|
178
215
|
}
|
|
179
216
|
else {
|
|
@@ -183,7 +220,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
183
220
|
}
|
|
184
221
|
}
|
|
185
222
|
if ((res.status === 402 || res.status === 403) && count > 1) {
|
|
186
|
-
showToast(`${res.status === 402 ? 'Quota exhausted' : 'Forbidden'} on ${acc.email}. Switching...`, 'warning');
|
|
223
|
+
showToast(`${res.status === 402 ? 'Quota exhausted' : 'Forbidden'} on ${acc.realEmail || acc.email}. Switching...`, 'warning');
|
|
187
224
|
am.markUnhealthy(acc, res.status === 402 ? 'Quota' : 'Forbidden');
|
|
188
225
|
await am.saveToDisk();
|
|
189
226
|
continue;
|
|
@@ -234,7 +271,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
234
271
|
const region = config.default_region;
|
|
235
272
|
try {
|
|
236
273
|
const authData = await authorizeKiroIDC(region);
|
|
237
|
-
const { url, waitForAuth } = await startIDCAuthServer(authData);
|
|
274
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
|
|
238
275
|
resolve({
|
|
239
276
|
url,
|
|
240
277
|
instructions: 'Opening browser...',
|