@danielbdev/opencode-go-usage 0.1.2 → 0.1.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
@@ -18,11 +18,14 @@ A `Go usage` section at the top of the sidebar content region, above the context
18
18
 
19
19
  ```
20
20
  Go usage
21
- 5h 0% · resets in 4h 52m
22
- wk 2% · resets in 3d 3h
23
- mo 1% · resets in 24d 20h
21
+ 5h 2% · resets in 50m
22
+ wk 0% · resets in 4d 3h
23
+ mo 0% · resets in 25d 20h
24
24
  ```
25
25
 
26
+ A window whose provider status is anything other than `ok` renders that status after the countdown, so
27
+ an exhausted or over-limit window is visible instead of showing a bare percentage.
28
+
26
29
  `/usage` (or the command palette) opens the same data as a detail dialog and forces a refresh.
27
30
 
28
31
  ## How it works
@@ -127,6 +130,10 @@ The workflow checks that the tag matches the version in the manifest, so a misty
127
130
  instead of publishing the wrong thing. It runs the tests and the typecheck before publishing, and npm
128
131
  attaches a provenance attestation automatically because the repository and the package are public.
129
132
 
133
+ After a successful publish the workflow opens a **release page for the tag**, with notes generated from the
134
+ commits since the previous release. That is where "what changed in this version" gets answered; edit the
135
+ notes afterwards when the generated list is broader than the package being released.
136
+
130
137
  Versions are **independent per package**: touching one plugin never forces a release of another.
131
138
 
132
139
  The repository root stays at `0.0.0` and is never published; it only holds the workspace configuration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielbdev/opencode-go-usage",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "OpenCode V2 plugin that shows the OpenCode Go subscription usage in the session sidebar",
6
6
  "license": "MIT",
@@ -1,14 +1,19 @@
1
1
  import { test } from "node:test"
2
2
  import assert from "node:assert/strict"
3
3
 
4
+ import pkg from "../../package.json" with { type: "json" }
5
+
4
6
  import {
5
7
  errorSnapshot,
6
8
  formatPercent,
7
9
  formatReset,
8
- formatSummary,
10
+ formatStatus,
9
11
  formatWindow,
12
+ formatWindowDetail,
10
13
  parseSnapshot,
11
14
  parseUsage,
15
+ USER_AGENT,
16
+ type UsageWindow,
12
17
  } from "./usage.ts"
13
18
 
14
19
  const NOW = Date.parse("2026-10-14T20:35:38.000Z")
@@ -85,6 +90,15 @@ test("parseUsage reports an error snapshot for a non-object payload", () => {
85
90
  }
86
91
  })
87
92
 
