@zhafron/opencode-kiro-auth 1.2.5 → 1.2.7

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.
@@ -0,0 +1,7 @@
1
+ export declare function promptAddAnotherAccount(currentCount: number): Promise<boolean>;
2
+ export type LoginMode = 'add' | 'fresh';
3
+ export interface ExistingAccountInfo {
4
+ email?: string;
5
+ index: number;
6
+ }
7
+ export declare function promptLoginMode(existingAccounts: ExistingAccountInfo[]): Promise<LoginMode>;
@@ -0,0 +1,38 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ export async function promptAddAnotherAccount(currentCount) {
4
+ const rl = createInterface({ input, output });
5
+ try {
6
+ const answer = await rl.question(`Add another account? (${currentCount} added) (y/n): `);
7
+ const normalized = answer.trim().toLowerCase();
8
+ return normalized === 'y' || normalized === 'yes';
9
+ }
10
+ finally {
11
+ rl.close();
12
+ }
13
+ }
14
+ export async function promptLoginMode(existingAccounts) {
15
+ const rl = createInterface({ input, output });
16
+ try {
17
+ console.log(`\n${existingAccounts.length} account(s) saved:`);
18
+ for (const acc of existingAccounts) {
19
+ const label = acc.email || `Account ${acc.index + 1}`;
20
+ console.log(` ${acc.index + 1}. ${label}`);
21
+ }
22
+ console.log('');
23
+ while (true) {
24
+ const answer = await rl.question('(a)dd new account(s) or (f)resh start? [a/f]: ');
25
+ const normalized = answer.trim().toLowerCase();
26
+ if (normalized === 'a' || normalized === 'add') {
27
+ return 'add';
28
+ }
29
+ if (normalized === 'f' || normalized === 'fresh') {
30
+ return 'fresh';
31
+ }
32
+ console.log("Please enter 'a' to add accounts or 'f' to start fresh.");
33
+ }
34
+ }
35
+ finally {
36
+ rl.close();
37
+ }
38
+ }
@@ -2,10 +2,39 @@ import * as crypto from 'crypto';
2
2
  import * as os from 'os';
3
3
  import { KIRO_CONSTANTS } from '../constants.js';
4
4
  import { resolveKiroModel } from './models.js';
5
- import * as logger from './logger.js';
5
+ function truncate(s, max) {
6
+ if (s.length <= max)
7
+ return s;
8
+ const half = Math.floor(max / 2);
9
+ return s.substring(0, half) + '\n... [TRUNCATED] ...\n' + s.substring(s.length - half);
10
+ }
11
+ function sanitizeHistory(history) {
12
+ const result = [];
13
+ for (let i = 0; i < history.length; i++) {
14
+ const m = history[i];
15
+ if (!m)
16
+ continue;
17
+ if (m.assistantResponseMessage?.toolUses) {
18
+ const next = history[i + 1];
19
+ if (next?.userInputMessage?.userInputMessageContext?.toolResults) {
20
+ result.push(m);
21
+ }
22
+ }
23
+ else if (m.userInputMessage?.userInputMessageContext?.toolResults) {
24
+ const prev = result[result.length - 1];
25
+ if (prev?.assistantResponseMessage?.toolUses) {
26
+ result.push(m);
27
+ }
28
+ }
29
+ else {
30
+ result.push(m);
31
+ }
32
+ }
33
+ return result;
34
+ }
6
35
  export function transformToCodeWhisperer(url, body, model, auth, think = false, budget = 20000) {
7
36
  const req = typeof body === 'string' ? JSON.parse(body) : body;
8
- const { messages, tools, system, conversationId } = req;
37
+ const { messages, tools, system } = req;
9
38
  const convId = crypto.randomUUID();
10
39
  if (!messages || messages.length === 0)
11
40
  throw new Error('No messages');
@@ -20,7 +49,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
20
49
  if (lastMsg && lastMsg.role === 'assistant' && getContentText(lastMsg) === '{')
21
50
  msgs.pop();
22
51
  const cwTools = tools ? convertToolsToCodeWhisperer(tools) : [];
23
- const history = [];
52
+ let history = [];
24
53
  let firstUserIndex = -1;
25
54
  for (let i = 0; i < msgs.length; i++) {
26
55
  if (msgs[i].role === 'user') {
@@ -38,9 +67,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
38
67
  ...m.content.filter((p) => p.type !== 'text')
39
68
  ];
40
69
  }
41
- else {
70
+ else
42
71
  m.content = `${sys}\n\n${oldContent}`;
43
- }
44
72
  }
