@vibescore/tracker 0.0.3 â 0.0.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/README.md +83 -12
- package/README.zh-CN.md +97 -0
- package/package.json +1 -1
- package/src/commands/init.js +8 -0
- package/src/commands/sync.js +5 -3
- package/src/lib/rollout.js +129 -27
- package/src/lib/upload-throttle.js +2 -2
- package/src/lib/uploader.js +19 -15
- package/src/lib/vibescore-api.js +3 -3
package/README.md
CHANGED
|
@@ -1,26 +1,97 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# đ˘ VIBESCORE
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**QUANTIFY YOUR AI OUTPUT**
|
|
6
|
+
_Real-time AI Analytics for Codex CLI_
|
|
7
|
+
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://www.apple.com/macos/)
|
|
11
|
+
|
|
12
|
+
[**English**](README.md) ⢠[**ä¸ć说ć**](README.zh-CN.md)
|
|
13
|
+
|
|
14
|
+
[**Documentation**](docs/) ⢠[**Dashboard**](dashboard/) ⢠[**Backend API**](BACKEND_API.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## đ Overview
|
|
21
|
+
|
|
22
|
+
**VibeScore** is an intelligent token usage tracking system designed specifically for macOS developers. It monitors Codex CLI output in real-time, transforming your **AI Output** into quantifiable metrics via a high-fidelity, **Matrix-themed** dashboard.
|
|
23
|
+
|
|
24
|
+
> [!TIP] > **Core Index**: Our signature metric that reflects your flow state by analyzing token consumption rates and patterns.
|
|
25
|
+
|
|
26
|
+
## đ Key Features
|
|
27
|
+
|
|
28
|
+
- đĄ **Live Sniffer**: Real-time interception of Codex CLI pipes using low-level hooks to capture every completion event.
|
|
29
|
+
- đ **Matrix Dashboard**: A high-performance React + Vite dashboard featuring heatmaps, trend charts, and live logs.
|
|
30
|
+
- ⥠**AI Analytics**: Deep analysis of Input/Output tokens, with dedicated tracking for Cached and Reasoning components.
|
|
31
|
+
- đ **Identity Core**: Robust authentication and permission management to secure your development data.
|
|
32
|
+
|
|
33
|
+
## đ ď¸ Quick Start
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
36
|
+
|
|
37
|
+
Initialize your environment with a single command:
|
|
6
38
|
|
|
7
39
|
```bash
|
|
8
40
|
npx --yes @vibescore/tracker init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Sync & Status
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Sync latest local session data
|
|
9
47
|
npx --yes @vibescore/tracker sync
|
|
48
|
+
|
|
49
|
+
# Check current link status
|
|
10
50
|
npx --yes @vibescore/tracker status
|
|
11
|
-
npx --yes @vibescore/tracker uninstall
|
|
12
51
|
```
|
|
13
52
|
|
|
14
|
-
##
|
|
53
|
+
## đď¸ Architecture
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD
|
|
57
|
+
A[Codex CLI] -->|Rollout Logs| B(Tracker CLI)
|
|
58
|
+
B -->|AI Tokens| C{Core Relay}
|
|
59
|
+
C --> D[VibeScore Dashboard]
|
|
60
|
+
C --> E[AI Analytics Engine]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## đť Developer Guide
|
|
64
|
+
|
|
65
|
+
To run locally or contribute:
|
|
15
66
|
|
|
16
|
-
|
|
17
|
-
|
|
67
|
+
### Dashboard Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Install dependencies
|
|
71
|
+
cd dashboard
|
|
72
|
+
npm install
|
|
73
|
+
|
|
74
|
+
# Start dev server
|
|
75
|
+
npm run dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Architecture Validation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Validate Copy Registry
|
|
82
|
+
npm run validate:copy
|
|
83
|
+
|
|
84
|
+
# Run smoke tests
|
|
85
|
+
npm run smoke
|
|
86
|
+
```
|
|
18
87
|
|
|
19
|
-
##
|
|
88
|
+
## đ License
|
|
20
89
|
|
|
21
|
-
|
|
22
|
-
- `sync` parses `~/.codex/sessions/**/rollout-*.jsonl` and uploads token_count deltas.
|
|
90
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
23
91
|
|
|
24
|
-
|
|
92
|
+
---
|
|
25
93
|
|
|
26
|
-
|
|
94
|
+
<div align="center">
|
|
95
|
+
<b>System_Ready // 2024 VibeScore OS</b><br/>
|
|
96
|
+
<i>"More Tokens. More Vibe."</i>
|
|
97
|
+
</div>
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# đ˘ VIBESCORE
|
|
4
|
+
|
|
5
|
+
**éĺä˝ ç AI äş§ĺş**
|
|
6
|
+
_Codex CLI ĺŽćś AI ĺć塼ĺ
ˇ_
|
|
7
|
+
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://www.apple.com/macos/)
|
|
11
|
+
|
|
12
|
+
[**English**](README.md) ⢠[**ä¸ć说ć**](README.zh-CN.md)
|
|
13
|
+
|
|
14
|
+
[**ć楣**](docs/) ⢠[**ć§ĺśĺ°**](dashboard/) ⢠[**ĺ獯ćĽĺŁ**](BACKEND_API.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## đ 饚çŽćŚčż°
|
|
21
|
+
|
|
22
|
+
**VibeScore** ćŻä¸ä¸Şä¸ä¸ş macOS ĺźĺč
莞莥çćşč˝äť¤çďźTokenďźä˝żç¨čż˝č¸ŞçłťçťăĺŽč˝ĺ¤ĺŽćśçć§ Codex CLI çčžĺşďźéčżéŤĺşŚĺŻč§ĺç **Matrix** éŁć źäťŞčĄ¨çďźĺ°ä˝ ç **AI äş§ĺş (AI Output)** 轏ĺ为ĺŻéĺçćć ă
|
|
23
|
+
|
|
24
|
+
> [!TIP] > **Core Index (ć ¸ĺżćć°)**: ć䝏çć ĺżć§ćć ďźéčżĺć Token ćśčéçä¸ć¨Ąĺźďźĺć ä˝ çĺźĺĺżćľçśćă
|
|
25
|
+
|
|
26
|
+
## đ ć ¸ĺżĺč˝
|
|
27
|
+
|
|
28
|
+
- đĄ **Live Sniffer (ĺŽćśĺ
ć˘)**: ĺŽćśçĺŹ Codex CLI 玥éďźéčżĺşĺą Hook ćčˇćŻä¸ćŹĄčĄĽĺ
¨äşäťśă
|
|
29
|
+
- đ **Matrix Dashboard (çŠéľć§ĺśĺ°)**: ĺşäş React + Vite çéŤć§č˝äťŞčĄ¨çďźĺ
ˇĺ¤çĺĺžăčśĺżĺžä¸ĺŽćśćĽĺżă
|
|
30
|
+
- ⥠**AI Analytics (AI ĺć)**: 桹庌ĺć Input/Output TokenďźćŻćçźĺ (Cached) ä¸ć¨ç (Reasoning) é¨ĺçĺ猝çć§ă
|
|
31
|
+
- đ **Identity Core (čşŤäť˝ć ¸ĺż)**: ĺŽĺ¤ç躍䝽éŞčŻä¸ćé玥çďźäżć¤ä˝ çĺźĺć°ćŽčľäş§ă
|
|
32
|
+
|
|
33
|
+
## đ ď¸ ĺżŤéĺźĺ§
|
|
34
|
+
|
|
35
|
+
### ĺŽčŁ
|
|
36
|
+
|
|
37
|
+
ĺŞéä¸čĄĺ˝äť¤ďźĺłĺŻĺĺ§ĺçŻĺ˘ďź
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx --yes @vibescore/tracker init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### ĺćĽä¸çśććĽç
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# ĺćĽćć°çćŹĺ°äźčŻć°ćŽ
|
|
47
|
+
npx --yes @vibescore/tracker sync
|
|
48
|
+
|
|
49
|
+
# ćĽçĺ˝ĺčżćĽçść
|
|
50
|
+
npx --yes @vibescore/tracker status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## đď¸ çłťçťćść
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD
|
|
57
|
+
A[Codex CLI] -->|Rollout Logs| B(Tracker CLI)
|
|
58
|
+
B -->|AI Tokens| C{Core Relay}
|
|
59
|
+
C --> D[VibeScore Dashboard]
|
|
60
|
+
C --> E[AI Analytics Engine]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## đť ĺźĺč
ćĺ
|
|
64
|
+
|
|
65
|
+
ĺŚćä˝ ćłĺ¨ćŹĺ°čżčĄćč´ĄçŽäťŁç ďź
|
|
66
|
+
|
|
67
|
+
### 䝪襨çĺźĺ
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# ĺŽčŁ
äžčľ
|
|
71
|
+
cd dashboard
|
|
72
|
+
npm install
|
|
73
|
+
|
|
74
|
+
# ĺŻĺ¨ĺźĺćĺĄĺ¨
|
|
75
|
+
npm run dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### ć´ä˝ćśćéŞčŻ
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# éŞčŻ Copy 注ĺ襨
|
|
82
|
+
npm run validate:copy
|
|
83
|
+
|
|
84
|
+
# ć§čĄçéžćľčŻ
|
|
85
|
+
npm run smoke
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## đ ĺźćşĺ莎
|
|
89
|
+
|
|
90
|
+
ćŹéĄšçŽĺşäş [MIT](LICENSE) ĺ莎ĺźćşă
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
<div align="center">
|
|
95
|
+
<b>System_Ready // 2014 VibeScore OS</b><br/>
|
|
96
|
+
<i>"More Tokens. More Vibe."</i>
|
|
97
|
+
</div>
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ const { prompt, promptHidden } = require('../lib/prompt');
|
|
|
7
7
|
const { upsertCodexNotify, loadCodexNotifyOriginal } = require('../lib/codex-config');
|
|
8
8
|
const { beginBrowserAuth } = require('../lib/browser-auth');
|
|
9
9
|
const { issueDeviceTokenWithPassword, issueDeviceTokenWithAccessToken } = require('../lib/insforge');
|
|
10
|
+
const { cmdSync } = require('./sync');
|
|
10
11
|
|
|
11
12
|
async function cmdInit(argv) {
|
|
12
13
|
const opts = parseArgs(argv);
|
|
@@ -104,6 +105,13 @@ async function cmdInit(argv) {
|
|
|
104
105
|
''
|
|
105
106
|
].join('\n')
|
|
106
107
|
);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await cmdSync([]);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err && err.message ? err.message : 'unknown error';
|
|
113
|
+
process.stderr.write(`Initial sync failed: ${msg}\n`);
|
|
114
|
+
}
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
function parseArgs(argv) {
|
package/src/commands/sync.js
CHANGED
|
@@ -45,7 +45,7 @@ async function cmdSync(argv) {
|
|
|
45
45
|
const rolloutFiles = await listRolloutFiles(sessionsDir);
|
|
46
46
|
|
|
47
47
|
if (progress?.enabled) {
|
|
48
|
-
progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files |
|
|
48
|
+
progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const parseResult = await parseRolloutIncremental({
|
|
@@ -56,7 +56,9 @@ async function cmdSync(argv) {
|
|
|
56
56
|
if (!progress?.enabled) return;
|
|
57
57
|
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
58
58
|
progress.update(
|
|
59
|
-
`Parsing ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files |
|
|
59
|
+
`Parsing ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
|
|
60
|
+
p.bucketsQueued
|
|
61
|
+
)}`
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
});
|
|
@@ -148,7 +150,7 @@ async function cmdSync(argv) {
|
|
|
148
150
|
[
|
|
149
151
|
'Sync finished:',
|
|
150
152
|
`- Parsed files: ${parseResult.filesProcessed}`,
|
|
151
|
-
`- New
|
|
153
|
+
`- New 30-min buckets queued: ${parseResult.bucketsQueued}`,
|
|
152
154
|
deviceToken
|
|
153
155
|
? `- Uploaded: ${uploadResult.inserted} inserted, ${uploadResult.skipped} skipped`
|
|
154
156
|
: '- Uploaded: skipped (no device token)',
|
package/src/lib/rollout.js
CHANGED
|
@@ -2,7 +2,6 @@ const fs = require('node:fs/promises');
|
|
|
2
2
|
const fssync = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const readline = require('node:readline');
|
|
5
|
-
const crypto = require('node:crypto');
|
|
6
5
|
|
|
7
6
|
const { ensureDir } = require('./fs');
|
|
8
7
|
|
|
@@ -37,10 +36,16 @@ async function listRolloutFiles(sessionsDir) {
|
|
|
37
36
|
async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onProgress }) {
|
|
38
37
|
await ensureDir(path.dirname(queuePath));
|
|
39
38
|
let filesProcessed = 0;
|
|
40
|
-
let
|
|
39
|
+
let eventsAggregated = 0;
|
|
41
40
|
|
|
42
41
|
const cb = typeof onProgress === 'function' ? onProgress : null;
|
|
43
42
|
const totalFiles = Array.isArray(rolloutFiles) ? rolloutFiles.length : 0;
|
|
43
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
44
|
+
const touchedBuckets = new Set();
|
|
45
|
+
|
|
46
|
+
if (!cursors.files || typeof cursors.files !== 'object') {
|
|
47
|
+
cursors.files = {};
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
for (let idx = 0; idx < rolloutFiles.length; idx++) {
|
|
46
51
|
const filePath = rolloutFiles[idx];
|
|
@@ -59,7 +64,8 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
59
64
|
startOffset,
|
|
60
65
|
lastTotal,
|
|
61
66
|
lastModel,
|
|
62
|
-
|
|
67
|
+
hourlyState,
|
|
68
|
+
touchedBuckets
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
cursors.files[key] = {
|
|
@@ -71,7 +77,7 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
filesProcessed += 1;
|
|
74
|
-
|
|
80
|
+
eventsAggregated += result.eventsAggregated;
|
|
75
81
|
|
|
76
82
|
if (cb) {
|
|
77
83
|
cb({
|
|
@@ -79,28 +85,32 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
79
85
|
total: totalFiles,
|
|
80
86
|
filePath,
|
|
81
87
|
filesProcessed,
|
|
82
|
-
|
|
88
|
+
eventsAggregated,
|
|
89
|
+
bucketsQueued: touchedBuckets.size
|
|
83
90
|
});
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
95
|
+
hourlyState.updatedAt = new Date().toISOString();
|
|
96
|
+
cursors.hourly = hourlyState;
|
|
97
|
+
|
|
98
|
+
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel,
|
|
101
|
+
async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, hourlyState, touchedBuckets }) {
|
|
91
102
|
const st = await fs.stat(filePath);
|
|
92
103
|
const endOffset = st.size;
|
|
93
104
|
if (startOffset >= endOffset) {
|
|
94
|
-
return { endOffset, lastTotal, lastModel,
|
|
105
|
+
return { endOffset, lastTotal, lastModel, eventsAggregated: 0 };
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
const stream = fssync.createReadStream(filePath, { encoding: 'utf8', start: startOffset });
|
|
98
109
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
99
110
|
|
|
100
|
-
const toAppend = [];
|
|
101
111
|
let model = typeof lastModel === 'string' ? lastModel : null;
|
|
102
112
|
let totals = lastTotal && typeof lastTotal === 'object' ? lastTotal : null;
|
|
103
|
-
let
|
|
113
|
+
let eventsAggregated = 0;
|
|
104
114
|
|
|
105
115
|
for await (const line of rl) {
|
|
106
116
|
if (!line) continue;
|
|
@@ -139,26 +149,122 @@ async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, q
|
|
|
139
149
|
totals = totalUsage;
|
|
140
150
|
}
|
|
141
151
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
const bucketStart = toUtcHalfHourStart(tokenTimestamp);
|
|
153
|
+
if (!bucketStart) continue;
|
|
154
|
+
|
|
155
|
+
const bucket = getHourlyBucket(hourlyState, bucketStart);
|
|
156
|
+
addTotals(bucket.totals, delta);
|
|
157
|
+
touchedBuckets.add(bucketStart);
|
|
158
|
+
eventsAggregated += 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets }) {
|
|
165
|
+
if (!touchedBuckets || touchedBuckets.size === 0) return 0;
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
const toAppend = [];
|
|
168
|
+
for (const bucketStart of touchedBuckets) {
|
|
169
|
+
const bucket = hourlyState.buckets[bucketStart];
|
|
170
|
+
if (!bucket || !bucket.totals) continue;
|
|
171
|
+
const key = totalsKey(bucket.totals);
|
|
172
|
+
if (bucket.queuedKey === key) continue;
|
|
173
|
+
toAppend.push(
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
hour_start: bucketStart,
|
|
176
|
+
input_tokens: bucket.totals.input_tokens,
|
|
177
|
+
cached_input_tokens: bucket.totals.cached_input_tokens,
|
|
178
|
+
output_tokens: bucket.totals.output_tokens,
|
|
179
|
+
reasoning_output_tokens: bucket.totals.reasoning_output_tokens,
|
|
180
|
+
total_tokens: bucket.totals.total_tokens
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
bucket.queuedKey = key;
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
if (toAppend.length > 0) {
|
|
158
187
|
await fs.appendFile(queuePath, toAppend.join('\n') + '\n', 'utf8');
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
return
|
|
190
|
+
return toAppend.length;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeHourlyState(raw) {
|
|
194
|
+
const state = raw && typeof raw === 'object' ? raw : {};
|
|
195
|
+
const buckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
|
|
196
|
+
return {
|
|
197
|
+
version: 1,
|
|
198
|
+
buckets,
|
|
199
|
+
updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getHourlyBucket(state, hourStart) {
|
|
204
|
+
const buckets = state.buckets;
|
|
205
|
+
let bucket = buckets[hourStart];
|
|
206
|
+
if (!bucket || typeof bucket !== 'object') {
|
|
207
|
+
bucket = { totals: initTotals(), queuedKey: null };
|
|
208
|
+
buckets[hourStart] = bucket;
|
|
209
|
+
return bucket;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!bucket.totals || typeof bucket.totals !== 'object') {
|
|
213
|
+
bucket.totals = initTotals();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
|
|
217
|
+
bucket.queuedKey = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return bucket;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function initTotals() {
|
|
224
|
+
return {
|
|
225
|
+
input_tokens: 0,
|
|
226
|
+
cached_input_tokens: 0,
|
|
227
|
+
output_tokens: 0,
|
|
228
|
+
reasoning_output_tokens: 0,
|
|
229
|
+
total_tokens: 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function addTotals(target, delta) {
|
|
234
|
+
target.input_tokens += delta.input_tokens || 0;
|
|
235
|
+
target.cached_input_tokens += delta.cached_input_tokens || 0;
|
|
236
|
+
target.output_tokens += delta.output_tokens || 0;
|
|
237
|
+
target.reasoning_output_tokens += delta.reasoning_output_tokens || 0;
|
|
238
|
+
target.total_tokens += delta.total_tokens || 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function totalsKey(totals) {
|
|
242
|
+
return [
|
|
243
|
+
totals.input_tokens || 0,
|
|
244
|
+
totals.cached_input_tokens || 0,
|
|
245
|
+
totals.output_tokens || 0,
|
|
246
|
+
totals.reasoning_output_tokens || 0,
|
|
247
|
+
totals.total_tokens || 0
|
|
248
|
+
].join('|');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toUtcHalfHourStart(ts) {
|
|
252
|
+
const dt = new Date(ts);
|
|
253
|
+
if (!Number.isFinite(dt.getTime())) return null;
|
|
254
|
+
const minutes = dt.getUTCMinutes();
|
|
255
|
+
const halfMinute = minutes >= 30 ? 30 : 0;
|
|
256
|
+
const bucketStart = new Date(
|
|
257
|
+
Date.UTC(
|
|
258
|
+
dt.getUTCFullYear(),
|
|
259
|
+
dt.getUTCMonth(),
|
|
260
|
+
dt.getUTCDate(),
|
|
261
|
+
dt.getUTCHours(),
|
|
262
|
+
halfMinute,
|
|
263
|
+
0,
|
|
264
|
+
0
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
return bucketStart.toISOString();
|
|
162
268
|
}
|
|
163
269
|
|
|
164
270
|
function pickDelta(lastUsage, totalUsage, prevTotals) {
|
|
@@ -209,10 +315,6 @@ function normalizeUsage(u) {
|
|
|
209
315
|
return out;
|
|
210
316
|
}
|
|
211
317
|
|
|
212
|
-
function sha256Hex(s) {
|
|
213
|
-
return crypto.createHash('sha256').update(s, 'utf8').digest('hex');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
318
|
function isNonEmptyObject(v) {
|
|
217
319
|
return Boolean(v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0);
|
|
218
320
|
}
|
package/src/lib/uploader.js
CHANGED
|
@@ -3,7 +3,7 @@ const fssync = require('node:fs');
|
|
|
3
3
|
const readline = require('node:readline');
|
|
4
4
|
|
|
5
5
|
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
6
|
-
const {
|
|
6
|
+
const { ingestHourly } = require('./vibescore-api');
|
|
7
7
|
|
|
8
8
|
async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePath, maxBatches, batchSize, onProgress }) {
|
|
9
9
|
await ensureDir(require('node:path').dirname(queueStatePath));
|
|
@@ -16,14 +16,14 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
16
16
|
|
|
17
17
|
const cb = typeof onProgress === 'function' ? onProgress : null;
|
|
18
18
|
const queueSize = await safeFileSize(queuePath);
|
|
19
|
-
const
|
|
19
|
+
const maxBuckets = Math.max(1, Math.floor(Number(batchSize || 200)));
|
|
20
20
|
|
|
21
21
|
for (let batch = 0; batch < maxBatches; batch++) {
|
|
22
|
-
const res = await readBatch(queuePath, offset,
|
|
23
|
-
if (res.
|
|
22
|
+
const res = await readBatch(queuePath, offset, maxBuckets);
|
|
23
|
+
if (res.buckets.length === 0) break;
|
|
24
24
|
|
|
25
|
-
attempted += res.
|
|
26
|
-
const ingest = await
|
|
25
|
+
attempted += res.buckets.length;
|
|
26
|
+
const ingest = await ingestHourly({ baseUrl, deviceToken, hourly: res.buckets });
|
|
27
27
|
inserted += ingest.inserted || 0;
|
|
28
28
|
skipped += ingest.skipped || 0;
|
|
29
29
|
|
|
@@ -47,33 +47,37 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
47
47
|
return { inserted, skipped, attempted };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function readBatch(queuePath, startOffset,
|
|
50
|
+
async function readBatch(queuePath, startOffset, maxBuckets) {
|
|
51
51
|
const st = await fs.stat(queuePath).catch(() => null);
|
|
52
|
-
if (!st || !st.isFile()) return {
|
|
53
|
-
if (startOffset >= st.size) return {
|
|
52
|
+
if (!st || !st.isFile()) return { buckets: [], nextOffset: startOffset };
|
|
53
|
+
if (startOffset >= st.size) return { buckets: [], nextOffset: startOffset };
|
|
54
54
|
|
|
55
55
|
const stream = fssync.createReadStream(queuePath, { encoding: 'utf8', start: startOffset });
|
|
56
56
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
57
57
|
|
|
58
|
-
const
|
|
58
|
+
const bucketMap = new Map();
|
|
59
59
|
let offset = startOffset;
|
|
60
|
+
let linesRead = 0;
|
|
60
61
|
for await (const line of rl) {
|
|
61
62
|
const bytes = Buffer.byteLength(line, 'utf8') + 1;
|
|
62
63
|
offset += bytes;
|
|
63
64
|
if (!line.trim()) continue;
|
|
64
|
-
let
|
|
65
|
+
let bucket;
|
|
65
66
|
try {
|
|
66
|
-
|
|
67
|
+
bucket = JSON.parse(line);
|
|
67
68
|
} catch (_e) {
|
|
68
69
|
continue;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
if (
|
|
71
|
+
const hourStart = typeof bucket?.hour_start === 'string' ? bucket.hour_start : null;
|
|
72
|
+
if (!hourStart) continue;
|
|
73
|
+
bucketMap.set(hourStart, bucket);
|
|
74
|
+
linesRead += 1;
|
|
75
|
+
if (linesRead >= maxBuckets) break;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
rl.close();
|
|
75
79
|
stream.close?.();
|
|
76
|
-
return {
|
|
80
|
+
return { buckets: Array.from(bucketMap.values()), nextOffset: offset };
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
async function safeFileSize(p) {
|
package/src/lib/vibescore-api.js
CHANGED
|
@@ -35,13 +35,13 @@ async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = '
|
|
|
35
35
|
return { token, deviceId };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function
|
|
38
|
+
async function ingestHourly({ baseUrl, deviceToken, hourly }) {
|
|
39
39
|
const data = await invokeFunctionWithRetry({
|
|
40
40
|
baseUrl,
|
|
41
41
|
accessToken: deviceToken,
|
|
42
42
|
slug: 'vibescore-ingest',
|
|
43
43
|
method: 'POST',
|
|
44
|
-
body: {
|
|
44
|
+
body: { hourly },
|
|
45
45
|
errorPrefix: 'Ingest failed',
|
|
46
46
|
retry: { maxRetries: 3, baseDelayMs: 500, maxDelayMs: 5000 }
|
|
47
47
|
});
|
|
@@ -72,7 +72,7 @@ async function syncHeartbeat({ baseUrl, deviceToken }) {
|
|
|
72
72
|
module.exports = {
|
|
73
73
|
signInWithPassword,
|
|
74
74
|
issueDeviceToken,
|
|
75
|
-
|
|
75
|
+
ingestHourly,
|
|
76
76
|
syncHeartbeat
|
|
77
77
|
};
|
|
78
78
|
|