@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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/index.mjs +58 -13
  3. 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
- offered.add(t.name)
710
- tools.push({ toolSpecification: { name: t.name, description: t.description || t.name, inputSchema: { json: t.input_schema ?? EMPTY } } })
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
- async function reply(it, model, stream) {
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",