45
73
  else {
46
74
  history.push({
@@ -65,7 +93,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
65
93
  uim.content += p.text || '';
66
94
  else if (p.type === 'tool_result')
67
95
  trs.push({
68
- content: [{ text: getContentText(p.content || p) }],
96
+ content: [{ text: truncate(getContentText(p.content || p), 250000) }],
69
97
  status: 'success',
70
98
  toolUseId: p.tool_use_id
71
99
  });
@@ -83,9 +111,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
83
111
  if (trs.length)
84
112
  uim.userInputMessageContext = { toolResults: deduplicateToolResults(trs) };
85
113
  const prev = history[history.length - 1];
86
- if (prev && prev.userInputMessage) {
114
+ if (prev && prev.userInputMessage)
87
115
  history.push({ assistantResponseMessage: { content: 'Continue' } });
88
- }
89
116
  history.push({ userInputMessage: uim });
90
117
  }
91
118
  else if (m.role === 'tool') {
@@ -93,22 +120,21 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
93
120
  if (m.tool_results) {
94
121
  for (const tr of m.tool_results)
95
122
  trs.push({
96
- content: [{ text: getContentText(tr) }],
123
+ content: [{ text: truncate(getContentText(tr), 250000) }],
97
124
  status: 'success',
98
125
  toolUseId: tr.tool_call_id
99
126
  });
100
127
  }
101
128
  else {
102
129
  trs.push({
103
- content: [{ text: getContentText(m) }],
130
+ content: [{ text: truncate(getContentText(m), 250000) }],
104
131
  status: 'success',
105
132
  toolUseId: m.tool_call_id
106
133
  });
107
134
  }
108
135
  const prev = history[history.length - 1];
109
- if (prev && prev.userInputMessage) {
136
+ if (prev && prev.userInputMessage)
110
137
  history.push({ assistantResponseMessage: { content: 'Continue' } });
111
- }
112
138
  history.push({
113
139
  userInputMessage: {
114
140
  content: 'Tool results provided.',
@@ -132,9 +158,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
132
158
  tus.push({ input: p.input, name: p.name, toolUseId: p.id });
133
159
  }
134
160
  }
135
- else {
161
+ else
136
162
  arm.content = getContentText(m);
137
- }
138
163
  if (m.tool_calls && Array.isArray(m.tool_calls)) {
139
164
  for (const tc of m.tool_calls) {
140
165
  tus.push({
@@ -155,6 +180,19 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
155
180
  history.push({ assistantResponseMessage: arm });
156
181
  }
157
182
  }
183
+ history = sanitizeHistory(history);
184
+ let historySize = JSON.stringify(history).length;
185
+ while (historySize > 850000 && history.length > 2) {
186
+ history.shift();
187
+ while (history.length > 0) {
188
+ const first = history[0];
189
+ if (first && first.userInputMessage)
190
+ break;
191
+ history.shift();
192
+ }
193
+ history = sanitizeHistory(history);
194
+ historySize = JSON.stringify(history).length;
195
+ }
158
196
  const curMsg = msgs[msgs.length - 1];
159
197
  if (!curMsg)
160
198
  throw new Error('Empty');
@@ -206,14 +244,14 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
206
244
  if (curMsg.tool_results) {
207
245
  for (const tr of curMsg.tool_results)
208
246
  curTrs.push({
209
- content: [{ text: getContentText(tr) }],
247
+ content: [{ text: truncate(getContentText(tr), 250000) }],
210
248
  status: 'success',
211
249
  toolUseId: tr.tool_call_id
212
250
  });
213
251
  }
214
252
  else {
215
253
  curTrs.push({
216
- content: [{ text: getContentText(curMsg) }],
254
+ content: [{ text: truncate(getContentText(curMsg), 250000) }],
217
255
  status: 'success',
218
256
  toolUseId: curMsg.tool_call_id
219
257
  });
@@ -225,7 +263,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
225
263
  curContent += p.text || '';
226
264
  else if (p.type === 'tool_result')
227
265
  curTrs.push({
228
- content: [{ text: getContentText(p.content || p) }],
266
+ content: [{ text: truncate(getContentText(p.content || p), 250000) }],
229
267
  status: 'success',
230
268
  toolUseId: p.tool_use_id
231
269
  });
@@ -254,10 +292,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
254
292
  }
255
293
  }
256
294
  };
257
- if (history.length > 0) {
258
- ;
295
+ if (history.length > 0)
259
296
  request.conversationState.history = history;
260
- }
261
297
  const uim = request.conversationState.currentMessage.userInputMessage;
262
298
  if (uim) {
263
299
  if (curImgs.length)
@@ -277,17 +313,11 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
277
313
  const existingToolNames = new Set(existingTools.map((t) => t.toolSpecification?.name).filter(Boolean));
278
314
  const missingToolNames = Array.from(toolNamesInHistory).filter((name) => !existingToolNames.has(name));
279
315
  if (missingToolNames.length > 0) {
280
- logger.log(`[Kiro] Adding ${missingToolNames.length} missing tool definitions: ${missingToolNames.join(', ')}`);
281
316
  const placeholderTools = missingToolNames.map((name) => ({
282
317
  toolSpecification: {
283
318
  name,
284
319
  description: 'Tool',
285
- inputSchema: {
286
- json: {
287
- type: 'object',
288
- properties: {}
289
- }
290
- }
320
+ inputSchema: { json: { type: 'object', properties: {} } }
291
321
  }
292
322
  }));
293
323
  if (!uim.userInputMessageContext)
@@ -76,7 +76,7 @@ function parseEventStreamChunk(rawText) {
76
76
  };
77
77
  });
78
78
  if (contextUsagePercentage !== undefined) {
79
- const totalTokens = Math.round((172500 * contextUsagePercentage) / 100);
79
+ const totalTokens = Math.round((200000 * contextUsagePercentage) / 100);
80
80
  outputTokens = estimateTokens(content);
81
81
  inputTokens = Math.max(0, totalTokens - outputTokens);
82
82
  }
@@ -224,7 +224,7 @@ export async function* transformKiroStream(response, model, conversationId) {
224
224
  }
225
225
  outputTokens = estimateTokens(totalContent);
226
226
  if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
227
- const totalTokens = Math.round((172500 * contextUsagePercentage) / 100);
227
+ const totalTokens = Math.round((200000 * contextUsagePercentage) / 100);
228
228
  inputTokens = Math.max(0, totalTokens - outputTokens);
229
229
  }
230
230
  yield convertToOpenAI({
package/dist/plugin.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare const createKiroPlugin: (id: string) => ({ client, directory }: a
10
10
  id: string;
11
11
  label: string;
12
12
  type: string;
13
- authorize: () => Promise<unknown>;
13
+ authorize: (inputs?: any) => Promise<unknown>;
14
14
  }[];
15
15
  };
16
16
  }>;
@@ -26,7 +26,7 @@ export declare const KiroOAuthPlugin: ({ client, directory }: any) => Promise<{
26
26
  id: string;
27
27
  label: string;
28
28
  type: string;
29
- authorize: () => Promise<unknown>;
29
+ authorize: (inputs?: any) => Promise<unknown>;
30
30
  }[];
31
31
  };
32
32
  }>;
package/dist/plugin.js CHANGED
@@ -10,6 +10,7 @@ import { fetchUsageLimits, updateAccountQuota } from './plugin/usage';
10
10
  import { authorizeKiroIDC } from './kiro/oauth-idc';
11
11
  import { startIDCAuthServer } from './plugin/server';
12
12
  import { KiroTokenRefreshError } from './plugin/errors';
13
+ import { promptAddAnotherAccount, promptLoginMode } from './plugin/cli';
13
14
  import { KIRO_CONSTANTS } from './constants';
14
15
  import * as logger from './plugin/logger';
15
16
  const KIRO_PROVIDER_ID = 'kiro';
@@ -292,8 +293,131 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
292
293
  id: 'idc',
293
294
  label: 'AWS Builder ID (IDC)',
294
295
  type: 'oauth',
295
- authorize: async () => new Promise(async (resolve) => {
296
+ authorize: async (inputs) => new Promise(async (resolve) => {
296
297
  const region = config.default_region;
298
+ if (inputs) {
299
+ const accounts = [];
300
+ let startFresh = true;
301
+ const existingAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
302
+ if (existingAm.getAccountCount() > 0) {
303
+ const existingAccounts = existingAm.getAccounts().map((acc, idx) => ({
304
+ email: acc.realEmail || acc.email,
305
+ index: idx
306
+ }));
307
+ const loginMode = await promptLoginMode(existingAccounts);
308
+ startFresh = loginMode === 'fresh';
309
+ console.log(startFresh
310
+ ? '\nStarting fresh - existing accounts will be replaced.\n'
311
+ : '\nAdding to existing accounts.\n');
312
+ }
313
+ while (true) {
314
+ console.log(`\n=== Kiro IDC Auth (Account ${accounts.length + 1}) ===\n`);
315
+ const result = await (async () => {
316
+ try {
317
+ const authData = await authorizeKiroIDC(region);
318
+ const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
319
+ console.log('OAuth URL:\n' + url + '\n');
320
+ openBrowser(url);
321
+ const res = await waitForAuth();
322
+ return res;
323
+ }
324
+ catch (e) {
325
+ return { type: 'failed', error: e.message };
326
+ }
327
+ })();
328
+ if ('type' in result && result.type === 'failed') {
329
+ if (accounts.length === 0) {
330
+ return resolve({
331
+ url: '',
332
+ instructions: `Authentication failed: ${result.error}`,
333
+ method: 'auto',
334
+ callback: async () => ({ type: 'failed' })
335
+ });
336
+ }
337
+ console.warn(`[opencode-kiro-auth] Skipping failed account ${accounts.length + 1}: ${result.error}`);
338
+ break;
339
+ }
340
+ const successResult = result;
341
+ accounts.push(successResult);
342
+ const isFirstAccount = accounts.length === 1;
343
+ const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
344
+ if (isFirstAccount && startFresh) {
345
+ am.getAccounts().forEach((acc) => am.removeAccount(acc));
346
+ }
347
+ const acc = {
348
+ id: generateAccountId(),
349
+ email: successResult.email,
350
+ authMethod: 'idc',
351
+ region,
352
+ clientId: successResult.clientId,
353
+ clientSecret: successResult.clientSecret,
354
+ refreshToken: successResult.refreshToken,
355
+ accessToken: successResult.accessToken,
356
+ expiresAt: successResult.expiresAt,
357
+ rateLimitResetTime: 0,
358
+ isHealthy: true
359
+ };
360
+ try {
361
+ const u = await fetchUsageLimits({
362
+ refresh: encodeRefreshToken({
363
+ refreshToken: successResult.refreshToken,
364
+ clientId: successResult.clientId,
365
+ clientSecret: successResult.clientSecret,
366
+ authMethod: 'idc'
367
+ }),
368
+ access: successResult.accessToken,
369
+ expires: successResult.expiresAt,
370
+ authMethod: 'idc',
371
+ region,
372
+ clientId: successResult.clientId,
373
+ clientSecret: successResult.clientSecret,
374
+ email: successResult.email
375
+ });
376
+ am.updateUsage(acc.id, {
377
+ usedCount: u.usedCount,
378
+ limitCount: u.limitCount,
379
+ realEmail: u.email
380
+ });
381
+ }
382
+ catch (e) {
383
+ logger.warn(`Initial usage fetch failed: ${e.message}`, e);
384
+ }
385
+ am.addAccount(acc);
386
+ await am.saveToDisk();
387
+ showToast(`Account ${accounts.length} authenticated${successResult.email ? ` (${successResult.email})` : ''}`, 'success');
388
+ let currentAccountCount = accounts.length;
389
+ try {
390
+ const currentStorage = await AccountManager.loadFromDisk(config.account_selection_strategy);
391
+ currentAccountCount = currentStorage.getAccountCount();
392
+ }
393
+ catch { }
394
+ const addAnother = await promptAddAnotherAccount(currentAccountCount);
395
+ if (!addAnother) {
396
+ break;
397
+ }
398
+ }
399
+ const primary = accounts[0];
400
+ if (!primary) {
401
+ return resolve({
402
+ url: '',
403
+ instructions: 'Authentication cancelled',
404
+ method: 'auto',
405
+ callback: async () => ({ type: 'failed' })
406
+ });
407
+ }
408
+ let actualAccountCount = accounts.length;
409
+ try {
410
+ const finalStorage = await AccountManager.loadFromDisk(config.account_selection_strategy);
411
+ actualAccountCount = finalStorage.getAccountCount();
412
+ }
413
+ catch { }
414
+ return resolve({
415
+ url: '',
416
+ instructions: `Multi-account setup complete (${actualAccountCount} account(s)).`,
417
+ method: 'auto',
418
+ callback: async () => ({ type: 'success', key: primary.accessToken })
419
+ });
420
+ }
297
421
  try {
298
422
  const authData = await authorizeKiroIDC(region);
299
423
  const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",