Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr

- [**Amp**](docs/providers/amp.md) / free tier, bonus, credits
- [**Antigravity**](docs/providers/antigravity.md) / all models
- [**Claude**](docs/providers/claude.md) / session, weekly, peak/off-peak, extra usage, local token usage (ccusage)
- [**Claude**](docs/providers/claude.md) / session, weekly, extra usage, local token usage (ccusage)
- [**Codex**](docs/providers/codex.md) / session, weekly, reviews, credits
- [**Copilot**](docs/providers/copilot.md) / premium, chat, completions
- [**Cursor**](docs/providers/cursor.md) / credits, total usage, auto usage, API usage, on-demand, CLI auth
Expand Down
13 changes: 0 additions & 13 deletions docs/providers/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
- **Utilization:** integer percentage (0-100)
- **Credits:** cents (divide by 100 for dollars)
- **Timestamps:** ISO 8601 (response), unix milliseconds (credentials file)
- **Peak hours status:** supplemental best-effort data from PromoClock's public API; does not affect Claude usage math

## Endpoints

Expand Down Expand Up @@ -60,18 +59,6 @@ Returns rate limit windows and optional extra credits.

All windows are enforced simultaneously — hitting any limit throttles the user.

## Supplemental Peak Hours Status

OpenUsage also augments the Claude card with PromoClock peak/off-peak status:

- **Endpoint:** `GET https://promoclock.co/api/status`
- **Auth:** none
- **Fields used:** `isPeak`, `isOffPeak`, `isWeekend`, `status` (fallback)
- **UI mapping:** binary Peak / Off-Peak badge (weekend is treated as off-peak)
- **Failure mode:** ignored on network, HTTP, or payload errors; Claude usage lines still render normally

This is informational only. PromoClock is an independent public service, not an official Anthropic API.

## Authentication

### Token Location
Expand Down
67 changes: 0 additions & 67 deletions plugins/claude/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
const PROD_REFRESH_URL = "https://platform.claude.com/v1/oauth/token"
const PROD_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
const NON_PROD_CLIENT_ID = "22422756-60c9-4084-8eb7-27705fd5cf9a"
const PROMOCLOCK_STATUS_URL = "https://promoclock.co/api/status"
const PROMOCLOCK_PEAK_COLOR = "#ef4444"
const PROMOCLOCK_OFF_PEAK_COLOR = "#22c55e"
const SCOPES =
"user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
Expand Down Expand Up @@ -616,66 +613,6 @@
}))
}

function getPromoClockBadgeText(data) {
if (!data || typeof data !== "object") return null
if (data.isPeak === true) return "Peak"
if (data.isOffPeak === true || data.isWeekend === true) return "Off-Peak"

const status = typeof data.status === "string" ? data.status.trim().toLowerCase() : ""
if (status === "peak") return "Peak"
if (status === "off_peak" || status === "off-peak" || status === "weekend") return "Off-Peak"
return null
}

function getPromoClockColor(badgeText) {
if (badgeText === "Peak") return PROMOCLOCK_PEAK_COLOR
if (badgeText === "Off-Peak") return PROMOCLOCK_OFF_PEAK_COLOR
return null
}

function fetchPromoClockLine(ctx) {
let resp
let json
try {
const result = ctx.util.requestJson({
method: "GET",
url: PROMOCLOCK_STATUS_URL,
headers: {
Accept: "application/json",
},
timeoutMs: 2000,
})
resp = result.resp
json = result.json
} catch (e) {
ctx.host.log.warn("promoclock request failed: " + String(e))
return null
}

if (!resp || resp.status < 200 || resp.status >= 300) {
ctx.host.log.warn("promoclock returned unexpected status: " + String(resp && resp.status))
return null
}

if (!json || typeof json !== "object") {
ctx.host.log.warn("promoclock response invalid")
return null
}

const badgeText = getPromoClockBadgeText(json)

if (!badgeText) {
ctx.host.log.warn("promoclock response missing expected fields")
return null
}

return ctx.line.badge({
label: "Peak Hours",
text: badgeText,
color: getPromoClockColor(badgeText),
})
}

