feat: add usage API HTTPS_PROXY support

This commit is contained in:
Matthew Breedlove committed 2026-03-05 18:07:10 -05:00
1 parent 7a0b979b01
commit e63591492a
6 files changed
+170 -14

No files matched your search

+4 -1
View File
@@ -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:
+7
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -171,4 +171,4 @@ describe('config utilities', () => {
expect(saved.version).toBe(CURRENT_VERSION);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
});
+118 -1
View File
@@ -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
View File
@@ -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');