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 +2 -1
- package/README.zh-CN.md +2 -1
- package/extensions/opencode-go-usage.ts +359 -331
- package/package.json +2 -2
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
|
-

|
|
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
|
-

|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
37
|
+
workspaceId?: string;
|
|
38
|
+
authCookie?: string;
|
|
39
|
+
compact?: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
type FetchFailure =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
+
return `${origin.replace(/\/+$/, "")}/workspace/${encodeURIComponent(workspaceId)}/go`;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function cookieHeader(authCookie: string): string {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
workspaceId: string,
|
|
155
|
+
authCookie: string,
|
|
156
|
+
origin = DEFAULT_ORIGIN,
|
|
156
157
|
): Promise<UsageMeter[]> {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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.
|
|
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/
|
|
43
|
+
"image": "https://raw.githubusercontent.com/Dakai/pi-opencode-go-usage/main/assets/pi-opencode-go-usage.png"
|
|
44
44
|
}
|
|
45
45
|
}
|