@leo000001/opencode-quota-sidebar 1.7.0 → 1.8.0
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 +32 -2
- package/dist/index.js +8 -2
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +57 -46
- package/dist/storage_paths.d.ts +6 -0
- package/dist/storage_paths.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,9 +136,35 @@ You can add these command templates in `opencode.json` so you can run `/qday`, `
|
|
|
136
136
|
}
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
##
|
|
139
|
+
## Configuration files
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Recommended global config:
|
|
142
|
+
|
|
143
|
+
- `~/.config/opencode/quota-sidebar.config.json`
|
|
144
|
+
|
|
145
|
+
Optional project overrides:
|
|
146
|
+
|
|
147
|
+
- `<worktree>/quota-sidebar.config.json`
|
|
148
|
+
- `<worktree>/.opencode/quota-sidebar.config.json`
|
|
149
|
+
|
|
150
|
+
Optional explicit override:
|
|
151
|
+
|
|
152
|
+
- `OPENCODE_QUOTA_CONFIG=/absolute/path/to/config.json`
|
|
153
|
+
|
|
154
|
+
Optional config-home override:
|
|
155
|
+
|
|
156
|
+
- `OPENCODE_QUOTA_CONFIG_HOME=/absolute/path/to/config-home`
|
|
157
|
+
|
|
158
|
+
Resolution order (low -> high):
|
|
159
|
+
|
|
160
|
+
1. Global config (`~/.config/opencode/...`)
|
|
161
|
+
2. Project root config
|
|
162
|
+
3. `.opencode` project config
|
|
163
|
+
4. `OPENCODE_QUOTA_CONFIG`
|
|
164
|
+
|
|
165
|
+
Values are layered; later sources override earlier ones.
|
|
166
|
+
|
|
167
|
+
Example config:
|
|
142
168
|
|
|
143
169
|
```json
|
|
144
170
|
{
|
|
@@ -215,6 +241,10 @@ Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
|
|
|
215
241
|
- State/chunk file writes refuse to write through symlinked targets (best-effort defense-in-depth).
|
|
216
242
|
- The `OPENCODE_QUOTA_DATA_HOME` env var overrides the OpenCode data directory
|
|
217
243
|
path (for testing); do not set this in production.
|
|
244
|
+
- The `OPENCODE_QUOTA_CONFIG_HOME` env var overrides global config directory
|
|
245
|
+
lookup (`<config-home>/opencode`).
|
|
246
|
+
- The `OPENCODE_QUOTA_CONFIG` env var points to an explicit config file and
|
|
247
|
+
applies as the highest-priority override.
|
|
218
248
|
|
|
219
249
|
## Contributing
|
|
220
250
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { renderMarkdownReport, renderSidebarTitle, renderToastMessage, } from './format.js';
|
|
3
3
|
import { createQuotaRuntime } from './quota.js';
|
|
4
|
-
import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, resolveOpencodeDataDir, saveState, stateFilePath, } from './storage.js';
|
|
4
|
+
import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, resolveOpencodeConfigDir, resolveOpencodeDataDir, saveState, stateFilePath, } from './storage.js';
|
|
5
5
|
import { debug, swallow } from './helpers.js';
|
|
6
6
|
import { normalizeBaseTitle } from './title.js';
|
|
7
7
|
import { createDescendantsResolver } from './descendants.js';
|
|
@@ -14,9 +14,15 @@ import { createUsageService } from './usage_service.js';
|
|
|
14
14
|
import { createTitleApplicator } from './title_apply.js';
|
|
15
15
|
export async function QuotaSidebarPlugin(input) {
|
|
16
16
|
const quotaRuntime = createQuotaRuntime();
|
|
17
|
+
const configDir = resolveOpencodeConfigDir();
|
|
18
|
+
const configOverride = process.env.OPENCODE_QUOTA_CONFIG?.trim();
|
|
17
19
|
const config = await loadConfig([
|
|
18
|
-
path.join(
|
|
20
|
+
path.join(configDir, 'quota-sidebar.config.json'),
|
|
19
21
|
path.join(input.worktree, 'quota-sidebar.config.json'),
|
|
22
|
+
path.join(input.directory, 'quota-sidebar.config.json'),
|
|
23
|
+
path.join(input.worktree, '.opencode', 'quota-sidebar.config.json'),
|
|
24
|
+
path.join(input.directory, '.opencode', 'quota-sidebar.config.json'),
|
|
25
|
+
...(configOverride ? [path.resolve(configOverride)] : []),
|
|
20
26
|
]);
|
|
21
27
|
const dataDir = resolveOpencodeDataDir();
|
|
22
28
|
const statePath = stateFilePath(dataDir);
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dateKeyFromTimestamp, normalizeTimestampMs } from './storage_dates.js';
|
|
2
|
-
import { authFilePath, resolveOpencodeDataDir, stateFilePath } from './storage_paths.js';
|
|
2
|
+
import { authFilePath, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath } from './storage_paths.js';
|
|
3
3
|
import type { CachedSessionUsage, IncrementalCursor, QuotaSidebarConfig, QuotaSidebarState, SessionState } from './types.js';
|
|
4
|
-
export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeDataDir, stateFilePath, };
|
|
4
|
+
export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, };
|
|
5
5
|
export declare const defaultConfig: QuotaSidebarConfig;
|
|
6
6
|
export declare function defaultState(): QuotaSidebarState;
|
|
7
7
|
export declare function loadConfig(paths: string[]): Promise<QuotaSidebarConfig>;
|
package/dist/storage.js
CHANGED
|
@@ -4,8 +4,8 @@ import { asBoolean, asNumber, debug, isRecord, mapConcurrent, swallow, } from '.
|
|
|
4
4
|
import { discoverChunks, readDayChunk, safeWriteFile, writeDayChunk, } from './storage_chunks.js';
|
|
5
5
|
import { dateKeyFromTimestamp, dateKeysInRange, dateStartFromKey, isDateKey, normalizeTimestampMs, } from './storage_dates.js';
|
|
6
6
|
import { parseQuotaCache } from './storage_parse.js';
|
|
7
|
-
import { authFilePath, chunkRootPathFromStateFile, resolveOpencodeDataDir, stateFilePath, } from './storage_paths.js';
|
|
8
|
-
export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeDataDir, stateFilePath, };
|
|
7
|
+
import { authFilePath, chunkRootPathFromStateFile, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, } from './storage_paths.js';
|
|
8
|
+
export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, };
|
|
9
9
|
// ─── Default config ──────────────────────────────────────────────────────────
|
|
10
10
|
export const defaultConfig = {
|
|
11
11
|
sidebar: {
|
|
@@ -44,43 +44,14 @@ export function defaultState() {
|
|
|
44
44
|
}
|
|
45
45
|
// ─── Config loading ──────────────────────────────────────────────────────────
|
|
46
46
|
export async function loadConfig(paths) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return defaultConfig;
|
|
56
|
-
const parsed = await fs
|
|
57
|
-
.readFile(selected, 'utf8')
|
|
58
|
-
.then((value) => JSON.parse(value))
|
|
59
|
-
.catch(swallow('loadConfig:read'));
|
|
60
|
-
if (!isRecord(parsed))
|
|
61
|
-
return defaultConfig;
|
|
62
|
-
const sidebar = isRecord(parsed.sidebar) ? parsed.sidebar : {};
|
|
63
|
-
const quota = isRecord(parsed.quota) ? parsed.quota : {};
|
|
64
|
-
const toast = isRecord(parsed.toast) ? parsed.toast : {};
|
|
65
|
-
const providers = isRecord(quota.providers) ? quota.providers : {};
|
|
66
|
-
return {
|
|
67
|
-
sidebar: {
|
|
68
|
-
enabled: asBoolean(sidebar.enabled, defaultConfig.sidebar.enabled),
|
|
69
|
-
width: Math.max(20, Math.min(60, asNumber(sidebar.width, defaultConfig.sidebar.width))),
|
|
70
|
-
showCost: asBoolean(sidebar.showCost, defaultConfig.sidebar.showCost),
|
|
71
|
-
showQuota: asBoolean(sidebar.showQuota, defaultConfig.sidebar.showQuota),
|
|
72
|
-
wrapQuotaLines: asBoolean(sidebar.wrapQuotaLines, defaultConfig.sidebar.wrapQuotaLines),
|
|
73
|
-
includeChildren: asBoolean(sidebar.includeChildren, defaultConfig.sidebar.includeChildren),
|
|
74
|
-
childrenMaxDepth: Math.max(1, Math.min(32, Math.floor(asNumber(sidebar.childrenMaxDepth, defaultConfig.sidebar.childrenMaxDepth)))),
|
|
75
|
-
childrenMaxSessions: Math.max(0, Math.min(2000, Math.floor(asNumber(sidebar.childrenMaxSessions, defaultConfig.sidebar.childrenMaxSessions)))),
|
|
76
|
-
childrenConcurrency: Math.max(1, Math.min(10, Math.floor(asNumber(sidebar.childrenConcurrency, defaultConfig.sidebar.childrenConcurrency)))),
|
|
77
|
-
},
|
|
78
|
-
quota: {
|
|
79
|
-
refreshMs: Math.max(30_000, asNumber(quota.refreshMs, defaultConfig.quota.refreshMs)),
|
|
80
|
-
includeOpenAI: asBoolean(quota.includeOpenAI, defaultConfig.quota.includeOpenAI),
|
|
81
|
-
includeCopilot: asBoolean(quota.includeCopilot, defaultConfig.quota.includeCopilot),
|
|
82
|
-
includeAnthropic: asBoolean(quota.includeAnthropic, defaultConfig.quota.includeAnthropic),
|
|
83
|
-
providers: Object.entries(providers).reduce((acc, [id, value]) => {
|
|
47
|
+
const mergeLayer = (base, parsed) => {
|
|
48
|
+
const sidebar = isRecord(parsed.sidebar) ? parsed.sidebar : {};
|
|
49
|
+
const quota = isRecord(parsed.quota) ? parsed.quota : {};
|
|
50
|
+
const toast = isRecord(parsed.toast) ? parsed.toast : {};
|
|
51
|
+
const providers = isRecord(quota.providers) ? quota.providers : {};
|
|
52
|
+
const mergedProviders = {
|
|
53
|
+
...base.quota.providers,
|
|
54
|
+
...Object.entries(providers).reduce((acc, [id, value]) => {
|
|
84
55
|
if (!isRecord(value))
|
|
85
56
|
return acc;
|
|
86
57
|
if (typeof value.enabled === 'boolean') {
|
|
@@ -88,14 +59,54 @@ export async function loadConfig(paths) {
|
|
|
88
59
|
}
|
|
89
60
|
return acc;
|
|
90
61
|
}, {}),
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
sidebar: {
|
|
65
|
+
enabled: asBoolean(sidebar.enabled, base.sidebar.enabled),
|
|
66
|
+
width: Math.max(20, Math.min(60, asNumber(sidebar.width, base.sidebar.width))),
|
|
67
|
+
showCost: asBoolean(sidebar.showCost, base.sidebar.showCost),
|
|
68
|
+
showQuota: asBoolean(sidebar.showQuota, base.sidebar.showQuota),
|
|
69
|
+
wrapQuotaLines: asBoolean(sidebar.wrapQuotaLines, base.sidebar.wrapQuotaLines),
|
|
70
|
+
includeChildren: asBoolean(sidebar.includeChildren, base.sidebar.includeChildren),
|
|
71
|
+
childrenMaxDepth: Math.max(1, Math.min(32, Math.floor(asNumber(sidebar.childrenMaxDepth, base.sidebar.childrenMaxDepth)))),
|
|
72
|
+
childrenMaxSessions: Math.max(0, Math.min(2000, Math.floor(asNumber(sidebar.childrenMaxSessions, base.sidebar.childrenMaxSessions)))),
|
|
73
|
+
childrenConcurrency: Math.max(1, Math.min(10, Math.floor(asNumber(sidebar.childrenConcurrency, base.sidebar.childrenConcurrency)))),
|
|
74
|
+
},
|
|
75
|
+
quota: {
|
|
76
|
+
refreshMs: Math.max(30_000, asNumber(quota.refreshMs, base.quota.refreshMs)),
|
|
77
|
+
includeOpenAI: asBoolean(quota.includeOpenAI, base.quota.includeOpenAI),
|
|
78
|
+
includeCopilot: asBoolean(quota.includeCopilot, base.quota.includeCopilot),
|
|
79
|
+
includeAnthropic: asBoolean(quota.includeAnthropic, base.quota.includeAnthropic),
|
|
80
|
+
providers: mergedProviders,
|
|
81
|
+
refreshAccessToken: asBoolean(quota.refreshAccessToken, base.quota.refreshAccessToken),
|
|
82
|
+
requestTimeoutMs: Math.max(1000, asNumber(quota.requestTimeoutMs, base.quota.requestTimeoutMs)),
|
|
83
|
+
},
|
|
84
|
+
toast: {
|
|
85
|
+
durationMs: Math.max(1000, asNumber(toast.durationMs, base.toast.durationMs)),
|
|
86
|
+
},
|
|
87
|
+
retentionDays: Math.max(1, asNumber(parsed.retentionDays, base.retentionDays)),
|
|
88
|
+
};
|
|
98
89
|
};
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
let config = defaultConfig;
|
|
92
|
+
for (const originalPath of paths) {
|
|
93
|
+
const filePath = path.resolve(originalPath);
|
|
94
|
+
const key = process.platform === 'win32' ? filePath.toLowerCase() : filePath;
|
|
95
|
+
if (seen.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
const stat = await fs.stat(filePath).catch(swallow('loadConfig:stat'));
|
|
99
|
+
if (!stat || !stat.isFile())
|
|
100
|
+
continue;
|
|
101
|
+
const parsed = await fs
|
|
102
|
+
.readFile(filePath, 'utf8')
|
|
103
|
+
.then((value) => JSON.parse(value))
|
|
104
|
+
.catch(swallow('loadConfig:read'));
|
|
105
|
+
if (!isRecord(parsed))
|
|
106
|
+
continue;
|
|
107
|
+
config = mergeLayer(config, parsed);
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
99
110
|
}
|
|
100
111
|
// ─── State loading ───────────────────────────────────────────────────────────
|
|
101
112
|
/** P2: Lazy chunk loading — only load chunks for sessions in sessionDateMap. */
|
package/dist/storage_paths.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* OPENCODE_QUOTA_DATA_HOME overrides the full data directory path.
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveOpencodeDataDir(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve OpenCode config directory.
|
|
14
|
+
*
|
|
15
|
+
* Uses XDG config conventions with optional plugin-scoped override.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveOpencodeConfigDir(): string;
|
|
12
18
|
export declare function stateFilePath(dataDir: string): string;
|
|
13
19
|
export declare function authFilePath(dataDir: string): string;
|
|
14
20
|
export declare function chunkRootPathFromStateFile(statePath: string): string;
|
package/dist/storage_paths.js
CHANGED
|
@@ -20,6 +20,20 @@ export function resolveOpencodeDataDir() {
|
|
|
20
20
|
return path.join(path.resolve(xdg), 'opencode');
|
|
21
21
|
return path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve OpenCode config directory.
|
|
25
|
+
*
|
|
26
|
+
* Uses XDG config conventions with optional plugin-scoped override.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveOpencodeConfigDir() {
|
|
29
|
+
const override = process.env.OPENCODE_QUOTA_CONFIG_HOME?.trim();
|
|
30
|
+
if (override)
|
|
31
|
+
return path.resolve(override);
|
|
32
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
33
|
+
if (xdg)
|
|
34
|
+
return path.join(path.resolve(xdg), 'opencode');
|
|
35
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
36
|
+
}
|
|
23
37
|
export function stateFilePath(dataDir) {
|
|
24
38
|
return path.join(dataDir, 'quota-sidebar.state.json');
|
|
25
39
|
}
|