@vibe-cafe/vibe-usage 0.2.6 → 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/README.md CHANGED
@@ -11,8 +11,7 @@ npx vibe-usage
11
11
  This will:
12
12
  1. Ask for your API key (get one at https://vibecafe.ai/usage/setup)
13
13
  2. Detect installed AI coding tools
14
- 3. Install session-end hooks for automatic syncing
15
- 4. Run an initial sync of your usage data
14
+ 3. Run an initial sync of your usage data
16
15
 
17
16
  ## Commands
18
17
 
@@ -20,18 +19,19 @@ This will:
20
19
  npx vibe-usage # Init (first run) or sync (subsequent runs)
21
20
  npx vibe-usage init # Re-run setup
22
21
  npx vibe-usage sync # Manual sync
22
+ npx vibe-usage reset # Delete all data and re-upload from local logs
23
23
  npx vibe-usage status # Show config & detected tools
24
24
  ```
25
25
 
26
26
  ## Supported Tools
27
27
 
28
- | Tool | Auto-sync | Data Location |
29
- |------|-----------|---------------|
30
- | Claude Code | Yes (session hook) | `~/.claude/projects/` |
31
- | Codex CLI | Yes (notify hook) | `~/.codex/sessions/` |
32
- | Gemini CLI | Yes (session hook) | `~/.gemini/tmp/` |
33
- | OpenCode | Manual only | `~/.local/share/opencode/opencode.db` (SQLite) |
34
- | OpenClaw | Manual only | `~/.openclaw/agents/` |
28
+ | Tool | Data Location |
29
+ |------|---------------|
30
+ | Claude Code | `~/.claude/projects/` |
31
+ | Codex CLI | `~/.codex/sessions/` |
32
+ | Gemini CLI | `~/.gemini/tmp/` |
33
+ | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite) |
34
+ | OpenClaw | `~/.openclaw/agents/` |
35
35
 
36
36
  ## How It Works
37
37
 
@@ -39,6 +39,7 @@ npx vibe-usage status # Show config & detected tools
39
39
  - Aggregates token usage into 30-minute buckets
40
40
  - Uploads to your vibecafe.ai dashboard
41
41
  - Only syncs new data since last sync (incremental)
42
+ - For continuous syncing, use the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app) (auto-syncs every 5 minutes)
42
43
 
43
44
  ## Config
44
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.2.6",
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
@@ -100,3 +100,54 @@ function _send(apiUrl, apiKey, buckets, onProgress) {
100
100
  writeNext();
101
101
  });
102
102
  }
103
+
104
+ /**
105
+ * DELETE usage data for the authenticated user.
106
+ * @param {string} apiUrl
107
+ * @param {string} apiKey
108
+ * @param {{hostname?: string}} [opts]
109
+ * @returns {Promise<{deleted: number}>}
110
+ */
111
+ export function deleteAllData(apiUrl, apiKey, opts) {
112
+ return new Promise((resolve, reject) => {
113
+ const url = new URL('/api/usage/ingest', apiUrl);
114
+ if (opts?.hostname) url.searchParams.set('hostname', opts.hostname);
115
+ const mod = url.protocol === 'https:' ? https : http;
116
+
117
+ const req = mod.request(url, {
118
+ method: 'DELETE',
119
+ timeout: 60_000,
120
+ headers: {
121
+ 'Authorization': `Bearer ${apiKey}`,
122
+ },
123
+ }, (res) => {
124
+ let data = '';
125
+ res.on('data', (chunk) => { data += chunk; });
126
+ res.on('end', () => {
127
+ if (res.statusCode === 401) {
128
+ reject(new Error('UNAUTHORIZED'));
129
+ return;
130
+ }
131
+ if (res.statusCode < 200 || res.statusCode >= 300) {
132
+ const err = new Error(`HTTP ${res.statusCode}: ${data}`);
133
+ err.statusCode = res.statusCode;
134
+ reject(err);
135
+ return;
136
+ }
137
+ try {
138
+ resolve(JSON.parse(data));
139
+ } catch {
140
+ reject(new Error(`Invalid JSON response: ${data}`));
141
+ }
142
+ });
143
+ });
144
+
145
+ req.on('error', (err) => reject(err));
146
+ req.on('timeout', () => {
147
+ req.destroy();
148
+ reject(new Error('Request timed out (60s)'));
149
+ });
150
+
151
+ req.end();
152
+ });
153
+ }
package/src/hooks.js CHANGED
@@ -1,178 +1,32 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
- const SYNC_CMD = 'npx @vibe-cafe/vibe-usage sync 2>/dev/null &';
6
-
7
- /**
8
- * Check if a SessionEnd hook array (new or old format) already contains a vibe-usage hook.
9
- */
10
- function hasVibeUsageHook(hooks) {
11
- if (!Array.isArray(hooks)) return false;
12
- return hooks.some(entry => {
13
- // New format: { matcher?: "...", hooks: [{ type, command }] }
14
- if (Array.isArray(entry.hooks)) {
15
- return entry.hooks.some(h => h.command && h.command.includes('vibe-usage'));
16
- }
17
- // Old format: { type, command } directly
18
- if (entry.command && entry.command.includes('vibe-usage')) return true;
19
- return false;
20
- });
21
- }
22
-
23
- /**
24
- * Migrate old-format hook entries to the new matcher format.
25
- * Old: [{ type: "command", command: "..." }]
26
- * New: [{ hooks: [{ type: "command", command: "..." }] }]
27
- */
28
- function migrateOldFormatHooks(hooks) {
29
- if (!Array.isArray(hooks)) return hooks;
30
- return hooks.map(entry => {
31
- // Already new format (has "hooks" array)
32
- if (Array.isArray(entry.hooks)) return entry;
33
- // Old format: bare handler → wrap in matcher group
34
- if (entry.type && entry.command) {
35
- return { hooks: [entry] };
36
- }
37
- return entry;
38
- });
39
- }
40
-
41
- export function injectClaudeCode() {
42
- const settingsPath = join(homedir(), '.claude', 'settings.json');
43
- let settings = {};
44
- if (existsSync(settingsPath)) {
45
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { settings = {}; }
46
- } else {
47
- mkdirSync(dirname(settingsPath), { recursive: true });
48
- }
49
-
50
- if (!settings.hooks) settings.hooks = {};
51
- if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
52
-
53
- // Migrate any old-format hooks first
54
- settings.hooks.SessionEnd = migrateOldFormatHooks(settings.hooks.SessionEnd);
55
-
56
- if (hasVibeUsageHook(settings.hooks.SessionEnd)) {
57
- // Update the command in existing hook to use latest
58
- for (const group of settings.hooks.SessionEnd) {
59
- if (Array.isArray(group.hooks)) {
60
- for (const h of group.hooks) {
61
- if (h.command && h.command.includes('vibe-usage')) {
62
- h.command = SYNC_CMD;
63
- }
64
- }
65
- }
66
- }
67
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
68
- return { injected: false, reason: 'already installed (updated)' };
69
- }
70
-
71
- // New format: matcher group with hooks array
72
- settings.hooks.SessionEnd.push({
73
- hooks: [{ type: 'command', command: SYNC_CMD }],
74
- });
75
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
76
- return { injected: true };
77
- }
78
-
79
- export function injectCodex() {
80
- const configPath = join(homedir(), '.codex', 'config.toml');
81
- let content = '';
82
- if (existsSync(configPath)) {
83
- content = readFileSync(configPath, 'utf-8');
84
- } else {
85
- mkdirSync(dirname(configPath), { recursive: true });
86
- }
87
-
88
- const notifyLine = `notify = "sh -c \\"${SYNC_CMD}\\""`;
89
-
90
- if (content.includes('vibe-usage')) {
91
- // Migrate broken [[notify]] / [notify] table format and array format from previous versions
92
- // to correct string format: notify = "sh -c \"...\""
93
- content = content.replace(
94
- /^\[\[?notify\]\]?\n(?:command\s*=\s*["'][^"']*["']\n?)?/gm,
95
- notifyLine + '\n',
96
- );
97
- // Migrate array format: notify = ["sh", "-c", "..."]
98
- content = content.replace(
99
- /^notify\s*=\s*\[.*vibe-usage.*\]$/gm,
100
- notifyLine,
101
- );
102
- // Update existing string format notify = "..." to use latest command
103
- content = content.replace(
104
- /^notify\s*=\s*".*vibe-usage.*"$/gm,
105
- notifyLine,
106
- );
107
- writeFileSync(configPath, content, 'utf-8');
108
- return { injected: false, reason: 'already installed (updated)' };
109
- }
110
-
111
- // Check if any notify line already exists
112
- const hasNotify = /^notify\s*=/m.test(content);
113
- if (hasNotify) {
114
- // Replace existing notify value
115
- content = content.replace(/^notify\s*=\s*.+$/gm, notifyLine);
116
- } else {
117
- content += `\n${notifyLine}\n`;
118
- }
119
-
120
- writeFileSync(configPath, content, 'utf-8');
121
- return { injected: true };
122
- }
123
-
124
- export function injectGeminiCli() {
125
- const settingsPath = join(homedir(), '.gemini', 'settings.json');
126
- let settings = {};
127
- if (existsSync(settingsPath)) {
128
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { settings = {}; }
129
- } else {
130
- mkdirSync(dirname(settingsPath), { recursive: true });
131
- }
132
-
133
- if (!settings.hooks) settings.hooks = {};
134
- if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
135
-
136
- if (hasVibeUsageHook(settings.hooks.SessionEnd)) {
137
- return { injected: false, reason: 'already installed' };
138
- }
139
-
140
- // Gemini CLI still uses the flat format (no matcher groups)
141
- settings.hooks.SessionEnd.push({ type: 'command', command: SYNC_CMD });
142
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
143
- return { injected: true };
144
- }
145
-
146
5
  export const TOOLS = [
147
6
  {
148
7
  name: 'Claude Code',
149
8
  id: 'claude-code',
150
9
  dataDir: join(homedir(), '.claude', 'projects'),
151
- inject: injectClaudeCode,
152
10
  },
153
11
  {
154
12
  name: 'Codex CLI',
155
13
  id: 'codex',
156
14
  dataDir: join(homedir(), '.codex', 'sessions'),
157
- inject: injectCodex,
158
15
  },
159
16
  {
160
17
  name: 'Gemini CLI',
161
18
  id: 'gemini-cli',
162
19
  dataDir: join(homedir(), '.gemini', 'tmp'),
163
- inject: injectGeminiCli,
164
20
  },
165
21
  {
166
22
  name: 'OpenCode',
167
23
  id: 'opencode',
168
24
  dataDir: join(homedir(), '.local', 'share', 'opencode'),
169
- inject: null,
170
25
  },
171
26
  {
172
27
  name: 'OpenClaw',
173
28
  id: 'openclaw',
174
29
  dataDir: join(homedir(), '.openclaw', 'agents'),
175
- inject: null,
176
30
  },
177
31
  ];
178
32
 
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:');
@@ -22,8 +21,7 @@ async function showStatus() {
22
21
  console.log(' (none)\n');
23
22
  } else {
24
23
  for (const tool of detected) {
25
- const hookStatus = tool.inject ? 'auto-sync' : 'manual only';
26
- console.log(` ${tool.name} (${hookStatus})`);
24
+ console.log(` ${tool.name}`);
27
25
  }
28
26
  console.log();
29
27
  }
@@ -36,7 +34,7 @@ async function showStatus() {
36
34
  console.log();
37
35
  }
38
36
 
39
- const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl', 'lastSync'];
37
+ const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl'];
40
38
 
41
39
  function handleConfig(args) {
42
40
  const sub = args[0];
@@ -104,6 +102,11 @@ export async function run(args) {
104
102
  await runSync();
105
103
  break;
106
104
  }
105
+ case 'reset': {
106
+ const { runReset } = await import('./reset.js');
107
+ await runReset(args.slice(1));
108
+ break;
109
+ }
107
110
  case 'config': {
108
111
  handleConfig(args.slice(1));
109
112
  break;
@@ -120,8 +123,10 @@ export async function run(args) {
120
123
 
121
124
  Usage:
122
125
  npx vibe-usage Init (first run) or sync
123
- npx vibe-usage init Set up API key and hooks
126
+ npx vibe-usage init Set up API key
124
127
  npx vibe-usage sync Manually sync usage data
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
125
130
  npx vibe-usage status Show config and detected tools
126
131
  npx vibe-usage config show Show full config as JSON
127
132
  npx vibe-usage config get <key> Get a config value
package/src/init.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { platform } from 'node:os';
4
- import { existsSync } from 'node:fs';
5
4
  import { loadConfig, saveConfig } from './config.js';
6
- import { detectInstalledTools } from './hooks.js';
7
5
  import { ingest } from './api.js';
8
6
  import { runSync } from './sync.js';
7
+ import { detectInstalledTools } from './hooks.js';
9
8
 
10
9
  function prompt(question) {
11
10
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -62,34 +61,13 @@ export async function runInit() {
62
61
  const config = {
63
62
  apiKey,
64
63
  apiUrl,
65
- lastSync: existing?.lastSync || null,
66
64
  };
67
65
  saveConfig(config);
68
66
 
69
67
  const tools = detectInstalledTools();
70
- const hooked = [];
71
- const manualOnly = [];
72
-
73
- for (const tool of tools) {
74
- if (tool.inject) {
75
- try {
76
- const result = tool.inject();
77
- hooked.push(tool.name + (result.injected ? '' : ' (already installed)'));
78
- } catch (err) {
79
- console.error(` warn: Failed to inject hook for ${tool.name}: ${err.message}`);
80
- }
81
- } else {
82
- manualOnly.push(tool.name);
83
- }
84
- }
85
-
86
- if (hooked.length > 0) {
87
- console.log(`Hooks installed for: ${hooked.join(', ')}`);
88
- }
89
- for (const name of manualOnly) {
90
- console.log(`${name} detected — use \`npx @vibe-cafe/vibe-usage sync\` to sync manually.`);
91
- }
92
- if (tools.length === 0) {
68
+ if (tools.length > 0) {
69
+ console.log(`Detected tools: ${tools.map(t => t.name).join(', ')}`);
70
+ } else {
93
71
  console.log('No AI coding tools detected. Install one and re-run init.');
94
72
  }
95
73
 
@@ -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 ADDED
@@ -0,0 +1,92 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { existsSync, unlinkSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir, hostname as getHostname } from 'node:os';
5
+ import { loadConfig, saveConfig } from './config.js';
6
+ import { deleteAllData } from './api.js';
7
+ import { runSync } from './sync.js';
8
+
9
+ const STATE_FILES = [
10
+ join(homedir(), '.vibe-usage', 'claude-code-state.json'),
11
+ ];
12
+
13
+ function prompt(question) {
14
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ });
20
+ });
21
+ }
22
+
23
+ export async function runReset(args = []) {
24
+ const hostOnly = args.includes('--host');
25
+ const config = loadConfig();
26
+ if (!config?.apiKey) {
27
+ console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
28
+ process.exit(1);
29
+ }
30
+
31
+ const currentHost = getHostname();
32
+ const apiUrl = config.apiUrl || 'https://vibecafe.ai';
33
+
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}`);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ // 2. Clear local state
77
+ config.lastSync = null;
78
+ saveConfig(config);
79
+
80
+ for (const stateFile of STATE_FILES) {
81
+ if (existsSync(stateFile)) {
82
+ unlinkSync(stateFile);
83
+ }
84
+ }
85
+ console.log('Cleared local sync state.');
86
+
87
+ // 3. Re-upload everything
88
+ console.log('\nRe-syncing all data...');
89
+ await runSync();
90
+
91
+ console.log(`\nReset complete! View your dashboard at: ${apiUrl}/usage`);
92
+ }
package/src/sync.js CHANGED
@@ -1,11 +1,7 @@
1
1
  import { hostname as osHostname } from 'node:os';
2
- import { existsSync, readFileSync, unlinkSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { homedir } from 'node:os';
5
2
  import { loadConfig, saveConfig } from './config.js';
6
3
  import { ingest } from './api.js';
7
4
  import { parsers, postSyncHooks } from './parsers/index.js';
8
- import { TOOLS } from './hooks.js';
9
5
 
10
6
  const BATCH_SIZE = 100;
11
7
 
@@ -16,21 +12,23 @@ function formatBytes(bytes) {
16
12
  }
17
13
 
18
14
  export async function runSync() {
19
- // Self-heal: re-inject any missing hooks before syncing
20
- ensureHooks();
21
-
22
15
  const config = loadConfig();
23
16
  if (!config?.apiKey) {
24
17
  console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
25
18
  process.exit(1);
26
19
  }
27
20
 
28
- 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
+
29
27
  const allBuckets = [];
30
28
 
31
29
  for (const [source, parse] of Object.entries(parsers)) {
32
30
  try {
33
- const buckets = await parse(lastSync);
31
+ const buckets = await parse();
34
32
  if (buckets.length > 0) {
35
33
  allBuckets.push(...buckets);
36
34
  }
@@ -65,14 +63,12 @@ export async function runSync() {
65
63
  const result = await ingest(apiUrl, config.apiKey, batch, {
66
64
  onProgress(sent, total) {
67
65
  const pct = Math.round((sent / total) * 100);
68
- process.stdout.write(`${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\r`);
66
+ process.stdout.write(`\r${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\x1b[K`);
69
67
  },
70
68
  });
71
69
  totalIngested += result.ingested ?? batch.length;
72
70
 
73
- // Save progress after each successful batch so partial uploads survive interruptions
74
- config.lastSync = new Date().toISOString();
75
- saveConfig(config);
71
+ // State commit happens after ALL batches complete (see postSyncHooks below)
76
72
  }
77
73
 
78
74
 
@@ -93,7 +89,7 @@ export async function runSync() {
93
89
  console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
94
90
  process.exit(1);
95
91
  }
96
- // Progress already saved per-batch — report partial success
92
+ // Report partial success
97
93
  if (totalIngested > 0) {
98
94
  console.error(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
99
95
  } else {
@@ -102,39 +98,3 @@ export async function runSync() {
102
98
  process.exit(1);
103
99
  }
104
100
  }
105
-
106
- /**
107
- * Re-inject hooks for any installed tool whose hook is missing.
108
- * Runs silently — meant as a self-healing side effect of sync.
109
- */
110
- function ensureHooks() {
111
- // Skip hook injection if Vibe Usage Mac app is running
112
- const markerPath = join(homedir(), '.vibe-usage', 'mac-app-active');
113
- if (existsSync(markerPath)) {
114
- try {
115
- const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
116
- if (marker.pid) {
117
- try {
118
- process.kill(marker.pid, 0);
119
- return;
120
- } catch {
121
- try { unlinkSync(markerPath); } catch { /* ignore */ }
122
- }
123
- }
124
- } catch {
125
- // Malformed marker file — ignore
126
- }
127
- }
128
-
129
- for (const tool of TOOLS) {
130
- if (!tool.inject) continue;
131
- try {
132
- const result = tool.inject();
133
- if (result.injected) {
134
- process.stderr.write(`hook: re-installed ${tool.name} hook\n`);
135
- }
136
- } catch {
137
- // ignore — best effort
138
- }
139
- }
140
- }