oc-go-usage-display 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +249 -68
  2. package/bin/lib.js +433 -29
  3. package/bin/oc-go-usage-display-init.js +42 -13
  4. package/bin/oc-go-usage-display-remove.js +38 -0
  5. package/bin/oc-go-usage-display-show.js +47 -39
  6. package/bin/oc-go-usage-display-status.js +33 -42
  7. package/bin/oc-go-usage-display-update.js +27 -14
  8. package/dist/helpers.d.ts +116 -0
  9. package/dist/helpers.d.ts.map +1 -0
  10. package/dist/helpers.js +568 -0
  11. package/dist/helpers.js.map +1 -0
  12. package/dist/index.d.ts +5 -9
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +258 -278
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugins/oc-go-usage-display.kilo.ts +678 -0
  17. package/dist/plugins/oc-go-usage-display.kilo.tsx +1272 -0
  18. package/dist/plugins/oc-go-usage-display.ts +678 -0
  19. package/dist/plugins/oc-go-usage-display.tsx +1018 -0
  20. package/dist/shared.d.ts +96 -0
  21. package/dist/shared.d.ts.map +1 -0
  22. package/dist/shared.js +575 -0
  23. package/dist/shared.js.map +1 -0
  24. package/dist/tui-shared.d.ts +109 -0
  25. package/dist/tui-shared.d.ts.map +1 -0
  26. package/dist/tui-shared.js +372 -0
  27. package/dist/tui-shared.js.map +1 -0
  28. package/dist/tui.d.ts.map +1 -1
  29. package/dist/tui.js +226 -377
  30. package/dist/tui.js.map +1 -1
  31. package/dist/tui.kilo.d.ts +8 -0
  32. package/dist/tui.kilo.d.ts.map +1 -0
  33. package/dist/tui.kilo.js +502 -0
  34. package/dist/tui.kilo.js.map +1 -0
  35. package/package.json +47 -13
  36. package/src/helpers.ts +733 -0
  37. package/src/index.ts +331 -304
  38. package/src/shared.ts +696 -0
  39. package/src/tui-shared.tsx +705 -0
  40. package/src/tui.kilo.tsx +816 -0
  41. package/src/tui.tsx +356 -465
