@songsid/agend 2.1.4-beta.4 → 2.1.4-beta.41

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