function probe(ctx) {
const creds = loadCredentials(ctx)
if (!creds || !creds.oauth || !creds.oauth.accessToken || !creds.oauth.accessToken.trim()) {
Expand Down Expand Up @@ -910,8 +847,6 @@
}
}

const promoClockLine = fetchPromoClockLine(ctx)

if (rateLimited) {
const retryText = retryAfterSeconds !== null
? fmtRateLimitMinutes(retryAfterSeconds)
Expand All @@ -928,8 +863,6 @@
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
}

if (promoClockLine) lines.push(promoClockLine)

return { plan: plan, lines: lines }
}

Expand Down
1 change: 0 additions & 1 deletion plugins/claude/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"lines": [
{ "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 },
{ "type": "progress", "label": "Weekly", "scope": "overview" },
{ "type": "badge", "label": "Peak Hours", "scope": "overview" },
{ "type": "progress", "label": "Sonnet", "scope": "detail" },
{ "type": "progress", "label": "Claude Design", "scope": "detail" },
{ "type": "progress", "label": "Extra usage spent", "scope": "detail" },
Expand Down
176 changes: 0 additions & 176 deletions plugins/claude/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,60 +22,6 @@ beforeEach(() => {

const loadPlugin = async () => plugin

const SAMPLE_PROMOCLOCK_RESPONSE = {
status: "off_peak",
isPeak: false,
isOffPeak: true,
isWeekend: false,
sessionLimitSpeed: "normal",
emoji: "🟢",
label: "Off-Peak — Normal Speed",
peakHours: "Weekdays 1pm–7pm UTC / 1:00 PM–7:00 PM GMT",
nextChange: "2026-04-09T13:00:00.000Z",
minutesUntilChange: 720,
timestamp: "2026-04-09T01:00:00.000Z",
utcHour: 1,
utcDay: 4,
note: "No known end date for peak hours adjustment. Weekly limits unchanged.",
}

function mockClaudeUsageAndPromoClock(
ctx,
{
usageBody = {
five_hour: { utilization: 10, resets_at: "2099-01-01T00:00:00.000Z" },
seven_day: { utilization: 20, resets_at: "2099-01-01T00:00:00.000Z" },
},
usageStatus = 200,
promoClockBody = SAMPLE_PROMOCLOCK_RESPONSE,
promoClockStatus = 200,
promoClockBodyText,
} = {}
) {
ctx.host.http.request.mockImplementation((opts) => {
const url = String(opts && opts.url ? opts.url : "")
if (url === "https://promoclock.co/api/status") {
return {
status: promoClockStatus,
headers: {},
bodyText:
promoClockBodyText !== undefined
? promoClockBodyText
: JSON.stringify(promoClockBody),
}
}

return {
status: usageStatus,
headers: {},
bodyText:
typeof usageBody === "string"
? usageBody
: JSON.stringify(usageBody),
}
})
}

describe("claude plugin", () => {
it("throws when no credentials", async () => {
const ctx = makeCtx()
Expand Down Expand Up @@ -452,120 +398,6 @@ describe("claude plugin", () => {
expect(result.lines.find((line) => line.label === "Weekly")).toBeTruthy()
})

describe("PromoClock integration", () => {
it("maps the real off-peak endpoint payload to the compact badge", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () =>
JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } })
ctx.host.fs.exists = () => true
mockClaudeUsageAndPromoClock(ctx)

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

expect(result.lines.find((line) => line.label === "Session")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Weekly")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Peak Hours")).toEqual({
type: "badge",
label: "Peak Hours",
text: "Off-Peak",
color: "#22c55e",
})
expect(result.lines.find((line) => line.label === "Next change")).toBeUndefined()
})

it("maps peak PromoClock responses into the badge-only UI", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () =>
JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } })
ctx.host.fs.exists = () => true
mockClaudeUsageAndPromoClock(ctx, {
promoClockBody: {
...SAMPLE_PROMOCLOCK_RESPONSE,
status: "peak",
isPeak: true,
isOffPeak: false,
emoji: "🔴",
label: "Peak Hours — Limits Drain Faster",
nextChange: "2026-04-08T19:00:00.000Z",
minutesUntilChange: 111,
timestamp: "2026-04-08T17:08:33.089Z",
utcHour: 17,
},
})

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

expect(result.lines.find((line) => line.label === "Peak Hours")?.text).toBe("Peak")
expect(result.lines.find((line) => line.label === "Peak Hours")?.color).toBe("#ef4444")
})

it("treats weekend as off-peak", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () =>
JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } })
ctx.host.fs.exists = () => true
mockClaudeUsageAndPromoClock(ctx, {
promoClockBody: {
...SAMPLE_PROMOCLOCK_RESPONSE,
status: "weekend",
isPeak: false,
isOffPeak: false,
isWeekend: true,
label: "Weekend — Normal Speed",
},
})

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

