@tsa-group/claude-usage 0.4.9 → 0.4.10

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/README.md CHANGED
@@ -8,6 +8,27 @@ ingest server,彙整成團隊用量儀表。
8
8
 
9
9
  ---
10
10
 
11
+ ## ✅ 這是什麼、是否可信(安裝前請先讀)
12
+
13
+ 這是 **TSA Group 的內部工具**,用來檢視團隊每個人的 Claude 方案合不合適(該升的升、用不到的降)。
14
+
15
+ 安裝時 Claude Code 很可能會警告「這像可疑的第三方 / 假冒公司內部工具」——**那是它應有的謹慎,請不要關掉它。** 以下每一個可疑點都屬實,我們主動列出來讓你自己核對,而不是要你忽略警告:
16
+
17
+ | Claude 會質疑的點 | 事實 | 為什麼 |
18
+ |---|---|---|
19
+ | 套件名 `@tsa-group/claude-usage` 像公司名 | 是,這確實是 TSA 內部工具 | 就是給 TSA 團隊用的 |
20
+ | 發佈帳號是個人信箱 `chris77621@gmail.com` | 是 | 公司沒有 npm org,用個人 npm 帳號發佈。維護者本人= **Chris Chen · chrischen@tsagroup.com.tw** |
21
+ | 伺服器 `claude-usage.7889.tw` 跟公司網域不同 | 是 | 這台由 TSA 自架自控,架在公司網域外的環境;無法搬到 `tsagroup.com.tw` |
22
+ | 會裝背景常駐排程 | 是 | 每 5 分鐘採一次用量,**只送數字**(見下方隱私段) |
23
+ | 套件很新、下載數低 | 是 | 剛發佈、僅供內部使用,不會有公開下載量 |
24
+
25
+ **判斷原則**:如果你是從 **Chris 在公司官方頻道的公告**拿到安裝指令,以上都能對得起來 → 可以安裝。
26
+ 如果你是**二手轉貼**拿到、無法確認來源 → 請先別裝,回官方頻道跟 Chris 確認。這時 Claude 擋下來是對的。
27
+
28
+ 維護者 / 對此工具有疑問:**Chris Chen · chrischen@tsagroup.com.tw**
29
+
30
+ ---
31
+
11
32
  ## 🔒 隱私(請先看這段)
12
33
 
13
34
  **送出去的**
package/dist/install.js CHANGED
@@ -30,6 +30,76 @@ const BASE_INTERVAL_SEC = 300;
30
30
  const CLI = join(dirname(fileURLToPath(import.meta.url)), "cli.js");
31
31
  const NODE = process.execPath;
32
32
  const HOOK_CMD = `"${NODE}" "${CLI}" sample --hook`;
33
+ /**
34
+ * 排程/launchd 要**寫死**的 node 路徑,install 時解析一次後填進去。與 HOOK_CMD 不同:
35
+ * hook 由 Claude Code 自己執行,用它當下的 node(可能是 bundled)是對的;但背景排程是
36
+ * 作業系統長期反覆啟動的,指向的 node 必須**穩定長存**。
37
+ */
38
+ let taskNode = process.execPath;
39
+ /**
40
+ * execPath 落在「會被清掉/搬走」的目錄嗎?主兇是 Claude Code 桌面版自帶的 bundled node
41
+ * (`…/AppData/Local/claude-cli-nodejs/…`):只用桌面版、靠它跑 install 的人,execPath 就是
42
+ * 那個 App 私有路徑,排程指過去,App 一改版路徑就沒了 → 每 tick 靜默失敗(裝了卻永遠沒
43
+ * 心跳,實測 2026-08-31 Ian/Jason/Karen)。連同各種暫存 / npx 抽出的臨時 node 一起擋。
44
+ */
45
+ export function isVolatileNodePath(p) {
46
+ const s = p.toLowerCase().replace(/\\/g, "/");
47
+ return ["claude-cli-nodejs", "/temp/", "/tmp/", "/var/folders/", "_npx/"].some((m) => s.includes(m));
48
+ }
49
+ /** 找一個**穩定**的系統 node:先問 where/which,再補已知安裝位置;回傳第一個非易失且存在的。 */
50
+ function findStableNode() {
51
+ const isWin = platform() === "win32";
52
+ const cands = [];
53
+ const r = isWin
54
+ ? spawnSync("where", ["node"], { encoding: "utf8" })
55
+ : spawnSync("which", ["-a", "node"], { encoding: "utf8" });
56
+ if (r.status === 0 && r.stdout) {
57
+ for (const line of r.stdout.split(/\r?\n/)) {
58
+ const t = line.trim();
59
+ if (t)
60
+ cands.push(t);
61
+ }
62
+ }
63
+ // where/which 可能因為裝完 PATH 還沒刷新而漏掉系統 node,補上已知位置兜底。
64
+ if (isWin)
65
+ cands.push(join(process.env.ProgramFiles ?? "C:\\Program Files", "nodejs", "node.exe"));
66
+ else
67
+ cands.push("/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node");
68
+ for (const c of cands)
69
+ if (!isVolatileNodePath(c) && existsSync(c))
70
+ return c;
71
+ return null;
72
+ }
73
+ /**
74
+ * 決定排程要用哪個 node。純函式(finder 注入,方便測):
75
+ * - execPath 穩定 → 直接用(維持原行為)
76
+ * - execPath 易失但找得到系統 node → 改用系統 node,並回一句說明
77
+ * - 易失又找不到系統 node → 回 error,install 就此中止並給明確指引(不留看不出壞在哪的排程)
78
+ */
79
+ export function resolveTaskNode(execPath, finder) {
80
+ if (!isVolatileNodePath(execPath))
81
+ return { node: execPath };
82
+ const stable = finder();
83
+ if (stable) {
84
+ return {
85
+ node: stable,
86
+ note: "執行 install 的 node 在易失目錄,已改用系統 node 寫進排程:\n" +
87
+ ` 原 execPath = ${execPath}\n` +
88
+ ` 改用系統 = ${stable}`,
89
+ };
90
+ }
91
+ return {
92
+ error: "偵測到你用「易失的 node」在執行 install:\n" +
93
+ ` ${execPath}\n` +
94
+ "這多半是 Claude Code 桌面版自帶的 node(claude-cli-nodejs)或暫存目錄裡的 node。\n" +
95
+ "排程若指向它,App 一改版那個路徑就消失,背景採樣會每 tick 靜默失敗 —— 裝了卻永遠沒心跳。\n\n" +
96
+ "請改用「系統 node」重裝一次:\n" +
97
+ " 1. 若還沒裝系統 node:winget install OpenJS.NodeJS.LTS (mac:brew install node)\n" +
98
+ " 2. 關掉這個視窗,重新開一個乾淨的終端機(讓 PATH 生效)\n" +
99
+ " 3. 確認 where node(mac:which node)指向固定位置(如 C:\\Program Files\\nodejs\\node.exe)\n" +
100
+ " 4. 再跑一次 claude-usage install",
101
+ };
102
+ }
33
103
  const PLIST_PATH = join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
