@songsid/agend 2.1.4-beta.6 → 2.1.4-beta.60

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 (185) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +143 -7
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +529 -89
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +28 -25
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +6 -4
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +16 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +506 -11
  66. package/dist/daemon.js +2162 -313
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +422 -12
  74. package/dist/fleet-manager.js +2961 -635
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  80. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +4 -1
  81. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  82. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  83. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  84. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  85. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  86. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  87. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  88. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  89. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  90. package/dist/general-knowledge/skills/session-management/SKILL.md +172 -8
  91. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  92. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  93. package/dist/instance-lifecycle.d.ts +99 -0
  94. package/dist/instance-lifecycle.js +357 -11
  95. package/dist/instance-lifecycle.js.map +1 -1
  96. package/dist/instructions.d.ts +12 -0
  97. package/dist/instructions.js +74 -3
  98. package/dist/instructions.js.map +1 -1
  99. package/dist/locale.js +1022 -310
  100. package/dist/locale.js.map +1 -1
  101. package/dist/login-flows.d.ts +91 -0
  102. package/dist/login-flows.js +170 -0
  103. package/dist/login-flows.js.map +1 -0
  104. package/dist/login-manager.d.ts +63 -0
  105. package/dist/login-manager.js +134 -0
  106. package/dist/login-manager.js.map +1 -0
  107. package/dist/network-family.d.ts +18 -0
  108. package/dist/network-family.js +20 -0
  109. package/dist/network-family.js.map +1 -0
  110. package/dist/outbound-handlers.d.ts +16 -1
  111. package/dist/outbound-handlers.js +174 -40
  112. package/dist/outbound-handlers.js.map +1 -1
  113. package/dist/outbound-schemas.d.ts +11 -3
  114. package/dist/outbound-schemas.js +11 -0
  115. package/dist/outbound-schemas.js.map +1 -1
  116. package/dist/pane-input-residue.d.ts +52 -0
  117. package/dist/pane-input-residue.js +107 -0
  118. package/dist/pane-input-residue.js.map +1 -0
  119. package/dist/process-memory.d.ts +6 -0
  120. package/dist/process-memory.js +45 -0
  121. package/dist/process-memory.js.map +1 -1
  122. package/dist/reply-dedup.d.ts +7 -8
  123. package/dist/reply-dedup.js +0 -0
  124. package/dist/reply-dedup.js.map +1 -1
  125. package/dist/restart-progress.d.ts +2 -0
  126. package/dist/restart-progress.js +3 -0
  127. package/dist/restart-progress.js.map +1 -1
  128. package/dist/scheduler/db.d.ts +12 -0
  129. package/dist/scheduler/db.js +59 -0
  130. package/dist/scheduler/db.js.map +1 -1
  131. package/dist/service-installer.d.ts +11 -0
  132. package/dist/service-installer.js +84 -18
  133. package/dist/service-installer.js.map +1 -1
  134. package/dist/settings-api.js +1 -1
  135. package/dist/settings-api.js.map +1 -1
  136. package/dist/setup-wizard.js +2 -2
  137. package/dist/setup-wizard.js.map +1 -1
  138. package/dist/spawn-gate.d.ts +30 -0
  139. package/dist/spawn-gate.js +79 -0
  140. package/dist/spawn-gate.js.map +1 -0
  141. package/dist/steer-capability.d.ts +1 -0
  142. package/dist/steer-capability.js +6 -0
  143. package/dist/steer-capability.js.map +1 -0
  144. package/dist/storm-window.d.ts +83 -0
  145. package/dist/storm-window.js +251 -0
  146. package/dist/storm-window.js.map +1 -0
  147. package/dist/tips.d.ts +44 -0
  148. package/dist/tips.js +355 -0
  149. package/dist/tips.js.map +1 -0
  150. package/dist/tmux-manager.d.ts +16 -0
  151. package/dist/tmux-manager.js +110 -25
  152. package/dist/tmux-manager.js.map +1 -1
  153. package/dist/tool-progress.d.ts +40 -0
  154. package/dist/tool-progress.js +289 -0
  155. package/dist/tool-progress.js.map +1 -0
  156. package/dist/topic-commands.d.ts +45 -6
  157. package/dist/topic-commands.js +394 -66
  158. package/dist/topic-commands.js.map +1 -1
  159. package/dist/transcript-monitor.d.ts +15 -2
  160. package/dist/transcript-monitor.js +63 -17
  161. package/dist/transcript-monitor.js.map +1 -1
  162. package/dist/transcript-sources.d.ts +131 -0
  163. package/dist/transcript-sources.js +580 -0
  164. package/dist/transcript-sources.js.map +1 -0
  165. package/dist/types.d.ts +11 -0
  166. package/dist/ui/dashboard.html +55 -32
  167. package/dist/ui/settings.html +200 -60
  168. package/dist/ui/view.html +147 -30
  169. package/dist/usage/format-rich.d.ts +1 -1
  170. package/dist/usage/format-rich.js +28 -24
  171. package/dist/usage/format-rich.js.map +1 -1
  172. package/dist/usage/i18n-keys.d.ts +7 -0
  173. package/dist/usage/i18n-keys.js +34 -0
  174. package/dist/usage/i18n-keys.js.map +1 -0
  175. package/dist/usage/i18n.d.ts +5 -0
  176. package/dist/usage/i18n.js +27 -0
  177. package/dist/usage/i18n.js.map +1 -0
  178. package/dist/usage/providers.d.ts +21 -0
  179. package/dist/usage/providers.js +153 -75
  180. package/dist/usage/providers.js.map +1 -1
  181. package/dist/usage/usage-api.d.ts +11 -3
  182. package/dist/usage/usage-api.js +61 -24
  183. package/dist/usage/usage-api.js.map +1 -1
  184. package/dist/workflow-templates/default.md +2 -1
  185. package/package.json +2 -2
@@ -25,6 +25,7 @@ import { homedir } from "node:os";
25
25
  import { getProviderRateLimit } from "./provider-alerts.js";
