@zhafron/opencode-kiro-auth 1.3.0 → 1.4.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/dist/plugin/accounts.d.ts +3 -4
- package/dist/plugin/accounts.js +37 -34
- package/dist/plugin/storage/migration.js +19 -20
- package/dist/plugin/storage/sqlite.d.ts +0 -5
- package/dist/plugin/storage/sqlite.js +63 -49
- package/dist/plugin/sync/kiro-cli.js +40 -21
- package/dist/plugin/types.d.ts +2 -2
- package/dist/plugin/usage.js +3 -3
- package/dist/plugin.js +79 -47
- 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;
|
|
@@ -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;
|
|
@@ -91,18 +82,29 @@ export class AccountManager {
|
|
|
91
82
|
}
|
|
92
83
|
return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
|
|
93
84
|
});
|
|
94
|
-
if (available.length === 0)
|
|
95
|
-
return null;
|
|
96
85
|
let selected;
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
if (available.length > 0) {
|
|
87
|
+
if (this.strategy === 'sticky') {
|
|
88
|
+
selected = available.find((_, i) => i === this.cursor) || available[0];
|
|
89
|
+
}
|
|
90
|
+
else if (this.strategy === 'round-robin') {
|
|
91
|
+
selected = available[this.cursor % available.length];
|
|
92
|
+
this.cursor = (this.cursor + 1) % available.length;
|
|
93
|
+
}
|
|
94
|
+
else if (this.strategy === 'lowest-usage') {
|
|
95
|
+
selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
96
|
+
}
|
|
103
97
|
}
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
if (!selected) {
|
|
99
|
+
const fallback = this.accounts
|
|
100
|
+
.filter((a) => !a.isHealthy && a.failCount < 3)
|
|
101
|
+
.sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
102
|
+
if (fallback) {
|
|
103
|
+
fallback.isHealthy = true;
|
|
104
|
+
delete fallback.unhealthyReason;
|
|
105
|
+
delete fallback.recoveryTime;
|
|
106
|
+
selected = fallback;
|
|
107
|
+
}
|
|
106
108
|
}
|
|
107
109
|
if (selected) {
|
|
108
110
|
selected.lastUsed = now;
|
|
@@ -117,11 +119,11 @@ export class AccountManager {
|
|
|
117
119
|
if (a) {
|
|
118
120
|
a.usedCount = meta.usedCount;
|
|
119
121
|
a.limitCount = meta.limitCount;
|
|
120
|
-
if (meta.
|
|
121
|
-
a.
|
|
122
|
+
if (meta.email)
|
|
123
|
+
a.email = meta.email;
|
|
124
|
+
a.failCount = 0;
|
|
125
|
+
kiroDb.upsertAccount(a);
|
|
122
126
|
}
|
|
123
|
-
this.usage[id] = { ...meta, lastSync: Date.now() };
|
|
124
|
-
kiroDb.upsertUsage(id, this.usage[id]);
|
|
125
127
|
}
|
|
126
128
|
addAccount(a) {
|
|
127
129
|
const i = this.accounts.findIndex((x) => x.id === a.id);
|
|
@@ -136,7 +138,6 @@ export class AccountManager {
|
|
|
136
138
|
if (removedIndex === -1)
|
|
137
139
|
return;
|
|
138
140
|
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
139
|
-
delete this.usage[a.id];
|
|
140
141
|
kiroDb.deleteAccount(a.id);
|
|
141
142
|
if (this.accounts.length === 0)
|
|
142
143
|
this.cursor = 0;
|
|
@@ -151,14 +152,15 @@ export class AccountManager {
|
|
|
151
152
|
acc.accessToken = auth.access;
|
|
152
153
|
acc.expiresAt = auth.expires;
|
|
153
154
|
acc.lastUsed = Date.now();
|
|
154
|
-
if (auth.email
|
|
155
|
-
acc.
|
|
155
|
+
if (auth.email)
|
|
156
|
+
acc.email = auth.email;
|
|
156
157
|
const p = decodeRefreshToken(auth.refresh);
|
|
157
158
|
acc.refreshToken = p.refreshToken;
|
|
158
159
|
if (p.profileArn)
|
|
159
160
|
acc.profileArn = p.profileArn;
|
|
160
161
|
if (p.clientId)
|
|
161
162
|
acc.clientId = p.clientId;
|
|
163
|
+
acc.failCount = 0;
|
|
162
164
|
kiroDb.upsertAccount(acc);
|
|
163
165
|
writeToKiroCli(acc).catch(() => { });
|
|
164
166
|
}
|
|
@@ -175,7 +177,8 @@ export class AccountManager {
|
|
|
175
177
|
if (acc) {
|
|
176
178
|
acc.isHealthy = false;
|
|
177
179
|
acc.unhealthyReason = reason;
|
|
178
|
-
acc.
|
|
180
|
+
acc.failCount = (acc.failCount || 0) + 1;
|
|
181
|
+
acc.recoveryTime = acc.failCount >= 3 ? undefined : recovery || Date.now() + 3600000;
|
|
179
182
|
kiroDb.upsertAccount(acc);
|
|
180
183
|
}
|
|
181
184
|
}
|
|
@@ -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,10 +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
|
-
getSetting(key: string): string | null;
|
|
13
|
-
setSetting(key: string, value: string): void;
|
|
14
9
|
close(): void;
|
|
15
10
|
}
|
|
16
11
|
export declare const kiroDb: KiroDatabase;
|
|
@@ -23,21 +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
|
|
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
|
|
31
32
|
)
|
|
32
33
|
`);
|
|
33
|
-
this.db.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
86
|
}
|
|
42
87
|
getAccounts() {
|
|
43
88
|
return this.db.prepare('SELECT * FROM accounts').all();
|
|
@@ -46,57 +91,26 @@ export class KiroDatabase {
|
|
|
46
91
|
this.db
|
|
47
92
|
.prepare(`
|
|
48
93
|
INSERT INTO accounts (
|
|
49
|
-
id, email,
|
|
94
|
+
id, email, auth_method, region, client_id, client_secret,
|
|
50
95
|
profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
51
|
-
is_healthy, unhealthy_reason, recovery_time, last_used
|
|
52
|
-
|
|
96
|
+
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
97
|
+
used_count, limit_count, last_sync
|
|
98
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
99
|
ON CONFLICT(id) DO UPDATE SET
|
|
54
|
-
email=excluded.email,
|
|
100
|
+
email=excluded.email, auth_method=excluded.auth_method,
|
|
55
101
|
region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
56
102
|
profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
|
|
57
103
|
access_token=excluded.access_token, expires_at=excluded.expires_at,
|
|
58
104
|
rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
|
|
59
105
|
unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
|
|
60
|
-
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
|
|
61
108
|
`)
|
|
62
|
-
.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);
|
|
63
110
|
}
|
|
64
111
|
deleteAccount(id) {
|
|
65
112
|
this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
|
|
66
113
|
}
|
|
67
|
-
getUsage() {
|
|
68
|
-
const rows = this.db.prepare('SELECT * FROM usage').all();
|
|
69
|
-
const usage = {};
|
|
70
|
-
for (const r of rows) {
|
|
71
|
-
usage[r.account_id] = {
|
|
72
|
-
usedCount: r.used_count,
|
|
73
|
-
limitCount: r.limit_count,
|
|
74
|
-
realEmail: r.real_email,
|
|
75
|
-
lastSync: r.last_sync
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
return usage;
|
|
79
|
-
}
|
|
80
|
-
upsertUsage(id, meta) {
|
|
81
|
-
this.db
|
|
82
|
-
.prepare(`
|
|
83
|
-
INSERT INTO usage (account_id, used_count, limit_count, real_email, last_sync)
|
|
84
|
-
VALUES (?, ?, ?, ?, ?)
|
|
85
|
-
ON CONFLICT(account_id) DO UPDATE SET
|
|
86
|
-
used_count=excluded.used_count, limit_count=excluded.limit_count,
|
|
87
|
-
real_email=excluded.real_email, last_sync=excluded.last_sync
|
|
88
|
-
`)
|
|
89
|
-
.run(id, meta.usedCount, meta.limitCount, meta.realEmail || null, meta.lastSync);
|
|
90
|
-
}
|
|
91
|
-
getSetting(key) {
|
|
92
|
-
const row = this.db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
|
93
|
-
return row ? row.value : null;
|
|
94
|
-
}
|
|
95
|
-
setSetting(key, value) {
|
|
96
|
-
this.db
|
|
97
|
-
.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
|
|
98
|
-
.run(key, value);
|
|
99
|
-
}
|
|
100
114
|
close() {
|
|
101
115
|
this.db.close();
|
|
102
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();
|
|
@@ -77,7 +96,7 @@ export async function writeToKiroCli(acc) {
|
|
|
77
96
|
return;
|
|
78
97
|
try {
|
|
79
98
|
const cliDb = new Database(dbPath);
|
|
80
|
-
cliDb.
|
|
99
|
+
cliDb.run('PRAGMA busy_timeout = 5000');
|
|
81
100
|
const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
|
|
82
101
|
const targetKey = acc.authMethod === 'idc' ? 'kirocli:odic:token' : 'kirocli:social:token';
|
|
83
102
|
const row = rows.find((r) => r.key === targetKey || r.key.endsWith(targetKey));
|
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,6 +103,14 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
87
103
|
}
|
|
88
104
|
throw new Error('All accounts are unhealthy or rate-limited');
|
|
89
105
|
}
|
|
106
|
+
if (count > 1 && am.shouldShowToast())
|
|
107
|
+
showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
108
|
+
if (am.shouldShowUsageToast() &&
|
|
109
|
+
acc.usedCount !== undefined &&
|
|
110
|
+
acc.limitCount !== undefined) {
|
|
111
|
+
const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
|
|
112
|
+
showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
|
|
113
|
+
}
|
|
90
114
|
const auth = am.toAuthDetails(acc);
|
|
91
115
|
if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
|
|
92
116
|
try {
|
|
@@ -102,6 +126,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
102
126
|
if (stillAcc &&
|
|
103
127
|
!accessTokenExpired(refreshedAm.toAuthDetails(stillAcc), config.token_expiry_buffer_ms)) {
|
|
104
128
|
showToast('Credentials recovered from Kiro CLI sync.', 'info');
|
|
129
|
+
acc = stillAcc;
|
|
105
130
|
continue;
|
|
106
131
|
}
|
|
107
132
|
if (e instanceof KiroTokenRefreshError &&
|
|
@@ -120,19 +145,19 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
120
145
|
let prep = prepRequest(reductionFactor);
|
|
121
146
|
const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
122
147
|
if (config.enable_log_api_request && apiTimestamp) {
|
|
123
|
-
let
|
|
148
|
+
let b = null;
|
|
124
149
|
try {
|
|
125
|
-
|
|
150
|
+
b = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
126
151
|
}
|
|
127
152
|
catch { }
|
|
128
153
|
logger.logApiRequest({
|
|
129
154
|
url: prep.url,
|
|
130
155
|
method: prep.init.method,
|
|
131
156
|
headers: prep.init.headers,
|
|
132
|
-
body:
|
|
157
|
+
body: b,
|
|
133
158
|
conversationId: prep.conversationId,
|
|
134
159
|
model: prep.effectiveModel,
|
|
135
|
-
email: acc.
|
|
160
|
+
email: acc.email
|
|
136
161
|
}, apiTimestamp);
|
|
137
162
|
}
|
|
138
163
|
try {
|
|
@@ -252,9 +277,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
252
277
|
conversationId: prep.conversationId,
|
|
253
278
|
model: prep.effectiveModel
|
|
254
279
|
};
|
|
255
|
-
let
|
|
280
|
+
let lastB = null;
|
|
256
281
|
try {
|
|
257
|
-
|
|
282
|
+
lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
258
283
|
}
|
|
259
284
|
catch { }
|
|
260
285
|
if (!config.enable_log_api_request)
|
|
@@ -262,10 +287,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
262
287
|
url: prep.url,
|
|
263
288
|
method: prep.init.method,
|
|
264
289
|
headers: prep.init.headers,
|
|
265
|
-
body:
|
|
290
|
+
body: lastB,
|
|
266
291
|
conversationId: prep.conversationId,
|
|
267
292
|
model: prep.effectiveModel,
|
|
268
|
-
email: acc.
|
|
293
|
+
email: acc.email
|
|
269
294
|
}, rData, logger.getTimestamp());
|
|
270
295
|
throw new Error(`Kiro Error: ${res.status}`);
|
|
271
296
|
}
|
|
@@ -294,11 +319,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
294
319
|
const accounts = [];
|
|
295
320
|
let startFresh = true;
|
|
296
321
|
const existingAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
email: acc.realEmail || acc.email,
|
|
322
|
+
const idcAccs = existingAm.getAccounts().filter((a) => a.authMethod === 'idc');
|
|
323
|
+
if (idcAccs.length > 0) {
|
|
324
|
+
const existingAccounts = idcAccs.map((acc, idx) => ({
|
|
325
|
+
email: acc.email,
|
|
302
326
|
index: idx
|
|
303
327
|
}));
|
|
304
328
|
startFresh = (await promptLoginMode(existingAccounts)) === 'fresh';
|
|
@@ -309,15 +333,29 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
309
333
|
const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
|
|
310
334
|
openBrowser(url);
|
|
311
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
|
+
}
|
|
312
349
|
accounts.push(res);
|
|
313
350
|
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
314
351
|
if (accounts.length === 1 && startFresh)
|
|
315
352
|
am.getAccounts()
|
|
316
353
|
.filter((a) => a.authMethod === 'idc')
|
|
317
354
|
.forEach((a) => am.removeAccount(a));
|
|
355
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
318
356
|
const acc = {
|
|
319
|
-
id
|
|
320
|
-
email:
|
|
357
|
+
id,
|
|
358
|
+
email: u.email,
|
|
321
359
|
authMethod: 'idc',
|
|
322
360
|
region,
|
|
323
361
|
clientId: res.clientId,
|
|
@@ -326,39 +364,18 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
326
364
|
accessToken: res.accessToken,
|
|
327
365
|
expiresAt: res.expiresAt,
|
|
328
366
|
rateLimitResetTime: 0,
|
|
329
|
-
isHealthy: true
|
|
367
|
+
isHealthy: true,
|
|
368
|
+
failCount: 0
|
|
330
369
|
};
|
|
331
|
-
try {
|
|
332
|
-
const u = await fetchUsageLimits({
|
|
333
|
-
refresh: encodeRefreshToken({
|
|
334
|
-
refreshToken: res.refreshToken,
|
|
335
|
-
clientId: res.clientId,
|
|
336
|
-
clientSecret: res.clientSecret,
|
|
337
|
-
authMethod: 'idc'
|
|
338
|
-
}),
|
|
339
|
-
access: res.accessToken,
|
|
340
|
-
expires: res.expiresAt,
|
|
341
|
-
authMethod: 'idc',
|
|
342
|
-
region,
|
|
343
|
-
clientId: res.clientId,
|
|
344
|
-
clientSecret: res.clientSecret,
|
|
345
|
-
email: res.email
|
|
346
|
-
});
|
|
347
|
-
am.updateUsage(acc.id, {
|
|
348
|
-
usedCount: u.usedCount,
|
|
349
|
-
limitCount: u.limitCount,
|
|
350
|
-
realEmail: u.email
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
catch { }
|
|
354
370
|
am.addAccount(acc);
|
|
371
|
+
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
355
372
|
await am.saveToDisk();
|
|
356
|
-
|
|
373
|
+
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
|
|
357
374
|
if (!(await promptAddAnotherAccount(am.getAccountCount())))
|
|
358
375
|
break;
|
|
359
376
|
}
|
|
360
377
|
catch (e) {
|
|
361
|
-
|
|
378
|
+
console.log(`\n[Error] Login failed: ${e.message}\n`);
|
|
362
379
|
break;
|
|
363
380
|
}
|
|
364
381
|
}
|
|
@@ -384,9 +401,21 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
384
401
|
callback: async () => {
|
|
385
402
|
try {
|
|
386
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);
|
|
387
416
|
const acc = {
|
|
388
|
-
id
|
|
389
|
-
email:
|
|
417
|
+
id,
|
|
418
|
+
email: u.email,
|
|
390
419
|
authMethod: 'idc',
|
|
391
420
|
region,
|
|
392
421
|
clientId: res.clientId,
|
|
@@ -395,9 +424,12 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
395
424
|
accessToken: res.accessToken,
|
|
396
425
|
expiresAt: res.expiresAt,
|
|
397
426
|
rateLimitResetTime: 0,
|
|
398
|
-
isHealthy: true
|
|
427
|
+
isHealthy: true,
|
|
428
|
+
failCount: 0
|
|
399
429
|
};
|
|
400
430
|
am.addAccount(acc);
|
|
431
|
+
if (u.email)
|
|
432
|
+
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
401
433
|
await am.saveToDisk();
|
|
402
434
|
return { type: 'success', key: res.accessToken };
|
|
403
435
|
}
|