@bitkyc08/opencodex 2.6.1 → 2.6.2

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.
Files changed (48) hide show
  1. package/README.md +8 -0
  2. package/bin/ocx.mjs +41 -9
  3. package/gui/dist/assets/index-LK87QnT7.js +9 -0
  4. package/gui/dist/index.html +1 -1
  5. package/package.json +1 -1
  6. package/src/abort.ts +22 -0
  7. package/src/adapters/base.ts +17 -4
  8. package/src/adapters/kiro-errors.ts +101 -0
  9. package/src/adapters/kiro-events.ts +48 -0
  10. package/src/adapters/kiro-images.ts +33 -0
  11. package/src/adapters/kiro-retry.ts +95 -0
  12. package/src/adapters/kiro-thinking.ts +82 -0
  13. package/src/adapters/kiro-tool-fallback.ts +36 -0
  14. package/src/adapters/kiro-tools.ts +44 -0
  15. package/src/adapters/kiro-truncation.ts +33 -0
  16. package/src/adapters/kiro-wire.ts +51 -0
  17. package/src/adapters/kiro.ts +527 -0
  18. package/src/adapters/openai-chat.ts +10 -1
  19. package/src/bridge.ts +1 -1
  20. package/src/cli.ts +25 -3
  21. package/src/codex-catalog.ts +97 -13
  22. package/src/codex-inject.ts +18 -0
  23. package/src/config.ts +52 -0
  24. package/src/crash-guard.ts +197 -9
  25. package/src/debug.ts +11 -0
  26. package/src/errors.ts +39 -3
  27. package/src/lib/eventstream-decoder.ts +244 -0
  28. package/src/lib/token-estimate.ts +43 -0
  29. package/src/oauth/anthropic.ts +1 -1
  30. package/src/oauth/index.ts +53 -6
  31. package/src/oauth/kiro-credentials.ts +256 -0
  32. package/src/oauth/kiro.ts +164 -0
  33. package/src/oauth/local-token-detect.ts +2 -1
  34. package/src/oauth/store.ts +36 -3
  35. package/src/oauth/types.ts +3 -0
  36. package/src/oauth/xai.ts +1 -1
  37. package/src/providers/kiro-models.ts +55 -0
  38. package/src/providers/registry.ts +15 -0
  39. package/src/redact.ts +71 -0
  40. package/src/server.ts +40 -22
  41. package/src/sidecar-tracker.ts +49 -0
  42. package/src/types.ts +3 -0
  43. package/src/usage-debug.ts +7 -4
  44. package/src/usage-log.ts +41 -3
  45. package/src/vision/describe.ts +11 -2
  46. package/src/web-search/executor.ts +10 -2
  47. package/src/web-search/loop.ts +27 -7
  48. package/gui/dist/assets/index-BmHrbTmO.js +0 -9
