pi-opencode-go-usage 1.0.1 → 1.0.2

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.
@@ -25,43 +25,43 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
25
  type MeterKind = "five_hour" | "calendar_week" | "product_period";
26
26
 
27
27
  interface UsageMeter {
28
- kind: MeterKind;
29
- /** 0-100, clamped. */
30
- percent: number;
31
- /** ISO timestamp of rollover, or null when the window is not open. */
32
- resetsAt: string | null;
33
- status: "ok" | "error" | "unknown";
28
+ kind: MeterKind;
29
+ /** 0-100, clamped. */
30
+ percent: number;
31
+ /** ISO timestamp of rollover, or null when the window is not open. */
32
+ resetsAt: string | null;
33
+ status: "ok" | "error" | "unknown";
34
34
  }
35
35
 
36
36
  interface Config {
37
- workspaceId?: string;
38
- authCookie?: string;
37
+ workspaceId?: string;
38
+ authCookie?: string;
39
39
  }
40
40
 
41
41
  type FetchFailure =
42
- | { kind: "noCredentials" }
43
- | { kind: "timeout" }
44
- | { kind: "network"; detail: string }
45
- | { kind: "unauthorized" }
46
- | { kind: "http"; status: number }
47
- | { kind: "noPayload"; sawLogin: boolean };
42
+ | { kind: "noCredentials" }
43
+ | { kind: "timeout" }
44
+ | { kind: "network"; detail: string }
45
+ | { kind: "unauthorized" }
46
+ | { kind: "http"; status: number }
47
+ | { kind: "noPayload"; sawLogin: boolean };
48
48
 
49
49
  const WINDOW_KEYS: { key: string; kind: MeterKind }[] = [
50
- { key: "rollingUsage", kind: "five_hour" },
51
- { key: "weeklyUsage", kind: "calendar_week" },
52
- { key: "monthlyUsage", kind: "product_period" },
50
+ { key: "rollingUsage", kind: "five_hour" },
51
+ { key: "weeklyUsage", kind: "calendar_week" },
52
+ { key: "monthlyUsage", kind: "product_period" },
53
53
  ];
54
54
 
55
55
  const METER_LABEL: Record<MeterKind, string> = {
56
- five_hour: "Rolling 5h",
57
- calendar_week: "Weekly",
58
- product_period: "Monthly",
56
+ five_hour: "Rolling 5h",
57
+ calendar_week: "Weekly",
58
+ product_period: "Monthly",
59
59
  };
60
60
 
61
61
  const METER_SHORT: Record<MeterKind, string> = {
62
- five_hour: "5h",
63
- calendar_week: "wk",
64
- product_period: "mo",
62
+ five_hour: "5h",
63
+ calendar_week: "wk",
64
+ product_period: "mo",
65
65
  };
66
66
 
67
67
  const CONFIG_PATH = join(homedir(), ".omp", "agent", "opencode-go-usage.json");
@@ -69,24 +69,24 @@ const DEFAULT_ORIGIN = "https://opencode.ai";
69
69
  const REFRESH_SECONDS = 300;
70
70
  const REQUEST_TIMEOUT_MS = 20_000;
71
71
  const USER_AGENT =
72
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
72
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
73
73
 
74
74
  // ---------------------------------------------------------------------------
75
75
  // Config persistence (0600 file; cookie is a credential)
76
76
  // ---------------------------------------------------------------------------
77
77
 
78
78
  async function loadConfig(): Promise<Config> {
79
- try {
80
- return JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as Config;
81
- } catch {
82
- return {};
83
- }
79
+ try {
80
+ return JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as Config;
81
+ } catch {
82
+ return {};
83
+ }
84
84
  }
85
85
 
