@songsid/agend 2.1.4-beta.4 → 2.1.4-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-endpoint.js +1 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.d.ts +9 -5
- package/dist/backend/antigravity.js +156 -31
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +62 -23
- package/dist/backend/claude-code.js +296 -43
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +148 -8
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/gemini-cli.js +2 -2
- package/dist/backend/gemini-cli.js.map +1 -1
- package/dist/backend/grok.js +2 -2
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +2 -0
- package/dist/backend/kiro.js +24 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode.d.ts +27 -11
- package/dist/backend/opencode.js +65 -41
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +44 -2
- package/dist/channel/adapters/discord.js +526 -78
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +30 -0
- package/dist/channel/adapters/telegram.js +212 -18
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/agy-mcp-launcher.d.ts +2 -0
- package/dist/channel/agy-mcp-launcher.js +28 -0
- package/dist/channel/agy-mcp-launcher.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +2 -1
- package/dist/channel/ipc-bridge.js +33 -4
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/mcp-server.js +28 -25
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.js +62 -1
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/types.d.ts +31 -1
- package/dist/classic-channel-manager.d.ts +40 -3
- package/dist/classic-channel-manager.js +218 -31
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +109 -37
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +48 -7
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +6 -0
- package/dist/cross-instance-envelope.js +30 -0
- package/dist/cross-instance-envelope.js.map +1 -0
- package/dist/daemon.d.ts +319 -9
- package/dist/daemon.js +1402 -255
- package/dist/daemon.js.map +1 -1
- package/dist/doctor.d.ts +41 -0
- package/dist/doctor.js +267 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fleet-context.d.ts +47 -0
- package/dist/fleet-manager.d.ts +265 -11
- package/dist/fleet-manager.js +2273 -455
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-slim.d.ts +10 -0
- package/dist/fleet-yaml-slim.js +59 -0
- package/dist/fleet-yaml-slim.js.map +1 -0
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
- package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
- package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
- package/dist/instance-lifecycle.d.ts +67 -0
- package/dist/instance-lifecycle.js +273 -8
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +35 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +958 -288
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.d.ts +91 -0
- package/dist/login-flows.js +170 -0
- package/dist/login-flows.js.map +1 -0
- package/dist/login-manager.d.ts +63 -0
- package/dist/login-manager.js +134 -0
- package/dist/login-manager.js.map +1 -0
- package/dist/network-family.d.ts +18 -0
- package/dist/network-family.js +20 -0
- package/dist/network-family.js.map +1 -0
- package/dist/outbound-handlers.d.ts +8 -0
- package/dist/outbound-handlers.js +101 -18
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +7 -1
- package/dist/outbound-schemas.js +9 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/reply-dedup.d.ts +7 -8
- package/dist/reply-dedup.js +0 -0
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-progress.d.ts +2 -0
- package/dist/restart-progress.js +3 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/db.d.ts +12 -0
- package/dist/scheduler/db.js +59 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-installer.d.ts +11 -0
- package/dist/service-installer.js +84 -18
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/spawn-gate.d.ts +30 -0
- package/dist/spawn-gate.js +79 -0
- package/dist/spawn-gate.js.map +1 -0
- package/dist/storm-window.d.ts +83 -0
- package/dist/storm-window.js +251 -0
- package/dist/storm-window.js.map +1 -0
- package/dist/tips.d.ts +44 -0
- package/dist/tips.js +355 -0
- package/dist/tips.js.map +1 -0
- package/dist/tmux-manager.d.ts +16 -0
- package/dist/tmux-manager.js +110 -25
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-progress.d.ts +40 -0
- package/dist/tool-progress.js +289 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/topic-commands.d.ts +42 -4
- package/dist/topic-commands.js +354 -66
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +15 -2
- package/dist/transcript-monitor.js +63 -17
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +131 -0
- package/dist/transcript-sources.js +580 -0
- package/dist/transcript-sources.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/ui/dashboard.html +55 -32
- package/dist/ui/settings.html +200 -60
- package/dist/ui/view.html +147 -30
- package/dist/usage/format-rich.js +24 -23
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +7 -0
- package/dist/usage/i18n-keys.js +34 -0
- package/dist/usage/i18n-keys.js.map +1 -0
- package/dist/usage/i18n.d.ts +5 -0
- package/dist/usage/i18n.js +27 -0
- package/dist/usage/i18n.js.map +1 -0
- package/dist/usage/providers.d.ts +19 -0
- package/dist/usage/providers.js +150 -74
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +8 -2
- package/dist/usage/usage-api.js +45 -23
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/workflow-templates/default.md +2 -1
- package/package.json +2 -2
package/dist/usage/providers.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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)", "
|
|
385
|
-
"
|
|
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:
|
|
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:
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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: `${
|
|
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
|
}
|