@vibe-cafe/vibe-usage 0.2.6 → 0.2.7
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 +10 -9
- package/package.json +1 -1
- package/src/api.js +49 -0
- package/src/hooks.js +2 -148
- package/src/index.js +8 -3
- package/src/init.js +4 -25
- package/src/reset.js +68 -0
- package/src/sync.js +1 -44
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.
|
|
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 |
|
|
29
|
-
|
|
30
|
-
| Claude Code |
|
|
31
|
-
| Codex CLI |
|
|
32
|
-
| Gemini CLI |
|
|
33
|
-
| OpenCode |
|
|
34
|
-
| OpenClaw |
|
|
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
package/src/api.js
CHANGED
|
@@ -100,3 +100,52 @@ function _send(apiUrl, apiKey, buckets, onProgress) {
|
|
|
100
100
|
writeNext();
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
|
|
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)
|
|
108
|
+
* @returns {Promise<{deleted: number}>}
|
|
109
|
+
*/
|
|
110
|
+
export function deleteAllData(apiUrl, apiKey) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const url = new URL('/api/usage/ingest', apiUrl);
|
|
113
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
114
|
+
|
|
115
|
+
const req = mod.request(url, {
|
|
116
|
+
method: 'DELETE',
|
|
117
|
+
timeout: 60_000,
|
|
118
|
+
headers: {
|
|
119
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
120
|
+
},
|
|
121
|
+
}, (res) => {
|
|
122
|
+
let data = '';
|
|
123
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
124
|
+
res.on('end', () => {
|
|
125
|
+
if (res.statusCode === 401) {
|
|
126
|
+
reject(new Error('UNAUTHORIZED'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
130
|
+
const err = new Error(`HTTP ${res.statusCode}: ${data}`);
|
|
131
|
+
err.statusCode = res.statusCode;
|
|
132
|
+
reject(err);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
resolve(JSON.parse(data));
|
|
137
|
+
} catch {
|
|
138
|
+
reject(new Error(`Invalid JSON response: ${data}`));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
req.on('error', (err) => reject(err));
|
|
144
|
+
req.on('timeout', () => {
|
|
145
|
+
req.destroy();
|
|
146
|
+
reject(new Error('Request timed out (60s)'));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
req.end();
|
|
150
|
+
});
|
|
151
|
+
}
|
package/src/hooks.js
CHANGED
|
@@ -1,178 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join
|
|
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
|
@@ -22,8 +22,7 @@ async function showStatus() {
|
|
|
22
22
|
console.log(' (none)\n');
|
|
23
23
|
} else {
|
|
24
24
|
for (const tool of detected) {
|
|
25
|
-
|
|
26
|
-
console.log(` ${tool.name} (${hookStatus})`);
|
|
25
|
+
console.log(` ${tool.name}`);
|
|
27
26
|
}
|
|
28
27
|
console.log();
|
|
29
28
|
}
|
|
@@ -104,6 +103,11 @@ export async function run(args) {
|
|
|
104
103
|
await runSync();
|
|
105
104
|
break;
|
|
106
105
|
}
|
|
106
|
+
case 'reset': {
|
|
107
|
+
const { runReset } = await import('./reset.js');
|
|
108
|
+
await runReset();
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
107
111
|
case 'config': {
|
|
108
112
|
handleConfig(args.slice(1));
|
|
109
113
|
break;
|
|
@@ -120,8 +124,9 @@ export async function run(args) {
|
|
|
120
124
|
|
|
121
125
|
Usage:
|
|
122
126
|
npx vibe-usage Init (first run) or sync
|
|
123
|
-
npx vibe-usage init Set up API key
|
|
127
|
+
npx vibe-usage init Set up API key
|
|
124
128
|
npx vibe-usage sync Manually sync usage data
|
|
129
|
+
npx vibe-usage reset Delete all data 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 });
|
|
@@ -67,29 +66,9 @@ export async function runInit() {
|
|
|
67
66
|
saveConfig(config);
|
|
68
67
|
|
|
69
68
|
const tools = detectInstalledTools();
|
|
70
|
-
|
|
71
|
-
|
|
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) {
|
|
69
|
+
if (tools.length > 0) {
|
|
70
|
+
console.log(`Detected tools: ${tools.map(t => t.name).join(', ')}`);
|
|
71
|
+
} else {
|
|
93
72
|
console.log('No AI coding tools detected. Install one and re-run init.');
|
|
94
73
|
}
|
|
95
74
|
|
package/src/reset.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } 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() {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
if (!config?.apiKey) {
|
|
26
|
+
console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
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
|
+
|
|
36
|
+
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
37
|
+
|
|
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.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.error(`Failed to delete remote data: ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Clear local state
|
|
53
|
+
config.lastSync = null;
|
|
54
|
+
saveConfig(config);
|
|
55
|
+
|
|
56
|
+
for (const stateFile of STATE_FILES) {
|
|
57
|
+
if (existsSync(stateFile)) {
|
|
58
|
+
unlinkSync(stateFile);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log('Cleared local sync state.');
|
|
62
|
+
|
|
63
|
+
// 3. Re-upload everything
|
|
64
|
+
console.log('\nRe-syncing all data...');
|
|
65
|
+
await runSync();
|
|
66
|
+
|
|
67
|
+
console.log(`\nReset complete! View your dashboard at: ${apiUrl}/usage`);
|
|
68
|
+
}
|
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,9 +12,6 @@ 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.');
|
|
@@ -65,7 +58,7 @@ export async function runSync() {
|
|
|
65
58
|
const result = await ingest(apiUrl, config.apiKey, batch, {
|
|
66
59
|
onProgress(sent, total) {
|
|
67
60
|
const pct = Math.round((sent / total) * 100);
|
|
68
|
-
process.stdout.write(
|
|
61
|
+
process.stdout.write(`\r${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\x1b[K`);
|
|
69
62
|
},
|
|
70
63
|
});
|
|
71
64
|
totalIngested += result.ingested ?? batch.length;
|
|
@@ -102,39 +95,3 @@ export async function runSync() {
|
|
|
102
95
|
process.exit(1);
|
|
103
96
|
}
|
|
104
97
|
}
|
|
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
|
-
}
|