86
86
  async function saveConfig(config: Config): Promise<void> {
87
- const tmp = `${CONFIG_PATH}.tmp`;
88
- await fs.writeFile(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
89
- await fs.rename(tmp, CONFIG_PATH);
87
+ const tmp = `${CONFIG_PATH}.tmp`;
88
+ await fs.writeFile(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
89
+ await fs.rename(tmp, CONFIG_PATH);
90
90
  }
91
91
 
92
92
  // ---------------------------------------------------------------------------
@@ -94,110 +94,110 @@ async function saveConfig(config: Config): Promise<void> {
94
94
  // ---------------------------------------------------------------------------
95
95
 
96
96
  function workspaceUrl(workspaceId: string, origin = DEFAULT_ORIGIN): string {
97
- return `${origin.replace(/\/+$/, "")}/workspace/${encodeURIComponent(workspaceId)}/go`;
97
+ return `${origin.replace(/\/+$/, "")}/workspace/${encodeURIComponent(workspaceId)}/go`;
98
98
  }
99
99
 
100
100
  function cookieHeader(authCookie: string): string {
101
- const trimmed = authCookie.trim().replace(/;$/, "");
102
- return /^auth=/.test(trimmed) ? trimmed : `auth=${trimmed}`;
101
+ const trimmed = authCookie.trim().replace(/;$/, "");
102
+ return /^auth=/.test(trimmed) ? trimmed : `auth=${trimmed}`;
103
103
  }
104
104
 
105
105
  function scriptBodies(html: string): string {
106
- const bodies: string[] = [];
107
- for (const m of html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)) {
108
- bodies.push(m[1]);
109
- }
110
- return bodies.join("\n");
106
+ const bodies: string[] = [];
107
+ for (const m of html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)) {
108
+ bodies.push(m[1]);
109
+ }
110
+ return bodies.join("\n");
111
111
  }
112
112
 
113
113
  function findObjectBody(html: string, key: string): string | null {
114
- const pattern = new RegExp(`${key}\\s*(?::\\s*\\$R\\[\\d+\\]\\s*)?=\\s*\\{([^{}]*)\\}`);
115
- return pattern.exec(html)?.[1] ?? null;
114
+ const pattern = new RegExp(`${key}\\s*(?::\\s*\\$R\\[\\d+\\]\\s*)?=\\s*\\{([^{}]*)\\}`);
115
+ return pattern.exec(html)?.[1] ?? null;
116
116
  }
117
117
 
118
118
  function readNumber(body: string, field: string): number | null {
119
- const m = new RegExp(`${field}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`).exec(body);
120
- if (!m) return null;
121
- const v = Number(m[1]);
122
- return Number.isFinite(v) ? v : null;
119
+ const m = new RegExp(`${field}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`).exec(body);
120
+ if (!m) return null;
121
+ const v = Number(m[1]);
122
+ return Number.isFinite(v) ? v : null;
123
123
  }
124
124
 
125
125
  function readStatus(body: string): UsageMeter["status"] {
126
- const v = /status\s*:\s*"([^"]*)"/.exec(body)?.[1];
127
- return v === "ok" || v === "error" ? v : "unknown";
126
+ const v = /status\s*:\s*"([^"]*)"/.exec(body)?.[1];
127
+ return v === "ok" || v === "error" ? v : "unknown";
128
128
  }
129
129
 
130
130
  export function parseWorkspaceHtml(html: string, now = Date.now()): UsageMeter[] {
131
- const meters: UsageMeter[] = [];
132
- const haystack = scriptBodies(html) || html;
133
- for (const { key, kind } of WINDOW_KEYS) {
134
- const body = findObjectBody(haystack, key);
135
- if (body === null) continue;
136
- const percent = readNumber(body, "usagePercent");
137
- if (percent === null) continue;
138
- const resetInSec = readNumber(body, "resetInSec") ?? readNumber(body, "resetsInSeconds");
139
- meters.push({
140
- kind,
141
- percent: Math.min(100, Math.max(0, percent)),
142
- resetsAt:
143
- resetInSec !== null && resetInSec > 0
144
- ? new Date(now + resetInSec * 1000).toISOString()
145
- : null,
146
- status: readStatus(body),
147
- });
148
- }
149
- return meters;
131
+ const meters: UsageMeter[] = [];
132
+ const haystack = scriptBodies(html) || html;
133
+ for (const { key, kind } of WINDOW_KEYS) {
134
+ const body = findObjectBody(haystack, key);
135
+ if (body === null) continue;
136
+ const percent = readNumber(body, "usagePercent");
137
+ if (percent === null) continue;
138
+ const resetInSec = readNumber(body, "resetInSec") ?? readNumber(body, "resetsInSeconds");
139
+ meters.push({
140
+ kind,
141
+ percent: Math.min(100, Math.max(0, percent)),
142
+ resetsAt:
143
+ resetInSec !== null && resetInSec > 0
144
+ ? new Date(now + resetInSec * 1000).toISOString()
145
+ : null,
146
+ status: readStatus(body),
147
+ });
148
+ }
149
+ return meters;
150
150
  }
