@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 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
- This will:
12
- 1. Ask for your API key (get one at https://vibecafe.ai/usage/setup)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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('Daemon install is not supported on this platform.');
113
- console.log('Supported: Linux (systemd), macOS (launchd).');
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('Warning: vibe-usage appears to be running from the npx cache.');
121
- console.log('The daemon may break when the cache is cleared.');
122
- console.log('For reliable operation, install globally first:');
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('Service is already installed. Run `vibe-usage daemon restart` or `daemon uninstall` first.');
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(`Created ${paths.file}`);
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(`Failed to start service: ${result.output}`);
142
+ console.error(failure(`启动服务失败: ${result.output}`));
143
143
  return;
144
144
  }
145
- console.log('Service enabled and started.');
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(`Created ${paths.file}`);
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(`Failed to load service: ${result.output}`);
155
+ console.error(failure(`加载服务失败: ${result.output}`));
156
156
  return;
157
157
  }
158
- console.log('Service loaded and started.');
158
+ console.log(success('服务已加载并启动。'));
159
159
  }
160
160
 
161
- console.log('\nDaemon installed. Usage data will sync automatically every 30 minutes.');
162
- console.log('Run `vibe-usage daemon status` to check.');
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('No supported service platform detected.');
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('No daemon service is installed.');
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('Service stopped, disabled, and removed.');
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('Service unloaded and removed.');
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('No supported service platform detected.');
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('No daemon service is installed.');
205
- console.log('Run `vibe-usage daemon install` to set up.');
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('Service is installed but not currently running.');
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('No supported service platform detected.');
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 ? 'Service stopped.' : `Failed: ${result.output}`);
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 ? 'Service stopped.' : `Failed: ${result.output}`);
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('No supported service platform detected.');
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 ? 'Service restarted.' : `Failed: ${result.output}`);
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 ? 'Service restarted.' : `Failed: ${result.output}`);
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(`Unknown daemon subcommand: ${subcommand}`);
268
- console.error('Usage: vibe-usage daemon <install|uninstall|status|stop|restart>');
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('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
19
+ console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
19
20
  process.exit(1);
20
21
  }
21
22
 
22
- log('Daemon started (sync every 30m, Ctrl+C to stop)');
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. Exiting.');
31
+ log('API key invalid, exiting.');
31
32
  process.exit(1);
32
33
  }
