@sunerpy/opencode-kiro-auth 0.8.0 → 0.10.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 +29 -16
- package/dist/core/account/account-selector.js +3 -0
- package/dist/core/auth/idc-auth-method.js +2 -2
- package/dist/plugin/accounts.d.ts +10 -0
- package/dist/plugin/accounts.js +40 -2
- package/dist/plugin/config/loader.d.ts +1 -1
- package/dist/plugin/config/loader.js +12 -17
- package/dist/plugin/config/schema.d.ts +13 -0
- package/dist/plugin/config/schema.js +11 -0
- package/dist/plugin/logger.js +3 -10
- package/dist/plugin/paths.d.ts +9 -0
- package/dist/plugin/paths.js +33 -0
- package/dist/plugin/storage/locked-operations.d.ts +1 -3
- package/dist/plugin/storage/locked-operations.js +7 -21
- package/dist/plugin/storage/migrate-layout.d.ts +1 -0
- package/dist/plugin/storage/migrate-layout.js +191 -0
- package/dist/plugin/storage/migrations.js +8 -0
- package/dist/plugin/storage/sqlite.js +8 -5
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin/usage.d.ts +16 -3
- package/dist/plugin/usage.js +31 -3
- package/dist/plugin.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ customize which models are exposed (including thinking variants), see
|
|
|
85
85
|
- Note: the TUI `/connect` flow does **not** currently run plugin OAuth
|
|
86
86
|
prompts (Start URL / region), so Identity Center logins may fall back to
|
|
87
87
|
Builder ID unless you use `opencode auth login` (or preconfigure
|
|
88
|
-
defaults in `~/.config/opencode/kiro.json`).
|
|
88
|
+
defaults in `~/.config/opencode/kiro-auth-plugin/kiro.json`).
|
|
89
89
|
- For **IAM Identity Center**, you may also need a **profile ARN**
|
|
90
90
|
(`profileArn`) — auto-detected from `kiro-cli profile` if available, or
|
|
91
91
|
set `idc_profile_arn` manually.
|
|
@@ -97,7 +97,7 @@ customize which models are exposed (including thinking variants), see
|
|
|
97
97
|
## Configuration
|
|
98
98
|
|
|
99
99
|
Plugin-wide behavior (auth sync, account selection, retry limits, effort
|
|
100
|
-
mapping) lives in `~/.config/opencode/kiro.json`. See
|
|
100
|
+
mapping) lives in `~/.config/opencode/kiro-auth-plugin/kiro.json`. See
|
|
101
101
|
[docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the full example and every
|
|
102
102
|
option.
|
|
103
103
|
|
|
@@ -108,6 +108,10 @@ multi-account or long-idle setups, enable
|
|
|
108
108
|
(`token_keepalive_enabled: true`) to keep idle accounts' tokens fresh while
|
|
109
109
|
OpenCode is running.
|
|
110
110
|
|
|
111
|
+
Paid-overage protection is on by default; see
|
|
112
|
+
[Overage protection](docs/CONFIGURATION.md#overage-protection) before disabling
|
|
113
|
+
`stop_on_overage`.
|
|
114
|
+
|
|
111
115
|
## Multiple accounts & rotation
|
|
112
116
|
|
|
113
117
|
You can register more than one Kiro account and let the plugin spread
|
|
@@ -127,13 +131,13 @@ requests across them for combined quota and automatic failover.
|
|
|
127
131
|
account (option 1) is the supported multi-account path.
|
|
128
132
|
|
|
129
133
|
**Rotation strategy** — set `account_selection_strategy` in
|
|
130
|
-
`~/.config/opencode/kiro.json`:
|
|
134
|
+
`~/.config/opencode/kiro-auth-plugin/kiro.json`:
|
|
131
135
|
|
|
132
|
-
| Strategy | Behavior
|
|
133
|
-
| -------------- |
|
|
134
|
-
| `lowest-usage` | Each request picks the healthy account with the lowest used quota
|
|
135
|
-
| `round-robin` | Cycles through accounts in order
|
|
136
|
-
| `sticky` | Always uses the first account; switches only when it becomes unhealthy |
|
|
136
|
+
| Strategy | Behavior | Default |
|
|
137
|
+
| -------------- | ---------------------------------------------------------------------- | ------- |
|
|
138
|
+
| `lowest-usage` | Each request picks the healthy account with the lowest used quota | ✅ |
|
|
139
|
+
| `round-robin` | Cycles through accounts in order | |
|
|
140
|
+
| `sticky` | Always uses the first account; switches only when it becomes unhealthy | |
|
|
137
141
|
|
|
138
142
|
**Automatic failover** requires no configuration: a rate-limited or 403
|
|
139
143
|
account is marked unhealthy and the next healthy account takes over. If every
|
|
@@ -161,7 +165,7 @@ per account, e.g. `929/10000`) and surfaces it in three places:
|
|
|
161
165
|
|
|
162
166
|
1. **Auth menu label** — running `opencode auth login` and selecting
|
|
163
167
|
`kiro-auth` shows `[current: <email> <used>/<limit> (<pct>%)]` on the
|
|
164
|
-
first login method; with multiple accounts they're joined with
|
|
168
|
+
first login method; with multiple accounts they're joined with `·`,
|
|
165
169
|
capped at 3 with a `+N more` suffix.
|
|
166
170
|
2. **Startup toast** — once per plugin init, a toast shows
|
|
167
171
|
`Kiro usage (<email>): <used>/<limit> (<pct>%)` a few seconds after
|
|
@@ -187,11 +191,11 @@ your `provider.kiro-auth.models` block.
|
|
|
187
191
|
Thinking budgets map to Kiro's native `effort` field automatically:
|
|
188
192
|
|
|
189
193
|
| OpenCode budget | Kiro effort |
|
|
190
|
-
|
|
|
191
|
-
| `<= 10000`
|
|
192
|
-
| `<= 20000`
|
|
193
|
-
| `<= 28000`
|
|
194
|
-
| `> 28000`
|
|
194
|
+
| --------------- | ----------- |
|
|
195
|
+
| `<= 10000` | `low` |
|
|
196
|
+
| `<= 20000` | `medium` |
|
|
197
|
+
| `<= 28000` | `high` |
|
|
198
|
+
| `> 28000` | `max` |
|
|
195
199
|
|
|
196
200
|
Details and the full JSON example: [docs/MODELS.md](docs/MODELS.md).
|
|
197
201
|
|
|
@@ -279,12 +283,21 @@ Releases are automated with
|
|
|
279
283
|
**Linux/macOS:**
|
|
280
284
|
|
|
281
285
|
- SQLite database: `~/.config/opencode/kiro.db`
|
|
282
|
-
- Plugin config: `~/.config/opencode/kiro.json`
|
|
286
|
+
- Plugin config: `~/.config/opencode/kiro-auth-plugin/kiro.json`
|
|
287
|
+
- Logs: `~/.config/opencode/kiro-auth-plugin/logs/`
|
|
288
|
+
- Refresh and keep-alive locks: `~/.config/opencode/kiro-auth-plugin/`
|
|
283
289
|
|
|
284
290
|
**Windows:**
|
|
285
291
|
|
|
286
292
|
- SQLite database: `%APPDATA%\opencode\kiro.db`
|
|
287
|
-
- Plugin config: `%APPDATA%\opencode\kiro.json`
|
|
293
|
+
- Plugin config: `%APPDATA%\opencode\kiro-auth-plugin\kiro.json`
|
|
294
|
+
- Logs: `%APPDATA%\opencode\kiro-auth-plugin\logs\`
|
|
295
|
+
- Refresh and keep-alive locks: `%APPDATA%\opencode\kiro-auth-plugin\`
|
|
296
|
+
|
|
297
|
+
Existing config, logs, and stale flat lock files are migrated automatically on
|
|
298
|
+
first startup; no action is needed. The SQLite database deliberately remains at
|
|
299
|
+
the flat `opencode/kiro.db` path because moving a live database during an upgrade
|
|
300
|
+
is unsafe.
|
|
288
301
|
|
|
289
302
|
## Acknowledgements
|
|
290
303
|
|
|
@@ -26,6 +26,9 @@ export class AccountSelector {
|
|
|
26
26
|
let acc = this.accountManager.getCurrentOrNext();
|
|
27
27
|
if (!acc) {
|
|
28
28
|
this.circuitBreakerTrips++;
|
|
29
|
+
if (this.accountManager.allSelectableBlockedByOverage()) {
|
|
30
|
+
throw new Error('All accounts have exceeded their free quota and entered paid overage. Set "stop_on_overage": false in ~/.config/opencode/kiro-auth-plugin/kiro.json to continue with paid overage, or wait for the quota to reset.');
|
|
31
|
+
}
|
|
29
32
|
const wait = this.accountManager.getMinWaitTime();
|
|
30
33
|
if (wait > 0 && wait < 30000) {
|
|
31
34
|
if (this.accountManager.shouldShowToast()) {
|
|
@@ -106,7 +106,7 @@ export class IdcAuthMethod {
|
|
|
106
106
|
error: e instanceof Error ? e.message : String(e)
|
|
107
107
|
});
|
|
108
108
|
if (startUrl && !profileArn) {
|
|
109
|
-
throw new Error(`Missing profile ARN for IAM Identity Center. Set "idc_profile_arn" in ~/.config/opencode/kiro.json, or run "kiro-cli profile" once so it can be auto-detected. Original error: ${e instanceof Error ? e.message : String(e)}`);
|
|
109
|
+
throw new Error(`Missing profile ARN for IAM Identity Center. Set "idc_profile_arn" in ~/.config/opencode/kiro-auth-plugin/kiro.json, or run "kiro-cli profile" once so it can be auto-detected. Original error: ${e instanceof Error ? e.message : String(e)}`);
|
|
110
110
|
}
|
|
111
111
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
112
112
|
if (errMsg.includes('FEATURE_NOT_SUPPORTED')) {
|
|
@@ -176,7 +176,7 @@ export class IdcAuthMethod {
|
|
|
176
176
|
catch (e) {
|
|
177
177
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
178
178
|
logger.error('IDC auth callback failed', err);
|
|
179
|
-
throw new Error(`IDC authorization failed: ${err.message}. Check ~/.config/opencode/kiro-logs/plugin.log for details. If this is an Identity Center account, ensure you have selected an AWS Q Developer/CodeWhisperer profile (try: kiro-cli profile).`);
|
|
179
|
+
throw new Error(`IDC authorization failed: ${err.message}. Check ~/.config/opencode/kiro-auth-plugin/logs/plugin.log for details. If this is an Identity Center account, ensure you have selected an AWS Q Developer/CodeWhisperer profile (try: kiro-cli profile).`);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
};
|
|
@@ -10,24 +10,33 @@ export declare class AccountManager {
|
|
|
10
10
|
private stickyId?;
|
|
11
11
|
private quotaAvoidanceEnabled;
|
|
12
12
|
private quotaReserveThreshold;
|
|
13
|
+
private stopOnOverage;
|
|
14
|
+
private overageThreshold;
|
|
13
15
|
constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy, opts?: {
|
|
14
16
|
quotaAvoidanceEnabled?: boolean;
|
|
15
17
|
quotaReserveThreshold?: number;
|
|
18
|
+
stopOnOverage?: boolean;
|
|
19
|
+
overageThreshold?: number;
|
|
16
20
|
});
|
|
17
21
|
static loadFromDisk(strategy?: AccountSelectionStrategy, opts?: {
|
|
18
22
|
quotaAvoidanceEnabled?: boolean;
|
|
19
23
|
quotaReserveThreshold?: number;
|
|
24
|
+
stopOnOverage?: boolean;
|
|
25
|
+
overageThreshold?: number;
|
|
20
26
|
}): Promise<AccountManager>;
|
|
21
27
|
getAccountCount(): number;
|
|
22
28
|
getAccounts(): ManagedAccount[];
|
|
23
29
|
shouldShowToast(debounce?: number): boolean;
|
|
24
30
|
shouldShowUsageToast(debounce?: number): boolean;
|
|
25
31
|
getMinWaitTime(): number;
|
|
32
|
+
allSelectableBlockedByOverage(): boolean;
|
|
26
33
|
getCurrentOrNext(): ManagedAccount | null;
|
|
27
34
|
updateUsage(id: string, meta: {
|
|
28
35
|
usedCount: number;
|
|
29
36
|
limitCount: number;
|
|
37
|
+
overageCount?: number;
|
|
30
38
|
email?: string;
|
|
39
|
+
lastSync?: number;
|
|
31
40
|
}): void;
|
|
32
41
|
addAccount(a: ManagedAccount): void;
|
|
33
42
|
removeAccount(a: ManagedAccount): void;
|
|
@@ -36,4 +45,5 @@ export declare class AccountManager {
|
|
|
36
45
|
markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
|
|
37
46
|
saveToDisk(): Promise<void>;
|
|
38
47
|
toAuthDetails(a: ManagedAccount): KiroAuthDetails;
|
|
48
|
+
private isOverageBlocked;
|
|
39
49
|
}
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -19,12 +19,16 @@ export class AccountManager {
|
|
|
19
19
|
stickyId;
|
|
20
20
|
quotaAvoidanceEnabled;
|
|
21
21
|
quotaReserveThreshold;
|
|
22
|
+
stopOnOverage;
|
|
23
|
+
overageThreshold;
|
|
22
24
|
constructor(accounts, strategy = 'sticky', opts) {
|
|
23
25
|
this.accounts = accounts;
|
|
24
26
|
this.cursor = 0;
|
|
25
27
|
this.strategy = strategy;
|
|
26
28
|
this.quotaAvoidanceEnabled = opts?.quotaAvoidanceEnabled ?? true;
|
|
27
29
|
this.quotaReserveThreshold = opts?.quotaReserveThreshold ?? 0.95;
|
|
30
|
+
this.stopOnOverage = opts?.stopOnOverage ?? true;
|
|
31
|
+
this.overageThreshold = opts?.overageThreshold ?? 0;
|
|
28
32
|
}
|
|
29
33
|
static async loadFromDisk(strategy, opts) {
|
|
30
34
|
const rows = kiroDb.getAccounts();
|
|
@@ -48,7 +52,9 @@ export class AccountManager {
|
|
|
48
52
|
failCount: r.fail_count || 0,
|
|
49
53
|
lastUsed: r.last_used,
|
|
50
54
|
usedCount: r.used_count,
|
|
51
|
-
limitCount: r.limit_count
|
|
55
|
+
limitCount: r.limit_count,
|
|
56
|
+
overageCount: r.overage_count || 0,
|
|
57
|
+
lastSync: r.last_sync
|
|
52
58
|
}));
|
|
53
59
|
return new AccountManager(accounts, strategy || 'sticky', opts);
|
|
54
60
|
}
|
|
@@ -75,9 +81,31 @@ export class AccountManager {
|
|
|
75
81
|
const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
|
|
76
82
|
return waits.length > 0 ? Math.min(...waits) : 0;
|
|
77
83
|
}
|
|
84
|
+
allSelectableBlockedByOverage() {
|
|
85
|
+
if (!this.stopOnOverage)
|
|
86
|
+
return false;
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const healthEligible = (a) => {
|
|
89
|
+
if (isPermanentError(a.unhealthyReason))
|
|
90
|
+
return false;
|
|
91
|
+
if (a.isHealthy)
|
|
92
|
+
return true;
|
|
93
|
+
if (isAccessTokenError(a.unhealthyReason))
|
|
94
|
+
return true;
|
|
95
|
+
return a.failCount < 10;
|
|
96
|
+
};
|
|
97
|
+
const rateLimited = (a) => !!(a.rateLimitResetTime && now < a.rateLimitResetTime);
|
|
98
|
+
const hasOverageBlockedEligible = this.accounts.some((a) => healthEligible(a) && this.isOverageBlocked(a));
|
|
99
|
+
const hasCleanRateLimited = this.accounts.some((a) => healthEligible(a) && rateLimited(a) && !this.isOverageBlocked(a));
|
|
100
|
+
const hasHealthySelectable = this.accounts.some((a) => a.isHealthy && !rateLimited(a) && !this.isOverageBlocked(a));
|
|
101
|
+
return hasOverageBlockedEligible && !hasCleanRateLimited && !hasHealthySelectable;
|
|
102
|
+
}
|
|
78
103
|
getCurrentOrNext() {
|
|
79
104
|
const now = Date.now();
|
|
105
|
+
const overageBlocked = (a) => this.isOverageBlocked(a);
|
|
80
106
|
const available = this.accounts.filter((a) => {
|
|
107
|
+
if (overageBlocked(a))
|
|
108
|
+
return false;
|
|
81
109
|
if (!a.isHealthy) {
|
|
82
110
|
if (isPermanentError(a.unhealthyReason)) {
|
|
83
111
|
return false;
|
|
@@ -129,7 +157,10 @@ export class AccountManager {
|
|
|
129
157
|
}
|
|
130
158
|
if (!selected) {
|
|
131
159
|
const fallback = this.accounts
|
|
132
|
-
.filter((a) => !a.isHealthy &&
|
|
160
|
+
.filter((a) => !a.isHealthy &&
|
|
161
|
+
a.failCount < 10 &&
|
|
162
|
+
!isPermanentError(a.unhealthyReason) &&
|
|
163
|
+
!overageBlocked(a))
|
|
133
164
|
.sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
134
165
|
if (fallback) {
|
|
135
166
|
fallback.isHealthy = true;
|
|
@@ -139,6 +170,8 @@ export class AccountManager {
|
|
|
139
170
|
}
|
|
140
171
|
}
|
|
141
172
|
if (selected) {
|
|
173
|
+
if (overageBlocked(selected))
|
|
174
|
+
return null;
|
|
142
175
|
selected.lastUsed = now;
|
|
143
176
|
selected.usedCount = (selected.usedCount || 0) + 1;
|
|
144
177
|
return selected;
|
|
@@ -150,6 +183,8 @@ export class AccountManager {
|
|
|
150
183
|
if (a) {
|
|
151
184
|
a.usedCount = meta.usedCount;
|
|
152
185
|
a.limitCount = meta.limitCount;
|
|
186
|
+
a.overageCount = meta.overageCount ?? 0;
|
|
187
|
+
a.lastSync = meta.lastSync ?? a.lastSync;
|
|
153
188
|
if (meta.email)
|
|
154
189
|
a.email = meta.email;
|
|
155
190
|
if (!isPermanentError(a.unhealthyReason)) {
|
|
@@ -290,4 +325,7 @@ export class AccountManager {
|
|
|
290
325
|
email: a.email
|
|
291
326
|
};
|
|
292
327
|
}
|
|
328
|
+
isOverageBlocked(a) {
|
|
329
|
+
return this.stopOnOverage && (a.overageCount ?? 0) > this.overageThreshold;
|
|
330
|
+
}
|
|
293
331
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type KiroConfig } from './schema.js';
|
|
2
|
-
export
|
|
2
|
+
export { getUserConfigPath } from '../paths.js';
|
|
3
3
|
export declare function getProjectConfigPath(directory: string): string;
|
|
4
4
|
export declare function loadConfig(directory: string): KiroConfig;
|
|
5
5
|
export declare function configExists(path: string): boolean;
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
4
3
|
import * as logger from '../logger.js';
|
|
4
|
+
import { getLegacyUserConfigPath, getLogsDir, getUserConfigPath } from '../paths.js';
|
|
5
5
|
import { AccountSelectionStrategySchema, DEFAULT_CONFIG, KiroConfigSchema, RegionSchema } from './schema.js';
|
|
6
|
-
|
|
7
|
-
const platform = process.platform;
|
|
8
|
-
if (platform === 'win32') {
|
|
9
|
-
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
10
|
-
}
|
|
11
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
12
|
-
return join(xdgConfig, 'opencode');
|
|
13
|
-
}
|
|
14
|
-
export function getUserConfigPath() {
|
|
15
|
-
return join(getConfigDir(), 'kiro.json');
|
|
16
|
-
}
|
|
6
|
+
export { getUserConfigPath } from '../paths.js';
|
|
17
7
|
function ensureUserConfigTemplate() {
|
|
18
8
|
const path = getUserConfigPath();
|
|
19
9
|
if (!existsSync(path)) {
|
|
@@ -155,11 +145,16 @@ function applyEnvOverrides(config) {
|
|
|
155
145
|
};
|
|
156
146
|
}
|
|
157
147
|
export function loadConfig(directory) {
|
|
158
|
-
ensureUserConfigTemplate();
|
|
159
|
-
backfillUserConfig(getUserConfigPath());
|
|
160
|
-
let config = { ...DEFAULT_CONFIG };
|
|
161
148
|
const userConfigPath = getUserConfigPath();
|
|
162
|
-
const
|
|
149
|
+
const legacyUserConfigPath = getLegacyUserConfigPath();
|
|
150
|
+
const configSourcePath = !existsSync(userConfigPath) && existsSync(legacyUserConfigPath)
|
|
151
|
+
? legacyUserConfigPath
|
|
152
|
+
: userConfigPath;
|
|
153
|
+
if (configSourcePath === userConfigPath)
|
|
154
|
+
ensureUserConfigTemplate();
|
|
155
|
+
backfillUserConfig(configSourcePath);
|
|
156
|
+
let config = { ...DEFAULT_CONFIG };
|
|
157
|
+
const userConfig = loadConfigFile(configSourcePath);
|
|
163
158
|
if (userConfig) {
|
|
164
159
|
config = mergeConfigs(config, userConfig);
|
|
165
160
|
}
|
|
@@ -175,5 +170,5 @@ export function configExists(path) {
|
|
|
175
170
|
return existsSync(path);
|
|
176
171
|
}
|
|
177
172
|
export function getDefaultLogsDir() {
|
|
178
|
-
return
|
|
173
|
+
return getLogsDir();
|
|
179
174
|
}
|
|
@@ -32,6 +32,15 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
32
32
|
* near-full and softly avoided. Default 0.95 (95%).
|
|
33
33
|
*/
|
|
34
34
|
quota_reserve_threshold: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
/**
|
|
36
|
+
* Exclude accounts that have entered AWS paid overage from selection.
|
|
37
|
+
*/
|
|
38
|
+
stop_on_overage: z.ZodDefault<z.ZodBoolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Paid-overage invocations tolerated before stopping an account. 0 means
|
|
41
|
+
* stop on any overage.
|
|
42
|
+
*/
|
|
43
|
+
overage_threshold: z.ZodDefault<z.ZodNumber>;
|
|
35
44
|
default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
|
|
36
45
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
37
46
|
rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
|
|
@@ -77,6 +86,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
77
86
|
account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
|
|
78
87
|
quota_avoidance_enabled: boolean;
|
|
79
88
|
quota_reserve_threshold: number;
|
|
89
|
+
stop_on_overage: boolean;
|
|
90
|
+
overage_threshold: number;
|
|
80
91
|
default_region: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1";
|
|
81
92
|
rate_limit_retry_delay_ms: number;
|
|
82
93
|
rate_limit_max_retries: number;
|
|
@@ -106,6 +117,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
106
117
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
107
118
|
quota_avoidance_enabled?: boolean | undefined;
|
|
108
119
|
quota_reserve_threshold?: number | undefined;
|
|
120
|
+
stop_on_overage?: boolean | undefined;
|
|
121
|
+
overage_threshold?: number | undefined;
|
|
109
122
|
default_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
110
123
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
111
124
|
rate_limit_max_retries?: number | undefined;
|
|
@@ -64,6 +64,15 @@ export const KiroConfigSchema = z.object({
|
|
|
64
64
|
* near-full and softly avoided. Default 0.95 (95%).
|
|
65
65
|
*/
|
|
66
66
|
quota_reserve_threshold: z.number().min(0).max(1).default(0.95),
|
|
67
|
+
/**
|
|
68
|
+
* Exclude accounts that have entered AWS paid overage from selection.
|
|
69
|
+
*/
|
|
70
|
+
stop_on_overage: z.boolean().default(true),
|
|
71
|
+
/**
|
|
72
|
+
* Paid-overage invocations tolerated before stopping an account. 0 means
|
|
73
|
+
* stop on any overage.
|
|
74
|
+
*/
|
|
75
|
+
overage_threshold: z.number().min(0).default(0),
|
|
67
76
|
default_region: RegionSchema.default('us-east-1'),
|
|
68
77
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
|
69
78
|
rate_limit_max_retries: z.number().min(0).max(10).default(3),
|
|
@@ -110,6 +119,8 @@ export const DEFAULT_CONFIG = {
|
|
|
110
119
|
account_selection_strategy: 'lowest-usage',
|
|
111
120
|
quota_avoidance_enabled: true,
|
|
112
121
|
quota_reserve_threshold: 0.95,
|
|
122
|
+
stop_on_overage: true,
|
|
123
|
+
overage_threshold: 0,
|
|
113
124
|
default_region: 'us-east-1',
|
|
114
125
|
rate_limit_retry_delay_ms: 5000,
|
|
115
126
|
rate_limit_max_retries: 3,
|
package/dist/plugin/logger.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
4
|
+
import { getLogsDir } from './paths.js';
|
|
5
5
|
const binaryToBase64Replacer = (_key, value) => {
|
|
6
6
|
if (value instanceof Uint8Array)
|
|
7
7
|
return Buffer.from(value).toString('base64');
|
|
8
8
|
return value;
|
|
9
9
|
};
|
|
10
|
-
const getLogDir = () => {
|
|
11
|
-
const platform = process.platform;
|
|
12
|
-
const base = platform === 'win32'
|
|
13
|
-
? join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode')
|
|
14
|
-
: join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
|
|
15
|
-
return join(base, 'kiro-logs');
|
|
16
|
-
};
|
|
17
10
|
const writeToFile = (level, message, ...args) => {
|
|
18
11
|
try {
|
|
19
|
-
const dir =
|
|
12
|
+
const dir = getLogsDir();
|
|
20
13
|
mkdirSync(dir, { recursive: true });
|
|
21
14
|
const path = join(dir, 'plugin.log');
|
|
22
15
|
const timestamp = new Date().toISOString();
|
|
@@ -42,7 +35,7 @@ const writeToFile = (level, message, ...args) => {
|
|
|
42
35
|
};
|
|
43
36
|
const writeApiLog = (type, data, timestamp, isError = false) => {
|
|
44
37
|
try {
|
|
45
|
-
const dir =
|
|
38
|
+
const dir = getLogsDir();
|
|
46
39
|
mkdirSync(dir, { recursive: true });
|
|
47
40
|
const prefix = isError ? 'error_' : '';
|
|
48
41
|
const filename = `${prefix}${timestamp}_${type}.json`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function getOpencodeConfigDir(): string;
|
|
2
|
+
export declare function getPluginDir(): string;
|
|
3
|
+
export declare function getUserConfigPath(): string;
|
|
4
|
+
export declare function getLegacyUserConfigPath(): string;
|
|
5
|
+
export declare function getLogsDir(): string;
|
|
6
|
+
export declare function getLegacyLogsDir(): string;
|
|
7
|
+
export declare function getRefreshLockPath(accountId: string): string;
|
|
8
|
+
export declare function getKeepAliveLockPath(): string;
|
|
9
|
+
export declare function getMigrationLockPath(): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function getOpencodeConfigDir() {
|
|
4
|
+
const p = process.platform;
|
|
5
|
+
if (p === 'win32')
|
|
6
|
+
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
7
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
|
|
8
|
+
}
|
|
9
|
+
export function getPluginDir() {
|
|
10
|
+
return join(getOpencodeConfigDir(), 'kiro-auth-plugin');
|
|
11
|
+
}
|
|
12
|
+
export function getUserConfigPath() {
|
|
13
|
+
return join(getPluginDir(), 'kiro.json');
|
|
14
|
+
}
|
|
15
|
+
export function getLegacyUserConfigPath() {
|
|
16
|
+
return join(getOpencodeConfigDir(), 'kiro.json');
|
|
17
|
+
}
|
|
18
|
+
export function getLogsDir() {
|
|
19
|
+
return join(getPluginDir(), 'logs');
|
|
20
|
+
}
|
|
21
|
+
export function getLegacyLogsDir() {
|
|
22
|
+
return join(getOpencodeConfigDir(), 'kiro-logs');
|
|
23
|
+
}
|
|
24
|
+
export function getRefreshLockPath(accountId) {
|
|
25
|
+
const safeAccountId = accountId.replace(/[^A-Za-z0-9_-]/g, '');
|
|
26
|
+
return join(getPluginDir(), `.kiro-refresh-${safeAccountId}.lock`);
|
|
27
|
+
}
|
|
28
|
+
export function getKeepAliveLockPath() {
|
|
29
|
+
return join(getPluginDir(), '.kiro-keepalive.lock');
|
|
30
|
+
}
|
|
31
|
+
export function getMigrationLockPath() {
|
|
32
|
+
return join(getOpencodeConfigDir(), '.kiro-auth-migration.lock');
|
|
33
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ManagedAccount } from '../types.js';
|
|
2
|
+
export { getKeepAliveLockPath, getRefreshLockPath } from '../paths.js';
|
|
2
3
|
type LockRelease = () => Promise<void>;
|
|
3
|
-
export declare function getRefreshLockPath(accountId: string): string;
|
|
4
|
-
export declare function getKeepAliveLockPath(): string;
|
|
5
4
|
export declare function withDatabaseLock<T>(dbPath: string, fn: () => Promise<T>): Promise<T>;
|
|
6
5
|
export declare function withRefreshLock<T>(accountId: string, fn: () => Promise<T>): Promise<T>;
|
|
7
6
|
export declare function tryAcquireKeepAliveLock(): Promise<LockRelease | null>;
|
|
@@ -9,4 +8,3 @@ export declare function withKeepAliveLock<T>(fn: () => Promise<T>): Promise<T |
|
|
|
9
8
|
export declare function createDeterministicId(email: string, authMethod: string, clientId?: string, profileArn?: string): string;
|
|
10
9
|
export declare function mergeAccounts(existing: ManagedAccount[], incoming: ManagedAccount[]): ManagedAccount[];
|
|
11
10
|
export declare function deduplicateAccounts(accounts: ManagedAccount[]): ManagedAccount[];
|
|
12
|
-
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, promises as fs } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
5
4
|
import lockfile from 'proper-lockfile';
|
|
6
5
|
import { isPermanentError } from '../health.js';
|
|
6
|
+
import { getKeepAliveLockPath, getRefreshLockPath } from '../paths.js';
|
|
7
|
+
export { getKeepAliveLockPath, getRefreshLockPath } from '../paths.js';
|
|
7
8
|
const LOCK_OPTIONS = {
|
|
8
9
|
stale: 10000,
|
|
9
10
|
retries: {
|
|
@@ -29,23 +30,6 @@ const KEEP_ALIVE_LOCK_OPTIONS = {
|
|
|
29
30
|
retries: 0,
|
|
30
31
|
realpath: false
|
|
31
32
|
};
|
|
32
|
-
function getBaseDir() {
|
|
33
|
-
// Keep this local instead of importing DB_PATH from sqlite.ts: sqlite.ts imports
|
|
34
|
-
// this module and also constructs `kiroDb` at module load, so importing it here
|
|
35
|
-
// would create a cycle and trigger database construction just to compute a lock path.
|
|
36
|
-
const p = process.platform;
|
|
37
|
-
if (p === 'win32') {
|
|
38
|
-
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
39
|
-
}
|
|
40
|
-
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
|
|
41
|
-
}
|
|
42
|
-
export function getRefreshLockPath(accountId) {
|
|
43
|
-
const safeAccountId = accountId.replace(/[^A-Za-z0-9_-]/g, '');
|
|
44
|
-
return join(getBaseDir(), `.kiro-refresh-${safeAccountId}.lock`);
|
|
45
|
-
}
|
|
46
|
-
export function getKeepAliveLockPath() {
|
|
47
|
-
return join(getBaseDir(), '.kiro-keepalive.lock');
|
|
48
|
-
}
|
|
49
33
|
export async function withDatabaseLock(dbPath, fn) {
|
|
50
34
|
if (!existsSync(dbPath)) {
|
|
51
35
|
const dir = dbPath.substring(0, dbPath.lastIndexOf('/'));
|
|
@@ -139,6 +123,7 @@ export function mergeAccounts(existing, incoming) {
|
|
|
139
123
|
// in-memory token triple from another process must not clobber a newer
|
|
140
124
|
// persisted token triple during this cross-process merge.
|
|
141
125
|
const tokenWinner = (acc.expiresAt || 0) >= (existingAcc.expiresAt || 0) ? acc : existingAcc;
|
|
126
|
+
const usageWinner = (acc.lastSync || 0) >= (existingAcc.lastSync || 0) ? acc : existingAcc;
|
|
142
127
|
accountMap.set(acc.id, {
|
|
143
128
|
...existingAcc,
|
|
144
129
|
...acc,
|
|
@@ -146,8 +131,9 @@ export function mergeAccounts(existing, incoming) {
|
|
|
146
131
|
accessToken: tokenWinner.accessToken,
|
|
147
132
|
expiresAt: tokenWinner.expiresAt,
|
|
148
133
|
lastUsed: Math.max(existingAcc.lastUsed || 0, acc.lastUsed || 0),
|
|
149
|
-
usedCount:
|
|
150
|
-
limitCount:
|
|
134
|
+
usedCount: usageWinner.usedCount ?? 0,
|
|
135
|
+
limitCount: usageWinner.limitCount ?? 0,
|
|
136
|
+
overageCount: usageWinner.overageCount ?? 0,
|
|
151
137
|
rateLimitResetTime: Math.max(existingAcc.rateLimitResetTime || 0, acc.rateLimitResetTime || 0),
|
|
152
138
|
isHealthy: incomingRecovered
|
|
153
139
|
? true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function migrateSafeFilesIfNeeded(): void;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, renameSync, rmdirSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import lockfile from 'proper-lockfile';
|
|
4
|
+
import * as logger from '../logger.js';
|
|
5
|
+
import { getLegacyLogsDir, getLegacyUserConfigPath, getLogsDir, getMigrationLockPath, getOpencodeConfigDir, getPluginDir, getUserConfigPath } from '../paths.js';
|
|
6
|
+
const MIGRATION_LOCK_OPTIONS = {
|
|
7
|
+
stale: 15000,
|
|
8
|
+
retries: 0,
|
|
9
|
+
realpath: false
|
|
10
|
+
};
|
|
11
|
+
const MIGRATION_LOCK_ATTEMPTS = 4;
|
|
12
|
+
function isCrossDeviceError(error) {
|
|
13
|
+
return error instanceof Error && 'code' in error && error.code === 'EXDEV';
|
|
14
|
+
}
|
|
15
|
+
function removeTemporaryFile(path) {
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
unlinkSync(path);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
logger.warn('Storage layout migration: temporary file cleanup failed', {
|
|
23
|
+
path,
|
|
24
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function moveFile(source, destination) {
|
|
29
|
+
try {
|
|
30
|
+
renameSync(source, destination);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!(error instanceof Error) || !isCrossDeviceError(error))
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const temporaryDestination = `${destination}.tmp-${process.pid}`;
|
|
38
|
+
try {
|
|
39
|
+
copyFileSync(source, temporaryDestination);
|
|
40
|
+
renameSync(temporaryDestination, destination);
|
|
41
|
+
unlinkSync(source);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
removeTemporaryFile(temporaryDestination);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function migrateUserConfig() {
|
|
49
|
+
const legacyPath = getLegacyUserConfigPath();
|
|
50
|
+
const destinationPath = getUserConfigPath();
|
|
51
|
+
if (!existsSync(legacyPath) || existsSync(destinationPath))
|
|
52
|
+
return false;
|
|
53
|
+
moveFile(legacyPath, destinationPath);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function migrateLogs() {
|
|
57
|
+
const legacyDir = getLegacyLogsDir();
|
|
58
|
+
if (!existsSync(legacyDir))
|
|
59
|
+
return 0;
|
|
60
|
+
const destinationDir = getLogsDir();
|
|
61
|
+
mkdirSync(destinationDir, { recursive: true });
|
|
62
|
+
let moved = 0;
|
|
63
|
+
for (const entry of readdirSync(legacyDir)) {
|
|
64
|
+
const source = join(legacyDir, entry);
|
|
65
|
+
const destination = join(destinationDir, entry);
|
|
66
|
+
if (existsSync(destination))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
moveFile(source, destination);
|
|
70
|
+
moved++;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.warn('Storage layout migration: log file move failed', {
|
|
74
|
+
source,
|
|
75
|
+
destination,
|
|
76
|
+
error: error instanceof Error ? error.message : String(error)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (readdirSync(legacyDir).length === 0) {
|
|
81
|
+
try {
|
|
82
|
+
rmdirSync(legacyDir);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.warn('Storage layout migration: legacy log directory cleanup failed', {
|
|
86
|
+
path: legacyDir,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return moved;
|
|
92
|
+
}
|
|
93
|
+
function removeStaleFlatLocks() {
|
|
94
|
+
const configDir = getOpencodeConfigDir();
|
|
95
|
+
let removed = 0;
|
|
96
|
+
for (const entry of readdirSync(configDir)) {
|
|
97
|
+
const isKeepAliveLock = entry === '.kiro-keepalive.lock';
|
|
98
|
+
const isRefreshLock = /^\.kiro-refresh-.*\.lock$/.test(entry);
|
|
99
|
+
if (!isKeepAliveLock && !isRefreshLock)
|
|
100
|
+
continue;
|
|
101
|
+
const path = join(configDir, entry);
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(path);
|
|
104
|
+
removed++;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.warn('Storage layout migration: stale lock cleanup failed', {
|
|
108
|
+
path,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return removed;
|
|
114
|
+
}
|
|
115
|
+
function acquireMigrationLock(path) {
|
|
116
|
+
let lastError;
|
|
117
|
+
for (let attempt = 0; attempt < MIGRATION_LOCK_ATTEMPTS; attempt++) {
|
|
118
|
+
try {
|
|
119
|
+
return lockfile.lockSync(path, MIGRATION_LOCK_OPTIONS);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
logger.warn('Storage layout migration: lock acquisition skipped', lastError);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function runMigration() {
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(getPluginDir(), { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
logger.warn('Storage layout migration: plugin directory creation failed', error instanceof Error ? error.message : String(error));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const migrationLockPath = getMigrationLockPath();
|
|
137
|
+
try {
|
|
138
|
+
closeSync(openSync(migrationLockPath, 'a'));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.warn('Storage layout migration: lock file creation failed', error instanceof Error ? error.message : String(error));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const release = acquireMigrationLock(migrationLockPath);
|
|
145
|
+
if (!release)
|
|
146
|
+
return;
|
|
147
|
+
let configMoved = false;
|
|
148
|
+
let logsMoved = 0;
|
|
149
|
+
let locksRemoved = 0;
|
|
150
|
+
try {
|
|
151
|
+
try {
|
|
152
|
+
configMoved = migrateUserConfig();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warn('Storage layout migration: config move failed', error instanceof Error ? error.message : String(error));
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
logsMoved = migrateLogs();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
logger.warn('Storage layout migration: logs migration failed', error instanceof Error ? error.message : String(error));
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
locksRemoved = removeStaleFlatLocks();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.warn('Storage layout migration: stale lock scan failed', error instanceof Error ? error.message : String(error));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
try {
|
|
172
|
+
release();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
logger.warn('Storage layout migration: lock release failed', error instanceof Error ? error.message : String(error));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
logger.log('Storage layout migration complete', {
|
|
179
|
+
configMoved,
|
|
180
|
+
logsMoved,
|
|
181
|
+
locksRemoved
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export function migrateSafeFilesIfNeeded() {
|
|
185
|
+
try {
|
|
186
|
+
runMigration();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger.warn('Storage layout migration failed non-fatally', error instanceof Error ? error.message : String(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -6,6 +6,7 @@ export function runMigrations(db) {
|
|
|
6
6
|
migrateOidcRegionColumn(db);
|
|
7
7
|
migrateDropRefreshTokenUniqueIndex(db);
|
|
8
8
|
migrateRemovedAccountsTable(db);
|
|
9
|
+
migrateOverageColumn(db);
|
|
9
10
|
}
|
|
10
11
|
function migrateToUniqueRefreshToken(db) {
|
|
11
12
|
const hasIndex = db
|
|
@@ -159,3 +160,10 @@ function migrateRemovedAccountsTable(db) {
|
|
|
159
160
|
// so kiro-cli auto-sync does not silently re-import them. Cleared only on deliberate login.
|
|
160
161
|
db.exec('CREATE TABLE IF NOT EXISTS removed_accounts (id TEXT PRIMARY KEY, removed_at INTEGER NOT NULL)');
|
|
161
162
|
}
|
|
163
|
+
function migrateOverageColumn(db) {
|
|
164
|
+
const columns = db.prepare('PRAGMA table_info(accounts)').all();
|
|
165
|
+
const names = new Set(columns.map((c) => c.name));
|
|
166
|
+
if (!names.has('overage_count')) {
|
|
167
|
+
db.exec('ALTER TABLE accounts ADD COLUMN overage_count INTEGER DEFAULT 0');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -33,7 +33,8 @@ export class KiroDatabase {
|
|
|
33
33
|
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
34
34
|
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
35
35
|
recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
|
|
36
|
-
used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
|
|
36
|
+
used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
|
|
37
|
+
overage_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
|
|
37
38
|
)
|
|
38
39
|
`);
|
|
39
40
|
runMigrations(this.db);
|
|
@@ -56,8 +57,8 @@ export class KiroDatabase {
|
|
|
56
57
|
id, email, auth_method, region, oidc_region, client_id, client_secret,
|
|
57
58
|
profile_arn, start_url, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
58
59
|
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
59
|
-
used_count, limit_count, last_sync
|
|
60
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
60
|
+
used_count, limit_count, overage_count, last_sync
|
|
61
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
61
62
|
ON CONFLICT(id) DO UPDATE SET
|
|
62
63
|
id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
|
|
63
64
|
region=excluded.region, oidc_region=excluded.oidc_region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
@@ -66,9 +67,10 @@ export class KiroDatabase {
|
|
|
66
67
|
rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
|
|
67
68
|
unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
|
|
68
69
|
fail_count=excluded.fail_count, last_used=excluded.last_used,
|
|
69
|
-
used_count=excluded.used_count, limit_count=excluded.limit_count,
|
|
70
|
+
used_count=excluded.used_count, limit_count=excluded.limit_count,
|
|
71
|
+
overage_count=excluded.overage_count, last_sync=excluded.last_sync
|
|
70
72
|
`)
|
|
71
|
-
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
|
|
73
|
+
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.overageCount || 0, acc.lastSync || 0);
|
|
72
74
|
}
|
|
73
75
|
isRemovedSync(id) {
|
|
74
76
|
return !!this.db.prepare('SELECT id FROM removed_accounts WHERE id = ?').get(id);
|
|
@@ -233,6 +235,7 @@ export class KiroDatabase {
|
|
|
233
235
|
lastUsed: row.last_used,
|
|
234
236
|
usedCount: row.used_count,
|
|
235
237
|
limitCount: row.limit_count,
|
|
238
|
+
overageCount: row.overage_count || 0,
|
|
236
239
|
lastSync: row.last_sync
|
|
237
240
|
};
|
|
238
241
|
}
|
package/dist/plugin/types.d.ts
CHANGED
package/dist/plugin/usage.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
import { KiroAuthDetails, ManagedAccount } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { KiroAuthDetails, ManagedAccount } from './types.js';
|
|
2
|
+
interface UsageSnapshot {
|
|
3
|
+
usedCount: number;
|
|
4
|
+
limitCount: number;
|
|
5
|
+
overageCount: number;
|
|
6
|
+
email?: string;
|
|
7
|
+
}
|
|
8
|
+
interface UsageUpdateMeta extends UsageSnapshot {
|
|
9
|
+
lastSync: number;
|
|
10
|
+
}
|
|
11
|
+
interface AccountUsageManager {
|
|
12
|
+
updateUsage(id: string, meta: UsageUpdateMeta): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function fetchUsageLimits(auth: KiroAuthDetails): Promise<UsageSnapshot>;
|
|
15
|
+
export declare function updateAccountQuota(account: ManagedAccount, usage: Partial<UsageSnapshot>, accountManager?: AccountUsageManager): void;
|
|
16
|
+
export {};
|
package/dist/plugin/usage.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const UsageLimitsResponseSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
usageBreakdownList: z
|
|
5
|
+
.array(z
|
|
6
|
+
.object({
|
|
7
|
+
currentUsage: z.number().optional(),
|
|
8
|
+
usageLimit: z.number().optional(),
|
|
9
|
+
currentOverages: z.number().optional(),
|
|
10
|
+
freeTrialInfo: z
|
|
11
|
+
.object({
|
|
12
|
+
currentUsage: z.number().optional(),
|
|
13
|
+
usageLimit: z.number().optional()
|
|
14
|
+
})
|
|
15
|
+
.passthrough()
|
|
16
|
+
.nullable()
|
|
17
|
+
.optional()
|
|
18
|
+
})
|
|
19
|
+
.passthrough())
|
|
20
|
+
.optional(),
|
|
21
|
+
userInfo: z.object({ email: z.string().optional() }).passthrough().optional()
|
|
22
|
+
})
|
|
23
|
+
.passthrough();
|
|
1
24
|
export async function fetchUsageLimits(auth) {
|
|
2
25
|
// Try different parameter combinations
|
|
3
26
|
const attempts = [
|
|
@@ -42,8 +65,8 @@ export async function fetchUsageLimits(auth) {
|
|
|
42
65
|
lastError = new Error(`Status: ${res.status}${errType ? ` (${errType})` : ''}${requestId ? ` [${requestId}]` : ''}: ${msg}`);
|
|
43
66
|
continue;
|
|
44
67
|
}
|
|
45
|
-
const data = await res.json();
|
|
46
|
-
let usedCount = 0, limitCount = 0;
|
|
68
|
+
const data = UsageLimitsResponseSchema.parse(await res.json());
|
|
69
|
+
let usedCount = 0, limitCount = 0, overageCount = 0;
|
|
47
70
|
if (Array.isArray(data.usageBreakdownList)) {
|
|
48
71
|
for (const s of data.usageBreakdownList) {
|
|
49
72
|
if (s.freeTrialInfo) {
|
|
@@ -52,9 +75,10 @@ export async function fetchUsageLimits(auth) {
|
|
|
52
75
|
}
|
|
53
76
|
usedCount += s.currentUsage || 0;
|
|
54
77
|
limitCount += s.usageLimit || 0;
|
|
78
|
+
overageCount += s.currentOverages || 0;
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
|
-
return { usedCount, limitCount, email: data.userInfo?.email };
|
|
81
|
+
return { usedCount, limitCount, overageCount, email: data.userInfo?.email };
|
|
58
82
|
}
|
|
59
83
|
catch (e) {
|
|
60
84
|
lastError = e instanceof Error ? e : new Error(String(e));
|
|
@@ -68,10 +92,14 @@ export function updateAccountQuota(account, usage, accountManager) {
|
|
|
68
92
|
const meta = {
|
|
69
93
|
usedCount: usage.usedCount || 0,
|
|
70
94
|
limitCount: usage.limitCount || 0,
|
|
95
|
+
overageCount: usage.overageCount || 0,
|
|
96
|
+
lastSync: Date.now(),
|
|
71
97
|
email: usage.email
|
|
72
98
|
};
|
|
73
99
|
account.usedCount = meta.usedCount;
|
|
74
100
|
account.limitCount = meta.limitCount;
|
|
101
|
+
account.overageCount = meta.overageCount;
|
|
102
|
+
account.lastSync = meta.lastSync;
|
|
75
103
|
if (usage.email)
|
|
76
104
|
account.email = usage.email;
|
|
77
105
|
if (accountManager)
|
package/dist/plugin.js
CHANGED
|
@@ -7,6 +7,7 @@ import { AccountRepository } from './infrastructure/database/account-repository.
|
|
|
7
7
|
import { AccountManager } from './plugin/accounts.js';
|
|
8
8
|
import { bootstrapAuthIfNeeded } from './plugin/auth-bootstrap.js';
|
|
9
9
|
import { loadConfig } from './plugin/config/index.js';
|
|
10
|
+
import { migrateSafeFilesIfNeeded } from './plugin/storage/migrate-layout.js';
|
|
10
11
|
const KIRO_PROVIDER_ID = 'kiro-auth';
|
|
11
12
|
let activeKeepAliveController = null;
|
|
12
13
|
let keepAliveTeardownRegistered = false;
|
|
@@ -34,6 +35,12 @@ export function __getActiveKeepAliveControllerForTest() {
|
|
|
34
35
|
return activeKeepAliveController;
|
|
35
36
|
}
|
|
36
37
|
export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
38
|
+
try {
|
|
39
|
+
migrateSafeFilesIfNeeded();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.warn('Kiro storage layout migration failed non-fatally:', error instanceof Error ? error.message : String(error));
|
|
43
|
+
}
|
|
37
44
|
const config = loadConfig(directory);
|
|
38
45
|
const showToast = (message, variant) => {
|
|
39
46
|
client.tui.showToast({ body: { message, variant } }).catch(() => { });
|
|
@@ -43,7 +50,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
43
50
|
const authHandler = new AuthHandler(config, repository);
|
|
44
51
|
const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy, {
|
|
45
52
|
quotaAvoidanceEnabled: config.quota_avoidance_enabled,
|
|
46
|
-
quotaReserveThreshold: config.quota_reserve_threshold
|
|
53
|
+
quotaReserveThreshold: config.quota_reserve_threshold,
|
|
54
|
+
stopOnOverage: config.stop_on_overage,
|
|
55
|
+
overageThreshold: config.overage_threshold
|
|
47
56
|
});
|
|
48
57
|
authHandler.setAccountManager(accountManager);
|
|
49
58
|
const requestHandler = new RequestHandler(accountManager, config, repository, client);
|