@leo000001/opencode-quota-sidebar 1.0.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,70 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.0
4
4
 
5
- ### Features
6
-
7
- - Refactor quota provider handling to an adapter registry (`src/providers/*`), so adding a new provider no longer requires editing core dispatch code.
8
- - Add RightCode provider support (`https://www.right.codes/account/summary`) with baseURL-based matching.
9
- - RightCode subscription display now uses daily quota logic and ignores tiny plans (`total_quota < 10`); when not matched, falls back to account balance.
10
-
11
- ### UX
12
-
13
- - Merge Reasoning tokens into Output across sidebar, toast, and markdown report.
14
- - Sidebar quota windows now support indented multiline layout with compact reset time (`Rst HH:MM` / `Rst MM-DD`).
15
- - Toast now includes a dedicated `Cost as API` section with per-provider equivalent API cost.
16
- - Quota rendering now de-duplicates provider snapshots before output (prevents duplicate RightCode lines).
17
- - Remove `sidebar.maxQuotaProviders` config (unused in current design).
18
-
19
- ### Bug Fixes
20
-
21
- - Invalidate stale RightCode cache entries that still use old non-daily window formats (auto-refetch on next render).
22
-
23
- ### Bug Fixes (Critical)
24
-
25
- - H1: Fix `saveState` empty dirty keys triggering full writeAll on every save.
26
- - H2: Fix `persistState` dirty-key race condition — delete captured keys instead of clearing the whole set.
27
- - H3: Fix `pendingAppliedTitle` TTL corruption — detect decorated titles to prevent double-decoration; increase TTL to 15s.
28
- - H4: Fix `updateAuth` silently dropping token persistence — log and return error snapshot on failure.
29
- - H5: Add per-session concurrency lock for `applyTitle` to prevent conflicting concurrent updates.
30
-
31
- ### Bug Fixes (Medium)
5
+ Initial release.
32
6
 
33
- - M1: `refreshTimer` entries are now cleaned up when timers fire.
34
- - M2: Sessions older than `retentionDays` (default 730 days) are evicted from memory on startup.
35
- - M3: v1→v2 migration now recovers `createdAt` from v1 data when available.
36
- - M4: State and chunk file writes are now atomic (write to temp + rename).
37
- - M5: `flushSave` now flushes current dirty keys even when no timer is pending.
38
- - M6: `shortNumber` now handles negative, NaN, and Infinity values gracefully.
39
- - M7: `dateKeysInRange` is capped at 400 iterations to prevent runaway loops.
40
- - M8: Deduplicated `isRecord`/`asNumber`/`asBoolean` into shared `helpers.ts`.
41
- - M9: `scanSessionsByCreatedRange` now prefers in-memory state over disk reads.
42
- - M10: `summarizeRangeUsage` now fetches session messages in parallel (concurrency 5).
43
- - M11: `saveState` now only iterates sessions belonging to dirty date keys.
44
- - M12: Removed double timestamp normalization in `dateKeyFromTimestamp`.
45
-
46
- ### Performance
47
-
48
- - P1: Incremental usage aggregation — tracks last processed message cursor per session.
49
- - P2: LRU chunk cache (64 entries) for loaded day chunks.
50
- - P3: `restoreAllVisibleTitles` limited to concurrency 5.
51
- - P4: `sessionDateMap` dirty tracking integrated with chunk-level dirty keys.
52
-
53
- ### Security
54
-
55
- - S1: Replaced 15+ silent `.catch(() => undefined)` with debug-mode logging via `swallow()`.
56
- - S2: Added screen-sharing privacy warning to README.
57
- - S3: State file writes now refuse to follow symlinks.
58
- - S4: Renamed `OPENCODE_TEST_HOME` env var to `OPENCODE_QUOTA_DATA_HOME`.
59
-
60
- ### Open-Source Prep
61
-
62
- - O1: Added `repository`, `homepage`, `bugs`, `author` to package.json; moved SDK deps to `peerDependencies`.
63
- - O2: Added `*.tsbuildinfo`, `.DS_Store`, `coverage/`, `.env` to `.gitignore`.
64
- - O3: README config example now includes `sidebar.enabled` and `retentionDays`.
65
- - O4: Added unit tests for helpers, storage, and usage modules.
66
- - O5: Main entry now exports consumer types (`QuotaSidebarConfig`, `QuotaSnapshot`, etc.).
67
-
68
- ## 0.1.0
7
+ ### Features
69
8
 
