@zhafron/opencode-kiro-auth 1.3.1 → 1.4.1
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/dist/plugin/accounts.d.ts +3 -4
- package/dist/plugin/accounts.js +23 -28
- package/dist/plugin/storage/migration.js +19 -20
- package/dist/plugin/storage/sqlite.d.ts +0 -3
- package/dist/plugin/storage/sqlite.js +63 -39
- package/dist/plugin/sync/kiro-cli.js +39 -20
- package/dist/plugin/types.d.ts +2 -2
- package/dist/plugin/usage.js +3 -3
- package/dist/plugin.js +71 -46
- package/package.json +1 -1
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount
|
|
1
|
+
import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount } from './types';
|
|
2
2
|
export declare function generateAccountId(): string;
|
|
3
3
|
export declare function createDeterministicAccountId(email: string, method: string, clientId?: string, profileArn?: string): string;
|
|
4
4
|
export declare class AccountManager {
|
|
5
5
|
private accounts;
|
|
6
|
-
private usage;
|
|
7
6
|
private cursor;
|
|
8
7
|
private strategy;
|
|
9
8
|
private lastToastTime;
|
|
10
9
|
private lastUsageToastTime;
|
|
11
|
-
constructor(accounts: ManagedAccount[],
|
|
10
|
+
constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
|
|
12
11
|
static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
|
|
13
12
|
getAccountCount(): number;
|
|
14
13
|
getAccounts(): ManagedAccount[];
|
|
@@ -19,7 +18,7 @@ export declare class AccountManager {
|
|
|
19
18
|
updateUsage(id: string, meta: {
|
|
20
19
|
usedCount: number;
|
|
21
20
|
limitCount: number;
|
|
22
|
-
|
|
21
|
+
email?: string;
|
|
23
22
|
}): void;
|
|
24
23
|
addAccount(a: ManagedAccount): void;
|
|
25
24
|
removeAccount(a: ManagedAccount): void;
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -12,32 +12,20 @@ export function createDeterministicAccountId(email, method, clientId, profileArn
|
|
|
12
12
|
}
|
|
13
13
|
export class AccountManager {
|
|
14
14
|
accounts;
|
|
15
|
-
usage;
|
|
16
15
|
cursor;
|
|
17
16
|
strategy;
|
|
18
17
|
lastToastTime = 0;
|
|
19
18
|
lastUsageToastTime = 0;
|
|
20
|
-
constructor(accounts,
|
|
19
|
+
constructor(accounts, strategy = 'sticky') {
|
|
21
20
|
this.accounts = accounts;
|
|
22
|
-
this.usage = usage;
|
|
23
21
|
this.cursor = 0;
|
|
24
22
|
this.strategy = strategy;
|
|
25
|
-
for (const a of this.accounts) {
|
|
26
|
-
const m = this.usage[a.id];
|
|
27
|
-
if (m) {
|
|
28
|
-
a.usedCount = m.usedCount;
|
|
29
|
-
a.limitCount = m.limitCount;
|
|
30
|
-
a.realEmail = m.realEmail;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
23
|
}
|
|
34
24
|
static async loadFromDisk(strategy) {
|
|
35
25
|
const rows = kiroDb.getAccounts();
|
|
36
|
-
const usage = kiroDb.getUsage();
|
|
37
26
|
const accounts = rows.map((r) => ({
|
|
38
27
|
id: r.id,
|
|
39
28
|
email: r.email,
|
|
40
|
-
realEmail: r.real_email,
|
|
41
29
|
authMethod: r.auth_method,
|
|
42
30
|
region: r.region,
|
|
43
31
|
clientId: r.client_id,
|
|
@@ -50,9 +38,12 @@ export class AccountManager {
|
|
|
50
38
|
isHealthy: r.is_healthy === 1,
|
|
51
39
|
unhealthyReason: r.unhealthy_reason,
|
|
52
40
|
recoveryTime: r.recovery_time,
|
|
53
|
-
|
|
41
|
+
failCount: r.fail_count || 0,
|
|
42
|
+
lastUsed: r.last_used,
|
|
43
|
+
usedCount: r.used_count,
|
|
44
|
+
limitCount: r.limit_count
|
|
54
45
|
}));
|
|
55
|
-
return new AccountManager(accounts,
|
|
46
|
+
return new AccountManager(accounts, strategy || 'sticky');
|
|
56
47
|
}
|
|
57
48
|
getAccountCount() {
|
|
58
49
|
return this.accounts.length;
|
|
@@ -60,13 +51,13 @@ export class AccountManager {
|
|
|
60
51
|
getAccounts() {
|
|
61
52
|
return [...this.accounts];
|
|
62
53
|
}
|
|
63
|
-
shouldShowToast(debounce =
|
|
54
|
+
shouldShowToast(debounce = 10000) {
|
|
64
55
|
if (Date.now() - this.lastToastTime < debounce)
|
|
65
56
|
return false;
|
|
66
57
|
this.lastToastTime = Date.now();
|
|
67
58
|
return true;
|
|
68
59
|
}
|
|
69
|
-
shouldShowUsageToast(debounce =
|
|
60
|
+
shouldShowUsageToast(debounce = 10000) {
|
|
70
61
|
if (Date.now() - this.lastUsageToastTime < debounce)
|
|
71
62
|
return false;
|
|
72
63
|
this.lastUsageToastTime = Date.now();
|
|
@@ -81,7 +72,7 @@ export class AccountManager {
|
|
|
81
72
|
const now = Date.now();
|
|
82
73
|
const available = this.accounts.filter((a) => {
|
|
83
74
|
if (!a.isHealthy) {
|
|
84
|
-
if (a.recoveryTime && now >= a.recoveryTime) {
|
|
75
|
+
if (a.failCount < 3 && a.recoveryTime && now >= a.recoveryTime) {
|
|
85
76
|
a.isHealthy = true;
|
|
86
77
|
delete a.unhealthyReason;
|
|
87
78
|
delete a.recoveryTime;
|
|
@@ -106,7 +97,7 @@ export class AccountManager {
|
|
|
106
97
|
}
|
|
107
98
|
if (!selected) {
|
|
108
99
|
const fallback = this.accounts
|
|
109
|
-
.filter((a) => !a.isHealthy)
|
|
100
|
+
.filter((a) => !a.isHealthy && a.failCount < 3)
|
|
110
101
|
.sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
111
102
|
if (fallback) {
|
|
112
103
|
fallback.isHealthy = true;
|
|
@@ -128,11 +119,11 @@ export class AccountManager {
|
|
|
128
119
|
if (a) {
|
|
129
120
|
a.usedCount = meta.usedCount;
|
|
130
121
|
a.limitCount = meta.limitCount;
|
|
131
|
-
if (meta.
|
|
132
|
-
a.
|
|
122
|
+
if (meta.email)
|
|
123
|
+
a.email = meta.email;
|
|
124
|
+
a.failCount = 0;
|
|
125
|
+
kiroDb.upsertAccount(a);
|
|
133
126
|
}
|
|
134
|
-
this.usage[id] = { ...meta, lastSync: Date.now() };
|
|
135
|
-
kiroDb.upsertUsage(id, this.usage[id]);
|
|
136
127
|
}
|
|
137
128
|
addAccount(a) {
|
|
138
129
|
const i = this.accounts.findIndex((x) => x.id === a.id);
|
|
@@ -147,7 +138,6 @@ export class AccountManager {
|
|
|
147
138
|
if (removedIndex === -1)
|
|
148
139
|
return;
|
|
149
140
|
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
150
|
-
delete this.usage[a.id];
|
|
151
141
|
kiroDb.deleteAccount(a.id);
|
|
152
142
|
if (this.accounts.length === 0)
|
|
153
143
|
this.cursor = 0;
|
|
@@ -162,14 +152,15 @@ export class AccountManager {
|
|
|
162
152
|
acc.accessToken = auth.access;
|
|
163
153
|
acc.expiresAt = auth.expires;
|
|
164
154
|
acc.lastUsed = Date.now();
|
|
165
|
-
if (auth.email
|
|
166
|
-
acc.
|
|
155
|
+
if (auth.email)
|
|
156
|
+
acc.email = auth.email;
|
|
167
157
|
const p = decodeRefreshToken(auth.refresh);
|
|
168
158
|
acc.refreshToken = p.refreshToken;
|
|
169
159
|
if (p.profileArn)
|
|
170
160
|
acc.profileArn = p.profileArn;
|
|
171
161
|
if (p.clientId)
|
|
172
162
|
acc.clientId = p.clientId;
|
|
163
|
+
acc.failCount = 0;
|
|
173
164
|
kiroDb.upsertAccount(acc);
|
|
174
165
|
writeToKiroCli(acc).catch(() => { });
|
|
175
166
|
}
|
|
@@ -184,9 +175,13 @@ export class AccountManager {
|
|
|
184
175
|
markUnhealthy(a, reason, recovery) {
|
|
185
176
|
const acc = this.accounts.find((x) => x.id === a.id);
|
|
186
177
|
if (acc) {
|
|
187
|
-
acc.
|
|
178
|
+
acc.failCount = (acc.failCount || 0) + 1;
|
|
188
179
|
acc.unhealthyReason = reason;
|
|
189
|
-
acc.
|
|
180
|
+
acc.lastUsed = Date.now();
|
|
181
|
+
if (acc.failCount >= 3) {
|
|
182
|
+
acc.isHealthy = false;
|
|
183
|
+
acc.recoveryTime = recovery || Date.now() + 3600000;
|
|
184
|
+
}
|
|
190
185
|
kiroDb.upsertAccount(acc);
|
|
191
186
|
}
|
|
192
187
|
}
|
|
@@ -4,10 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import * as logger from '../logger';
|
|
5
5
|
import { kiroDb } from './sqlite';
|
|
6
6
|
function getBaseDir() {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
7
|
+
const p = process.platform;
|
|
8
|
+
if (p === 'win32')
|
|
9
9
|
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
10
|
-
}
|
|
11
10
|
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
|
|
12
11
|
}
|
|
13
12
|
export async function migrateJsonToSqlite() {
|
|
@@ -19,32 +18,32 @@ export async function migrateJsonToSqlite() {
|
|
|
19
18
|
.access(accPath)
|
|
20
19
|
.then(() => true)
|
|
21
20
|
.catch(() => false);
|
|
21
|
+
const useExists = await fs
|
|
22
|
+
.access(usePath)
|
|
23
|
+
.then(() => true)
|
|
24
|
+
.catch(() => false);
|
|
22
25
|
if (accExists) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const accData = JSON.parse(await fs.readFile(accPath, 'utf-8'));
|
|
27
|
+
const useData = useExists ? JSON.parse(await fs.readFile(usePath, 'utf-8')) : { usage: {} };
|
|
28
|
+
if (accData.accounts && Array.isArray(accData.accounts)) {
|
|
29
|
+
for (const acc of accData.accounts) {
|
|
30
|
+
const usage = useData.usage[acc.id] || {};
|
|
26
31
|
kiroDb.upsertAccount({
|
|
27
32
|
...acc,
|
|
33
|
+
email: acc.realEmail || acc.email,
|
|
28
34
|
rateLimitResetTime: acc.rateLimitResetTime || 0,
|
|
29
35
|
isHealthy: acc.isHealthy !== false,
|
|
30
|
-
|
|
36
|
+
failCount: 0,
|
|
37
|
+
lastUsed: acc.lastUsed || 0,
|
|
38
|
+
usedCount: usage.usedCount || 0,
|
|
39
|
+
limitCount: usage.limitCount || 0,
|
|
40
|
+
lastSync: usage.lastSync || 0
|
|
31
41
|
});
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
await fs.rename(accPath, accPath + '.bak');
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.access(usePath)
|
|
38
|
-
.then(() => true)
|
|
39
|
-
.catch(() => false);
|
|
40
|
-
if (useExists) {
|
|
41
|
-
const data = JSON.parse(await fs.readFile(usePath, 'utf-8'));
|
|
42
|
-
if (data.usage && typeof data.usage === 'object') {
|
|
43
|
-
for (const [id, meta] of Object.entries(data.usage)) {
|
|
44
|
-
kiroDb.upsertUsage(id, meta);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
await fs.rename(usePath, usePath + '.bak');
|
|
45
|
+
if (useExists)
|
|
46
|
+
await fs.rename(usePath, usePath + '.bak');
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
catch (e) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { UsageMetadata } from '../types';
|
|
2
1
|
export declare const DB_PATH: string;
|
|
3
2
|
export declare class KiroDatabase {
|
|
4
3
|
private db;
|
|
@@ -7,8 +6,6 @@ export declare class KiroDatabase {
|
|
|
7
6
|
getAccounts(): any[];
|
|
8
7
|
upsertAccount(acc: any): void;
|
|
9
8
|
deleteAccount(id: string): void;
|
|
10
|
-
getUsage(): Record<string, UsageMetadata>;
|
|
11
|
-
upsertUsage(id: string, meta: UsageMetadata): void;
|
|
12
9
|
close(): void;
|
|
13
10
|
}
|
|
14
11
|
export declare const kiroDb: KiroDatabase;
|
|
@@ -23,20 +23,66 @@ export class KiroDatabase {
|
|
|
23
23
|
this.db.run('PRAGMA journal_mode = WAL');
|
|
24
24
|
this.db.run(`
|
|
25
25
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
26
|
-
id TEXT PRIMARY KEY, email TEXT NOT NULL,
|
|
26
|
+
id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
|
|
27
27
|
region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
28
28
|
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
29
29
|
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
30
|
-
recovery_time INTEGER, last_used INTEGER DEFAULT 0
|
|
31
|
-
|
|
32
|
-
`);
|
|
33
|
-
this.db.run(`
|
|
34
|
-
CREATE TABLE IF NOT EXISTS usage (
|
|
35
|
-
account_id TEXT PRIMARY KEY, used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
|
|
36
|
-
real_email TEXT, last_sync INTEGER,
|
|
37
|
-
FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
|
30
|
+
recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
|
|
31
|
+
used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
|
|
38
32
|
)
|
|
39
33
|
`);
|
|
34
|
+
const columns = this.db.prepare('PRAGMA table_info(accounts)').all();
|
|
35
|
+
const names = new Set(columns.map((c) => c.name));
|
|
36
|
+
if (names.has('real_email')) {
|
|
37
|
+
this.db.run('BEGIN TRANSACTION');
|
|
38
|
+
try {
|
|
39
|
+
this.db.run("UPDATE accounts SET email = real_email WHERE real_email IS NOT NULL AND real_email != '' AND email LIKE 'builder-id@aws.amazon.com%'");
|
|
40
|
+
this.db.run(`
|
|
41
|
+
CREATE TABLE accounts_new (
|
|
42
|
+
id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
|
|
43
|
+
region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
44
|
+
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
45
|
+
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
46
|
+
recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
|
|
47
|
+
used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
this.db.run(`
|
|
51
|
+
INSERT INTO accounts_new (id, email, auth_method, region, client_id, client_secret, profile_arn, refresh_token, access_token, expires_at, rate_limit_reset, is_healthy, unhealthy_reason, recovery_time, fail_count, last_used, used_count, limit_count, last_sync)
|
|
52
|
+
SELECT id, email, auth_method, region, client_id, client_secret, profile_arn, refresh_token, access_token, expires_at, COALESCE(rate_limit_reset, 0), COALESCE(is_healthy, 1), unhealthy_reason, recovery_time, COALESCE(fail_count, 0), COALESCE(last_used, 0), 0, 0, 0 FROM accounts
|
|
53
|
+
`);
|
|
54
|
+
this.db.run('DROP TABLE accounts');
|
|
55
|
+
this.db.run('ALTER TABLE accounts_new RENAME TO accounts');
|
|
56
|
+
this.db.run('COMMIT');
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
this.db.run('ROLLBACK');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const needed = {
|
|
64
|
+
fail_count: 'INTEGER DEFAULT 0',
|
|
65
|
+
used_count: 'INTEGER DEFAULT 0',
|
|
66
|
+
limit_count: 'INTEGER DEFAULT 0',
|
|
67
|
+
last_sync: 'INTEGER DEFAULT 0'
|
|
68
|
+
};
|
|
69
|
+
for (const [n, d] of Object.entries(needed)) {
|
|
70
|
+
if (!names.has(n))
|
|
71
|
+
this.db.run(`ALTER TABLE accounts ADD COLUMN ${n} ${d}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const hasUsageTable = this.db
|
|
75
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='usage'")
|
|
76
|
+
.get();
|
|
77
|
+
if (hasUsageTable) {
|
|
78
|
+
this.db.run(`
|
|
79
|
+
UPDATE accounts SET
|
|
80
|
+
used_count = COALESCE((SELECT used_count FROM usage WHERE usage.account_id = accounts.id), used_count),
|
|
81
|
+
limit_count = COALESCE((SELECT limit_count FROM usage WHERE usage.account_id = accounts.id), limit_count),
|
|
82
|
+
last_sync = COALESCE((SELECT last_sync FROM usage WHERE usage.account_id = accounts.id), last_sync)
|
|
83
|
+
`);
|
|
84
|
+
this.db.run('DROP TABLE usage');
|
|
85
|
+
}
|
|
40
86
|
}
|
|
41
87
|
getAccounts() {
|
|
42
88
|
return this.db.prepare('SELECT * FROM accounts').all();
|
|
@@ -45,48 +91,26 @@ export class KiroDatabase {
|
|
|
45
91
|
this.db
|
|
46
92
|
.prepare(`
|
|
47
93
|
INSERT INTO accounts (
|
|
48
|
-
id, email,
|
|
94
|
+
id, email, auth_method, region, client_id, client_secret,
|
|
49
95
|
profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
50
|
-
is_healthy, unhealthy_reason, recovery_time, last_used
|
|
51
|
-
|
|
96
|
+
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
97
|
+
used_count, limit_count, last_sync
|
|
98
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
99
|
ON CONFLICT(id) DO UPDATE SET
|
|
53
|
-
email=excluded.email,
|
|
100
|
+
email=excluded.email, auth_method=excluded.auth_method,
|
|
54
101
|
region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
55
102
|
profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
|
|
56
103
|
access_token=excluded.access_token, expires_at=excluded.expires_at,
|
|
57
104
|
rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
|
|
58
105
|
unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
|
|
59
|
-
last_used=excluded.last_used
|
|
106
|
+
fail_count=excluded.fail_count, last_used=excluded.last_used,
|
|
107
|
+
used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
|
|
60
108
|
`)
|
|
61
|
-
.run(acc.id, acc.email, acc.
|
|
109
|
+
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || 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);
|
|
62
110
|
}
|
|
63
111
|
deleteAccount(id) {
|
|
64
112
|
this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
|
|
65
113
|
}
|
|
66
|
-
getUsage() {
|
|
67
|
-
const rows = this.db.prepare('SELECT * FROM usage').all();
|
|
68
|
-
const usage = {};
|
|
69
|
-
for (const r of rows) {
|
|
70
|
-
usage[r.account_id] = {
|
|
71
|
-
usedCount: r.used_count,
|
|
72
|
-
limitCount: r.limit_count,
|
|
73
|
-
realEmail: r.real_email,
|
|
74
|
-
lastSync: r.last_sync
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
return usage;
|
|
78
|
-
}
|
|
79
|
-
upsertUsage(id, meta) {
|
|
80
|
-
this.db
|
|
81
|
-
.prepare(`
|
|
82
|
-
INSERT INTO usage (account_id, used_count, limit_count, real_email, last_sync)
|
|
83
|
-
VALUES (?, ?, ?, ?, ?)
|
|
84
|
-
ON CONFLICT(account_id) DO UPDATE SET
|
|
85
|
-
used_count=excluded.used_count, limit_count=excluded.limit_count,
|
|
86
|
-
real_email=excluded.real_email, last_sync=excluded.last_sync
|
|
87
|
-
`)
|
|
88
|
-
.run(id, meta.usedCount, meta.limitCount, meta.realEmail || null, meta.lastSync);
|
|
89
|
-
}
|
|
90
114
|
close() {
|
|
91
115
|
this.db.close();
|
|
92
116
|
}
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { createDeterministicAccountId } from '../accounts';
|
|
6
6
|
import * as logger from '../logger';
|
|
7
7
|
import { kiroDb } from '../storage/sqlite';
|
|
8
|
+
import { fetchUsageLimits } from '../usage';
|
|
8
9
|
function getCliDbPath() {
|
|
9
10
|
const p = platform();
|
|
10
11
|
if (p === 'win32')
|
|
@@ -32,8 +33,8 @@ export async function syncFromKiroCli() {
|
|
|
32
33
|
}
|
|
33
34
|
if (!data.access_token)
|
|
34
35
|
continue;
|
|
35
|
-
const email = data.email || 'cli-account@kiro.dev';
|
|
36
36
|
const authMethod = row.key.includes('odic') ? 'idc' : 'desktop';
|
|
37
|
+
const region = data.region || 'us-east-1';
|
|
37
38
|
const clientId = data.client_id ||
|
|
38
39
|
(authMethod === 'idc'
|
|
39
40
|
? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
|
|
@@ -44,25 +45,43 @@ export async function syncFromKiroCli() {
|
|
|
44
45
|
? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
|
|
45
46
|
.client_secret
|
|
46
47
|
: undefined);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
try {
|
|
49
|
+
const u = await fetchUsageLimits({
|
|
50
|
+
refresh: '',
|
|
51
|
+
access: data.access_token,
|
|
52
|
+
expires: 0,
|
|
53
|
+
authMethod,
|
|
54
|
+
region,
|
|
55
|
+
clientId,
|
|
56
|
+
clientSecret
|
|
57
|
+
});
|
|
58
|
+
const email = u.email;
|
|
59
|
+
if (!email)
|
|
60
|
+
continue;
|
|
61
|
+
const id = createDeterministicAccountId(email, authMethod, clientId, data.profile_arn);
|
|
62
|
+
const existing = kiroDb.getAccounts().find((a) => a.id === id);
|
|
63
|
+
const cliExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() : 0;
|
|
64
|
+
if (existing && existing.is_healthy === 1 && existing.expires_at >= cliExpiresAt)
|
|
65
|
+
continue;
|
|
66
|
+
kiroDb.upsertAccount({
|
|
67
|
+
id,
|
|
68
|
+
email,
|
|
69
|
+
authMethod,
|
|
70
|
+
region,
|
|
71
|
+
clientId,
|
|
72
|
+
clientSecret,
|
|
73
|
+
profileArn: data.profile_arn,
|
|
74
|
+
refreshToken: data.refresh_token,
|
|
75
|
+
accessToken: data.access_token,
|
|
76
|
+
expiresAt: cliExpiresAt || Date.now() + 3600000,
|
|
77
|
+
isHealthy: 1,
|
|
78
|
+
failCount: 0,
|
|
79
|
+
usedCount: u.usedCount,
|
|
80
|
+
limitCount: u.limitCount,
|
|
81
|
+
lastSync: Date.now()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
cliDb.close();
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -21,7 +21,6 @@ export interface RefreshParts {
|
|
|
21
21
|
export interface ManagedAccount {
|
|
22
22
|
id: string;
|
|
23
23
|
email: string;
|
|
24
|
-
realEmail?: string;
|
|
25
24
|
authMethod: KiroAuthMethod;
|
|
26
25
|
region: KiroRegion;
|
|
27
26
|
clientId?: string;
|
|
@@ -34,14 +33,15 @@ export interface ManagedAccount {
|
|
|
34
33
|
isHealthy: boolean;
|
|
35
34
|
unhealthyReason?: string;
|
|
36
35
|
recoveryTime?: number;
|
|
36
|
+
failCount: number;
|
|
37
37
|
usedCount?: number;
|
|
38
38
|
limitCount?: number;
|
|
39
|
+
lastSync?: number;
|
|
39
40
|
lastUsed?: number;
|
|
40
41
|
}
|
|
41
42
|
export interface UsageMetadata {
|
|
42
43
|
usedCount: number;
|
|
43
44
|
limitCount: number;
|
|
44
|
-
realEmail?: string;
|
|
45
45
|
lastSync: number;
|
|
46
46
|
}
|
|
47
47
|
export interface CodeWhispererMessage {
|
package/dist/plugin/usage.js
CHANGED
|
@@ -34,12 +34,12 @@ export function updateAccountQuota(account, usage, accountManager) {
|
|
|
34
34
|
const meta = {
|
|
35
35
|
usedCount: usage.usedCount || 0,
|
|
36
36
|
limitCount: usage.limitCount || 0,
|
|
37
|
-
|
|
37
|
+
email: usage.email
|
|
38
38
|
};
|
|
39
39
|
account.usedCount = meta.usedCount;
|
|
40
40
|
account.limitCount = meta.limitCount;
|
|
41
|
-
if (
|
|
42
|
-
account.
|
|
41
|
+
if (usage.email)
|
|
42
|
+
account.email = usage.email;
|
|
43
43
|
if (accountManager)
|
|
44
44
|
accountManager.updateUsage(account.id, meta);
|
|
45
45
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
2
|
import { KIRO_CONSTANTS } from './constants';
|
|
3
|
-
import { accessTokenExpired
|
|
3
|
+
import { accessTokenExpired } from './kiro/auth';
|
|
4
4
|
import { authorizeKiroIDC } from './kiro/oauth-idc';
|
|
5
|
-
import { AccountManager,
|
|
5
|
+
import { AccountManager, createDeterministicAccountId } from './plugin/accounts';
|
|
6
6
|
import { promptAddAnotherAccount, promptLoginMode } from './plugin/cli';
|
|
7
7
|
import { loadConfig } from './plugin/config';
|
|
8
8
|
import { KiroTokenRefreshError } from './plugin/errors';
|
|
@@ -54,6 +54,22 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
54
54
|
if (config.auto_sync_kiro_cli)
|
|
55
55
|
await syncFromKiroCli();
|
|
56
56
|
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
57
|
+
const allAccs = am.getAccounts();
|
|
58
|
+
for (const acc of allAccs) {
|
|
59
|
+
if (acc.isHealthy && (!acc.lastSync || Date.now() - acc.lastSync > 3600000)) {
|
|
60
|
+
try {
|
|
61
|
+
const auth = am.toAuthDetails(acc);
|
|
62
|
+
const u = await fetchUsageLimits(auth);
|
|
63
|
+
am.updateUsage(acc.id, {
|
|
64
|
+
usedCount: u.usedCount,
|
|
65
|
+
limitCount: u.limitCount,
|
|
66
|
+
email: u.email
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await am.saveToDisk();
|
|
57
73
|
return {
|
|
58
74
|
apiKey: '',
|
|
59
75
|
baseURL: KIRO_CONSTANTS.BASE_URL.replace('/generateAssistantResponse', '').replace('{{region}}', config.default_region || 'us-east-1'),
|
|
@@ -76,7 +92,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
76
92
|
const count = am.getAccountCount();
|
|
77
93
|
if (count === 0)
|
|
78
94
|
throw new Error('No accounts');
|
|
79
|
-
|
|
95
|
+
let acc = am.getCurrentOrNext();
|
|
80
96
|
if (!acc) {
|
|
81
97
|
const wait = am.getMinWaitTime();
|
|
82
98
|
if (wait > 0 && wait < 30000) {
|
|
@@ -87,13 +103,13 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
87
103
|
}
|
|
88
104
|
throw new Error('All accounts are unhealthy or rate-limited');
|
|
89
105
|
}
|
|
90
|
-
if (
|
|
91
|
-
showToast(`Using ${acc.
|
|
106
|
+
if (am.shouldShowToast())
|
|
107
|
+
showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
92
108
|
if (am.shouldShowUsageToast() &&
|
|
93
109
|
acc.usedCount !== undefined &&
|
|
94
110
|
acc.limitCount !== undefined) {
|
|
95
111
|
const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
|
|
96
|
-
showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.
|
|
112
|
+
showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
|
|
97
113
|
}
|
|
98
114
|
const auth = am.toAuthDetails(acc);
|
|
99
115
|
if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
|
|
@@ -110,6 +126,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
110
126
|
if (stillAcc &&
|
|
111
127
|
!accessTokenExpired(refreshedAm.toAuthDetails(stillAcc), config.token_expiry_buffer_ms)) {
|
|
112
128
|
showToast('Credentials recovered from Kiro CLI sync.', 'info');
|
|
129
|
+
acc = stillAcc;
|
|
113
130
|
continue;
|
|
114
131
|
}
|
|
115
132
|
if (e instanceof KiroTokenRefreshError &&
|
|
@@ -128,19 +145,19 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
128
145
|
let prep = prepRequest(reductionFactor);
|
|
129
146
|
const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
130
147
|
if (config.enable_log_api_request && apiTimestamp) {
|
|
131
|
-
let
|
|
148
|
+
let b = null;
|
|
132
149
|
try {
|
|
133
|
-
|
|
150
|
+
b = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
134
151
|
}
|
|
135
152
|
catch { }
|
|
136
153
|
logger.logApiRequest({
|
|
137
154
|
url: prep.url,
|
|
138
155
|
method: prep.init.method,
|
|
139
156
|
headers: prep.init.headers,
|
|
140
|
-
body:
|
|
157
|
+
body: b,
|
|
141
158
|
conversationId: prep.conversationId,
|
|
142
159
|
model: prep.effectiveModel,
|
|
143
|
-
email: acc.
|
|
160
|
+
email: acc.email
|
|
144
161
|
}, apiTimestamp);
|
|
145
162
|
}
|
|
146
163
|
try {
|
|
@@ -260,9 +277,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
260
277
|
conversationId: prep.conversationId,
|
|
261
278
|
model: prep.effectiveModel
|
|
262
279
|
};
|
|
263
|
-
let
|
|
280
|
+
let lastB = null;
|
|
264
281
|
try {
|
|
265
|
-
|
|
282
|
+
lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
266
283
|
}
|
|
267
284
|
catch { }
|
|
268
285
|
if (!config.enable_log_api_request)
|
|
@@ -270,10 +287,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
270
287
|
url: prep.url,
|
|
271
288
|
method: prep.init.method,
|
|
272
289
|
headers: prep.init.headers,
|
|
273
|
-
body:
|
|
290
|
+
body: lastB,
|
|
274
291
|
conversationId: prep.conversationId,
|
|
275
292
|
model: prep.effectiveModel,
|
|
276
|
-
email: acc.
|
|
293
|
+
email: acc.email
|
|
277
294
|
}, rData, logger.getTimestamp());
|
|
278
295
|
throw new Error(`Kiro Error: ${res.status}`);
|
|
279
296
|
}
|
|
@@ -305,7 +322,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
305
322
|
const idcAccs = existingAm.getAccounts().filter((a) => a.authMethod === 'idc');
|
|
306
323
|
if (idcAccs.length > 0) {
|
|
307
324
|
const existingAccounts = idcAccs.map((acc, idx) => ({
|
|
308
|
-
email: acc.
|
|
325
|
+
email: acc.email,
|
|
309
326
|
index: idx
|
|
310
327
|
}));
|
|
311
328
|
startFresh = (await promptLoginMode(existingAccounts)) === 'fresh';
|
|
@@ -316,15 +333,29 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
316
333
|
const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
|
|
317
334
|
openBrowser(url);
|
|
318
335
|
const res = await waitForAuth();
|
|
336
|
+
const u = await fetchUsageLimits({
|
|
337
|
+
refresh: '',
|
|
338
|
+
access: res.accessToken,
|
|
339
|
+
expires: res.expiresAt,
|
|
340
|
+
authMethod: 'idc',
|
|
341
|
+
region,
|
|
342
|
+
clientId: res.clientId,
|
|
343
|
+
clientSecret: res.clientSecret
|
|
344
|
+
});
|
|
345
|
+
if (!u.email) {
|
|
346
|
+
console.log('\n[Error] Failed to fetch account email. Skipping...\n');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
319
349
|
accounts.push(res);
|
|
320
350
|
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
321
351
|
if (accounts.length === 1 && startFresh)
|
|
322
352
|
am.getAccounts()
|
|
323
353
|
.filter((a) => a.authMethod === 'idc')
|
|
324
354
|
.forEach((a) => am.removeAccount(a));
|
|
355
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
325
356
|
const acc = {
|
|
326
|
-
id
|
|
327
|
-
email:
|
|
357
|
+
id,
|
|
358
|
+
email: u.email,
|
|
328
359
|
authMethod: 'idc',
|
|
329
360
|
region,
|
|
330
361
|
clientId: res.clientId,
|
|
@@ -333,39 +364,18 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
333
364
|
accessToken: res.accessToken,
|
|
334
365
|
expiresAt: res.expiresAt,
|
|
335
366
|
rateLimitResetTime: 0,
|
|
336
|
-
isHealthy: true
|
|
367
|
+
isHealthy: true,
|
|
368
|
+
failCount: 0
|
|
337
369
|
};
|
|
338
|
-
try {
|
|
339
|
-
const u = await fetchUsageLimits({
|
|
340
|
-
refresh: encodeRefreshToken({
|
|
341
|
-
refreshToken: res.refreshToken,
|
|
342
|
-
clientId: res.clientId,
|
|
343
|
-
clientSecret: res.clientSecret,
|
|
344
|
-
authMethod: 'idc'
|
|
345
|
-
}),
|
|
346
|
-
access: res.accessToken,
|
|
347
|
-
expires: res.expiresAt,
|
|
348
|
-
authMethod: 'idc',
|
|
349
|
-
region,
|
|
350
|
-
clientId: res.clientId,
|
|
351
|
-
clientSecret: res.clientSecret,
|
|
352
|
-
email: res.email
|
|
353
|
-
});
|
|
354
|
-
am.updateUsage(acc.id, {
|
|
355
|
-
usedCount: u.usedCount,
|
|
356
|
-
limitCount: u.limitCount,
|
|
357
|
-
realEmail: u.email
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
catch { }
|
|
361
370
|
am.addAccount(acc);
|
|
371
|
+
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
362
372
|
await am.saveToDisk();
|
|
363
|
-
|
|
373
|
+
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
|
|
364
374
|
if (!(await promptAddAnotherAccount(am.getAccountCount())))
|
|
365
375
|
break;
|
|
366
376
|
}
|
|
367
377
|
catch (e) {
|
|
368
|
-
|
|
378
|
+
console.log(`\n[Error] Login failed: ${e.message}\n`);
|
|
369
379
|
break;
|
|
370
380
|
}
|
|
371
381
|
}
|
|
@@ -391,9 +401,21 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
391
401
|
callback: async () => {
|
|
392
402
|
try {
|
|
393
403
|
const res = await waitForAuth(), am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
404
|
+
const u = await fetchUsageLimits({
|
|
405
|
+
refresh: '',
|
|
406
|
+
access: res.accessToken,
|
|
407
|
+
expires: res.expiresAt,
|
|
408
|
+
authMethod: 'idc',
|
|
409
|
+
region,
|
|
410
|
+
clientId: res.clientId,
|
|
411
|
+
clientSecret: res.clientSecret
|
|
412
|
+
});
|
|
413
|
+
if (!u.email)
|
|
414
|
+
throw new Error('No email');
|
|
415
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
394
416
|
const acc = {
|
|
395
|
-
id
|
|
396
|
-
email:
|
|
417
|
+
id,
|
|
418
|
+
email: u.email,
|
|
397
419
|
authMethod: 'idc',
|
|
398
420
|
region,
|
|
399
421
|
clientId: res.clientId,
|
|
@@ -402,9 +424,12 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
402
424
|
accessToken: res.accessToken,
|
|
403
425
|
expiresAt: res.expiresAt,
|
|
404
426
|
rateLimitResetTime: 0,
|
|
405
|
-
isHealthy: true
|
|
427
|
+
isHealthy: true,
|
|
428
|
+
failCount: 0
|
|
406
429
|
};
|
|
407
430
|
am.addAccount(acc);
|
|
431
|
+
if (u.email)
|
|
432
|
+
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
408
433
|
await am.saveToDisk();
|
|
409
434
|
return { type: 'success', key: res.accessToken };
|
|
410
435
|
}
|