@sunerpy/opencode-kiro-auth 0.9.0 → 0.11.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 +30 -16
- package/dist/core/account/account-selector.js +1 -1
- package/dist/core/auth/idc-auth-method.js +2 -2
- package/dist/plugin/accounts.d.ts +6 -0
- package/dist/plugin/accounts.js +56 -6
- package/dist/plugin/config/loader.d.ts +1 -1
- package/dist/plugin/config/loader.js +12 -17
- package/dist/plugin/config/schema.d.ts +14 -0
- package/dist/plugin/config/schema.js +12 -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 +2 -3
- package/dist/plugin/storage/locked-operations.js +42 -17
- 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 +15 -5
- package/dist/plugin/storage/sqlite.d.ts +1 -0
- package/dist/plugin/storage/sqlite.js +10 -2
- 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
|
|
|
@@ -112,6 +112,11 @@ Paid-overage protection is on by default; see
|
|
|
112
112
|
[Overage protection](docs/CONFIGURATION.md#overage-protection) before disabling
|
|
113
113
|
`stop_on_overage`.
|
|
114
114
|
|
|
115
|
+
Running multiple OpenCode processes at once? `distribute_across_processes`
|
|
116
|
+
(default `true`) and `per_request_spread` (default `false`) control how load
|
|
117
|
+
spreads across accounts — see
|
|
118
|
+
[Account distribution across processes](docs/CONFIGURATION.md#account-distribution-across-processes).
|
|
119
|
+
|
|
115
120
|
## Multiple accounts & rotation
|
|
116
121
|
|
|
117
122
|
You can register more than one Kiro account and let the plugin spread
|
|
@@ -131,13 +136,13 @@ requests across them for combined quota and automatic failover.
|
|
|
131
136
|
account (option 1) is the supported multi-account path.
|
|
132
137
|
|
|
133
138
|
**Rotation strategy** — set `account_selection_strategy` in
|
|
134
|
-
`~/.config/opencode/kiro.json`:
|
|
139
|
+
`~/.config/opencode/kiro-auth-plugin/kiro.json`:
|
|
135
140
|
|
|
136
|
-
| Strategy | Behavior
|
|
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 |
|
|
141
|
+
| Strategy | Behavior | Default |
|
|
142
|
+
| -------------- | ---------------------------------------------------------------------- | ------- |
|
|
143
|
+
| `lowest-usage` | Each request picks the healthy account with the lowest used quota | ✅ |
|
|
144
|
+
| `round-robin` | Cycles through accounts in order | |
|
|
145
|
+
| `sticky` | Always uses the first account; switches only when it becomes unhealthy | |
|
|
141
146
|
|
|
142
147
|
**Automatic failover** requires no configuration: a rate-limited or 403
|
|
143
148
|
account is marked unhealthy and the next healthy account takes over. If every
|
|
@@ -165,7 +170,7 @@ per account, e.g. `929/10000`) and surfaces it in three places:
|
|
|
165
170
|
|
|
166
171
|
1. **Auth menu label** — running `opencode auth login` and selecting
|
|
167
172
|
`kiro-auth` shows `[current: <email> <used>/<limit> (<pct>%)]` on the
|
|
168
|
-
first login method; with multiple accounts they're joined with
|
|
173
|
+
first login method; with multiple accounts they're joined with `·`,
|
|
169
174
|
capped at 3 with a `+N more` suffix.
|
|
170
175
|
2. **Startup toast** — once per plugin init, a toast shows
|
|
171
176
|
`Kiro usage (<email>): <used>/<limit> (<pct>%)` a few seconds after
|
|
@@ -191,11 +196,11 @@ your `provider.kiro-auth.models` block.
|
|
|
191
196
|
Thinking budgets map to Kiro's native `effort` field automatically:
|
|
192
197
|
|
|
193
198
|
| OpenCode budget | Kiro effort |
|
|
194
|
-
|
|
|
195
|
-
| `<= 10000`
|
|
196
|
-
| `<= 20000`
|
|
197
|
-
| `<= 28000`
|
|
198
|
-
| `> 28000`
|
|
199
|
+
| --------------- | ----------- |
|
|
200
|
+
| `<= 10000` | `low` |
|
|
201
|
+
| `<= 20000` | `medium` |
|
|
202
|
+
| `<= 28000` | `high` |
|
|
203
|
+
| `> 28000` | `max` |
|
|
199
204
|
|
|
200
205
|
Details and the full JSON example: [docs/MODELS.md](docs/MODELS.md).
|
|
201
206
|
|
|
@@ -283,12 +288,21 @@ Releases are automated with
|
|
|
283
288
|
**Linux/macOS:**
|
|
284
289
|
|
|
285
290
|
- SQLite database: `~/.config/opencode/kiro.db`
|
|
286
|
-
- Plugin config: `~/.config/opencode/kiro.json`
|
|
291
|
+
- Plugin config: `~/.config/opencode/kiro-auth-plugin/kiro.json`
|
|
292
|
+
- Logs: `~/.config/opencode/kiro-auth-plugin/logs/`
|
|
293
|
+
- Refresh and keep-alive locks: `~/.config/opencode/kiro-auth-plugin/`
|
|
287
294
|
|
|
288
295
|
**Windows:**
|
|
289
296
|
|
|
290
297
|
- SQLite database: `%APPDATA%\opencode\kiro.db`
|
|
291
|
-
- Plugin config: `%APPDATA%\opencode\kiro.json`
|
|
298
|
+
- Plugin config: `%APPDATA%\opencode\kiro-auth-plugin\kiro.json`
|
|
299
|
+
- Logs: `%APPDATA%\opencode\kiro-auth-plugin\logs\`
|
|
300
|
+
- Refresh and keep-alive locks: `%APPDATA%\opencode\kiro-auth-plugin\`
|
|
301
|
+
|
|
302
|
+
Existing config, logs, and stale flat lock files are migrated automatically on
|
|
303
|
+
first startup; no action is needed. The SQLite database deliberately remains at
|
|
304
|
+
the flat `opencode/kiro.db` path because moving a live database during an upgrade
|
|
305
|
+
is unsafe.
|
|
292
306
|
|
|
293
307
|
## Acknowledgements
|
|
294
308
|
|
|
@@ -27,7 +27,7 @@ export class AccountSelector {
|
|
|
27
27
|
if (!acc) {
|
|
28
28
|
this.circuitBreakerTrips++;
|
|
29
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.json to continue with paid overage, or wait for the quota to reset.');
|
|
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
31
|
}
|
|
32
32
|
const wait = this.accountManager.getMinWaitTime();
|
|
33
33
|
if (wait > 0 && wait < 30000) {
|
|
@@ -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
|
};
|
|
@@ -8,6 +8,8 @@ export declare class AccountManager {
|
|
|
8
8
|
private lastUsageToastTime;
|
|
9
9
|
private rrCursor;
|
|
10
10
|
private stickyId?;
|
|
11
|
+
private startIndex;
|
|
12
|
+
private perRequestSpread;
|
|
11
13
|
private quotaAvoidanceEnabled;
|
|
12
14
|
private quotaReserveThreshold;
|
|
13
15
|
private stopOnOverage;
|
|
@@ -17,12 +19,16 @@ export declare class AccountManager {
|
|
|
17
19
|
quotaReserveThreshold?: number;
|
|
18
20
|
stopOnOverage?: boolean;
|
|
19
21
|
overageThreshold?: number;
|
|
22
|
+
startIndex?: number;
|
|
23
|
+
perRequestSpread?: boolean;
|
|
20
24
|
});
|
|
21
25
|
static loadFromDisk(strategy?: AccountSelectionStrategy, opts?: {
|
|
22
26
|
quotaAvoidanceEnabled?: boolean;
|
|
23
27
|
quotaReserveThreshold?: number;
|
|
24
28
|
stopOnOverage?: boolean;
|
|
25
29
|
overageThreshold?: number;
|
|
30
|
+
distributeAcrossProcesses?: boolean;
|
|
31
|
+
perRequestSpread?: boolean;
|
|
26
32
|
}): Promise<AccountManager>;
|
|
27
33
|
getAccountCount(): number;
|
|
28
34
|
getAccounts(): ManagedAccount[];
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -15,8 +15,10 @@ export class AccountManager {
|
|
|
15
15
|
strategy;
|
|
16
16
|
lastToastTime = 0;
|
|
17
17
|
lastUsageToastTime = 0;
|
|
18
|
-
rrCursor
|
|
18
|
+
rrCursor;
|
|
19
19
|
stickyId;
|
|
20
|
+
startIndex;
|
|
21
|
+
perRequestSpread;
|
|
20
22
|
quotaAvoidanceEnabled;
|
|
21
23
|
quotaReserveThreshold;
|
|
22
24
|
stopOnOverage;
|
|
@@ -29,6 +31,9 @@ export class AccountManager {
|
|
|
29
31
|
this.quotaReserveThreshold = opts?.quotaReserveThreshold ?? 0.95;
|
|
30
32
|
this.stopOnOverage = opts?.stopOnOverage ?? true;
|
|
31
33
|
this.overageThreshold = opts?.overageThreshold ?? 0;
|
|
34
|
+
this.startIndex = opts?.startIndex ?? 0;
|
|
35
|
+
this.perRequestSpread = opts?.perRequestSpread ?? false;
|
|
36
|
+
this.rrCursor = this.startIndex;
|
|
32
37
|
}
|
|
33
38
|
static async loadFromDisk(strategy, opts) {
|
|
34
39
|
const rows = kiroDb.getAccounts();
|
|
@@ -56,7 +61,23 @@ export class AccountManager {
|
|
|
56
61
|
overageCount: r.overage_count || 0,
|
|
57
62
|
lastSync: r.last_sync
|
|
58
63
|
}));
|
|
59
|
-
|
|
64
|
+
let startIndex = 0;
|
|
65
|
+
if (opts?.distributeAcrossProcesses !== false) {
|
|
66
|
+
try {
|
|
67
|
+
startIndex = await kiroDb.nextAssignmentIndex();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
logger.warn('assignment index failed, using 0', {
|
|
71
|
+
error: error instanceof Error ? error.message : String(error)
|
|
72
|
+
});
|
|
73
|
+
startIndex = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return new AccountManager(accounts, strategy || 'sticky', {
|
|
77
|
+
...opts,
|
|
78
|
+
startIndex,
|
|
79
|
+
perRequestSpread: opts?.perRequestSpread
|
|
80
|
+
});
|
|
60
81
|
}
|
|
61
82
|
getAccountCount() {
|
|
62
83
|
return this.accounts.length;
|
|
@@ -140,10 +161,26 @@ export class AccountManager {
|
|
|
140
161
|
const nearFull = available.filter((a) => ratio(a) >= this.quotaReserveThreshold);
|
|
141
162
|
candidatePool = ample.length > 0 ? ample : nearFull;
|
|
142
163
|
}
|
|
164
|
+
const sorted = [...this.accounts].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
165
|
+
const N = sorted.length;
|
|
143
166
|
let selected;
|
|
144
167
|
if (candidatePool.length > 0) {
|
|
145
|
-
if (this.
|
|
146
|
-
selected = candidatePool.
|
|
168
|
+
if (this.perRequestSpread) {
|
|
169
|
+
selected = [...candidatePool].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
170
|
+
}
|
|
171
|
+
else if (this.strategy === 'sticky') {
|
|
172
|
+
if (this.stickyId) {
|
|
173
|
+
selected = candidatePool.find((a) => a.id === this.stickyId);
|
|
174
|
+
}
|
|
175
|
+
if (!selected) {
|
|
176
|
+
for (let k = 0; k < N; k++) {
|
|
177
|
+
const candidate = sorted[(this.startIndex + k) % N];
|
|
178
|
+
if (candidate && candidatePool.some((account) => account.id === candidate.id)) {
|
|
179
|
+
selected = candidate;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
147
184
|
if (selected)
|
|
148
185
|
this.stickyId = selected.id;
|
|
149
186
|
}
|
|
@@ -151,8 +188,21 @@ export class AccountManager {
|
|
|
151
188
|
selected = candidatePool[this.rrCursor % candidatePool.length];
|
|
152
189
|
this.rrCursor++;
|
|
153
190
|
}
|
|
154
|
-
else
|
|
155
|
-
|
|
191
|
+
else {
|
|
192
|
+
const sortedIndexById = new Map(sorted.map((account, index) => [account.id, index]));
|
|
193
|
+
selected = [...candidatePool].sort((a, b) => {
|
|
194
|
+
const usageDifference = (a.usedCount || 0) - (b.usedCount || 0);
|
|
195
|
+
if (usageDifference !== 0)
|
|
196
|
+
return usageDifference;
|
|
197
|
+
const lastUsedDifference = (a.lastUsed || 0) - (b.lastUsed || 0);
|
|
198
|
+
if (lastUsedDifference !== 0)
|
|
199
|
+
return lastUsedDifference;
|
|
200
|
+
const aIndex = sortedIndexById.get(a.id) ?? 0;
|
|
201
|
+
const bIndex = sortedIndexById.get(b.id) ?? 0;
|
|
202
|
+
const aOffset = (((aIndex - this.startIndex) % N) + N) % N;
|
|
203
|
+
const bOffset = (((bIndex - this.startIndex) % N) + N) % N;
|
|
204
|
+
return aOffset - bOffset;
|
|
205
|
+
})[0];
|
|
156
206
|
}
|
|
157
207
|
}
|
|
158
208
|
if (!selected) {
|
|
@@ -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
|
}
|
|
@@ -19,6 +19,16 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
19
19
|
idc_region: z.ZodOptional<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"]>>;
|
|
20
20
|
idc_profile_arn: z.ZodOptional<z.ZodString>;
|
|
21
21
|
account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
|
|
22
|
+
/**
|
|
23
|
+
* Give each process a distinct start index so simultaneous starts spread
|
|
24
|
+
* across accounts.
|
|
25
|
+
*/
|
|
26
|
+
distribute_across_processes: z.ZodDefault<z.ZodBoolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Re-pick the lowest-usage account for every request instead of pinning one.
|
|
29
|
+
* Overrides sticky selection.
|
|
30
|
+
*/
|
|
31
|
+
per_request_spread: z.ZodDefault<z.ZodBoolean>;
|
|
22
32
|
/**
|
|
23
33
|
* Softly avoid accounts whose usage ratio is at/above
|
|
24
34
|
* `quota_reserve_threshold` when other accounts still have room. When ALL
|
|
@@ -84,6 +94,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
84
94
|
auto_effort_mapping: z.ZodDefault<z.ZodBoolean>;
|
|
85
95
|
}, "strip", z.ZodTypeAny, {
|
|
86
96
|
account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
|
|
97
|
+
distribute_across_processes: boolean;
|
|
98
|
+
per_request_spread: boolean;
|
|
87
99
|
quota_avoidance_enabled: boolean;
|
|
88
100
|
quota_reserve_threshold: number;
|
|
89
101
|
stop_on_overage: boolean;
|
|
@@ -115,6 +127,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
115
127
|
idc_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;
|
|
116
128
|
idc_profile_arn?: string | undefined;
|
|
117
129
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
130
|
+
distribute_across_processes?: boolean | undefined;
|
|
131
|
+
per_request_spread?: boolean | undefined;
|
|
118
132
|
quota_avoidance_enabled?: boolean | undefined;
|
|
119
133
|
quota_reserve_threshold?: number | undefined;
|
|
120
134
|
stop_on_overage?: boolean | undefined;
|
|
@@ -51,6 +51,16 @@ export const KiroConfigSchema = z.object({
|
|
|
51
51
|
idc_region: RegionSchema.optional(),
|
|
52
52
|
idc_profile_arn: z.string().optional(),
|
|
53
53
|
account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
|
|
54
|
+
/**
|
|
55
|
+
* Give each process a distinct start index so simultaneous starts spread
|
|
56
|
+
* across accounts.
|
|
57
|
+
*/
|
|
58
|
+
distribute_across_processes: z.boolean().default(true),
|
|
59
|
+
/**
|
|
60
|
+
* Re-pick the lowest-usage account for every request instead of pinning one.
|
|
61
|
+
* Overrides sticky selection.
|
|
62
|
+
*/
|
|
63
|
+
per_request_spread: z.boolean().default(false),
|
|
54
64
|
/**
|
|
55
65
|
* Softly avoid accounts whose usage ratio is at/above
|
|
56
66
|
* `quota_reserve_threshold` when other accounts still have room. When ALL
|
|
@@ -117,6 +127,8 @@ export const KiroConfigSchema = z.object({
|
|
|
117
127
|
});
|
|
118
128
|
export const DEFAULT_CONFIG = {
|
|
119
129
|
account_selection_strategy: 'lowest-usage',
|
|
130
|
+
distribute_across_processes: true,
|
|
131
|
+
per_request_spread: false,
|
|
120
132
|
quota_avoidance_enabled: true,
|
|
121
133
|
quota_reserve_threshold: 0.95,
|
|
122
134
|
stop_on_overage: true,
|
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,7 @@
|
|
|
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
|
|
4
|
-
export declare function getKeepAliveLockPath(): string;
|
|
4
|
+
export declare function withDatabaseLockSync<T>(dbPath: string, fn: () => T): T;
|
|
5
5
|
export declare function withDatabaseLock<T>(dbPath: string, fn: () => Promise<T>): Promise<T>;
|
|
6
6
|
export declare function withRefreshLock<T>(accountId: string, fn: () => Promise<T>): Promise<T>;
|
|
7
7
|
export declare function tryAcquireKeepAliveLock(): Promise<LockRelease | null>;
|
|
@@ -9,4 +9,3 @@ export declare function withKeepAliveLock<T>(fn: () => Promise<T>): Promise<T |
|
|
|
9
9
|
export declare function createDeterministicId(email: string, authMethod: string, clientId?: string, profileArn?: string): string;
|
|
10
10
|
export declare function mergeAccounts(existing: ManagedAccount[], incoming: ManagedAccount[]): ManagedAccount[];
|
|
11
11
|
export declare function deduplicateAccounts(accounts: ManagedAccount[]): ManagedAccount[];
|
|
12
|
-
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, promises as fs } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { existsSync, promises as fs, mkdirSync, writeFileSync } from 'node:fs';
|
|
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,22 +30,46 @@ const KEEP_ALIVE_LOCK_OPTIONS = {
|
|
|
29
30
|
retries: 0,
|
|
30
31
|
realpath: false
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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');
|
|
33
|
+
const SYNC_LOCK_OPTIONS = { stale: 10000, retries: 0, realpath: false };
|
|
34
|
+
const SYNC_LOCK_DEADLINE_MS = 10000;
|
|
35
|
+
function blockingBackoff(ms) {
|
|
36
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
41
37
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return join(getBaseDir(), `.kiro-refresh-${safeAccountId}.lock`);
|
|
38
|
+
function isLockContention(e) {
|
|
39
|
+
return typeof e === 'object' && e !== null && 'code' in e && e.code === 'ELOCKED';
|
|
45
40
|
}
|
|
46
|
-
export function
|
|
47
|
-
|
|
41
|
+
export function withDatabaseLockSync(dbPath, fn) {
|
|
42
|
+
if (!existsSync(dbPath)) {
|
|
43
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
44
|
+
writeFileSync(dbPath, '');
|
|
45
|
+
}
|
|
46
|
+
// proper-lockfile's sync API rejects a retries object, so bounded lock-acquisition backoff is
|
|
47
|
+
// done here to serialize the synchronous constructor/init (schema + migrations) across processes.
|
|
48
|
+
const deadline = Date.now() + SYNC_LOCK_DEADLINE_MS;
|
|
49
|
+
let release = null;
|
|
50
|
+
let attempt = 0;
|
|
51
|
+
for (;;) {
|
|
52
|
+
try {
|
|
53
|
+
release = lockfile.lockSync(dbPath, SYNC_LOCK_OPTIONS);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (!isLockContention(e) || Date.now() >= deadline)
|
|
58
|
+
throw e;
|
|
59
|
+
blockingBackoff(Math.min(100 * 2 ** attempt++, 500));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return fn();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
try {
|
|
67
|
+
release();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.warn('Failed to release lock:', e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
}
|
|
49
74
|
export async function withDatabaseLock(dbPath, fn) {
|
|
50
75
|
if (!existsSync(dbPath)) {
|
|
@@ -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
|
+
}
|
|
@@ -7,15 +7,22 @@ export function runMigrations(db) {
|
|
|
7
7
|
migrateDropRefreshTokenUniqueIndex(db);
|
|
8
8
|
migrateRemovedAccountsTable(db);
|
|
9
9
|
migrateOverageColumn(db);
|
|
10
|
+
migratePluginMetaTable(db);
|
|
10
11
|
}
|
|
11
12
|
function migrateToUniqueRefreshToken(db) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
.get();
|
|
15
|
-
if (hasIndex)
|
|
13
|
+
const indexProbe = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_refresh_token_unique'");
|
|
14
|
+
if (indexProbe.get())
|
|
16
15
|
return;
|
|
17
|
-
|
|
16
|
+
// BEGIN IMMEDIATE (not deferred BEGIN): a deferred read-then-write txn raises unrecoverable
|
|
17
|
+
// SQLITE_BUSY_SNAPSHOT (busy_timeout cannot retry it) if another connection writes after the
|
|
18
|
+
// read snapshot. Taking the write lock at BEGIN makes concurrent processes serialize via
|
|
19
|
+
// busy_timeout instead of racing into a snapshot conflict.
|
|
20
|
+
db.exec('BEGIN IMMEDIATE');
|
|
18
21
|
try {
|
|
22
|
+
if (indexProbe.get()) {
|
|
23
|
+
db.exec('COMMIT');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
19
26
|
const duplicates = db
|
|
20
27
|
.prepare(`
|
|
21
28
|
SELECT refresh_token, COUNT(*) as count
|
|
@@ -167,3 +174,6 @@ function migrateOverageColumn(db) {
|
|
|
167
174
|
db.exec('ALTER TABLE accounts ADD COLUMN overage_count INTEGER DEFAULT 0');
|
|
168
175
|
}
|
|
169
176
|
}
|
|
177
|
+
function migratePluginMetaTable(db) {
|
|
178
|
+
db.exec('CREATE TABLE IF NOT EXISTS plugin_meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL)');
|
|
179
|
+
}
|
|
@@ -18,6 +18,7 @@ export declare class KiroDatabase {
|
|
|
18
18
|
isAccountRemoved(id: string): Promise<boolean>;
|
|
19
19
|
clearRemovedAccount(id: string): Promise<void>;
|
|
20
20
|
listRemovedAccounts(): Promise<string[]>;
|
|
21
|
+
nextAssignmentIndex(): Promise<number>;
|
|
21
22
|
markAccountsUnhealthy(ids: string[], reason: string): Promise<void>;
|
|
22
23
|
private rowToAccount;
|
|
23
24
|
close(): void;
|
|
@@ -2,7 +2,7 @@ import Database from 'libsql';
|
|
|
2
2
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { deduplicateAccounts, mergeAccounts, withDatabaseLock } from './locked-operations.js';
|
|
5
|
+
import { deduplicateAccounts, mergeAccounts, withDatabaseLock, withDatabaseLockSync } from './locked-operations.js';
|
|
6
6
|
import { runMigrations } from './migrations.js';
|
|
7
7
|
function getBaseDir() {
|
|
8
8
|
const p = process.platform;
|
|
@@ -21,7 +21,7 @@ export class KiroDatabase {
|
|
|
21
21
|
mkdirSync(dir, { recursive: true });
|
|
22
22
|
this.db = new Database(path);
|
|
23
23
|
this.db.pragma('busy_timeout = 5000');
|
|
24
|
-
this.init();
|
|
24
|
+
withDatabaseLockSync(this.path, () => this.init());
|
|
25
25
|
}
|
|
26
26
|
init() {
|
|
27
27
|
this.db.pragma('journal_mode = WAL');
|
|
@@ -185,6 +185,14 @@ export class KiroDatabase {
|
|
|
185
185
|
async listRemovedAccounts() {
|
|
186
186
|
return this.db.prepare('SELECT id FROM removed_accounts').all().map((r) => r.id);
|
|
187
187
|
}
|
|
188
|
+
async nextAssignmentIndex() {
|
|
189
|
+
return withDatabaseLock(this.path, async () => {
|
|
190
|
+
const row = this.db
|
|
191
|
+
.prepare("INSERT INTO plugin_meta(key,value) VALUES('assignment_cursor',0) ON CONFLICT(key) DO UPDATE SET value=value+1 RETURNING value")
|
|
192
|
+
.get();
|
|
193
|
+
return row?.value ?? 0;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
188
196
|
async markAccountsUnhealthy(ids, reason) {
|
|
189
197
|
if (ids.length === 0)
|
|
190
198
|
return;
|
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(() => { });
|
|
@@ -45,7 +52,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
45
52
|
quotaAvoidanceEnabled: config.quota_avoidance_enabled,
|
|
46
53
|
quotaReserveThreshold: config.quota_reserve_threshold,
|
|
47
54
|
stopOnOverage: config.stop_on_overage,
|
|
48
|
-
overageThreshold: config.overage_threshold
|
|
55
|
+
overageThreshold: config.overage_threshold,
|
|
56
|
+
distributeAcrossProcesses: config.distribute_across_processes,
|
|
57
|
+
perRequestSpread: config.per_request_spread
|
|
49
58
|
});
|
|
50
59
|
authHandler.setAccountManager(accountManager);
|
|
51
60
|
const requestHandler = new RequestHandler(accountManager, config, repository, client);
|