@songsid/agend 2.1.4-beta.9 → 2.1.4
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-cli-instructions.md +2 -2
- package/dist/agent-endpoint.js +1 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.d.ts +12 -8
- package/dist/backend/antigravity.js +189 -42
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +87 -23
- package/dist/backend/claude-code.js +361 -46
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +139 -5
- 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 +52 -2
- package/dist/backend/kiro.js +267 -12
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode.js +17 -4
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +122 -3
- package/dist/backend/types.js +5 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-outage.d.ts +61 -0
- package/dist/backend-outage.js +71 -0
- package/dist/backend-outage.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +44 -2
- package/dist/channel/adapters/discord.js +523 -90
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +30 -0
- package/dist/channel/adapters/telegram.js +221 -19
- 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/markdown-chunk.d.ts +42 -0
- package/dist/channel/markdown-chunk.js +205 -0
- package/dist/channel/markdown-chunk.js.map +1 -0
- package/dist/channel/mcp-server.js +34 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.js +64 -8
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/types.d.ts +31 -1
- package/dist/classic-channel-manager.d.ts +65 -0
- package/dist/classic-channel-manager.js +172 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +124 -83
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +13 -0
- 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 +491 -14
- package/dist/daemon.js +2121 -276
- 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 +63 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-manager.d.ts +402 -11
- package/dist/fleet-manager.js +2594 -422
- 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/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
- package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
- package/dist/instance-lifecycle.d.ts +114 -1
- package/dist/instance-lifecycle.js +404 -14
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +42 -4
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +1024 -320
- 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 +16 -1
- package/dist/outbound-handlers.js +174 -40
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +10 -3
- package/dist/outbound-schemas.js +10 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-input-residue.d.ts +52 -0
- package/dist/pane-input-residue.js +107 -0
- package/dist/pane-input-residue.js.map +1 -0
- package/dist/process-memory.d.ts +6 -0
- package/dist/process-memory.js +45 -0
- package/dist/process-memory.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/steer-capability.d.ts +1 -0
- package/dist/steer-capability.js +6 -0
- package/dist/steer-capability.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/topic-commands.d.ts +31 -7
- package/dist/topic-commands.js +342 -70
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +35 -10
- package/dist/transcript-sources.js +190 -38
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +6 -1
- 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.d.ts +1 -1
- package/dist/usage/format-rich.js +28 -24
- 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 +21 -0
- package/dist/usage/providers.js +153 -75
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +11 -3
- package/dist/usage/usage-api.js +61 -24
- package/dist/usage/usage-api.js.map +1 -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,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
|
-
|
|
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"
|
|
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
|
-
|
|
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)", "
|
|
385
|
-
"
|
|
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:
|
|
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:
|
|
660
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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: `${
|
|
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
|
}
|