dsh-opencode-go-usage 1.1.1 → 1.2.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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
+ > ⚡ Get an **OpenCode GO plan**: [buycodingplan.com](https://buycodingplan.com/)
6
+
5
7
  A [DSH](https://github.com/deepseek-ai/deepseek-harness) (DeepSeek Harness) plugin that watches your **OpenCode GO plan** quota — the $10/month subscription that gives you usage limits on open-source models (rolling 5-hour, weekly, and monthly windows).
6
8
 
7
9
  ## Features
package/README.zh.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
+ > ⚡ **OpenCode GO 套餐**: [buycodingplan.com](https://buycodingplan.com/)
6
+
5
7
  [DSH](https://github.com/deepseek-ai/deepseek-harness)(DeepSeek Harness)插件:监控你的 **OpenCode GO 套餐**额度 —— 10 美元/月的订阅,按模型提供滚动 5 小时 / 每周 / 每月三个窗口的用量限额。
6
8
 
7
9
  ## 功能
package/lib/client.js CHANGED
@@ -25,19 +25,23 @@ window.__ModuleLoader__.load({
25
25
  tag.dataset.plugin = "dsh-opencode-go-usage";
26
26
  tag.dataset.pluginCss = CSS_ID;
27
27
  tag.textContent = [
28
- ".ocg-widget{box-sizing:border-box;width:100%;min-width:0;padding:8px 6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
28
+ ".ocg-widget{box-sizing:border-box;width:100%;min-width:0;padding:6px 6px 4px;display:flex;flex-direction:column;gap:4px;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
29
29
  ".ocg-widget:hover{background:var(--dsw-alias-interactive-bg-hover)}",
30
30
  ".ocg-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;font-weight:600;line-height:18px}",
31
31
  ".ocg-refresh{cursor:pointer;border:none;background:none;padding:0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:18px}",
32
32
  ".ocg-refresh:hover{color:var(--dsw-alias-label-primary)}",
33
33
  ".ocg-row{display:flex;flex-direction:column;gap:3px;min-width:0}",
34
- ".ocg-row-label{display:flex;justify-content:space-between;gap:8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
34
+ ".ocg-row-label{display:flex;justify-content:space-between;gap:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary)}",
35
35
  ".ocg-row-label b{font-weight:500;color:var(--dsw-alias-label-primary)}",
36
+ ".ocg-meta{display:flex;align-items:center;gap:6px;min-width:0}",
37
+ ".ocg-meta .ocg-track{flex:1;min-width:0}",
38
+ ".ocg-meta>span{font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}",
36
39
  ".ocg-track{box-sizing:border-box;height:5px;border-radius:3px;background:var(--dsw-alias-border-l2);overflow:hidden}",
37
40
  ".ocg-fill{height:100%;border-radius:3px;transition:width .4s ease}",
38
41
  ".ocg-err{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
39
42
  ".ocg-rail{box-sizing:border-box;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:10px;font-weight:700;color:var(--dsw-alias-label-primary);cursor:default}",
40
- ".ocg-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}"
43
+ ".ocg-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}",
44
+ ".ocg-update{display:inline-block;padding:1px 6px;border-radius:8px;font-size:10px;line-height:16px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);text-decoration:none;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:110px}"
41
45
  ].join("");
42
46
  document.head.appendChild(tag);
43
47
  }
@@ -48,31 +52,33 @@ window.__ModuleLoader__.load({
48
52
  "window.rolling": "滚动窗口 (5h)",
49
53
  "window.weekly": "周窗口",
50
54
  "window.monthly": "月窗口",
51
- "window.rolling.hint": "5 小时",
52
- "status.ok": "正常",
55
+ "window.rolling.hint": "5h",
53
56
  "time.soon": "即将重置",
54
- "time.minutes": "{n} 分后重置",
55
- "time.hours": "{n} 小时 {m} 分后",
56
- "time.days": "{n} {m} 小时后",
57
+ "time.minutes": "{n}",
58
+ "time.hours": "{n}小时{m}",
59
+ "time.days": "{n}天{m}小时",
57
60
  "refresh": "刷新",
58
61
  "loading": "加载中…",
59
62
  "rail.title": "OpenCode GO 月用量 {pct}%",
60
- "rail.plain": "OpenCode GO"
63
+ "rail.plain": "OpenCode GO",
64
+ "update.available": "新版本 v{v}",
65
+ "update.title": "点击查看升级说明"
61
66
  };
62
67
  const en = {
63
68
  "window.rolling": "Rolling (5h)",
64
69
  "window.weekly": "Weekly",
65
70
  "window.monthly": "Monthly",
66
- "window.rolling.hint": "5 hours",
67
- "status.ok": "OK",
71
+ "window.rolling.hint": "5h",
68
72
  "time.soon": "resets soon",
69
- "time.minutes": "in {n} min",
70
- "time.hours": "in {n}h {m}m",
71
- "time.days": "in {n}d {m}h",
73
+ "time.minutes": "{n}m",
74
+ "time.hours": "{n}h{m}m",
75
+ "time.days": "{n}d{m}h",
72
76
  "refresh": "Refresh",
73
77
  "loading": "Loading…",
74
78
  "rail.title": "OpenCode GO monthly {pct}%",
75
- "rail.plain": "OpenCode GO"
79
+ "rail.plain": "OpenCode GO",
80
+ "update.available": "v{v} available",
81
+ "update.title": "Click for upgrade instructions"
76
82
  };
77
83
 
78
84
  // ── data helpers ──
@@ -95,11 +101,11 @@ window.__ModuleLoader__.load({
95
101
  return t("time.days", { n: Math.floor(hours / 24), m: hours % 24 });
96
102
  }
97
103
 
98
- /** Fill color by usage percent. */
104
+ /** Fill color by usage percent — themed alias tokens (light/dark safe). */
99
105
  function fillColor(percent) {
100
- if (percent >= 90) return "#ff3b30";
101
- if (percent >= 70) return "#ff9500";
102
- return "#34c759";
106
+ if (percent >= 90) return "var(--dsw-alias-state-error-primary)";
107
+ if (percent >= 70) return "var(--dsw-alias-state-warn-primary)";
108
+ return "var(--dsw-alias-state-success-primary)";
103
109
  }
104
110
 
105
111
  // ── widget component ──
@@ -159,28 +165,34 @@ window.__ModuleLoader__.load({
159
165
  rows.push(react_jsx_runtime.jsx("div", {
160
166
  className: "ocg-row",
161
167
  children: [
168
+ // Name + percentage on one line (percentage right-aligned).
162
169
  react_jsx_runtime.jsx("div", {
163
170
  className: "ocg-row-label",
164
171
  children: [
165
172
  react_jsx_runtime.jsx("b", { children: t(w.labelKey) }),
166
- react_jsx_runtime.jsx("span", { children: (win.resetsAt ? timeUntil(win.resetsAt, t) : (w.hintKey ? t(w.hintKey) : "")) })
173
+ react_jsx_runtime.jsx("b", { children: win.percent + "%" })
167
174
  ]
168
175
  }),
176
+ // Track + countdown on one line (track flexes, countdown
177
+ // right-aligned small text; status replaces it when not ok).
169
178
  react_jsx_runtime.jsx("div", {
170
- className: "ocg-track",
171
- children: react_jsx_runtime.jsx("div", {
172
- className: "ocg-fill",
173
- style: {
174
- width: Math.max(0, Math.min(100, win.percent)) + "%",
175
- background: fillColor(win.percent)
176
- }
177
- })
178
- }),
179
- react_jsx_runtime.jsx("div", {
180
- className: "ocg-row-label",
179
+ className: "ocg-meta",
181
180
  children: [
182
- react_jsx_runtime.jsx("span", { children: win.status === "ok" ? t("status.ok") : win.status }),
183
- react_jsx_runtime.jsx("span", { children: win.percent + "%" })
181
+ react_jsx_runtime.jsx("div", {
182
+ className: "ocg-track",
183
+ children: react_jsx_runtime.jsx("div", {
184
+ className: "ocg-fill",
185
+ style: {
186
+ width: Math.max(0, Math.min(100, win.percent)) + "%",
187
+ background: fillColor(win.percent)
188
+ }
189
+ })
190
+ }),
191
+ react_jsx_runtime.jsx("span", {
192
+ children: win.status === "ok"
193
+ ? (win.resetsAt ? timeUntil(win.resetsAt, t) : (w.hintKey ? t(w.hintKey) : ""))
194
+ : win.status
195
+ })
184
196
  ]
185
197
  })
186
198
  ]
@@ -194,6 +206,16 @@ window.__ModuleLoader__.load({
194
206
  className: "ocg-head",
195
207
  children: [
196
208
  react_jsx_runtime.jsx("span", { children: "OpenCode GO" }),
209
+ (data && data.update && data.update.available && data.update.latest
210
+ ? react_jsx_runtime.jsx("a", {
211
+ className: "ocg-update",
212
+ href: "https://www.npmjs.com/package/dsh-opencode-go-usage",
213
+ target: "_blank",
214
+ rel: "noreferrer",
215
+ title: t("update.title"),
216
+ children: t("update.available", { v: data.update.latest })
217
+ })
218
+ : null),
197
219
  react_jsx_runtime.jsx("button", {
198
220
  className: "ocg-refresh",
199
221
  onClick: () => setStamp(stamp + 1),
package/lib/index.js CHANGED
@@ -23,9 +23,12 @@
23
23
  * settings.
24
24
  *
25
25
  * Settings namespace `dsh-opencode-go-usage`:
26
- * - apiKeyEnv: credential ref / env var for the API key (default OPENCODE_GO_API_KEY)
27
- * - baseUrl: GO gateway base (default https://opencode.ai/zen/go)
28
- * - cacheMs: host-side cache TTL (default 30000)
26
+ * - apiKeyEnv: credential ref / env var for the API key (default OPENCODE_GO_API_KEY)
27
+ * - baseUrl: GO gateway base (default https://opencode.ai/zen/go)
28
+ * - cacheMs: host-side cache TTL (default 30000)
29
+ * - updateCheck: check npm for newer versions (default true); result shows
30
+ * in the widget and the command — the plugin never installs
31
+ * itself, upgrading stays an explicit user action.
29
32
  */
30
33
 
31
34
  import { readFileSync } from "node:fs";
@@ -43,12 +46,43 @@ export const namespace = "dsh-opencode-go-usage";
43
46
  const DEFAULT_BASE_URL = "https://opencode.ai/zen/go";
44
47
  const DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
45
48
  const DEFAULT_CACHE_MS = 30_000;
49
+ /** npm package name this plugin is published under (update checks). */
50
+ const NPM_PACKAGE = "dsh-opencode-go-usage";
51
+ /** How often the update check may hit the npm registry (ms). */
52
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 3600_000;
53
+
54
+ /** The version this running copy was installed as (read once at load). */
55
+ const CURRENT_VERSION = readPackageVersion();
56
+
57
+ /** Read `version` from the installed package.json next to this file. */
58
+ function readPackageVersion() {
59
+ try {
60
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
61
+ if (typeof pkg?.version === "string") return pkg.version;
62
+ } catch {
63
+ // fall through
64
+ }
65
+ return "0.0.0";
66
+ }
67
+
68
+ /** Compare two `x.y.z` version strings; returns true when `a` is newer than `b`. */
69
+ function isNewer(a, b) {
70
+ const pa = String(a).split("-")[0].split(".").map(Number);
71
+ const pb = String(b).split("-")[0].split(".").map(Number);
72
+ for (let i = 0; i < 3; i++) {
73
+ const na = pa[i] ?? 0;
74
+ const nb = pb[i] ?? 0;
75
+ if (na !== nb) return na > nb;
76
+ }
77
+ return false;
78
+ }
46
79
 
47
80
  /** Settings schema for this plugin's namespace. */
48
81
  export const Config = z.object({
49
82
  apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV),
50
83
  baseUrl: z.string().default(DEFAULT_BASE_URL),
51
- cacheMs: z.number().default(DEFAULT_CACHE_MS)
84
+ cacheMs: z.number().default(DEFAULT_CACHE_MS),
85
+ updateCheck: z.boolean().default(true)
52
86
  });
53
87
 
54
88
  /**
@@ -150,10 +184,50 @@ export function apply(ctx, rawConfig = {}) {
150
184
  apiKeyEnv: stored.apiKeyEnv,
151
185
  baseUrl: stored.baseUrl,
152
186
  cacheMs: stored.cacheMs,
187
+ updateCheck: stored.updateCheck !== false,
153
188
  locale
154
189
  };
155
190
  };
156
191
 
192
+ // ── update check (npm) ─────────────────────────────────────────────────
193
+ // Checks the registry at most once per UPDATE_CHECK_INTERVAL_MS; failures
194
+ // are silent. The plugin only reports — upgrading stays a user action
195
+ // (`dsh plugin --profile web add dsh-opencode-go-usage@latest`).
196
+ /** @type {{ at: number, latest: string | null } | null} */
197
+ let updateState = null;
198
+ const checkForUpdate = async (config) => {
199
+ const now = Date.now();
200
+ if (updateState !== null && now - updateState.at < UPDATE_CHECK_INTERVAL_MS) return updateState;
201
+ const snapshot = { at: now, latest: null };
202
+ try {
203
+ const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, {
204
+ signal: AbortSignal.timeout(10_000)
205
+ });
206
+ if (res.ok) {
207
+ const json = await res.json();
208
+ if (typeof json?.version === "string" && isNewer(json.version, CURRENT_VERSION)) {
209
+ snapshot.latest = json.version;
210
+ }
211
+ }
212
+ } catch {
213
+ // offline / registry hiccup — keep the previous result, refresh later
214
+ snapshot.at = updateState?.at ?? now;
215
+ snapshot.latest = updateState?.latest ?? null;
216
+ }
217
+ updateState = snapshot;
218
+ return snapshot;
219
+ };
220
+ const updateInfoOf = (config) => {
221
+ const st = updateState;
222
+ if (!config.updateCheck || st === null || st.latest === null) {
223
+ return { current: CURRENT_VERSION, available: false, latest: null };
224
+ }
225
+ return { current: CURRENT_VERSION, available: true, latest: st.latest };
226
+ };
227
+
228
+ // Kick off the first check shortly after boot (non-blocking).
229
+ void checkForUpdate(readConfig()).catch(() => {});
230
+
157
231
  // Host-side cache: one in-flight promise + a TTL, so several open tabs or
158
232
  // the command never hammer the gateway.
159
233
  /** @type {{ at: number, promise: Promise<unknown> } | null} */
@@ -180,8 +254,12 @@ export function apply(ctx, rawConfig = {}) {
180
254
  res.setHeader("content-type", "application/json; charset=utf-8");
181
255
  res.setHeader("cache-control", "no-store");
182
256
  try {
257
+ const config = readConfig();
258
+ // Trigger a background refresh when the interval elapsed (never
259
+ // block the response on the registry).
260
+ void checkForUpdate(config).catch(() => {});
183
261
  const data = await quotaOnce();
184
- res.end(JSON.stringify(data));
262
+ res.end(JSON.stringify({ ...data, update: updateInfoOf(config) }));
185
263
  } catch (error) {
186
264
  res.statusCode = 502;
187
265
  res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
@@ -235,8 +313,16 @@ export function apply(ctx, rawConfig = {}) {
235
313
  description: "show OpenCode GO plan usage (rolling/weekly/monthly windows)",
236
314
  handler: async () => {
237
315
  try {
316
+ const config = readConfig();
238
317
  const data = await quotaOnce();
239
- return { kind: "success", text: renderUsageText(data, readConfig().locale) };
318
+ let text = renderUsageText(data, config.locale);
319
+ const update = updateInfoOf(config);
320
+ if (update.available && update.latest !== null) {
321
+ text += config.locale === "en"
322
+ ? `\n\nUpdate available: v${update.latest} — run \`dsh plugin --profile web add ${NPM_PACKAGE}@latest\` to upgrade.`
323
+ : `\n\n发现新版本 v${update.latest} — 执行 \`dsh plugin --profile web add ${NPM_PACKAGE}@latest\` 升级。`;
324
+ }
325
+ return { kind: "success", text };
240
326
  } catch (error) {
241
327
  return { kind: "error", text: error instanceof Error ? error.message : String(error) };
242
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-go-usage",
3
- "version": "1.1.1",
3
+ "version": "1.2.2",
4
4
  "description": "DSH (DeepSeek Harness) plugin: OpenCode GO plan quota widget in the sidebar, same-origin usage proxy, and a /opencode-go chat command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",