@zhafron/opencode-kiro-auth 1.2.8 → 1.3.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
@@ -7,14 +7,13 @@ OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude Sonnet a
7
7
 
8
8
  ## Features
9
9
 
10
- - AWS Builder ID (IDC) authentication with seamless device code flow.
11
- - Intelligent multi-account rotation prioritized by lowest usage.
12
- - Automated token refresh and rate limit handling with exponential backoff.
13
- - Native thinking mode support via virtual model mappings.
14
- - Decoupled storage for credentials and real-time usage metadata.
15
- - Configurable request timeout and iteration limits to prevent hangs.
16
- - Automatic port selection for auth server to avoid conflicts.
17
- - Usage tracking with automatic retry on sync failures.
10
+ - **Multiple Auth Methods**: Supports AWS Builder ID (IDC) and Kiro Desktop (CLI-based) authentication.
11
+ - **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from your local `kiro-cli` SQLite database.
12
+ - **Gradual Context Truncation**: Intelligently prevents error 400 by reducing context size dynamically during retries.
13
+ - **Intelligent Account Rotation**: Prioritizes multi-account usage based on lowest available quota.
14
+ - **High-Performance Storage**: Efficient account and usage management using native Bun SQLite.
15
+ - **Native Thinking Mode**: Full support for Claude reasoning capabilities via virtual model mappings.
16
+ - **Automated Recovery**: Exponential backoff for rate limits and automated token refresh.
18
17
 
19
18
  ## Installation
20
19
 
@@ -54,10 +53,14 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
54
53
 
55
54
  ## Setup
56
55
 
57
- 1. Run `opencode auth login`.
58
- 2. Select `Other`, type `kiro`, and press enter.
59
- 3. Follow the terminal instructions to complete the AWS Builder ID authentication.
60
- 4. Configuration template will be automatically created at `~/.config/opencode/kiro.json` on first load.
56
+ 1. **Authentication via Kiro CLI (Recommended)**:
57
+ - Perform login directly in your terminal using `kiro-cli login`.
58
+ - The plugin will automatically detect and import your session on startup.
59
+ 2. **Direct Authentication**:
60
+ - Run `opencode auth login`.
61
+ - Select `Other`, type `kiro`, and press enter.
62
+ - Follow the instructions for **AWS Builder ID (IDC)**.
63
+ 3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
61
64
 
62
65
  ## Configuration
63
66
 
@@ -65,6 +68,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
65
68
 
