pi-opencode-go-usage 1.0.1 → 1.0.3

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