@xtruder/opencode-claude-max-plugin 0.4.4 → 2.0.0-rc.1
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 +109 -124
- package/build/index.d.ts +514 -19
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1818
- package/build/model.d.ts +0 -5
- package/build/model.d.ts.map +1 -1
- package/build/server.d.ts +4 -11
- package/build/server.d.ts.map +1 -1
- package/build/server.js +68 -1818
- package/build/src-Dvxl6Bub.js +1527 -0
- package/build/tui-state.d.ts +12 -0
- package/build/tui-state.d.ts.map +1 -0
- package/build/tui.d.ts +11 -3
- package/build/tui.d.ts.map +1 -1
- package/build/tui.js +239 -0
- package/build/usage-pZdEiX94.js +336 -0
- package/build/usage.d.ts +1 -1
- package/build/usage.d.ts.map +1 -1
- package/package.json +28 -42
- package/src/credentials.ts +0 -156
- package/src/tui.tsx +0 -579
- package/src/usage.ts +0 -331
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fetchUsage, readUsageCache, writeUsageCache, type UsageData } from "./usage.ts";
|
|
2
|
+
export declare function createUsageStore(intervalMs?: number, io?: {
|
|
3
|
+
read: typeof readUsageCache;
|
|
4
|
+
write: typeof writeUsageCache;
|
|
5
|
+
fetch: typeof fetchUsage;
|
|
6
|
+
}, credentialsPath?: string): {
|
|
7
|
+
usage: import("solid-js").Accessor<UsageData | null>;
|
|
8
|
+
status: import("solid-js").Accessor<string>;
|
|
9
|
+
refresh: () => Promise<void>;
|
|
10
|
+
dispose: () => void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=tui-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui-state.d.ts","sourceRoot":"","sources":["../src/tui-state.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,UAAU,EACV,cAAc,EAEd,eAAe,EACf,KAAK,SAAS,EACf,MAAM,YAAY,CAAA;AAGnB,wBAAgB,gBAAgB,CAAC,UAAU,SAAS,EAAE,EAAE;;;;CAAW,EAAE,eAAe,CAAC,EAAE,MAAM;;;mBAQvE,OAAO,CAAC,IAAI,CAAC;;EA8ClC"}
|
package/build/tui.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { Plugin } from "@opencode/plugin/tui";
|
|
3
|
+
import { createUsageStore } from "./tui-state.ts";
|
|
4
|
+
type Store = Pick<ReturnType<typeof createUsageStore>, "usage" | "status">;
|
|
5
|
+
export declare function UsageView(props: {
|
|
6
|
+
store: Store;
|
|
7
|
+
theme: Plugin.Context["theme"];
|
|
8
|
+
detail?: boolean;
|
|
9
|
+
}): any;
|
|
10
|
+
declare const _default: {
|
|
4
11
|
id: string;
|
|
12
|
+
setup(ctx: Plugin.Context): (() => void) | undefined;
|
|
5
13
|
};
|
|
6
|
-
export default
|
|
14
|
+
export default _default;
|
|
7
15
|
//# sourceMappingURL=tui.d.ts.map
|
package/build/tui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAElD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAMjD,KAAK,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAA;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAC/B,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,OAqEA;;;;;AAED,wBA8C6B"}
|
package/build/tui.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { c as writeUsageCache, i as formatReset, o as readUsageCache, r as fetchUsage, s as shouldFetchApi, t as bestUsageFromCache } from "./usage-pZdEiX94.js";
|
|
2
|
+
import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, setProp } from "@opentui/solid";
|
|
3
|
+
import { For, Show, createSignal } from "solid-js";
|
|
4
|
+
//#region src/tui-state.ts
|
|
5
|
+
var defaults = {
|
|
6
|
+
read: readUsageCache,
|
|
7
|
+
write: writeUsageCache,
|
|
8
|
+
fetch: fetchUsage
|
|
9
|
+
};
|
|
10
|
+
function createUsageStore(intervalMs = 6e4, io = defaults, credentialsPath) {
|
|
11
|
+
const [usage, setUsage] = createSignal(null);
|
|
12
|
+
const [status, setStatus] = createSignal("loading");
|
|
13
|
+
let pending;
|
|
14
|
+
let disposed = false;
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const unavailable = (message) => setStatus(usage() ? `Showing cached usage. ${message}` : message);
|
|
17
|
+
const refresh = () => {
|
|
18
|
+
if (disposed) return Promise.resolve();
|
|
19
|
+
if (pending) return pending;
|
|
20
|
+
pending = (async () => {
|
|
21
|
+
try {
|
|
22
|
+
const cache = io.read();
|
|
23
|
+
const cached = bestUsageFromCache(cache);
|
|
24
|
+
if (cached) setUsage(cached);
|
|
25
|
+
if (!shouldFetchApi(cache)) {
|
|
26
|
+
if (cache.apiRateLimitUntil > Date.now()) unavailable("Rate limited; retrying later.");
|
|
27
|
+
else setStatus(cached ? "ready" : "Usage temporarily unavailable; retrying later.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const result = await io.fetch(credentialsPath, AbortSignal.any([controller.signal, AbortSignal.timeout(15e3)]));
|
|
31
|
+
if (disposed) return;
|
|
32
|
+
if (result.data) {
|
|
33
|
+
setUsage(result.data);
|
|
34
|
+
setStatus("ready");
|
|
35
|
+
io.write({
|
|
36
|
+
api: result.data,
|
|
37
|
+
apiFetchedAt: Date.now(),
|
|
38
|
+
apiRateLimitUntil: 0
|
|
39
|
+
});
|
|
40
|
+
} else if (result.retryAfterMs > 0) {
|
|
41
|
+
io.write({ apiRateLimitUntil: Date.now() + result.retryAfterMs });
|
|
42
|
+
unavailable("Rate limited; retrying later.");
|
|
43
|
+
} else unavailable("Usage unavailable. Check Claude login: claude auth login");
|
|
44
|
+
} catch {
|
|
45
|
+
if (!disposed) unavailable("Usage request failed; retrying later.");
|
|
46
|
+
}
|
|
47
|
+
})().finally(() => {
|
|
48
|
+
pending = void 0;
|
|
49
|
+
});
|
|
50
|
+
return pending;
|
|
51
|
+
};
|
|
52
|
+
refresh();
|
|
53
|
+
const timer = setInterval(() => void refresh(), intervalMs);
|
|
54
|
+
return {
|
|
55
|
+
usage,
|
|
56
|
+
status,
|
|
57
|
+
refresh,
|
|
58
|
+
dispose: () => {
|
|
59
|
+
disposed = true;
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
controller.abort();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/tui.tsx
|
|
67
|
+
var percent = (value) => Math.round(Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0)));
|
|
68
|
+
function UsageView(props) {
|
|
69
|
+
const rows = () => {
|
|
70
|
+
const data = props.store.usage();
|
|
71
|
+
return [
|
|
72
|
+
["5h", data?.five_hour],
|
|
73
|
+
["7d", data?.seven_day],
|
|
74
|
+
...props.detail ? [
|
|
75
|
+
["Sonnet (7d)", data?.seven_day_sonnet],
|
|
76
|
+
["Opus (7d)", data?.seven_day_opus],
|
|
77
|
+
["OAuth apps (7d)", data?.seven_day_oauth_apps],
|
|
78
|
+
["Cowork (7d)", data?.seven_day_cowork]
|
|
79
|
+
] : []
|
|
80
|
+
].filter(([, window]) => window);
|
|
81
|
+
};
|
|
82
|
+
return (() => {
|
|
83
|
+
var _el$ = createElement("box"), _el$2 = createElement("text"), _el$3 = createElement("b");
|
|
84
|
+
insertNode(_el$, _el$2);
|
|
85
|
+
setProp(_el$, "width", "100%");
|
|
86
|
+
setProp(_el$, "flexDirection", "column");
|
|
87
|
+
setProp(_el$, "gap", 0);
|
|
88
|
+
insertNode(_el$2, _el$3);
|
|
89
|
+
insert(_el$3, () => props.detail ? "Claude Subscription Usage" : "Claude Usage");
|
|
90
|
+
insert(_el$, createComponent(For, {
|
|
91
|
+
get each() {
|
|
92
|
+
return rows();
|
|
93
|
+
},
|
|
94
|
+
children: ([label, window]) => {
|
|
95
|
+
const [width, setWidth] = createSignal(0);
|
|
96
|
+
const line = () => {
|
|
97
|
+
const suffix = `] ${percent(window.utilization)}%`;
|
|
98
|
+
const prefix = `${label} [`;
|
|
99
|
+
const size = Math.max(0, width() - prefix.length - suffix.length);
|
|
100
|
+
const filled = Math.round(percent(window.utilization) * size / 100);
|
|
101
|
+
return prefix + "█".repeat(filled) + "░".repeat(size - filled) + suffix;
|
|
102
|
+
};
|
|
103
|
+
return (() => {
|
|
104
|
+
var _el$9 = createElement("box"), _el$0 = createElement("text");
|
|
105
|
+
insertNode(_el$9, _el$0);
|
|
106
|
+
setProp(_el$9, "width", "100%");
|
|
107
|
+
setProp(_el$9, "flexDirection", "column");
|
|
108
|
+
setProp(_el$9, "onSizeChange", function() {
|
|
109
|
+
setWidth(this.width);
|
|
110
|
+
});
|
|
111
|
+
insert(_el$0, line);
|
|
112
|
+
insert(_el$9, createComponent(Show, {
|
|
113
|
+
get when() {
|
|
114
|
+
return window?.resets_at;
|
|
115
|
+
},
|
|
116
|
+
get children() {
|
|
117
|
+
var _el$1 = createElement("text");
|
|
118
|
+
insert(_el$1, () => formatReset(window.resets_at));
|
|
119
|
+
effect((_$p) => setProp(_el$1, "fg", props.theme.text.muted, _$p));
|
|
120
|
+
return _el$1;
|
|
121
|
+
}
|
|
122
|
+
}), null);
|
|
123
|
+
effect((_$p) => setProp(_el$0, "fg", props.theme.text.action.primary.base, _$p));
|
|
124
|
+
return _el$9;
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
}), null);
|
|
128
|
+
insert(_el$, createComponent(Show, {
|
|
129
|
+
get when() {
|
|
130
|
+
return memo(() => !!props.detail)() && props.store.usage()?.extra_usage;
|
|
131
|
+
},
|
|
132
|
+
children: (extra) => (() => {
|
|
133
|
+
var _el$10 = createElement("text");
|
|
134
|
+
insert(_el$10, (() => {
|
|
135
|
+
var _c$2 = memo(() => !!extra().is_enabled);
|
|
136
|
+
return () => _c$2() ? `Extra usage: ${extra().utilization == null ? "enabled" : `${percent(extra().utilization)}% used`}` : "Extra usage: disabled";
|
|
137
|
+
})());
|
|
138
|
+
effect((_$p) => setProp(_el$10, "fg", props.theme.text.muted, _$p));
|
|
139
|
+
return _el$10;
|
|
140
|
+
})()
|
|
141
|
+
}), null);
|
|
142
|
+
insert(_el$, createComponent(Show, {
|
|
143
|
+
get when() {
|
|
144
|
+
return props.store.status() !== "ready";
|
|
145
|
+
},
|
|
146
|
+
get children() {
|
|
147
|
+
var _el$4 = createElement("text");
|
|
148
|
+
insert(_el$4, (() => {
|
|
149
|
+
var _c$ = memo(() => props.store.status() === "loading");
|
|
150
|
+
return () => _c$() ? "Loading usage…" : props.store.status();
|
|
151
|
+
})());
|
|
152
|
+
effect((_$p) => setProp(_el$4, "fg", props.theme.text.muted, _$p));
|
|
153
|
+
return _el$4;
|
|
154
|
+
}
|
|
155
|
+
}), null);
|
|
156
|
+
insert(_el$, createComponent(Show, {
|
|
157
|
+
get when() {
|
|
158
|
+
return memo(() => props.store.status() === "ready")() && rows().length === 0;
|
|
159
|
+
},
|
|
160
|
+
get children() {
|
|
161
|
+
var _el$5 = createElement("text");
|
|
162
|
+
insertNode(_el$5, createTextNode(`No usage windows available.`));
|
|
163
|
+
effect((_$p) => setProp(_el$5, "fg", props.theme.text.muted, _$p));
|
|
164
|
+
return _el$5;
|
|
165
|
+
}
|
|
166
|
+
}), null);
|
|
167
|
+
insert(_el$, createComponent(Show, {
|
|
168
|
+
get when() {
|
|
169
|
+
return props.detail;
|
|
170
|
+
},
|
|
171
|
+
get children() {
|
|
172
|
+
var _el$7 = createElement("text");
|
|
173
|
+
insertNode(_el$7, createTextNode(`Esc to close · /usage refreshes (API cache: 5m)`));
|
|
174
|
+
effect((_$p) => setProp(_el$7, "fg", props.theme.text.muted, _$p));
|
|
175
|
+
return _el$7;
|
|
176
|
+
}
|
|
177
|
+
}), null);
|
|
178
|
+
effect((_p$) => {
|
|
179
|
+
var _v$ = props.detail ? 1 : 0, _v$2 = props.theme.text.base;
|
|
180
|
+
_v$ !== _p$.e && (_p$.e = setProp(_el$, "padding", _v$, _p$.e));
|
|
181
|
+
_v$2 !== _p$.t && (_p$.t = setProp(_el$2, "fg", _v$2, _p$.t));
|
|
182
|
+
return _p$;
|
|
183
|
+
}, {
|
|
184
|
+
e: void 0,
|
|
185
|
+
t: void 0
|
|
186
|
+
});
|
|
187
|
+
return _el$;
|
|
188
|
+
})();
|
|
189
|
+
}
|
|
190
|
+
var tui_default = {
|
|
191
|
+
id: "anthropic-sdk-usage",
|
|
192
|
+
setup(ctx) {
|
|
193
|
+
if (ctx.options.enabled === false || ctx.options.enabled === "false") return;
|
|
194
|
+
const seconds = Number(ctx.options.poll_interval ?? 60);
|
|
195
|
+
const store = createUsageStore((Number.isFinite(seconds) ? Math.max(10, seconds) : 60) * 1e3, void 0, ctx.options.credentialsPath);
|
|
196
|
+
const offCommand = ctx.ui.slot({
|
|
197
|
+
append: "app",
|
|
198
|
+
render: () => {
|
|
199
|
+
ctx.keymap.layer(() => ({
|
|
200
|
+
mode: "global",
|
|
201
|
+
commands: [{
|
|
202
|
+
id: "anthropic-sdk.usage",
|
|
203
|
+
title: "Claude subscription usage",
|
|
204
|
+
group: "Claude",
|
|
205
|
+
palette: true,
|
|
206
|
+
slash: { name: "usage" },
|
|
207
|
+
run() {
|
|
208
|
+
store.refresh();
|
|
209
|
+
ctx.ui.dialog.show(() => createComponent(UsageView, {
|
|
210
|
+
store,
|
|
211
|
+
get theme() {
|
|
212
|
+
return ctx.theme;
|
|
213
|
+
},
|
|
214
|
+
detail: true
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}]
|
|
218
|
+
}));
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
const offSidebar = ctx.options.sidebar === false || ctx.options.sidebar === "false" ? () => {} : ctx.ui.slot({
|
|
223
|
+
append: "sidebar.content",
|
|
224
|
+
render: () => createComponent(UsageView, {
|
|
225
|
+
store,
|
|
226
|
+
get theme() {
|
|
227
|
+
return ctx.theme;
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
return () => {
|
|
232
|
+
store.dispose();
|
|
233
|
+
offCommand();
|
|
234
|
+
offSidebar();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
export { UsageView, tui_default as default };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/credentials.ts
|
|
6
|
+
/**
|
|
7
|
+
* How long to cache credentials in memory before re-reading from disk.
|
|
8
|
+
*/
|
|
9
|
+
var CREDENTIAL_CACHE_TTL_MS = 3e4;
|
|
10
|
+
/**
|
|
11
|
+
* Buffer before expiry at which we consider the token "near expiry"
|
|
12
|
+
* and trigger a CLI refresh. 60 seconds matches opencode-claude-auth.
|
|
13
|
+
*/
|
|
14
|
+
var EXPIRY_BUFFER_MS = 6e4;
|
|
15
|
+
var cachedCreds = null;
|
|
16
|
+
var cachedAt = 0;
|
|
17
|
+
var cachedPath;
|
|
18
|
+
/**
|
|
19
|
+
* Default path to Claude Code credentials file.
|
|
20
|
+
*/
|
|
21
|
+
function getCredentialsPath() {
|
|
22
|
+
return join(homedir(), ".claude", ".credentials.json");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read OAuth credentials from Claude Code's credentials file.
|
|
26
|
+
* Returns null if the file doesn't exist or can't be parsed.
|
|
27
|
+
*/
|
|
28
|
+
function readClaudeCredentials(path) {
|
|
29
|
+
const credPath = path ?? getCredentialsPath();
|
|
30
|
+
if (!existsSync(credPath)) return null;
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(credPath, "utf-8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!parsed.claudeAiOauth?.accessToken) return null;
|
|
35
|
+
return parsed.claudeAiOauth;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if credentials are expired (with 60s buffer).
|
|
42
|
+
*/
|
|
43
|
+
function isExpired(creds) {
|
|
44
|
+
return Date.now() > creds.expiresAt - EXPIRY_BUFFER_MS;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run the Claude CLI to trigger an OAuth token refresh.
|
|
48
|
+
* The CLI writes refreshed credentials to ~/.claude/.credentials.json.
|
|
49
|
+
* Retries once on failure (matching opencode-claude-auth behavior).
|
|
50
|
+
*/
|
|
51
|
+
function refreshViaCli() {
|
|
52
|
+
const maxAttempts = 2;
|
|
53
|
+
for (let i = 0; i < maxAttempts; i++) try {
|
|
54
|
+
execSync("claude -p . --model haiku", {
|
|
55
|
+
timeout: 6e4,
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
TERM: "dumb"
|
|
60
|
+
},
|
|
61
|
+
stdio: "ignore"
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
} catch {}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read credentials, refreshing via Claude CLI if the token is near expiry.
|
|
69
|
+
* Returns null only if credentials are completely unavailable.
|
|
70
|
+
*/
|
|
71
|
+
function refreshIfNeeded(path) {
|
|
72
|
+
const creds = readClaudeCredentials(path);
|
|
73
|
+
if (!creds) return null;
|
|
74
|
+
if (!isExpired(creds)) return creds;
|
|
75
|
+
console.warn("[anthropic-sdk-provider] OAuth token near expiry, refreshing via Claude CLI...");
|
|
76
|
+
if (!refreshViaCli()) {
|
|
77
|
+
console.warn("[anthropic-sdk-provider] CLI refresh failed. Token may be expired.");
|
|
78
|
+
return creds;
|
|
79
|
+
}
|
|
80
|
+
const fresh = readClaudeCredentials(path);
|
|
81
|
+
if (fresh && !isExpired(fresh)) {
|
|
82
|
+
console.warn("[anthropic-sdk-provider] Token refreshed successfully.");
|
|
83
|
+
return fresh;
|
|
84
|
+
}
|
|
85
|
+
return fresh ?? creds;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get cached credentials with automatic refresh.
|
|
89
|
+
* Uses in-memory caching with a 30-second TTL to avoid
|
|
90
|
+
* hitting the filesystem on every request.
|
|
91
|
+
* When the cached token is near expiry, triggers a CLI refresh.
|
|
92
|
+
*/
|
|
93
|
+
function getCachedCredentials(path) {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const resolvedPath = path ?? cachedPath;
|
|
96
|
+
if (cachedCreds && now - cachedAt < CREDENTIAL_CACHE_TTL_MS && !isExpired(cachedCreds)) return cachedCreds;
|
|
97
|
+
const fresh = refreshIfNeeded(resolvedPath);
|
|
98
|
+
if (fresh) {
|
|
99
|
+
cachedCreds = fresh;
|
|
100
|
+
cachedAt = now;
|
|
101
|
+
cachedPath = resolvedPath;
|
|
102
|
+
} else cachedCreds = null;
|
|
103
|
+
return cachedCreds;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/usage.ts
|
|
107
|
+
/**
|
|
108
|
+
* Claude subscription usage tracking.
|
|
109
|
+
*
|
|
110
|
+
* Two data sources:
|
|
111
|
+
* 1. Usage API (GET /api/oauth/usage) — full per-model breakdown, rate limited
|
|
112
|
+
* 2. Response headers (anthropic-ratelimit-unified-*) — 5h/7d only, every inference call
|
|
113
|
+
*
|
|
114
|
+
* Both are persisted to a shared cache file (~/.local/state/opencode/usage-cache.json)
|
|
115
|
+
* so the TUI plugin (main process) can read data written by the server plugin (worker process).
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* In-memory header usage, updated on every inference call.
|
|
119
|
+
* Only meaningful in the server/worker process.
|
|
120
|
+
*/
|
|
121
|
+
var cachedUsage = {};
|
|
122
|
+
var XDG_STATE = process.env.XDG_STATE_HOME || join(homedir(), ".local", "state");
|
|
123
|
+
var USAGE_CACHE_DIR = join(XDG_STATE, "opencode");
|
|
124
|
+
var USAGE_CACHE_FILE = join(USAGE_CACHE_DIR, "usage-cache.json");
|
|
125
|
+
function emptyCache() {
|
|
126
|
+
return {
|
|
127
|
+
api: null,
|
|
128
|
+
headers: null,
|
|
129
|
+
updatedAt: 0,
|
|
130
|
+
apiFetchedAt: 0,
|
|
131
|
+
apiRateLimitUntil: 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
var USAGE_LOCK_FILE = USAGE_CACHE_FILE + ".lock";
|
|
135
|
+
var USAGE_TMP_FILE = USAGE_CACHE_FILE + ".tmp";
|
|
136
|
+
/** Max time a lock can be held before we consider it stale (ms). */
|
|
137
|
+
var LOCK_STALE_MS = 5e3;
|
|
138
|
+
/**
|
|
139
|
+
* Acquire an exclusive lock using O_CREAT|O_EXCL (atomic on all filesystems).
|
|
140
|
+
* Returns true if acquired, false if another process holds it.
|
|
141
|
+
* Stale locks (older than LOCK_STALE_MS) are automatically broken.
|
|
142
|
+
*/
|
|
143
|
+
function acquireLock() {
|
|
144
|
+
try {
|
|
145
|
+
mkdirSync(USAGE_CACHE_DIR, { recursive: true });
|
|
146
|
+
const fd = openSync(USAGE_LOCK_FILE, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
147
|
+
writeFileSync(fd, `${process.pid}\n${Date.now()}`);
|
|
148
|
+
closeSync(fd);
|
|
149
|
+
return true;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err?.code !== "EEXIST") return false;
|
|
152
|
+
try {
|
|
153
|
+
const text = readFileSync(USAGE_LOCK_FILE, "utf-8");
|
|
154
|
+
const ts = parseInt(text.split("\n")[1] ?? "0");
|
|
155
|
+
if (Date.now() - ts > LOCK_STALE_MS) {
|
|
156
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
157
|
+
return acquireLock();
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
try {
|
|
161
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
162
|
+
} catch {}
|
|
163
|
+
return acquireLock();
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function releaseLock() {
|
|
169
|
+
try {
|
|
170
|
+
unlinkSync(USAGE_LOCK_FILE);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Read the shared cache file. Returns empty cache if not found.
|
|
175
|
+
*/
|
|
176
|
+
function readUsageCache() {
|
|
177
|
+
try {
|
|
178
|
+
const text = readFileSync(USAGE_CACHE_FILE, "utf-8");
|
|
179
|
+
const data = JSON.parse(text);
|
|
180
|
+
if (data && typeof data.updatedAt === "number") return data;
|
|
181
|
+
} catch {}
|
|
182
|
+
return emptyCache();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Transactional read-modify-write of the cache file.
|
|
186
|
+
* Uses a lockfile for mutual exclusion and write-to-tmp+rename for atomicity.
|
|
187
|
+
*/
|
|
188
|
+
function writeUsageCache(update) {
|
|
189
|
+
if (!acquireLock()) return;
|
|
190
|
+
try {
|
|
191
|
+
const merged = {
|
|
192
|
+
...readUsageCache(),
|
|
193
|
+
...update,
|
|
194
|
+
updatedAt: Date.now()
|
|
195
|
+
};
|
|
196
|
+
mkdirSync(USAGE_CACHE_DIR, { recursive: true });
|
|
197
|
+
writeFileSync(USAGE_TMP_FILE, JSON.stringify(merged));
|
|
198
|
+
renameSync(USAGE_TMP_FILE, USAGE_CACHE_FILE);
|
|
199
|
+
} catch {
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(USAGE_TMP_FILE);
|
|
202
|
+
} catch {}
|
|
203
|
+
} finally {
|
|
204
|
+
releaseLock();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Persist current in-memory header usage to the cache file.
|
|
209
|
+
* Called from the server plugin's wrappedFetch after parsing response headers.
|
|
210
|
+
*/
|
|
211
|
+
function persistCachedUsage() {
|
|
212
|
+
if (cachedUsage.fiveHourUtil == null && cachedUsage.sevenDayUtil == null) return;
|
|
213
|
+
writeUsageCache({ headers: { ...cachedUsage } });
|
|
214
|
+
}
|
|
215
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
216
|
+
/**
|
|
217
|
+
* Fetch current Claude subscription usage from the API.
|
|
218
|
+
* Returns { data, retryAfterMs } so callers can track rate limit state.
|
|
219
|
+
* retryAfterMs > 0 means the API returned 429 with a Retry-After header.
|
|
220
|
+
*/
|
|
221
|
+
async function fetchUsage(credentialsPath, signal) {
|
|
222
|
+
const creds = readClaudeCredentials(credentialsPath);
|
|
223
|
+
if (!creds) return {
|
|
224
|
+
data: null,
|
|
225
|
+
retryAfterMs: 0
|
|
226
|
+
};
|
|
227
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) return {
|
|
228
|
+
data: null,
|
|
229
|
+
retryAfterMs: 0
|
|
230
|
+
};
|
|
231
|
+
const resp = await fetch(USAGE_URL, {
|
|
232
|
+
signal,
|
|
233
|
+
headers: {
|
|
234
|
+
authorization: `Bearer ${creds.accessToken}`,
|
|
235
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
236
|
+
"content-type": "application/json",
|
|
237
|
+
"user-agent": "claude-cli/2.1.81 (external, sdk-cli)",
|
|
238
|
+
"x-app": "cli"
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
if (resp.status === 429) {
|
|
242
|
+
const retryAfter = parseInt(resp.headers.get("retry-after") ?? "60");
|
|
243
|
+
return {
|
|
244
|
+
data: null,
|
|
245
|
+
retryAfterMs: Math.max(6e4, retryAfter * 1e3)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (!resp.ok) return {
|
|
249
|
+
data: null,
|
|
250
|
+
retryAfterMs: 0
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
data: await resp.json(),
|
|
254
|
+
retryAfterMs: 0
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Convert HeaderUsage (from response headers) to UsageData (API format).
|
|
259
|
+
* Provides a partial view — only 5h and 7d windows are available.
|
|
260
|
+
*/
|
|
261
|
+
function headerUsageToUsageData(h) {
|
|
262
|
+
return {
|
|
263
|
+
five_hour: h.fiveHourUtil != null ? {
|
|
264
|
+
utilization: h.fiveHourUtil * 100,
|
|
265
|
+
resets_at: h.fiveHourReset ? (/* @__PURE__ */ new Date(h.fiveHourReset * 1e3)).toISOString() : null
|
|
266
|
+
} : null,
|
|
267
|
+
seven_day: h.sevenDayUtil != null ? {
|
|
268
|
+
utilization: h.sevenDayUtil * 100,
|
|
269
|
+
resets_at: h.sevenDayReset ? (/* @__PURE__ */ new Date(h.sevenDayReset * 1e3)).toISOString() : null
|
|
270
|
+
} : null,
|
|
271
|
+
seven_day_sonnet: null,
|
|
272
|
+
seven_day_opus: null,
|
|
273
|
+
seven_day_oauth_apps: null,
|
|
274
|
+
seven_day_cowork: null,
|
|
275
|
+
extra_usage: null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the best available UsageData from the cache by merging both sources.
|
|
280
|
+
* - Headers provide the freshest 5h/7d numbers (updated every inference call)
|
|
281
|
+
* - API provides per-model breakdown and extra usage (updated less frequently)
|
|
282
|
+
* When both exist, header 5h/7d values override API values since they're fresher.
|
|
283
|
+
*/
|
|
284
|
+
function bestUsageFromCache(cache) {
|
|
285
|
+
if (!cache.api && !cache.headers) return null;
|
|
286
|
+
const fromHeaders = cache.headers ? headerUsageToUsageData(cache.headers) : null;
|
|
287
|
+
const fromApi = cache.api;
|
|
288
|
+
if (!fromApi) return fromHeaders;
|
|
289
|
+
if (!fromHeaders) return fromApi;
|
|
290
|
+
return {
|
|
291
|
+
five_hour: fromHeaders.five_hour ?? fromApi.five_hour,
|
|
292
|
+
seven_day: fromHeaders.seven_day ?? fromApi.seven_day,
|
|
293
|
+
seven_day_sonnet: fromApi.seven_day_sonnet,
|
|
294
|
+
seven_day_opus: fromApi.seven_day_opus,
|
|
295
|
+
seven_day_oauth_apps: fromApi.seven_day_oauth_apps,
|
|
296
|
+
seven_day_cowork: fromApi.seven_day_cowork,
|
|
297
|
+
extra_usage: fromApi.extra_usage
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** How long API data is considered fresh before re-fetching (5 minutes). */
|
|
301
|
+
var API_TTL_MS = 3e5;
|
|
302
|
+
/**
|
|
303
|
+
* Check if the API should be called.
|
|
304
|
+
* Returns true when not rate limited AND either never fetched or TTL expired.
|
|
305
|
+
*/
|
|
306
|
+
function shouldFetchApi(cache) {
|
|
307
|
+
if (cache.apiRateLimitUntil && Date.now() < cache.apiRateLimitUntil) return false;
|
|
308
|
+
if (!cache.apiFetchedAt) return true;
|
|
309
|
+
return Date.now() - cache.apiFetchedAt > API_TTL_MS;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Format a reset timestamp as relative time.
|
|
313
|
+
*/
|
|
314
|
+
function formatReset(resetsAt) {
|
|
315
|
+
if (!resetsAt) return "";
|
|
316
|
+
const reset = new Date(resetsAt);
|
|
317
|
+
const now = /* @__PURE__ */ new Date();
|
|
318
|
+
const diffMs = reset.getTime() - now.getTime();
|
|
319
|
+
if (diffMs <= 0) return "Reset now";
|
|
320
|
+
const diffMin = Math.floor(diffMs / 6e4);
|
|
321
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
322
|
+
const timeStr = reset.toLocaleTimeString("en-US", {
|
|
323
|
+
hour: "numeric",
|
|
324
|
+
minute: "2-digit",
|
|
325
|
+
timeZoneName: "short"
|
|
326
|
+
});
|
|
327
|
+
const dateStr = reset.toLocaleDateString("en-US", {
|
|
328
|
+
month: "short",
|
|
329
|
+
day: "numeric"
|
|
330
|
+
});
|
|
331
|
+
if (diffHour < 1) return `Resets in ${diffMin}m`;
|
|
332
|
+
if (diffHour < 24) return `Resets ${timeStr}`;
|
|
333
|
+
return `Resets ${dateStr}, ${timeStr}`;
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { persistCachedUsage as a, writeUsageCache as c, formatReset as i, getCachedCredentials as l, cachedUsage as n, readUsageCache as o, fetchUsage as r, shouldFetchApi as s, bestUsageFromCache as t, readClaudeCredentials as u };
|
package/build/usage.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export declare function persistCachedUsage(): void;
|
|
|
67
67
|
* Returns { data, retryAfterMs } so callers can track rate limit state.
|
|
68
68
|
* retryAfterMs > 0 means the API returned 429 with a Retry-After header.
|
|
69
69
|
*/
|
|
70
|
-
export declare function fetchUsage(credentialsPath?: string): Promise<{
|
|
70
|
+
export declare function fetchUsage(credentialsPath?: string, signal?: AbortSignal): Promise<{
|
|
71
71
|
data: UsageData | null;
|
|
72
72
|
retryAfterMs: number;
|
|
73
73
|
}>;
|
package/build/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;IACrB,iEAAiE;IACjE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,WAAgB,CAAA;AA8D1C;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAS3C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAoBjE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAMD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,eAAe,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;IACrB,iEAAiE;IACjE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,WAAgB,CAAA;AA8D1C;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAS3C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAoBjE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAMD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,eAAe,CAAC,EAAE,MAAM,EACxB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B3D;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,CAsBhE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAmBtE;AAKD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAwB3D"}
|