summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/credentials/opencode.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/credentials/opencode.ts')
-rw-r--r--packages/core/src/credentials/opencode.ts129
1 files changed, 129 insertions, 0 deletions
diff --git a/packages/core/src/credentials/opencode.ts b/packages/core/src/credentials/opencode.ts
new file mode 100644
index 0000000..7a74486
--- /dev/null
+++ b/packages/core/src/credentials/opencode.ts
@@ -0,0 +1,129 @@
+// ─── OpenCode Usage Tracking ──────────────────────────────────
+// OpenCode has no public usage API. We scrape usage from the
+// SolidStart SSR-rendered workspace page using a session cookie.
+// Requires OPENCODE_COOKIE env var.
+// Workspace IDs: OPENCODE_WS1_ID for opencode-1, OPENCODE_WS2_ID for opencode-2.
+
+export interface OpencodeUsageBucket {
+ utilization?: number; // 0-1 fraction
+ resetsAt?: number; // Unix timestamp ms
+}
+
+export interface OpencodeUsageReport {
+ fiveHour?: OpencodeUsageBucket;
+ weekly?: OpencodeUsageBucket;
+ monthly?: OpencodeUsageBucket;
+}
+
+function getWorkspaceId(keyId: string): string | undefined {
+ // Match ai-usage convention: opencode-1 → OPENCODE_WS1_ID, opencode-2 → OPENCODE_WS2_ID
+ const match = keyId.match(/opencode-(\d+)$/i);
+ if (match) {
+ const num = match[1];
+ const specific = process.env[`OPENCODE_WS${num}_ID`];
+ if (specific) return specific;
+ }
+ return process.env.OPENCODE_WS_ID;
+}
+
+function parseOcDouble(html: string, key: string): number {
+ const idx = html.indexOf(`${key}:`);
+ if (idx === -1) return 0;
+ let start = idx + key.length + 1;
+ while (start < html.length && html[start] === " ") start++;
+ let end = start;
+ while (end < html.length && html[end] !== "," && html[end] !== "}") {
+ end++;
+ }
+ const val = parseFloat(html.slice(start, end));
+ return Number.isNaN(val) ? 0 : val;
+}
+
+function parseOcInt(html: string, key: string): number {
+ const idx = html.indexOf(`${key}:`);
+ if (idx === -1) return 0;
+ let i = idx + key.length + 1;
+ while (i < html.length && html[i] === " ") i++;
+ return parseInt(html.slice(i), 10) || 0;
+}
+
+function parseOcBucket(
+ html: string,
+ bucketName: string,
+): { utilization: number; resetsAt: number } | null {
+ const search = `${bucketName}:`;
+ const pos = html.indexOf(search);
+ if (pos === -1) return null;
+
+ // Find the opening brace after the bucket name
+ const brace = html.indexOf("{", pos);
+ if (brace === -1) return null;
+
+ const resetSecs = parseOcInt(html.slice(brace), "resetInSec");
+ const usagePct = parseOcDouble(html.slice(brace), "usagePercent");
+
+ const utilization = usagePct / 100; // convert 0-100% to 0-1 fraction
+ const resetsAt = Date.now() + resetSecs * 1000;
+
+ return { utilization, resetsAt };
+}
+
+export async function fetchOpencodeUsage(
+ keyId: string,
+): Promise<OpencodeUsageReport | null> {
+ const cookie = process.env.OPENCODE_COOKIE;
+ const wsId = getWorkspaceId(keyId);
+
+ if (!cookie || !wsId) {
+ return null;
+ }
+
+ const url = `https://opencode.ai/workspace/${encodeURIComponent(wsId)}/go`;
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ accept: "text/html",
+ cookie: `auth=${cookie}`,
+ },
+ redirect: "follow",
+ });
+
+ if (!response.ok) return null;
+
+ const html = await response.text();
+
+ // Auth redirect check
+ if (
+ html.includes("/auth/authorize") ||
+ html.includes('window.location="/auth/authorize"')
+ ) {
+ return null;
+ }
+
+ // Find the lite.subscription data block.
+ // HTML contains: lite.subscription.get[\"<wsId>\"]
+ // We need literal backslashes; use \x5c (hex for backslash).
+ const wsKey = `lite.subscription.get[\x5c"${wsId}\x5c"]`;
+ const wsPos = html.indexOf(wsKey);
+ if (wsPos === -1) return null;
+
+ // Search for the resolved data starting from the ws key position
+ const minePos = html.indexOf("mine:", wsPos);
+ const slice = minePos !== -1 ? html.slice(minePos) : "";
+
+ const fiveHour = parseOcBucket(slice, "rollingUsage");
+ const weekly = parseOcBucket(slice, "weeklyUsage");
+ const monthly = parseOcBucket(slice, "monthlyUsage");
+
+ if (!fiveHour && !weekly && !monthly) return null;
+
+ return {
+ fiveHour: fiveHour ?? undefined,
+ weekly: weekly ?? undefined,
+ monthly: monthly ?? undefined,
+ };
+ } catch {
+ return null;
+ }
+}