@tsa-group/claude-usage 0.3.3 → 0.3.4
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/dist/cli.js +4 -0
- package/dist/doctor.js +168 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* prompt / 回應內容**永遠不離開這台機器**。
|
|
7
7
|
*/
|
|
8
8
|
import { printHealth, tick } from "./daemon.js";
|
|
9
|
+
import { doctor } from "./doctor.js";
|
|
9
10
|
import { install, status, uninstall } from "./install.js";
|
|
10
11
|
import { DEFAULT_SERVER, configPath, setConfig } from "./paths.js";
|
|
11
12
|
import { enroll, report } from "./upload.js";
|
|
@@ -27,6 +28,7 @@ const HELP = `claude-usage v${VERSION} — per-user Claude usage collector
|
|
|
27
28
|
enroll [--enroll-secret <密語>]
|
|
28
29
|
report [--full] [--dry-run]
|
|
29
30
|
health 背景任務心跳;不健康時 exit 1
|
|
31
|
+
doctor 自我診斷:印出解析到的路徑與那裡實際有什麼
|
|
30
32
|
uninstall 移除 hook 與背景任務
|
|
31
33
|
|
|
32
34
|
隱私:Claude OAuth token 與 prompt/回應內容永不離開本機。只送數字與帳號 email/uuid。`;
|
|
@@ -78,6 +80,8 @@ async function main() {
|
|
|
78
80
|
return 0;
|
|
79
81
|
case "health":
|
|
80
82
|
return printHealth();
|
|
83
|
+
case "doctor":
|
|
84
|
+
return await doctor();
|
|
81
85
|
case "enroll":
|
|
82
86
|
return await enroll(argv);
|
|
83
87
|
case "report":
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `claude-usage doctor` — 把「為什麼不動」變成工具自己能回答的問題。
|
|
3
|
+
*
|
|
4
|
+
* 存在的理由:第一台 Windows 機器卡在憑證讀取時,錯誤訊息只說「讀不到」,於是
|
|
5
|
+
* 診斷變成「管理者手寫一段指令 → 請對方貼回來 → 猜 → 再寫一段」。這個迴圈每多一位
|
|
6
|
+
* 同事就重演一次,而卡住的人通常不知道自己卡住了。
|
|
7
|
+
*
|
|
8
|
+
* 原則:**印出「我解析到什麼路徑」與「那裡實際有什麼」,而不是只印結論。**
|
|
9
|
+
* 路徑對不對是使用者一眼就能判斷的事(「那不是我的 Claude Code 裝的地方」),
|
|
10
|
+
* 但前提是我們得把路徑印出來。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
13
|
+
import { homedir, platform, release, userInfo } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { EXIT, loadToken } from "./creds.js";
|
|
16
|
+
import { claudeDir, claudeSettingsPath, configPath, credsPath, devicePath, projectsDir, serverUrl, stateDir, } from "./paths.js";
|
|
17
|
+
import { healthWarning, loadHealth } from "./health.js";
|
|
18
|
+
import { readJson } from "./util.js";
|
|
19
|
+
import { VERSION } from "./version.js";
|
|
20
|
+
const ok = (s) => ` [ok] ${s}`;
|
|
21
|
+
const bad = (s) => ` [BAD] ${s}`;
|
|
22
|
+
const info = (s) => ` [info] ${s}`;
|
|
23
|
+
/** 目錄裡的 .jsonl 檔數與最新修改時間 —— 用來判斷 Claude Code 是不是真的在這裡跑 */
|
|
24
|
+
function jsonlSummary(dir) {
|
|
25
|
+
let files = 0;
|
|
26
|
+
let newest = null;
|
|
27
|
+
const walk = (d, depth) => {
|
|
28
|
+
if (depth > 3)
|
|
29
|
+
return;
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const e of entries) {
|
|
38
|
+
const p = join(d, e.name);
|
|
39
|
+
if (e.isDirectory())
|
|
40
|
+
walk(p, depth + 1);
|
|
41
|
+
else if (e.name.endsWith(".jsonl")) {
|
|
42
|
+
files++;
|
|
43
|
+
try {
|
|
44
|
+
const m = statSync(p).mtime;
|
|
45
|
+
if (!newest || m > newest)
|
|
46
|
+
newest = m;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* 檔案在走訪途中被換掉是正常的 */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(dir, 0);
|
|
55
|
+
return { files, newest };
|
|
56
|
+
}
|
|
57
|
+
export async function doctor() {
|
|
58
|
+
const problems = [];
|
|
59
|
+
const out = [];
|
|
60
|
+
out.push(`claude-usage doctor v${VERSION}`);
|
|
61
|
+
out.push("");
|
|
62
|
+
out.push("環境");
|
|
63
|
+
out.push(info(`平台 ${platform()} ${release()} node ${process.version}`));
|
|
64
|
+
out.push(info(`使用者 ${userInfo().username}`));
|
|
65
|
+
out.push(info(`家目錄 ${homedir()}`));
|
|
66
|
+
// ── Claude Code 的設定目錄 ──
|
|
67
|
+
out.push("");
|
|
68
|
+
out.push("Claude Code 資料");
|
|
69
|
+
const fromEnv = Boolean(process.env["CLAUDE_CONFIG_DIR"]);
|
|
70
|
+
out.push(info(`設定目錄 ${claudeDir()}${fromEnv ? " <- 來自 CLAUDE_CONFIG_DIR" : " (預設)"}`));
|
|
71
|
+
if (!existsSync(claudeDir())) {
|
|
72
|
+
out.push(bad(`設定目錄不存在 —— Claude Code 沒有在這個使用者/這台機器上跑過`));
|
|
73
|
+
problems.push(`找不到 ${claudeDir()}。若你的 Claude Code 跑在 WSL 或別的使用者底下,` +
|
|
74
|
+
`本工具要裝在同一個環境裡才看得到資料`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
out.push(ok(`設定目錄存在`));
|
|
78
|
+
}
|
|
79
|
+
// ── 憑證 ──
|
|
80
|
+
// ⚠️ 這裡的呈現順序很重要。mac 的**主儲存是 Keychain**,`.credentials.json` 只是
|
|
81
|
+
// 可能過時的副本 —— 所以「檔案不存在」在 mac 上不是問題,在 Windows/Linux 上才是。
|
|
82
|
+
// 先前把兩者都印成 [BAD]/[ok],結果出現「憑證檔不存在」緊接著「憑證可讀」這種
|
|
83
|
+
// 自相矛盾的畫面。判定一律以 loadToken() 的結果為準,上面兩行只是事實陳述。
|
|
84
|
+
const cp = credsPath();
|
|
85
|
+
const isMac = platform() === "darwin";
|
|
86
|
+
out.push(info(`憑證主儲存 ${isMac ? "macOS Keychain(檔案只是副本,可能過時)" : "檔案"}`));
|
|
87
|
+
out.push(info(`憑證檔 ${cp} -> ${existsSync(cp) ? `存在 ${statSync(cp).size} bytes` : "不存在"}` +
|
|
88
|
+
(!existsSync(cp) && !isMac ? " <- 這個平台只有檔案這條路,所以是問題" : "")));
|
|
89
|
+
let credOk = false;
|
|
90
|
+
try {
|
|
91
|
+
const t = loadToken();
|
|
92
|
+
credOk = true;
|
|
93
|
+
out.push(ok(`憑證可讀(來源 ${t.source},tier=${t.tier ?? "?"},方案=${t.subscription ?? "?"})`));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
const err = e;
|
|
97
|
+
out.push(bad(`憑證讀取失敗`));
|
|
98
|
+
for (const line of err.message.split("\n"))
|
|
99
|
+
out.push(` ${line.trim()}`);
|
|
100
|
+
problems.push(err.code === EXIT.TOKEN_EXPIRED
|
|
101
|
+
? "token 過期 —— 在 Claude Code 裡跑 /login"
|
|
102
|
+
: "讀不到憑證(細節見上)。本工具需要**帳號登入**(/login);用 API key / Bedrock / Vertex 時取不到額度資訊");
|
|
103
|
+
}
|
|
104
|
+
// ── session 資料 ──
|
|
105
|
+
const pd = projectsDir();
|
|
106
|
+
out.push(info(`session 目錄 ${pd}`));
|
|
107
|
+
const js = jsonlSummary(pd);
|
|
108
|
+
if (js.files > 0) {
|
|
109
|
+
out.push(ok(`找到 ${js.files} 個 .jsonl,最新 ${js.newest?.toLocaleString() ?? "?"}`));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
out.push(bad(`沒有任何 .jsonl —— 這個目錄裡沒有 Claude Code 的使用紀錄`));
|
|
113
|
+
problems.push(`${pd} 是空的。最常見原因是 Claude Code 實際跑在別的地方(WSL、另一個 Windows 帳號、` +
|
|
114
|
+
`或設了 CLAUDE_CONFIG_DIR),本工具看的目錄和它不是同一個`);
|
|
115
|
+
}
|
|
116
|
+
// hook
|
|
117
|
+
const sp = claudeSettingsPath();
|
|
118
|
+
const settings = readJson(sp);
|
|
119
|
+
out.push(settings
|
|
120
|
+
? ok(`settings.json 可讀(SessionStart 群組 ${settings.hooks?.SessionStart?.length ?? 0} 個)`)
|
|
121
|
+
: info(`settings.json 不存在或無法解析:${sp}`));
|
|
122
|
+
// ── 本工具自己的狀態 ──
|
|
123
|
+
out.push("");
|
|
124
|
+
out.push("claude-usage 狀態");
|
|
125
|
+
out.push(info(`狀態目錄 ${stateDir()}`));
|
|
126
|
+
out.push(info(`設定檔 ${configPath()}`));
|
|
127
|
+
out.push(info(`server ${serverUrl()}`));
|
|
128
|
+
const enrolled = existsSync(devicePath());
|
|
129
|
+
out.push(enrolled ? ok("已註冊(device.json 存在)") : bad("尚未註冊 —— 沒有 device.json"));
|
|
130
|
+
if (!enrolled && credOk) {
|
|
131
|
+
problems.push("憑證正常但尚未註冊 —— 跑 claude-usage install(或 claude-usage enroll)");
|
|
132
|
+
}
|
|
133
|
+
const h = loadHealth();
|
|
134
|
+
if (Object.keys(h).length) {
|
|
135
|
+
const warn = healthWarning(h);
|
|
136
|
+
out.push(warn
|
|
137
|
+
? bad(`採集狀態 UNHEALTHY:${warn}`)
|
|
138
|
+
: ok(`採集狀態正常(上次成功 ${h.last_ok_at ?? "-"})`));
|
|
139
|
+
if (h.last_error)
|
|
140
|
+
out.push(` 上次錯誤: ${h.last_error}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
out.push(info("尚無 health 紀錄(背景任務還沒跑過)"));
|
|
144
|
+
}
|
|
145
|
+
// ── server 連通性 ──
|
|
146
|
+
try {
|
|
147
|
+
const r = await fetch(serverUrl() + "/v1/health", { signal: AbortSignal.timeout(10_000) });
|
|
148
|
+
out.push(r.ok ? ok(`server 可連線(HTTP ${r.status})`) : bad(`server 回應 HTTP ${r.status}`));
|
|
149
|
+
if (!r.ok)
|
|
150
|
+
problems.push(`server 回應 ${r.status} —— 確認 configure --server 的位址正確`);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
out.push(bad(`server 連不上:${e.message}`));
|
|
154
|
+
problems.push("連不到 ingest server —— 檢查網路/VPN 與 configure --server 的位址");
|
|
155
|
+
}
|
|
156
|
+
out.push("");
|
|
157
|
+
if (problems.length === 0) {
|
|
158
|
+
out.push("結論:沒有發現問題。");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
out.push(`結論:${problems.length} 個問題`);
|
|
162
|
+
problems.forEach((p, i) => out.push(` ${i + 1}. ${p}`));
|
|
163
|
+
out.push("");
|
|
164
|
+
out.push("把以上整段貼給管理者,通常一眼就能看出是哪一種。");
|
|
165
|
+
}
|
|
166
|
+
console.log(out.join("\n"));
|
|
167
|
+
return problems.length ? 1 : 0;
|
|
168
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsa-group/claude-usage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Per-user Claude usage collector — measures Claude Code token detail and account-level rate-limit utilization locally, reports to your own ingest server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|