@tsa-group/claude-usage 0.4.8 → 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
  **送出去的**
@@ -350,6 +371,12 @@ v0.4.7 起支援自我更新,但**由伺服器決定**,不是去問 npm:
350
371
  ## 已知限制
351
372
 
352
373
  - **非官方 endpoint**:額度 % 來自 Claude Code 內部的 `/api/oauth/usage`,Anthropic 可能變更。
374
+ - **macOS 桌面版憑證也是逆向出來的**:Claude.app 把 OAuth token 存在
375
+ `~/Library/Application Support/Claude/config.json` 的 `oauth:tokenCacheV2`,用 Chromium 的
376
+ macOS 方案(Keychain「Claude Safe Storage」主金鑰 + PBKDF2-SHA1 + AES-128-CBC)加密。
377
+ 首次背景存取會彈 Keychain 授權對話 —— 在終端機跑一次
378
+ `security find-generic-password -w -s "Claude Safe Storage"` 並按**一律允許**即可,
379
+ 之後 daemon 讀得到。`doctor` 會偵測這個狀態並提示。與 `/api/oauth/usage` 同一風險類別。
353
380
  - **Windows 桌面版憑證是逆向出來的**:桌面版把 OAuth token 存在
354
381
  `%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\config.json` 的
355
382
  `oauth:tokenCacheV2`,以 Chromium 的 `v10` 方案(DPAPI + AES-256-GCM)加密。
