cliproxyapi-statusline
npx skills add https://github.com/tinycellcorp/cliproxyapi-statusline --skill cliproxyapi-statusline
Agent 安装分布
Skill 文档
cliproxy-statusline
cliproxyapi íë¡ì ìë²ì ë¤ì¤ ê³ì ì¿¼í° ì¬ì©ëì Claude Code ìíë¼ì¸ì íìíë ë°©ë²ì ìë´í©ëë¤.
0. ì¤ì (Setup)
0-1. ì¤ì íì¼
ì´ ì¤í¬ì ~/.cliproxy-statusline.json íì¼ìì íë¡ì ìë² ì 보를 ì½ìµëë¤.
{
"proxyUrl": "http://localhost:3000",
"managementKey": "your-management-key"
}
proxyUrl: cliproxyapi íë¡ì ìë² URLmanagementKey: ê´ë¦¬ API ì¸ì¦ í¤
0-2. ìµì´ ì¤ì íë¦
ì¤ì íì¼ì´ ìì¼ë©´ ì¬ì©ììê² ë¤ìì ììëë¡ ì§ë¬¸í©ëë¤:
- “cliproxyapi íë¡ì ìë² URLì ì ë ¥í´ì£¼ì¸ì (기본ê°: http://localhost:3000):”
- “ê´ë¦¬ API í¤(management key)를 ì ë ¥í´ì£¼ì¸ì:”
ì
ë ¥ë°ì ê°ì¼ë¡ ~/.cliproxy-statusline.jsonì ìì±í©ëë¤:
import { writeFileSync, chmodSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CONFIG_PATH = join(homedir(), '.cliproxy-statusline.json');
writeFileSync(CONFIG_PATH, JSON.stringify({
proxyUrl: userInputUrl || 'http://localhost:3000',
managementKey: userInputKey
}, null, 2));
// Unix: íì¼ ê¶í ì í (ìì ìë§ ì½ê¸°/ì°ê¸°)
try { chmodSync(CONFIG_PATH, 0o600); } catch {}
0-3. ì¤ì ë¡ë
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CONFIG_PATH = join(homedir(), '.cliproxy-statusline.json');
function loadConfig() {
try {
const raw = readFileSync(CONFIG_PATH, 'utf8');
const config = JSON.parse(raw);
if (!config.proxyUrl || !config.managementKey) {
throw new Error('Missing required fields: proxyUrl, managementKey');
}
return config;
} catch {
return null; // config not found or invalid
}
}
ì¤ì ì´ ìì¼ë©´(loadConfig() returns null) 0-2ì ì¤ì íë¦ì ì¤íí©ëë¤.
1. ê°ì
cliproxyapië ì¬ë¬ Anthropic ê³ì ì OAuth í í°ì ê´ë¦¬íë íë¡ì ìë²ì ëë¤. ê° ê³ì ë§ë¤ 5ìê° ë° 7ì¼ ë¨ìì ì¬ì©ë 쿼í°ê° ì¡´ì¬íë©°, fill-first ë¼ì°í ë°©ìì¼ë¡ ê°ì¥ ë§ì´ ì±ìì§ ì¿¼í°ë¥¼ ì°ì ìì§í©ëë¤.
ìíë¼ì¸ì 쿼í°ë¥¼ íìíë©´ íì¬ ì´ë ê³ì ì´ íì± ì¤ì¸ì§, ê° ê³ì ì ë¨ì ì¬ì 를 íëì íì í ì ììµëë¤.
2. API ìëí¬ì¸í¸
2-1. auth-files ëª©ë¡ ì¡°í
GET {PROXY_URL}/v0/management/auth-files
Authorization: Bearer {MGMT_KEY}
ìëµ: files ë°°ì´ì í¬í¨íë ê°ì²´. provider === "claude" íëª©ë§ íí°ë§í©ëë¤.
{
"files": [
{ "name": "account1.json", "provider": "claude" },
{ "name": "account2.json", "provider": "claude" }
]
}
2-2. ê³ì ë³ í í° ë¤ì´ë¡ë
GET {PROXY_URL}/v0/management/auth-files/download?name={file.name}
Authorization: Bearer {MGMT_KEY}
ìëµìì access_token íë를 ì¶ì¶í©ëë¤.
2-3. Anthropic OAuth usage API
GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer {access_token}
anthropic-beta: oauth-2025-04-20
2-4. ìëµ êµ¬ì¡°
{
"five_hour": {
"utilization": 0.52,
"resets_at": "2026-02-19T15:30:00Z"
},
"seven_day": {
"utilization": 0.07,
"resets_at": "2026-02-25T12:00:00Z"
}
}
utilization: í¼ì¼í¸ ë¨ì (ì: 37.0 = 37%). ì½ëìì 0.0~1.0 ë¹ì¨ë¡ ì ê·í íìresets_at: ISO 8601 리ì ìê°
3. ìíë¼ì¸ 기본 ì리
3-1. Claude Code settings.json ë±ë¡
~/.claude/settings.jsonì statusLine.commandì ì¤í íì¼ ê²½ë¡ë¥¼ ë±ë¡í©ëë¤.
{
"statusLine": {
"command": "node /path/to/proxy-status.mjs"
}
}
3-2. One-shot íë¡ì¸ì¤ 모ë¸
ìíë¼ì¸ íë¡ì¸ì¤ë 매 ë ëë§ë§ë¤ ë 립 ì¤íë©ëë¤.
- Claude Codeê° íë¡ì¸ì¤ë¥¼ ììí©ëë¤.
- stdinì¼ë¡ JSON 컨í ì¤í¸ë¥¼ ì ì¡í©ëë¤.
- íë¡ì¸ì¤ë stdoutì 결과를 ì¶ë ¥íê³ ì¢ ë£í©ëë¤.
// stdin ìë¹
const input = await new Promise(resolve => {
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => resolve(JSON.parse(data || '{}')));
});
// stdout ì¶ë ¥ í ì¢
ë£
process.stdout.write(output + '\n');
process.exit(0);
3-3. stdin 주ì íë
| íë | ì¤ëª |
|---|---|
context_window |
íì¬ ì»¨í ì¤í¸ ì¬ì©ë |
model |
íì¬ ëª¨ë¸ ì´ë¦ |
cwd |
íì¬ ìì ëë í 리 |
transcript_path |
ëí íì¼ ê²½ë¡ |
3-4. ANSI ìì ì½ë
| ì½ë | ì©ë | ì¡°ê±´ |
|---|---|---|
\x1b[32m |
ì´ë¡ (ì ì) | utilization < 0.70 |
\x1b[33m |
ë ¸ë (주ì) | 0.70 ⤠utilization < 0.90 |
\x1b[31m |
ë¹¨ê° (ìí) | utilization ⥠0.90 |
\x1b[2m |
dim (ë³´ì¡° í ì¤í¸) | 리ì ìê°, ë ì´ë¸ ë± |
\x1b[0m |
reset | ìì ì¢ ë£ |
3-5. ë° ì°¨í¸ íì
function makeBar(utilization, width = 8) {
const filled = Math.round(utilization * width);
return 'â'.repeat(filled) + 'â'.repeat(width - filled);
}
íì ìì:
Q1 5h:[ââââââââ]52%(1h17m) wk:[ââââââââ]7%(6d20h)
Q2 5h:[ââââââââ]78%(0h43m) wk:[ââââââââ]52%(3d12h)
리ì ê¹ì§ ë¨ì ìê° í¬ë§·:
function formatRemaining(resetsAt) {
const ms = new Date(resetsAt) - Date.now();
if (ms <= 0) return '0m';
const h = Math.floor(ms / 3600000);
const m = Math.floor((ms % 3600000) / 60000);
if (h >= 24) {
const d = Math.floor(h / 24);
return `${d}d${h % 24}h`;
}
return `${h}h${m}m`;
}
4. 구í í¨í´
4-1. HTTP ì¡°í (node:http / node:https)
import { request } from 'node:https';
function httpGet(url, headers = {}) {
return new Promise((resolve) => {
const mod = url.startsWith('https') ? require('node:https') : require('node:http');
const req = mod.get(url, { headers, timeout: 5000 }, res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try { resolve(JSON.parse(body)); }
catch { resolve(null); }
});
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
});
}
4-2. íì¼ ê¸°ë° ìºì± (30ì´ TTL)
ì´ì¤ ìê³ê° ì ëµ:
- TTL (30s): ë§ë£ ì 백그ë¼ì´ë ê°±ì í¸ë¦¬ê±°, 기존 ìºì ë°í
- TTL*3 (90s): ì´ê³¼ ì ìºì í기, null ë°í
import { readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CACHE_PATH = join(homedir(), '.cliproxy-statusline-cache.json');
const TTL = 30_000;
function readCache() {
try {
const { ts, data } = JSON.parse(readFileSync(CACHE_PATH, 'utf8'));
const age = Date.now() - ts;
return { data, stale: age > TTL, dead: age > TTL * 3 };
} catch {
return { data: null, stale: true, dead: true };
}
}
function writeCache(data) {
writeFileSync(CACHE_PATH, JSON.stringify({ ts: Date.now(), data }));
}
4-3. Fill-first ì ë ¬
weekly utilization ë´ë¦¼ì°¨ì ì ë ¬ â ê°ì¥ ëì ê³ì ì´ íì¬ íì±(fill-first ìì§ ì¤).
accounts.sort((a, b) => b.seven_day.utilization - a.seven_day.utilization);
4-4. íë¡ì íì± íë³
íì¬ ì¸ì
ì´ íë¡ì를 ê²½ì íëì§ íë³í©ëë¤. ANTHROPIC_BASE_URL íê²½ë³ìê° ì¤ì ëì´ ìê³ , ê·¸ ê°ì´ ì¤ì íì¼ì proxyUrlê³¼ ëì¼í í¸ì¤í¸ë¥¼ ê°ë¦¬í¬ ëë§ íë¡ì ì¸ì
ì¼ë¡ ê°ì£¼í©ëë¤.
function isProxySession(config) {
const baseUrl = process.env.ANTHROPIC_BASE_URL;
if (!baseUrl) return false;
try {
const proxyHost = new URL(config.proxyUrl).host;
const sessionHost = new URL(baseUrl).host;
return proxyHost === sessionHost;
} catch {
return false;
}
}
4-5. ì ì²´ ì¡°í í¨ì
const config = loadConfig();
if (!config || !isProxySession(config)) {
// ì¤ì ìì ëë íë¡ì ë¹ê²½ì ì¸ì
-- ì¿¼í° ë¼ì¸ ìëµ
process.stdout.write('\n');
process.exit(0);
}
async function fetchProxyUsage(proxyUrl, mgmtKey) {
const authHeader = { Authorization: `Bearer ${mgmtKey}` };
// 1. auth-files ëª©ë¡ (ìëµì´ {files:[...]} ê°ì²´)
const resp = await httpGet(`${proxyUrl}/v0/management/auth-files`, authHeader);
const files = resp.files || [];
const claudeFiles = files.filter(f => f.provider === 'claude');
// 2. ê° íì¼ìì í í° ë¤ì´ë¡ë
const tokens = await Promise.all(
claudeFiles.map(f =>
httpGet(`${proxyUrl}/v0/management/auth-files/download?name=${encodeURIComponent(f.name)}`, authHeader)
.then(d => d.access_token)
)
);
// 3. Anthropic usage API ì¡°í
const usages = await Promise.all(
tokens.map(token =>
httpGet('https://api.anthropic.com/api/oauth/usage', {
Authorization: `Bearer ${token}`,
'anthropic-beta': 'oauth-2025-04-20',
})
)
);
return usages;
}
const usages = await fetchProxyUsage(config.proxyUrl, config.managementKey);
4-6. utilization ì ê·í
Anthropic APIë utilizationì í¼ì¼í¸ ë¨ì(ì: 37.0 = 37%)ë¡ ë°íí©ëë¤. ë´ë¶ì ì¼ë¡ 0.0~1.0 ë¹ì¨ë¡ ì ê·íí©ëë¤.
function normalizeUtil(val) {
return val > 1 ? val / 100 : val;
}
4-7. íì ë ëë§
function colorize(utilization, text) {
const color = utilization >= 0.9 ? '\x1b[31m'
: utilization >= 0.7 ? '\x1b[33m'
: '\x1b[32m';
return `${color}${text}\x1b[0m`;
}
function renderQuota(usages) {
return usages.map((u, i) => {
const fh = { utilization: normalizeUtil(u.five_hour.utilization), resets_at: u.five_hour.resets_at };
const wk = { utilization: normalizeUtil(u.seven_day.utilization), resets_at: u.seven_day.resets_at };
const fhPct = Math.round(fh.utilization * 100);
const wkPct = Math.round(wk.utilization * 100);
return (
`\x1b[2mQ${i + 1}\x1b[0m ` +
`\x1b[2m5h:\x1b[0m[${colorize(fh.utilization, makeBar(fh.utilization))}]` +
`${colorize(fh.utilization, `${fhPct}%`)}` +
`\x1b[2m(${formatRemaining(fh.resets_at)})\x1b[0m ` +
`\x1b[2mwk:\x1b[0m[${colorize(wk.utilization, makeBar(wk.utilization))}]` +
`${colorize(wk.utilization, `${wkPct}%`)}` +
`\x1b[2m(${formatRemaining(wk.resets_at)})\x1b[0m`
);
}).join('\n');
}
5. OMC HUD ì°ë (ì í ì¬í)
oh-my-claudecode(OMC) HUD를 ì¬ì©íë ê²½ì°, ë³ë statusLine 커맨ë ìì´ HUD ì¶ë ¥ì ì¿¼í° ë¼ì¸ì íµí©í ì ììµëë¤. OMC ìì´ standalone 모ë를 ì¬ì©íë ê²½ì° ì´ ì¹ì ì ê±´ëë°ì¸ì.
ìì¸ êµ¬íì references/omc-hud.md를 참조íì¸ì.
6. í¸ë¬ë¸ìí
íë¡ì ìë² ë¯¸ìëµ
ì¦ì: ì¿¼í° ë¼ì¸ì´ íìëì§ ìê±°ë ë¹ ìí.
ëì:
- TTL ì´ë´ ìºìê° ìì¼ë©´ ìºì ë°ì´í° íì (stale ë§í¹)
- TTL*3 ì´ê³¼ ì null ë°í â ì¿¼í° ë¼ì¸ ìëµ
í´ê²°: íë¡ì ìë² ì¤í ìí íì¸. curl {PROXY_URL}/v0/management/auth-files -H "Authorization: Bearer {KEY}" ë¡ ì§ì í
ì¤í¸.
ì¤ì íì¼ì ì°¾ì ì ìì
ì¦ì: ì¿¼í° ë¼ì¸ì´ íìëì§ ìì.
íì¸: ~/.cliproxy-statusline.json íì¼ì´ ì¡´ì¬íëì§ íì¸í©ëë¤.
cat ~/.cliproxy-statusline.json
í´ê²°: íì¼ì´ ìì¼ë©´ ìëì¼ë¡ ìì±í©ëë¤:
echo '{"proxyUrl":"http://localhost:3000","managementKey":"YOUR_KEY"}' > ~/.cliproxy-statusline.json
ëë ì¤í¬ì ë¤ì í¸ì¶íì¬ ì¤ì íë¦ì ì¤íí©ëë¤.
settings.jsonì envê° ìíë¼ì¸ì ì ë¬ëì§ ìì
ì¦ì: settings.jsonì env ì¹ì
ì ì¤ì í íê²½ë³ìê° statusLine íë¡ì¸ì¤ìì ì½íì§ ìì.
í´ê²°: ~/.cliproxy-statusline.json ì¤ì íì¼ì ì¬ì©í©ëë¤ (Section 0 참조). íê²½ë³ì ëì , loadConfig()ë¡ ì¤ì ì ì½ìµëë¤.
const config = loadConfig(); // ~/.cliproxy-statusline.json
const { proxyUrl, managementKey } = config;
íë¡ì ê²½ì ì¸ë° 쿼í°ê° íìëì§ ìì
ì¦ì: íë¡ì를 íµí´ ì ìíëë° ì¿¼í° ë¼ì¸ì´ íìëì§ ìì.
ìì¸: isProxySession()ì process.env.ANTHROPIC_BASE_URLì íì¸í©ëë¤. ì´ íê²½ë³ìê° ì
¸ ë 벨ìì ì¤ì ëì´ì¼ statusLine íë¡ì¸ì¤ê° ììë°ì ì ììµëë¤. settings.jsonì env ì¹ì
ìë§ ì¤ì ë ê²½ì° statusLine íë¡ì¸ì¤ìë ì ë¬ëì§ ììµëë¤.
í´ê²°: íë¡ì ì¤í ëí¼(ì: ccs alias)ìì ANTHROPIC_BASE_URLì ì
¸ íê²½ë³ìë¡ exportí©ëë¤:
export ANTHROPIC_BASE_URL="http://localhost:3000"
claude # ì´ íë¡ì¸ì¤ì statusLineì´ íê²½ë³ì를 ììë°ì
OMC non-breaking space ë³íì¼ë¡ 문ìì´ ë§¤ì¹ ì¤í¨
ì¦ì: OMC HUDê° ì¶ë ¥ 문ìì´ ë´ ì¼ë° 공백ì \u00A0(non-breaking space)ì¼ë¡ ë³ííì¬ ì ê·ìì´ë 문ìì´ ë¹êµê° ì¤í¨í¨.
í´ê²°: 공백 ë¹êµ ì \u00A0ë í¨ê» ì²ë¦¬:
const normalized = str.replace(/\u00A0/g, ' ');
첫 ì¤í ì ìºì ìì
ì¦ì: ìµì´ ì¤í ì ì¿¼í° ë¼ì¸ì´ íìëì§ ìì.
ëì (ì ì): ìºì íì¼ì´ ìì¼ë©´ fetch를 awaitíì¬ ìºì를 ìì±íê³ , 결과를 íìí©ëë¤.
const config = loadConfig();
if (!config || !isProxySession(config)) {
process.stdout.write('\n');
process.exit(0);
}
const { data, stale, dead } = readCache();
if (!dead && data) {
output = renderQuota(data);
if (stale) {
// stale: 기존 ìºì íì í 백그ë¼ì´ë ê°±ì (fire-and-forget OK)
fetchProxyUsage(config.proxyUrl, config.managementKey)
.then(writeCache)
.catch(() => {});
}
} else {
// dead ëë ìºì ìì: awaitë¡ fetch ìë£ ë기
try {
const fresh = await fetchProxyUsage(config.proxyUrl, config.managementKey);
writeCache(fresh);
output = renderQuota(fresh);
} catch {
output = '';
}
}