66
69
  ```json
67
70
  {
71
+ "auto_sync_kiro_cli": true,
68
72
  "account_selection_strategy": "lowest-usage",
69
73
  "default_region": "us-east-1",
70
74
  "rate_limit_retry_delay_ms": 5000,
@@ -82,46 +86,28 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
82
86
 
83
87
  ### Configuration Options
84
88
 
85
- - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`)
86
- - `default_region`: AWS region (`us-east-1`, `us-west-2`)
87
- - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms)
88
- - `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10)
89
- - `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000)
90
- - `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms)
91
- - `token_expiry_buffer_ms`: Token refresh buffer time (30000-300000ms)
92
- - `usage_sync_max_retries`: Retry attempts for usage sync (0-5)
93
- - `auth_server_port_start`: Starting port for auth server (1024-65535)
94
- - `auth_server_port_range`: Number of ports to try (1-100)
95
- - `usage_tracking_enabled`: Enable usage tracking and toast notifications
96
- - `enable_log_api_request`: Enable detailed API request logging
97
-
98
- ### Environment Variables
99
-
100
- All configuration options can be overridden via environment variables:
101
-
102
- - `KIRO_ACCOUNT_SELECTION_STRATEGY`
103
- - `KIRO_DEFAULT_REGION`
104
- - `KIRO_RATE_LIMIT_RETRY_DELAY_MS`
105
- - `KIRO_RATE_LIMIT_MAX_RETRIES`
106
- - `KIRO_MAX_REQUEST_ITERATIONS`
107
- - `KIRO_REQUEST_TIMEOUT_MS`
108
- - `KIRO_TOKEN_EXPIRY_BUFFER_MS`
109
- - `KIRO_USAGE_SYNC_MAX_RETRIES`
110
- - `KIRO_AUTH_SERVER_PORT_START`
111
- - `KIRO_AUTH_SERVER_PORT_RANGE`
112
- - `KIRO_USAGE_TRACKING_ENABLED`
113
- - `KIRO_ENABLE_LOG_API_REQUEST`
89
+ - `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
90
+ - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
91
+ - `default_region`: AWS region (`us-east-1`, `us-west-2`).
92
+ - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
93
+ - `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
94
+ - `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
95
+ - `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms).
96
+ - `token_expiry_buffer_ms`: Token refresh buffer time (30000-300000ms).
97
+ - `usage_sync_max_retries`: Retry attempts for usage sync (0-5).
98
+ - `auth_server_port_start`: Starting port for auth server (1024-65535).
99
+ - `auth_server_port_range`: Number of ports to try (1-100).
100
+ - `usage_tracking_enabled`: Enable usage tracking and toast notifications.
101
+ - `enable_log_api_request`: Enable detailed API request logging.
114
102
 
115
103
  ## Storage
116
104
 
117
105
  **Linux/macOS:**
118
- - Credentials: `~/.config/opencode/kiro-accounts.json`
119
- - Usage Tracking: `~/.config/opencode/kiro-usage.json`
106
+ - SQLite Database: `~/.config/opencode/kiro.db`
120
107
  - Plugin Config: `~/.config/opencode/kiro.json`
121
108
 
122
109
  **Windows:**
123
- - Credentials: `%APPDATA%\opencode\kiro-accounts.json`
124
- - Usage Tracking: `%APPDATA%\opencode\kiro-usage.json`
110
+ - SQLite Database: `%APPDATA%\opencode\kiro.db`
125
111
  - Plugin Config: `%APPDATA%\opencode\kiro.json`
126
112
 
127
113
  ## Acknowledgements
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { KiroOAuthPlugin } from './plugin.js';
2
2
  export type { KiroConfig } from './plugin/config/index.js';
3
- export type { KiroRegion, KiroAuthMethod, ManagedAccount } from './plugin/types.js';
3
+ export type { KiroAuthMethod, KiroRegion, ManagedAccount } from './plugin/types.js';
package/dist/kiro/auth.js CHANGED
@@ -1,12 +1,14 @@
1
1
  export function decodeRefreshToken(refresh) {
2
2
  const parts = refresh.split('|');
3
3
  if (parts.length < 2)
4
- return { refreshToken: parts[0], authMethod: 'idc' };
4
+ return { refreshToken: parts[0], authMethod: 'desktop' };
5
5
  const refreshToken = parts[0];
6
6
  const authMethod = parts[parts.length - 1];
7
7
  if (authMethod === 'idc')
8
8
  return { refreshToken, clientId: parts[1], clientSecret: parts[2], authMethod: 'idc' };
9
- return { refreshToken, authMethod: 'idc' };
9
+ if (authMethod === 'desktop')
10
+ return { refreshToken, authMethod: 'desktop' };
11
+ return { refreshToken, authMethod: 'desktop' };
10
12
  }
11
13
  export function accessTokenExpired(auth, bufferMs = 120000) {
12
14
  if (!auth.access || !auth.expires)
@@ -14,10 +16,17 @@ export function accessTokenExpired(auth, bufferMs = 120000) {
14
16
  return Date.now() >= auth.expires - bufferMs;
15
17
  }
16
18
  export function validateAuthDetails(auth) {
17
- return !!auth.refresh && auth.authMethod === 'idc' && !!auth.clientId && !!auth.clientSecret;
19
+ if (!auth.refresh)
20
+ return false;
21
+ if (auth.authMethod === 'idc')
22
+ return !!auth.clientId && !!auth.clientSecret;
23
+ return true;
18
24
  }
19
25
  export function encodeRefreshToken(parts) {
20
- if (!parts.clientId || !parts.clientSecret)
21
- throw new Error('Missing credentials');
22
- return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
26
+ if (parts.authMethod === 'idc') {
27
+ if (!parts.clientId || !parts.clientSecret)
28
+ throw new Error('Missing credentials');
29
+ return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
30
+ }
31
+ return `${parts.refreshToken}|desktop`;
23
32
  }
@@ -1,5 +1,6 @@
1
- import type { ManagedAccount, AccountSelectionStrategy, KiroAuthDetails, UsageMetadata } from './types';
1
+ import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount, UsageMetadata } from './types';
2
2
  export declare function generateAccountId(): string;
3
+ export declare function createDeterministicAccountId(email: string, method: string, clientId?: string, profileArn?: string): string;
3
4
  export declare class AccountManager {
4
5
  private accounts;
5
6
  private usage;
@@ -1,10 +1,15 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { loadAccounts, saveAccounts, loadUsage, saveUsage } from './storage';
3
- import { KIRO_CONSTANTS } from '../constants';
4
- import { encodeRefreshToken, decodeRefreshToken } from '../kiro/auth';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
3
+ import { kiroDb } from './storage/sqlite';
4
+ import { writeToKiroCli } from './sync/kiro-cli';
5
5
  export function generateAccountId() {
6
6
  return randomBytes(16).toString('hex');
7
7
  }
8
+ export function createDeterministicAccountId(email, method, clientId, profileArn) {
9
+ return createHash('sha256')
10
+ .update(`${email}:${method}:${clientId || ''}:${profileArn || ''}`)
11
+ .digest('hex');
12
+ }
8
13
  export class AccountManager {
9
14
  accounts;
10
15
  usage;
@@ -27,13 +32,27 @@ export class AccountManager {
27
32
  }
28
33
  }
29
34
  static async loadFromDisk(strategy) {
30
- const s = await loadAccounts();
31
- const u = await loadUsage();
32
- const accounts = s.accounts.map((m) => ({
33
- ...m,
34
- region: m.region || KIRO_CONSTANTS.DEFAULT_REGION
35
+ const rows = kiroDb.getAccounts();
36
+ const usage = kiroDb.getUsage();
37
+ const accounts = rows.map((r) => ({
38
+ id: r.id,
39
+ email: r.email,
40
+ realEmail: r.real_email,
41
+ authMethod: r.auth_method,
42
+ region: r.region,
43
+ clientId: r.client_id,
44
+ clientSecret: r.client_secret,
45
+ profileArn: r.profile_arn,
46
+ refreshToken: r.refresh_token,
47
+ accessToken: r.access_token,
48
+ expiresAt: r.expires_at,
49
+ rateLimitResetTime: r.rate_limit_reset,
50
+ isHealthy: r.is_healthy === 1,
51
+ unhealthyReason: r.unhealthy_reason,
52
+ recoveryTime: r.recovery_time,
53
+ lastUsed: r.last_used
35
54
  }));
36
- return new AccountManager(accounts, u.usage, strategy || 'sticky');
55
+ return new AccountManager(accounts, usage, strategy || 'sticky');
37
56
  }
38
57
  getAccountCount() {
39
58
  return this.accounts.length;
@@ -102,6 +121,7 @@ export class AccountManager {
102
121
  a.realEmail = meta.realEmail;
103
122
  }
104
123
  this.usage[id] = { ...meta, lastSync: Date.now() };
124
+ kiroDb.upsertUsage(id, this.usage[id]);
105
125
  }
106
126
  addAccount(a) {
107
127
  const i = this.accounts.findIndex((x) => x.id === a.id);
@@ -109,6 +129,7 @@ export class AccountManager {
109
129
  this.accounts.push(a);
110
130
  else
111
131
  this.accounts[i] = a;
132
+ kiroDb.upsertAccount(a);
112
133
  }
113
134
  removeAccount(a) {
114
135
  const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
@@ -116,15 +137,13 @@ export class AccountManager {
116
137
  return;
117
138
  this.accounts = this.accounts.filter((x) => x.id !== a.id);
118
139
  delete this.usage[a.id];
119
- if (this.accounts.length === 0) {
140
+ kiroDb.deleteAccount(a.id);
141
+ if (this.accounts.length === 0)
120
142
  this.cursor = 0;
121
- }
122
- else if (this.cursor >= this.accounts.length) {
143
+ else if (this.cursor >= this.accounts.length)
123
144
  this.cursor = this.accounts.length - 1;
124
- }
125
- else if (removedIndex <= this.cursor && this.cursor > 0) {
145
+ else if (removedIndex <= this.cursor && this.cursor > 0)
126
146
  this.cursor--;
127
- }
128
147
  }
129
148
  updateFromAuth(a, auth) {
130
149
  const acc = this.accounts.find((x) => x.id === a.id);
@@ -140,12 +159,16 @@ export class AccountManager {
140
159
  acc.profileArn = p.profileArn;
141
160
  if (p.clientId)
142
161
  acc.clientId = p.clientId;
162
+ kiroDb.upsertAccount(acc);
163
+ writeToKiroCli(acc).catch(() => { });
143
164
  }
144
165
  }
145
166
  markRateLimited(a, ms) {
146
167
  const acc = this.accounts.find((x) => x.id === a.id);
147
- if (acc)
168
+ if (acc) {
148
169
  acc.rateLimitResetTime = Date.now() + ms;
170
+ kiroDb.upsertAccount(acc);
171
+ }
149
172
  }
150
173
  markUnhealthy(a, reason, recovery) {
151
174
  const acc = this.accounts.find((x) => x.id === a.id);
@@ -153,12 +176,12 @@ export class AccountManager {
153
176
  acc.isHealthy = false;
154
177
  acc.unhealthyReason = reason;
155
178
  acc.recoveryTime = recovery;
179
+ kiroDb.upsertAccount(acc);
156
180
  }
157
181
  }
158
182
  async saveToDisk() {
159
- const metadata = this.accounts.map(({ usedCount, limitCount, lastUsed, ...rest }) => rest);
160
- await saveAccounts({ version: 1, accounts: metadata, activeIndex: this.cursor });
161
- await saveUsage({ version: 1, usage: this.usage });
183
+ for (const a of this.accounts)
184
+ kiroDb.upsertAccount(a);
162
185
  }
163
186
  toAuthDetails(a) {
164
187
  const p = {
@@ -1,5 +1,5 @@
1
- import { createInterface } from 'node:readline/promises';
2
1
  import { stdin as input, stdout as output } from 'node:process';
2
+ import { createInterface } from 'node:readline/promises';
3
3
  export async function promptAddAnotherAccount(currentCount) {
4
4
  const rl = createInterface({ input, output });
5
5
  try {
@@ -1,3 +1,3 @@
1
- export { KiroConfigSchema, DEFAULT_CONFIG } from './schema';
1
+ export { configExists, getDefaultLogsDir, getProjectConfigPath, getUserConfigPath, loadConfig } from './loader';
2
+ export { DEFAULT_CONFIG, KiroConfigSchema } from './schema';
2
3
  export type { KiroConfig } from './schema';
3
- export { loadConfig, getUserConfigPath, getProjectConfigPath, getDefaultLogsDir, configExists } from './loader';
@@ -1,2 +1,2 @@
1
- export { KiroConfigSchema, DEFAULT_CONFIG } from './schema';
2
- export { loadConfig, getUserConfigPath, getProjectConfigPath, getDefaultLogsDir, configExists } from './loader';
1
+ export { configExists, getDefaultLogsDir, getProjectConfigPath, getUserConfigPath, loadConfig } from './loader';
2
+ export { DEFAULT_CONFIG, KiroConfigSchema } from './schema';
@@ -1,8 +1,8 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
2
  import { homedir } from 'node:os';
4
- import { AccountSelectionStrategySchema, KiroConfigSchema, RegionSchema, DEFAULT_CONFIG } from './schema';
3
+ import { dirname, join } from 'node:path';
5
4
  import * as logger from '../logger';
5
+ import { AccountSelectionStrategySchema, DEFAULT_CONFIG, KiroConfigSchema, RegionSchema } from './schema';
6
6
  function getConfigDir() {
7
7
  const platform = process.platform;
8
8
  if (platform === 'win32') {
@@ -16,6 +16,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
16
16
  auth_server_port_start: z.ZodDefault<z.ZodNumber>;
17
17
  auth_server_port_range: z.ZodDefault<z.ZodNumber>;
18
18
  usage_tracking_enabled: z.ZodDefault<z.ZodBoolean>;
19
+ auto_sync_kiro_cli: z.ZodDefault<z.ZodBoolean>;
19
20
  enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
20
21
  }, "strip", z.ZodTypeAny, {
21
22
  account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
@@ -29,6 +30,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
29
30
  auth_server_port_start: number;
30
31
  auth_server_port_range: number;
31
32
  usage_tracking_enabled: boolean;
33
+ auto_sync_kiro_cli: boolean;
32
34
  enable_log_api_request: boolean;
33
35
  $schema?: string | undefined;
34
36
  }, {
@@ -44,6 +46,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
44
46
  auth_server_port_start?: number | undefined;
45
47
  auth_server_port_range?: number | undefined;
46
48
  usage_tracking_enabled?: boolean | undefined;
49
+ auto_sync_kiro_cli?: boolean | undefined;
47
50
  enable_log_api_request?: boolean | undefined;
48
51
  }>;
49
52
  export type KiroConfig = z.infer<typeof KiroConfigSchema>;
@@ -14,6 +14,7 @@ export const KiroConfigSchema = z.object({
14
14
  auth_server_port_start: z.number().min(1024).max(65535).default(19847),
15
15
  auth_server_port_range: z.number().min(1).max(100).default(10),
16
16
  usage_tracking_enabled: z.boolean().default(true),
17
+ auto_sync_kiro_cli: z.boolean().default(true),
17
18
  enable_log_api_request: z.boolean().default(false)
18
19
  });
19
20
  export const DEFAULT_CONFIG = {
@@ -28,5 +29,6 @@ export const DEFAULT_CONFIG = {
28
29
  auth_server_port_start: 19847,
29
30
  auth_server_port_range: 10,
30
31
  usage_tracking_enabled: true,
32
+ auto_sync_kiro_cli: true,
31
33
  enable_log_api_request: false
32
34
  };
@@ -1,4 +1,4 @@
1
1
  import type { KiroAuthDetails, PreparedRequest } from './types';
2
- export declare function transformToCodeWhisperer(url: string, body: any, model: string, auth: KiroAuthDetails, think?: boolean, budget?: number): PreparedRequest;
2
+ export declare function transformToCodeWhisperer(url: string, body: any, model: string, auth: KiroAuthDetails, think?: boolean, budget?: number, reductionFactor?: number): PreparedRequest;
3
3
  export declare function mergeAdjacentMessages(msgs: any[]): any[];
4
4
  export declare function convertToolsToCodeWhisperer(tools: any[]): any[];
@@ -46,7 +46,7 @@ function findOriginalToolCall(msgs, toolUseId) {
46
46
  }
47
47
  return null;
48
48
  }
49
- export function transformToCodeWhisperer(url, body, model, auth, think = false, budget = 20000) {
49
+ export function transformToCodeWhisperer(url, body, model, auth, think = false, budget = 20000, reductionFactor = 1.0) {
50
50
  const req = typeof body === 'string' ? JSON.parse(body) : body;
51
51
  const { messages, tools, system } = req;
52
52
  const convId = crypto.randomUUID();
@@ -94,6 +94,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
94
94
  });
95
95
  }
96
96
  }
97
+ const toolResultLimit = Math.floor(250000 * reductionFactor);
97
98
  for (let i = 0; i < msgs.length - 1; i++) {
98
99
  const m = msgs[i];
99
100
  if (!m)
@@ -107,7 +108,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
107
108
  uim.content += p.text || '';
108
109
  else if (p.type === 'tool_result')
109
110
  trs.push({
110
- content: [{ text: truncate(getContentText(p.content || p), 250000) }],
111
+ content: [{ text: truncate(getContentText(p.content || p), toolResultLimit) }],
111
112
  status: 'success',
112
113
  toolUseId: p.tool_use_id
113
114
  });
@@ -134,14 +135,14 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
134
135
  if (m.tool_results) {
135
136
  for (const tr of m.tool_results)
136
137
  trs.push({
137
- content: [{ text: truncate(getContentText(tr), 250000) }],
138
+ content: [{ text: truncate(getContentText(tr), toolResultLimit) }],
138
139
  status: 'success',
139
140
  toolUseId: tr.tool_call_id
140
141
  });
141
142
  }
142
143
  else {
143
144
  trs.push({
144
- content: [{ text: truncate(getContentText(m), 250000) }],
145
+ content: [{ text: truncate(getContentText(m), toolResultLimit) }],
145
146
  status: 'success',
146
147
  toolUseId: m.tool_call_id
147
148
  });
@@ -196,7 +197,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
196
197
  }
197
198
  history = sanitizeHistory(history);
198
199
  let historySize = JSON.stringify(history).length;
199
- while (historySize > 850000 && history.length > 2) {
200
+ const historyLimit = Math.floor(850000 * reductionFactor);
201
+ while (historySize > historyLimit && history.length > 2) {
200
202
  history.shift();
201
203
  while (history.length > 0) {
202
204
  const first = history[0];
@@ -258,14 +260,14 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
258
260
  if (curMsg.tool_results) {
259
261
  for (const tr of curMsg.tool_results)
260
262
  curTrs.push({
261
- content: [{ text: truncate(getContentText(tr), 250000) }],
263
+ content: [{ text: truncate(getContentText(tr), toolResultLimit) }],
262
264
  status: 'success',
263
265
  toolUseId: tr.tool_call_id
264
266
  });
265
267
  }
266
268
  else {
267
269
  curTrs.push({
268
- content: [{ text: truncate(getContentText(curMsg), 250000) }],
270
+ content: [{ text: truncate(getContentText(curMsg), toolResultLimit) }],
269
271
  status: 'success',
270
272
  toolUseId: curMsg.tool_call_id
271
273
  });
@@ -277,7 +279,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
277
279
  curContent += p.text || '';
278
280
  else if (p.type === 'tool_result')
279
281
  curTrs.push({
280
- content: [{ text: truncate(getContentText(p.content || p), 250000) }],
282
+ content: [{ text: truncate(getContentText(p.content || p), toolResultLimit) }],
281
283
  status: 'success',
282
284
  toolUseId: p.tool_use_id
283
285
  });
@@ -1,4 +1,4 @@
1
- import { ToolCall, ParsedResponse } from './types';
1
+ import { ParsedResponse, ToolCall } from './types';
2
2
  export declare function parseEventStream(rawResponse: string): ParsedResponse;
3
3
  export declare function parseEventLine(line: string): any | null;
4
4
  export declare function parseBracketToolCalls(text: string): ToolCall[];
@@ -1,5 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
- import { getIDCAuthHtml, getSuccessHtml, getErrorHtml } from './auth-page';
2
+ import { getErrorHtml, getIDCAuthHtml, getSuccessHtml } from './auth-page';
3
3
  import * as logger from './logger';
4
4
  async function tryPort(port) {
5
5
  return new Promise((resolve) => {
@@ -0,0 +1 @@
1
+ export declare function migrateJsonToSqlite(): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import * as logger from '../logger';
5
+ import { kiroDb } from './sqlite';
6
+ function getBaseDir() {
7
+ const platform = process.platform;
8
+ if (platform === 'win32') {
9
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
10
+ }
11
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
12
+ }
13
+ export async function migrateJsonToSqlite() {
14
+ const base = getBaseDir();
15
+ const accPath = join(base, 'kiro-accounts.json');
16
+ const usePath = join(base, 'kiro-usage.json');
17
+ try {
18
+ const accExists = await fs
19
+ .access(accPath)
20
+ .then(() => true)
21
+ .catch(() => false);
22
+ if (accExists) {
23
+ const data = JSON.parse(await fs.readFile(accPath, 'utf-8'));
24
+ if (data.accounts && Array.isArray(data.accounts)) {
25
+ for (const acc of data.accounts) {
26
+ kiroDb.upsertAccount({
27
+ ...acc,
28
+ rateLimitResetTime: acc.rateLimitResetTime || 0,
29
+ isHealthy: acc.isHealthy !== false,
30
+ lastUsed: acc.lastUsed || 0
31
+ });
32
+ }
33
+ }
34
+ await fs.rename(accPath, accPath + '.bak');
35
+ }
36
+ const useExists = await fs
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');
48
+ }
49
+ }
50
+ catch (e) {
51
+ logger.error('Migration failed', e);
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ import type { UsageMetadata } from '../types';
2
+ export declare const DB_PATH: string;
3
+ export declare class KiroDatabase {
4
+ private db;
5
+ constructor(path?: string);
6
+ private init;
7
+ getAccounts(): any[];
8
+ upsertAccount(acc: any): void;
9
+ 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
+ close(): void;
15
+ }
16
+ export declare const kiroDb: KiroDatabase;
@@ -0,0 +1,104 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ function getBaseDir() {
6
+ const p = process.platform;
7
+ if (p === 'win32')
8
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
9
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
10
+ }
11
+ export const DB_PATH = join(getBaseDir(), 'kiro.db');
12
+ export class KiroDatabase {
13
+ db;
14
+ constructor(path = DB_PATH) {
15
+ const dir = join(path, '..');
16
+ if (!existsSync(dir))
17
+ mkdirSync(dir, { recursive: true });
18
+ this.db = new Database(path);
19
+ this.db.run('PRAGMA busy_timeout = 5000');
20
+ this.init();
21
+ }
22
+ init() {
23
+ this.db.run('PRAGMA journal_mode = WAL');
24
+ this.db.run(`
25
+ CREATE TABLE IF NOT EXISTS accounts (
26
+ id TEXT PRIMARY KEY, email TEXT NOT NULL, real_email TEXT, auth_method TEXT NOT NULL,
27
+ region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
28
+ refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
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
38
+ )
39
+ `);
40
+ this.db.run('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)');
41
+ }
42
+ getAccounts() {
43
+ return this.db.prepare('SELECT * FROM accounts').all();
44
+ }
45
+ upsertAccount(acc) {
46
+ this.db
47
+ .prepare(`
48
+ INSERT INTO accounts (
49
+ id, email, real_email, auth_method, region, client_id, client_secret,
50
+ profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
51
+ is_healthy, unhealthy_reason, recovery_time, last_used
52
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
+ ON CONFLICT(id) DO UPDATE SET
54
+ email=excluded.email, real_email=excluded.real_email, auth_method=excluded.auth_method,
55
+ region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
56
+ profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
57
+ access_token=excluded.access_token, expires_at=excluded.expires_at,
58
+ rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
59
+ unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
60
+ last_used=excluded.last_used
61
+ `)
62
+ .run(acc.id, acc.email, acc.realEmail || null, 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.lastUsed || 0);
63
+ }
64
+ deleteAccount(id) {
65
+ this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
66
+ }
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
+ close() {
101
+ this.db.close();
102
+ }
103
+ }
104
+ export const kiroDb = new KiroDatabase();
@@ -0,0 +1,2 @@
1
+ export declare function syncFromKiroCli(): Promise<void>;
2
+ export declare function writeToKiroCli(acc: any): Promise<void>;