26
26
  import { readStatuslineRateLimits, } from "./statusline-usage.js";
27
27
  import Database from "better-sqlite3";
28
+ const i18n = (key, ...args) => args.length ? { key, args } : { key };
28
29
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
29
30
  const SESSION_MS = 5 * 60 * 60 * 1000;
30
31
  function jwtExpiryMs(token) {
@@ -173,19 +174,20 @@ function readShellRcToken() {
173
174
  */
174
175
  export function statuslineUsage(reading, plan, now = Date.now()) {
175
176
  const metrics = [];
176
- const add = (w, label, windowMs) => {
177
+ const add = (w, label, labelKey, windowMs) => {
177
178
  if (!w)
178
179
  return;
179
180
  metrics.push({
180
181
  label,
182
+ labelI18n: i18n(labelKey),
181
183
  type: "percent",
182
184
  used: w.usedPercent,
183
185
  resetsAt: w.resetsAtMs !== null ? new Date(w.resetsAtMs).toISOString() : null,
184
186
  windowMs,
185
187
  });
186
188
  };
187
- add(reading.fiveHour, "Session", SESSION_MS);
188
- add(reading.sevenDay, "Weekly", WEEK_MS);
189
+ add(reading.fiveHour, "Session", "usage.metric.session", SESSION_MS);
190
+ add(reading.sevenDay, "Weekly", "usage.metric.weekly", WEEK_MS);
189
191
  const ageMin = Math.round((now - reading.observedAtMs) / 60_000);
190
192
  const age = ageMin <= 0 ? "just now" : `${ageMin}m ago`;
191
193
  return {
@@ -194,9 +196,27 @@ export function statuslineUsage(reading, plan, now = Date.now()) {
194
196
  // Say what is missing as well as where it came from: someone comparing this
195
197
  // against claude.ai should not have to guess why per-model rows vanished.
196
198
  hint: `from ${reading.instance}'s statusline, ${age} — no API call; plan and per-model limits need a CLI login`,
199
+ hintI18n: i18n(ageMin <= 0 ? "usage.hint.statusline_now" : "usage.hint.statusline_minutes", reading.instance, ageMin),
197
200
  metrics,
198
201
  };
199
202
  }
203
+ /**
204
+ * Best-effort OAuth access token for direct Anthropic API calls (the /model
205
+ * catalog probe). Same resolution order as the usage panel: credentials file →
206
+ * CLAUDE_CODE_OAUTH_TOKEN → shell rc. Null when nothing usable exists.
207
+ */
208
+ export async function getClaudeOAuthToken() {
209
+ const home = process.env.CLAUDE_HOME || join(homedir(), ".claude");
210
+ let file;
211
+ try {
212
+ file = JSON.parse(await readFile(join(home, ".credentials.json"), "utf8")).claudeAiOauth ?? null;
213
+ }
214
+ catch {
215
+ file = null;
216
+ }
217
+ const auth = resolveClaudeAuth(file);
218
+ return auth && "token" in auth ? auth.token : null;
219
+ }
200
220
  /** Exported for tests — the statusline/API preference order is the subject. */
201
221
  export async function fetchClaudeUsage() {
202
222
  const home = process.env.CLAUDE_HOME || join(homedir(), ".claude");
@@ -220,12 +240,19 @@ export async function fetchClaudeUsage() {
220
240
  const hint = process.env.ANTHROPIC_API_KEY
221
241
  ? "API-key login has no subscription usage. Use `claude /login` or set CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`)."
222
242
  : "Log in with the Claude Code CLI (`claude`).";
223
- return { status: "no-credentials", hint, metrics: [] };
243
+ return {
244
+ status: "no-credentials",
245
+ hint,
246
+ hintI18n: process.env.ANTHROPIC_API_KEY
247
+ ? i18n("usage.hint.claude_api_key")
248
+ : i18n("usage.hint.login_claude"),
249
+ metrics: [],
250
+ };
224
251
  }
225
252
  if ("error" in auth) {
226
253
  if (statusline)
227
254
  return statuslineUsage(statusline, auth.plan);
228
- return { status: "error", plan: auth.plan, error: auth.error, metrics: [] };
255
+ return { status: "error", plan: auth.plan, error: auth.error, errorI18n: i18n("usage.error.claude_token_expired"), metrics: [] };
229
256
  }
230
257
  const plan = auth.plan;
231
258
  /** Every API failure degrades to the statusline rather than to an error row. */
@@ -244,33 +271,40 @@ export async function fetchClaudeUsage() {
244
271
  });
245
272
  }
246
273
  catch {
247
- return orStatusline({ status: "error", plan, error: "Could not reach api.anthropic.com.", metrics: [] });
274
+ return orStatusline({ status: "error", plan, error: "Could not reach api.anthropic.com.", errorI18n: i18n("usage.error.unreachable", "api.anthropic.com"), metrics: [] });
248
275
  }
249
276
  if (res.status === 401 || res.status === 403) {
250
- return orStatusline({ status: "error", plan, error: "Token rejected. Run `claude` once to refresh the login.", metrics: [] });
277
+ return orStatusline({ status: "error", plan, error: "Token rejected. Run `claude` once to refresh the login.", errorI18n: i18n("usage.error.token_rejected", "claude"), metrics: [] });
251
278
  }
252
279
  if (res.status === 429) {
253
- return orStatusline({ status: "error", plan, error: "Rate limited by Anthropic — try again later.", metrics: [] });
280
+ return orStatusline({ status: "error", plan, error: "Rate limited by Anthropic — try again later.", errorI18n: i18n("usage.error.rate_limited", "Anthropic"), metrics: [] });
254
281
  }
255
282
  if (!res.ok)
256
- return orStatusline({ status: "error", plan, error: `Usage request failed (HTTP ${res.status}).`, metrics: [] });
283
+ return orStatusline({ status: "error", plan, error: `Usage request failed (HTTP ${res.status}).`, errorI18n: i18n("usage.error.http", res.status), metrics: [] });
257
284
  let body;
258
285
  try {
259
286
  body = await res.json();
260
287
  }
261
288
  catch {
262
- return orStatusline({ status: "error", plan, error: "Invalid response from usage endpoint.", metrics: [] });
289
+ return orStatusline({ status: "error", plan, error: "Invalid response from usage endpoint.", errorI18n: i18n("usage.error.invalid_response"), metrics: [] });
263
290
  }
264
291
  const metrics = [];
265
- const window = (obj, label, windowMs) => {
292
+ const window = (obj, label, labelKey, windowMs) => {
266
293
  const o = obj;
267
294
  if (typeof o?.utilization !== "number")
268
295
  return;
269
- metrics.push({ label, type: "percent", used: o.utilization, resetsAt: claudeResetIso(o.resets_at), windowMs });
296
+ metrics.push({ label, labelI18n: i18n(labelKey), type: "percent", used: o.utilization, resetsAt: claudeResetIso(o.resets_at), windowMs });
270
297
  };
271
- window(body.five_hour, "Session", SESSION_MS);
272
- window(body.seven_day, "Weekly", WEEK_MS);
273
- window(body.seven_day_sonnet, "Sonnet (weekly)", WEEK_MS);
298
+ window(body.five_hour, "Session", "usage.metric.session", SESSION_MS);
299
+ window(body.seven_day, "Weekly", "usage.metric.weekly", WEEK_MS);
300
+ const sonnet = body.seven_day_sonnet;
301
+ if (typeof sonnet?.utilization === "number") {
302
+ metrics.push({
303
+ label: "Sonnet (weekly)", labelI18n: i18n("usage.metric.named_weekly", "Sonnet"),
304
+ scope: "model",
305
+ type: "percent", used: sonnet.utilization, resetsAt: claudeResetIso(sonnet.resets_at), windowMs: WEEK_MS,
306
+ });
307
+ }
274
308
  // Per-model weekly windows live in the `limits` array (kind: weekly_scoped).
275
309
  // `limits` also carries which window is currently BINDING (`is_active`) and a
276
310
  // severity the top-level windows do not expose. Probed live on a Team account:
@@ -288,31 +322,39 @@ export async function fetchClaudeUsage() {
288
322
  continue;
289
323
  metrics.push({
290
324
  label: `${model} (weekly)`,
325
+ labelI18n: i18n("usage.metric.named_weekly", model),
326
+ scope: "model",
291
327
  type: "percent",
292
328
  used: e.percent,
293
329
  resetsAt: claudeResetIso(e.resets_at),
294
330
  windowMs: WEEK_MS,
295
- ...(e.is_active === true ? { note: "binding" } : {}),
296
- ...(typeof e.severity === "string" && e.severity !== "normal" ? { note: `binding · ${e.severity}` } : {}),
331
+ ...(e.is_active === true ? { note: "binding", noteI18n: i18n("usage.note.binding") } : {}),
332
+ ...(typeof e.severity === "string" && e.severity !== "normal"
333
+ ? { note: `binding · ${e.severity}`, noteI18n: i18n("usage.note.binding_severity", e.severity) }
334
+ : {}),
297
335
  });
298
336
  }
299
337
  // Mark the two plain windows too, using the same source.
300
338
  for (const m of metrics) {
301
339
  if (m.note)
302
340
  continue;
303
- if (m.label === "Session" && activeKinds.has("session"))
341
+ if (m.label === "Session" && activeKinds.has("session")) {
304
342
  m.note = "binding";
305
- if (m.label === "Weekly" && activeKinds.has("weekly_all"))
343
+ m.noteI18n = i18n("usage.note.binding");
344
+ }
345
+ if (m.label === "Weekly" && activeKinds.has("weekly_all")) {
306
346
  m.note = "binding";
347
+ m.noteI18n = i18n("usage.note.binding");
348
+ }
307
349
  }
308
350
  const extra = body.extra_usage;
309
351
  if (extra?.is_enabled === true && typeof extra.used_credits === "number") {
310
352
  const used = extra.used_credits / 100;
311
353
  const limit = typeof extra.monthly_limit === "number" && extra.monthly_limit > 0 ? extra.monthly_limit / 100 : null;
312
354
  if (limit)
313
- metrics.push({ label: "Extra usage", type: "dollars", used, limit });
355
+ metrics.push({ label: "Extra usage", labelI18n: i18n("usage.metric.extra_usage"), type: "dollars", used, limit });
314
356
  else if (used > 0)
315
- metrics.push({ label: "Extra usage", type: "dollars", used });
357
+ metrics.push({ label: "Extra usage", labelI18n: i18n("usage.metric.extra_usage"), type: "dollars", used });
316
358
  }
317
359
  // A 200 with nothing in it is not better than the file on disk.
318
360
  if (metrics.length === 0 && statusline)
@@ -381,8 +423,10 @@ let agyTokenCache = null;
381
423
  */
382
424
  export function agyPoolMetrics(buckets) {
383
425
  const POOLED = {
384
- "gemini-5h": "Gemini (session)", "gemini-weekly": "Gemini (weekly)",
385
- "3p-5h": "Claude & others (session)", "3p-weekly": "Claude & others (weekly)",
426
+ "gemini-5h": { text: "Gemini (session)", i18n: i18n("usage.metric.named_session", "Gemini") },
427
+ "gemini-weekly": { text: "Gemini (weekly)", i18n: i18n("usage.metric.named_weekly", "Gemini") },
428
+ "3p-5h": { text: "Claude & others (session)", i18n: i18n("usage.metric.agy_claude_others_session") },
429
+ "3p-weekly": { text: "Claude & others (weekly)", i18n: i18n("usage.metric.agy_claude_others_weekly") },
386
430
  };
387
431
  const direct = [];
388
432
  const pools = new Map();
@@ -392,7 +436,7 @@ export function agyPoolMetrics(buckets) {
392
436
  const reset = b.resetTime ?? null;
393
437
  const pooledLabel = POOLED[b.bucketId];
394
438
  if (pooledLabel) {
395
- direct.push({ label: pooledLabel, type: "percent", used: (1 - b.remainingFraction) * 100, resetsAt: reset });
439
+ direct.push({ label: pooledLabel.text, labelI18n: pooledLabel.i18n, type: "percent", used: (1 - b.remainingFraction) * 100, resetsAt: reset });
396
440
  continue;
397
441
  }
398
442
  const pool = b.bucketId.startsWith("gemini") ? "Gemini" : "Claude & others";
@@ -413,6 +457,9 @@ export function agyPoolMetrics(buckets) {
413
457
  return direct;
414
458
  return [...pools.entries()].map(([label, p]) => ({
415
459
  label,
460
+ labelI18n: label === "Claude & others"
461
+ ? i18n("usage.metric.agy_claude_others")
462
+ : i18n("usage.metric.named", label),
416
463
  type: "percent",
417
464
  used: (1 - p.fraction) * 100,
418
465
  resetsAt: p.resetsAt,
@@ -420,6 +467,7 @@ export function agyPoolMetrics(buckets) {
420
467
  // it a flat 0% right after a weekly rollover is indistinguishable from a
421
468
  // broken reading — which is exactly how it was reported.
422
469
  note: `busiest of ${p.models} model${p.models === 1 ? "" : "s"}`,
470
+ noteI18n: i18n(p.models === 1 ? "usage.note.busiest_model" : "usage.note.busiest_models", p.models),
423
471
  }));
424
472
  }
425
473
  /**
@@ -465,6 +513,9 @@ async function agyAccessToken() {
465
513
  error: client
466
514
  ? "Access token expired and no refresh token is stored. Run `agy` once."
467
515
  : "Access token expired. Run `agy` once to refresh it (or set AGY_OAUTH_CLIENT_ID/SECRET to refresh automatically).",
516
+ errorI18n: client
517
+ ? i18n("usage.error.agy_no_refresh")
518
+ : i18n("usage.error.agy_token_expired"),
468
519
  };
469
520
  }
470
521
  try {
@@ -480,15 +531,15 @@ async function agyAccessToken() {
480
531
  signal: AbortSignal.timeout(15_000),
481
532
  });
482
533
  if (!res.ok)
483
- return { error: "Google sign-in expired. Run `agy` once to log in again." };
534
+ return { error: "Google sign-in expired. Run `agy` once to log in again.", errorI18n: i18n("usage.error.agy_login_expired") };
484
535
  const body = await res.json();
485
536
  if (!body.access_token)
486
- return { error: "Google token refresh returned no token." };
537
+ return { error: "Google token refresh returned no token.", errorI18n: i18n("usage.error.agy_refresh_empty") };
487
538
  agyTokenCache = { token: body.access_token, until: Date.now() + Math.max(60, (body.expires_in ?? 3600) - 120) * 1000 };
488
539
  return { token: body.access_token };
489
540
  }
490
541
  catch {
491
- return { error: "Could not reach oauth2.googleapis.com." };
542
+ return { error: "Could not reach oauth2.googleapis.com.", errorI18n: i18n("usage.error.unreachable", "oauth2.googleapis.com") };
492
543
  }
493
544
  }
494
545
  async function fetchAntigravityUsage() {
@@ -506,15 +557,16 @@ async function fetchAntigravityUsage() {
506
557
  status: "error",
507
558
  error: `Individual cap reached — CLI is rate limited (resets in ~${resetText}). `
508
559
  + "The per-model pool quotas are a separate counter and may still read 0% used.",
560
+ errorI18n: i18n("usage.error.agy_cap", resetText),
509
561
  metrics: [],
510
562
  };
511
563
  }
512
564
  const auth = await agyAccessToken();
513
565
  if (auth === null) {
514
- return { status: "no-credentials", hint: "Log in with the Antigravity CLI (`agy`).", metrics: [] };
566
+ return { status: "no-credentials", hint: "Log in with the Antigravity CLI (`agy`).", hintI18n: i18n("usage.hint.login_agy"), metrics: [] };
515
567
  }
516
568
  if ("error" in auth)
517
- return { status: "error", error: auth.error, metrics: [] };
569
+ return { status: "error", error: auth.error, errorI18n: auth.errorI18n, metrics: [] };
518
570
  for (const base of AGY_CLOUDCODE_URLS) {
519
571
  let res;
520
572
  try {
@@ -533,7 +585,7 @@ async function fetchAntigravityUsage() {
533
585
  continue;
534
586
  }
535
587
  if (res.status === 401 || res.status === 403) {
536
- return { status: "error", error: "Token rejected. Run `agy` once to refresh the login.", metrics: [] };
588
+ return { status: "error", error: "Token rejected. Run `agy` once to refresh the login.", errorI18n: i18n("usage.error.token_rejected", "agy"), metrics: [] };
537
589
  }
538
590
  if (!res.ok)
539
591
  continue; // try the other base
@@ -548,9 +600,9 @@ async function fetchAntigravityUsage() {
548
600
  const metrics = agyPoolMetrics(buckets);
549
601
  return metrics.length
550
602
  ? { status: "ok", plan: null, metrics }
551
- : { status: "ok", plan: null, hint: "No quota information reported for this account.", metrics: [] };
603
+ : { status: "ok", plan: null, hint: "No quota information reported for this account.", hintI18n: i18n("usage.hint.no_quota"), metrics: [] };
552
604
  }
553
- return { status: "error", error: "Could not reach the Cloud Code quota endpoints.", metrics: [] };
605
+ return { status: "error", error: "Could not reach the Cloud Code quota endpoints.", errorI18n: i18n("usage.error.unreachable", "Cloud Code quota endpoints"), metrics: [] };
554
606
  }
555
607
  // ── Codex ────────────────────────────────────────────────────────────────────
556
608
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -589,7 +641,7 @@ function codexWindows(rateLimit, labels, nowMs) {
589
641
  label = labels.other(Math.round(windowMs / 864e5));
590
642
  else
591
643
  label = fallback === "session" ? labels.session : labels.weekly;
592
- metrics.push({ label, type: "percent", used: win.used_percent, resetsAt: codexResetIso(win, nowMs), windowMs });
644
+ metrics.push({ label: label.text, labelI18n: label.i18n, type: "percent", used: win.used_percent, resetsAt: codexResetIso(win, nowMs), windowMs });
593
645
  }
594
646
  return metrics;
595
647
  }
@@ -602,22 +654,22 @@ function codexPlan(planType) {
602
654
  default: return planType.trim().split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
603
655
  }
604
656
  }
605
- async function fetchCodexUsage() {
657
+ export async function fetchCodexUsage() {
606
658
  const home = process.env.CODEX_HOME || join(homedir(), ".codex");
607
659
  let auth;
608
660
  try {
609
661
  auth = JSON.parse(await readFile(join(home, "auth.json"), "utf8"));
610
662
  }
611
663
  catch {
612
- return { status: "no-credentials", hint: "Log in with the Codex CLI (`codex`).", metrics: [] };
664
+ return { status: "no-credentials", hint: "Log in with the Codex CLI (`codex`).", hintI18n: i18n("usage.hint.login_codex"), metrics: [] };
613
665
  }
614
666
  const accessToken = auth?.tokens?.access_token;
615
667
  if (!accessToken) {
616
- return { status: "no-credentials", hint: "auth.json has no OAuth login (API-key-only auth cannot read usage).", metrics: [] };
668
+ return { status: "no-credentials", hint: "auth.json has no OAuth login (API-key-only auth cannot read usage).", hintI18n: i18n("usage.hint.codex_no_oauth"), metrics: [] };
617
669
  }
618
670
  const exp = jwtExpiryMs(accessToken);
619
671
  if (exp && exp < Date.now()) {
620
- return { status: "error", error: "Access token expired. Run `codex` once to refresh it.", metrics: [] };
672
+ return { status: "error", error: "Access token expired. Run `codex` once to refresh it.", errorI18n: i18n("usage.error.codex_token_expired"), metrics: [] };
621
673
  }
622
674
  const headers = {
623
675
  Authorization: `Bearer ${accessToken}`,
@@ -631,23 +683,26 @@ async function fetchCodexUsage() {
631
683
  res = await fetch(CODEX_USAGE_URL, { headers, signal: AbortSignal.timeout(10_000) });
632
684
  }
633
685
  catch {
634
- return { status: "error", error: "Could not reach chatgpt.com.", metrics: [] };
686
+ return { status: "error", error: "Could not reach chatgpt.com.", errorI18n: i18n("usage.error.unreachable", "chatgpt.com"), metrics: [] };
635
687
  }
636
688
  if (res.status === 401 || res.status === 403) {
637
- return { status: "error", error: "Token rejected. Run `codex` once to refresh the login.", metrics: [] };
689
+ return { status: "error", error: "Token rejected. Run `codex` once to refresh the login.", errorI18n: i18n("usage.error.token_rejected", "codex"), metrics: [] };
638
690
  }
639
691
  if (!res.ok)
640
- return { status: "error", error: `Usage request failed (HTTP ${res.status}).`, metrics: [] };
692
+ return { status: "error", error: `Usage request failed (HTTP ${res.status}).`, errorI18n: i18n("usage.error.http", res.status), metrics: [] };
641
693
  let body;
642
694
  try {
643
695
  body = await res.json();
644
696
  }
645
697
  catch {
646
- return { status: "error", error: "Invalid response from usage endpoint.", metrics: [] };
698
+ return { status: "error", error: "Invalid response from usage endpoint.", errorI18n: i18n("usage.error.invalid_response"), metrics: [] };
647
699
  }
648
700
  const nowMs = Date.now();
649
701
  const metrics = codexWindows(body.rate_limit, {
650
- session: "Session", weekly: "Weekly", monthly: "Monthly", other: d => `${d}-day window`,
702
+ session: { text: "Session", i18n: i18n("usage.metric.session") },
703
+ weekly: { text: "Weekly", i18n: i18n("usage.metric.weekly") },
704
+ monthly: { text: "Monthly", i18n: i18n("usage.metric.monthly") },
705
+ other: d => ({ text: `${d}-day window`, i18n: i18n("usage.metric.days_window", d) }),
651
706
  }, nowMs);
652
707
  // Model-specific limits (e.g. Spark) ride in additional_rate_limits, same window shape.
653
708
  for (const entry of Array.isArray(body.additional_rate_limits) ? body.additional_rate_limits : []) {
@@ -656,12 +711,19 @@ async function fetchCodexUsage() {
656
711
  continue;
657
712
  const rawName = e.limit_name || e.metered_feature || "Model limit";
658
713
  metrics.push(...codexWindows(e.rate_limit, {
659
- session: rawName, weekly: `${rawName} (weekly)`, monthly: `${rawName} (monthly)`, other: d => `${rawName} (${d}d)`,
660
- }, nowMs));
714
+ session: { text: rawName, i18n: i18n("usage.metric.named", rawName) },
715
+ weekly: { text: `${rawName} (weekly)`, i18n: i18n("usage.metric.named_weekly", rawName) },
716
+ monthly: { text: `${rawName} (monthly)`, i18n: i18n("usage.metric.named_monthly", rawName) },
717
+ other: d => ({ text: `${rawName} (${d}d)`, i18n: i18n("usage.metric.named_days", rawName, d) }),
718
+ }, nowMs).map(metric => ({ ...metric, scope: "model" })));
661
719
  }
662
720
  const resets = body.rate_limit_reset_credits;
663
721
  if (typeof resets?.available_count === "number" && resets.available_count >= 0) {
664
- metrics.push({ label: "Rate limit resets", type: "count", value: Math.floor(resets.available_count), unit: "available" });
722
+ metrics.push({
723
+ label: "Rate limit resets", labelI18n: i18n("usage.metric.rate_limit_resets"),
724
+ type: "count", value: Math.floor(resets.available_count),
725
+ unit: "available", unitI18n: i18n("usage.unit.available"),
726
+ });
665
727
  }
666
728
  const credits = body.credits;
667
729
  let remaining = null;
@@ -671,7 +733,10 @@ async function fetchCodexUsage() {
671
733
  remaining = 0;
672
734
  if (remaining !== null) {
673
735
  const count = Math.max(0, Math.floor(remaining));
674
- metrics.push({ label: "Credits", type: "count", value: count, unit: "credits", note: `≈ $${(count * CREDIT_USD_RATE).toFixed(2)}` });
736
+ metrics.push({
737
+ label: "Credits", labelI18n: i18n("usage.metric.credits"), type: "count", value: count,
738
+ unit: "credits", unitI18n: i18n("usage.unit.credits"), note: `≈ $${(count * CREDIT_USD_RATE).toFixed(2)}`,
739
+ });
675
740
  }
676
741
  return { status: "ok", plan: codexPlan(body.plan_type), metrics };
677
742
  }
@@ -701,11 +766,11 @@ async function grokRefreshAndPersist(file, auth, entryKey, entry) {
701
766
  await access(file, constants.W_OK);
702
767
  }
703
768
  catch {
704
- return { error: "Token expired and auth.json is read-only. Run `grok` once to refresh it." };
769
+ return { error: "Token expired and auth.json is read-only. Run `grok` once to refresh it.", errorI18n: i18n("usage.error.grok_auth_readonly") };
705
770
  }
706
771
  const refreshToken = (entry.refresh_token || entry.refresh || "").trim();
707
772
  if (!refreshToken)
708
- return { error: "Token expired and no refresh token found. Run `grok login`." };
773
+ return { error: "Token expired and no refresh token found. Run `grok login`.", errorI18n: i18n("usage.error.grok_no_refresh") };
709
774
  const clientId = entry.oidc_client_id?.trim()
710
775
  || entryKey.split("::").pop()?.trim()
711
776
  || GROK_DEFAULT_CLIENT_ID;
@@ -719,19 +784,19 @@ async function grokRefreshAndPersist(file, auth, entryKey, entry) {
719
784
  });
720
785
  }
721
786
  catch {
722
- return { error: "Could not reach auth.x.ai to refresh the token." };
787
+ return { error: "Could not reach auth.x.ai to refresh the token.", errorI18n: i18n("usage.error.grok_refresh_unreachable") };
723
788
  }
724
789
  if (!res.ok)
725
- return { error: "Grok session expired. Run `grok login` again." };
790
+ return { error: "Grok session expired. Run `grok login` again.", errorI18n: i18n("usage.error.grok_expired") };
726
791
  let body;
727
792
  try {
728
793
  body = await res.json();
729
794
  }
730
795
  catch {
731
- return { error: "Invalid refresh response from auth.x.ai." };
796
+ return { error: "Invalid refresh response from auth.x.ai.", errorI18n: i18n("usage.error.grok_invalid_refresh") };
732
797
  }
733
798
  if (!body.access_token)
734
- return { error: "Grok session expired. Run `grok login` again." };
799
+ return { error: "Grok session expired. Run `grok login` again.", errorI18n: i18n("usage.error.grok_expired") };
735
800
  // Update only this entry's fields, preserving everything else in the file
736
801
  // (other accounts' entries included).
737
802
  const updated = { ...(auth[entryKey] ?? {}) };
@@ -746,7 +811,7 @@ async function grokRefreshAndPersist(file, auth, entryKey, entry) {
746
811
  await writeFile(file, JSON.stringify({ ...auth, [entryKey]: updated }, null, 2));
747
812
  }
748
813
  catch {
749
- return { error: "Refreshed but could not write auth.json — refusing to continue without persisting. Run `grok` once instead." };
814
+ return { error: "Refreshed but could not write auth.json — refusing to continue without persisting. Run `grok` once instead.", errorI18n: i18n("usage.error.grok_persist") };
750
815
  }
751
816
  return { token: body.access_token };
752
817
  }
@@ -766,18 +831,18 @@ async function fetchGrokUsage() {
766
831
  auth = JSON.parse(await readFile(file, "utf8"));
767
832
  }
768
833
  catch {
769
- return { status: "no-credentials", hint: "Log in with the Grok CLI (`grok login`).", metrics: [] };
834
+ return { status: "no-credentials", hint: "Log in with the Grok CLI (`grok login`).", hintI18n: i18n("usage.hint.login_grok"), metrics: [] };
770
835
  }
771
836
  const entries = Object.entries(auth).filter(([, e]) => typeof e?.key === "string" && e.key.trim());
772
837
  if (!entries.length)
773
- return { status: "no-credentials", hint: "auth.json has no usable login. Run `grok login`.", metrics: [] };
838
+ return { status: "no-credentials", hint: "auth.json has no usable login. Run `grok login`.", hintI18n: i18n("usage.hint.grok_no_login"), metrics: [] };
774
839
  const [entryKey, entry] = entries[0];
775
840
  let token = entry.key.trim();
776
841
  const expiry = grokEntryExpiryMs(entry, token);
777
842
  if (expiry && expiry - Date.now() <= GROK_REFRESH_BUFFER_MS) {
778
843
  const refreshed = await grokRefreshAndPersist(file, auth, entryKey, entry);
779
844
  if (refreshed.error)
780
- return { status: "error", error: refreshed.error, metrics: [] };
845
+ return { status: "error", error: refreshed.error, errorI18n: refreshed.errorI18n, metrics: [] };
781
846
  token = refreshed.token;
782
847
  }
783
848
  const fetchBoth = async (tok) => {
@@ -793,30 +858,30 @@ async function fetchGrokUsage() {
793
858
  if (credits.status === 401 || credits.status === 403) {
794
859
  const refreshed = await grokRefreshAndPersist(file, auth, entryKey, entry);
795
860
  if (refreshed.error)
796
- return { status: "error", error: refreshed.error, metrics: [] };
861
+ return { status: "error", error: refreshed.error, errorI18n: refreshed.errorI18n, metrics: [] };
797
862
  token = refreshed.token;
798
863
  ({ credits, settings } = await fetchBoth(token));
799
864
  }
800
865
  }
801
866
  catch {
802
- return { status: "error", error: "Could not reach cli-chat-proxy.grok.com.", metrics: [] };
867
+ return { status: "error", error: "Could not reach cli-chat-proxy.grok.com.", errorI18n: i18n("usage.error.unreachable", "cli-chat-proxy.grok.com"), metrics: [] };
803
868
  }
804
869
  if (credits.status === 401 || credits.status === 403) {
805
- return { status: "error", error: "Grok session expired. Run `grok login` again.", metrics: [] };
870
+ return { status: "error", error: "Grok session expired. Run `grok login` again.", errorI18n: i18n("usage.error.grok_expired"), metrics: [] };
806
871
  }
807
872
  if (!credits.ok)
808
- return { status: "error", error: `Billing request failed (HTTP ${credits.status}).`, metrics: [] };
873
+ return { status: "error", error: `Billing request failed (HTTP ${credits.status}).`, errorI18n: i18n("usage.error.billing_http", credits.status), metrics: [] };
809
874
  let body;
810
875
  try {
811
876
  body = await credits.json();
812
877
  }
813
878
  catch {
814
- return { status: "error", error: "Invalid response from billing endpoint.", metrics: [] };
879
+ return { status: "error", error: "Invalid response from billing endpoint.", errorI18n: i18n("usage.error.invalid_billing_response"), metrics: [] };
815
880
  }
816
881
  const config = body?.config;
817
882
  const period = config?.currentPeriod;
818
883
  if (!config || !period?.type)
819
- return { status: "error", error: "Grok billing response changed.", metrics: [] };
884
+ return { status: "error", error: "Grok billing response changed.", errorI18n: i18n("usage.error.grok_response_changed"), metrics: [] };
820
885
  const metrics = [];
821
886
  if (period.type === GROK_WEEKLY_PERIOD) {
822
887
  // proto-JSON drops zero-valued fields: an absent creditUsagePercent means 0%.
@@ -825,6 +890,7 @@ async function fetchGrokUsage() {
825
890
  const end = new Date(period.end ?? "").getTime();
826
891
  metrics.push({
827
892
  label: "Weekly limit",
893
+ labelI18n: i18n("usage.metric.weekly_limit"),
828
894
  type: "percent",
829
895
  used: Math.min(100, Math.max(0, pct)),
830
896
  resetsAt: Number.isNaN(end) ? null : new Date(end).toISOString(),
@@ -832,7 +898,11 @@ async function fetchGrokUsage() {
832
898
  });
833
899
  }
834
900
  const cap = typeof config.onDemandCap?.val === "number" ? config.onDemandCap.val : 0;
835
- metrics.push({ label: "Pay as you go", type: "text", value: cap > 0 ? `${cap} cap` : "Disabled" });
901
+ metrics.push({
902
+ label: "Pay as you go", labelI18n: i18n("usage.metric.pay_as_you_go"), type: "text",
903
+ value: cap > 0 ? `${cap} cap` : "Disabled",
904
+ valueI18n: cap > 0 ? i18n("usage.value.cap", cap) : i18n("usage.value.disabled"),
905
+ });
836
906
  let plan = null;
837
907
  if (settings?.ok) {
838
908
  try {
@@ -925,12 +995,12 @@ export async function fetchKiroUsage() {
925
995
  const { token, kind, missing, notInstalled } = readKiroToken();
926
996
  if (notInstalled) {
927
997
  // The one case that should disappear from the panel: kiro-cli isn't here.
928
- return { status: "no-credentials", hint: "Log in with the Kiro CLI (`kiro-cli`).", metrics: [] };
998
+ return { status: "no-credentials", hint: "Log in with the Kiro CLI (`kiro-cli`).", hintI18n: i18n("usage.hint.login_kiro"), metrics: [] };
929
999
  }
930
1000
  if (missing || !token) {
931
1001
  // Installed but no readable login. Previously indistinguishable from
932
1002
  // not-installed, so the whole Kiro row vanished instead of saying this.
933
- return { status: "ok", plan: "Kiro", hint: "Signed out — run `kiro-cli` to log in.", metrics: [] };
1003
+ return { status: "ok", plan: "Kiro", hint: "Signed out — run `kiro-cli` to log in.", hintI18n: i18n("usage.hint.kiro_signed_out"), metrics: [] };
934
1004
  }
935
1005
  const expiresAt = token.expires_at ? new Date(token.expires_at).getTime() : null;
936
1006
  if (expiresAt && expiresAt < Date.now()) {
@@ -938,7 +1008,7 @@ export async function fetchKiroUsage() {
938
1008
  // panel showing a red failure for a routine token rollover is just noise.
939
1009
  // Still `ok` rather than no-credentials, so the row stays on screen with its
940
1010
  // explanation instead of the provider silently disappearing.
941
- return { status: "ok", plan: "Kiro", hint: "Token refreshing — try again in a moment.", metrics: [] };
1011
+ return { status: "ok", plan: "Kiro", hint: "Token refreshing — try again in a moment.", hintI18n: i18n("usage.hint.token_refreshing"), metrics: [] };
942
1012
  }
943
1013
  if (kind === "q-developer-pro") {
944
1014
  // Seat-licensed subscription: GetUsageLimits describes Builder-ID credit
@@ -947,6 +1017,7 @@ export async function fetchKiroUsage() {
947
1017
  status: "ok",
948
1018
  plan: "Q Developer Pro",
949
1019
  hint: "Subscription — no credit usage to report.",
1020
+ hintI18n: i18n("usage.hint.subscription_no_credit"),
950
1021
  metrics: [],
951
1022
  };
952
1023
  }
@@ -967,35 +1038,40 @@ export async function fetchKiroUsage() {
967
1038
  });
968
1039
  }
969
1040
  catch {
970
- return { status: "error", error: `Could not reach codewhisperer.${region}.amazonaws.com.`, metrics: [] };
1041
+ return { status: "error", error: `Could not reach codewhisperer.${region}.amazonaws.com.`, errorI18n: i18n("usage.error.unreachable", `codewhisperer.${region}.amazonaws.com`), metrics: [] };
971
1042
  }
972
1043
  if (res.status === 401 || res.status === 403) {
973
- return { status: "error", error: "Token rejected. Use `kiro-cli` once to refresh the login.", metrics: [] };
1044
+ return { status: "error", error: "Token rejected. Use `kiro-cli` once to refresh the login.", errorI18n: i18n("usage.error.token_rejected", "kiro-cli"), metrics: [] };
974
1045
  }
975
1046
  if (!res.ok)
976
- return { status: "error", error: `Usage request failed (HTTP ${res.status}).`, metrics: [] };
1047
+ return { status: "error", error: `Usage request failed (HTTP ${res.status}).`, errorI18n: i18n("usage.error.http", res.status), metrics: [] };
977
1048
  let body;
978
1049
  try {
979
1050
  body = await res.json();
980
1051
  }
981
1052
  catch {
982
- return { status: "error", error: "Invalid response from GetUsageLimits.", metrics: [] };
1053
+ return { status: "error", error: "Invalid response from GetUsageLimits.", errorI18n: i18n("usage.error.invalid_kiro_response"), metrics: [] };
983
1054
  }
984
1055
  const plan = typeof body.subscriptionInfo?.subscriptionTitle === "string"
985
1056
  ? kiroTitleCase(body.subscriptionInfo.subscriptionTitle)
986
1057
  : null;
987
1058
  const metrics = [];
988
1059
  for (const ub of Array.isArray(body.usageBreakdownList) ? body.usageBreakdownList : []) {
989
- const unit = (ub.displayNamePlural || ub.displayName || "credits").toLowerCase();
1060
+ const displayName = ub.displayNamePlural || ub.displayName;
1061
+ const unit = (displayName || "credits").toLowerCase();
990
1062
  const used = ub.currentUsageWithPrecision ?? ub.currentUsage ?? null;
991
1063
  const limit = ub.usageLimitWithPrecision ?? ub.usageLimit ?? null;
992
1064
  if (used !== null && limit !== null && limit > 0) {
993
1065
  metrics.push({
994
- label: `${ub.displayNamePlural || ub.displayName || "Usage"} (monthly)`,
1066
+ label: `${displayName || "Usage"} (monthly)`,
1067
+ labelI18n: displayName
1068
+ ? i18n("usage.metric.named_monthly", displayName)
1069
+ : i18n("usage.metric.usage_monthly"),
995
1070
  type: "percent",
996
1071
  used: Math.min(100, (used / limit) * 100),
997
1072
  resetsAt: kiroEpochIso(ub.nextDateReset ?? body.nextDateReset),
998
1073
  note: `${+used.toFixed(1)} / ${+limit.toFixed(0)} ${unit}`,
1074
+ noteI18n: i18n("usage.note.used_limit_unit", +used.toFixed(1), +limit.toFixed(0), unit),
999
1075
  });
1000
1076
  }
1001
1077
  // Bonus / gift credits (redeemed promo codes), aggregated across active codes.
@@ -1007,27 +1083,29 @@ export async function fetchKiroUsage() {
1007
1083
  if (bLimit > 0) {
1008
1084
  metrics.push({
1009
1085
  label: "Bonus credits",
1086
+ labelI18n: i18n("usage.metric.bonus_credits"),
1010
1087
  type: "percent",
1011
1088
  used: Math.min(100, (bUsed / bLimit) * 100),
1012
1089
  resetsAt: expiries.length ? kiroEpochIso(Math.min(...expiries)) : null,
1013
1090
  note: `${+bUsed.toFixed(1)} / ${+bLimit.toFixed(0)} ${unit} · ${bonuses.length} codes`,
1091
+ noteI18n: i18n("usage.note.bonus_codes", +bUsed.toFixed(1), +bLimit.toFixed(0), unit, bonuses.length),
1014
1092
  });
1015
1093
  }
1016
1094
  }
1017
1095
  const overages = ub.currentOveragesWithPrecision ?? ub.currentOverages ?? 0;
1018
1096
  if (overages > 0 && typeof ub.overageCharges === "number") {
1019
- metrics.push({ label: "Overage charges", type: "dollars", used: ub.overageCharges });
1097
+ metrics.push({ label: "Overage charges", labelI18n: i18n("usage.metric.overage_charges"), type: "dollars", used: ub.overageCharges });
1020
1098
  }
1021
1099
  }
1022
1100
  const overageStatus = body.overageConfiguration?.overageStatus;
1023
1101
  if (typeof overageStatus === "string") {
1024
- metrics.push({ label: "Pay-per-use", type: "text", value: kiroTitleCase(overageStatus.replace(/_/g, " ")) });
1102
+ metrics.push({ label: "Pay-per-use", labelI18n: i18n("usage.metric.pay_per_use"), type: "text", value: kiroTitleCase(overageStatus.replace(/_/g, " ")) });
1025
1103
  }
1026
1104
  if (metrics.length === 0) {
1027
1105
  // Amazon Q logins whose account type reports no credit buckets (#398's
1028
1106
  // sibling case): an OK answer with nothing in it must say so, not render an
1029
1107
  // empty row that reads like a failure.
1030
- return { status: "ok", plan: plan ?? "Amazon Q", hint: "No quota information available for this account type.", metrics: [] };
1108
+ return { status: "ok", plan: plan ?? "Amazon Q", hint: "No quota information available for this account type.", hintI18n: i18n("usage.hint.no_quota_account"), metrics: [] };
1031
1109
  }
1032
1110
  return { status: "ok", plan, metrics };
1033
1111
  }