@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 +12 -65
- package/README.md +8 -15
- package/dist/storage.js +1 -41
- package/dist/storage_parse.d.ts +1 -2
- package/dist/storage_parse.js +0 -3
- package/dist/title.js +0 -3
- package/dist/usage.d.ts +2 -2
- package/dist/usage.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,70 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.0.0
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
+
[](https://www.npmjs.com/package/@leo000001/opencode-quota-sidebar)
|
|
4
|
+
[](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
|
+

|
|
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
|
|
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
|
|
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 ────────────────────────────────────────────────────────────
|
package/dist/storage_parse.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SessionState
|
|
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>;
|
package/dist/storage_parse.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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,
|