@@ -0,0 +1,244 @@
1
+ /**
2
+ * `application/vnd.amazon.eventstream` decoder.
3
+ *
4
+ * Ported from jawcode `packages/ai/src/providers/aws-eventstream.ts` (verbatim logic).
5
+ * Foundational dependency for AWS-eventstream providers: kiro (CodeWhisperer
6
+ * GenerateAssistantResponse) and amazon-bedrock (Converse). Self-contained — only
7
+ * uses Buffer / DataView / TextDecoder (Bun + Node compatible).
8
+ *
9
+ * Wire format (all integers big-endian):
10
+ *
11
+ * [total length u32]
12
+ * [headers length u32]
13
+ * [prelude CRC32 u32] <- CRC over the first 8 bytes
14
+ * [headers headers_length]
15
+ * [payload total_length - headers_length - 16]
16
+ * [message CRC32 u32] <- CRC over the entire message minus the trailing 4 bytes
17
+ *
18
+ * Headers: a sequence of `[name_len u8][name utf8][value_type u8][value …]`.
19
+ */
20
+
21
+ const PRELUDE_LEN = 8;
22
+ const PRELUDE_CRC_LEN = 4;
23
+ const MESSAGE_CRC_LEN = 4;
24
+ const HEADER_BLOCK_OFFSET = PRELUDE_LEN + PRELUDE_CRC_LEN;
25
+ const MIN_MESSAGE_LEN = HEADER_BLOCK_OFFSET + MESSAGE_CRC_LEN;
26
+ const MAX_MESSAGE_LEN = 16 * 1024 * 1024;
27
+
28
+ export interface EventStreamMessage {
29
+ /** Header casing is preserved verbatim (e.g. `:event-type`, `:message-type`). */
30
+ headers: Record<string, string>;
31
+ payload: Uint8Array;
32
+ }
33
+
34
+ /** CRC32 (IEEE / zlib polynomial 0xEDB88320), matches `@aws-crypto/crc32`. */
35
+ const CRC_TABLE = (() => {
36
+ const t = new Uint32Array(256);
37
+ for (let i = 0; i < 256; i++) {
38
+ let c = i;
39
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
40
+ t[i] = c >>> 0;
41
+ }
42
+ return t;
43
+ })();
44
+
45
+ export function crc32(bytes: Uint8Array, seed = 0): number {
46
+ let c = (seed ^ 0xffffffff) >>> 0;
47
+ for (let i = 0; i < bytes.length; i++) c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
48
+ return (c ^ 0xffffffff) >>> 0;
49
+ }
50
+
51
+ /**
52
+ * Decode a single, fully buffered eventstream message. Throws if the framing is
53
+ * malformed or either CRC mismatches.
54
+ */
55
+ export function decodeMessage(frame: Uint8Array): EventStreamMessage {
56
+ if (frame.length < MIN_MESSAGE_LEN) throw new Error("eventstream: frame too short");
57
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
58
+ const total = view.getUint32(0, false);
59
+ if (total !== frame.length) throw new Error(`eventstream: framed length ${total} != buffer ${frame.length}`);
60
+ if (total > MAX_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} exceeds maximum`);
61
+ const headersLen = view.getUint32(4, false);
62
+ const preludeCrc = view.getUint32(8, false);
63
+ const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
64
+ if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
65
+ if (headersLen > total - MIN_MESSAGE_LEN) throw new Error("eventstream: headers length exceeds frame payload");
66
+ const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
67
+ const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
68
+ if (computedMsgCrc !== msgCrc) throw new Error("eventstream: message CRC mismatch");
69
+
70
+ const headersBytes = frame.subarray(HEADER_BLOCK_OFFSET, HEADER_BLOCK_OFFSET + headersLen);
71
+ const payload = frame.subarray(HEADER_BLOCK_OFFSET + headersLen, total - MESSAGE_CRC_LEN);
72
+ return { headers: parseHeaders(headersBytes), payload };
73
+ }
74
+
75
+ function parseHeaders(buf: Uint8Array): Record<string, string> {
76
+ const out: Record<string, string> = {};
77
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
78
+ const decoder = new TextDecoder();
79
+ let p = 0;
80
+ const need = (n: number, label: string) => {
81
+ if (p + n > buf.length) throw new Error(`eventstream: truncated header ${label}`);
82
+ };
83
+ while (p < buf.length) {
84
+ need(1, "name length");
85
+ const nameLen = view.getUint8(p);
86
+ p += 1;
87
+ need(nameLen, "name");
88
+ const name = decoder.decode(buf.subarray(p, p + nameLen));
89
+ p += nameLen;
90
+ need(1, "type");
91
+ const type = view.getUint8(p);
92
+ p += 1;
93
+ switch (type) {
94
+ case 0: // bool true
95
+ out[name] = "true";
96
+ break;
97
+ case 1: // bool false
98
+ out[name] = "false";
99
+ break;
100
+ case 2: // byte
101
+ need(1, "byte value");
102
+ out[name] = String(view.getInt8(p));
103
+ p += 1;
104
+ break;
105
+ case 3: // short
106
+ need(2, "short value");
107
+ out[name] = String(view.getInt16(p, false));
108
+ p += 2;
109
+ break;
110
+ case 4: // integer
111
+ need(4, "integer value");
112
+ out[name] = String(view.getInt32(p, false));
113
+ p += 4;
114
+ break;
115
+ case 5: // long — decimal string to avoid precision loss
116
+ need(8, "long value");
117
+ out[name] = bigIntFromBytes(buf.subarray(p, p + 8)).toString();
118
+ p += 8;
119
+ break;
120
+ case 6: {
121
+ // byte array — base64 for safe transport
122
+ need(2, "byte-array length");
123
+ const len = view.getUint16(p, false);
124
+ p += 2;
125
+ need(len, "byte-array value");
126
+ out[name] = Buffer.from(buf.buffer, buf.byteOffset + p, len).toString("base64");
127
+ p += len;
128
+ break;
129
+ }
130
+ case 7: {
131
+ // string
132
+ need(2, "string length");
133
+ const len = view.getUint16(p, false);
134
+ p += 2;
135
+ need(len, "string value");
136
+ out[name] = decoder.decode(buf.subarray(p, p + len));
137
+ p += len;
138
+ break;
139
+ }
140
+ case 8: // timestamp (ms since epoch as i64)
141
+ need(8, "timestamp value");
142
+ out[name] = new Date(Number(bigIntFromBytes(buf.subarray(p, p + 8)))).toISOString();
143
+ p += 8;
144
+ break;
145
+ case 9: {
146
+ // uuid
147
+ need(16, "uuid value");
148
+ const u = buf.subarray(p, p + 16);
149
+ const hex: string[] = [];
150
+ for (let i = 0; i < 16; i++) hex.push(u[i].toString(16).padStart(2, "0"));
151
+ out[name] =
152
+ `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
153
+ p += 16;
154
+ break;
155
+ }
156
+ default:
157
+ throw new Error(`eventstream: unknown header value type ${type}`);
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+
163
+ function bigIntFromBytes(b: Uint8Array): bigint {
164
+ let v = 0n;
165
+ for (let i = 0; i < b.length; i++) v = (v << 8n) | BigInt(b[i]);
166
+ // sign-extend (two's complement)
167
+ if (b.length === 8 && b[0] & 0x80) v -= 1n << 64n;
168
+ return v;
169
+ }
170
+
171
+ /**
172
+ * Async generator that consumes a `ReadableStream<Uint8Array>` (a fetch response
173
+ * body) and yields fully-framed messages, handling arbitrary chunk boundaries.
174
+ */
175
+ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): AsyncGenerator<EventStreamMessage> {
176
+ const reader = source.getReader();
177
+ let buf: Uint8Array = new Uint8Array(0);
178
+ try {
179
+ while (true) {
180
+ const { value, done } = await reader.read();
181
+ if (value && value.length > 0) buf = buf.length === 0 ? value : Buffer.concat([buf, value]);
182
+ let offset = 0;
183
+ while (buf.length - offset >= 4) {
184
+ const dv = new DataView(buf.buffer, buf.byteOffset + offset, buf.length - offset);
185
+ const total = dv.getUint32(0, false);
186
+ if (total < MIN_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} below minimum`);
187
+ if (total > MAX_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} exceeds maximum`);
188
+ if (buf.length - offset < total) break;
189
+ const frame = buf.subarray(offset, offset + total);
190
+ yield decodeMessage(frame);
191
+ offset += total;
192
+ }
193
+ if (offset > 0) buf = buf.slice(offset);
194
+ if (buf.length > MAX_MESSAGE_LEN) throw new Error(`eventstream: buffered frame exceeds maximum ${MAX_MESSAGE_LEN}`);
195
+ if (done) break;
196
+ }
197
+ if (buf.length > 0) throw new Error("eventstream: truncated message at end of stream");
198
+ } finally {
199
+ // Early termination (consumer break/return, turn abort, or an HTTP/2 mid-body reset) can leave
200
+ // an in-flight `reader.read()` pending when this generator's `finally` runs. Releasing the lock
201
+ // does NOT settle that orphaned read — Bun then surfaces it as an off-path
202
+ // `unhandledRejection: TypeError: null is not an object` that no caller try/catch can intercept.
203
+ // Cancel first (settles the pending read + closes the body), then release. On a clean `done`
204
+ // finish the read is already settled, so cancel() is a harmless no-op.
205
+ try {
206
+ await reader.cancel();
207
+ } catch {
208
+ /* body already errored/closed — nothing to cancel */
209
+ }
210
+ try {
211
+ reader.releaseLock();
212
+ } catch {
213
+ /* lock already released by cancel() on some runtimes */
214
+ }
215
+ }
216
+ }
217
+
218
+ /** Build a single eventstream frame (string headers only) — used by tests and fixtures. */
219
+ export function encodeMessage(headers: Record<string, string>, payload: Uint8Array): Uint8Array {
220
+ const enc = new TextEncoder();
221
+ const headerParts: Uint8Array[] = [];
222
+ for (const [name, value] of Object.entries(headers)) {
223
+ const nameBytes = enc.encode(name);
224
+ const valueBytes = enc.encode(value);
225
+ const head = new Uint8Array(1 + nameBytes.length + 1 + 2);
226
+ const hv = new DataView(head.buffer);
227
+ hv.setUint8(0, nameBytes.length);
228
+ head.set(nameBytes, 1);
229
+ hv.setUint8(1 + nameBytes.length, 7); // string type
230
+ hv.setUint16(1 + nameBytes.length + 1, valueBytes.length, false);
231
+ headerParts.push(head, valueBytes);
232
+ }
233
+ const headersBytes = Buffer.concat(headerParts.map(p => Buffer.from(p)));
234
+ const total = HEADER_BLOCK_OFFSET + headersBytes.length + payload.length + MESSAGE_CRC_LEN;
235
+ const frame = new Uint8Array(total);
236
+ const dv = new DataView(frame.buffer);
237
+ dv.setUint32(0, total, false);
238
+ dv.setUint32(4, headersBytes.length, false);
239
+ dv.setUint32(8, crc32(frame.subarray(0, PRELUDE_LEN)), false);
240
+ frame.set(headersBytes, HEADER_BLOCK_OFFSET);
241
+ frame.set(payload, HEADER_BLOCK_OFFSET + headersBytes.length);
242
+ dv.setUint32(total - MESSAGE_CRC_LEN, crc32(frame.subarray(0, total - MESSAGE_CRC_LEN)), false);
243
+ return frame;
244
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Heuristic token-estimation sidecar.
3
+ *
4
+ * Some providers (notably kiro / CodeWhisperer) return no token usage in their stream, so Codex's
5
+ * usage display and auto-compact (which read response.completed.usage) never engage. This module
6
+ * provides a cheap, dependency-free char-based estimate to fill that gap.
7
+ *
8
+ * Grounding (web): 1 token ~= 4 chars for English prose; empirical model ratios are ~Claude 3.5,
9
+ * ~GPT 3.6, ~Gemini 3.8 chars/token (within ~10%). Code / JSON / tool-args (the dominant Codex
10
+ * traffic) pack MORE tokens per char, so a lower chars-per-token ratio is used for those models.
11
+ * Over-counting fails safe (auto-compact fires earlier); under-counting risks context overflow.
12
+ */
13
+
14
+ /** Generic English-prose fallback ratio (chars per token). */
15
+ const DEFAULT_CHARS_PER_TOKEN = 4;
16
+
17
+ /**
18
+ * Kiro routes code/JSON-heavy agent traffic whose true ratio is ~3.0-3.3 chars/token. 3.5 keeps a
19
+ * small safety margin (slight over-count) without wildly inflating; tune toward 3.3 if overflow is
20
+ * ever observed. All kiro models are text LLMs, so a single ratio applies to the whole family.
21
+ */
22
+ const KIRO_CHARS_PER_TOKEN = 3.5;
23
+
24
+ const KIRO_MODEL_PREFIXES = ["kiro", "claude", "deepseek", "minimax", "glm", "qwen"];
25
+
26
+ /** Model-aware chars-per-token ratio. Unknown models fall back to the generic English ratio. */
27
+ export function charsPerToken(modelId?: string): number {
28
+ if (!modelId) return DEFAULT_CHARS_PER_TOKEN;
29
+ const id = modelId.toLowerCase();
30
+ if (KIRO_MODEL_PREFIXES.some(p => id.startsWith(p))) return KIRO_CHARS_PER_TOKEN;
31
+ return DEFAULT_CHARS_PER_TOKEN;
32
+ }
33
+
34
+ /**
35
+ * Estimate the token count of a text blob. Pure and deterministic.
36
+ * Returns 0 for empty/whitespace-free-empty input; otherwise ceil(length / ratio), min 1.
37
+ */
38
+ export function estimateTokens(text: string, modelId?: string): number {
39
+ if (!text) return 0;
40
+ const len = text.length;
41
+ if (len === 0) return 0;
42
+ return Math.max(1, Math.ceil(len / charsPerToken(modelId)));
43
+ }
@@ -128,7 +128,7 @@ export async function loginAnthropic(
128
128
  ctrl.onProgress?.("Found Claude Code token, importing automatically");
129
129
  if (local.expires >= Date.now() + 60_000) return local;
130
130
  try {
131
- return await refreshAnthropicToken(local.refresh);
131
+ return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
132
132
  } catch (error) {
133
133
  if (importLocal === "only") {
134
134
  throw new Error(`Claude Code token expired and could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
@@ -6,10 +6,12 @@ import { getCredential, saveCredential } from "./store";
6
6
  import { loginXai, refreshXaiToken } from "./xai";
7
7
  import { ANTHROPIC_OAUTH_BETA, loginAnthropic, refreshAnthropicToken } from "./anthropic";
8
8
  import { loginKimi, refreshKimiToken } from "./kimi";
9
+ import { loginKiro, readKiroCliSqlite, refreshKiroToken } from "./kiro";
9
10
  import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
10
11
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
11
12
 
12
13
  const REFRESH_SKEW_MS = 60_000;
14
+ const tokenRefreshes = new Map<string, Promise<string>>();
13
15
 
14
16
  export interface LoginOpts { forceLogin?: boolean }
15
17
 
@@ -52,6 +54,12 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
52
54
  providerConfig: oauthConfig("kimi"),
53
55
  defaultModel: oauthDefaultModel("kimi"),
54
56
  },
57
+ kiro: {
58
+ login: (ctrl) => loginKiro(ctrl),
59
+ refresh: (rt, signal) => refreshKiroToken(rt, signal),
60
+ providerConfig: oauthConfig("kiro"),
61
+ defaultModel: oauthDefaultModel("kiro"),
62
+ },
55
63
  chatgpt: {
56
64
  login: loginChatGPT,
57
65
  refresh: (rt) => refreshChatGPTToken(rt),
@@ -90,9 +98,47 @@ export async function getValidAccessToken(provider: string): Promise<string> {
90
98
  const cred = getCredential(provider);
91
99
  if (!cred) throw new OAuthLoginRequiredError(provider);
92
100
  if (cred.expires > Date.now() + REFRESH_SKEW_MS) return cred.access;
93
- const fresh = await def.refresh(cred.refresh);
94
- saveCredential(provider, fresh);
95
- return fresh.access;
101
+ const existing = tokenRefreshes.get(provider);
102
+ if (existing) return existing;
103
+ const refresh = refreshAndPersistAccessToken(provider, def, cred).finally(() => {
104
+ if (tokenRefreshes.get(provider) === refresh) tokenRefreshes.delete(provider);
105
+ });
106
+ tokenRefreshes.set(provider, refresh);
107
+ return refresh;
108
+ }
109
+
110
+ function readFreshKiroCliCredential(): OAuthCredentials | undefined {
111
+ const imported = readKiroCliSqlite();
112
+ if (!imported || imported.expires <= Date.now() + REFRESH_SKEW_MS) return undefined;
113
+ return { access: imported.access, refresh: imported.refresh, expires: imported.expires, source: "local-cli" };
114
+ }
115
+
116
+ async function refreshAndPersistAccessToken(
117
+ provider: string,
118
+ def: OAuthProviderDef,
119
+ cred: OAuthCredentials,
120
+ ): Promise<string> {
121
+ if (provider === "kiro") {
122
+ const imported = readFreshKiroCliCredential();
123
+ if (imported) {
124
+ saveCredential(provider, imported);
125
+ return imported.access;
126
+ }
127
+ }
128
+ try {
129
+ const fresh = await def.refresh(cred.refresh);
130
+ saveCredential(provider, { ...fresh, source: fresh.source ?? cred.source ?? "oauth" });
131
+ return fresh.access;
132
+ } catch (err) {
133
+ if (provider === "kiro") {
134
+ const imported = readFreshKiroCliCredential();
135
+ if (imported) {
136
+ saveCredential(provider, imported);
137
+ return imported.access;
138
+ }
139
+ }
140
+ throw err;
141
+ }
96
142
  }
97
143
 
98
144
  /**
@@ -205,7 +251,8 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
205
251
  export async function runLogin(provider: string, ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials> {
206
252
  const def = OAUTH_PROVIDERS[provider];
207
253
  if (!def) throw new UnsupportedOAuthProviderError(provider);
208
- const cred = await def.login(ctrl, opts);
254
+ const rawCred = await def.login(ctrl, opts);
255
+ const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
209
256
  saveCredential(provider, cred);
210
257
  const config = loadConfig();
211
258
  upsertOAuthProvider(config, provider);
@@ -221,10 +268,10 @@ export async function runLogin(provider: string, ctrl: OAuthController, opts?: L
221
268
  const loginState = new Map<string, { error?: string; done: boolean }>();
222
269
  const loginAbort = new Map<string, AbortController>();
223
270
 
224
- export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; error?: string; done: boolean } {
271
+ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; done: boolean } {
225
272
  const cred = getCredential(provider);
226
273
  const st = loginState.get(provider);
227
- return { loggedIn: !!cred, email: maskEmail(cred?.email) ?? undefined, error: st?.error, done: st?.done ?? false };
274
+ return { loggedIn: !!cred, email: maskEmail(cred?.email) ?? undefined, source: cred?.source, error: st?.error, done: st?.done ?? false };
228
275
  }
229
276
 
230
277
  export function clearLoginState(provider: string): void {
@@ -0,0 +1,256 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { Database } from "bun:sqlite";
5
+
6
+ const DEFAULT_EXPIRES_MS = 3600_000;
7
+ const KIRO_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d$/;
8
+ const CLIENT_ID_HASH_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
9
+ const TOKEN_KEYS = ["kirocli:social:token", "kirocli:odic:token", "codewhisperer:odic:token"];
10
+ const REGISTRATION_KEYS = ["kirocli:odic:device-registration", "codewhisperer:odic:device-registration"];
11
+
12
+ export type KiroAuthType = "kiro_desktop" | "aws_sso_oidc";
13
+ export type KiroCredentialSource = "json" | "sqlite";
14
+ export type KiroDiagnosticStatus =
15
+ | "missing"
16
+ | "unreadable"
17
+ | "schema_mismatch"
18
+ | "invalid_json"
19
+ | "token_missing"
20
+ | "token_found"
21
+ | "registration_found";
22
+
23
+ export interface KiroImportDiagnostic {
24
+ location: "kiro-creds-file" | "kiro-cli-db-env" | "kiro-cli-data" | "kiro-cli-linux-data" | "amazon-q-data" | "kiro-sso-cache";
25
+ status: KiroDiagnosticStatus;
26
+ }
27
+
28
+ export interface ImportedKiroCredential {
29
+ access: string;
30
+ refresh: string;
31
+ expires: number;
32
+ source: KiroCredentialSource;
33
+ authType: KiroAuthType;
34
+ profileArn?: string;
35
+ ssoRegion?: string;
36
+ apiRegion?: string;
37
+ clientId?: string;
38
+ clientSecret?: string;
39
+ }
40
+
41
+ type JsonObject = Record<string, unknown>;
42
+
43
+ function userHome(): string {
44
+ return process.env.HOME || homedir();
45
+ }
46
+
47
+ function expandPath(path: string): string {
48
+ if (path.startsWith("~/")) return join(userHome(), path.slice(2));
49
+ return isAbsolute(path) ? path : join(process.cwd(), path);
50
+ }
51
+
52
+ function stringField(data: JsonObject, ...keys: string[]): string | undefined {
53
+ for (const key of keys) {
54
+ const value = data[key];
55
+ if (typeof value === "string" && value.length > 0) return value;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ function parseExpires(value: unknown): number {
61
+ if (typeof value === "number" && Number.isFinite(value)) return value < 10_000_000_000 ? value * 1000 : value;
62
+ if (typeof value === "string" && value.length > 0) {
63
+ const parsed = Date.parse(value);
64
+ if (Number.isFinite(parsed)) return parsed;
65
+ }
66
+ return Date.now() + DEFAULT_EXPIRES_MS;
67
+ }
68
+
69
+ export function inferRegionFromProfileArn(arn: string | undefined): string | undefined {
70
+ if (!arn) return undefined;
71
+ const region = arn.split(":")[3];
72
+ return normalizeKiroRegion(region);
73
+ }
74
+
75
+ export function normalizeKiroRegion(region: string | undefined): string | undefined {
76
+ const trimmed = region?.trim();
77
+ return trimmed && KIRO_REGION_PATTERN.test(trimmed) ? trimmed : undefined;
78
+ }
79
+
80
+ export function requireKiroRegion(region: string | undefined): string {
81
+ const normalized = normalizeKiroRegion(region);
82
+ if (!normalized) throw new Error("Kiro: invalid region value.");
83
+ return normalized;
84
+ }
85
+
86
+ function jsonCredentialPaths(): string[] {
87
+ return [process.env.KIRO_CREDS_FILE, process.env.KIRO_CREDENTIALS_FILE]
88
+ .filter((value): value is string => !!value)
89
+ .map(expandPath);
90
+ }
91
+
92
+ function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
93
+ const home = userHome();
94
+ const entries: Array<{ location: KiroImportDiagnostic["location"]; path: string }> = [];
95
+ if (process.env.KIRO_CLI_DB_FILE) entries.push({ location: "kiro-cli-db-env", path: expandPath(process.env.KIRO_CLI_DB_FILE) });
96
+ entries.push(
97
+ { location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") },
98
+ { location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") },
99
+ { location: "amazon-q-data", path: join(home, ".local", "share", "amazon-q", "data.sqlite3") },
100
+ { location: "kiro-sso-cache", path: join(home, ".kiro", "sso", "cache.db") },
101
+ );
102
+ return entries;
103
+ }
104
+
105
+ function credentialFromJson(data: JsonObject, source: KiroCredentialSource): ImportedKiroCredential | undefined {
106
+ const access = stringField(data, "accessToken", "access_token");
107
+ if (!access) return undefined;
108
+ const profileArn = stringField(data, "profileArn", "profile_arn");
109
+ const ssoRegion = stringField(data, "region");
110
+ const apiRegion = stringField(data, "apiRegion", "api_region") || inferRegionFromProfileArn(profileArn) || ssoRegion;
111
+ const clientId = stringField(data, "clientId", "client_id");
112
+ const clientSecret = stringField(data, "clientSecret", "client_secret");
113
+ return {
114
+ access,
115
+ refresh: stringField(data, "refreshToken", "refresh_token") || "",
116
+ expires: parseExpires(data.expiresAt ?? data.expires_at),
117
+ source,
118
+ authType: clientId && clientSecret ? "aws_sso_oidc" : "kiro_desktop",
119
+ ...(profileArn ? { profileArn } : {}),
120
+ ...(ssoRegion ? { ssoRegion } : {}),
121
+ ...(apiRegion ? { apiRegion } : {}),
122
+ ...(clientId ? { clientId } : {}),
123
+ ...(clientSecret ? { clientSecret } : {}),
124
+ };
125
+ }
126
+
127
+ function loadEnterpriseRegistration(data: JsonObject): JsonObject | undefined {
128
+ const hash = stringField(data, "clientIdHash");
129
+ if (!hash) return undefined;
130
+ if (!CLIENT_ID_HASH_PATTERN.test(hash)) return undefined;
131
+ const path = join(userHome(), ".aws", "sso", "cache", `${hash}.json`);
132
+ if (!existsSync(path)) return undefined;
133
+ try {
134
+ return JSON.parse(readFileSync(path, "utf8")) as JsonObject;
135
+ } catch {
136
+ return undefined;
137
+ }
138
+ }
139
+
140
+ function readJsonCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKiroCredential | undefined {
141
+ for (const path of jsonCredentialPaths()) {
142
+ if (!existsSync(path)) {
143
+ diagnostics.push({ location: "kiro-creds-file", status: "missing" });
144
+ continue;
145
+ }
146
+ let data: JsonObject;
147
+ try {
148
+ const raw = readFileSync(path, "utf8");
149
+ try {
150
+ data = JSON.parse(raw) as JsonObject;
151
+ } catch {
152
+ diagnostics.push({ location: "kiro-creds-file", status: "invalid_json" });
153
+ continue;
154
+ }
155
+ } catch {
156
+ diagnostics.push({ location: "kiro-creds-file", status: "unreadable" });
157
+ continue;
158
+ }
159
+ const registration = loadEnterpriseRegistration(data);
160
+ const merged = registration ? { ...data, ...registration } : data;
161
+ const credential = credentialFromJson(merged, "json");
162
+ diagnostics.push({ location: "kiro-creds-file", status: credential ? "token_found" : "token_missing" });
163
+ if (credential) return credential;
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ function readStateProfile(db: Database): { profileArn?: string; apiRegion?: string } {
169
+ try {
170
+ const row = db.query("SELECT value FROM state WHERE key = ?").get("api.codewhisperer.profile") as { value: string } | null;
171
+ if (!row) return {};
172
+ const data = JSON.parse(row.value) as JsonObject;
173
+ const profileArn = stringField(data, "arn", "profileArn", "profile_arn");
174
+ return { ...(profileArn ? { profileArn } : {}), ...(profileArn ? { apiRegion: inferRegionFromProfileArn(profileArn) } : {}) };
175
+ } catch {
176
+ return {};
177
+ }
178
+ }
179
+
180
+ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKiroCredential | undefined {
181
+ for (const { location, path } of sqliteEntries()) {
182
+ if (!existsSync(path)) {
183
+ diagnostics.push({ location, status: "missing" });
184
+ continue;
185
+ }
186
+ let db: Database | undefined;
187
+ try {
188
+ db = new Database(path, { readonly: true });
189
+ try { db.exec("PRAGMA busy_timeout = 5000"); } catch { /* read-only best effort */ }
190
+ } catch {
191
+ diagnostics.push({ location, status: "unreadable" });
192
+ continue;
193
+ }
194
+ try {
195
+ let tokenData: JsonObject | undefined;
196
+ for (const key of TOKEN_KEYS) {
197
+ const row = db.query("SELECT value FROM auth_kv WHERE key = ?").get(key) as { value: string } | null;
198
+ if (!row) continue;
199
+ try {
200
+ tokenData = JSON.parse(row.value) as JsonObject;
201
+ } catch {
202
+ diagnostics.push({ location, status: "invalid_json" });
203
+ continue;
204
+ }
205
+ if (stringField(tokenData, "access_token", "accessToken")) break;
206
+ }
207
+ if (!tokenData) {
208
+ diagnostics.push({ location, status: "token_missing" });
209
+ continue;
210
+ }
211
+ let registrationData: JsonObject = {};
212
+ for (const key of REGISTRATION_KEYS) {
213
+ const row = db.query("SELECT value FROM auth_kv WHERE key = ?").get(key) as { value: string } | null;
214
+ if (!row) continue;
215
+ try {
216
+ registrationData = JSON.parse(row.value) as JsonObject;
217
+ diagnostics.push({ location, status: "registration_found" });
218
+ } catch {
219
+ diagnostics.push({ location, status: "invalid_json" });
220
+ }
221
+ break;
222
+ }
223
+ const profile = readStateProfile(db);
224
+ const merged = { ...registrationData, ...tokenData, ...profile };
225
+ const credential = credentialFromJson(merged, "sqlite");
226
+ diagnostics.push({ location, status: credential ? "token_found" : "token_missing" });
227
+ if (credential) return credential;
228
+ } catch {
229
+ diagnostics.push({ location, status: "schema_mismatch" });
230
+ } finally {
231
+ db.close();
232
+ }
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ export function inspectKiroCredentialSources(): { credential: ImportedKiroCredential | null; diagnostics: KiroImportDiagnostic[] } {
238
+ const diagnostics: KiroImportDiagnostic[] = [];
239
+ const json = readJsonCredentials(diagnostics);
240
+ if (json) return { credential: json, diagnostics };
241
+ const sqlite = readSqliteCredentials(diagnostics);
242
+ return { credential: sqlite ?? null, diagnostics };
243
+ }
244
+
245
+ export function inspectKiroCliSqliteSources(): { credential: ImportedKiroCredential | null; diagnostics: KiroImportDiagnostic[] } {
246
+ const diagnostics: KiroImportDiagnostic[] = [];
247
+ return { credential: readSqliteCredentials(diagnostics) ?? null, diagnostics };
248
+ }
249
+
250
+ export function readImportedKiroCredential(): ImportedKiroCredential | null {
251
+ return inspectKiroCredentialSources().credential;
252
+ }
253
+
254
+ export function readKiroCliSqliteCredential(): ImportedKiroCredential | null {
255
+ return inspectKiroCliSqliteSources().credential;
256
+ }