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.
- package/extensions/opencode-go-usage.ts +349 -331
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
39
|
}
|
|
40
40
|
|
|
41
41
|
type FetchFailure =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
97
|
+
return `${origin.replace(/\/+$/, "")}/workspace/${encodeURIComponent(workspaceId)}/go`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function cookieHeader(authCookie: string): string {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
workspaceId: string,
|
|
154
|
+
authCookie: string,
|
|
155
|
+
origin = DEFAULT_ORIGIN,
|
|
156
156
|
): 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;
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
462
|
+
await refresh(ctx);
|
|
463
|
+
renderReport(ctx);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
448
466
|
}
|
package/package.json
CHANGED