mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-14 00:16:32 +00:00
feat: add usage API HTTPS_PROXY support
This commit is contained in:
1 parent
7a0b979b01
commit
e63591492a
6 files changed
+170
-14
No files matched your search
@@ -46,13 +46,14 @@
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
### v2.2.0 - v2.2.1 - Token Speed + Skills widget updates
|
||||
### v2.2.0 - v2.2.2 - Token Speed + Skills widget updates
|
||||
|
||||
- **🚀 New Token Speed widgets** - Added three widgets: **Input Speed**, **Output Speed**, and **Total Speed**.
|
||||
- Each speed widget supports a configurable window of `0-120` seconds in the widget editor (`w` key).
|
||||
- `0` disables window mode and uses a full-session average speed.
|
||||
- `1-120` calculates recent speed over the selected rolling window.
|
||||
- **🧩 New Skills widget controls (v2.2.1)** - Added configurable Skills modes (last/count/list), optional hide-when-empty behavior, and list-size limiting with most-recent-first ordering.
|
||||
- **🌐 Usage API proxy support (v2.2.2)** - Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
|
||||
- **🤝 Better subagent-aware speed reporting** - Token speed calculations continue to include referenced subagent activity so displayed speeds better reflect actual concurrent work.
|
||||
|
||||
### v2.1.0 - v2.1.10 - Usage widgets, links, new git insertions / deletions widgets, and reliability fixes
|
||||
@@ -203,6 +204,8 @@ The interactive configuration tool provides a terminal UI where you can:
|
||||
> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude"
|
||||
> ```
|
||||
|
||||
> 🌐 **Usage API proxy:** Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
|
||||
|
||||
### Claude Code settings.json format
|
||||
|
||||
When you install from the TUI, ccstatusline writes a `statusLine` command object to your Claude Code settings:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"eslint-plugin-import-newlines": "^1.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ink": "6.2.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
@@ -332,6 +333,8 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
@@ -566,6 +569,8 @@
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
@@ -978,6 +983,8 @@
|
||||
|
||||
"gradient-string/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"ink-gradient/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
@@ -34,6 +34,7 @@
|
||||
"eslint-plugin-import-newlines": "^1.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ink": "6.2.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
|
||||
@@ -171,4 +171,4 @@ describe('config utilities', () => {
|
||||
expect(saved.version).toBe(CURRENT_VERSION);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ interface UsageProbeResult {
|
||||
lockExists: boolean;
|
||||
cacheExists: boolean;
|
||||
requestCount: number;
|
||||
proxyAgentConfigured: boolean;
|
||||
requestHost: string | null;
|
||||
}
|
||||
|
||||
describe('fetchUsageData error handling', () => {
|
||||
@@ -27,6 +29,7 @@ describe('fetchUsageData error handling', () => {
|
||||
const apiErrorHome = path.join(tempRoot, 'home-api-error');
|
||||
const apiErrorBin = path.join(tempRoot, 'bin-api-error');
|
||||
const apiErrorClaudeConfig = path.join(tempRoot, 'claude-api-error');
|
||||
const invalidProxyHome = path.join(tempRoot, 'home-invalid-proxy');
|
||||
const successHome = path.join(tempRoot, 'home-success');
|
||||
const successBin = path.join(tempRoot, 'bin-success');
|
||||
const successClaudeConfig = path.join(tempRoot, 'claude-success');
|
||||
@@ -49,6 +52,7 @@ describe('fetchUsageData error handling', () => {
|
||||
fs.mkdirSync(apiErrorHome, { recursive: true });
|
||||
fs.mkdirSync(apiErrorBin, { recursive: true });
|
||||
fs.mkdirSync(apiErrorClaudeConfig, { recursive: true });
|
||||
fs.mkdirSync(invalidProxyHome, { recursive: true });
|
||||
fs.mkdirSync(successHome, { recursive: true });
|
||||
fs.mkdirSync(successBin, { recursive: true });
|
||||
fs.mkdirSync(successClaudeConfig, { recursive: true });
|
||||
@@ -71,10 +75,15 @@ const https = require('https');
|
||||
const mode = process.env.TEST_REQUEST_MODE || 'success';
|
||||
const responseBody = process.env.TEST_RESPONSE_BODY || '';
|
||||
let requestCount = 0;
|
||||
let proxyAgentConfigured = false;
|
||||
let requestHost = null;
|
||||
|
||||
https.request = (...args) => {
|
||||
requestCount += 1;
|
||||
const callback = args.find(value => typeof value === 'function');
|
||||
const options = args.find(value => value && typeof value === 'object' && !Buffer.isBuffer(value));
|
||||
proxyAgentConfigured = Boolean(options?.agent);
|
||||
requestHost = options?.hostname ?? null;
|
||||
const requestHandlers = new Map();
|
||||
const responseHandlers = new Map();
|
||||
|
||||
@@ -145,7 +154,9 @@ process.stdout.write(JSON.stringify({
|
||||
second,
|
||||
lockExists: fs.existsSync(lockFile),
|
||||
cacheExists: fs.existsSync(cacheFile),
|
||||
requestCount
|
||||
requestCount,
|
||||
proxyAgentConfigured,
|
||||
requestHost
|
||||
}));
|
||||
`;
|
||||
|
||||
@@ -169,6 +180,7 @@ process.stdout.write(JSON.stringify({
|
||||
expect(noCredentialsResult.lockExists).toBe(false);
|
||||
expect(noCredentialsResult.cacheExists).toBe(false);
|
||||
expect(noCredentialsResult.requestCount).toBe(0);
|
||||
expect(noCredentialsResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const apiErrorOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
@@ -187,6 +199,8 @@ process.stdout.write(JSON.stringify({
|
||||
expect(apiErrorResult.second).toEqual({ error: 'api-error' });
|
||||
expect(apiErrorResult.cacheExists).toBe(false);
|
||||
expect(apiErrorResult.requestCount).toBe(1);
|
||||
expect(apiErrorResult.proxyAgentConfigured).toBe(false);
|
||||
expect(apiErrorResult.requestHost).toBe('api.anthropic.com');
|
||||
|
||||
const successOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
@@ -211,6 +225,109 @@ process.stdout.write(JSON.stringify({
|
||||
expect(successResult.second).toEqual(successResult.first);
|
||||
expect(successResult.cacheExists).toBe(true);
|
||||
expect(successResult.requestCount).toBe(1);
|
||||
expect(successResult.proxyAgentConfigured).toBe(false);
|
||||
expect(successResult.requestHost).toBe('api.anthropic.com');
|
||||
|
||||
const httpsProxyOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: apiErrorHome,
|
||||
PATH: apiErrorBin,
|
||||
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
|
||||
HTTPS_PROXY: 'http://proxy.local:8080',
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
});
|
||||
|
||||
const httpsProxyResult = JSON.parse(httpsProxyOutput) as UsageProbeResult;
|
||||
expect(httpsProxyResult.first).toEqual(successResult.first);
|
||||
expect(httpsProxyResult.second).toEqual(successResult.first);
|
||||
expect(httpsProxyResult.requestCount).toBe(1);
|
||||
expect(httpsProxyResult.proxyAgentConfigured).toBe(true);
|
||||
expect(httpsProxyResult.requestHost).toBe('api.anthropic.com');
|
||||
|
||||
const lowercaseProxyOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: apiErrorHome,
|
||||
PATH: apiErrorBin,
|
||||
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
|
||||
https_proxy: 'http://proxy.local:8080',
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
});
|
||||
|
||||
const lowercaseProxyResult = JSON.parse(lowercaseProxyOutput) as UsageProbeResult;
|
||||
expect(lowercaseProxyResult.first).toEqual(successResult.first);
|
||||
expect(lowercaseProxyResult.second).toEqual(successResult.first);
|
||||
expect(lowercaseProxyResult.requestCount).toBe(1);
|
||||
expect(lowercaseProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const blankProxyOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: apiErrorHome,
|
||||
PATH: apiErrorBin,
|
||||
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
|
||||
HTTPS_PROXY: ' ',
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
});
|
||||
|
||||
const blankProxyResult = JSON.parse(blankProxyOutput) as UsageProbeResult;
|
||||
expect(blankProxyResult.first).toEqual(successResult.first);
|
||||
expect(blankProxyResult.second).toEqual(successResult.first);
|
||||
expect(blankProxyResult.requestCount).toBe(1);
|
||||
expect(blankProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const invalidProxyOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: invalidProxyHome,
|
||||
PATH: apiErrorBin,
|
||||
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
|
||||
HTTPS_PROXY: '://bad-proxy',
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
});
|
||||
|
||||
const invalidProxyResult = JSON.parse(invalidProxyOutput) as UsageProbeResult;
|
||||
expect(invalidProxyResult.first).toEqual({ error: 'api-error' });
|
||||
expect(invalidProxyResult.second).toEqual({ error: 'api-error' });
|
||||
expect(invalidProxyResult.requestCount).toBe(0);
|
||||
expect(invalidProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const staleProxyOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: successHome,
|
||||
PATH: successBin,
|
||||
CLAUDE_CONFIG_DIR: successClaudeConfig,
|
||||
HTTPS_PROXY: '://bad-proxy',
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000181000'
|
||||
}
|
||||
});
|
||||
|
||||
const staleProxyResult = JSON.parse(staleProxyOutput) as UsageProbeResult;
|
||||
expect(staleProxyResult.first).toEqual(successResult.first);
|
||||
expect(staleProxyResult.second).toEqual(successResult.first);
|
||||
expect(staleProxyResult.requestCount).toBe(0);
|
||||
expect(staleProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const cachedSuccessOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
|
||||
+38
-10
@@ -1,6 +1,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { z } from 'zod';
|
||||
@@ -174,6 +175,36 @@ function readStaleUsageCache(): UsageData | null {
|
||||
|
||||
const USAGE_API_HOST = 'api.anthropic.com';
|
||||
const USAGE_API_PATH = '/api/oauth/usage';
|
||||
const USAGE_API_TIMEOUT_MS = 5000;
|
||||
|
||||
function getUsageApiProxyUrl(): string | null {
|
||||
const proxyUrl = process.env.HTTPS_PROXY?.trim();
|
||||
if (proxyUrl === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return proxyUrl ?? null;
|
||||
}
|
||||
|
||||
function getUsageApiRequestOptions(token: string): https.RequestOptions | null {
|
||||
const proxyUrl = getUsageApiProxyUrl();
|
||||
|
||||
try {
|
||||
return {
|
||||
hostname: USAGE_API_HOST,
|
||||
path: USAGE_API_PATH,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'anthropic-beta': 'oauth-2025-04-20'
|
||||
},
|
||||
timeout: USAGE_API_TIMEOUT_MS,
|
||||
...(proxyUrl ? { agent: new HttpsProxyAgent(proxyUrl) } : {})
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFromUsageApi(token: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -187,16 +218,13 @@ async function fetchFromUsageApi(token: string): Promise<string | null> {
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const request = https.request({
|
||||
hostname: USAGE_API_HOST,
|
||||
path: USAGE_API_PATH,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'anthropic-beta': 'oauth-2025-04-20'
|
||||
},
|
||||
timeout: 5000
|
||||
}, (response) => {
|
||||
const requestOptions = getUsageApiRequestOptions(token);
|
||||
if (!requestOptions) {
|
||||
finish(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = https.request(requestOptions, (response) => {
|
||||
let data = '';
|
||||
response.setEncoding('utf8');
|
||||
|
||||
|
||||
Reference in new issue
Block a user