@@ -0,0 +1,148 @@
1
+ /**
2
+ * macOS 桌面版(Claude.app)的 Claude OAuth 憑證讀取。
3
+ *
4
+ * 為什麼需要這條路徑:跟 Windows 桌面版是**同一個問題**(見 creds-win.ts 檔頭)——
5
+ * 只用桌面版、沒在終端機開過 CLI 的人,`~/.claude/.credentials.json` 裡不會有可用的
6
+ * token,而 Keychain 裡那份是早期 CLI 登入的殘留,沒人 refresh 就會過期。實機
7
+ * (2026-08-31, Ashlyn)就是這樣:Claude Code 照常運作(它自己 refresh),但我們讀到
8
+ * 的 Keychain 憑證從 02:00 起過期,採樣連續 token_expired,而她其實一直在用。
9
+ *
10
+ * 桌面版把 token 存在 `~/Library/Application Support/Claude/config.json` 的
11
+ * `oauth:tokenCacheV2`,用 Chromium 的 macOS 加密方案:
12
+ * 主金鑰 = PBKDF2-HMAC-SHA1(Keychain「Claude Safe Storage」密碼, "saltysalt", 1003 輪, 16B)
13
+ * 密文 = base64( "v10"(3B) + AES-128-CBC(IV = 16 個 0x20) ) + PKCS7
14
+ *
15
+ * ★ 與 Windows 版的唯一差異就是加密那一步(DPAPI+AES-256-GCM ↔ Keychain+AES-128-CBC)。
16
+ * 解出來的 JSON 形狀完全相同(實機驗證:2 條目、user:inference 那筆是目標),所以
17
+ * **挑選邏輯直接重用 creds-win.ts 的 pickEntry**,不另寫一份。
18
+ *
19
+ * ★ 為什麼主金鑰用 `security` CLI 而不是 native Keychain API:與 mac 上讀 Keychain
20
+ * token、Windows 上 spawn PowerShell 做 DPAPI 完全對稱 —— 零執行期依賴、免編譯。
21
+ *
22
+ * ★ 為什麼 AES-GCM 用 Node crypto 而 KDF 用 Node crypto、只有取主金鑰密碼 spawn
23
+ * 外部程式:跟 Windows 版同一個分工原則 —— 只有「拿受作業系統保護的秘密」那一步
24
+ * 非得走系統工具,其餘純運算留在 Node,測得到、也不受外部工具版本影響。
25
+ *
26
+ * ⚠️ 未公開的內部格式,Anthropic 改版就可能失效 —— 與 /api/oauth/usage 同一風險類別。
27
+ * 失敗一律回可辨識的 source 字串,不拋例外把 daemon 打掛。
28
+ *
29
+ * SECURITY:解出的 master key 與 token 只留在記憶體,**永不列印、永不寫檔、永不上傳**。
30
+ */
31
+ import { spawnSync } from "node:child_process";
32
+ import { createDecipheriv, pbkdf2Sync } from "node:crypto";
33
+ import { existsSync, readFileSync } from "node:fs";
34
+ import { homedir } from "node:os";
35
+ import { join } from "node:path";
36
+ import { pickEntry } from "./creds-win.js";
37
+ /** Chromium 加密區塊前綴。看到別的就是格式變了,不硬解。 */
38
+ const V10 = "v10";
39
+ /** Chromium 在 macOS 的固定 KDF 參數(公開常數,非機密)。 */
40
+ const KDF_SALT = "saltysalt";
41
+ const KDF_ITER = 1003;
42
+ const KDF_KEYLEN = 16; // AES-128
43
+ /** 桌面版憑證檔位置。Claude.app 走 Electron 的 userData 慣例。 */
44
+ function configPath() {
45
+ return join(homedir(), "Library", "Application Support", "Claude", "config.json");
46
+ }
47
+ /**
48
+ * 從 Keychain 取「Claude Safe Storage」密碼,PBKDF2 成 16-byte AES 金鑰。
49
+ *
50
+ * `security find-generic-password -w -s <svc>` 只回密碼本體。首次存取可能跳授權
51
+ * 對話(App 首次要求時使用者已同意過,daemon 走同一個 ACL),非互動下拿不到就回
52
+ * 可辨識原因,不卡住。
53
+ */
54
+ function masterKey() {
55
+ const r = spawnSync("security", ["find-generic-password", "-w", "-s", "Claude Safe Storage"],
56
+ // ★ timeout 短是刻意的:`Claude Safe Storage` 的 Keychain ACL 只授權給 Claude.app,
57
+ // 從別的行程存取會**彈授權對話框並阻塞等點擊**。daemon 是非互動的,等不到人點,
58
+ // 所以與其掛在那裡耗一個 tick,不如快速 timeout 回一個可辨識的原因。
59
+ // 使用者只要在 Keychain 存取.app 對這個項目按一次「一律允許」,之後就不再彈。
60
+ { encoding: "utf8", timeout: 8_000 });
61
+ // ETIMEDOUT 幾乎必然是「在等授權對話」,與「security 不存在」是兩種不同處置:
62
+ // 前者要使用者去 Keychain 授權一次,後者是環境壞了。分開回報。
63
+ if (r.error) {
64
+ const timedOut = r.error.code === "ETIMEDOUT";
65
+ return [null, timedOut ? "keychain-needs-authorization" : "keychain-spawn-failed"];
66
+ }
67
+ // 非零通常是「找不到項目」(44) 或「使用者拒絕授權」(128)。分開讓資料看得出哪一種。
68
+ if (r.status !== 0) {
69
+ const code = r.status === 44 ? "no-item" : r.status === 128 ? "denied" : `rc${r.status}`;
70
+ return [null, `keychain-${code}`];
71
+ }
72
+ const pw = (r.stdout ?? "").trim();
73
+ if (!pw)
74
+ return [null, "keychain-empty"];
75
+ try {
76
+ const key = pbkdf2Sync(pw, KDF_SALT, KDF_ITER, KDF_KEYLEN, "sha1");
77
+ return [key, "ok"];
78
+ }
79
+ catch {
80
+ return [null, "kdf-failed"];
81
+ }
82
+ }
83
+ /**
84
+ * 拆 v10 區塊並用 AES-128-CBC 解密。
85
+ * 結構:`"v10"`(3B) │ ciphertext(16 的倍數,含 PKCS7)。IV 固定為 16 個 0x20。
86
+ *
87
+ * 與 Windows 的 decryptV10 分開實作而非共用:那邊是 GCM(有 nonce/tag 切割),
88
+ * 這邊是 CBC(固定 IV、無 tag),共用只會讓兩邊都變成一堆 if。挑選邏輯才是該共用的
89
+ * 部分,那個已經共用(pickEntry)。
90
+ */
91
+ export function decryptDesktopMac(b64, key) {
92
+ let blob;
93
+ try {
94
+ blob = Buffer.from(b64, "base64");
95
+ }
96
+ catch {
97
+ return [null, "cache-bad-base64"];
98
+ }
99
+ if (blob.subarray(0, 3).toString("latin1") !== V10) {
100
+ return [null, `cache-not-v10:${blob.subarray(0, 3).toString("latin1")}`];
101
+ }
102
+ const ct = blob.subarray(3);
103
+ // CBC 密文長度必為 16 的倍數且至少一個區塊。不符 = 資料壞了,別餵給 decipher。
104
+ if (ct.length === 0 || ct.length % 16 !== 0)
105
+ return [null, `cache-bad-ct-len:${ct.length}`];
106
+ try {
107
+ const iv = Buffer.alloc(16, 0x20); // 16 個空白
108
+ const d = createDecipheriv("aes-128-cbc", key, iv);
109
+ // autoPadding 預設 true,Node 會驗並剝 PKCS7;padding 不對會在 final() 拋。
110
+ return [Buffer.concat([d.update(ct), d.final()]).toString("utf8"), "ok"];
111
+ }
112
+ catch {
113
+ // PKCS7 驗證失敗 = 金鑰不對或資料被改過。不要當成「格式變了」。
114
+ return [null, "cache-decrypt-failed"];
115
+ }
116
+ }
117
+ /**
118
+ * macOS 桌面版憑證。回 `[creds, source]`;失敗時 creds 為 null、source 是可辨識原因
119
+ * (會記進 health / cred_source,之後從資料就看得出卡在哪一步)。
120
+ */
121
+ export function fromMacDesktop() {
122
+ if (process.platform !== "darwin")
123
+ return [null, "not-macos"];
124
+ const cfgPath = configPath();
125
+ if (!existsSync(cfgPath))
126
+ return [null, "desktop-no-config"];
127
+ let cfg;
128
+ try {
129
+ cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
130
+ }
131
+ catch {
132
+ return [null, "desktop-config-unreadable"];
133
+ }
134
+ // V2 是新版;舊版鍵名無 V2 後綴。先新後舊,與 Windows 版一致。
135
+ const cached = cfg["oauth:tokenCacheV2"] ?? cfg["oauth:tokenCache"];
136
+ if (typeof cached !== "string" || !cached)
137
+ return [null, "desktop-no-token-cache"];
138
+ const [key, keyWhy] = masterKey();
139
+ if (!key)
140
+ return [null, `desktop-${keyWhy}`];
141
+ const [plain, decWhy] = decryptDesktopMac(cached, key);
142
+ if (!plain)
143
+ return [null, `desktop-${decWhy}`];
144
+ const [creds, pickWhy] = pickEntry(plain); // ★ 與 Windows 共用挑選邏輯
145
+ if (!creds)
146
+ return [null, `desktop-${pickWhy}`];
147
+ return [creds, "desktop-mac"];
148
+ }
package/dist/creds.js CHANGED
@@ -14,6 +14,7 @@ import { userInfo } from "node:os";
14
14
  import { readFileSync } from "node:fs";
