@vibe-cafe/vibe-usage 0.7.5 → 0.7.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 +12 -2
- package/package.json +1 -1
- package/src/daemon-service.js +35 -34
- package/src/daemon.js +6 -5
- package/src/index.js +35 -5
- package/src/init.js +59 -21
- package/src/output.js +67 -0
- package/src/reset.js +20 -24
- package/src/skill.js +13 -12
- package/src/sync.js +30 -34
package/README.md
CHANGED
|
@@ -4,19 +4,29 @@ Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe
|
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
+
Get your API key at [vibecafe.ai/usage](https://vibecafe.ai/usage), then copy the one-liner shown there:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @vibe-cafe/vibe-usage --key vbu_xxxxxxxxxxxx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run without a key and paste it interactively:
|
|
14
|
+
|
|
7
15
|
```bash
|
|
8
16
|
npx @vibe-cafe/vibe-usage
|
|
9
17
|
```
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
1.
|
|
19
|
+
Either path will:
|
|
20
|
+
1. Save your API key to `~/.vibe-usage/config.json`
|
|
13
21
|
2. Detect installed AI coding tools
|
|
14
22
|
3. Run an initial sync of your usage data
|
|
23
|
+
4. Prompt you to enable the background daemon for continuous syncing (recommended)
|
|
15
24
|
|
|
16
25
|
## Commands
|
|
17
26
|
|
|
18
27
|
```bash
|
|
19
28
|
npx @vibe-cafe/vibe-usage # Init (first run) or sync (subsequent runs)
|
|
29
|
+
npx @vibe-cafe/vibe-usage --key <vbu_...> # One-shot init with a pre-copied key
|
|
20
30
|
npx @vibe-cafe/vibe-usage init # Re-run setup
|
|
21
31
|
npx @vibe-cafe/vibe-usage sync # Manual sync
|
|
22
32
|
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 30m, foreground)
|
package/package.json
CHANGED
package/src/daemon-service.js
CHANGED
|
@@ -3,6 +3,7 @@ import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir, platform } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { success, failure, warn, dim } from './output.js';
|
|
6
7
|
|
|
7
8
|
const SERVICE_NAME = 'vibe-usage';
|
|
8
9
|
const LAUNCHD_LABEL = 'ai.vibecafe.vibe-usage';
|
|
@@ -109,24 +110,23 @@ function run(cmd, args) {
|
|
|
109
110
|
function install() {
|
|
110
111
|
const plat = detectPlatform();
|
|
111
112
|
if (!plat) {
|
|
112
|
-
console.log('
|
|
113
|
-
console.log('
|
|
113
|
+
console.log(failure('当前平台不支持 daemon。'));
|
|
114
|
+
console.log(dim(' 支持: Linux (systemd) / macOS (launchd)'));
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
const { nodePath, binPath, isNpxCache } = resolvePaths();
|
|
118
119
|
|
|
119
120
|
if (isNpxCache) {
|
|
120
|
-
console.log('
|
|
121
|
-
console.log('
|
|
122
|
-
console.log(
|
|
123
|
-
console.log(' npm install -g @vibe-cafe/vibe-usage\n');
|
|
121
|
+
console.log(warn('检测到从 npx 缓存运行 vibe-usage,缓存清理后 daemon 会失效。'));
|
|
122
|
+
console.log(dim(' 建议先全局安装: npm install -g @vibe-cafe/vibe-usage'));
|
|
123
|
+
console.log();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const paths = getServicePaths(plat);
|
|
127
127
|
|
|
128
128
|
if (existsSync(paths.file)) {
|
|
129
|
-
console.log('
|
|
129
|
+
console.log(warn('Daemon 已安装,运行 `vibe-usage daemon restart` 或 `uninstall` 先处理。'));
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -134,45 +134,46 @@ function install() {
|
|
|
134
134
|
|
|
135
135
|
if (plat === 'systemd') {
|
|
136
136
|
writeFileSync(paths.file, generateSystemdUnit(nodePath, binPath), 'utf-8');
|
|
137
|
-
console.log(`
|
|
137
|
+
console.log(dim(` 已写入 ${paths.file}`));
|
|
138
138
|
|
|
139
139
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
140
140
|
const result = run('systemctl', ['--user', 'enable', '--now', `${SERVICE_NAME}.service`]);
|
|
141
141
|
if (!result.ok) {
|
|
142
|
-
console.error(
|
|
142
|
+
console.error(failure(`启动服务失败: ${result.output}`));
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
-
console.log('
|
|
145
|
+
console.log(success('服务已启用并启动。'));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (plat === 'launchd') {
|
|
149
149
|
mkdirSync(join(homedir(), '.vibe-usage'), { recursive: true });
|
|
150
150
|
writeFileSync(paths.file, generateLaunchdPlist(nodePath, binPath), 'utf-8');
|
|
151
|
-
console.log(`
|
|
151
|
+
console.log(dim(` 已写入 ${paths.file}`));
|
|
152
152
|
|
|
153
153
|
const result = run('launchctl', ['load', paths.file]);
|
|
154
154
|
if (!result.ok) {
|
|
155
|
-
console.error(
|
|
155
|
+
console.error(failure(`加载服务失败: ${result.output}`));
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
|
-
console.log('
|
|
158
|
+
console.log(success('服务已加载并启动。'));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
console.log(
|
|
162
|
-
console.log('
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(success('Daemon 已安装,用量数据将每 30 分钟自动同步。'));
|
|
163
|
+
console.log(dim(' 运行 `vibe-usage daemon status` 查看状态。'));
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function uninstall() {
|
|
166
167
|
const plat = detectPlatform();
|
|
167
168
|
if (!plat) {
|
|
168
|
-
console.log('
|
|
169
|
+
console.log(failure('未检测到支持的服务平台。'));
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
const paths = getServicePaths(plat);
|
|
173
174
|
|
|
174
175
|
if (!existsSync(paths.file)) {
|
|
175
|
-
console.log('
|
|
176
|
+
console.log(dim('未安装 daemon 服务。'));
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
179
|
|
|
@@ -181,43 +182,43 @@ function uninstall() {
|
|
|
181
182
|
run('systemctl', ['--user', 'disable', `${SERVICE_NAME}.service`]);
|
|
182
183
|
unlinkSync(paths.file);
|
|
183
184
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
184
|
-
console.log('
|
|
185
|
+
console.log(success('服务已停止、禁用并删除。'));
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
if (plat === 'launchd') {
|
|
188
189
|
run('launchctl', ['unload', paths.file]);
|
|
189
190
|
unlinkSync(paths.file);
|
|
190
|
-
console.log('
|
|
191
|
+
console.log(success('服务已卸载并删除。'));
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
function status() {
|
|
195
196
|
const plat = detectPlatform();
|
|
196
197
|
if (!plat) {
|
|
197
|
-
console.log('
|
|
198
|
+
console.log(failure('未检测到支持的服务平台。'));
|
|
198
199
|
return;
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
const paths = getServicePaths(plat);
|
|
202
203
|
|
|
203
204
|
if (!existsSync(paths.file)) {
|
|
204
|
-
console.log('
|
|
205
|
-
console.log('
|
|
205
|
+
console.log(dim('未安装 daemon 服务。'));
|
|
206
|
+
console.log(dim(' 运行 `vibe-usage daemon install` 安装。'));
|
|
206
207
|
return;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
if (plat === 'systemd') {
|
|
210
211
|
const result = run('systemctl', ['--user', 'status', `${SERVICE_NAME}.service`]);
|
|
211
|
-
console.log(result.output);
|
|
212
|
+
console.log(dim(result.output));
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
if (plat === 'launchd') {
|
|
215
216
|
const result = run('launchctl', ['list', LAUNCHD_LABEL]);
|
|
216
217
|
if (result.ok) {
|
|
217
|
-
console.log(`Service: ${LAUNCHD_LABEL}`);
|
|
218
|
-
console.log(result.output);
|
|
218
|
+
console.log(dim(`Service: ${LAUNCHD_LABEL}`));
|
|
219
|
+
console.log(dim(result.output));
|
|
219
220
|
} else {
|
|
220
|
-
console.log('
|
|
221
|
+
console.log(warn('服务已安装但当前未运行。'));
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
}
|
|
@@ -225,37 +226,37 @@ function status() {
|
|
|
225
226
|
function stop() {
|
|
226
227
|
const plat = detectPlatform();
|
|
227
228
|
if (!plat) {
|
|
228
|
-
console.log('
|
|
229
|
+
console.log(failure('未检测到支持的服务平台。'));
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
if (plat === 'systemd') {
|
|
233
234
|
const result = run('systemctl', ['--user', 'stop', `${SERVICE_NAME}.service`]);
|
|
234
|
-
console.log(result.ok ? '
|
|
235
|
+
console.log(result.ok ? success('服务已停止。') : failure(`停止失败: ${result.output}`));
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
if (plat === 'launchd') {
|
|
238
239
|
const result = run('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
239
|
-
console.log(result.ok ? '
|
|
240
|
+
console.log(result.ok ? success('服务已停止。') : failure(`停止失败: ${result.output}`));
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
function restart() {
|
|
244
245
|
const plat = detectPlatform();
|
|
245
246
|
if (!plat) {
|
|
246
|
-
console.log('
|
|
247
|
+
console.log(failure('未检测到支持的服务平台。'));
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
if (plat === 'systemd') {
|
|
251
252
|
const result = run('systemctl', ['--user', 'restart', `${SERVICE_NAME}.service`]);
|
|
252
|
-
console.log(result.ok ? '
|
|
253
|
+
console.log(result.ok ? success('服务已重启。') : failure(`重启失败: ${result.output}`));
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
if (plat === 'launchd') {
|
|
256
257
|
run('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
257
258
|
const result = run('launchctl', ['start', LAUNCHD_LABEL]);
|
|
258
|
-
console.log(result.ok ? '
|
|
259
|
+
console.log(result.ok ? success('服务已重启。') : failure(`重启失败: ${result.output}`));
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
|
|
@@ -264,8 +265,8 @@ const SUBCOMMANDS = { install, uninstall, status, stop, restart };
|
|
|
264
265
|
export async function manageDaemon(subcommand) {
|
|
265
266
|
const fn = SUBCOMMANDS[subcommand];
|
|
266
267
|
if (!fn) {
|
|
267
|
-
console.error(
|
|
268
|
-
console.error('
|
|
268
|
+
console.error(failure(`未知 daemon 子命令: ${subcommand}`));
|
|
269
|
+
console.error(dim(' 用法: vibe-usage daemon <install|uninstall|status|stop|restart>'));
|
|
269
270
|
process.exit(1);
|
|
270
271
|
}
|
|
271
272
|
fn();
|
package/src/daemon.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { loadConfig } from './config.js';
|
|
2
2
|
import { runSync } from './sync.js';
|
|
3
|
+
import { failure, dim } from './output.js';
|
|
3
4
|
|
|
4
5
|
const INTERVAL = 30 * 60_000; // 30 minutes
|
|
5
6
|
|
|
6
7
|
function log(msg) {
|
|
7
8
|
const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
8
|
-
process.stdout.write(`[${ts}] ${msg}\n`);
|
|
9
|
+
process.stdout.write(dim(`[${ts}] ${msg}\n`));
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
function sleep(ms) {
|
|
@@ -15,11 +16,11 @@ function sleep(ms) {
|
|
|
15
16
|
export async function runDaemon() {
|
|
16
17
|
const config = loadConfig();
|
|
17
18
|
if (!config?.apiKey) {
|
|
18
|
-
console.error('
|
|
19
|
+
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
log('
|
|
23
|
+
log('daemon started (sync every 30m, Ctrl+C to stop)');
|
|
23
24
|
|
|
24
25
|
// eslint-disable-next-line no-constant-condition
|
|
25
26
|
while (true) {
|
|
@@ -27,10 +28,10 @@ export async function runDaemon() {
|
|
|
27
28
|
await runSync({ throws: true, quiet: true });
|
|
28
29
|
} catch (err) {
|
|
29
30
|
if (err.message === 'UNAUTHORIZED') {
|
|
30
|
-
log('API key invalid
|
|
31
|
+
log('API key invalid, exiting.');
|
|
31
32
|
process.exit(1);
|
|
32
33
|
}
|
|
33
|
-
log(`
|
|
34
|
+
log(`sync error: ${err.message}`);
|
|
34
35
|
}
|
|
35
36
|
await sleep(INTERVAL);
|
|
36
37
|
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { loadConfig, saveConfig, getConfigPath } from './config.js';
|
|
2
2
|
import { detectInstalledTools, TOOLS } from './tools.js';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { smallHeader } from './output.js';
|
|
5
|
+
|
|
6
|
+
function printSmallHeader() {
|
|
7
|
+
console.log();
|
|
8
|
+
console.log(smallHeader());
|
|
9
|
+
console.log();
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
async function showStatus() {
|
|
6
13
|
const config = loadConfig();
|
|
@@ -88,21 +95,36 @@ function handleConfig(args) {
|
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
function extractOption(args, name) {
|
|
99
|
+
const flag = `--${name}`;
|
|
100
|
+
const idx = args.findIndex(a => a === flag);
|
|
101
|
+
if (idx === -1) return { args, value: undefined };
|
|
102
|
+
const value = args[idx + 1];
|
|
103
|
+
if (value === undefined || value.startsWith('--')) {
|
|
104
|
+
console.error(`Option ${flag} requires a value.`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
return { args: [...args.slice(0, idx), ...args.slice(idx + 2)], value };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function run(rawArgs) {
|
|
111
|
+
const { args, value: apiKey } = extractOption(rawArgs, 'key');
|
|
92
112
|
const command = args[0];
|
|
93
113
|
|
|
94
114
|
switch (command) {
|
|
95
115
|
case 'init': {
|
|
96
116
|
const { runInit } = await import('./init.js');
|
|
97
|
-
await runInit();
|
|
117
|
+
await runInit({ apiKey });
|
|
98
118
|
break;
|
|
99
119
|
}
|
|
100
120
|
case 'sync': {
|
|
121
|
+
printSmallHeader();
|
|
101
122
|
const { runSync } = await import('./sync.js');
|
|
102
123
|
await runSync();
|
|
103
124
|
break;
|
|
104
125
|
}
|
|
105
126
|
case 'reset': {
|
|
127
|
+
printSmallHeader();
|
|
106
128
|
const { runReset } = await import('./reset.js');
|
|
107
129
|
await runReset(args.slice(1));
|
|
108
130
|
break;
|
|
@@ -111,15 +133,18 @@ export async function run(args) {
|
|
|
111
133
|
case '--daemon': {
|
|
112
134
|
const sub = args[1];
|
|
113
135
|
if (sub && ['install', 'uninstall', 'status', 'stop', 'restart'].includes(sub)) {
|
|
136
|
+
printSmallHeader();
|
|
114
137
|
const { manageDaemon } = await import('./daemon-service.js');
|
|
115
138
|
await manageDaemon(sub);
|
|
116
139
|
} else {
|
|
140
|
+
// Foreground daemon loop — no header, just start syncing
|
|
117
141
|
const { runDaemon } = await import('./daemon.js');
|
|
118
142
|
await runDaemon();
|
|
119
143
|
}
|
|
120
144
|
break;
|
|
121
145
|
}
|
|
122
146
|
case 'skill': {
|
|
147
|
+
printSmallHeader();
|
|
123
148
|
const { runSkill } = await import('./skill.js');
|
|
124
149
|
await runSkill(args.slice(1));
|
|
125
150
|
break;
|
|
@@ -140,7 +165,9 @@ export async function run(args) {
|
|
|
140
165
|
|
|
141
166
|
Usage:
|
|
142
167
|
npx @vibe-cafe/vibe-usage Init (first run) or sync
|
|
143
|
-
npx @vibe-cafe/vibe-usage init
|
|
168
|
+
npx @vibe-cafe/vibe-usage --key <vbu_...> One-shot init with a pre-copied key
|
|
169
|
+
npx @vibe-cafe/vibe-usage init Set up API key (interactive)
|
|
170
|
+
npx @vibe-cafe/vibe-usage init --key <vbu_...> Init with key, skip paste prompt
|
|
144
171
|
npx @vibe-cafe/vibe-usage sync Manually sync usage data
|
|
145
172
|
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 30m, foreground)
|
|
146
173
|
npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd)
|
|
@@ -162,10 +189,13 @@ export async function run(args) {
|
|
|
162
189
|
}
|
|
163
190
|
default: {
|
|
164
191
|
const config = loadConfig();
|
|
165
|
-
if (!config?.apiKey) {
|
|
192
|
+
if (!config?.apiKey || apiKey) {
|
|
193
|
+
// First run OR user passed --key for a one-shot setup — init.js prints the big header
|
|
166
194
|
const { runInit } = await import('./init.js');
|
|
167
|
-
await runInit();
|
|
195
|
+
await runInit({ apiKey });
|
|
168
196
|
} else {
|
|
197
|
+
// Already configured: small header + sync
|
|
198
|
+
printSmallHeader();
|
|
169
199
|
const { runSync } = await import('./sync.js');
|
|
170
200
|
await runSync();
|
|
171
201
|
}
|
package/src/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadConfig, saveConfig } from './config.js';
|
|
|
5
5
|
import { ingest } from './api.js';
|
|
6
6
|
import { runSync } from './sync.js';
|
|
7
7
|
import { detectInstalledTools } from './tools.js';
|
|
8
|
+
import { bigHeader, success, failure, warn, arrow, link, dim, divider } from './output.js';
|
|
8
9
|
|
|
9
10
|
function prompt(question) {
|
|
10
11
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -23,39 +24,61 @@ function openBrowser(url) {
|
|
|
23
24
|
execFile(cmd, [url], () => {});
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
function isDaemonPlatform() {
|
|
28
|
+
return process.platform === 'linux' || process.platform === 'darwin';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runInit(options = {}) {
|
|
32
|
+
const { apiKey: providedKey } = options;
|
|
33
|
+
|
|
34
|
+
console.log(bigHeader());
|
|
28
35
|
|
|
29
36
|
const existing = loadConfig();
|
|
30
37
|
if (existing?.apiKey) {
|
|
31
|
-
|
|
38
|
+
if (providedKey && existing.apiKey === providedKey) {
|
|
39
|
+
console.log(dim('已配置同一个 Key,直接同步数据。'));
|
|
40
|
+
console.log();
|
|
41
|
+
await runSync();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const answer = await prompt('检测到已有配置,是否覆盖? (y/N) ');
|
|
32
45
|
if (answer.toLowerCase() !== 'y') {
|
|
33
|
-
console.log('
|
|
46
|
+
console.log(dim('已取消。'));
|
|
34
47
|
return;
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
const apiUrl = process.env.VIBE_USAGE_API_URL || 'https://vibecafe.ai';
|
|
39
|
-
console.log(`Get your API key at: ${apiUrl}/usage/setup\n`);
|
|
40
|
-
openBrowser(`${apiUrl}/usage/setup`);
|
|
41
52
|
|
|
42
53
|
let apiKey;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (providedKey) {
|
|
55
|
+
if (!providedKey.startsWith('vbu_')) {
|
|
56
|
+
console.error(failure('API Key 无效,必须以 vbu_ 开头。'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
apiKey = providedKey;
|
|
60
|
+
} else {
|
|
61
|
+
console.log(`${arrow('获取 API Key')} ${link(`${apiUrl}/usage`)}`);
|
|
62
|
+
console.log(dim(' 浏览器会自动打开,登录后复制 Key 粘贴到下方。'));
|
|
63
|
+
console.log();
|
|
64
|
+
openBrowser(`${apiUrl}/usage`);
|
|
65
|
+
|
|
66
|
+
while (true) {
|
|
67
|
+
apiKey = await prompt('粘贴 API Key: ');
|
|
68
|
+
if (apiKey.startsWith('vbu_')) break;
|
|
69
|
+
console.log(warn('必须以 vbu_ 开头,请重试。'));
|
|
70
|
+
}
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
console.log(`\nVerifying key ${apiKey.slice(0, 8)}...`);
|
|
50
73
|
try {
|
|
51
74
|
await ingest(apiUrl, apiKey, []);
|
|
52
|
-
console.log(
|
|
75
|
+
console.log(success(`验证通过 ${dim(apiKey.slice(0, 12) + '...')}`));
|
|
53
76
|
} catch (err) {
|
|
54
77
|
if (err.message === 'UNAUTHORIZED') {
|
|
55
|
-
console.error('
|
|
78
|
+
console.error(failure('API Key 无效,请检查后重试。'));
|
|
56
79
|
process.exit(1);
|
|
57
80
|
}
|
|
58
|
-
console.log(
|
|
81
|
+
console.log(warn(`网络异常(${err.message}),跳过验证直接保存。`));
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
const config = {
|
|
@@ -67,17 +90,32 @@ export async function runInit() {
|
|
|
67
90
|
|
|
68
91
|
const tools = detectInstalledTools();
|
|
69
92
|
if (tools.length > 0) {
|
|
70
|
-
console.log(
|
|
93
|
+
console.log(success(`检测到 ${tools.length} 款工具: ${dim(tools.map(t => t.name).join(' · '))}`));
|
|
71
94
|
} else {
|
|
72
|
-
console.log('
|
|
95
|
+
console.log(warn('未检测到 AI 编码工具,安装后重新运行即可。'));
|
|
73
96
|
}
|
|
74
97
|
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(divider());
|
|
100
|
+
console.log();
|
|
77
101
|
|
|
78
|
-
|
|
102
|
+
await runSync();
|
|
79
103
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
104
|
+
if (isDaemonPlatform()) {
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
console.log();
|
|
107
|
+
const answer = await prompt(`开启后台自动同步?${dim('(推荐)')} [Y/n] `);
|
|
108
|
+
const normalized = answer.toLowerCase();
|
|
109
|
+
if (normalized === '' || normalized === 'y' || normalized === 'yes') {
|
|
110
|
+
const { manageDaemon } = await import('./daemon-service.js');
|
|
111
|
+
await manageDaemon('install');
|
|
112
|
+
} else {
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(dim('随时运行 `npx @vibe-cafe/vibe-usage daemon install` 开启后台同步。'));
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(dim('提示: 运行 `npx @vibe-cafe/vibe-usage daemon install` 开启后台自动同步。'));
|
|
119
|
+
}
|
|
82
120
|
}
|
|
83
121
|
}
|
package/src/output.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Terminal output helpers: colors, status markers, OSC 8 clickable links.
|
|
2
|
+
// Falls back to plain text when NO_COLOR is set or stdout is not a TTY.
|
|
3
|
+
|
|
4
|
+
const NO_COLOR = !!process.env.NO_COLOR || !process.stdout.isTTY;
|
|
5
|
+
|
|
6
|
+
const CODES = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
underline: '\x1b[4m',
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
gray: '\x1b[90m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function wrap(code, text) {
|
|
19
|
+
if (NO_COLOR) return String(text);
|
|
20
|
+
return `${code}${text}${CODES.reset}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const bold = (t) => wrap(CODES.bold, t);
|
|
24
|
+
export const dim = (t) => wrap(CODES.dim, t);
|
|
25
|
+
export const red = (t) => wrap(CODES.red, t);
|
|
26
|
+
export const green = (t) => wrap(CODES.green, t);
|
|
27
|
+
export const yellow = (t) => wrap(CODES.yellow, t);
|
|
28
|
+
export const cyan = (t) => wrap(CODES.cyan, t);
|
|
29
|
+
export const gray = (t) => wrap(CODES.gray, t);
|
|
30
|
+
|
|
31
|
+
/** OSC 8 hyperlink — supported by modern terminals (iTerm2, Kitty, Warp, VSCode, macOS Terminal 14+). */
|
|
32
|
+
export function link(url, text = url) {
|
|
33
|
+
if (NO_COLOR) return url === text ? url : `${text} (${url})`;
|
|
34
|
+
return `\x1b]8;;${url}\x1b\\${CODES.cyan}${CODES.underline}${text}${CODES.reset}\x1b]8;;\x1b\\`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const success = (msg) => `${green('✓')} ${msg}`;
|
|
38
|
+
export const failure = (msg) => `${red('✗')} ${msg}`;
|
|
39
|
+
export const warn = (msg) => `${yellow('!')} ${msg}`;
|
|
40
|
+
export const arrow = (msg) => `${cyan('→')} ${msg}`;
|
|
41
|
+
|
|
42
|
+
export const divider = () => dim('─'.repeat(48));
|
|
43
|
+
|
|
44
|
+
/** Print a blank line. */
|
|
45
|
+
export const nl = () => console.log();
|
|
46
|
+
|
|
47
|
+
const LOGO_LINES = [
|
|
48
|
+
'██╗ ██╗██╗██████╗ ███████╗ ██╗ ██╗███████╗ █████╗ ██████╗ ███████╗',
|
|
49
|
+
'██║ ██║██║██╔══██╗██╔════╝ ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝',
|
|
50
|
+
'██║ ██║██║██████╔╝█████╗ ██║ ██║███████╗███████║██║ ███╗█████╗ ',
|
|
51
|
+
'╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██║╚════██║██╔══██║██║ ██║██╔══╝ ',
|
|
52
|
+
' ╚████╔╝ ██║██████╔╝███████╗ ╚██████╔╝███████║██║ ██║╚██████╔╝███████╗',
|
|
53
|
+
' ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Big ASCII logo — used once at the top of `init` / first-run. */
|
|
57
|
+
export function bigHeader() {
|
|
58
|
+
const logo = NO_COLOR
|
|
59
|
+
? LOGO_LINES.join('\n')
|
|
60
|
+
: LOGO_LINES.map(l => `${CODES.cyan}${l}${CODES.reset}`).join('\n');
|
|
61
|
+
return `\n${logo}\n${dim(' Vibe Usage · by VibeCafé')}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Compact one-line header — used for `sync`, `daemon`, `reset`, `skill`. */
|
|
65
|
+
export function smallHeader() {
|
|
66
|
+
return `${bold('Vibe Usage')} ${dim('· by VibeCafé')}`;
|
|
67
|
+
}
|
package/src/reset.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
-
import {
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { homedir, hostname as getHostname } from 'node:os';
|
|
2
|
+
import { hostname as getHostname } from 'node:os';
|
|
5
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
6
4
|
import { deleteAllData } from './api.js';
|
|
7
5
|
import { runSync } from './sync.js';
|
|
8
|
-
|
|
6
|
+
import { success, failure, arrow, link, dim } from './output.js';
|
|
9
7
|
|
|
10
8
|
function prompt(question) {
|
|
11
9
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -21,7 +19,7 @@ export async function runReset(args = []) {
|
|
|
21
19
|
const hostOnly = args.includes('--local');
|
|
22
20
|
const config = loadConfig();
|
|
23
21
|
if (!config?.apiKey) {
|
|
24
|
-
console.error('
|
|
22
|
+
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
|
|
25
23
|
process.exit(1);
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -29,55 +27,53 @@ export async function runReset(args = []) {
|
|
|
29
27
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
30
28
|
|
|
31
29
|
if (hostOnly) {
|
|
32
|
-
const answer = await prompt(
|
|
30
|
+
const answer = await prompt(`将删除当前机器(${currentHost})的用量数据并从本地日志重新上传,继续? (y/N) `);
|
|
33
31
|
if (answer.toLowerCase() !== 'y') {
|
|
34
|
-
console.log('
|
|
32
|
+
console.log(dim('已取消。'));
|
|
35
33
|
return;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
console.log(`Deleting remote data for host: ${currentHost}...`);
|
|
36
|
+
console.log(dim(` 正在删除 ${currentHost} 的云端数据...`));
|
|
40
37
|
try {
|
|
41
38
|
const result = await deleteAllData(apiUrl, config.apiKey, { hostname: currentHost });
|
|
42
|
-
console.log(
|
|
39
|
+
console.log(success(`已删除 ${result.deleted} buckets · ${result.sessions ?? 0} sessions`));
|
|
43
40
|
} catch (err) {
|
|
44
41
|
if (err.message === 'UNAUTHORIZED') {
|
|
45
|
-
console.error('
|
|
42
|
+
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
46
43
|
process.exit(1);
|
|
47
44
|
}
|
|
48
|
-
console.error(
|
|
45
|
+
console.error(failure(`删除云端数据失败: ${err.message}`));
|
|
49
46
|
process.exit(1);
|
|
50
47
|
}
|
|
51
48
|
} else {
|
|
52
|
-
const answer = await prompt('
|
|
49
|
+
const answer = await prompt('将删除所有用量数据并从本地日志重新上传,继续? (y/N) ');
|
|
53
50
|
if (answer.toLowerCase() !== 'y') {
|
|
54
|
-
console.log('
|
|
51
|
+
console.log(dim('已取消。'));
|
|
55
52
|
return;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
console.log('Deleting all remote data...');
|
|
55
|
+
console.log(dim(' 正在删除所有云端数据...'));
|
|
60
56
|
try {
|
|
61
57
|
const result = await deleteAllData(apiUrl, config.apiKey);
|
|
62
|
-
console.log(
|
|
58
|
+
console.log(success(`已删除 ${result.deleted} buckets · ${result.sessions ?? 0} sessions`));
|
|
63
59
|
} catch (err) {
|
|
64
60
|
if (err.message === 'UNAUTHORIZED') {
|
|
65
|
-
console.error('
|
|
61
|
+
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
66
62
|
process.exit(1);
|
|
67
63
|
}
|
|
68
|
-
console.error(
|
|
64
|
+
console.error(failure(`删除云端数据失败: ${err.message}`));
|
|
69
65
|
process.exit(1);
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
//
|
|
69
|
+
// Clear local state (legacy — no state files needed for current parsers)
|
|
74
70
|
config.lastSync = null;
|
|
75
71
|
saveConfig(config);
|
|
76
|
-
console.log('Cleared local sync state.');
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
console.log('
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(dim(' 从本地日志重新同步...'));
|
|
80
75
|
await runSync();
|
|
81
76
|
|
|
82
|
-
console.log(
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(`${arrow('Dashboard')} ${link(`${apiUrl}/usage`)}`);
|
|
83
79
|
}
|
package/src/skill.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { success, dim, green, red } from './output.js';
|
|
4
5
|
|
|
5
6
|
const SKILL_TARGETS = [
|
|
6
7
|
{
|
|
@@ -84,19 +85,18 @@ Other available commands:
|
|
|
84
85
|
export async function runSkill(args = []) {
|
|
85
86
|
const remove = args.includes('--remove');
|
|
86
87
|
|
|
87
|
-
console.log('
|
|
88
|
-
|
|
89
|
-
console.log(' AI coding tools:');
|
|
88
|
+
console.log(' 检测到的工具:');
|
|
90
89
|
for (const t of SKILL_TARGETS) {
|
|
91
90
|
const found = existsSync(t.detectDir);
|
|
92
|
-
|
|
91
|
+
const mark = found ? green('✓') : red('✗');
|
|
92
|
+
console.log(` ${mark} ${t.name}`);
|
|
93
93
|
}
|
|
94
94
|
console.log();
|
|
95
95
|
|
|
96
96
|
const detected = SKILL_TARGETS.filter(t => existsSync(t.detectDir));
|
|
97
97
|
|
|
98
98
|
if (detected.length === 0) {
|
|
99
|
-
console.log('
|
|
99
|
+
console.log(dim(' 未检测到支持的工具,无需安装 Skill。'));
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -107,14 +107,15 @@ export async function runSkill(args = []) {
|
|
|
107
107
|
if (existsSync(skillFile)) {
|
|
108
108
|
unlinkSync(skillFile);
|
|
109
109
|
try { rmdirSync(t.skillDir); } catch {}
|
|
110
|
-
console.log(`
|
|
110
|
+
console.log(dim(` 已移除: ${tildePath(skillFile)}`));
|
|
111
111
|
removed++;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
if (removed === 0) {
|
|
115
|
-
console.log('
|
|
115
|
+
console.log(dim(' 没有已安装的 Skill。'));
|
|
116
116
|
} else {
|
|
117
|
-
console.log(
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(success(`已从 ${removed} 个工具移除 Skill。`));
|
|
118
119
|
}
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
@@ -124,11 +125,11 @@ export async function runSkill(args = []) {
|
|
|
124
125
|
const skillFile = join(t.skillDir, 'SKILL.md');
|
|
125
126
|
mkdirSync(t.skillDir, { recursive: true });
|
|
126
127
|
writeFileSync(skillFile, SKILL_CONTENT, 'utf-8');
|
|
127
|
-
console.log(`
|
|
128
|
+
console.log(dim(` 已安装: ${tildePath(skillFile)}`));
|
|
128
129
|
installed++;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
console.log(
|
|
132
|
-
console.log(
|
|
133
|
-
console.log('
|
|
132
|
+
console.log();
|
|
133
|
+
console.log(success(`已为 ${installed} 个工具安装 Skill。`));
|
|
134
|
+
console.log(dim(' AI 助手现在可以自主帮你同步用量数据。'));
|
|
134
135
|
}
|
package/src/sync.js
CHANGED
|
@@ -2,6 +2,7 @@ import { hostname as osHostname } from 'node:os';
|
|
|
2
2
|
import { loadConfig, saveConfig } from './config.js';
|
|
3
3
|
import { ingest, fetchSettings } from './api.js';
|
|
4
4
|
import { parsers } from './parsers/index.js';
|
|
5
|
+
import { success, failure, arrow, link, dim } from './output.js';
|
|
5
6
|
|
|
6
7
|
const BATCH_SIZE = 100;
|
|
7
8
|
const SESSION_BATCH_SIZE = 500;
|
|
@@ -15,7 +16,7 @@ function formatBytes(bytes) {
|
|
|
15
16
|
export async function runSync({ throws = false, quiet = false } = {}) {
|
|
16
17
|
const config = loadConfig();
|
|
17
18
|
if (!config?.apiKey) {
|
|
18
|
-
console.error('
|
|
19
|
+
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
|
|
19
20
|
if (throws) throw new Error('NOT_CONFIGURED');
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
@@ -41,12 +42,13 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
41
42
|
parserResults.push({ source, buckets: buckets.length, sessions: sessions.length });
|
|
42
43
|
}
|
|
43
44
|
} catch (err) {
|
|
44
|
-
|
|
45
|
+
// Parser errors are non-fatal — pass-through in dim gray (no translation).
|
|
46
|
+
process.stderr.write(`${dim(` ${source}: ${err.message}`)}\n`);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
if (allBuckets.length === 0 && allSessions.length === 0) {
|
|
49
|
-
if (!quiet) console.log('
|
|
51
|
+
if (!quiet) console.log(dim('暂无新数据。'));
|
|
50
52
|
return 0;
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -55,7 +57,7 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
55
57
|
const parts = [];
|
|
56
58
|
if (p.buckets > 0) parts.push(`${p.buckets} buckets`);
|
|
57
59
|
if (p.sessions > 0) parts.push(`${p.sessions} sessions`);
|
|
58
|
-
console.log(` ${p.source}
|
|
60
|
+
console.log(` ${dim(p.source.padEnd(14))}${parts.join(' · ')}`);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -65,29 +67,25 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
65
67
|
config.hostname = host;
|
|
66
68
|
saveConfig(config);
|
|
67
69
|
}
|
|
68
|
-
for (const b of allBuckets)
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
for (const s of allSessions) {
|
|
72
|
-
s.hostname = host;
|
|
73
|
-
}
|
|
70
|
+
for (const b of allBuckets) b.hostname = host;
|
|
71
|
+
for (const s of allSessions) s.hostname = host;
|
|
74
72
|
|
|
75
73
|
// Privacy: check if user allows project name upload
|
|
76
74
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
77
75
|
const settings = await fetchSettings(apiUrl, config.apiKey);
|
|
78
76
|
const uploadProject = settings?.uploadProject === true;
|
|
79
77
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
b.project = 'unknown';
|
|
86
|
-
}
|
|
87
|
-
for (const s of allSessions) {
|
|
88
|
-
s.project = 'unknown';
|
|
78
|
+
if (!quiet) {
|
|
79
|
+
if (uploadProject) {
|
|
80
|
+
console.log(dim(' 项目名: 上传(可在 Web 设置中关闭)'));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(dim(' 项目名: 已隐藏'));
|
|
89
83
|
}
|
|
90
84
|
}
|
|
85
|
+
if (!uploadProject) {
|
|
86
|
+
for (const b of allBuckets) b.project = 'unknown';
|
|
87
|
+
for (const s of allSessions) s.project = 'unknown';
|
|
88
|
+
}
|
|
91
89
|
|
|
92
90
|
let totalIngested = 0;
|
|
93
91
|
let totalSessionsSynced = 0;
|
|
@@ -95,22 +93,17 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
95
93
|
const sessionBatches = Math.ceil(allSessions.length / SESSION_BATCH_SIZE);
|
|
96
94
|
const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
|
|
97
95
|
|
|
98
|
-
const parts = [];
|
|
99
|
-
if (allBuckets.length > 0) parts.push(`${allBuckets.length} buckets`);
|
|
100
|
-
if (allSessions.length > 0) parts.push(`${allSessions.length} sessions`);
|
|
101
|
-
console.log(`Uploading ${parts.join(' + ')} (${totalBatches} batch${totalBatches > 1 ? 'es' : ''})...`);
|
|
102
|
-
|
|
103
96
|
try {
|
|
104
97
|
for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
|
|
105
98
|
const batch = allBuckets.slice(batchIdx * BATCH_SIZE, (batchIdx + 1) * BATCH_SIZE);
|
|
106
99
|
const batchSessions = allSessions.slice(batchIdx * SESSION_BATCH_SIZE, (batchIdx + 1) * SESSION_BATCH_SIZE);
|
|
107
100
|
const batchNum = batchIdx + 1;
|
|
108
|
-
const prefix = totalBatches > 1 ? ` [${batchNum}/${totalBatches}] ` : ' ';
|
|
101
|
+
const prefix = totalBatches > 1 ? ` ${dim(`[${batchNum}/${totalBatches}]`)} 上传中 ` : ' 上传中 ';
|
|
109
102
|
|
|
110
103
|
const result = await ingest(apiUrl, config.apiKey, batch, {
|
|
111
104
|
onProgress(sent, total) {
|
|
112
105
|
const pct = Math.round((sent / total) * 100);
|
|
113
|
-
process.stdout.write(`\r${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\x1b[K`);
|
|
106
|
+
process.stdout.write(`\r${prefix}${dim(`${formatBytes(sent)}/${formatBytes(total)} (${pct}%)`)}\x1b[K`);
|
|
114
107
|
},
|
|
115
108
|
}, batchSessions.length > 0 ? batchSessions : undefined);
|
|
116
109
|
totalIngested += result.ingested ?? batch.length;
|
|
@@ -118,11 +111,11 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
if (totalBatches > 1 || allBuckets.length > 0) {
|
|
121
|
-
process.stdout.write('\
|
|
114
|
+
process.stdout.write('\r\x1b[K');
|
|
122
115
|
}
|
|
123
116
|
const syncParts = [`${totalIngested} buckets`];
|
|
124
117
|
if (totalSessionsSynced > 0) syncParts.push(`${totalSessionsSynced} sessions`);
|
|
125
|
-
console.log(
|
|
118
|
+
console.log(success(`已同步 ${syncParts.join(' · ')}`));
|
|
126
119
|
|
|
127
120
|
if (!quiet && totalSessionsSynced > 0) {
|
|
128
121
|
const totalActive = allSessions.reduce((s, x) => s + x.activeSeconds, 0);
|
|
@@ -134,25 +127,28 @@ export async function runSync({ throws = false, quiet = false } = {}) {
|
|
|
134
127
|
const m = Math.floor((secs % 3600) / 60);
|
|
135
128
|
return h > 0 ? (m > 0 ? `${h}h ${m}m` : `${h}h`) : `${m}m`;
|
|
136
129
|
};
|
|
137
|
-
console.log(`
|
|
130
|
+
console.log(dim(` 活跃 ${fmtTime(totalActive)} / 总时长 ${fmtTime(totalDuration)} · ${totalMsgs} 条消息`));
|
|
138
131
|
}
|
|
139
132
|
|
|
140
|
-
if (!quiet)
|
|
133
|
+
if (!quiet) {
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(`${arrow('前往 Dashboard 查看详情')} ${link(`${apiUrl}/usage`)}`);
|
|
136
|
+
}
|
|
141
137
|
|
|
142
138
|
return totalIngested;
|
|
143
139
|
} catch (err) {
|
|
144
140
|
if (err.message === 'UNAUTHORIZED') {
|
|
145
|
-
console.error('
|
|
141
|
+
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
146
142
|
if (throws) throw err;
|
|
147
143
|
process.exit(1);
|
|
148
144
|
}
|
|
149
|
-
// Report partial success
|
|
150
145
|
if (totalIngested > 0) {
|
|
151
|
-
console.error(
|
|
146
|
+
console.error(failure(`部分完成(已上传 ${totalIngested} buckets): ${err.message}`));
|
|
152
147
|
} else {
|
|
153
|
-
console.error(
|
|
148
|
+
console.error(failure(`同步失败: ${err.message}`));
|
|
154
149
|
}
|
|
155
150
|
if (throws) throw err;
|
|
156
151
|
process.exit(1);
|
|
157
152
|
}
|
|
158
153
|
}
|
|
154
|
+
|