@sunerpy/opencode-kiro-auth 0.7.0 → 0.9.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 CHANGED
@@ -101,6 +101,17 @@ mapping) lives in `~/.config/opencode/kiro.json`. See
101
101
  [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the full example and every
102
102
  option.
103
103
 
104
+ New default keys are backfilled into an existing `kiro.json` automatically when
105
+ the plugin loads (additive only — your existing values are never changed). For
106
+ multi-account or long-idle setups, enable
107
+ [token keep-alive](docs/CONFIGURATION.md#token-keep-alive)
108
+ (`token_keepalive_enabled: true`) to keep idle accounts' tokens fresh while
109
+ OpenCode is running.
110
+
111
+ Paid-overage protection is on by default; see
112
+ [Overage protection](docs/CONFIGURATION.md#overage-protection) before disabling
113
+ `stop_on_overage`.
114
+
104
115
  ## Multiple accounts & rotation
105
116
 
106
117
  You can register more than one Kiro account and let the plugin spread
@@ -113,10 +124,11 @@ requests across them for combined quota and automatic failover.
113
124
  AWS identity is stored separately in `kiro.db`. Re-running login for the
114
125
  same identity updates it in place; logging in with a different identity
115
126
  adds a new account.
116
- 2. Auto-sync from Kiro CLI: with `auto_sync_kiro_cli: true` (the default),
117
- the plugin imports credentials from your local `kiro-cli` database.
118
- Switching or adding accounts via `kiro-cli login` flows into the plugin
119
- automatically, no extra step needed.
127
+ 2. Auto-sync from Kiro CLI: with `auto_sync_kiro_cli: true` (opt-in, default
128
+ `false`), the plugin imports credentials from your local `kiro-cli`
129
+ database. Note `kiro-cli` stores only one token per auth method, so it
130
+ cannot represent multiple accounts — manual `opencode auth login` per
131
+ account (option 1) is the supported multi-account path.
120
132
 
121
133
  **Rotation strategy** — set `account_selection_strategy` in
122
134
  `~/.config/opencode/kiro.json`:
@@ -26,6 +26,9 @@ export class AccountSelector {
26
26
  let acc = this.accountManager.getCurrentOrNext();
27
27
  if (!acc) {
28
28
  this.circuitBreakerTrips++;
29
+ if (this.accountManager.allSelectableBlockedByOverage()) {
30
+ throw new Error('All accounts have exceeded their free quota and entered paid overage. Set "stop_on_overage": false in ~/.config/opencode/kiro.json to continue with paid overage, or wait for the quota to reset.');
31
+ }
29
32
  const wait = this.accountManager.getMinWaitTime();
30
33
  if (wait > 0 && wait < 30000) {
31
34
  if (this.accountManager.shouldShowToast()) {
@@ -10,24 +10,33 @@ export declare class AccountManager {
10
10
  private stickyId?;
11
11
  private quotaAvoidanceEnabled;
12
12
  private quotaReserveThreshold;
13
+ private stopOnOverage;
14
+ private overageThreshold;
13
15
  constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy, opts?: {
14
16
  quotaAvoidanceEnabled?: boolean;
15
17
  quotaReserveThreshold?: number;
18
+ stopOnOverage?: boolean;
19
+ overageThreshold?: number;
16
20
  });
17
21
  static loadFromDisk(strategy?: AccountSelectionStrategy, opts?: {
18
22
  quotaAvoidanceEnabled?: boolean;
19
23
  quotaReserveThreshold?: number;
24
+ stopOnOverage?: boolean;
25
+ overageThreshold?: number;
20
26
  }): Promise<AccountManager>;
21
27
  getAccountCount(): number;
22
28
  getAccounts(): ManagedAccount[];
23
29
  shouldShowToast(debounce?: number): boolean;
24
30
  shouldShowUsageToast(debounce?: number): boolean;
25
31
  getMinWaitTime(): number;
32
+ allSelectableBlockedByOverage(): boolean;
26
33
  getCurrentOrNext(): ManagedAccount | null;
27
34
  updateUsage(id: string, meta: {
28
35
  usedCount: number;
29
36
  limitCount: number;
37
+ overageCount?: number;
30
38
  email?: string;
39
+ lastSync?: number;
31
40
  }): void;
32
41
  addAccount(a: ManagedAccount): void;
33
42
  removeAccount(a: ManagedAccount): void;
@@ -36,4 +45,5 @@ export declare class AccountManager {
36
45
  markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
37
46
  saveToDisk(): Promise<void>;
38
47
  toAuthDetails(a: ManagedAccount): KiroAuthDetails;
48
+ private isOverageBlocked;
39
49
  }
@@ -19,12 +19,16 @@ export class AccountManager {
19
19
  stickyId;
20
20
  quotaAvoidanceEnabled;
21
21
  quotaReserveThreshold;
22
+ stopOnOverage;
23
+ overageThreshold;
22
24
  constructor(accounts, strategy = 'sticky', opts) {
23
25
  this.accounts = accounts;
24
26
  this.cursor = 0;
25
27
  this.strategy = strategy;
26
28
  this.quotaAvoidanceEnabled = opts?.quotaAvoidanceEnabled ?? true;
27
29
  this.quotaReserveThreshold = opts?.quotaReserveThreshold ?? 0.95;
30
+ this.stopOnOverage = opts?.stopOnOverage ?? true;
31
+ this.overageThreshold = opts?.overageThreshold ?? 0;
28
32
  }
29
33
  static async loadFromDisk(strategy, opts) {
30
34
  const rows = kiroDb.getAccounts();
@@ -48,7 +52,9 @@ export class AccountManager {
48
52
  failCount: r.fail_count || 0,
49
53
  lastUsed: r.last_used,
50
54
  usedCount: r.used_count,
51
- limitCount: r.limit_count
55
+ limitCount: r.limit_count,
56
+ overageCount: r.overage_count || 0,
57
+ lastSync: r.last_sync
52
58
  }));
53
59
  return new AccountManager(accounts, strategy || 'sticky', opts);
54
60
  }
@@ -75,9 +81,31 @@ export class AccountManager {
75
81
  const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
76
82
  return waits.length > 0 ? Math.min(...waits) : 0;
77
83
  }
84
+ allSelectableBlockedByOverage() {
85
+ if (!this.stopOnOverage)
86
+ return false;
87
+ const now = Date.now();
88
+ const healthEligible = (a) => {
89
+ if (isPermanentError(a.unhealthyReason))
90
+ return false;
91
+ if (a.isHealthy)
92
+ return true;
93
+ if (isAccessTokenError(a.unhealthyReason))
94
+ return true;
95
+ return a.failCount < 10;
96
+ };
97
+ const rateLimited = (a) => !!(a.rateLimitResetTime && now < a.rateLimitResetTime);
98
+ const hasOverageBlockedEligible = this.accounts.some((a) => healthEligible(a) && this.isOverageBlocked(a));
99
+ const hasCleanRateLimited = this.accounts.some((a) => healthEligible(a) && rateLimited(a) && !this.isOverageBlocked(a));
100
+ const hasHealthySelectable = this.accounts.some((a) => a.isHealthy && !rateLimited(a) && !this.isOverageBlocked(a));
101
+ return hasOverageBlockedEligible && !hasCleanRateLimited && !hasHealthySelectable;
102
+ }
78
103
  getCurrentOrNext() {
79
104
  const now = Date.now();
105
+ const overageBlocked = (a) => this.isOverageBlocked(a);
80
106
  const available = this.accounts.filter((a) => {
107
+ if (overageBlocked(a))
108
+ return false;
81
109
  if (!a.isHealthy) {
82
110
  if (isPermanentError(a.unhealthyReason)) {
83
111
  return false;
@@ -129,7 +157,10 @@ export class AccountManager {
129
157
  }
130
158
  if (!selected) {
131
159
  const fallback = this.accounts
132
- .filter((a) => !a.isHealthy && a.failCount < 10 && !isPermanentError(a.unhealthyReason))
160
+ .filter((a) => !a.isHealthy &&
161
+ a.failCount < 10 &&
162
+ !isPermanentError(a.unhealthyReason) &&
163
+ !overageBlocked(a))
133
164
  .sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
134
165
  if (fallback) {
135
166
  fallback.isHealthy = true;
@@ -139,6 +170,8 @@ export class AccountManager {
139
170
  }
140
171
  }
141
172
  if (selected) {
173
+ if (overageBlocked(selected))
174
+ return null;
142
175
  selected.lastUsed = now;
143
176
  selected.usedCount = (selected.usedCount || 0) + 1;
144
177
  return selected;
@@ -150,6 +183,8 @@ export class AccountManager {
150
183
  if (a) {
151
184
  a.usedCount = meta.usedCount;
152
185
  a.limitCount = meta.limitCount;
186
+ a.overageCount = meta.overageCount ?? 0;
187
+ a.lastSync = meta.lastSync ?? a.lastSync;
153
188
  if (meta.email)
154
189
  a.email = meta.email;
155
190
  if (!isPermanentError(a.unhealthyReason)) {
@@ -290,4 +325,7 @@ export class AccountManager {
290
325
  email: a.email
291
326
  };
292
327
  }
328
+ isOverageBlocked(a) {
329
+ return this.stopOnOverage && (a.overageCount ?? 0) > this.overageThreshold;
330
+ }
293
331
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import * as logger from '../logger.js';
@@ -30,6 +30,53 @@ function ensureUserConfigTemplate() {
30
30
  export function getProjectConfigPath(directory) {
31
31
  return join(directory, '.opencode', 'kiro.json');
32
32
  }
33
+ // Additively write any DEFAULT_CONFIG key missing from an existing user
34
+ // kiro.json so new-version keys become visible/toggleable. Additive-only,
35
+ // parse-safe, atomic, idempotent, user-config-only. See plan config-backfill.md.
36
+ function backfillUserConfig(path) {
37
+ if (!existsSync(path)) {
38
+ return;
39
+ }
40
+ let raw;
41
+ try {
42
+ raw = JSON.parse(readFileSync(path, 'utf-8'));
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
48
+ return;
49
+ }
50
+ const obj = raw;
51
+ const defaultKeys = Object.keys(DEFAULT_CONFIG);
52
+ const missing = defaultKeys.filter((key) => !(key in obj));
53
+ if (missing.length === 0) {
54
+ return;
55
+ }
56
+ const next = { ...obj };
57
+ for (const key of missing) {
58
+ next[key] = DEFAULT_CONFIG[key];
59
+ }
60
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
61
+ try {
62
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf-8');
63
+ try {
64
+ renameSync(tmp, path);
65
+ }
66
+ catch (renameError) {
67
+ try {
68
+ if (existsSync(tmp))
69
+ unlinkSync(tmp);
70
+ }
71
+ catch { }
72
+ throw renameError;
73
+ }
74
+ logger.log(`Backfilled ${missing.length} new config key(s) into ${path}: ${missing.join(', ')}`);
75
+ }
76
+ catch (error) {
77
+ logger.warn(`Config backfill failed for ${path}: ${String(error)}`);
78
+ }
79
+ }
33
80
  function loadConfigFile(path) {
34
81
  try {
35
82
  if (!existsSync(path)) {
@@ -109,6 +156,7 @@ function applyEnvOverrides(config) {
109
156
  }
110
157
  export function loadConfig(directory) {
111
158
  ensureUserConfigTemplate();
159
+ backfillUserConfig(getUserConfigPath());
112
160
  let config = { ...DEFAULT_CONFIG };
113
161
  const userConfigPath = getUserConfigPath();
114
162
  const userConfig = loadConfigFile(userConfigPath);
@@ -32,6 +32,15 @@ export declare const KiroConfigSchema: z.ZodObject<{
32
32
  * near-full and softly avoided. Default 0.95 (95%).
33
33
  */
34
34
  quota_reserve_threshold: z.ZodDefault<z.ZodNumber>;
35
+ /**
36
+ * Exclude accounts that have entered AWS paid overage from selection.
37
+ */
38
+ stop_on_overage: z.ZodDefault<z.ZodBoolean>;
39
+ /**
40
+ * Paid-overage invocations tolerated before stopping an account. 0 means
41
+ * stop on any overage.
42
+ */
43
+ overage_threshold: z.ZodDefault<z.ZodNumber>;
35
44
  default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
36
45
  rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
37
46
  rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
@@ -77,6 +86,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
77
86
  account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
78
87
  quota_avoidance_enabled: boolean;
79
88
  quota_reserve_threshold: number;
89
+ stop_on_overage: boolean;
90
+ overage_threshold: number;
80
91
  default_region: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1";
81
92
  rate_limit_retry_delay_ms: number;
82
93
  rate_limit_max_retries: number;
@@ -106,6 +117,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
106
117
  account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
107
118
  quota_avoidance_enabled?: boolean | undefined;
108
119
  quota_reserve_threshold?: number | undefined;
120
+ stop_on_overage?: boolean | undefined;
121
+ overage_threshold?: number | undefined;
109
122
  default_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
110
123
  rate_limit_retry_delay_ms?: number | undefined;
111
124
  rate_limit_max_retries?: number | undefined;
@@ -64,6 +64,15 @@ export const KiroConfigSchema = z.object({
64
64
  * near-full and softly avoided. Default 0.95 (95%).
65
65
  */
66
66
  quota_reserve_threshold: z.number().min(0).max(1).default(0.95),
67
+ /**
68
+ * Exclude accounts that have entered AWS paid overage from selection.
69
+ */
70
+ stop_on_overage: z.boolean().default(true),
71
+ /**
72
+ * Paid-overage invocations tolerated before stopping an account. 0 means
73
+ * stop on any overage.
74
+ */
75
+ overage_threshold: z.number().min(0).default(0),
67
76
  default_region: RegionSchema.default('us-east-1'),
68
77
  rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
69
78
  rate_limit_max_retries: z.number().min(0).max(10).default(3),
@@ -110,6 +119,8 @@ export const DEFAULT_CONFIG = {
110
119
  account_selection_strategy: 'lowest-usage',
111
120
  quota_avoidance_enabled: true,
112
121
  quota_reserve_threshold: 0.95,
122
+ stop_on_overage: true,
123
+ overage_threshold: 0,
113
124
  default_region: 'us-east-1',
114
125
  rate_limit_retry_delay_ms: 5000,
115
126
  rate_limit_max_retries: 3,
@@ -139,6 +139,7 @@ export function mergeAccounts(existing, incoming) {
139
139
  // in-memory token triple from another process must not clobber a newer
140
140
  // persisted token triple during this cross-process merge.
141
141
  const tokenWinner = (acc.expiresAt || 0) >= (existingAcc.expiresAt || 0) ? acc : existingAcc;
142
+ const usageWinner = (acc.lastSync || 0) >= (existingAcc.lastSync || 0) ? acc : existingAcc;
142
143
  accountMap.set(acc.id, {
143
144
  ...existingAcc,
144
145
  ...acc,
@@ -146,8 +147,9 @@ export function mergeAccounts(existing, incoming) {
146
147
  accessToken: tokenWinner.accessToken,
147
148
  expiresAt: tokenWinner.expiresAt,
148
149
  lastUsed: Math.max(existingAcc.lastUsed || 0, acc.lastUsed || 0),
149
- usedCount: Math.max(existingAcc.usedCount || 0, acc.usedCount || 0),
150
- limitCount: Math.max(existingAcc.limitCount || 0, acc.limitCount || 0),
150
+ usedCount: usageWinner.usedCount ?? 0,
151
+ limitCount: usageWinner.limitCount ?? 0,
152
+ overageCount: usageWinner.overageCount ?? 0,
151
153
  rateLimitResetTime: Math.max(existingAcc.rateLimitResetTime || 0, acc.rateLimitResetTime || 0),
152
154
  isHealthy: incomingRecovered
153
155
  ? true
@@ -6,6 +6,7 @@ export function runMigrations(db) {
6
6
  migrateOidcRegionColumn(db);
7
7
  migrateDropRefreshTokenUniqueIndex(db);
8
8
  migrateRemovedAccountsTable(db);
9
+ migrateOverageColumn(db);
9
10
  }
10
11
  function migrateToUniqueRefreshToken(db) {
11
12
  const hasIndex = db
@@ -159,3 +160,10 @@ function migrateRemovedAccountsTable(db) {
159
160
  // so kiro-cli auto-sync does not silently re-import them. Cleared only on deliberate login.
160
161
  db.exec('CREATE TABLE IF NOT EXISTS removed_accounts (id TEXT PRIMARY KEY, removed_at INTEGER NOT NULL)');
161
162
  }
163
+ function migrateOverageColumn(db) {
164
+ const columns = db.prepare('PRAGMA table_info(accounts)').all();
165
+ const names = new Set(columns.map((c) => c.name));
166
+ if (!names.has('overage_count')) {
167
+ db.exec('ALTER TABLE accounts ADD COLUMN overage_count INTEGER DEFAULT 0');
168
+ }
169
+ }
@@ -33,7 +33,8 @@ export class KiroDatabase {
33
33
  refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
34
34
  rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
35
35
  recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
36
- used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
36
+ used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
37
+ overage_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
37
38
  )
38
39
  `);
39
40
  runMigrations(this.db);
@@ -56,8 +57,8 @@ export class KiroDatabase {
56
57
  id, email, auth_method, region, oidc_region, client_id, client_secret,
57
58
  profile_arn, start_url, refresh_token, access_token, expires_at, rate_limit_reset,
58
59
  is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
59
- used_count, limit_count, last_sync
60
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
60
+ used_count, limit_count, overage_count, last_sync
61
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
61
62
  ON CONFLICT(id) DO UPDATE SET
62
63
  id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
63
64
  region=excluded.region, oidc_region=excluded.oidc_region, client_id=excluded.client_id, client_secret=excluded.client_secret,
@@ -66,9 +67,10 @@ export class KiroDatabase {
66
67
  rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
67
68
  unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
68
69
  fail_count=excluded.fail_count, last_used=excluded.last_used,
69
- used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
70
+ used_count=excluded.used_count, limit_count=excluded.limit_count,
71
+ overage_count=excluded.overage_count, last_sync=excluded.last_sync
70
72
  `)
71
- .run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
73
+ .run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.overageCount || 0, acc.lastSync || 0);
72
74
  }
73
75
  isRemovedSync(id) {
74
76
  return !!this.db.prepare('SELECT id FROM removed_accounts WHERE id = ?').get(id);
@@ -233,6 +235,7 @@ export class KiroDatabase {
233
235
  lastUsed: row.last_used,
234
236
  usedCount: row.used_count,
235
237
  limitCount: row.limit_count,
238
+ overageCount: row.overage_count || 0,
236
239
  lastSync: row.last_sync
237
240
  };
238
241
  }
@@ -42,6 +42,7 @@ export interface ManagedAccount {
42
42
  failCount: number;
43
43
  usedCount?: number;
44
44
  limitCount?: number;
45
+ overageCount?: number;
45
46
  lastSync?: number;
46
47
  lastUsed?: number;
47
48
  }
@@ -1,3 +1,16 @@
1
- import { KiroAuthDetails, ManagedAccount } from './types.js';
2
- export declare function fetchUsageLimits(auth: KiroAuthDetails): Promise<any>;
3
- export declare function updateAccountQuota(account: ManagedAccount, usage: any, accountManager?: any): void;
1
+ import type { KiroAuthDetails, ManagedAccount } from './types.js';
2
+ interface UsageSnapshot {
3
+ usedCount: number;
4
+ limitCount: number;
5
+ overageCount: number;
6
+ email?: string;
7
+ }
8
+ interface UsageUpdateMeta extends UsageSnapshot {
9
+ lastSync: number;
10
+ }
11
+ interface AccountUsageManager {
12
+ updateUsage(id: string, meta: UsageUpdateMeta): void;
13
+ }
14
+ export declare function fetchUsageLimits(auth: KiroAuthDetails): Promise<UsageSnapshot>;
15
+ export declare function updateAccountQuota(account: ManagedAccount, usage: Partial<UsageSnapshot>, accountManager?: AccountUsageManager): void;
16
+ export {};
@@ -1,3 +1,26 @@
1
+ import { z } from 'zod';
2
+ const UsageLimitsResponseSchema = z
3
+ .object({
4
+ usageBreakdownList: z
5
+ .array(z
6
+ .object({
7
+ currentUsage: z.number().optional(),
8
+ usageLimit: z.number().optional(),
9
+ currentOverages: z.number().optional(),
10
+ freeTrialInfo: z
11
+ .object({
12
+ currentUsage: z.number().optional(),
13
+ usageLimit: z.number().optional()
14
+ })
15
+ .passthrough()
16
+ .nullable()
17
+ .optional()
18
+ })
19
+ .passthrough())
20
+ .optional(),
21
+ userInfo: z.object({ email: z.string().optional() }).passthrough().optional()
22
+ })
23
+ .passthrough();
1
24
  export async function fetchUsageLimits(auth) {
2
25
  // Try different parameter combinations
3
26
  const attempts = [
@@ -42,8 +65,8 @@ export async function fetchUsageLimits(auth) {
42
65
  lastError = new Error(`Status: ${res.status}${errType ? ` (${errType})` : ''}${requestId ? ` [${requestId}]` : ''}: ${msg}`);
43
66
  continue;
44
67
  }
45
- const data = await res.json();
46
- let usedCount = 0, limitCount = 0;
68
+ const data = UsageLimitsResponseSchema.parse(await res.json());
69
+ let usedCount = 0, limitCount = 0, overageCount = 0;
47
70
  if (Array.isArray(data.usageBreakdownList)) {
48
71
  for (const s of data.usageBreakdownList) {
49
72
  if (s.freeTrialInfo) {
@@ -52,9 +75,10 @@ export async function fetchUsageLimits(auth) {
52
75
  }
53
76
  usedCount += s.currentUsage || 0;
54
77
  limitCount += s.usageLimit || 0;
78
+ overageCount += s.currentOverages || 0;
55
79
  }
56
80
  }
57
- return { usedCount, limitCount, email: data.userInfo?.email };
81
+ return { usedCount, limitCount, overageCount, email: data.userInfo?.email };
58
82
  }
59
83
  catch (e) {
60
84
  lastError = e instanceof Error ? e : new Error(String(e));
@@ -68,10 +92,14 @@ export function updateAccountQuota(account, usage, accountManager) {
68
92
  const meta = {
69
93
  usedCount: usage.usedCount || 0,
70
94
  limitCount: usage.limitCount || 0,
95
+ overageCount: usage.overageCount || 0,
96
+ lastSync: Date.now(),
71
97
  email: usage.email
72
98
  };
73
99
  account.usedCount = meta.usedCount;
74
100
  account.limitCount = meta.limitCount;
101
+ account.overageCount = meta.overageCount;
102
+ account.lastSync = meta.lastSync;
75
103
  if (usage.email)
76
104
  account.email = usage.email;
77
105
  if (accountManager)
package/dist/plugin.js CHANGED
@@ -43,7 +43,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
43
43
  const authHandler = new AuthHandler(config, repository);
44
44
  const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy, {
45
45
  quotaAvoidanceEnabled: config.quota_avoidance_enabled,
46
- quotaReserveThreshold: config.quota_reserve_threshold
46
+ quotaReserveThreshold: config.quota_reserve_threshold,
47
+ stopOnOverage: config.stop_on_overage,
48
+ overageThreshold: config.overage_threshold
47
49
  });
48
50
  authHandler.setAccountManager(accountManager);
49
51
  const requestHandler = new RequestHandler(accountManager, config, repository, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",