@vibescore/tracker 0.1.1 → 0.2.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/package.json +1 -1
- package/src/cli.js +1 -0
- package/src/commands/init.js +46 -2
- package/src/lib/insforge.js +8 -2
- package/src/lib/rollout.js +268 -20
- package/src/lib/vibescore-api.js +28 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -48,6 +48,7 @@ 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
|
+
' - optional: pass --link-code <code> to skip browser login when provided by Dashboard.',
|
|
51
52
|
' - when ~/.code/config.toml exists, init also installs an Every Code notify hook.',
|
|
52
53
|
' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted landing page.',
|
|
53
54
|
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl (Every Code), then uploads token_count deltas.',
|
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('node:path');
|
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
4
|
const fssync = require('node:fs');
|
|
5
5
|
const cp = require('node:child_process');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
6
7
|
|
|
7
8
|
const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
|
|
8
9
|
const { prompt, promptHidden } = require('../lib/prompt');
|
|
@@ -14,7 +15,11 @@ const {
|
|
|
14
15
|
} = require('../lib/codex-config');
|
|
15
16
|
const { upsertClaudeHook, buildClaudeHookCommand } = require('../lib/claude-config');
|
|
16
17
|
const { beginBrowserAuth } = require('../lib/browser-auth');
|
|
17
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
issueDeviceTokenWithPassword,
|
|
20
|
+
issueDeviceTokenWithAccessToken,
|
|
21
|
+
issueDeviceTokenWithLinkCode
|
|
22
|
+
} = require('../lib/insforge');
|
|
18
23
|
|
|
19
24
|
async function cmdInit(argv) {
|
|
20
25
|
const opts = parseArgs(argv);
|
|
@@ -29,6 +34,7 @@ async function cmdInit(argv) {
|
|
|
29
34
|
|
|
30
35
|
const configPath = path.join(trackerDir, 'config.json');
|
|
31
36
|
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
37
|
+
const linkCodeStatePath = path.join(trackerDir, 'link_code_state.json');
|
|
32
38
|
|
|
33
39
|
const baseUrl = opts.baseUrl || process.env.VIBESCORE_INSFORGE_BASE_URL || 'https://5tmappuk.us-east.insforge.app';
|
|
34
40
|
let dashboardUrl = opts.dashboardUrl || process.env.VIBESCORE_DASHBOARD_URL || null;
|
|
@@ -44,7 +50,36 @@ async function cmdInit(argv) {
|
|
|
44
50
|
|
|
45
51
|
await installLocalTrackerApp({ appDir });
|
|
46
52
|
|
|
47
|
-
if (!deviceToken &&
|
|
53
|
+
if (!deviceToken && opts.linkCode) {
|
|
54
|
+
const deviceName = opts.deviceName || os.hostname();
|
|
55
|
+
const platform = normalizePlatform(process.platform);
|
|
56
|
+
const linkCode = String(opts.linkCode);
|
|
57
|
+
const linkCodeHash = crypto.createHash('sha256').update(linkCode).digest('hex');
|
|
58
|
+
const existingLinkState = await readJson(linkCodeStatePath);
|
|
59
|
+
let requestId =
|
|
60
|
+
existingLinkState?.linkCodeHash === linkCodeHash && existingLinkState?.requestId
|
|
61
|
+
? existingLinkState.requestId
|
|
62
|
+
: null;
|
|
63
|
+
if (!requestId) {
|
|
64
|
+
requestId = crypto.randomUUID();
|
|
65
|
+
await writeJson(linkCodeStatePath, {
|
|
66
|
+
linkCodeHash,
|
|
67
|
+
requestId,
|
|
68
|
+
createdAt: new Date().toISOString()
|
|
69
|
+
});
|
|
70
|
+
await chmod600IfPossible(linkCodeStatePath);
|
|
71
|
+
}
|
|
72
|
+
const issued = await issueDeviceTokenWithLinkCode({
|
|
73
|
+
baseUrl,
|
|
74
|
+
linkCode,
|
|
75
|
+
requestId,
|
|
76
|
+
deviceName,
|
|
77
|
+
platform
|
|
78
|
+
});
|
|
79
|
+
deviceToken = issued.token;
|
|
80
|
+
deviceId = issued.deviceId;
|
|
81
|
+
await fs.rm(linkCodeStatePath, { force: true });
|
|
82
|
+
} else if (!deviceToken && !opts.noAuth) {
|
|
48
83
|
const deviceName = opts.deviceName || os.hostname();
|
|
49
84
|
|
|
50
85
|
if (opts.email || opts.password) {
|
|
@@ -171,6 +206,7 @@ function parseArgs(argv) {
|
|
|
171
206
|
email: null,
|
|
172
207
|
password: null,
|
|
173
208
|
deviceName: null,
|
|
209
|
+
linkCode: null,
|
|
174
210
|
noAuth: false,
|
|
175
211
|
noOpen: false
|
|
176
212
|
};
|
|
@@ -182,6 +218,7 @@ function parseArgs(argv) {
|
|
|
182
218
|
else if (a === '--email') out.email = argv[++i] || null;
|
|
183
219
|
else if (a === '--password') out.password = argv[++i] || null;
|
|
184
220
|
else if (a === '--device-name') out.deviceName = argv[++i] || null;
|
|
221
|
+
else if (a === '--link-code') out.linkCode = argv[++i] || null;
|
|
185
222
|
else if (a === '--no-auth') out.noAuth = true;
|
|
186
223
|
else if (a === '--no-open') out.noOpen = true;
|
|
187
224
|
else throw new Error(`Unknown option: ${a}`);
|
|
@@ -194,6 +231,13 @@ function maskSecret(s) {
|
|
|
194
231
|
return `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
195
232
|
}
|
|
196
233
|
|
|
234
|
+
function normalizePlatform(value) {
|
|
235
|
+
if (value === 'darwin') return 'macos';
|
|
236
|
+
if (value === 'win32') return 'windows';
|
|
237
|
+
if (value === 'linux') return 'linux';
|
|
238
|
+
return 'unknown';
|
|
239
|
+
}
|
|
240
|
+
|
|
197
241
|
function buildNotifyHandler({ trackerDir, packageName }) {
|
|
198
242
|
// Keep this file dependency-free: Node built-ins only.
|
|
199
243
|
// It must never block Codex; it spawns sync in the background and exits 0.
|
package/src/lib/insforge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { issueDeviceToken, signInWithPassword } = require('./vibescore-api');
|
|
1
|
+
const { exchangeLinkCode, issueDeviceToken, signInWithPassword } = require('./vibescore-api');
|
|
2
2
|
|
|
3
3
|
async function issueDeviceTokenWithPassword({ baseUrl, email, password, deviceName }) {
|
|
4
4
|
const accessToken = await signInWithPassword({ baseUrl, email, password });
|
|
@@ -11,7 +11,13 @@ async function issueDeviceTokenWithAccessToken({ baseUrl, accessToken, deviceNam
|
|
|
11
11
|
return issued;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
async function issueDeviceTokenWithLinkCode({ baseUrl, linkCode, requestId, deviceName, platform }) {
|
|
15
|
+
const issued = await exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, platform });
|
|
16
|
+
return { token: issued.token, deviceId: issued.deviceId };
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
module.exports = {
|
|
15
20
|
issueDeviceTokenWithPassword,
|
|
16
|
-
issueDeviceTokenWithAccessToken
|
|
21
|
+
issueDeviceTokenWithAccessToken,
|
|
22
|
+
issueDeviceTokenWithLinkCode
|
|
17
23
|
};
|
package/src/lib/rollout.js
CHANGED
|
@@ -470,44 +470,207 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
470
470
|
if (touchedGroups.size === 0) return 0;
|
|
471
471
|
|
|
472
472
|
const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
|
|
473
|
+
let codexTouched = false;
|
|
473
474
|
const legacyGroups = new Set();
|
|
474
475
|
for (const groupKey of touchedGroups) {
|
|
475
476
|
if (Object.prototype.hasOwnProperty.call(groupQueued, groupKey)) {
|
|
476
477
|
legacyGroups.add(groupKey);
|
|
477
478
|
}
|
|
479
|
+
if (!codexTouched && groupKey.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`)) {
|
|
480
|
+
codexTouched = true;
|
|
481
|
+
}
|
|
478
482
|
}
|
|
479
483
|
|
|
480
|
-
const
|
|
481
|
-
for (const
|
|
482
|
-
|
|
484
|
+
const groupedBuckets = new Map();
|
|
485
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
486
|
+
if (!bucket || !bucket.totals) continue;
|
|
487
|
+
const parsed = parseBucketKey(key);
|
|
483
488
|
const hourStart = parsed.hourStart;
|
|
484
489
|
if (!hourStart) continue;
|
|
485
490
|
const groupKey = groupBucketKey(parsed.source, hourStart);
|
|
486
|
-
if (legacyGroups.has(groupKey)) continue;
|
|
491
|
+
if (!touchedGroups.has(groupKey) || legacyGroups.has(groupKey)) continue;
|
|
492
|
+
|
|
493
|
+
const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
494
|
+
const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
|
|
495
|
+
let group = groupedBuckets.get(groupKey);
|
|
496
|
+
if (!group) {
|
|
497
|
+
group = { source, hourStart, buckets: new Map() };
|
|
498
|
+
groupedBuckets.set(groupKey, group);
|
|
499
|
+
}
|
|
487
500
|
|
|
488
|
-
const normalizedKey = bucketKey(parsed.source, parsed.model, hourStart);
|
|
489
|
-
const bucket = hourlyState.buckets ? hourlyState.buckets[normalizedKey] : null;
|
|
490
|
-
if (!bucket || !bucket.totals) continue;
|
|
491
501
|
if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
|
|
492
502
|
bucket.queuedKey = null;
|
|
493
503
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
504
|
+
group.buckets.set(model, bucket);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (codexTouched) {
|
|
508
|
+
const recomputeGroups = new Set();
|
|
509
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
510
|
+
if (!bucket || !bucket.totals) continue;
|
|
511
|
+
const parsed = parseBucketKey(key);
|
|
512
|
+
const hourStart = parsed.hourStart;
|
|
513
|
+
if (!hourStart) continue;
|
|
514
|
+
const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
515
|
+
if (source !== 'every-code') continue;
|
|
516
|
+
const groupKey = groupBucketKey(source, hourStart);
|
|
517
|
+
if (legacyGroups.has(groupKey) || groupedBuckets.has(groupKey)) continue;
|
|
518
|
+
const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
|
|
519
|
+
if (model !== DEFAULT_MODEL) continue;
|
|
520
|
+
recomputeGroups.add(groupKey);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (recomputeGroups.size > 0) {
|
|
524
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
525
|
+
if (!bucket || !bucket.totals) continue;
|
|
526
|
+
const parsed = parseBucketKey(key);
|
|
527
|
+
const hourStart = parsed.hourStart;
|
|
528
|
+
if (!hourStart) continue;
|
|
529
|
+
const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
530
|
+
const groupKey = groupBucketKey(source, hourStart);
|
|
531
|
+
if (!recomputeGroups.has(groupKey)) continue;
|
|
532
|
+
let group = groupedBuckets.get(groupKey);
|
|
533
|
+
if (!group) {
|
|
534
|
+
group = { source, hourStart, buckets: new Map() };
|
|
535
|
+
groupedBuckets.set(groupKey, group);
|
|
536
|
+
}
|
|
537
|
+
if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
|
|
538
|
+
bucket.queuedKey = null;
|
|
539
|
+
}
|
|
540
|
+
const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
|
|
541
|
+
group.buckets.set(model, bucket);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const codexDominants = collectCodexDominantModels(hourlyState);
|
|
547
|
+
|
|
548
|
+
const toAppend = [];
|
|
549
|
+
for (const group of groupedBuckets.values()) {
|
|
550
|
+
const unknownBucket = group.buckets.get(DEFAULT_MODEL) || null;
|
|
551
|
+
const dominantModel = pickDominantModel(group.buckets);
|
|
552
|
+
let alignedModel = null;
|
|
553
|
+
if (unknownBucket?.alignedModel) {
|
|
554
|
+
const normalized = normalizeModelInput(unknownBucket.alignedModel);
|
|
555
|
+
alignedModel = normalized && normalized !== DEFAULT_MODEL ? normalized : null;
|
|
556
|
+
}
|
|
557
|
+
const zeroTotals = initTotals();
|
|
558
|
+
const zeroKey = totalsKey(zeroTotals);
|
|
559
|
+
|
|
560
|
+
if (dominantModel) {
|
|
561
|
+
if (alignedModel && !group.buckets.has(alignedModel)) {
|
|
562
|
+
toAppend.push(
|
|
563
|
+
JSON.stringify({
|
|
564
|
+
source: group.source,
|
|
565
|
+
model: alignedModel,
|
|
566
|
+
hour_start: group.hourStart,
|
|
567
|
+
input_tokens: zeroTotals.input_tokens,
|
|
568
|
+
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
569
|
+
output_tokens: zeroTotals.output_tokens,
|
|
570
|
+
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
571
|
+
total_tokens: zeroTotals.total_tokens
|
|
572
|
+
})
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (unknownBucket && !alignedModel && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
|
|
576
|
+
if (unknownBucket.retractedUnknownKey !== zeroKey) {
|
|
577
|
+
toAppend.push(
|
|
578
|
+
JSON.stringify({
|
|
579
|
+
source: group.source,
|
|
580
|
+
model: DEFAULT_MODEL,
|
|
581
|
+
hour_start: group.hourStart,
|
|
582
|
+
input_tokens: zeroTotals.input_tokens,
|
|
583
|
+
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
584
|
+
output_tokens: zeroTotals.output_tokens,
|
|
585
|
+
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
586
|
+
total_tokens: zeroTotals.total_tokens
|
|
587
|
+
})
|
|
588
|
+
);
|
|
589
|
+
unknownBucket.retractedUnknownKey = zeroKey;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (unknownBucket) unknownBucket.alignedModel = null;
|
|
593
|
+
for (const [model, bucket] of group.buckets.entries()) {
|
|
594
|
+
if (model === DEFAULT_MODEL) continue;
|
|
595
|
+
let totals = bucket.totals;
|
|
596
|
+
if (model === dominantModel && unknownBucket?.totals) {
|
|
597
|
+
totals = cloneTotals(bucket.totals);
|
|
598
|
+
addTotals(totals, unknownBucket.totals);
|
|
599
|
+
}
|
|
600
|
+
const key = totalsKey(totals);
|
|
601
|
+
if (bucket.queuedKey === key) continue;
|
|
602
|
+
toAppend.push(
|
|
603
|
+
JSON.stringify({
|
|
604
|
+
source: group.source,
|
|
605
|
+
model,
|
|
606
|
+
hour_start: group.hourStart,
|
|
607
|
+
input_tokens: totals.input_tokens,
|
|
608
|
+
cached_input_tokens: totals.cached_input_tokens,
|
|
609
|
+
output_tokens: totals.output_tokens,
|
|
610
|
+
reasoning_output_tokens: totals.reasoning_output_tokens,
|
|
611
|
+
total_tokens: totals.total_tokens
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
bucket.queuedKey = key;
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!unknownBucket?.totals) continue;
|
|
620
|
+
let outputModel = DEFAULT_MODEL;
|
|
621
|
+
if (group.source === 'every-code') {
|
|
622
|
+
const aligned = findNearestCodexModel(group.hourStart, codexDominants);
|
|
623
|
+
if (aligned) outputModel = aligned;
|
|
624
|
+
}
|
|
625
|
+
const nextAligned = outputModel !== DEFAULT_MODEL ? outputModel : null;
|
|
626
|
+
if (alignedModel && alignedModel !== nextAligned) {
|
|
627
|
+
toAppend.push(
|
|
628
|
+
JSON.stringify({
|
|
629
|
+
source: group.source,
|
|
630
|
+
model: alignedModel,
|
|
631
|
+
hour_start: group.hourStart,
|
|
632
|
+
input_tokens: zeroTotals.input_tokens,
|
|
633
|
+
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
634
|
+
output_tokens: zeroTotals.output_tokens,
|
|
635
|
+
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
636
|
+
total_tokens: zeroTotals.total_tokens
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (!alignedModel && nextAligned && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
|
|
641
|
+
if (unknownBucket.retractedUnknownKey !== zeroKey) {
|
|
642
|
+
toAppend.push(
|
|
643
|
+
JSON.stringify({
|
|
644
|
+
source: group.source,
|
|
645
|
+
model: DEFAULT_MODEL,
|
|
646
|
+
hour_start: group.hourStart,
|
|
647
|
+
input_tokens: zeroTotals.input_tokens,
|
|
648
|
+
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
649
|
+
output_tokens: zeroTotals.output_tokens,
|
|
650
|
+
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
651
|
+
total_tokens: zeroTotals.total_tokens
|
|
652
|
+
})
|
|
653
|
+
);
|
|
654
|
+
unknownBucket.retractedUnknownKey = zeroKey;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (unknownBucket) unknownBucket.alignedModel = nextAligned;
|
|
658
|
+
const key = totalsKey(unknownBucket.totals);
|
|
659
|
+
const outputKey = outputModel === DEFAULT_MODEL ? key : `${key}|${outputModel}`;
|
|
660
|
+
if (unknownBucket.queuedKey === outputKey) continue;
|
|
498
661
|
toAppend.push(
|
|
499
662
|
JSON.stringify({
|
|
500
|
-
source,
|
|
501
|
-
model,
|
|
502
|
-
hour_start: hourStart,
|
|
503
|
-
input_tokens:
|
|
504
|
-
cached_input_tokens:
|
|
505
|
-
output_tokens:
|
|
506
|
-
reasoning_output_tokens:
|
|
507
|
-
total_tokens:
|
|
663
|
+
source: group.source,
|
|
664
|
+
model: outputModel,
|
|
665
|
+
hour_start: group.hourStart,
|
|
666
|
+
input_tokens: unknownBucket.totals.input_tokens,
|
|
667
|
+
cached_input_tokens: unknownBucket.totals.cached_input_tokens,
|
|
668
|
+
output_tokens: unknownBucket.totals.output_tokens,
|
|
669
|
+
reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
|
|
670
|
+
total_tokens: unknownBucket.totals.total_tokens
|
|
508
671
|
})
|
|
509
672
|
);
|
|
510
|
-
|
|
673
|
+
unknownBucket.queuedKey = outputKey;
|
|
511
674
|
}
|
|
512
675
|
|
|
513
676
|
if (legacyGroups.size > 0) {
|
|
@@ -564,6 +727,91 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
564
727
|
return toAppend.length;
|
|
565
728
|
}
|
|
566
729
|
|
|
730
|
+
function pickDominantModel(buckets) {
|
|
731
|
+
let dominantModel = null;
|
|
732
|
+
let dominantTotal = -1;
|
|
733
|
+
for (const [model, bucket] of buckets.entries()) {
|
|
734
|
+
if (model === DEFAULT_MODEL) continue;
|
|
735
|
+
const total = Number(bucket?.totals?.total_tokens || 0);
|
|
736
|
+
if (
|
|
737
|
+
dominantModel == null ||
|
|
738
|
+
total > dominantTotal ||
|
|
739
|
+
(total === dominantTotal && model < dominantModel)
|
|
740
|
+
) {
|
|
741
|
+
dominantModel = model;
|
|
742
|
+
dominantTotal = total;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return dominantModel;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function cloneTotals(totals) {
|
|
749
|
+
const cloned = initTotals();
|
|
750
|
+
addTotals(cloned, totals || {});
|
|
751
|
+
return cloned;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function collectCodexDominantModels(hourlyState) {
|
|
755
|
+
const grouped = new Map();
|
|
756
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
757
|
+
if (!bucket || !bucket.totals) continue;
|
|
758
|
+
const parsed = parseBucketKey(key);
|
|
759
|
+
const hourStart = parsed.hourStart;
|
|
760
|
+
if (!hourStart) continue;
|
|
761
|
+
const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
762
|
+
if (source !== DEFAULT_SOURCE) continue;
|
|
763
|
+
const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
|
|
764
|
+
if (model === DEFAULT_MODEL) continue;
|
|
765
|
+
|
|
766
|
+
let models = grouped.get(hourStart);
|
|
767
|
+
if (!models) {
|
|
768
|
+
models = new Map();
|
|
769
|
+
grouped.set(hourStart, models);
|
|
770
|
+
}
|
|
771
|
+
const total = Number(bucket.totals.total_tokens || 0);
|
|
772
|
+
models.set(model, (models.get(model) || 0) + total);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const dominants = [];
|
|
776
|
+
for (const [hourStart, models] of grouped.entries()) {
|
|
777
|
+
let dominantModel = null;
|
|
778
|
+
let dominantTotal = -1;
|
|
779
|
+
for (const [model, total] of models.entries()) {
|
|
780
|
+
if (
|
|
781
|
+
dominantModel == null ||
|
|
782
|
+
total > dominantTotal ||
|
|
783
|
+
(total === dominantTotal && model < dominantModel)
|
|
784
|
+
) {
|
|
785
|
+
dominantModel = model;
|
|
786
|
+
dominantTotal = total;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (dominantModel) {
|
|
790
|
+
dominants.push({ hourStart, model: dominantModel });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return dominants;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function findNearestCodexModel(hourStart, dominants) {
|
|
798
|
+
if (!hourStart || !dominants || dominants.length === 0) return null;
|
|
799
|
+
const target = Date.parse(hourStart);
|
|
800
|
+
if (!Number.isFinite(target)) return null;
|
|
801
|
+
|
|
802
|
+
let best = null;
|
|
803
|
+
for (const entry of dominants) {
|
|
804
|
+
const candidate = Date.parse(entry.hourStart);
|
|
805
|
+
if (!Number.isFinite(candidate)) continue;
|
|
806
|
+
const diff = Math.abs(candidate - target);
|
|
807
|
+
if (!best || diff < best.diff || (diff === best.diff && candidate < best.time)) {
|
|
808
|
+
best = { diff, time: candidate, model: entry.model };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return best ? best.model : null;
|
|
813
|
+
}
|
|
814
|
+
|
|
567
815
|
function normalizeHourlyState(raw) {
|
|
568
816
|
const state = raw && typeof raw === 'object' ? raw : {};
|
|
569
817
|
const version = Number(state.version || 1);
|
package/src/lib/vibescore-api.js
CHANGED
|
@@ -35,6 +35,33 @@ async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = '
|
|
|
35
35
|
return { token, deviceId };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
async function exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, platform = 'macos' }) {
|
|
39
|
+
const data = await invokeFunction({
|
|
40
|
+
baseUrl,
|
|
41
|
+
accessToken: null,
|
|
42
|
+
slug: 'vibescore-link-code-exchange',
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: {
|
|
45
|
+
link_code: linkCode,
|
|
46
|
+
request_id: requestId,
|
|
47
|
+
device_name: deviceName,
|
|
48
|
+
platform
|
|
49
|
+
},
|
|
50
|
+
errorPrefix: 'Link code exchange failed'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const token = data?.token;
|
|
54
|
+
const deviceId = data?.device_id;
|
|
55
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
56
|
+
throw new Error('Link code exchange failed: missing token');
|
|
57
|
+
}
|
|
58
|
+
if (typeof deviceId !== 'string' || deviceId.length === 0) {
|
|
59
|
+
throw new Error('Link code exchange failed: missing device_id');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { token, deviceId, userId: data?.user_id || null };
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
async function ingestHourly({ baseUrl, deviceToken, hourly }) {
|
|
39
66
|
const data = await invokeFunctionWithRetry({
|
|
40
67
|
baseUrl,
|
|
@@ -72,6 +99,7 @@ async function syncHeartbeat({ baseUrl, deviceToken }) {
|
|
|
72
99
|
module.exports = {
|
|
73
100
|
signInWithPassword,
|
|
74
101
|
issueDeviceToken,
|
|
102
|
+
exchangeLinkCode,
|
|
75
103
|
ingestHourly,
|
|
76
104
|
syncHeartbeat
|
|
77
105
|
};
|