93
+ test("parseUsage carries the provider status through untouched", () => {
94
+ const snapshot = parseUsage(
95
+ { usage: { rolling: { status: "overage", percent: 100, resetsAt: "2026-10-14T21:25:45.000Z" } } },
96
+ NOW,
97
+ )
98
+
99
+ assert.equal(snapshot.windows[0]?.status, "overage")
100
+ })
101
+
88
102
  test("formatPercent renders an integer percent or an em dash", () => {
89
103
  assert.equal(formatPercent(2), "2%")
90
104
  assert.equal(formatPercent(0), "0%")
@@ -109,19 +123,44 @@ test("formatReset handles the last minutes, the expired case and unknown input",
109
123
  assert.equal(formatReset("not-a-date", now), "unknown")
110
124
  })
111
125
 
112
- test("formatWindow and formatSummary compose the rendered line", () => {
126
+ test("formatWindow and formatWindowDetail compose the rendered line", () => {
113
127
  const snapshot = parseUsage(LIVE_PAYLOAD, NOW)
114
128
 
115
129
  assert.equal(formatWindow(snapshot.windows[0]!, NOW), "5h 2% · resets in 50m")
116
130
  assert.equal(formatWindow(snapshot.windows[1]!, NOW), "wk 0% · resets in 4d 3h")
117
- assert.equal(formatSummary(snapshot, NOW), "GO 5h 2% · wk 0% · mo 0%")
131
+ assert.equal(formatWindowDetail(snapshot.windows[0]!, NOW), "5h window 2% resets in 50m")
118
132
  })
119
133
 
120
- test("formatSummary surfaces the failure state instead of percentages", () => {
134
+ test("formatStatus stays silent in the steady state and surfaces anything else", () => {
135
+ // `ok` is the steady state, so it adds nothing to the line.
136
+ assert.equal(formatStatus("ok"), "")
137
+ assert.equal(formatStatus(null), "")
138
+ assert.equal(formatStatus(""), " ()")
139
+
140
+ // Any other status is unknown territory on an undocumented endpoint: show it
141
+ // verbatim rather than guessing a friendlier wording.
142
+ assert.equal(formatStatus("overage"), " (overage)")
143
+ assert.equal(formatStatus("exceeded"), " (exceeded)")
144
+ })
145
+
146
+ test("formatWindow and formatWindowDetail surface a non-ok provider status", () => {
147
+ const window: UsageWindow = {
148
+ key: "rolling",
149
+ short: "5h",
150
+ long: "5h window",
151
+ percent: 100,
152
+ resetsAt: "2026-10-14T21:25:45.000Z",
153
+ status: "overage",
154
+ }
155
+
156
+ assert.equal(formatWindow(window, NOW), "5h 100% · resets in 50m (overage)")
157
+ assert.equal(formatWindowDetail(window, NOW), "5h window 100% resets in 50m (overage)")
158
+ })
159
+
160
+ test("an error snapshot renders as an unknown window, not fabricated percentages", () => {
121
161
  const snapshot = errorSnapshot("Go integration is not connected", NOW)
122
162
 
123
163
  assert.equal(snapshot.ok, false)
124
- assert.equal(formatSummary(snapshot, NOW), "GO usage unavailable")
125
164
  assert.equal(formatWindow(snapshot.windows[0]!, NOW), "5h — · unknown")
126
165
  })
127
166
 
@@ -138,7 +177,6 @@ test("parseSnapshot accepts a snapshot that crossed the RPC boundary", () => {
138
177
  )
139
178
  assert.equal(received!.windows[0]!.short, "5h")
140
179
  assert.equal(received!.windows[0]!.long, "5h window")
141
- assert.equal(formatSummary(received!, NOW), "GO 5h 2% · wk 0% · mo 0%")
142
180
  })
143
181
 
144
182
  test("parseSnapshot rejects anything that is not a snapshot object", () => {
@@ -165,3 +203,8 @@ test("parseSnapshot degrades a malformed window entry instead of throwing", () =
165
203
  assert.equal(received!.windows[2]!.key, "monthly")
166
204
  })
167
205
 
206
+ test("USER_AGENT tracks the published package version", () => {
207
+ // Guards the drift a hardcoded literal invites: bump the manifest without
208
+ // touching the constant and this fails before the package is published.
209
+ assert.equal(USER_AGENT, `opencode-go-usage/${pkg.version}`)
210
+ })
package/src/core/usage.ts CHANGED
@@ -17,6 +17,17 @@
17
17
  * plain `node --test`.
18
18
  */
19
19
 
20
+ /**
21
+ * User agent sent to the usage endpoint.
22
+ *
23
+ * The version travels with the request so a provider-side failure is
24
+ * attributable to a build. It stays a literal deliberately: `usage.test.ts`
25
+ * pins it to `package.json`, so a release bump fails the suite before publish
26
+ * (bump this line alongside the manifest) instead of adding a JSON import to
27
+ * the server plugin's load path.
28
+ */
29
+ export const USER_AGENT = "opencode-go-usage/0.1.3"
30
+
20
31
  export const WINDOW_KEYS = ["rolling", "weekly", "monthly"] as const
21
32
 
22
33
  export type WindowKey = (typeof WINDOW_KEYS)[number]
@@ -173,9 +184,24 @@ export function formatReset(resetsAt: string | null, now: number): string {
173
184
  return `resets in ${minutes}m`
174
185
  }
175
186
 
187
+ /**
188
+ * Provider status that qualifies the percentage, e.g. ` (overage)`.
189
+ *
190
+ * `ok` is the steady state and renders as nothing, so the common line is
191
+ * unchanged. Any other non-null status is surfaced verbatim: the endpoint is
192
+ * undocumented, so a status is the only signal that a window needs attention and
193
+ * it is not ours to reinterpret.
194
+ */
195
+ export function formatStatus(status: string | null): string {
196
+ return status === null || status === "ok" ? "" : ` (${status})`
197
+ }
198
+
176
199
  /** One window as a single line, e.g. `5h 2% · resets in 50m`. */
177
200
  export function formatWindow(window: UsageWindow, now: number): string {
178
- return `${window.short} ${formatPercent(window.percent)} · ${formatReset(window.resetsAt, now)}`
201
+ return `${window.short} ${formatPercent(window.percent)} · ${formatReset(
202
+ window.resetsAt,
203
+ now,
204
+ )}${formatStatus(window.status)}`
179
205
  }
180
206
 
181
207
  /** One window as a detail row, e.g. `5h window 2% resets in 50m`. */
@@ -183,11 +209,5 @@ export function formatWindowDetail(window: UsageWindow, now: number): string {
183
209
  return `${window.long.padEnd(10)} ${formatPercent(window.percent).padStart(4)} ${formatReset(
184
210
  window.resetsAt,
185
211
  now,
186
- )}`
187
- }
188
-
189
- /** Compact status line for the TUI slot, e.g. `GO 5h 2% · wk 0% · mo 0%`. */
190
- export function formatSummary(snapshot: UsageSnapshot, _now: number): string {
191
- if (!snapshot.ok) return "GO usage unavailable"
192
- return `GO ${snapshot.windows.map((w) => `${w.short} ${formatPercent(w.percent)}`).join(" · ")}`
212
+ )}${formatStatus(window.status)}`
193
213
  }
package/src/index.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { Plugin } from "@opencode/plugin"
2
2
 
3
- import { errorSnapshot, parseUsage, type UsageSnapshot } from "./core/usage.ts"
3
+ import { errorSnapshot, parseUsage, USER_AGENT, type UsageSnapshot } from "./core/usage.ts"
4
4
  import { UsageRpc } from "./rpc.ts"
5
5
 
6
6
  const INTEGRATION_ID = "opencode-go"
7
7
  const USAGE_URL = "https://opencode.ai/inference/go/v1/usage"
8
- const USER_AGENT = "opencode-go-usage/0.1.0"
9
8
 
10
9
  /** Usage moves slowly; the endpoint is undocumented, so we stay conservative. */
11
10
  const REFRESH_MS = 5 * 60_000
@@ -75,12 +74,20 @@ export default Plugin.define({
75
74
  inFlight = null
76
75
  }
77
76
 
78
- await registration.events.emit("updated", {
79
- fetchedAt: snapshot.fetchedAt,
80
- ok: snapshot.ok,
81
- error: snapshot.error,
82
- windows: snapshot.windows.map((window) => ({ ...window })),
83
- })
77
+ // Publishing must not fail the refresh: the snapshot is already read, and
78
+ // `refresh()` is awaited by setup and fired-and-forgotten by the interval,
79
+ // where a rejection would surface as an unhandled rejection.
80
+ try {
81
+ await registration.events.emit("updated", {
82
+ fetchedAt: snapshot.fetchedAt,
83
+ ok: snapshot.ok,
84
+ error: snapshot.error,
85
+ windows: snapshot.windows.map((window) => ({ ...window })),
86
+ })
87
+ } catch {
88
+ // Subscribers are optional; the caller still gets the fresh snapshot.
89
+ }
90
+
84
91
  return snapshot
85
92
  }
86
93