@token-dashboard/codex-usage-uploader 0.1.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/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @token-dashboard/codex-usage-uploader
2
+
3
+ 自动采集本机 [Codex CLI](https://github.com/openai/codex) 的 token 用量数据,并上报至 Token Data Dashboard 后端,用于团队用量统计与成本分析。
4
+
5
+ - 后台常驻运行,增量扫描本地 Codex session 数据
6
+ - 首次安装自动补采全量历史
7
+ - macOS launchd 托管,开机自启
8
+
9
+ > 仅支持 macOS,需要 Node.js >= 22.13.0。
10
+
11
+ ## 快速开始
12
+
13
+ 一行命令完成安装、身份绑定、历史补采和后台服务启动:
14
+
15
+ ```bash
16
+ npx @token-dashboard/codex-usage-uploader init
17
+ ```
18
+
19
+ 安装过程中会自动读取 Codex 登录态(`~/.codex/auth.json`)获取身份信息。如果读取不到邮箱,会在终端交互式提示你手动填写。
20
+
21
+ 如需跳过交互提示,可以直接指定邮箱:
22
+
23
+ ```bash
24
+ npx @token-dashboard/codex-usage-uploader init \
25
+ --email you@company.com \
26
+ --yes
27
+ ```
28
+
29
+ 安装完成后,`codex-usage-uploader` 命令会被写入 `~/bin/`,后续可直接使用。
30
+
31
+ ## 命令一览
32
+
33
+ ### 初始化 / 升级
34
+
35
+ ```bash
36
+ codex-usage-uploader init
37
+ ```
38
+
39
+ 首次运行时完成安装与配置;重复运行会升级本地版本并重启后台服务。后端地址默认为 `http://101.126.66.51:8086`,如需自定义可通过 `--backend-url` 指定。
40
+
41
+ ### 绑定身份
42
+
43
+ ```bash
44
+ codex-usage-uploader bind
45
+ codex-usage-uploader bind --email you@company.com --yes
46
+ ```
47
+
48
+ 更新上报身份。如果后台服务正在运行,会自动重启以应用新身份。
49
+
50
+ ### 后台服务管理
51
+
52
+ | 命令 | 说明 |
53
+ | --------------------------------------- | ------------------------------- |
54
+ | `codex-usage-uploader start` | 启动后台服务 |
55
+ | `codex-usage-uploader stop` | 停止后台服务 |
56
+ | `codex-usage-uploader restart` | 重启后台服务 |
57
+ | `codex-usage-uploader status` | 查看服务状态与本地队列 |
58
+ | `codex-usage-uploader logs` | 查看服务日志(默认最近 100 行) |
59
+ | `codex-usage-uploader logs --lines 500` | 查看更多日志 |
60
+
61
+ ### 清除本地状态
62
+
63
+ ```bash
64
+ codex-usage-uploader clear --yes
65
+ ```
66
+
67
+ 清空本地补采进度和待上传队列,不影响已绑定的身份。服务端清库后需要重新补历史时使用:
68
+
69
+ ```bash
70
+ codex-usage-uploader stop
71
+ codex-usage-uploader clear --yes
72
+ codex-usage-uploader init
73
+ ```
74
+
75
+ ### 卸载
76
+
77
+ ```bash
78
+ codex-usage-uploader uninstall
79
+ ```
80
+
81
+ 停止后台服务并删除本地安装目录。
82
+
83
+ ## 常用选项
84
+
85
+ | 选项 | 说明 |
86
+ | -------------------------- | ------------------------------- |
87
+ | `--backend-url <url>` | Dashboard 后端地址(默认 `http://101.126.66.51:8086`) |
88
+ | `--email <email>` | 绑定邮箱 |
89
+ | `--employee-name <name>` | 绑定姓名 |
90
+ | `--employee-id <id>` | 绑定工号 |
91
+ | `--interval <seconds>` | 扫描间隔秒数(默认 30) |
92
+ | `--yes` | 跳过所有交互确认 |
93
+ | `--lines <n>` | `logs` 命令输出行数(默认 100) |
94
+ | `-h, --help` | 显示帮助 |
95
+
96
+ ## 工作原理
97
+
98
+ 1. **init** 将包安装到 `~/.codex-usage-uploader/`,绑定身份后前台执行一次全量历史补采,完成后启动 macOS launchd 后台服务。
99
+ 2. 后台服务每 30 秒(可通过 `--interval` 调整)增量扫描 `~/.codex/sessions/` 下的 rollout 文件。
100
+ 3. 扫描到的 token 用量数据暂存在本地 SQLite 队列中,满足条件后自动封批上传至后端。
101
+ 4. 所有数据在上报前已做去敏处理,仅包含 session 元信息、turn 上下文摘要和 token 计数。
102
+
103
+ ## 本地文件
104
+
105
+ | 路径 | 说明 |
106
+ | -------------------------------------- | -------------- |
107
+ | `~/.codex-usage-uploader/config.json` | 运行配置 |
108
+ | `~/.codex-usage-uploader/state.sqlite` | 本地状态数据库 |
109
+ | `~/.codex-usage-uploader/logs/` | 服务日志 |
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ if (!process.env.NODE_NO_WARNINGS) {
3
+ process.env.NODE_NO_WARNINGS = '1';
4
+ }
5
+ process.emitWarning = () => {};
6
+
7
+ const { main } = await import('../src/cli.js');
8
+ const exitCode = await main(process.argv.slice(2));
9
+ process.exit(exitCode);
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@token-dashboard/codex-usage-uploader",
3
+ "version": "0.1.0",
4
+ "description": "Codex 用量上报 CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "codex-usage-uploader": "./bin/codex-usage-uploader.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --no-warnings --test tests/*.test.mjs"
16
+ },
17
+ "engines": {
18
+ "node": ">=22.13.0"
19
+ }
20
+ }
package/src/auth.js ADDED
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import readline from 'node:readline/promises';
3
+ import { stdin as input, stdout as output } from 'node:process';
4
+
5
+ export function decodeJwtClaims(token) {
6
+ if (!token) return {};
7
+ const parts = String(token).split('.');
8
+ if (parts.length < 2 || !parts[1]) return {};
9
+ try {
10
+ const decoded = Buffer.from(parts[1], 'base64url').toString('utf8');
11
+ const data = JSON.parse(decoded);
12
+ return data && typeof data === 'object' ? data : {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ export function loadCodexAuthIdentity(authPath) {
19
+ try {
20
+ const payload = JSON.parse(fs.readFileSync(authPath, 'utf8'));
21
+ const tokens = payload?.tokens;
22
+ if (!tokens || typeof tokens !== 'object') return {};
23
+ const claims = decodeJwtClaims(tokens.id_token);
24
+ const identity = {};
25
+ if (typeof claims.email === 'string' && claims.email.trim()) {
26
+ identity.employeeEmail = claims.email.trim();
27
+ }
28
+ if (typeof claims.name === 'string' && claims.name.trim()) {
29
+ identity.employeeName = claims.name.trim();
30
+ }
31
+ return identity;
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ export function identityIsBound(values) {
38
+ return Boolean(values.employeeId || values.employeeEmail || values.employeeName);
39
+ }
40
+
41
+ export async function promptValue(label, defaultValue) {
42
+ const rl = readline.createInterface({ input, output });
43
+ try {
44
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
45
+ const answer = (await rl.question(`${label}${suffix}: `)).trim();
46
+ return answer || defaultValue || null;
47
+ } finally {
48
+ rl.close();
49
+ }
50
+ }
51
+
52
+ export async function promptConfirm(label, defaultYes = false) {
53
+ const defaultValue = defaultYes ? 'y' : 'n';
54
+ const answer = await promptValue(`${label} (${defaultYes ? 'Y/n' : 'y/N'})`, defaultValue);
55
+ return /^(y|yes)$/i.test(String(answer ?? '').trim());
56
+ }