@oscarfalero/pi-opencode-go 0.1.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/extensions/opencode-go-usage.ts +328 -0
- package/package.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oscarfalero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# pi-opencode-go
|
|
2
|
+
|
|
3
|
+
OpenCode Go usage in pi status bar — rolling 5h / weekly / monthly + `/go-usage` + `/go-status` toggle.
|
|
4
|
+
|
|
5
|
+
Ultra-compact status bar indicator for your [OpenCode Go](https://opencode.ai) subscription, designed for [pi](https://github.com/earendil-works/pi-coding-agent) (the pi coding agent / TUI).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### From npm via pi
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pi install npm:@oscarfalero/pi-opencode-go
|
|
13
|
+
pi install npm:@oscarfalero/pi-opencode-go@0.1.0 # pinned
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### From git via pi
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pi install git:github.com/oscarfalero/pi-opencode-go
|
|
20
|
+
pi install git:github.com/oscarfalero/pi-opencode-go@v0.1.0 # pinned tag
|
|
21
|
+
pi install https://github.com/oscarfalero/pi-opencode-go
|
|
22
|
+
# local path for development
|
|
23
|
+
pi install ./path/to/pi-opencode-go
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Restart pi after installation (or reload extensions) to activate.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### `/go-usage`
|
|
31
|
+
|
|
32
|
+
Show detailed OpenCode Go usage (Rolling / Weekly / Monthly) as a notification:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
OpenCode Go usage:
|
|
36
|
+
Rolling: 42% — resets in 2h 15m
|
|
37
|
+
Weekly: 18% — resets in 3d 4h
|
|
38
|
+
Monthly: 7% — resets in 12d 6h
|
|
39
|
+
|
|
40
|
+
Toggle compact status with /go-status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If the key is not configured you will see a warning. If your current model is not an `opencode-go` model, a hint is appended.
|
|
44
|
+
|
|
45
|
+
### `/go-status`
|
|
46
|
+
|
|
47
|
+
Toggle the compact status bar indicator on/off.
|
|
48
|
+
|
|
49
|
+
- `Go compact: on — fetching…` (when a Go model is active)
|
|
50
|
+
- `Go compact: on — will appear when a Go model is selected` (when enabled but non-Go model active)
|
|
51
|
+
- `Go compact: off — hidden from status bar` (when disabled)
|
|
52
|
+
|
|
53
|
+
### Status bar behavior
|
|
54
|
+
|
|
55
|
+
- **Only when provider configured and Go model active:** the indicator appears only if `opencode-go` is configured (valid `OPENCODE_API_KEY`) **and** the current model provider is `opencode-go` **and** compact is enabled.
|
|
56
|
+
- **Pastel yellow:** fixed color `mdHeading` (`#f0c674`) — no conditional warning/error colors.
|
|
57
|
+
- **Post-turn refresh:** no polling interval. Refreshes on `turn_end` / `agent_settled` (debounced 800ms) plus `session_start` and `model_select`. Shows `Go …` while fetching, `Go --` on failure with no cache, or `Go 12/34/56%` (rolling/weekly/monthly percents) on success.
|
|
58
|
+
|
|
59
|
+
Example status bar text: `Go 12/34/56%`
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- `pi` coding agent (`@earendil-works/pi-coding-agent`) and `@earendil-works/pi-tui`
|
|
64
|
+
- OpenCode Go subscription and API key
|
|
65
|
+
|
|
66
|
+
Set your API key via `~/.pi/agent/auth.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"opencode-go": {
|
|
71
|
+
"type": "api_key",
|
|
72
|
+
"key": "sk-..."
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or via environment variable (pi resolves `OPENCODE_API_KEY` for the provider):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export OPENCODE_API_KEY="..."
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The extension authenticates with `GET https://opencode.ai/zen/go/v1/usage` using `Authorization: Bearer <OPENCODE_API_KEY>`. It also respects a custom `baseUrl` if your provider is configured with a non-localhost override containing `/zen/go/v1`.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
The `/go-status` toggle persists to:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
~/.pi/agent/opencode-go-usage.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Format:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"visible": true
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `true` (default) — compact status visible when conditions are met
|
|
102
|
+
- `false` — hidden from status bar (still allows `/go-usage` on demand)
|
|
103
|
+
|
|
104
|
+
You can edit this file manually; it is reloaded on each `session_start`. Delete it to restore defaults.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# run pi with this extension loaded from the working directory
|
|
110
|
+
pi -e .
|
|
111
|
+
|
|
112
|
+
# install local build into pi (from inside the repo)
|
|
113
|
+
pi install ./.
|
|
114
|
+
|
|
115
|
+
# after changes, reinstall
|
|
116
|
+
pi install ./.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
No build step required — the extension is plain TypeScript loaded directly. No runtime dependencies; `typebox`, `pi-coding-agent`, and `pi-tui` are peer dependencies.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT — see [LICENSE](./LICENSE).
|
|
124
|
+
|
|
125
|
+
## Security
|
|
126
|
+
|
|
127
|
+
- Your OpenCode API key is sent **only** to the OpenCode usage endpoint: `https://opencode.ai/zen/go/v1/usage` by default, or a custom provider `baseUrl` that includes `/zen/go/v1`. No other host ever receives it.
|
|
128
|
+
- The key is resolved through pi's own auth layer (`modelRegistry`) and is never logged or stored by this extension.
|
|
129
|
+
- No telemetry, analytics, or data collection — the only network traffic is the usage query described above.
|
|
130
|
+
- The only file this extension writes is its toggle state at `<agent-dir>/opencode-go-usage.json` (e.g. `~/.pi/agent/opencode-go-usage.json`).
|
|
131
|
+
- Review the source before installing: [extensions/opencode-go-usage.ts](./extensions/opencode-go-usage.ts) is a single small file.
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-go usage — status bar for OpenCode Go subscription
|
|
3
|
+
*
|
|
4
|
+
* Shows ultra-compact usage in the footer only when:
|
|
5
|
+
* 1. opencode-go provider is configured (auth.json / env)
|
|
6
|
+
* 2. current model provider is "opencode-go"
|
|
7
|
+
* 3. compact is enabled (toggle via /go-status)
|
|
8
|
+
*
|
|
9
|
+
* Refresh: post-turn (turn_end / agent_settled) + session_start + model_select.
|
|
10
|
+
* No time-based interval.
|
|
11
|
+
*
|
|
12
|
+
* Endpoint: GET https://opencode.ai/zen/go/v1/usage
|
|
13
|
+
* Header: authorization: Bearer <OPENCODE_API_KEY>
|
|
14
|
+
* 200: { usage: { rolling:{status, percent, resetsAt}, weekly:{...}, monthly:{...} } }
|
|
15
|
+
*
|
|
16
|
+
* Status key: "opencode-go-usage"
|
|
17
|
+
* Color: fixed pastel yellow (mdHeading #f0c674) — no conditional warning/error.
|
|
18
|
+
*
|
|
19
|
+
* Toggle: /go-status — persists to <agent-dir>/opencode-go-usage.json (rebrand-safe via getAgentDir()).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
const STATUS_KEY = "opencode-go-usage";
|
|
28
|
+
const PROD_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
29
|
+
const CONFIG_PATH = join(getAgentDir(), "opencode-go-usage.json");
|
|
30
|
+
|
|
31
|
+
type UsageBucket = {
|
|
32
|
+
status: "ok" | "rate-limited";
|
|
33
|
+
percent: number;
|
|
34
|
+
resetsAt: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type UsageResponse = {
|
|
38
|
+
usage: {
|
|
39
|
+
rolling: UsageBucket;
|
|
40
|
+
weekly: UsageBucket;
|
|
41
|
+
monthly: UsageBucket;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function loadCompactVisible(): boolean {
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(CONFIG_PATH)) return true;
|
|
48
|
+
const raw = readFileSync(CONFIG_PATH, "utf8");
|
|
49
|
+
const data = JSON.parse(raw) as { visible?: boolean };
|
|
50
|
+
// explicit false disables, anything else enables (default true)
|
|
51
|
+
return data.visible !== false;
|
|
52
|
+
} catch {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveCompactVisible(visible: boolean): void {
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(CONFIG_PATH, JSON.stringify({ visible }, null, 2) + "\n", "utf8");
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function (pi: ExtensionAPI) {
|
|
64
|
+
let cached: UsageResponse["usage"] | null = null;
|
|
65
|
+
let isFetching = false;
|
|
66
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
+
let compactVisible = loadCompactVisible();
|
|
68
|
+
|
|
69
|
+
function isGoActive(ctx: ExtensionContext): boolean {
|
|
70
|
+
if (!compactVisible) return false;
|
|
71
|
+
const auth = ctx.modelRegistry.getProviderAuthStatus("opencode-go");
|
|
72
|
+
if (!auth.configured) return false;
|
|
73
|
+
return ctx.model?.provider === "opencode-go";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isProviderConfigured(ctx: ExtensionContext): boolean {
|
|
77
|
+
return ctx.modelRegistry.getProviderAuthStatus("opencode-go").configured;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUsageUrl(ctx: ExtensionContext): string {
|
|
81
|
+
const provider = ctx.modelRegistry.getProvider("opencode-go") as unknown as
|
|
82
|
+
| { baseUrl?: string; api?: string }
|
|
83
|
+
| undefined;
|
|
84
|
+
|
|
85
|
+
const candidates: (string | undefined)[] = [
|
|
86
|
+
(provider as any)?.baseUrl,
|
|
87
|
+
(provider as any)?.api,
|
|
88
|
+
(ctx.model as any)?.baseUrl,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
for (const base of candidates) {
|
|
92
|
+
if (!base || base.includes("localhost") || base.includes("127.0.0.1")) continue;
|
|
93
|
+
if (base.includes("/zen/go/v1")) {
|
|
94
|
+
return base.replace(/\/$/, "") + "/usage";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return PROD_USAGE_URL;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatResetsIn(iso: string): string {
|
|
101
|
+
const diff = new Date(iso).getTime() - Date.now();
|
|
102
|
+
if (diff <= 0) return "now";
|
|
103
|
+
const mins = Math.round(diff / 60000);
|
|
104
|
+
if (mins < 60) return `${mins}m`;
|
|
105
|
+
if (mins < 1440) {
|
|
106
|
+
const h = Math.floor(mins / 60);
|
|
107
|
+
const m = mins % 60;
|
|
108
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
109
|
+
}
|
|
110
|
+
const days = Math.floor(mins / 1440);
|
|
111
|
+
const rem = mins % 1440;
|
|
112
|
+
const h = Math.floor(rem / 60);
|
|
113
|
+
const m = rem % 60;
|
|
114
|
+
const parts: string[] = [`${days}d`];
|
|
115
|
+
if (h > 0) parts.push(`${h}h`);
|
|
116
|
+
if (m > 0) parts.push(`${m}m`);
|
|
117
|
+
return parts.join(" ");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fixed pastel yellow — no conditional colors. Uses mdHeading (#f0c674) which is the
|
|
121
|
+
// built-in pastel yellow in both dark/light themes.
|
|
122
|
+
function pastel(theme: ExtensionContext["ui"]["theme"], text: string): string {
|
|
123
|
+
try {
|
|
124
|
+
return theme.fg("mdHeading", text);
|
|
125
|
+
} catch {
|
|
126
|
+
// fallback to warning (yellow) if theme lacks mdHeading for some reason
|
|
127
|
+
return theme.fg("warning", text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderCompact(usage: UsageResponse["usage"], theme: ExtensionContext["ui"]["theme"]): string {
|
|
132
|
+
const text = `Go ${usage.rolling.percent}/${usage.weekly.percent}/${usage.monthly.percent}%`;
|
|
133
|
+
return pastel(theme, text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function fetchUsage(ctx: ExtensionContext): Promise<UsageResponse["usage"]> {
|
|
137
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider("opencode-go");
|
|
138
|
+
if (!apiKey) throw new Error("Missing OPENCODE_API_KEY");
|
|
139
|
+
|
|
140
|
+
const url = getUsageUrl(ctx);
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const t = setTimeout(() => controller.abort(), 6000);
|
|
143
|
+
|
|
144
|
+
// Merge the agent's turn signal (so Esc aborts) with our own timeout, and
|
|
145
|
+
// always detach the listener afterwards so we never leak handlers on a
|
|
146
|
+
// long-lived signal.
|
|
147
|
+
let onAbort: (() => void) | undefined;
|
|
148
|
+
if (ctx.signal) {
|
|
149
|
+
if (ctx.signal.aborted) controller.abort();
|
|
150
|
+
else {
|
|
151
|
+
onAbort = () => controller.abort();
|
|
152
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
159
|
+
signal: controller.signal,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (res.status === 401) throw new Error("401 Unauthorized — check OPENCODE_API_KEY");
|
|
163
|
+
if (res.status === 403) throw new Error("403 No OpenCode Go subscription for this key");
|
|
164
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
165
|
+
|
|
166
|
+
const json = (await res.json()) as UsageResponse;
|
|
167
|
+
if (!json.usage?.rolling || !json.usage?.weekly || !json.usage?.monthly) {
|
|
168
|
+
throw new Error("Unexpected usage response shape");
|
|
169
|
+
}
|
|
170
|
+
return json.usage;
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(t);
|
|
173
|
+
if (onAbort && ctx.signal) ctx.signal.removeEventListener("abort", onAbort);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** UI setters can throw when pi invalidates ctx after session replacement/reload. Never let that escape as an unhandled rejection. */
|
|
178
|
+
function safeSetStatus(ctx: ExtensionContext, value: string | undefined) {
|
|
179
|
+
try {
|
|
180
|
+
ctx.ui.setStatus(STATUS_KEY, value);
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function refresh(ctx: ExtensionContext) {
|
|
185
|
+
if (!isGoActive(ctx)) {
|
|
186
|
+
safeSetStatus(ctx, undefined);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (isFetching) return;
|
|
190
|
+
isFetching = true;
|
|
191
|
+
try {
|
|
192
|
+
const usage = await fetchUsage(ctx);
|
|
193
|
+
cached = usage;
|
|
194
|
+
safeSetStatus(ctx, renderCompact(usage, ctx.ui.theme));
|
|
195
|
+
} catch {
|
|
196
|
+
if (cached) {
|
|
197
|
+
safeSetStatus(ctx, pastel(ctx.ui.theme, `Go ${cached.rolling.percent}/${cached.weekly.percent}/${cached.monthly.percent}%`));
|
|
198
|
+
} else {
|
|
199
|
+
safeSetStatus(ctx, pastel(ctx.ui.theme, "Go --"));
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
isFetching = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scheduleRefresh(ctx: ExtensionContext) {
|
|
207
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
208
|
+
debounceTimer = setTimeout(() => {
|
|
209
|
+
debounceTimer = null;
|
|
210
|
+
void refresh(ctx).catch(() => {});
|
|
211
|
+
}, 800);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- lifecycle ---
|
|
215
|
+
|
|
216
|
+
pi.on("session_start", async (_evt, ctx) => {
|
|
217
|
+
// Reload persisted preference on each session start (covers external edits)
|
|
218
|
+
compactVisible = loadCompactVisible();
|
|
219
|
+
if (!isGoActive(ctx)) {
|
|
220
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
ctx.ui.setStatus(STATUS_KEY, pastel(ctx.ui.theme, "Go …"));
|
|
224
|
+
await refresh(ctx);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("model_select", async (_evt, ctx) => {
|
|
228
|
+
if (!isGoActive(ctx)) {
|
|
229
|
+
if (debounceTimer) {
|
|
230
|
+
clearTimeout(debounceTimer);
|
|
231
|
+
debounceTimer = null;
|
|
232
|
+
}
|
|
233
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
ctx.ui.setStatus(STATUS_KEY, pastel(ctx.ui.theme, "Go …"));
|
|
237
|
+
await refresh(ctx);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
pi.on("turn_end", async (_evt, ctx) => {
|
|
241
|
+
if (!isGoActive(ctx)) return;
|
|
242
|
+
scheduleRefresh(ctx);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
pi.on("agent_settled", async (_evt, ctx) => {
|
|
246
|
+
if (!isGoActive(ctx)) return;
|
|
247
|
+
scheduleRefresh(ctx);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
pi.on("session_shutdown", async (_evt, ctx) => {
|
|
251
|
+
if (debounceTimer) {
|
|
252
|
+
clearTimeout(debounceTimer);
|
|
253
|
+
debounceTimer = null;
|
|
254
|
+
}
|
|
255
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- command: /go-usage (details) ---
|
|
259
|
+
pi.registerCommand("go-usage", {
|
|
260
|
+
description: "Show OpenCode Go usage (Rolling / Weekly / Monthly)",
|
|
261
|
+
handler: async (_args, ctx) => {
|
|
262
|
+
if (!isProviderConfigured(ctx)) {
|
|
263
|
+
ctx.ui.notify("opencode-go not configured — set OPENCODE_API_KEY in auth.json or env", "warning");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const hint = ctx.model?.provider !== "opencode-go"
|
|
268
|
+
? " (current model is not opencode-go — status bar hidden until you select a Go model)"
|
|
269
|
+
: "";
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const usage = await fetchUsage(ctx);
|
|
273
|
+
cached = usage;
|
|
274
|
+
if (isGoActive(ctx)) {
|
|
275
|
+
ctx.ui.setStatus(STATUS_KEY, renderCompact(usage, ctx.ui.theme));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fmtPct = (n: number) => `${String(n).padStart(3)}%`;
|
|
279
|
+
const fmtStatus = (s: UsageBucket["status"]) => (s === "rate-limited" ? "rate-limited ⚠ — " : "");
|
|
280
|
+
const fmtLine = (label: string, pct: number, status: UsageBucket["status"], resets: string) => {
|
|
281
|
+
const spaces = " ".repeat(8 - label.length); // align % vertically (max label 7 + colon = 8)
|
|
282
|
+
return ` ${label}:${spaces}${fmtPct(pct)} — ${fmtStatus(status)}resets in ${resets}`;
|
|
283
|
+
};
|
|
284
|
+
const lines = [
|
|
285
|
+
`OpenCode Go usage${hint}:`,
|
|
286
|
+
fmtLine("Rolling", usage.rolling.percent, usage.rolling.status, formatResetsIn(usage.rolling.resetsAt)),
|
|
287
|
+
fmtLine("Weekly", usage.weekly.percent, usage.weekly.status, formatResetsIn(usage.weekly.resetsAt)),
|
|
288
|
+
fmtLine("Monthly", usage.monthly.percent, usage.monthly.status, formatResetsIn(usage.monthly.resetsAt)),
|
|
289
|
+
``,
|
|
290
|
+
` Toggle compact status with /go-status`,
|
|
291
|
+
];
|
|
292
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
293
|
+
} catch (e) {
|
|
294
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
295
|
+
ctx.ui.notify(`Go usage fetch failed: ${msg}`, "error");
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// --- command: /go-status (toggle compact bar) ---
|
|
301
|
+
pi.registerCommand("go-status", {
|
|
302
|
+
description: "Toggle OpenCode Go compact status bar",
|
|
303
|
+
handler: async (_args, ctx) => {
|
|
304
|
+
compactVisible = !compactVisible;
|
|
305
|
+
saveCompactVisible(compactVisible);
|
|
306
|
+
|
|
307
|
+
if (!compactVisible) {
|
|
308
|
+
if (debounceTimer) {
|
|
309
|
+
clearTimeout(debounceTimer);
|
|
310
|
+
debounceTimer = null;
|
|
311
|
+
}
|
|
312
|
+
safeSetStatus(ctx, undefined);
|
|
313
|
+
ctx.ui.notify("Go compact: off — hidden from status bar", "info");
|
|
314
|
+
} else {
|
|
315
|
+
if (isProviderConfigured(ctx) && ctx.model?.provider === "opencode-go") {
|
|
316
|
+
ctx.ui.setStatus(STATUS_KEY, pastel(ctx.ui.theme, "Go …"));
|
|
317
|
+
ctx.ui.notify("Go compact: on — fetching…", "info");
|
|
318
|
+
await refresh(ctx);
|
|
319
|
+
} else if (!isProviderConfigured(ctx)) {
|
|
320
|
+
ctx.ui.notify("Go compact: on — will appear when opencode-go is configured and a Go model is selected", "info");
|
|
321
|
+
} else {
|
|
322
|
+
ctx.ui.notify("Go compact: on — will appear when a Go model is selected", "info");
|
|
323
|
+
safeSetStatus(ctx, undefined);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oscarfalero/pi-opencode-go",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode Go usage in pi status bar — rolling 5h / weekly / monthly + /go-usage + /go-status toggle",
|
|
5
|
+
"keywords": ["pi-package", "opencode", "pi-extension", "go"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "oscarfalero",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": ["extensions/", "README.md", "LICENSE"],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/oscarfalero/pi-opencode-go.git"
|
|
13
|
+
},
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": ["./extensions"]
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
19
|
+
}
|
|
20
|
+
}
|