@@ -0,0 +1,678 @@
1
+ // oc-go-usage-display: kilo server plugin bundle (self-contained)
2
+
3
+ // src/index.ts
4
+ import { tool } from "@kilocode/plugin";
5
+ import * as fs3 from "node:fs";
6
+ import * as path2 from "node:path";
7
+
8
+ // src/helpers.ts
9
+ import * as fs2 from "node:fs";
10
+
11
+ // src/shared.ts
12
+ import * as fs from "node:fs";
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ function resolveHomedir() {
16
+ const envHome = process.env.HOME;
17
+ if (envHome && envHome.length > 0) return envHome;
18
+ return os.homedir();
19
+ }
20
+ function resolveUsageHost() {
21
+ return true ? "kilo" : "opencode";
22
+ }
23
+ var ENV_PREFIX = {
24
+ opencode: "OPENCODE_OC_GO_",
25
+ kilo: "KILO_OC_GO_"
26
+ };
27
+ function hostEnvName(host, suffix) {
28
+ return `${ENV_PREFIX[host]}${suffix}`;
29
+ }
30
+ function hostEnv(host, suffix, env = process.env) {
31
+ return env?.[hostEnvName(host, suffix)];
32
+ }
33
+ function homedirOrEmpty(homedir2) {
34
+ try {
35
+ return homedir2();
36
+ } catch {
37
+ return "";
38
+ }
39
+ }
40
+ function resolveHostRoots(host, env = process.env, homedir2 = resolveHomedir) {
41
+ const home = homedirOrEmpty(homedir2);
42
+ const configRoot = toNonEmptyString(env.XDG_CONFIG_HOME) ?? safeJoinPath(home, ".config");
43
+ const dataRoot = toNonEmptyString(env.XDG_DATA_HOME) ?? safeJoinPath(home, ".local", "share");
44
+ if (host === "kilo") {
45
+ return {
46
+ configDir: toNonEmptyString(env.KILO_CONFIG_DIR) ?? safeJoinPath(configRoot, "kilo"),
47
+ dataDir: safeJoinPath(dataRoot, "kilo")
48
+ };
49
+ }
50
+ return {
51
+ configDir: safeJoinPath(configRoot, "opencode"),
52
+ dataDir: safeJoinPath(dataRoot, "opencode")
53
+ };
54
+ }
55
+ var CONFIG_DIR = resolveHostRoots("opencode").configDir;
56
+ function toPathSegment(value) {
57
+ if (typeof value === "string") return value;
58
+ try {
59
+ return String(value);
60
+ } catch {
61
+ return "";
62
+ }
63
+ }
64
+ function safeJoinPath(base, ...segments) {
65
+ return path.join(toPathSegment(base), ...segments.map(toPathSegment));
66
+ }
67
+ function authJsonPaths(host = "opencode", env = process.env, homedir2 = resolveHomedir) {
68
+ const roots = resolveHostRoots(host, env, homedir2);
69
+ return [safeJoinPath(roots.dataDir, "auth.json"), safeJoinPath(roots.configDir, "auth.json")];
70
+ }
71
+ function isRecord(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value);
73
+ }
74
+ function toFiniteNumber(value) {
75
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
76
+ return value;
77
+ }
78
+ function toNonEmptyString(value) {
79
+ if (typeof value !== "string") return null;
80
+ const trimmed = value.trim();
81
+ return trimmed.length > 0 ? trimmed : null;
82
+ }
83
+ function errorMessage(error) {
84
+ if (error === null || error === void 0) return "unknown error";
85
+ if (error instanceof Error) {
86
+ return toNonEmptyString(error.message) ?? toNonEmptyString(error.name) ?? "unknown error";
87
+ }
88
+ try {
89
+ return toNonEmptyString(String(error)) ?? "unknown error";
90
+ } catch {
91
+ return "unknown error";
92
+ }
93
+ }
94
+ var SECONDS_PER_MINUTE = 60;
95
+ var SECONDS_PER_HOUR = 3600;
96
+ var SECONDS_PER_DAY = 86400;
97
+ var SECONDS_PER_WEEK = 604800;
98
+ function formatResetDuration(totalSec) {
99
+ if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
100
+ const sec = Math.floor(totalSec);
101
+ if (sec >= SECONDS_PER_WEEK) {
102
+ const weeks = Math.floor(sec / SECONDS_PER_WEEK);
103
+ const days = Math.floor(sec % SECONDS_PER_WEEK / SECONDS_PER_DAY);
104
+ return `${weeks}w ${days}d`;
105
+ }
106
+ if (sec >= SECONDS_PER_DAY) {
107
+ const days = Math.floor(sec / SECONDS_PER_DAY);
108
+ const hours2 = Math.floor(sec % SECONDS_PER_DAY / SECONDS_PER_HOUR);
109
+ return `${days}d ${hours2}h`;
110
+ }
111
+ const hours = Math.floor(sec / SECONDS_PER_HOUR);
112
+ const minutes = Math.floor(sec % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
113
+ if (hours > 0) return `${hours}h${minutes}m`;
114
+ if (minutes > 0) return `${minutes}m`;
115
+ return `${sec}s`;
116
+ }
117
+ function readAuthJsonApiKey(host = "opencode", env = process.env, homedir2 = resolveHomedir) {
118
+ for (const authPath of authJsonPaths(host, env, homedir2)) {
119
+ let raw;
120
+ try {
121
+ raw = fs.readFileSync(authPath, "utf8");
122
+ } catch {
123
+ continue;
124
+ }
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(raw);
128
+ } catch {
129
+ continue;
130
+ }
131
+ if (!isRecord(parsed)) continue;
132
+ const goEntry = parsed["opencode-go"];
133
+ const fallbackEntry = parsed["opencode"];
134
+ const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
135
+ if (goKey) return goKey;
136
+ const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
137
+ if (fallbackKey) return fallbackKey;
138
+ }
139
+ return null;
140
+ }
141
+ function usableCountdown(seconds) {
142
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return null;
143
+ return seconds;
144
+ }
145
+ function secondsUntilReset(resetsAt, now) {
146
+ const instant = toNonEmptyString(resetsAt);
147
+ if (instant === null) return null;
148
+ return usableCountdown(Math.round((Date.parse(instant) - now) / 1e3));
149
+ }
150
+ var LIMITED_STATUSES = /* @__PURE__ */ new Set(["rate-limited", "limited", "exhausted", "capped"]);
151
+ function isLimitedStatus(status) {
152
+ if (status === null) return false;
153
+ const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, "-");
154
+ return LIMITED_STATUSES.has(normalized);
155
+ }
156
+ function extractWindow(candidate, now = Date.now()) {
157
+ if (!isRecord(candidate)) return null;
158
+ const percent = toFiniteNumber(
159
+ candidate.percent ?? candidate.usagePercent ?? candidate.usedPercent ?? candidate.value ?? candidate.usage
160
+ );
161
+ if (percent === null) return null;
162
+ const status = toNonEmptyString(candidate.status);
163
+ const resetInSec = usableCountdown(candidate.resetInSec ?? candidate.resetInSeconds) ?? secondsUntilReset(candidate.resetsAt, now);
164
+ const resetText = toNonEmptyString(candidate.resetText ?? candidate.reset ?? null);
165
+ return { percent: Math.round(percent), status, limited: isLimitedStatus(status), resetInSec, resetText };
166
+ }
167
+ function extractSnapshotFromApiPayload(payload, now = Date.now()) {
168
+ if (!isRecord(payload)) return null;
169
+ const containers = [payload];
170
+ for (const key of ["usage", "data", "go"]) {
171
+ if (isRecord(payload[key])) containers.push(payload[key]);
172
+ }
173
+ for (const container of containers) {
174
+ if (!isRecord(container)) continue;
175
+ const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"], now);
176
+ const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"], now);
177
+ const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"], now);
178
+ if (rolling !== null || weekly !== null || monthly !== null) {
179
+ return {
180
+ rolling,
181
+ weekly,
182
+ monthly,
183
+ source: "api",
184
+ fetchedAt: now
185
+ };
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ function mockSnapshot() {
191
+ return {
192
+ rolling: { percent: 42, status: "active", limited: false, resetInSec: 7543, resetText: null },
193
+ weekly: { percent: 15, status: "active", limited: false, resetInSec: null, resetText: null },
194
+ monthly: { percent: 61, status: "active", limited: false, resetInSec: null, resetText: null },
195
+ source: "mock",
196
+ fetchedAt: Date.now()
197
+ };
198
+ }
199
+ function unavailableSnapshot(error) {
200
+ return {
201
+ rolling: null,
202
+ weekly: null,
203
+ monthly: null,
204
+ source: "unavailable",
205
+ fetchedAt: Date.now(),
206
+ apiUnavailable: true,
207
+ apiError: error
208
+ };
209
+ }
210
+
211
+ // src/helpers.ts
212
+ function formatWindow(window) {
213
+ if (!window) return "n/a";
214
+ return `${window.percent}%`;
215
+ }
216
+ function formatServerLine(snapshot) {
217
+ if (snapshot.apiUnavailable || !snapshot.rolling && !snapshot.weekly && !snapshot.monthly) {
218
+ const reason = snapshot.apiError ?? "unknown error";
219
+ return `Go n/a (${reason})`;
220
+ }
221
+ const rollingReset = formatResetDuration(snapshot.rolling?.resetInSec ?? null) ?? snapshot.rolling?.resetText ?? null;
222
+ const rollingText = snapshot.rolling === null ? "5h n/a" : `5h ${snapshot.rolling.percent}%${rollingReset ? ` (reset ${rollingReset})` : ""}`;
223
+ return `Go ${rollingText} | 7d ${formatWindow(snapshot.weekly)} | 30d ${formatWindow(snapshot.monthly)}`;
224
+ }
225
+ function fileConfigPath(host = "opencode") {
226
+ return safeJoinPath(resolveHostRoots(host).configDir, "oc-go-usage-display.json");
227
+ }
228
+ function readFileConfig(host = "opencode") {
229
+ let raw;
230
+ try {
231
+ raw = fs2.readFileSync(fileConfigPath(host), "utf8");
232
+ } catch {
233
+ return { workspaceId: null, authCookie: null };
234
+ }
235
+ let parsed;
236
+ try {
237
+ parsed = JSON.parse(raw);
238
+ } catch {
239
+ return { workspaceId: null, authCookie: null };
240
+ }
241
+ if (!isRecord(parsed)) return { workspaceId: null, authCookie: null };
242
+ return {
243
+ workspaceId: toNonEmptyString(parsed.workspaceId),
244
+ authCookie: toNonEmptyString(parsed.authCookie)
245
+ };
246
+ }
247
+ function isMalformedAuthCookie(cookie) {
248
+ return /[\r\n;,\t\0"]/.test(cookie);
249
+ }
250
+ function hasMalformedAuthCookie(fileConfig = readFileConfig(), host = resolveUsageHost()) {
251
+ const raw = toNonEmptyString(hostEnv(host, "AUTH_COOKIE")) ?? fileConfig.authCookie;
252
+ if (!raw) return false;
253
+ return isMalformedAuthCookie(raw);
254
+ }
255
+ var ALLOWED_REDIRECT_HOSTS = /* @__PURE__ */ new Set(["opencode.ai", "auth.opencode.ai"]);
256
+ var MAX_REDIRECT_HOPS = 3;
257
+ function isAllowedHttpsUrl(url) {
258
+ return url.protocol === "https:" && url.username === "" && url.password === "" && url.port === "" && ALLOWED_REDIRECT_HOSTS.has(url.hostname);
259
+ }
260
+ function resolveRedirectUrl(fromUrl, location) {
261
+ let base;
262
+ try {
263
+ base = new URL(fromUrl);
264
+ } catch {
265
+ return null;
266
+ }
267
+ if (!isAllowedHttpsUrl(base)) return null;
268
+ let next;
269
+ try {
270
+ next = new URL(location, base);
271
+ } catch {
272
+ return null;
273
+ }
274
+ return isAllowedHttpsUrl(next) ? next : null;
275
+ }
276
+ function redirectTargetHost(fromUrl, location) {
277
+ try {
278
+ const host = new URL(location, fromUrl).host;
279
+ return host.length > 0 ? host : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+ function resolveAllowedRedirect(fromUrl, location, hopsFollowed) {
285
+ if (hopsFollowed >= MAX_REDIRECT_HOPS) {
286
+ return { follow: false, reason: `too many redirects (limit ${MAX_REDIRECT_HOPS})` };
287
+ }
288
+ if (location === null || location.trim().length === 0) {
289
+ return { follow: false, reason: "redirect without a Location header" };
290
+ }
291
+ const next = resolveRedirectUrl(fromUrl, location);
292
+ if (next === null) {
293
+ const host = redirectTargetHost(fromUrl, location);
294
+ return {
295
+ follow: false,
296
+ reason: `redirect blocked (target not allowed${host ? `: ${host}` : ""})`
297
+ };
298
+ }
299
+ return { follow: true, url: next.toString() };
300
+ }
301
+ var COUNT_FORMAT = new Intl.NumberFormat("en-US");
302
+ var CURRENCY_FORMAT = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
303
+
304
+ // src/index.ts
305
+ var API_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
306
+ var CACHE_TTL_MS = 6e4;
307
+ var FETCH_TIMEOUT_MS = 1e4;
308
+ var WINDOW_KEYS = ["rolling", "weekly", "monthly"];
309
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
310
+ var HOST = resolveUsageHost();
311
+ var DISK_CACHE_PATH = safeJoinPath(
312
+ resolveHostRoots(HOST).configDir,
313
+ "oc-go-usage-display-cache.json"
314
+ );
315
+ function resolveCredentials(fileConfig = readFileConfig(HOST)) {
316
+ if (hostEnv(HOST, "MOCK") === "1") return { kind: "mock" };
317
+ const apiKey = toNonEmptyString(hostEnv(HOST, "API_KEY"));
318
+ if (apiKey) return { kind: "apiKey", apiKey };
319
+ const authJsonKey = readAuthJsonApiKey(HOST);
320
+ if (authJsonKey) return { kind: "apiKey", apiKey: authJsonKey };
321
+ const workspaceId = toNonEmptyString(hostEnv(HOST, "WORKSPACE_ID")) ?? fileConfig.workspaceId;
322
+ const authCookie = toNonEmptyString(hostEnv(HOST, "AUTH_COOKIE")) ?? fileConfig.authCookie;
323
+ if (workspaceId && authCookie) {
324
+ if (isMalformedAuthCookie(authCookie)) return { kind: "none" };
325
+ return { kind: "cookie", workspaceId, authCookie };
326
+ }
327
+ return { kind: "none" };
328
+ }
329
+ var memoryCache = null;
330
+ function isFresh(at, now) {
331
+ return at <= now && now - at < CACHE_TTL_MS;
332
+ }
333
+ function readDiskCache(now) {
334
+ let raw;
335
+ try {
336
+ raw = fs3.readFileSync(DISK_CACHE_PATH, "utf8");
337
+ } catch {
338
+ return null;
339
+ }
340
+ let parsed;
341
+ try {
342
+ parsed = JSON.parse(raw);
343
+ } catch {
344
+ return null;
345
+ }
346
+ if (!isRecord(parsed)) return null;
347
+ const at = toFiniteNumber(parsed.at);
348
+ if (at === null || !isFresh(at, now)) return null;
349
+ if (!isRecord(parsed.snapshot)) return null;
350
+ const snapshot = parsed.snapshot;
351
+ if (!("rolling" in snapshot && "weekly" in snapshot && "monthly" in snapshot)) return null;
352
+ const windows = {
353
+ rolling: null,
354
+ weekly: null,
355
+ monthly: null
356
+ };
357
+ for (const key of WINDOW_KEYS) {
358
+ const cachedWindow = snapshot[key];
359
+ if (cachedWindow === null) continue;
360
+ const window = extractWindow(cachedWindow);
361
+ if (window === null) return null;
362
+ windows[key] = window;
363
+ }
364
+ return { ...snapshot, ...windows };
365
+ }
366
+ function writeDiskCache(snapshot) {
367
+ try {
368
+ fs3.mkdirSync(path2.dirname(DISK_CACHE_PATH), { recursive: true });
369
+ fs3.writeFileSync(
370
+ DISK_CACHE_PATH,
371
+ JSON.stringify({ at: Date.now(), snapshot }),
372
+ "utf8"
373
+ );
374
+ } catch {
375
+ }
376
+ }
377
+ async function fetchWithTimeout(url, init, read) {
378
+ const controller = new AbortController();
379
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
380
+ try {
381
+ const response = await fetch(url, { ...init, signal: controller.signal });
382
+ return await read(response, controller.signal);
383
+ } finally {
384
+ clearTimeout(timer);
385
+ }
386
+ }
387
+ async function fetchViaApiKey(apiKey) {
388
+ try {
389
+ return await fetchWithTimeout(
390
+ API_USAGE_URL,
391
+ {
392
+ headers: {
393
+ Authorization: `Bearer ${apiKey}`,
394
+ Accept: "application/json"
395
+ }
396
+ },
397
+ async (response, signal) => {
398
+ if (response.status === 401 || response.status === 403) {
399
+ return unavailableSnapshot("API key rejected (401/403)");
400
+ }
401
+ if (!response.ok) {
402
+ return unavailableSnapshot(`upstream returned HTTP ${response.status}`);
403
+ }
404
+ let payload;
405
+ try {
406
+ payload = await response.json();
407
+ } catch {
408
+ return unavailableSnapshot(
409
+ signal.aborted ? "request failed" : "unexpected API response shape"
410
+ );
411
+ }
412
+ return extractSnapshotFromApiPayload(payload) ?? unavailableSnapshot("unexpected API response shape");
413
+ }
414
+ );
415
+ } catch {
416
+ return unavailableSnapshot("request failed");
417
+ }
418
+ }
419
+ var USAGE_KEYS = {
420
+ rolling: "rollingUsage",
421
+ weekly: "weeklyUsage",
422
+ monthly: "monthlyUsage"
423
+ };
424
+ var DOM_ORDER = ["rolling", "weekly", "monthly"];
425
+ var LOGIN_TITLE_MARKER = "<title>OpenAuth</title>";
426
+ function isLoginPage(finalUrl, html) {
427
+ let host = "";
428
+ let pathname = "";
429
+ try {
430
+ const parsed = new URL(finalUrl);
431
+ host = parsed.hostname;
432
+ pathname = parsed.pathname;
433
+ } catch {
434
+ }
435
+ if (host === "auth.opencode.ai") return true;
436
+ if (host === "opencode.ai" && pathname.startsWith("/auth")) return true;
437
+ return html.includes(LOGIN_TITLE_MARKER);
438
+ }
439
+ function isNoSubscription(html) {
440
+ if (!html.includes('data-slot="subscribe-button"')) return false;
441
+ return /lite\s*:\s*null/.test(html) && /liteSubscriptionID\s*:\s*null/.test(html);
442
+ }
443
+ function escapeRegex(text) {
444
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
445
+ }
446
+ function extractUsageBlock(html, key) {
447
+ const pattern = new RegExp(`${escapeRegex(key)}\\s*:\\s*(?:\\$R\\[\\d+\\]\\s*=\\s*)?\\{`);
448
+ const match = pattern.exec(html);
449
+ if (!match || match.index === void 0) return null;
450
+ const braceStart = match.index + match[0].length - 1;
451
+ let depth = 0;
452
+ for (let i = braceStart; i < html.length; i++) {
453
+ if (html[i] === "{") depth += 1;
454
+ else if (html[i] === "}") {
455
+ depth -= 1;
456
+ if (depth === 0) return html.slice(braceStart, i + 1);
457
+ }
458
+ }
459
+ return null;
460
+ }
461
+ function parseIntField(block, field) {
462
+ const match = new RegExp(`${escapeRegex(field)}\\s*:\\s*(-?\\d+)`).exec(block);
463
+ const digits = match?.[1];
464
+ return digits === void 0 ? null : Number.parseInt(digits, 10);
465
+ }
466
+ function parseStrField(block, field) {
467
+ const match = new RegExp(`${escapeRegex(field)}\\s*:\\s*"([^"]*)"`).exec(block);
468
+ return match?.[1] ?? null;
469
+ }
470
+ function parseInlineUsage(html) {
471
+ const result = {};
472
+ for (const [name, key] of Object.entries(USAGE_KEYS)) {
473
+ const window = name;
474
+ const block = extractUsageBlock(html, key);
475
+ if (!block) continue;
476
+ const percent = parseIntField(block, "usagePercent");
477
+ if (percent === null) continue;
478
+ const status = parseStrField(block, "status");
479
+ result[window] = {
480
+ percent,
481
+ status,
482
+ limited: isLimitedStatus(status),
483
+ resetInSec: parseIntField(block, "resetInSec"),
484
+ resetText: null
485
+ };
486
+ }
487
+ return result;
488
+ }
489
+ function cleanResetText(raw) {
490
+ const cleaned = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/\s+/g, " ").trim();
491
+ return cleaned || null;
492
+ }
493
+ function parseDomUsage(html) {
494
+ const result = {};
495
+ const itemStarts = [];
496
+ for (const match of html.matchAll(/data-slot="usage-item"/g)) {
497
+ if (match.index !== void 0) itemStarts.push(match.index);
498
+ }
499
+ for (let idx = 0; idx < DOM_ORDER.length; idx++) {
500
+ const window = DOM_ORDER[idx];
501
+ const start = itemStarts[idx];
502
+ if (window === void 0 || start === void 0) break;
503
+ const nextStart = itemStarts[idx + 1];
504
+ const segment = html.slice(start, nextStart !== void 0 ? nextStart : start + 800);
505
+ const valueMatch = /data-slot="usage-value">\s*(?:<!--[\s\S]*?-->)?\s*(\d+)/s.exec(segment) ?? /width:\s*(\d+)%/.exec(segment);
506
+ const percent = valueMatch?.[1];
507
+ if (percent === void 0) continue;
508
+ const resetMatch = /data-slot="reset-time">\s*([\s\S]*?)<\/span>/.exec(segment);
509
+ const resetText = resetMatch?.[1];
510
+ result[window] = {
511
+ percent: Number.parseInt(percent, 10),
512
+ status: null,
513
+ limited: false,
514
+ resetInSec: null,
515
+ resetText: resetText !== void 0 ? cleanResetText(resetText) : null
516
+ };
517
+ }
518
+ return result;
519
+ }
520
+ function parseScrapedUsage(html) {
521
+ const inline = parseInlineUsage(html);
522
+ if (Object.keys(inline).length === 3) return inline;
523
+ return { ...parseDomUsage(html), ...inline };
524
+ }
525
+ async function fetchViaCookie(workspaceId, authCookie) {
526
+ const workspaceUrl = `https://opencode.ai/workspace/${encodeURIComponent(workspaceId)}/go`;
527
+ let currentUrl = workspaceUrl;
528
+ let hopsFollowed = 0;
529
+ for (; ; ) {
530
+ let hop;
531
+ try {
532
+ hop = await fetchWithTimeout(
533
+ currentUrl,
534
+ {
535
+ redirect: "manual",
536
+ headers: {
537
+ Cookie: `auth=${authCookie}`,
538
+ "User-Agent": "oc-go-usage-display-plugin",
539
+ Accept: "text/html,application/xhtml+xml"
540
+ }
541
+ },
542
+ async (response) => {
543
+ if (REDIRECT_STATUSES.has(response.status)) {
544
+ return { kind: "redirect", location: response.headers.get("location") };
545
+ }
546
+ return { kind: "body", status: response.status, html: await response.text() };
547
+ }
548
+ );
549
+ } catch {
550
+ return unavailableSnapshot("request failed");
551
+ }
552
+ if (hop.kind === "redirect") {
553
+ const decision = resolveAllowedRedirect(currentUrl, hop.location, hopsFollowed);
554
+ if (!decision.follow) return unavailableSnapshot(decision.reason);
555
+ currentUrl = decision.url;
556
+ hopsFollowed += 1;
557
+ continue;
558
+ }
559
+ const { status, html } = hop;
560
+ if (isLoginPage(currentUrl, html)) {
561
+ return unavailableSnapshot("login expired (refresh auth cookie)");
562
+ }
563
+ if (status !== 200) {
564
+ return unavailableSnapshot(`upstream returned HTTP ${status}`);
565
+ }
566
+ const usages = parseScrapedUsage(html);
567
+ if (Object.keys(usages).length === 0) {
568
+ if (isNoSubscription(html)) return unavailableSnapshot("no OpenCode Go subscription");
569
+ return unavailableSnapshot("usage markup not recognized");
570
+ }
571
+ return {
572
+ rolling: usages.rolling ?? null,
573
+ weekly: usages.weekly ?? null,
574
+ monthly: usages.monthly ?? null,
575
+ source: "scrape",
576
+ fetchedAt: Date.now()
577
+ };
578
+ }
579
+ }
580
+ async function getUsageSnapshot() {
581
+ const now = Date.now();
582
+ if (hostEnv(HOST, "MOCK") === "1") {
583
+ return mockSnapshot();
584
+ }
585
+ if (memoryCache && isFresh(memoryCache.at, now)) return memoryCache.snapshot;
586
+ const diskCached = readDiskCache(now);
587
+ if (diskCached) {
588
+ memoryCache = { at: now, snapshot: diskCached };
589
+ return diskCached;
590
+ }
591
+ const fileConfig = readFileConfig(HOST);
592
+ const credentials = resolveCredentials(fileConfig);
593
+ if (credentials.kind === "mock") {
594
+ return mockSnapshot();
595
+ }
596
+ if (credentials.kind === "none") {
597
+ const workspaceId = toNonEmptyString(hostEnv(HOST, "WORKSPACE_ID")) ?? fileConfig.workspaceId;
598
+ if (workspaceId && hasMalformedAuthCookie(fileConfig)) {
599
+ return unavailableSnapshot("not configured (malformed auth cookie)");
600
+ }
601
+ return unavailableSnapshot(`not configured (set ${hostEnvName(HOST, "API_KEY")})`);
602
+ }
603
+ const snapshot = credentials.kind === "apiKey" ? await fetchViaApiKey(credentials.apiKey) : await fetchViaCookie(credentials.workspaceId, credentials.authCookie);
604
+ if (!snapshot.apiUnavailable) {
605
+ memoryCache = { at: now, snapshot };
606
+ writeDiskCache(snapshot);
607
+ }
608
+ return snapshot;
609
+ }
610
+ function sanitizeHooks(hooks) {
611
+ if (!isRecord(hooks)) return {};
612
+ const clean = {};
613
+ for (const [key, value] of Object.entries(hooks)) {
614
+ if (value === null || value === void 0) continue;
615
+ if (key === "tool" && isRecord(value)) {
616
+ const tools = {};
617
+ for (const [name, definition] of Object.entries(value)) {
618
+ if (definition === null || definition === void 0) continue;
619
+ tools[name] = definition;
620
+ }
621
+ clean[key] = tools;
622
+ continue;
623
+ }
624
+ clean[key] = value;
625
+ }
626
+ return clean;
627
+ }
628
+ async function logServerError(input, message) {
629
+ try {
630
+ const client = input?.client;
631
+ await client?.app?.log?.({
632
+ service: "oc-go-usage-display",
633
+ level: "error",
634
+ message
635
+ });
636
+ } catch {
637
+ }
638
+ }
639
+ var server = async (input, _options) => {
640
+ try {
641
+ return sanitizeHooks({
642
+ tool: {
643
+ go_usage: tool({
644
+ description: "Show OpenCode Go subscription usage: rolling 5h, weekly, and monthly windows. Takes no arguments.",
645
+ args: {},
646
+ execute: async () => {
647
+ try {
648
+ const snapshot = await getUsageSnapshot().catch(
649
+ () => unavailableSnapshot("request failed")
650
+ );
651
+ const line = formatServerLine(snapshot);
652
+ return `${line}
653
+ ${JSON.stringify(snapshot, null, 2)}`;
654
+ } catch (error) {
655
+ const snapshot = unavailableSnapshot("request failed");
656
+ void logServerError(
657
+ input,
658
+ `go_usage tool execution failed: ${errorMessage(error)}`
659
+ );
660
+ return `${formatServerLine(snapshot)}
661
+ ${JSON.stringify(snapshot, null, 2)}`;
662
+ }
663
+ }
664
+ })
665
+ }
666
+ });
667
+ } catch (error) {
668
+ void logServerError(
669
+ input,
670
+ `Go usage plugin failed to initialize: ${errorMessage(error)}; continuing without hooks`
671
+ );
672
+ return {};
673
+ }
674
+ };
675
+ var index_default = { id: "oc-go-usage-display", server };
676
+ export {
677
+ index_default as default
678
+ };