@vibe-cafe/vibe-usage 0.2.7 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -102,14 +102,16 @@ function _send(apiUrl, apiKey, buckets, onProgress) {
102
102
  }
103
103
 
104
104
  /**
105
- * DELETE all usage data for the authenticated user.
106
- * @param {string} apiUrl - Base URL (e.g. "https://vibecafe.ai")
107
- * @param {string} apiKey - Bearer token (vbu_xxx)
105
+ * DELETE usage data for the authenticated user.
106
+ * @param {string} apiUrl
107
+ * @param {string} apiKey
108
+ * @param {{hostname?: string}} [opts]
108
109
  * @returns {Promise<{deleted: number}>}
109
110
  */
110
- export function deleteAllData(apiUrl, apiKey) {
111
+ export function deleteAllData(apiUrl, apiKey, opts) {
111
112
  return new Promise((resolve, reject) => {
112
113
  const url = new URL('/api/usage/ingest', apiUrl);
114
+ if (opts?.hostname) url.searchParams.set('hostname', opts.hostname);
113
115
  const mod = url.protocol === 'https:' ? https : http;
114
116
 
115
117
  const req = mod.request(url, {
package/src/index.js CHANGED
@@ -13,7 +13,6 @@ async function showStatus() {
13
13
  console.log(` Config: ${getConfigPath()}`);
14
14
  console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
15
15
  console.log(` API URL: ${config.apiUrl || 'https://vibecafe.ai'}`);
16
- console.log(` Last sync: ${config.lastSync || 'never'}`);
17
16
  }
18
17
 
19
18
  console.log('\n Detected tools:');
@@ -35,7 +34,7 @@ async function showStatus() {
35
34
  console.log();
36
35
  }
37
36
 
38
- const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl', 'lastSync'];
37
+ const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl'];
39
38
 
40
39
  function handleConfig(args) {
41
40
  const sub = args[0];
@@ -105,7 +104,7 @@ export async function run(args) {
105
104
  }
106
105
  case 'reset': {
107
106
  const { runReset } = await import('./reset.js');
108
- await runReset();
107
+ await runReset(args.slice(1));
109
108
  break;
110
109
  }
111
110
  case 'config': {
@@ -127,6 +126,7 @@ export async function run(args) {
127
126
  npx vibe-usage init Set up API key
128
127
  npx vibe-usage sync Manually sync usage data
129
128
  npx vibe-usage reset Delete all data and re-upload
129
+ npx vibe-usage reset --host Delete data for this host only and re-upload
130
130
  npx vibe-usage status Show config and detected tools
131
131
  npx vibe-usage config show Show full config as JSON
132
132
  npx vibe-usage config get <key> Get a config value
package/src/init.js CHANGED
@@ -61,7 +61,6 @@ export async function runInit() {
61
61
  const config = {
62
62
  apiKey,
63
63
  apiUrl,
64
- lastSync: existing?.lastSync || null,
65
64
  };
66
65
  saveConfig(config);
67
66
 
@@ -35,7 +35,7 @@ export function commitState() {
35
35
  }
36
36
  }
37
37
 
38
- export async function parse(lastSync) {
38
+ export async function parse() {
39
39
  let sessions;
40
40
  try {
41
41
  sessions = await loadSessionData({ mode: 'display' });
@@ -50,7 +50,6 @@ export async function parse(lastSync) {
50
50
  const entries = [];
51
51
 
52
52
  for (const session of sessions) {
53
- if (lastSync && new Date(session.lastActivity) <= new Date(lastSync)) continue;
54
53
 
55
54
  const project = resolveProject(session);
56
55
  const sessionKey = `${session.projectPath}\0${session.sessionId}`;
@@ -27,21 +27,13 @@ function findJsonlFiles(dir) {
27
27
  return results;
28
28
  }
29
29
 
30
- export async function parse(lastSync) {
30
+ export async function parse() {
31
31
  if (!existsSync(SESSIONS_DIR)) return [];
32
32
 
33
33
  const entries = [];
34
34
  const files = findJsonlFiles(SESSIONS_DIR);
35
35
  if (files.length === 0) return [];
36
36
  for (const filePath of files) {
37
- if (lastSync) {
38
- try {
39
- const stat = statSync(filePath);
40
- if (stat.mtime <= new Date(lastSync)) continue;
41
- } catch {
42
- continue;
43
- }
44
- }
45
37
 
46
38
  let content;
47
39
  try {
@@ -99,7 +91,6 @@ export async function parse(lastSync) {
99
91
 
100
92
  const timestamp = obj.timestamp ? new Date(obj.timestamp) : null;
101
93
  if (!timestamp || isNaN(timestamp.getTime())) continue;
102
- if (lastSync && timestamp <= new Date(lastSync)) continue;
103
94
 
104
95
  // Prefer incremental per-request usage; compute delta from cumulative total as fallback
105
96
  let usage = info.last_token_usage;
@@ -30,21 +30,13 @@ function findSessionFiles(baseDir) {
30
30
  return results;
31
31
  }
32
32
 
33
- export async function parse(lastSync) {
33
+ export async function parse() {
34
34
  const sessionFiles = findSessionFiles(TMP_DIR);
35
35
  if (sessionFiles.length === 0) return [];
36
36
 
37
37
  const entries = [];
38
38
 
39
39
  for (const filePath of sessionFiles) {
40
- if (lastSync) {
41
- try {
42
- const stat = statSync(filePath);
43
- if (stat.mtime <= new Date(lastSync)) continue;
44
- } catch {
45
- continue;
46
- }
47
- }
48
40
 
49
41
  let data;
50
42
  try {
@@ -65,7 +57,6 @@ export async function parse(lastSync) {
65
57
  if (!timestamp) continue;
66
58
  const ts = new Date(timestamp);
67
59
  if (isNaN(ts.getTime())) continue;
68
- if (lastSync && ts <= new Date(lastSync)) continue;
69
60
 
70
61
  if (tokens) {
71
62
  // New format: { input, output, cached, thoughts, tool, total }
@@ -20,7 +20,7 @@ function getTokens(usage, ...keys) {
20
20
  return 0;
21
21
  }
22
22
 
23
- export async function parse(lastSync) {
23
+ export async function parse() {
24
24
  const entries = [];
25
25
 
26
26
  for (const root of POSSIBLE_ROOTS) {
@@ -49,14 +49,6 @@ export async function parse(lastSync) {
49
49
 
50
50
  for (const file of files) {
51
51
  const filePath = join(sessionsDir, file);
52
- if (lastSync) {
53
- try {
54
- const stat = statSync(filePath);
55
- if (stat.mtime <= new Date(lastSync)) continue;
56
- } catch {
57
- continue;
58
- }
59
- }
60
52
 
61
53
  let content;
62
54
  try {
@@ -82,7 +74,6 @@ export async function parse(lastSync) {
82
74
  if (!timestamp) continue;
83
75
  const ts = new Date(typeof timestamp === 'number' ? timestamp : timestamp);
84
76
  if (isNaN(ts.getTime())) continue;
85
- if (lastSync && ts <= new Date(lastSync)) continue;
86
77
 
87
78
  entries.push({
88
79
  source: 'openclaw',
@@ -12,26 +12,22 @@ const MESSAGES_DIR = join(DATA_DIR, 'storage', 'message');
12
12
  * Parse opencode usage data.
13
13
  * Tries SQLite database first (opencode >= v0.2), falls back to legacy JSON files.
14
14
  */
15
- export async function parse(lastSync) {
15
+ export async function parse() {
16
16
  if (existsSync(DB_PATH)) {
17
17
  try {
18
- return parseFromSqlite(lastSync);
18
+ return parseFromSqlite();
19
19
  } catch (err) {
20
20
  process.stderr.write(`warn: opencode sqlite parse failed (${err.message}), trying legacy json...\n`);
21
21
  }
22
22
  }
23
- return parseFromJson(lastSync);
23
+ return parseFromJson();
24
24
  }
25
25
 
26
- function parseFromSqlite(lastSync) {
26
+ function parseFromSqlite() {
27
27
  // Build WHERE clause: only messages with token data
28
28
  const conditions = [
29
29
  "(json_extract(data, '$.tokens.input') > 0 OR json_extract(data, '$.tokens.output') > 0)",
30
30
  ];
31
- if (lastSync) {
32
- const sinceMs = new Date(lastSync).getTime();
33
- conditions.push(`time_created > ${sinceMs}`);
34
- }
35
31
 
36
32
  const query = `SELECT data FROM message WHERE ${conditions.join(' AND ')}`;
37
33
 
@@ -76,7 +72,6 @@ function parseFromSqlite(lastSync) {
76
72
 
77
73
  const timestamp = new Date(data.time?.created);
78
74
  if (isNaN(timestamp.getTime())) continue;
79
- if (lastSync && timestamp <= new Date(lastSync)) continue;
80
75
 
81
76
  const rootPath = data.path?.root;
82
77
  const project = rootPath ? basename(rootPath) : 'unknown';
@@ -97,7 +92,7 @@ function parseFromSqlite(lastSync) {
97
92
  }
98
93
 
99
94
  /** Legacy parser: reads JSON files from storage/message directories. */
100
- function parseFromJson(lastSync) {
95
+ function parseFromJson() {
101
96
  if (!existsSync(MESSAGES_DIR)) return [];
102
97
 
103
98
  const entries = [];
@@ -120,14 +115,6 @@ function parseFromJson(lastSync) {
120
115
 
121
116
  for (const file of msgFiles) {
122
117
  const filePath = join(sessionPath, file);
123
- if (lastSync) {
124
- try {
125
- const stat = statSync(filePath);
126
- if (stat.mtime <= new Date(lastSync)) continue;
127
- } catch {
128
- continue;
129
- }
130
- }
131
118
 
132
119
  let data;
133
120
  try {
@@ -144,7 +131,6 @@ function parseFromJson(lastSync) {
144
131
 
145
132
  const timestamp = new Date(data.time?.created);
146
133
  if (isNaN(timestamp.getTime())) continue;
147
- if (lastSync && timestamp <= new Date(lastSync)) continue;
148
134
 
149
135
  const rootPath = data.path?.root;
150
136
  const project = rootPath ? basename(rootPath) : 'unknown';
package/src/reset.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { existsSync, unlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { homedir } from 'node:os';
4
+ import { homedir, hostname as getHostname } from 'node:os';
5
5
  import { loadConfig, saveConfig } from './config.js';
6
6
  import { deleteAllData } from './api.js';
7
7
  import { runSync } from './sync.js';
@@ -20,33 +20,57 @@ function prompt(question) {
20
20
  });
21
21
  }
22
22
 
23
- export async function runReset() {
23
+ export async function runReset(args = []) {
24
+ const hostOnly = args.includes('--host');
24
25
  const config = loadConfig();
25
26
  if (!config?.apiKey) {
26
27
  console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
27
28
  process.exit(1);
28
29
  }
29
30
 
30
- const answer = await prompt('This will delete ALL your usage data and re-upload from local logs. Continue? (y/N) ');
31
- if (answer.toLowerCase() !== 'y') {
32
- console.log('Cancelled.');
33
- return;
34
- }
35
-
31
+ const currentHost = getHostname();
36
32
  const apiUrl = config.apiUrl || 'https://vibecafe.ai';
37
33
 
38
- // 1. Delete remote data
39
- console.log('Deleting remote data...');
40
- try {
41
- const result = await deleteAllData(apiUrl, config.apiKey);
42
- console.log(`Deleted ${result.deleted} buckets from server.`);
43
- } catch (err) {
44
- if (err.message === 'UNAUTHORIZED') {
45
- console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
34
+ if (hostOnly) {
35
+ const answer = await prompt(`This will delete usage data for this host (${currentHost}) and re-upload from local logs. Continue? (y/N) `);
36
+ if (answer.toLowerCase() !== 'y') {
37
+ console.log('Cancelled.');
38
+ return;
39
+ }
40
+
41
+ // 1. Delete remote data for this host
42
+ console.log(`Deleting remote data for host: ${currentHost}...`);
43
+ try {
44
+ const result = await deleteAllData(apiUrl, config.apiKey, { hostname: currentHost });
45
+ console.log(`Deleted ${result.deleted} buckets from server.`);
46
+ } catch (err) {
47
+ if (err.message === 'UNAUTHORIZED') {
48
+ console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
49
+ process.exit(1);
50
+ }
51
+ console.error(`Failed to delete remote data: ${err.message}`);
52
+ process.exit(1);
53
+ }
54
+ } else {
55
+ const answer = await prompt('This will delete ALL your usage data and re-upload from local logs. Continue? (y/N) ');
56
+ if (answer.toLowerCase() !== 'y') {
57
+ console.log('Cancelled.');
58
+ return;
59
+ }
60
+
61
+ // 1. Delete all remote data
62
+ console.log('Deleting all remote data...');
63
+ try {
64
+ const result = await deleteAllData(apiUrl, config.apiKey);
65
+ console.log(`Deleted ${result.deleted} buckets from server.`);
66
+ } catch (err) {
67
+ if (err.message === 'UNAUTHORIZED') {
68
+ console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
69
+ process.exit(1);
70
+ }
71
+ console.error(`Failed to delete remote data: ${err.message}`);
46
72
  process.exit(1);
47
73
  }
48
- console.error(`Failed to delete remote data: ${err.message}`);
49
- process.exit(1);
50
74
  }
51
75
 
52
76
  // 2. Clear local state
package/src/sync.js CHANGED
@@ -18,12 +18,17 @@ export async function runSync() {
18
18
  process.exit(1);
19
19
  }
20
20
 
21
- const lastSync = config.lastSync || null;
21
+ // Migration: remove deprecated lastSync field from config
22
+ if ('lastSync' in config) {
23
+ delete config.lastSync;
24
+ saveConfig(config);
25
+ }
26
+
22
27
  const allBuckets = [];
23
28
 
24
29
  for (const [source, parse] of Object.entries(parsers)) {
25
30
  try {
26
- const buckets = await parse(lastSync);
31
+ const buckets = await parse();
27
32
  if (buckets.length > 0) {
28
33
  allBuckets.push(...buckets);
29
34
  }
@@ -63,9 +68,7 @@ export async function runSync() {
63
68
  });
64
69
  totalIngested += result.ingested ?? batch.length;
65
70
 
66
- // Save progress after each successful batch so partial uploads survive interruptions
67
- config.lastSync = new Date().toISOString();
68
- saveConfig(config);
71
+ // State commit happens after ALL batches complete (see postSyncHooks below)
69
72
  }
70
73
 
71
74
 
@@ -86,7 +89,7 @@ export async function runSync() {
86
89
  console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
87
90
  process.exit(1);
88
91
  }
89
- // Progress already saved per-batch — report partial success
92
+ // Report partial success
90
93
  if (totalIngested > 0) {
91
94
  console.error(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
92
95
  } else {