From bb68d90ef2d41edc4685fdad0dd466d554399ff3 Mon Sep 17 00:00:00 2001
From: Hung Pham Sy <25026496+hungps@users.noreply.github.com>
Date: Sun, 26 Apr 2026 16:18:16 +0700
Subject: [PATCH 1/5] feat(neuralwatt): add Neuralwatt provider plugin
---
plugins/neuralwatt/icon.svg | 3 +
plugins/neuralwatt/plugin.js | 148 ++++++++++++
plugins/neuralwatt/plugin.json | 13 ++
plugins/neuralwatt/plugin.test.js | 291 ++++++++++++++++++++++++
src-tauri/src/plugin_engine/host_api.rs | 3 +-
5 files changed, 457 insertions(+), 1 deletion(-)
create mode 100644 plugins/neuralwatt/icon.svg
create mode 100644 plugins/neuralwatt/plugin.js
create mode 100644 plugins/neuralwatt/plugin.json
create mode 100644 plugins/neuralwatt/plugin.test.js
diff --git a/plugins/neuralwatt/icon.svg b/plugins/neuralwatt/icon.svg
new file mode 100644
index 00000000..e011a3e5
--- /dev/null
+++ b/plugins/neuralwatt/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/plugins/neuralwatt/plugin.js b/plugins/neuralwatt/plugin.js
new file mode 100644
index 00000000..71eba38d
--- /dev/null
+++ b/plugins/neuralwatt/plugin.js
@@ -0,0 +1,148 @@
+(function () {
+ var API_KEY_ENV_VARS = ["NEURALWATT_API_KEY"]
+ var QUOTA_URL = "https://api.neuralwatt.com/v1/quota"
+
+ function readNumber(value) {
+ var n = Number(value)
+ return Number.isFinite(n) ? n : null
+ }
+
+ function readString(value) {
+ if (typeof value !== "string") return null
+ var trimmed = value.trim()
+ return trimmed || null
+ }
+
+ function parseDateMs(value) {
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
+ if (typeof value === "string") {
+ var parsed = Date.parse(value)
+ return Number.isFinite(parsed) ? parsed : null
+ }
+ return null
+ }
+
+ function parseSubscriptionPeriodMs(sub) {
+ if (!sub || !sub.current_period_start || !sub.current_period_end) return null
+ var startMs = parseDateMs(sub.current_period_start)
+ var endMs = parseDateMs(sub.current_period_end)
+ if (startMs !== null && endMs !== null && endMs > startMs) return endMs - startMs
+ return null
+ }
+
+ function loadApiKey(ctx) {
+ for (var i = 0; i < API_KEY_ENV_VARS.length; i += 1) {
+ var name = API_KEY_ENV_VARS[i]
+ var value = null
+ try {
+ value = ctx.host.env.get(name)
+ } catch (e) {
+ ctx.host.log.warn("env read failed for " + name + ": " + String(e))
+ }
+ if (value && typeof value === "string" && value.trim()) {
+ ctx.host.log.info("api key loaded from " + name)
+ return { value: value.trim(), source: name }
+ }
+ }
+ return null
+ }
+
+ function probe(ctx) {
+ var apiKeyInfo = loadApiKey(ctx)
+ if (!apiKeyInfo) {
+ throw "Neuralwatt API key missing. Set NEURALWATT_API_KEY."
+ }
+
+ var resp
+ try {
+ resp = ctx.util.request({
+ method: "GET",
+ url: QUOTA_URL,
+ headers: {
+ Authorization: "Bearer " + apiKeyInfo.value,
+ Accept: "application/json",
+ "User-Agent": "OpenUsage",
+ },
+ timeoutMs: 10000,
+ })
+ } catch (e) {
+ throw "Request failed. Check your connection."
+ }
+
+ if (ctx.util.isAuthStatus(resp.status)) {
+ throw "Invalid API key. Check NEURALWATT_API_KEY."
+ }
+ if (resp.status < 200 || resp.status >= 300) {
+ throw "Request failed (HTTP " + String(resp.status) + "). Try again later."
+ }
+
+ var data = ctx.util.tryParseJson(resp.bodyText)
+ if (!data || typeof data !== "object") {
+ throw "Response invalid. Try again later."
+ }
+
+ var sub = data.subscription && typeof data.subscription === "object" ? data.subscription : null
+ var balance = data.balance && typeof data.balance === "object" ? data.balance : null
+ var plan = null
+ var resetsAt = null
+ var periodDurationMs = null
+
+ if (sub) {
+ if (typeof sub.plan === "string" && sub.plan) {
+ plan = sub.plan.charAt(0).toUpperCase() + sub.plan.slice(1)
+ }
+ if (sub.current_period_end) {
+ var endMs = parseDateMs(sub.current_period_end)
+ resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null
+ }
+ periodDurationMs = parseSubscriptionPeriodMs(sub)
+ }
+
+ var lines = []
+
+ // Subscription energy line (hidden if no subscription)
+ if (sub) {
+ var kwhIncluded = readNumber(sub.kwh_included)
+ var kwhUsed = readNumber(sub.kwh_used)
+ if (kwhIncluded !== null && kwhIncluded > 0 && kwhUsed !== null) {
+ var energyLine = {
+ label: "Subscription",
+ used: Math.round(kwhUsed * 10000) / 10000,
+ limit: Math.round(kwhIncluded * 10000) / 10000,
+ format: { kind: "count", suffix: "kWh" },
+ }
+ if (resetsAt) energyLine.resetsAt = resetsAt
+ if (periodDurationMs) energyLine.periodDurationMs = periodDurationMs
+ lines.push(ctx.line.progress(energyLine))
+ }
+ }
+
+ // Balance line (hidden if total credits is 0)
+ if (balance) {
+ var totalCredits = readNumber(balance.total_credits_usd)
+ var usedCredits = readNumber(balance.credits_used_usd)
+ if (totalCredits !== null && totalCredits > 0 && usedCredits !== null) {
+ lines.push(ctx.line.progress({
+ label: "Balance",
+ used: Math.round(usedCredits * 100) / 100,
+ limit: Math.round(totalCredits * 100) / 100,
+ format: { kind: "dollars" },
+ }))
+ }
+
+ // Accounting method badge
+ var method = readString(balance.accounting_method)
+ if (method) {
+ lines.push(ctx.line.badge({ label: "Method", text: method.charAt(0).toUpperCase() + method.slice(1) }))
+ }
+ }
+
+ if (lines.length === 0) {
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
+ }
+
+ return { plan: plan, lines: lines }
+ }
+
+ globalThis.__openusage_plugin = { id: "neuralwatt", probe: probe }
+})()
diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json
new file mode 100644
index 00000000..167fbd93
--- /dev/null
+++ b/plugins/neuralwatt/plugin.json
@@ -0,0 +1,13 @@
+{
+ "schemaVersion": 1,
+ "id": "neuralwatt",
+ "name": "Neuralwatt",
+ "version": "0.0.2",
+ "entry": "plugin.js",
+ "icon": "icon.svg",
+ "brandColor": "#D55934",
+ "lines": [
+ { "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 },
+ { "type": "progress", "label": "Balance", "scope": "detail" }
+ ]
+}
diff --git a/plugins/neuralwatt/plugin.test.js b/plugins/neuralwatt/plugin.test.js
new file mode 100644
index 00000000..ddd47fb9
--- /dev/null
+++ b/plugins/neuralwatt/plugin.test.js
@@ -0,0 +1,291 @@
+import { beforeEach, describe, expect, it, vi } from "vitest"
+import { makeCtx } from "../test-helpers.js"
+
+const loadPlugin = async () => {
+ await import("./plugin.js")
+ return globalThis.__openusage_plugin
+}
+
+const FULL_RESPONSE = {
+ snapshot_at: "2026-04-16T18:30:00Z",
+ balance: {
+ credits_remaining_usd: 32.6774,
+ total_credits_usd: 52.34,
+ credits_used_usd: 19.6626,
+ accounting_method: "energy",
+ },
+ usage: {
+ lifetime: { cost_usd: 243.9145, requests: 37801, tokens: 1235477176, energy_kwh: 15.6009 },
+ current_month: { cost_usd: 160.1463, requests: 23902, tokens: 1116658995, energy_kwh: 9.7278 },
+ },
+ limits: { overage_limit_usd: null, rate_limit_tier: "standard" },
+ subscription: {
+ plan: "standard",
+ status: "active",
+ billing_interval: "month",
+ current_period_start: "2026-04-11T05:05:25Z",
+ current_period_end: "2026-05-11T05:05:25Z",
+ auto_renew: true,
+ kwh_included: 20.0,
+ kwh_used: 13.9023,
+ kwh_remaining: 6.0977,
+ in_overage: false,
+ },
+ key: { name: "my-production-key", allowance: null },
+}
+
+describe("neuralwatt plugin", () => {
+ beforeEach(() => {
+ delete globalThis.__openusage_plugin
+ vi.resetModules()
+ })
+
+ it("throws when API key is missing", async () => {
+ const ctx = makeCtx()
+ const plugin = await loadPlugin()
+ expect(() => plugin.probe(ctx)).toThrow("Neuralwatt API key missing")
+ })
+
+ it("renders subscription + balance + method from full response", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify(FULL_RESPONSE),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+
+ expect(result.plan).toBe("Standard")
+ expect(result.lines).toHaveLength(3)
+
+ const sub = result.lines.find((l) => l.label === "Subscription")
+ expect(sub).toBeTruthy()
+ expect(sub.used).toBeCloseTo(13.9023, 4)
+ expect(sub.limit).toBeCloseTo(20, 4)
+ expect(sub.format).toEqual({ kind: "count", suffix: "kWh" })
+
+ const bal = result.lines.find((l) => l.label === "Balance")
+ expect(bal).toBeTruthy()
+ expect(bal.used).toBe(19.66)
+ expect(bal.limit).toBe(52.34)
+ expect(bal.format).toEqual({ kind: "dollars" })
+ expect(bal.resetsAt).toBeUndefined()
+ expect(bal.periodDurationMs).toBeUndefined()
+
+ const method = result.lines.find((l) => l.label === "Method")
+ expect(method).toBeTruthy()
+ expect(method.text).toBe("Energy")
+ })
+
+ it("includes resetsAt and periodDurationMs from subscription period", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify(FULL_RESPONSE),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+
+ const sub = result.lines.find((l) => l.label === "Subscription")
+ expect(sub.resetsAt).toBeTruthy()
+ expect(sub.periodDurationMs).toBeTruthy()
+ })
+
+ it("hides subscription line when subscription is null", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify({
+ balance: { credits_remaining_usd: 10, total_credits_usd: 20, credits_used_usd: 10, accounting_method: "token" },
+ subscription: null,
+ }),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+
+ expect(result.lines.find((l) => l.label === "Subscription")).toBeUndefined()
+ const bal = result.lines.find((l) => l.label === "Balance")
+ expect(bal).toBeTruthy()
+ })
+
+ it("hides balance line when total credits is 0", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify({
+ balance: { credits_remaining_usd: 0, total_credits_usd: 0, credits_used_usd: 0, accounting_method: "energy" },
+ subscription: FULL_RESPONSE.subscription,
+ }),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+
+ expect(result.lines.find((l) => l.label === "Balance")).toBeUndefined()
+ expect(result.lines.find((l) => l.label === "Subscription")).toBeTruthy()
+ })
+
+ it("hides method badge when accounting_method is missing", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify({
+ balance: { credits_remaining_usd: 10, total_credits_usd: 20, credits_used_usd: 10 },
+ subscription: FULL_RESPONSE.subscription,
+ }),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+
+ expect(result.lines.find((l) => l.label === "Method")).toBeUndefined()
+ })
+
+ it("returns badge when no data at all", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify({ subscription: null, balance: null }),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+ expect(result.lines).toEqual([{ type: "badge", label: "Status", text: "No usage data", color: "#a3a3a3" }])
+ expect(result.plan).toBeNull()
+ })
+
+ it("throws on 401", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-bad-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({ status: 401, bodyText: "" })
+
+ const plugin = await loadPlugin()
+ expect(() => plugin.probe(ctx)).toThrow("Invalid API key")
+ })
+
+ it("throws on non-2xx", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({ status: 500, bodyText: "" })
+
+ const plugin = await loadPlugin()
+ expect(() => plugin.probe(ctx)).toThrow("HTTP 500")
+ })
+
+ it("throws on invalid JSON", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({ status: 200, bodyText: "not-json" })
+
+ const plugin = await loadPlugin()
+ expect(() => plugin.probe(ctx)).toThrow("Response invalid")
+ })
+
+ it("throws on network error", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ ctx.host.http.request.mockImplementation(() => {
+ throw new Error("network down")
+ })
+
+ const plugin = await loadPlugin()
+ expect(() => plugin.probe(ctx)).toThrow("Check your connection")
+ })
+
+ it("capitalizes plan name", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ const resp = JSON.parse(JSON.stringify(FULL_RESPONSE))
+ resp.subscription.plan = "premium"
+
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify(resp),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+ expect(result.plan).toBe("Premium")
+ })
+
+ it("capitalizes accounting method", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-test-key"
+ return null
+ })
+ const resp = JSON.parse(JSON.stringify(FULL_RESPONSE))
+ resp.balance.accounting_method = "token"
+
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify(resp),
+ })
+
+ const plugin = await loadPlugin()
+ const result = plugin.probe(ctx)
+ expect(result.lines.find((l) => l.label === "Method").text).toBe("Token")
+ })
+
+ it("sends correct authorization header", async () => {
+ const ctx = makeCtx()
+ ctx.host.env.get.mockImplementation((name) => {
+ if (name === "NEURALWATT_API_KEY") return "sk-my-key"
+ return null
+ })
+ ctx.host.http.request.mockReturnValue({
+ status: 200,
+ bodyText: JSON.stringify(FULL_RESPONSE),
+ })
+
+ const plugin = await loadPlugin()
+ plugin.probe(ctx)
+
+ const call = ctx.host.http.request.mock.calls[0][0]
+ expect(call.headers.Authorization).toBe("Bearer sk-my-key")
+ expect(call.url).toBe("https://api.neuralwatt.com/v1/quota")
+ expect(call.method).toBe("GET")
+ })
+})
diff --git a/src-tauri/src/plugin_engine/host_api.rs b/src-tauri/src/plugin_engine/host_api.rs
index a39ac09d..40b94b94 100644
--- a/src-tauri/src/plugin_engine/host_api.rs
+++ b/src-tauri/src/plugin_engine/host_api.rs
@@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
-const WHITELISTED_ENV_VARS: [&str; 16] = [
+const WHITELISTED_ENV_VARS: [&str; 17] = [
"CODEX_HOME",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_OAUTH_TOKEN",
@@ -29,6 +29,7 @@ const WHITELISTED_ENV_VARS: [&str; 16] = [
"MINIMAX_CN_API_KEY",
"SYNTHETIC_API_KEY",
"PI_CODING_AGENT_DIR",
+ "NEURALWATT_API_KEY",
];
fn last_non_empty_trimmed_line(text: &str) -> Option {
From d4f5fc05fab03cf9732eac203bc73ab74fd7feaf Mon Sep 17 00:00:00 2001
From: Hung Pham Sy <25026496+hungps@users.noreply.github.com>
Date: Tue, 28 Apr 2026 17:04:00 +0700
Subject: [PATCH 2/5] docs(neuralwatt): add Neuralwatt docs
---
README.md | 1 +
docs/providers/neuralwatt.md | 70 ++++++++++++++++++++++++++++++++++
plugins/neuralwatt/plugin.js | 8 +++-
plugins/neuralwatt/plugin.json | 2 +-
4 files changed, 78 insertions(+), 3 deletions(-)
create mode 100644 docs/providers/neuralwatt.md
diff --git a/README.md b/README.md
index d992761a..2c96c17e 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr
- [**Kiro**](docs/providers/kiro.md) / credits, bonus credits, overages
- [**Kimi Code**](docs/providers/kimi.md) / session, weekly
- [**MiniMax**](docs/providers/minimax.md) / coding plan session
+- [**Neuralwatt**](docs/providers/neuralwatt.md) / subscription energy, balance credits
- [**OpenCode Go**](docs/providers/opencode-go.md) / 5h, weekly, monthly spend limits
- [**Windsurf**](docs/providers/windsurf.md) / prompt credits, flex credits
- [**Z.ai**](docs/providers/zai.md) / session, weekly, web searches
diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md
new file mode 100644
index 00000000..3baed3d0
--- /dev/null
+++ b/docs/providers/neuralwatt.md
@@ -0,0 +1,70 @@
+# Neuralwatt
+
+> Uses the Neuralwatt quota API with a user-provided API key.
+
+## Overview
+
+- **Protocol:** HTTPS (JSON)
+- **Endpoint:** `GET https://api.neuralwatt.com/v1/quota`
+- **Auth:** `Authorization: Bearer `
+- **Env var:** `NEURALWATT_API_KEY`
+
+## Authentication
+
+The plugin reads `NEURALWATT_API_KEY` from the environment. If the key is missing, it throws:
+
+- `Neuralwatt API key missing. Set NEURALWATT_API_KEY.`
+
+## Data Source
+
+Request:
+
+```http
+GET /v1/quota HTTP/1.1
+Host: api.neuralwatt.com
+Authorization: Bearer
+Accept: application/json
+User-Agent: OpenUsage
+```
+
+Expected payload fields:
+
+- `balance.credits_remaining_usd`, `balance.total_credits_usd`, `balance.credits_used_usd`
+- `balance.accounting_method` (e.g. `"energy"`, `"token"`)
+- `subscription.plan`, `subscription.status`, `subscription.billing_interval`
+- `subscription.current_period_start`, `subscription.current_period_end`
+- `subscription.kwh_included`, `subscription.kwh_used`, `subscription.kwh_remaining`
+- `subscription.auto_renew`, `subscription.in_overage`
+
+## Usage Mapping
+
+- **Subscription** (progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`.
+- **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`.
+- **Method** (badge): `accounting_method`, capitalized. Hidden when absent.
+
+Both lines include `resetsAt` and `periodDurationMs` from the subscription period dates when available.
+
+## Output
+
+- **Plan**: from `subscription.plan`, capitalized
+- **Subscription** (overview progress line):
+ - `format`: count with `kWh` suffix
+ - `used`: `kwh_used`
+ - `limit`: `kwh_included`
+ - `resetsAt`: from `current_period_end`
+ - `periodDurationMs`: `current_period_end` – `current_period_start`
+- **Balance** (detail progress line):
+ - `format`: dollars
+ - `used`: `credits_used_usd`
+ - `limit`: `total_credits_usd`
+- **Method** (badge): capitalized `accounting_method`
+
+## Errors
+
+| Condition | Message |
+|---|---|
+| Missing API key | `Neuralwatt API key missing. Set NEURALWATT_API_KEY.` |
+| HTTP 401/403 | `Invalid API key. Check NEURALWATT_API_KEY.` |
+| Non-2xx | `Request failed (HTTP {status}). Try again later.` |
+| Network failure | `Request failed. Check your connection.` |
+| Unparseable payload | `Response invalid. Try again later.` |
diff --git a/plugins/neuralwatt/plugin.js b/plugins/neuralwatt/plugin.js
index 71eba38d..8954529b 100644
--- a/plugins/neuralwatt/plugin.js
+++ b/plugins/neuralwatt/plugin.js
@@ -3,8 +3,12 @@
var QUOTA_URL = "https://api.neuralwatt.com/v1/quota"
function readNumber(value) {
- var n = Number(value)
- return Number.isFinite(n) ? n : null
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
+ if (typeof value === "string") {
+ var parsed = Number(value)
+ return Number.isFinite(parsed) ? parsed : null
+ }
+ return null
}
function readString(value) {
diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json
index 167fbd93..e495583d 100644
--- a/plugins/neuralwatt/plugin.json
+++ b/plugins/neuralwatt/plugin.json
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"id": "neuralwatt",
"name": "Neuralwatt",
- "version": "0.0.2",
+ "version": "0.0.1",
"entry": "plugin.js",
"icon": "icon.svg",
"brandColor": "#D55934",
From e169a6991d376ee38f0a5d538d1b1a53d98bd043 Mon Sep 17 00:00:00 2001
From: Hung Pham Sy <25026496+hungps@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:16:34 +0700
Subject: [PATCH 3/5] docs(neuralwatt): resetAt and periodDurationMs only
supports subscription
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
docs/providers/neuralwatt.md | 2 +-
plugins/neuralwatt/plugin.json | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md
index 3baed3d0..37a6be7f 100644
--- a/docs/providers/neuralwatt.md
+++ b/docs/providers/neuralwatt.md
@@ -42,7 +42,7 @@ Expected payload fields:
- **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`.
- **Method** (badge): `accounting_method`, capitalized. Hidden when absent.
-Both lines include `resetsAt` and `periodDurationMs` from the subscription period dates when available.
+The **Subscription** line includes `resetsAt` and `periodDurationMs` from the subscription period dates when available; the **Balance** line does not.
## Output
diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json
index e495583d..558edbf7 100644
--- a/plugins/neuralwatt/plugin.json
+++ b/plugins/neuralwatt/plugin.json
@@ -8,6 +8,7 @@
"brandColor": "#D55934",
"lines": [
{ "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 },
- { "type": "progress", "label": "Balance", "scope": "detail" }
+ { "type": "progress", "label": "Method", "scope": "overview", "primaryOrder": 2 },
+ { "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 }
]
}
From dde3dd0c581163d90ad9979d62d2fca9cba611e2 Mon Sep 17 00:00:00 2001
From: Hung Pham Sy <25026496+hungps@users.noreply.github.com>
Date: Sun, 3 May 2026 14:53:43 +0700
Subject: [PATCH 4/5] fix(neuralwatt): correct Method line type from progress
to badge
plugin.json declared Method as a progress line with primaryOrder, but
probe() emits it as ctx.line.badge(). This mismatch caused wrong UI
skeleton rendering and incorrect tray primary-candidate ordering.
---
plugins/neuralwatt/plugin.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json
index 558edbf7..3f80f4af 100644
--- a/plugins/neuralwatt/plugin.json
+++ b/plugins/neuralwatt/plugin.json
@@ -8,7 +8,7 @@
"brandColor": "#D55934",
"lines": [
{ "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 },
- { "type": "progress", "label": "Method", "scope": "overview", "primaryOrder": 2 },
+ { "type": "badge", "label": "Method", "scope": "overview" },
{ "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 }
]
}
From fd78d3f0b30d81db74a4431fc09932282895d0a6 Mon Sep 17 00:00:00 2001
From: Hung Pham Sy <25026496+hungps@users.noreply.github.com>
Date: Sun, 3 May 2026 20:26:15 +0700
Subject: [PATCH 5/5] fix(neuralwatt): align manifest line order, scopes, and
Status badge with probe and docs
---
docs/providers/neuralwatt.md | 11 ++++++-----
plugins/neuralwatt/plugin.json | 5 +++--
plugins/neuralwatt/plugin.test.js | 3 +++
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md
index 37a6be7f..d6482a80 100644
--- a/docs/providers/neuralwatt.md
+++ b/docs/providers/neuralwatt.md
@@ -38,9 +38,9 @@ Expected payload fields:
## Usage Mapping
-- **Subscription** (progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`.
-- **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`.
-- **Method** (badge): `accounting_method`, capitalized. Hidden when absent.
+- **Subscription** (overview progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`.
+- **Balance** (overview progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`.
+- **Method** (detail badge): `accounting_method`, capitalized. Hidden when absent.
The **Subscription** line includes `resetsAt` and `periodDurationMs` from the subscription period dates when available; the **Balance** line does not.
@@ -53,11 +53,12 @@ The **Subscription** line includes `resetsAt` and `periodDurationMs` from the su
- `limit`: `kwh_included`
- `resetsAt`: from `current_period_end`
- `periodDurationMs`: `current_period_end` – `current_period_start`
-- **Balance** (detail progress line):
+- **Balance** (overview progress line):
- `format`: dollars
- `used`: `credits_used_usd`
- `limit`: `total_credits_usd`
-- **Method** (badge): capitalized `accounting_method`
+- **Method** (detail badge): capitalized `accounting_method`
+- **Status** (overview badge): shown as "No usage data" (gray) when subscription and balance are both absent
## Errors
diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json
index 3f80f4af..cc2f0177 100644
--- a/plugins/neuralwatt/plugin.json
+++ b/plugins/neuralwatt/plugin.json
@@ -8,7 +8,8 @@
"brandColor": "#D55934",
"lines": [
{ "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 },
- { "type": "badge", "label": "Method", "scope": "overview" },
- { "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 }
+ { "type": "progress", "label": "Balance", "scope": "overview" },
+ { "type": "badge", "label": "Method", "scope": "detail" },
+ { "type": "badge", "label": "Status", "scope": "overview" }
]
}
diff --git a/plugins/neuralwatt/plugin.test.js b/plugins/neuralwatt/plugin.test.js
index ddd47fb9..02dbdfd0 100644
--- a/plugins/neuralwatt/plugin.test.js
+++ b/plugins/neuralwatt/plugin.test.js
@@ -80,6 +80,9 @@ describe("neuralwatt plugin", () => {
const method = result.lines.find((l) => l.label === "Method")
expect(method).toBeTruthy()
expect(method.text).toBe("Energy")
+
+ // Line order must match manifest: Subscription, Balance, Method
+ expect(result.lines.map((l) => l.label)).toEqual(["Subscription", "Balance", "Method"])
})
it("includes resetsAt and periodDurationMs from subscription period", async () => {