claude-nexus 0.32.0 → 0.33.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.en.md +6 -1
- package/README.md +6 -1
- package/package.json +1 -1
- package/scripts/statusline.mjs +177 -17
- package/settings.json +1 -1
package/README.en.md
CHANGED
|
@@ -51,7 +51,12 @@ With the plugin enabled, each new Claude Code session runs the `lead` agent as t
|
|
|
51
51
|
|
|
52
52
|
## Optional: statusline
|
|
53
53
|
|
|
54
|
-
The plugin ships a two-line statusline script. Line one shows `◆Nexus vX.Y.Z`, the model, the project, and the git branch with staged/unstaged counts. Line two shows context-window usage plus
|
|
54
|
+
The plugin ships a two-line statusline script. Line one shows `◆Nexus vX.Y.Z`, the model, the project, and the git branch with staged/unstaged counts. Line two shows context-window usage plus mode-specific information:
|
|
55
|
+
|
|
56
|
+
- **OAuth session (Claude Pro / Max)** — 5-hour and 7-day Claude usage gauges with the time until each resets. `$CLAUDE_CONFIG_DIR/.usage_cache` (defaults to `~/.claude/.usage_cache`) is shared across local sessions so concurrent Claude Code windows never re-fetch.
|
|
57
|
+
- **API mode (`ANTHROPIC_API_KEY` set)** — `API $X.XX today` showing the cost incurred today (UTC midnight boundary). Claude Code's local jsonl session logs are scanned directly to sum tokens per model and convert to USD using Anthropic's published pricing — no admin key setup required.
|
|
58
|
+
|
|
59
|
+
When you use `CLAUDE_CONFIG_DIR` to separate multiple OAuth accounts, the statusline's cache path, keychain query, and cost scan all branch automatically using the same algorithm as Claude Code itself.
|
|
55
60
|
|
|
56
61
|
Claude Code does not let a plugin auto-configure the user's `statusLine`, so register the `claude-nexus-statusline` CLI (shipped with the same npm package) from your own `~/.claude/settings.json`.
|
|
57
62
|
|
package/README.md
CHANGED
|
@@ -51,7 +51,12 @@ Claude Code 안에서 플러그인 마켓플레이스로 설치한다.
|
|
|
51
51
|
|
|
52
52
|
## 선택: statusline
|
|
53
53
|
|
|
54
|
-
플러그인은 2줄 statusline 스크립트를 함께 배포한다. 첫 줄은 `◆Nexus vX.Y.Z`·모델·프로젝트·git 브랜치(staged/unstaged), 둘째 줄은 컨텍스트 사용률과
|
|
54
|
+
플러그인은 2줄 statusline 스크립트를 함께 배포한다. 첫 줄은 `◆Nexus vX.Y.Z`·모델·프로젝트·git 브랜치(staged/unstaged), 둘째 줄은 컨텍스트 사용률과 모드별 사용량 정보:
|
|
55
|
+
|
|
56
|
+
- **OAuth 세션 (Claude Pro·Max)** — 5h/7d 사용 한도 게이지(리셋까지 남은 시간). 로컬의 여러 Claude 세션이 `$CLAUDE_CONFIG_DIR/.usage_cache`(미설정 시 `~/.claude/.usage_cache`)를 공유하므로 API 중복 호출 없이 경합이 방지된다.
|
|
57
|
+
- **API 모드 (`ANTHROPIC_API_KEY` 설정)** — `API $X.XX today` 형식으로 오늘(UTC 자정 기준) 발생한 비용을 표시. Claude Code가 기록하는 로컬 jsonl 세션 로그를 직접 스캔해 모델별 토큰을 합산하고 Anthropic 공식 가격표 기반으로 USD 환산하므로, 별도 admin key 셋업이 불필요하다.
|
|
58
|
+
|
|
59
|
+
`CLAUDE_CONFIG_DIR` 환경변수로 다중 OAuth 계정을 분리해 쓰는 경우, statusline의 캐시·키체인 조회·비용 스캔이 모두 본체와 동일한 알고리즘으로 자동 분기된다.
|
|
55
60
|
|
|
56
61
|
Claude Code는 플러그인이 사용자 `statusLine`을 자동 등록하는 걸 허용하지 않으므로, 별도 CLI로 배포된 `claude-nexus`를 본인의 `~/.claude/settings.json`에 등록한다.
|
|
57
62
|
|
package/package.json
CHANGED
package/scripts/statusline.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/statusline/statusline.ts
|
|
4
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
var stdinRaw = "";
|
|
10
11
|
try {
|
|
11
12
|
stdinRaw = readFileSync(0, "utf-8");
|
|
@@ -29,7 +30,16 @@ function findProjectRoot(start) {
|
|
|
29
30
|
}
|
|
30
31
|
var PROJECT_ROOT = findProjectRoot(getVal("cwd") || process.cwd());
|
|
31
32
|
var HOME = homedir();
|
|
33
|
+
var CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(HOME, ".claude");
|
|
32
34
|
var PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || "";
|
|
35
|
+
var KEYCHAIN_SERVICE = (() => {
|
|
36
|
+
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
37
|
+
if (!envDir)
|
|
38
|
+
return "Claude Code-credentials";
|
|
39
|
+
const normalized = envDir.normalize("NFC");
|
|
40
|
+
const suffix = createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
41
|
+
return `Claude Code-credentials-${suffix}`;
|
|
42
|
+
})();
|
|
33
43
|
function getPluginVersion() {
|
|
34
44
|
if (PLUGIN_ROOT) {
|
|
35
45
|
try {
|
|
@@ -72,7 +82,7 @@ function makeBar(pct, width) {
|
|
|
72
82
|
function meter(label, pct, width) {
|
|
73
83
|
return `${DIM}${label}${RESET} ${pctColor(pct)}${makeBar(pct, width)} ${Math.round(pct)}%${RESET}`;
|
|
74
84
|
}
|
|
75
|
-
var VERSION_CACHE_PATH = join(
|
|
85
|
+
var VERSION_CACHE_PATH = join(CLAUDE_CONFIG_DIR, ".nexus_version_cache");
|
|
76
86
|
var VERSION_CACHE_TTL = 86400;
|
|
77
87
|
function updateAvailable(current) {
|
|
78
88
|
if (!current)
|
|
@@ -142,7 +152,7 @@ function buildLine1() {
|
|
|
142
152
|
const nexusTag = `\x1B[38;5;141m◆Nexus${versionStr}${RESET}${updateTag}`;
|
|
143
153
|
return `${nexusTag} ${SEP} ${modelColor}${BOLD}${model}${RESET} ${SEP} \x1B[36m${project}${RESET} ${SEP} ${gitPart}`;
|
|
144
154
|
}
|
|
145
|
-
var USAGE_CACHE_PATH = join(
|
|
155
|
+
var USAGE_CACHE_PATH = join(CLAUDE_CONFIG_DIR, ".usage_cache");
|
|
146
156
|
var CACHE_TTL_DEFAULT = 60;
|
|
147
157
|
var FETCH_BACKOFF = 300;
|
|
148
158
|
var STALE_THRESHOLD = 300;
|
|
@@ -166,9 +176,9 @@ ${cachedData}`);
|
|
|
166
176
|
try {
|
|
167
177
|
let tokenCmd = "";
|
|
168
178
|
if (process.platform === "darwin") {
|
|
169
|
-
tokenCmd = `TOKEN=$(security find-generic-password -s "
|
|
179
|
+
tokenCmd = `TOKEN=$(security find-generic-password -s "${KEYCHAIN_SERVICE}" -w 2>/dev/null | grep -o '"accessToken":"[^"]*"' | sed 's/"accessToken":"//;s/"//')`;
|
|
170
180
|
} else {
|
|
171
|
-
const credFile = join(
|
|
181
|
+
const credFile = join(CLAUDE_CONFIG_DIR, ".credentials.json");
|
|
172
182
|
tokenCmd = `TOKEN=$(grep -o '"accessToken":"[^"]*"' "${credFile}" 2>/dev/null | sed 's/"accessToken":"//;s/"//')`;
|
|
173
183
|
}
|
|
174
184
|
const script = `
|
|
@@ -218,12 +228,12 @@ function readUsage() {
|
|
|
218
228
|
try {
|
|
219
229
|
let credJson = "";
|
|
220
230
|
if (process.platform === "darwin") {
|
|
221
|
-
credJson = execSync(
|
|
231
|
+
credJson = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -w`, {
|
|
222
232
|
encoding: "utf-8",
|
|
223
233
|
stdio: ["pipe", "pipe", "pipe"]
|
|
224
234
|
}).trim();
|
|
225
235
|
} else {
|
|
226
|
-
const credFile = join(
|
|
236
|
+
const credFile = join(CLAUDE_CONFIG_DIR, ".credentials.json");
|
|
227
237
|
if (existsSync(credFile))
|
|
228
238
|
credJson = readFileSync(credFile, "utf-8");
|
|
229
239
|
}
|
|
@@ -274,27 +284,177 @@ function resetRemain(parsed, section) {
|
|
|
274
284
|
function isApiMode() {
|
|
275
285
|
return !!process.env.ANTHROPIC_API_KEY;
|
|
276
286
|
}
|
|
277
|
-
|
|
287
|
+
var COST_CACHE_PATH = join(CLAUDE_CONFIG_DIR, ".api_cost_cache");
|
|
288
|
+
var COST_CACHE_TTL = 60;
|
|
289
|
+
var COST_STALE_THRESHOLD = 300;
|
|
290
|
+
function priceFor(model) {
|
|
291
|
+
const m = model.toLowerCase();
|
|
292
|
+
const TABLE = [
|
|
293
|
+
[/opus-4-[5-9]/, 5, 25],
|
|
294
|
+
[/opus-(?:4-[01]|4)(?:[-_]|$)/, 15, 75],
|
|
295
|
+
[/opus-3/, 15, 75],
|
|
296
|
+
[/sonnet-4(?:-\d+)?(?:[-_]|$)|4-sonnet/, 3, 15],
|
|
297
|
+
[/sonnet-3-7|3-7-sonnet/, 3, 15],
|
|
298
|
+
[/sonnet-3-5|3-5-sonnet/, 3, 15],
|
|
299
|
+
[/haiku-4-5/, 1, 5],
|
|
300
|
+
[/haiku-3-5|3-5-haiku/, 0.8, 4],
|
|
301
|
+
[/haiku-3/, 0.25, 1.25]
|
|
302
|
+
];
|
|
303
|
+
for (const [re, inp, out] of TABLE) {
|
|
304
|
+
if (re.test(m)) {
|
|
305
|
+
const inputPerToken = inp / 1e6;
|
|
306
|
+
return {
|
|
307
|
+
input: inputPerToken,
|
|
308
|
+
output: out / 1e6,
|
|
309
|
+
cache5m: inputPerToken * 1.25,
|
|
310
|
+
cache1h: inputPerToken * 2,
|
|
311
|
+
cacheRead: inputPerToken * 0.1
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function turnCostUsd(model, usage) {
|
|
318
|
+
const rates = priceFor(model);
|
|
319
|
+
if (!rates)
|
|
320
|
+
return 0;
|
|
321
|
+
const inp = usage.input_tokens ?? 0;
|
|
322
|
+
const out = usage.output_tokens ?? 0;
|
|
323
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
324
|
+
const c5m = usage.cache_creation?.ephemeral_5m_input_tokens;
|
|
325
|
+
const c1h = usage.cache_creation?.ephemeral_1h_input_tokens;
|
|
326
|
+
let cacheWriteCost = 0;
|
|
327
|
+
if (c5m !== undefined || c1h !== undefined) {
|
|
328
|
+
cacheWriteCost = (c5m ?? 0) * rates.cache5m + (c1h ?? 0) * rates.cache1h;
|
|
329
|
+
} else {
|
|
330
|
+
cacheWriteCost = (usage.cache_creation_input_tokens ?? 0) * rates.cache5m;
|
|
331
|
+
}
|
|
332
|
+
return inp * rates.input + out * rates.output + cacheRead * rates.cacheRead + cacheWriteCost;
|
|
333
|
+
}
|
|
334
|
+
function scanLocalCostUsd() {
|
|
335
|
+
const projectsRoot = join(CLAUDE_CONFIG_DIR, "projects");
|
|
336
|
+
if (!existsSync(projectsRoot))
|
|
337
|
+
return null;
|
|
338
|
+
const todayStart = new Date;
|
|
339
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
340
|
+
const todayStartMs = todayStart.getTime();
|
|
341
|
+
let total = 0;
|
|
342
|
+
let projectDirs;
|
|
278
343
|
try {
|
|
279
|
-
|
|
280
|
-
const resp = execSync(`curl -s --max-time 3 "https://api.anthropic.com/v1/organizations/cost_report?start_date=${today}&end_date=${today}" -H "x-api-key: ${adminKey}" -H "anthropic-version: 2023-06-01"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
281
|
-
const m = resp.match(/"total_cost"\s*:\s*([0-9.]+)/);
|
|
282
|
-
return m ? parseFloat(m[1]) : null;
|
|
344
|
+
projectDirs = readdirSync(projectsRoot);
|
|
283
345
|
} catch {
|
|
284
346
|
return null;
|
|
285
347
|
}
|
|
348
|
+
for (const proj of projectDirs) {
|
|
349
|
+
const projPath = join(projectsRoot, proj);
|
|
350
|
+
let entries;
|
|
351
|
+
try {
|
|
352
|
+
entries = readdirSync(projPath);
|
|
353
|
+
} catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
for (const file of entries) {
|
|
357
|
+
if (!file.endsWith(".jsonl"))
|
|
358
|
+
continue;
|
|
359
|
+
const fp = join(projPath, file);
|
|
360
|
+
try {
|
|
361
|
+
const st = statSync(fp);
|
|
362
|
+
if (st.mtimeMs < todayStartMs)
|
|
363
|
+
continue;
|
|
364
|
+
} catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
let raw;
|
|
368
|
+
try {
|
|
369
|
+
raw = readFileSync(fp, "utf-8");
|
|
370
|
+
} catch {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const lines = raw.split(`
|
|
374
|
+
`);
|
|
375
|
+
for (const line of lines) {
|
|
376
|
+
if (!line || !line.includes('"assistant"'))
|
|
377
|
+
continue;
|
|
378
|
+
let entry;
|
|
379
|
+
try {
|
|
380
|
+
entry = JSON.parse(line);
|
|
381
|
+
} catch {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (entry.type !== "assistant")
|
|
385
|
+
continue;
|
|
386
|
+
const ts = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
|
|
387
|
+
if (!Number.isFinite(ts) || ts < todayStartMs)
|
|
388
|
+
continue;
|
|
389
|
+
const model = entry.message?.model;
|
|
390
|
+
const usage = entry.message?.usage;
|
|
391
|
+
if (!model || !usage)
|
|
392
|
+
continue;
|
|
393
|
+
total += turnCostUsd(model, usage);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return total;
|
|
398
|
+
}
|
|
399
|
+
function writeCostCacheAtomic(content) {
|
|
400
|
+
try {
|
|
401
|
+
writeFileSync(COST_CACHE_PATH + ".tmp", content);
|
|
402
|
+
renameSync(COST_CACHE_PATH + ".tmp", COST_CACHE_PATH);
|
|
403
|
+
} catch {
|
|
404
|
+
try {
|
|
405
|
+
unlinkSync(COST_CACHE_PATH + ".tmp");
|
|
406
|
+
} catch {}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function readApiCost() {
|
|
410
|
+
const now = Math.floor(Date.now() / 1000);
|
|
411
|
+
let dataTimestamp = 0;
|
|
412
|
+
let nextRescanAfter = 0;
|
|
413
|
+
let cachedValue = "";
|
|
414
|
+
if (existsSync(COST_CACHE_PATH)) {
|
|
415
|
+
try {
|
|
416
|
+
const lines = readFileSync(COST_CACHE_PATH, "utf-8").split(`
|
|
417
|
+
`);
|
|
418
|
+
dataTimestamp = parseInt(lines[0]) || 0;
|
|
419
|
+
nextRescanAfter = parseInt(lines[1]) || 0;
|
|
420
|
+
cachedValue = (lines[2] || "").trim();
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
const age = dataTimestamp > 0 ? now - dataTimestamp : 0;
|
|
424
|
+
const parseCached = () => {
|
|
425
|
+
if (!cachedValue)
|
|
426
|
+
return null;
|
|
427
|
+
const n = parseFloat(cachedValue);
|
|
428
|
+
return Number.isFinite(n) ? n : null;
|
|
429
|
+
};
|
|
430
|
+
if (cachedValue && now < nextRescanAfter) {
|
|
431
|
+
return { cost: parseCached(), stale: age >= COST_STALE_THRESHOLD, ageSeconds: age };
|
|
432
|
+
}
|
|
433
|
+
const cost = scanLocalCostUsd();
|
|
434
|
+
if (cost === null) {
|
|
435
|
+
return { cost: null, stale: false, ageSeconds: 0 };
|
|
436
|
+
}
|
|
437
|
+
writeCostCacheAtomic(`${now}
|
|
438
|
+
${now + COST_CACHE_TTL}
|
|
439
|
+
${cost}`);
|
|
440
|
+
return { cost, stale: false, ageSeconds: 0 };
|
|
286
441
|
}
|
|
287
442
|
function buildLine2() {
|
|
288
443
|
const BAR_WIDTH = 6;
|
|
289
444
|
const ctxPct = Math.round(getNum("used_percentage"));
|
|
290
445
|
const ctx = meter("ctx", ctxPct, BAR_WIDTH);
|
|
291
446
|
if (isApiMode()) {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
447
|
+
const { cost, stale, ageSeconds } = readApiCost();
|
|
448
|
+
if (cost !== null) {
|
|
449
|
+
let stalePart2 = "";
|
|
450
|
+
if (stale) {
|
|
451
|
+
const ageMin = Math.floor(ageSeconds / 60);
|
|
452
|
+
const hh = Math.floor(ageMin / 60);
|
|
453
|
+
const mm = ageMin % 60;
|
|
454
|
+
const ageStr = hh > 0 ? `${hh}h${mm}m` : `${mm}m`;
|
|
455
|
+
stalePart2 = ` ${SEP} \x1B[33m${ageStr} ago\x1B[0m`;
|
|
297
456
|
}
|
|
457
|
+
return `${ctx} ${SEP} ${DIM}API${RESET} ${pctColor(0)}$${cost.toFixed(2)} today${RESET}${stalePart2}`;
|
|
298
458
|
}
|
|
299
459
|
return `${ctx} ${SEP} ${DIM}API mode${RESET}`;
|
|
300
460
|
}
|
package/settings.json
CHANGED