15
15
  import { claudeDir, credsPath } from "./paths.js";
16
16
  import { fromWindowsDesktop } from "./creds-win.js";
17
+ import { fromMacDesktop } from "./creds-mac-desktop.js";
17
18
  /** 可區分的結束碼 —— daemon 靠這個把「token 過期」和「網路壞掉」分開記錄。
18
19
  * 過去兩者都是 exit 1,daemon 只能寫 sample-failed,於是 2026-08-22 那次
19
20
  * token 過期靜默死了 17.6 小時沒人發現。 */
@@ -111,7 +112,9 @@ const expired = (o) => Boolean(o.expiresAt && Date.now() >= o.expiresAt);
111
112
  const SOURCES = [
112
113
  fromKeychain, // macOS 主儲存
113
114
  fromFile, // CLI 的檔案(Windows/Linux 是主儲存;mac 上只是過時副本)
114
- fromWindowsDesktop, // Windows 桌面版(MSIX,加密)
115
+ fromWindowsDesktop, // Windows 桌面版(MSIX,DPAPI+AES-GCM)
116
+ fromMacDesktop, // macOS 桌面版(Keychain+AES-CBC)—— 排最後:只有前面都過期/沒有
117
+ // 才需要它,且取 Keychain 主金鑰有成本,不該每次都跑
115
118
  ];
116
119
  export function loadToken() {
117
120
  const why = [];
package/dist/doctor.js CHANGED
@@ -15,6 +15,7 @@ import { homedir, platform, release, userInfo } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { EXIT, loadToken } from "./creds.js";
17
17
  import { desktopDataDirs } from "./creds-win.js";
18
+ import { fromMacDesktop } from "./creds-mac-desktop.js";
18
19
  import { claudeDir, claudeSettingsPath, configPath, credsPath, devicePath, projectsDir, serverUrl, stateDir, } from "./paths.js";
19
20
  import { healthWarning, loadHealth } from "./health.js";
20
21
  import { readJson } from "./util.js";
@@ -113,6 +114,31 @@ export async function doctor() {
113
114
  out.push(ok(`執行原則 ${policy}(不會擋 .ps1 啟動器)`));
114
115
  }
115
116
  }
117
+ // ── macOS 桌面版(Claude.app)──
118
+ // 與 Windows 段對稱:只用桌面版、沒開過 CLI 的 mac 使用者,Keychain 那份是舊 CLI
119
+ // 殘留、會過期,真正的活 token 在桌面版加密庫裡。把狀態攤開,否則診斷會誤判成
120
+ // 「token 過期,去 /login」——但那治標不治本。
121
+ if (platform() === "darwin") {
122
+ const cfg = join(homedir(), "Library", "Application Support", "Claude", "config.json");
123
+ if (!existsSync(cfg)) {
124
+ out.push(info("桌面版 找不到 config.json(沒裝桌面版就正常)"));
125
+ }
126
+ else {
127
+ const [creds, why] = fromMacDesktop();
128
+ if (creds) {
129
+ out.push(ok(`桌面版 憑證可讀(來源 ${why}, tier=${creds.rateLimitTier})`));
130
+ }
131
+ else if (why === "desktop-keychain-needs-authorization") {
132
+ out.push(bad("桌面版 Keychain 需要授權一次"));
133
+ problems.push("macOS 桌面版憑證庫需要授權:在終端機跑一次 " +
134
+ '`security find-generic-password -w -s "Claude Safe Storage"`,' +
135
+ "跳出對話框時按**一律允許**(Always Allow)。之後背景採樣就讀得到。");
136
+ }
137
+ else {
138
+ out.push(info(`桌面版 ${why}`));
139
+ }
140
+ }
141
+ }
116
142
  // ── Windows 桌面版(MSIX)──
117
143
  // 只用桌面版、沒登入過 CLI 的人,.credentials.json 裡根本不會有 claudeAiOauth。
118
144
  // 這一段把「桌面版憑證庫在哪、有沒有東西」攤開,否則診斷又會退回「猜 + 手寫指令」。
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.8",
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"