151
151
 
152
152
  export async function fetchUsage(
153
- workspaceId: string,
154
- authCookie: string,
155
- origin = DEFAULT_ORIGIN,
153
+ workspaceId: string,
154
+ authCookie: string,
155
+ origin = DEFAULT_ORIGIN,
156
156
  ): Promise<UsageMeter[]> {
157
- if (!workspaceId.trim() || !authCookie.trim()) {
158
- throw { kind: "noCredentials" } as FetchFailure;
159
- }
160
- const url = workspaceUrl(workspaceId.trim(), origin);
161
- const controller = new AbortController();
162
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
163
- let response: Response;
164
- try {
165
- response = await fetch(url, {
166
- headers: {
167
- Cookie: cookieHeader(authCookie),
168
- "User-Agent": USER_AGENT,
169
- Accept: "text/html,application/xhtml+xml",
170
- },
171
- signal: controller.signal,
172
- redirect: "manual",
173
- });
174
- } catch (err) {
175
- if (err instanceof Error && err.name === "AbortError") {
176
- throw { kind: "timeout" } as FetchFailure;
177
- }
178
- throw {
179
- kind: "network",
180
- detail: err instanceof Error ? err.message : String(err),
181
- } as FetchFailure;
182
- } finally {
183
- clearTimeout(timer);
184
- }
185
- if (response.status >= 300 && response.status < 400) {
186
- const location = response.headers.get("location") ?? "";
187
- if (/auth|login|sign-?in/i.test(location)) throw { kind: "unauthorized" } as FetchFailure;
188
- throw { kind: "http", status: response.status } as FetchFailure;
189
- }
190
- if (response.status === 401 || response.status === 403) {
191
- throw { kind: "unauthorized" } as FetchFailure;
192
- }
193
- if (!response.ok) throw { kind: "http", status: response.status } as FetchFailure;
194
- const html = await response.text();
195
- const meters = parseWorkspaceHtml(html);
196
- if (meters.length === 0) {
197
- const sawLogin = /\/auth\/authorize|sign\s?in to opencode/i.test(html);
198
- throw { kind: "noPayload", sawLogin } as FetchFailure;
157
+ if (!workspaceId.trim() || !authCookie.trim()) {
158
+ throw { kind: "noCredentials" } as FetchFailure;
159
+ }
160
+ const url = workspaceUrl(workspaceId.trim(), origin);
161
+ const controller = new AbortController();
162
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
163
+ let response: Response;
164
+ try {
165
+ response = await fetch(url, {
166
+ headers: {
167
+ Cookie: cookieHeader(authCookie),
168
+ "User-Agent": USER_AGENT,
169
+ Accept: "text/html,application/xhtml+xml",
170
+ },
171
+ signal: controller.signal,
172
+ redirect: "manual",
173
+ });
174
+ } catch (err) {
175
+ if (err instanceof Error && err.name === "AbortError") {
176
+ throw { kind: "timeout" } as FetchFailure;
199
177
  }
200
- return meters;
178
+ throw {
179
+ kind: "network",
180
+ detail: err instanceof Error ? err.message : String(err),
181
+ } as FetchFailure;
182
+ } finally {
183
+ clearTimeout(timer);
184
+ }
185
+ if (response.status >= 300 && response.status < 400) {
186
+ const location = response.headers.get("location") ?? "";
187
+ if (/auth|login|sign-?in/i.test(location)) throw { kind: "unauthorized" } as FetchFailure;
188
+ throw { kind: "http", status: response.status } as FetchFailure;
189
+ }
190
+ if (response.status === 401 || response.status === 403) {
191
+ throw { kind: "unauthorized" } as FetchFailure;
192
+ }
193
+ if (!response.ok) throw { kind: "http", status: response.status } as FetchFailure;
194
+ const html = await response.text();
195
+ const meters = parseWorkspaceHtml(html);
196
+ if (meters.length === 0) {
197
+ const sawLogin = /\/auth\/authorize|sign\s?in to opencode/i.test(html);
198
+ throw { kind: "noPayload", sawLogin } as FetchFailure;
199
+ }
200
+ return meters;
201
201
  }
202
202
 
203
203
  // ---------------------------------------------------------------------------
@@ -205,43 +205,43 @@ export async function fetchUsage(
205
205
  // ---------------------------------------------------------------------------
206
206
 
207
207
  export function bar(percent: number, width = 10): string {
208
- const clamped = Math.min(100, Math.max(0, percent));
209
- const filled = Math.round((clamped / 100) * width);
210
- return "█".repeat(filled) + "░".repeat(width - filled);
208
+ const clamped = Math.min(100, Math.max(0, percent));
209
+ const filled = Math.round((clamped / 100) * width);
210
+ return "█".repeat(filled) + "░".repeat(width - filled);
211
211
  }
212
212
 
213
213
  export function countdown(resetsAt: string | null, now = Date.now()): string | null {
214
- if (!resetsAt) return null;
215
- const target = Date.parse(resetsAt);
216
- if (!Number.isFinite(target)) return null;
217
- const ms = target - now;
218
- if (ms <= 0) return "resets now";
219
- const totalMinutes = Math.floor(ms / 60_000);
220
- const days = Math.floor(totalMinutes / (60 * 24));
221
- const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
222
- const minutes = totalMinutes % 60;
223
- if (days > 0) return `${days}d ${hours}h`;
224
- if (hours > 0) return `${hours}h ${minutes}m`;
225
- return `${minutes}m`;
214
+ if (!resetsAt) return null;
215
+ const target = Date.parse(resetsAt);
216
+ if (!Number.isFinite(target)) return null;
217
+ const ms = target - now;
218
+ if (ms <= 0) return "resets now";
219
+ const totalMinutes = Math.floor(ms / 60_000);
220
+ const days = Math.floor(totalMinutes / (60 * 24));
221
+ const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
222
+ const minutes = totalMinutes % 60;
223
+ if (days > 0) return `${days}d ${hours}h`;
224
+ if (hours > 0) return `${hours}h ${minutes}m`;
225
+ return `${minutes}m`;
226
226
  }
227
227
 
228
228
  function describeFailure(f: FetchFailure): string {
229
- switch (f.kind) {
230
- case "noCredentials":
231
- return "Not connected. Run /opencode-go --connect <wrk_…> <auth-cookie>";
232
- case "timeout":
233
- return "Request timed out";
234
- case "network":
235
- return `Network error: ${f.detail}`;
236
- case "unauthorized":
237
- return "Cookie expired — reconnect with a fresh auth cookie";
238
- case "http":
239
- return `HTTP ${f.status}`;
240
- case "noPayload":
241
- return f.sawLogin
242
- ? "Cookie expired (login page served)"
243
- : "Page carried no usage data — opencode.ai markup may have changed";
244
- }
229
+ switch (f.kind) {
230
+ case "noCredentials":
231
+ return "Not connected. Run /opencode-go --connect <wrk_…> <auth-cookie>";
232
+ case "timeout":
233
+ return "Request timed out";
234
+ case "network":
235
+ return `Network error: ${f.detail}`;
236
+ case "unauthorized":
237
+ return "Cookie expired — reconnect with a fresh auth cookie";
238
+ case "http":
239
+ return `HTTP ${f.status}`;
240
+ case "noPayload":
241
+ return f.sawLogin
242
+ ? "Cookie expired (login page served)"
243
+ : "Page carried no usage data — opencode.ai markup may have changed";
244
+ }
245
245
  }
246
246
 
247
247
  // ---------------------------------------------------------------------------
@@ -249,200 +249,218 @@ function describeFailure(f: FetchFailure): string {
249
249
  // ---------------------------------------------------------------------------
250
250
 
251
251
  interface UiCtx {
252
- hasUI: boolean;
253
- ui: {
254
- setStatus(key: string, text: string | undefined): void;
255
- setWidget(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
256
- notify(message: string, type?: "info" | "warning" | "error"): void;
257
- };
252
+ hasUI: boolean;
253
+ ui: {
254
+ setStatus(key: string, text: string | undefined): void;
255
+ setWidget(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
256
+ notify(message: string, type?: "info" | "warning" | "error"): void;
257
+ };
258
258
  }
259
259
 
260
260
  export default function opencodeGoUsage(pi: ExtensionAPI): void {
261
- let config: Config = {};
262
- let meters: UsageMeter[] = [];
263
- let lastError: string | null = null;
264
- let lastFetchedAt = 0;
265
- let timer: ReturnType<typeof setInterval> | undefined;
266
-
267
- const resolvedCreds = (): { workspaceId: string; authCookie: string } | null => {
268
- const workspaceId = (process.env.OPENCODE_GO_WORKSPACE_ID ?? config.workspaceId ?? "").trim();
269
- const authCookie = (process.env.OPENCODE_GO_AUTH_COOKIE ?? config.authCookie ?? "").trim();
270
- return workspaceId && authCookie ? { workspaceId, authCookie } : null;
271
- };
272
-
273
- const renderStatus = (ctx: UiCtx): void => {
274
- if (!ctx.hasUI) return;
275
- const creds = resolvedCreds();
276
- if (!creds) {
277
- ctx.ui.setStatus("opencode-go", "Go: not connected (/opencode-go --connect)");
278
- return;
279
- }
280
- if (lastError) {
281
- ctx.ui.setStatus("opencode-go", `Go: ${lastError}`);
282
- return;
283
- }
284
- if (meters.length === 0) {
285
- ctx.ui.setStatus("opencode-go", "Go: loading…");
286
- return;
287
- }
288
- ctx.ui.setStatus("opencode-go", `Go ${meters.map((m) => `${METER_SHORT[m.kind]} ${m.percent}%`).join(" · ")}`);
289
- };
290
-
291
- const renderReport = (ctx: UiCtx): void => {
292
- if (!ctx.hasUI) return;
293
- const creds = resolvedCreds();
294
- const lines: string[] = ["OpenCode Go Usage"];
295
- if (!creds) {
296
- lines.push("Not connected.");
297
- lines.push("Run /opencode-go --connect <wrk_…> <auth-cookie>");
298
- lines.push("Or set OPENCODE_GO_WORKSPACE_ID + OPENCODE_GO_AUTH_COOKIE");
299
- ctx.ui.setWidget("opencode-go", lines, { placement: "aboveEditor" });
300
- return;
301
- }
302
- lines.push(`Workspace: ${creds.workspaceId}`);
303
- if (lastError) {
304
- lines.push(`Error: ${lastError}`);
305
- } else if (meters.length === 0) {
306
- lines.push("Loading…");
307
- } else {
308
- for (const m of meters) {
309
- const cd = countdown(m.resetsAt);
310
- lines.push(`${METER_LABEL[m.kind].padEnd(10)} ${bar(m.percent, 10)} ${m.percent}%${cd ? ` · ${cd}` : ""}`);
311
- }
261
+ let config: Config = {};
262
+ let meters: UsageMeter[] = [];
263
+ let lastError: string | null = null;
264
+ let lastFetchedAt = 0;
265
+ let timer: ReturnType<typeof setInterval> | undefined;
266
+
267
+ const resolvedCreds = (): { workspaceId: string; authCookie: string } | null => {
268
+ const workspaceId = (process.env.OPENCODE_GO_WORKSPACE_ID ?? config.workspaceId ?? "").trim();
269
+ const authCookie = (process.env.OPENCODE_GO_AUTH_COOKIE ?? config.authCookie ?? "").trim();
270
+ return workspaceId && authCookie ? { workspaceId, authCookie } : null;
271
+ };
272
+
273
+ const fmtUpdate = (ts: number): string => {
274
+ const d = new Date(ts);
275
+ const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
276
+ let tz: string;
277
+ try {
278
+ tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "local";
279
+ } catch {
280
+ tz = "local";
281
+ }
282
+ return `update ${time} (${tz})`;
283
+ };
284
+
285
+ const renderStatus = (ctx: UiCtx): void => {
286
+ if (!ctx.hasUI) return;
287
+ const creds = resolvedCreds();
288
+ if (!creds) {
289
+ ctx.ui.setStatus("opencode-go", "OpenCode Go: not connected (/opencode-go --connect)");
290
+ return;
291
+ }
292
+ if (lastError) {
293
+ ctx.ui.setStatus("opencode-go", `OpenCode Go: ${lastError}`);
294
+ return;
295
+ }
296
+ if (meters.length === 0) {
297
+ ctx.ui.setStatus("opencode-go", "OpenCode Go: loading…");
298
+ return;
299
+ }
300
+ const parts = meters.map((m) => {
301
+ const cd = countdown(m.resetsAt);
302
+ return `${METER_SHORT[m.kind]} ${m.percent}%${cd ? ` (${cd})` : ""}`;
303
+ });
304
+ let text = `OpenCode Go: ${parts.join(" · ")}`;
305
+ if (lastFetchedAt) text += ` · ${fmtUpdate(lastFetchedAt)}`;
306
+ ctx.ui.setStatus("opencode-go", text);
307
+ };
308
+
309
+ const renderReport = (ctx: UiCtx): void => {
310
+ if (!ctx.hasUI) return;
311
+ const creds = resolvedCreds();
312
+ const lines: string[] = ["OpenCode Go Usage"];
313
+ if (!creds) {
314
+ lines.push("Not connected.");
315
+ lines.push("Run /opencode-go --connect <wrk_…> <auth-cookie>");
316
+ lines.push("Or set OPENCODE_GO_WORKSPACE_ID + OPENCODE_GO_AUTH_COOKIE");
317
+ ctx.ui.setWidget("opencode-go", lines, { placement: "aboveEditor" });
318
+ return;
319
+ }
320
+ lines.push(`Workspace: ${creds.workspaceId}`);
321
+ if (lastError) {
322
+ lines.push(`Error: ${lastError}`);
323
+ } else if (meters.length === 0) {
324
+ lines.push("Loading…");
325
+ } else {
326
+ for (const m of meters) {
327
+ const cd = countdown(m.resetsAt);
328
+ lines.push(`${METER_LABEL[m.kind].padEnd(10)} ${bar(m.percent, 10)} ${m.percent}%${cd ? ` · ${cd}` : ""}`);
329
+ }
330
+ }
331
+ if (lastFetchedAt) lines.push(fmtUpdate(lastFetchedAt));
332
+ ctx.ui.setWidget("opencode-go", lines, { placement: "aboveEditor" });
333
+ };
334
+
335
+ const refresh = async (ctx: UiCtx): Promise<void> => {
336
+ const creds = resolvedCreds();
337
+ if (!creds) {
338
+ meters = [];
339
+ lastError = null;
340
+ renderStatus(ctx);
341
+ return;
342
+ }
343
+ try {
344
+ meters = await fetchUsage(creds.workspaceId, creds.authCookie, DEFAULT_ORIGIN);
345
+ lastError = null;
346
+ lastFetchedAt = Date.now();
347
+ } catch (err) {
348
+ meters = [];
349
+ lastError = describeFailure(err as FetchFailure);
350
+ }
351
+ renderStatus(ctx);
352
+ };
353
+
354
+ pi.on("session_start", async (_event, ctx) => {
355
+ config = await loadConfig();
356
+ if (timer) {
357
+ clearInterval(timer);
358
+ timer = undefined;
359
+ }
360
+ if (ctx.hasUI && resolvedCreds()) ctx.ui.notify("OpenCode Go usage tracker loaded", "info");
361
+ // Fire-and-forget: don't block session startup on a network round-trip.
362
+ void refresh(ctx);
363
+ // Plain setInterval with the callback body fully wrapped so a throw cannot
364
+ // escape and tear down the session.
365
+ timer = setInterval(() => {
366
+ void refresh(ctx).catch(() => { });
367
+ }, REFRESH_SECONDS * 1000);
368
+ });
369
+
370
+ pi.on("session_shutdown", () => {
371
+ if (timer) {
372
+ clearInterval(timer);
373
+ timer = undefined;
374
+ }
375
+ });
376
+
377
+ pi.registerCommand("opencode-go", {
378
+ description:
379
+ "Show OpenCode Go usage. Subcommands: --connect <wrk> <cookie> | --workspace <id> | --cookie <v> | --disconnect | --refresh | --json",
380
+ handler: async (args, ctx) => {
381
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
382
+ const sub = tokens[0];
383
+ const rest = tokens.slice(1);
384
+
385
+ if (sub === "--connect" || sub === "--setup") {
386
+ const workspaceId = rest[0];
387
+ const cookie = rest.slice(1).join(" ");
388
+ if (!workspaceId || !cookie) {
389
+ ctx.ui.notify("Usage: /opencode-go --connect <wrk_…> <auth-cookie>", "warning");
390
+ return;
312
391
  }
313
- if (lastFetchedAt) lines.push(`Updated ${new Date(lastFetchedAt).toLocaleTimeString()}`);
314
- ctx.ui.setWidget("opencode-go", lines, { placement: "aboveEditor" });
315
- };
316
-
317
- const refresh = async (ctx: UiCtx): Promise<void> => {
318
- const creds = resolvedCreds();
319
- if (!creds) {
320
- meters = [];
321
- lastError = null;
322
- renderStatus(ctx);
323
- return;
392
+ config.workspaceId = workspaceId.trim();
393
+ config.authCookie = cookie.trim();
394
+ await saveConfig(config);
395
+ ctx.ui.notify("Saved. Fetching usage…", "info");
396
+ await refresh(ctx);
397
+ renderReport(ctx);
398
+ return;
399
+ }
400
+
401
+ if (sub === "--workspace") {
402
+ if (!rest[0]) {
403
+ ctx.ui.notify("Usage: /opencode-go --workspace <wrk_…>", "warning");
404
+ return;
324
405
  }
325
- try {
326
- meters = await fetchUsage(creds.workspaceId, creds.authCookie, DEFAULT_ORIGIN);
327
- lastError = null;
328
- lastFetchedAt = Date.now();
329
- } catch (err) {
330
- meters = [];
331
- lastError = describeFailure(err as FetchFailure);
406
+ config.workspaceId = rest[0].trim();
407
+ await saveConfig(config);
408
+ ctx.ui.notify(`Workspace set to ${config.workspaceId}`, "info");
409
+ return;
410
+ }
411
+
412
+ if (sub === "--cookie") {
413
+ const cookie = rest.join(" ");
414
+ if (!cookie) {
415
+ ctx.ui.notify("Usage: /opencode-go --cookie <auth-cookie>", "warning");
416
+ return;
332
417
  }
418
+ config.authCookie = cookie.trim();
419
+ await saveConfig(config);
420
+ ctx.ui.notify("Cookie saved", "info");
421
+ return;
422
+ }
423
+
424
+ if (sub === "--disconnect") {
425
+ delete config.workspaceId;
426
+ delete config.authCookie;
427
+ await saveConfig(config);
428
+ meters = [];
429
+ lastError = null;
333
430
  renderStatus(ctx);
334
- };
335
-
336
- pi.on("session_start", async (_event, ctx) => {
337
- config = await loadConfig();
338
- if (timer) {
339
- clearInterval(timer);
340
- timer = undefined;
341
- }
342
- if (ctx.hasUI && resolvedCreds()) ctx.ui.notify("OpenCode Go usage tracker loaded", "info");
343
- // Fire-and-forget: don't block session startup on a network round-trip.
344
- void refresh(ctx);
345
- // Plain setInterval with the callback body fully wrapped so a throw cannot
346
- // escape and tear down the session.
347
- timer = setInterval(() => {
348
- void refresh(ctx).catch(() => {});
349
- }, REFRESH_SECONDS * 1000);
350
- });
351
-
352
- pi.on("session_shutdown", () => {
353
- if (timer) {
354
- clearInterval(timer);
355
- timer = undefined;
431
+ ctx.ui.setWidget("opencode-go", undefined);
432
+ ctx.ui.notify("Disconnected", "info");
433
+ return;
434
+ }
435
+
436
+ if (sub === "--refresh") {
437
+ await refresh(ctx);
438
+ renderReport(ctx);
439
+ return;
440
+ }
441
+
442
+ if (sub === "--json") {
443
+ await refresh(ctx);
444
+ const report = {
445
+ workspaceId: resolvedCreds()?.workspaceId ?? null,
446
+ fetchedAt: lastFetchedAt ? new Date(lastFetchedAt).toISOString() : null,
447
+ error: lastError,
448
+ meters,
449
+ };
450
+ const outPath = join(homedir(), ".omp", "agent", "opencode-go-usage-report.json");
451
+ try {
452
+ const tmp = `${outPath}.tmp`;
453
+ await fs.writeFile(tmp, JSON.stringify(report, null, 2), "utf8");
454
+ await fs.rename(tmp, outPath);
455
+ ctx.ui.notify(`JSON report written to ${outPath}`, "info");
456
+ } catch {
457
+ ctx.ui.notify("Failed to write JSON report", "error");
356
458
  }
357
- });
459
+ return;
460
+ }
358
461
 
359
- pi.registerCommand("opencode-go", {
360
- description:
361
- "Show OpenCode Go usage. Subcommands: --connect <wrk> <cookie> | --workspace <id> | --cookie <v> | --disconnect | --refresh | --json",
362
- handler: async (args, ctx) => {
363
- const tokens = args.trim().split(/\s+/).filter(Boolean);
364
- const sub = tokens[0];
365
- const rest = tokens.slice(1);
366
-
367
- if (sub === "--connect" || sub === "--setup") {
368
- const workspaceId = rest[0];
369
- const cookie = rest.slice(1).join(" ");
370
- if (!workspaceId || !cookie) {
371
- ctx.ui.notify("Usage: /opencode-go --connect <wrk_…> <auth-cookie>", "warning");
372
- return;
373
- }
374
- config.workspaceId = workspaceId.trim();
375
- config.authCookie = cookie.trim();
376
- await saveConfig(config);
377
- ctx.ui.notify("Saved. Fetching usage…", "info");
378
- await refresh(ctx);
379
- renderReport(ctx);
380
- return;
381
- }
382
-
383
- if (sub === "--workspace") {
384
- if (!rest[0]) {
385
- ctx.ui.notify("Usage: /opencode-go --workspace <wrk_…>", "warning");
386
- return;
387
- }
388
- config.workspaceId = rest[0].trim();
389
- await saveConfig(config);
390
- ctx.ui.notify(`Workspace set to ${config.workspaceId}`, "info");
391
- return;
392
- }
393
-
394
- if (sub === "--cookie") {
395
- const cookie = rest.join(" ");
396
- if (!cookie) {
397
- ctx.ui.notify("Usage: /opencode-go --cookie <auth-cookie>", "warning");
398
- return;
399
- }
400
- config.authCookie = cookie.trim();
401
- await saveConfig(config);
402
- ctx.ui.notify("Cookie saved", "info");
403
- return;
404
- }
405
-
406
- if (sub === "--disconnect") {
407
- delete config.workspaceId;
408
- delete config.authCookie;
409
- await saveConfig(config);
410
- meters = [];
411
- lastError = null;
412
- renderStatus(ctx);
413
- ctx.ui.setWidget("opencode-go", undefined);
414
- ctx.ui.notify("Disconnected", "info");
415
- return;
416
- }
417
-
418
- if (sub === "--refresh") {
419
- await refresh(ctx);
420
- renderReport(ctx);
421
- return;
422
- }
423
-
424
- if (sub === "--json") {
425
- await refresh(ctx);
426
- const report = {
427
- workspaceId: resolvedCreds()?.workspaceId ?? null,
428
- fetchedAt: lastFetchedAt ? new Date(lastFetchedAt).toISOString() : null,
429
- error: lastError,
430
- meters,
431
- };
432
- const outPath = join(homedir(), ".omp", "agent", "opencode-go-usage-report.json");
433
- try {
434
- const tmp = `${outPath}.tmp`;
435
- await fs.writeFile(tmp, JSON.stringify(report, null, 2), "utf8");
436
- await fs.rename(tmp, outPath);
437
- ctx.ui.notify(`JSON report written to ${outPath}`, "info");
438
- } catch {
439
- ctx.ui.notify("Failed to write JSON report", "error");
440
- }
441
- return;
442
- }
443
-
444
- await refresh(ctx);
445
- renderReport(ctx);
446
- },
447
- });
462
+ await refresh(ctx);
463
+ renderReport(ctx);
464
+ },
465
+ });
448
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-opencode-go-usage",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Track OpenCode Go usage limits (rolling 5h / weekly / monthly) in Pi — status bar + /opencode-go report via HTML scrape.",
5
5
  "author": "Dakai",
6
6
  "keywords": [