70
- - Initial release.
9
+ - Show token usage (Input/Output/Cache) in session sidebar title.
10
+ - Show subscription quota for OpenAI Codex, GitHub Copilot, and RightCode.
11
+ - `quota_summary` tool — usage report for session/day/week/month (markdown + toast).
12
+ - `quota_show` tool — toggle sidebar title display on/off.
13
+ - Provider adapter registry — add new quota providers without editing core code.
14
+ - Incremental usage aggregation with per-session cursor.
15
+ - Date-partitioned storage with LRU chunk cache.
16
+ - API-equivalent cost display for subscription providers.
17
+ - Atomic file writes with symlink refusal.
package/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # opencode-quota-sidebar
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@leo000001/opencode-quota-sidebar.svg)](https://www.npmjs.com/package/@leo000001/opencode-quota-sidebar)
4
+ [![license](https://img.shields.io/npm/l/@leo000001/opencode-quota-sidebar.svg)](https://github.com/xihuai18/opencode-quota-sidebar/blob/main/LICENSE)
5
+
3
6
  OpenCode plugin: show token usage and subscription quota in the session sidebar title.
4
7
 
8
+ ![Example sidebar title with usage and quota](./assets/OpenCode-Quota-Sidebar.png)
9
+
5
10
  ## Install
6
11
 
7
12
  Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to install it automatically on startup:
@@ -27,6 +32,8 @@ Add the built file to your `opencode.json`:
27
32
  }
28
33
  ```
29
34
 
35
+ On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/index.js"`
36
+
30
37
  ## Supported quota providers
31
38
 
32
39
  | Provider | Endpoint | Auth | Status |
@@ -63,7 +70,7 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
63
70
  - Incremental usage aggregation — only processes new messages since last cursor
64
71
  - Sidebar token units are adaptive (`k`/`m` with one decimal where applicable)
65
72
 
66
- ## Storage layout (v2)
73
+ ## Storage layout
67
74
 
68
75
  The plugin stores lightweight global state and date-partitioned session chunks.
69
76
 
@@ -89,19 +96,9 @@ Example tree:
89
96
  24.json
90
97
  ```
91
98
 
92
- This replaces the old fixed-entry cap approach. `quota_summary` now scans date chunks
93
- for day/week/month ranges by session creation date.
94
-
95
99
  Sessions older than `retentionDays` (default 730 days / 2 years) are evicted from
96
100
  memory on startup. Chunk files remain on disk for historical range scans.
97
101
 
98
- ## Migration from v1
99
-
100
- If an old `quota-sidebar.state.json` exists (`version: 1`), the plugin migrates it
101
- to `version: 2` automatically on load and then persists data in the new chunked layout.
102
-
103
- On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/index.js"`
104
-
105
102
  ## Compatibility
106
103
 
107
104
  - Node.js: >= 18 (for `fetch` + `AbortController`)
@@ -183,10 +180,6 @@ Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
183
180
  - Session eviction counts
184
181
  - Symlink write refusals
185
182
 
186
- ## Independent repository
187
-
188
- This folder is initialized as its own git repository.
189
-
190
183
  ## Security & privacy notes
191
184
 
192
185
  - The plugin reads OpenCode credentials from `<opencode-data>/auth.json`.
package/dist/storage.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { asBoolean, asNumber, debug, isRecord, swallow } from './helpers.js';
4
4
  import { discoverChunks, readDayChunk, safeWriteFile, writeDayChunk, } from './storage_chunks.js';
5
5
  import { dateKeyFromTimestamp, dateKeysInRange, dateStartFromKey, isDateKey, normalizeTimestampMs, } from './storage_dates.js';
6
- import { parseQuotaCache, parseSessionTitleForMigration, } from './storage_parse.js';
6
+ import { parseQuotaCache } from './storage_parse.js';
7
7
  import { authFilePath, chunkRootPathFromStateFile, resolveOpencodeDataDir, stateFilePath, } from './storage_paths.js';
8
8
  export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeDataDir, stateFilePath, };
9
9
  // ─── Default config ──────────────────────────────────────────────────────────
@@ -131,40 +131,6 @@ async function loadVersion2State(raw, statePath) {
131
131
  quotaCache,
132
132
  };
133
133
  }
