@vibe-cafe/vibe-usage 0.10.1 → 0.10.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/api.js CHANGED
@@ -14,7 +14,7 @@ export async function ingest(apiUrl, apiKey, buckets, opts, sessions) {
14
14
  let lastError;
15
15
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
16
16
  try {
17
- return await _send(apiUrl, apiKey, buckets, opts?.onProgress, sessions);
17
+ return await _send(apiUrl, apiKey, buckets, opts, sessions);
18
18
  } catch (err) {
19
19
  lastError = err;
20
20
  // Don't retry auth errors or client errors
@@ -30,14 +30,22 @@ export async function ingest(apiUrl, apiKey, buckets, opts, sessions) {
30
30
  throw lastError;
31
31
  }
32
32
 
33
- function _send(apiUrl, apiKey, buckets, onProgress, sessions) {
33
+ export function encodeIngestBody(buckets, opts, sessions) {
34
+ const payload = { buckets };
35
+ if (sessions && sessions.length > 0) payload.sessions = sessions;
36
+ if (opts?.client) payload.client = opts.client;
37
+ const raw = Buffer.from(JSON.stringify(payload));
38
+ const useGzip = raw.length >= GZIP_MIN_BYTES;
39
+ return {
40
+ body: useGzip ? gzipSync(raw) : raw,
41
+ useGzip,
42
+ };
43
+ }
44
+
45
+ function _send(apiUrl, apiKey, buckets, opts, sessions) {
34
46
  return new Promise((resolve, reject) => {
35
47
  const url = new URL('/api/usage/ingest', apiUrl);
36
- const payload = { buckets };
37
- if (sessions && sessions.length > 0) payload.sessions = sessions;
38
- const raw = Buffer.from(JSON.stringify(payload));
39
- const useGzip = raw.length >= GZIP_MIN_BYTES;
40
- const body = useGzip ? gzipSync(raw) : raw;
48
+ const { body, useGzip } = encodeIngestBody(buckets, opts, sessions);
41
49
  const totalBytes = body.length;
42
50
  const mod = url.protocol === 'https:' ? https : http;
43
51
 
@@ -89,7 +97,7 @@ function _send(apiUrl, apiKey, buckets, onProgress, sessions) {
89
97
  while (ok && sent < totalBytes) {
90
98
  const slice = body.subarray(sent, sent + CHUNK);
91
99
  sent += slice.length;
92
- if (onProgress) onProgress(sent, totalBytes);
100
+ if (opts?.onProgress) opts.onProgress(sent, totalBytes);
93
101
  ok = req.write(slice);
94
102
  }
95
103
  if (sent < totalBytes) {
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ const pkg = JSON.parse(
5
+ readFileSync(new URL('../package.json', import.meta.url), 'utf-8'),
6
+ );
7
+
8
+ const SURFACES = new Set(['cli', 'daemon', 'mac-app', 'windows-app']);
9
+
10
+ export const COLLECTOR_VERSION = String(pkg.version);
11
+
12
+ function cleanEnv(value, maxLength = 50) {
13
+ if (typeof value !== 'string') return null;
14
+ const cleaned = value.trim().slice(0, maxLength);
15
+ return cleaned || null;
16
+ }
17
+
18
+ export function createSyncClient({ defaultSurface = 'cli', hostname } = {}) {
19
+ const requestedSurface = cleanEnv(process.env.VIBE_USAGE_SURFACE, 30);
20
+ const surface = requestedSurface && SURFACES.has(requestedSurface)
21
+ ? requestedSurface
22
+ : defaultSurface;
23
+ const runtime = process.versions.bun ? 'bun' : 'node';
24
+ const runtimeVersion = process.versions.bun || process.versions.node;
25
+
26
+ return {
27
+ collectorVersion: COLLECTOR_VERSION,
28
+ surface,
29
+ surfaceVersion: cleanEnv(process.env.VIBE_USAGE_SURFACE_VERSION) || COLLECTOR_VERSION,
30
+ runtime,
31
+ runtimeVersion,
32
+ platform: process.platform,
33
+ hostname: cleanEnv(hostname, 200) || 'unknown',
34
+ syncId: randomUUID(),
35
+ };
36
+ }
37
+
38
+ export function forBatch(client, batchIndex, batchCount) {
39
+ return {
40
+ ...client,
41
+ batchIndex,
42
+ batchCount,
43
+ };
44
+ }
package/src/daemon.js CHANGED
@@ -34,7 +34,7 @@ export async function runDaemon() {
34
34
  // eslint-disable-next-line no-constant-condition
35
35
  while (true) {
36
36
  try {
37
- await runSync({ throws: true, quiet: true });
37
+ await runSync({ throws: true, quiet: true, surface: 'daemon' });
38
38
  consecutiveAuthFailures = 0;
39
39
  } catch (err) {
40
40
  if (err.message === 'UNAUTHORIZED') {
package/src/sync.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  bucketKey, bucketHash, sessionKey, sessionHash,
6
6
  } from './state.js';
7
7
  import { ingest, fetchSettings } from './api.js';
8
+ import { createSyncClient, forBatch } from './client-meta.js';
8
9
  import { parsers } from './parsers/index.js';
9
10
  import { success, failure, arrow, link, dim } from './output.js';
10
11
 
@@ -17,7 +18,7 @@ function formatBytes(bytes) {
17
18
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
18
19
  }
19
20
 
20
- export async function runSync({ throws = false, quiet = false } = {}) {
21
+ export async function runSync({ throws = false, quiet = false, surface = 'cli' } = {}) {
21
22
  const config = loadConfig();
22
23
  if (!config?.apiKey) {
23
24
  console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
@@ -192,10 +193,12 @@ export async function runSync({ throws = false, quiet = false } = {}) {
192
193
  let totalDroppedBuckets = 0;
193
194
  let totalDroppedUnknownModels = 0;
194
195
  let totalDroppedImplausible = 0;
196
+ let totalProtectedBuckets = 0;
195
197
  const droppedSources = new Set();
196
198
  const bucketBatches = Math.ceil(allBucketsToSend.length / BATCH_SIZE);
197
199
  const sessionBatches = Math.ceil(allSessionsToSend.length / SESSION_BATCH_SIZE);
198
200
  const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
201
+ const syncClient = createSyncClient({ defaultSurface: surface, hostname: host });
199
202
 
200
203
  try {
201
204
  for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
@@ -205,6 +208,7 @@ export async function runSync({ throws = false, quiet = false } = {}) {
205
208
  const prefix = totalBatches > 1 ? ` ${dim(`[${batchNum}/${totalBatches}]`)} 上传中 ` : ' 上传中 ';
206
209
 
207
210
  const result = await ingest(apiUrl, config.apiKey, batch, {
211
+ client: forBatch(syncClient, batchIdx, totalBatches),
208
212
  onProgress(sent, total) {
209
213
  const pct = Math.round((sent / total) * 100);
210
214
  process.stdout.write(`\r${prefix}${dim(`${formatBytes(sent)}/${formatBytes(total)} (${pct}%)`)}\x1b[K`);
@@ -219,6 +223,7 @@ export async function runSync({ throws = false, quiet = false } = {}) {
219
223
  totalDroppedImplausible += Number(result.dropped.implausible) || 0;
220
224
  for (const s of result.dropped.unknownSources || []) droppedSources.add(s);
221
225
  }
226
+ totalProtectedBuckets += Number(result.protected?.buckets) || 0;
222
227
 
223
228
  // Commit only this batch's hashes, only after it uploaded successfully.
224
229
  // A batch that throws aborts the loop with its keys still absent from
@@ -259,6 +264,10 @@ export async function runSync({ throws = false, quiet = false } = {}) {
259
264
  console.log(dim(` ${totalDroppedBuckets} buckets dropped (${reasons.join(';')})`));
260
265
  }
261
266
 
267
+ if (totalProtectedBuckets > 0) {
268
+ console.log(dim(` 服务端保留了 ${totalProtectedBuckets} 个更大的已有 bucket(本次较小快照未覆盖)`));
269
+ }
270
+
262
271
  if (!quiet && totalSessionsSynced > 0) {
263
272
  const totalActive = allSessionsToSend.reduce((s, x) => s + x.activeSeconds, 0);
264
273
  const totalDuration = allSessionsToSend.reduce((s, x) => s + x.durationSeconds, 0);