33
- log(`Sync error: ${err.message}`);
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
- export async function run(args) {
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 Set up API key
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
- export async function runInit() {
27
- console.log('\n vibe-usage - Vibe Usage Tracker by VibeCaf\u00e9\n');
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
- const answer = await prompt('Config already exists. Overwrite? (y/N) ');
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('Cancelled.');
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
- while (true) {
44
- apiKey = await prompt('Paste your API key: ');
45
- if (apiKey.startsWith('vbu_')) break;
46
- console.log('Invalid key — must start with "vbu_". Try again.');
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('Key verified.\n');
75
+ console.log(success(`验证通过 ${dim(apiKey.slice(0, 12) + '...')}`));
53
76
  } catch (err) {
54
77
  if (err.message === 'UNAUTHORIZED') {
55
- console.error('Invalid API key. Please check and try again.');
78
+ console.error(failure('API Key 无效,请检查后重试。'));
56
79
  process.exit(1);
57
80
  }
58
- console.log('Could not verify key (network error). Saving anyway.\n');
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(`Detected tools: ${tools.map(t => t.name).join(', ')}`);
93
+ console.log(success(`检测到 ${tools.length} 款工具: ${dim(tools.map(t => t.name).join(' · '))}`));
71
94
  } else {
72
- console.log('No AI coding tools detected. Install one and re-run init.');
95
+ console.log(warn('未检测到 AI 编码工具,安装后重新运行即可。'));
73
96
  }
74
97
 
75
- console.log('\nRunning initial sync...');
76
- await runSync();
98
+ console.log();
99
+ console.log(divider());
100
+ console.log();
77
101
 
78
- console.log(`\nSetup complete! View your dashboard at: ${apiUrl}/usage`);
102
+ await runSync();
79
103
 
80
- if (process.platform === 'linux' || process.platform === 'darwin') {
81
- console.log('\nTip: Run `npx @vibe-cafe/vibe-usage daemon install` to sync automatically in the background.');
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 { existsSync } from 'node:fs';
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('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
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(`This will delete usage data for this host (${currentHost}) and re-upload from local logs. Continue? (y/N) `);
30
+ const answer = await prompt(`将删除当前机器(${currentHost})的用量数据并从本地日志重新上传,继续? (y/N) `);
33
31
  if (answer.toLowerCase() !== 'y') {
34
- console.log('Cancelled.');
32
+ console.log(dim('已取消。'));
35
33
  return;
36
34
  }
37
35
 
38
- // 1. Delete remote data for this host
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(`Deleted ${result.deleted} buckets, ${result.sessions ?? 0} sessions from server.`);
39
+ console.log(success(`已删除 ${result.deleted} buckets · ${result.sessions ?? 0} sessions`));
43
40
  } catch (err) {
44
41
  if (err.message === 'UNAUTHORIZED') {
45
- console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
42
+ console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
46
43
  process.exit(1);
47
44
  }
48
- console.error(`Failed to delete remote data: ${err.message}`);
45
+ console.error(failure(`删除云端数据失败: ${err.message}`));
49
46
  process.exit(1);
50
47
  }
51
48
  } else {
52
- const answer = await prompt('This will delete ALL your usage data and re-upload from local logs. Continue? (y/N) ');
49
+ const answer = await prompt('将删除所有用量数据并从本地日志重新上传,继续? (y/N) ');
53
50
  if (answer.toLowerCase() !== 'y') {
54
- console.log('Cancelled.');
51
+ console.log(dim('已取消。'));
55
52
  return;
56
53
  }
57
54
 
58
- // 1. Delete all remote data
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(`Deleted ${result.deleted} buckets, ${result.sessions ?? 0} sessions from server.`);
58
+ console.log(success(`已删除 ${result.deleted} buckets · ${result.sessions ?? 0} sessions`));
63
59
  } catch (err) {
64
60
  if (err.message === 'UNAUTHORIZED') {
65
- console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
61
+ console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
66
62
  process.exit(1);
67
63
  }
68
- console.error(`Failed to delete remote data: ${err.message}`);
64
+ console.error(failure(`删除云端数据失败: ${err.message}`));
69
65
  process.exit(1);
70
66
  }
71
67
  }
72
68
 
73
- // 2. Clear local state (legacy — no state files needed for current parsers)
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
- // 3. Re-upload everything
79
- console.log('\nRe-syncing all data...');
73
+ console.log();
74
+ console.log(dim(' 从本地日志重新同步...'));
80
75
  await runSync();
81
76
 
82
- console.log(`\nReset complete! View your dashboard at: ${apiUrl}/usage`);
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('\nvibe-usage skill\n');
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
- console.log(` ${found ? '\u2713' : '\u2717'} ${t.name}`);
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(' No supported tools detected. Nothing to do.\n');
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(` Removed: ${tildePath(skillFile)}`);
110
+ console.log(dim(` 已移除: ${tildePath(skillFile)}`));
111
111
  removed++;
112
112
  }
113
113
  }
114
114
  if (removed === 0) {
115
- console.log(' No skills installed to remove.\n');
115
+ console.log(dim(' 没有已安装的 Skill。'));
116
116
  } else {
117
- console.log(`\n Removed vibe-usage skill from ${removed} tool${removed > 1 ? 's' : ''}.\n`);
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(` Installed: ${tildePath(skillFile)}`);
128
+ console.log(dim(` 已安装: ${tildePath(skillFile)}`));
128
129
  installed++;
129
130
  }
130
131
 
131
- console.log(`\n Installed vibe-usage skill for ${installed} tool${installed > 1 ? 's' : ''}.\n`);
132
- console.log(' Your AI coding assistant now knows how to sync usage data.');
133
- console.log(' Try asking: "sync my vibe usage" or "how much have I spent?"\n');
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('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
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
- process.stderr.write(`warn: ${source} parser failed: ${err.message}\n`);
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('No new usage data found.');
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}: ${parts.join(', ')}`);
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
- b.hostname = host;
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 (uploadProject) {
81
- console.log('📂 项目名: 上传 (可在 vibecafe.ai/usage 设置中关闭)');
82
- } else {
83
- console.log('🔒 项目名: 已隐藏');
84
- for (const b of allBuckets) {
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('\n');
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(`Synced ${syncParts.join(' + ')}.`);
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(` active: ${fmtTime(totalActive)} / total: ${fmtTime(totalDuration)}, ${totalMsgs} messages`);
130
+ console.log(dim(` 活跃 ${fmtTime(totalActive)} / 总时长 ${fmtTime(totalDuration)} · ${totalMsgs} 条消息`));
138
131
  }
139
132
 
140
- if (!quiet) console.log(`\nView your dashboard at: ${apiUrl}/usage`);
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('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
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(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
146
+ console.error(failure(`部分完成(已上传 ${totalIngested} buckets): ${err.message}`));
152
147
  } else {
153
- console.error(`Sync failed: ${err.message}`);
148
+ console.error(failure(`同步失败: ${err.message}`));
154
149
  }
155
150
  if (throws) throw err;
156
151
  process.exit(1);
157
152
  }
158
153
  }
154
+