@vibescore/tracker 0.0.6 → 0.0.7
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 +19 -2
- package/README.zh-CN.md +19 -2
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +82 -6
- package/src/commands/status.js +6 -1
- package/src/commands/sync.js +17 -2
- package/src/commands/uninstall.js +46 -8
- package/src/lib/codex-config.js +83 -16
- package/src/lib/diagnostics.js +13 -4
- package/src/lib/rollout.js +78 -16
- package/src/lib/uploader.js +16 -1
package/README.md
CHANGED
|
@@ -25,9 +25,19 @@ _Real-time AI Analytics for Codex CLI_
|
|
|
25
25
|
|
|
26
26
|
> [!TIP] > **Core Index**: Our signature metric that reflects your flow state by analyzing token consumption rates and patterns.
|
|
27
27
|
|
|
28
|
+
## 🔒 Privacy-First Architecture (Stealth Protocol)
|
|
29
|
+
|
|
30
|
+
We believe your code and thoughts are your own. VibeScore is built with strict privacy pillars to ensure your data never leaves your control.
|
|
31
|
+
|
|
32
|
+
- 🛡️ **Zero Code Infiltration**: We never touch your source code or prompts. Our sniffer only extracts numeric token counts (Input, Output, Reasoning, Cached).
|
|
33
|
+
- 📡 **Local Aggregation**: All token consumption analysis happens on your machine. We only relay quantized 30-minute usage buckets to the cloud.
|
|
34
|
+
- 🔐 **Hashed Identity**: Device tokens are hashed using SHA-256 server-side. Your raw credentials never exist in our database.
|
|
35
|
+
- 🔦 **Full Transparency**: Audit the sync logic yourself in `src/lib/rollout.js`. We literally only capture numbers and timestamps.
|
|
36
|
+
|
|
28
37
|
## 🚀 Key Features
|
|
29
38
|
|
|
30
39
|
- 📡 **Live Sniffer**: Real-time interception of Codex CLI pipes using low-level hooks to capture every completion event.
|
|
40
|
+
- 🧭 **Multi-source Ingestion**: Supports Codex CLI and Every Code (tagged as `source=every-code`) without modifying Every Code.
|
|
31
41
|
- 📊 **Matrix Dashboard**: A high-performance React + Vite dashboard featuring heatmaps, trend charts, and live logs.
|
|
32
42
|
- ⚡ **AI Analytics**: Deep analysis of Input/Output tokens, with dedicated tracking for Cached and Reasoning components.
|
|
33
43
|
- 🔒 **Identity Core**: Robust authentication and permission management to secure your development data.
|
|
@@ -42,6 +52,8 @@ Initialize your environment with a single command:
|
|
|
42
52
|
npx --yes @vibescore/tracker init
|
|
43
53
|
```
|
|
44
54
|
|
|
55
|
+
Note: If `~/.code/config.toml` exists (or `CODE_HOME`), `init` also configures Every Code `notify` automatically; it will not create the file when missing.
|
|
56
|
+
|
|
45
57
|
### Sync & Status
|
|
46
58
|
|
|
47
59
|
```bash
|
|
@@ -52,6 +64,11 @@ npx --yes @vibescore/tracker sync
|
|
|
52
64
|
npx --yes @vibescore/tracker status
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
### Sources
|
|
68
|
+
|
|
69
|
+
- Codex CLI logs: `~/.codex/sessions/**/rollout-*.jsonl` (override with `CODEX_HOME`)
|
|
70
|
+
- Every Code logs: `~/.code/sessions/**/rollout-*.jsonl` (override with `CODE_HOME`)
|
|
71
|
+
|
|
55
72
|
## 🧰 Troubleshooting
|
|
56
73
|
|
|
57
74
|
### Streak shows 0 days while totals look correct
|
|
@@ -60,9 +77,9 @@ npx --yes @vibescore/tracker status
|
|
|
60
77
|
- If you expect a non-zero streak, clear cached auth/heatmap data and sign in again:
|
|
61
78
|
|
|
62
79
|
```js
|
|
63
|
-
localStorage.removeItem(
|
|
80
|
+
localStorage.removeItem("vibescore.dashboard.auth.v1");
|
|
64
81
|
Object.keys(localStorage)
|
|
65
|
-
.filter((k) => k.startsWith(
|
|
82
|
+
.filter((k) => k.startsWith("vibescore.heatmap."))
|
|
66
83
|
.forEach((k) => localStorage.removeItem(k));
|
|
67
84
|
location.reload();
|
|
68
85
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -25,9 +25,19 @@ _Codex CLI 实时 AI 分析工具_
|
|
|
25
25
|
|
|
26
26
|
> [!TIP] > **Core Index (核心指数)**: 我们的标志性指标,通过分析 Token 消耗速率与模式,反映你的开发心流状态。
|
|
27
27
|
|
|
28
|
+
## 🔒 隐私优先架构 (隐身协议)
|
|
29
|
+
|
|
30
|
+
我们坚信你的代码和思想属于你自己。VibeScore 建立在严格的隐私支柱之上,确保你的数据始终处于受控状态。
|
|
31
|
+
|
|
32
|
+
- 🛡️ **代码零入侵**:我们绝不触碰你的源代码或 Prompt。我们的嗅探器仅提取数值化的 Token 计数(输入、输出、推理、缓存)。
|
|
33
|
+
- 📡 **本地聚合**:所有 Token 消耗分析均在你的机器上完成。我们仅将量化的 30 分钟使用桶(Usage Buckets)中继到云端。
|
|
34
|
+
- 🔐 **身份哈希**:设备令牌在服务端使用 SHA-256 进行哈希处理。你的原始凭据绝不会存在于我们的数据库中。
|
|
35
|
+
- 🔦 **全程透明**:你可以亲自审计 `src/lib/rollout.js` 中的同步逻辑。我们真正采集的只有数字和时间戳。
|
|
36
|
+
|
|
28
37
|
## 🚀 核心功能
|
|
29
38
|
|
|
30
39
|
- 📡 **Live Sniffer (实时嗅探)**: 实时监听 Codex CLI 管道,通过底层 Hook 捕获每一次补全事件。
|
|
40
|
+
- 🧭 **多来源采集**:支持 Codex CLI 与 Every Code(标记为 `source=every-code`),无需修改 Every Code 客户端。
|
|
31
41
|
- 📊 **Matrix Dashboard (矩阵控制台)**: 基于 React + Vite 的高性能仪表盘,具备热力图、趋势图与实时日志。
|
|
32
42
|
- ⚡ **AI Analytics (AI 分析)**: 深度分析 Input/Output Token,支持缓存 (Cached) 与推理 (Reasoning) 部分的分离监控。
|
|
33
43
|
- 🔒 **Identity Core (身份核心)**: 完备的身份验证与权限管理,保护你的开发数据资产。
|
|
@@ -42,6 +52,8 @@ _Codex CLI 实时 AI 分析工具_
|
|
|
42
52
|
npx --yes @vibescore/tracker init
|
|
43
53
|
```
|
|
44
54
|
|
|
55
|
+
说明:若存在 `~/.code/config.toml`(或 `CODE_HOME`),`init` 会自动配置 Every Code 的 `notify`;缺失时不会创建该文件。
|
|
56
|
+
|
|
45
57
|
### 同步与状态查看
|
|
46
58
|
|
|
47
59
|
```bash
|
|
@@ -52,6 +64,11 @@ npx --yes @vibescore/tracker sync
|
|
|
52
64
|
npx --yes @vibescore/tracker status
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
### 日志来源
|
|
68
|
+
|
|
69
|
+
- Codex CLI 日志:`~/.codex/sessions/**/rollout-*.jsonl`(可用 `CODEX_HOME` 覆盖)
|
|
70
|
+
- Every Code 日志:`~/.code/sessions/**/rollout-*.jsonl`(可用 `CODE_HOME` 覆盖)
|
|
71
|
+
|
|
55
72
|
## 🧰 常见问题
|
|
56
73
|
|
|
57
74
|
### Streak 显示 0 天但总量正常
|
|
@@ -60,9 +77,9 @@ npx --yes @vibescore/tracker status
|
|
|
60
77
|
- 如果你确认应该有 streak,请清理本地缓存并重新登录:
|
|
61
78
|
|
|
62
79
|
```js
|
|
63
|
-
localStorage.removeItem(
|
|
80
|
+
localStorage.removeItem("vibescore.dashboard.auth.v1");
|
|
64
81
|
Object.keys(localStorage)
|
|
65
|
-
.filter((k) => k.startsWith(
|
|
82
|
+
.filter((k) => k.startsWith("vibescore.heatmap."))
|
|
66
83
|
.forEach((k) => localStorage.removeItem(k));
|
|
67
84
|
location.reload();
|
|
68
85
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -48,8 +48,9 @@ function printHelp() {
|
|
|
48
48
|
'',
|
|
49
49
|
'Notes:',
|
|
50
50
|
' - init installs a Codex notify hook and issues a device token (default: browser sign in/up).',
|
|
51
|
+
' - when ~/.code/config.toml exists, init also installs an Every Code notify hook.',
|
|
51
52
|
' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted landing page.',
|
|
52
|
-
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and uploads token_count deltas.',
|
|
53
|
+
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl (Every Code), then uploads token_count deltas.',
|
|
53
54
|
' - --debug prints original backend errors when they are normalized.',
|
|
54
55
|
''
|
|
55
56
|
].join('\n')
|
package/src/commands/init.js
CHANGED
|
@@ -4,7 +4,12 @@ const fs = require('node:fs/promises');
|
|
|
4
4
|
|
|
5
5
|
const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
|
|
6
6
|
const { prompt, promptHidden } = require('../lib/prompt');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
upsertCodexNotify,
|
|
9
|
+
loadCodexNotifyOriginal,
|
|
10
|
+
upsertEveryCodeNotify,
|
|
11
|
+
loadEveryCodeNotifyOriginal
|
|
12
|
+
} = require('../lib/codex-config');
|
|
8
13
|
const { beginBrowserAuth } = require('../lib/browser-auth');
|
|
9
14
|
const { issueDeviceTokenWithPassword, issueDeviceTokenWithAccessToken } = require('../lib/insforge');
|
|
10
15
|
const { cmdSync } = require('./sync');
|
|
@@ -84,6 +89,8 @@ async function cmdInit(argv) {
|
|
|
84
89
|
|
|
85
90
|
// Configure Codex notify hook.
|
|
86
91
|
const codexConfigPath = path.join(home, '.codex', 'config.toml');
|
|
92
|
+
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
93
|
+
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
87
94
|
const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
|
|
88
95
|
const result = await upsertCodexNotify({
|
|
89
96
|
codexConfigPath,
|
|
@@ -92,6 +99,19 @@ async function cmdInit(argv) {
|
|
|
92
99
|
});
|
|
93
100
|
|
|
94
101
|
const chained = await loadCodexNotifyOriginal(notifyOriginalPath);
|
|
102
|
+
const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
|
|
103
|
+
const codeConfigExists = await isFile(codeConfigPath);
|
|
104
|
+
let codeResult = null;
|
|
105
|
+
let codeChained = null;
|
|
106
|
+
if (codeConfigExists) {
|
|
107
|
+
const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
|
|
108
|
+
codeResult = await upsertEveryCodeNotify({
|
|
109
|
+
codeConfigPath,
|
|
110
|
+
notifyCmd: codeNotifyCmd,
|
|
111
|
+
notifyOriginalPath: codeNotifyOriginalPath
|
|
112
|
+
});
|
|
113
|
+
codeChained = await loadEveryCodeNotifyOriginal(codeNotifyOriginalPath);
|
|
114
|
+
}
|
|
95
115
|
|
|
96
116
|
process.stdout.write(
|
|
97
117
|
[
|
|
@@ -101,6 +121,17 @@ async function cmdInit(argv) {
|
|
|
101
121
|
`- Codex config: ${codexConfigPath}`,
|
|
102
122
|
result.changed ? '- Codex notify: updated' : '- Codex notify: already set',
|
|
103
123
|
chained ? '- Codex notify: chained (original preserved)' : '- Codex notify: no original',
|
|
124
|
+
codeConfigExists ? `- Every Code config: ${codeConfigPath}` : '- Every Code notify: skipped (config.toml not found)',
|
|
125
|
+
codeConfigExists && codeResult
|
|
126
|
+
? codeResult.changed
|
|
127
|
+
? '- Every Code notify: updated'
|
|
128
|
+
: '- Every Code notify: already set'
|
|
129
|
+
: null,
|
|
130
|
+
codeConfigExists
|
|
131
|
+
? codeChained
|
|
132
|
+
? '- Every Code notify: chained (original preserved)'
|
|
133
|
+
: '- Every Code notify: no original'
|
|
134
|
+
: null,
|
|
104
135
|
deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
|
|
105
136
|
''
|
|
106
137
|
].join('\n')
|
|
@@ -160,13 +191,32 @@ const os = require('node:os');
|
|
|
160
191
|
const path = require('node:path');
|
|
161
192
|
const cp = require('node:child_process');
|
|
162
193
|
|
|
163
|
-
const
|
|
194
|
+
const rawArgs = process.argv.slice(2);
|
|
195
|
+
let source = 'codex';
|
|
196
|
+
const payloadArgs = [];
|
|
197
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
198
|
+
const arg = rawArgs[i];
|
|
199
|
+
if (arg === '--source') {
|
|
200
|
+
source = rawArgs[i + 1] || source;
|
|
201
|
+
i += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (arg.startsWith('--source=')) {
|
|
205
|
+
source = arg.slice('--source='.length) || source;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
payloadArgs.push(arg);
|
|
209
|
+
}
|
|
210
|
+
|
|
164
211
|
const trackerDir = ${JSON.stringify(trackerDir)};
|
|
165
212
|
const signalPath = ${JSON.stringify(queueSignalPath)};
|
|
166
|
-
const
|
|
213
|
+
const codexOriginalPath = ${JSON.stringify(originalPath)};
|
|
214
|
+
const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
|
|
167
215
|
const trackerBinPath = ${JSON.stringify(trackerBinPath)};
|
|
168
216
|
const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
|
|
169
217
|
const fallbackPkg = ${JSON.stringify(fallbackPkg)};
|
|
218
|
+
const selfPath = path.resolve(__filename);
|
|
219
|
+
const home = os.homedir();
|
|
170
220
|
|
|
171
221
|
try {
|
|
172
222
|
fs.mkdirSync(trackerDir, { recursive: true });
|
|
@@ -191,13 +241,14 @@ try {
|
|
|
191
241
|
}
|
|
192
242
|
} catch (_) {}
|
|
193
243
|
|
|
194
|
-
// Chain the original
|
|
244
|
+
// Chain the original notify if present.
|
|
195
245
|
try {
|
|
246
|
+
const originalPath = source === 'every-code' ? codeOriginalPath : codexOriginalPath;
|
|
196
247
|
const original = JSON.parse(fs.readFileSync(originalPath, 'utf8'));
|
|
197
248
|
const cmd = Array.isArray(original?.notify) ? original.notify : null;
|
|
198
|
-
if (cmd && cmd.length > 0) {
|
|
249
|
+
if (cmd && cmd.length > 0 && !isSelfNotify(cmd)) {
|
|
199
250
|
const args = cmd.slice(1);
|
|
200
|
-
args.push(
|
|
251
|
+
if (payloadArgs.length > 0) args.push(...payloadArgs);
|
|
201
252
|
spawnDetached([cmd[0], ...args]);
|
|
202
253
|
}
|
|
203
254
|
} catch (_) {}
|
|
@@ -214,6 +265,22 @@ function spawnDetached(argv) {
|
|
|
214
265
|
child.unref();
|
|
215
266
|
} catch (_) {}
|
|
216
267
|
}
|
|
268
|
+
|
|
269
|
+
function resolveMaybeHome(p) {
|
|
270
|
+
if (typeof p !== 'string') return null;
|
|
271
|
+
if (p.startsWith('~/')) return path.join(home, p.slice(2));
|
|
272
|
+
return path.resolve(p);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isSelfNotify(cmd) {
|
|
276
|
+
for (const part of cmd) {
|
|
277
|
+
if (typeof part !== 'string') continue;
|
|
278
|
+
if (!part.includes('notify.cjs')) continue;
|
|
279
|
+
const resolved = resolveMaybeHome(part);
|
|
280
|
+
if (resolved && resolved === selfPath) return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
217
284
|
`;
|
|
218
285
|
}
|
|
219
286
|
|
|
@@ -248,6 +315,15 @@ async function checkUrlReachable(url) {
|
|
|
248
315
|
}
|
|
249
316
|
}
|
|
250
317
|
|
|
318
|
+
async function isFile(p) {
|
|
319
|
+
try {
|
|
320
|
+
const st = await fs.stat(p);
|
|
321
|
+
return st.isFile();
|
|
322
|
+
} catch (_e) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
251
327
|
async function installLocalTrackerApp({ appDir }) {
|
|
252
328
|
// Copy the current package's runtime (bin + src) into ~/.vibescore so notify can run sync without npx.
|
|
253
329
|
const packageRoot = path.resolve(__dirname, '../..');
|
package/src/commands/status.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('node:path');
|
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
4
|
|
|
5
5
|
const { readJson } = require('../lib/fs');
|
|
6
|
-
const { readCodexNotify } = require('../lib/codex-config');
|
|
6
|
+
const { readCodexNotify, readEveryCodeNotify } = require('../lib/codex-config');
|
|
7
7
|
const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
|
|
8
8
|
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
9
9
|
|
|
@@ -27,6 +27,8 @@ async function cmdStatus(argv = []) {
|
|
|
27
27
|
const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
|
|
28
28
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
29
29
|
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
30
|
+
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
31
|
+
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
30
32
|
|
|
31
33
|
const config = await readJson(configPath);
|
|
32
34
|
const cursors = await readJson(cursorsPath);
|
|
@@ -42,6 +44,8 @@ async function cmdStatus(argv = []) {
|
|
|
42
44
|
|
|
43
45
|
const codexNotify = await readCodexNotify(codexConfigPath);
|
|
44
46
|
const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
|
|
47
|
+
const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
|
|
48
|
+
const everyCodeConfigured = Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
|
|
45
49
|
|
|
46
50
|
const lastUpload = uploadThrottle.lastSuccessMs
|
|
47
51
|
? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
|
|
@@ -75,6 +79,7 @@ async function cmdStatus(argv = []) {
|
|
|
75
79
|
lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
|
|
76
80
|
autoRetryLine,
|
|
77
81
|
`- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : 'unset'}`,
|
|
82
|
+
`- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : 'unset'}`,
|
|
78
83
|
''
|
|
79
84
|
]
|
|
80
85
|
.filter(Boolean)
|
package/src/commands/sync.js
CHANGED
|
@@ -43,8 +43,23 @@ async function cmdSync(argv) {
|
|
|
43
43
|
let uploadThrottleState = uploadThrottle;
|
|
44
44
|
|
|
45
45
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
47
|
+
|
|
48
|
+
const sources = [
|
|
49
|
+
{ source: 'codex', sessionsDir: path.join(codexHome, 'sessions') },
|
|
50
|
+
{ source: 'every-code', sessionsDir: path.join(codeHome, 'sessions') }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const rolloutFiles = [];
|
|
54
|
+
const seenSessions = new Set();
|
|
55
|
+
for (const entry of sources) {
|
|
56
|
+
if (seenSessions.has(entry.sessionsDir)) continue;
|
|
57
|
+
seenSessions.add(entry.sessionsDir);
|
|
58
|
+
const files = await listRolloutFiles(entry.sessionsDir);
|
|
59
|
+
for (const filePath of files) {
|
|
60
|
+
rolloutFiles.push({ path: filePath, source: entry.source });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
if (progress?.enabled) {
|
|
50
65
|
progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
|
|
@@ -2,8 +2,7 @@ const os = require('node:os');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
const { restoreCodexNotify } = require('../lib/codex-config');
|
|
5
|
+
const { restoreCodexNotify, restoreEveryCodeNotify } = require('../lib/codex-config');
|
|
7
6
|
|
|
8
7
|
async function cmdUninstall(argv) {
|
|
9
8
|
const opts = parseArgs(argv);
|
|
@@ -11,15 +10,32 @@ async function cmdUninstall(argv) {
|
|
|
11
10
|
const trackerDir = path.join(home, '.vibescore', 'tracker');
|
|
12
11
|
const binDir = path.join(home, '.vibescore', 'bin');
|
|
13
12
|
const codexConfigPath = path.join(home, '.codex', 'config.toml');
|
|
13
|
+
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
14
|
+
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
15
|
+
const notifyPath = path.join(binDir, 'notify.cjs');
|
|
14
16
|
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
17
|
+
const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
|
|
18
|
+
const codexNotifyCmd = ['/usr/bin/env', 'node', notifyPath];
|
|
19
|
+
const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
|
|
15
20
|
|
|
16
|
-
await
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const codexConfigExists = await isFile(codexConfigPath);
|
|
22
|
+
const codeConfigExists = await isFile(codeConfigPath);
|
|
23
|
+
const codexRestore = codexConfigExists
|
|
24
|
+
? await restoreCodexNotify({
|
|
25
|
+
codexConfigPath,
|
|
26
|
+
notifyOriginalPath,
|
|
27
|
+
notifyCmd: codexNotifyCmd
|
|
28
|
+
})
|
|
29
|
+
: { restored: false, skippedReason: 'config-missing' };
|
|
30
|
+
const codeRestore = codeConfigExists
|
|
31
|
+
? await restoreEveryCodeNotify({
|
|
32
|
+
codeConfigPath,
|
|
33
|
+
notifyOriginalPath: codeNotifyOriginalPath,
|
|
34
|
+
notifyCmd: codeNotifyCmd
|
|
35
|
+
})
|
|
36
|
+
: { restored: false, skippedReason: 'config-missing' };
|
|
20
37
|
|
|
21
38
|
// Remove installed notify handler.
|
|
22
|
-
const notifyPath = path.join(binDir, 'notify.cjs');
|
|
23
39
|
await fs.unlink(notifyPath).catch(() => {});
|
|
24
40
|
|
|
25
41
|
// Remove local app runtime (installed by init for notify-driven sync).
|
|
@@ -32,7 +48,20 @@ async function cmdUninstall(argv) {
|
|
|
32
48
|
process.stdout.write(
|
|
33
49
|
[
|
|
34
50
|
'Uninstalled:',
|
|
35
|
-
|
|
51
|
+
codexConfigExists
|
|
52
|
+
? codexRestore?.restored
|
|
53
|
+
? `- Codex notify restored: ${codexConfigPath}`
|
|
54
|
+
: codexRestore?.skippedReason === 'no-backup-not-installed'
|
|
55
|
+
? '- Codex notify: skipped (no backup; not installed)'
|
|
56
|
+
: '- Codex notify: no change'
|
|
57
|
+
: '- Codex notify: skipped (config.toml not found)',
|
|
58
|
+
codeConfigExists
|
|
59
|
+
? codeRestore?.restored
|
|
60
|
+
? `- Every Code notify restored: ${codeConfigPath}`
|
|
61
|
+
: codeRestore?.skippedReason === 'no-backup-not-installed'
|
|
62
|
+
? '- Every Code notify: skipped (no backup; not installed)'
|
|
63
|
+
: '- Every Code notify: no change'
|
|
64
|
+
: '- Every Code notify: skipped (config.toml not found)',
|
|
36
65
|
opts.purge ? `- Purged: ${path.join(home, '.vibescore')}` : '- Purge: skipped (use --purge)',
|
|
37
66
|
''
|
|
38
67
|
].join('\n')
|
|
@@ -50,3 +79,12 @@ function parseArgs(argv) {
|
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
module.exports = { cmdUninstall };
|
|
82
|
+
|
|
83
|
+
async function isFile(p) {
|
|
84
|
+
try {
|
|
85
|
+
const st = await fs.stat(p);
|
|
86
|
+
return st.isFile();
|
|
87
|
+
} catch (_e) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/lib/codex-config.js
CHANGED
|
@@ -3,10 +3,11 @@ const path = require('node:path');
|
|
|
3
3
|
|
|
4
4
|
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
5
5
|
|
|
6
|
-
async function
|
|
7
|
-
const originalText = await fs.readFile(
|
|
6
|
+
async function upsertNotify({ configPath, notifyCmd, notifyOriginalPath, configLabel }) {
|
|
7
|
+
const originalText = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
8
8
|
if (originalText == null) {
|
|
9
|
-
|
|
9
|
+
const label = typeof configLabel === 'string' && configLabel.length > 0 ? configLabel : 'Config';
|
|
10
|
+
throw new Error(`${label} not found: ${configPath}`);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const existingNotify = extractNotify(originalText);
|
|
@@ -23,39 +24,97 @@ async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPat
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const updated = setNotify(originalText, notifyCmd);
|
|
26
|
-
const backupPath = `${
|
|
27
|
-
await fs.copyFile(
|
|
28
|
-
await fs.writeFile(
|
|
27
|
+
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
28
|
+
await fs.copyFile(configPath, backupPath);
|
|
29
|
+
await fs.writeFile(configPath, updated, 'utf8');
|
|
29
30
|
return { changed: true, backupPath };
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
return { changed: false, backupPath: null };
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
async function
|
|
36
|
-
const text = await fs.readFile(
|
|
37
|
-
if (text == null) return;
|
|
36
|
+
async function restoreNotify({ configPath, notifyOriginalPath, expectedNotify }) {
|
|
37
|
+
const text = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
38
|
+
if (text == null) return { restored: false, skippedReason: 'config-missing' };
|
|
38
39
|
|
|
39
40
|
const original = await readJson(notifyOriginalPath);
|
|
40
41
|
const originalNotify = Array.isArray(original?.notify) ? original.notify : null;
|
|
42
|
+
const currentNotify = extractNotify(text);
|
|
43
|
+
|
|
44
|
+
if (!originalNotify && expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
|
|
45
|
+
return { restored: false, skippedReason: 'no-backup-not-installed' };
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
if (updated === text) return { restored: false, skippedReason: 'no-change' };
|
|
50
|
+
|
|
51
|
+
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
52
|
+
await fs.copyFile(configPath, backupPath).catch(() => {});
|
|
53
|
+
await fs.writeFile(configPath, updated, 'utf8');
|
|
54
|
+
return { restored: true, skippedReason: null };
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
async function
|
|
57
|
+
async function loadNotifyOriginal(notifyOriginalPath) {
|
|
49
58
|
const original = await readJson(notifyOriginalPath);
|
|
50
59
|
return Array.isArray(original?.notify) ? original.notify : null;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
async function
|
|
54
|
-
const text = await fs.readFile(
|
|
62
|
+
async function readNotify(configPath) {
|
|
63
|
+
const text = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
55
64
|
if (text == null) return null;
|
|
56
65
|
return extractNotify(text);
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPath }) {
|
|
69
|
+
return upsertNotify({
|
|
70
|
+
configPath: codexConfigPath,
|
|
71
|
+
notifyCmd,
|
|
72
|
+
notifyOriginalPath,
|
|
73
|
+
configLabel: 'Codex config'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function restoreCodexNotify({ codexConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
78
|
+
return restoreNotify({
|
|
79
|
+
configPath: codexConfigPath,
|
|
80
|
+
notifyOriginalPath,
|
|
81
|
+
expectedNotify: notifyCmd
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function loadCodexNotifyOriginal(notifyOriginalPath) {
|
|
86
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readCodexNotify(codexConfigPath) {
|
|
90
|
+
return readNotify(codexConfigPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function upsertEveryCodeNotify({ codeConfigPath, notifyCmd, notifyOriginalPath }) {
|
|
94
|
+
return upsertNotify({
|
|
95
|
+
configPath: codeConfigPath,
|
|
96
|
+
notifyCmd,
|
|
97
|
+
notifyOriginalPath,
|
|
98
|
+
configLabel: 'Every Code config'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function restoreEveryCodeNotify({ codeConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
103
|
+
return restoreNotify({
|
|
104
|
+
configPath: codeConfigPath,
|
|
105
|
+
notifyOriginalPath,
|
|
106
|
+
expectedNotify: notifyCmd
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadEveryCodeNotifyOriginal(notifyOriginalPath) {
|
|
111
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readEveryCodeNotify(codeConfigPath) {
|
|
115
|
+
return readNotify(codeConfigPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
59
118
|
function extractNotify(text) {
|
|
60
119
|
// Heuristic parse: find a line that starts with "notify =".
|
|
61
120
|
const lines = text.split(/\r?\n/);
|
|
@@ -150,8 +209,16 @@ function arraysEqual(a, b) {
|
|
|
150
209
|
}
|
|
151
210
|
|
|
152
211
|
module.exports = {
|
|
212
|
+
upsertNotify,
|
|
213
|
+
restoreNotify,
|
|
214
|
+
loadNotifyOriginal,
|
|
215
|
+
readNotify,
|
|
153
216
|
upsertCodexNotify,
|
|
154
217
|
restoreCodexNotify,
|
|
155
218
|
loadCodexNotifyOriginal,
|
|
156
|
-
readCodexNotify
|
|
219
|
+
readCodexNotify,
|
|
220
|
+
upsertEveryCodeNotify,
|
|
221
|
+
restoreEveryCodeNotify,
|
|
222
|
+
loadEveryCodeNotifyOriginal,
|
|
223
|
+
readEveryCodeNotify
|
|
157
224
|
};
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -3,12 +3,13 @@ const path = require('node:path');
|
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
4
|
|
|
5
5
|
const { readJson } = require('./fs');
|
|
6
|
-
const { readCodexNotify } = require('./codex-config');
|
|
6
|
+
const { readCodexNotify, readEveryCodeNotify } = require('./codex-config');
|
|
7
7
|
const { normalizeState: normalizeUploadState } = require('./upload-throttle');
|
|
8
8
|
|
|
9
9
|
async function collectTrackerDiagnostics({
|
|
10
10
|
home = os.homedir(),
|
|
11
|
-
codexHome = process.env.CODEX_HOME || path.join(home, '.codex')
|
|
11
|
+
codexHome = process.env.CODEX_HOME || path.join(home, '.codex'),
|
|
12
|
+
codeHome = process.env.CODE_HOME || path.join(home, '.code')
|
|
12
13
|
} = {}) {
|
|
13
14
|
const trackerDir = path.join(home, '.vibescore', 'tracker');
|
|
14
15
|
const configPath = path.join(trackerDir, 'config.json');
|
|
@@ -20,6 +21,7 @@ async function collectTrackerDiagnostics({
|
|
|
20
21
|
const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
|
|
21
22
|
const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
|
|
22
23
|
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
24
|
+
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
23
25
|
|
|
24
26
|
const config = await readJson(configPath);
|
|
25
27
|
const cursors = await readJson(cursorsPath);
|
|
@@ -37,6 +39,9 @@ async function collectTrackerDiagnostics({
|
|
|
37
39
|
const codexNotifyRaw = await readCodexNotify(codexConfigPath);
|
|
38
40
|
const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
|
|
39
41
|
const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
|
|
42
|
+
const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
|
|
43
|
+
const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
|
|
44
|
+
const everyCodeNotify = everyCodeConfigured ? everyCodeNotifyRaw.map((v) => redactValue(v, home)) : null;
|
|
40
45
|
|
|
41
46
|
const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
|
|
42
47
|
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
@@ -53,7 +58,9 @@ async function collectTrackerDiagnostics({
|
|
|
53
58
|
paths: {
|
|
54
59
|
tracker_dir: redactValue(trackerDir, home),
|
|
55
60
|
codex_home: redactValue(codexHome, home),
|
|
56
|
-
codex_config: redactValue(codexConfigPath, home)
|
|
61
|
+
codex_config: redactValue(codexConfigPath, home),
|
|
62
|
+
code_home: redactValue(codeHome, home),
|
|
63
|
+
code_config: redactValue(codeConfigPath, home)
|
|
57
64
|
},
|
|
58
65
|
config: {
|
|
59
66
|
base_url: typeof config?.baseUrl === 'string' ? config.baseUrl : null,
|
|
@@ -75,7 +82,9 @@ async function collectTrackerDiagnostics({
|
|
|
75
82
|
last_notify: lastNotify,
|
|
76
83
|
last_notify_triggered_sync: lastNotifySpawn,
|
|
77
84
|
codex_notify_configured: notifyConfigured,
|
|
78
|
-
codex_notify: codexNotify
|
|
85
|
+
codex_notify: codexNotify,
|
|
86
|
+
every_code_notify_configured: everyCodeConfigured,
|
|
87
|
+
every_code_notify: everyCodeNotify
|
|
79
88
|
},
|
|
80
89
|
upload: {
|
|
81
90
|
last_success_at: lastSuccessAt,
|
package/src/lib/rollout.js
CHANGED
|
@@ -5,6 +5,9 @@ const readline = require('node:readline');
|
|
|
5
5
|
|
|
6
6
|
const { ensureDir } = require('./fs');
|
|
7
7
|
|
|
8
|
+
const DEFAULT_SOURCE = 'codex';
|
|
9
|
+
const SOURCE_SEPARATOR = '|';
|
|
10
|
+
|
|
8
11
|
async function listRolloutFiles(sessionsDir) {
|
|
9
12
|
const out = [];
|
|
10
13
|
const years = await safeReadDir(sessionsDir);
|
|
@@ -33,7 +36,7 @@ async function listRolloutFiles(sessionsDir) {
|
|
|
33
36
|
return out;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onProgress }) {
|
|
39
|
+
async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onProgress, source }) {
|
|
37
40
|
await ensureDir(path.dirname(queuePath));
|
|
38
41
|
let filesProcessed = 0;
|
|
39
42
|
let eventsAggregated = 0;
|
|
@@ -42,13 +45,18 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
42
45
|
const totalFiles = Array.isArray(rolloutFiles) ? rolloutFiles.length : 0;
|
|
43
46
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
44
47
|
const touchedBuckets = new Set();
|
|
48
|
+
const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
45
49
|
|
|
46
50
|
if (!cursors.files || typeof cursors.files !== 'object') {
|
|
47
51
|
cursors.files = {};
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
for (let idx = 0; idx < rolloutFiles.length; idx++) {
|
|
51
|
-
const
|
|
55
|
+
const entry = rolloutFiles[idx];
|
|
56
|
+
const filePath = typeof entry === 'string' ? entry : entry?.path;
|
|
57
|
+
if (!filePath) continue;
|
|
58
|
+
const fileSource =
|
|
59
|
+
typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
|
|
52
60
|
const st = await fs.stat(filePath).catch(() => null);
|
|
53
61
|
if (!st || !st.isFile()) continue;
|
|
54
62
|
|
|
@@ -65,7 +73,8 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
65
73
|
lastTotal,
|
|
66
74
|
lastModel,
|
|
67
75
|
hourlyState,
|
|
68
|
-
touchedBuckets
|
|
76
|
+
touchedBuckets,
|
|
77
|
+
source: fileSource
|
|
69
78
|
});
|
|
70
79
|
|
|
71
80
|
cursors.files[key] = {
|
|
@@ -98,7 +107,15 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
98
107
|
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
async function parseRolloutFile({
|
|
110
|
+
async function parseRolloutFile({
|
|
111
|
+
filePath,
|
|
112
|
+
startOffset,
|
|
113
|
+
lastTotal,
|
|
114
|
+
lastModel,
|
|
115
|
+
hourlyState,
|
|
116
|
+
touchedBuckets,
|
|
117
|
+
source
|
|
118
|
+
}) {
|
|
102
119
|
const st = await fs.stat(filePath);
|
|
103
120
|
const endOffset = st.size;
|
|
104
121
|
if (startOffset >= endOffset) {
|
|
@@ -130,13 +147,13 @@ async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, h
|
|
|
130
147
|
continue;
|
|
131
148
|
}
|
|
132
149
|
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
150
|
+
const token = extractTokenCount(obj);
|
|
151
|
+
if (!token) continue;
|
|
135
152
|
|
|
136
|
-
const info =
|
|
153
|
+
const info = token.info;
|
|
137
154
|
if (!info || typeof info !== 'object') continue;
|
|
138
155
|
|
|
139
|
-
const tokenTimestamp = typeof
|
|
156
|
+
const tokenTimestamp = typeof token.timestamp === 'string' ? token.timestamp : null;
|
|
140
157
|
if (!tokenTimestamp) continue;
|
|
141
158
|
|
|
142
159
|
const lastUsage = info.last_token_usage;
|
|
@@ -152,9 +169,9 @@ async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, h
|
|
|
152
169
|
const bucketStart = toUtcHalfHourStart(tokenTimestamp);
|
|
153
170
|
if (!bucketStart) continue;
|
|
154
171
|
|
|
155
|
-
const bucket = getHourlyBucket(hourlyState, bucketStart);
|
|
172
|
+
const bucket = getHourlyBucket(hourlyState, source, bucketStart);
|
|
156
173
|
addTotals(bucket.totals, delta);
|
|
157
|
-
touchedBuckets.add(bucketStart);
|
|
174
|
+
touchedBuckets.add(bucketKey(source, bucketStart));
|
|
158
175
|
eventsAggregated += 1;
|
|
159
176
|
}
|
|
160
177
|
|
|
@@ -166,13 +183,17 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
166
183
|
|
|
167
184
|
const toAppend = [];
|
|
168
185
|
for (const bucketStart of touchedBuckets) {
|
|
169
|
-
const
|
|
186
|
+
const parsedKey = parseBucketKey(bucketStart);
|
|
187
|
+
const source = parsedKey.source || DEFAULT_SOURCE;
|
|
188
|
+
const hourStart = parsedKey.hourStart;
|
|
189
|
+
const bucket = hourlyState.buckets[bucketKey(source, hourStart)] || hourlyState.buckets[bucketStart];
|
|
170
190
|
if (!bucket || !bucket.totals) continue;
|
|
171
191
|
const key = totalsKey(bucket.totals);
|
|
172
192
|
if (bucket.queuedKey === key) continue;
|
|
173
193
|
toAppend.push(
|
|
174
194
|
JSON.stringify({
|
|
175
|
-
|
|
195
|
+
source,
|
|
196
|
+
hour_start: hourStart,
|
|
176
197
|
input_tokens: bucket.totals.input_tokens,
|
|
177
198
|
cached_input_tokens: bucket.totals.cached_input_tokens,
|
|
178
199
|
output_tokens: bucket.totals.output_tokens,
|
|
@@ -192,7 +213,15 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
192
213
|
|
|
193
214
|
function normalizeHourlyState(raw) {
|
|
194
215
|
const state = raw && typeof raw === 'object' ? raw : {};
|
|
195
|
-
const
|
|
216
|
+
const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
|
|
217
|
+
const buckets = {};
|
|
218
|
+
for (const [key, value] of Object.entries(rawBuckets)) {
|
|
219
|
+
if (key.includes(SOURCE_SEPARATOR)) {
|
|
220
|
+
buckets[key] = value;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
buckets[bucketKey(DEFAULT_SOURCE, key)] = value;
|
|
224
|
+
}
|
|
196
225
|
return {
|
|
197
226
|
version: 1,
|
|
198
227
|
buckets,
|
|
@@ -200,12 +229,14 @@ function normalizeHourlyState(raw) {
|
|
|
200
229
|
};
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
function getHourlyBucket(state, hourStart) {
|
|
232
|
+
function getHourlyBucket(state, source, hourStart) {
|
|
204
233
|
const buckets = state.buckets;
|
|
205
|
-
|
|
234
|
+
const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
235
|
+
const key = bucketKey(normalizedSource, hourStart);
|
|
236
|
+
let bucket = buckets[key];
|
|
206
237
|
if (!bucket || typeof bucket !== 'object') {
|
|
207
238
|
bucket = { totals: initTotals(), queuedKey: null };
|
|
208
|
-
buckets[
|
|
239
|
+
buckets[key] = bucket;
|
|
209
240
|
return bucket;
|
|
210
241
|
}
|
|
211
242
|
|
|
@@ -267,6 +298,37 @@ function toUtcHalfHourStart(ts) {
|
|
|
267
298
|
return bucketStart.toISOString();
|
|
268
299
|
}
|
|
269
300
|
|
|
301
|
+
function bucketKey(source, hourStart) {
|
|
302
|
+
const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
303
|
+
return `${safeSource}${SOURCE_SEPARATOR}${hourStart}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parseBucketKey(key) {
|
|
307
|
+
if (typeof key !== 'string') return { source: DEFAULT_SOURCE, hourStart: '' };
|
|
308
|
+
const idx = key.indexOf(SOURCE_SEPARATOR);
|
|
309
|
+
if (idx <= 0) return { source: DEFAULT_SOURCE, hourStart: key };
|
|
310
|
+
return { source: key.slice(0, idx), hourStart: key.slice(idx + 1) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeSourceInput(value) {
|
|
314
|
+
if (typeof value !== 'string') return null;
|
|
315
|
+
const trimmed = value.trim().toLowerCase();
|
|
316
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function extractTokenCount(obj) {
|
|
320
|
+
const payload = obj?.payload;
|
|
321
|
+
if (!payload) return null;
|
|
322
|
+
if (payload.type === 'token_count') {
|
|
323
|
+
return { info: payload.info, timestamp: obj?.timestamp || null };
|
|
324
|
+
}
|
|
325
|
+
const msg = payload.msg;
|
|
326
|
+
if (msg && msg.type === 'token_count') {
|
|
327
|
+
return { info: msg.info, timestamp: obj?.timestamp || null };
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
270
332
|
function pickDelta(lastUsage, totalUsage, prevTotals) {
|
|
271
333
|
const hasLast = isNonEmptyObject(lastUsage);
|
|
272
334
|
const hasTotal = isNonEmptyObject(totalUsage);
|
package/src/lib/uploader.js
CHANGED
|
@@ -5,6 +5,9 @@ const readline = require('node:readline');
|
|
|
5
5
|
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
6
6
|
const { ingestHourly } = require('./vibescore-api');
|
|
7
7
|
|
|
8
|
+
const DEFAULT_SOURCE = 'codex';
|
|
9
|
+
const SOURCE_SEPARATOR = '|';
|
|
10
|
+
|
|
8
11
|
async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePath, maxBatches, batchSize, onProgress }) {
|
|
9
12
|
await ensureDir(require('node:path').dirname(queueStatePath));
|
|
10
13
|
|
|
@@ -70,7 +73,9 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
|
|
|
70
73
|
}
|
|
71
74
|
const hourStart = typeof bucket?.hour_start === 'string' ? bucket.hour_start : null;
|
|
72
75
|
if (!hourStart) continue;
|
|
73
|
-
|
|
76
|
+
const source = normalizeSource(bucket?.source) || DEFAULT_SOURCE;
|
|
77
|
+
bucket.source = source;
|
|
78
|
+
bucketMap.set(bucketKey(source, hourStart), bucket);
|
|
74
79
|
linesRead += 1;
|
|
75
80
|
if (linesRead >= maxBuckets) break;
|
|
76
81
|
}
|
|
@@ -89,4 +94,14 @@ async function safeFileSize(p) {
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
function bucketKey(source, hourStart) {
|
|
98
|
+
return `${source}${SOURCE_SEPARATOR}${hourStart}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeSource(value) {
|
|
102
|
+
if (typeof value !== 'string') return null;
|
|
103
|
+
const trimmed = value.trim().toLowerCase();
|
|
104
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
module.exports = { drainQueueToCloud };
|