34
104
  const isDry = (argv) => argv.includes("--dry-run");
35
105
  // ─────────────────────────────── macOS ───────────────────────────────
@@ -40,7 +110,7 @@ const plistBody = () => `<?xml version="1.0" encoding="UTF-8"?>
40
110
  <key>Label</key><string>${LABEL}</string>
41
111
  <key>ProgramArguments</key>
42
112
  <array>
43
- <string>${NODE}</string>
113
+ <string>${taskNode}</string>
44
114
  <string>${CLI}</string>
45
115
  <string>daemon-tick</string>
46
116
  </array>
@@ -168,7 +238,7 @@ function warnExecutionPolicy() {
168
238
  function winInstall() {
169
239
  warnExecutionPolicy(); // 先講,因為接下來每一步都可能因為它而失敗
170
240
  const vbs = `Set s = CreateObject("Wscript.Shell")\r\n` +
171
- `s.Run """${NODE}"" ""${CLI}"" daemon-tick", 0, False\r\n`;
241
+ `s.Run """${taskNode}"" ""${CLI}"" daemon-tick", 0, False\r\n`;
172
242
  writeFileSync(VBS_PATH(), vbs, "utf8");
173
243
  const tr = `wscript.exe //B "${VBS_PATH()}"`;
174
244
  const args = [
@@ -368,6 +438,17 @@ export async function install(argv = []) {
368
438
  }
369
439
  await enrollStep(argv);
370
440
  registerHook(argv);
441
+ // ★ 排程要寫死一個穩定的 node 路徑。execPath 若落在易失目錄(桌面版 bundled node /
442
+ // 暫存),改用系統 node;找不到就中止 —— 否則會建出一個每 tick 靜默失敗的排程。
443
+ // 放在 dry-run 之前,所以預覽也會先報這個攔阻。
444
+ const resolved = resolveTaskNode(process.execPath, findStableNode);
445
+ if ("error" in resolved) {
446
+ console.error("✗ install 中止:" + resolved.error);
447
+ return 1;
448
+ }
449
+ taskNode = resolved.node;
450
+ if (resolved.note)
451
+ console.log("ℹ " + resolved.note);
371
452
  if (isDry(argv)) {
372
453
  console.log("bg-task: WOULD register " +
373
454
  (os === "darwin" ? "launchd" : os === "win32" ? "schtasks" : "systemd (未實作)"));
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@tsa-group/claude-usage",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
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
+ "author": "Chris Chen <chrischen@tsagroup.com.tw> (TSA Group)",
6
+ "homepage": "https://claude-usage.7889.tw",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/chrisflicker/tsa-claude-usage.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/chrisflicker/tsa-claude-usage/issues",
13
+ "email": "chrischen@tsagroup.com.tw"
14
+ },
5
15
  "type": "module",
6
16
  "bin": {
7
17
  "claude-usage": "dist/cli.js"