134
- /**
135
- * M3 fix: use session.createdAt from v1 state if available,
136
- * otherwise fall back to Date.now() (unavoidable for truly missing data).
137
- */
138
- function migrateVersion1State(raw) {
139
- const titleEnabled = asBoolean(raw.titleEnabled, true);
140
- const quotaCache = parseQuotaCache(raw.quotaCache);
141
- const sessionsRaw = isRecord(raw.sessions) ? raw.sessions : {};
142
- const sessions = {};
143
- const sessionDateMap = {};
144
- for (const [sessionID, value] of Object.entries(sessionsRaw)) {
145
- const title = parseSessionTitleForMigration(value);
146
- if (!title)
147
- continue;
148
- // M3: try to recover createdAt from v1 data
149
- const rawCreatedAt = isRecord(value) ? asNumber(value.createdAt) : undefined;
150
- const createdAt = rawCreatedAt
151
- ? normalizeTimestampMs(rawCreatedAt)
152
- : Date.now();
153
- const dateKey = dateKeyFromTimestamp(createdAt);
154
- sessions[sessionID] = {
155
- ...title,
156
- createdAt,
157
- };
158
- sessionDateMap[sessionID] = dateKey;
159
- }
160
- return {
161
- version: 2,
162
- titleEnabled,
163
- sessionDateMap,
164
- sessions,
165
- quotaCache,
166
- };
167
- }
168
134
  export async function loadState(statePath) {
169
135
  const raw = await fs
170
136
  .readFile(statePath, 'utf8')
@@ -174,12 +140,6 @@ export async function loadState(statePath) {
174
140
  return defaultState();
175
141
  if (raw.version === 2)
176
142
  return loadVersion2State(raw, statePath);
177
- if (raw.version === 1) {
178
- const migrated = migrateVersion1State(raw);
179
- // Persist immediately so chunk files exist for range scans.
180
- await saveState(statePath, migrated, { writeAll: true }).catch(swallow('loadState:migrate'));
181
- return migrated;
182
- }
183
143
  return defaultState();
184
144
  }
185
145
  // ─── State saving ────────────────────────────────────────────────────────────
@@ -1,4 +1,3 @@
1
- import type { SessionState, SessionTitleState } from './types.js';
1
+ import type { SessionState } from './types.js';
2
2
  export declare function parseSessionState(value: unknown): SessionState | undefined;
3
- export declare function parseSessionTitleForMigration(value: unknown): SessionTitleState | undefined;
4
3
  export declare function parseQuotaCache(value: unknown): Record<string, import("./types.js").QuotaSnapshot>;
@@ -76,9 +76,6 @@ export function parseSessionState(value) {
76
76
  cursor: parseCursor(value.cursor),
77
77
  };
78
78
  }
79
- export function parseSessionTitleForMigration(value) {
80
- return parseSessionTitleState(value);
81
- }
82
79
  export function parseQuotaCache(value) {
83
80
  const raw = isRecord(value) ? value : {};
84
81
  return Object.entries(raw).reduce((acc, [key, item]) => {
package/dist/title.js CHANGED
@@ -28,9 +28,6 @@ export function looksDecorated(title) {
28
28
  return true;
29
29
  if (/^\$\S+\s+as API cost/.test(line))
30
30
  return true;
31
- // Backward compatibility: old plugin versions had a separate Reasoning line.
32
- if (/^Reasoning\s+\S+/.test(line))
33
- return true;
34
31
  if (/^(OpenAI|Copilot|Anthropic|RightCode|RC)\b/.test(line))
35
32
  return true;
36
33
  return false;
package/dist/usage.d.ts CHANGED
@@ -4,7 +4,7 @@ export type ProviderUsage = {
4
4
  providerID: string;
5
5
  input: number;
6
6
  output: number;
7
- /** Legacy field kept for cache compatibility; display merges into output. */
7
+ /** Reasoning tokens (merged into output for display; persisted as 0). */
8
8
  reasoning: number;
9
9
  cacheRead: number;
10
10
  cacheWrite: number;
@@ -16,7 +16,7 @@ export type ProviderUsage = {
16
16
  export type UsageSummary = {
17
17
  input: number;
18
18
  output: number;
19
- /** Legacy field kept for cache compatibility; display merges into output. */
19
+ /** Reasoning tokens (merged into output for display; persisted as 0). */
20
20
  reasoning: number;
21
21
  cacheRead: number;
22
22
  cacheWrite: number;
package/dist/usage.js CHANGED
@@ -185,7 +185,7 @@ export function toCachedSessionUsage(summary) {
185
185
  acc[providerID] = {
186
186
  input: provider.input,
187
187
  output: provider.output,
188
- // Legacy persisted field for backward compatibility with old chunks.
188
+ // Always 0 after merge into output; kept for serialization shape.
189
189
  reasoning: provider.reasoning,
190
190
  cacheRead: provider.cacheRead,
191
191
  cacheWrite: provider.cacheWrite,
@@ -199,7 +199,7 @@ export function toCachedSessionUsage(summary) {
199
199
  return {
200
200
  input: summary.input,
201
201
  output: summary.output,
202
- // Legacy persisted field for backward compatibility with old chunks.
202
+ // Always 0 after merge into output; kept for serialization shape.
203
203
  reasoning: summary.reasoning,
204
204
  cacheRead: summary.cacheRead,
205
205
  cacheWrite: summary.cacheWrite,
@@ -211,7 +211,7 @@ export function toCachedSessionUsage(summary) {
211
211
  };
212
212
  }
213
213
  export function fromCachedSessionUsage(cached, sessionCount = 1) {
214
- // Merge legacy cached reasoning into output for a single output metric.
214
+ // Merge cached reasoning into output for a single output metric.
215
215
  const mergedOutputValue = cached.output + cached.reasoning;
216
216
  return {
217
217
  input: cached.input,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",