expect(result.lines.find((line) => line.label === "Peak Hours")?.text).toBe("Off-Peak")
expect(result.lines.find((line) => line.label === "Peak Hours")?.color).toBe("#22c55e")
})

it("ignores PromoClock failures and still returns Claude usage lines", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () =>
JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } })
ctx.host.fs.exists = () => true
mockClaudeUsageAndPromoClock(ctx, {
promoClockStatus: 503,
promoClockBody: { error: "temporarily unavailable" },
})

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

expect(result.lines.find((line) => line.label === "Session")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Weekly")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Peak Hours")).toBeUndefined()
expect(result.lines.find((line) => line.label === "Next change")).toBeUndefined()
})

it("falls back to status string when boolean flags are absent", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () =>
JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } })
ctx.host.fs.exists = () => true
mockClaudeUsageAndPromoClock(ctx, {
promoClockBody: {
...SAMPLE_PROMOCLOCK_RESPONSE,
status: "off_peak",
isPeak: undefined,
isOffPeak: undefined,
isWeekend: undefined,
},
})

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

expect(result.lines.find((line) => line.label === "Peak Hours")?.text).toBe("Off-Peak")
expect(result.lines.find((line) => line.label === "Peak Hours")?.color).toBe("#22c55e")
})
})

it("appends max rate limit tier to the plan label when present", async () => {
const runCase = async (rateLimitTier, expectedPlan) => {
const ctx = makeCtx()
Expand Down Expand Up @@ -2039,8 +1871,6 @@ describe("claude plugin", () => {
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
ctx.host.fs.exists = () => true
// Isolate Promoclock so it doesn't add extra calls to ctx.host.http.request
ctx.util.requestJson = vi.fn(() => ({ resp: { status: 200, bodyText: "{}", headers: {} }, json: {} }))
ctx.host.http.request.mockReturnValue({
status: 429,
bodyText: "",
Expand Down Expand Up @@ -2071,8 +1901,6 @@ describe("claude plugin", () => {
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
ctx.host.fs.exists = () => true
// Isolate Promoclock so it doesn't add extra calls to ctx.host.http.request
ctx.util.requestJson = vi.fn(() => ({ resp: { status: 200, bodyText: "{}", headers: {} }, json: {} }))
const usageBody = JSON.stringify({ five_hour: { utilization: 50, resets_at: null } })
ctx.host.http.request
.mockReturnValueOnce({ status: 429, bodyText: "", headers: { "Retry-After": "60" } })
Expand Down Expand Up @@ -2101,8 +1929,6 @@ describe("claude plugin", () => {
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
ctx.host.fs.exists = () => true
// Isolate Promoclock so it doesn't add extra calls to ctx.host.http.request
ctx.util.requestJson = vi.fn(() => ({ resp: { status: 200, bodyText: "{}", headers: {} }, json: {} }))
ctx.host.http.request.mockReturnValue({ status: 200, bodyText: "{}", headers: {} })
const plugin = await loadPlugin()

Expand Down Expand Up @@ -2160,8 +1986,6 @@ describe("claude plugin", () => {
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
ctx.host.fs.exists = () => true
// Isolate Promoclock so it doesn't add extra calls to ctx.host.http.request
ctx.util.requestJson = vi.fn(() => ({ resp: { status: 200, bodyText: "{}", headers: {} }, json: {} }))
ctx.host.http.request
.mockReturnValueOnce({ status: 429, bodyText: "", headers: {} }) // no Retry-After
.mockReturnValue({ status: 200, bodyText: "{}", headers: {} })
Expand Down
Loading