@zhafron/opencode-kiro-auth 1.0.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 +85 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/kiro/auth.d.ts +5 -0
- package/dist/kiro/auth.js +24 -0
- package/dist/kiro/oauth-idc.d.ts +24 -0
- package/dist/kiro/oauth-idc.js +132 -0
- package/dist/kiro/oauth-social.d.ts +17 -0
- package/dist/kiro/oauth-social.js +51 -0
- package/dist/plugin/accounts.d.ts +28 -0
- package/dist/plugin/accounts.js +156 -0
- package/dist/plugin/auth-page.d.ts +3 -0
- package/dist/plugin/auth-page.js +568 -0
- package/dist/plugin/cli.d.ts +6 -0
- package/dist/plugin/cli.js +98 -0
- package/dist/plugin/config/index.d.ts +3 -0
- package/dist/plugin/config/index.js +2 -0
- package/dist/plugin/config/loader.d.ts +6 -0
- package/dist/plugin/config/loader.js +125 -0
- package/dist/plugin/config/schema.d.ts +44 -0
- package/dist/plugin/config/schema.js +28 -0
- package/dist/plugin/debug.d.ts +2 -0
- package/dist/plugin/debug.js +9 -0
- package/dist/plugin/errors.d.ts +17 -0
- package/dist/plugin/errors.js +34 -0
- package/dist/plugin/logger.d.ts +4 -0
- package/dist/plugin/logger.js +37 -0
- package/dist/plugin/models.d.ts +3 -0
- package/dist/plugin/models.js +14 -0
- package/dist/plugin/oauth-parser.d.ts +5 -0
- package/dist/plugin/oauth-parser.js +23 -0
- package/dist/plugin/quota.d.ts +15 -0
- package/dist/plugin/quota.js +68 -0
- package/dist/plugin/recovery.d.ts +19 -0
- package/dist/plugin/recovery.js +302 -0
- package/dist/plugin/refresh-queue.d.ts +14 -0
- package/dist/plugin/refresh-queue.js +69 -0
- package/dist/plugin/request.d.ts +4 -0
- package/dist/plugin/request.js +240 -0
- package/dist/plugin/response.d.ts +6 -0
- package/dist/plugin/response.js +246 -0
- package/dist/plugin/server.d.ts +24 -0
- package/dist/plugin/server.js +96 -0
- package/dist/plugin/storage.d.ts +7 -0
- package/dist/plugin/storage.js +75 -0
- package/dist/plugin/streaming.d.ts +3 -0
- package/dist/plugin/streaming.js +503 -0
- package/dist/plugin/token.d.ts +2 -0
- package/dist/plugin/token.js +56 -0
- package/dist/plugin/types.d.ts +148 -0
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/usage.d.ts +3 -0
- package/dist/plugin/usage.js +36 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.js +222 -0
- package/dist/src/constants.d.ts +22 -0
- package/dist/src/constants.js +35 -0
- package/dist/src/kiro/auth.d.ts +5 -0
- package/dist/src/kiro/auth.js +69 -0
- package/dist/src/kiro/oauth-idc.d.ts +22 -0
- package/dist/src/kiro/oauth-idc.js +99 -0
- package/dist/src/kiro/oauth-social.d.ts +17 -0
- package/dist/src/kiro/oauth-social.js +69 -0
- package/dist/src/plugin/accounts.d.ts +23 -0
- package/dist/src/plugin/accounts.js +265 -0
- package/dist/src/plugin/cli.d.ts +6 -0
- package/dist/src/plugin/cli.js +98 -0
- package/dist/src/plugin/config/index.d.ts +3 -0
- package/dist/src/plugin/config/index.js +2 -0
- package/dist/src/plugin/config/loader.d.ts +7 -0
- package/dist/src/plugin/config/loader.js +143 -0
- package/dist/src/plugin/config/schema.d.ts +68 -0
- package/dist/src/plugin/config/schema.js +44 -0
- package/dist/src/plugin/debug.d.ts +2 -0
- package/dist/src/plugin/debug.js +9 -0
- package/dist/src/plugin/errors.d.ts +17 -0
- package/dist/src/plugin/errors.js +34 -0
- package/dist/src/plugin/logger.d.ts +4 -0
- package/dist/src/plugin/logger.js +17 -0
- package/dist/src/plugin/models.d.ts +3 -0
- package/dist/src/plugin/models.js +14 -0
- package/dist/src/plugin/oauth-parser.d.ts +5 -0
- package/dist/src/plugin/oauth-parser.js +23 -0
- package/dist/src/plugin/quota.d.ts +25 -0
- package/dist/src/plugin/quota.js +175 -0
- package/dist/src/plugin/recovery.d.ts +19 -0
- package/dist/src/plugin/recovery.js +302 -0
- package/dist/src/plugin/refresh-queue.d.ts +14 -0
- package/dist/src/plugin/refresh-queue.js +69 -0
- package/dist/src/plugin/request.d.ts +35 -0
- package/dist/src/plugin/request.js +411 -0
- package/dist/src/plugin/response.d.ts +6 -0
- package/dist/src/plugin/response.js +246 -0
- package/dist/src/plugin/server.d.ts +10 -0
- package/dist/src/plugin/server.js +203 -0
- package/dist/src/plugin/storage.d.ts +5 -0
- package/dist/src/plugin/storage.js +106 -0
- package/dist/src/plugin/streaming.d.ts +12 -0
- package/dist/src/plugin/streaming.js +444 -0
- package/dist/src/plugin/token.d.ts +8 -0
- package/dist/src/plugin/token.js +130 -0
- package/dist/src/plugin/types.d.ts +144 -0
- package/dist/src/plugin/types.js +0 -0
- package/dist/src/plugin/usage.d.ts +28 -0
- package/dist/src/plugin/usage.js +159 -0
- package/dist/src/plugin.d.ts +2 -0
- package/dist/src/plugin.js +341 -0
- package/package.json +57 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import lockfile from 'proper-lockfile';
|
|
5
|
+
import { xdgConfig } from 'xdg-basedir';
|
|
6
|
+
import * as logger from './logger';
|
|
7
|
+
import { KiroTokenRefreshError, KiroQuotaExhaustedError, KiroRateLimitError, KiroAuthError } from './errors';
|
|
8
|
+
const LOCK_OPTIONS = {
|
|
9
|
+
stale: 10000,
|
|
10
|
+
retries: {
|
|
11
|
+
retries: 5,
|
|
12
|
+
minTimeout: 100,
|
|
13
|
+
maxTimeout: 1000,
|
|
14
|
+
factor: 2,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const MAX_ERROR_COUNT = 3;
|
|
18
|
+
function getRecoveryPath() {
|
|
19
|
+
const configDir = xdgConfig || `${process.env.HOME}/.config`;
|
|
20
|
+
return `${configDir}/opencode/kiro-recovery.json`;
|
|
21
|
+
}
|
|
22
|
+
async function ensureFileExists(path) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(path);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
28
|
+
const defaultStorage = {
|
|
29
|
+
version: 1,
|
|
30
|
+
sessions: {},
|
|
31
|
+
};
|
|
32
|
+
await fs.writeFile(path, JSON.stringify(defaultStorage, null, 2), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function withFileLock(path, fn) {
|
|
36
|
+
await ensureFileExists(path);
|
|
37
|
+
let release = null;
|
|
38
|
+
try {
|
|
39
|
+
release = await lockfile.lock(path, LOCK_OPTIONS);
|
|
40
|
+
return await fn();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error('Recovery file lock operation failed', error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (release) {
|
|
48
|
+
try {
|
|
49
|
+
await release();
|
|
50
|
+
}
|
|
51
|
+
catch (unlockError) {
|
|
52
|
+
logger.warn('Failed to release recovery lock', unlockError);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function loadRecoveryState() {
|
|
58
|
+
const path = getRecoveryPath();
|
|
59
|
+
try {
|
|
60
|
+
await ensureFileExists(path);
|
|
61
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
62
|
+
const data = JSON.parse(content);
|
|
63
|
+
if (data.version !== 1) {
|
|
64
|
+
logger.warn('Unknown recovery storage version, returning default');
|
|
65
|
+
return {
|
|
66
|
+
version: 1,
|
|
67
|
+
sessions: {},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (typeof data.sessions !== 'object' || data.sessions === null) {
|
|
71
|
+
logger.warn('Invalid sessions object, returning default');
|
|
72
|
+
return {
|
|
73
|
+
version: 1,
|
|
74
|
+
sessions: {},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const code = error.code;
|
|
81
|
+
if (code === 'ENOENT') {
|
|
82
|
+
return {
|
|
83
|
+
version: 1,
|
|
84
|
+
sessions: {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
logger.error('Failed to load recovery state', error);
|
|
88
|
+
return {
|
|
89
|
+
version: 1,
|
|
90
|
+
sessions: {},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function saveRecoveryState(state) {
|
|
95
|
+
const path = getRecoveryPath();
|
|
96
|
+
await withFileLock(path, async () => {
|
|
97
|
+
const tempPath = `${path}.${randomBytes(6).toString('hex')}.tmp`;
|
|
98
|
+
const content = JSON.stringify(state, null, 2);
|
|
99
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
100
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
101
|
+
await fs.rename(tempPath, path);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function isNetworkError(error) {
|
|
105
|
+
if (!error)
|
|
106
|
+
return false;
|
|
107
|
+
const errorCode = error.code || error.errno;
|
|
108
|
+
if (typeof errorCode === 'string') {
|
|
109
|
+
const networkCodes = [
|
|
110
|
+
'ECONNRESET',
|
|
111
|
+
'ECONNREFUSED',
|
|
112
|
+
'ETIMEDOUT',
|
|
113
|
+
'ENOTFOUND',
|
|
114
|
+
'ENETUNREACH',
|
|
115
|
+
'EHOSTUNREACH',
|
|
116
|
+
'EPIPE',
|
|
117
|
+
'EAI_AGAIN',
|
|
118
|
+
];
|
|
119
|
+
if (networkCodes.includes(errorCode)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const message = error.message || error.toString?.() || '';
|
|
124
|
+
const lowerMessage = message.toLowerCase();
|
|
125
|
+
const networkPatterns = [
|
|
126
|
+
'network',
|
|
127
|
+
'connection',
|
|
128
|
+
'timeout',
|
|
129
|
+
'econnreset',
|
|
130
|
+
'econnrefused',
|
|
131
|
+
'socket hang up',
|
|
132
|
+
'fetch failed',
|
|
133
|
+
];
|
|
134
|
+
return networkPatterns.some(pattern => lowerMessage.includes(pattern));
|
|
135
|
+
}
|
|
136
|
+
function getStatusCode(error) {
|
|
137
|
+
if (!error)
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof error.statusCode === 'number') {
|
|
140
|
+
return error.statusCode;
|
|
141
|
+
}
|
|
142
|
+
if (typeof error.status === 'number') {
|
|
143
|
+
return error.status;
|
|
144
|
+
}
|
|
145
|
+
if (error.response && typeof error.response.status === 'number') {
|
|
146
|
+
return error.response.status;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function isRecoverableStatusCode(statusCode) {
|
|
151
|
+
const recoverableCodes = [
|
|
152
|
+
429,
|
|
153
|
+
502,
|
|
154
|
+
503,
|
|
155
|
+
504,
|
|
156
|
+
];
|
|
157
|
+
return recoverableCodes.includes(statusCode);
|
|
158
|
+
}
|
|
159
|
+
function isUnrecoverableStatusCode(statusCode) {
|
|
160
|
+
const unrecoverableCodes = [
|
|
161
|
+
400,
|
|
162
|
+
402,
|
|
163
|
+
403,
|
|
164
|
+
404,
|
|
165
|
+
];
|
|
166
|
+
return unrecoverableCodes.includes(statusCode);
|
|
167
|
+
}
|
|
168
|
+
function isRecoverableErrorImpl(error) {
|
|
169
|
+
if (!error)
|
|
170
|
+
return false;
|
|
171
|
+
if (error instanceof KiroTokenRefreshError) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (error instanceof KiroQuotaExhaustedError) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (error instanceof KiroRateLimitError) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (error instanceof KiroAuthError) {
|
|
181
|
+
const statusCode = error.statusCode;
|
|
182
|
+
if (statusCode === 401) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
if (statusCode && isUnrecoverableStatusCode(statusCode)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (isNetworkError(error)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const statusCode = getStatusCode(error);
|
|
194
|
+
if (statusCode !== null) {
|
|
195
|
+
if (isUnrecoverableStatusCode(statusCode)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (isRecoverableStatusCode(statusCode)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function getErrorMessage(error) {
|
|
205
|
+
if (!error)
|
|
206
|
+
return 'Unknown error';
|
|
207
|
+
if (typeof error === 'string') {
|
|
208
|
+
return error;
|
|
209
|
+
}
|
|
210
|
+
if (error.message && typeof error.message === 'string') {
|
|
211
|
+
return error.message;
|
|
212
|
+
}
|
|
213
|
+
if (error.toString && typeof error.toString === 'function') {
|
|
214
|
+
return error.toString();
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return JSON.stringify(error);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return 'Unknown error';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function createSessionRecoveryHook(enabled, autoResume) {
|
|
224
|
+
const handleSessionError = async (error, sessionId) => {
|
|
225
|
+
if (!enabled) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (!sessionId) {
|
|
229
|
+
logger.warn('No session ID provided for recovery');
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (!isRecoverableErrorImpl(error)) {
|
|
233
|
+
logger.debug('Error is not recoverable', { sessionId, error: getErrorMessage(error) });
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const storage = await loadRecoveryState();
|
|
238
|
+
let sessionState = storage.sessions[sessionId];
|
|
239
|
+
if (!sessionState) {
|
|
240
|
+
sessionState = {
|
|
241
|
+
sessionId,
|
|
242
|
+
conversationId: '',
|
|
243
|
+
model: '',
|
|
244
|
+
lastMessageIndex: 0,
|
|
245
|
+
errorCount: 1,
|
|
246
|
+
lastError: getErrorMessage(error),
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
sessionState.errorCount += 1;
|
|
252
|
+
sessionState.lastError = getErrorMessage(error);
|
|
253
|
+
sessionState.timestamp = Date.now();
|
|
254
|
+
}
|
|
255
|
+
if (sessionState.errorCount > MAX_ERROR_COUNT) {
|
|
256
|
+
logger.warn('Session error count exceeded maximum', {
|
|
257
|
+
sessionId,
|
|
258
|
+
errorCount: sessionState.errorCount
|
|
259
|
+
});
|
|
260
|
+
delete storage.sessions[sessionId];
|
|
261
|
+
await saveRecoveryState(storage);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
storage.sessions[sessionId] = sessionState;
|
|
265
|
+
await saveRecoveryState(storage);
|
|
266
|
+
logger.log('Session error recorded for recovery', {
|
|
267
|
+
sessionId,
|
|
268
|
+
errorCount: sessionState.errorCount,
|
|
269
|
+
shouldRetry: true
|
|
270
|
+
});
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch (storageError) {
|
|
274
|
+
logger.error('Failed to handle session error', storageError);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const isRecoverableError = (error) => {
|
|
279
|
+
return isRecoverableErrorImpl(error);
|
|
280
|
+
};
|
|
281
|
+
const clearSession = async (sessionId) => {
|
|
282
|
+
if (!sessionId) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const storage = await loadRecoveryState();
|
|
287
|
+
if (storage.sessions[sessionId]) {
|
|
288
|
+
delete storage.sessions[sessionId];
|
|
289
|
+
await saveRecoveryState(storage);
|
|
290
|
+
logger.debug('Session cleared from recovery state', { sessionId });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
logger.error('Failed to clear session', error);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
handleSessionError,
|
|
299
|
+
isRecoverableError,
|
|
300
|
+
clearSession,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { KiroAuthDetails } from './types';
|
|
2
|
+
import type { AccountManager } from './accounts';
|
|
3
|
+
export interface ProactiveRefreshQueue {
|
|
4
|
+
start(): void;
|
|
5
|
+
stop(): void;
|
|
6
|
+
setAccountManager(manager: AccountManager): void;
|
|
7
|
+
}
|
|
8
|
+
export interface RefreshQueueConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
checkIntervalSeconds: number;
|
|
11
|
+
bufferSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function isTokenExpiringSoon(auth: KiroAuthDetails, bufferMs: number): boolean;
|
|
14
|
+
export declare function createProactiveRefreshQueue(config: RefreshQueueConfig): ProactiveRefreshQueue;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { refreshAccessToken } from './token';
|
|
2
|
+
import * as logger from './logger';
|
|
3
|
+
export function isTokenExpiringSoon(auth, bufferMs) {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const expiresAt = auth.expires;
|
|
6
|
+
const timeUntilExpiry = expiresAt - now;
|
|
7
|
+
return timeUntilExpiry <= bufferMs && timeUntilExpiry > 0;
|
|
8
|
+
}
|
|
9
|
+
export function createProactiveRefreshQueue(config) {
|
|
10
|
+
let intervalId = null;
|
|
11
|
+
let accountManager = null;
|
|
12
|
+
let isRunning = false;
|
|
13
|
+
async function checkAndRefresh() {
|
|
14
|
+
if (!accountManager) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const accounts = accountManager.getAccounts();
|
|
19
|
+
const bufferMs = config.bufferSeconds * 1000;
|
|
20
|
+
for (const account of accounts) {
|
|
21
|
+
try {
|
|
22
|
+
const auth = accountManager.toAuthDetails(account);
|
|
23
|
+
if (isTokenExpiringSoon(auth, bufferMs)) {
|
|
24
|
+
logger.log(`[RefreshQueue] Token expiring soon for account ${account.email}, refreshing...`);
|
|
25
|
+
const refreshedAuth = await refreshAccessToken(auth);
|
|
26
|
+
accountManager.updateFromAuth(account, refreshedAuth);
|
|
27
|
+
await accountManager.saveToDisk();
|
|
28
|
+
logger.log(`[RefreshQueue] Successfully refreshed token for account ${account.email}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error(`[RefreshQueue] Failed to refresh token for account ${account.email}: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error(`[RefreshQueue] Error during refresh check: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function start() {
|
|
41
|
+
if (!config.enabled || isRunning) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const intervalMs = config.checkIntervalSeconds * 1000;
|
|
45
|
+
intervalId = setInterval(() => {
|
|
46
|
+
checkAndRefresh().catch((error) => {
|
|
47
|
+
logger.error(`[RefreshQueue] Unhandled error in refresh check: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
});
|
|
49
|
+
}, intervalMs);
|
|
50
|
+
isRunning = true;
|
|
51
|
+
logger.log(`[RefreshQueue] Started proactive refresh queue (interval: ${config.checkIntervalSeconds}s, buffer: ${config.bufferSeconds}s)`);
|
|
52
|
+
}
|
|
53
|
+
function stop() {
|
|
54
|
+
if (intervalId) {
|
|
55
|
+
clearInterval(intervalId);
|
|
56
|
+
intervalId = null;
|
|
57
|
+
}
|
|
58
|
+
isRunning = false;
|
|
59
|
+
logger.log('[RefreshQueue] Stopped proactive refresh queue');
|
|
60
|
+
}
|
|
61
|
+
function setAccountManager(manager) {
|
|
62
|
+
accountManager = manager;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
start,
|
|
66
|
+
stop,
|
|
67
|
+
setAccountManager,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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;
|
|
3
|
+
export declare function mergeAdjacentMessages(msgs: any[]): any[];
|
|
4
|
+
export declare function convertToolsToCodeWhisperer(tools: any[]): any[];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import { KIRO_CONSTANTS } from '../constants';
|
|
4
|
+
import { resolveKiroModel } from './models';
|
|
5
|
+
export function transformToCodeWhisperer(url, body, model, auth, think = false, budget = 20000) {
|
|
6
|
+
const req = typeof body === 'string' ? JSON.parse(body) : body;
|
|
7
|
+
const { messages, tools, system } = req;
|
|
8
|
+
if (!messages || messages.length === 0)
|
|
9
|
+
throw new Error('No messages');
|
|
10
|
+
const resolved = resolveKiroModel(model);
|
|
11
|
+
const convId = crypto.randomUUID();
|
|
12
|
+
let sys = system || '';
|
|
13
|
+
if (think) {
|
|
14
|
+
const pfx = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`;
|
|
15
|
+
sys = sys.includes('<thinking_mode>') ? sys : sys ? `${pfx}\n${sys}` : pfx;
|
|
16
|
+
}
|
|
17
|
+
const msgs = mergeAdjacentMessages([...messages]);
|
|
18
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
19
|
+
if (lastMsg && lastMsg.role === 'assistant' && getContentText(lastMsg) === '{')
|
|
20
|
+
msgs.pop();
|
|
21
|
+
const cwTools = tools ? convertToolsToCodeWhisperer(tools) : [];
|
|
22
|
+
const history = [];
|
|
23
|
+
let start = 0;
|
|
24
|
+
if (sys) {
|
|
25
|
+
const first = msgs[0];
|
|
26
|
+
if (first && first.role === 'user') {
|
|
27
|
+
history.push({ userInputMessage: { content: `${sys}\n\n${getContentText(first)}`, modelId: resolved, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR } });
|
|
28
|
+
start = 1;
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
history.push({ userInputMessage: { content: sys, modelId: resolved, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR } });
|
|
32
|
+
}
|
|
33
|
+
for (let i = start; i < msgs.length - 1; i++) {
|
|
34
|
+
const m = msgs[i];
|
|
35
|
+
if (!m)
|
|
36
|
+
continue;
|
|
37
|
+
if (m.role === 'user') {
|
|
38
|
+
const uim = { content: '', modelId: resolved, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR };
|
|
39
|
+
const trs = [], imgs = [];
|
|
40
|
+
if (Array.isArray(m.content)) {
|
|
41
|
+
for (const p of m.content) {
|
|
42
|
+
if (p.type === 'text')
|
|
43
|
+
uim.content += p.text || '';
|
|
44
|
+
else if (p.type === 'tool_result')
|
|
45
|
+
trs.push({ content: [{ text: getContentText(p) }], status: 'success', toolUseId: p.tool_use_id });
|
|
46
|
+
else if (p.type === 'image' && p.source)
|
|
47
|
+
imgs.push({ format: p.source.media_type?.split('/')[1] || 'png', source: { bytes: p.source.data } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
uim.content = getContentText(m);
|
|
52
|
+
if (imgs.length)
|
|
53
|
+
uim.images = imgs;
|
|
54
|
+
if (trs.length)
|
|
55
|
+
uim.userInputMessageContext = { toolResults: deduplicateToolResults(trs) };
|
|
56
|
+
const prev = history[history.length - 1];
|
|
57
|
+
if (prev && prev.userInputMessage)
|
|
58
|
+
history.push({ assistantResponseMessage: { content: 'Continue' } });
|
|
59
|
+
history.push({ userInputMessage: uim });
|
|
60
|
+
}
|
|
61
|
+
else if (m.role === 'assistant') {
|
|
62
|
+
const arm = { content: '' };
|
|
63
|
+
const tus = [];
|
|
64
|
+
let th = '';
|
|
65
|
+
if (Array.isArray(m.content)) {
|
|
66
|
+
for (const p of m.content) {
|
|
67
|
+
if (p.type === 'text')
|
|
68
|
+
arm.content += p.text || '';
|
|
69
|
+
else if (p.type === 'thinking')
|
|
70
|
+
th += p.thinking || p.text || '';
|
|
71
|
+
else if (p.type === 'tool_use')
|
|
72
|
+
tus.push({ input: p.input, name: p.name, toolUseId: p.id });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
arm.content = getContentText(m);
|
|
77
|
+
if (th)
|
|
78
|
+
arm.content = arm.content ? `<thinking>${th}</thinking>\n\n${arm.content}` : `<thinking>${th}</thinking>`;
|
|
79
|
+
if (tus.length)
|
|
80
|
+
arm.toolUses = tus;
|
|
81
|
+
const prev = history[history.length - 1];
|
|
82
|
+
if (prev && prev.assistantResponseMessage)
|
|
83
|
+
history.push({ userInputMessage: { content: 'Continue', modelId: resolved, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR } });
|
|
84
|
+
history.push({ assistantResponseMessage: arm });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const curMsg = msgs[msgs.length - 1];
|
|
88
|
+
if (!curMsg)
|
|
89
|
+
throw new Error('Empty');
|
|
90
|
+
let curContent = '';
|
|
91
|
+
const curTrs = [], curImgs = [];
|
|
92
|
+
if (curMsg.role === 'assistant') {
|
|
93
|
+
const arm = { content: '' };
|
|
94
|
+
let th = '';
|
|
95
|
+
if (Array.isArray(curMsg.content)) {
|
|
96
|
+
for (const p of curMsg.content) {
|
|
97
|
+
if (p.type === 'text')
|
|
98
|
+
arm.content += p.text || '';
|
|
99
|
+
else if (p.type === 'thinking')
|
|
100
|
+
th += p.thinking || p.text || '';
|
|
101
|
+
else if (p.type === 'tool_use') {
|
|
102
|
+
if (!arm.toolUses)
|
|
103
|
+
arm.toolUses = [];
|
|
104
|
+
arm.toolUses.push({ input: p.input, name: p.name, toolUseId: p.id });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
arm.content = getContentText(curMsg);
|
|
110
|
+
if (th)
|
|
111
|
+
arm.content = arm.content ? `<thinking>${th}</thinking>\n\n${arm.content}` : `<thinking>${th}</thinking>`;
|
|
112
|
+
history.push({ assistantResponseMessage: arm });
|
|
113
|
+
curContent = 'Continue';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const prev = history[history.length - 1];
|
|
117
|
+
if (prev && !prev.assistantResponseMessage)
|
|
118
|
+
history.push({ assistantResponseMessage: { content: 'Continue' } });
|
|
119
|
+
if (Array.isArray(curMsg.content)) {
|
|
120
|
+
for (const p of curMsg.content) {
|
|
121
|
+
if (p.type === 'text')
|
|
122
|
+
curContent += p.text || '';
|
|
123
|
+
else if (p.type === 'tool_result')
|
|
124
|
+
curTrs.push({ content: [{ text: getContentText(p) }], status: 'success', toolUseId: p.tool_use_id });
|
|
125
|
+
else if (p.type === 'image' && p.source)
|
|
126
|
+
curImgs.push({ format: p.source.media_type?.split('/')[1] || 'png', source: { bytes: p.source.data } });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
curContent = getContentText(curMsg);
|
|
131
|
+
if (!curContent)
|
|
132
|
+
curContent = curTrs.length ? 'Tool results provided.' : 'Continue';
|
|
133
|
+
}
|
|
134
|
+
const request = {
|
|
135
|
+
conversationState: {
|
|
136
|
+
chatTriggerType: KIRO_CONSTANTS.CHAT_TRIGGER_TYPE_MANUAL,
|
|
137
|
+
conversationId: convId,
|
|
138
|
+
history,
|
|
139
|
+
currentMessage: { userInputMessage: { content: curContent, modelId: resolved, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR } }
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const uim = request.conversationState.currentMessage.userInputMessage;
|
|
143
|
+
if (uim) {
|
|
144
|
+
if (curImgs.length)
|
|
145
|
+
uim.images = curImgs;
|
|
146
|
+
const ctx = {};
|
|
147
|
+
if (curTrs.length)
|
|
148
|
+
ctx.toolResults = deduplicateToolResults(curTrs);
|
|
149
|
+
if (cwTools.length)
|
|
150
|
+
ctx.tools = cwTools;
|
|
151
|
+
if (Object.keys(ctx).length)
|
|
152
|
+
uim.userInputMessageContext = ctx;
|
|
153
|
+
}
|
|
154
|
+
const machineId = crypto
|
|
155
|
+
.createHash('sha256')
|
|
156
|
+
.update(auth.profileArn || auth.clientId || 'KIRO_DEFAULT_MACHINE')
|
|
157
|
+
.digest('hex');
|
|
158
|
+
const osP = os.platform(), osR = os.release(), nodeV = process.version.replace('v', ''), kiroV = KIRO_CONSTANTS.KIRO_VERSION;
|
|
159
|
+
const osN = osP === 'win32' ? `windows#${osR}` : osP === 'darwin' ? `macos#${osR}` : `${osP}#${osR}`;
|
|
160
|
+
const ua = `aws-sdk-js/1.0.0 ua/2.1 os/${osN} lang/js md/nodejs#${nodeV} api/codewhispererruntime#1.0.0 m/E KiroIDE-${kiroV}-${machineId}`;
|
|
161
|
+
return {
|
|
162
|
+
url: KIRO_CONSTANTS.BASE_URL.replace('{{region}}', auth.region),
|
|
163
|
+
init: {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
Accept: 'application/json',
|
|
168
|
+
Authorization: `Bearer ${auth.access}`,
|
|
169
|
+
'amz-sdk-invocation-id': crypto.randomUUID(),
|
|
170
|
+
'amz-sdk-request': 'attempt=1; max=1',
|
|
171
|
+
'x-amzn-kiro-agent-mode': 'vibe',
|
|
172
|
+
'x-amz-user-agent': `aws-sdk-js/1.0.0 KiroIDE-${kiroV}-${machineId}`,
|
|
173
|
+
'user-agent': ua,
|
|
174
|
+
Connection: 'close'
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(request)
|
|
177
|
+
},
|
|
178
|
+
streaming: true,
|
|
179
|
+
effectiveModel: resolved,
|
|
180
|
+
conversationId: convId
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function mergeAdjacentMessages(msgs) {
|
|
184
|
+
const merged = [];
|
|
185
|
+
for (const m of msgs) {
|
|
186
|
+
if (!merged.length)
|
|
187
|
+
merged.push({ ...m });
|
|
188
|
+
else {
|
|
189
|
+
const last = merged[merged.length - 1];
|
|
190
|
+
if (last && m.role === last.role) {
|
|
191
|
+
if (Array.isArray(last.content) && Array.isArray(m.content))
|
|
192
|
+
last.content.push(...m.content);
|
|
193
|
+
else if (typeof last.content === 'string' && typeof m.content === 'string')
|
|
194
|
+
last.content += '\n' + m.content;
|
|
195
|
+
else if (Array.isArray(last.content) && typeof m.content === 'string')
|
|
196
|
+
last.content.push({ type: 'text', text: m.content });
|
|
197
|
+
else if (typeof last.content === 'string' && Array.isArray(m.content))
|
|
198
|
+
last.content = [{ type: 'text', text: last.content }, ...m.content];
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
merged.push({ ...m });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return merged;
|
|
205
|
+
}
|
|
206
|
+
export function convertToolsToCodeWhisperer(tools) {
|
|
207
|
+
return tools
|
|
208
|
+
.filter((t) => !['web_search', 'websearch'].includes((t.name || t.function?.name || '').toLowerCase()))
|
|
209
|
+
.map((t) => ({
|
|
210
|
+
toolSpecification: {
|
|
211
|
+
name: t.name || t.function?.name,
|
|
212
|
+
description: (t.description || t.function?.description || '').substring(0, 9216),
|
|
213
|
+
inputSchema: { json: t.input_schema || t.function?.parameters || {} }
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
function getContentText(m) {
|
|
218
|
+
if (!m)
|
|
219
|
+
return '';
|
|
220
|
+
if (typeof m === 'string')
|
|
221
|
+
return m;
|
|
222
|
+
if (typeof m.content === 'string')
|
|
223
|
+
return m.content;
|
|
224
|
+
if (Array.isArray(m.content))
|
|
225
|
+
return m.content
|
|
226
|
+
.filter((p) => p.type === 'text')
|
|
227
|
+
.map((p) => p.text || '')
|
|
228
|
+
.join('');
|
|
229
|
+
return m.text || '';
|
|
230
|
+
}
|
|
231
|
+
function deduplicateToolResults(trs) {
|
|
232
|
+
const u = [], s = new Set();
|
|
233
|
+
for (const t of trs) {
|
|
234
|
+
if (!s.has(t.toolUseId)) {
|
|
235
|
+
s.add(t.toolUseId);
|
|
236
|
+
u.push(t);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return u;
|
|
240
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ToolCall, ParsedResponse } from './types';
|
|
2
|
+
export declare function parseEventStream(rawResponse: string): ParsedResponse;
|
|
3
|
+
export declare function parseEventLine(line: string): any | null;
|
|
4
|
+
export declare function parseBracketToolCalls(text: string): ToolCall[];
|
|
5
|
+
export declare function deduplicateToolCalls(toolCalls: ToolCall[]): ToolCall[];
|
|
6
|
+
export declare function cleanToolCallsFromText(text: string, toolCalls: ToolCall[]): string;
|