@magpie-community/opencode-kiro-auth 0.1.6 → 0.1.8
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 +3 -1
- package/index.mjs +58 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,9 @@ request:
|
|
|
73
73
|
- The system prompt goes at the head of the first message.
|
|
74
74
|
- Tool calls and results are paired: an unanswered call gets an error
|
|
75
75
|
result, and a result longer than 250,000 characters is cut.
|
|
76
|
-
- Tool ids Kiro wouldn't take are rewritten.
|
|
76
|
+
- Tool ids Kiro wouldn't take are rewritten. A tool name over 64
|
|
77
|
+
characters (an MCP plugin's, in Claude Code) is sent shortened with a
|
|
78
|
+
hash and its calls come back under the full name.
|
|
77
79
|
- Only the latest images are sent.
|
|
78
80
|
- Tools the history used but the request doesn't offer are declared.
|
|
79
81
|
- It turns the AWS event stream Kiro answers with back into Messages'
|
package/index.mjs
CHANGED
|
@@ -43,6 +43,13 @@ const SCOPES = ["codewhisperer:completions", "codewhisperer:analysis", "codewhis
|
|
|
43
43
|
const IDE_UA = "KiroIDE-1.1.70-" + createHash("sha256").update("magpie:" + hostname()).digest("hex")
|
|
44
44
|
const DESKTOP_UA = "Kiro-Desktop/0.2.13 (darwin; arm64)"
|
|
45
45
|
const SIGN_IN_TIMEOUT = 10 * 60 * 1000
|
|
46
|
+
// A Builder ID sign-in has no profile of its own, and Kiro won't list one
|
|
47
|
+
// for it (List-Available-Profiles is a 403, "AWS Builder ID is not supported
|
|
48
|
+
// for this operation"); the Kiro IDE and kiro-cli name this service profile
|
|
49
|
+
// for it instead, so its models, usage and chat are asked with it too.
|
|
50
|
+
const BUILDER_ID_START = "https://view.awsapps.com/start"
|
|
51
|
+
const BUILDER_ID_PROFILE = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX"
|
|
52
|
+
const isBuilderID = (provider) => String(provider ?? "").toLowerCase() === "builderid"
|
|
46
53
|
|
|
47
54
|
const env = (k) => process.env[k] ?? ""
|
|
48
55
|
const home = () => env("HOME") || homedir()
|
|
@@ -118,6 +125,8 @@ async function readCLI() {
|
|
|
118
125
|
if (kind === "social") c.method = "social"
|
|
119
126
|
else if (kind === "odic") {
|
|
120
127
|
c.method = "idc"
|
|
128
|
+
// as kiro-cli tells them apart: Builder ID's start URL, or none
|
|
129
|
+
c.builderID = !m.start_url || m.start_url === BUILDER_ID_START
|
|
121
130
|
const reg = get("kirocli:odic:device-registration")
|
|
122
131
|
if (reg) [c.clientId, c.clientSecret] = [reg.client_id ?? "", reg.client_secret ?? ""]
|
|
123
132
|
} else {
|
|
@@ -145,7 +154,7 @@ async function readIDE() {
|
|
|
145
154
|
}
|
|
146
155
|
if (!t?.accessToken) return null
|
|
147
156
|
const c = { access: t.accessToken, refresh: t.refreshToken ?? "", expires: time(t.expiresAt), region: t.region || "us-east-1",
|
|
148
|
-
profile: t.profileArn ?? "", method: "idc", idePath: path }
|
|
157
|
+
profile: t.profileArn ?? "", method: "idc", idePath: path, builderID: isBuilderID(t.provider) }
|
|
149
158
|
if (t.clientIdHash) {
|
|
150
159
|
try {
|
|
151
160
|
const reg = JSON.parse(await readFile(join(ideDir(), t.clientIdHash + ".json"), "utf8"))
|
|
@@ -289,6 +298,7 @@ class KiroError extends Error {
|
|
|
289
298
|
} catch {}
|
|
290
299
|
super(msg ? `Kiro: ${msg} (${status})` : `Kiro: ${STATUS_TEXT[status] ?? ""}`)
|
|
291
300
|
this.status = status
|
|
301
|
+
this.body = String(body ?? "")
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
|
|
@@ -309,7 +319,7 @@ async function managementCall(region, a, method, query, body) {
|
|
|
309
319
|
}
|
|
310
320
|
|
|
311
321
|
// profileOf finds the profile a sign-in that doesn't name one uses: an API
|
|
312
|
-
// key's own, or the first Kiro lists in either of its regions.
|
|
322
|
+
// key's own, Builder ID's, or the first Kiro lists in either of its regions.
|
|
313
323
|
async function profileOf(c) {
|
|
314
324
|
if (c.method === "apikey") {
|
|
315
325
|
const res = await fetch(management("us-east-1") + "/", {
|
|
@@ -327,6 +337,7 @@ async function profileOf(c) {
|
|
|
327
337
|
if (!arn) throw new Error("Kiro didn't say which profile the API key is for")
|
|
328
338
|
return arn
|
|
329
339
|
}
|
|
340
|
+
if (c.builderID) return BUILDER_ID_PROFILE
|
|
330
341
|
let last
|
|
331
342
|
for (const region of ["us-east-1", "eu-central-1"]) {
|
|
332
343
|
try {
|
|
@@ -334,6 +345,8 @@ async function profileOf(c) {
|
|
|
334
345
|
const arn = (out?.profiles ?? []).find((p) => p?.arn)?.arn
|
|
335
346
|
if (arn) return arn
|
|
336
347
|
} catch (e) {
|
|
348
|
+
// a Builder ID sign-in the token didn't say was one
|
|
349
|
+
if (c.method === "idc" && e?.status === 403 && e.body.includes("Builder ID")) return BUILDER_ID_PROFILE
|
|
337
350
|
last = e
|
|
338
351
|
}
|
|
339
352
|
}
|
|
@@ -396,7 +409,7 @@ async function credOf(auth) {
|
|
|
396
409
|
if (!auth.access) return null
|
|
397
410
|
return { access: auth.access, refresh: auth.refresh ?? "", expires: auth.expires ?? 0, method: auth.method || "social",
|
|
398
411
|
region: auth.region || "us-east-1", profile: auth.profileArn ?? "", clientId: auth.clientId ?? "", clientSecret: auth.clientSecret ?? "",
|
|
399
|
-
own: true }
|
|
412
|
+
builderID: isBuilderID(auth.loginProvider), own: true }
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
// Account holds the credentials in use, refreshing them when near their
|
|
@@ -563,6 +576,36 @@ const TOOL_ID = /^[a-zA-Z0-9_.:-]{1,64}$/
|
|
|
563
576
|
// one of Kiro's own, the same each time.
|
|
564
577
|
const toolID = (id) => (TOOL_ID.test(id ?? "") ? id : "t_" + createHash("sha256").update(String(id ?? "")).digest("base64url").slice(0, 32))
|
|
565
578
|
|
|
579
|
+
// Kiro answers a tool named past 64 characters, or outside these, "Invalid
|
|
580
|
+
// tool use format." and won't raise the limit (kirodotdev/Kiro#7684), while
|
|
581
|
+
// MCP tools in Claude Code are named mcp__plugin_<plugin>_<server>__<tool>
|
|
582
|
+
// (yetone/magpie#1393).
|
|
583
|
+
const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/
|
|
584
|
+
|
|
585
|
+
// toolName is a tool's name as Kiro takes one: another is its head with a
|
|
586
|
+
// hash of the whole name, the same each time, so the conversation's later
|
|
587
|
+
// turns name it the same way. toolNames maps each such name back.
|
|
588
|
+
const toolName = (name) => {
|
|
589
|
+
const n = String(name ?? "")
|
|
590
|
+
if (TOOL_NAME.test(n)) return n
|
|
591
|
+
const hash = createHash("sha256").update(n).digest("hex").slice(0, 12)
|
|
592
|
+
return n.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64 - 1 - hash.length) + "_" + hash
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// toolNames is the name the caller knows for each tool a request names
|
|
596
|
+
// that Kiro is sent under another name.
|
|
597
|
+
function toolNames(req) {
|
|
598
|
+
const names = new Map()
|
|
599
|
+
const add = (n) => {
|
|
600
|
+
if (typeof n !== "string" || !n) return
|
|
601
|
+
const k = toolName(n)
|
|
602
|
+
if (k !== n) names.set(k, n)
|
|
603
|
+
}
|
|
604
|
+
for (const t of req.tools ?? []) add(t?.name)
|
|
605
|
+
for (const m of req.messages ?? []) if (Array.isArray(m.content)) for (const p of m.content) if (p?.type === "tool_use") add(p.name)
|
|
606
|
+
return names
|
|
607
|
+
}
|
|
608
|
+
|
|
566
609
|
function imageFormat(mediaType) {
|
|
567
610
|
const t = String(mediaType ?? "").toLowerCase()
|
|
568
611
|
const f = t.replace(/^image\//, "")
|
|
@@ -607,7 +650,7 @@ function buildKiro(req, model, profile, budget) {
|
|
|
607
650
|
if (p?.type === "text" && p.text) texts.push(p.text)
|
|
608
651
|
else if (p?.type === "tool_use") {
|
|
609
652
|
const input = p.input && typeof p.input === "object" && !Array.isArray(p.input) ? p.input : {}
|
|
610
|
-
a.toolUses.push({ name: p.name, toolUseId: toolID(p.id), input })
|
|
653
|
+
a.toolUses.push({ name: toolName(p.name), toolUseId: toolID(p.id), input })
|
|
611
654
|
}
|
|
612
655
|
}
|
|
613
656
|
a.content = texts.join("\n\n")
|
|
@@ -706,8 +749,9 @@ function buildKiro(req, model, profile, budget) {
|
|
|
706
749
|
const EMPTY = { type: "object", properties: {} }
|
|
707
750
|
for (const t of req.tools ?? []) {
|
|
708
751
|
if (!t?.name || (t.type && t.type !== "custom" && !t.input_schema)) continue // server tools Kiro hasn't
|
|
709
|
-
|
|
710
|
-
|
|
752
|
+
const name = toolName(t.name)
|
|
753
|
+
offered.add(name)
|
|
754
|
+
tools.push({ toolSpecification: { name, description: t.description || t.name, inputSchema: { json: t.input_schema ?? EMPTY } } })
|
|
711
755
|
}
|
|
712
756
|
for (const e of entries)
|
|
713
757
|
for (const c of e.assistantResponseMessage?.toolUses ?? [])
|
|
@@ -1034,8 +1078,9 @@ const QUOTA_WORDS = /quota|insufficient|balance|credit|billing|exceeded|rate.?li
|
|
|
1034
1078
|
const failedBefore = (text) => errorResponse(QUOTA_WORDS.test(text) ? 429 : 502, text)
|
|
1035
1079
|
|
|
1036
1080
|
// reply answers the Messages request from Kiro's events: a stream of
|
|
1037
|
-
// Messages' events, or one message.
|
|
1038
|
-
|
|
1081
|
+
// Messages' events, or one message. names gives back the caller's name of
|
|
1082
|
+
// a tool Kiro was sent under another (toolNames).
|
|
1083
|
+
async function reply(it, model, stream, names = new Map()) {
|
|
1039
1084
|
const id = "msg_" + randomBytes(12).toString("hex")
|
|
1040
1085
|
if (!stream) {
|
|
1041
1086
|
const content = []
|
|
@@ -1053,7 +1098,7 @@ async function reply(it, model, stream) {
|
|
|
1053
1098
|
else add({ type: "thinking", thinking: e.text, signature: "" })
|
|
1054
1099
|
} else if (e.kind === "sig") {
|
|
1055
1100
|
if (last?.type === "thinking") last.signature += e.text
|
|
1056
|
-
} else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: e.name, input: "" })
|
|
1101
|
+
} else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: names.get(e.name) ?? e.name, input: "" })
|
|
1057
1102
|
else if (e.kind === "toolArgs") {
|
|
1058
1103
|
if (last?.type === "tool_use") last.input += e.text
|
|
1059
1104
|
} else if (e.kind === "error") {
|
|
@@ -1114,7 +1159,7 @@ async function reply(it, model, stream) {
|
|
|
1114
1159
|
if (open === "thinking") ctl.enqueue(sse("content_block_delta", { index, delta: { type: "signature_delta", signature: e.text } }))
|
|
1115
1160
|
break
|
|
1116
1161
|
case "toolStart":
|
|
1117
|
-
begin("tool", { type: "tool_use", id: e.id, name: e.name, input: {} })
|
|
1162
|
+
begin("tool", { type: "tool_use", id: e.id, name: names.get(e.name) ?? e.name, input: {} })
|
|
1118
1163
|
break
|
|
1119
1164
|
case "toolArgs":
|
|
1120
1165
|
if (open === "tool") ctl.enqueue(sse("content_block_delta", { index, delta: { type: "input_json_delta", partial_json: e.text } }))
|
|
@@ -1176,7 +1221,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1176
1221
|
const f = failure(res.status, (await res.text()).slice(0, 1 << 20))
|
|
1177
1222
|
return errorResponse(f.status, f.message)
|
|
1178
1223
|
}
|
|
1179
|
-
return kept(await reply(events(res.body, model, budget), model, req.stream === true))
|
|
1224
|
+
return kept(await reply(events(res.body, model, budget), model, req.stream === true, toolNames(req)))
|
|
1180
1225
|
}
|
|
1181
1226
|
|
|
1182
1227
|
// ---- signing in ----------------------------------------------------------------------
|
|
@@ -1275,7 +1320,7 @@ async function browserSignIn() {
|
|
|
1275
1320
|
grantType: "authorization_code", redirectUri: awsRedirect, code: q.get("code"), codeVerifier: aws.verifier })
|
|
1276
1321
|
if (!t.accessToken) throw new Error("AWS sent back no token")
|
|
1277
1322
|
c = { access: t.accessToken, refresh: t.refreshToken ?? "", expires: expiry(t.expiresIn), method: "idc", loginProvider: aws.provider,
|
|
1278
|
-
region: aws.region, clientId: aws.clientId, clientSecret: aws.clientSecret, profile: "" }
|
|
1323
|
+
region: aws.region, clientId: aws.clientId, clientSecret: aws.clientSecret, profile: "", builderID: isBuilderID(aws.provider) }
|
|
1279
1324
|
} else if (q.get("state") !== state) {
|
|
1280
1325
|
// not ours: someone else's page, or a stale tab
|
|
1281
1326
|
return html(200, page(false, "This link isn't from this sign-in", "Start it again."))
|
|
@@ -1440,4 +1485,4 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1440
1485
|
}
|
|
1441
1486
|
|
|
1442
1487
|
// for tests
|
|
1443
|
-
export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
|
|
1488
|
+
export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, toolName, toolNames, thinking, readCLI, readIDE, regionOf, planName, frames }
|